Uh oh!
There was an error while loading. Please reload this page.
fix(cli): name the remainder at every truncating render in build/validate/init - #11776
Conversation
…date/init Nine diagnostic renders in `os build`, `os validate` and `os init` cut their list at a fixed cap and printed nothing saying so. Truncated output that carries no notice is indistinguishable from complete output, so an author who reads it and sees no further problems has read a list that stopped early — and past the cap each round of fixes reveals a new batch that reads as fresh breakage, which is the round-trip `os validate` removed when it started reporting every failing rule at once. The population was re-derived from the defect (a truncating render with no remainder line) rather than from the `slice(0, 50)` literal the card was scoped by, and the two differ in both directions: the `--strict-body` refusal path caps at 20 and was missing from the ledger, while the `bodyExtractionWarnings` block also caps at 20 and already names its remainder — that one is the in-repo precedent this change copies, and it is untouched. The notice sentence now has one implementation, `printTruncationNotice`, which `printAuthoringAdvisories` (#11529) was refactored onto without changing a byte of its output. Its `remedy` is optional because the pointer has to be verified per site: `--json` publishes each list at the exact exit whose text face carries the notice, but `os init` declares no `--json` flag at all, so both of its notices state the remainder with no pointer rather than naming a remedy that returns the same truncated view. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
📓 Docs Drift CheckThis PR changes 1 package(s): 18 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 511cd4f7482feca6abfbcc670a7db1e44b0b3297 && git checkout 511cd4f7482feca6abfbcc670a7db1e44b0b3297
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 006c181a892e53b05db75f0b0ca4a4cec3059833 95ea4cc0bf0ce9bde5ab24c6595eb635aab39841 && git checkout -B drift-repro 006c181a892e53b05db75f0b0ca4a4cec3059833 && git merge --no-ff 95ea4cc0bf0ce9bde5ab24c6595eb635aab39841
node scripts/docs-audit/affected-docs.mjs --json 006c181a892e53b05db75f0b0ca4a4cec3059833
|
os-zhuang
commented
Aug 24, 2026
PM review — ACCEPT the work. One small change requested, and it is this PR's own principle applied to one of its own comments.
Clause ②: The |
…ional The undeclared-authoring-key notice points at `--json`, and `warnings` lives in `os build`'s TERMINAL payload — so the pointer resolves on the success exit and not when a later gate (access matrix, package docs, the runtime bundle) fails first and emits its own payload. The site's comment stated the honest half without the condition, which is the same shape as the silence this change exists to remove: a claim true in one branch, read as general. Comment only. The caveat and the tracking issue for the payload-shape question are now stated where the pointer is chosen, and the six error-path sites are named as the ones with no such gap because their `--json` branch sits in the same block as the text face. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019siH5jDmk5hrayvfyojUqR
Requested change pushed as The Your reason is in the comment too, because it is the better half of the point: an unqualified claim that holds in one branch is the same shape as the silence this change exists to remove. The Verified on the new head, closure built first (a fresh worktree gave the expected unbuilt-dependency collection error before the build, not a code failure): pins 39/39 green, Still draft, auto-merge unarmed — confirmed from the remote after the push ( Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
Requested change accepted — verified from the commit, not the report.
⭐ And you got the part I did not ask for right, which is the part that mattered. You left The new comment also does something the PR body could not: it puts the caveat where the pointer is chosen, names #11772 as where the payload-shape question lives, and contrasts the six error-path sites as the ones with no such gap. The next author meets the rule at the decision, not in a thread. Arming stateCI is re-running at No gate to wait on: Clause ② is ⛔ Nothing further from you. Do not mark ready, do not arm. Generated by Claude Code |
os-zhuang
commented
Aug 24, 2026
ARMED — ready flipped, auto-merge enabled (squash).
All 31 check runs Preconditions, each discharged rather than assumed:
For the record, since this card was a sweep and sweeps are where vacuity hides: what makes it complete is not the nine fixes but the two negative results around them — Generated by Claude Code |
Fixes#11642
Nine diagnostic renders across
os build,os validateandos initcut their list at a fixed cap and printed nothing saying so. The cap is not the defect; the silence is. Truncated output that carries no notice is indistinguishable from complete output, so an author who reads it and sees no further problems has read a list that stopped early — and on the gating lists that quietly restores the round-tripos validatewent out of its way to remove ("the command used to exit at the first failing gate, so an author with three unrelated problems fixed them in three round trips and could not see how deep the hole went").Every cap keeps the value it had. Over it, the output now names the exact remainder:
1. The worklist, re-derived from the DEFECT
The card's population was selected by grepping
slice(0, 50), which answers "where does the number 50 appear", not "where is output truncated in silence". Measured here, those differ in both directions, so the worklist below was re-derived from the defect — a truncating render with no remainder line — and every candidate's surrounding lines were read before it was counted. Line numbers are current (945ffbea8); the card's are two merges stale.IN — 9 sites, all fixed
compile.ts:145issues(--strict-bodyrefusal)compile.ts:236ruleErrorsexit 1, no notice.compile.ts:306unknownKeyWarningsUndeclared authoring keys (N)), so the block stated two numbers that disagreed and explained neither. Renamed fromunknownKeyFindingsand re-formatted at the computation site by #11726; itsslice(0, 50)is byte-unchanged and was still silent.compile.ts:346driftexit 1, no notice.compile.ts:373docErrorsexit 1, no notice.validate.ts:140ruleErrorsexit 1, no notice.validate.ts:212docErrorsexit 1, no notice.init.ts:893report.advisoriesinit.ts:908report.errorsOUT — read, judged, untouched
compile.ts:174lowering.bodyExtractionWarnings(cap 20)… and ${n - 20} moreplus a pointer at the complete-output path (os build --strict-body). Listing it would have been the obvious next move from the grep and it would have been wrong. ⛔ Untouched; a pin asserts both of its lines are still there byte for byte, because a change to it is a change to the standard everything else now copies.compile.ts:222ruleAdvisoriescompile.ts:108conversionNotices,:270/:276capPreflight.errors/.warnings,:380docWarningsvalidate.ts:180capProviderErrors,:363warningsinit.ts:61-63s.slice(0, 20)init.tsprintCreatedFilesSummary,formatZodErrors(all three commands)formatZodErrorseven closes withN validation error(s) total.Outside the three printers, the same sweep over
packages/**found four more capped renders (runtime/app-plugin.ts,service-storage/verify-file-references.ts, three inspec/scripts/build-schemas.ts) — all of them already name their remainder, so there is no wider silent-truncation population to file. The one genuine find is a different defect and is filed, not fixed: see §6.2. Per-site
--jsoncheck — the pointer is verified, never assumed#11645's notice ends by pointing at
--json"for the full list", and that is honest only for a list--jsonactually carries. A notice whose remedy does not work is worse than a silent cut: it sends the author down a path that returns the same truncated view. So each site was read against its own payload before a pointer was offered.--jsoncarry that list?compileissues(strict-body){ success: false, error: 'strict-body: missing body', issues }— re-run with --json for the full listcompileruleErrors{ …, error: 'author-time rules failed', issues: ruleErrors, … }compileunknownKeyWarningswarnings: [...ruleAdvisories, ...unknownKeyWarnings]compiledrift{ …, error: 'access matrix drift', changes: drift }compiledocErrors{ …, error: 'docs validation failed', issues: docErrors }validateruleErrors{ valid: false, errors: ruleErrors, … }validatedocErrors{ valid: false, errors: docErrors, … }initreport.advisories--jsonflag at allinitreport.errorsos init'sstatic override flagsdeclarestemplate,installandpackage-managerand nothing else. A pin asserts that (with a positive control: the same pattern is found incompile.tsandvalidate.ts), so adding--jsontoinitlater fails here rather than silently leaving two dead pointers behind.init.tsalso contains no reference to the shared pointer constant, which the same pin checks.⛔ Nothing was added to any
--jsonpayload to make a notice true. That is #11643's class and a machine-contract change.One caveat, recorded honestly rather than papered over. The six error-path pointers are airtight: the
--jsonbranch sits in the same block as the text face, so re-running the identical command returns the whole list from the same exit. The two advisory pointers —unknownKeyWarningshere, andruleAdvisoriesfrom #11645 — resolve through the terminal success payload, so a run that fails at a later gate emits a failure payload that carries nowarnings. Filed as #11772 rather than fixed here: closing it means changing a--jsonpayload shape, which this card may not do.3.
printAuthoringAdvisoriesis reused, not duplicatedinit.ts:893now callsprintAuthoringAdvisoriesitself. That block was a byte-for-byte copy of the printer's body, cap included; it is now the same callos buildmakes, withremedy: null.printTruncationNoticeinpackages/cli/src/utils/format.ts.printAuthoringAdvisorieswas refactored onto it and its rendered output is byte-for-byte whatobjectstack build/validateprint at most 50 author-time warnings and drop the rest with no notice #11529 shipped — fix(cli):os buildnames the author-time warnings it withheld instead of stopping dead at 50 #11645's own pin file is untouched and still passes, and the ablation in §4 takes 3 of its assertions down with the rest, which is what proves the two really share one implementation rather than merely agreeing today.printAuthoringRuleErrors,printDocIssueErrors,printBulletList) each render their own rows and then call that one notice. Nine copies of the wording would have been nine chances to drift.4. Pins — red before, green after
New file:
packages/cli/test/truncation-remainder-notices.test.ts(32 tests). Run with #11645's untouched pin file (7 tests) throughout.Green, after:
Test Files 2 passed (2) · Tests 39 passed (39)Red-before #1 — the sites. Restore the three command files to
origin/main, keep the helper and the pins (git restore --source=origin/main --worktree; mutation proved on disk before measuring: 0 printer calls in each file, 6 + 2 + 2 capped for-of loops back):The sweep names the defect in its own words, including the site the card's ledger was missing:
Red-before #2 — the rendered output (ablation). The printers are new, so "the function does not exist" is not a meaningful red. Instead the shared notice was ablated to a no-op (
return;at the top ofprintTruncationNotice) and the whole suite re-measured. Mutation proved on disk first — injected marker present ×1, function and its 4 call sites still there — and the suite resolves../src/utils/format.js, i.e. source, so no build stands between the mutation and the measurement; the script carriestrap … EXIT INT TERMso a kill mid-mutation cannot leave the tree ablated. Restore leg verified bygit hash-objectequal to the committed blob and the marker absent.Controls — green in BOTH states, which is the point. A printer that always printed a notice would satisfy every red-going pin above. So each printer is pinned from the other end too, and every one of these stayed green under the ablation:
Instrument discipline. Each per-site pin asserts a positive first — text independently known present at that site, never a substring of the term under test — before any absence is read as evidence. The
--json-flag pin likewise proves itself oncompile.tsandvalidate.tsbefore reporting the absence ininit.ts.5. Clause ② — no
Text output only. No
--jsonpayload was read differently, written differently, or added to; the one payload-shaped question this card ran into is filed as #11772 rather than acted on. The--jsonbranches in these files are untouched — the pins assert each one still publishes the key its text-face notice points at.6. Out-of-scope findings
Both filed unassigned and unlabelled for triage to grade:
os build --json's failure payloads carry nowarnings, so the truncation notice's--jsonpointer is a dead end when a later gate fails #11772 —os build --json's failure payloads carry nowarnings, so the advisory notices'--jsonpointer is a dead end when a later gate fails. Adjacent toos build --jsonalso drops the capability-provider and package-docs warnings thatos validate --jsoncarries #11727 but a distinct defect: that one is about lists computed under a!flags.jsonguard; this one is about lists that do reach the payload, on one exit only.7. Checks run
All at
d087304ea, the final commit, and the diff-scoped and ratchet families were re-run after it (the changeset gates only see the changeset once it is committed).pnpm --filter '@objectstack/cli^...' build— dependency closure built first, before any verdict was read.pnpm --filter @objectstack/cli typecheck—tsc --noEmit, exit 0. Note it coverssrconly; the new test file is inside the hidden test tree measured byTEST_DEBT['@objectstack/cli'], andcheck:type-check-debt --re-measurereports "none above its recorded number", which is where that file's type-check evidence comes from.pnpm --filter @objectstack/cli exec vitest run --maxWorkers=4— 172 files / 1972 tests passed, the whole package.pnpm lint— the full repo-wideeslint . --no-inline-config, exit 0 in 81s. Not narrowed.node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(derived from the real change set, not from a hand-written list) plus the five convention-triggered ones for adding a test file, all green:changeset-gate-self-tests,cli-test-child-env,cross-package-test-inputs,i18n,i18n-coverage,objectui-changeset,published-files,slot-lookup,test-source-alias,type-check-coverage,type-check-debt,type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-empty-changeset,check-plugin-teardown-shape,docs-audit/check-affected-docs,release-rehearsal-clone --self-test,nul-bytes,engine-double-contract,where-matcher,query-options-erasure.check:i18nandcheck:i18n-coverageeach refused at first as PREREQUISITE NOT MET (unbuilt CLI, then unbuiltconnector-mcp/connector-openapi/ the showcase closure). Recorded because that refusal is not a pass: both were re-run green only after the closures were built.scripts/pm/os-verify-lock.sh; every exit code was captured before any pipe.Generated by Claude Code