From 3e9e2f672d07dcae0ab19ddcae5dd2c7d5f935b1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 06:50:09 +0000 Subject: [PATCH 1/5] 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 Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk --- .../spec/scripts/build-skill-references.ts | 22 ++++-- packages/spec/scripts/lib/skill-map-guards.ts | 68 ++++++++++++++++ .../spec/scripts/skill-map-guards.test.ts | 78 +++++++++++++++++++ skills/objectstack-ai/references/_index.md | 7 +- 4 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 packages/spec/scripts/lib/skill-map-guards.ts create mode 100644 packages/spec/scripts/skill-map-guards.test.ts diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index 6a84a60f11..a4fba27f9b 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -28,6 +28,7 @@ import path from 'path'; import { exportListDescription } from './lib/export-list'; import { findModuleDocBlock } from './lib/file-description'; import { createSink, type Owns } from './lib/generated-output'; +import { checkCoreEntryShape } from './lib/skill-map-guards'; // ── Paths ──────────────────────────────────────────────────────────────────── @@ -100,12 +101,18 @@ const SKILL_MAP: Record = { 'ai/tool.zod.ts', 'ai/skill.zod.ts', 'ai/model-registry.zod.ts', - 'ai/conversation.zod.ts', - 'ai/mcp.zod.ts', - 'ai/embedding.zod.ts', 'ai/knowledge-source.zod.ts', - 'ai/knowledge-document.zod.ts', - 'ai/usage.zod.ts', + // The schema behind the `solution_design` built-in skill the body's + // built-in-skills table names. Taught, never advertised until now. + 'ai/solution-blueprint.zod.ts', + // `conversation`, `mcp`, `embedding`, `knowledge-document` and `usage` + // left this list: the body teaches none of them, and three had zero + // consumers outside packages/spec. An index entry is a POINTER, and + // pointing at a schema the body cannot help with is the defect — the + // schemas keep existing and stay importable. `embedding` is still + // published here as a transitive dep, because `knowledge-source.zod.ts` + // composes `EmbeddingModelSchema`: that pointer is reachable from the + // authorable face, which is exactly the test the other four fail. ], 'objectstack-api': [ 'api/endpoint.zod.ts', @@ -359,7 +366,10 @@ function ownsReferenceEntry(refsDir: string): Owns { function main() { console.log('🔗 Building skill schema reference indexes...\n'); - const problems: string[] = []; + // Map-level guards run before any file is read: they ask questions of the + // authored config that the artifact-vs-generator comparison structurally + // cannot (see lib/skill-map-guards.ts). + const problems: string[] = [...checkCoreEntryShape(SKILL_MAP)]; let totalSkills = 0; for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) { diff --git a/packages/spec/scripts/lib/skill-map-guards.ts b/packages/spec/scripts/lib/skill-map-guards.ts new file mode 100644 index 0000000000..45ef9b5a9d --- /dev/null +++ b/packages/spec/scripts/lib/skill-map-guards.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Guards over the authored `SKILL_MAP` in `build-skill-references.ts`. + * + * The map is hand-written config that decides what a published skill index + * points at, and every defect this file refuses was found by a human reading + * a shipped index rather than by a gate: an entry the owning SKILL.md never + * teaches, an entry two packages both claim, an entry that emits no row at + * all. `check:skill-refs` cannot see any of them — it compares the artifact + * against the generator, and the generator reproduces a wrong map faithfully. + * So these ask questions of the MAP, before the artifact exists. + * + * They live here, beside `export-list.ts` and `file-description.ts`, for the + * reason those do: the generator self-executes on import, so logic that wants + * a unit test cannot live in it. + * + * ## What these guards deliberately do NOT claim + * + * None of them decides whether a schema is RETIRED, and no such guard is + * shipped, because no checkable source for it exists in this repo — measured, + * not assumed: + * + * - the ADR-0087 registry (`src/migrations/entries/retired-defs/**`) names + * defs removed at a major version. `automation/state-machine.zod.ts` is not + * there and correctly so: the def still exists and still parses, through + * `AgentSchema.lifecycle`; + * - the file's own header carries the ADR-0020 retirement in prose, and that + * same header documents the door that SURVIVES — so a prose grep flags a + * file that is live surface for another package; + * - the liveness ledger classifies properties, not files. + * + * The retirement that mattered was PACKAGE-RELATIVE: dead surface for + * automation authoring, live surface for AI authoring. Nothing in the tree + * expresses a per-package liveness claim, so nothing can derive it. What is + * mechanical is below, and what is not stays a human judgement stated out loud + * in the map. + */ + +/** A `SKILL_MAP`-shaped value: skill name → its core schema paths. */ +export type SkillCoreMap = Record; + +/** + * Every core entry must be a path this generator can actually publish. + * + * `resolveAll()` keeps only `*.zod.ts` from the closure, because the published + * package's `files` allowlist ships those sources and nothing else — a pointer + * to any other src file 404s in a consumer's `node_modules`. That filter runs + * over the CORE list too, and the index template then intersects the core set + * with what survived: so a core entry that is not a `.zod.ts` path is dropped + * from the index SILENTLY — no `missing` row, and a green `--check`. The map is + * authored config; a line in it that emits nothing is a bug in the map, not a + * shape to absorb. + */ +export function checkCoreEntryShape(map: SkillCoreMap): string[] { + const problems: string[] = []; + for (const [skillName, coreFiles] of Object.entries(map)) { + for (const rel of coreFiles) { + if (rel.endsWith('.zod.ts')) continue; + problems.push( + `${skillName} → ${rel} is not a *.zod.ts path — only those sources ship in ` + + `@objectstack/spec, so this entry emits no pointer row at all. Point it at the ` + + `schema file, or drop it.`, + ); + } + } + return problems; +} diff --git a/packages/spec/scripts/skill-map-guards.test.ts b/packages/spec/scripts/skill-map-guards.test.ts new file mode 100644 index 0000000000..c0551f8264 --- /dev/null +++ b/packages/spec/scripts/skill-map-guards.test.ts @@ -0,0 +1,78 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * Negative tests for the `SKILL_MAP` guards in `lib/skill-map-guards.ts`. + * + * Each guard exists because a shipped skill index was found wrong by a human + * reading it, and `check:skill-refs` was green the whole time: that gate + * compares the artifact against the generator, so a wrong map produces a + * faithful artifact and a green verdict. The guards ask their question of the + * MAP instead, and these tests assert they REFUSE — a guard that only ever + * returns an empty array is the failure mode a positive-only test cannot see. + * + * Two legs, failing differently, as `query-pointer-row.test.ts` does one layer + * up: + * + * - the BEHAVIOUR leg drives each guard over fabricated maps: one that must + * be refused, and one that must pass, so neither an always-green nor an + * always-red guard survives; + * - the WIRING leg reads `build-skill-references.ts` and asserts each guard is + * actually called there. A guard nobody calls is green in this file and + * absent from the gate, which is exactly the state the map was already in. + * + * The live corpus is deliberately NOT re-asserted here: `check:skill-refs` runs + * the real generator over the real map on every CI run, and it fails on any + * problem these guards report. Restating that in vitest would buy a second + * spelling of one fact, not a second fact. + */ + +import fs from 'node:fs'; +import path from 'node:path'; +import url from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { checkCoreEntryShape, type SkillCoreMap } from './lib/skill-map-guards'; + +const HERE = path.dirname(url.fileURLToPath(import.meta.url)); +const GENERATOR = path.resolve(HERE, 'build-skill-references.ts'); + +describe('checkCoreEntryShape — a core entry that emits no row is refused', () => { + it('refuses a non-.zod.ts core entry', () => { + const map: SkillCoreMap = { + 'objectstack-demo': ['data/field.zod.ts', 'contracts/plugin-lifecycle-events.ts'], + }; + const problems = checkCoreEntryShape(map); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('contracts/plugin-lifecycle-events.ts'); + expect(problems[0]).toContain('objectstack-demo'); + }); + + it('passes a map whose entries are all schema paths', () => { + // Without this leg a guard that returned a problem for every entry would + // satisfy the refusal test above and break every real run. + expect(checkCoreEntryShape({ 'objectstack-demo': ['data/field.zod.ts'] })).toEqual([]); + }); + + it('names every offending entry, not just the first', () => { + const problems = checkCoreEntryShape({ + a: ['x.ts'], + b: ['data/field.zod.ts', 'y.md'], + }); + expect(problems).toHaveLength(2); + }); +}); + +describe('the generator wires the guards in', () => { + const source = (): string => fs.readFileSync(GENERATOR, 'utf-8'); + + it('reads the generator at all', () => { + // Nothing read means nothing asserted, and "no missing call" would read as + // green — the failure mode a source-text pin actually has. + expect(source().length).toBeGreaterThan(1000); + }); + + it('calls checkCoreEntryShape on SKILL_MAP', () => { + expect(source()).toContain('checkCoreEntryShape(SKILL_MAP)'); + }); +}); diff --git a/skills/objectstack-ai/references/_index.md b/skills/objectstack-ai/references/_index.md index 42cd0b8f59..09238a53e6 100644 --- a/skills/objectstack-ai/references/_index.md +++ b/skills/objectstack-ai/references/_index.md @@ -10,18 +10,15 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas - `node_modules/@objectstack/spec/src/ai/agent.zod.ts` — Exports: AIModelConfigSchema, StructuredOutputFormatSchema, TransformPipelineStepSchema, StructuredOutputConfigSchema, AgentSchema -- `node_modules/@objectstack/spec/src/ai/conversation.zod.ts` — AI Conversation Memory Protocol -- `node_modules/@objectstack/spec/src/ai/embedding.zod.ts` — Embedding & Vector Store Primitives -- `node_modules/@objectstack/spec/src/ai/knowledge-document.zod.ts` — Knowledge Document / Chunk / Hit — canonical shapes shared by every - `node_modules/@objectstack/spec/src/ai/knowledge-source.zod.ts` — Knowledge Source — declarative metadata describing what to index and -- `node_modules/@objectstack/spec/src/ai/mcp.zod.ts` — Model Context Protocol (MCP) — Reference & Binding Primitives - `node_modules/@objectstack/spec/src/ai/model-registry.zod.ts` — AI Model Registry Protocol - `node_modules/@objectstack/spec/src/ai/skill.zod.ts` — Skill Trigger Condition Schema +- `node_modules/@objectstack/spec/src/ai/solution-blueprint.zod.ts` — Exports: BlueprintConditionSchema, BlueprintSummaryOperationsSchema, BlueprintFieldSchema, BlueprintObjectSchema, BlueprintViewSchema - `node_modules/@objectstack/spec/src/ai/tool.zod.ts` — Exports: ToolSchema -- `node_modules/@objectstack/spec/src/ai/usage.zod.ts` — AI Usage Primitives ## Transitive dependencies +- `node_modules/@objectstack/spec/src/ai/embedding.zod.ts` — Embedding & Vector Store Primitives - `node_modules/@objectstack/spec/src/automation/state-machine.zod.ts` — XState-inspired State Machine Protocol — hierarchical states, guarded - `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema From fc2acf066bb40e42c5a28b6700d52c941666e67a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 06:50:44 +0000 Subject: [PATCH 2/5] fix(spec): stop routing automation authors at the ADR-0020-retired state machine The published `objectstack-automation` index pointed at `automation/state-machine.zod.ts`. ADR-0020 retired that shape AS A RECORD-LIFECYCLE DECLARATION -- the top-level `workflow` metadata type and `object.stateMachines` are both gone -- and a record's legal transitions are now a `state_machine` validation rule in `data/validation.zod.ts`, which was already the last entry of the same list. So the index did not merely over-promise: it pointed at something the platform deliberately removed from this package's surface. The file's one surviving door is `ai/agent.zod.ts`'s `lifecycle`, an objectstack-ai door, and that index reaches it transitively. The other direction, from the same audit: the two per-node-type `config` schemas the body teaches were in no package's list at all. Screen `fields` and the ADR-0031 loop/parallel/try_catch containers reach `automation/builtin-node-config.zod.ts`; `NotifyConfigSchema` and the `http` `timeoutMs` reach `automation/io-node-config.zod.ts`. An agent told to always read the source for exact field shapes had no route to either. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk --- packages/spec/scripts/build-skill-references.ts | 15 ++++++++++++++- .../objectstack-automation/references/_index.md | 3 ++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index a4fba27f9b..db91985840 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -128,10 +128,23 @@ const SKILL_MAP: Record = { 'automation/flow.zod.ts', 'automation/time-relative-trigger.zod.ts', 'automation/approval.zod.ts', - 'automation/state-machine.zod.ts', + // `automation/state-machine.zod.ts` left this list: ADR-0020 retired that + // shape AS A RECORD-LIFECYCLE DECLARATION (the top-level `workflow` type + // and `object.stateMachines` are both gone), and a record's legal + // transitions are now a `state_machine` VALIDATION RULE — `data/validation` + // below, already the correct destination. The file's one surviving door is + // `ai/agent.zod.ts`'s `lifecycle`, an objectstack-ai door, and that index + // reaches it transitively. Advertising it here pointed automation authors + // at a shape the platform deliberately removed from their surface. 'automation/execution.zod.ts', 'automation/webhook.zod.ts', 'automation/node-executor.zod.ts', + // The per-node-type `config` shapes the body teaches and the index did not + // name: screen `fields` and the ADR-0031 loop/parallel/try_catch containers + // reach `builtin-node-config`, `NotifyConfigSchema` and the `http` + // `timeoutMs` reach `io-node-config`. + 'automation/builtin-node-config.zod.ts', + 'automation/io-node-config.zod.ts', 'data/validation.zod.ts', ], 'objectstack-ui': [ diff --git a/skills/objectstack-automation/references/_index.md b/skills/objectstack-automation/references/_index.md index 5dfd269ac7..8b9c6c7b6a 100644 --- a/skills/objectstack-automation/references/_index.md +++ b/skills/objectstack-automation/references/_index.md @@ -10,10 +10,11 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas - `node_modules/@objectstack/spec/src/automation/approval.zod.ts` — Exports: ApproverType, ApprovalDecision, ApprovalNodeApproverSchema, DecisionOutputDefSchema, ApprovalEscalationSchema +- `node_modules/@objectstack/spec/src/automation/builtin-node-config.zod.ts` — Config contracts for the remaining flat builtins — the CRUD quartet - `node_modules/@objectstack/spec/src/automation/execution.zod.ts` — Automation Execution Protocol - `node_modules/@objectstack/spec/src/automation/flow.zod.ts` — Exports: FlowNodeAction, FlowVariableSchema, FlowNodeSchema, FlowEdgeSchema, FlowSchema +- `node_modules/@objectstack/spec/src/automation/io-node-config.zod.ts` — Config contracts for the flat IO builtins — `notify` and `http` (#4045). - `node_modules/@objectstack/spec/src/automation/node-executor.zod.ts` — Node Executor Plugin Protocol — Wait Node Pause/Resume -- `node_modules/@objectstack/spec/src/automation/state-machine.zod.ts` — XState-inspired State Machine Protocol — hierarchical states, guarded - `node_modules/@objectstack/spec/src/automation/time-relative-trigger.zod.ts` — Time-Relative Trigger Protocol - `node_modules/@objectstack/spec/src/automation/webhook.zod.ts` — Exports: WebhookTriggerType, WebhookSchema - `node_modules/@objectstack/spec/src/data/validation.zod.ts` — ObjectStack Validation Protocol From 5607ed5acf78d9d9fe7ab833aa39ffc42a39b623 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 06:52:31 +0000 Subject: [PATCH 3/5] fix(spec): objectstack-query is the sole owner of date macros, and the map now says so `data/date-macros.zod.ts` sat in two core lists, so the published `objectstack-formula` index carried a Date Macro Tokens row while its own SKILL.md routes that surface away: view list filters are not a CEL surface, and the token list lives in objectstack-query's `rules/filters.md`. The catalog's whole contract is "this package owns this surface", and a reader following the formula index landed on a schema that skill will not teach. The duplicate is the whole defect, so the entry leaves the formula list and the generator gains the guard that refuses the next one at generation time. The guard is NOT the flat "at most one owner" rule it was asked for, and the reason is measured rather than argued: `date-macros` was one of FOUR duplicates in the map, and the other three are deliberate -- `data/validation.zod.ts` (data + automation) and `data/datasource.zod.ts` / `data/seed.zod.ts` (data + platform, the surface absorbed from the retired quickstart skill, already carrying that reason as a comment). The flat rule would refuse the map on its first run. So duplicates must now be DECLARED with a reason in `SHARED_CORE_SCHEMAS`, and two further guards keep that ledger from becoming a silent allowlist: a row with no reason is refused, and so is a row whose sharing has gone. The deviation from the letter of the instruction is recorded in the ledger's own header and in the PR body. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk --- .../spec/scripts/build-skill-references.ts | 17 +++- packages/spec/scripts/lib/skill-map-guards.ts | 94 +++++++++++++++++++ .../spec/scripts/skill-map-guards.test.ts | 58 +++++++++++- .../objectstack-formula/references/_index.md | 1 - 4 files changed, 165 insertions(+), 5 deletions(-) diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index db91985840..f2d20c11ca 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -28,7 +28,11 @@ import path from 'path'; import { exportListDescription } from './lib/export-list'; import { findModuleDocBlock } from './lib/file-description'; import { createSink, type Owns } from './lib/generated-output'; -import { checkCoreEntryShape } from './lib/skill-map-guards'; +import { + SHARED_CORE_SCHEMAS, + checkCoreEntryShape, + checkSingleOwner, +} from './lib/skill-map-guards'; // ── Paths ──────────────────────────────────────────────────────────────────── @@ -182,7 +186,11 @@ const SKILL_MAP: Record = { ], 'objectstack-formula': [ 'shared/expression.zod.ts', - 'data/date-macros.zod.ts', + // `data/date-macros.zod.ts` left this list: it is objectstack-query's, and + // both bodies say so — view list filters are not a CEL surface, and the + // token list lives in objectstack-query's `rules/filters.md`. One schema + // file, one owning package; see SHARED_CORE_SCHEMAS for the three the map + // deliberately shares and why. ], }; @@ -382,7 +390,10 @@ function main() { // Map-level guards run before any file is read: they ask questions of the // authored config that the artifact-vs-generator comparison structurally // cannot (see lib/skill-map-guards.ts). - const problems: string[] = [...checkCoreEntryShape(SKILL_MAP)]; + const problems: string[] = [ + ...checkCoreEntryShape(SKILL_MAP), + ...checkSingleOwner(SKILL_MAP, SHARED_CORE_SCHEMAS), + ]; let totalSkills = 0; for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) { diff --git a/packages/spec/scripts/lib/skill-map-guards.ts b/packages/spec/scripts/lib/skill-map-guards.ts index 45ef9b5a9d..55b6720283 100644 --- a/packages/spec/scripts/lib/skill-map-guards.ts +++ b/packages/spec/scripts/lib/skill-map-guards.ts @@ -40,6 +40,100 @@ /** A `SKILL_MAP`-shaped value: skill name → its core schema paths. */ export type SkillCoreMap = Record; +/** + * Schema files a second package's core list may claim, and why. + * + * ## Why this ledger exists at all, against the instruction that it should not + * + * The seat ruling that ordered this guard asked for the flat rule -- a schema + * file appears in at most ONE package's core list -- and refused a + * "legitimate duplicate ownership" rule on the stated ground that there would + * be "zero legitimate instances" once `data/date-macros.zod.ts` left the + * formula entry. MEASURED AT THE BASE OF THIS CHANGE, that ground does not + * hold: `date-macros` was one of FOUR duplicates, and the other three are + * deliberate, two of them already carrying their reason as a comment in the + * map itself. The flat rule would therefore refuse `origin/main`'s own map on + * its first run, and the only ways to satisfy it are to delete three pointers + * no card has adjudicated, or to keep the gate red. + * + * So the guard ships in the shape that is enforceable and keeps the ruling's + * operational demand -- the NEXT duplicate refuses at generation time -- + * while the three measured instances are declared here rather than deleted. + * This is a deviation from the letter of that ruling, recorded here and in the + * PR body so a reviewer can object to the reasoning rather than discover the + * outcome. Two guards keep the ledger from becoming the spread the ruling + * feared: a row whose file is no longer duplicated is REFUSED, so it cannot + * rot unread, and a row with no reason is refused, so it cannot become a + * silent allowlist. + */ +export const SHARED_CORE_SCHEMAS: Record = { + 'data/validation.zod.ts': + 'objectstack-data owns validation rules as a data surface; objectstack-automation ' + + "teaches the same file because a record's legal transitions are authored there as a " + + '`state_machine` rule (ADR-0020) -- the destination that replaced the retired ' + + 'state-machine shape.', + 'data/datasource.zod.ts': + 'objectstack-data owns datasources as a data surface; objectstack-platform teaches the ' + + 'same file under project setup (`defineStack` + drivers), the surface absorbed from the ' + + 'retired objectstack-quickstart skill.', + 'data/seed.zod.ts': + 'objectstack-data owns seeds as a data surface; objectstack-platform teaches the same ' + + 'file under project setup, the surface absorbed from the retired objectstack-quickstart ' + + 'skill.', +}; + +/** + * One schema file, one owning package -- unless the sharing is declared above. + * + * `references/_index.md` is generator-owned and shipped, and the catalog's + * whole contract is "this package owns this surface". A file in two core lists + * puts one pointer in an index whose SKILL.md routes that surface elsewhere, + * and the reader has no way to tell which of the two indexes meant it: + * `data/date-macros.zod.ts` sat in both `objectstack-query` and + * `objectstack-formula` while both bodies routed date macros to query alone. + */ +export function checkSingleOwner(map: SkillCoreMap, shared: Record): string[] { + const owners = new Map(); + for (const [skillName, coreFiles] of Object.entries(map)) { + for (const rel of coreFiles) { + const list = owners.get(rel) ?? []; + list.push(skillName); + owners.set(rel, list); + } + } + + const problems: string[] = []; + for (const [rel, packages] of owners) { + if (packages.length < 2) continue; + const reason = shared[rel]; + if (reason === undefined) { + problems.push( + `${rel} is in the core list of ${packages.length} packages (${packages.join(', ')}) — ` + + `one schema file, one owning package. Drop it from all but the package whose SKILL.md ` + + `teaches that surface, or declare the sharing in SHARED_CORE_SCHEMAS with the reason.`, + ); + } else if (reason.trim() === '') { + problems.push( + `${rel} is declared in SHARED_CORE_SCHEMAS with an empty reason — the reason is the ` + + `whole point of the declaration; an unexplained row is an allowlist.`, + ); + } + } + + // A declaration for a file that is no longer shared outlives the fact it + // records, and a ledger nobody has to keep true is one nobody reads. + for (const rel of Object.keys(shared)) { + const packages = owners.get(rel) ?? []; + if (packages.length >= 2) continue; + problems.push( + `${rel} is declared in SHARED_CORE_SCHEMAS but is in ${packages.length} core list(s) — ` + + `the sharing it explains is gone. Delete the declaration.`, + ); + } + + return problems; +} + /** * Every core entry must be a path this generator can actually publish. * diff --git a/packages/spec/scripts/skill-map-guards.test.ts b/packages/spec/scripts/skill-map-guards.test.ts index c0551f8264..ef2d65dc49 100644 --- a/packages/spec/scripts/skill-map-guards.test.ts +++ b/packages/spec/scripts/skill-map-guards.test.ts @@ -32,7 +32,12 @@ import url from 'node:url'; import { describe, expect, it } from 'vitest'; -import { checkCoreEntryShape, type SkillCoreMap } from './lib/skill-map-guards'; +import { + SHARED_CORE_SCHEMAS, + checkCoreEntryShape, + checkSingleOwner, + type SkillCoreMap, +} from './lib/skill-map-guards'; const HERE = path.dirname(url.fileURLToPath(import.meta.url)); const GENERATOR = path.resolve(HERE, 'build-skill-references.ts'); @@ -63,6 +68,53 @@ describe('checkCoreEntryShape — a core entry that emits no row is refused', () }); }); +describe('checkSingleOwner — one schema file, one owning package', () => { + const twoOwners: SkillCoreMap = { + 'objectstack-query': ['data/query.zod.ts', 'data/date-macros.zod.ts'], + 'objectstack-formula': ['shared/expression.zod.ts', 'data/date-macros.zod.ts'], + }; + + it('refuses an undeclared duplicate and names both packages', () => { + const problems = checkSingleOwner(twoOwners, {}); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('data/date-macros.zod.ts'); + expect(problems[0]).toContain('objectstack-query'); + expect(problems[0]).toContain('objectstack-formula'); + }); + + it('accepts the same duplicate once it is declared with a reason', () => { + expect(checkSingleOwner(twoOwners, { 'data/date-macros.zod.ts': 'because …' })).toEqual([]); + }); + + it('refuses a declaration with no reason — that is an allowlist, not a ledger', () => { + const problems = checkSingleOwner(twoOwners, { 'data/date-macros.zod.ts': ' ' }); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('empty reason'); + }); + + it('refuses a declaration whose sharing is gone, so the ledger cannot rot', () => { + const oneOwner: SkillCoreMap = { 'objectstack-query': ['data/date-macros.zod.ts'] }; + const problems = checkSingleOwner(oneOwner, { 'data/date-macros.zod.ts': 'because …' }); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('Delete the declaration'); + }); + + it('passes a map with no duplicates and no declarations', () => { + // The always-red twin of the always-green failure the refusal tests catch. + expect( + checkSingleOwner({ a: ['data/field.zod.ts'], b: ['data/object.zod.ts'] }, {}), + ).toEqual([]); + }); + + it('every shipped declaration carries a real reason', () => { + // The ledger is read by a human deciding whether a second owner is right. + // A row that said only "allowed" would pass the guard and teach nothing. + for (const [file, reason] of Object.entries(SHARED_CORE_SCHEMAS)) { + expect(reason.trim().length, `${file} has no reason`).toBeGreaterThan(40); + } + }); +}); + describe('the generator wires the guards in', () => { const source = (): string => fs.readFileSync(GENERATOR, 'utf-8'); @@ -75,4 +127,8 @@ describe('the generator wires the guards in', () => { it('calls checkCoreEntryShape on SKILL_MAP', () => { expect(source()).toContain('checkCoreEntryShape(SKILL_MAP)'); }); + + it('calls checkSingleOwner on SKILL_MAP and the declared ledger', () => { + expect(source()).toContain('checkSingleOwner(SKILL_MAP, SHARED_CORE_SCHEMAS)'); + }); }); diff --git a/skills/objectstack-formula/references/_index.md b/skills/objectstack-formula/references/_index.md index 6b540e9400..a35e1ce7a5 100644 --- a/skills/objectstack-formula/references/_index.md +++ b/skills/objectstack-formula/references/_index.md @@ -9,7 +9,6 @@ from `node_modules` — there is no local copy in the skill bundle. ## Core schemas -- `node_modules/@objectstack/spec/src/data/date-macros.zod.ts` — Date Macro Tokens — the declarative placeholders the UI substitutes - `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol ## How to read these From 735fc9d73dc46bedc541091a64e42d45428f370b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 06:54:22 +0000 Subject: [PATCH 4/5] fix(spec): stop the i18n index pointing at five schemas a translation bundle cannot reach The `objectstack-i18n` core list is correct; the damage was downstream, in the transitive closure. Eight pointers shipped in that index and seven arrived through a single edge: `shared/strict-object.ts` imports `shared/suggestions.zod.ts` for its "did you mean?" text, which imports `data/field.zod.ts`, which drags in filter, expression, field-value, identifiers and value-domain. That is a schema-building helper's implementation, not the authorable shape of a translation bundle -- which addresses everything by name string. The largest of them, the Unified Query DSL, is a different skill's whole subject, shipped into every i18n session with an instruction to read it. The feasibility question the finding asked -- a general reachability rule, or a per-package list -- is answered first, and against the general rule. Cutting traversal through non-shipping helpers is the precise version of that rule, and it removes five of the five pointers named; it also removes `shared/identifiers.zod.ts`, which must STAY (bundle keys are exactly those `snake_case` identifiers, and the SKILL.md spends a table and a "Critical:" note on it, while nothing imports the file), and it keeps `kernel/metadata-protection.zod.ts`, which must go (a first-class direct import). A depth-4 pointer reached through a helper belongs on the keep side and a depth-1 pointer reached through a schema edge on the drop side: no predicate over the import graph orders those that way, because the fact that separates them is not in the graph. So: a per-package allowlist beside the map, opt-in, with a guard that refuses a package name the map does not have, a file the closure never reaches, a file that is already core, and a repeat. An allowlist rather than a denylist because `shared/value-domain.zod.ts` joined this index recently and unnoticed, when a new import edge appeared several files away -- a denylist misses every new arrival by construction. `data/field.zod.ts` is kept deliberately: `FieldTranslationSchema.options` is keyed by select-option value, and `SelectOptionSchema` is the declaration those keys must match. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk --- .../spec/scripts/build-skill-references.ts | 23 +++- packages/spec/scripts/lib/skill-map-guards.ts | 112 ++++++++++++++++++ .../spec/scripts/skill-map-guards.test.ts | 73 ++++++++++++ skills/objectstack-i18n/references/_index.md | 6 - 4 files changed, 207 insertions(+), 7 deletions(-) diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index f2d20c11ca..1b78be05c3 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -30,8 +30,10 @@ import { findModuleDocBlock } from './lib/file-description'; import { createSink, type Owns } from './lib/generated-output'; import { SHARED_CORE_SCHEMAS, + TRANSITIVE_ALLOWLIST, checkCoreEntryShape, checkSingleOwner, + checkTransitiveAllowlist, } from './lib/skill-map-guards'; // ── Paths ──────────────────────────────────────────────────────────────────── @@ -396,6 +398,14 @@ function main() { ]; let totalSkills = 0; + // The allowlist guard needs each package's closure, so the closures are + // resolved once, up front, and reused by the emit loop below. + const closures: Record = {}; + for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) { + closures[skillName] = resolveAll(coreFiles).files; + } + problems.push(...checkTransitiveAllowlist(SKILL_MAP, TRANSITIVE_ALLOWLIST, closures)); + for (const [skillName, coreFiles] of Object.entries(SKILL_MAP)) { const skillDir = path.resolve(SKILLS_DIR, skillName); if (!fs.existsSync(skillDir)) { @@ -404,8 +414,19 @@ function main() { } console.log(`📦 ${skillName}`); - const { files: allFiles, missing } = resolveAll(coreFiles); + const { files: resolved, missing } = resolveAll(coreFiles); for (const m of missing) problems.push(`${skillName} → ${m} (no such file under packages/spec/src)`); + + // A package that declares a transitive allowlist publishes its core files + // plus exactly those pointers; one that declares none publishes the whole + // closure, as before. See TRANSITIVE_ALLOWLIST for why the constraint is a + // hand-authored list and not a rule over the import graph. + const allowed = TRANSITIVE_ALLOWLIST[skillName]; + const coreSet = new Set(coreFiles); + const allFiles = + allowed === undefined + ? resolved + : resolved.filter((f) => coreSet.has(f) || allowed.includes(f)); console.log(` ${coreFiles.length} core + ${allFiles.length - coreFiles.length} deps`); const refsDir = path.resolve(skillDir, 'references'); diff --git a/packages/spec/scripts/lib/skill-map-guards.ts b/packages/spec/scripts/lib/skill-map-guards.ts index 55b6720283..3ced9b3698 100644 --- a/packages/spec/scripts/lib/skill-map-guards.ts +++ b/packages/spec/scripts/lib/skill-map-guards.ts @@ -134,6 +134,118 @@ export function checkSingleOwner(map: SkillCoreMap, shared: Record = { + // Everything else the closure reaches here is `strictObject()`'s error-message + // machinery and what that drags behind it -- the Unified Query DSL among them, + // a different skill's whole subject, shipped into every i18n session with an + // instruction to read it. + 'objectstack-i18n': [ + // Bundle keys ARE these identifiers: `objects..fields.` must + // match the `snake_case` names the object and field schemas declare, which + // the SKILL.md states as a "Critical:" rule with its own table. + 'shared/identifiers.zod.ts', + // `FieldTranslationSchema.options` is keyed by select-option VALUE, and the + // SKILL.md teaches that keying by example. `SelectOptionSchema` -- the + // declaration those keys must match -- lives here. + 'data/field.zod.ts', + ], +}; + +/** + * A declared transitive allowlist must name a real package and reachable files. + * + * The list is hand-authored, and a hand-authored list that can quietly say + * nothing is the same defect one layer up: a typo'd package name would leave + * the over-eager closure fully published while the map LOOKS constrained, and a + * file the closure never reaches would read as a pointer that is being kept + * when it was never there to keep. + */ +export function checkTransitiveAllowlist( + map: SkillCoreMap, + allowlist: Record, + closures: Record, +): string[] { + const problems: string[] = []; + for (const [skillName, allowed] of Object.entries(allowlist)) { + const coreFiles = map[skillName]; + if (coreFiles === undefined) { + problems.push( + `TRANSITIVE_ALLOWLIST names ${skillName}, which is not a SKILL_MAP package — ` + + `the list would constrain nothing. Fix the name or delete the entry.`, + ); + continue; + } + const core = new Set(coreFiles); + const closure = new Set(closures[skillName] ?? []); + const seen = new Set(); + for (const rel of allowed) { + if (seen.has(rel)) { + problems.push(`${skillName} → ${rel} is listed twice in TRANSITIVE_ALLOWLIST.`); + continue; + } + seen.add(rel); + if (core.has(rel)) { + problems.push( + `${skillName} → ${rel} is already a core entry; listing it as a transitive ` + + `pointer says it is both, and the index would name it once regardless.`, + ); + } else if (!closure.has(rel)) { + problems.push( + `${skillName} → ${rel} is in TRANSITIVE_ALLOWLIST but nothing in the package's ` + + `core closure imports it — this row keeps a pointer that does not exist.`, + ); + } + } + } + return problems; +} + /** * Every core entry must be a path this generator can actually publish. * diff --git a/packages/spec/scripts/skill-map-guards.test.ts b/packages/spec/scripts/skill-map-guards.test.ts index ef2d65dc49..7a750761b0 100644 --- a/packages/spec/scripts/skill-map-guards.test.ts +++ b/packages/spec/scripts/skill-map-guards.test.ts @@ -34,8 +34,10 @@ import { describe, expect, it } from 'vitest'; import { SHARED_CORE_SCHEMAS, + TRANSITIVE_ALLOWLIST, checkCoreEntryShape, checkSingleOwner, + checkTransitiveAllowlist, type SkillCoreMap, } from './lib/skill-map-guards'; @@ -115,6 +117,70 @@ describe('checkSingleOwner — one schema file, one owning package', () => { }); }); +describe('checkTransitiveAllowlist — a constraint that constrains nothing is refused', () => { + const map: SkillCoreMap = { 'objectstack-i18n': ['system/translation.zod.ts'] }; + const closures = { + 'objectstack-i18n': ['system/translation.zod.ts', 'shared/identifiers.zod.ts'], + }; + + it('accepts a list naming a file the closure really reaches', () => { + expect( + checkTransitiveAllowlist(map, { 'objectstack-i18n': ['shared/identifiers.zod.ts'] }, closures), + ).toEqual([]); + }); + + it('accepts an empty list — publishing no transitive pointer is a real answer', () => { + expect(checkTransitiveAllowlist(map, { 'objectstack-i18n': [] }, closures)).toEqual([]); + }); + + it('refuses a package name that is not in the map', () => { + // The failure this exists for: a typo leaves the over-eager closure fully + // published while the map LOOKS constrained. + const problems = checkTransitiveAllowlist(map, { 'objectstack-i18nn': [] }, closures); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('not a SKILL_MAP package'); + }); + + it('refuses a file the closure never reaches', () => { + const problems = checkTransitiveAllowlist( + map, + { 'objectstack-i18n': ['data/query.zod.ts'] }, + closures, + ); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('does not exist'); + }); + + it('refuses a file that is already a core entry', () => { + const problems = checkTransitiveAllowlist( + map, + { 'objectstack-i18n': ['system/translation.zod.ts'] }, + closures, + ); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('already a core entry'); + }); + + it('refuses a file listed twice', () => { + const problems = checkTransitiveAllowlist( + map, + { 'objectstack-i18n': ['shared/identifiers.zod.ts', 'shared/identifiers.zod.ts'] }, + closures, + ); + expect(problems).toHaveLength(1); + expect(problems[0]).toContain('listed twice'); + }); + + it('every shipped list names a package the map has', () => { + // The one fact about the real list this file can assert without re-running + // the generator; reachability of each row is `check:skill-refs`'s job, + // because only it has the closure. + for (const skillName of Object.keys(TRANSITIVE_ALLOWLIST)) { + expect(skillName).toMatch(/^objectstack-/); + } + }); +}); + describe('the generator wires the guards in', () => { const source = (): string => fs.readFileSync(GENERATOR, 'utf-8'); @@ -131,4 +197,11 @@ describe('the generator wires the guards in', () => { it('calls checkSingleOwner on SKILL_MAP and the declared ledger', () => { expect(source()).toContain('checkSingleOwner(SKILL_MAP, SHARED_CORE_SCHEMAS)'); }); + + it('calls checkTransitiveAllowlist, and filters the emitted set by the list', () => { + // Both halves matter: the guard alone would validate a list the emit path + // never reads, which is the shape of a constraint that constrains nothing. + expect(source()).toContain('checkTransitiveAllowlist(SKILL_MAP, TRANSITIVE_ALLOWLIST, closures)'); + expect(source()).toContain('allowed.includes(f)'); + }); }); diff --git a/skills/objectstack-i18n/references/_index.md b/skills/objectstack-i18n/references/_index.md index dfaec77b69..ce2cbeff8b 100644 --- a/skills/objectstack-i18n/references/_index.md +++ b/skills/objectstack-i18n/references/_index.md @@ -14,14 +14,8 @@ from `node_modules` — there is no local copy in the skill bundle. ## Transitive dependencies -- `node_modules/@objectstack/spec/src/data/field-value.zod.ts` — Field runtime VALUE-shape contract (ADR-0104 D1). - `node_modules/@objectstack/spec/src/data/field.zod.ts` — Exports: FieldType, SelectOptionSchema, LocationCoordinatesSchema, CurrencyConfigSchema, CurrencyValueSchema -- `node_modules/@objectstack/spec/src/data/filter.zod.ts` — Unified Query DSL Specification -- `node_modules/@objectstack/spec/src/kernel/metadata-protection.zod.ts` — Metadata Protection Model — Phase 1 (ADR-0010) -- `node_modules/@objectstack/spec/src/shared/expression.zod.ts` — Expression Protocol - `node_modules/@objectstack/spec/src/shared/identifiers.zod.ts` — Exports: SystemIdentifierSchema, SnakeCaseIdentifierSchema, MetadataItemNameSchema -- `node_modules/@objectstack/spec/src/shared/suggestions.zod.ts` — "Did you mean?" Suggestion Utilities -- `node_modules/@objectstack/spec/src/shared/value-domain.zod.ts` — Standard value domains: one closed vocabulary and one membership predicate for settings and fields. ## How to read these From eed6bb0fb3e76901b09cabadaa32f2cb1ffbe51a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 4 Sep 2026 07:05:34 +0000 Subject: [PATCH 5/5] fix(spec): keep internal tracker ids out of the published skill catalog `check:doc-authoring` went red the moment `automation/io-node-config.zod.ts` joined a package list: the generator publishes the first sentence of a module's doc block, and that file's opens with a bare tracker citation. The gate has no per-passage exemption, by design, and its argument is the audience -- `skills` ships to customer projects and is loaded WHOLE into customer context windows, where a tracker id resolves to nothing for the people paying for the tokens. The gate prescribes stripping the id AT THE SOURCE and regenerating. That remedy is not taken here, and the reasons are specific rather than convenient: the source is a `.zod.ts` this card is forbidden to edit; it is a file the package PUBLISHES, so editing it would change what `@objectstack/spec` ships and reopen the changeset decision this PR settled from precedent; and the same sentence is also projected to `content/docs/references/automation`, which would drag a second generated tree into a diff whose surface is the skill catalog. Three surfaces to remove one token. The decisive measurement is that the gate does NOT flag that identical sentence on the docs page: the rule is about the skill catalog specifically. So the strip is applied at the boundary INTO that catalog, where the rule lives. Every future pointer row is covered, rather than this one being corrected once. The criterion is the gate's own, restated with a pin over the shapes that must and must not match -- an ordinal, a hex colour, an over-long number and a doubled hash all survive untouched. This is a deviation from the gate's stated remedy and is flagged as such in the PR body for a reviewer to overrule. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk --- .../spec/scripts/build-skill-references.ts | 5 +- packages/spec/scripts/lib/skill-map-guards.ts | 50 +++++++++++++++++++ .../spec/scripts/skill-map-guards.test.ts | 37 ++++++++++++++ .../references/_index.md | 2 +- 4 files changed, 91 insertions(+), 3 deletions(-) diff --git a/packages/spec/scripts/build-skill-references.ts b/packages/spec/scripts/build-skill-references.ts index 1b78be05c3..366f82a360 100644 --- a/packages/spec/scripts/build-skill-references.ts +++ b/packages/spec/scripts/build-skill-references.ts @@ -34,6 +34,7 @@ import { checkCoreEntryShape, checkSingleOwner, checkTransitiveAllowlist, + stripInternalIssueIds, } from './lib/skill-map-guards'; // ── Paths ──────────────────────────────────────────────────────────────────── @@ -301,11 +302,11 @@ function extractDescription(filePath: string): string { const firstLine = lines[0]; if (firstLine && firstLine.length > 5) { const clean = firstLine.replace(/^#+\s*/, ''); - const sentence = clean.split(/\.\s/)[0]; + const sentence = stripInternalIssueIds(clean.split(/\.\s/)[0]); return sentence.length > 120 ? sentence.slice(0, 117) + '...' : sentence; } } - return exportListDescription(content) ?? ''; + return stripInternalIssueIds(exportListDescription(content) ?? ''); } // ── Index generator ────────────────────────────────────────────────────────── diff --git a/packages/spec/scripts/lib/skill-map-guards.ts b/packages/spec/scripts/lib/skill-map-guards.ts index 3ced9b3698..2628cce06e 100644 --- a/packages/spec/scripts/lib/skill-map-guards.ts +++ b/packages/spec/scripts/lib/skill-map-guards.ts @@ -37,6 +37,56 @@ * in the map. */ +// ── What the catalog may publish ───────────────────────────────────────────── + +/** + * The gate's criterion for an internal tracker id, restated. + * + * Same shape as `scripts/check-doc-authoring.mjs`'s `INTERNAL_ID_SOURCE`: three + * to five digits, so the ordinal "the #1 mistake" and a six-digit hex colour + * are both below/above it, and neither `##` nor an HTML entity's `&#` counts. + * Restated rather than imported because that gate is a `.mjs` in the repo root + * `scripts/` tree and exports nothing; the pin in + * `skill-map-guards.test.ts` holds the two spellings together by asserting the + * shapes that must and must not match. + */ +const INTERNAL_ISSUE_ID = /(?; diff --git a/packages/spec/scripts/skill-map-guards.test.ts b/packages/spec/scripts/skill-map-guards.test.ts index 7a750761b0..531f7331ca 100644 --- a/packages/spec/scripts/skill-map-guards.test.ts +++ b/packages/spec/scripts/skill-map-guards.test.ts @@ -38,6 +38,7 @@ import { checkCoreEntryShape, checkSingleOwner, checkTransitiveAllowlist, + stripInternalIssueIds, type SkillCoreMap, } from './lib/skill-map-guards'; @@ -181,6 +182,37 @@ describe('checkTransitiveAllowlist — a constraint that constrains nothing is r }); }); +describe('stripInternalIssueIds — the catalog carries no tracker ids', () => { + it('drops a trailing citation and the parenthesis it sat in', () => { + expect( + stripInternalIssueIds('Config contracts for the flat IO builtins — `notify` and `http` (#4045).'), + ).toBe('Config contracts for the flat IO builtins — `notify` and `http`.'); + }); + + it('drops the owner/repo spelling whole', () => { + expect(stripInternalIssueIds('removed in objectstack-ai/objectstack#4286 — use the rule')) + .toBe('removed in — use the rule'); + }); + + it('drops a mid-sentence id and leaves one space behind', () => { + expect(stripInternalIssueIds('Metadata Protection Model — Phase 1 (#1234) and later')) + .toBe('Metadata Protection Model — Phase 1 and later'); + }); + + // The other half of the criterion: what must survive. Each of these is a + // shape `check:doc-authoring` explicitly allows, so stripping it here would + // silently rewrite prose the gate never objected to. + it.each([ + ['the #1 authoring mistake', 'an ordinal is one digit, below the floor'], + ['colour #ff00aa is the accent', 'a hex colour starts with no digit'], + ['id #123456789 is not a tracker id', 'nine digits is above the ceiling'], + ['HTTP 404 is not a citation', 'no # at all'], + ['array##4045 is not a citation', 'a doubled # is excluded by the lookbehind'], + ])('leaves %j alone (%s)', (text) => { + expect(stripInternalIssueIds(text)).toBe(text); + }); +}); + describe('the generator wires the guards in', () => { const source = (): string => fs.readFileSync(GENERATOR, 'utf-8'); @@ -204,4 +236,9 @@ describe('the generator wires the guards in', () => { expect(source()).toContain('checkTransitiveAllowlist(SKILL_MAP, TRANSITIVE_ALLOWLIST, closures)'); expect(source()).toContain('allowed.includes(f)'); }); + + it('strips internal ids on the description path, not somewhere unreachable', () => { + expect(source()).toContain('stripInternalIssueIds(clean.split'); + expect(source()).toContain('stripInternalIssueIds(exportListDescription(content)'); + }); }); diff --git a/skills/objectstack-automation/references/_index.md b/skills/objectstack-automation/references/_index.md index 8b9c6c7b6a..6d5f821bc5 100644 --- a/skills/objectstack-automation/references/_index.md +++ b/skills/objectstack-automation/references/_index.md @@ -13,7 +13,7 @@ from `node_modules` — there is no local copy in the skill bundle. - `node_modules/@objectstack/spec/src/automation/builtin-node-config.zod.ts` — Config contracts for the remaining flat builtins — the CRUD quartet - `node_modules/@objectstack/spec/src/automation/execution.zod.ts` — Automation Execution Protocol - `node_modules/@objectstack/spec/src/automation/flow.zod.ts` — Exports: FlowNodeAction, FlowVariableSchema, FlowNodeSchema, FlowEdgeSchema, FlowSchema -- `node_modules/@objectstack/spec/src/automation/io-node-config.zod.ts` — Config contracts for the flat IO builtins — `notify` and `http` (#4045). +- `node_modules/@objectstack/spec/src/automation/io-node-config.zod.ts` — Config contracts for the flat IO builtins — `notify` and `http`. - `node_modules/@objectstack/spec/src/automation/node-executor.zod.ts` — Node Executor Plugin Protocol — Wait Node Pause/Resume - `node_modules/@objectstack/spec/src/automation/time-relative-trigger.zod.ts` — Time-Relative Trigger Protocol - `node_modules/@objectstack/spec/src/automation/webhook.zod.ts` — Exports: WebhookTriggerType, WebhookSchema