Skip to content

[48] Sketch for "start-of-match" boundary solution - #51

Closed
TomStrepsil wants to merge 1 commit into
mainfrom
fix/48-regex-start-boundary-illusion
Closed

[48] Sketch for "start-of-match" boundary solution#51
TomStrepsil wants to merge 1 commit into
mainfrom
fix/48-regex-start-boundary-illusion

Conversation

@TomStrepsil

Copy link
Copy Markdown
Owner

Issue

relates to #48

This PR is a proof-of-concept sketch, raised for posterity.

Despite it resolving the stated issue, any potential solution to the complentary issue #49 (that must be solved for this fix to be useful) would fundamentally break the "yield as early as possible" intent of the library, needlessly requiring any matches that touch the end of a chunk to be deferred until the next chunk arrives, or the end of the stream is met. The latter case currently gets processed via flush(), the interface of which only currently supports outputting plain "non match" strings.

Boundary matching represents the same family of problem as the documented Lookbehinds and Negative lookaheads, such that without a regex state machine (thus a fundamentally different approach than the regex-partial-match that underpins the search strategy), no assumption can be made about when such buffering should take place, since support of disjunctions etc. prevent naive analysis of the regex source to detect when a boundary is "next atomic match" within a partially matched string.

Use-cases for genuinely matching just the very start or very end of a stream are undefined. \b and \B have more obvious utility, but seem somewhat incompatible with timely yielding streams.

Details

RegexSearchStrategy re-slices the remaining haystack via .substring() every time it advances past a match. Native ^ (no m), \b, and \B evaluate relative to that substring's own index 0, not the true stream's start — so a truncated slice's artificial edge gets mistaken for a real one, producing confirmed false-positive matches. This reproduces with zero chunking at all (/\bOLD/.exec("OLDOLD".substring(3)) wrongly matches), so it isn't only a chunk-boundary problem — it's inherent to how the scan loop re-slices after every match.

The fix retains one real character of trailing context (state.precedingChar across a chunk boundary, or the real character already in the haystack for a mid-buffer position) and re-verifies any candidate landing at a slice's index 0 against it, using a sticky-anchored clone of the pattern forced to start exactly past that character. A rejected candidate resumes the search one code point later rather than abandoning it, so a genuinely valid match later in the same haystack isn't lost. Patterns that can never invoke ^/\b/\B skip the check entirely (checked once, at construction) — benchmarked to matter: an early, ungated version of this cost up to +57% on match-dense content before the gate was added.

  • src/search-strategies/regex/search-strategy.tsprecedingChar state, the construction-time gate, findCompleteMatch's verify/retry loop
  • src/search-strategies/regex/search-strategy.test.ts — repro cases proving the false positives are gone, regression guards proving legitimate cross-chunk/mid-buffer matches still work, and coverage for every branch added (each verified by disabling it and confirming a test fails)
  • src/search-strategies/regex/README.md — an addendum to "How It Works" explaining the mechanism, plus updated Limitations/Supported Features entries

Semantic Version Impact

  • PATCH - Bug fixes and minor changes (backwards compatible)
  • MINOR - New features (backwards compatible)
  • MAJOR - Breaking changes (not backwards compatible)

Checklist

  • I have added an entry to the [Unreleased] section in CHANGELOG.md
  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Copilot AI review requested due to automatic review settings July 26, 2026 10:42
Comment thread src/search-strategies/regex/search-strategy.ts Dismissed
Comment thread src/search-strategies/regex/search-strategy.ts Dismissed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR sketches a proof-of-concept fix for issue #48 in RegexSearchStrategy, where ^/\b/\B (and ^ under /m) can produce false positives when exec() is run against truncated substrings during streaming scanning. It introduces a one-code-unit “preceding context” state and re-verifies matches that start at slice index 0 using a sticky clone of the regex, plus adds targeted regression tests and documentation updates.

Changes:

  • Add precedingChar to RegexSearchStrategy state and implement findCompleteMatch() to re-verify boundary-sensitive candidates.
  • Add a sticky clone (stickyMatchRegex) and a construction-time gate to skip the verification path when boundary assertions can’t be present.
  • Add tests covering false-positive elimination and regression cases; update the regex strategy README with a detailed explanation and new limitation notes.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/search-strategies/regex/search-strategy.ts Adds precedingChar state + verification loop using a sticky regex to prevent start-boundary false positives.
src/search-strategies/regex/search-strategy.test.ts Adds issue #48 repro/regression coverage around start-boundary “slice illusion” behavior.
src/search-strategies/regex/README.md Documents the start-boundary verification approach and clarifies remaining end-boundary limitations.
Comments suppressed due to low confidence (1)

src/search-strategies/regex/search-strategy.ts:92

  • This doc comment also lists ^ twice (and omits \\B). It should reflect the actual set of assertions being guarded so the behavior is clear.
   * candidate whose success at position 0 of its own exec'd slice depends on
   * `^`/`\b`/`^` (`/m`) treating a truncated slice's edge as the true stream
   * edge. Rejected candidates are re-verified against real preceding context

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

* The last UTF-16 code unit actually emitted so far — as match or
* non-match content — or `undefined` if nothing has been emitted yet
* (i.e. we are still at the true start of the stream). This is the one
* piece of context `^` (no `m`), `\b`, and `^` under `/m` ever need to
Comment thread src/search-strategies/regex/search-strategy.ts
@TomStrepsil

Copy link
Copy Markdown
Owner Author

Closed as unplanned

@TomStrepsil TomStrepsil changed the title sketch for solution [48] Sketch for "start-of-match" boundary solution Jul 26, 2026
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.

3 participants