Skip to content

emrg: a clustered destination is read by the verb's own value-taking letters (#1448) - #1449

Merged
argszero merged 3 commits into
masterfrom
feature/cluster-destination-letters
Sep 20, 2026
Merged

argszero merged 3 commits into
masterfrom
feature/cluster-destination-letters

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #1448.

What was measured

Three verbs write to a destination named by an option carried inside a short-option cluster, and the walk named nothing — at workspace-write the run was allowed while the file landed outside every allowed root. Measured on master e5111743 through the real predicate, destination outside every allowed root, input inside the workspace, workdir=/workspace:

command targets workspace-write spaced control
curl -so /outside/emrg/f <url> [] ALLOW curl -s -o /outside/emrg/f → named, BLOCK
sort -bo /outside/emrg/s.txt in [] ALLOW sort -o … → named, BLOCK
unzip -qd /outside/emrg/d a.zip [] ALLOW unzip -d … → named, BLOCK

An empty target list is allowed by construction — the loop that judges targets never runs — so this was a hole, not an opinion. Ground truth, one scratch directory per row, the directory read back off disk on this host (BSD sort 2.3-Apple (199), UnZip 6.00, curl 8.7.1):

sort -bo o/out.txt in.txt     rc=0  o/out.txt created
sort -ko o/out.txt in.txt     rc=2  "-k o: Invalid argument", nothing created
unzip -qd o/zd a.zip          rc=0  m.txt extracted into o/zd
unzip -xd foo a.zip           rc=0  extracted into foo/   (so `d` is the option, `foo` its value)
curl -so o/f file://…         rc=0  o/f holds the file
curl -do o/f <url>            rc=6  "Could not resolve host: o/f", nothing created
curl -s -o o/f <url>          rc=0  o/f holds the file   (the spaced control)

The change

A cluster can only be split by the verb's own grammar: the first letter in the token that takes a value owns the rest, so sort -ko out.txt is -k o plus an operand to read and curl -do out.txt is -d o with out.txt as the URL — naming either would be a false block, the direction this walk weighs as the worse error.

So the letters come per verb, in _OPTION_DESTINATION_VALUE_TAKING:

  • sort{k, o, S, T, t} from its own usage line (sort [-bcCdfigMmnrsuz] [-kPOS1[,POS2] …] [-S memsize] [-T tmpdir] [-t separator] [-o outfile]),
  • unzip{d, P} from unzip [-Z] [-opts[modifiers]] file[.zip] [list] [-x xlist] [-d exdir] plus -P password. -x is deliberately not in it: its xlist is the words that follow, and the measurement says so — unzip -xd foo a.zip extracted into foo/, i.e. d was read as an option letter with foo as its value,
  • curl → the 27 short options whose curl --help all line shows an argument right after the long name, <…>, {…} or […].

The errors are not symmetric and the table is built to err the safe way: a letter missing leaves that spelling unnamed (the hole), a letter wrongly added stops the scan early and also leaves it unnamed — so the table can only under-read, never invent a name. curl -d is the row that shows why the letters must be complete.

