ADR-039: Drive the CLI with Fire: typed functions, derived flags

Status Active · Filed 2026-08-07 · Issue #32

Context

Review on ADR-030 proposed the Fire library, and the analysis split into a naive shape (wrapping the existing argparse main()s — a dependency added to keep the dispatch table) and an idiomatic one: delete the per-module argparse layers and expose typed functions Fire drives directly. The review chose the idiomatic shape, as a draft PR to be judged on the diff.

The load-bearing constraints, found up front:

  • Exit codes are not return values. Fire prints what a function returns, so luria lint returning 1 would print “1” and exit 0 — a CI gate that green-lights everything. Every command signals failure by raising SystemExit, and returns None always.
  • Each module already parsed sys.argv itself, so nothing could be wrapped: the argparse layer had to come out module by module, replaced by a typed run() whose signature is the flag surface.

Decision

fire.Fire(COMMANDS) over a dict of eight typed run functions. Flags and help derive from signatures and docstrings; there is no parsing layer left to drift from the functions it wraps. Every module keeps a standalone entry (python -m luria.ref_status --all) through the same functions via fire.Fire(run) under its __main__ guard, so the vendor-one-file story survives with Fire as the one convention.

The invocation surface is unchanged. Every spelling CI and the composite actions use — luria lint, luria link --fix, luria index --check, luria new adr --title X, luria collect --commit, luria init --issue-url U — parses identically under Fire; the end-to-end smoke (init → index → new → lint) passes byte-for-byte on exit codes.

Alternatives considered

  • Keep the hand-rolled dispatcher (ADR-030’s shape) — 40 lines, zero dependencies, tiered help. The default this proposal must beat; the draft PR’s writeup argues the trade in both directions.
  • Naive Fire (fire.Fire over SystemExit-wrapped argparse mains). Adds the dependency without removing the thing it was meant to replace; rejected in the review thread before this draft.
  • Click/typer instead. The same deletion is available with decorators and explicit registration; Fire was the named proposal, and its derive-from-signature posture is the most aggressive version of the “code is the interface” bet being evaluated.

Consequences

  • argparse is gone from the package; fire>=0.7 joins PyYAML as the runtime dependencies.
  • Help output is Fire’s house format (NAME/SYNOPSIS/FLAGS), replacing the tiered contributor/CI usage text — the tier distinction now lives only in this file’s docstring and the docs.
  • An unknown command exits 2 with “Cannot find key” plus the list of available commands — the ADR-030 refusal property, now Fire’s rather than ours.
  • A future command author must know one rule the old shape enforced by type signature: raise SystemExit, never return a status — Fire prints return values.