Skip to content

test(service-settings): scan every carrier extension in the shared-predicate ratchet - #18269

Merged
claude[bot] merged 2 commits into
mainfrom
claude/issue-15610-shared-predicate-ratchet-json-carrier
Sep 15, 2026
Merged

claude[bot] merged 2 commits into
mainfrom
claude/issue-15610-shared-predicate-ratchet-json-carrier

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #15610

Clause-②: no

The shared-predicate ratchet walked .ts files only, so a membership table carried in a
.json file was never opened. This admits every carrier extension the toolchain follows
and adds the scope half the shape scans never had.

The green-while-broken state, reproduced first

The card's specimen, rebuilt and measured on this branch's base before anything was
changed: 249 ISO 3166-1 alpha-2 codes as a JSON array at
packages/services/service-settings/src/zz-alpha2.json, imported by the door's CALLER
(import ALPHA2 from './zz-alpha2.json' with { type: 'json' } in settings-service.ts)
and judging iso_3166_alpha2 itself at the validatePatch refusal, falling through to
firstRejectedDomainMember for the other two domains.

instrument reading with the specimen in the tree
value-domains.shared-predicate.pin.test.ts 7 passed (7)
settings-service.test.ts 139 passed (139)
tsc --noEmit exit 0
tsup build exit 0

It was live code, not decoration: deleting CH from the JSON turned exactly 2 of 139
behavioural cases red — the two that write an accepted country through the save path — so
the second definition really was deciding the value_domain FieldError.

What the strengthened ratchet does to it

Same specimen, same command, on this PR's commit:

AssertionError: zz-alpha2.json carries an array-literal alpha-2 code list: expected true to be false
 Tests  1 failed | 7 passed (8)

Nothing but the file's extension was missing — the existing ARRAY shape catches a JSON
array of quoted codes on its current spelling once the walk opens the file.

Each admitted carrier measured separately

One carrier on disk at a time, because a single run with several aborts at the first
assertion and names only one file:

carrier placed in src/ ratchet file named in the failure
zz-alpha2.mts exit 1 — 1 failed / 7 passed yes
zz-alpha2.cts exit 1 — 1 failed / 7 passed yes
zz-alpha2.js exit 1 — 1 failed / 7 passed yes
zz-alpha2.mjs exit 1 — 1 failed / 7 passed yes
zz-alpha2.cjs exit 1 — 1 failed / 7 passed yes
zz-alpha2.json exit 1 — 1 failed / 7 passed yes
zz-alpha2.md (control — not a carrier) exit 0 — 8 passed n/a
zz-alpha2.test.ts (control — evidence, not enforcement) exit 0 — 8 passed n/a

.mts and .cts are the two the card records as never separately measured. They are
measured here, and so are the three JS spellings: the argument is about what the bundler
opens, not about which language the table is typed in. The two controls are what keep the
admission list a LIST — a walk that reported on prose or on test evidence would be a
different and worse instrument.

Which of the three blind spots this closes, and which it does not

  1. runtimeSources() reading *.ts only — CLOSED. The walk admits .ts .mts .cts .js .mjs .cjs .json, and the test exclusion is spelled per stem so it covers each. Six
    separate red measurements above.
  2. The import-surface pin reading the door only — PARTIALLY CLOSED, and deliberately
    not in the door's shape.
    No caller-side whitelist is possible: settings-service.ts
    legitimately imports dozens of modules, so there is no one-specifier assertion to make
    against it. What is closable is the SCOPE of the scans that do cover callers, and that
    is the eighth assertion this PR adds — no runtime source may relatively name a module
    the walk does not open, so a carrier one directory above src/ cannot hide the way the
    .json carrier hid behind an extension. A table reached by a BARE package specifier
    stays outside both pins, for the door and the caller alike; it was already in the
    NOT-covered list and stays there.
  3. The caller carrying no dense two-letter run because its table lives in the .json
    CLOSED for a carrier inside this package, which is the measured specimen; NOT a general
    close.
    The density scan still sees only what the walk opens, so the table is now found
    where it LIVES rather than where it is consulted. A caller consulting a table that lives
    outside this package remains uncovered, and a table in a shape no scan knows — the
    object-key map, now including its JSON spelling { "AD": 1, … } — is still the
    documented three-character-separator class. The header says both.

⚠️ So: two closed, one closed in shape and not in general. Reported that way rather than as
"the finding is closed".

Census re-run

The header carried a false-positive census, and a widened scan that moves that number owes
an explanation. It does not move. Measured on this branch at de98ec29a, comments masked
with the repo's shared mask, over git-tracked non-test files:

scope files DENSE SPACED ARRAY
.ts only, under a src root 2,495 1 1 0
all seven carriers, under a src root 2,503 1 1 0
all seven carriers, every tracked file 3,583 1 1 0

