Skip to content

file.edit: count overlapping old_text matches; refuse overlapping edits - #52

Merged
DaveHomeAssist merged 2 commits into
mainfrom
claude/file-edit-overlap
Oct 2, 2026
Merged

DaveHomeAssist merged 2 commits into
mainfrom
claude/file-edit-overlap

Conversation

@DaveHomeAssist

@DaveHomeAssist DaveHomeAssist commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

Problem

edit_file in davellm_edit.py checked old_text with text.count(needle). str.count skips overlapping matches, so in a file holding aaa, old_text="aa" with expected_count=1 passed even though aa occurs twice (at 0 and 1). text.replace then silently edited the first one, while the approval card (approvalPreview) showed the change as if it were unambiguous.

A Hypothesis property test found this while fuzzing the Notion adapter on claude/notion-adapter-v1 (#48), where the same bug was fixed in davellm_notion.py (_occurrences).

Fix

  • Overlapping matches are counted. The new occurrences(text, needle, limit) scans with find(needle, start + 1), so aaa/aa, babab/bab and 🎉🎉🎉/🎉🎉 each count 2 and are refused with the existing COUNT_MISMATCH message.
  • Overlaps are refused when the count matches. expected_count=2 on aaa/aa now gets a new fixed message, OVERLAPPING: "old_text was found 2 times, but the occurrences overlap, so they cannot all be replaced; nothing was written". No single replacement can apply to two occurrences that share text. Matches that only touch (abab/ab) still edit.
  • The scan is bounded. It stops after EDIT_MAX_REPLACEMENTS + 1 (101) matches. An unbounded overlapping scan is quadratic on repetitive input: a 10 MiB aaa… file with a 20k-character aaa… needle measured about 550 s. Bounded, it takes about 10 ms. Counts past 100 now read "more than 100", which no expected_count can reach anyway.
  • Replacement is unchanged. Once the matches are known to be disjoint, str.replace finds exactly those matches.

The approval card itself is unchanged. Its "replaces N occurrences" line is now enforced when the edit runs: the edit replaces exactly N separate occurrences or writes nothing.

Constraints kept

  • The app.py handler and the file.edit definition are untouched, so tests/fixtures/davellm/tool_catalog.json (extended_handler_code) and the definition fingerprints are unchanged.
  • docs/DAVEHARNESS_CAPABILITIES.{json,md} were regenerated with python scripts/generate_capabilities_manifest.py for the new OVERLAPPING constant.
  • docs/DAVELLM_TOOLS.md (semantics plus the error table) and CLAUDE.md describe the overlap rule.

Tests (tests/test_file_edit.py)

  • Parametrized regressions for aaa/aa, babab/bab, an emoji run, and a CRLF file (x\nx matches as x\r\nx twice, overlapping on the x). Each is refused at expected_count 1 (COUNT_MISMATCH, found 2) and 2 (OVERLAPPING), with zero effects.
  • An overlap found only in a later pair is refused; touching matches (abab/ab, count 2) still edit.
  • Over-limit reporting: 149 overlapping aa matches (75 by str.count) and 101 a matches.
  • A Hypothesis property checks occurrences against a brute-force startswith scan. Its two-symbol alphabet makes self-overlapping needles common; a four-symbol version missed the bug in 300 examples.
  • All seven new tests fail when occurrences is switched to non-overlapping steps (the old str.count semantics). The property shrinks to text='aaa', needle='aa'.

Checks (all from CLAUDE.md, run locally)

  • py_compile, compileall, mypy daveharness: clean
  • pytest -q: 764 passed, 1 skipped
  • node --check, node --test (4/4), bash -n: clean
  • npm ci, npm ls, npm audit --audit-level=high: 0 vulnerabilities
  • git diff --check: clean

Merge note: conflicts with #51

#51 (claude/tool-preflight) moves this same counting block from edit_file into a shared _plan() used by both the approval preflight (check_edit) and the write. That block still uses text.count. The two PRs conflict textually in davellm_edit.py and in the generated docs/DAVEHARNESS_CAPABILITIES.*. Whichever lands second needs a rebase:

  1. Put the occurrences scan, the more than 100 report, and the overlap refusal in _plan() in place of found = text.count(needle) … text.index(needle).
  2. Rerun python scripts/generate_capabilities_manifest.py.

After both land, an ambiguous or overlapping edit is refused at preflight, before any approval card is shown.

edit_file counted old_text with str.count, which skips overlapping
matches. In a file holding "aaa", old_text="aa" with expected_count=1
passed even though "aa" occurs twice, and the edit silently took the
first one while the approval card showed it as unambiguous.

Occurrences are now found with a find(start + 1) scan (occurrences),
so "aaa"/"aa", "babab"/"bab" and an emoji run each count 2 and are
refused with COUNT_MISMATCH. When the count matches but two occurrences
share text, the edit is refused with the new OVERLAPPING message: no
single replacement can apply to both. Matches that only touch ("abab"
/ "ab") still edit.

The scan stops after EDIT_MAX_REPLACEMENTS + 1 matches. An unbounded
overlapping scan is quadratic on repetitive input (a 10 MiB "aaa..."
file with a 20k-character "aaa..." needle measured ~550 s); bounded, it
takes ~10 ms. Counts past 100 are reported as "more than 100", which
no expected_count can reach anyway.

Tests: parametrized regressions (aaa, babab, emoji run, CRLF), a later
overlapping pair, touching matches, the over-limit report, and a
Hypothesis property of occurrences against a brute-force scan. All
new tests fail against the old non-overlapping count. The app.py
handler and tool definition are unchanged, so the pinned catalog
fingerprints hold; the capabilities manifest is regenerated for the
new constant.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@DaveHomeAssist
DaveHomeAssist merged commit 8a5f06a into main Oct 2, 2026
3 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.

1 participant