feat(spec): 让 #4001 严格性账本接受机器校验(首次运行抓到 11 处漂移) - #4232
Merged
Conversation
The ledger is the campaign's map — which z.object sites are authorable (the .strict() ratchet target), which are wire, which are deliberately open. Every step reads it to pick the next move, and nothing kept it honest. It went stale twice in one week: a classification verification disproved (hook.zod.ts carried a blanket `authorable (p)` that covered a runtime shape, #4207), and a "next step" that had shipped months earlier (the unknown-key warning layer, #4218), which sent the next reader off to rebuild something already in production. A map that drifts is worse than no map, because it gets followed. WHAT THE GATE CHECKS. Only what is mechanically checkable: - Site counts. The ledger states its own method — "z.object( occurrences per file" — so every number in it is verifiable. A stale count means schemas were added or removed under a Class verdict nobody re-examined. This is the ratchet: touching a file forces you back through the ledger. - Coverage. Every *.zod.ts with sites in a triaged directory must have a row. - Section totals sum, and a row claiming "strict as of" names a file that really contains .strict(). The Class column is deliberately NOT checked. Authorable vs wire vs open is a judgement about who writes the input, and this campaign's rule is verify-before-tightening — encoding it would automate exactly the call that must stay human. The gate protects the arithmetic and the coverage so that judgement is always made against current code. ZERO-SITE FILES ARE SKIPPED. Five of the six files the first run flagged as undeclared (date-macros, context-tokens, feed, action-params, time-relative-trigger) hold no z.object at all — they are enum/token modules, and the ledger classifies sites. Requiring rows for them would be noise. Not a hole: the day such a file grows its first z.object( it becomes undeclared and the gate says so. FIRST RUN: 11 DRIFTS, in a file being actively edited by the campaign that owns it. Six counts had moved (ui/app.zod.ts 11 → 18, ui/touch.zod.ts 4 → 7, ui/action.zod.ts 8 → 9, ui/view.zod.ts 51 → 50, ui/responsive.zod.ts 6 → 4, automation/flow.zod.ts 12 → 11), two section totals no longer summed, and automation/io-node-config.zod.ts had no row (now classified: the sibling contracts validating the open `config` slot on flow notify/http nodes). The app.zod.ts gap was self-inflicted — the app step (#4165) added seven schemas to that file and updated the row's prose without touching its count. That is the argument for this gate in one sentence: the people most familiar with this ledger, editing it the same week, still left eleven drifts in it. Verified the gate bites rather than just passes: a moved count fails (plus its section total), a deleted row resurfaces as an undeclared file, and stripping .strict() from approval.zod.ts fails its "strict as of" claim. Restored, green. check:docs green and content/docs/references unchanged (the #4207 lesson). Refs #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
|
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:
|
…ripping-strictness-h3a3zt
The ledger gate did its job before it merged. `main` landed automation/builtin-node-config.zod.ts (#4045/#4228) — eight new sites, sibling to io-node-config.zod.ts — and merging main turned the gate red on a branch whose own diff touches no schema at all. That is the intended behaviour: a new authorable surface arrived, so somebody has to classify it rather than let it sit in the ratchet's blind spot. Classified `authorable`, same family as io-node-config: the CRUD quartet, `screen` and `map` config contracts. Worth noting on the row — these were written from what the executors actually read rather than from the descriptors' configSchema literals, and `builtin-node-form-zod-ledger.test.ts` reconciles the two bidirectionally, so this row already carries a drift check of its own. Every pre-existing count survived the merge unchanged, so the failure was exactly as narrow as it should have been: one new file, one new row, one section total (automation 81 → 89). Verified after merging: spec suite 7163 green, tsc clean, and the strictness-ledger / liveness / variant-docs / docs gates all green with content/docs/references unchanged. Refs #4001 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
os-zhuang
marked this pull request as ready for review
July 31, 2026 00:27
This was referenced Jul 31, 2026
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…d omission fails instead of shipping (#3786) (#4252) A recursive Zod schema carries a hand-written `z.ZodType<...>` annotation. `z.ZodType` takes <Output, Input> and Input DEFAULTS TO `unknown`, so naming only the first compiles, validates correctly at runtime, and silently un-types every authoring path through the schema. This package has made that mistake twice. #4171 fixed the output half of the nav annotation, and check-exported-any.ts — built to hold that fix — reads output only, so it reported green over the half still broken. #4221 found the consequence (`defineApp` compiled `navigation: [{ totally: 'made up' }, 42, 'nonsense']`) and #4227 named both parameters on the six remaining recursive schemas. Both fixes are correct; neither was pinned. #4227 declined a .d.ts scanner for a good reason — separating a deliberate single-parameter `z.ZodType<T>` (contracts/llm-adapter.ts takes a caller-supplied schema) from an omission needs heuristics on emitted type names, against check-exported-any.ts's zero-false-positive rule — and nominated #4221's assertion pattern instead. This applies it to the eight schemas #4221 did not cover: Query, JoinNode, FieldNode, FilterCondition, NormalizedFilter, StateNode, ValidationRule, FormField. Each gets a positive probe (the authoring shape still compiles, guarding an input type drawn too tight) and a negative probe reached through `z.input<typeof Schema>` — the way a consumer gets there, so the pin covers the WIRING and not just the type alias. Dropping a type parameter turns the suppression unused and tsc fails by name. Mutation-tested in both shapes a regression can take: - QuerySchema back to one parameter → the pin fires AND JoinNodeSchema cascades, since JoinNodeInput.subquery is QueryInput. - StateNodeSchema back to one parameter → ONLY the pin fires. Nothing else references its input, so without this file that regression is silent. No runtime change and no new public export; the module is referenced by no tsup entry and re-exported by no barrel. Also classifies check:strictness-ledger in check-generated.ts's ledger — it landed in #4232 with no entry, so check:generated was failing on main itself, the same cross-PR race the check:variant-docs entry above it documents.
os-zhuang
added a commit
that referenced
this pull request
Jul 31, 2026
…4255) (#4257) The aggregate reconciles its GATED/NO_GENERATOR ledger against package.json on every run, in both directions — but only where the aggregate runs, which was locally. CI runs the gates as individual steps, so a PR adding a check:/gen: script without classifying it kept every CI gate green while the wrapper AGENTS.md prescribes exited red on main at the reconcile stage, running zero gates. Twice in three days. #4177 added check:variant-docs and main stayed red for local wrappers until #4194 collided with the same wall (#4203). Then #4232 added check:strictness-ledger — found unclassified while wiring this fix, so the wrapper was red on main again at the moment of writing. Classified here as NO_GENERATOR: it audits the hand-written strictness ledger (docs/audits/) against the code it describes; there is no generator to name. The gate is a --reconcile-only mode (reconcile, report, exit — no gates; reads package.json only, no build, sub-second) wired into lint.yml's "TypeScript Type Check" job. Deliberately not ci.yml's check-generated job: its `generated` paths filter does not watch packages/spec/package.json — the one file every offending PR must touch — and both offenders' changed files missed every entry in that filter, so both skipped the job entirely. The typecheck job is unfiltered and required, so the meta-gate cannot go dormant — the reasoning that already placed check:docs, check:skill-refs and check:react-blocks there. Verified: --reconcile-only exits 0 on this tree (16 check: + 10 gen:, all classified) after catching #4232's script pre-classification; an injected unclassified script and a deleted ledger-named script both fail with the existing diagnostics; the default mode still reconciles and then runs the gates. AGENTS.md's wrapper section now states the CI backstop. Closes #4255 Refs #4203, #4232 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QCrKazC5o5a77yXd7ykaZg
This was referenced Jul 31, 2026
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.
账本是这场战役的地图——哪些
z.object站点是 authorable(.strict()棘轮的目标)、哪些是 wire、哪些刻意保持开放。每一步都靠读它来决定下一步做什么,而没有任何东西保证它是真的。它在一周内失效了两次:
hook.zod.ts上笼统的authorable (p)被验证推翻——同文件的HookContextSchema是运行时形状(feat(spec)!: hook 与 datasource 拒绝未知键(#4001 data 步,收尾最后两个待验证分类) #4207)会漂移的地图比没有地图更糟,因为它会被照着走。
闸门校验什么
只校验机器能判定的部分:
z.object(occurrences per file」),所以每个数字都可验证。计数对不上,意味着有人在某个没人重新审视过的Class判定下增删了 schema。这就是棘轮:动了文件就必须回到账本。*.zod.ts都必须有一行。.strict()的文件。刻意不校验
Class列authorable / wire / open 是关于「谁写这个 schema 的输入」的人类判断,而这场战役的规则是 verify-before-tightening。把它编码进闸门,等于自动化掉那个恰恰必须由人做的判断。
闸门守住算术和覆盖,好让那个判断永远是对着当前代码做出的。
0 站点文件跳过
首次运行标为「未申报」的 6 个文件里,有 5 个根本没有
z.object(date-macros、context-tokens、feed、action-params、time-relative-trigger都是枚举/token 模块)。账本分类的是站点,给它们加行只是噪音。这不是漏洞:这类文件长出第一个
z.object(的那天,它就变成未申报文件,闸门会立刻说话。首次运行:11 处漂移
而且是在一个正被拥有它的战役主动编辑的文件里:
ui/app.zod.ts11→18、ui/touch.zod.ts4→7、ui/action.zod.ts8→9、ui/view.zod.ts51→50、ui/responsive.zod.ts6→4、automation/flow.zod.ts12→11ui/192→197、data/163→154automation/io-node-config.zod.ts(2 站点),已分类app.zod.ts那处是我自己造成的:app 步(#4165)往那个文件加了七个 schema,更新了该行的叙述文字,却没动它的计数。一句话概括这道闸门的理由:最熟悉这份账本、并且在同一周里编辑过它的人,仍然在里面留下了十一处漂移。
验证它会咬人,而不只是会通过
approval.zod.ts剥掉.strict()接入与影响
pnpm --filter @objectstack/spec check:strictness-ledgerdocs/audits/**加进触发路径——账本本身是文档,改它同样能打破这道闸门check:docs绿,content/docs/references零变动(feat(spec)!: hook 与 datasource 拒绝未知键(#4001 data 步,收尾最后两个待验证分类) #4207 的教训)Refs #4001
🤖 Generated with Claude Code
https://claude.ai/code/session_0147tNF4Snk7Ry1KGt4a5PY4
Generated by Claude Code