Skip to content

fix(spec): regenerate drifted skills output and gate it in CI#3143

Closed
os-zhuang wants to merge 1 commit into
mainfrom
claude/inspiring-dubinsky-f1d663
Closed

fix(spec): regenerate drifted skills output and gate it in CI#3143
os-zhuang wants to merge 1 commit into
mainfrom
claude/inspiring-dubinsky-f1d663

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

skills/ is generated from the spec by gen:skill-refs and gen:react-blocks and committed, but neither generator had a check: sibling and nothing in CI regenerated them. Both existing gates miss this surface:

So it drifted: 8 committed files were stale on main (verified on c22e2c3fc).

This bites harder than the content/docs case — skills/ is published to third parties via npx skills add objectstack-ai/framework --all, so stale output ships to users.

1. A real bug, not just drift

Two published skills pointed agents at data/dataset.zod.ts — renamed to data/seed.zod.ts by #1620 ("complete Dataset->Seed rename"). The generator warned and skipped, staying green, so objectstack-data and objectstack-platform silently lost their seed schema pointer and told agents to Read a path that does not exist.

Fixed at the source (SKILL_MAP), not in the output. A missing mapped file is now a hard error rather than a warning — warn-and-skip is exactly what let this half-rename hide.

The rest of the diff is legitimate spec drift: a drawerWidth deprecation, a new relationshipValueField prop, columns becoming optional.

2. --check on both generators

Follows the convention #3134 established in build-docs.ts: every write goes through emit(), every regenerated folder through manageDir(), and nothing touches the tree until flush() — so check and write run byte-for-byte identical generation logic and differ only in final disposition. Not git diff --exit-code, which only sees tracked files and would miss a new or leftover page.

