Skip to content

Commit ee2cb6b

Browse files
claude[bot]claudezhuangjianguo
authored
test(spec): pin the blueprint pointer row to its subject (#16994)
The published skill-index row for `ai/solution-blueprint.zod.ts` is generated from the module's own doc block, with an export list as the fallback when no block qualifies. Neither generator can see the difference: `check:skill-refs` and `check:docs` each compare an artifact against the generator that wrote it, and the generator reproduces the header selector faithfully — so a generator-only check passes on exactly the state where the row says nothing about the file's subject. This pins the fact the artifact must state instead. Two legs that fail differently. The SOURCE leg reds the moment the separator between the module header and `SNAKE_CASE` goes away — no regeneration needed. The CORPUS leg stays green through that, because it reads checked-in bytes, and reds once an index is regenerated from a file whose header no longer qualifies. Both directions measured by ablation. The pin reads `skills/*/references/_index.md`, outside its own package, so it is declared in `vitest.repo-tests.json` and runs under the `repo` project, whose hash moves with what it actually reads. Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Steedos <zhuangjianguo@steedos.com>
1 parent ab1c585 commit ee2cb6b

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Pin for WHAT the published pointer row for `ai/solution-blueprint.zod.ts`
5+
* names — plan-first authoring, not a list of the symbols it happens to export.
6+
*
7+
* `build-skill-references.ts` describes each source by the module's own doc
8+
* block (`lib/file-description.ts` selects it: top-level, in the header zone,
9+
* documenting no symbol), and falls through to `Exports: …` when no block
10+
* qualifies. This file HAD its header — ADR-0033 §4, the `apply_blueprint`
11+
* expansion — but a single blank line was all that separated it from
12+
* `const SNAKE_CASE`, so TSDoc's own attachment rule made it that regex
13+
* constant's documentation and the selector disqualified it. The published
14+
* index therefore stated a true fact ABOUT the file and nothing about its
15+
* SUBJECT, on the very row whose job is to route an agent to the source for
16+
* exact field shapes.
17+
*
18+
* Same root cause as #14441 pointing the other way: there the wrong block was
19+
* published, here the right one was suppressed and a machine-generated list
20+
* took its place. Both are the header-zone selector deciding against a header
21+
* a human wrote — and in this direction the selector was RIGHT under its own
22+
* rule, so the repair is in the source, not in the selector.
23+
*
24+
* No generator can see this class. `check:skill-refs` and `check:docs` compare
25+
* the artifact against the generator, and the generator reproduced the
26+
* selector faithfully — a generator-only check PASSES on the defect. So pin
27+
* the fact the artifact must state, not the pipeline that states it.
28+
*
29+
* Two legs that fail DIFFERENTLY, which is why both exist. The SOURCE leg reds
30+
* the moment the separator between the header and `SNAKE_CASE` is removed —
31+
* no regeneration needed. The CORPUS leg stays green through that (it reads
32+
* checked-in bytes, which only move when someone regenerates) and reds on the
33+
* state this card found: an index regenerated from a file whose header no
34+
* longer qualifies. MEASURED both ways in the fix's reverse verification.
35+
*/
36+
37+
import fs from 'fs';
38+
import path from 'path';
39+
import url from 'url';
40+
41+
import { describe, expect, it } from 'vitest';
42+
43+
import { findModuleDocBlock } from './lib/file-description';
44+
45+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
46+
const REPO_ROOT = path.resolve(HERE, '../../..');
47+
const SKILLS_DIR = path.resolve(REPO_ROOT, 'skills');
48+
const BLUEPRINT_SOURCE = path.resolve(HERE, '../src/ai/solution-blueprint.zod.ts');
49+
50+
/**
51+
* The module's own opening sentence. Spelled out rather than derived from the
52+
* source: deriving it would re-assert the generator's rule and say nothing
53+
* about WHICH subject the row names, which is the whole defect.
54+
*/
55+
const BLUEPRINT_SENTENCE = 'Solution Blueprint Schema (ADR-0033 §4 — plan-first authoring)';
56+
57+
/** The pointer path the generator writes for this source in every index. */
58+
const POINTER = 'node_modules/@objectstack/spec/src/ai/solution-blueprint.zod.ts';
59+
60+
/** First prose line of the block the generator would publish for a source. */
61+
const firstDescriptionLine = (source: string): string | null => {
62+
const block = findModuleDocBlock(source);
63+
if (block === null) return null;
64+
const lines = block
65+
.split('\n')
66+
.map((line) => line.replace(/^\s*\*\s?/, '').trim())
67+
.filter((line) => line && !line.startsWith('@') && !line.startsWith('```'));
68+
return lines[0] ?? null;
69+
};
70+
71+
describe('ai/solution-blueprint.zod.ts — the module block describes the module', () => {
72+
it('opens on the plan-first authoring sentence, not on `SNAKE_CASE`', () => {
73+
const source = fs.readFileSync(BLUEPRINT_SOURCE, 'utf-8');
74+
expect(firstDescriptionLine(source)).toBe(BLUEPRINT_SENTENCE);
75+
});
76+
77+
it('still documents `SNAKE_CASE` — the fix ADDS a symbol doc, it does not delete the header', () => {
78+
// The cheapest way to satisfy the leg above is to delete the separator's
79+
// reason for existing. `SNAKE_CASE` is what the header must not be glued
80+
// to, and what a reader of this file still needs explained.
81+
const source = fs.readFileSync(BLUEPRINT_SOURCE, 'utf-8');
82+
const documented = /\/\*\*[^\n]*\*\/\n(?:export )?const SNAKE_CASE\b/.test(source);
83+
expect(documented).toBe(true);
84+
});
85+
});
86+
87+
describe('published catalog — every pointer row for the blueprint names its subject', () => {
88+
/** Every checked-in skill-index row pointing at `ai/solution-blueprint.zod.ts`. */
89+
const publishedRows = (): { file: string; description: string }[] => {
90+
const rows: { file: string; description: string }[] = [];
91+
for (const skill of fs.readdirSync(SKILLS_DIR)) {
92+
const index = path.resolve(SKILLS_DIR, skill, 'references/_index.md');
93+
if (!fs.existsSync(index)) continue;
94+
for (const line of fs.readFileSync(index, 'utf-8').split('\n')) {
95+
const match = /^- `([^`]+)` (.+)$/.exec(line);
96+
if (match && match[1] === POINTER) {
97+
rows.push({ file: path.relative(REPO_ROOT, index), description: match[2].trim() });
98+
}
99+
}
100+
}
101+
return rows;
102+
};
103+
104+
it('finds the rows at all', () => {
105+
// Nothing parsed means nothing compared, and "no bad row" would read as
106+
// green — the failure mode this whole file exists to refuse.
107+
expect(publishedRows().length).toBeGreaterThan(0);
108+
});
109+
110+
it('reads the blueprint sentence on every one of them, never the `Exports:` fallback', () => {
111+
const offenders = publishedRows()
112+
.filter((row) => row.description !== BLUEPRINT_SENTENCE)
113+
.map((row) => `${row.file}: ${row.description}`);
114+
expect(offenders).toEqual([]);
115+
});
116+
});

packages/spec/vitest.repo-tests.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"scripts/references-banner.test.ts",
1818
"scripts/root-index.test.ts",
1919
"scripts/schema-tree-freshness.test.ts",
20+
"scripts/solution-blueprint-header-row.test.ts",
2021
"scripts/strictness-ledger-doc.test.ts",
2122
"scripts/strictness-ledger.test.ts",
2223
"src/api/error-catalog-docs.test.ts",

0 commit comments

Comments
 (0)