Skip to content

fix(service-settings): re-point value-domains.ts onto @objectstack/spec/shared and refuse with value_domain - #15434

Merged
os-warren merged 5 commits into
mainfrom
claude/issue-15162-value-domain-shared-predicate
Sep 5, 2026
Merged

fix(service-settings): re-point value-domains.ts onto @objectstack/spec/shared and refuse with value_domain#15434
os-warren merged 5 commits into
mainfrom
claude/issue-15162-value-domain-shared-predicate

Conversation

@os-warren

@os-warren os-warren commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

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 onto isValueDomainMember, 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.ts and settings-service.ts have been byte-identical since round 1 cleared them (618033da… and 15e807fa…, 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/localization with body {"timezone": "Mars/Olympus"}, base a56baa2bd vs 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 by git hash-object against the HEAD blobs), not reconstructed from the diff.

field of error.details.fields[0] before after
code invalid_value value_domain
message Default 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")
field timezone timezone
label Default timezone Default timezone
constraint { valueDomain: 'iana_time_zone' } { valueDomain: 'iana_time_zone' }
value Mars/Olympus Mars/Olympus
HTTP status / envelope code 400 / SETTINGS_VALIDATION 400 / SETTINGS_VALIDATION

So 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 on code === '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_length names the bound it breached. This branch took invalid_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 calls renderValidationMessage with value_domain_DOMAIN, in en. For an encrypted specifier 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, and fields[0].value stays absent as before:

Region code must be a valid ISO 3166-1 alpha-2 country code, e.g. CH (got "••••••••")

Both doors are covered. The second door — effectiveEnvOverride, which refuses an OS_* override — emits a log line, not a FieldError: it has no error code to change, so what it carries is prose, and valueDomainPhrasing survives 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-time Intl.supportedValuesOf('currency') probe becomes the key set of the checked-in CLDR snapshot CURRENCY_FRACTION_DIGITS. Measured 162 vs 162, symmetric difference 0 both ways, positive control CHF in both, negative control XYZ in 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 identical Intl.DateTimeFormat probe on both sides, unmoved.
  • The prototype-chain guard is not lost. knownValueDomain now filters through the closed enum's own safeParse, 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:

  • a table the door reaches is closed at its root by the import-surface pinvalue-domains.ts may import from @objectstack/spec/shared and nothing else, so a table anywhere in the tree is inert while nothing here can name it;
  • a judge standing in front of the door is closed by the package-wide density scan — seven or more bare two-letter tokens separated by one or two non-alphanumerics, newline included. Round 3 falsified the door-only version of this scan; see section 10.

The agreement pins (value-domains.test.ts) are behavioural: the door and isValueDomainMember must answer alike over each domain's whole population — every code Intl.supportedValuesOf gives 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 (SpecifierValueDomainSchema is ValueDomainSchema, identity not equal members).

4 · Assumption audit

verdict decided by
A1 iana_time_zone cannot move confirmed identical probe bodies; door and predicate agree over all 418 zones this runtime enumerates
A2 the two alpha-2 lists are equal confirmed by measurement literal-extraction diff: 249/249, symmetric difference 0, order identical
A3 iso_4217 changes definition, keeps its accept set confirmed 162/162, symmetric difference 0 both ways
A4 value_domain occurs zero times in the package confirmed, with a labelling correction grep = 0. The card's two line refs are swapped: :1025 is inside effectiveEnvOverride (log line, no code at all), :2060 inside validatePatch (the FieldError). Only one site had a code to change; both are addressed
A5 the prototype-chain guard must survive confirmed still null, now via safeParse; pins extended
A6 spec exports four things, no phrasing equivalent confirmed valueDomainPhrasing survives as the env-door presenter, pinned against the catalog
A7 the XDR sentence is false confirmed XDR is in both sets; VED/XAU/XAG in neither. Deleted with the file, not carried forward
A8 liveness/field.json:228 names this card confirmed, deliberately left false the same 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. Flagged to the PM to sequence

