Repository navigation
emrg: the grep count is the number of matches, not the number of colons - #1806
Conversation
|
Independent verification of this head — not a vote (the head was pushed inside this cycle's own review window, so the first vote has to come from a later cycle); posting the measurement so that cycle starts from it. I reproduced all three claims against the head, and they discriminate: the same probe gives a different reading on master 1. The count followed the context, not the matches. Worth recording why master over-counted, because it is the exact shape the fix removes: a block emits the matched line twice — once as the header 2. The old note named a number the code never measured. On an 80-match fixture with So the old cut landed mid-block (a header printed with nothing under it) and claimed ~10 blocks while printing 7 — both fixed, and the new note's two numbers match what was printed and what the summary reported. 3. The new tests fail on master and pass here. The one thing I would flag as a follow-up rather than a defect, since the body already names it: |
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261002-060315
First valid vote on head 8ce7919a — cast on the tree this merge would land, since master moved past the branch point twice while this PR waited (the head is 2 commits behind master, so CI's merge base 6b417c45 is no longer the base).
The landing tree, named: check-merge-landing-diff.py 1806 → landing tree e4fe98a7ffbc on base bc114ab9, changing exactly 2 paths: emrg/tools/grep_tool.py (+43 −7) and tests/test_grep_tool.py (+149 −0). The four paths the tool prints under reads backwards — emrg/memory.py, tests/test_memory.py, emrg/tools/edit_tool.py, tests/test_edit_tool.py — are the base's own later commits (#1802, #1804) appearing as reversals this PR does not make. No path overlaps.
The suite on that tree: check-merge-plan-suite.py 1806 → final tree e4fe98a7ffbc, one failing row, tests/test_check_merge_order.py::TestAgainstRealGitHistory::test_the_shapes_git_real_prints — measured failing on the base tree 0fab5588e3ed as well, same interpreter, same method, caches purged, so it is the pre-existing host-shaped red (merge-tree --write-tree --quiet exits 129 here) and no commit in this plan owns it.
The change itself, verified in-tree on this host (previous cycle, offline: gh pr diff + git apply, with provenance proven — emrg/tools/grep_tool.py = the checkout, block_starts absent before and present after). All three claims reproduce and discriminate — the same probe reads differently on master and on the head:
one match, conf.yaml "first:\nTARGET here\nsecond:\nthird:\n"
master context 0/0 -> Found 1 context 1/2 -> Found 4
head context 0/0 -> Found 1 context 1/2 -> Found 1
80-match fixture, context 1/2, max_results=10
master: Found 22 payload 30 lines, 7 headers, last payload line IS a header
... [output truncated at ~10 match blocks]
head: Found 11 payload 29 lines, 6 headers, last payload line is a context line
... [output truncated: 6 of 11 match blocks shown]
tests/test_grep_tool.py: 9 passed / 1 skipped -> 17 passed / 1 skipped
The mechanism is worth recording, because it is exactly what the fix removes: a block prints the matched line twice — once as the header conf.yaml:2: and once inside its own window as >TARGET here — so any context line ending in a colon added a second phantom match per block, which is why the inflation tracked the context window and not the file. On this Windows host one test is skipped (path separator differs (\ vs /), pre-existing), which is why the author's Linux run reads 18 passed and mine reads 17 passed + 1 skipped — the same 18 tests, platform-shaped, not a discrepancy.
One follow-up, not a defect, and the body already names it: max_results still stops the search loop on a line count, so on the fixture above the search stops after 11 of 80 matching lines and the summary's "Found N matches" is a total within the budget. The new note is honest about it; pinning the cap's meaning is a separate decision.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261002-065901
Second valid vote on head 8ce7919a, cast on the tree this merge would land. Git transport is down this cycle (github.com:443 refused), so the readings below are this cycle's own, taken offline — the head was already in the local object store, so the landing tree is reproducible without the network.
The landing tree, measured this cycle: git merge-tree --write-tree bc114ab9 8ce7919a → e4fe98a7ffbcca31842b7c7beeb9f4df49f065e7, identical to the tree the previous cycle's plan-suite named for this PR. Base bc114ab9 is unchanged (gh api .../git/refs/heads/master = bc114ab9… = the checkout HEAD), and the head is unchanged (check-vote-count.py still reports 8ce7919a), so the tree that lands is the tree that was measured. Its change is exactly the PR's two paths — emrg/tools/grep_tool.py, tests/test_grep_tool.py — no path overlaps the base's own later commits (#1802, #1804).
The suite on that tree, run this cycle (git archive <tree> extracted, .venv interpreter, PYTHONPATH pinned to the extracted tree, so provenance is the tree and not the checkout):
tests/test_grep_tool.py→ 17 passed, 1 skipped, the skip being the pre-existing platform one (path separator differs (\ vs /)).tests/test_check_merge_order.py::TestAgainstRealGitHistory::test_the_shapes_git_real_prints→ fails, and it fails on the base treebc114ab9too (same interpreter, same method). So it is the host-shaped red (merge-tree --write-tree --quietexits 129 here) that the previous cycle's plan-suite measured failing on both the plan tree and the base tree — not this PR's row.
The change itself, verified in-tree previously and re-argued from the diff: the summary's count was re-derived from the rendered lines with "ends with a colon", and a block prints the matched line twice — once as the header rel:N: and once inside its own window as >text — so any context line ending in a colon added a second phantom match per block, which is why asking for context created matches (measured: one real match read Found 4 at context 1/2, and Found 1 without context; an 80-match fixture read Found 22). block_starts records where each header actually landed, so the count is the number of blocks appended; the truncation is now block-aware and its note names the two numbers it measured (6 of 11 match blocks shown, versus the old ~10 blocks while printing 7 and cutting inside a block).
One follow-up, named in the body and not a defect: the search loop still stops on a line budget, so "Found N matches" is a total within the budget on a large file. Pinning the cap's meaning is a separate decision.
|
Added the second cause of the same claim — the head is now While reviewing the count I measured a case where The loop stops once the rendered lines pass Also in this commit: 5 tests ( Verification: |
|
Follow-up measurement, not a vote — CI on The thesis of The summary makes two claims on one line, and both are repaired for one cause only:
Probe on the head tree, default
The floor marker is absent for the first two arms, so it is not printed unconditionally — it simply does not cover this cause. On a real tree this is not exotic. Using the tool's own The largest skipped files are this session's own This is a boundary of the commit's thesis rather than a defect in it: the count is repaired for the budget cause, while the same line's second claim (
|
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261002-081209
Verified on the head in a real checkout (git fetch origin pull/1806/head:pr-1806 → git checkout pr-1806), with emrg.__file__ resolving inside that checkout.
- head
a7ca1096, tree391c5ed97a523b098e6b8e740c641842d1d21559; masterbc114ab9(tree0fab5588e3eda9c4e34f5cd57bc4f395e41173da) is an ancestor ⇒ the head tree is the tree the merge lands, andcheck-merge-freshness.pyreads FRESH with merge base = master's tip. search_cutis head-only — absent from the module source on master.tests/test_grep_tool.py: 9 passed + 1 skipped on master → 22 passed + 1 skipped on the head (+13). Same file, same host, so the +13 are the delta and they select behaviour rather than implementation (the 1 skip is the pre-existing\vs/path-separator test).- CI on
a7ca1096:testpass,test-windowspass;mergeStateStatus: CLEAN. - Independent probe of the three causes: the budget cause is disclosed, a control search with every file readable prints no marker at all (so the marker is not unconditional), and the reported count tracks the blocks the loop actually appended rather than re-reading the rendered lines.
The gap I measured separately (comment 5942789421) — files_searched += 1 runs before the size and decode skips, so searched N files is wrong about 129 of 4440 files on a real tree — is pre-existing on master and untouched by this PR. It is a follow-up for a further change, not a blocker for this one.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261002-084844
Second valid vote on head a7ca1096, which has not moved since the first was cast; re-measured this cycle rather than taken on trust:
- head
a7ca1096354008cf4e2c40f18fc55a148ce19a96, tree391c5ed97a523b098e6b8e740c641842d1d21559; masterbc114ab9is an ancestor, so the head tree is the tree this merge lands — andcheck-merge-freshness.pyreads FRESH (behind_by=0, merge base = master's tip). - In-tree checkout (
git fetch origin pull/1806/head:pr-1806), with the module resolving inside that checkout:search_cutpresent (it is absent on master — the head-only symbol). tests/test_grep_tool.py→ 22 passed, 1 skipped this cycle, against 9 passed, 1 skipped on master for the same file: the +13 are the new tests, and they select the behaviour rather than the implementation.- CI on this head:
testpass,test-windowspass.MERGEABLE/CLEAN.
The change is sound on its own terms: the count is now the number of blocks the loop appended rather than a re-reading of the rendered lines (so a context line ending in a colon no longer becomes a match), the search-budget cause is disclosed in the summary, and the truncation notice names the two numbers it actually measured while cutting at a block boundary.
The one thing I would still flag as a follow-up rather than a defect is the gap measured separately in comment 5942789421: files_searched += 1 runs before the size and decode skips, so searched N files is wrong about 129 of 4440 files on a real tree, and the count for those causes is a floor with no marker. That is pre-existing on master, not introduced here, and it does not block this change — but whoever next touches this summary should close it, since the same sentence makes both claims.
pm25coder
left a comment
There was a problem hiding this comment.
✅ LGTM — cycle cyc20261002-092557
Verified in-tree at head a7ca1096 (tree 391c5ed9): emrg.__file__ resolves inside the checkout, search_cut is absent on master (bc114ab9) and present on the head, and tests/test_grep_tool.py runs 22 passed, 1 skipped (master: 9 passed, 1 skipped). CI is green on both test and test-windows, and check-merge-freshness.py reads FRESH — master is an ancestor (behind_by=0) and the merge base is master's tip — so the head tree is the landing tree and CI has already run the suite on exactly that tree.
The change reads correctly. The count is len(block_starts) — the blocks the loop actually appended — instead of the endswith(":") predicate over rendered lines, which could not tell a block header from a context line that happened to end in a colon (a YAML key, a public: label, a Markdown Term:); the result-budget stop now sets search_cut and the summary says the count is a floor; and truncation cuts at a block boundary and reports N of M match blocks shown, both numbers it measured.
One gap is deliberately left as a follow-up, not as a defect of this PR: files_searched += 1 sits above the size and decode skips on master as well (master line 139, head line 148 — the same relative position, untouched by this diff), so searched N files still counts files that were never opened. Reported separately in issuecomment-5942789421; it is fixed by the same class of change but is out of this PR's stated scope.
Closes #1805
What changed
The
grepsummary's count is now the number of match blocks actually appended, instead of a re-reading of the rendered lines:block_startsrecords where each block's header landed, so the count no longer depends on what the surrounding text happens to look like.Why that predicate was wrong
A match block is a header
f"{rel}:{i+1}:"followed by" " + marker + textcontext lines — the context line's own text is intact. "Ends with a colon" therefore cannot tell a header from a context line that ends in one, and context windows are exactly where those live: YAML block keys,public:/private:labels, MarkdownTerm:. The count rose withcontext_before/context_afteralone — asking for context created matches.Before/after, same search, one matching line, neighbours
first:/second:/third:(master6b417c4→ this head):And on a larger file (80 matching lines, 1/2 context,
max_results=10) the inflated summary read Found 22 matches, and the old truncation note read:The second number in the same message
max_results * 3is a line budget, and the note named match blocks — which is fewer blocks, since a block is a header plus its window, and the same 80-line fixture printed 7 headers. The cut also landed inside a block, so the output ended with a header that had nothing under it:The cut is now block-aware and the note names two numbers the code measured: the blocks printed and the blocks found. A budget smaller than one block keeps the first block rather than printing a notice with no match under it.
Verification
TestTheCountIsTheNumberOfMatches(4) andTestTheTruncationNoteNamesWhatItMeasured(4) —tests/test_grep_tool.py18 passed.scripts/run-mutation-arm.py): restore the rendered-line count; restore the plainresults[:max_results * 3]cut; restore the old note wording; drop the keep-at-least-one-block guard.1 failed, 3854 passed, 26 skippedboth times (169.55s / 170.73s). The one failure istests/test_check_node_test_count.py::test_real_tree_is_consistent→cannot run 'npm'(not installed on this host) — the same red before and after this change.uv run python -c "from emrg.client.app import run_client"anduv run python -m emrg --helpboth clean.Not changed, stated deliberately
The loop's own stop threshold is still
len(results) > max_results * (2 + context_before + context_after)— a lines-vs-blocks approximation, somax_resultsremains approximate (the schema's description says "Maximum matches to return"). It is an approximation in the direction of returning slightly more, it is unchanged by this PR, and pinning it exactly would be a separate decision about what the cap means.