Skip to content

fix(audit): keep example commands, usage wraps and wrapped sentences out of the flag definitions - #181

Open
brettdavies wants to merge 3 commits into
refactor/help-flags-shared-helpersfrom
fix/help-flags-usage-wraps-and-examples
Open

brettdavies wants to merge 3 commits into
refactor/help-flags-shared-helpersfrom
fix/help-flags-usage-wraps-and-examples

Conversation

@brettdavies

@brettdavies brettdavies commented Oct 9, 2026 •

Copy link
Copy Markdown
Owner

Summary

From the code review of the stack. An indented line that starts with a dash was a definition unless it carried on the description above it, so three shapes that are not definitions still got through:

  • An example command carried over a line with a backslash. aws s3 ls s3://amzn-s3-demo-bucket \ followed by --recursive declared --recursive.
  • A usage synopsis that wraps onto a required option. argparse prints usage: prog [-h] [--verbose] --input INPUT --mode {fast,slow} and wraps to --output OUTPUT --format FORMAT, which declared --output and --format from a usage line, while four audits' evidence says usage lines are not read.
  • A sentence that wraps onto a flag name. java's -m or --module <module>/<mainclass> are passed as the arguments to declared -m.

The parser on dev reads these lines the same way, so this is not something the stack broke. It is what the stack's README section and evidence text now promise, and one consequence is new: a single --name among such lines switches off the single-dash rule for a Go flag help.

The classifier reads each shape as text, and each rule is narrow enough to keep the definitions that sit where such text could:

  • Example command. A line under one that ends in a backslash, while no description is being read. A description can end in a backslash too (Field delimiter, default \), and the definition after it is still read.
  • Usage wrap. A line indented to the arguments of the usage: line above it, unless a gap or a marker sets off a description of its own. A tool that prints its option rows in the same block as its usage line keeps them; a synopsis wrap never has such a description.
  • Wrapped sentence. A line whose description follows its names after one space, directly under same-indent prose that has not ended its sentence (it does not end in ., :, ! or ?). Rows written -h, --help: show this help under an indented Options: label are still read.

A colon that ends a row's names sets its description off, as a gap and the markers #, =, -- and - already did. nmap is why: it prints -iL <inputfilename>: Input from list of hosts/networks and three more rows directly under an Ex: scanme.nmap.org, ... line that does not end a sentence, and without the colon rule the wrapped-sentence rule drops all four. The same marker counts for dash-led text at column 0, so python3's --help-env: print help about Python environment variables and exit and the two rows beside it are read.