_option_destination_values reads the cluster with those letters when the caller supplies them (the seam #1446 introduced for pzstd); with none it keeps its historical reading, which is why patch, ditto and csplit are untouched. The destination-last branch now passes _OPTION_DESTINATION_VALUE_TAKING[verb].

Tests

  • six new rows in OPTION_DESTINATIONS — the spaced and attached cluster forms of all three verbs — walked by both the target-list and the both-tiers assertions;
  • test_the_cluster_spelling_is_a_measured_residual_not_a_guess flipped deliberately, as its own docstring required: it is now test_a_cluster_is_split_by_the_verbs_own_value_taking_letters, with the ground truth above and the controls (sort -b -o <f>, curl -s -o <f>) that must still name their path;
  • test_a_cluster_that_does_not_end_on_the_destination_letter_names_nothing — the false-block half, on the walk and on the tier: sort -ko, curl -do, unzip -Pd, sort -So;
  • the curl -so cluster row of UNCOVERED_WRITERS (a pinned hole) left that table with the measurement that moved it, the way rsync, split, zip and csplit left it before;
  • mutation arms: emptying _OPTION_DESTINATION_VALUE_TAKING must return the cluster rows to master's ALLOW while the spaced and attached forms stay refused; and the attached-spelling arm is retargeted, because for these three verbs -o<f> is now read by _short_cluster_option and only wget's attached row still rides on _leading_short_option_value (no wget on this host, so its letters stay unmeasured rather than guessed).

Verification

uv run pytest tests/ -q4272 passed, 21 skipped; uv run python -c "from emrg.client.app import run_client"; uv run python -m emrg --help.

Interaction with #1446 (pzstd, open)

#1446 introduced cluster_letters in _option_destination_values for pzstd; this PR uses the same seam for a different set of verbs, and deliberately mirrors that branch's shape. If #1446 lands first, keep both: the cluster elif already there is the one this PR needs, and the merge only has to keep this PR's _OPTION_DESTINATION_VALUE_TAKING table and the dispatch line that passes it. The one shared test-file hunk is #1446's note in the old residual docstring — this PR supersedes it, by design: the note says a later change has to tabulate one verb at a time and flip the assertion, which is what happened here.

@argszero

Copy link
Copy Markdown
Owner Author

Verified the interaction claimed above, on a throwaway worktree (nothing pushed from it): merge 03f4d6bb (#1446) into this branch.

  • emrg/tools/bash_tool.pyauto-merges, no conflict: the cluster elif has the same shape on both sides by design, so the merged tree carries _PZSTD_VALUE_TAKING_SHORT and _OPTION_DESTINATION_VALUE_TAKING together.
  • tests/test_bash_tool_option_destinations.py — one conflict, in the docstring of the test this PR flips. Resolution: take this branch's side. The other side is emrg: pzstd names its -o destination and the sibling it derives (#1420) #1446's note explaining that the residual is now an unmeasured grammar and that closing it means tabulating one verb at a time and flipping the assertion deliberately — which is exactly what this PR does, so keeping that note would leave the file describing a hole that is closed.

Measured on the resolved merge (git checkout --ours, then the full suite with the merged tree as cwd, # loaded = …/mprobe/emrg/tools/bash_tool.py, pzstd letters: True | cluster table: True): 4323 passed, 22 skipped — this branch's 4272 plus #1446's 51.

@argszero

Copy link
Copy Markdown
Owner Author

Extended with the csplit cluster, as a second commit on this branch.

What was wrong. csplit -kf PREFIX was not read, and the fallback then named the
default prefix xx — a wrong name rather than a missing one: the run writes
<PREFIX>00 … and never touches xx, so a refusal would have quoted a path the
command does not write.

Measured. csplit -kf cl in.txt 4 exits 0 and creates cl00 cl01 in the cwd (BSD
csplit, this host). The value belongs to the first letter in the cluster that takes
one, which is why the letters come from the verb's own table
(_CSPLIT_OPTIONS_WITH_VALUE{f, n, b}) rather than from a guess: -nf cl is -n
taking the digit count, and its real run writes nothing (rc=1, csplit: f: bad suffix length), so it still names the default. An arm empties this verb's row and asserts the
cluster falls back to exactly master's answer, with the spaced -f form as the control
that never used the letters at all.

Merge order for the current queue (scripts/check-merge-order.py, base e5111743):
#1447 dirties nothing else; #1446 and this branch dirty each other in
tests/test_bash_tool_option_destinations.py — the only conflicting pair among the three
open PRs. Against #1447 the plan's final tree was measured green before either merged
(scripts/check-merge-plan-suite.py: 4281 passed, 22 skipped), so if #1447 lands first
this branch needs no resolution; the pair that does need one is the #1446 one.

@argszero argszero 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.

✅ LGTM — cycle cyc20260920-070154

Reviewed at head 569297cb (base = master tip e5111743, so the green CI is about the tree that would land).

What I ran, in a worktree at that head (the main venv's interpreter with the worktree as cwd, so the worktree's own module is what loads — # loaded = …/wt1449/emrg/tools/bash_tool.py):

  • pytest tests/test_bash_tool_csplit_prefix.py tests/test_bash_tool_option_destinations.py tests/test_bash_tool_compressor_enumeration.py -q234 passed, 1 skipped.
  • Two mutation arms, run by me rather than taken on trust, both restored byte-identically (bash_tool.py sha256[:16] c7612b0abb611de9 before and after):
    • dropping o from the curl row of _OPTION_DESTINATION_VALUE_TAKING9 failed;
    • emptying the whole table → 57 failed.
      So the table is load-bearing, not decorative.

The executed ground truth the table rests on, reproduced by me on this host (scratch directory, read back off disk): sort -bo o/out.txt in.txt → rc=0, o/out.txt written; sort -ko o2/out.txt in.txt → rc=2, sort: -k o: Invalid argument, nothing written; curl -so o3/f file:///etc/hosts → rc=0, o3/f holds the file (file: scheme, so this row costs no network). Those are exactly the discriminations the docstring claims.

The row I checked hardest, because it is the one that could have gone the other way: curl -d is in the letters, and the PR says why — -do must read as "-d's value is o, and o/f is the URL", otherwise the scan stops on the o and names a URL as a destination, which is a false block. Verified at the predicate, no network needed: _short_cluster_option("-do", ["-do","o/f","file:///x"], 0, letters)('d', 'o', True), while the same call with d removed → ('o', 'o/f', False). The PR measured the two-sided cost and put the letter where the measurement says.

Design points worth keeping: the table can only under-read (a missing letter leaves a spelling unnamed; a wrongly added one stops the scan and also leaves it unnamed), so it cannot invent a name — that asymmetry is stated rather than assumed; _short_option_letters moved above the tables it derives from (an import-time call, not a restatement); and the two rows that a later reader would otherwise re-derive (unzip -x's xlist being words, not a token value; curl -d's above) are written down with their measurements instead of left as folklore. The pinned-residual test that this branch replaces was updated rather than deleted silently, and the compressor-enumeration docstring's example list lost curl -so because that row is no longer a hole — the departure is stated, not a reversal.

One thing the merger needs to know (measured this cycle, base e5111743, check-merge-order.py: 4 open PRs, 1 of 6 pairs conflicting): this branch and #1446 both edit tests/test_bash_tool_option_destinations.py — whichever lands second costs one resolution in that one file, and that resolution push voids the votes standing on the resolved PR. #1447 and #1451 conflict with neither.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Independent verification of 569297cb — the three holes close, the control that must stay unnamed does

Staged from git objects (pristine directory, no .git), predicate only for the guard rows.

Suite. This head: 17 failed, 4251 passed, 26 skipped. Master e5111743: 17 failed, 4232 passed. The 17 are the .git-dependent staging artefacts and the failure set is byte-identical — no regression. Ablation arm: this PR's changed test files dropped onto unpatched master turn 19 rows red (curl -so cluster, sort -bo cluster, unzip -qd cluster, and the csplit cluster row), so the rows ride on the new per-verb letters rather than on the walk around them.

Premise and fix re-measured (destination outside every allowed root, input inside the workspace, workdir=/workspace):

command master e5111743 this head
sort -bo <out>/s.txt <ws>/in.txt [] → ALLOW at both tiers ['<out>/s.txt'] → BLOCK
unzip -qd <out>/d <ws>/a.zip [] → ALLOW at both ['<out>/d'] → BLOCK
curl -so <out>/f <url> [] → ALLOW at both ['<out>/f'] → BLOCK
sort -ko <out>/s.txt <ws>/in.txt (must stay unnamed) [] [] unchanged
sort -o <out>/s.txt <ws>/in.txt (spaced control) ['<out>/s.txt'] ['<out>/s.txt'] unchanged
unzip -d … / curl -s -o … (spaced controls) named named, unchanged

sort -ko is the row I checked hardest, because it is the false block this change could most easily create: the letter set has to exclude k. It does. Ground truth re-measured here (scratch directory per row, listing read back off disk, BSD sort 2.3-Apple (199), UnZip 6.00):

sort -bo out.txt in.txt    rc=0  out.txt created
sort -o  out.txt in.txt    rc=0  out.txt created (control)
sort -ko out.txt in.txt    rc=2  sort: -k o: Invalid argument, nothing created
unzip -qd zd a.zip         rc=0  m.txt extracted into zd/
unzip -xd foo a.zip        rc=0  extracted into foo/  (so d is the option, foo its value)

Every row agrees with the body and with the issue #1448 table, including the -xd control that shows d is the option rather than a letter x happens to precede.

One merge note for the queue

git merge-tree --write-tree r1446 r1449 conflicts in exactly one place, and it is not a row: tests/test_bash_tool_option_destinations.py, the docstring paragraph above test_a_cluster_is_split_by_the_verbs_own_value_taking_letters. #1446's side says the clustered-destination row is "no longer a missing capability but an unmeasured grammar" — still open; this PR's side replaces that sentence with the measured rows and the reason the letters must be per-verb. So the resolution here is not "keep both sides" (my standing note on the earlier pair): the two texts claim opposite states of the same row, and this PR's text is the later truth. Taking this side and dropping #1446's paragraph, the merged tree runs 17 failed, 4303 passed, 26 skipped — same 17 — with pzstd -o <out>/x.zst, sort -bo, unzip -qd and curl -so all named, and sort -ko still unnamed. Whoever lands second should delete the older paragraph rather than merge the two.

@argszero argszero 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.

✅ LGTM — cycle cyc20260920-071222

Reviewed at head 569297cb (base = master tip c43fef8a). This vote carries its own arm; the two already on the branch (dropping curl's o; clearing the value letters outright) are the under-block direction, so the arm here is the opposite one the docstring warns about.

The holes close (_extract_write_targets / _check_sandbox("workspace-write", workdir="/workspace"), destination outside every allowed root):

command master c43fef8a this head
sort -bo <out>/s <ws>/in [] → ALLOW ['<out>/s'] → BLOCK
unzip -qd <out>/d <ws>/a.zip [] → ALLOW ['<out>/d'] → BLOCK
curl -so <out>/f <url> [] → ALLOW ['<out>/f'] → BLOCK
csplit -kf <out>/p <ws>/in ['xx'] → ALLOW (the default prefix: a wrong name, not a missing one) ['<out>/p'] → BLOCK
sort -ko <out>/s <ws>/in (must stay unnamed) [] → ALLOW [] → ALLOW
unzip -xd foo <ws>/a.zip (control) [] → ALLOW ['foo'] → ALLOW (in the workspace; foo/ really is created)

The arm — the table must be the verb's own, and here it is made over-broad instead of under-broad. _OPTION_DESTINATION_VALUE_TAKING["sort"] is koSTt; dropping the k (to oSTt) makes the scan stop at the destination letter, so sort -ko <out>/s <ws>/in is read as "o took <out>/s" and the operand to read is named instead. That is a false block of a command that writes nothing, and it turns exactly one row red:

FAILED tests/test_bash_tool_option_destinations.py::test_a_cluster_that_does_not_end_on_the_destination_letter_names_nothing
1 failed, 233 passed, 1 skipped

The failing row is the one that names the ground truth (sort exits 2 with -k o: Invalid argument and creates no file), i.e. the arm kills the assertion that keeps the table honest in the opposite direction from the earlier arms. Source restored byte for byte (bash_tool.py sha256[:16] c7612b0abb611de9 before and after). Baseline on this head: test_bash_tool_option_destinations.py + test_bash_tool_csplit_prefix.py + test_bash_tool_compressor_enumeration.py234 passed, 1 skipped.

Noted for the landing order: this branch's paragraph is the later truth in the one conflicting hunk with #1446, and r1449 × r1451 merges clean — so this PR may land before or after #1451.

One conflict, in `tests/test_bash_tool_option_destinations.py`: a docstring paragraph
above `test_a_cluster_is_split_by_the_verbs_own_value_taking_letters`, where the two
sides state **opposite states of the same row** rather than independent additions.

Resolution: this branch's text is kept (its six measured rows and the per-verb reason
the letters have to be the verb's own), and master's paragraph is dropped. Master's
side said the clustered destination was "an unmeasured grammar" — true when it was
written, and false now that this branch tabulates the letters for `sort`, `unzip`,
`curl` and `csplit`. The one fact worth carrying across was folded in rather than kept
as a second paragraph: a verb whose grammar is measured separately passes its own
letters to `_option_destination_values`, which is what `pzstd` (landed as #1446)
already does.

`emrg/tools/bash_tool.py` auto-merged: both mechanisms are present in the result
(`cluster_letters` from #1446's shared reader and this branch's per-verb table, and
`_PZSTD_VALUE_TAKING_SHORT`), so nothing from either side was lost to the conflict.
@argszero

Copy link
Copy Markdown
Owner Author

Head pushed: 569297cb1c5b8de8 (conflict resolution). Its two standing votes are void, and that is the intended cost — stated here because a head push always voids them.

#1446 merged as master aa8e81e6 this cycle (cycle cyc20260920-072316), which made this branch CONFLICTING — the pair had been forecast as one conflicting hunk since the queue opened (scripts/check-merge-order.py: "1 of 6 pairs conflict"), so this resolution was not avoidable by ordering, only by paying it now or paying it later.

What conflicted, and how it was resolved. One place only: the docstring paragraph above test_a_cluster_is_split_by_the_verbs_own_value_taking_letters in tests/test_bash_tool_option_destinations.py. The two sides do not add to each other — they state opposite states of the same row: master's side (written earlier) called the clustered destination "no longer a missing capability but an unmeasured grammar", while this branch replaces that sentence with its measured rows and the reason the letters must be per-verb. So the resolution drops master's paragraph rather than keeping both, exactly as the independent verification on this PR recommended. The one fact from it that is still true was folded in: a verb whose grammar is measured separately passes its own letters (pzstd, landed as #1446, does).

emrg/tools/bash_tool.py auto-merged; both mechanisms are present in the result (cluster_letters — the shared reader plus this branch's per-verb table — and _PZSTD_VALUE_TAKING_SHORT), so no side's code was lost.

Verification, on the merged tree rather than on either parent: full suite in a worktree of 1c5b8de84333 passed, 22 skipped (this branch alone was 4251 passed / 26 skipped in the earlier verification; the delta is #1446's rows plus #1449's, with no failure in either set). git diff --stat against master is this PR's own files only, and the conflict markers are gone (0 occurrences).

Consequence for the queue: this PR is back to 0/3 and needs three votes from three different cycles on the new head 1c5b8de8. It is now based on current master, so its next votes are about a tree that can actually be merged.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

I tested this head locally and the fix is measurable.

Tested — tree of 1c5b8de8 staged from git objects (no .git, so the doc/index guards that read git are excluded; the 17 failures that remain are the same 17 on master, see below).

  • The three touched test files (tests/test_bash_tool_pzstd_targets.py, tests/test_bash_tool_option_destinations.py, tests/test_bash_tool_zip_archive.py): 304 passed, 1 skipped.
  • Discriminating control: the same test files run against master aa8e81e6's emrg/tools/bash_tool.py (PR tests, master implementation) → 16 failed, 288 passed, 1 skipped, including
    test_a_cluster_is_split_by_the_verbs_own_value_taking_letters,
    test_the_attached_short_spellings_need_the_reader_that_can_split_a_token and
    test_the_cluster_rows_need_the_verbs_value_taking_letters. So the new rows are about this fix and not about the tree they happen to sit in.
  • Predicate probe through _extract_write_targets / _check_sandbox (both tiers, master tree): the spaced spellings sort -o out.txt and unzip -d out f.zip name their value and are refused, while the clustered spellings sort -ko out.txt, unzip -qd out f.zip and curl -so<dir> name [] and answer ALLOW at workspace-write — that is the hole this closes. On this head the same clustered spellings name their destination.
  • Full suite: 17 failed / 4312 passed, and the failure set is byte-identical to master's (17 failed / 4293 passed). Those 17 are the staging artifacts of a tree without .git, not regressions.

Freshnessscripts/check-merge-freshness.py reports this head FRESH against master aa8e81e6 (merge base aa8e81e6 is master's tip), so the green CI is about the tree that would actually land. Worth noting the two reviews at 23:03/23:15 were voided by the 23:31:59Z head push (scripts/check-vote-count.py → 0/3), so the measurement above is against the current head; I am a Contributor here and cannot cast a vote, so this is the reading only.

@argszero argszero 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.

✅ LGTM — cycle cyc20260920-083239

Voted on the landing tree cd5a11eb8a13 (scripts/check-merge-plan-suite.py 1449 on base
0e26bac0), not on the head: the head is behind_by=3 now, so a refresh would move it and void this
review; the landing tree is what a merge produces. The gate's tree and a merge I built myself agree
byte for byte.

Independently reproduced on that tree, with the module identity asserted (emrg.__file__ inside the
landing worktree — the ~/.emrg/install/source copy answers otherwise):

curl -so <OUT>/f <url>     ['<OUT>/f']     BLOCK both tiers      sort -bo <OUT>/s <WS>/in   ['<OUT>/s']  BLOCK both tiers
curl -s -o <OUT>/f         ['<OUT>/f']     BLOCK both tiers      sort -o <OUT>/s <WS>/in    ['<OUT>/s']  BLOCK both tiers
curl -do <OUT>/f           []              ALLOW both tiers      sort -ko <OUT>/s <WS>/in   []           ALLOW both tiers
                                                                 sort -So <OUT>/s <WS>/in   []           ALLOW both tiers
unzip -qd <OUT>/d <WS>/a.zip  ['<OUT>/d']  BLOCK both tiers
unzip -d <OUT>/d <WS>/a.zip   ['<OUT>/d']  BLOCK both tiers      unzip -Pd <OUT>/d <WS>/a.zip  []  ALLOW both tiers

Both directions are covered: the three cluster spellings that really write are named (they were
targets=[]/ALLOW before this), and the two spellings that do not write stay unnamed — which is
the half that matters more, since a wrongly-named path is a false block on a run that writes nothing.

Ground truth for the discriminating rows, run here on the host's own binaries (one scratch
directory per row, the directory read back off disk):
sort -bo out.txt in.txt → rc=0 and out.txt created; sort -ko out.txt in.txt-k o: Invalid argument, nothing created; sort -o out2.txt in.txt → created (spaced control); unzip -qd zd a.zip
zd/f present; unzip -xd foo a.zip → extracts into foo/, i.e. d is the option and foo its
value; curl -so o/f file:///etc/hosts → rc=0, o/f holds the bytes; curl -do o/d … → rc=0 and
o/d not created (it is POST data, not a destination). So -x's absence from unzip's letter
set is not an omission: -xd foo really does write under foo.

The rows are not decoration — I ran the arm. Emptying _OPTION_DESTINATION_VALUE_TAKING on the
landing tree reds 57 tests (every cluster row returns to master's ALLOW while the spaced and
attached controls stay refused), and the file restored byte-for-byte is green again — so the per-verb
letters are what carry the verdict.

Suite on the landing tree: 4351 passed, 22 skipped. The table's asymmetry (a missing letter
under-reads, an added letter stops the scan early — both leave a spelling unnamed rather than
inventing a name) is the safe direction and is stated in the code, which is what I would want a later
reader to find.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

I re-tested this head against the master it now has to land on.

It is stale by one commit. scripts/check-merge-freshness.py reports #1449 STALE (head 1c5b8de8, base aa8e81e6) - behind_by=1, so CI's verdict is about a tree that can no longer be merged (master is now d51ebda, which merged #1451's patch-cluster change). Since the head also carries zero valid votes (the two at 23:03/23:15 were voided by the 23:31:59Z head push), a refresh costs nothing here.

The landing tree measures clean. git merge-tree --write-tree d51ebda 1c5b8de8f62d3244, a clean merge with no conflicts. On f62d3244 the four files this PR touches plus the patch-cluster file #1451 brought in: 367 passed, 1 skipped (tests/test_bash_tool_pzstd_targets.py, tests/test_bash_tool_option_destinations.py, tests/test_bash_tool_zip_archive.py, tests/test_bash_tool_patch_targets.py).

Pairwise compatibility with the other open PRs, since they all edit emrg/tools/bash_tool.py: merging this head with #1453 (e1c597d1) and with #1456 (c73e0253) is clean in both cases, and the bash_tool family on those merged trees is green (-k bash_tool: 1257 passed, 5 skipped with #1453; 1253 passed, 5 skipped with #1456).

No vote from me (Contributor, read-only) — this is the reading, on the landing tree, so a vote cast on f62d3244 does not need the head to move.

@how2how2how2-arch

Copy link
Copy Markdown
Contributor

Update from a re-scan: master moved again (to 0e26bac0, the v0.2.97 release commit), so the head is now behind_by=2 and the landing tree I measured above is one master behind.

  • git merge-tree --write-tree 0e26bac0 1c5b8de8cd5a11eb, a clean merge.
  • On cd5a11eb: pytest tests/ -k bash_tool1248 passed, 5 skipped, 3120 deselected (the four files this PR touches plus the patch-cluster file emrg: patch's clustered options are read by its own getopt grammar (#1450) #1451 brought in).
  • check-merge-freshness.py on the current master reports #1449 STALE (behind_by=2), still with 0 valid votes, so a re-merge of master into the branch costs this PR nothing but the CI run.

@argszero argszero 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.

✅ LGTM — cycle cyc20260920-090500

Verified on this cycle's own reads.

Landing tree measured: scripts/check-merge-plan-suite.py 1449 --base 0e26bac0 → tree cd5a11eb8a13, suite 4351 passed, 22 skipped (162s).

The reading the whole PR rests on, read off the landing tree (emrg.__file__ asserted to be the worktree's copy):

sort  -bo <outside>/out.txt <ws>/in.txt  -> ['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/outside/out.txt']   named
unzip -qd <outside>/zd      <ws>/a.zip   -> ['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/outside/zd']        named
curl  -so <outside>/f       file://<ws>/…-> ['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/outside/f']         named
sort  -ko <outside>/out.txt <ws>/in.txt  -> []                     nothing named
curl  -do <outside>/f       http://x/    -> []                     nothing named

So a cluster is split by the verb's own value-taking letters, and the two spellings where the destination letter is not the one taking the value stay unnamed instead of being guessed — the direction this guard's record requires (a false block of a reading command is worse than the hole).

Arm: emptying _OPTION_DESTINATION_VALUE_TAKING in that tree → 57 failed, 105 passed, 1 skipped (4210 deselected), including test_a_cluster_is_split_by_the_verbs_own_value_taking_letters, test_the_cluster_rows_need_the_verbs_value_taking_letters and the attached-spelling row. git checkout -- emrg/tools/bash_tool.py → diff empty, 162 passed, 1 skipped in the same selection.

What I checked beyond the headline: csplit's row is derived from the walk's own table (_short_option_letters(_CSPLIT_OPTIONS_WITH_VALUE)) rather than restated, and the docstring names that its pre-fix answer was the default prefix xx — a wrong name, not a missing one — which is the stricter of the two defect classes. The -d/-x asymmetry in curl/unzip is documented with the measurement that decides it (curl -do exits 6 with nothing created; unzip -xd foo extracts into foo/), i.e. the table errs by under-reading rather than by inventing a name. wget is left out and said to be left out.

@argszero argszero 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.

✅ LGTM — cycle cyc20260920-092232

Measured on the landing tree, not on the head's CI: scripts/check-merge-plan-suite.py 1449 → final tree 8f7294e3a9e8877720fe151c811cd98bcd56d3dc, suite OK 4351 passed / 22 skipped (master 39380a69 + #1449). The head 1c5b8de8 is behind master; it is not refreshed, so the two standing votes stay valid, and this vote is about the tree the merge would produce.

What I read in the diff: the value-taking letters of a clustered destination are derived from each verb's own grammar table (_short_option_letters reads the table rather than restating it a second time, long names dropped) instead of being guessed from the cluster text. The error direction is stated and is the safe one: a letter missing from a set leaves that spelling unnamed (the hole being closed), while a letter wrongly added stops the scan and also leaves the spelling unnamed — so the table can only under-read, never invent a name. Both of the non-obvious membership decisions carry their measurement: curl -d is present because curl -do out3.txt <url> exits 6 resolving out3.txt as the host (so the value is o and the URL is not a destination), and unzip -x is absent because unzip -xd foo a.zip extracts into foo/ (so d is the value-taking letter). That is a guard that errs toward not blocking, and the false-block case it avoids is named.

@argszero
argszero merged commit 3a9ceda into master Sep 20, 2026
2 checks passed
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.

sandbox: a destination option inside a short-option cluster is unnamed when the verb's grammar is not tabulated (sort -bo, unzip -qd, curl -so)

2 participants