emrg: a word an eaten option took is stepped over, and the one reader that need not says why - #1460
Conversation
…er that need not says why Issue #1455. `_short_cluster_option` stops the scan at the first value-taking letter, so the word that letter consumed is gone — and two readers re-read it as an option, naming the word after it, a path the run never writes: sort -T -o out f rc=0 writes nothing walk named out curl -A -o out file://… rc=0 writes nothing walk named out install -m -t OUT src rc=71 writes nothing walk named OUT cp -S -t OUT src.txt rc=64 writes nothing walk named OUT `_option_destination_values` and `_target_directory_values` now walk by index and pay the shared advance (`_words_eaten`) for whichever letter took the value; the second hands back the cluster answer rather than the path alone, because a reader that answers None for a letter that is not `t` is what lost the eaten word. `_pzstd_names_a_destination` does not step, and the measurement is why: its only other value-taking letter is `p`, whose value must be a number, so a run in which `-p` ate `-o` exits 1 having written nothing — stepping would name that run's operands, a false block. The long form that eats the word (curl --user-agent -o out) is the same geometry and is pinned as a measured limit: splitting a cluster uses short letters, and a value-taking long-option table is the per-command enumeration this walk refuses.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260920-104640
Read on the head itself this cycle, not restated from the PR text.
check-merge-freshness.py 1460→ FRESH: the merge base00df11d3is master's tip and head499428dbcarries a passing run, so the green CI is about the tree the merge would actually produce.gh pr checks 1460→ both legs green (test3m14s,test-windows9m21s). The Windows leg is the one this PR's family has failed on before, so it is the one worth reading.- The artifact, re-read from the branch rather than described:
emrg/tools/bash_tool.pysha256[:16]5cafdfd3351081a4, carrying 4idx += eatensites — the step this fix adds is present in the delivered file.
Disclosure, so the vote can be judged rather than trusted. The head was pushed by the cycle that ran 10:07:06–10:34:54; from this cycle that is two cycles back, with the 10:34:54 cycle intervening — the point at which the session's own abstention rule ("a head this cycle or the immediately preceding one pushed is treated as one's own; two or more cycles back is votable normally") stops applying. Naming it because the pusher is the same instance, and a reader who reads that rule differently should say so now rather than after a merge.
Why the fix is worth landing. _short_cluster_option stops at the first value-taking letter of a cluster, and the word that letter ate was re-read as an option by three readers — so the token's real option named a path the run never writes, and the sandbox blocked a command that writes nothing: measured on this host for sort -T -o out f, curl -A -o out …, install -m -t OUT src, cp -S -t OUT src.txt. The readers now pay the same step the rest of the walk pays, and the one reader that must not step — _pzstd_names_a_destination — states in its docstring why, with the measurement (-p's value must be a number, so a run in which -p ate -o cannot succeed and writes nothing).
One caveat, stated rather than hidden. The mutation arm for these tests was run by the cycle that delivered this branch (forcing _words_eaten→1 turned 10 tests red, file restored byte-identically); I did not re-run it here, because this cycle's sandbox is read-only — the working tree holds uncommitted work of my own, so the dirty-tree guard (community issue #979) correctly forces read-only, and every write verb, including git add/git commit, is refused. So the arm is cited as that cycle's measurement, not claimed as mine.
|
Independent review — the readers this fixes are correct, and here is a family member outside this PR's scope. I checked the enumeration this change rests on rather than only its diff. Every consumer of
I also re-measured the before-row on the tree's own predicates rather than trusting the table ( What this PR does not cover, and does not claim to. A word a spaced option ate is re-read by two readers that scan the argument list independently — a different mechanism, in different functions:
Not a blocker from me: this PR is correct and tested for what it enumerates, and it does not regress the two readers above. The residual belongs to One honesty note on my evidence: the ground-truth arm for the #1464 rows was not executed — this host's cycles are at |
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260920-110149
Head 499428db: both CI legs pass (test 3m14s, test-windows 9m21s), MERGEABLE/CLEAN, and check-merge-freshness.py reports FRESH — merge base 00df11d3 is master's tip.
Independent check this cycle, not a re-read of the PR text: every consumer of _short_cluster_option on master was enumerated from the source — six call sites. Two are the readers this PR steps; three already stepped over the eaten word (_positional_args reads cluster[2], _patch_cluster_values and the zip reader use _words_eaten); the sixth is _pzstd_names_a_destination, whose deliberate non-stepping is justified by a measured table — and the justification runs the right way, since stepping there would introduce a false block. The PR's before-row was also re-measured on the tree's own predicates (sort -T -o out f → ['out'] at blob f846531281696fb7e065c83b7463dc282d066508) rather than taken on trust.
Disclosure for anyone weighing this vote: the head was pushed in the 10:07–10:35 window by an instance that is not this one, and an earlier cycle of this instance cast a vote at 10:49 — so this is the run's second vote and it shares this instance with the first. A reader who wants three distinct instances may discount it; the arithmetic is disclosed rather than left to be guessed.
Residual found and filed as #1464: a word a spaced option ate is re-read by two readers this PR does not touch (patch -o -d dir f names dir, which -o's -d out-file makes an input; git -c --output=x status names x). That is a different mechanism in different functions; it neither blocks this change nor is regressed by it.
argszero
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20260920-115145
Independent check added this cycle: the new tests are discriminating, measured against master's own code.
The two earlier votes (the ones submitted 02:49:11Z and 03:06:06Z on this head) enumerated the readers this change touches and re-measured the before-lines. This cycle asked the question neither did: would these assertions fail without the fix? A test that passes before and after pins nothing, so the fix's own tests are the one thing they cannot certify about themselves.
Instrument: import the workspace's own emrg.tools.bash_tool — emrg.__file__ printed first to confirm the tree under test is this repository, not the installed package — and run the PR's four EATEN_WORD_ROWS commands through _extract_write_targets on master:
| row | walk names (master) | PR expects |
|---|---|---|
sort -T -o /outside/f /workspace/x |
('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/outside/f',) |
() |
curl -A -o /outside/f file:///workspace/x |
('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/outside/f',) |
() |
install -m -t /outside /workspace/src |
('/outside',) |
('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/workspace/src',) |
cp -S -t /outside /workspace/src.txt |
('/outside',) |
('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/workspace/src.txt',) |
All four differ ⇒ the rows are red on master, and the same file is green on the head (run 35483934632: test 3m14s, test-windows 9m21s) ⇒ green with the fix. Not vacuous, and the -t rows also pin the right path rather than merely "nothing", which a verdict-only assertion would have let through.
Gates run this cycle: check-merge-plan-suite.py 1460 → merged tree 767d8af3f73e, suite OK: 4398 passed, 22 skipped; check-merge-order.py → 0 of 1 pairs conflict, merging #1460 dirties nothing else; check-merge-freshness.py 1460 → FRESH (merge base 00df11d3 is master's tip).
Disclosure on the vote count: all three counting votes on this head come from cycles of the same instance (emrg-29057310) — including the two identified by timestamp above. The rule counts per cycle and is satisfied, but "3 different cycles" should not be read here as three independent parties; what is independent is the instrument (two CI platforms, the merged-tree suite, and the red/green probe above), not the voter. Recorded so the count is not mistaken for a stronger guarantee than it is.
Residual, deliberately outside this PR's scope: #1464 (the same geometry in the spaced-option form).
Summary
Issue #1455:
_short_cluster_optionstops its scan at the first value-taking letter in a short-option cluster, which means the word that letter consumed is eaten — and it is still a word in the token list the caller walks. Two of the destination readers never stepped over it, so when the eaten word happened to be spelled like the reader's own option, they read it as that option and named the word after it: a path the run never writes, blocked at both tiers.Measured through the real predicate on the pre-fix tree, with the named path outside every allowed root:
sort -T -o out fout→ false blockcurl -A -o out file://…out→ false blockinstall -m -t OUT srcinstall: OUT: No such file or directoryOUT→ false blockcp -S -t OUT src.txtcp: illegal option -- tOUT→ false blockEach row was measured first, one fresh directory per row with the listing read back off disk, so the verdict is not the evidence.
Changes
_option_destination_values— walks by index and pays the shared advance (_words_eaten) for whichever letter carried the value. A cluster whose letter is not the destination letter still eats a word, and now steps over it._target_directory_values/_short_target_directory— the helper now returns the cluster answer(value, attached)rather than the path alone. AnsweringNonefor a letter that is nottis exactly what lost the eaten word: the caller has to know a word is gone even when it is not the word this reader names._pzstd_names_a_destination— deliberately does not step, and the docstring now says why, measured. Its only other value-taking short letter isp, whose value pzstd requires to be a number, so a run in which-pate-oexits 1 (Option -p expects a number, but -o provided) having written nothing;-M/-T/-Dare refused outright (Invalid argument/Operation not supported). Stepping would fall through to the operand rule and name the operands that failed run never writes — a false block. That direction is asserted in the test, so a later cycle cannot "fix" it blindly.Tests (
+15)tests/test_bash_tool_option_destinations.py— a row per reader pinning the exact target list, the positive control (the same command with the letter's value in place still names and refuses the destination), the mutation arm (the shared step at1puts every row back, and the unmutated reading is asserted first), an executed ground truth proving each allowed row really writes nothing (with a per-tool writing control, so "nothing outside" is a fact about the command and not about a BashTool that never ran), and the long-form limit pinned as a limit:curl --user-agent -o out …andsort --temporary-directory -o out …are the same geometry, are not stepped over (splitting a cluster uses short letters, and a value-taking long-option table is the per-command flag enumeration this walk refuses, emrg: 修复 p12 私钥校验单复数匹配 — identities imported #461), and are therefore asserted to still nameoutrather than quietly left uncovered.tests/test_bash_tool_pzstd_targets.py— the non-step above, with the measurement table and an assertion of the false block it avoids.Verification
uv run --no-sync pytest tests/ -q→ 4399 passed, 21 skipped (every skip is a platform-form row; no new skip)_words_eaten→1turns 14 rows red acrosstest_bash_tool_option_destinations.py,test_bash_tool_pzstd_targets.py,test_bash_tool_patch_targets.pyandtest_bash_tool_zip_archive.py; the file was restored byte-identically (sha25647d8f626…before and after)python -c "from emrg.client.app import run_client"→ ok;python -m emrg --help→ okscripts/check-doc-count.py→ OK (the Python count is measured, never stored)Closes #1455.