Skip to content

Commit 3e9e2f6

Browse files
committed
fix(spec): narrow the objectstack-ai reference index to what its SKILL.md teaches
The published `objectstack-ai` index advertised five schemas the body never teaches -- `conversation`, `mcp`, `embedding`, `knowledge-document`, `usage`, three of them with zero consumers outside `packages/spec` -- and omitted `ai/solution-blueprint.zod.ts`, the schema behind the `solution_design` built-in skill the body's own table names. An index entry is a POINTER, so pointing at a schema the body cannot help with sends an agent reading into `node_modules` for guidance that does not exist. The schemas keep existing and stay importable; only the false promise leaves. `embedding` is still published, as a transitive dependency: `knowledge-source.zod.ts` composes `EmbeddingModelSchema`, so that pointer IS reachable from the authorable face -- the test the other four fail. Adds the first map-level guard, in `scripts/lib/` beside `export-list.ts` and `file-description.ts` because the generator self-executes on import: a core entry that is not a `*.zod.ts` path emits no pointer row at all today, with no `missing` row and a green `--check`, because the closure filter drops it silently before the index template ever sees it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
1 parent 9c1bcda commit 3e9e2f6

4 files changed

Lines changed: 164 additions & 11 deletions

File tree

packages/spec/scripts/build-skill-references.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import path from 'path';
2828
import { exportListDescription } from './lib/export-list';
2929
import { findModuleDocBlock } from './lib/file-description';
3030
import { createSink, type Owns } from './lib/generated-output';
31+
import { checkCoreEntryShape } from './lib/skill-map-guards';
3132

3233
// ── Paths ────────────────────────────────────────────────────────────────────
3334

