Skip to content

Commit 48a3549

Browse files
claude[bot]claude
andauthored
test(service-settings): scan every carrier extension in the shared-predicate ratchet (#18269)
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 test` — **33 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](https://claude.ai/code/session_01URLHobLUJB9K1ABV6ofdjj)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent e909148 commit 48a3549

1 file changed

Lines changed: 106 additions & 8 deletions

File tree

‎packages/services/service-settings/src/value-domains.shared-predicate.pin.test.ts‎

Lines changed: 106 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@
3232
* first. Both table SHAPES are detected — the space-separated string this file
3333
* replaced and the array literal a re-typing would more likely produce.
3434
*
35+
* "Non-test source" means every CARRIER the toolchain follows, not every `.ts`
36+
* file. That distinction is the file's third measured falsification and it is
37+
* argued at {@link runtimeSources}: a JSON array of the 249 codes read the
38+
* package green through all seven assertions below.
39+
*
3540
* ## What each check does and does not cover — measured, not claimed
3641
*
3742
* The absence checks are a source scan, so they see shapes. Two mutations were
@@ -48,6 +53,17 @@
4853
* else, so a table anywhere in the tree is inert while nothing here can name
4954
* it, and a relative specifier is how it would be named.
5055
*
56+
* ⚠️ That pin reads the DOOR, and only the door — a fact worth stating because
57+
* it reads like a package-wide guarantee and is not one. No caller-side
58+
* equivalent is possible in the same shape: `settings-service.ts` legitimately
59+
* imports dozens of modules, so there is no one-specifier whitelist to assert
60+
* against it. For a CALLER the shape scans are the instrument, which is why
61+
* their SCOPE is pinned instead — the last assertion in this file states that
62+
* no runtime source can relatively name a module the walk does not open, so
63+
* "scanned" and "reachable" cannot drift apart silently. A table reached by a
64+
* BARE package specifier stays outside both, for the door and the caller
65+
* alike; it is in the NOT-covered list below and stays there.
66+
*
5167
* A judge STANDING IN FRONT OF THE DOOR is closed by the package-wide density
5268
* scan, and that distinction is a measured falsification, not a design
5369
* flourish. An earlier draft ran the density scan on the door alone and
@@ -88,7 +104,14 @@
88104
* complement is infinite. The trap corpus seeds the plausible members of
89105
* that complement rather than pretending to close it.
90106
* - **A table reached through a BARE package specifier** rather than a
91-
* relative one would pass the import pin.
107+
* relative one would pass the import pin, and passes the scope pin too: both
108+
* read relative specifiers, which is how a module inside this repo is named.
109+
* - **A data carrier in a shape no scan knows**, `.json` included: the walk
110+
* now OPENS every carrier, so the array shape is caught wherever it lives,
111+
* but `{ "AD": 1, … }` in a JSON file is the same object-key map already
112+
* listed two bullets up — quotes and a colon push the separator past two
113+
* characters. Admitting the extension buys the ARRAY and SPACED shapes, not
114+
* a new shape class.
92115
* - Two further routes are closed by the toolchain rather than by a pin here,
93116
* and are recorded because a toolchain is not a guarantee: the suite's
94117
* module resolution, and the `tsup` es2020 target.
@@ -99,7 +122,7 @@
99122
import { describe, it, expect } from 'vitest';
100123
import { readFileSync, readdirSync } from 'node:fs';
101124
import { fileURLToPath } from 'node:url';
102-
import { join, relative } from 'node:path';
125+
import { dirname, extname, join, relative, resolve } from 'node:path';
103126
// The repo's ONE answer to "is this span a comment, or code?" — a private
104127
// stripper here would be the drift its header records (and
105128
// `check:comment-mask-adoption` refuses one). `stripComments` is the right
@@ -117,22 +140,61 @@ const SRC = fileURLToPath(new URL('.', import.meta.url));
117140
const DOOR = join(SRC, 'value-domains.ts');
118141

119142
/**
120-
* Every non-test `.ts` under this package's `src/`, RECURSIVELY, as
143+
* Every module extension a table can be CARRIED in — the walk's admission list.
144+
*
145+
* It read `.ts` alone until a JSON carrier was measured against it. The
146+
* mutation: the 249 codes as a JSON array in `src/`, imported by the door's
147+
* CALLER (`import ALPHA2 from './zz-alpha2.json' with { type: 'json' }` in
148+
* `settings-service.ts`) and judging `iso_3166_alpha2` itself at the
149+
* `validatePatch` refusal, falling through to the shared predicate for the
150+
* other two. Every instrument in this file was green — 7/7 — and so was the
151+
* package: 139/139 in `settings-service.test.ts`, `tsc --noEmit` exit 0
152+
* (`resolveJsonModule` is on at the root) and `tsup` exit 0. Deleting one code
153+
* from the JSON turned two behavioural cases red, so it was live code deciding
154+
* the `value_domain` FieldError, not decoration.
155+
*
156+
* ⚠️ It was ONE walk away from being seen the whole time: the shapes below
157+
* catch a JSON array of quoted codes on their existing spelling. Nothing was
158+
* missing but the file's extension.
159+
*
160+
* `.mts` and `.cts` were blind for exactly the same reason and had never been
161+
* separately measured, so they are admitted in the same edit rather than left
162+
* behind as the next carrier; `.js`, `.mjs` and `.cjs` follow because the
163+
* argument is about what the bundler opens, not about which language the table
164+
* is typed in. The cost is measured, not assumed: this package's `src/` is 64
165+
* files and 100% `.ts`, so the admission adds zero files HERE, and the
166+
* repo-wide census in the density check below is unchanged by it.
167+
*
168+
* ⛔ Not a deny-list of "everything that is not a test": a `.md` or a fixture
169+
* snapshot is prose, and a scan that reports on prose is how a ratchet earns
170+
* the reputation that gets it deleted.
171+
*/
172+
const CARRIER_EXTENSIONS = ['.ts', '.mts', '.cts', '.js', '.mjs', '.cjs', '.json'];
173+
174+
/**
175+
* Every non-test carrier under this package's `src/`, RECURSIVELY, as
121176
* `[path-relative-to-src, source]`.
122177
*
123178
* Recursive because `readdirSync` is not: `src/manifests/` and
124179
* `src/translations/` exist today, and a table placed in either passed an
125180
* earlier draft of this pin green.
181+
*
182+
* The test exclusion is spelled per-STEM rather than as `.test.ts`, so it
183+
* covers each admitted extension: `foo.test.mts` is evidence for the same
184+
* reason `foo.test.ts` is.
126185
*/
127186
function runtimeSources(dir: string = SRC): Array<[string, string]> {
128187
const out: Array<[string, string]> = [];
129188
for (const entry of readdirSync(dir, { withFileTypes: true })) {
130189
const full = join(dir, entry.name);
131190
if (entry.isDirectory()) {
132191
out.push(...runtimeSources(full));
133-
} else if (entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts')) {
134-
out.push([relative(SRC, full), readFileSync(full, 'utf8')]);
192+
continue;
135193
}
194+
const ext = extname(entry.name);
195+
if (!CARRIER_EXTENSIONS.includes(ext)) continue;
196+
if (/\.(test|spec)$/.test(entry.name.slice(0, -ext.length))) continue;
197+
out.push([relative(SRC, full), readFileSync(full, 'utf8')]);
136198
}
137199
return out;
138200
}
@@ -218,9 +280,15 @@ describe('no membership table anywhere in this package', () => {
218280
// FieldError on every alpha-2 save.
219281
//
220282
// The false-positive exposure is a census, not a hope: this pattern hits
221-
// 1 of 1,885 runtime `.ts` files repo-wide (49 `src` roots, comments
222-
// masked) — the shared module `value-domain.zod.ts` in the spec package,
223-
// i.e. the table itself, which is the one place the definition belongs.
283+
// exactly 1 runtime file repo-wide — the shared module
284+
// `value-domain.zod.ts` in the spec package, i.e. the table itself, which
285+
// is the one place the definition belongs. Re-measured when the walk
286+
// admitted the six carriers beside `.ts`, because a widened scan that
287+
// changes that number owes an explanation: over every tracked non-test
288+
// carrier under a `src` root the count is 2,503 files and the same 1 hit
289+
// (2,495 files at `.ts` alone), and over every tracked carrier in the
290+
// repository, `src` root or not, 3,583 files and still that 1 hit. SPACED
291+
// agrees with DENSE on that file; ARRAY hits nothing anywhere.
224292
// (Named without a repo-relative path on purpose: this test does not READ
225293
// that file, and `check:cross-package-test-inputs` reads a spelled path as
226294
// a declared input. Its scan is source text, comments included.) Quoted
@@ -248,4 +316,34 @@ describe('no membership table anywhere in this package', () => {
248316
const callers = runtimeSources().filter(([, src]) => stripComments(src).includes('isValueDomainMember('));
249317
expect(callers.map(([name]) => name)).toEqual(['value-domains.ts']);
250318
});
319+
320+
it('names no module the walk does not open — scanned and reachable cannot drift apart', () => {
321+
// The scope half of the shape scans, and the only caller-side answer to the
322+
// question the door's import pin answers for the door. Every check above is
323+
// "this text is not in the files I read"; none of them says anything about
324+
// a file NOT read. A carrier one directory above `src/` would be named
325+
// relatively and opened by the toolchain while this walk never sees it —
326+
// the same failure the JSON carrier had, moved from the extension to the
327+
// path.
328+
//
329+
// `rootDir: src` already makes this nearly true, and that is the reason to
330+
// pin it rather than to skip it: this file's own NOT-covered list ends by
331+
// recording two routes "closed by the toolchain rather than by a pin here,
332+
// and … because a toolchain is not a guarantee". A `tsconfig.json` edit is
333+
// not a reviewable event in a card about country codes.
334+
//
335+
// Relative specifiers only, in every shape that names one — static `from`,
336+
// `import(…)` and `require(…)` — with the same delimiter-agnosticism the
337+
// import pin above had to learn. A BARE specifier reaches outside by
338+
// design and is in the NOT-covered list, unchanged.
339+
const RELATIVE = /(?:\bfrom\s*|\b(?:import|require)\s*\(\s*)(['"])(\.[^'"]*)\1/g;
340+
const escaping: string[] = [];
341+
for (const [name, src] of runtimeSources()) {
342+
for (const [, , specifier] of stripComments(src).matchAll(RELATIVE)) {
343+
const landed = relative(SRC, resolve(dirname(join(SRC, name)), specifier));
344+
if (landed.startsWith('..')) escaping.push(`${name} -> ${specifier}`);
345+
}
346+
}
347+
expect(escaping).toEqual([]);
348+
});
251349
});

0 commit comments

Comments
 (0)