From 78adeb1ec72b2a9384bd885d14ccf7cbfa63af06 Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 9 Oct 2026 10:18:36 -0500 Subject: [PATCH 1/2] test(audit): capture a required option from typer and rich-click typer and rich-click mark a required option with `*` in the first cell of its row. Neither probe declared one, so no fixture held that row. Both probes gain a required `-t, --target`. The snapshots record what the parser reads from the new captures today: every row but that one. The probe image is rebuilt for the new sources, and its Dockerfile now installs the Thor gem from a file fetched by checksum, as it already does for the picocli jar. Thor was the one dependency of the image installed without an integrity check. The other 15 probe captures come out of the rebuilt image byte for byte. --- .../snapshots/probe-rich-click__--help.snap | 6 +++--- .../snapshots/probe-typer__--help.snap | 8 ++++---- tests/fixtures/help/index.json | 8 +++++--- tests/fixtures/help/probe-rich-click__--help.txt | 14 ++++++++------ tests/fixtures/help/probe-typer__--help.txt | 16 +++++++++------- tests/fixtures/help/probes/Dockerfile | 4 +++- .../fixtures/help/probes/python/py_richclick.py | 1 + tests/fixtures/help/probes/python/py_typer.py | 3 ++- 8 files changed, 35 insertions(+), 25 deletions(-) diff --git a/src/runner/help_probe/snapshots/probe-rich-click__--help.snap b/src/runner/help_probe/snapshots/probe-rich-click__--help.snap index 66864e8..45f53ee 100644 --- a/src/runner/help_probe/snapshots/probe-rich-click__--help.snap +++ b/src/runner/help_probe/snapshots/probe-rich-click__--help.snap @@ -4,6 +4,6 @@ input_file: tests/fixtures/help/probe-rich-click__--help.txt --- 7 | --force, -f | skip confirmation prompts 8 | --limit, -n | maximum number of results to return, a deliberately long des - 10 | --dry-run | print what would change - 11 | --print-config-dir, -cd | multi-letter single-dash alias - 12 | --help | Show this message and exit. + 11 | --dry-run | print what would change + 12 | --print-config-dir, -cd | multi-letter single-dash alias + 14 | --help | Show this message and exit. diff --git a/src/runner/help_probe/snapshots/probe-typer__--help.snap b/src/runner/help_probe/snapshots/probe-typer__--help.snap index 3ff61af..521ae68 100644 --- a/src/runner/help_probe/snapshots/probe-typer__--help.snap +++ b/src/runner/help_probe/snapshots/probe-typer__--help.snap @@ -4,7 +4,7 @@ input_file: tests/fixtures/help/probe-typer__--help.txt --- 7 | --force, -f | skip confirmation prompts 8 | --limit, -n | maximum number of results to return, a deliberately long des - 10 | --dry-run | print what would change - 11 | --print-config-dir, -cd | multi-letter single-dash alias - 12 | --output, -o | write output here - 13 | --help | Show this message and exit. + 11 | --dry-run | print what would change + 12 | --print-config-dir, -cd | multi-letter single-dash alias + 13 | --output, -o | write output here + 15 | --help | Show this message and exit. diff --git a/tests/fixtures/help/index.json b/tests/fixtures/help/index.json index a735e34..993dba2 100644 --- a/tests/fixtures/help/index.json +++ b/tests/fixtures/help/index.json @@ -2,7 +2,7 @@ "probe_env": {"NO_COLOR": "1", "TERM": "dumb", "COLUMNS": "80", "PAGER": "cat"}, "images": { "scorer": {"id": "sha256:05db16cf226cd14a93a2682aea41b72d3e6b4d240cadba006f2881d3ffa996b3", "dockerfile": "agentnative-site:docker/score/Dockerfile"}, - "probes": {"id": "sha256:f1626f912f98f5152d1c34dc6ece1765025ccd4ee331ab479b34580b323fd327", "dockerfile": "probes/Dockerfile"} + "probes": {"id": "sha256:40f5b1b77bc4f747fb27d9ccba260ad7b7abdd27477be88eaa3eb2732c3e5b24", "dockerfile": "probes/Dockerfile"} }, "fixtures": [ { @@ -692,7 +692,8 @@ ["-f", "--force"], ["-n", "--limit"], ["--dry-run"], - ["-cd", "--print-config-dir"] + ["-cd", "--print-config-dir"], + ["-t", "--target"] ] }, { @@ -711,7 +712,8 @@ ["-n", "--limit"], ["--dry-run"], ["-cd", "--print-config-dir"], - ["-o", "--output"] + ["-o", "--output"], + ["-t", "--target"] ] }, { diff --git a/tests/fixtures/help/probe-rich-click__--help.txt b/tests/fixtures/help/probe-rich-click__--help.txt index d43d3d6..8e4e835 100644 --- a/tests/fixtures/help/probe-rich-click__--help.txt +++ b/tests/fixtures/help/probe-rich-click__--help.txt @@ -4,10 +4,12 @@ rich-click probe ╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --force -f skip confirmation prompts │ -│ --limit -n INTEGER maximum number of results to return, a │ -│ deliberately long description that wraps │ -│ --dry-run print what would change │ -│ --print-config-dir -cd multi-letter single-dash alias │ -│ --help Show this message and exit. │ +│ --force -f skip confirmation prompts │ +│ --limit -n INTEGER maximum number of results to return, a │ +│ deliberately long description that │ +│ wraps │ +│ --dry-run print what would change │ +│ --print-config-dir -cd multi-letter single-dash alias │ +│ * --target -t TEXT where to deploy [required] │ +│ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ diff --git a/tests/fixtures/help/probe-typer__--help.txt b/tests/fixtures/help/probe-typer__--help.txt index 61ccdda..0ed6bd5 100644 --- a/tests/fixtures/help/probe-typer__--help.txt +++ b/tests/fixtures/help/probe-typer__--help.txt @@ -4,12 +4,14 @@ typer probe ╭─ Options ────────────────────────────────────────────────────────────────────╮ -│ --force -f skip confirmation prompts │ -│ --limit -n maximum number of results to return, a │ -│ deliberately long description that wraps │ -│ --dry-run print what would change │ -│ --print-config-dir -cd multi-letter single-dash alias │ -│ --output -o write output here │ -│ --help Show this message and exit. │ +│ --force -f skip confirmation prompts │ +│ --limit -n maximum number of results to return, │ +│ a deliberately long description that │ +│ wraps │ +│ --dry-run print what would change │ +│ --print-config-dir -cd multi-letter single-dash alias │ +│ --output -o write output here │ +│ * --target -t where to deploy [required] │ +│ --help Show this message and exit. │ ╰──────────────────────────────────────────────────────────────────────────────╯ diff --git a/tests/fixtures/help/probes/Dockerfile b/tests/fixtures/help/probes/Dockerfile index 022773f..69b4d91 100644 --- a/tests/fixtures/help/probes/Dockerfile +++ b/tests/fixtures/help/probes/Dockerfile @@ -39,7 +39,9 @@ COPY python/requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir --require-hashes --requirement /tmp/requirements.txt FROM ruby:4.0.7-slim-trixie@sha256:073f6464ca7c0b66fa86715ec57339a39da08fdc803583140edc66e486af9aeb -RUN gem install thor --version 1.5.0 --no-document +ADD --chmod=644 --checksum=sha256:e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 \ + https://rubygems.org/downloads/thor-1.5.0.gem /tmp/thor-1.5.0.gem +RUN gem install --local --no-document /tmp/thor-1.5.0.gem COPY --from=python312 /usr/local/ /usr/local/ COPY --from=python313 /usr/local/ /usr/local/ diff --git a/tests/fixtures/help/probes/python/py_richclick.py b/tests/fixtures/help/probes/python/py_richclick.py index 99b12f0..2b3558a 100644 --- a/tests/fixtures/help/probes/python/py_richclick.py +++ b/tests/fixtures/help/probes/python/py_richclick.py @@ -4,6 +4,7 @@ @click.option("-n", "--limit", type=int, help="maximum number of results to return, a deliberately long description that wraps") @click.option("--dry-run", is_flag=True, help="print what would change") @click.option("-cd", "--print-config-dir", is_flag=True, help="multi-letter single-dash alias") +@click.option("-t", "--target", required=True, help="where to deploy") def main(**kw): """rich-click probe""" main() diff --git a/tests/fixtures/help/probes/python/py_typer.py b/tests/fixtures/help/probes/python/py_typer.py index fef2afa..f888adb 100644 --- a/tests/fixtures/help/probes/python/py_typer.py +++ b/tests/fixtures/help/probes/python/py_typer.py @@ -6,6 +6,7 @@ def main(force: bool = typer.Option(False, "-f", "--force", help="skip confirmat limit: Optional[int] = typer.Option(None, "-n", "--limit", help="maximum number of results to return, a deliberately long description that wraps"), dry_run: bool = typer.Option(False, "--dry-run", help="print what would change"), cd: bool = typer.Option(False, "-cd", "--print-config-dir", help="multi-letter single-dash alias"), - output: Optional[str] = typer.Option(None, "-o", "--output", help="write output here")): + output: Optional[str] = typer.Option(None, "-o", "--output", help="write output here"), + target: str = typer.Option(..., "-t", "--target", help="where to deploy")): """typer probe""" app() From bfebab609d179bc836d8a49042826d0e2fa9079d Mon Sep 17 00:00:00 2001 From: Brett Date: Fri, 9 Oct 2026 10:20:59 -0500 Subject: [PATCH 2/2] fix(audit): read typer's and rich-click's required options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit typer and rich-click mark a required option with `*` in the first cell of its box-table row: │ * --target -t where to deploy [required] │ A row has to lead with a flag name to be a definition, so that row declared nothing while the rows around it were read. A tool's required flags were the ones `anc` could not see. The text cleanup that turns box edges into gaps now blanks a `*` that leads a box-table row and stands in a cell of its own. The row then leads with its name. A `*` outside a box table, or one that is part of a word (`*.md`), is left alone. --- README.md | 7 ++-- src/runner/help_probe/flags/classify.rs | 9 +++++ src/runner/help_probe/flags/normalize.rs | 40 ++++++++++++++++++- .../snapshots/probe-rich-click__--help.snap | 1 + .../snapshots/probe-typer__--help.snap | 1 + 5 files changed, 53 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 08ced24..e49e296 100644 --- a/README.md +++ b/README.md @@ -451,9 +451,10 @@ start is a definition, not that description: `-x, --long` puts the long name fou `--format ` under an undescribed `-x` declares `--format`. A definition can sit at any indent. rsync, ffmpeg and Miller print theirs at column 0, typer and broot print them as -rows of a box table with the short name in a cell of its own, Thor brackets its long names (`-f, [--force]`), and fzf -has shorts that start with a plus (`+x, --no-extended`). Text is read as a terminal shows it: without ANSI escapes, with -groff's overstruck bold collapsed, and with TABs expanded. +rows of a box table with the short name in a cell of its own (typer marks a required option with `*` in a first cell, +which is not part of the name), Thor brackets its long names (`-f, [--force]`), and fzf has shorts that start with a +plus (`+x, --no-extended`). Text is read as a terminal shows it: without ANSI escapes, with groff's overstruck bold +collapsed, and with TABs expanded. Two shapes need more than a leading dash, because usage lines wrap into them. Dash-led text at column 0 is a definition only when a gap or a marker sets a description off from its names, or its description starts at the column the diff --git a/src/runner/help_probe/flags/classify.rs b/src/runner/help_probe/flags/classify.rs index 2340b52..a0cf8ec 100644 --- a/src/runner/help_probe/flags/classify.rs +++ b/src/runner/help_probe/flags/classify.rs @@ -484,6 +484,11 @@ Advanced: "╭─ Options ────────────────────────────────────────────╮\n│ --force -f skip confirmation prompts │\n│ --limit -n maximum number of results to return, a │\n│ deliberately long description that wraps │\n│ --dry-run print what would change │\n╰──────────────────────────────────────────────────────╯\n", &[&["--force", "-f"], &["--limit", "-n"], &["--dry-run"]], ), + ( + "typer 0.27.2: a required option, marked `*` in the first cell of its row", + "╭─ Options ────────────────────────────────────────────╮\n│ --output -o write output here │\n│ * --target -t where to deploy [required] │\n│ --help Show this message and exit. │\n╰──────────────────────────────────────────────────────╯\n", + &[&["--output", "-o"], &["--target", "-t"], &["--help"]], + ), ( "broot 1.56: a box table whose cells touch their edges", "│ -d │--dates │Show the last modified date of files │\n│ │--conf │Semicolon separated paths to specific │\n│ │ │config files │\n", @@ -753,6 +758,10 @@ Advanced: let typer = declared(&fixture("probe-typer__--help.txt")); assert!(typer.contains(&vec!["--limit".to_string(), "-n".to_string()])); + assert!(typer.contains(&vec!["--target".to_string(), "-t".to_string()])); + + let rich_click = declared(&fixture("probe-rich-click__--help.txt")); + assert!(rich_click.contains(&vec!["--target".to_string(), "-t".to_string()])); } #[test] diff --git a/src/runner/help_probe/flags/normalize.rs b/src/runner/help_probe/flags/normalize.rs index a109fa5..30b6966 100644 --- a/src/runner/help_probe/flags/normalize.rs +++ b/src/runner/help_probe/flags/normalize.rs @@ -9,13 +9,16 @@ const BELL: char = '\u{7}'; /// `raw`, line for line, with ANSI escape sequences removed, each overstruck /// character reduced to the one printed last (groff bold is `X\bX`), TABs /// expanded to the next stop of eight, and each vertical edge of a box table -/// turned into a gap. Lines keep their order and count. +/// turned into a gap. A box-table row that leads with a `*` cell, where typer +/// and rich-click mark a required option, has the marker blanked, so the row +/// leads with its name. Lines keep their order and count. pub(super) fn normalize(raw: &str) -> String { raw.lines().map(shown).collect::>().join("\n") } fn shown(line: &str) -> String { let mut out: Vec = Vec::with_capacity(line.len()); + let mut boxed = false; let mut chars = line.chars(); while let Some(c) = chars.next() { match c { @@ -27,13 +30,30 @@ fn shown(line: &str) -> String { let stop = (out.len() / TAB_STOP + 1) * TAB_STOP; out.resize(stop, ' '); } - '│' | '┃' | '║' => out.extend([' ', ' ']), + '│' | '┃' | '║' => { + boxed = true; + out.extend([' ', ' ']); + } _ => out.push(c), } } + if boxed { + blank_required_marker(&mut out); + } out.into_iter().collect() } +/// Blank a `*` that leads the row and stands in a cell of its own. +fn blank_required_marker(row: &mut [char]) { + let Some(first) = row.iter().position(|c| !c.is_whitespace()) else { + return; + }; + let alone = row.get(first + 1..first + 3) == Some(&[' ', ' ']); + if row[first] == '*' && alone { + row[first] = ' '; + } +} + /// Consume the rest of an escape sequence: a CSI sequence through its final /// byte, an OSC sequence through its terminator, or one character. fn skip_escape(chars: &mut std::str::Chars<'_>) { @@ -99,6 +119,22 @@ mod tests { ); } + #[test] + fn a_required_marker_in_a_box_table_becomes_a_space() { + assert_eq!( + normalize("│ * --target -t where to deploy [required] │"), + " --target -t where to deploy [required] " + ); + assert_eq!( + normalize(" * --all: every item, as a bullet"), + " * --all: every item, as a bullet" + ); + assert_eq!( + normalize("│ *.md files are read │"), + " *.md files are read " + ); + } + #[test] fn lines_keep_their_count_and_multibyte_text() { let raw = "选项:\n\n -H, --header
自定义请求头\n"; diff --git a/src/runner/help_probe/snapshots/probe-rich-click__--help.snap b/src/runner/help_probe/snapshots/probe-rich-click__--help.snap index 45f53ee..09132f2 100644 --- a/src/runner/help_probe/snapshots/probe-rich-click__--help.snap +++ b/src/runner/help_probe/snapshots/probe-rich-click__--help.snap @@ -6,4 +6,5 @@ input_file: tests/fixtures/help/probe-rich-click__--help.txt 8 | --limit, -n | maximum number of results to return, a deliberately long des 11 | --dry-run | print what would change 12 | --print-config-dir, -cd | multi-letter single-dash alias + 13 | --target, -t | where to deploy [required] 14 | --help | Show this message and exit. diff --git a/src/runner/help_probe/snapshots/probe-typer__--help.snap b/src/runner/help_probe/snapshots/probe-typer__--help.snap index 521ae68..a246d67 100644 --- a/src/runner/help_probe/snapshots/probe-typer__--help.snap +++ b/src/runner/help_probe/snapshots/probe-typer__--help.snap @@ -7,4 +7,5 @@ input_file: tests/fixtures/help/probe-typer__--help.txt 11 | --dry-run | print what would change 12 | --print-config-dir, -cd | multi-letter single-dash alias 13 | --output, -o | write output here + 14 | --target, -t | where to deploy [required] 15 | --help | Show this message and exit.