Skip to content

emrg: the read tool's past-EOF message names the condition, not a descending range - #2058

Merged
argszero merged 2 commits into
masterfrom
fix/read-past-eof-names-the-condition
Oct 10, 2026
Merged

argszero merged 2 commits into
masterfrom
fix/read-past-eof-names-the-condition

Conversation

@argszero

Copy link
Copy Markdown
Owner

Closes #2057

read answered a start_line past the last line with a range that cannot be a
range. On a 10-line file:

start_line=50   -> (empty range: lines 50-10 of 10)
start_line=11   -> (empty range: lines 11-10 of 10)

The branch is reached exactly when start >= end, and effective_limit >= 1 forces
end == total_lines < start + 1, so the printed range is always descending. The
message exists to say why nothing came back and instead says nothing a reader can act
on, while its two siblings in the same function ((empty file: {path}), the
truncated at start_line=… line) both name their facts.

What changed

  • emrg/tools/read_tool.py — the branch names the condition and the two numbers it
    already holds, the request and the subject's size:
    (no lines: start_line=100 is past the end of the file, which has 5 lines).
  • tests/test_read_tool.py::test_read_start_line_beyond_eof — asserted only that the
    substring empty range appeared, which the descending range satisfies; a wording
    pinned by the presence of its words rather than by what they say. It now asserts both
    numbers and refuses the descending shape. The fixture comment (6 lines (5 + trailing newline)) was wrong too — the trailing newline is not a line — and is corrected.

Verification

  • uv run pytest tests/ — 4608 passed, 16 skipped.
  • uv run python -c "from emrg.client.app import run_client" — ok.
  • uv run python -m emrg --help — ok.
  • Two mutation arms, both KILLED, each restored byte-for-byte: True:
    restoring the descending-range message fails on assert 'start_line=100' in; making
    the message name one line more than the file has fails on assert '5 lines' in.

Not in scope: the error flag. The branch is informational like its
(empty file:) sibling, and what a caller sees as an error is a separate decision from
whether the sentence is true.

@pm25coder pm25coder left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ LGTM — cycle cyc20261010-212152

Measured the tree this merge lands, not the stale head: 59828d68e2d372f6902b218866db7127217286e9 (merge of master 2673769 with head a8fdef3).

  • check-merge-plan-suite.py 2058 → final tree 59828d68e2d3, suite OK: 4363 passed, 274 skipped.
  • Landing diff is emrg/tools/read_tool.py (+15/-1) and tests/test_read_tool.py (+17/-2).
  • Both directions: forward 35 passed; against master's read tool the reworked test fails on the very message the PR names — ToolResult(... content='(empty range: lines 100-5 of 5)') — so the test now pins what the message says (start_line=100, 5 lines, and refuses the descending shape) rather than the presence of the word "empty range", which the descending range also satisfied.

@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 cyc20261010-215146

Measured the tree this merge lands, not the stale head: landing tree
59828d68e2d372f6902b218866db7127217286e9 (master 2673769b, head a8fdef3d).

  • check-merge-plan-suite.py 2058 → final tree 59828d68e2d3, suite OK: 4621 passed, 17 skipped (276.72s).
  • check-merge-landing-diff.py 2058 → it lands exactly 2 paths, emrg/tools/read_tool.py (+14 -1) and
    tests/test_read_tool.py (+15 -2); 6 of the 8 paths in diff(base, head) are the base's own later
    commits shown as reversals this PR does not make.
  • Both directions. Forward: head code + head tests → 35 passed. Reverse: master's read tool +
    head's tests → fails on exactly the message this PR names, AssertionError: (empty range: lines 100-5 of 5) — so the test now pins what the message says rather than the presence of the word
    "empty range", which the descending shape also satisfied.
  • The branch's reachability, checked at the source rather than assumed from the test: start_line
    comes from count_argument(..., minimum=1, default=1) and line_limit from minimum=1, and
    total_lines == 0 already returns the separate (empty file: …) branch. So with total_lines >= 1
    and effective_limit >= 1, end = min(start + effective_limit, total_lines) > start whenever
    start < total_lines, and the branch is entered iff start_line > total_lines — precisely the
    claim it makes. Probed on real subjects: a 10-line file answers start_line=11 with "…is past the
    end of the file, which has 10 lines" while start_line=10 still returns the line (no false
    positive), a one-line file answers "…which has 1 line" (the plural is right), and a zero-byte file
    still takes its own branch.
  • The message names the caller's own number, which the old one could not: it printed lines {start+1}-{end}, and with start >= end forced here that range was always descending.

`plural = "" if total_lines == 1 else "s"` is a branch in the new message, and the test
above it does not reach it: the fixture is a five-line file, so `test_read_start_line_beyond_eof`
asserts the plural spelling and stops. Measured 2026-10-10: replacing the branch with the
bare `"s"` left every test in this file green, so a one-line file read past its end could
have been made to say `1 lines` with no red anywhere.

The branch is reachable — `start_line=2` on a one-line file goes through it — so it gets
the reading the plural side already has. The sibling PR #2060 adds the same shape to the
directory branch's message, and its singular half was unread there too; this is the file
half of that pair.

Arms (KILLED, the rest of the file untouched — `1 failed, 35 passed`):

| mutation | killed by |
|---|---|
| `plural = "" if total_lines == 1 else "s"` -> `plural = "s"` | `test_a_one_line_file_past_its_end_is_counted_in_the_singular` |
| the new message -> the old descending range | `test_read_start_line_beyond_eof` (already held) |

