feat(spec): check:generated — report every stale artifact in one run - #4183
Merged
Conversation
`packages/spec` has eight checked-in generated artifacts, each with its own gate, split across two CI jobs that run their gates SEQUENTIALLY. The first stale artifact therefore masks every one behind it: you fix it, push, and learn about the next on the following run. Two pushes on #4040 (`check:docs`, then `check:api-surface`), two more on #4161 (`check:spec-changes`, then `check:upgrade-guide`) — four round trips spent discovering something one local run could have said at once. Every gate runs; a failure does not stop the rest. The summary lists all stale artifacts with the exact `gen:` command for each. `--fix` regenerates ONLY what this run proved stale. Deliberately not a regenerate-everything button: blanket regeneration rewrites artifacts whose staleness you never saw, which is how a real semantic change lands silently inside a mechanical diff. The gate -> generator ledger reconciles against `package.json` on EVERY run, in both directions, rather than behind a `--self-test` flag. An unclassified `check:`/`gen:` script fails the run instead of quietly dropping out of coverage — otherwise the summary would still say "all artifacts up to date" while checking fewer, the exact class of lie this exists to remove. It proved itself by rejecting its own `package.json` entry on the first run. Two things it refuses to be silent about: the `check:api-surface` stale-`dist` trap is printed inline when that gate is the one failing (it reads the built `.d.ts`, so an unbuilt tree reports newly-added exports as breaking removals), and the four source audits it does NOT run are named, so "all up to date" never reads as "everything passed". It also reports that `gen:openapi` and `gen:sbom` have no gate at all. Verified both directions: clean built tree -> all 8 pass; a change injected into a migration step's `rationale` -> `check:upgrade-guide` fails and `--fix` regenerates that artifact and no other. That run also corrected an earlier reading of mine: the two ADR-0087 gates have different inputs — the conversion registry drives `spec-changes.json`, the rationale prose drives `protocol-upgrade-guide.md` — they had only appeared to fail together because #4161 changed both. AGENTS.md's hand-rolled loop over eight hardcoded gate names is replaced by the command; that list could not survive a ninth artifact, and the ledger can. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
os-zhuang
marked this pull request as ready for review
July 30, 2026 14:38
This was referenced Jul 30, 2026
Merged
os-zhuang
added a commit
that referenced
this pull request
Jul 30, 2026
…ial-cased regex (#4162) (#4206) The drift mapper special-cased only `packages/plugins/*`, so the 30 packages nested under the other six container dirs collapsed into their container — whose missing package.json killed the npm-name matching arm. A doc naming `@objectstack/service-automation` but never the repo path was a guaranteed miss (`automation/flows.mdx` on a service-automation- only change). The root is now the deepest ancestor with a package.json, with no hardcoded container list to drift out of sync. Also from the issue thread: `<packageRoot>/scripts/**` is build tooling, not documented behaviour, and no longer counts as an implementation change (#4183); `package.json` and `src/scripts/**` stay counted. Both the derivation and the classifiers are now pinned by `--self-test`. Claude-Session: https://claude.ai/code/session_01J2x8Tie9WT1VgWFifqMKrR Co-authored-by: Claude <noreply@anthropic.com>
os-zhuang
pushed a commit
that referenced
this pull request
Jul 30, 2026
Two conflicts, both in code #4165 ("reject unknown keys across the app shell and navigation tree") rewrote underneath this branch: - `packages/spec/src/ui/app.zod.ts` — main turned the nav union into a `z.discriminatedUnion('type', …)` with `.strict()` members and kept the `z.ZodType<any>` annotation; this branch replaced that annotation with the real `NavigationItem`. Both are kept. Reconciling them needed one addition: main widens the member array (`as unknown as readonly [ZodObject<ZodRawShape>, …]`) because the members are lazySchema Proxies and a superRefine-wrapped variant, and `discriminatedUnion` then reports the union's output as `Record<string, unknown>` — which fits `z.ZodType<any>` and nothing sharper. The union therefore takes a matching cast back to `z.ZodType<NavigationItem>`, documented at the declaration: every branch of the type is still `z.infer<typeof XNavItemSchema>`, so only the MEMBERSHIP of the list is unchecked, and app.test.ts parses all nine variants. - `AGENTS.md` — main added the `check:generated` aggregate section; this branch added `check:exported-any` to the "pure checks with no generator" list. Both are kept. Also registers `check:exported-any` in `check-generated.ts`'s ledger, which main added and which reconciles itself against package.json on every run. Registers `check:variant-docs` in the same ledger while here: #4177 added the script and #4183 added the reconciliation, neither PR could see the other, so `check:generated` was already failing on `main` itself. One line, in a file this branch was editing anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XY3swmCsdYx6AMGiRmFt1H
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
问题
packages/spec有八个签入的生成产物,各有闸门,分在两个 CI job 里而且顺序执行。于是第一个过期的产物会挡住后面全部:你修好它、推上去、下一轮才知道还有一个。实际代价:#4040 两次(
check:docs→check:api-surface),#4161 又两次(check:spec-changes→check:upgrade-guide)。四个来回,换来的是一次本地运行就能一次说完的信息。做法
--fix是刻意做窄的不是「一键全量重生成」。全量重生成会毁掉信号 —— 它会改写那些你根本没注意到已经过期的产物,于是一个真实的语义变更就悄悄躲进了一堆机械 diff 里。先让检查告诉你哪些过期,再只重生成那些。
账本每次运行都对账,而不是藏在
--self-test后面gate → generator 的账本会在每次运行时与
package.json双向对账。一个没被归类的新check:/gen:脚本会让本次运行失败,而不是静默地退出覆盖范围 —— 否则汇总仍会说「全部产物都是最新的」,同时实际检查得更少,而这正是这个脚本要消除的那类谎言。它当场证明了自己:第一次运行就拒绝了它自己的
package.json注册项(未归类),一个产物都还没检查。两件它拒绝沉默的事
check:api-surface的过期-dist 陷阱:它读的是构建产物.d.ts,所以未构建的树会把新增导出报成「breaking removal」。现在当这个闸门失败时会就地打印这条说明,而不是让下一位读者去追幽灵。写这个 PR 时它就当场救了我一次 —— 干净树上报AuthoringKeySurface/FIELD_KEY_GUIDANCE被删除,实为我并入 main 后没重建。check:liveness/check:empty-state/check:react-conformance/check:skill-examples,都是无产物的源码审计)会被列出来,所以「全部最新」永远不会被读成「全部通过」。顺带曝出的一个既有缺口
gen:openapi和gen:sbom没有任何闸门 —— 它们产出的东西无人验证是否最新。脚本每次运行都会报这一条,而不是装作不存在。验证
两个方向都做实了:
rationale注入一处改动check:upgrade-guide失败,--fix只重生成了docs/protocol-upgrade-guide.md,其余七个未被触碰(git status确认)check:api-surface失败并打印 dist 陷阱说明;构建后消失这轮验证还纠正了我自己先前的一个误读:我原以为改
rationale会让两个 ADR-0087 闸门一起失败。实测只有check:upgrade-guide失败 —— 两者输入不同:转换注册表驱动spec-changes.json,rationale 散文驱动protocol-upgrade-guide.md。它们在 #4161 上「一起失败」,是因为那个 PR 恰好两样都改了。另一条实测:
spec-changes.json只覆盖 major 11–17(到当前PROTOCOL_MAJOR为止),所以 step 18 的 rationale 根本不进生成物 —— 第一次负控就是因为改了 step18 而没咬,这才查出来。根级 8 道闸门、
tsc --noEmit(CI 的 typecheck 门禁)均通过。AGENTS.md
那段手写的
for循环被这条命令取代。它硬编码了八个闸门名 —— 第九个产物出现时会静默漏掉,正是 #3786 排查的那个「手抄清单没有闸门」形状。脚本的账本不会漂,因为它对着package.json对账。🤖 Generated with Claude Code
https://claude.ai/code/session_01QoA8AV99Ss1RRkLLAYmDzq
Generated by Claude Code