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
--jsoncommand 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=Truecallback - 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.
okisfalseerroris an object with a non-empty stringcodeand a non-empty stringmessage
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
errorkey 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).