5 · 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 — a finding awaiting first grading): its premise appears already dead on main. resolve-authz-context.ts imports isValueDomainMember at :62 and calls it at :1019; the module-private isValidTimeZone is 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's src. Triage's call, not mine.

6 · Changeset

minor + a BREAKING banner. Inside the launch window major is 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, and minor is the highest level the window allows.

7 · Verification

All of it at final commit 17eb7941d. Earlier heads: 46b06feec6dad1547e (round 1) → 6f2f77edf (round 2) → 17eb7941d (round 3).

  • Gates — derived from the real diff on the final commit and re-run in full: 57 run · 57 exit 0 · 0 red. Three were genuinely red on the way and are fixed in the diff, never routed around: 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-loads and check:type-check-debt answer exit 3 / PREREQUISITE NOT MET until the workspace closure exists — read as NOT MEASURED, never as a pass — so turbo run build (71/71) runs first and both are re-run to a real 0.
  • Tests30 files, 533 tests, all passing; tsc --noEmit clean, and --listFiles confirms 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/TERM trap, the restore proven by an empty git status and a git hash-object comparison.

  • M4 — the table one directory down (src/manifests/); readdirSync is not recursive. Green 5/5 → 1 failed | 5 passed. Fixed by walking src/ recursively.
  • M5 — 249 codes as an array literal in a sibling module, imported by the door. Ratchet green 5/5 → 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 quotes lint 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 .includes list) 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 callersettings-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, 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 the value_domain FieldError on every alpha-2 save. N6d — the same table with US and CH deleted — turned settings-service.test.ts to 2 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: DENSE runs over runtimeSources() — the package-wide scope SPACED/ARRAY already had — with a separator class that admits a newline:

/(?:\b[A-Z]{2}\b[^A-Za-z0-9]{1,2}){7}/

After, at 17eb7941d: N6 → 1 failed | 6 passed, naming settings-service.ts. N2b (a one-code-per-line template literal inside the door, which the old separator class excluded) 1 failed | 6 passed, naming value-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 .ts files in 76 src roots repo-wide — the shared table's own module in the spec package, which is where the definition belongs. SPACED finds 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 seeds XK, EU, AN, CS, SU, YU, BTC, CNH, XTS rather 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 the tsup es2020 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

os-warren and others added 2 commits September 4, 2026 14:56
…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
@github-actions github-actions Bot added the size/l label Sep 4, 2026
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-settings, touching 17 documentable anchor(s).

6 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/error-catalog.mdx (via invalid_value (literal, a string literal in validatePatch), value_domain (literal, a string literal in validatePatch))
  • content/docs/data-modeling/drivers.mdx (via SettingsService (symbol, a top-level class))
  • content/docs/data-modeling/validation-rules.mdx (via value_domain (literal, a string literal in validatePatch))
  • content/docs/protocol/kernel/config-resolution.mdx (via SettingsService (symbol, a top-level class), /api/settings/:namespace (route, a path literal in validatePatch))
  • content/docs/protocol/kernel/i18n-standard.mdx (via timeZone (literal, a string literal on a changed line))
  • content/docs/protocol/kernel/index.mdx (via SettingsService (symbol, a top-level class))
What this run could not see
  • the SDK route bridge reached 61 of 219 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 7 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json f1d787294f052a5ee79332b4c0459e24c9ff9f3fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 0863433b829e5f980721333efd679779c9bae61c — the merge of head 17eb7941d2c21e211056f210e9427a3289f319d8 into base f1d787294f052a5ee79332b4c0459e24c9ff9f3f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs f1d787294f052a5ee79332b4c0459e24c9ff9f3f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

Copy link
Copy Markdown
Collaborator Author

Contract review — PR #15434 (Clause-②), head 46b06feec

FUSE: serving model claude-fable-5-1; scripts/pm/dispatch-gates.mjs:8659 holds CONTRACT_REVIEW_TIER = 'claude-fable-5-1' — exact match, this review is valid at the required tier.

VERDICT: CHANGES REQUIRED — two changes, both confined to test files this PR adds (value-domains.shared-predicate.pin.test.ts, value-domains.test.ts). No defect found in value-domains.ts or settings-service.ts: the wire change, the redaction on every path, and the equivalence measurements all hold under attack. Posted as a comment, not REQUEST_CHANGES (shared identity with the author); this VERDICT line is the ruling.

Method. Fresh worktree /home/user/objectstack-cr-15434 at 46b06feec (the pre-existing objectstack-review-15434 worktree was not reused); git status --porcelain --untracked-files=all empty before the first run and after the last. Dependency closure built (turbo run build --filter=@objectstack/service-settings..., exit 0). Every gate/test exit code captured by redirect, never across a pipe. Host: Node v22.22.2, ICU 78.2, CLDR 48.0. @objectstack/spec/shared resolves to source in this package's vitest (vitest.config.ts rule /^@objectstack\/spec\/([a-z-]+)$/), so every verdict below reads the checkout, not a dist.

Per claim

1. "No ablation was performed" — can each new pin go red? — confirmed, with two ratchet limits (→ R1).
Each pin was driven red by a plausible wrong implementation (pnpm exec vitest run <file> per mutation; tree reverted and git status clean between runs):

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.ts instead of src/. runtimeSources() is readdirSync(SRC) (pin.test.ts:59), non-recursive; src/manifests/ and src/translations/ exist today.
  • M5 — green: ratchet 5/5 AND value-domains.test.ts 14/14. A full 249-code table as an array literal (new Set(['AD','AE',…])) in a new src/zz-alpha2-array.ts, imported by value-domains.ts and consulted for iso_3166_alpha2 while isValueDomainMember is kept for the other two domains. RUN (:90) matches only the space-separated string shape; new Set( is checked only in value-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].value absent; JSON.stringify({message, fields, stack}) canary-free.
  • save door, multiselect encrypted: masked, value absent, canary-free.
  • REST PUT /api/settings/:namespace through registerSettingsRoutes: the whole {status, body} is canary-free — including top-level error.message, which is SettingsValidationError'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 from valueDomainPhrasing, 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 })), with secret = reg.encryptedKeys.has(key) (:2084) — the same set secretKeysOf() (: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 no translate hook and no locale (→ en), so no override path can re-render the raw value. Only non-test consumer of the error is settings-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

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

Copy link
Copy Markdown
Collaborator Author

Round 1 addressed — patched head 6dad1547e344488416146ebc733afa57ef940f86 (6dad1547e). Still a draft; not undrafted, not armed, not enqueued. Re-review at tier is owed on this head. Both required changes are in test files only — no line of value-domains.ts or settings-service.ts moved, so the parts the review cleared under attack are byte-identical.

R1 — the ratchet now covers what its header claims

Three changes, and the third is the one that closes M5 at its root rather than by recognising a shape:

  1. runtimeSources() walks src/ recursively and reports paths relative to src/.
  2. Both table SHAPES are detected — SPACED = /'(?:[A-Z]{2} ){7}/ beside ARRAY = /(?:'[A-Z]{2}',\s*){7}/.
  3. A new import-surface pin: the specifiers in value-domains.ts must be exactly ['@objectstack/spec/shared']. A table can exist anywhere in the tree without harm while the door cannot reach it, and a relative import is how it would be reached.

new Set( was not widened package-wide, as directed.

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 — sed/python exit 0 on zero hits), and the tree restored under an EXIT/INT/TERM trap with absolute paths, the restore proven by an empty git status plus a git hash-object comparison against the HEAD blob.

before (pre-review pin) after (6dad1547e) failing case
M4 — table at src/manifests/zz-alpha2-table.ts green 5/5 1 failed | 5 passed carries no ISO 3166-1 alpha-2 code list, in either shape
M5 — 249-code array literal in src/zz-alpha2-array.ts, imported by the door and consulted for iso_3166_alpha2 green 5/5, door suite green 2 failed | 4 passed imports from NOTHING ELSE — the door cannot reach a table wherever one is put, and the array shape

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 gone

