fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it - #15268

Merged
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation
Sep 4, 2026
Merged

fix(spec): reconcile SKILL_MAP with the SKILL.md bodies that own each surface, and guard it#15268
os-zhuang merged 6 commits into
mainfrom
claude/issue-14462-skill-map-reconciliation

Conversation

@os-justin

Copy link
Copy Markdown
Collaborator

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

No SKILL.md body was widened. No _index.md was hand-edited — every one is the output of pnpm --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. checkSingleOwner and checkCoreEntryShape ask questions of the hand-written core list; checkTransitiveAllowlist constrains 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:

  1. The ADR-0087 registry (packages/spec/src/migrations/entries/retired-defs/, 94 entries) names defs removed at a major version. StateMachine is not there, and correctly so — the def still exists and still parses, through AgentSchema.lifecycle.
  2. The file's own header prose.automation/state-machine.zod.ts states 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.
  3. "The owning SKILL.md must mention it." This one looked promising and was measured: the string state machineis in skills/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-i18n published eight transitive pointers, and seven arrive through one 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.

Cutting traversal through non-shipping helpers is the precise version of the reachability rule. Measured against what the ruling requires:

pointerrequiredwhat the precise rule doeswhy
data/filter.zod.tsdropdropsreached only through the helper
shared/expression.zod.tsdropdropsreached only through the helper
data/field-value.zod.tsdropdropsreached only through the helper
shared/suggestions.zod.tsdropdropsreached only through the helper
kernel/metadata-protection.zod.tsdropkeepsa first-class direct import of system/translation.zod.ts
shared/identifiers.zod.tskeepdropsnothing imports it; bundle keys ARE those identifiers

The 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.ts does not import shared/identifiers.zod.ts at all.

So: TRANSITIVE_ALLOWLIST, beside SKILL_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.ts joined 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.ts was left to this PR's judgement and is kept: FieldTranslationSchema.options is keyed by select-option value, the SKILL.md teaches that keying by example (options: { direct_mail: '直邮' } for options: [{ value: 'direct_mail', … }]), and SelectOptionSchema is the declaration those keys must match.

The census — all nine SKILL_MAP entries

The triage said eleven. There are nine, and nine generated indexes: data · query · ai · api · automation · ui · platform · i18n · formula. 59 core entries.

packageadvertised, not taughttaught, not advertisedaction
objectstack-aiconversation, mcp, embedding, knowledge-document, usage (3 with zero consumers outside packages/spec)ai/solution-blueprint.zod.ts — the schema behind the solution_design built-in skill the body's table namesnarrowed + added
objectstack-automationautomation/state-machine.zod.ts — ADR-0020 retired it as a record-lifecycle declarationautomation/builtin-node-config.zod.ts, automation/io-node-config.zod.tsswapped
objectstack-formuladata/date-macros.zod.ts — both bodies route it to objectstack-querydropped
objectstack-i18ncore list correct; 6 of 8 transitive pointers unreachable from a bundleclosure constrained
objectstack-apiapi/rest-server.zod.ts — zero mentions of the file or any of its six exported config schemasrecorded, no edit — filed as #15264
objectstack-platformkernel/plugin-capability.zod.ts (thin, not wrong — the body teaches requires: but routes to kernel/platform-capabilities.ts)stack.zod.ts — the body's whole subject is defineStack and names four of its exportsrecorded, no edit — filed as #15264
objectstack-datanonenonenone
objectstack-querynonenonenone
objectstack-uinonenonenone

The 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 in packages/spec/scripts/skill-map-guards.test.ts (29 assertions). They live in lib/ for the reason export-list.ts and file-description.ts do: the generator self-executes on import.

guardrefusesnegative tests
checkSingleOwnera schema file in two core lists that is not declared in SHARED_CORE_SCHEMAS with a reason; a declaration with an empty reason; a declaration whose sharing has gone6, incl. a passing map so an always-red guard cannot survive
checkCoreEntryShapea core entry that is not a *.zod.ts path — today those are dropped from the index silently, with no missing row and a green --check3
checkTransitiveAllowlista package name the map does not have; a file the closure never reaches; a file already in core; a repeat6
stripInternalIssueIds(a publication rule, not a guard) internal tracker ids in a published row8, incl. five shapes that must survive untouched

A second leg reads build-skill-references.ts and 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-macros was one of four duplicates:

filepackagesdeliberate?
data/date-macros.zod.tsquery + formulano — the defect #14562 filed
data/validation.zod.tsdata + automationyes — a record's transitions are a state_machine validation rule (ADR-0020), the destination that replaced the retired shape
data/datasource.zod.tsdata + platformyes — already carrying its reason as a comment in the map (project setup (was objectstack-quickstart))
data/seed.zod.tsdata + platformyes — same comment

The 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.ts turned check:doc-authoring red: 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.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 below; and the same sentence is projected to content/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.

indexaddedremovednet
objectstack-ai25−3
objectstack-automation21+1
objectstack-formula01−1
objectstack-i18n06−6

Three 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.ts moves from the Core section to Transitive in the same file, because knowledge-source.zod.ts composes EmbeddingModelSchema — that pointer IS reachable from the authorable face, which is exactly the test the other four narrowed schemas fail. Net published pointers for objectstack-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 way

PR #14737 (the "Sort Node" labelling fix on this same generator, landed 2026-09-03) carried a patch changeset — and its file list says why: it edited packages/spec/src/data/query.zod.ts, which is inside the package's files allowlist (src/**/*.zod.ts). That half is absent here by ruling: no .zod.ts is edited.

