ADR-030: The CLI surface is the workflows, not the module layout
Status Active · Filed 2026-08-07
Context
The CLI had eleven commands because the dispatcher mapped one command per
module: every module keeps a standalone main() (so a project can vendor one
file), and registering each in cli.py was free. The surface therefore
mirrored the package layout, and it kept growing with it — which is a
projection of an internal structure onto an interface, the drift shape
DP-3 names.
The evidence that the tail was dead was already written down, in three places:
reports.pysays in its own docstring that the detail “only existed behindref-statusandpending, which nobody runs” — the reason the markdown reports exist at all.- Bare
luria badgesprinted a stderr warning that in normal useluria indexwrites the badges — a command whose main output was advice not to use it. ADR-029 records the incident that prompted the warning: an adopter’s workflow ran it to no effect. - This repository’s own CLAUDE.md never told contributors about
badges,reportsorcollect. The documented surface and the actual surface had already diverged.
Meanwhile two of the survivors are real but not human-facing: luria collect --commit is invoked exactly once, in ci.yml — running it by hand consumes
fragments a reviewer was meant to see on the branch — and luria reports
exists to produce the artifact actions/lint uploads.
Decision
Two tiers, and the help text says which is which.
- Contributor commands —
lint,link,index,journal,remotes,init. One per workflow: check, fix, regenerate, file an entry, resolve foreign references, adopt. - CI commands —
reports,collect. Listed under “run by CI, rarely by hand”, still runnable directly. - Removed —
badges,ref-status,pending. Each was subsumed before it was removed:luria indexwrites the badge region andluria lintchecks it (ADR-029); both status reports print as one-line lint warnings (ADR-007) and land in full detail in theluria reportsartifact.
Removed means gone. A removed name is an ordinary unknown command — exit 2, the usage, nothing else. A deprecation carries an obligation to a user base with something to migrate; this library is days old and has one user, so a shim that answers for the old names would be an affordance in service of a behavior that only exists because the affordance does. The usage text the refusal prints already names every command that took the old ones’ jobs, which is all of DP-1 the situation supports.
The modules keep their main()s — here, not as a rule. Removing a
command can and often should take its code with it: less sandbag code makes
the code that matters easier to find. These three entry points survive for
reasons unrelated to the commands they used to back — the test suite drives
them directly, python -m luria.ref_status --all is the interactive escape
hatch, and the vendor-one-file property depends on them. When those reasons
lapse, the code goes too.
Alternatives considered
- Status quo — keep all eleven. Costs nothing today, but every command is documentation: eleven top-level names claim eleven workflows, and three of those claims were false. A surface that lies about what you’re supposed to run is the same drift the record machinery exists to prevent, on the machinery itself.
- Fold
linkintolint --fix. The familiar linter idiom, the two already sharedoc_refs.py, and the check/fix shape would survive as a flag — shortening the list is a legitimate goal of this decision, so none of that is the objection. The reasonlinkstays its own name (review’s, replacing the first draft’s): a futurelint --fixplausibly repairs more than references — dates, populated fields, whatever else becomes mechanically fixable — andluria linkis then the tool that does exactly the reference rewrite and nothing else, including path-scoped. - Delete the retired modules’
main()s too. A cleaner amputation, but it breaks the vendor-one-file property for zero surface gain — the CLI no longer advertises them either way — and it would have forced the interactive detail (--all,--as-of) to grow back insidereportsimmediately. - A
RETIREDtable: each old name refuses by pointing at its successor. The first draft shipped this, argued from DP-1 — the dispatcher knows where the job went, so say so. Review removed it: a deprecation shim communicates a migration timeline to users who need one, and this project has no such users — the names would survive solely to serve typists of names that no longer exist, which is the legacy-preservation reflex this whole decision exists to cut. DP-1 is satisfied by the refusal printing the usage, which names what does exist.
Consequences
luria --help, the README’s command table, both CLAUDE.mds and the Makefile now describe the same six-plus-two surface; theref-statusandpendingmake targets are gone.- ADR-007 is corrected in place to v2: its choice (reported, never enforced) stands, but its Decision section named the two standalone commands as the delivery mechanism, and that part is now this decision’s.
- A script that still says
luria badgesorluria ref-statusgets exit code 2 and the usage — and ADR-029 already moved every scaffolded workflow ontoactions/generate/actions/lint, which never used the removed names, so no known caller breaks. - The one-command-per-module reflex is gone: the next module added to the package starts with no CLI presence, and earns one only when a workflow needs it.