fix(cli): every author-time rule that can gate runs on all three commands (#4409) - #4445
Merged
Merged
Conversation
…ands (#4409) `os validate`, `os build` and `os lint` each hand-wired their own subset of the author-time rules. Nothing connected the three lists, so "which rules run here?" was answerable only by diffing three 800-line files by eye — and the answer drifted every time a rule landed. The audit found 23 of 26 rules running on some strict subset, nine of them able to emit `error`. The worst direction was the least obvious: `os build` — the command that PUBLISHES — was the weakest gate of the three. A flow whose expression approver does not parse (`approval-expression-invalid`) built and published green; only `os lint` stopped it, and CI usually runs the other two. This is the same failure mode's fifth appearance (#3583, #3782, #4384/#4394, #4402). Each earlier repair removed an instance and left the MODE: a rule's command coverage was whatever its author remembered to type, and forgetting was silent. #4402's guard could not catch the rest — it filtered on the current member names of one suite, so a rule hand-wired into two commands from outside that suite passed it without a word. Replace remembering with a table: - `packages/cli/src/lint/authoring-rules.ts` declares all 26 rules as data — tier (gating/advisory), which stack tier they read (pre-parse vs parsed), which commands run them, and a written reason for the one narrowing. All three commands consume it through `runAuthoringRules()`; the three command files shrink by ~1000 lines between them. - `authoring-rule-wiring.test.ts` upgrades the guard from a name list to a ratchet: a gating rule on fewer than three commands fails, a narrowed rule with no reason fails, a command that calls a rule directly fails, and an `advisory` claim is checked against the rule's own source so a gate cannot wear an advisory label to buy partial coverage. Remaining direct calls are listed with reasons in `DIRECT_CALL_RATCHET` / `LINT_IMPORT_RATCHET`. - `authoring-rule-command-parity.test.ts` proves the verdict, not just the wiring: one case per previously-blind gating rule, plus the issue's own repro driven through the real CLI — exit 1 on all three commands where it was 1/0/0 before. Two things fall out of one report per run rather than exiting at the first failing gate: an author with three unrelated problems sees all three, and `--strict` now covers every advisory instead of the roughly half that happened to be printed inline. Also closes the same hole one gate over: `collectAndLintDocs` failed `os build` and never ran on `os validate`, invisible because the parity guard keyed on the `lint*`/`validate*` naming convention and that gate is called `collect*`. The guard now names each shared non-registry gate explicitly. Cost is not what argued against this: the heavy deps (typescript, sucrase) are already lazy, and the heaviest rule of the set has run on all three commands as a suite member since #4340 without anyone noticing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz61GE1CWCSnGW4qShSEXs
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 1 package(s): 20 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz61GE1CWCSnGW4qShSEXs
…4409) The role-word ratchet (ADR-0090 D3) counts occurrences per file, and two doc lines describing `validateSemanticRoles` reintroduced the banned word for a non-permission concept. Name what the rule actually checks instead: `stageField` / `highlightFields` pointers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sz61GE1CWCSnGW4qShSEXs
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.
Closes #4409.
问题
os validate/os build/os lint各自手工接线自己的规则子集。三份 import 列表之间没有任何东西相连,"这个命令跑哪些规则"只能靠肉眼 diff 三个 800 行的文件——每落一条规则答案就漂一次。26 条规则里 23 条只跑部分命令,其中 9 条能报error。最危险的方向不是最显眼的那个:
os build(负责发布的那个命令)是三个门里最弱的。一个审批人表达式解析不了的流(approval-expression-invalid)构建、发布一路绿灯,只有os lint拦得住,而 CI 通常跑的恰恰是另外两个。这是同一失败模式的第五次出现(#3583、#3782、#4384/#4394、#4402)。每次都修掉"那一例",留下模式:一条规则的命令覆盖 = 作者记得敲什么,忘了就是静默的。#4402 的守卫拦不住剩下的 23 条——它只过滤某一个 suite 的现有成员名,suite 之外被手工接进两个命令的规则,它一声不吭。
改成什么
把"记得"换成一张表。
packages/cli/src/lint/authoring-rules.ts— 26 条规则全部登记为数据:tier(gating / advisory)、读哪一层 stack(pre-parsenormalizedvsparsed)、跑哪些命令、以及唯一一处收窄的书面理由。三个命令都通过runAuthoringRules()消费,三个命令文件合计缩了约 1000 行。守卫从"点名单条"升级为棘轮(
authoring-rule-wiring.test.ts,取代reference-integrity-wiring.test.ts):GATING_COVERAGE_DEBT今天是空的,这是健康状态)advisory声明会回读规则自己的源码核对——否则把 gate 标成 advisory 就能白嫖部分覆盖,而 The #1888 user-less fail-open is wider than the lint that guards it — record-change flows fired by a system write run UNSCOPED, unlinted #3760 恰好干过"把一条 flow 规则从 advisory 提成 gating、没人问覆盖要不要跟上"DIRECT_CALL_RATCHET/LINT_IMPORT_RATCHET,每项带理由;棘轮条目失效会被检出,避免烂成一张永久通行证行为验证(
authoring-rule-command-parity.test.ts)——证明的是判决,不只是接线。一个命令可以跑了规则却不按它的结论 gate,#3782 那四条就是这么"接上了"的。每条原先有盲区的 gating 规则一个用例,外加 issue 自己的 repro 用真实 CLI 进程跑。实测
issue 里的复现(app-todo 植入 CEL 坏掉的 expression approver):
os lintos validateos build三个示例 app(todo / crm / showcase)× 三个命令仍全绿;告警量基本不变(lint 122→124,validate 4→4,build 2→5,新增的正是 build 原先瞎掉的那些)。
顺带修的同类洞
collectAndLintDocs会让os build失败却从不在os validate跑——同一类"build 拒绝、validate 放行",只是方向相反。它逃过旧守卫是因为旧守卫按lint*/validate*命名约定匹配,而它叫collect*。现在守卫逐个点名共享的非 registry gate,而不是靠模式匹配去撞。两点行为变化
--strict现在覆盖所有 advisory,而不是原先恰好被 inline 打印的那一半。os lint现在会因 gating 规则退出 1——这正是"预检不撒谎"的定义。关于成本
成本从来不是维持现状的理由:typescript(~9MB)和 sucrase 已经是惰性加载(
@objectstack/lint的lazy-deps.test.ts钉住了这个契约),而这组里最重的validateReactPageProps自 #4340 起就以 suite 成员身份跑在三个命令上,没人察觉过开销。所以 gating 层没有一条需要按成本豁免。唯一收窄的lintUniqueDeclarations理由是os lint已经通过lintDataModel报同一条规则,登记进去会重复报告——这是记录下来的覆盖,不是缺失的覆盖。验证
@objectstack/lint729 tests 全过tsc --noEmit、eslint --no-inline-config干净文档
content/docs/deployment/validating-metadata.mdx的"one gate, two entry points"改成三入口的完整矩阵;cli.mdx里os lint「beyond validate's hard gates」的说法此前是不准确的,已改为如实描述。Generated by Claude Code