This diff is packages/spec/scripts/** (not in the files allowlist, so nothing published) plus skills/** (shipped by npx skills add from 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 on main carry no changeset. check:empty-changeset and check:changeset-gate-self-tests are green.

Measurements — every gate at head eed6bb0f

Derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands from the worktree, no path arguments: 53 commands, all exit 0. Exits captured before any pipe. The load-bearing ones:

gateexitverdict line
pnpm --filter @objectstack/spec check:skill-refs0✅ 9 generated files in sync with packages/spec
pnpm check:doc-authoring0✓ doc authoring guard: 46 published skill files clean — no internal issue-id references.
pnpm --filter @objectstack/spec check:skill-examples0✅ 257 prose examples type-check across 3 surface(s)
pnpm --filter @objectstack/spec check:llms-txt0✓ packages/spec/llms.txt: 97 claim(s) re-derived
pnpm --filter @objectstack/spec typecheck0covers tsc --noEmit + check:scripts-typecheck + check:test-typecheck; tsconfig.scripts.json includes scripts/**/*, so the new lib and its test file are both in a checked program
node scripts/check-skills-token-ratchet.mjs0✓ 36 authored bundle file(s) within their ceilings; 11 generator-owned file(s) measured, not ratcheted
pnpm check:pm-skill-ratchet0✓ check-skill-line-ratchet: declared cross-file moves: 1, total ceilings down 1 line.
pnpm check:nul-bytes0✓ check-nul-bytes --self-test: 75 assertions over a temp git repo
node scripts/check-system-context-census.mjs0OK — 106 elevation read sites in 20 packages across 45 files, all anchored
pnpm check:changeset-gate-self-tests0✓ check-changeset-no-major --self-test: 116 assertions
pnpm check:watch-hint-literal0✓ 49 declaration(s) across 4 rostered name(s) … every one an array
node scripts/pm/bare-root-worklist.mjs --self-test0OK self-test: 54 live row(s) … none stale, none missing, none contradicted — the pin that holds this generator's ROOT_DIR_WATCH_HINTS declaration honest
pnpm --filter @objectstack/spec exec vitest run scripts/skill-map-guards.test.ts0Test 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 after pnpm exec turbo run build over 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:

mutationexitthe gate's own line
baseline, unmutated head0✅ 9 generated files in sync with packages/spec
data/date-macros.zod.ts put back in the formula entry1data/date-macros.zod.ts is in the core list of 2 packages (objectstack-query, objectstack-formula) — one schema file, one owning package.
a non-.zod.ts core entry added1objectstack-formula → contracts/plugin-lifecycle-events.ts is not a *.zod.ts path — … emits no pointer row at all
TRANSITIVE_ALLOWLIST keyed on a package the map lacks1TRANSITIVE_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 empty git diff HEADand a git hash-object match against the HEAD blob. No build or dist/ is involved: tsx and vitest resolve ./lib/skill-map-guards as a relative source path, with no package exports boundary in between, so the mutated bytes are the bytes that ran.


Generated by Claude Code

…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
`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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing 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
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 0 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 460134af85f7ab2cf68abc62f1bbb9783b8899ddpackageMentionDocs.

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Seat review: ACCEPT — review of record on the family anchor #14462 (comment 5537457796), domain:spec seat, 2026-09-04T07:56Z; member pointers on #14466, #14562, #14565.

Governed surface (skills/** regenerated together with its generator — check-governed-merges reads that as governed, not as the pure-regeneration exception) ⇒ this PR stays draft. No agent seat flips it ready, enqueues it, or arms auto-merge; a human merge is the review record. Review requested from os-zhuang and hotlong.

For the human merger, what to read:

  • The four regenerated indexes: net −9 lines in skills/**; every added pointer is one the members' rulings named (solution-blueprint, the two automation config schemas); the fourth added line is embedding moving from Core to Transitive in the same file.
  • Two deviations from seat rulings, both argued in the body and both accepted by the seat with the reasoning in 5537457796: SHARED_CORE_SCHEMAS (three deliberate cross-package duplicates exist on main, so the flat one-owner rule would red the current map) and stripInternalIssueIds at the catalog boundary (the doc-authoring gate is zero-tolerance on skills/** and baseline-held on content/docs). Either can be overruled here; the flat rule is a three-pointer deletion, the source strip is a .zod.ts edit + changeset + docs regeneration.
  • data/field.zod.ts stays in the i18n index on the SelectOptionSchema reasoning; state-machine.zod.ts survives only in objectstack-ai's transitive list, through agent.zod.ts.
  • skip-changeset is measured: no .zod.ts edited, packages/spec/scripts/** is outside the package's files allowlist, skills/ is in no package.

CI on eed6bb0f at 07:50Z: 17 completed checks green, the long jobs still running; merge when the required checks report on this head.


Generated by Claude Code

@os-justinClaude

Copy link
Copy Markdown
CollaboratorAuthor

Status (seat, 2026-09-04T08:36Z): approved by os-zhuang at 07:53:09Z on eed6bb0f, marked ready at 07:54Z, and every check on that head is green as of 08:12Z (48 check runs: Lint & Repo Gates, all four Type Check jobs, Test Core ×6, Dogfood ×4, Temporal Conformance, Governed Surface Queue Guard re-run after the approval — all success). The PR is not in the merge queue (git ls-remote origin 'refs/heads/gh-readonly-queue/*' at 08:32Z lists #15073#15236#15251#15257#15266). Governed surface: the agent seat does not enqueue or arm auto-merge — os-zhuang / hotlong, enabling auto-merge (squash) puts it in the queue, as was done for #15257.


Generated by Claude Code

Merged via the queue into main with commit 445a25fSep 4, 2026
49 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment