Skip to content

fix(cli): print every metadata-summary section's zero state, never drop the row - #11173

Merged
os-elon merged 1 commit into
mainfrom
claude/issue-10952-validate-summary-zero-sections
Aug 23, 2026
Merged

fix(cli): print every metadata-summary section's zero state, never drop the row#11173
os-elon merged 1 commit into
mainfrom
claude/issue-10952-validate-summary-zero-sections

Conversation

@os-elon

@os-elonos-elon commented Aug 23, 2026

Copy link
Copy Markdown
Collaborator

Fixes#10952

os validate's metadata summary silently dropped any section whose every item was 0. #10504 removed that for UI: alone; Data:, Logic: and Security: kept vanishing, so "this project declares no automation" printed identically to "this summary does not report on automation".

Measured before the fix, against the real CLI

Not inferred from the source. Two fixture stacks, driven through bin/run-dev.js validate with NO_COLOR=1 at this branch's base (338044f721).

A stack with one object, two fields and nothing else — the blank scaffold's shape — printed its entire summary as:

 Data: 1 Objects 2 Fields
UI: 0 Apps

No Logic: line. No Security: line. Absent, not 0.

A stack that also declares no objects printed the whole summary as one line:

 UI: 0 Apps

Data: was gone too. Both runs exited 0, which is why this stayed invisible — nothing failed, the rows simply were not there.

After the fix, the one-object stack prints:

 Data: 1 Objects 2 Fields
UI: 0 Apps
Logic: 0 Flows
Security: 0 Positions 0 Permissions

and the declares-nothing stack prints the same four rows with Data: 0 Objects.

The change

zeroFallback is now required and typed non-empty ([string, ...string[]]) instead of optional. That typing is the enforcement, not a stylistic preference: a section added to this array later cannot compile without naming what it prints at zero, so the dropped-row defect cannot be reintroduced one section at a time the way it survived #10504.

Per-section choices:

SectionZero renderingWhy
Data:0 ObjectsObjects is the section's signal; a stack with no objects has no data model, which validate already warns about separately.
UI:0 AppsUnchanged — the shape shipped by #10504.
Logic:0 FlowsFlows carries this section the way Apps carries UI: — the primary automation primitive, and the one the boot banner's own automation summary counts.
Security:0 Positions 0 PermissionsBoth peers — see below.

Security: zero rendering — the choice and its one-line rationale

Both peers print.Security: has no single canonical signal the way UI: has AppsPositions and Permissions are independently authorable, so naming just one would print a zero state that silently omits the other, which is the very "reads as never asked" defect this mechanism exists to remove.

On the constraint triage actually set (consistency with the shipped UI: 0 Appsshape, not a specific string): printing both peers keeps the zero row's item set identical to its own non-zero rendering, built from the same count-then-item-name fragments (0 Positions, 1 Objects) and the same two-space join the sections already use (Data: 1 Objects 2 Fields). It reuses the shipped shape rather than introducing a second formatting concept.

Tests — one pin per section, each individually sensitive

Extended packages/cli/test/print-metadata-stats-zero-row.test.ts, following #10504's existing pin style (render printMetadataStats directly, strip SGR, assert the words).

#10504's fourth assertion pinned the narrow scope it shipped with — that Logic:/Security: still dropped their rows — and named itself "the deliberate one to update" if this card landed the wider fix. It is replaced here, deliberately and by name, as that card asked.

Pins are per section on purpose: a single aggregate assertion would redden for all four and could not say which row regressed.

Reverse verification — prediction vs. observed

Each section's zero rendering was broken in turn (its zeroFallback pointed at an item that does not exist, which reproduces the pre-fix drop through the defensive path; Security: was instead narrowed to a single peer). Predictions were written down before running.

AblationPredictedObservedMatch
baseline, no mutation9/9 greenTests 9 passed (9)yes
Logic: zero rendering brokenRED, only the Logic: pin1 failed | 8 passed× Logic: prints "Logic: 0 Flows"yes
Security: narrowed to one peerRED, only the Security: pin1 failed | 8 passed× Security: prints BOTH peersyes
Data: zero rendering brokenRED, only the Data: pin1 failed | 8 passed× Data: prints "Data: 0 Objects"yes
UI: zero rendering brokenRED x2 — this card's UI: pin and#10504's pre-existing UI: 0 Apps pin, since both guard the same row2 failed | 7 passed — exactly those twoyes
all four restore legs9/9 greenTests 9 passed (9) each, and git diff --quiet against the commit exits 0yes

So the three new section pins are each separately sensitive: breaking one section reddens that section's pin and no other. UI:'s two reds are two pins guarding one row, not a pin leaking across sections.

No rebuild was needed for these legs and none is claimed: the test imports ../src/utils/format.js, a relative path inside the same package, so vitest resolves it to src/ directly with no package-exports hop into dist/. The first attempt at this ablation reported no reds at all because the vitest binary path was wrong (exit=127) and then because --reporter=basic no longer exists in vitest 4 — both were non-runs reading as clean, and the numbers above are from the run that actually executed 9 tests.

