Uh oh!
There was an error while loading. Please reload this page.
fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268
Conversation
…L.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
…ate 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
…e 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
… 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
…ill-map-reconciliation
`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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01H2oQebDDxYKfWZusyd8GXk
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-justin
commented
Sep 4, 2026
Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), Governed surface ( For the human merger, what to read:
CI on Generated by Claude Code |
os-justin
commented
Sep 4, 2026
Status (seat, 2026-09-04T08:36Z): approved by Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#14462
Fixes#14466
Fixes#14562
Fixes#14565
One family, one branch, one commit per member. The map that decides what nine published skill indexes point at is reconciled against the bodies that own those surfaces, and the generator gains the guards that keep it reconciled.
skills/**is a governed surface: this PR stays draft for a human merge. It is never flipped ready and no auto-merge is armed.The rulings this implements, quoted where operative
solution-blueprint. ⛔ Do not widen the body." · "This card carries the census — for each of the [SKILL_MAP] entries, the set difference in both directions against what its SKILL.md teaches — and applies the ruling above uniformly." · "whatever reconciliation lands must make a retired schema in the map impossible, not merely corrected once … If deriving the map from a checkable source is out of reach, say so in the PR rather than hand-fixing eleven lists."objectstack-automationreference index routes to the ADR-0020-retiredstate-machine.zod.tsand omits both per-nodeconfigschemas —SKILL_MAPinbuild-skill-references.ts#14466, triage 5506975178: dropautomation/state-machine.zod.ts; the two per-nodeconfigschemas join — "its retired-schema pointer is a distinct defect worth its own line".objectstack-formulareference index advertisesdata/date-macros.zod.ts, a surface the SKILL.md routes to objectstack-query —SKILL_MAPinbuild-skill-references.tsmaps it to both #14562, seat ruling 5536681114: dropdata/date-macros.zod.tsfrom the formula entry; a loud one-owner guard with a negative test.objectstack-i18nreference index routes 5 of its 7 transitive pointers at schemas unreachable from a translation bundle — the closure step inbuild-skill-references.ts#14565, triage 5514974653: "decide that feasibility question first and say so in the PR body, because it determines whether this is one fix or three." · "⛔ Do not hand-edit_index.md."objectstack-formulareference index advertisesdata/date-macros.zod.ts, a surface the SKILL.md routes to objectstack-query —SKILL_MAPinbuild-skill-references.tsmaps it to both #14562, triage 5515135764: "The class has two mechanisms … ⛔ Do not build theSKILL_MAP-only gate and declare the class closed."No SKILL.md body was widened. No
_index.mdwas hand-edited — every one is the output ofpnpm --filter @objectstack/spec gen:skill-refs.Answer 1 — does one gate cover both mechanisms? No, and they are in the same file
The class has two mechanisms and they get two guards, in one module,
packages/spec/scripts/lib/skill-map-guards.ts.checkSingleOwnerandcheckCoreEntryShapeask questions of the hand-written core list;checkTransitiveAllowlistconstrains the closure. Neither would catch the other's defect:objectstack-i18n's core list is correct and its index was still wrong, and no closure rule would have noticed that two packages both claimed date macros.Answer 2 — is there a checkable source for "retired"? No. Measured, three ways
The design was gated on this, so it was checked rather than assumed:
packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version.StateMachineis not there, and correctly so — the def still exists and still parses, throughAgentSchema.lifecycle.automation/state-machine.zod.tsstates the ADR-0020 retirement, and the same header documents the door that survives. A prose grep flags a file that is live surface for another package.state machineis inskills/objectstack-automation/SKILL.md— the skill's own description names it. A text-mention gate keeps precisely the pointer this PR removes. It is also vacuous in the other direction, since the stems are single common words (tool,view,app,action,page,context) that match any prose.The retirement that mattered was package-relative: dead surface for automation authoring, live surface for AI authoring — and nothing in the tree expresses a per-package liveness claim. So the answer the triage said it would accept is the answer: no checkable source exists, it is said here rather than hidden, and the guards that DO ship are the mechanical ones, each with negative tests. The judgement stays a judgement, stated out loud in the map.
Answer 3 — the closure step: reachability rule, or a per-package list?
A per-package list. The general rule is not merely unbuilt, it is unbuildable from this input, and the required outcome is the proof.
objectstack-i18npublished eight transitive pointers, and seven arrive through one edge:shared/strict-object.tsimportsshared/suggestions.zod.tsfor its "did you mean?" text, which importsdata/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.Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:
data/filter.zod.tsshared/expression.zod.tsdata/field-value.zod.tsshared/suggestions.zod.tskernel/metadata-protection.zod.tssystem/translation.zod.tsshared/identifiers.zod.tsThe required set puts a depth-4 pointer reached through a helper 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 two that way, because the fact that separates them — what a translation bundle can address, by name string — is not in the graph.
system/translation.zod.tsdoes not importshared/identifiers.zod.tsat all.So:
TRANSITIVE_ALLOWLIST, besideSKILL_MAP, opt-in per package. A package that declares nothing publishes its full closure and its index does not move. It is an allowlist, not a denylist, and that is the half that matters:shared/value-domain.zod.tsjoined the i18n index recently and unnoticed, when a new import edge appeared several files away — a denylist misses every new arrival by construction.data/field.zod.tswas left to this PR's judgement and is kept:FieldTranslationSchema.optionsis keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' }foroptions: [{ value: 'direct_mail', … }]), andSelectOptionSchemais the declaration those keys must match.The census — all nine
SKILL_MAPentriesThe triage said eleven. There are nine, and nine generated indexes:
data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.objectstack-aiconversation,mcp,embedding,knowledge-document,usage(3 with zero consumers outsidepackages/spec)ai/solution-blueprint.zod.ts— the schema behind thesolution_designbuilt-in skill the body's table namesobjectstack-automationautomation/state-machine.zod.ts— ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts,automation/io-node-config.zod.tsobjectstack-formuladata/date-macros.zod.ts— both bodies route it to objectstack-queryobjectstack-i18nobjectstack-apiapi/rest-server.zod.ts— zero mentions of the file or any of its six exported config schemasobjectstack-platformkernel/plugin-capability.zod.ts(thin, not wrong — the body teachesrequires:but routes tokernel/platform-capabilities.ts)stack.zod.ts— the body's whole subject isdefineStackand names four of its exportsobjectstack-dataobjectstack-queryobjectstack-uiThe two unedited rows are deliberate and the reasoning is on #15264: every row this PR edited came with a measured consumer census or an explicit ADR retirement, and those two have only the sweep. Deleting or adding a published pointer on a governed customer surface is worth its own decision rather than a rider on a PR a reviewer is already checking four things in.
The guards, and what each negative test drives
All in
packages/spec/scripts/lib/skill-map-guards.ts; tests inpackages/spec/scripts/skill-map-guards.test.ts(29 assertions). They live inlib/for the reasonexport-list.tsandfile-description.tsdo: the generator self-executes on import.checkSingleOwnerSHARED_CORE_SCHEMASwith a reason; a declaration with an empty reason; a declaration whose sharing has gonecheckCoreEntryShape*.zod.tspath — today those are dropped from the index silently, with nomissingrow and a green--checkcheckTransitiveAllowliststripInternalIssueIdsA second leg reads
build-skill-references.tsand asserts each guard is actually called there, and that the allowlist reaches the emit path. A guard nobody calls is green in a unit test and absent from the gate — which is the state the map was already in.Deviation 1 — the one-owner guard is not the flat rule it was asked for
The #14562 seat ruling asked for "at most one owner" and refused a duplicate-ownership rule, on the stated ground that there would be "zero legitimate instances after this drop". Measured at this PR's base, that ground does not hold.
date-macroswas one of four duplicates:data/date-macros.zod.tsdata/validation.zod.tsstate_machinevalidation rule (ADR-0020), the destination that replaced the retired shapedata/datasource.zod.tsproject setup (was objectstack-quickstart))data/seed.zod.tsThe flat rule refuses
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 — with the three measured instances declared rather than deleted. 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. A reviewer who prefers the literal rule should say so; reverting to it is a three-pointer deletion, not a code change.Deviation 2 — internal tracker ids are stripped at the catalog boundary, not at the source
Adding
automation/io-node-config.zod.tsturnedcheck:doc-authoringred: the generator publishes the first sentence of a module's doc block, and that file's opens with a bare tracker citation. That gate has no per-passage exemption, by design.Its prescribed remedy is to strip the id at the source and regenerate. That remedy is not taken here, for three specific reasons: the source is a
.zod.tsthis card is forbidden to edit; it is a file the package publishes, so editing it would change what@objectstack/specships and reopen the changeset decision below; and the same sentence is projected tocontent/docs/references/automation/io-node-config.mdx, which would pull a second generated tree into a diff whose surface is the skill catalog. Three surfaces to remove one token.The decisive measurement: the gate does not flag that identical sentence on the docs page. The rule is about the skill catalog specifically —
skills/**is loaded whole into customer context windows — so the strip is applied at the boundary into that catalog. 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, a doubled hash). A reviewer who wants the source edit instead can have it; it needs a changeset and a docs regeneration.skills/**net line delta: −9 (budget: not more than +6)4 lines added, 13 removed, across four of the nine indexes.
objectstack-aiobjectstack-automationobjectstack-formulaobjectstack-i18nThree of the four added lines are the members' named additions:
ai/solution-blueprint.zod.ts,automation/builtin-node-config.zod.ts,automation/io-node-config.zod.ts. The fourth is not a new pointer:ai/embedding.zod.tsmoves from the Core section to Transitive in the same file, becauseknowledge-source.zod.tscomposesEmbeddingModelSchema— that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers forobjectstack-ai: 20 down to 17.The other five indexes are byte-identical to
origin/main.Changeset:
skip-changeset, and why the named precedent points that wayPR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a
patchchangeset — and its file list says why: it editedpackages/spec/src/data/query.zod.ts, which is inside the package'sfilesallowlist (src/**/*.zod.ts). That half is absent here by ruling: no.zod.tsis edited.This diff is
packages/spec/scripts/**(not in thefilesallowlist, so nothing published) plusskills/**(shipped bynpx skills addfrom the repo, not from any released package). AGENTS.md: the label "is for a diff that publishes nothing from any released package." Five recent skills-only commits onmaincarry no changeset.check:empty-changesetandcheck:changeset-gate-self-testsare green.Measurements — every gate at head
eed6bb0fDerived with
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsfrom the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:pnpm --filter @objectstack/spec check:skill-refs✅ 9 generated files in sync with packages/specpnpm check:doc-authoring✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.pnpm --filter @objectstack/spec check:skill-examples✅ 257 prose examples type-check across 3 surface(s)pnpm --filter @objectstack/spec check:llms-txt✓ packages/spec/llms.txt: 97 claim(s) re-derivedpnpm --filter @objectstack/spec typechecktsc --noEmit+check:scripts-typecheck+check:test-typecheck;tsconfig.scripts.jsonincludesscripts/**/*, so the new lib and its test file are both in a checked programnode scripts/check-skills-token-ratchet.mjs✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratchetedpnpm check:pm-skill-ratchet✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.pnpm check:nul-bytes✓ check-nul-bytes --self-test: 75 assertions over a temp git reponode scripts/check-system-context-census.mjsOK — 106 elevation read sites in 20 packages across 45 files, all anchoredpnpm check:changeset-gate-self-tests✓ check-changeset-no-major --self-test: 116 assertionspnpm check:watch-hint-literal✓ 49 declaration(s) across 4 rostered name(s) … every one an arraynode scripts/pm/bare-root-worklist.mjs --self-testOK self-test: 54 live row(s) … none stale, none missing, none contradicted— the pin that holds this generator'sROOT_DIR_WATCH_HINTSdeclaration honestpnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.tsTest Files 1 passed (1) · Tests 29 passed (29)Four gates first answered
PREREQUISITE NOT MET(exit 3 or a build refusal) and were re-run green afterpnpm exec turbo run buildover the package closure:check:doc-formula-expressions,check:dual-build-cjs-loads,check:type-check-debt,check:skill-examples.Reverse verification — each guard driven to red, then restored
Predicted direction: red, with a named refusal. Observed, all three:
✅ 9 generated files in sync with packages/specdata/date-macros.zod.tsput back in the formula entrydata/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package..zod.tscore entry addedobjectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at allTRANSITIVE_ALLOWLISTkeyed on a package the map lacksTRANSITIVE_ALLOWLIST names objectstack-i18nn, which is not a SKILL_MAP package — the list would constrain nothing.Each mutation was confirmed on disk by counting the injected text before the run, and each restore is
git checkout HEAD --against absolute paths with a trap, proven by an emptygit diff HEADand agit hash-objectmatch against the HEAD blob. No build ordist/is involved:tsxand vitest resolve./lib/skill-map-guardsas a relative source path, with no packageexportsboundary in between, so the mutated bytes are the bytes that ran.Generated by Claude Code