From 8c4cb183c4694480607bc74a7a53bcc5744bdeb5 Mon Sep 17 00:00:00 2001 From: Sadig Akhund Date: Sun, 13 Sep 2026 21:10:01 +0400 Subject: [PATCH 1/2] [S-167] stop double-naming usage lines; [S-096] keep the full dashdash value usage_form no longer prepends the node name when the usage line already names it through S-167's own bracket-abbreviated word (moved the reconstruction into mandible-core so the renderer can reach it). The detail pane now shows a node's short-prefix alias as its own line. The `--` end-of-options marker keeps every word of a bare multi-word value instead of truncating at the first space. Co-Authored-By: Claude Fable 5.1 --- mandible-core/src/lib.rs | 4 +- mandible-core/src/node.rs | 28 ++++++ mandible-extract/src/help_text/grammar.rs | 47 ++++++++++ .../help_text/sections/usage_optional_word.rs | 85 ++++++------------- mandible-tui/src/render/detail_pane/mod.rs | 73 ++++++++++++++++ .../src/render/detail_pane/usage_form.rs | 16 +++- 6 files changed, 187 insertions(+), 66 deletions(-) diff --git a/mandible-core/src/lib.rs b/mandible-core/src/lib.rs index 54c9c1c..2175394 100644 --- a/mandible-core/src/lib.rs +++ b/mandible-core/src/lib.rs @@ -28,7 +28,9 @@ pub use entity::{is_literal_choice_value, Choice, Dashes, Entity, EntityKind, Sp pub use merge::{ merge_entity_lists, merge_nodes, merge_subcommand_lists, pair_aliases, MergeError, }; -pub use node::{is_command_name_shaped, CommandNode, Confession, Example, ValueKind}; +pub use node::{ + is_command_name_shaped, reconstruct_abbrev_word, CommandNode, Confession, Example, ValueKind, +}; pub use noderef::{resolve, resolve_flag, resolve_mut, FlagKey, NodeRef}; pub use provenance::{Authority, Axis, ManFormat, Provenance, Source}; pub use snapshot::{ diff --git a/mandible-core/src/node.rs b/mandible-core/src/node.rs index 6df3856..44406dc 100644 --- a/mandible-core/src/node.rs +++ b/mandible-core/src/node.rs @@ -251,6 +251,34 @@ pub fn is_command_name_shaped(s: &str) -> bool { chars.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '_' | '.' | '-')) } +/// `token` read back to the whole command word it names when `token` is +/// spelled with S-167's own optional-abbreviation bracket — one bracket +/// group opened right after a single leading lowercase letter, holding +/// nothing but lowercase letters, closing at the token's own end +/// (`lldb-server`'s `g[dbserver]` reads back to `gdbserver`). `None` for +/// any other shape. Lives here, not in `mandible-extract`, because +/// `mandible-tui`'s usage-line renderer needs the same read-back +/// (docs/shapes.md S-167, S-151's render path) and only depends on +/// `mandible-core`/`mandible-search` as ordinary dependencies — +/// `mandible-extract` is a dev-dependency there, tests only. +pub fn reconstruct_abbrev_word(token: &str) -> Option { + let mut chars = token.chars(); + let lead = chars.next()?; + if !lead.is_ascii_lowercase() { + return None; + } + let rest = &token[lead.len_utf8()..]; + let inner = rest.strip_prefix('[')?.strip_suffix(']')?; + if inner.is_empty() || inner.contains(['[', ']']) { + return None; + } + if !inner.chars().all(|c| c.is_ascii_lowercase()) { + return None; + } + let whole = format!("{lead}{inner}"); + is_command_name_shaped(&whole).then_some(whole) +} + impl CommandNode { /// A minimal, empty node with the given name and provenance. Useful as /// a starting point for tiers and for tests. diff --git a/mandible-extract/src/help_text/grammar.rs b/mandible-extract/src/help_text/grammar.rs index c5dfc1c..783319d 100644 --- a/mandible-extract/src/help_text/grammar.rs +++ b/mandible-extract/src/help_text/grammar.rs @@ -245,6 +245,21 @@ pub fn parse_flag_spec(input: &str) -> FlagSpec { return spec; } + // S-096's own marker takes its whole remaining row as one value: + // `lldb-server`'s `-- program args` names not one word but the + // phrase `program args`, since everything past `--` is what it is + // saying gets passed through untouched. Only when the marker is + // the row's one and only spelling so far and what follows is a + // bare word run, never an already-structured ``/`[value]` + // spec `try_value` already reads correctly (`cargo fmt`'s own + // `-- ...`). + if is_bare_end_of_options_marker(&spec) && !rest.starts_with(['<', '[', '=']) { + spec.value_name = Some(rest.trim_end().to_string()); + spec.value_kind = ValueKind::Required; + spec.fully_consumed = true; + return spec; + } + // Whatever remains is treated as a value spec: `=VALUE`, ` VALUE`, // `[=VALUE]`, `[VALUE]`, or a bare ``/`VALUE` token. let Some((value_name, kind, tail)) = try_value(rest) else { @@ -763,6 +778,15 @@ fn try_bare_sigil(input: &str) -> Option<(Spelling, &str)> { None } +/// True when `spec`'s only spelling so far is the bare end-of-options +/// marker itself (S-096), never a flag that merely happens to be spelled +/// `--` on some other row this grammar cannot produce. Gates the +/// whole-rest-of-row value capture right above this function's own +/// caller. +fn is_bare_end_of_options_marker(spec: &FlagSpec) -> bool { + matches!(spec.spellings.as_slice(), [s] if s.dashes == Dashes::None && s.name == "--") +} + /// Strips a leading `[no-]`/`[no]` prefix, if present. Recognized /// structurally (content exactly `no`/`no-`), never by tool name. See /// docs/shapes.md S-077. @@ -2755,4 +2779,27 @@ mod tests { assert_eq!(spec.spellings[0].name, "--"); assert_eq!(spec.value_name.as_deref(), Some("")); } + + #[test] + fn a_dashdash_row_with_a_multi_word_bare_value_keeps_every_word() { + // `lldb-server gdbserver`'s real row: "-- program args", where + // "program args" is a phrase, not a placeholder ending at the + // first space. Before this test the second word was dropped + // silently (AGENTS.md §3.9). See docs/shapes.md S-096. + let spec = parse_flag_spec("-- program args"); + assert_eq!(spec.spellings.len(), 1); + assert_eq!(spec.spellings[0].name, "--"); + assert_eq!(spec.value_name.as_deref(), Some("program args")); + assert!(spec.fully_consumed); + } + + #[test] + fn a_dashdash_rows_angle_bracket_value_is_unaffected_by_the_multi_word_capture() { + // The multi-word capture above must never widen past the shape + // `try_value` already reads correctly: an angle-bracketed + // placeholder still stops there, `...` and all handled the way it + // always was. + let spec = parse_flag_spec("-- ..."); + assert_eq!(spec.value_name.as_deref(), Some("")); + } } diff --git a/mandible-extract/src/help_text/sections/usage_optional_word.rs b/mandible-extract/src/help_text/sections/usage_optional_word.rs index 74a3d90..5d2f2f4 100644 --- a/mandible-extract/src/help_text/sections/usage_optional_word.rs +++ b/mandible-extract/src/help_text/sections/usage_optional_word.rs @@ -1,59 +1,30 @@ -//! F11 / docs/shapes.md S-167: a `Usage:` line's leading word is a -//! subcommand spelled with an optional abbreviation suffix — -//! `lldb-server`'s `v[ersion]`, `g[dbserver]`, `p[latform]`. Distinct from -//! S-020's modifier table (`ar`'s `r[ab][f][u]`): there the bracket groups -//! name separate modifier LETTERS glued onto one command letter; here one -//! bracket group spells the rest of ONE whole command word. The node's -//! name and displayed form are that whole word (`gdbserver`); the row's -//! own short prefix (`g`) is kept as an alias, never as `display_name` -//! (docs/design.md §16). Never confused with S-020's own code path. +//! docs/shapes.md S-167: a `Usage:` line's leading word is a subcommand +//! spelled with an optional abbreviation suffix (`lldb-server`'s +//! `g[dbserver]`). Distinct from S-020's modifier table (`ar`'s +//! `r[ab][f][u]`, several LETTERS on one command). The node's name and +//! displayed form are the whole word; the row's short prefix is an alias. use super::heading::{starts_with_tool_name, starts_with_tool_name_spelled_differently}; -use mandible_core::{is_command_name_shaped, CommandNode, Provenance, Source}; +use mandible_core::{CommandNode, Provenance, Source}; + +/// `token` read back to the emitted name (docs/design.md §7 Tier B rule 7, +/// §16), moved to `mandible-core` so `mandible-tui` can share it without a +/// real dependency on this crate; re-exported for every existing caller. +pub use mandible_core::reconstruct_abbrev_word; -/// `token` reads as one command word with an optional-abbreviation suffix -/// only when it carries exactly one bracket group, opened right after a -/// single leading lowercase letter, holding nothing but lowercase letters, -/// and closing at the token's own end. `ar`'s `r[ab][f][u]` fails this (a -/// second group follows the first), so the two shapes never collide. -/// /// Returns `(whole_word, short_alias)`: the full command word with the /// brackets removed (`gdbserver`), and the bare leading letter the row /// spelled as its abbreviation prefix (`g`) — the node's own alias, never /// its displayed name (docs/design.md §16). fn optional_abbrev_word(token: &str) -> Option<(String, String)> { - let mut chars = token.chars(); - let lead = chars.next()?; - if !lead.is_ascii_lowercase() { - return None; - } - let rest = &token[lead.len_utf8()..]; - let inner = rest.strip_prefix('[')?.strip_suffix(']')?; - if inner.is_empty() || inner.contains(['[', ']']) { - return None; - } - if !inner.chars().all(|c| c.is_ascii_lowercase()) { - return None; - } - let whole = format!("{lead}{inner}"); - if !is_command_name_shaped(&whole) { - return None; - } + let whole = reconstruct_abbrev_word(token)?; + let lead = token.chars().next()?; Some((whole, lead.to_string())) } -/// The existence oracle's own reconstruction (docs/design.md §7 Tier B -/// rule 7, §16): `token` read back to the emitted name, or `None` when it -/// is not this exact shape. Only the bracket characters are removed. -pub fn reconstruct_abbrev_word(token: &str) -> Option { - optional_abbrev_word(token).map(|(whole, _)| whole) -} - -/// One recognized row: the tool's own name (in whatever spelling it printed -/// itself under), then a word matched by [`optional_abbrev_word`], then -/// zero or more further tokens that must each be a single bracketed -/// lowercase word (`[options]`) — anything else and the whole row is -/// refused rather than partially accepted. +/// One recognized row: the tool's own name, a word matched by +/// [`optional_abbrev_word`], then zero or more `[options]`-shaped tokens — +/// anything else refuses the whole row. fn parse_row(line: &str, tool_name: &str) -> Option<(String, String)> { let t = line.trim(); let is_own_name = starts_with_tool_name(t, tool_name) @@ -76,19 +47,14 @@ fn parse_row(line: &str, tool_name: &str) -> Option<(String, String)> { Some((name, alias)) } -/// The fewest recognized rows before this shape is trusted at all: one row -/// alone is too cheap a coincidence to act on. `lldb-server` documents -/// three. See docs/design.md §16 — below AGENTS.md §3.1's five-tool bar, -/// shipped as a recorded exception the way S-103/S-104/S-143 were. +/// Fewest recognized rows before this shape is trusted (docs/design.md +/// §16, below AGENTS.md §3.1's five-tool bar, a recorded exception). const MIN_ROWS: usize = 2; -/// Scan the labelled `Usage:` block starting at `heading_idx` (the bare -/// `Usage:` line itself, with nothing else on it) for this shape. -/// `Some((end, nodes))` only when every line from `heading_idx + 1` parses -/// as a row up to the first line that does not, and at least [`MIN_ROWS`] -/// did — a mixed or under-populated run is refused whole, never partially -/// accepted, so an ordinary usage synopsis is never swallowed by a guess. -/// `end` is the index of the first line NOT consumed. +/// Scan the labelled `Usage:` block at `heading_idx` for this shape. +/// `Some((end, nodes))` only when every line parses as a row up to the +/// first that doesn't, and at least [`MIN_ROWS`] did — refused whole, +/// never partially accepted. `end` is the first line NOT consumed. pub(super) fn scan_usage_optional_word_table( lines: &[&str], heading_idx: usize, @@ -116,11 +82,8 @@ pub(super) fn scan_usage_optional_word_table( .into_iter() .map(|(name, alias)| { let mut node = CommandNode::new(name.clone(), Provenance::single(Source::HelpText)); - // Invocation-attested, never heading-attested (§7 Tier B rule - // 8), but this recognizer's own third bit admits a probe of - // the full word anyway (§6 rule 0, docs/design.md §16). The - // row's short prefix is kept as an alias, never as a display - // spelling. + // invocation_attested, never heading_attested (§7 rule 8); + // abbrev_probe_attested admits a probe anyway (§6 rule 0). node.invocation_attested = true; node.heading_attested = false; node.children_filled = false; diff --git a/mandible-tui/src/render/detail_pane/mod.rs b/mandible-tui/src/render/detail_pane/mod.rs index 6871f32..aa31fd5 100644 --- a/mandible-tui/src/render/detail_pane/mod.rs +++ b/mandible-tui/src/render/detail_pane/mod.rs @@ -530,6 +530,16 @@ fn build_lines( } } + // S-167 (docs/design.md §16): the node's own short prefix (`g` for + // `gdbserver`) is kept as `CommandNode::aliases`, never as a second + // display spelling, so it renders nowhere unless something reads this + // field. A one-line subtitle right under the summary is the smallest + // surface that still makes it reachable without touching the commands + // tree's own row layout (spec §9.1). + if !node.aliases.is_empty() { + lines.push(Line::from(format!("alias: {}", node.aliases.join(", ")))); + } + if let Some(description) = &node.description { open_block(&mut lines, SECTION_BLANKS); lines.push(heading_line_ruled( @@ -1164,6 +1174,49 @@ mod tests { ); } + /// S-167: the row's own short prefix (`g` for `gdbserver`) is kept as + /// `CommandNode::aliases`, never as a display spelling, so it rendered + /// nowhere until this line. Fixture: `corpus/lldb-server/18.1.3`. + #[test] + fn a_nodes_alias_renders_as_its_own_line() { + let mut node = node_with_flags(); + node.aliases = vec!["g".to_string()]; + let built = build_lines( + &node, + 80, + style::Palette::extended(), + None, + crate::glyphs::UNICODE, + &test_app(), + ); + let text: Vec = built.lines.iter().map(text_of).collect(); + assert!( + text.iter().any(|l| l.trim() == "alias: g"), + "the alias must render as its own line: {text:?}" + ); + } + + /// The anti-case: a node with no aliases renders no `alias:` line at + /// all, so the addition above never shows up as noise on the vast + /// majority of nodes that don't carry one. + #[test] + fn a_node_with_no_aliases_renders_no_alias_line() { + let node = node_with_flags(); + let built = build_lines( + &node, + 80, + style::Palette::extended(), + None, + crate::glyphs::UNICODE, + &test_app(), + ); + let text: Vec = built.lines.iter().map(text_of).collect(); + assert!( + !text.iter().any(|l| l.trim().starts_with("alias:")), + "no alias line without aliases: {text:?}" + ); + } + /// The anti-case at the render layer: ordinary hard-wrapped prose has /// no preserved breaks to honour, so it still reflows to the pane's /// width as one paragraph. @@ -3746,6 +3799,26 @@ mod tests { ); } + /// S-167: the node's own USAGE line names it through the bracket- + /// abbreviated spelling the tool actually printed (`g[dbserver]`), + /// not the plain word `usage_naming_span` looked for before. Without + /// the fix this doubled the node's name in front of text that already + /// named both the tool and the subcommand + /// (`gdbserver lldb-server g[dbserver] [options] ...`); the repair + /// substitutes the bracketed word with the full one instead, the same + /// way `word_names_node`'s other cases already do. + #[test] + fn a_usage_form_substitutes_the_nodes_own_bracket_abbreviated_word() { + assert_eq!( + usage_form( + "gdbserver", + "lldb-server g[dbserver] [options] [[host]:port] [[--] program args...]" + ) + .1, + "lldb-server gdbserver [options] [[host]:port] [[--] program args...]" + ); + } + /// The other direction, which is why the fix can't just delete the /// prepending: some tools print usage with no command name in it at /// all (`Usage: [OPTIONS] FILE`), and mandible adds the name so the diff --git a/mandible-tui/src/render/detail_pane/usage_form.rs b/mandible-tui/src/render/detail_pane/usage_form.rs index bb3f3c9..53eb5de 100644 --- a/mandible-tui/src/render/detail_pane/usage_form.rs +++ b/mandible-tui/src/render/detail_pane/usage_form.rs @@ -1,9 +1,9 @@ //! The USAGE section's own line shaping: strip the redundant label and //! program word, and decide when a leading word IS this program. -//! docs/shapes.md S-108, S-150, S-151. +//! docs/shapes.md S-108, S-150, S-151, S-167. use crate::sanitize::defensive_single_line; -use mandible_core::Text; +use mandible_core::{reconstruct_abbrev_word, Text}; /// One usage line with its redundant prefix stripped: the `Usage:` or /// `or:` label, and the program word the heading already names. Returns @@ -135,13 +135,21 @@ pub(super) fn usage_naming_span(text: &str, name: &str) -> Option<(usize, usize) } /// Whether `word` names the node: an exact match, its basename after the -/// last `/` (`cp`'s `/usr/bin/cp`), or the node name's own prefix before -/// its first `.` (`vim.basic`'s own `vim`). +/// last `/` (`cp`'s `/usr/bin/cp`), the node name's own prefix before its +/// first `.` (`vim.basic`'s own `vim`), or S-167's own bracket-abbreviated +/// spelling read back to the whole word it names (`g[dbserver]` for node +/// `gdbserver`) — without this, the usage line's own leading word never +/// matches the node under `usage_naming_span`, so `usage_form` falls +/// through to prepending a second, redundant `gdbserver` in front of text +/// that already names both the tool and the subcommand. fn word_names_node(word: &str, name: &str) -> bool { let basename = word.rsplit('/').next().unwrap_or(word); if basename == name { return true; } + if reconstruct_abbrev_word(basename).as_deref() == Some(name) { + return true; + } match name.split_once('.') { Some((prefix, _)) if !prefix.is_empty() => basename == prefix, _ => false, From a46ab72e5d80835978de9a9e7f019e072d0f2b9e Mon Sep 17 00:00:00 2001 From: Sadig Akhund Date: Sun, 13 Sep 2026 21:10:07 +0400 Subject: [PATCH 2/2] corpus: capture lldb-server gdbserver's own document Second [[capture]] for corpus/lldb-server/18.1.3 pins the gdbserver node's USAGE line and its `--` row's value. Atlas and changelog updated. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 6 ++ corpus/lldb-server/18.1.3/expected.snap | 90 ++++++++++++++++++++ corpus/lldb-server/18.1.3/gdbserver.help.txt | 32 +++++++ corpus/lldb-server/18.1.3/meta.toml | 16 ++++ docs/shapes.md | 25 +++++- 5 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 corpus/lldb-server/18.1.3/gdbserver.help.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e05ad6..69b4bae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ once it reaches a published 0.1.0 release. ## [Unreleased] +### Fixed + +- [S-167] An S-167 node's own USAGE line no longer doubles its name in front of text that already named the tool and the subcommand (`mandible lldb-server` into `gdbserver`). +- [S-096] A `--` end-of-options row whose value is a bare multi-word phrase keeps every word instead of the first (`mandible lldb-server gdbserver`'s `-- program args`). +- [S-167] A node's own short-prefix alias now renders as an `alias:` line in the detail pane (`mandible lldb-server gdbserver` shows `alias: g`). + ## [0.8.0] - 2026-09-14 ### Changed diff --git a/corpus/lldb-server/18.1.3/expected.snap b/corpus/lldb-server/18.1.3/expected.snap index 120cc94..9e2b31a 100644 --- a/corpus/lldb-server/18.1.3/expected.snap +++ b/corpus/lldb-server/18.1.3/expected.snap @@ -17,9 +17,99 @@ subcommands: - name: gdbserver aliases: - g + description: 'OVERVIEW: lldb-server' + usage: + - 'USAGE: lldb-server g[dbserver] [options] [[host]:port] [[--] program args...]' + flags: + - spellings: + - --fd + value_name: + value_kind: Required + group: 'CONNECTION:' + description: Communicate over the given file descriptor. + provenance: + sources: + - help-text + - spellings: + - --named-pipe + value_name: + value_kind: Required + group: 'CONNECTION:' + description: Write port lldb-server will listen on to the given named pipe. + provenance: + sources: + - help-text + - spellings: + - --pipe + value_name: + value_kind: Required + group: 'CONNECTION:' + description: Write port lldb-server will listen on to the given file descriptor. + provenance: + sources: + - help-text + - spellings: + - --reverse-connect + group: 'CONNECTION:' + description: Connect to the client instead of passively waiting for a connection. In this case [host]:port denotes the remote address to connect to. + provenance: + sources: + - help-text + - spellings: + - --help + group: 'GENERAL OPTIONS:' + description: Prints out the usage information for lldb-server. + provenance: + sources: + - help-text + - spellings: + - --log-channels + value_name: + value_kind: Required + group: 'GENERAL OPTIONS:' + description: Channels to log. A colon-separated list of entries. Each entry starts with a channel followed by a space-separated list of categories. + provenance: + sources: + - help-text + - spellings: + - --log-file + value_name: + value_kind: Required + group: 'GENERAL OPTIONS:' + description: Destination file to log to. If empty, log to stderr. + provenance: + sources: + - help-text + - spellings: + - --setsid + group: 'GENERAL OPTIONS:' + description: Run lldb-server in a new session. + provenance: + sources: + - help-text + - spellings: + - --attach + value_name: + value_kind: Required + group: 'TARGET SELECTION:' + description: Attach to the process given by a (numeric) process id or a name. + provenance: + sources: + - help-text + - spellings: + - -- + value_name: program args + value_kind: Required + group: 'TARGET SELECTION:' + description: Launch program for debugging. + provenance: + sources: + - help-text provenance: sources: - help-text + confidence: 0.45 + children_filled: true invocation_attested: true abbrev_probe_attested: true - name: platform diff --git a/corpus/lldb-server/18.1.3/gdbserver.help.txt b/corpus/lldb-server/18.1.3/gdbserver.help.txt new file mode 100644 index 0000000..9192c79 --- /dev/null +++ b/corpus/lldb-server/18.1.3/gdbserver.help.txt @@ -0,0 +1,32 @@ +OVERVIEW: lldb-server + +USAGE: lldb-server g[dbserver] [options] [[host]:port] [[--] program args...] + +CONNECTION: + --fd Communicate over the given file descriptor. + --named-pipe Write port lldb-server will listen on to the given named pipe. + --pipe Write port lldb-server will listen on to the given file descriptor. + --reverse-connect Connect to the client instead of passively waiting for a connection. In this case [host]:port denotes the remote address to connect to. + +GENERAL OPTIONS: + --help Prints out the usage information for lldb-server. + --log-channels + Channels to log. A colon-separated list of entries. Each entry starts with a channel followed by a space-separated list of categories. + --log-file Destination file to log to. If empty, log to stderr. + --setsid Run lldb-server in a new session. + +TARGET SELECTION: + --attach Attach to the process given by a (numeric) process id or a name. + -- program args Launch program for debugging. + +DESCRIPTION + lldb-server connects to the LLDB client, which drives the debugging session. + If no connection options are given, the [host]:port argument must be present + and will denote the address that lldb-server will listen on. [host] defaults + to "localhost" if empty. Port can be zero, in which case the port number will + be chosen dynamically and written to destinations given by --named-pipe and + --pipe arguments. + + If no target is selected at startup, lldb-server can be directed by the LLDB + client to launch or attach to a process. + diff --git a/corpus/lldb-server/18.1.3/meta.toml b/corpus/lldb-server/18.1.3/meta.toml index bba818a..03595c4 100644 --- a/corpus/lldb-server/18.1.3/meta.toml +++ b/corpus/lldb-server/18.1.3/meta.toml @@ -8,6 +8,14 @@ # flags; this frozen fixture captures only the root parse, with no # subprocess, so it cannot show that fill. The help text sits entirely on # stderr; stdout is empty. +# +# The second capture is the `gdbserver` node's own document (stdout this +# time, stderr empty). It pins two repairs: its USAGE line no longer +# double-names the node (`gdbserver lldb-server g[dbserver] ...`, S-167's +# own render fix in `mandible-tui`'s `usage_form`), and its `-- program +# args` row recovers as the end-of-options marker `--` with value +# `program args`, never a fabricated flag `--program` (S-096's own +# widened value capture in `mandible-extract`'s `grammar.rs`). [bless] provenance = "agent" @@ -24,6 +32,11 @@ stdout = "help.txt" stderr = "help.stderr.txt" exit_code = 0 +[[capture]] +argv = ["lldb-server", "gdbserver", "--help"] +stdout = "gdbserver.help.txt" +exit_code = 0 + [contract] expected_framework = "generic" min_subcommands = 3 @@ -32,3 +45,6 @@ min_subcommands = 3 version = "version" gdbserver = "gdbserver" platform = "platform" + +[contract.must_contain_flags_by_path] +gdbserver = ["--fd", "--named-pipe", "--pipe", "--reverse-connect", "--help", "--log-channels", "--log-file", "--setsid", "--attach", "--"] diff --git a/docs/shapes.md b/docs/shapes.md index 50a0439..fd5cf5d 100644 --- a/docs/shapes.md +++ b/docs/shapes.md @@ -1592,7 +1592,7 @@ entry's `tools` field and nothing else. It does not get a new entry. - id: S-096 - looks like: | -- Only file names after this -- tools: vim.basic, nvim +- tools: vim.basic, nvim, lldb-server, lldb-server-18 - handling: Fixed. `parse_flag_spec`'s `try_bare_sigil` reads a bare `--` fragment as spelling `--` (`Dashes::None`, so it renders verbatim). Only a real terminator (nothing left, or whitespace/an alias @@ -1600,7 +1600,14 @@ entry's `tools` field and nothing else. It does not get a new entry. may follow the marker; glued onto more name-shaped text (`objdump`'s `--[section-]headers` optional-bracket-prefix convention) it is left alone, so the marker is never fabricated out of an unrelated long name's - own unread tail. + own unread tail. Revised: the value that follows the marker used to + truncate at its first space, dropping every later word — `lldb-server + gdbserver`'s `-- program args` kept `program` and silently lost `args` + (AGENTS.md §3.9). A row whose only spelling so far is the bare marker, + followed by a bare word run (never an already-structured ``/ + `[value]` spec `try_value` already reads correctly), now takes the + whole remaining row as one value; `cargo fmt`'s own angle-bracket case + is untouched. - fleet: `end-of-options-marker` (`xtask/src/end_of_options_marker.rs`) fell from 26 tool(s)/26 finding(s) to 0/0 in a full-PATH sweep, 2026-09-03 (`step3-sweepdiff-plus-prefixed-option.txt`): 0 losses. Ratchet-gated at @@ -3571,7 +3578,19 @@ entry's `tools` field and nothing else. It does not get a new entry. nothing else changed, which is why design §7 Tier B rule 7's existence oracle needed a narrow amendment (§16): `gdbserver` is not a contiguous substring of the raw text, only `g[dbserver]` is, so the oracle now also - attests a subcommand name reached this way. + attests a subcommand name reached this way. Revised twice more. The + probed child's own USAGE line used to gain a second, redundant copy of + the node's name in front of text that already named both the tool and + the subcommand (`gdbserver lldb-server g[dbserver] [options] ...`): + `mandible-tui`'s `usage_form::word_names_node` now also recognizes a + usage line's own bracket-abbreviated leading word as naming the node, + through `mandible_core::reconstruct_abbrev_word` (moved there from + `mandible-extract` so the renderer can reach it without a real + dependency on that crate), so the existing S-108 substitution path + replaces `g[dbserver]` with `gdbserver` in place instead of prepending. + And the row's own short prefix, kept as `CommandNode::aliases` per the + ruling above, rendered nowhere: the detail pane now prints an `alias:` + line under the node's summary when `aliases` is non-empty. - fleet: `usage-optional-word-table` (`xtask/src/usage_optional_word_table.rs`) named 10 tools/17 findings as a raw shape before the round. What moved on a full-`PATH` sweep of 2323