fix(service-settings): re-point value-domains.ts onto @objectstack/spec/shared and refuse with value_domain - #15434
Conversation
…ue-domain predicate and refuse with `value_domain`
The services half of the maintainer's ruling A (2026-09-02): one closed
vocabulary and one membership predicate shared by settings specifiers and
object fields. The spec half landed the shared module; this deletes the
door's second copy of all three definitions and re-points onto it.
- `value-domains.ts` keeps only what is the DOOR's — which declarations it
enforces, how a multi-value carrier is walked, and the env log line's
prose. The `Intl.DateTimeFormat` probe, the run-time
`Intl.supportedValuesOf('currency')` set and the 249 alpha-2 codes are
gone; `isValueDomainMember` answers instead.
- `knownValueDomain` filters with the closed enum's own `safeParse`, which
makes the prototype-chain guard structural rather than remembered.
- The save door's refusal code becomes `value_domain` (ADR-0114: the code is
the constraint's own name) and its message renders the published catalog
template, the same catalog the record write path renders.
- A ratchet pin so a re-added membership table in this package goes red.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
…e ratchet's input radius - `settings-routes.test.ts` gains a pin that states the refusal envelope as a CHANGE: `value_domain` (not `invalid_value`) plus the catalog sentence, with status, envelope code, `field`, `label`, `constraint` and `value` asserted unchanged beside it, so the blast radius is measured rather than believed. - The encrypted-specifier case pins the redacted sentence: the catalog template always interpolates the offending value, so a secret key renders with the REST boundary's mask and the value still never appears. - The ratchet routes its comment stripping through `scripts/js-comment-mask.mjs` (`check:comment-mask-adoption` refuses a private one) and declares that import's radius in `cross-package-test-inputs.mjs` + `turbo.json`, so the pin is visible to the affected-subset filter and the turbo cache. - Changeset: `minor` + a BREAKING banner, which is the carrier for breaking-ness inside the launch window where `major` is forbidden. `packages/spec/liveness/field.json` is deliberately NOT touched: the same `valueDomain` row is being rewritten by the open engine-half PR, so an edit here would conflict and force the ledger counts to be re-derived. The sentence this branch falsifies is reported to the PM to sequence instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
📓 Docs Drift CheckThis PR changes 1 package(s): 6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
What this run could not see
Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0863433b829e5f980721333efd679779c9bae61c && git checkout 0863433b829e5f980721333efd679779c9bae61c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f1d787294f052a5ee79332b4c0459e24c9ff9f3f 17eb7941d2c21e211056f210e9427a3289f319d8 && git checkout -B drift-repro f1d787294f052a5ee79332b4c0459e24c9ff9f3f && git merge --no-ff 17eb7941d2c21e211056f210e9427a3289f319d8
node scripts/docs-audit/affected-docs.mjs --json f1d787294f052a5ee79332b4c0459e24c9ff9f3f
|
Contract review — PR #15434 (Clause-②), head
|
| mutation | pin | result |
|---|---|---|
M1 export const LOCAL_TABLE = new Set(['CH']) appended to value-domains.ts |
ratchet pin.test.ts:80 |
red 1/5 |
M2 new src/zz-probe-local.ts carrying supportedValuesOf('currency') + 'AD AE AF AG AI AL AM AO AQ' |
ratchet :90, :98 |
red 2/5 |
M3 new src/zz-second-caller.ts calling isValueDomainMember( |
ratchet :107 |
red 1/5 |
M6 spec alpha-2 literal drops CH |
value-domains.test.ts:169 |
red 2/14 |
M6b spec iso_3166_alpha2: () => true |
:42 garbage probe |
red 2/14 |
M7 snapshot drops CHF: 2, (currency-fraction-digits.ts:55) |
:143 re-measurement |
red 3/14 |
M8 example: 'Europe/Berlin' in valueDomainPhrasing |
:88 catalog-prose |
red 1/14 |
M9 value: String(offending) (mask dropped) |
settings-service.test.ts vaultdom |
red 1/139 |
M10 code: 'invalid_value' |
settings-routes.test.ts |
red 2/26 |
M11 messageKey: 'value_domain' (generic template) |
settings-routes.test.ts |
red 1/26 |
M12 door asks String(v).toLowerCase() |
value-domains.test.ts |
red 5/14 |
So every pin measures something. But the ratchet measures the spelling of the deleted table, not the capability:
- M4 — green 5/5. The M2 file placed at
src/manifests/zz-alpha2-table.tsinstead ofsrc/.runtimeSources()isreaddirSync(SRC)(pin.test.ts:59), non-recursive;src/manifests/andsrc/translations/exist today. - M5 — green: ratchet 5/5 AND
value-domains.test.ts14/14. A full 249-code table as an array literal (new Set(['AD','AE',…])) in a newsrc/zz-alpha2-array.ts, imported byvalue-domains.tsand consulted foriso_3166_alpha2whileisValueDomainMemberis kept for the other two domains.RUN(:90) matches only the space-separated string shape;new Set(is checked only invalue-domains.ts(:80); "exactly one caller" (:107) still holds. This is precisely the "move the table to a new file" case the header (:28-31) claims the whole-package scan closes.
2. The admitted vacuous pin — confirmed; the replacement questions are non-vacuous, one is control-shaped.
Found: packages/spec/src/shared/value-domain.test.ts:38 expect(SpecifierValueDomainSchema).toBe(ValueDomainSchema) (identity; :39 options equal) and again packages/spec/src/system/settings-manifest.test.ts:408 — both on main, untouched by this PR. Replacement (a) "every member actually refuses something" (value-domains.test.ts:42): red under M6b (accept-all), but green under M6c — a denylist stub (v) => ['ZZ','UK','XX','us'].includes(v) passes 14/14; the spec's own test catches M6c (3 failed of 18), so the system is not blind, only the door-level question is. Replacement (b) prose-vs-catalog (:88): red under M8, non-vacuous. Also :163-173 loops ISO_3166_ALPHA2_CODES through a door that answers from ISO_3166_ALPHA2_CODES — the set against itself: what it pins is the door's plumbing (M12 catches it), not membership; its comment "stronger and not a duplicate" overstates.
3. The labelling correction — confirmed. At base a56baa2bd: settings-service.ts:1025 is const rejected = firstRejectedDomainMember(domain, value); inside private effectiveEnvOverride( (declared :963); :2060 is the same call inside private async validatePatch( (declared :1883). On the head they sit at :1031 (in effectiveEnvOverride, :969) and :2074 (in validatePatch, :1889). Only validatePatch pushes a FieldError (:2091-2109); the env door writes a log line via reportRejectedEnvOverride (:1102). Both doors are covered; the dev did not invert it again.
4. Information disclosure — confirmed on every path, by probe. A temporary probe test (deleted afterwards; 5/5) registered a manifest with encrypted: true + valueDomain on a text key and a multiselect key plus a plain control key, canaries Mars/CANARY7731 / CNRY:
- save door, scalar:
fields[0].message=TZ secret must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "••••••••"),fields[0].valueabsent;JSON.stringify({message, fields, stack})canary-free. - save door, multiselect encrypted: masked,
valueabsent, canary-free. - REST
PUT /api/settings/:namespacethroughregisterSettingsRoutes: the whole{status, body}is canary-free — including top-levelerror.message, which isSettingsValidationError's concatenation of field messages (settings-service.types.ts:589-592) and therefore carries the mask. - env door, encrypted key: one log line, no value — the
Rejected value:clause is dropped (settings-service.ts:1124-1125) and the sentence is built fromvalueDomainPhrasing, which has no{{value}}. - positive controls: the plain key echoes the canary on the save door, the REST door and the env log line, so the detector is live.
Where the mask is applied:settings-service.ts:2100(value: secret ? SETTINGS_SECRET_MASK : String(offending)) and:2108(...(secret ? {} : { value })), withsecret = reg.encryptedKeys.has(key)(:2084) — the same setsecretKeysOf()(:1456-1460) hands the REST read mask, so write-time redaction and read-time masking cannot disagree.renderValidationMessage(validation-message.ts:300-337) is called with notranslatehook and no locale (→en), so no override path can re-render the raw value. Only non-test consumer of the error issettings-routes.ts:155-166, verbatim pass-through. M9 shows the vaultdom pin catches a dropped mask.
5. Scope growth into tooling — confirmed sanctioned; one nit. check-cross-package-test-inputs.mjs:1444-1470 (Layer B) requires a turbo.json "<pkg>#test" task hashing the declared globs, so both edits are the gate's own prescribed shape. Re-run on the head: check-cross-package-test-inputs.mjs (+--self-test) exit 0; check-comment-mask-adoption.mjs (+--self-test, invoked as lint.yml:505-506 does — it is not a root script) exit 0; check-turbo-task-graph.mjs exit 0. inputs = the default four + the two mask files, the same shape as eight sibling entries; package-scoped, so no other package's affected-subset filter moves. Not minimal: dependsOn: ["build"] (turbo.json:27) vs the default ["^build"], copied from @objectstack/metadata#test; nothing in this package's tests reads its own dist (every workspace import is aliased to src; settings-prebind-read-warning.test.ts:42-46 says so), and the commit message gives no reason. Harmless (a self-build before tests) but unexplained.
6. Currency re-measurement pin — defect (→ R2). value-domains.test.ts:143-157 asserts at run time that Intl.supportedValuesOf('currency') ⊆ door-admits, while the door now answers from CURRENCY_FRACTION_DIGITS. The spec already carries the drift detector beside the snapshot: packages/spec/src/shared/value-domain.test.ts:121-131 asserts probe == snapshot in both directions plus size === 162, and the snapshot header carries the regeneration snippet. CI runs node-version: '22' (ci.yml:313 et al.; .nvmrc = 22) — a floating patch, so the ICU build moves with each 22.x release. On any host whose ICU enumerates a currency the snapshot lacks, this test goes red for a correct implementation — at the same moment the spec's does, in a package that has nothing to fix (the fix is regenerating the spec snapshot); on a host that lost one, only the spec's goes red. It is a strictly weaker duplicate of the spec's detector, in the wrong package. Measured here: probe == snapshot (A3), so green today; the CI host's ICU is not measured from this seat. What it should assert instead: iterate Object.keys(CURRENCY_FRACTION_DIGITS) through firstRejectedDomainMember (deterministic: "every snapshot member is admitted through the door" — the thing this package owns) and leave probe-vs-snapshot to spec. Side effect: the ratchet's test-source exemption for supportedValuesOf (pin.test.ts:31-33) loses its only reason to exist.
7. Changeset disposition — confirmed. ADR-0087's categories reason about metadata objectstack migrate meta can reach (check-adr-0087-registration.mjs:186-188; ADR D2 scope at docs/adr/0087…md:139: "losslessly mappable changes only — renames, alias removals, field moves"); a FieldError.code on a REST refusal is not that, and no registered/already-registered alternative fits. no-migration-prescription is mechanically valid (no FROM/TO block; the gate's exit 0 was measured by the PM and is not re-derived here). Launch-window rule: check-changeset-no-major.mjs:33-56 makes the BREAKING banner + ADR-0087 disposition the carriers; WHICH LEVEL (pr-automation.yml:667-682) puts a behaviour change on a published package at minor. Re-ran node scripts/check-changeset-no-major.mjs --base origin/main --head HEAD → exit 0; check-empty-changeset.mjs → exit 0. Nit: the body says the settings door renders "the same catalog the record write path renders" — on this tree nothing outside packages/spec and service-settings renders value_domain at all (grep -rln value_domain packages --include=*.ts minus tests/spec/settings: zero hits); the engine half is an open PR. Present tense for a future state.
8. Equivalence measurements — confirmed. A2 (string literals extracted mechanically from base value-domains.ts and packages/spec/src/shared/value-domain.zod.ts): 249/249, 249 unique each, order identical, symmetric difference ∅, all ^[A-Z]{2}$. A3 (tsx over CURRENCY_FRACTION_DIGITS vs Intl.supportedValuesOf('currency')): 162/162, both differences ∅, CHF in both, XYZ in neither, VED/XAU in neither. Node v22.22.2 / ICU 78.2 / CLDR 48.0.
Not in 1–8
- Full package suite on the head:
pnpm exec vitest run→ 30 files / 527 tests green;tsc --noEmitexit 0; also exit 0 on the head:check-test-source-alias,check-undeclared-dep-imports,check-error-code-casing,check-dispatcher-error-vocabulary,check-pnpm-filter-targets,check-type-source-resolution. - No published-surface loss:
index.tsnever re-exportedvalue-domains.ts(checked at base and head), soISO_3166_ALPHA2_CODESleaving the module changes no package export. - Console (
objectui@24e027e):apps/console/src/pages/settings/SettingsView.tsx:223,233branch only on envelope codes and the valueDomain test renders a plainerrorstring — the first-party client does not break on the code move.SettingsField.tsx:177andSettingsField.valueDomain.test.tsx:221still sayinvalid_valuein prose: a sibling-repo follow-up, not a blocker. - Residual
invalid_valueinsettings-service.ts(:173,:180,:377,:1831,:1835,:1942,:1947) are the step-grid (settings 的step是同族第五个声明但不执行的值约束键 —— 先要一个「它算不算值约束」的判断 #6199) andvisible-predicate (settings-manifestvisibleis declaredExpressionInputSchema(CEL) but evaluated by a non-CEL grammar — a CEL predicate there silently skips the save-timerequiredgate #7169) refusals — correct as-is. - The core pin the ratchet header cites exists:
packages/core/src/security/resolve-authz-context.time-zone-domain.pin.test.ts.
Required changes (tests only; the implementation stays as is)
R1 — value-domains.shared-predicate.pin.test.ts: make the ratchet cover what its header claims. (a) Scan recursively (readdirSync(SRC, { recursive: true }) or an explicit walk) so src/manifests/, src/translations/ and any new subdirectory are covered — M4. (b) Detect the array-literal table shape in every runtime source, e.g. /(?:'[A-Z]{2}',\s*){7}/ beside RUN — M5. Do not widen the new Set( check package-wide: settings-service.ts (6 sites) and sys-secret-orphan-report.ts (1) use it legitimately; instead pin value-domains.ts's import surface (no relative imports, only @objectstack/spec/shared), which is what closes the M5 route. Alternatively narrow the header at :28-31 to what is measured. Evidence: M4 and M5 above.
R2 — value-domains.test.ts:143-157: replace the live Intl.supportedValuesOf('currency') loop with a loop over Object.keys(CURRENCY_FRACTION_DIGITS) through firstRejectedDomainMember; the probe-vs-snapshot invariant already lives at packages/spec/src/shared/value-domain.test.ts:121. The A3 measurement belongs in the PR/changeset record (already there), not as a run-time invariant on a floating-ICU host. Then drop the supportedValuesOf exemption sentence from the ratchet header.
Nits, waived: turbo.json:27 dependsOn: ["build"] → ["^build"] or a one-line reason; the changeset sentence "the same catalog the record write path renders" → future tense or cite the engine PR; value-domains.test.ts:163-173 comment "stronger and not a duplicate" → "pins the door's plumbing"; the objectui prose follow-up.
Not measured: the CI host's ICU/CLDR (claim 6 is argued from the floating node-version: '22' pin and measured only on v22.22.2); the ADR-0087 gate exit (PM-measured, not re-derived).
Generated by Claude Code
…ew found, and drop the ICU-drift currency pin
R1 — the ratchet did not cover what its header claimed. Two mutations passed
it green: a table one directory down (`src/manifests/`, which `readdirSync`
never reached) and a 249-code ARRAY literal in a sibling module that
`value-domains.ts` imported and consulted for `iso_3166_alpha2` while the
shared predicate still served the other two.
- the source walk is recursive, and reports paths relative to `src/`;
- both table SHAPES are detected — the space-separated string and the array
literal a fresh re-typing produces;
- the root close is a new import-surface pin: `value-domains.ts` may import
from `@objectstack/spec/shared` and NOTHING else, so a table anywhere in the
tree is harmless because the door cannot reach it. `new Set(` is deliberately
NOT widened package-wide — `settings-service.ts` has six legitimate sites;
- the header now states what is and is not covered, at the size of the
evidence.
R2 — the currency pin asserted at run time that everything
`Intl.supportedValuesOf('currency')` enumerates is admitted by a door that
answers from the checked-in snapshot. CI pins the Node MAJOR only, so an ICU
build that enumerates one code the snapshot lacks would redden a CORRECT
implementation in a package with nothing to fix — and probe-versus-snapshot is
already pinned beside the snapshot, in both directions and with a size. It is
replaced by the invariant this package owns: the door AGREES with
`isValueDomainMember` value by value, over a corpus carrying every trap, so a
local filter creeping back into the walker reddens even though every
representative case would still pass.
Also, from the review's waived list: `turbo.json` uses `^build` (nothing in
this package's tests reads its own dist); the changeset's "the same catalog the
record write path renders" is future tense and cites the open engine PR; and
the alpha-2 population pin is described as the plumbing pin it is rather than
as something stronger.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
|
Round 1 addressed — patched head R1 — the ratchet now covers what its header claimsThree changes, and the third is the one that closes M5 at its root rather than by recognising a shape:
Both mutations re-run against the patched pin. Each was written to disk, the mutation confirmed on disk by observing the anchor text (never the writer's exit code —
One observation from M5 worth recording: the door suite stays green (15/15) under it, correctly — the duplicate table has identical membership, so no behavioural test can see it. That is the argument for the ratchet being a source scan, and for the import pin being the load-bearing check rather than the shape regexes. The header now states what is covered and what is not — a 3-letter currency array is not shape-detected, and a table reached through a bare package specifier would pass the import pin — so the claim is the size of the evidence rather than the other way round. R2 — the ICU-drift pin is goneReplaced by the invariant this package actually owns: One deviation from the suggested wording, flagged rather than silently reshaped: looping The ratchet header's Waived items — all three taken, since the files were open
Not acted on, as instructed: the note that "every member actually refuses something" is weaker than it reads. Recorded in the PR body so the pin is not over-trusted. Verification on the patched headGate family re-derived on this head (same 57 families, no additions) and re-run in full: 57 run · 57 exit 0 · 0 red, with the workspace closure built first ( Generated by Claude Code |
Contract review — PR #15434 (Clause-②), round 2, head
|
| mutation | ratchet | door suite |
|---|---|---|
M4 — space-separated table at src/manifests/zz-alpha2-table.ts |
red 1 failed | 5 passed on carries no ISO 3166-1 alpha-2 code list, in either shape |
— |
M5 — 249-code array literal in src/zz-alpha2-array.ts, from './zz-alpha2-array.js', consulted for iso_3166_alpha2, predicate kept for the other two |
red 2 failed | 4 passed on imports from NOTHING ELSE… and the shape case |
green 15/15 — as the dev said: identical membership, no behaviour to see |
M5q — M5 verbatim, with the array strings and the specifier double-quoted (from "./zz-alpha2-array.js", ["AD", "AE", …]) |
green 6/6 | green 15/15 |
M16 — the 249 codes as a regex alternation inside the door itself (/^(?:AD|AE|…|ZW)$/): no import, no quotes, no new Set( |
green 6/6 | green 15/15 |
M5q: the import-surface pin (pin.test.ts:115, /\bfrom\s+'([^']+)'/g) and both shape regexes (:139 '(?:[A-Z]{2} ){7}, :140 (?:'[A-Z]{2}',\s*){7}) recognise single quotes only, so the pin sees ['@objectstack/spec/shared'] and passes. It is lint-legal: eslint --print-config for this path lists 6 active rules and no quotes / @stylistic/quotes / @typescript-eslint/quotes; a live control (import { Data } from "@objectstack/spec" in a src/ file) exits 1 on no-restricted-imports while the double-quoted strings beside it draw no report; ESLint over the M5q files exits 0. The header's NOT-covered list (:49-53) names a 3-letter currency array and a bare package specifier — not this — and the check it defeats is the one the header calls the root close (:44-47, :108-113). Same class, not run: from"./x" (no whitespace — \s+ requires one) and a template-literal or leading-space spaced table. Demonstrated in a scratch script over the M5q door text: /\bfrom\s*(['"])([^'"]+)\1/g sees ["@objectstack/spec/shared","./zz-alpha2-array.js"]; /(?:(['"])[A-Z]{2}\1,\s*){7}/ catches the double-quoted array; both plus /(['"])(?:[A-Z]{2} ){7}/` return 0 hits across the 31 runtime sources on the head. M16 is the admitted class ("a shape it does not know") — belongs in the NOT-covered list, not required.
R2 — the time bomb is gone; the substitute does not cover the class its header claims
Discharged: value-domains.test.ts no longer probes Intl.supportedValuesOf (:145-195), and the ratchet header's supportedValuesOf exemption sentence is gone (pin.test.ts:55 is now the bare "test sources are exempt"). The dev's premise is confirmed: CURRENCY_FRACTION_DIGITS is declared at packages/spec/src/data/currency-fraction-digits.ts:51, not re-exported by data/index.ts, absent from api-surface/*.json, and spec's exports map has no wildcard — unreachable from this package without widening a published surface; round 1's literal suggestion was infeasible and flagging it was right. ICU-independent: yes, structurally — both sides of every assertion are computed, so no host can redden a delegating door. Probe-vs-snapshot is where the dev says: packages/spec/src/shared/value-domain.test.ts:120-130, both directions plus size 162.
Not discharged. The header at :150-153 says "a local filter creeping back in (an extra case fold, a length or shape check, a curated allow-list) reddens here". Measured in value-domains.ts:
| mutation | ratchet | door suite | whole package |
|---|---|---|---|
M14 — iso_4217_currency answered by ['CHF','USD','EUR','GBP','JPY','CNY','INR','AUD','CAD','BRL'].includes(v), predicate kept for the other two |
green 6/6 | green 15/15 | green 529/529 |
M15 — predicate AND !v.startsWith('X') (drops XAF, XOF, XCD, XPF — real currencies) |
green 6/6 | green 15/15 | — |
The corpus (:165-174) carries four currency members, all inside M14's list; outside this file the package pushes only CHF, USD, XYZ through the door; and the ratchet says itself that a 3-letter array is not shape-detected. So a second currency membership definition inside the settings door — the exact divergence the ruling closed — ships green through every gate in this package. The alpha-2 door gets its whole population (:208-212, 249 codes); the currency door gets four samples and a false sentence. The regression class round 1 named ("every snapshot member is admitted through the door") is not covered.
A reachable, ICU-independent, population-level form exists — the same agreement shape with the host's enumeration as the POPULATION, not the ORACLE: for each c of Intl.supportedValuesOf('currency'), door(c) === null must equal isValueDomainMember('iso_4217_currency', c). A correct door can never redden on any ICU (a code the snapshot lacks is false on both sides; a code the host lacks is never asked). Measured with a scratch tsx probe at the package root: head → population=162 disagreements=0; M14 → disagreements=152 (AED, AFN, ALL, …); M15 → disagreements=7 (XAF, XCD, XCG, XDR, XOF, XPF, XSU). This does not reverse round 1: the objection was to the enumeration as an oracle over a snapshot-backed door; under an agreement assertion it carries no ICU verdict. Test sources stay exempt from the ratchet scan, so nothing there moves.
Waived nits — confirmed taken, cheaply
turbo.json:27 dependsOn: ["^build"]; the changeset is future-tense and cites #15316; :200-207 calls the alpha-2 loop the plumbing pin it is. Gates that touch the changed ratchet file, re-run on the clean head: check-cross-package-test-inputs.mjs exit 0 (27 packages, every declared glob hashed); check-comment-mask-adoption.mjs and --self-test exit 0; turbo.json inputs still name both mask files.
Required changes (tests only — value-domains.ts / settings-service.ts untouched and still cleared)
R1′ — value-domains.shared-predicate.pin.test.ts: make the import-surface pin and both shape regexes delimiter-agnostic: :115 → /\bfrom\s*(['"])([^'"]+)\1/g (read group 2); :139 → /(['"])(?:[A-Z]{2} ){7}/; :140→/(?:(['"])[A-Z]{2}\1,\s*){7}/. Evidence: M5q green 6/6 on the head; the replacements see the relative specifier and the double-quoted array, and hit 0 of 31 runtime sources. Then either add M16's class (an alternation or .includestable inside the door itself) to the NOT-covered list at:49-53`, or pin the door's stripped code against any run of ≥7 two-uppercase-letter tokens in any delimiter.
R2′ — value-domains.test.ts:145-195: add a population-level agreement case for iso_4217_currency over Intl.supportedValuesOf('currency') (optionally iana_time_zone over supportedValuesOf('timeZone') in the same form), asserting door === predicate per code, with a comment stating the enumeration is the population and not the oracle. Evidence: M14 and M15 are green in the ratchet, the door suite and (M14) the whole package today, and red under that case by 152 and 7 disagreements. Make the sentence at :150-153 true or delete the allow-list clause.
Not measured: the CI host's ICU (the ICU-independence argument is structural; measured only on 78.2); the other 55 gate families (PM-measured on this head; no runtime file moved).
Generated by Claude Code
…ain agreement over populations rather than samples Round 2 of the contract review found both instruments narrower than their own headers claimed, and measured exactly how much. R1' — every scan was single-quote-shaped, so a VERBATIM double-quoted copy of the array mutation passed the whole file green: the import pin read `/\bfrom\s+'([^']+)'/` and both shape regexes hard-coded `'`. This package has no `quotes` lint rule active, so both spellings are legal here and only these pins can tell them apart. - the import surface is read as `/\bfrom\s*(['"])([^'"]+)\1/` — either delimiter, and the space after `from` optional; - the two table shapes accept `'`, `"` and `` ` ``; - a third shape needs no quotes and no import at all — the 249 codes as a regex alternation inside the door — so it is caught by DENSITY rather than spelling: seven or more bare two-uppercase-letter tokens separated by one or two non-alphanumerics, checked on the door, which is the only place the shape can live now that the import pin holds. All three scans hit 0 of the 31 runtime sources on this head. R2' — the agreement pin covered a corpus, and its header claimed it covered "a curated allow-list". Measured false: a door answering `iso_4217_currency` from a ten-entry `.includes` list passed the whole package green, because all four sampled currency members were inside that list. Agreement is now asserted over each domain's whole POPULATION — every code `Intl.supportedValuesOf` gives for currency and time zone, and the published 249 for alpha-2 — with the corpus kept for the traps no population contains. The host enumeration is the POPULATION here, never the ORACLE, which is what made the round-1 version a time bomb: each code is put to BOTH the door and the shared predicate and the two must answer alike, so a code the snapshot lacks is false on both sides and a code the host lacks is never asked. No ICU build can redden a correct door — or hide a divergent one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
|
Round 2 addressed — patched head R1′ — the delimiter, and the alternationThe three replacements are in as prescribed, and hit 0 of the 31 runtime sources on this head: M16 is closed rather than documented. A new door-only check catches the shape by DENSITY, which is what survives every spelling: The NOT-covered list is now honest about what remains: a 3-letter table (a currency list) is not shape-detected in any spelling, and a bare-specifier import would pass the import pin. Currency is reached behaviourally instead — see R2′. R2′ — populations, not samplesThe header sentence is now true. Agreement is asserted over each domain's whole population, with the corpus kept only for traps no population contains:
Enumeration as population, never oracle, exactly as you framed it: each code goes to the door and to the predicate and the two must answer alike, so a code the snapshot lacks is false on both sides and a code the host lacks is never asked. Round 1's objection is preserved intact — no ICU build can redden a correct door, and none can hide a divergent one either. Mutation results — all four, each confirmed on disk and restored under a trap
Both currency numbers reproduce yours exactly. Worth recording: under M14 and M15 the ratchet stays green 7/7, and under M5q and M16 the door suite stays green — the two instruments are catching disjoint classes, which is the argument for keeping both rather than merging them. On your method note — taken, and applied here rather than only read. Each population pin carries a vacuity guard ( Confirmed and unchanged: Verification on the patched headGate family re-derived on this head (same 57, none added, none dropped) and re-run in full: 57 run · 57 exit 0 · 0 red, with the workspace closure built first ( Generated by Claude Code |
Contract review — PR #15434 (Clause-②), round 3, head
|
| mutation | ratchet (pin.test.ts) |
door suite | other |
|---|---|---|---|
M5q — M5 verbatim, double-quoted (249 "XX" / 0 'XX' on disk) |
2 failed | 5 passed — import pin + array shape |
18/18 | — |
M16 — 249-code alternation in the door (0 new Set(, 0 injected quotes) |
1 failed | 6 passed — density |
18/18 | — |
N1 — from"./zz-alpha2-nl.js" (no whitespace) + sibling table as a one-code-per-line template literal (0 quoted codes) |
1 failed | 6 passed — the import pin alone |
18/18 | \s* works; the shape scans are blind to that sibling, so the import pin is load-bearing |
| N2 — space-separated template-literal table in a sibling, relative import | 2 failed | 5 passed — import pin + SPACED (backtick) |
18/18 | — |
| N2b — the same one-code-per-line template literal inside the door (377 lines, 247 bare-code lines on disk) | green 7/7 | 18/18 | DENSE's separator class excludes \n (:149); SPACED needs spaces; ARRAY needs quotes |
| N3 — double-space-separated string in the door | 1 failed | 6 passed — density |
18/18 | as the 1–2 separator claims |
| N3b — triple-space-separated | green 7/7 | 18/18 | gap, implausible |
N4 — one 498-char string, .match(/../g) (the .split('') class) |
green 7/7 | 18/18 | gap, implausible |
N5 — .json table + import … with { type: 'json' } in the door |
1 failed | 6 passed — import pin |
— | the walk not reading .json/.mts/.cts is harmless while the door is the consumer |
N8 — import ZZ = require("./zz-alpha2-nl.js") (no from) |
green 7/7 | crash: Cannot find module './zz-alpha2-nl.js' |
tsc exit 0 — closed by the suite's own module resolution, not by any pin |
N9 — top-level await import('./zz-alpha2-nl.js') (no from) |
green 7/7 | 18/18 | tsc exit 0; tsup exit 1: Top-level await is not available in the configured target environment ("es2020") — closed by the build gate only |
N6 — the second judge (the finding). M16's alternation, unchanged, inserted in settings-service.ts in place of const rejected = firstRejectedDomainMember(domain, value); at the validatePatch call site (:2074): a walk that asks /^(?:AD|AE|…|ZW)$/ for iso_3166_alpha2 and falls through to the door for the other two domains. On disk: door blob == HEAD, env-door call site (:1031) untouched, alternation present, 0 quoted two-letter codes added.
- ratchet 7/7, door suite 18/18, whole package 30 files / 533 tests green.
- Reachable — measured, not read: N6d = the same mutation with
USandCHdeleted from its alternation →settings-service.test.ts2 failed | 137 passed(default_country: the domain refuses what the pattern admits,write door: admits an assigned code, refuses ZZ/UK …);settings-env-pattern.test.ts12/12, correctly, since the env door was not patched. So the second judge decides thevalue_domainFieldError on every alpha-2 save; it is not dead code. - Why every instrument misses it: the import-surface pin reads
DOORonly (:134-136);DENSEreadsDOORonly (:148);SPACED/ARRAYare package-wide but need a quote (:176-177); the single-caller pin counts callers ofisValueDomainMember(and N6 adds none (:195); the population pins drivefirstRejectedDomainMember, nevervalidatePatch(value-domains.test.ts:195-200). The header's argument for door-only scope — "the import pin is what keeps one in a sibling module out of reach" (:145-147) — covers a table the door reaches; N6's table is never reached by the door, it stands in front of it in the door's caller. That is the case the argument does not cover, and the measurement above is what it costs. - The false-positive question — answered by census, not argument.
DENSEover the package's 31 runtime sources on the clean head: 0 hits. Over 1,885 runtime.tsfiles in 49srcroots repo-wide (comments masked by the repo's ownstripComments): 1 hit, and it ispackages/spec/src/shared/value-domain.zod.ts— the shared table itself, the one place the definition belongs (positive control:SPACEDfinds the same file). Under N6 the same census reports exactlysettings-service.tsand nothing else. Named legitimate shapes, measured against the as-is regex: a quoted state array['CA', 'NY', …], a string enumMO = 'MO', a union type'US' | 'GB', a{ AD: 1, … }map andYYYY-MM-DD HH:mm:ssdo not trip it (quotes and digits push the separator past two characters); an RRULEMO,TU,WE,TH,FR,SA,SUstring or a bare numericenum Weekday { MO, TU, … }with ≥7 members would — neither exists anywhere in the tree. The exposure the header fears from "a curated dropdown" is already carried by the package-wideARRAYscan (a quoted dropdown trips that one, notDENSE), so wideningDENSEadds no false-positive class the ratchet does not already accept. Package-wide is measured-safe on this tree.
Widening the separator class to admit \n ([^A-Za-z0-9]{1,2}) catches the N2b text and still M16's (checked on the injected declarations), with the same repo-wide count: 1 hit, the spec table. N3b and N4 stay uncaught by either — documented below.
Item 2 — population pins: discharged
- Population, never oracle — by reading:
agrees/disagreements(value-domains.test.ts:195-200) compare the door and the predicate per code; the currency and zone pins (:214-218,:226-229) assert onlydisagreements(...) === []overIntl.supportedValuesOf, guarded bylength > 100. The alpha-2 population (:233) isISO_3166_ALPHA2_CODESfrom@objectstack/spec/shared— the published set, not anything derived from the door; it carries no inline guard, but:257(size === 249) in the same file guards it, so "each pin carries a vacuity guard" is true for two of three inline and for the third one describe down. - By measurement (temporary
setupFilesconfig, deleted after, status clean): host enumeration patched to addZZZ/QQQ(which the snapshot lacks) and dropCHF, and to addMars/Olympus/Europe/Munichand dropEurope/Zurich→ door suite 18/18 green; a control case confirmed the pin saw the patched population (163 codes, noCHF,ZZZpresent). Enumeration returning[]→2 failed | 16 passedonthe runtime enumerated no currencies/… no time zones.supportedValuesOfdeleted →2 failedbyTypeError. So no ICU build reddens a correct door, and the guards guard. Round 1's objection remains satisfied — checked, not assumed. - M14 (ten-entry
.includesallow-list) → door suite1 failed | 17 passed,[ 'AED', 'AFN', 'ALL', 'AMD', …(148) ]= 152; M15 (&& !startsWith('X')) →1 failed | 17 passed,[ 'XAF', 'XCD', 'XCG', 'XDR', …(3) ]= 7; ratchet 7/7 under both. Both reproduce. - N7 — widening by one literal (
|| v === 'XK'for alpha-2): ratchet 7/7, door 18/18, package 533/533. Inherent: the population covers the narrowing direction exhaustively; the complement is infinite and only the corpus samples it. Documented gap, not required — the corpus could carry the well-known near-misses (XK,EU,AN,CS,SU,YU;BTC,CNH,XTS) if the dev wants it closed. - Header
:150-156: the allow-list clause is gone and its replacement is true for a divergent second definition (M14/M15); an identical duplicate still passes, aspin.test.ts:22-26says itself. Nit: "a second membership definition … reddens" → "a divergent one".
Waived nits from round 1 — still taken: turbo.json:27 dependsOn: ["^build"]; the changeset is future-tense and cites #15316 (:18-19, :42-43); value-domains.test.ts:249-256 calls the alpha-2 loop the plumbing pin it is.
Required change (test-only; value-domains.ts / settings-service.ts untouched)
R1″ — value-domains.shared-predicate.pin.test.ts:139-151: run DENSE over runtimeSources() — every runtime source, the same scope SPACED/ARRAY already have — instead of DOOR, and let its separator class admit a newline: /(?:\b[A-Z]{2}\b[^A-Za-z0-9]{1,2}){7}/. Evidence: N6 green 533/533 on a path N6d proves live; N2b green 7/7; census 0/31 in the package and 1/1,885 repo-wide, the one hit being the shared definition itself. Rewrite the header's door-only paragraph (:56-57, :139-147) to what is measured: the import pin covers a table the door reaches; the density scan covers a judge in front of the door. Confirm by re-running N6 (expect 1 failed, naming settings-service.ts) and the clean head (expect 7/7).
Documented gaps — no change asked, so they are not a fourth round: N3b (≥3-char separators), N4 (no separator), N7 (widening; corpus-sampled), N8 (closed by the suite's module resolution, not a pin), N9 (closed by the tsup target, not a pin). Worth one sentence each in the NOT-covered list so the claim stays the size of the evidence.
Not measured: the CI host's ICU (the independence argument is structural, plus the simulation above; measured on 78.2 only); the other 55 gate families (PM-measured on this head; no runtime file moved — my own hash check agrees); ESLint controls (not re-run this round; nothing here depends on lint).
Generated by Claude Code
… judge stands in front of the door, not behind it Round 3, one change. The density scan ran on the door alone, on the argument that the import-surface pin kept every other table out of reach. That argument is now measured false, and the counter-example is not a missed shape but a missed TOPOLOGY: the same 249-code alternation placed in the door's CALLER — `settings-service.ts`, replacing the `firstRejectedDomainMember(…)` call at the save-path refusal — is never reached by the door. It stands in front of it, answers `iso_3166_alpha2` itself and falls through for the other two. Door blob unchanged, no quoted codes, no new caller of the predicate; every instrument green, whole package green, and a second judge deciding the `value_domain` FieldError on every alpha-2 save. Deleting two codes from it turned two behavioural cases red, so it was live code. - `DENSE` now runs over `runtimeSources()` — the package-wide scope `SPACED` and `ARRAY` already had — and its separator class admits a newline, which also closes the one-code-per-line template literal inside the door. - The header states the division the evidence supports: the import pin covers a table the door REACHES, the density scan covers a judge IN FRONT of it. Widening the scope is backed by census rather than by argument: 0 hits over this package's 31 runtime sources, and 1 hit over 2,186 runtime `.ts` files in 76 `src` roots repo-wide — `packages/spec/src/shared/value-domain.zod.ts`, the shared table itself, which is where the definition belongs. `SPACED` finds the same single file as a positive control. Also from the review, without chasing them: the NOT-covered list now names the five routes measured open (3-character separators, no separator at all, widening by one literal, and the two closed by the toolchain rather than by a pin), the trap corpus seeds the plausible widenings (XK, EU, AN, CS, SU, YU, BTC, CNH, XTS), and the single-caller comment no longer claims a second membership definition reddens — only a divergent one does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
|
Round 3 addressed — patched head R1″ — the one change
Your finding is accepted as a falsification, not a gap. The door-only version rested on an argument — the import pin keeps a table in a sibling module out of reach — that is true only of a table the door reaches. N6's table is never reached by the door; it stands in front of it, in the door's caller. The header now says exactly that, as the division of labour between the two checks: the import pin covers a table the door reaches; the density scan covers a judge in front of the door. That sentence is the part I got wrong, so it is the part that got rewritten rather than merely extended. Results at the patched head
Under N6 with the full table the settings-service suite is 139/139 — the judge is behaviourally invisible, which is the whole point of catching it by shape. Census, reproduced independentlyRepo's own Documented gaps, added and not chasedThe NOT-covered list now names each with its reason: 3-character separators (N3b); no separator at all, one 498-char string plus One gate finding of my own, from this round
Method note, takenYour backtick incident is the same lesson as my VerificationGate family re-derived on this head (57, unchanged) and re-run in full: 57 run · 57 exit 0 · 0 red, workspace closure built first ( Generated by Claude Code |
Contract review — PR #15434 (Clause-②), round 4, head
|
| on disk (counted back) | result | naming | |
|---|---|---|---|
| clean head | — | ratchet 7/7 (exit 0); package 30 files / 533 tests (exit 0); tsc --noEmit exit 0 |
— |
N6 — M16's alternation as a second judge in settings-service.ts, replacing firstRejectedDomainMember(…) at the validatePatch refusal (:2074) |
249 codes AD…ZW; 0 quoted pairs added; 0 new Set(; 0 imports; env-door site :1031 intact |
ratchet 1 failed; settings-service.test.ts 139/139 green in the same run (146 = 145 passed + 1 failed) |
settings-service.ts carries a dense run of two-letter tokens |
N6d — same, US/CH deleted |
247 codes, both confirmed absent | settings-service.test.ts 2 failed | 137 passed; settings-env-pattern.test.ts 12/12 |
default_country: the domain refuses what the pattern admits, write door: admits an assigned code, refuses ZZ/UK … — round 3's two names exactly |
| N2b — one-code-per-line template literal inside the door, consulted for alpha-2 | 249 bare-code lines AD…ZW; 2 backticks added; quoted pairs unchanged (1→1); 0 imports; original findIndex line 0 remaining |
ratchet 1 failed; door suite 18/18 (expected — the table agrees) |
value-domains.ts carries a dense run of two-letter tokens |
So the second judge is behaviourally invisible with the full table (139/139), live when it diverges (N6d), and now caught by shape (N6) — which is the whole argument for widening the scan, and it is now true as measured.
Census — re-run on my own scopes, repo's own stripComments, clean tree:
| scope | files | DENSE |
SPACED (control) |
ARRAY |
|---|---|---|---|---|
this package's runtime src/ |
31 | 0 | 0 | 0 |
every src/ root under packages/ apps/ examples/ (77 roots), runtime .ts |
2,186 | 1 | 1 | 0 |
all git-tracked .ts/.tsx/.mts/.cts, non-test |
2,464 | 1 | 1 | 0 |
all git-tracked .ts/.tsx/.mts/.cts/.js/.mjs/.cjs, tests included |
5,909 | 1 | 1 | 2 |
The one DENSE hit is packages/spec/src/shared/value-domain.zod.ts at every scope — the shared table itself, with SPACED finding the same file as the positive control; the two ARRAY hits at the widest scope are the two test files, which the ratchet exempts as evidence. My 2,186 reproduces the dev's file count exactly (77 vs their 76 roots is a find boundary, not a hit). Widening the scan to the package costs nothing on this tree and would cost nothing on the whole tree; the false-positive risk is what the census says.
The rewritten header sentence is true. "The import pin covers a table the door reaches; the density scan covers a judge in front of the door" — the first half was measured in round 3 (N1/N2/N5: relative import → import pin red), the second half is N6 above. The header's other new claims are each a measurement I reproduced: "deleting two codes turned two behavioural cases red" (N6d), "whole package green" under the full table (139/139 here, 533/533 in round 3), "0 hits" in the package. The census comment at pin.test.ts:220-223 carries round 3's 1,885/49 rather than the dev's own 2,186/76; both read "1 hit", so it is true at its stated scope — noted, not asked. tsc --noEmit covers the file (exit 0).
check:cross-package-test-inputs — confirmed both ways
- Green on this head:
pnpm run check:cross-package-test-inputsexit 0 — self-test 117/117, thenOK: 27 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob. - Globs not widened:
scripts/cross-package-test-inputs.mjsis blobac41dc56cat46b06feec,6f2f77edfand17eb7941d— unchanged since round 1. The PR's only entry for this package (vs merge-basea56baa2bd) is the round-1 declaration ofscripts/js-comment-mask.mjs+.d.mts, which the test does import. Nothing underpackages/spec/**is declared. - The red reproduces: I spelled
packages/spec/src/shared/value-domain.zod.tsback into the census comment (1 occurrence on disk) → gate exit 1:@objectstack/service-settings names path(s) no declared glob covers … packages/spec/src/shared/value-domain.zod.ts (named in …/value-domains.shared-predicate.pin.test.ts). Restored, blob MATCH, status clean. So the dev's account is accurate, and naming the module without a path was the right fix rather than a false declaration.
New — one shape, dispositioned as a gap
G2 — the N5 carrier in the caller. import ALPHA2 from './zz-alpha2.json' with { type: 'json' } in settings-service.ts, judging alpha-2 at the validatePatch refusal (249 codes counted back from the .json, the door untouched): ratchet 7/7, settings-service.test.ts 139/139, tsc --noEmit exit 0 (resolveJsonModule is on at the root), tsup build exit 0 — fully open, closed by nothing. Why: runtimeSources() reads only *.ts (:133), so the carrier is never scanned; the import pin reads the door only; the caller itself carries no dense run. Round 3 recorded the walk's .json/.mts/.cts blindness as "harmless while the door is the consumer" — the door-only argument that N6 then falsified, so this is that record meeting that falsification, not a new class. Disposition: documented gap, not required (reasoning in the VERDICT paragraph). If the dev wants it closed, the closure is one line — admit .json in the walk's extension filter, at which point ARRAY catches a JSON array of quoted codes — but I am not making it a condition, and I have not measured that closure.
Shapes I considered and did not open: an object-key map ({ AD: 1, … }) or a string enum (AD = 'AD') used as a judge — both are the ≥3-character-separator class the header already lists as N3b; a quoted array or Set in the caller — caught by the package-wide ARRAY scan already; a caller that consults ISO_3166_ALPHA2_CODES from @objectstack/spec/shared directly — a second call site, not a second definition, so not the divergence the ratchet exists for.
Not measured: the CI host's ICU (structural independence argument stands from round 3; measured here on the same Node 22.22.2 build only); the other gate families beyond check:cross-package-test-inputs (PM-measured on this head; no runtime file moved — my own hash check agrees); ESLint (not re-run; nothing here depends on lint); the wire change, redaction paths and A2/A3 (explicitly out of this round's scope; unchanged blobs).
Generated by Claude Code
Fixes #15162
The services half of the maintainer's ruling A of 2026-09-02 on #14168: one closed vocabulary and one membership predicate shared by settings specifiers and object fields. The spec half landed the shared module in #15133; this deletes
service-settings' second copy of all three definitions, re-points the door ontoisValueDomainMember, and adopts the catalog member the same ruling created.Clause ② — contract review is owed on this PR. Three rounds so far, every finding in the test files;
value-domains.tsandsettings-service.tshave been byte-identical since round 1 cleared them (618033da…and15e807fa…, re-verified by hash after every mutation restore). Sections 8-10 carry each round's fixes with the mutations that prove them.1 · The wire-visible change
Measured on
PUT /api/settings/localizationwith body{"timezone": "Mars/Olympus"}, basea56baa2bdvs this branch. The BEFORE row was produced by checking the two files out at the base into this same tree and running the same script against them (restore verified byte-identical afterwards bygit hash-objectagainst the HEAD blobs), not reconstructed from the diff.error.details.fields[0]codeinvalid_valuevalue_domainmessageDefault timezone must be a valid IANA time zone identifier (e.g. 'Europe/Zurich'). Received 'Mars/Olympus'.Default timezone must be a valid IANA time zone identifier, e.g. Europe/Zurich (got "Mars/Olympus")fieldtimezonetimezonelabelDefault timezoneDefault timezoneconstraint{ valueDomain: 'iana_time_zone' }{ valueDomain: 'iana_time_zone' }valueMars/OlympusMars/OlympusSETTINGS_VALIDATIONSETTINGS_VALIDATIONSo the break is exactly one cell wide plus the sentence. A client reading
constraint.valueDomain— the machine-readable half ADR-0114 asks it to read — is unaffected; a client branching oncode === 'invalid_value'for a domain breach must move.Why the code moved (seat ruling 2 on #15133, implemented as directed and not re-decided here): ADR-0114's rule is that the code is the constraint's own name, the way
max_lengthnames the bound it breached. This branch tookinvalid_value— the catalog's declared slot for "rejected for a reason no other member names" — only while no member named a standard-domain breach. The spec half added one.Why the message moved: the card asks for the catalog templates, and the sibling door under the same ruling (
record-validator.ts) already renders from them. The door now callsrenderValidationMessagewithvalue_domain_DOMAIN, inen. For anencryptedspecifier the offending value is still never echoed — the catalog template always interpolates the value, so the redaction is the REST boundary's own mask rather than a truncated sentence, andfields[0].valuestays absent as before:Both doors are covered. The second door —
effectiveEnvOverride, which refuses anOS_*override — emits a log line, not a FieldError: it has no error code to change, so what it carries is prose, andvalueDomainPhrasingsurvives as its presenter, pinned fragment-by-fragment against the catalog template for the same domain.2 · Equivalence, measured before anything was deleted
Node v22.22.2, the repo's baseline.
iso_3166_alpha2— the two 249-code literals were diffed mechanically before either was deleted: 249 unique each, symmetric difference 0 in both directions, ordered sequences identical. This was the one assumption that could have turned the card into a ruling question; it did not.iso_4217_currency— this one changes definition: a run-timeIntl.supportedValuesOf('currency')probe becomes the key set of the checked-in CLDR snapshotCURRENCY_FRACTION_DIGITS. Measured 162 vs 162, symmetric difference 0 both ways, positive controlCHFin both, negative controlXYZin neither. What changes is that the verdict no longer varies with the host's ICU build. Probe-versus-snapshot as a definition question stays pinned beside the snapshot in the spec package; what this package pins is agreement (section 9).iana_time_zone— the identicalIntl.DateTimeFormatprobe on both sides, unmoved.knownValueDomainnow filters through the closed enum's ownsafeParse, which makes the guard structural rather than hand-written.'constructor','toString','__proto__'and'hasOwnProperty'stay pinned across the swap.3 · The two instruments, and the division of labour between them
The ratchet (
value-domains.shared-predicate.pin.test.ts) is a source scan over this package's whole non-test source, recursively, in every string delimiter or none. Two of its checks are not interchangeable and the difference is measured, not asserted:value-domains.tsmay import from@objectstack/spec/sharedand nothing else, so a table anywhere in the tree is inert while nothing here can name it;The agreement pins (
value-domains.test.ts) are behavioural: the door andisValueDomainMembermust answer alike over each domain's whole population — every codeIntl.supportedValuesOfgives for currency (162 here) and time zone (418 here), the published 249 for alpha-2 — plus a corpus for the traps and the plausible widenings no population contains.Stated rather than hidden: the original vocabulary-parity pin went vacuous with the re-point, exactly as the card warned. The equality that still matters is pinned where both sides live (
SpecifierValueDomainSchemaisValueDomainSchema, identity not equal members).4 · Assumption audit
iana_time_zonecannot moveiso_4217changes definition, keeps its accept setvalue_domainoccurs zero times in the package:1025is insideeffectiveEnvOverride(log line, no code at all),:2060insidevalidatePatch(the FieldError). Only one site had a code to change; both are addressedsafeParse; pins extendedvalueDomainPhrasingsurvives as the env-door presenter, pinned against the catalogXDRsentence is falseXDRis in both sets;VED/XAU/XAGin neither. Deleted with the file, not carried forwardliveness/field.json:228names this card5 · Out of scope, and one finding for the reader of #15134
The engine write path is untouched, and so are
packages/spec/liveness/*— this branch touches no file under the spec package at all. On #15134 (the core copy — afindingawaiting first grading): its premise appears already dead onmain.resolve-authz-context.tsimportsisValueDomainMemberat:62and calls it at:1019; the module-privateisValidTimeZoneis gone, with a pin guarding the removal, and a repo-wide scan finds no ISO 3166 / ISO 4217 / IANA membership table anywhere under that package'ssrc. Triage's call, not mine.6 · Changeset
minor+ a BREAKING banner. Inside the launch windowmajoris forbidden and the bump level is not the carrier for breaking-ness — the banner and the ADR-0087 disposition are, so both are written. This is not an additive widening of a published surface (b337a1308's rule); it is a changed refusal code on a published door, strictly more than that rule's floor, andminoris the highest level the window allows.7 · Verification
All of it at final commit
17eb7941d. Earlier heads:46b06feec→6dad1547e(round 1) →6f2f77edf(round 2) →17eb7941d(round 3).check:comment-mask-adoption,check:cross-package-test-inputs(twice — most recently because a round-3 comment spelled a cross-package path the test does not read; the sentence was reworded rather than a false input declared).check:dual-build-cjs-loadsandcheck:type-check-debtanswerexit 3 / PREREQUISITE NOT METuntil the workspace closure exists — read as NOT MEASURED, never as a pass — soturbo run build(71/71) runs first and both are re-run to a real 0.tsc --noEmitclean, and--listFilesconfirms it covers this package's 30 test files.8 · Round-1 review
Every mutation in sections 8-10 was written to disk, confirmed on disk by observing the injected and removed content — not the writer's exit code, and from round 3 by counting the injected codes, since an empty or truncated table would fake a green — and restored under an
EXIT/INT/TERMtrap, the restore proven by an emptygit statusand agit hash-objectcomparison.src/manifests/);readdirSyncis not recursive. Green 5/5 →1 failed | 5 passed. Fixed by walkingsrc/recursively.2 failed | 4 passed. Fixed by the array shape and, at the root, the import-surface pin.Under M5 the door suite stays green: a duplicate table with identical membership changes no behaviour. That is the argument for the ratchet being a source scan.
9 · Round-2 review
R1′ — every scan was single-quote-shaped. M5q (M5 verbatim, double-quoted) was green 6/6; this package has no
quoteslint rule active, so both spellings are legal here. M16 (249 codes as a regex alternation, no quotes and no import) was green. Both closed: the scans now read/\bfrom\s*(['"])([^'"]+)\1/,/(['"])(?:[A-Z]{2} ){7}/and/(?:(['"])[A-Z]{2}\1,\s*){7}/, plus a density scan for the shapes that carry no delimiter at all. **After:2 failed | 5 passedand1 failed | 6 passed`.**R2′ — the agreement pin covered samples while claiming to cover "a curated allow-list". M14 (a ten-entry
.includeslist) passed the whole package green 529/529 — all four sampled currency members were inside the list. M15 (predicate&& !v.startsWith('X')) likewise. Fixed by asserting agreement over each domain's population. After: 152 and 7 disagreements (XAF, XCD, XCG, XDR, XOF, XPF, XSU), both reproducing the review's numbers exactly. The host enumeration is the population, never the oracle — each code goes to the door and the predicate and the two must answer alike — so no ICU build can redden a correct door or hide a divergent one. The review confirmed that independently under a hostile ICU simulation.10 · Round-3 review — the topology, not the shape
N6 falsified an argument rather than missing a case. The same alternation, placed in the door's caller —
settings-service.ts, replacing thefirstRejectedDomainMember(…)call at the save-path refusal — is never reached by the door: it stands in front of it, answersiso_3166_alpha2itself and falls through for the other two. Door blob unchanged, env-door call site untouched, zero quoted codes added. Ratchet 7/7, door suite 18/18, whole package 533/533 green, while a second judge decided thevalue_domainFieldError on every alpha-2 save. N6d — the same table withUSandCHdeleted — turnedsettings-service.test.tsto2 failed | 137 passed(default_country: the domain refuses what the pattern admits,write door: admits an assigned code, refuses ZZ/UK), so it was live code, not decoration.The one change:
DENSEruns overruntimeSources()— the package-wide scopeSPACED/ARRAYalready had — with a separator class that admits a newline:After, at
17eb7941d: N6 →1 failed | 6 passed, namingsettings-service.ts. N2b (a one-code-per-line template literal inside the door, which the old separator class excluded) →1 failed | 6 passed, namingvalue-domains.ts. Clean head → 7/7.The header's old justification — the import pin keeps a table in a sibling module out of reach — is replaced by what is measured: the import pin covers a table the door reaches; the density scan covers a judge in front of the door.
Widening the scope is backed by census, not by argument. Reproduced independently here with the repo's own
stripComments: 0 hits over this package's 31 runtime sources; 1 hit over 2,186 runtime.tsfiles in 76srcroots repo-wide — the shared table's own module in the spec package, which is where the definition belongs.SPACEDfinds the same single file as a positive control.Documented gaps, added to the header's NOT-covered list and deliberately not chased: 3-character separators (N3b); no separator at all, one 498-char string plus
.match(/../g)(N4); widening by a single literal such as|| v === 'XK'— the population covers narrowing exhaustively but the complement is infinite, so the trap corpus now seedsXK,EU,AN,CS,SU,YU,BTC,CNH,XTSrather than pretending to close it (N7); and two routes closed by the toolchain rather than by a pin — the suite's module resolution (N8) and thetsupes2020 target (N9) — recorded because a toolchain is not a guarantee. The nit is taken too: the single-caller comment now says a divergent second definition reddens, since an identical duplicate does not.Generated by Claude Code