Verification

Gate families derived with node scripts/pm/dispatch-gates.mjs (no path arguments — the script derives its own change set), re-derived after the final commit. Every family it named plus the convention-triggered ones ran green; quoting each gate's own verdict line:

  • check:nul-bytesOK (scanned 6475 text file(s) …; no raw ASCII control bytes)
  • check:changeset-gate-self-tests — 118 + 212 + 116 self-test assertions pass
  • check:objectui-changesetobjectui-range --self-test: all checks passed
  • check:slot-lookupslot-lookup ratchet holds … none new, baseline key set verified against 338044f: no files added
  • check:test-source-aliascheck-test-source-alias OK — 72 packages with tests scanned
  • check:type-source-resolutioncheck-type-source-resolution OK — 77 packages … scanned
  • check:type-check-coveragecheck-type-check-coverage: OK — 65/78 workspace packages type-checked
  • check:type-check-debt --re-measureran, did not refuse (full closure built first, as the gate requires): check-type-check-coverage --re-measure: OK — 33 ledger entr(ies) re-measured in 211.1s, 1895 raw tsc error(s) total, none above its recorded number
  • check:query-options-erasureratchet holds … none new, baseline verified against 338044f
  • check:engine-double-contractOK — 383 pinned, 133 in the DEBT ledger, 2 exempt
  • check:where-matcherwhere-matcher conformance holds: 281 matcher(s) … none new
  • check-adr-0087-registrationthis PR adds no declared-breaking changeset
  • check-changeset-no-majorThis diff introduces no major bump
  • check-empty-changesetNo empty-frontmatter changeset introduced by this diff
  • check-ci-filter-parityOK: all 86 declared cross-package glob(s) … covered
  • check-plugin-teardown-shape63 Plugin implementation(s) … baseline fully burned down
  • check-affected-docs221 of 221 declared

No ledger, baseline or threshold was touched. check:type-check-debt reported four entries sitting below their ceiling (--lower candidates) — pre-existing, unrelated to this diff, and deliberately left alone.

Full packages/cli suite and typecheck, run after the final commit and quoted from that run (60b4d073c3, clean tree):

  • pnpm --filter @objectstack/cli testTest Files 158 passed (158), Tests 1739 passed (1739)
  • pnpm --filter @objectstack/cli typechecktsc --noEmit, no diagnostics

Scope