The one hit is packages/spec/src/shared/value-domain.zod.ts in all three scopes — the
shared table itself, which is the one place the definition belongs. Widening the extension
list admitted 8 more files under src roots and 1,080 more across the repository and hit
none of them.

Verification

Run on de98ec29a, after merging origin/main:

  • pnpm --filter @objectstack/service-settings test33 files, 579 tests, all passed
  • pnpm --filter @objectstack/service-settings exec tsc --noEmit — exit 0
  • pnpm --filter @objectstack/service-settings build (tsup + check-dts-emitted) — exit 0
  • pnpm --filter '@objectstack/service-settings^...' build (dependency closure) — exit 0
  • pnpm lint (eslint . --no-inline-config, the repo-wide style authority) — exit 0
  • Gates, derived with scripts/pm/dispatch-gates.mjs --commands and reconciled with
    --ran: 55 derived · 53 run and green · 2 NOT MEASURED · 0 UNRUN. The two are
    check:dual-build-cjs-loads and check:type-check-debt, both exit 3 = PREREQUISITE
    NOT MET
    — each reads built output for 50+ packages and refuses to measure without a
    full pnpm build, which CI performs. Exit 3 is recorded as not-measured, never as a pass.
  • Every mutation ran under a trap and restored against HEAD by blob hash, with the
    working tree proven clean afterwards; no probe file is committed.

Release surface

No changeset: nothing this package publishes moves. files is ["dist", "README.md", "CHANGELOG.md"]; the diff is one *.test.ts file, which tsup does not emit. Measured
after a build — the symbols this PR adds return zero hits under dist/, while the positive
control (firstRejectedDomainMember, knownValueDomain) is present in dist/index.js and
dist/index.cjs, so the grep discriminates. skip-changeset applied.

Contract-surface reachability (the ② derivation)

Derived from the DELIVERED diff by reachability from the published entry, not from the word
export and not from a grep of dist/index.js. The published entry is
exports["."] -> ./dist/index.{js,d.ts}, built from the src/index.ts barrel. The static
import closure of that barrel is 29 files. The delivered file is not one of them; the
positive control src/value-domains.ts is in the closure, and a second test file
(src/value-domains.test.ts) is outside it exactly as the delivered one is. The diff adds
no schema key, no closed-set member, no published export and no registry entry. Declared
no above.

Acceptance notes

  • Noted, not filed: the pin header's previous census sentence read "1 of 1,885 runtime
    .ts files repo-wide (49 src roots)". That count does not reproduce on this tree by
    the method stated here (2,495 non-test .ts files under 78 src roots, same single
    hit). The difference is one of scope definition and of the tree each was taken against,
    not a disagreement about the finding — the header now carries numbers a reader can
    re-derive with the mask this repository ships.
  • Noted, not filed: the ARRAY/SPACED/DENSE assertions sit inside a for loop, so a
    tree with several offending carriers reddens naming only the first. That is ordinary
    vitest shape, not a defect, and it is why the per-carrier table above was measured one
    file at a time. Whoever next edits this pin is the reader who needs to know.

Generated by Claude Code

…edicate ratchet

The walk read `.ts` alone, so a code table in a `.json` file was never opened.
Measured on a specimen the card describes: 249 ISO 3166-1 alpha-2 codes as a
JSON array in `src/`, imported by the door's CALLER with
`with { type: 'json' }` and judging `iso_3166_alpha2` at the `validatePatch`
refusal. It shipped entirely green — ratchet 7/7, `settings-service.test.ts`
139/139, `tsc --noEmit` exit 0, `tsup` exit 0 — while a second membership
definition decided the `value_domain` FieldError on every alpha-2 save
(dropping one code from the JSON turned two behavioural cases red, so it was
live code).

Admit `.mts`, `.cts`, `.js`, `.mjs`, `.cjs` and `.json` beside `.ts`, and spell
the test exclusion per stem so it covers each. The existing ARRAY shape catches
a JSON array of quoted codes once the file is opened; nothing else had to move.

Add the scope half the shape scans never had: no runtime source may relatively
name a module the walk does not open, so "scanned" and "reachable" cannot drift
apart the way the extension list and the toolchain just did.

Census re-run over the widened set, comments masked: 1 hit over 2,503 tracked
non-test carriers under a `src` root (2,495 at `.ts` alone) and 1 over all
3,583 tracked carriers — `packages/spec/src/shared/value-domain.zod.ts`, the
shared table itself, unchanged from the number the header carried.

Claude-Session: https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj
Co-authored-by: Claude <noreply@anthropic.com>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 15, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see
  • 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 — 0 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 e592b4b6b7fe62e465a3eead0c6f9a8e94838585packageMentionDocs.

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

1 participant