Skip to content

The word grammar's --force rule is unguarded in the same way - #506

Merged
blooop merged 7 commits into
mainfrom
wayfinder/devlaunch-354
Aug 29, 2026
Merged

The word grammar's --force rule is unguarded in the same way#506
blooop merged 7 commits into
mainfrom
wayfinder/devlaunch-354

Conversation

@blooop

@blooop blooop commented Aug 29, 2026

Copy link
Copy Markdown
Owner

The flag-verb half of this ticket died with #369: verb_command is gone, and --rm --force is refused unconditionally ahead of every ordering. What was left is the part #344 was reviewed for and never landed with: a guard over the argv space itself.

What lands

One walk of every ordering of every subset of {ws, rm, rme, --force, --rm}, each word used at most once, resolved through resolve(Cli, argv). Each line is checked against the rule written from Python ("--force" in args[2:], args[0] the workspace and args[1] the verb) plus the verb table, so the expectation is a second statement of the rule rather than a paraphrase of force_placement. The pair refusal is held over the same space, which retires the guesswork in the hand-written list beside it.

rm and rme are in the alphabet, and that is the point of the ticket. They are the only two words that can turn cli.force into a destroyed workspace; every other verb drops the flag on the floor. Without them a matrix proves --force is placed right and nothing at all about what a misplacement would cost.

How the guard was proven non-vacuous

Four mutations, each reverted after:

