Uh oh!
There was an error while loading. Please reload this page.
fix(cli): print every metadata-summary section's zero state, never drop the row - #11173
Conversation
…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
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ 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): Which tree this was computed onThis run read A worktree cut from an older # 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 |
os-elon
commented
Aug 23, 2026
复核记录 —— 接受把 |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#10952
os validate's metadata summary silently dropped any section whose every item was0. #10504 removed that forUI:alone;Data:,Logic:andSecurity: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 validatewithNO_COLOR=1at this branch's base (338044f721).A stack with one object, two fields and nothing else — the
blankscaffold's shape — printed its entire summary as:No
Logic:line. NoSecurity:line. Absent, not0.A stack that also declares no objects printed the whole summary as one line:
Data:was gone too. Both runs exited0, which is why this stayed invisible — nothing failed, the rows simply were not there.After the fix, the one-object stack prints:
and the declares-nothing stack prints the same four rows with
Data: 0 Objects.The change
zeroFallbackis 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:
Data:0 ObjectsObjectsis the section's signal; a stack with no objects has no data model, whichvalidatealready warns about separately.UI:0 AppsLogic:0 FlowsFlowscarries this section the wayAppscarriesUI:— the primary automation primitive, and the one the boot banner's own automation summary counts.Security:0 Positions 0 PermissionsSecurity:zero rendering — the choice and its one-line rationaleBoth peers print.
Security:has no single canonical signal the wayUI:hasApps—PositionsandPermissionsare 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 (renderprintMetadataStatsdirectly, 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
zeroFallbackpointed 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.Tests 9 passed (9)Logic:zero rendering brokenLogic:pin1 failed | 8 passed—× Logic: prints "Logic: 0 Flows"Security:narrowed to one peerSecurity:pin1 failed | 8 passed—× Security: prints BOTH peersData:zero rendering brokenData:pin1 failed | 8 passed—× Data: prints "Data: 0 Objects"UI:zero rendering brokenUI:pin and#10504's pre-existingUI: 0 Appspin, since both guard the same row2 failed | 7 passed— exactly those twoTests 9 passed (9)each, andgit diff --quietagainst the commit exits0So 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 tosrc/directly with no package-exportshop intodist/. The first attempt at this ablation reported no reds at all because the vitest binary path was wrong (exit=127) and then because--reporter=basicno 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-bytes—OK (scanned 6475 text file(s) …; no raw ASCII control bytes)check:changeset-gate-self-tests— 118 + 212 + 116 self-test assertions passcheck:objectui-changeset—objectui-range --self-test: all checks passedcheck:slot-lookup—slot-lookup ratchet holds … none new,baseline key set verified against 338044f: no files addedcheck:test-source-alias—check-test-source-alias OK — 72 packages with tests scannedcheck:type-source-resolution—check-type-source-resolution OK — 77 packages … scannedcheck:type-check-coverage—check-type-check-coverage: OK — 65/78 workspace packages type-checkedcheck:type-check-debt --re-measure— ran, 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 numbercheck:query-options-erasure—ratchet holds … none new, baseline verified against338044fcheck:engine-double-contract—OK — 383 pinned, 133 in the DEBT ledger, 2 exemptcheck:where-matcher—where-matcher conformance holds: 281 matcher(s) … none newcheck-adr-0087-registration—this PR adds no declared-breaking changesetcheck-changeset-no-major—This diff introduces no major bumpcheck-empty-changeset—No empty-frontmatter changeset introduced by this diffcheck-ci-filter-parity—OK: all 86 declared cross-package glob(s) … coveredcheck-plugin-teardown-shape—63 Plugin implementation(s) … baseline fully burned downcheck-affected-docs—221 of 221 declaredNo ledger, baseline or threshold was touched.
check:type-check-debtreported four entries sitting below their ceiling (--lowercandidates) — pre-existing, unrelated to this diff, and deliberately left alone.Full
packages/clisuite and typecheck, run after the final commit and quoted from that run (60b4d073c3, clean tree):pnpm --filter @objectstack/cli test—Test Files 158 passed (158),Tests 1739 passed (1739)pnpm --filter @objectstack/cli typecheck—tsc --noEmit, no diagnosticsScope
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-base338044f721: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
--jsonpayload 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--jsonbranch. 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.tsis 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, andstats.translationsis counted bycollectMetadataStatsbut 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