ADR 007 — CLI JSON envelope contract
Status: Accepted
Date: 2026-05-09
Deciders: Abdisamed Mohamed
Related ADRs: ADR 006 (orchestrator boundary)
Context
CI pipelines, editors, and custom wrappers need a stable machine-readable surface from the CLI. Human-oriented stderr (banners, progress, summaries) must not leak into stdout when integrators expect a single JSON document. Refactors and new subcommands repeatedly touched this boundary; parity tests now freeze envelope shape, exit codes, and issue codes on the sample fixture.
Decision
- Canonical contract lives only in JSON output (
--json) — one envelope:ok,kind,data,issues[],meta(withapiVersion). --jsonapplies only when the subcommand is listed inCOMMANDS_WITH_JSON_OUTPUT(packages/cli/src/constants/jsonoutput.ts). Passing--jsonon other commands does not switch stdout to JSON.- Issue codes are API — stable
i18nprune.*identifiers; display copy may change; codes are not renamed without a deliberate breaking release. - JSON path rules: one primary JSON document on stdout; no
printCommandSummarysecond line; progress and prompts stay off stdout (stderr or suppressed). - Report split:
report --format json --outwrites a project-report document;report --jsonemits the CLI envelope withdata.document— different contracts, both documented in json.md. meta.apiVersionvs npm semver: envelope contract version ("1"today) is independent of@i18nprune/corepackage semver (0.1.x). BumpapiVersiononly when envelope semantics change — see JSON output § Version fields.
Implementation
bash
# CI gate: fail on issues or ok=false
i18nprune validate --json | jq -e '.ok and ((.issues | length) == 0)'typescript
// Envelope shape (illustrative)
type CliJsonEnvelope = {
ok: boolean;
kind: string;
data: Record<string, unknown>;
issues: Array<{ code: string; severity: string; message?: string; docHref?: string }>;
meta: { apiVersion: string; cwd: string };
};Consequences
Positive
- One jq-friendly contract across supported commands.
- Parity tests catch accidental envelope or exit-code drift during refactors.
meta.apiVersion("1"today) is versioned separately from npm package semver (0.1.x) and from stableissues[].codeidentifiers — integrators can adopt additivedatafields without envelope migrations. See JSON output § Version fields.
Negative
- Every new JSON-capable command must update
COMMANDS_WITH_JSON_OUTPUTand implement a thin JSON path inrun.ts. - Optional
datafields evolve; consumers should tolerate unknown keys.
Mitigation
- Solved: CLI
--jsoncommand parity documents theCOMMANDS_WITH_JSON_OUTPUTpitfall. - jq cookbook shows robust CI predicates.
Alternatives Considered
Per-command ad hoc JSON schemas
- Pros: Maximum flexibility per command.
- Cons: No shared
ok/issues[]semantics; CI scripts fragment.
SDK-only machine output (no CLI envelope)
- Pros: Smaller CLI surface.
- Cons: Forces subprocess or duplicate wiring for shell/Actions users.
References
- JSON output (
--json) - Issues hub
- Solved: CLI
--jsoncommand parity - Git: parity snapshot tests under
tests/parity/;packages/cli/src/constants/jsonoutput.ts