Skip to content

Commit 1da1f32

Browse files
os-helpclaude
andauthored
test(spec): give the strictness ledger's older-spelling case an owned fixture (#6940) (#7238)
The case "reads the OLDER z.object(....strict() spelling as strict too" borrowed a live site to carry its assertion, and the #4001 campaign keeps converting whatever site it lands on: three evictions in three PRs (#5593, #6619/PR #6804, #6805/PR #6935). The reading under test belongs to the reader in scripts/lib/strictness-ledger.ts, not to any file in this tree, so the material now comes from a synthetic fixture the test writes itself. The live tree is still read, as a control - but as a POPULATION reading with no site named, so no conversion wave can evict it and the campaign's terminal state (zero carriers) stays green. No gate changes: the reader and every gate verdict are untouched, and the generated counts artifact does not move. Claude-Session: https://claude.ai/code/session_01KJATVrh6V2ysutYUJigh3B Co-authored-by: Claude <noreply@anthropic.com>
1 parent d127ff0 commit 1da1f32

1 file changed

Lines changed: 127 additions & 62 deletions

File tree

packages/spec/scripts/strictness-ledger.test.ts

Lines changed: 127 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@ import path from 'node:path';
2828
import url from 'node:url';
2929
import { describe, expect, it } from 'vitest';
3030

31-
import { analyzeSites, countSites, countStripSites, listSchemaFiles } from './lib/strictness-ledger';
31+
import {
32+
analyzeSites,
33+
analyzeTree,
34+
countSites,
35+
countStripSites,
36+
listSchemaFiles,
37+
} from './lib/strictness-ledger';
3238

