Skip to content

feat(cli): one table drives both dispatch and --help - #443

Open
avrabe wants to merge 1 commit into
mainfrom
feat/cli-surface
Open

feat(cli): one table drives both dispatch and --help#443
avrabe wants to merge 1 commit into
mainfrom
feat/cli-surface

Conversation

@avrabe

@avrabe avrabe commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Second item of v0.43.0 "stop grading ourselves"REQ-CLI-SURFACE-001
(proposed in #441).

--help documented 16 commands. main.rs dispatched 20. The four
missing: sysml2, extract, generate, version — so the entire SysML v2
surface, 8378 LOC and 276 tests, was undiscoverable to anyone who had not read
the source.

Not a guard — a single source

The obvious fix is a checker that diffs the two lists. This deletes the second
list instead. main and print_usage read the same COMMANDS table:

const COMMANDS: &[(&str, &str, CmdFn)] = &[
    ("parse",  "Parse AADL file(s) and report diagnostics", cmd_parse),
    ("sysml2", "Parse SysML v2 / KerML, or lower it to AADL", cmd_sysml2),];

Dispatch does a table lookup; print_usage iterates the same slice and pads the
description column to the longest name. A command that dispatches is documented
by construction.

That is stronger than a test. Deleting an entry does not fail a check — it
does not compile
, because the entry point becomes dead code under -D warnings:

error: function `cmd_sysml2` is never used
error: could not compile `spar` (bin "spar") due to 3 previous errors

version / help and their flag spellings stay out of the table deliberately —
they short-circuit before it and are not operations over a model, so listing them
under Commands: would imply spar version <file>. They get their own lines.

The gap mutation testing found

Three mutations, two killed. The third survived every test in the file:

match args[1].as_str() {
    "secret" => cmd_parse(&args[2..]),      // <- dispatches, invisible to --help
    "help" | "--help" | "-h" => {}

That is not hypothetical — a hand-written arm beside the table is exactly how
the four undocumented commands came to exist. The table makes the commands in
it
consistent; it cannot stop one being added next to it.

So no_command_dispatches_outside_the_table reads the dispatcher via
include_str! and asserts every literal arm is one of the six allowed
short-circuit spellings. That is modelling rather than measuring, which this repo
normally avoids — but the thing guarded against is a source edit, and the
alternative is enumerating every string a user might type. It now reports:

these dispatch around COMMANDS: ["secret"]

A companion test asserts the scan can see the known arms at all, so an empty
result means "clean" and never "read nothing".

Before / after

Commands:                              Commands:
  parse      …                           parse     …
  …          (16 listed)                 …         (19 listed)
                                         sysml2    Parse SysML v2 / KerML, or lower it to AADL
                                         extract   Extract SysML v2 requirements to rivet YAML
                                         generate  Generate SysML v2 from rivet YAML (--from-rivet)

                                         --version  Print the version and exit
                                         --help     Print this help and exit

Verification

cargo test  -p spar  -D warnings     26 test binaries green, 0 failed
cargo clippy -p spar --all-targets   clean
cargo fmt --all --check              clean (run BEFORE staging, re-checked after)

mutation: entry removed from COMMANDS      -> does not compile
mutation: "Commands:" header renamed       -> blindness guard fires
mutation: arm added outside the table      -> new test fails
restored                                   -> 6 passed

The dispatcher rewrite is the real risk here; the 300+ existing spar-cli
integration tests cover it and all pass. Every new test runs the real binary
via CARGO_BIN_EXE_spar, as cli_version.rs does.

Not claimed

That every command is well documented. This is about existence, not quality —
the per-command Options: block is still hand-written, and a command can still
be listed with a poor description.

Refs #441.

🤖 Generated with Claude Code

REQ-CLI-SURFACE-001. `--help` documented 16 commands; `main.rs` dispatched 20.
The four missing ones were `sysml2`, `extract`, `generate` and `version` —
which made the entire SysML v2 surface, 8378 LOC and 276 tests, undiscoverable
to anyone who had not read the source.

## Not a guard — a single source

The obvious fix is a checker that diffs the two lists. This removes the second
list instead: `main` and `print_usage` now read the same `COMMANDS` table, so a
command that dispatches is documented by construction and the description
column is computed from the longest name rather than hand-padded.

That turns out to be stronger than a test. Deleting an entry from `COMMANDS`
does not fail a check — it does not COMPILE, because the command's entry point
becomes dead code under `-D warnings`:

    error: function `cmd_sysml2` is never used
    error: could not compile `spar` (bin "spar")

`version`, `help` and their flag spellings stay out of the table deliberately:
they short-circuit before it and are not operations over a model, so listing
them under `Commands:` would imply `spar version <file>`. They get their own
lines in the help instead.

## The gap mutation found

Three mutations were run against the new tests. Two were killed. The third —
adding `"secret" => cmd_parse(&args[2..])` to the fallback match — SURVIVED
every test in the file.

That is not a hypothetical: a hand-written arm beside the table is exactly how
the four undocumented commands came to exist. The table makes the commands IN
it consistent; it cannot stop one being added next to it.

So `no_command_dispatches_outside_the_table` reads the dispatcher's source via
`include_str!` and asserts every literal match arm is one of the six allowed
short-circuit spellings. That is modelling rather than measuring, which this
repo normally avoids — but the thing being guarded against is a source edit,
and the alternative is enumerating every string a user might type. It now
reports `these dispatch around COMMANDS: ["secret"]` and fails.

## Verification

- `cargo test -p spar` under `-D warnings`: 26 test binaries green, 0 failed.
  The dispatcher change is the real risk here and the 300+ existing CLI
  integration tests cover it.
- `cargo clippy -p spar --all-targets` under `-D warnings`: clean.
- `cargo fmt --all --check`: clean, run BEFORE staging and re-checked after.
- Mutations: entry removed -> does not compile; `Commands:` header renamed ->
  blindness guard fires; arm added outside the table -> new test fails;
  restored -> 6 passed.

Every test runs the REAL binary via `CARGO_BIN_EXE_spar`, as `cli_version.rs`
does, so what is graded is the artifact users get.

NOT CLAIMED: that every command is well documented. This is about existence,
not quality — the per-command `Options:` block is still hand-written and a
command may still be listed with a poor description.

Refs #441.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Rivet verification gate

20/20 passed

count
Passed 20
Failed 0
Skipped (no steps) 0

Filter: (and (= type "feature") (or (has-tag "v093") (has-tag "v0100")))

Failed artifacts

(none)

Updated automatically by tools/post_verification_comment.py. Source of truth: artifacts/verification.yaml.

@codecov

codecov Bot commented Aug 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant