Skip to content

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

Draft
os-warren wants to merge 4 commits into
mainfrom
claude/issue-15162-value-domain-shared-predicate
Draft

fix(service-settings): re-point value-domains.ts onto @objectstack/spec/shared and refuse with value_domain#15434
os-warren wants to merge 4 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. Rounds 1 and 2 both returned CHANGES REQUIRED, both entirely in the test files; value-domains.ts and settings-service.ts have been byte-identical since round 1 cleared them. Sections 8 and 9 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. Nothing in the measurement argued against the ruling, so there is nothing to report back on it.

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 now 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 "••••••••")

The sentence this produces for a country field is the same one the engine door produces for the same domain (... must be a valid ISO 3166-1 alpha-2 country code, e.g. CH (got "ZZ")), which is the point of rendering from the catalog rather than composing locally: one ruling, one predicate, one set of words.

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. A pin holds each of its fragments equal to the catalog template for the same domain, so the log line and the wire message cannot drift apart.

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 (a script that extracts the string literals out of both files and compares): 249 unique each, symmetric difference 0 in both directions, and the ordered sequences are 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 in both directions, positive control CHF in both, negative control XYZ in neither. So no settings value changes verdict on this runtime; what changes is that the verdict no longer varies with the host's ICU build — the direction the shared module's header argues for. Probe-versus-snapshot as a definition question stays pinned beside the snapshot in packages/spec/src/shared/value-domain.test.ts; what this package pins is agreement, described in section 9.
  • iana_time_zone — the identical Intl.DateTimeFormat probe on both sides, unmoved.
  • The prototype-chain guard is not lost. knownValueDomain used a hand-written hasOwnProperty guard precisely so 'constructor' / 'toString' could not be admitted as domains; it now filters through the closed enum's own safeParse, which makes the guard structural. The same two names stay pinned across the swap, plus '__proto__' and 'hasOwnProperty'.

3 · The two instruments

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: no membership table in either literal shape, no Intl enumeration probe on an enforcement path, no dense run of two-letter tokens in the door (the alternation shape, which needs no quotes at all), exactly one caller of the predicate, and — the root close — value-domains.ts may import from @objectstack/spec/shared and nothing else. Comment stripping routes through scripts/js-comment-mask.mjs, and that import's radius is declared in cross-package-test-inputs.mjs + turbo.json.

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 no population contains. Between them, a re-typed table is caught by shape and a re-derived judgement is caught by behaviour.

Stated rather than hidden: the original vocabulary-parity pin went vacuous with the re-point, exactly as the card warned — knownValueDomain now answers from ValueDomainSchema, so there is nothing left here to diverge from. The equality that still matters is pinned where both sides live (value-domain.test.ts holds SpecifierValueDomainSchema to be ValueDomainSchema, identity not equal members).

4 · Assumption audit

verdict decided by
A1 iana_time_zone cannot move confirmed identical probe bodies on both sides; 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, CHF in both, XYZ in neither
A4 value_domain occurs zero times in the package confirmed, with a labelling correction grep -rn value_domain packages/services/service-settings = 0. The card labels :1025 validatePatch and :2060 "the second door" — it is the other way round: :1025 is inside effectiveEnvOverride (the env door, log line, no code at all) and :2060 is inside validatePatch (the FieldError). So only one of the two sites had a code to change; the other's before/after is prose. Both are covered above
A5 the prototype-chain guard must survive confirmed knownValueDomain('constructor'/'toString') still null, now via safeParse; pins extended
A6 spec exports four things, no phrasing equivalent confirmed valueDomainPhrasing survives, scope narrowed to the env log line, now pinned against the catalog
A7 the XDR sentence is false confirmed XDR is in both sets; VED / XAU / XAG in neither. The sentence is deleted with the file and is not carried forward
A8 liveness/field.json:228 names this card confirmed, and deliberately left false the row's note says the settings door "re-points onto the shared predicate in its own follow-up card and is unchanged until then". Landing this makes that sentence false. It is not fixed here: the same row is being rewritten by the open engine-half PR, so an edit from this branch would conflict and force a re-derivation of the ledger counts. Flagged to the PM to sequence; the row's planned status is correct either way, since it tracks the engine write path

5 · Out of scope, and one finding for the reader of #15134

The engine write path on object fields is untouched, and so are packages/spec/liveness/* — the valueDomain row there is contended with the open engine-half PR, so this branch touches no file under packages/spec at all. On #15134 (the packages/core copy — a finding awaiting first grading, not touched here): its premise appears already dead on main. packages/core/src/security/resolve-authz-context.ts imports isValueDomainMember from @objectstack/spec/shared at :62 and calls it at :1019; the module-private isValidTimeZone is gone, with resolve-authz-context.time-zone-domain.pin.test.ts guarding the removal. A repo-wide scan finds no ISO 3166 / ISO 4217 / IANA membership table anywhere under packages/core/src. Triage's call, not mine.

6 · Changeset

minor + a BREAKING banner. Under the launch-window convention (check-changeset-no-major.mjs) 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 (the rule that landed in b337a1308); it is a changed refusal code on a published door, which is strictly more than that rule's floor, and minor is the highest level the window allows.

7 · Verification

All of it at final commit 6f2f77edf. Earlier heads: 46b06feec (pre-review), 6dad1547e (after round 1).

  • Gates — derived from the real diff on the final commit with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, re-derived on every head (57 families since round 1, unchanged since). Re-run in full on 6f2f77edf: 57 run · 57 exit 0 · 0 red. Two were genuinely red on the way and are fixed in the diff, not worked around: check:comment-mask-adoption and check:cross-package-test-inputs. 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 tasks) runs first and both are re-run to a real 0.
  • Testspnpm --filter @objectstack/service-settings test: 30 files, 533 tests, all passing. pnpm --filter @objectstack/service-settings typecheck (tsc --noEmit): clean, and --listFiles confirms it covers the 30 test files in this package.
  • The dependency closure was built before any of it, so no verdict here is read off a stale dist.

8 · Round-1 review — required changes, with the mutations that prove them

Every mutation in this section and the next was written to disk, confirmed on disk by observing the anchor text (never the writer's exit code — sed/python exit 0 on zero hits), and restored under an EXIT/INT/TERM trap with absolute paths, the restore proven by an empty git status and a git hash-object comparison against the HEAD blob.

  • M4 — the table one directory down, at src/manifests/zz-alpha2-table.ts. readdirSync is not recursive and src/manifests/ exists. Before: green 5/5. After: 1 failed | 5 passed. Fixed by walking src/ recursively.
  • M5 — 249 codes as an array literal in src/zz-alpha2-array.ts, imported by the door and consulted for iso_3166_alpha2. Before: ratchet green 5/5, door suite green. After: 2 failed | 4 passed. Fixed by the array shape regex and, at the root, the import-surface pin.

Under M5 the door suite stays green, correctly — a duplicate table with identical membership changes no behaviour. That is the argument for the ratchet being a source scan.

9 · Round-2 review — the delimiter, the alternation, and populations over samples

R1′ — every scan was single-quote-shaped.

  • M5q = M5 verbatim but double-quoted (from "./zz-alpha2-array.js", ["AD", "AE", …]). Before: ratchet green 6/6. The import pin read /\bfrom\s+'([^']+)'/ and both shape regexes hard-coded ', and this package has no quotes lint rule active, so both spellings are legal here. After: 2 failed | 5 passed — the import pin and the array shape.
  • M16 — the 249 codes as a regex alternation inside the door (/^(?:AD|AE|…|ZW)$/): no import, no quotes, no new Set(. Before: green. Rather than record it as NOT-covered, it is closed by density: seven or more bare two-uppercase-letter tokens separated by one or two non-alphanumerics, checked on the door — the only place the shape can live now that the import pin holds. After: 1 failed | 6 passed.

The three scans now read /\bfrom\s*(['"])([^'"]+)\1/, /(['"])(?:[A-Z]{2} ){7}/and/(?:(['"])[A-Z]{2}\1,\s*){7}/`, and hit 0 of the 31 runtime sources on this head. Still NOT covered, and now stated as such in the file's header: a 3-letter table (a currency list) is not shape-detected in any spelling, and a table reached through a bare package specifier would pass the import pin. Currency is instead reached behaviourally, below.

R2′ — the agreement pin covered a corpus while its header claimed it covered "a curated allow-list". Measured false:

  • M14iso_4217_currency answered by a ten-entry .includes list, predicate kept for the other two. Before: ratchet green, door suite green, whole package green 529/529 — all four sampled currency members were inside the list. After: 1 failed | 17 passed, 152 disagreements (['AED', 'AFN', 'ALL', 'AMD', …(148)]).
  • M15 — predicate and !v.startsWith('X'), which drops real codes. Before: green. After: 1 failed | 17 passed, 7 disagreements (XAF, XCD, XCG, XDR, XOF, XPF, XSU).

Both numbers reproduce the review's exactly. The fix uses the host enumeration as the population, never the oracle — each code is put to the door and to the predicate and the two must answer alike — so a code the snapshot lacks is false on both sides, a code the host lacks is never asked, and no ICU build can redden a correct door or hide a divergent one. That is what round 1's objection was about, and it is preserved. Populations on this runtime: currency 162, time zone 418, alpha-2 249. Each pin guards against a vacuous population (length > 100), and M14/M15 are its live positive controls: the pin is only meaningful because it demonstrably fails.

Confirmed and unchanged from round 1: Object.keys(CURRENCY_FRACTION_DIGITS) is unreachable from this package — the snapshot is declared at currency-fraction-digits.ts:51, not re-exported from data/index.ts, absent from api-surface/, with no wildcard in spec's exports.


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 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0packageMentionDocs.

Which tree this was computed on

This run read content/docs from a54c5b90a71aae01ab996ed6bcf465fc238d78ca — the merge of head 6f2f77edf9be1837b69f7e5a87386115eb4a71f8 into base 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0, 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 a54c5b90a71aae01ab996ed6bcf465fc238d78ca && git checkout a54c5b90a71aae01ab996ed6bcf465fc238d78ca
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0 6f2f77edf9be1837b69f7e5a87386115eb4a71f8 && git checkout -B drift-repro 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0 && git merge --no-ff 6f2f77edf9be1837b69f7e5a87386115eb4a71f8

node scripts/docs-audit/affected-docs.mjs --json 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0

⚠️ 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 954cb0bbfa02a5f19b8d82f64179a5e9a211bbe0 → 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

1 participant