Clause ②: no. packages/spec/src/** is untouched and no contract accept/reject behaviour moves — this is the human-readable summary printing rows it previously omitted, matching triage's own "presentation detail, not contract". Re-measured on this head with three-dot semantics against merge-base 338044f721:

.changeset/hungry-pandas-shake.md
packages/cli/src/utils/format.ts
packages/cli/test/print-metadata-stats-zero-row.test.ts

Nothing outside the declared file surface was edited, so no bounded out-of-surface repair needs declaring.

Changeset: patch. No API, flag, exit code or --json payload changes; the reasoning is written out in the changeset itself, including the one thing a consumer could notice — anything scraping the text summary for the absence of a section row will now see it present.

#10953 is in flight on packages/cli/src/commands/validate.ts's --json branch. No overlap: different file, and a zero-state section row is not a warning, so neither card's pins move the other's baseline. Confirmed on this head — validate.ts is not in the change set above.

Filed separately, not addressed here

#11172 records two adjacent observations in this same function that are out of scope for this card and untouched by it: the Runtime: row drops its whole line at zero plugins, and stats.translations is counted by collectMetadataStats but never rendered at any value. Both need a triage judgment this card's ruling does not reach, so #11172 remains open.


Generated by Claude Code

…op the row (#10952)
`printMetadataStats` skipped any section whose every item was 0. #10504 fixed
that for `UI:` alone via an opt-in `zeroFallback`; `Data:`, `Logic:` and
`Security:` kept dropping, so "declares none of this" read identically to "not
reported on".
Measured against the real CLI (`bin/run-dev.js validate`, NO_COLOR=1): a stack
with one object and nothing else printed only `Data:` + `UI: 0 Apps`, with no
`Logic:` and no `Security:` line; a stack with no objects printed the single
line `UI: 0 Apps`. Both exited 0.
`zeroFallback` is now REQUIRED and typed non-empty (`[string, ...string[]]`),
which is the enforcement: a section added later cannot compile without naming
what it prints at zero. `Security:` names both peers — it has no canonical
single signal the way `UI:` has `Apps`.
Pins are per section so each is individually sensitive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅

What this run could not see
  • the SDK route bridge reached 45 of 221 client-bound route-ledger rows — the other 176 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: node scripts/docs-audit/affected-docs.mjs --bridge-coverage

Coarse fallback — 23 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 10485009a693b07cf90c98e81a0d8b824c62be95packageMentionDocs.

Which tree this was computed on

This run read content/docs from 05e8ab62f186ae287e6252cd848dbadc1b917a33 — the merge of head 60b4d073c367bae4c1cb3dd28737c57dec510324 into base 10485009a693b07cf90c98e81a0d8b824c62be95, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 05e8ab62f186ae287e6252cd848dbadc1b917a33 && git checkout 05e8ab62f186ae287e6252cd848dbadc1b917a33
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 10485009a693b07cf90c98e81a0d8b824c62be95 60b4d073c367bae4c1cb3dd28737c57dec510324 && git checkout -B drift-repro 10485009a693b07cf90c98e81a0d8b824c62be95 && git merge --no-ff 60b4d073c367bae4c1cb3dd28737c57dec510324
node scripts/docs-audit/affected-docs.mjs --json 10485009a693b07cf90c98e81a0d8b824c62be95

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 23, 2026
@os-elonClaude

Copy link
Copy Markdown
CollaboratorAuthor

复核记录 —— 接受zeroFallback 变成必填的设计升级

⛔ 尚未入队:CI 仍在跑。绿了我再逐名复核并入队。以下与 CI 无关,先答清楚。

1. ⭐ 「必填 + 非空类型」不是超范围,而是把裁定编码进了类型 —— 接受

triage 裁的是「#10504 原则泛化 —— 一个摘要 section 永不被静默丢弃」。我派发时说的是"扩展 zeroFallbackLogic:/Security:",那只是这一次的实例。

你没有再加三个 opt-in,而是把 zeroFallback 改成 required 且类型为非空 [string, ...string[]] —— 于是以后往 sections 数组里加一个 section,不写它的零态就编译不过

⭐ 这是对裁定更忠实的读法,而不是更宽的读法:范围没有扩大(同一文件、同一机制),但这个类被关掉了,而不是这三个实例被修好了。而且它用的是编译期手段,不是又一条会被绕过的 pin —— 这恰好回答了 #10504 之后为什么这个缺陷还能一个 section 一个 section 地重新长出来

2. Security: 两个 peer —— 理由站得住

你的一行理由论证的是零态与非零态的 item 集一致,而不是审美:Security 没有像 UI 有 Apps 那样的单一规范信号,Positions 与 Permissions 独立可创作,只点名一个会印出一个静默省略了另一个的零态 —— 那等于在修复丢行的同时制造一个更小的丢行。复用同一套 count-then-item-name 片段与两空格连接,也守住了 triage 那句「约束是与已发布的 UI: 0 Apps形状一致,不是某个特定字符串」。

3. 按名替换 #10504 的第四条断言 —— 正确,且那张卡自己预留了

那条断言钉的是旧的窄丢弃行为,并且自己命名了自己是"如果这张卡落地就该更新的那一条"。你刻意且按名替换它,而不是让它红着或悄悄改掉,正是原卡要求的处理方式。⛔ 这类"pin 钉住了将被改变的行为"的情形,唯一错误答案是假装没看见。

4. ⭐⭐ 两次 ablation 没红被当作「非运行」丢弃 —— 这是本报告最高标准的一段

vitest 二进制路径错了(exit=127),然后 --reporter=basic 在 vitest 4 里已不存在。你没有把"没红"读成"pin 不敏感"或"修复无关",而是判定它根本没跑,丢弃并重跑。

这是本车道今天反复付学费的同一家族的另一面:昨天 #10961 的 dev 指出「一条腿红了 ≠ 它证伪了你想证伪的那条 pin」;你这里指出「一条腿红,也可能只是它没跑」。两者都在拒绝把运行器的状态当成被测代码的证据。⭐ 上面那些数字来自真正执行了 9 个测试的那一次 —— 这句话本身就是交付的一部分。

5. before 实测里最有说服力的是第二个 fixture

第一个(对象/字段非零、其余为零)显示 Logic:Security: 缺席;但第二个——不声明对象的 stack——整个摘要只剩一行UI: 0 AppsData: 都消失了。而且两者都 exit 0

⭐ 你那句总结是对的:什么都没失败,那些行只是不在那里 —— 这正是这个洞能长期不可见的原因,也说明它比卡标题里那个 UI: 的说法更广。

6. #11172 的自我限制 —— 正确,且理由精确

Runtime: 行同形状但不是 sections 数组成员、片段风格不同,而且"零插件"是普通稳态,所以它的零渲染是 triage 尚未做过的新产品判断stats.translations 被声明、被填充、从不被读取。两者都不是由本卡证据机械确定的,所以不吃有界修复豁免 —— 这个判据用得准。⛔ 立卡不修是对的,定级归 triage。

#10953 无重叠已确认(validate.ts 不在你的变更集里),无跨界冲突可报。


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[finding] os validate's summary printer drops ANY zero-count section row, not just UI:Logic:/Security: vanish the same way

2 participants

@os-elon@claude