It also stops losing a real layout, which is new in this stack (#168). Under a short flag with no description, clap prints the long-only rows at the long column, and they were read as that flag's description:

Options:
  -x
      --format <FORMAT>
      --quiet            Say less
  -h, --help             Print help

That help declared -x and --help only. A short at column c now marks column c + 4 as a name column, where -x, --long puts its long name. Under a definition that has no description yet, a line that declares a name at a name column is a definition and not that description. A wrapped description line is still never a definition: -1 means no limit. under -n Number of results to return. stays with -n, and so does a bulleted description (- fast: skip verification) under an undescribed flag.

What this does not do:

  • A flag-led sentence that starts its own paragraph still reads as a definition: terraform's -state, state-out, and -backup are legacy options ... is the known case.
  • The plan's deferred item on example lines at definition indent is only partly met: sqlite-utils insert's --text --convert '...' follows a backslash and is now text, and an example line with no backslash above it is still read.
  • A row whose description follows its names after one space, with no colon, is read as a sentence when it sits directly under same-indent text that has not ended a sentence, and the rows under it go with it. No help in the fixtures, the registry captures or 122 other help texts has that shape. PR test(audit): cover the shared deny note and the safe-probe suffix #185 lists it with the other open review items.

Changelog

Fixed

  • Fix flags being read from a wrapped usage: synopsis, from an example command continued with a backslash, and from a sentence that wraps onto a flag name. None of those lines declares a flag.
  • Fix long-only flags going unread under a short flag that has no description, as clap prints an undocumented -x above --format <FORMAT>.
  • Fix flags at column 0 going unread when a colon, not a gap, sets off their description, as python3 prints --help-env: print help about Python environment variables and exit.

Documentation

  • The README's section on how behavioral audits read --help names the three text shapes, the long-column rule, and the colon among the description markers.

Type of Change

  • fix: Bug fix (non-breaking change which fixes an issue)

Related Issues/Stories

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing completed
  • All tests passing

Test Summary:

  • Unit tests: five in classify.rs and one in header.rs. Six excerpts of indented text declare nothing (two aws examples, the aws and java sentences, a sentence that wraps onto two flag-led lines, an argparse 3.14.8 usage wrap). Thirteen definitions that sit where such text could are still read, among them rows directly under a usage line, BIND host's and dig's rows under a wrapped usage line, colon-described rows under an indented label, nmap's rows under its Ex: line, python3's column-0 colon rows, one-space rows whose descriptions wrap back to the name indent, and a definition after a description that ends in a backslash. An example's --out x no longer switches off the single-dash rule. The clap layout above declares all four of its flags, and a wrapped or bulleted description at the long column stays a description. A gap, a marker or a closing colon sets a description off; a header with no description and a sentence after one space are not set off.
  • 1321 passing across the suite. Self-audit: cargo test --test dogfood passes.
  • Snapshots: 2 of 71 change. The aws capture loses five rows (lines 267, 291, 292 and 293 of two examples, and the sentence at line 285), and java loses the sentence at line 12. Nothing is added.

Negatives observed failing. The branch has three commits, and the tests were run against the classifier and tokenizer before each. Against the base (#180):

---- runner::help_probe::flags::classify::tests::a_long_only_row_under_an_undescribed_short_is_a_definition stdout ----
assertion `left == right` failed
  left: [2, 5]
 right: [2, 3, 4, 5]
---- runner::help_probe::flags::classify::tests::an_example_does_not_switch_off_the_single_dash_rule stdout ----
assertion `left == right` failed
  left: None
 right: Some("-force")
---- runner::help_probe::flags::header::tests::a_gap_or_a_marker_sets_the_description_off stdout ----
-iL <inputfilename>: Input from list of hosts/networks
---- runner::help_probe::flags::classify::tests::definitions_beside_prose_and_usage_are_still_read stdout ----
[
    "Python 3.14.8: column-0 rows described after a colon: read [], declares [[\"--help-env\"], [\"--help-xoptions\"], [\"--help-all\"]]",
    "a described long-only row under a short-only row with no description: read [[\"-c\"], [\"-h\", \"--help\"]], declares [[\"-c\"], [\"--json\"], [\"-h\", \"--help\"]]",
    "a definition after an example block that ended with a blank line: read [[\"--all\"], [\"-a\", \"--all\"]], declares [[\"-a\", \"--all\"]]",
]
---- runner::help_probe::flags::classify::tests::indented_examples_usage_wraps_and_sentences_declare_nothing stdout ----
[
    "aws-cli 2 s3 ls: an example command continued with a backslash: read [[\"--recursive\"]]",
    "aws-cli 2 s3 ls: three continued lines, each a flag: read [[\"--recursive\"], [\"--human-readable\"], [\"--summarize\"]]",
    "aws-cli 2 s3 ls: a sentence that wraps onto a flag name: read [[\"--human-readable\"]]",
    "OpenJDK 21 java: a sentence that wraps onto a flag name: read [[\"-m\"]]",
    "a sentence that wraps onto two flag-led lines: read [[\"--human-readable\"], [\"-h\"]]",
    "argparse (Python 3.14.8): a usage line that wraps onto required options: read [[\"--output\", \"--format\"]]",
]
test result: FAILED. 49 passed; 5 failed; 0 ignored; 0 measured; 1002 filtered out; finished in 0.00s

Against the first commit, which wrote the three rules without the narrowing:

---- runner::help_probe::flags::classify::tests::a_wrapped_description_at_the_long_column_stays_a_description stdout ----
assertion `left == right` failed
  left: [2, 3, 4, 5]
 right: [2, 4]
---- runner::help_probe::flags::header::tests::a_gap_or_a_marker_sets_the_description_off stdout ----
-iL <inputfilename>: Input from list of hosts/networks
---- runner::help_probe::flags::classify::tests::definitions_beside_prose_and_usage_are_still_read stdout ----
[
    "Nmap 7.991SVN: colon-described rows under an example line that does not end its sentence: read [], declares [[\"-iL\"], [\"-iR\"], [\"--exclude\"], [\"--excludefile\"]]",
    "Python 3.14.8: column-0 rows described after a colon: read [], declares [[\"--help-env\"], [\"--help-xoptions\"], [\"--help-all\"]]",
    "rows with a description of their own, in one block with the usage line: read [], declares [[\"-4\"], [\"-6\"], [\"-q\"]]",
    "colon-described rows under an indented label that ends in a colon: read [], declares [[\"-h\", \"--help\"], [\"-q\", \"--quiet\"]]",
    "one-space rows whose descriptions wrap back to the name indent and end a sentence: read [[\"--recursive\"]], declares [[\"--recursive\"], [\"--page-size\"], [\"--quiet\"]]",
    "a definition after a description that ends in a backslash: read [[\"-d\", \"--delimiter\"]], declares [[\"-d\", \"--delimiter\"], [\"-q\", \"--quiet\"]]",
]
test result: FAILED. 51 passed; 3 failed; 0 ignored; 0 measured; 1002 filtered out; finished in 0.00s

Against the second commit, which had no colon marker:

---- runner::help_probe::flags::header::tests::a_gap_or_a_marker_sets_the_description_off stdout ----
-iL <inputfilename>: Input from list of hosts/networks
---- runner::help_probe::flags::classify::tests::definitions_beside_prose_and_usage_are_still_read stdout ----
[
    "Nmap 7.991SVN: colon-described rows under an example line that does not end its sentence: read [], declares [[\"-iL\"], [\"-iR\"], [\"--exclude\"], [\"--excludefile\"]]",
    "Python 3.14.8: column-0 rows described after a colon: read [], declares [[\"--help-env\"], [\"--help-xoptions\"], [\"--help-all\"]]",
]
test result: FAILED. 52 passed; 2 failed; 0 ignored; 0 measured; 1002 filtered out; finished in 0.00s

The two-line sentence case fails only against the base. It pins that a dropped row counts as prose for the row under it, so it was also run against the head with that carry-over removed:

---- runner::help_probe::flags::classify::tests::indented_examples_usage_wraps_and_sentences_declare_nothing stdout ----
[
    "a sentence that wraps onto two flag-led lines: read [[\"-h\"]]",
]
test result: FAILED. 53 passed; 1 failed; 0 ignored; 0 measured; 1002 filtered out; finished in 0.00s

Expected moves. Written before the corpus run.

No row moves. Across the full registry capture set (910 help texts) the change removes 14 definitions and adds none, and no audit asks for any of the 14:

capture lines what they are
sqlite-utils insert-files --help 8 to 14 an example: -c name:name \ and five more -c lines, then --pk name
sqlite-utils transform --help 8, 9 an example: --drop column1 \, --rename column2 column_renamed
sqlite-utils convert --help 7 an example: --import=textwrap
sqlite-utils insert --help 38, 44 a sentence (--text a "text" variable.) and an example (--text --convert '...')
cargo-binstall --help 61 a sentence: --version option.
xr --help 300 a sentence: --color always is set) and human-only banners ...

Each was read against its capture. cargo-binstall and xr declare --version and --color on real definition lines elsewhere in the same help, so their rows hold. Replaying the capture set through the base and head builds moves no row. The second and third commits read the same definitions from all 910 captures as the first: no registry tool prints the layouts they fix. nmap is not in the registry; its upstream usage text (docs/nmap.usage.txt, Nmap 7.991SVN) reads 87 definitions at the base and at the head.

Corpus before/after. Run after the table above was written, and run twice. Base 8134d09b7510, head afbfeffb80a4. Image sha256:05db16cf226cd14a93a2682aea41b72d3e6b4d240cadba006f2881d3ffa996b3, network bridge. 98 tools selected, 95 scored under both builds, 1 rerun three times (mods).

The first run reported one moved row, and it is the one row on the A/A noise list:

Moved rows (1)

tool id audit_id status confidence evidence note
mods p3-should-about-long-about p3-about-long-about warn → pass medium base: -h and --help produce byte-identical output. SHOULD-tier — clap renders the short summary on -h and the full description on --help when long_about is set; collapsing them gives agents no concise list-level grep target.
head: null

Derived fields moved (4)

tool field base head
mods badge.score_pct 79 80
mods band 75-79 80-84
mods summary.pass 17 18
mods summary.warn 10 9

The harness reports a noise-listed row as moved only when the two builds share no result. In this run all four base runs returned warn and all four head runs returned pass. That split comes from the row, not from this change. The row compares mods's -h and --help output byte for byte, and no file this PR touches is read by that audit. Each build returns both results in other runs: the base build returned warn, pass, pass, warn as the head of #180's run, and this head returned pass, pass, pass, warn as the base of #182's run.

The second run, with the same two builds in the same image:

Moved rows (0)

None.

Derived fields moved (0)

None.

In the second run the mods row returned warn, pass, pass, pass under the base and warn, pass, warn, pass under the head, so it is reported as not moved. No row and no derived field moved, as expected. In neither run is a row unstable, a harness bug flagged, or a tool scored under one build only. cursor, nvidia-smi and xai-grok-build are absent from the image and ran under neither build.

Files Modified

Modified:

  • src/runner/help_probe/flags/classify.rs: the three text shapes with their narrowing, the long column after a short, and their tests.
  • src/runner/help_probe/flags/header.rs: a colon that ends the names sets the description off.
  • README.md
  • src/runner/help_probe/snapshots/aws__s3_ls_help.snap, java__-help.snap

Created:

  • None.

Renamed:

  • None.

Deleted:

  • None.

Breaking Changes

  • No breaking changes

Deployment Notes

  • No special deployment steps required

Checklist

  • Code follows project conventions and style guidelines
  • Commit messages follow Conventional Commits
  • Self-review of code completed
  • Tests added/updated and passing
  • No new warnings or errors introduced
  • Changes are backward compatible (or breaking changes documented)

…out of the flag definitions

An indented line that starts with a dash was a definition unless it carried on the description above it. Three shapes that are not definitions got through:

- An example command carried over a line with a backslash. `aws s3 ls s3://amzn-s3-demo-bucket \` followed by `--recursive` declared `--recursive`.
- A usage synopsis that wraps onto a required option. argparse prints `usage: prog [-h] [--verbose] --input INPUT --mode {fast,slow}` and wraps to `--output OUTPUT --format FORMAT`, which declared `--output` and `--format` from a usage line.
- A sentence that wraps onto a flag name. java's ` -m or --module <module>/<mainclass> are passed as the arguments to` declared `-m`.

A phantom definition can credit a flag the help does not declare, and one `--name` among them switches off the single-dash rule for a Go `flag` help.

The classifier now reads each of those as text: a line under one that ends in a backslash, a line indented to the arguments of the usage line above it, and a line that reads on as a sentence directly under prose at the same indent.

It also stops losing a real layout. Under a short flag with no description, clap prints the long-only rows at the long column, and they were read as that flag's description: `  -x` followed by `      --format <FORMAT>` declared only `-x`. A short at column c now marks column c + 4 as a name column, where `-x, --long` puts its long name. A row there with a description of its own is a definition even when it sits at the description column of the row above.

Across the 71 fixture captures, six rows go: five in the aws capture (two examples and a wrapped sentence) and the java sentence. Nothing else changes.
A second review pass found layouts where each of the three text rules dropped a real definition, and one place where the long-column rule turned a wrapped description into a flag.

- **Backslash.** A line under one that ends in a backslash was text. A description can end in a backslash too (`Field delimiter, default \`), and the definition after it was lost. The rule applies only while no description is being read.
- **Usage synopsis.** Every line indented to the usage line's arguments was text until a blank or shallower line. A tool that prints its option rows in the same block, at or right of that column, lost all of them. A row whose own description a gap sets off is a definition; a synopsis wrap never has one.
- **Wrapped sentence.** A flag-led line with a one-space description was text under any same-indent line that was not a definition. Rows written `-h, --help: show this help` under an indented `Options:` label were all dropped, and each dropped row counted as prose for the next. A sentence can only wrap from a line that has not ended, so the line above has to run on: it must not end in `.`, `:`, `!` or `?`.
- **Long column.** A row at a name column with a description of its own was a definition even at the description column of the row above. `      -1  means no limit.` under `  -n  Number of results to return.` became a flag. A wrapped description line is never a definition, so that exception is removed. Under a definition with no description, a line at a name column is a definition only when it declares a name, so a bulleted description (`- fast: skip verification`) stays with its flag.

The definitions read from the 71 fixtures and from the 910 registry captures are unchanged by this commit.
…ts names

nmap closes a row's names with a colon and one space: `-iL <inputfilename>: Input from list of hosts/networks`. Under its `Ex: scanme.nmap.org, ...` line, which does not end a sentence, the wrapped-sentence rule read `-iL`, `-iR`, `--exclude` and `--excludefile` as the rest of that sentence, and each dropped row counted as prose for the next. A third review pass found it, and nmap's help lost four of its 87 definitions.

A colon that ends a name or its placeholder now sets the description off, as a gap and the markers `#`, `=`, `--` and `-` already do. All three rules that ask whether a description is set off agree on it: the wrapped-sentence rule, the usage-wrap rule, and the rule for dash-led text at column 0. The last one means python's column-0 rows `--help-env: print help about Python environment variables and exit` are read as definitions.

A header with no description (`-A, --all-namespaces=false:`) and a sentence that carries on after one space (`-m or --module <module>/<mainclass> are passed ...`) are not set off.

A new case pins a sentence that wraps onto two flag-led lines, the only shape that needs a dropped row to count as prose for the next.

The definitions read from the 71 fixtures and the 910 registry captures are unchanged. nmap's usage text (`docs/nmap.usage.txt` upstream, Nmap 7.991SVN) reads 87 definitions again. Of 121 help texts captured on the development machine, one changes: python3 gains `--help-env`, `--help-xoptions` and `--help-all`.
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