The sink is extracted to scripts/lib/generated-output.ts since both generators need it. manageDir() takes a predicate because a skill's references/ folder is a selective wipe, not a wholesale one — hand-written siblings (data-hooks.md, plugin-hooks.md) and the other generator's react-blocks.md live there and must survive. build-docs.ts keeps its own inline copy for now; it predates the extraction and is under active change (#3126), so migrating it belongs in its own PR.

3. Wired into "TypeScript Type Check", not ESLint

main requires exactly 4 status checks (TypeScript Type Check, Build Core, Test Core, Dogfood Regression Gate). ESLint is not one of them, so a gate placed there runs but cannot block a merge. (Re-verified: the one ruleset is enforcement: disabled and adds no checks.) Same job as check:docs, same reason: no paths filter, required, and both read src/ via tsx with no build — they fail in ~2s.

Why not the existing "Check Generated Artifacts" job?

That job in ci.yml looks like the natural home — it already runs check:skill-docs. But it is the wrong place twice over, and its own filter comments document this class of bug biting twice before:

  • It is paths-filtered. The generated filter watches skills/** and build-skill-docs.ts, but not packages/spec/src/**. Every file in this PR's drift traces to a packages/spec/src/** change (the seed.zod.ts rename, view.zod.ts's drawerWidth deprecation, the new relationshipValueField). A spec-only PR changes the generated output without touching skills/, so the filter would not match and the job would skip — which is exactly how this drift shipped.
  • It is not a required check. Only the 4 checks above are, so even when it does run it cannot block a merge.

check:docs landed in the typecheck job for precisely this reason (#3134), and its comment says so. These gates follow it.

Proving the gate can go red

A gate that cannot fail is theatre, so each failure mode was exercised — all 14 as expected:

case expect got
clean tree (both gates) 0
tampered content 1
deleted generated file 1
leftover file in references/ 1
leftover file in a sub-folder 1
SKILL_MAP → missing skill dir 1
SKILL_MAP → renamed/missing zod file 1
vacuous: empty SKILL_MAP 1
vacuous: empty REACT_BLOCKS 1
tampered contract json 1
deleted react-blocks.md 1
leftover file in contracts/ 1
negative control: hand-written .md in references/ 0

The vacuous guards matter because a run that finds no input emits nothing, and "nothing differs" would read as success — the gate would pass while checking nothing at all. Both now fail loudly instead of greenly. The negative control proves the selective-wipe predicate is not over-broad.

Also verified: the original repro (gen:skill-refs && gen:react-blocks && git status skills/) now produces zero drift; check:docs, check:role-word (bidirectional ratchet, scopes skills/), check:doc-authoring, and check:nul-bytes all stay green.

🤖 Generated with Claude Code

@os-zhuang os-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Jul 17, 2026
@vercel

vercel Bot commented Jul 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 17, 2026 2:30pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation ci/cd dependencies Pull requests that update a dependency file tooling size/m labels Jul 17, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec.

102 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

`skills/` is generated from the spec by `gen:skill-refs` and `gen:react-blocks`
and committed, but neither had a `check:` sibling and nothing in CI regenerated
them. Both existing gates miss this surface: `check:docs` (#3134) covers only
content/docs/references/, and `check:skill-docs` only SKILL.md frontmatter →
README + skills-reference.mdx. So it drifted — 8 committed files were stale on
main.

This matters more than the content/docs case: `skills/` is published to third
parties via `npx skills add objectstack-ai/framework --all`, so stale output
ships to users.

Fix the drift's root cause, not the output
------------------------------------------
Two published skills pointed agents at `data/dataset.zod.ts`, which #1620
renamed to `data/seed.zod.ts`. The generator warned and skipped, staying green,
so objectstack-data and objectstack-platform silently lost their seed schema
pointer and told agents to Read a path that no longer exists. SKILL_MAP now
names seed.zod.ts, and a missing mapped file is a hard error instead of a
warning — that half-rename is exactly what warn-and-skip is good at hiding.

Add --check to both generators
------------------------------
Following the convention #3134 established in build-docs.ts: every write goes
through emit(), every regenerated folder through manageDir(), and nothing
touches the tree until flush() — so check and write share byte-for-byte
identical generation logic and differ only in final disposition. Not
`git diff --exit-code`, which sees only tracked files and would miss a new or
leftover page.

The sink is extracted to scripts/lib/generated-output.ts since both generators
need it; manageDir() takes a predicate because a skill's references/ folder is a
selective wipe, not a wholesale one — hand-written siblings (data-hooks.md,
plugin-hooks.md) and the other generator's react-blocks.md live there and must
survive.

Wire into "TypeScript Type Check", not ESLint
---------------------------------------------
main requires exactly 4 status checks (TypeScript Type Check, Build Core, Test
Core, Dogfood Regression Gate). ESLint is not one of them, so a gate placed
there runs but cannot block a merge. Same job as check:docs, same reason: no
paths filter, required, and both read src/ via tsx with no build (~2s).

Proven able to go red before shipping: tampered content, deleted file, leftover
file, leftover in a sub-folder, stale SKILL_MAP entry, and both vacuous cases
(empty SKILL_MAP / empty REACT_BLOCKS) each exit 1; a hand-written .md dropped
in references/ stays green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@xuyushun441-sys
xuyushun441-sys force-pushed the claude/inspiring-dubinsky-f1d663 branch from 01ed191 to 68a6a59 Compare July 17, 2026 13:22
@os-zhuang

Copy link
Copy Markdown
Contributor Author

Heads-up: we collided. #3138 was landed on main as 6cc73e33c a few minutes ago covering the same ground — two agents picked up this task in parallel. Flagging rather than closing, since this is your branch to call.

The generated output is fully redundant now. I diffed all 8 of this PR's skills/ files against main:

identical: 8   differing: 0

Byte-identical — including the same SKILL_MAP fix (data/dataset.zod.tsdata/seed.zod.ts in both objectstack-data and objectstack-platform). Reassuring rather than annoying: two independent runs converging byte-for-byte is decent evidence the regeneration is right and the generator is deterministic.

We also independently reached the same three conclusions, which I'd take as corroboration of the design:

  • manageDir() needs an ownership predicatereferences/ is only partially generated (_index.md is the generator's; react-blocks.md from the sibling generator and hand-written notes must survive a wipe).
  • The generator's warn-and-skip on a missing SKILL_MAP entry is the actual false-green vector — --check alone would have locked the dropped seed pointer in as correct. It has to be fatal.
  • ci.yml's check-generated is the plausible-looking wrong home: filter-gated without the schema dirs, and not required. lint.yml's TypeScript Type Check is the one with teeth.

What's on main now: check:skill-refs + check:react-blocks, sharing scripts/lib/generated-output.ts, wired into TypeScript Type Check after #3134's check:docs. Each was proven red before being trusted (13 cases — stale content, missing file, stale untracked leftover, dangling SKILL_MAP entry, missing skill dir, unabsorbed spec change), including the check that git diff --exit-code returns 0 on the stale-leftover case, which is why manageDir exists at all.

If anything in your implementation is better than what landed, it's worth salvaging as a follow-up on top of main — otherwise this can just close. One genuine leftover either way: build-docs.ts still carries its own inline emit()/manageDir() copy and should migrate onto the shared helper. Also filed #3139 for the resolver's multi-line-import blind spot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd dependencies Pull requests that update a dependency file documentation Improvements or additions to documentation size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant