Contracts

enumeration-gate-contract.md

Contract — --json enumeration gate + OptionInfo guard (WP06)

Two durable (non-transitional) class guards. They may extend tests/architectural/test_cli_console_single_seam.py or live in a new tests/architectural/test_json_contract_enumeration.py.

G0 — Discovery / triage (WP06 first step)

string is --json (param names json_output, output_json, bare json). Introspect the declared option — name-string matching alone is insufficient.

already-parseable (parses on the error arm today), non-parseable-deferred (does not parse — e.g. events.py → stderr/empty-stdout). Record the classification.

commands are filed as a follow-up and EXCLUDED from the gate (so it cannot red on an unowned command).

  • Walk the Typer app tree; select every command declaring a bool option whose flag
  • Classify each into: adopted-shape (this mission's owned commands),
  • The parse allow-list = adopted-shape ∪ already-parseable. Non-parseable-deferred

G1 — --json enumeration gate

(or with an equivalent generic error input); assert C1 parseability; for adopted commands assert C2 shape + C4 exit code.

empty-case fixture list (e.g. agent tasks status on a zero-WP mission), assert C1 + C3.

the error arm; an adopted command that regresses its envelope shape or exit code.

NFR-003's red-first test, not this gate.

  • Drive (error arm, allow-list): invoke each allow-list command outside a project
  • Drive (empty arm, explicit list): for each command in an extensible
  • Failure conditions: a newly added --json command that is non-parseable on
  • Boundary: cannot cover the import-time #4600 crash (pre-flag-parse) — that is

G2 — OptionInfo negative-assertion guard

(__init__.py, migrate_cmd.py ×2, context.py, charter/list_cmd.py).

OptionInfo / ArgumentInfo / typer.models.OptionInfo repr.

mislabel the safe Annotated[...] = <real> form).

  • Parameterized over every invoke_without_command=True callback
  • Invoke each app with no subcommand; assert combined stdout+stderr contains no
  • Behavioral/output-based only — no AST or binding-style inspection (cannot

json-envelope-contract.md

Contract — --json machine contract

Authoritative behavior every adopted --json command must satisfy. Test surface: CliRunner-driven integration tests + the WP06 enumeration gate.

C1 — Parseability (universal)

For every --json-capable command, on its driven error path and driven empty path: json.loads(stdout) MUST succeed. stdout contains no non-JSON bytes.

C2 — Error envelope shape (adopted commands)

On an error path, json.loads(stdout) == an object where:

Human prose (if any) is on stderr, not stdout.

  • ok is false
  • error is an object with a non-empty string code and a non-empty string message

C3 — Empty-success shape (adopted commands)

On an empty-but-success path:

  • exit code == 0
  • json.loads(stdout) is the command's success payload with empty collections
  • NO error key is present

C4 — Exit-code fidelity

A --json invocation's exit code == the same command's non---json exit code for the same condition. (Per frozen test_doctor_json_not_in_project.py: mostly 1; exit 2 only for doctor shim-registry/contracts/tool-surfaces.) The fix MUST NOT homogenize exit codes.

C5 — Stream discipline

Under --json, stdout is JSON-only on every exit path; all human-readable text goes to stderr/err_console.

C6 — Happy-path invariance

The existing successful --json output of every adopted command is unchanged.

C7 — Not-in-project (universal via helper)

get_project_root_or_exit(..., json_output=True) emits the C2 envelope (code: not_in_project) at the command's existing exit code; the prose form is unchanged when json_output=False (default).