Skip to content

Commit df736ee

Browse files
committed
fix(gates): judge spec member citations in published prose, not only in comments (objectui#7995)
`check-spec-symbol-derivation` read only the comments of `packages/*/src`, so a false `@objectstack/spec` attribution written as prose in `content/docs/**` was judged by nothing. objectui#7537 is the receipt: PR #7510 corrected three JSDoc copies of one such claim and a fourth copy in `content/docs/fields/lookup.mdx` shipped untouched, because no gate reads it. Rule 4 at member granularity asks only whether a name the prose spells exists in the spec, which needs no declaration, no tie and no claim phrase. So its scan surface widens to `content/docs/**` and the rule itself does not move: the citation grammar is factored into one `memberCitationsIn`, and the TypeScript comment scanner was refactored ONTO it rather than having a second copy written beside it. A comment BLOCK is the scope on one side, a markdown PARAGRAPH on the other; everything that decides a verdict is shared. Census taken BEFORE enforcement, on 411a132 against `@objectstack/spec@17.3.0`: 184 documentation pages, 32 mentioning the spec, 183 raw `Symbol.member` citations, 5 naming a spec export with a knowable member set, 3 surviving proximity and same-sentence, 3 resolving, 0 dangling. Zero corrections, zero waivers, no ledger. Skipping fenced code moves the raw count 183 -> 128 and leaves the judged population identical, so there is no fence rule. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr
1 parent 411a132 commit df736ee

2 files changed

Lines changed: 333 additions & 35 deletions

File tree

‎scripts/__tests__/check-spec-symbol-derivation.test.ts‎

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
scanFile,
1616
scanFileForClaims,
1717
scanFileForMemberCitations,
18+
scanProseForMemberCitations,
1819
} from '../check-spec-symbol-derivation.mjs';
1920

