Uh oh!
There was an error while loading. Please reload this page.
fix(cli): carry the four structural advisories in os validate --json - #11175
Conversation
#10953) The JSON payload was emitted and the command returned above the block that computes the no-objects / no-apps+plugins / missing-manifest.id / missing-manifest.namespace advisories, so all four were printed for a human and structurally unreachable for `--json` — the one audience the flag exists for. Compute them once above the `if (flags.json)` branch and let both faces consume that list, the move this file already made for `unknownKeyWarnings`. Text-face warning order is unchanged. Pinned by an equivalence test asserting both faces carry the same warning set for the same config, with each side derived from its own real output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📓 Docs Drift CheckNothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)), so this run has no opinion about the docs. What this run could not see
Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
os-elon
commented
Aug 23, 2026
复核记录 —— 等价 pin 达标,且堵上了一个我在派发里没想到的漏洞⛔ 尚未入队:CI 仍在跑。绿了我再逐名复核并入队。以下与 CI 无关,先记。 1. ⭐⭐ 反空洞下限 —— 这一条是我漏掉的我在派发里压的是「pin 必须断言两个面的警告集相等,且两边从产生源派生」。你照做了,然后指出了那条要求自身的洞:
⭐ 「两边都空 ⇒ 相等」正是等价 pin 的经典失效方式,而我的要求原样执行会正好落进去。下限不是期望列表(那会退回硬编码),而是一个非空性约束 —— 这个区分是对的。 2. ⭐⭐ 声明的范围断言 —— 第二个我没想到的点文本面把另外两条 advisory 流( 而关键在你接下来做的事:每个 fixture 都断言这两者为空,于是「精确集合相等」对它自己的范围是诚实的,若这一点不再成立会大声失败,而不是悄悄在一个被缩小的范围上成立。⛔ 不这么做的话,这条 pin 会在某天变成一个自以为在比较全集、实则只比较了子集的绿灯。 3. 双射的构造,以及用「包含」而非「相等」的理由「每个 JSON message 消耗一个不同的文本行,然后两边剩余都必须为空」—— ⭐ 消耗一个不同的行才使它成为双射,而不是互相覆盖检查。这个细节决定了重复项与遗漏项都会被抓到。 用包含比较也给了理由而非偷懒:结构化 finding 把 ⭐ 而且「既不是裸字符串也不是带 4. ⭐ 腿 4 才是验收标准的承重腿删掉本 PR 从未触碰的 ⭐ 而你给的界限同样重要且准确:config 驱动的 pin 只能抓到某个 fixture 实际触发的分歧,一个两个 fixture 都不激发的未来 advisory 会逃过它;真正结构性的是修复本身 —— 放进 5. Clause-② 的 JSON 形状判断 —— 分裂且有先例,正是我要的我说了「⛔ 不许默认它不触及契约面」,你给的是分裂判断:声明的形状未变( ⭐ 并且引用了仓库自己的先例:同一数组里缺失的 readonly flow-write 警告, 6. before 实测里那个 fixture 的构造值得记
⭐ 这解释了为什么不能"随便造个缺 id 的 config",也是四 vs 零、输入逐字节相同这个对比能成立的前提。混合 fixture(文本 2 / JSON 1)则单独证明了丢的是 structural 那条而非全部。 7. 与 #10952 的边界 —— 确认了而不是假定我要求你自己确认零态 section 行不会进入被比较的集合。你不但读了机制(section 行不带 warning glyph、从不进入任何 (#10952 的 PR #11173 刚已入队,与你零文件重叠。) 8. #11174 的分量比它的措辞重
9. 一处诚实的更正早期一次单文件测试调用传了 repo-root 相对路径而 vitest 的 cwd 在 Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10953
--jsonexists so CI can gate on the advisoriesos validatecomputes. Four of them could never reach it: the JSON payload was emitted and the commandreturned above the block that computes them, so they were printed for a human and structurally unreachable for the machine — the documented purpose of the flag defeated for that whole family.Measured before the fix
Same config both ways (no
manifest, no objects, no apps — the only shape that trips all four conditions in one run, sincemanifest.idis schema-required once amanifestis present, so merely omitting the id fails the parse long before any warning is computed):os validate(text)os validate --json⚠ No objects defined — this stack has no data model"warnings": []⚠ No apps or plugins defined — this stack may not do much⚠ Missing manifest.id — required for deployment⚠ Missing manifest.namespace — required for multi-app hostingFour versus zero, byte-identical input.
The fix
The shape the card prescribes, and the one this file already used for
unknownKeyWarnings: the four conditions compute once into astructuralWarningslist above theif (flags.json)branch, and both faces consume that one list. A single list cannot drift from itself.The text face appends it in the exact position the four inline
ifblocks occupied, so text-face warning order is unchanged.Does this touch the contract surface?
Judged rather than assumed, and the answer is split:
warningswas already heterogeneous — registry and package-doc findings ride as objects, unknown-key advisories as strings (formatUnknownAuthoringKeyreturnsstring). The four arriving as strings introduce no new element type and no new key, so no consumer's parse breaks.warnings.length === 0now sees these four where it previously saw an empty array. That is the defect being corrected, not a new signal — which is why this is apatch, matching the bump this repo used for the previous fix of exactly this class (readonly flow-write warnings missing from this same array).The pin, and why it is an equivalence pin
packages/cli/test/validate-json-warning-parity.e2e.test.tsasserts the two faces carry the same warning set for the same config, so the class stays closed rather than just these four instances. Both sides are derived from their own production source — the real text stdout and the real JSON payload of two real CLI runs. Nothing in the file hardcodes an expected message; a transcribed list would pin the transcription, not the command.Details that make it load-bearing:
whereas its own field while the text face renders${where}: ${message}; comparing on the message avoids re-implementing that join and pinning the formatting instead of the parity).⚠block that the payload carries as its own top-level fields —conversionsandspecVersionGap. That is a difference in shape, not a drop. Every fixture asserts both are empty, so the exact set equality is honest about its scope and fails loudly if that ever stops holding.Reverse verification
Each leg predicted before running; every prediction held.
Tests 3 passed (3)structuralWarnings)2 failed | 1 passed— exactly those twostructuralWarnings.slice(1))2 failed | 1 passed;textOnly: ["Missing manifest.namespace — required for multi-app hosting"]ruleAdvisories) from the payload — a warning family that is not one of the four1 failed | 2 passed;textOnly: ["object 'parity_ticket': sets \externalSharingModel` …"]`Tests 3 passed (3), working tree byte-clean vs the commitThe third leg is the acceptance criterion's own test — a warning returned to text-only must go red. The fourth shows the pin closes the class: it catches a text/JSON divergence in a family this PR never touched.
Boundary with #10952
That card is in flight on
packages/cli/src/utils/format.ts(zeroFallback, which adds zero-state section rows such asLogic: 0 …). No overlap: this PR's landing zone ispackages/cli/src/commands/validate.tsplus a new test. Confirmed rather than assumed — section rows are a different output element from a non-blocking warning, carry no⚠, and never enter eitherwarningsarray, so they cannot enter this comparison. The fixtures here already print such rows (UI: 0 Apps) while the warning sets stay exactly equal. #10952 is not addressed here.Out-of-scope finding
A second, distinct divergence in this file, filed unassigned as #11174 and deliberately left untouched:
os validate --json --strictexits 0 on a config the text-mode--strictexits 1 for, because the onlyflags.strictreader also sits below thereturn. That one is exit-code semantics rather than advisory reachability, and an exit code is a declared contract — a decision, not a mechanical repair.Verification
Full
packages/clisuite: 159 files / 1737 tests passed.pnpm --filter @objectstack/cli typecheckclean.Gate families derived with
node scripts/pm/dispatch-gates.mjs(no path arguments — the script derives its own change set), all run against final head037b0faand all green, quoting each gate's own verdict line:check-nul-bytes: OK (scanned 6476 text file(s) … no raw ASCII control bytes).check-cross-package-test-inputs—OK: 13 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.check-test-source-alias OK — 72 packages with tests scanned; 61 registered …check-type-check-coverage: OK — 65/78 workspace packages type-checked …check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 213.1s, 1895 raw tsc error(s) total, none above its recorded number.(the full package closure was built first, so this measured rather than refusing)check-engine-double-contract: OK — 383 pinned, 133 in the DEBT ledger, 2 exempt.check:changeset-gate-self-tests,check:objectui-changeset,check:slot-lookup,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-ci-filter-parity,check-empty-changeset,check-plugin-teardown-shape,check-affected-docs,check:query-options-erasure,check:where-matcher— all exit 0.No gate was weakened, no threshold moved, no baseline raised, no ledger touched.
CI is still in flight — nothing here waits on it.
Generated by Claude Code
Generated by Claude Code