Skip to content

feat(spec): make unknown flags configurable, and keep them as values - #810

Merged
jdx merged 5 commits into
agent/mount-docsfrom
agent/unknown-flags
Aug 11, 2026
Merged

feat(spec): make unknown flags configurable, and keep them as values#810
jdx merged 5 commits into
agent/mount-docsfrom
agent/unknown-flags

Conversation

@jdx

@jdx jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Your call, implemented. An unrecognized flag-like token stays what it has always been here — a word, offered to the positionals — and that is now a documented decision rather than an accident. The grammar said one thing and the parser did another, which was the only indefensible state.

Why the outlier position is the right one

Every comparable parser refuses the token, and they are right for what they do: parse their own argv, where a dash-word is a flag or a typo. A usage spec also parses command lines whose flags it does not own:

  • a shell script run through usage exec, forwarding options to a tool it wraps
  • a task's arguments, where the task script is the authority on what it accepts
  • a completion, asked about a line that is still half-typed

In those, an unrecognized token is data in transit far more often than a mistake, and refusing it breaks the wrapper for anyone who did not enumerate the flags of the program behind it. The grammar page now says this, with the cost stated plainly: a misspelled --hekp becomes an argument, and whether it does depends on whether a positional happens to be free.

Configurable at both levels

unknown_flags "error"               // for the whole CLI
cmd "exec" unknown_flags="value"    // except here, which forwards a command line

Nearest command that states a preference wins, then the spec, then value. Unlike effect this is inherited — it describes how a command line is read rather than what a command does, and a CLI that forwards options tends to forward them everywhere.

From Rust: #[usage(unknown_flags = "error")], which is the case that usually wants it — a compiled binary generally owns every flag it accepts.

The carve-out oclif had to learn

Even when refusing, a lone - and a negative number stay values. oclif/core#600 is that exact regression: they switched to "anything starting with a hyphen is a flag", broke negative-number arguments, and had to special-case them back. The check here skips non-flag-like tokens before it ever consults the setting, and a test pins -1 under unknown_flags "error".

One subtlety worth flagging: the rejection had to go inside the flag branches, where the lookup has just failed and nothing from the token has been applied. Placed downstream, -az set -a before discovering that z names nothing — a partly-applied bundle from a rejected token.

Scope

  • lib: the setting on Spec and SpecCommand, KDL parse and emit, inherited resolution, refusal in both flag branches.
  • argv: lenient by default, with the effective mode stored per command — inheritance is resolved by whoever builds the tables, keeping it out of the parse.
  • derive: the struct attribute, emitted into both the tables and the spec.
  • corpus: six vectors move from expecting an error to expecting a value; five new ones cover strict mode, the per-command override, whole-bundle rejection, and the negative number.
  • docs: /spec/argv#unrecognized-flags explains the reasoning; /spec/reference/cmd documents the knob.

This closes the last of the big recorded divergences. 91 vectors, 10 remaining — and those are now narrow.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.


Note

Medium Risk
Changes default argv parsing for unrecognized flags (reject → treat as values) across both parsers, which can alter existing CLI behavior. Mitigated by an explicit strict mode and broad corpus coverage.

Overview
Unrecognized flag-like tokens are now values by default, offered to positionals like any other word. Specs that wrap or forward argv (scripts, tasks, completions) no longer reject tokens they do not own. CLIs that want typo detection opt in with unknown_flags "error".

The setting lives on both the root spec and individual commands, inherits (nearest enclosing preference wins), and is overridable per command — e.g. strict overall except cmd "exec" unknown_flags="value". Derive exposes the same knob via #[usage(unknown_flags = "error")].

Also tightens number detection so -1e5 / -1.5e-3 stay values while -inf and -1x remain flag-shaped, and ensures unknown short bundles like -az apply none of their letters before falling through or erroring.

