Repository navigation
fix(cli): os verify --json writes exactly one JSON document to stdout; boot diagnostics move to stderr - #21381
Conversation
…ostics move to stderr Under --json the run now takes the CLI's stdout reservation before loading the config, so every write to process.stdout (the kernel logger's INFO/WARN records, the ObjectQL registry's console.log lines, HonoServerPlugin's stop line) is forwarded to stderr, and the report leaves through emitJson on the real stdout. The text face takes no reservation and is unchanged. Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…riter at its shipped level Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
…the logs go; the stage-1 boundary names the defineStack provenance refusal Claude-Session: https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 26 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 bcaae368a0be9f22df7497ebadd61506395ed3da && git checkout bcaae368a0be9f22df7497ebadd61506395ed3da
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 43e928dd4c0be18b5a10430239371332f2ee47f4 f9ee0ac5e8182cecba6ae2b083867e07b9e803fa && git checkout -B drift-repro 43e928dd4c0be18b5a10430239371332f2ee47f4 && git merge --no-ff f9ee0ac5e8182cecba6ae2b083867e07b9e803fa
node scripts/docs-audit/affected-docs.mjs --json 43e928dd4c0be18b5a10430239371332f2ee47f4
|
Fixes #21324
Clause-②: no
os verify --json > report.jsonnow leaves a fileJSON.parsereads whole. Under--jsonthe command takes the CLI's existing stdout reservation (reserveStdoutForJson, the routebootSchemaStacktook for the same defect in commit 2b641dd) before it loads the config. Every write toprocess.stdout, whoever makes it, is forwarded to stderr, and the report leaves throughemitJsonon the real stdout. Nothing is silenced, and the text face is unchanged.Measured first: where the stdout lines came from
Measured at
f39760864c(the claim's base) on a two-objectdefineStackstack that passes the author-time rules and reaches the runtime stage. The command wasobjectstack verify --json > report.json, run through this checkout's source entry (bin/run-dev.js):f39760864c)0c2c904fea)JSON.parse(report.json)exitUnexpected non-whitespace character after JSON at position 4A stdout-write probe attributed the 318 lines ahead of the document. The probe is a
--importpreload in the scratchpad that tags each write by its writer, and it touches no repo file.ObjectLogger(169 INFO, 5 WARN)console.log([Registry] ...)HonoServerPlugin'sconsole.logon stopAfter the fix, the same probe sees exactly one write on the real stdout: the document, from
writeStdoutDirect. A sorted diff of the after-run's stderr against the before-run's stdout lines differs only in timestamps, ids, ports and durations. So nothing was dropped. With--rls(two boots; measured at866de1d7c8, whoseverify.tsis byte-identical) the after-run gives exit 0, a parseable stdout of 80 lines (the document) and 653 lines on stderr.The route, and why not the one the claim expected
The claim expected a new optional kernel-logger member on
@objectstack/verify'sBootOptions, which the CLI would pass under--json. The measurement rules that out as the fix:console.logcalls (144 lines) never go through it, so stdout would still not parse.silentwould also throw away the 5 WARN records: the missingjobservice, the analytics read-grant notice, OAuth served unencrypted on loopback, and the port fallback.info/warnto stderr from inside the logger would need a change inpackages/core(stop clause 1). It is not needed either: the CLI already owns a seam that moves every stdout write to stderr for a--jsonrun (packages/cli/src/utils/json-stdout.ts). That module's header records why it rejected the silent route, for the same two reasons.The reservation redirects each write as it happens. Nothing is captured and filtered afterwards, so stop clause 2 does not apply.
@objectstack/verifyis untouched and its publishedBootOptionsdoes not move. That is why the declaration line readsno: this PR adds no new key to a published payload and widens no accept set.What changed
packages/cli/src/commands/verify.tsrun(): under--json, callsreserveStdoutForJson()beforeloadConfig. The reservation is never released, because the run is one-shot and its last act is the payload and the exit. That way a booted stack's late timers cannot land under the document.emitJson(...)instead ofthis.log(JSON.stringify(..., null, 2)). The bytes are the same (pretty, with a trailing newline), but they now reach the real stdout and are drained beforethis.exit. The oldthis.logfollowed by an exit could truncate a report larger than one pipe buffer. The other two--jsondocuments (the stage-1 refusal and the could-not-run envelope) already usedemitJson.packages/cli/test/verify-json-stdout.test.ts(new; integration tier, because it spawns the CLI; not an.e2ename, so it runs on every PR) spawnsverify --jsonandverifyon a clean stack that reaches the runtime stage, and pins four things:crud,hardFailures: 0);console.loglines (moved, never destroyed);verify passed.The child is spawned with
OS_REGISTRY_LOG: 'info', the shipped default, written out at the call site. This package's vitest config setswarnfor its own workers, and atwarnthe registry's writer stays quiet.content/docs/deployment/cli.mdx, the#### os verifyentry only:--jsonparagraph now says that stdout carries exactly one document, that everything else goes to stderr and none of it is dropped, and that without--jsonthe lines stay on stdout;defineStackprovenance refusal (STACK_PROVENANCE_MISSING, the 1a step ofvalidate/buildthatverifydoes not run). This was carried from the contract review on fix(cli)!: os verify runs the author-time rules first, and a stack they refuse fails verify with the findings os validate reports #21364, item 3.2..changeset/21324-verify-json-stdout.md:@objectstack/cliatpatch.Tests
All runs are at
f9ee0ac5e8(afterorigin/mainwas merged in) unless a row says otherwise.pnpm --filter @objectstack/cli exec vitest run test/verify-json-stdout.test.ts test/verify-author-time-stage.test.ts test/published-subpath-console.pin.test.ts test/published-subpath-hook-body.pin.test.tspnpm --filter @objectstack/cli exec vitest run --project unitpublished-subpath-*) refused with "packages/cli is not built". Afterpnpm --filter @objectstack/cli build, both pass (row above).pnpm --filter @objectstack/cli typechecktsc --noEmit, andcheck:test-typecheckOK)verify-author-time-stage.test.ts, the existing pin)OS_TEST_TIERS=nightlytest/config-miss-stdout-purity.e2e.test.ts(theverifyface is a member, and the reservation is taken beforeloadConfig)verifybranches on both facesOS_TEST_TIERS=nightlytest/json-stdout-purity.e2e.test.tsThe integration layer is the spawn tier. The two spawn files this diff touches or depends on ran locally (rows above). The rest of the integration tier is declared to CI.
Ablations (fix committed first; each leg goes through
scripts/ablation-replace.mjs, which restores and proves blob == HEAD with an emptygit diff HEAD)packages/cli/distwas absent during these legs, so the spawned CLI ransrc/through tsx, and the mutation reached the run with no rebuild.if (flags.json) reserveStdoutForJson();this.log(JSON.stringify(..., null, 2))Unexpected end of JSON input: the report went to stderr), 3 passedUnexpected non-whitespace character after JSON at position 4; 1 passedif (flags.json || !flags.json))Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands(no paths) derived 94 commands atf9ee0ac5e8. All 94 were run with their exit codes recorded, and--ranreports94 run, 0 NOT-MEASURED, 0 UNRUN. Five of them first refused with exit 3 (PREREQUISITE NOT MET: unbuilt CLI and client-react). They were re-run afterturbo run buildfor exactly the closure each gate named, and all five exited 0. Those five arecheck:skill-examples,check:i18n,check:i18n-coverage,check:i18n-walk-parityandcheck:dual-build-cjs-loads.pnpm lint(repo-wide) is CI's. The narrowed run below counts as a measurement because all three of these hold:.tsfiles and answers "File ignored because no matching configuration was supplied" for the.mdxand the.md.--format jsonreturned 4 results:verify.tsandverify-json-stdout.test.tswith 0 errors and 0 warnings, atf9ee0ac5e8.eslint.config.mjsenables no type-aware linting (noparserOptions.projectand no typed rules, as its own header states).Acceptance notes
f39760864cwithOS_TEST_TIERS=nightlyonpackages/cli/test/json-stdout-purity.e2e.test.ts: 1 failed, 40 passed. The failing case isthe family this contract has to hold across > is exactly the set listed here — a new member goes red until it is driven too, withexpected [ 'meta resync', …(13) ] to deeply equal [ 'meta resync', …(12) ]. The source-discovered family now includesmigrate audit-metadata-bodies(it callsbootSchemaStack(and declares--json), and the file'sFAMILYmap does not list it.os verifyis not a member of that family at all, because it boots through@objectstack/verify'sbootStack, notbootSchemaStack. So no case there isverify --json. All 13 driven members pass their three cases. Per the dispatch, that test file is not touched here.bootSchemaStack(, so it cannot seeos verify. The new pin is what holdsverify --jsonto one document. A grep overpackages/cli/src/commandsfor a--jsonflag next to a kernel boot finds 15 commands: 14 reserve throughbootSchemaStack({ jsonOutput }), and the 15th isverify, which now reserves too.[Registry] ...housekeeping throughconsole.log, at its ownOS_REGISTRY_LOGlevel, outside the kernel logger. That is why a logger-level route could not have fixed this. This is an observation, not a defect, and nothing is filed.Generated by Claude Code