feat(compat-v7): /matchers, v7's website matchers with the sticky random
Stacked on #702 (feat/compat-v7-core).
What
@decocms/compat-v7/matchers: v7's website matchers with v7's results, on v8.
device,cookie,host,pathname,queryString,location,multi,random. A v8 block function gets only its inputs, so each takes the request as its second argument: aRequest, or{ request, url?, flags? }(url= the page's URL when the request is a client-side navigation's server-function call;flags= one array per request for A/B decisions). In the block map:"website/matchers/device.ts": (props: DeviceProps) => device(props, request).environmentthrows with the fix: it read the process environment, which no v8 package does.always,never,datestay v8 built-ins.randomis latest 7.x's sticky random (PO decision): thedeco_segmentcookie, same bytes, re-drawn once when the traffic share changes.random/decideRandomrecord each decision on the request'sflags, andsegmentSetCookie(request, flags)gives theSet-Cookie(deco_segment=ā¦; Max-Age=31536000; Path=/; SameSite=Lax, ornullwhen unchanged, merged over the visitor's cookie as v7's route did).decideRandomreturns the decision and that cookie. Sending it is the framework subpaths' job (next PRs).- Cohort = the saved block's name. v7 keyed the cookie on the name of the saved matcher block the rule pointed at. A v8 block doesn't learn its saved name, so the block carries it in
cohort; an inline rule useswebsite/matchers/random.ts, as v7 did. The test shows a visitor v7 drew keeps their group whencohortis the saved name, and is re-drawn under any other. - Also exported: v7's
deco_segmenthelpers (parseSegmentCookie,serializeSegmentCookie,segmentCacheToken,trafficToPct,SEGMENT_COOKIE),parseCookies(h3's parse, as v7'sgetCookies()read them) and the location matcher's country table. The matchers are only at/matchers(their names are too generic for the root).
Pinned to v7 (latest 7.x, 9131e9d9)
v7's own files are added verbatim to src/__fixtures__/v7/ with pinned hashes: cms/resolve.ts, matchers/builtins.ts, matchers/countryNames.ts, matchers/override.ts, sdk/flags.ts, sdk/experiments.ts, sdk/djb2.ts, tanstack/routes/cmsRoute.ts, plus h3 2.0.1-rc.16's cookie parse/serialize (what v7's TanStack route ran). src/__tests__/loadV7.ts runs them unchanged (esbuild strips types; imports unrelated to matching are no-op stubs), so the tests call v7's real evaluateMatcher (with registerBuiltinMatchers()), evaluateVariantRule and persistFlags.
- 20k seeded requests Ć props per matcher (UAs, hosts, encoded paths, query strings, cookie headers,
cf-*headers,request.cf, malformed props), v7 context built as v7's TanStack route built it (decoded splat path, h3-parsed cookies). - 10k seeded sticky page requests: several saved/inline random rules per page, cookies from v7's serializer or garbage, with and without
flags; decisions, recorded flags and theSet-Cookieall equal v7's. - 20k cookie headers vs h3's parse; 20k
deco_segmentround trips;multi5k; country table and lookup. - Mutations (device regex, legacy host,
Greater, cf latitude, negate, empty cookie, path decoding, cookie quotes, default traffic, the-1legacy fingerprint,accepts, cookie attribute order, re-issue on no change, merge with the existing cookie) each fail the suite. tests/compat-v7.schema.test.ts: a matcher wrapped in the block map keeps its fields indeco schema(random:traffic,cohort).
Differences from v7 (listed)
- Demo's and storefront's A/B pages become sticky (PO-approved: latest 7.x sticky random for every site).
multireceives its rules' results (v8 resolves them first). An innerrandomis therefore sticky; v7 re-drew it per request.- No
x-deco-matchers-override: v8 previews force variants with the draft pointer's__variant. - The content migration does not yet write
cohortinto saved random blocks:deco checkwould reject the field on sites whose random block doesn't declare it. That step belongs with the migrator change that wires these matchers into the block map.
Gates
bun run check: pass.bun run build: pass. Plain-Node import of every built export: 11 packages, compat-v7 7/7,deco --helpok.skills:check+skills:readme:check: pass.tests/release.test.ts41/41.bun run test(2 GB cap): 2515 passed, 1 failed (DS-2, the known flake: fails the same alone, 10/11, in a file this PR doesn't touch), 95 skipped.
The docs site's /next pages that say "copy the matchers into the site" should point here in a follow-up docs PR.
Main refresh (2026-10-17, 3ddc7b5b)
- Merged C1
183bc4b7into this branch (normal merge, no conflicts), then merged up the stack to M2 #707. - Gates at the top of the stack (M2
5131a3c1):bun run check,bun run build,skills:check,skills:readme:checkpass.bun run test(2 GB memory cap): 122 files pass, 1 skipped; 2,745 tests pass, 95 skipped, 0 fail. DS-2 now passes (fixed on main by #709); no timeouts this run. - CI on this PR's new head: all checks pass.
š¤ Generated with Claude Code
https://claude.ai/code/session_01WNwbSEePYNcY5YCgqZURig
Summary by cubic
Adds v7's website matchers to @decocms/compat-v7/matchers so v8 sites get v7's exact matcher results. device, cookie, host, pathname, queryString, location, multi and random each take the request as a second argument, since a v8 block function only receives its inputs; environment throws, as v7 read the process environment. random is latest 7.x's sticky random, persisted via the deco_segment cookie and keyed on cohort ā the saved matcher block's name, as v7 did.
Differences from v7
multireceives its rules' results (v8 resolves them first), so an innerrandomis sticky; v7 re-drew it per request.- No
x-deco-matchers-override: v8 previews force variants with the draft pointer's__variant. - The content migration doesn't yet write
cohortinto saved random blocks.
Testing
- Every matcher is compared against v7's own code, pinned verbatim under
src/__fixtures__/v7/and run unchanged through a test-only esbuild loader. - 20k seeded cases per matcher and 10k sticky page requests match v7's
evaluateMatcher,evaluateVariantRuleandpersistFlags. - A matcher wrapped in the site's block map keeps its fields in
deco schema, includingrandom'scohort.