JSON output done right: one Response contract across all commands (#32, #37, #38) #48
feat/json-output-contract
into main
JSON output, done right — one contract across all 140 commands
Collapses the JSON-output trio (#32 coverage, #38 competing mechanisms, #37 misleading --json help) into a single architectural fix, plus enforcement so it can’t regress.
The contract (correct-by-construction)
Every leaf command returns an output::Response { json, exit }; commands::run is the single place that writes stdout under --json. The human helpers (detail/header/success/info/line/print_table) self-suppress under --json. So a command can’t forget to emit JSON (the compiler requires a Response) and can’t leak human text (helpers no-op + raw println! is a clippy error). Errors are a {"ok":false,"error":…} envelope from main — as a fallback, so a gate that prints-then-exits never double-writes.
Envelope: reads echo the server payload (or {"items":[…]} for client-built lists); mutations are {"ok":true,…}; deletes {"ok":true,"deleted":…}; gates emit their payload then exit nonzero.
Audit fixes folded in
deploy statusno longer prints a human header and a raw pretty-JSON dump (was invalid JSON and ugly human output).registry token createreturns the one-time secret as a JSON field instead of a rawprintln!.releasedrops its competingOutputFormat/--formatenum — it routes through the global--jsonnow.board liston a feature-disabled repo exits nonzero with an error envelope instead of exit 0 masking the failure.label/milestonestop double-nesting the server envelope ({"label":{"label":…}}).epic viewincludes children under--json;requirement list --kind allemits one document, not two.runnerlist/view/token/logs/status + service subcommands, andci job-view, now emit JSON.- The
--jsonhelp is now the concise, accurate contract instead of a 380-char requirement-specific paragraph reprinted on every subcommand.
Enforcement
[lib]target so tests introspect the real command surface;main.rsis a thin shell.clippy.tomlbansprintln!/print!outside theoutput.rsrenderer — a raw stdout write in a command is a build error.tests/json_contract.rs::every_leaf_is_coveredwalks the clap tree and pins all 140 leaves — a new subcommand fails CI until it’s wired through the contract.- Regression tests for the deploy-status,
--kind all, registry-token, and board-feature-disabled fixes.
How it was built
WS-0 (the Response contract) landed first, then a 10-agent workflow migrated each file-group against the locked API, then central integration + the enforcement layer. cargo fmt/clippy -D warnings/cargo test (198 tests) all green.
🤖 Generated with Claude Code