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-zhuangos-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:

caseexpectgot
clean tree (both gates)0
tampered content1
deleted generated file1
leftover file in references/1
leftover file in a sub-folder1
SKILL_MAP → missing skill dir1
SKILL_MAP → renamed/missing zod file1
vacuous: empty SKILL_MAP1
vacuous: empty REACT_BLOCKS1
tampered contract json1
deleted react-blocks.md1
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-zhuangos-zhuang added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Jul 17, 2026
@vercel

vercelBot commented Jul 17, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 17, 2026 2:30pm

Request Review

@github-actionsgithub-actionsBot 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-sysforce-pushed the claude/inspiring-dubinsky-f1d663 branch from 01ed191 to 68a6a59CompareJuly 17, 2026 13:22
@os-zhuang

Copy link
Copy Markdown
ContributorAuthor

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cddependenciesPull requests that update a dependency filedocumentationImprovements or additions to documentationsize/mskip-changesetPR has no user-facing published change; bypasses the changeset gatetooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@os-zhuang