Reviewed by Cursor Bugbot for commit 2dd7590. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • New Features

    • Added configurable handling for unrecognized flags, with lenient value handling by default and strict rejection as an option.
    • Supports command-specific overrides, inherited settings, and specification serialization.
    • Unknown short-flag bundles are validated as a whole.
  • Bug Fixes

    • Preserved negative numbers, numeric exponent forms, and lone hyphens as positional values.
    • Prevented partial application of short-flag bundles when validation fails.
  • Documentation

    • Documented unknown-flag behavior, inheritance, precedence, and parsing rules.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds per-command UnknownFlags configuration. Unknown flag-like tokens become positional values by default or raise UnknownFlag in strict mode. The policy propagates through specifications, derive output, recursive command tables, parsing, conformance fixtures, and documentation.

Changes

Unknown flag handling

Layer / File(s) Summary
Policy and specification contract
lib/src/spec/*, lib/src/lib.rs, lib/src/docs/models.rs, argv/src/spec.rs
Adds the public UnknownFlags enum and specification fields. The policy supports validation, inheritance, merging, KDL serialization, and public re-export.
Command and parser behavior
argv/src/lib.rs, lib/src/parse.rs
Adds lenient and strict handling for unknown long flags and short bundles. Strict mode rejects the whole token before partial short-flag processing. Lone hyphens and valid negative numbers remain values.
Derive generation and conformance wiring
derive/src/model.rs, derive/src/codegen.rs, conformance/src/argv.rs
Parses the derive option, emits the resolved policy, and propagates inherited or command-specific settings through recursive command tables.
Conformance fixtures and regression coverage
conformance/tests/*, corpus/01-long-flags.json, corpus/02-short-flags.json, corpus/04-subcommands.json, corpus/05-globals.json
Updates expected default behavior and adds strict-mode, short-bundle, numeric-token, and subcommand-override vectors.
Documentation and reference output
docs/spec/argv.md, docs/spec/reference/cmd.md, docs/cli/reference/commands.json
Documents unknown-flag resolution, inheritance, precedence, numeric-token recognition, and generated unknown_flags metadata.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant DeriveCodegen
  participant CommandTable
  participant Parser
  participant PositionalArguments
  participant UsageError
  CLI->>DeriveCodegen: configure unknown_flags
  DeriveCodegen->>CommandTable: emit effective policy
  CommandTable->>Parser: parse flag-like token
  Parser->>PositionalArguments: forward token in Value mode
  Parser->>UsageError: reject token in Error mode
Loading

Possibly related PRs

  • jdx/usage#797: This PR extends the argv grammar, parser, and conformance corpus introduced there.
  • jdx/usage#798: This PR extends the usage-argv command model and parser introduced there.
  • jdx/usage#801: This PR extends argv specification serialization with per-command unknown_flags.

Poem

A rabbit found a flag in the hay,
“Value,” it said, “may pass this way.”
Strict mode thumped: “You shall not stray!”
Short bundles check every letter,
While tests and docs keep parsing better.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: configurable handling of unknown flags while preserving them as values by default.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread lib/src/parse.rs
@greptile-apps

greptile-apps Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR makes unknown-flag handling configurable and consistently propagates the effective mode through the spec model, generated parser tables, derive output, and serialization.

  • Defaults unknown flags to positional values while supporting inherited strict rejection.
  • Validates short bundles before applying any constituent flag.
  • Aligns numeric-token classification across both parsers and documents the resulting grammar.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
lib/src/parse.rs Implements inherited strict-mode enforcement and complete numeric-token classification, resolving the previously reported digit-prefix bypass.
argv/src/lib.rs Adds per-command unknown-flag behavior and recognizes scientific notation consistently before short-flag parsing.
lib/src/spec/mod.rs Parses, serializes, and merges the top-level unknown-flag mode, preserving it through includes.
lib/src/spec/cmd.rs Adds command-level overrides with parsing, serialization, and merge support.
derive/src/codegen.rs Emits the configured effective mode into generated argv command tables.
conformance/src/argv.rs Resolves inheritance while constructing parser tables for conformance testing.

Reviews (6): Last reviewed commit: "fix(argv): write a subcommand's unknown_..." | Re-trigger Greptile

Comment thread lib/src/parse.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
docs/spec/argv.md (1)

158-161: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Clarify that the current command participates in precedence.

The phrase “nearest enclosing command” can be read as excluding the command that is currently parsing the token. State that the current command or its nearest ancestor wins before the spec-level setting and the value default.

Suggested wording
- The nearest enclosing command that states a preference wins, then the spec, then `value`.
+ The current command or nearest ancestor that states a preference wins, then the spec, then `value`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/spec/argv.md` around lines 158 - 161, Update the precedence description
in the argv documentation to explicitly include the current command alongside
its nearest ancestor: the current command or nearest enclosing ancestor
preference wins first, followed by the spec-level setting, then the value
default. Preserve the existing explanation of inherited option forwarding.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/spec/reference/cmd.md`:
- Around line 97-100: Update the argv grammar link in the surrounding command
documentation to use the source-relative target ../argv.md#unrecognized-flags,
or the documentation builder’s validated equivalent, so markdownlint MD051
passes while preserving the intended fragment.

In `@lib/src/parse.rs`:
- Line 883: Update the short-bundle parsing flow around
reject_unknown_flag_if_asked to pre-scan the entire bundle before applying any
flags, stopping at an unknown letter or value-taking flag. In Error mode, return
InvalidFlag before mutating state; in Value mode, route the unchanged original
token to positional parsing without setting earlier flags. Add regression
coverage for -a, -az, and a positional argument in both strict and lenient
modes.

In `@lib/src/spec/mod.rs`:
- Around line 82-84: Update Spec::merge to merge the spec-level unknown_flags
option using merge_opt!(unknown_flags), preserving an included specification’s
policy. Add a regression test covering an include that sets unknown_flags to
"error" and verifies the merged spec retains that setting.

---

Nitpick comments:
In `@docs/spec/argv.md`:
- Around line 158-161: Update the precedence description in the argv
documentation to explicitly include the current command alongside its nearest
ancestor: the current command or nearest enclosing ancestor preference wins
first, followed by the spec-level setting, then the value default. Preserve the
existing explanation of inherited option forwarding.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 550a71cb-c39e-4234-825e-d293ac2f3b7f

📥 Commits

Reviewing files that changed from the base of the PR and between e27df4f and 8421047.

📒 Files selected for processing (20)
  • argv/src/lib.rs
  • argv/src/spec.rs
  • conformance/src/argv.rs
  • conformance/tests/argv.rs
  • conformance/tests/derive.rs
  • corpus/01-long-flags.json
  • corpus/02-short-flags.json
  • corpus/04-subcommands.json
  • corpus/05-globals.json
  • derive/src/codegen.rs
  • derive/src/model.rs
  • docs/cli/reference/commands.json
  • docs/spec/argv.md
  • docs/spec/reference/cmd.md
  • lib/src/docs/models.rs
  • lib/src/lib.rs
  • lib/src/parse.rs
  • lib/src/spec/cmd.rs
  • lib/src/spec/mod.rs
  • lib/src/spec/unknown_flags.rs

Comment thread docs/spec/reference/cmd.md Outdated
Comment thread lib/src/parse.rs
Comment thread lib/src/spec/mod.rs
@github-actions

github-actions Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Instruction counts

benchmark trend instructions Δ wall (min) Δ
markdown ▁▁▁▁▁▁▁▁▁▁▁▁██████ 148,146,307 → 148,166,987 +0.01% 13.82 → 13.81ms -0.06%
startup ▅▅▆▄▄▄▄▄▄▄▄▄█████▁ 1,204,030 → 1,199,589 -0.37% 0.99 → 0.92ms -6.21%

No instruction-count regression above 1%.

Only instruction counts gate. Wall clock is shown for context — on identical hardware it moves 4-20% run to run.

Measured by tak — instruction-counted CLI benchmarks, stored in this repository's git notes.

2dd75908c41c vs cbdf8265b174 · measured on the runner, not pushed to the history.

Comment thread lib/src/parse.rs Outdated
Comment thread argv/src/lib.rs
@jdx
jdx force-pushed the agent/unknown-flags branch from 4088564 to 8ed7529 Compare August 11, 2026 19:08
Comment thread argv/src/lib.rs
Comment thread argv/src/spec.rs

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Both fixed, and the first one is the best catch on this PR.

The two parsers disagreed about -1e5. usage-lib asked f64::from_str, which takes an exponent; usage-argv used a hand-written byte scanner that stopped at e and called the token a flag. So --offset -1e5 was a value under one parser and a refused unknown flag under the other — precisely the drift the corpus exists to prevent, in the one place the corpus had no vector.

One rule now, spelled out identically on both sides: digits, at most one ., and an optional signed exponent. Narrower than a float parse deliberately — f64 also accepts inf and NaN, and -inf is far likelier to be a misspelled flag than a number somebody meant to pass. Four vectors pin the edges so they cannot drift again:

token reading
-1e5, -1.5e-3 value
-inf, -1e, -1x flag-shaped, names nothing

The grammar page states the rule rather than leaving "a negative number" to interpretation.

unknown_flags was not emitted for subcommands. The tables carry the effective value per command, so repeating an inherited answer says nothing — but a command that differs has to say so, or the setting never reaches the spec. Now written only where it changes, with a test asserting both halves: a matching subcommand stays quiet, a differing one declares it.

One note on CI: the remaining IN_SCOPE bump in this PR disappears when #813 lands, which replaces that count with a snapshot of the exempt vectors. #813 also unbreaks main, so it is worth taking first.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 10f765f. Configure here.

Comment thread argv/src/spec.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@argv/src/spec.rs`:
- Around line 353-379: Remove the duplicate unknown_flags serialization branch
in the spec-writing function, keeping a single comparison against
inherited_unknown_flags and one write! call using meta.cmd.unknown_flags. Update
the relevant test assertion to verify that an overriding command emits
unknown_flags exactly once.

In `@conformance/tests/argv.rs`:
- Line 16: Replace the hardcoded IN_SCOPE constant in argv.rs with the
exempt-vector snapshot from PR `#813`, so the expected count tracks the intended
exempt corpus rather than ordinary vector additions. Preserve the existing type
and usage of IN_SCOPE.

In `@docs/spec/argv.md`:
- Around line 171-172: Update the wording near “add the number case back
afterwards” to use the American English form “afterward,” preserving the rest of
the sentence.
- Around line 34-37: Update the numeric-argument description around the
“optional exponent” wording to define the grammar precisely: specify the
exponent marker, optional sign, and required exponent digits, and list the
accepted decimal forms, including the behavior for values such as -1e-3, -1e,
and -1. Keep the documented grammar aligned with the parser’s strict
classification rules.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Central YAML (base), Organization UI (inherited)

Review profile: CHILL

Plan: Pro Plus

Run ID: 88e0c6ed-b060-454c-8072-8f47bc381581

📥 Commits

Reviewing files that changed from the base of the PR and between 8421047 and 10f765f.

📒 Files selected for processing (11)
  • argv/src/lib.rs
  • argv/src/spec.rs
  • conformance/tests/argv.rs
  • conformance/tests/derive.rs
  • corpus/01-long-flags.json
  • corpus/02-short-flags.json
  • docs/spec/argv.md
  • docs/spec/reference/cmd.md
  • lib/src/parse.rs
  • lib/src/spec/cmd.rs
  • lib/src/spec/mod.rs
🚧 Files skipped from review as they are similar to previous changes (5)
  • corpus/01-long-flags.json
  • conformance/tests/derive.rs
  • lib/src/spec/cmd.rs
  • lib/src/spec/mod.rs
  • argv/src/lib.rs

Comment thread argv/src/spec.rs Outdated
Comment on lines +353 to +379
// Written only where it changes, since the spec inherits it. The tables hold the
// effective value per command, so repeating the enclosing command's answer would
// say nothing — but a command that differs has to say so, or the setting is lost
// on the way out.
if meta.cmd.unknown_flags != inherited_unknown_flags {
write!(
out,
" unknown_flags={}",
quoted(match meta.cmd.unknown_flags {
UnknownFlags::Value => "value",
UnknownFlags::Error => "error",
})
)?;
}
// Written only where it changes, since the spec inherits it: the tables hold the
// effective value per command, so the same value as the enclosing one says nothing.
let unknown_flags = meta.cmd.unknown_flags;
if unknown_flags != inherited_unknown_flags {
write!(
out,
" unknown_flags={}",
quoted(match unknown_flags {
UnknownFlags::Value => "value",
UnknownFlags::Error => "error",
})
)?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Remove the duplicate unknown_flags property write.

Lines 357-379 append the same property twice when a command overrides its inherited policy. This emits unknown_flags="value" unknown_flags="value" for exec. Keep one branch. Update the test to assert one occurrence.

Proposed fix
-    // Written only where it changes, since the spec inherits it: the tables hold the
-    // effective value per command, so the same value as the enclosing one says nothing.
-    let unknown_flags = meta.cmd.unknown_flags;
-    if unknown_flags != inherited_unknown_flags {
-        write!(
-            out,
-            " unknown_flags={}",
-            quoted(match unknown_flags {
-                UnknownFlags::Value => "value",
-                UnknownFlags::Error => "error",
-            })
-        )?;
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Written only where it changes, since the spec inherits it. The tables hold the
// effective value per command, so repeating the enclosing command's answer would
// say nothing — but a command that differs has to say so, or the setting is lost
// on the way out.
if meta.cmd.unknown_flags != inherited_unknown_flags {
write!(
out,
" unknown_flags={}",
quoted(match meta.cmd.unknown_flags {
UnknownFlags::Value => "value",
UnknownFlags::Error => "error",
})
)?;
}
// Written only where it changes, since the spec inherits it: the tables hold the
// effective value per command, so the same value as the enclosing one says nothing.
let unknown_flags = meta.cmd.unknown_flags;
if unknown_flags != inherited_unknown_flags {
write!(
out,
" unknown_flags={}",
quoted(match unknown_flags {
UnknownFlags::Value => "value",
UnknownFlags::Error => "error",
})
)?;
}
// Written only where it changes, since the spec inherits it. The tables hold the
// effective value per command, so repeating the enclosing command's answer would
// say nothing — but a command that differs has to say so, or the setting is lost
// on the way out.
if meta.cmd.unknown_flags != inherited_unknown_flags {
write!(
out,
" unknown_flags={}",
quoted(match meta.cmd.unknown_flags {
UnknownFlags::Value => "value",
UnknownFlags::Error => "error",
})
)?;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@argv/src/spec.rs` around lines 353 - 379, Remove the duplicate unknown_flags
serialization branch in the spec-writing function, keeping a single comparison
against inherited_unknown_flags and one write! call using
meta.cmd.unknown_flags. Update the relevant test assertion to verify that an
overriding command emits unknown_flags exactly once.

Comment thread conformance/tests/argv.rs Outdated

/// Vectors that exercise binding, which is what usage-argv implements.
const IN_SCOPE: usize = 64;
const IN_SCOPE: usize = 75;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target file ---'
cat -n conformance/tests/argv.rs | sed -n '1,140p'

printf '%s\n' '--- repository references to IN_SCOPE and PR `#813` ---'
rg -n -C 3 'IN_SCOPE|`#813`|813' . --glob '!target' --glob '!node_modules' || true

printf '%s\n' '--- concise repository status and recent commits ---'
git status --short
git log --oneline -12

Repository: jdx/usage

Length of output: 11554


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- conformance files ---'
git ls-files conformance

printf '%s\n' '--- count-update commit ---'
git show --stat --oneline 8ed75291
git show --format=fuller --no-ext-diff 8ed75291 -- conformance/tests/argv.rs conformance 2>/dev/null | sed -n '1,220p'

printf '%s\n' '--- corpus loading and argv outcome logic ---'
rg -n -C 5 'fn corpus_dir|pub fn corpus_dir|enum Outcome|OutOfScope|fn run|pub fn run|load\(' conformance --glob '*.rs'

printf '%s\n' '--- corpus manifests and vector files ---'
find conformance -maxdepth 3 -type f | sort | sed -n '1,220p'

Repository: jdx/usage

Length of output: 10705


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- repository refs and remotes ---'
git remote -v || true
git branch --all --contains HEAD
git log --all --oneline --decorate -20

printf '%s\n' '--- workspace and corpus configuration ---'
cat -n Cargo.toml | sed -n '1,180p'
cat -n conformance/Cargo.toml | sed -n '1,180p'
find . -maxdepth 2 -type d -print | sort
find . -maxdepth 2 -type f \( -name '*.json' -o -name '.gitmodules' \) -print | sort

printf '%s\n' '--- pull request 813 metadata ---'
curl -fsSL https://api.github.com/repos/jdx/usage/pulls/813 |
  jq '{number,title,state,merged_at,base:.base.ref,head:.head.ref,head_sha:.head.sha,merge_commit_sha,body}'

Repository: jdx/usage

Length of output: 7854


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- PR `#813` changed files ---'
curl -fsSL https://api.github.com/repos/jdx/usage/pulls/813/files?per_page=100 |
  jq -r '.[] | [.filename, .status, (.additions|tostring), (.deletions|tostring)] | `@tsv`'

printf '%s\n' '--- PR `#813` patch for the test ---'
curl -fsSL https://api.github.com/repos/jdx/usage/pulls/813/files?per_page=100 |
  jq -r '.[] | select(.filename == "conformance/tests/argv.rs") | .patch'

printf '%s\n' '--- changes from main to the reviewed commit ---'
git diff --stat origin/main...HEAD
git diff -- conformance/tests/argv.rs corpus | sed -n '1,260p'

printf '%s\n' '--- corpus vector counts by file and current hardcoded count history ---'
python3 - <<'PY'
import json
from pathlib import Path

total = 0
for path in sorted(Path("corpus").glob("*.json")):
    data = json.loads(path.read_text())
    vectors = data.get("vectors", [])
    print(f"{path}: {len(vectors)}")
    total += len(vectors)
print(f"total vectors: {total}")
PY
git log --oneline --all -- corpus conformance/tests/argv.rs | sed -n '1,30p'

Repository: jdx/usage

Length of output: 8029


Replace the hardcoded IN_SCOPE count with the exempt-vector snapshot from PR #813.

A count changes when ordinary corpus vectors are added and can make concurrent changes fail CI.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@conformance/tests/argv.rs` at line 16, Replace the hardcoded IN_SCOPE
constant in argv.rs with the exempt-vector snapshot from PR `#813`, so the
expected count tracks the intended exempt corpus rather than ordinary vector
additions. Preserve the existing type and usage of IN_SCOPE.

Comment thread docs/spec/argv.md Outdated
Comment on lines +34 to +37
A number here means digits, at most one `.`, and an optional exponent —
deliberately narrower than what a float parser accepts, since `-inf` is far
likelier to be a misspelled flag than a number somebody meant to pass. `-1x` and
`-1e` are not numbers either, and so name flags that do not exist.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Define the numeric grammar precisely.

The text says “optional exponent”, but strict classification depends on the signed-exponent form. State the exponent marker, optional sign, required digits, and accepted decimal forms. This prevents documentation consumers from classifying -1e-3, -1e, and -1. differently from the parser.

Suggested clarification
-A number here means digits, at most one `.`, and an optional exponent —
+A number here means digits, at most one `.`, and an optional signed exponent.
+Define the exponent marker, required digits, and accepted decimal forms.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/spec/argv.md` around lines 34 - 37, Update the numeric-argument
description around the “optional exponent” wording to define the grammar
precisely: specify the exponent marker, optional sign, and required exponent
digits, and list the accepted decimal forms, including the behavior for values
such as -1e-3, -1e, and -1. Keep the documented grammar aligned with the
parser’s strict classification rules.

Comment thread docs/spec/argv.md Outdated
Comment on lines +171 to +172
add the number case back afterwards.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use afterward in American English.

Replace afterwards with afterward to satisfy the locale check.

🧰 Tools
🪛 LanguageTool

[locale-violation] ~171-~171: In American English, ‘afterward’ is the preferred variant. ‘Afterwards’ is more commonly used in British English and other dialects.
Context: ...gs, and had to add the number case back afterwards. ## Positional arguments A word that ...

(AFTERWARDS_US)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/spec/argv.md` around lines 171 - 172, Update the wording near “add the
number case back afterwards” to use the American English form “afterward,”
preserving the rest of the sentence.

Source: Linters/SAST tools

jdx and others added 4 commits August 11, 2026 19:40
The mount page describes only the behavior of the first commit in #806 and
never picked up what the later ones changed: it still claims flags never
trigger discovery, which is untrue of completions and help, and
`overrides_default` shipped with no documentation at all.

My fault, and worth writing down how: the edits were made by string
replacement against text prettier had already rewrapped and turned `*own*`
into `_own_`, so both replacements silently matched nothing. I said in the
pull request that the page had been corrected, and it had not. Diff verified
this time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A flag-like token that names no flag stays what it has always been here: a
word, offered to the positionals like any other. That is deliberate, and now
documented as a decision rather than left as an accident — the grammar said
one thing and the parser did another, which was the only indefensible state.

Every comparable parser refuses the token, and they are right for what they
do: parse their own argv, where a dash-word is a flag or a typo. A usage
spec also parses command lines whose flags it does not own — a script
forwarding options through `usage exec`, a task whose script is the
authority on what it accepts, a completion asked about half-typed input. In
those, an unrecognized token is data in transit far more often than a
mistake, and refusing it breaks the wrapper for anyone who did not
enumerate the flags of the program behind it.

The cost is a misspelled `--hekp` becoming an argument, so a CLI that owns
all of its flags can say `unknown_flags "error"` and get typo detection. It
is settable per CLI and per command, and inherited — a CLI can be strict
everywhere except the one command that forwards. `#[usage(unknown_flags =
"error")]` says it from a Rust struct, which is the case that usually wants
it.

Even when refusing, a lone `-` and a negative number stay values. oclif made
exactly this mistake on the same switch and had to add the number back.

Six corpus vectors move from expecting an error to expecting a value, and
five new ones cover the strict mode, the inherited override, and the
negative-number carve-out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four review findings, and the first is the interesting one.

`-az` with only `-a` declared was read as a bundle: `-a` was applied and `z`
left over as a value. A token containing an unrecognized letter is not a
bundle at all, so none of its letters apply — the grammar said so and
usage-argv did it, but usage-lib did not.

The check had to go in phase 1, not just phase 2. Phase 1 keys a short token
on its first letter, so `-az` was recorded as a binding for `-a`, and phase 2
trusting that binding is what defeated a check placed only there. It now
decides whether the whole token is a bundle where the token is first read.

The corpus could not see this: the vector for it declares no argument, so
both readings produce `unexpected_arg`. A second vector gives it a free
argument, where the difference is the whole point — `-a` unset and `-az`
bound as one word.

`is_flag_like` also treated anything after a digit as a number, so `-1x`
slipped past a CLI that asked for unknown flags to be refused. It now asks
whether the token *is* a number. usage-argv gets the same rule, spelled out
by hand rather than deferred to a float parse, since it runs on the hot path.

And `Spec::merge` dropped `unknown_flags`, so an `include` lost the policy it
declared. Plus a docs link that markdownlint could not resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two implementations disagreed about `-1e5`. usage-lib asked
`f64::from_str`, which accepts an exponent; usage-argv used a hand-written
scanner that stopped at `e` and called the token a flag. So a CLI refusing
unknown flags would accept `--offset -1e5` under one parser and reject it
under the other — exactly the drift the corpus exists to prevent, in the one
place the corpus had no vector.

One rule now, spelled out identically in both: digits, at most one `.`, and an
optional signed exponent. Narrower than a float parse on purpose, since `-inf`
is far likelier to be a misspelled flag than a number somebody meant to pass.
Four vectors pin the edges — `-1e5`, `-1.5e-3`, `-inf`, `-1e` — so the two
cannot drift again without a test saying so.

Also emits `unknown_flags` for a subcommand that differs from the command
enclosing it. The tables carry the effective value per command, so repeating
an inherited answer says nothing, but a command that differs has to say so or
the setting never reaches the spec.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jdx
jdx changed the base branch from main to agent/mount-docs August 11, 2026 19:44
@jdx
jdx force-pushed the agent/unknown-flags branch from 10f765f to 938f8f9 Compare August 11, 2026 19:44
The property was written twice, so an overriding subcommand emitted
`unknown_flags="value" unknown_flags="value"` — a KDL node carrying the same
property twice, which keeps only the last. Exactly the bug this same pull
request fixes for `overrides` and `required_unless`, reintroduced two commits
later by a botched relocation of the block.

The test did not catch it because it asked whether the output *contained*
`unknown_flags="value"`, which a doubled write also does. It now finds the
line and counts, and I checked it fails on the duplicate before removing it.

Also states the numeric grammar exactly — digits, at most one `.`, then
optionally `e`/`E`, an optional sign, and at least one digit — rather than
leaving "an optional exponent" to be guessed at, and uses the American
spelling the locale check wants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

jdx commented Aug 11, 2026

Copy link
Copy Markdown
Owner Author

Two fixes, and the first is embarrassing in a useful way.

The property was written twice. An overriding subcommand emitted unknown_flags="value" unknown_flags="value" — a KDL node carrying the same property twice, which keeps only the last. That is exactly the bug this PR fixes for overrides and required_unless, reintroduced two commits later when I relocated the block and left a copy behind.

The test did not catch it because it asked whether the output contained unknown_flags="value", and a doubled write contains that too. It now finds the line and counts occurrences — and I put the duplicate back to watch it fail before removing it again:

assertion `left == right` failed: a differing subcommand declares it exactly once:
cmd "exec" unknown_flags="value" unknown_flags="value" {

Worth naming the pattern, since it is the third time in this stack: contains is a weak assertion for generated output, because the wrong output usually contains the right output.

The numeric grammar is now stated exactly — digits, at most one ., then optionally e/E, an optional sign, and at least one digit — rather than leaving "an optional exponent" to interpretation. Plus the American spelling the locale check asked for.

The IN_SCOPE thread above is stale: it reviewed this branch before the rebase, and recommends #813, which is merged. That constant no longer exists here.

AI-assisted — Tool: Claude Code; model: anthropic/claude-fable-5; version: unavailable.

@jdx
jdx merged commit 3dad7f1 into main Aug 11, 2026
8 checks passed
@jdx
jdx deleted the agent/unknown-flags branch August 11, 2026 21:34
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