Skip to content

Commit 2b11b75

Browse files
committed
test(spec): pin the blueprint pointer row to its subject, and regenerate
The published row and the reference page both regain the file's own sentence: - `.../ai/solution-blueprint.zod.ts` — Solution Blueprint Schema (ADR-0033 §4 — plan-first authoring) Neither generator can see this class on its own — `check:skill-refs` and `check:docs` compare the artifact against the generator, and the generator reproduces the selector faithfully, so a generator-only check passes on the defect. The pin asserts the fact the artifact must state instead. Two legs that fail differently: the SOURCE leg reds the moment the separator between the header and `SNAKE_CASE` goes away; the CORPUS leg stays green through that and reds once an index is regenerated from a file whose header no longer qualifies. Both directions measured. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016N6xmWt5hYm94ffVEwGH8x
1 parent 155a8ad commit 2b11b75

3 files changed

Lines changed: 131 additions & 1 deletion

File tree

content/docs/references/ai/solution-blueprint.mdx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@ description: Solution Blueprint protocol schemas
55

66
{/* ⚠️ AUTO-GENERATED — DO NOT EDIT. Run build-docs.ts to regenerate. Hand-written docs live in the module folders under content/docs/. */}
77

8+
Solution Blueprint Schema (ADR-0033 §4 — plan-first authoring)
9+
10+
The structured-output target an AI agent emits for a *high-level* goal
11+
("build me a project-management system") instead of transcribing a field
12+
list. It is a **simplified proposal shape** — deliberately lighter than the
13+
full `ObjectSchema` / `ViewSchema` / `DashboardSchema`.
14+
The `apply_blueprint` tool expands each entry into a proper metadata body
15+
and stages it as a draft (so the per-type Zod schema still validates the
16+
real artifact at write time).
17+
18+
The blueprint is **never persisted on its own**: the agent presents it for
19+
conversational confirmation/edit (cheap), and only on human approval does it
20+
batch-draft. This is the safety valve for low-specificity input.
21+
822
<Callout type="info">
923
**Source:** `packages/spec/src/ai/solution-blueprint.zod.ts`
1024
</Callout>
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+
});

skills/objectstack-ai/references/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ from `node_modules` — there is no local copy in the skill bundle.
1313
- `node_modules/@objectstack/spec/src/ai/knowledge-source.zod.ts` — Knowledge Source — declarative metadata describing what to index and
1414
- `node_modules/@objectstack/spec/src/ai/model-registry.zod.ts` — AI Model Registry Protocol
1515
- `node_modules/@objectstack/spec/src/ai/skill.zod.ts` — Skill Trigger Condition Schema
16-
- `node_modules/@objectstack/spec/src/ai/solution-blueprint.zod.ts`Exports: BlueprintConditionSchema, BlueprintSummaryOperationsSchema, BlueprintFieldSchema, BlueprintObjectSchema, BlueprintViewSchema
16+
- `node_modules/@objectstack/spec/src/ai/solution-blueprint.zod.ts`Solution Blueprint Schema (ADR-0033 §4 — plan-first authoring)
1717
- `node_modules/@objectstack/spec/src/ai/tool.zod.ts` — Exports: ToolSchema
1818

1919
## Transitive dependencies

0 commit comments

Comments
 (0)