2021
/**
@@ -1286,3 +1287,144 @@ export type X = string;
12861287
).toEqual([]);
12871288
});
12881289
});
1290+
1291+
// ── Rule 4's second transport: published prose (objectui#7995) ───────────────
1292+
1293+
const proseCitations = (file: string) =>
1294+
scanProseForMemberCitations(file, MEMBERS_OF).map(
1295+
(c: { symbol: string; member: string; line: number }) => `${c.symbol}.${c.member}@${c.line}`
1296+
);
1297+
1298+
/**
1299+
* ⭐ The bite leg for the widening, and it is the objectui#7537 receipt rather
1300+
* than an invented shape.
1301+
*
1302+
* PR #7510 corrected three JSDoc claims that the installed `@objectstack/spec`
1303+
* declared a key it refused by name. A FOURTH copy of one of them sat in
1304+
* `content/docs/fields/lookup.mdx` in prose, survived that PR untouched, and
1305+
* shipped — corrected later only because a human read it. Against `SPEC_MEMBERS`
1306+
* above (faithful to the spec of that moment, where `SelectOptionSchema` spelled
1307+
* five keys and `description` was not among them) that page is what these
1308+
* fixtures reproduce.
1309+
*
1310+
* A widening asserted only green on today's tree proves nothing about the
1311+
* widening — the tree is green with the prose scan deleted. These are the
1312+
* fixtures that can tell the difference.
1313+
*/
1314+
describe('a false spec attribution written as PROSE is judged (objectui#7995)', () => {
1315+
const page = (source: string, name = 'lookup.mdx') =>
1316+
withFixture({ [name]: source }, (paths) => proseCitations(paths[name]));
1317+
1318+
it('flags the documentation copy that outlived the correction of its three JSDoc twins', () => {
1319+
expect(
1320+
page(`# Lookup Field
1321+
1322+
A static option may also carry a \`description\` — secondary text the picker's
1323+
typeahead searches alongside the label. It is declared by \`@objectstack/spec\`
1324+
as \`SelectOptionSchema.description\`, so authoring it on an option in an object
1325+
document is spec-compliant.
1326+
`)
1327+
).toEqual(['SelectOptionSchema.description@3']);
1328+
});
1329+
1330+
it('…and is GREEN on the repaired wording that names the symbol and the key without joining them', () => {
1331+
// The other half of the pair, and the escape hatch the rule leaves open: the
1332+
// page still mentions the spec and still names the key, it just stops
1333+
// claiming the spec DECLARES it. That is the distinction the rule draws, and
1334+
// it is the wording `content/docs/fields/lookup.mdx` carries today.
1335+
expect(
1336+
page(`# Lookup Field
1337+
1338+
Measured on the installed \`@objectstack/spec\`, \`SelectOptionSchema\` is strict
1339+
over exactly \`{label, value, color, default, visibleWhen}\` and refuses
1340+
\`description\` **by name** (\`unrecognized_keys\`).
1341+
`)
1342+
).toEqual([]);
1343+
});
1344+
1345+
it('a PARAGRAPH is the scope — a heading between the mention and the citation stops it', () => {
1346+
// The markdown analogue of the comment BLOCK, and load-bearing rather than
1347+
// tidy: a heading carries no sentence terminator, so the same-sentence test
1348+
// has nothing to stop it without blank lines as hard boundaries. Both
1349+
// fragments below would read as one sentence if the page were flattened.
1350+
expect(
1351+
page(`Authoring is governed by \`@objectstack/spec\`
1352+
1353+
## Read model
1354+
1355+
\`SelectOptionSchema.description\` is the widget-side key
1356+
`)
1357+
).toEqual([]);
1358+
});
1359+
1360+
it('reports the paragraph the citation sits in, not the top of the page', () => {
1361+
expect(
1362+
page(`# Title
1363+
1364+
Intro paragraph with no citations at all.
1365+
1366+
Per \`@objectstack/spec\`, \`FieldSchema.rows\` is authorable.
1367+
`)
1368+
).toEqual(['FieldSchema.rows@5']);
1369+
});
1370+
1371+
it('fenced code is scanned like the rest of the page', () => {
1372+
// The measured decision, pinned: skipping fences moved the raw count and
1373+
// left the judged population identical, so there is no fence rule. This is
1374+
// what "no fence rule" means in behaviour.
1375+
expect(
1376+
page(`Example:
1377+
1378+
\`\`\`ts
1379+
// Per \`@objectstack/spec\`, \`DashboardSchema.title\` is the header text.
1380+
const t = dashboard.title;
1381+
\`\`\`
1382+
`)
1383+
).toEqual(['DashboardSchema.title@3']);
1384+
});
1385+
1386+
it('a page that never mentions the spec is out of jurisdiction', () => {
1387+
expect(page(`Our own \`DashboardSchema.title\`, nothing to do with the protocol.\n`)).toEqual([]);
1388+
});
1389+
1390+
it('every precision rule inherited from the comment transport still applies', () => {
1391+
// Not a re-test of the rules — a proof that prose gets the SAME ones, which
1392+
// is the whole reason the grammar was factored out instead of copied.
1393+
expect(page(`Per \`@objectstack/spec\`, \`ui/DashboardSchema.json\` spells it.\n`)).toEqual([]);
1394+
expect(page(`Per \`@objectstack/spec\`, \`FieldSchema.safeParse\` rejects it.\n`)).toEqual([]);
1395+
expect(page(`Per \`@objectstack/spec\`, \`ChartAxis.shape\` is authorable.\n`)).toEqual([]);
1396+
expect(page(`Per \`@objectstack/spec\`, \`HttpRequestSchema.body\` carries it.\n`)).toEqual([]);
1397+
expect(page(`Renamed off \`@objectstack/spec\`. \`DashboardSchema.title\` is ours.\n`)).toEqual([]);
1398+
});
1399+
});
1400+
1401+
describe('one grammar, two transports (objectui#7995)', () => {
1402+
/**
1403+
* The regression this pins is the one a second copy of the rule would create:
1404+
* the two surfaces answering differently about the same sentence. Both sides
1405+
* call `memberCitationsIn`; if either grows its own copy, one of these two
1406+
* expectations moves and the other does not.
1407+
*/
1408+
const sentence = 'Aligns `@objectstack/spec` `SelectOptionSchema.description`.';
1409+
1410+
it('the same passage is judged identically as a comment and as prose', () => {
1411+
const asComment = withFixture({ 'probe.ts': `\n/** ${sentence} */\nexport type X = string;\n` }, (paths) =>
1412+
citations(paths['probe.ts'])
1413+
);
1414+
const asProse = withFixture({ 'page.mdx': `${sentence}\n` }, (paths) => proseCitations(paths['page.mdx']));
1415+
1416+
expect(asComment).toEqual(['SelectOptionSchema.description@2']);
1417+
expect(asProse).toEqual(['SelectOptionSchema.description@1']);
1418+
expect(asComment.map((c) => c.split('@')[0])).toEqual(asProse.map((c) => c.split('@')[0]));
1419+
});
1420+
1421+
it('…and identically when the citation is correct', () => {
1422+
const ok = 'Aligns `@objectstack/spec` `DashboardSchema.label`.';
1423+
const asComment = withFixture({ 'probe.ts': `\n/** ${ok} */\nexport type X = string;\n` }, (paths) =>
1424+
citations(paths['probe.ts'])
1425+
);
1426+
const asProse = withFixture({ 'page.mdx': `${ok}\n` }, (paths) => proseCitations(paths['page.mdx']));
1427+
expect(asComment).toEqual([]);
1428+
expect(asProse).toEqual([]);
1429+
});
1430+
});

0 commit comments

Comments
 (0)