@@ -100,12 +101,18 @@ const SKILL_MAP: Record<string, string[]> = {
100101
'ai/tool.zod.ts',
101102
'ai/skill.zod.ts',
102103
'ai/model-registry.zod.ts',
103-
'ai/conversation.zod.ts',
104-
'ai/mcp.zod.ts',
105-
'ai/embedding.zod.ts',
106104
'ai/knowledge-source.zod.ts',
107-
'ai/knowledge-document.zod.ts',
108-
'ai/usage.zod.ts',
105+
// The schema behind the `solution_design` built-in skill the body's
106+
// built-in-skills table names. Taught, never advertised until now.
107+
'ai/solution-blueprint.zod.ts',
108+
// `conversation`, `mcp`, `embedding`, `knowledge-document` and `usage`
109+
// left this list: the body teaches none of them, and three had zero
110+
// consumers outside packages/spec. An index entry is a POINTER, and
111+
// pointing at a schema the body cannot help with is the defect — the
112+
// schemas keep existing and stay importable. `embedding` is still
113+
// published here as a transitive dep, because `knowledge-source.zod.ts`
114+
// composes `EmbeddingModelSchema`: that pointer is reachable from the
115+
// authorable face, which is exactly the test the other four fail.
109116
],
110117
'objectstack-api': [
111118
'api/endpoint.zod.ts',
@@ -359,7 +366,10 @@ function ownsReferenceEntry(refsDir: string): Owns {
359366

360367
function main() {
361368
console.log('🔗 Building skill schema reference indexes...\n');
362-
const problems: string[] = [];
369+
// Map-level guards run before any file is read: they ask questions of the
370+
// authored config that the artifact-vs-generator comparison structurally
371+
// cannot (see lib/skill-map-guards.ts).
372+
const problems: string[] = [...checkCoreEntryShape(SKILL_MAP)];
363373
let totalSkills = 0;
364374

365375
for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Guards over the authored `SKILL_MAP` in `build-skill-references.ts`.
5+
*
6+
* The map is hand-written config that decides what a published skill index
7+
* points at, and every defect this file refuses was found by a human reading
8+
* a shipped index rather than by a gate: an entry the owning SKILL.md never
9+
* teaches, an entry two packages both claim, an entry that emits no row at
10+
* all. `check:skill-refs` cannot see any of them — it compares the artifact
11+
* against the generator, and the generator reproduces a wrong map faithfully.
12+
* So these ask questions of the MAP, before the artifact exists.
13+
*
14+
* They live here, beside `export-list.ts` and `file-description.ts`, for the
15+
* reason those do: the generator self-executes on import, so logic that wants
16+
* a unit test cannot live in it.
17+
*
18+
* ## What these guards deliberately do NOT claim
19+
*
20+
* None of them decides whether a schema is RETIRED, and no such guard is
21+
* shipped, because no checkable source for it exists in this repo — measured,
22+
* not assumed:
23+
*
24+
* - the ADR-0087 registry (`src/migrations/entries/retired-defs/**`) names
25+
* defs removed at a major version. `automation/state-machine.zod.ts` is not
26+
* there and correctly so: the def still exists and still parses, through
27+
* `AgentSchema.lifecycle`;
28+
* - the file's own header carries the ADR-0020 retirement in prose, and that
29+
* same header documents the door that SURVIVES — so a prose grep flags a
30+
* file that is live surface for another package;
31+
* - the liveness ledger classifies properties, not files.
32+
*
33+
* The retirement that mattered was PACKAGE-RELATIVE: dead surface for
34+
* automation authoring, live surface for AI authoring. Nothing in the tree
35+
* expresses a per-package liveness claim, so nothing can derive it. What is
36+
* mechanical is below, and what is not stays a human judgement stated out loud
37+
* in the map.
38+
*/
39+
40+
/** A `SKILL_MAP`-shaped value: skill name → its core schema paths. */
41+
export type SkillCoreMap = Record<string, readonly string[]>;
42+
43+
/**
44+
* Every core entry must be a path this generator can actually publish.
45+
*
46+
* `resolveAll()` keeps only `*.zod.ts` from the closure, because the published
47+
* package's `files` allowlist ships those sources and nothing else — a pointer
48+
* to any other src file 404s in a consumer's `node_modules`. That filter runs
49+
* over the CORE list too, and the index template then intersects the core set
50+
* with what survived: so a core entry that is not a `.zod.ts` path is dropped
51+
* from the index SILENTLY — no `missing` row, and a green `--check`. The map is
52+
* authored config; a line in it that emits nothing is a bug in the map, not a
53+
* shape to absorb.
54+
*/
55+
export function checkCoreEntryShape(map: SkillCoreMap): string[] {
56+
const problems: string[] = [];
57+
for (const [skillName, coreFiles] of Object.entries(map)) {
58+
for (const rel of coreFiles) {
59+
if (rel.endsWith('.zod.ts')) continue;
60+
problems.push(
61+
`${skillName}${rel} is not a *.zod.ts path — only those sources ship in ` +
62+
`@objectstack/spec, so this entry emits no pointer row at all. Point it at the ` +
63+
`schema file, or drop it.`,
64+
);
65+
}
66+
}
67+
return problems;
68+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* Negative tests for the `SKILL_MAP` guards in `lib/skill-map-guards.ts`.
5+
*
6+
* Each guard exists because a shipped skill index was found wrong by a human
7+
* reading it, and `check:skill-refs` was green the whole time: that gate
8+
* compares the artifact against the generator, so a wrong map produces a
9+
* faithful artifact and a green verdict. The guards ask their question of the
10+
* MAP instead, and these tests assert they REFUSE — a guard that only ever
11+
* returns an empty array is the failure mode a positive-only test cannot see.
12+
*
13+
* Two legs, failing differently, as `query-pointer-row.test.ts` does one layer
14+
* up:
15+
*
16+
* - the BEHAVIOUR leg drives each guard over fabricated maps: one that must
17+
* be refused, and one that must pass, so neither an always-green nor an
18+
* always-red guard survives;
19+
* - the WIRING leg reads `build-skill-references.ts` and asserts each guard is
20+
* actually called there. A guard nobody calls is green in this file and
21+
* absent from the gate, which is exactly the state the map was already in.
22+
*
23+
* The live corpus is deliberately NOT re-asserted here: `check:skill-refs` runs
24+
* the real generator over the real map on every CI run, and it fails on any
25+
* problem these guards report. Restating that in vitest would buy a second
26+
* spelling of one fact, not a second fact.
27+
*/
28+
29+
import fs from 'node:fs';
30+
import path from 'node:path';
31+
import url from 'node:url';
32+
33+
import { describe, expect, it } from 'vitest';
34+
35+
import { checkCoreEntryShape, type SkillCoreMap } from './lib/skill-map-guards';
36+
37+
const HERE = path.dirname(url.fileURLToPath(import.meta.url));
38+
const GENERATOR = path.resolve(HERE, 'build-skill-references.ts');
39+
40+
describe('checkCoreEntryShape — a core entry that emits no row is refused', () => {
41+
it('refuses a non-.zod.ts core entry', () => {
42+
const map: SkillCoreMap = {
43+
'objectstack-demo': ['data/field.zod.ts', 'contracts/plugin-lifecycle-events.ts'],
44+
};
45+
const problems = checkCoreEntryShape(map);
46+
expect(problems).toHaveLength(1);
47+
expect(problems[0]).toContain('contracts/plugin-lifecycle-events.ts');
48+
expect(problems[0]).toContain('objectstack-demo');
49+
});
50+
51+
it('passes a map whose entries are all schema paths', () => {
52+
// Without this leg a guard that returned a problem for every entry would
53+
// satisfy the refusal test above and break every real run.
54+
expect(checkCoreEntryShape({ 'objectstack-demo': ['data/field.zod.ts'] })).toEqual([]);
55+
});
56+
57+
it('names every offending entry, not just the first', () => {
58+
const problems = checkCoreEntryShape({
59+
a: ['x.ts'],
60+
b: ['data/field.zod.ts', 'y.md'],
61+
});
62+
expect(problems).toHaveLength(2);
63+
});
64+
});
65+
66+
describe('the generator wires the guards in', () => {
67+
const source = (): string => fs.readFileSync(GENERATOR, 'utf-8');
68+
69+
it('reads the generator at all', () => {
70+
// Nothing read means nothing asserted, and "no missing call" would read as
71+
// green — the failure mode a source-text pin actually has.
72+
expect(source().length).toBeGreaterThan(1000);
73+
});
74+
75+
it('calls checkCoreEntryShape on SKILL_MAP', () => {
76+
expect(source()).toContain('checkCoreEntryShape(SKILL_MAP)');
77+
});
78+
});

skills/objectstack-ai/references/_index.md

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,15 @@ from `node_modules` — there is no local copy in the skill bundle.
1010
## Core schemas
1111

1212
- `node_modules/@objectstack/spec/src/ai/agent.zod.ts` — Exports: AIModelConfigSchema, StructuredOutputFormatSchema, TransformPipelineStepSchema, StructuredOutputConfigSchema, AgentSchema
13-
- `node_modules/@objectstack/spec/src/ai/conversation.zod.ts` — AI Conversation Memory Protocol
14-
- `node_modules/@objectstack/spec/src/ai/embedding.zod.ts` — Embedding & Vector Store Primitives
15-
- `node_modules/@objectstack/spec/src/ai/knowledge-document.zod.ts` — Knowledge Document / Chunk / Hit — canonical shapes shared by every
1613
- `node_modules/@objectstack/spec/src/ai/knowledge-source.zod.ts` — Knowledge Source — declarative metadata describing what to index and
17-
- `node_modules/@objectstack/spec/src/ai/mcp.zod.ts` — Model Context Protocol (MCP) — Reference & Binding Primitives
1814
- `node_modules/@objectstack/spec/src/ai/model-registry.zod.ts` — AI Model Registry Protocol
1915
- `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
2017
- `node_modules/@objectstack/spec/src/ai/tool.zod.ts` — Exports: ToolSchema
21-
- `node_modules/@objectstack/spec/src/ai/usage.zod.ts` — AI Usage Primitives
2218

2319
## Transitive dependencies
2420

21+
- `node_modules/@objectstack/spec/src/ai/embedding.zod.ts` — Embedding & Vector Store Primitives
2522
- `node_modules/@objectstack/spec/src/automation/state-machine.zod.ts` — XState-inspired State Machine Protocol — hierarchical states, guarded
2623
- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1).
2724
- `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema

0 commit comments

Comments
 (0)