Replaced by the invariant this package actually owns: firstRejectedDomainMember agrees with isValueDomainMember value by value, across all three domains, over a corpus carrying every trap the shared module names (Mars/Olympus, Europe/Munich, VED, XAU, usd, ZZ, UK, XX, us, ' CH', ''), plus an element-wise carrier case. A local filter creeping back into the walker reddens even though every representative case would still pass, and nothing in it moves with the host's ICU build.

One deviation from the suggested wording, flagged rather than silently reshaped: looping Object.keys(CURRENCY_FRACTION_DIGITS) is not reachable from this package. The snapshot is internal to packages/spec — not re-exported from data/index.ts and absent from api-surface/ — so importing it would mean widening a published surface, which is outside this card. The agreement pin is the strongest form available to a consumer and covers the same regression class; probe-versus-snapshot stays pinned beside the snapshot, in both directions and with a size, where you pointed.

The ratchet header's supportedValuesOf exemption sentence is dropped, as asked.

Waived items — all three taken, since the files were open

turbo.json uses ^build; the changeset now says the record write path will render from the catalog and cites #15316; and the alpha-2 population loop is described as the plumbing pin it is ("it loops the published set through a door that answers from the published set, so it cannot detect a wrong table — it detects the door failing to reach the right one").

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 head

Gate 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 (turbo run build, 71/71) so check:dual-build-cjs-loads and check:type-check-debt are real zeroes rather than exit 3 / PREREQUISITE NOT MET. Package suite 30 files / 529 tests passing (527 before: the currency probe case is replaced by two agreement cases and the import-surface pin is new), tsc --noEmit clean.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review — PR #15434 (Clause-②), round 2, head 6dad1547e

FUSE: serving model claude-fable-5-1; scripts/pm/dispatch-gates.mjs:8659 holds CONTRACT_REVIEW_TIER = 'claude-fable-5-1' — exact match, this review is valid at the required tier.

VERDICT: CHANGES REQUIRED — two changes, both again confined to the two test files this PR adds (value-domains.shared-predicate.pin.test.ts, value-domains.test.ts). The runtime files are byte-identical to the head round 1 cleared and stay cleared. Posted as a comment (shared identity with the author); this VERDICT line is the ruling.

Method. Fresh worktree /home/user/objectstack-cr2-15434 at 6dad1547e; git status --porcelain --untracked-files=all empty before the first run and after the last. Blob ids re-read, not taken on faith: value-domains.ts 618033da9… and settings-service.ts 15e807fa1… are IDENTICAL at 46b06feec6dad1547e; control value-domains.shared-predicate.pin.test.ts 2efa3281b…953b3fb03… DIFFERS. Nothing below re-derives the wire change, the redaction paths or A2/A3. Every mutation was written to disk, confirmed by observing its ZZMUT anchor on disk, run, and restored under an EXIT/INT/TERM trap — restore proved each time by git hash-object == HEAD blob 618033da9… and an empty status (all MATCH/CLEAN). Exit codes by redirect. Node v22.22.2 / ICU 78.2 / CLDR 48.0; @objectstack/spec/shared resolves to source (vitest.config.ts:87). Baseline on the untouched head: ratchet 6/6, door suite 15/15, tsc --noEmit exit 0, full package 30 files / 529 tests — after turbo run build --filter='@objectstack/service-settings^...' (15/15); the first full-suite run returned Failed to resolve entry for package "@objectstack/metadata-core", which is NOT MEASURED, not a pass. One weak control caught and replaced: my first ESLint control (unused var + debugger;) exited 0 because neither rule is configured here — replaced by --print-config plus a live positive (below).

R1 — M4 and M5 are red as reported; the new "root close" is defeated by the next shape

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
M14iso_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

Copy link
Copy Markdown
Collaborator Author