Mutation Result
Some(1) => Trailing (the verb slot honours --force again, the #303 bug) red on dl ws --force rm, which resolved to Remove { force: true }
Some(0) => Trailing (the workspace slot) red on dl --force ws rm, same
the three-word arm resolves instead of refusing red on dl ws rm --force rme, which force-deleted ws
the cli.rm && cli.force refusal deleted red on dl ws rm --force rme --rm

The third is the one worth reading: 149 other tests pass under it, and only the two guards this PR adds go red. That is the ticket's latent hole made live, --force at index 2 with a name still after it, honoured as the modifier.

The globals test is proven the same way: add Chosen::Install to accepts_force and it fails on the assertion that the one global taking a word must not also take --force.

What the ticket claimed that is no longer true

  • aid's leading --rm --force form. Closed on main. rm_and_force_are_handed_to_dl_whole_so_dl_can_refuse_the_pair and force_never_lands_in_dls_verb_slot_whichever_order_it_was_typed_in cover both orderings, and the second asserts the emitted index.
  • lib.rs:336 advertises a line the grammar refuses. The line number is stale after --rm is docker's --rm: retire --stop, --autorm and the suffix override (0.9.0) #369 and there is nothing wrong at it. The refusal's suggestion (dl <ws> -- --force) is a line the grammar accepts.
  • Globals have no placement rule. True, and deliberate. Position is read for a workspace line because it has slots a --force could be mistaken for a word in. A global line has none: global_command refuses a target outright, and --install, the one that does take a word, is not one of the two that accept --force. Written down as a test rather than as prose, so the premise fails loudly if somebody gives a force-accepting command a word.

What it found

Something believed and untrue: num_args = 0..=2 counts values per occurrence. dl a b c is TooManyValues at exit 2, but dl a b --force c opens a second occurrence and all three words reach the grammar. So the _ => arm that carried the comment "a third word never arrives here" is reached, and it is that arm's refusal, not the cap, that stops a trailing --force from being honoured with a name still after it. Comment corrected, fact pinned. No behaviour change either way, since the refusal was already there.

Gates

cargo test --workspace (1,410 in the core suite plus 20 others, all green), cargo clippy --locked --all-targets -- -D warnings, cargo fmt --check. CHANGELOG entry under [Unreleased].

Closes #354

🤖 Generated with Claude Code

Summary by Sourcery

Harden --force argument handling by basing placement on workspace words, rejecting unsafe combinations consistently, and validating the grammar across the full argument space.

Bug Fixes:

  • Prevent --force from being misinterpreted when flags precede workspace arguments, avoiding unintended forced workspace deletion.
  • Ensure --rm --force is refused consistently regardless of argument ordering.

Enhancements:

  • Replace hand-written force-placement cases with exhaustive coverage of argument orderings, including rm and rme verbs.
  • Clarify positional-word handling when flags split arguments and document global-command force semantics.

Documentation:

  • Add an Unreleased changelog entry describing the force-placement safeguards and corrected argument-counting behavior.

Tests:

  • Add exhaustive grammar tests covering all orderings and subsets of workspace, delete verbs, and force-related flags.
  • Add regression tests for leading flags, aid-emitted option pairs, split positional arguments, and global command behavior.

The placement rule was covered by hand-written example lines, and between
them they never named rm or rme -- the only two verbs that turn cli.force
into a destroyed workspace. So the matrix proved --force was placed
correctly and nothing about what a misplacement would cost.

One test now walks every ordering of every subset of
{workspace, rm, rme, --force, --rm} through resolve, and checks each
against the rule as Python stated it (`"--force" in args[2:]`) plus the
verb table, rather than against force_placement's own phrasing. The pair
refusal is held over the same space.

It found that clap's two-word cap is not what people thought: it counts
values per occurrence, so `dl a b --force c` hands the grammar three
positionals where `dl a b c` is refused. Nothing forced escapes, because
the third word is refused first, but that refusal is what keeps
force_placement's "index 2 or later is the modifier" honest, not the cap.
The comment claiming a third word never arrives is corrected and pinned.

Globals stay position-free deliberately: no command that accepts --force
also takes a word, so there is nothing for a leading --force to be read
as. That premise is now a test.

Closes #354
@sourcery-ai

sourcery-ai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds exhaustive, independently specified coverage for --force placement, destructive delete semantics, pair refusal, and global-command behavior, while documenting the clap positional parsing edge case and the resulting test coverage.

Sequence diagram for force-aware workspace resolution

sequenceDiagram
    participant Test
    participant Clap
    participant Resolve
    participant Grammar
    Test->>Clap: try_parse_from(argv)
    alt clap accepts argv
        Clap-->>Test: Cli
        Test->>Resolve: resolve(cli, argv)
        Resolve->>Grammar: evaluate workspace and verb slots
        alt --force follows workspace and delete verb
            Grammar-->>Resolve: Command::Workspace or Command::Select Remove force=true
        else --rm and --force are both present
            Grammar-->>Resolve: GrammarError::RmForced
        else misplaced force or unsupported verb
            Grammar-->>Resolve: non-destructive command or grammar error
        end
        Resolve-->>Test: outcome
    else positional parsing refuses argv
        Clap-->>Test: None
    end
Loading

Flow diagram for exhaustive force-placement validation

flowchart TD
    A[Generate every ordering of subsets] --> B[Parse with Cli::try_parse_from]
    B --> C{clap accepts argv?}
    C -- No --> D[Skip clap refusal]
    C -- Yes --> E[resolve cli argv]
    E --> F[Compare destructive outcome with force_is_earned]
    F --> G{--rm and --force present?}
    G -- Yes --> H[Expect GrammarError::RmForced]
    G -- No --> I[Record force-delete lines]
    H --> J[Assert complete expected delete set]
    I --> J
Loading

File-Level Changes

Change Details Files
Exhaustively validate --force placement and destructive-delete behavior across the CLI argument space.
  • Generate every ordering of every nonempty subset of workspace, delete verbs, and flags.
  • Parse each argv through clap and resolve, comparing outcomes with an independently written Python-derived predicate.
  • Include rm/rme and verify --rm plus --force is refused in every ordering.
  • Assert the exact set of force-deleting command lines and require the walk to cover more than 100 accepted inputs.
rust/dl/src/cli.rs
Pin edge cases around positional parsing and global-command force handling with focused tests.
  • Document and test that a flag-separated third positional reaches the grammar and is refused there.
  • Verify global commands read --force consistently regardless of order.
  • Verify force is rejected for --install, the global command that accepts a word.
rust/dl/src/cli.rs
Clarify the positional-argument behavior and the new whole-argv coverage in release documentation.
  • Correct the stale assumption that clap's two-value limit prevents all third words.
  • Document the exhaustive placement test and its lack of intended behavior changes.
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#354 State the workspace word grammar's --force placement rule using the same name/verb-slot invariant as the flag-verb grammar, rather than relying on an absolute positional-index coincidence. The PR documents the issue and adds an independent predicate in the test, but the production workspace_command/force_placement rule remains expressed using the absolute index-based logic. No corresponding grammar implementation change is shown.
#354 Expand the argv matrix to include rm and rme, exercise the word half of the force-placement property, and demonstrate that deliberate regressions make the widened test fail.
#354 Resolve or document the related gaps: the aid leading --rm --force form, the stale lib.rs advertisement, global-command force placement, and synchronized README/GRAMMAR documentation. The PR adds a globals test and explains that the aid and lib.rs concerns are already obsolete, but it does not edit the README or GRAMMAR documentation as the issue explicitly requires. The diff only adds a CHANGELOG entry and code comments/tests, so the required documentation synchronization is missing.

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai 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.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@codecov

codecov Bot commented Aug 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.63014% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 95.68%. Comparing base (0950216) to head (ff8f91c).

Files with missing lines Patch % Lines
rust/dl/src/cli.rs 98.55% 2 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
python 42.98% <ø> (ø)
rust 95.98% <98.63%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
shipped code (rust) 95.98% <98.63%> (+0.02%) ⬆️
harness and tooling (python) 42.98% <ø> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This was generated by AI during review.

Reviewed on two independent axes in fresh context, with the enumeration re-derived by hand, the clap claim reproduced, and two of the four mutations replayed. Local gates match the PR body: cargo test --workspace green (1,410 in core, 151 in dl), clippy -D warnings clean, fmt --check clean, CI green, CHANGELOG entry under [Unreleased].

The two things this PR asks to be believed both check out.

The enumeration is total. every_ordering over a 5-word alphabet emits 325 lines, which is exactly sum(P(5,k) for k in 1..=5) = 5 + 20 + 60 + 120 + 120, with no duplicates. 259 of them survive clap; the other 66 are the contiguous-three-positional lines clap refuses at TooManyValues, correctly skipped by resolved.

The oracle is independent. force_is_earned is a predicate over the raw &[&str] line. It calls no implementation function, and in particular it does not consult force_placement, so a change to the implementation cannot drag the expectation along with it. Three of its five clauses restate Python ("--force" in args[2:] plus the verb table); the other two (the --rm pair, and "at most two non-flag words") are Rust-era rules, both honestly annotated as such, and both separately asserted in the same loop. deletes_by_force is also complete for the right reason rather than by luck: VerbWord::with (cli.rs:154) shows Remove is the only variant that reads force at all, so widening the alphabet with up/stop/kill/reset would add lines that cannot discriminate.

Non-vacuity spot-checked. Some(1) => Trailing goes red on three tests including the new walk. The three-word arm resolving instead of refusing reproduces the PR's claim to the number: 149 pass, exactly two red, and both are the guards this PR adds.

The clap discovery is real. dl a b c is TooManyValues; dl a b --force c hands resolve words = ["a","b","c"]. Worth adding: --force=true is also TooManyValues, so there is no bypass by that spelling either, and infer_long_args is off, so no abbreviation reaches cli.force without matching the == "--force" string compare.


Standards

  1. assert!(walked > 100) (cli.rs:2134) is a weak guard over a space whose size is known exactly. Measured: 259 walked. The assertion tolerates losing 158 of them. Concretely, a regression that truncated every_ordering at length 4 would leave 205 walked and still match the named forced list at :2122, because all six forced lines are three words long. The longest orderings are the ones the walk exists for, and they are the ones that can vanish silently. assert_eq!(walked, 259) costs nothing and closes it. Commented inline. Non-blocking.

  2. resolved (:2028) is parse (:1104) with .ok()? instead of a panic. The Cli::try_parse_from(once("dl").chain(...)) incantation is now in the module three times. parse is expressible in terms of resolved. Duplicated Code, small. Non-blocking.

  3. the_pair_is_refused_wherever_force_sits_in_the_line (:1959) is now strictly subsumed — all five of its lines fall inside the walk, which asserts the same RmForced at :2101. The PR keeps it deliberately and says so, which is defensible; noting it so the duplication reads as a decision rather than an oversight. Non-blocking.

  4. Doc-comment length is house style for this file, and I found no factual error in the new ones. CHANGELOG prose is in the clear: test/test_docs_prose.py scopes the dash rule to README.md plus docs/*.md, and the entry carries none regardless.

Spec

  1. Ask 1 is discharged in one direction and left open in the other, and the open direction is the ticket's own sentence. #354: "state the word grammar's rule as the same invariant the flag-verb grammar now uses", because the absolute index "is equivalent ... only because clap caps positionals at 2 — an incidental fact, not a stated invariant, and exactly the kind of coincidence that stops being true without anyone noticing."

    The PR fully addresses the trailing direction, and improves on the ticket: the cap is not what makes the index sound, the _ => refusal is, and that is now pinned. But force_placement (:796) counts position over every argv token, so a leading token shifts the index the other way. dl <someflag> ws --force rm leaves cli.words = ["ws","rm"], never reaches the _ => arm, and reads --force at index 2 as Trailing — a force-delete, where the same line without the flag (dl ws --force rm) is refused as UnknownVerb.

    No such flag exists today. I probed every candidate: --json/--size carry requires = "ls" so clap refuses them; -y/--yes is refused above force_placement at :902; --stop/--autorm are refused by retired_flag() at :767; the --rm/--force pair is refused at :922; --devcontainer is stripped by hand at :803-809; every remaining flag routes to global_command. So it is latent, not live — but it is five independent coincidences holding up one rule, none of them stated, none of them pinned, and the new matrix cannot see any of it because --rm is its only flag and the pair is refused before placement is ever read.

    Either count position over the positional-looking words rather than over all of argv, or add a guard that fails when a flag clap accepts on a workspace line is neither stripped by force_placement nor refused above it. Blocking, narrowly: this is the invariant the ticket was opened to stop relying on.

  2. The aid claim in the PR body is right about the outcome and wrong about the evidence. #354: "aid's leading --rm --force form still loses force." The PR answers "Closed on main" and cites two tests. I ran aid: aid --rm --force owner/repo "fix it" emits ["--rm","--force","owner/repo","--",…]--force at index 1, which is dl's verb slot. Neither cited test covers that; both peel a trailing suffix, and force_never_lands_in_dls_verb_slot_whichever_order_it_was_typed_in (rewrite.rs:1278) asserts Some(2) for the only order it tries, despite the name. The emitted line is safe, but for a third reason neither test names: dl's cli.rm && cli.force refusal at :922 runs ahead of placement. Nothing in aid pins it. Non-blocking, but the ticket item should be closed on the real reason.

    Refuted from the ticket, in the PR's favour: lib.rs:336 is stale as the PR says, and both live suggestions parse (dl <ws> rm --force is in the walk's own forced list). The globals rationale is genuine at :836-853, and pinning a premise as a test rather than as prose is this repo's habit.

  3. The README/GRAMMAR debt the ticket names is untouched. #354: "No README/GRAMMAR update accompanied #344 either; the two files must be edited together or it becomes the drift CLAUDE.md forbids." Nothing in README.md, docs/cli.md or the GRAMMAR help const says --force's position is load-bearing; rm's entry says only "add --force to delete it anyway", so dl --force ws rm answering Unknown workspace '--force' is undocumented. Nothing user-visible moved in this PR, which is why this is non-blocking, but the drift the ticket opened on is still there.

Verdict

Request changes — one blocking finding: Spec 1, the leading-token half of force_placement's absolute index. It is a small ask (pin the coincidence, or count position over the words), and it is the specific thing #354 was opened to remove.

Everything else is non-blocking: Standards 1 (walked > 100 should be == 259), Standards 2 and 3, Spec 2 (aid's ticket item closed on evidence that does not cover the case), Spec 3 (docs drift).

The coverage this PR adds is real, total, independently oracled and demonstrably non-vacuous. Nothing here argues against the approach.

Comment thread rust/dl/src/cli.rs Outdated
],
"the set of lines that force-delete moved"
);
assert!(walked > 100, "the walk collapsed to {walked} lines");

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

The anti-shrink guard is much looser than the space it guards.

I measured it: every_ordering emits 325 lines (exactly sum(P(5,k) for k in 1..=5), no duplicates), and 259 of them survive clap. So walked is 259 and this assertion tolerates losing 158 of them.

That window is not hypothetical in the way it looks. Truncate the walk at length 4 and you get walked == 205, which passes here and still matches the named forced list above, because all six forced lines are three words long. The 120 five-word orderings could stop being generated and nothing in this test would say so. Those are the longest orderings, which is to say the ones this walk exists for.

The count is deterministic and knowable, so it can just be pinned:

assert_eq!(walked, 259, "the walk stopped covering the argv space");

The alphabet-shrink cases are already caught (dropping rm, rme or --force breaks the forced list; dropping --rm drops walked to 64), so this is only about the generator itself losing depth. Which is precisely the defect class this map exists for.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Pinned, and your two numbers were exact:

assert_eq!(
    every_ordering(&["ws", "rm", "rme", "--force", "--rm"]).len(),
    325,
    "the generator is no longer every ordering of every subset of five"
);
assert_eq!(walked, 259, "the walk stopped covering the argv space");

Both assertions passed first run against the measured values, which is its own small confirmation that we counted the same space.

Two assertions rather than one because they fail for different reasons and a single number cannot say which. 325 is the generator: every ordering of every subset, no duplicates, depth intact. 259 is what clap accepts of it. A change that made clap stricter would move only the second, and reading a failure that says "the generator lost depth" when clap changed under you is the kind of wrong diagnosis a pinned count is supposed to prevent.

Your truncation case is the one I checked it against: cut the walk at length 4 and walked is 205, which the old floor of 100 waved through while the forced list stayed green because all six forced lines are three words long. Both assertions now fail on it, and the generator one names the cause.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Correcting one number in my own reply before it becomes folklore: under the length-4 truncation the generator emits 205 lines and walked is 175, not 205. 205 is the generator's count, which is what the first assertion reports; 175 is what survives clap, which is what the second reports.

Does not change the conclusion, and if anything sharpens the case for keeping both assertions: the two numbers move together but are not the same number, so a single pinned count would have named whichever half it happened to be measuring.

Verified rather than reasoned, since that is what I got wrong the first time:

assertion `left == right` failed: the generator is no longer every ordering of every subset of five
  left: 205
 right: 325

assertion `left == right` failed: the walk stopped covering the argv space
  left: 175
 right: 259

Comment thread rust/dl/src/cli.rs
// arrive. So this arm is reached, and it is what keeps `force_placement`'s
// "index 2 or later is the modifier" honest — a `--force` at index 2 with a
// name still after it is refused here rather than honoured. Pinned by
// `a_third_word_still_arrives_when_a_flag_splits_it`.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This comment is right, and it closes one of the two ways the absolute index can be wrong. The other one is still open.

The trailing direction is handled: a --force at index 2 with a name after it is refused here, and the discovery that the cap is not what does that work is a real correction.

But force_placement counts position over every argv token, not over the positional words, so a token before the --force shifts the index the other way. dl <flag> ws --force rm leaves cli.words = ["ws", "rm"], so it never reaches this arm, and --force sits at stream index 2 and reads as Trailing: a force-delete of ws, where the same line without the flag (dl ws --force rm) is refused as UnknownVerb.

I probed for a live instance and there is none, which is the good news and also the problem. It is latent behind five separate facts, none stated and none pinned:

  • --json / --size carry requires = "ls", so clap refuses them on a workspace line
  • --yes is refused at :902, above force_placement
  • --stop / --autorm are refused by retired_flag() at :767
  • the --rm / --force pair is refused at :922
  • --devcontainer is stripped by hand at :803-809

Drop requires = "ls" from --json for any unrelated reason and dl ws --json --force rm becomes a force-delete, silently, with the new walk still green (its only flag is --rm, and the pair is refused before placement is read).

This is #354's own sentence, still true of the surviving half: "an incidental fact, not a stated invariant, and exactly the kind of coincidence that stops being true without anyone noticing."

Two ways out, either is small:

  1. Count position in force_placement over the words that could occupy a slot, skipping anything else --prefixed. Then the index is the positional index by construction rather than by coincidence, and the rule really is the same invariant the flag-verb half uses.
  2. Keep the index and pin the coincidence: a test asserting that every flag clap accepts alongside positional words is either stripped by force_placement or refused above it. That fails loudly the day somebody adds the sixth flag.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Took option 1: force_placement now counts words, skipping anything --prefixed and taking --devcontainer's value with it. The index is the positional index by construction, and the two lines you named now agree.

You were right that no live instance exists and right about why that is the problem. I went looking for the sixth flag from the other end and found the vehicle already built: aid takes any leading - word it does not recognise and pushes it into the options it emits ahead of the spec, without knowing what it means (finding 3 is the same mechanism). So the coincidence was not five rules deep on dl's side alone. It was five rules deep with another binary feeding it.

Proven on the exact line from your comment. Revert to token counting and a_flag_before_the_words_does_not_move_the_slots goes red with:

left:  Some(Ok(Workspace { target: "ws", verb: Remove { force: true, after: LeaveTheShell }, devcontainer: None }))
right: Some(Err(UnknownVerb { target: "ws", word: "--force" }))

That is dl <flag> ws --force rm force-deleting ws. 150 other tests pass under that same revert, so the named test and the walk are the only things holding it.

The walk covers the leading direction too, and across the whole space rather than at one line: every ordering is resolved twice, once with a synthetic flag prefixed to argv while the Cli is parsed from the line without it. That is the shape every future flag has by the time resolve sees it, consumed out of the words and still in argv, so the guard does not depend on which flag somebody adds next. I preferred it to adding a real placement-shifting token to the alphabet because there isn't one to add: any token available today is one of the five that gets refused or stripped, and would exercise the refusal rather than the placement.

Behaviour is unchanged for every line anybody can type: 152 dl tests pass with no expectation edited. The differences are confined to lines already refused above force_placement, which is --rm-carrying argv.

Review of #506 found the leading half of the same coincidence still open.
force_placement counted position over every argv token, so a flag clap had
consumed -- gone from cli.words, still present in argv -- shifted every
slot up by one. `dl <flag> ws --force rm` then read the verb-slot --force
as trailing and force-deleted ws, where `dl ws --force rm` refuses.

No spelling of <flag> exists today, and that was the problem rather than
the reassurance: it held only because --json/--size require --ls, --yes
and the retired spellings and the --rm/--force pair are each refused
above, and --devcontainer is stripped by hand. Five unrelated rules are
not an invariant, and aid pushes every leading `-` word it is handed
straight through to dl without knowing what any of them mean.

A slot is a place a word goes, so words are what is counted. Every line
anybody can type today reads exactly as it did; 152 dl tests pass
unchanged.

Also from the review:

- The walk now resolves every line twice, once with a synthetic flag
  ahead of it in argv, so the leading direction is guarded across the
  whole space rather than by the five refusals standing in for it.
- walked is pinned at 259 and the generator at 325 rather than a floor of
  100, which tolerated losing 158 lines: every forced line is three words
  long, so truncating the walk at length 4 passed both other assertions.
- aid really does emit a leading pair, and neither test cited for closing
  that item covered it. Both halves are pinned now: aid's emission, and
  dl answering the pair ahead of reading placement.
- parse is resolved().expect(), and the hand-listed pair test the walk
  subsumes is gone.
@blooop

blooop commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

All four addressed in 6327ffe. The two inline findings are answered on their threads; findings 3 and 4 had no anchor, so they are here.

3. The aid item was closed on wrong evidence, and you were right

I checked the emission path rather than the tests this time. aid walks the leading words and pushes any --prefixed word it does not recognise into dl_options, which build_dl_args emits ahead of the spec. So aid --rm --force owner/repo 'fix it' reaches dl as:

--rm --force owner/repo -- claude -p 'fix it'

Both flags before the workspace. Neither test I cited covers that, exactly as you said: both are about the trailing run, which is the position a recalled line grows in and therefore the position everybody wrote tests for.

What I had actually verified was that the pair is refused, not that it is refused for a reason anything pins. It is refused because cli.rm && cli.force is answered above force_placement, and nothing in aid knows that or should.

Pinned as two halves that meet at one written-down line: a_leading_pair_reaches_dl_as_the_pair_it_is in aid asserts that this is the argv aid emits, and the_pair_aid_emits_is_refused_ahead_of_where_force_sits in dl asserts that argv earns RmForced. Each names the other, so a change to either end has somewhere to look.

This is also what settled finding 1 for me. The missing sixth flag is not hypothetical when another binary forwards arbitrary leading flags into dl's argv without inspecting them.

4. Duplication and the subsumed test

  • parse is now resolved(argv).unwrap_or_else(...). One implementation; parse is the panicking form every other test wants, resolved the Option form the walk needs.
  • the_pair_is_refused_wherever_force_sits_in_the_line is deleted. Its five lines are a subset of the walk, and the walk now asserts RmForced for every ordering carrying both flags rather than for the five somebody listed. Its rationale moved into the walk so the reasoning did not go with it.

README/GRAMMAR: nothing to change, and I want to be explicit rather than silently skip it. #354 raised that against #344, which changed which lines are refused. This PR changes no line anybody can type: the rule now counts words instead of tokens, and every difference falls on argv that is refused above force_placement. Neither the README's flag tables nor the GRAMMAR help text describes --force's placement at all, so there is no sentence that is now wrong. Breadcrumbed on #354 so the item is visibly closed rather than dropped.

Gates

cargo test --workspace (28 suites, all green, 152 in dl's lib), cargo clippy --locked --all-targets -- -D warnings, cargo fmt --check. No existing expectation was edited to make any of this pass.

@blooop blooop closed this Aug 29, 2026
@blooop blooop reopened this Aug 29, 2026
@blooop

blooop commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

CI note, so the empty checks list is not read as a failure. GitHub did not create a pull_request run for the push of 6327ffe. Not a filter: ci.yml and prek.yaml both take pull_request with no branches:, and the same events fired normally for other branches in the same minutes. Closing and reopening the PR did not produce one either, so the event was dropped upstream rather than filtered.

6327ffe is verified green anyway, by workflow_dispatch on this branch, run 33266838802: ci, rust, e2e, packaging, public-api, review, rust-coverage, gate. Same jobs and same commit as a pull_request run, only the event differs. The codecov checks now on the PR came from it.

prek is the one job with no workflow_dispatch to reach it, so I ran it locally over every changed file instead: all hooks pass or skip, none fail.

Locally, from rust/: cargo test --workspace (28 suites green, 152 in dl's lib), cargo clippy --locked --all-targets -- -D warnings, cargo fmt --check.

Whoever merges should get the checks back from a fresh event. If they do not reappear, an empty commit on the branch is the cheap way to re-fire them, and worth doing before merge rather than after: this is a branch protection would wave through on absence rather than block, which is its own small finding.

@blooop

blooop commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

The empty commit did not re-fire it either, so the hazard is still open. This one needs a human at merge time.

Tried, in order: a normal push of 6327ffe, close and reopen, and now an empty commit (40a58ca) — the standard fix, and the one that usually works. All three produced a push event (Auto-publish ran each time) and no pull_request event.

It is specific to this PR rather than to the repo or the workflows. While 40a58ca produced nothing, wayfinder/devlaunch-308 fired CI [pull_request] and prek [pull_request] normally, minutes apart on the same repo and the same two workflow files. So pull_request deliveries are working; they are not reaching #506.

40a58ca is verified green anyway, now on the actual head rather than on its parent: run 33267089291, workflow_dispatchci, rust, e2e, packaging, public-api, review, rust-coverage, gate. The empty commit changed no tree, so this is the same content the earlier run passed, but the record now sits on the commit anybody will look at. prek is still the one job nothing can dispatch; it passes locally over every changed file.

What is left for whoever merges. The PR carries codecov and GitGuardian and nothing else. ci, prek and friends are absent rather than failing, and branch protection does not block on a check that never reported. Re-run them from the Actions UI, or push one more commit and hope the delivery resumes, before merging on a green that is not there.

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.

The word grammar's --force rule is unguarded in the same way

1 participant