3339
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
3440
const SPEC = path.resolve(HERE, '..');
@@ -136,20 +142,37 @@ describe('site counting reads the AST, not the source text', () => {
136142
* is the exact instrument this campaign keeps getting burned by.
137143
*/
138144
describe('posture reading, with a red control for each', () => {
139-
/** Analyze a mutated copy of a real source file. */
140-
const mutate = (rel: string, from: string, to: string) => {
141-
const src = fs.readFileSync(path.join(SRC, rel), 'utf-8');
142-
expect(src, `mutation anchor missing in ${rel}`).toContain(from);
145+
/**
146+
* Analyze source the tree does not have to contain, from a temp file the test
147+
* owns for the duration of the case.
148+
*
149+
* `rel` is what the reading reports as the site's `file`; the temp file itself
150+
* is written flat under one mkdtemp dir, so a nested `rel` needs no mkdir.
151+
*/
152+
const analyzeSource = (source: string, rel: string) => {
143153
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'strictness-'));
144154
const tmp = path.join(dir, path.basename(rel));
145-
fs.writeFileSync(tmp, src.replace(from, to));
155+
fs.writeFileSync(tmp, source);
146156
try {
147157
return analyzeSites(tmp, rel);
148158
} finally {
149159
fs.rmSync(dir, { recursive: true, force: true });
150160
}
151161
};
152162

163+
/** Analyze a mutated copy of a real source file. */
164+
const mutate = (rel: string, from: string, to: string) => {
165+
const src = fs.readFileSync(path.join(SRC, rel), 'utf-8');
166+
expect(src, `mutation anchor missing in ${rel}`).toContain(from);
167+
return analyzeSource(src.replace(from, to), rel);
168+
};
169+
170+
/** Every site under the triaged directories, `file` relative to `src/`. */
171+
const triagedSites = () =>
172+
TRIAGED.flatMap((dir) =>
173+
analyzeTree(path.join(SRC, dir)).map((s) => ({ ...s, file: `${dir}/${s.file}` })),
174+
);
175+
153176
it('reads the strictObject( helper as strict — and as strip once un-converted', () => {
154177
const rel = 'ui/action.zod.ts';
155178
const live = analyzeSites(at(rel)).find((s) => s.name === 'ActionAiSchema');
@@ -162,49 +185,98 @@ describe('posture reading, with a red control for each', () => {
162185
});
163186

164187
it('reads the OLDER z.object(…).strict() spelling as strict too', () => {
165-
// The reading the `strictObject(`-only count could not make.
188+
// The reading the `strictObject(`-only count could not make, carried by an
189+
// OWNED fixture (#6940).
190+
//
191+
// ## Why this is synthetic and not a real site
166192
//
167-
// The fixture used to be `security/permission.zod.ts`, whose four sites
168-
// were the campaign's canonical `z.object(shape, { error }).strict()`
169-
// wiring; #5593 migrated all four to `strictObject`, so the file no longer
170-
// exercised the branch under test, and the fixture moved to
171-
// `TenancyConfigSchema` — until #6619 folded ITS hand-written map into the
172-
// shared template. It then moved to `ObjectCapabilities`, same file, on the
173-
// reading that its map (`strictCapabilitiesError`) emitted NO trailing
174-
// history sentence and so could not fold. **#6805 disproved that reading**:
175-
// the missing sentence was a gap in the TEXT, not a limit of the template
176-
// (`history` encodes position, and `enable` had a real history nobody had
177-
// written down), so that site is `strictObject` too now.
193+
// It used to be a real one, and it moved three times in three PRs:
194+
// `security/permission.zod.ts`'s four sites (#5593 migrated all four), then
195+
// `TenancyConfigSchema` (#6619 folded its hand-written map into the shared
196+
// template), then `ObjectCapabilities` (#6805 folded that one too), then
197+
// `PerOperationRequiredPermissionsSchema`. Every author obeyed the "move
198+
// this fixture rather than deleting the assertion" instruction — the
199+
// instruction was the problem, not its readers. The #4001 campaign's whole
200+
// direction is to convert every remaining `z.object(shape, { error }).strict()`
201+
// carrier to `strictObject`, so each wave evicts whatever site is standing
202+
// here, and the TERMINAL state — zero carriers in the tree — leaves that
203+
// instruction with nowhere to point.
178204
//
179-
// `PerOperationRequiredPermissionsSchema`, still the same file, is the
180-
// spelling's carrier today — and a more durable one, because it carries no
181-
// guidance table at all and therefore nothing pulls it toward the helper.
182-
// If it is ever converted, move this fixture AGAIN rather than deleting the
183-
// assertion: the AST reader still has to make the reading, and
184-
// `packages/spec` is not the only tree it reads.
185-
const objectSites = analyzeSites(at('data/object.zod.ts'));
186-
const perOperation = objectSites.find((s) => s.name === 'PerOperationRequiredPermissionsSchema');
187-
expect(perOperation?.posture, 'a plain `.strict()` chain is still strict').toBe('strict');
188-
expect(perOperation?.idiom).toBe('z.object');
189-
190-
// …and the two sites the fixture vacated read as the helper now, which is
191-
// the control that keeps the line above a statement about the READER
192-
// rather than about one lucky survivor. Without it, a reader that simply
193-
// stopped distinguishing idioms would still satisfy the assertion.
194-
for (const name of ['ObjectCapabilities', 'TenancyConfigSchema']) {
195-
const folded = objectSites.find((s) => s.name === name);
196-
expect(folded, `${name} is not a site any more — re-point this test, do not delete it`).toBeDefined();
197-
expect(folded?.idiom, `${name} folded into the helper at #6619/#6805`).toBe('strictObject');
198-
expect(folded?.posture).toBe('strict');
205+
// The reading under test is a property of the READER
206+
// (`lib/strictness-ledger.ts`), not of any file in this tree, and
207+
// `packages/spec` is not the only tree that reader reads. So the load-bearing
208+
// material is written by the test, is outside the campaign's conversion
209+
// surface, and survives the terminal state unchanged. The live tree is still
210+
// read below — as a control, not as the carrier.
211+
const owned = analyzeSource(
212+
[
213+
"import { z } from 'zod';",
214+
'',
215+
'/** The campaign-era spelling: shape, error map, then the closing call. */',
216+
'export const OwnedClosedSchema = z.object(',
217+
' { a: z.string() },',
218+
" { error: () => 'unknown key' },",
219+
').strict();',
220+
'',
221+
'/** The same spelling as prettier wraps it once the chain gets long. */',
222+
'export const OwnedWrappedClosedSchema = z',
223+
' .object({ b: z.string() })',
224+
' .strict();',
225+
'',
226+
'/** Control: same idiom, left open — a reader that calls every site strict fails here. */',
227+
'export const OwnedOpenSchema = z.object({ c: z.string() });',
228+
'',
229+
'/** Control: the helper the campaign converts TO — the idioms must stay distinct. */',
230+
'export const OwnedHelperSchema = strictObject({ d: z.string() });',
231+
].join('\n'),
232+
'owned-older-spelling.zod.ts',
233+
);
234+
235+
// Pinned as whole maps rather than per-site lookups, so an extra or missing
236+
// site is a failure too — the counting defects in this campaign have all
237+
// been omissions that a `find()` would have walked past.
238+
expect(Object.fromEntries(owned.map((s) => [s.name, s.posture]))).toEqual({
239+
OwnedClosedSchema: 'strict',
240+
OwnedWrappedClosedSchema: 'strict',
241+
OwnedOpenSchema: 'strip',
242+
OwnedHelperSchema: 'strict',
243+
});
244+
expect(Object.fromEntries(owned.map((s) => [s.name, s.idiom]))).toEqual({
245+
OwnedClosedSchema: 'z.object',
246+
OwnedWrappedClosedSchema: 'z.object',
247+
OwnedOpenSchema: 'z.object',
248+
OwnedHelperSchema: 'strictObject',
249+
});
250+
251+
// ## The live control, stated so the campaign cannot evict it
252+
//
253+
// What the borrowed site really bought was evidence that this spelling is
254+
// not a museum piece the reader keeps supporting for nobody. That is a
255+
// statement about the POPULATION, so it is read as one — no site is named,
256+
// and an empty population is a legitimate reading (it is the campaign's
257+
// declared goal), which is why there is no `toBeGreaterThan(0)` here.
258+
const live = triagedSites();
259+
const carriers = live.filter((s) => s.idiom === 'z.object' && s.posture === 'strict');
260+
261+
// Corroboration that does NOT share the AST reader — the same tactic as the
262+
// coverage-walk case above, for the same reason: a bug in the reader must
263+
// not be able to confirm itself. Coarse by construction (file-level text,
264+
// not the site's own chain), so it cannot prove the reading is right; it can
265+
// only catch the reader claiming a file closes keys that never says so.
266+
for (const site of carriers) {
267+
const text = fs.readFileSync(path.join(SRC, site.file), 'utf-8');
268+
expect(text, `${site.file} is read as closing ${site.name}, but never spells it`).toContain(
269+
'.strict()',
270+
);
199271
}
200272

201-
// The permission file's four are now the helper, and still strict — the
202-
// control that keeps this test a statement about the READER rather than
203-
// about one file.
204-
const perm = analyzeSites(at('security/permission.zod.ts'));
205-
expect(perm.filter((s) => s.posture === 'strict')).toHaveLength(4);
206-
expect(perm.find((s) => s.name === 'PermissionSetSchema')?.idiom).toBe('strictObject');
207-
expect(countStripSites(at('security/permission.zod.ts'))).toBe(0);
273+
// And the anti-collapse control, on real material and with nothing named: a
274+
// reader that stopped distinguishing idioms — or postures — would satisfy
275+
// every assertion above about the owned fixture only by accident, and fails
276+
// here outright. True in the terminal state too, where the surviving idioms
277+
// are `strictObject` and friends rather than `z.object`.
278+
expect(new Set(live.map((s) => s.idiom)).size).toBeGreaterThan(1);
279+
expect(new Set(live.map((s) => s.posture)).size).toBeGreaterThan(1);
208280
});
209281

210282
it('reads a default site as strip — and as strict once closed', () => {
@@ -289,10 +361,7 @@ describe('posture reading, with a red control for each', () => {
289361
// `z.looseObject(` under a triaged directory — the early return for
290362
// `z.looseObject` was the same defect waiting for its first instance, so it
291363
// is covered before that instance exists rather than after.
292-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'strictness-idiom-'));
293-
const file = path.join(dir, 'synthetic.zod.ts');
294-
fs.writeFileSync(
295-
file,
364+
const sites = analyzeSource(
296365
[
297366
'export const A = strictObject({ a: 1 }).passthrough();',
298367
'export const B = strictObject({ b: 1 });',
@@ -302,19 +371,15 @@ describe('posture reading, with a red control for each', () => {
302371
// Last explicit call wins, not the first.
303372
'export const F = strictObject({ f: 1 }).passthrough().strict();',
304373
].join('\n'),
374+
'synthetic.zod.ts',
305375
);
306-
try {
307-
const posture = Object.fromEntries(analyzeSites(file).map((s) => [s.name, s.posture]));
308-
expect(posture).toEqual({
309-
A: 'passthrough',
310-
B: 'strict',
311-
C: 'strict',
312-
D: 'passthrough',
313-
E: 'catchall',
314-
F: 'strict',
315-
});
316-
} finally {
317-
fs.rmSync(dir, { recursive: true, force: true });
318-
}
376+
expect(Object.fromEntries(sites.map((s) => [s.name, s.posture]))).toEqual({
377+
A: 'passthrough',
378+
B: 'strict',
379+
C: 'strict',
380+
D: 'passthrough',
381+
E: 'catchall',
382+
F: 'strict',
383+
});
319384
});
320385
});

0 commit comments

Comments
 (0)