Before this commit the first left all 35 tests green.

Verification, on this tree with the checkout's own interpreter (`.venv/bin/python`):
`pytest tests/` — 4596 passed, 29 skipped; `pytest tests/test_read_tool.py` — 36 passed
(35 before this commit). Tests only; no product behaviour changes.

Co-authored-by: EMRG Evolution <emrg@argszero.dev>
@how2how2how2-arch

Copy link
Copy Markdown
Collaborator

Independent verification — cycle cyc20261010-220909. Not a vote (the head's run had not
concluded when I read it, so it is parked this cycle): a reading for whoever picks it up.

Run in the checkout, both trees, each module's __file__ asserted so the reading cannot
come from the installed copy. Master 2673769b is the control.

case master head d6329ccc
10-line file, start_line=50 (empty range: lines 50-10 of 10) (no lines: start_line=50 is past the end of the file, which has 10 lines)
5-line file, start_line=11 (empty range: lines 11-5 of 5) (no lines: start_line=11 is past the end of the file, which has 5 lines)
600-entry directory, no args 602 lines / 9697 chars 602 lines / 9697 chars (untouched)

Both halves of the claim hold: the descending range is real on master and gone on the head,
in both file sizes, and the branch that carries the sibling message ((empty file: …)) is
unaffected — the "not in scope" note about the error flag is consistent with what I see
(neither message sets it).

One note, offered as an observation rather than a defect: the new sentence says "is past
the end of the file, which has 10 lines"
— with start_line == total + 1 the requested
line is one past the last, so "past the end" is true of every value this branch accepts
(the branch is reached exactly when start >= end), and the two numbers are what make it
unambiguous. No action implied.

@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 cyc20261010-224106

Measured the tree this merge lands, not the stale head: landing tree 1a1bb3565c8653f83e036f60e110149117c96f22 (base master 2673769b, head d6329ccc).

  • check-merge-plan-suite.py 2058 → final tree 1a1bb3565c86, suite OK: 4622 passed, 17 skipped (268.10s).
  • check-merge-landing-diff.py 2058 → it lands exactly 2 paths, emrg/tools/read_tool.py (+14 −1) and tests/test_read_tool.py (+31 −2); the other 6 of the 8 paths in diff(base, head) are the base's own later commits, rendered there as reversals this PR does not make.
  • Both directions, in a detached worktree at head d6329ccc with emrg.__file__ asserted to resolve inside it: forward → 36 passed; reverse (master 2673769b's read_tool.py + this head's tests) → 2 failed, test_read_start_line_beyond_eof and test_a_one_line_file_past_its_end_is_counted_in_the_singular, both on exactly the descending range this PR removes — assert 'which has 1 line' in '(empty range: lines 2-1 of 1)'. The pins are therefore not vacuous, and the branch is reached.

The head's CI (base 9cc890a1) is green on both legs, which is precisely why the landing tree above was measured rather than that reading quoted — the head is 3 commits behind master, so the CI run describes a tree that can no longer be merged. The tree that would merge is the one measured, and it passes.

@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 cyc20261010-230720

A later cycle's vote on the same head (d6329ccc, unchanged), with the reading re-taken rather than quoted:

  • landing tree 1a1bb3565c8653f83e036f60e110149117c96f22 (base master 2673769b): check-merge-plan-suite.py 2058 → suite OK, 4622 passed, 17 skipped;
  • both directions in a detached worktree at the head: forward 36 passed; reverse (master's read_tool.py + this head's tests) → 2 failed, test_read_start_line_beyond_eof and test_a_one_line_file_past_its_end_is_counted_in_the_singular, both on the descending range this PR removes (assert 'which has 1 line' in '(empty range: lines 2-1 of 1)').

Sequencing note for the merger, measured this cycle: this lands second in a clean plan. check-merge-order.py reports no conflict between it and any of the other three open PRs; check-merge-pairs.py 2058 2060 finds both ordered pairs clean; and check-merge-plan-suite.py 2060 2058 2062 → final tree fc8440e723df2e9bad86a13faa0e927fbe16f50f, suite OK: 4646 passed, 17 skipped. So the order #2060 → #2058 → #2062 needs no resolution at any step.

Even so, this branch is 3 commits behind master, so the tree that would actually merge is the landing tree measured above — not the head's CI, which describes a base (9cc890a1) that can no longer be merged. The reading above is the one to merge on.

@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 cyc20261010-232808

Landing tree measured after the sibling landed, on the base that exists now: with #2060 merged,
check-merge-plan-suite.py 2060 2058 --steps names this merge's tree as fc8440e723df (step 2),
suite OK — 4646 passed, 17 skipped.

The reading carries by equality rather than by assertion: #2060 landed as d77c8c64, whose tree is
c8b7a1a0a954 — bit-for-bit the step-1 tree that run measured — so step 2's tree is this PR's landing
tree on the base that now exists.

What it lands: emrg/tools/read_tool.py stops printing a descending range for a past-EOF read and prints
the condition instead — (empty range: lines 2-1 of 1) was a range that is not a range;
tests/test_read_tool.py pins the new wording rather than only that some words appear.

Review evidence, both directions: forward 36 passed; reverse 2 failed, both on the descending range this
PR removes — the assertions that stop holding are exactly the ones the change targets, so the test is
pinned to the behaviour and not to its phrasing alone.

— EMRG Evolution

@argszero
argszero merged commit 4a03930 into master Oct 10, 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.

The read tool's past-EOF branch prints a descending range that is not a range, and the test pinning it asserts only that the words appear

3 participants