Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions packages/spec/scripts/dropped-refinements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
*/
import { describe, expect, it } from 'vitest';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { z } from 'zod';
import {
Expand Down Expand Up @@ -292,6 +293,60 @@ describe('the ratchet adjudicates against the ledger', () => {
});
});

describe("the reader's refusal names the shape the reader ACCEPTS", () => {
// The trap (#18747): the shape diagnostic used to say the entries are
// `key -> { count, reason }` while the very next check in the same function
// requires `sites: string[]` and the shipped `DroppedRefinementsEntry` has no
// `count` and no `reason` at all. An author — or an AI — repairing a broken
// ledger by following that sentence writes a ledger the SAME function refuses
// again. So the pin is a closed loop, not a wording match: whatever the
// refusal names has to be what the reader then takes.
const withLedger = <T,>(json: string, fn: (pkgDir: string) => T): T => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'os-dropped-refinements-'));
try {
fs.writeFileSync(path.join(dir, DROPPED_REFINEMENTS_BASELINE_FILE), json, 'utf8');
return fn(dir);
} finally {
fs.rmSync(dir, { recursive: true, force: true });
}
};

const refusalFor = (json: string): string =>
withLedger(json, (dir) => {
try {
readDroppedRefinementsBaseline(dir);
} catch (error) {
return (error as Error).message;
}
throw new Error('the reader accepted a ledger it should have refused');
});

it('the shape diagnostic names `sites`', () => {
// `entries` as an array is the branch that prints the shape.
expect(refusalFor('{ "entries": [] }')).toContain('sites');
});

it('a ledger written to that shape is then ACCEPTED — the loop closes', () => {
const accepted = withLedger('{ "entries": { "a/One": { "sites": ["x"] } } }', (dir) =>
readDroppedRefinementsBaseline(dir),
);
expect(accepted?.entries['a/One'].sites).toEqual(['x']);
});

it('LIT CONTROL — the shape the OLD diagnostic named is refused, and the refusal still says `sites`', () => {
// Without this leg the two assertions above pass on a reader that accepts
// anything: this is the ledger an author following the old sentence wrote.
const message = refusalFor('{ "entries": { "a/One": { "count": 1, "reason": "zod drops custom checks" } } }');
expect(message).toContain('sites');
});

it('the shape diagnostic names no key the entry shape does not have', () => {
const message = refusalFor('{ "entries": [] }');
expect(message).not.toContain('count');
expect(message).not.toContain('reason');
});
});

describe('the committed ledger', () => {
it('names at least one site per entry, and its header totals match its body', () => {
const baseline = readDroppedRefinementsBaseline(PKG_DIR);
Expand Down
23 changes: 14 additions & 9 deletions packages/spec/scripts/lib/dropped-refinements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,18 +56,23 @@
*
* - a published schema with dropped refinements that is NOT recorded fails
* the build — a new gap has to be a reviewed line in a diff;
* - a recorded `count` the build does not observe ALSO fails, in either
* direction. A ledger that keeps saying 3 while the tree grew to 4 has
* stopped describing the tree, and the 4th arrives inside a number nobody
* re-read.
* - a recorded `sites` list the build does not observe ALSO fails, in either
* direction and path by path. A ledger that keeps naming a site the tree no
* longer has stopped describing the tree, and the next gap arrives inside a
* list nobody re-read.
*
* ## Why the ledger is HAND-EDITED and has no `gen:` script
*
* Identical to `unemitted-schemas.baseline.json`: a generator would let a new
* gap be admitted by running a command instead of by a decision. Every entry
* carries a `reason` in prose and the gate requires it to be non-empty, because
* a baseline that records only a COUNT lets the next gap slip in behind a
* repaired one with nobody able to see which was replaced.
* gap be admitted by running a command instead of by a decision. Where THAT
* ledger requires a non-empty per-entry `reason`, this one requires a non-empty
* `sites` list and has no `reason` field at all — the reason is the same for
* every site here and is written once, above and in the ledger's own
* `description`, so a per-entry copy would be exactly the prose a required
* `reason` exists to prevent (`DroppedRefinementsEntry` below argues that in
* full). Both refuse the same thing: an entry recording only MEMBERSHIP, which
* lets the next gap slip in behind a repaired one with nobody able to see which
* was replaced.
*/
import fs from 'fs';
import path from 'path';
Expand Down Expand Up @@ -493,7 +498,7 @@ export function readDroppedRefinementsBaseline(pkgDir: string): DroppedRefinemen
const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as { entries?: unknown };
const entries = parsed.entries;
if (typeof entries !== 'object' || entries === null || Array.isArray(entries)) {
throw new Error(`${DROPPED_REFINEMENTS_BASELINE_FILE}: "entries" must be an object of key -> { count, reason }`);
throw new Error(`${DROPPED_REFINEMENTS_BASELINE_FILE}: "entries" must be an object of key -> { sites: string[] }`);
}
for (const [key, value] of Object.entries(entries as Record<string, unknown>)) {
const entry = value as Partial<DroppedRefinementsEntry>;
Expand Down
Loading