Round 2 addressed — patched head 6f2f77edf9be1837b69f7e5a87386115eb4a71f8 (6f2f77edf). Still a draft; not undrafted, not armed, not enqueued. Re-review at tier is owed on this head. Test-only again: value-domains.ts and settings-service.ts are still the blobs round 1 cleared (618033da909d7e16384f53fd0b7b590ca123de95 and 15e807fa15e547902d5153557cfaf052f2d0eab8) — every mutation below restored them and each restore was proven by hash, not by an exit code.

R1′ — the delimiter, and the alternation

The three replacements are in as prescribed, and hit 0 of the 31 runtime sources on this head:

:135  /\bfrom\s*(['"])([^'"]+)\1/g      → group 2
:176  /(['"`])(?:[A-Z]{2} ){7}/
:177  /(?:(['"])[A-Z]{2}\1,\s*){7}/

M16 is closed rather than documented. A new door-only check catches the shape by DENSITY, which is what survives every spelling: /(?:\b[A-Z]{2}\b[^A-Za-z0-9\n]{1,2}){7}/ — seven or more bare two-letter tokens separated by one or two non-alphanumerics. Door-only is deliberate and is the coherent scope: that shape has to live in the door to matter, because the import pin is what keeps one in a sibling module out of reach. It is 0 package-wide on this head too, but pinning it package-wide would put a false positive one curated dropdown away.

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 samples

The header sentence is now true. Agreement is asserted over each domain's whole population, with the corpus kept only for traps no population contains:

  • iso_4217_currency over Intl.supportedValuesOf('currency') — 162 here
  • iana_time_zone over Intl.supportedValuesOf('timeZone') — 418 here
  • iso_3166_alpha2 over the published ISO_3166_ALPHA2_CODES — 249

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

before (round-1 head) after (6f2f77edf) evidence
M5q — M5 verbatim, double-quoted ratchet green 6/6, door suite 15/15 ratchet 2 failed | 5 passed imports from NOTHING ELSE … and carries no ISO 3166-1 alpha-2 code list, in either shape; probe verified to contain 0 single-quoted codes
M16 — 249 codes as an alternation in the door green ratchet 1 failed | 6 passed carries no dense run of two-letter tokens in ANY delimiter; probe verified to contain 0 quoted codes and 0 new Set(
M14 — ten-entry .includes allow-list for currency ratchet green, door suite green, package green 529/529 door suite 1 failed | 17 passed, 152 disagreements expected [ 'AED', 'AFN', 'ALL', 'AMD', …(148) ] to deeply equal []
M15 — predicate && !v.startsWith('X') green door suite 1 failed | 17 passed, 7 disagreements expected [ 'XAF', 'XCD', 'XCG', 'XDR', …(3) ] to deeply equal [] — XOF, XPF, XSU are the other three

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 (expect(population.length).toBeGreaterThan(100)) so an empty enumeration cannot pass it silently, and the live positive controls for the pins are M14 and M15 themselves: I did not report the new pin green until I had watched it fail on a real mutation. The same reason each mutation's landing is verified by grepping the injected and the removed text rather than by the writer's exit code — sed, perl -i, str.replace and re.sub all exit 0 on zero hits.

Confirmed and unchanged: Object.keys(CURRENCY_FRACTION_DIGITS) stays unreachable from this package, so the population form is what a consumer can actually assert.

Verification on the patched head

Gate 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 (turbo run build, 71/71) so check:dual-build-cjs-loads and check:type-check-debt are real zeroes rather than exit 3 / PREREQUISITE NOT MET. Package suite 30 files / 533 tests passing (529 before: three population pins replace one corpus pin, plus the alternation check), tsc --noEmit clean. Both re-run after the final commit on a clean tree.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review — PR #15434 (Clause-②), round 3, head 6f2f77edf

FUSE: serving model claude-fable-5-1; scripts/pm/dispatch-gates.mjs:8659 holds CONTRACT_REVIEW_TIER = 'claude-fable-5-1' — exact match, this review is valid at the required tier.

VERDICT: CHANGES REQUIRED — one change, confined to value-domains.shared-predicate.pin.test.ts, and it is one identifier wide. Items 1 and 2 of the brief are otherwise discharged: all four round-2 mutations are red as reported, with the currency numbers reproducing (152 / 7), and the population pins are population-not-oracle under a hostile ICU simulation. What is not discharged is the door-only scope of the density check: its stated justification is falsified by measurement, not merely incomplete — a 249-code alternation placed as a second judge in settings-service.ts ships 533/533 green on a path proven live, while a package-wide density scan finds exactly that file and nothing else across the whole repo. Judged under the proportionality rule: that is a shape a plausible author writes, so it is required, not documented. The remaining shapes are listed as gaps so a fourth round does not chase them. value-domains.ts / settings-service.ts stay byte-identical to the round-1 head and stay cleared. Posted as a comment (shared identity with the author); this VERDICT line is the ruling.

Method. Fresh detached worktree /home/user/objectstack-cr3-15434 at 6f2f77edf; git status --porcelain --untracked-files=all empty before the first run and after the last (HEAD re-read at the end: 6f2f77edf). Own hash check, not the PM's: value-domains.ts and settings-service.ts sha256-identical at 46b06feec → HEAD, control value-domains.test.ts differs. Closure built (turbo run build --filter='@objectstack/service-settings^...', exit 0). Node v22.22.2 / ICU 78.2 / CLDR 48.0; @objectstack/spec/shared resolves to source (vitest.config.ts:87). Baseline on the untouched head: ratchet 7/7, door suite 18/18, package 30 files / 533 tests, tsc --noEmit exit 0. Every mutation was written to disk, confirmed by counting both the injected ZZMUT anchors and the removed original call line (0 remaining), and restored under an EXIT/INT/TERM trap — restore proven each time by git hash-object == HEAD blob for both runtime files plus an empty status (all MATCH/CLEAN). Exit codes by redirect.

Three of my own instruments failed first and are reported as such: (a) the first pass of the "extra" rows (full suite / tsc / tsup) ran in $HOME because the harness did not export its paths into bash -c — those rc=1 rows were NOT MEASURED (ERR_PNPM_RECURSIVE_EXEC_NO_PACKAGE) and were re-run with exports; (b) the M16 and N1 scripts aborted on a quoting error after patching (the trap restored; re-run); (c) the first N2b pass injected an empty table — bash executed the template literal's backtick (AD: command not found) — so its green was no measurement; re-run with the text passed through a file and 247 bare-code lines verified on disk.

Item 1 — M5q and M16 closed; the hunt found a fourth shape

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
N1from"./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
N8import 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 US and CH deleted from its alternation → settings-service.test.ts 2 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.ts 12/12, correctly, since the env door was not patched. So the second judge decides the value_domain FieldError on every alpha-2 save; it is not dead code.
  • Why every instrument misses it: the import-surface pin reads DOOR only (:134-136); DENSE reads DOOR only (:148); SPACED/ARRAY are package-wide but need a quote (:176-177); the single-caller pin counts callers of isValueDomainMember( and N6 adds none (:195); the population pins drive firstRejectedDomainMember, never validatePatch (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. DENSE over the package's 31 runtime sources on the clean head: 0 hits. Over 1,885 runtime .ts files in 49 src roots repo-wide (comments masked by the repo's own stripComments): 1 hit, and it is packages/spec/src/shared/value-domain.zod.ts — the shared table itself, the one place the definition belongs (positive control: SPACED finds the same file). Under N6 the same census reports exactly settings-service.ts and nothing else. Named legitimate shapes, measured against the as-is regex: a quoted state array ['CA', 'NY', …], a string enum MO = 'MO', a union type 'US' | 'GB', a { AD: 1, … } map and YYYY-MM-DD HH:mm:ss do not trip it (quotes and digits push the separator past two characters); an RRULE MO,TU,WE,TH,FR,SA,SU string or a bare numeric enum 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-wide ARRAY scan (a quoted dropdown trips that one, not DENSE), so widening DENSE adds 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 only disagreements(...) === [] over Intl.supportedValuesOf, guarded by length > 100. The alpha-2 population (:233) is ISO_3166_ALPHA2_CODES from @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 setupFiles config, deleted after, status clean): host enumeration patched to add ZZZ/QQQ (which the snapshot lacks) and drop CHF, and to add Mars/Olympus/Europe/Munich and drop Europe/Zurich → door suite 18/18 green; a control case confirmed the pin saw the patched population (163 codes, no CHF, ZZZ present). Enumeration returning []2 failed | 16 passed on the runtime enumerated no currencies / … no time zones. supportedValuesOf deleted → 2 failed by TypeError. So no ICU build reddens a correct door, and the guards guard. Round 1's objection remains satisfied — checked, not assumed.
  • M14 (ten-entry .includes allow-list) → door suite 1 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, as pin.test.ts:22-26 says 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

Copy link
Copy Markdown
Collaborator Author

Round 3 addressed — patched head 17eb7941d2c21e211056f210e9427a3289f319d8 (17eb7941d). Still a draft. Test-only again; value-domains.ts and settings-service.ts are still 618033da909d7e16384f53fd0b7b590ca123de95 and 15e807fa15e547902d5153557cfaf052f2d0eab8, re-verified by hash after each mutation restore.

R1″ — the one change

DENSE now runs over runtimeSources() with a newline-admitting separator class:

/(?:\b[A-Z]{2}\b[^A-Za-z0-9]{1,2}){7}/

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

before after (17eb7941d) naming
clean head ratchet 7/7, package 533/533
N6 — the alternation in settings-service.ts at the validatePatch refusal ratchet 7/7, door suite 18/18, package 533/533 ratchet 1 failed | 6 passed settings-service.ts carries a dense run of two-letter tokens
N6d — same, with US/CH deleted settings-service.test.ts 2 failed | 137 passed default_country: the domain refuses what the pattern admits, write door: admits an assigned code, refuses ZZ/UK — reproduces your liveness evidence exactly
N2b — one-code-per-line template literal inside the door green (old separator class excluded \n) ratchet 1 failed | 6 passed value-domains.ts carries a dense run of two-letter tokens

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 independently

Repo's own stripComments, my own walk rather than your numbers: 0 hits over this package's 31 runtime sources; 1 hit over 2,186 runtime .ts files across 76 src roots (I swept packages/, apps/ and examples/, which is why my file and root counts run above your 1,885/49) — and the single hit is the same one: the shared table's own module. SPACED finds the same file as a positive control.

Documented gaps, added and not chased

The NOT-covered list now names each with its reason: 3-character separators (N3b); no separator at all, one 498-char string plus .match(/../g) (N4); widening by a single literal (N7) — the population pins cover narrowing exhaustively while the complement is infinite, so the trap corpus seeds XK, EU, AN, CS, SU, YU, BTC, CNH, XTS rather than pretending to close it; and the two routes closed by the toolchain rather than by a pin — module resolution (N8) and the tsup es2020 target (N9) — recorded because a toolchain is not a guarantee. Nit taken: the single-caller comment now says a divergent second definition reddens.

One gate finding of my own, from this round

check:cross-package-test-inputs went red on the first version of this patch — because the census paragraph I wrote spelled packages/spec/src/shared/value-domain.zod.ts in a comment, and that gate's scan is source text, comments included, so a spelled path reads as a declared input. The test does not read that file. Fixed by naming the module without a repo-relative path, not by widening the package's globs — declaring an input that does not exist would have been the easier green and the wrong one.

Method note, taken

Your backtick incident is the same lesson as my sed-exits-0 note, from the other side, so I applied it rather than just agreeing with it: both round-3 mutations build their table in Python with the backtick as chr(96) — never through a shell — and then count the codes back off disk and assert the count, the endpoints, and that no quotes, imports or new Set( were introduced. N6 and N2b each report 249 codes (expected 249) before a single test runs; N6d reports 247 with US/CH confirmed absent. An empty table would have aborted the run rather than produced a green.

Verification

Gate family re-derived on this head (57, unchanged) and re-run in full: 57 run · 57 exit 0 · 0 red, workspace closure built first (turbo run build, 71/71) so the two prerequisite gates are real zeroes. Package suite 30 files / 533 tests, tsc --noEmit clean, both after the final commit on a clean tree with every mutation restored.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review — PR #15434 (Clause-②), round 4, head 17eb7941d

FUSE: serving model claude-fable-5-1; scripts/pm/dispatch-gates.mjs:8659 holds CONTRACT_REVIEW_TIER = 'claude-fable-5-1' — exact match, this review is valid at the required tier.

VERDICT: PASS. R1″ — the one change round 3 required — is discharged by my own measurement: N6 and N2b go red naming the right file, N6d reproduces round 3's liveness evidence to the test name, the clean head is 7/7 and 533/533, and the census number holds at every scope I chose, including two wider than either the dev's or round 3's. The check:cross-package-test-inputs item is confirmed both ways (green on this head; red on the spelled path, with the gate naming the file; globs not widened). I found one further shape that defeats the ratchet (G2 below, a .json carrier read by the caller) and chose the documented-gap disposition, not a fifth round: it is round 3's N5 carrier — already recorded as unread by the walk — combined with the caller topology round 3 established, so it is a known-carrier × known-topology corner rather than a new front; a .ts re-typing, the shape a plausible author reaches for first, is caught; and a new .json under src/ is loud in a file list where a regex alternation inside a .ts was not. Not a condition of this PASS. value-domains.ts / settings-service.ts stay byte-identical to the round-1 head (my own hash check) and stay cleared; the wire change, redaction paths and A2/A3 were not re-audited, per the brief. Posted as a comment (shared identity with the author); this VERDICT line is the ruling.

Method. Fresh detached worktree /home/user/objectstack-cr4-15434 at 17eb7941d; git status --porcelain --untracked-files=all empty before the first run and after the last, HEAD re-read at the end (17eb7941d). Own hash check: value-domains.ts = 618033da… and settings-service.ts = 15e807fa… at 46b06feec6dad1547e6f2f77edf17eb7941d; control: the ratchet file differs at each head (2efa3281…953b3fb0…91c2cceff…421086077…). Round-4 diff measured: 2 test files, +91/−31, nothing else. Closure built (turbo run build --filter='@objectstack/service-settings^...', exit 0). Node v22.22.2. Every mutation table was built in Python (backtick as chr(96), no shell), counted back off disk before any test ran, and restored under an EXIT/INT/TERM trap — restore proven each time by git hash-object == HEAD blob for both runtime files plus an empty status (all MATCH / CLEAN). Exit codes by redirect. No 429 was hit; nothing below is unmeasured except the items in the last paragraph.

R1″ — discharged

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-inputs exit 0 — self-test 117/117, then OK: 27 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
  • Globs not widened: scripts/cross-package-test-inputs.mjs is blob ac41dc56c at 46b06feec, 6f2f77edf and 17eb7941d — unchanged since round 1. The PR's only entry for this package (vs merge-base a56baa2bd) is the round-1 declaration of scripts/js-comment-mask.mjs + .d.mts, which the test does import. Nothing under packages/spec/** is declared.
  • The red reproduces: I spelled packages/spec/src/shared/value-domain.zod.ts back 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

@os-warren
os-warren marked this pull request as ready for review September 4, 2026 23:33
@os-warren
os-warren enabled auto-merge September 4, 2026 23:33
@os-warren
os-warren added this pull request to the merge queue Sep 4, 2026
Merged via the queue into main with commit 6b8c677 Sep 5, 2026
41 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

1 participant