Skip to content

feat: add route pattern overlap utilities - #183

Merged
pi0 merged 10 commits into
mainfrom
feat/overlap-util
Jul 2, 2026
Merged

pi0 merged 10 commits into
mainfrom
feat/overlap-util

Conversation

@pi0x

@pi0x pi0x commented Jul 2, 2026 •

Copy link
Copy Markdown
Contributor

Motivation

rou3 only matches a concrete path against registered patterns (findRoute/findAllRoutes). Some use cases need to reason about patterns against patterns: given registered route patterns, know when two patterns can match a common concrete path so you can resolve an "effective" merged config over a whole scope.

Example (inherited/merged config):

/**                 -> { isr: true }
/protected/**       -> { basicAuth: {...} }
/protected/feed/**  -> { isr: 60 }

/protected/feed/** overlaps all three, but there's no single concrete path you can hand to findAllRoutes that reliably represents "the whole scope of pattern P". The prior workaround (synthesizing a fake path like /protected/feed/_) silently ignores richer syntax (*.png, :id(\d+), {s}?, :x?, \:) and samples only one representative path.

What this adds

Two utilities computed with rou3's own segment/radix semantics (exported from rou3):

  • routesOverlap(patternA, patternB): boolean — pure, router-free. true iff there exists a concrete path matched by both patterns (overlap, not subset).
  • findOverlappingRoutes(ctx, method, pattern): MatchedRoute[] — like findAllRoutes, but the query is a pattern (scope) instead of a concrete path. Returns every registered route whose match-set intersects it, ordered least→most specific, with the same method handling (method → "" bucket). Matches carry only data (a scope has no single concrete path → no params). A single route that optional/group syntax expanded into several tree entries (sharing one data reference) is reported once; distinct routes are always reported separately, even when they carry an equal primitive data value (or none).

Design

  • src/operations/overlap.ts is fully self-contained and tree-shakeable — the core bundle is unaffected (add.ts/types.ts untouched). Each pattern is modeled as a RouteShape: fixed single-segment matchers (string literal | RegExp | any) + a variable tail [min,max]. Tails match any values, so overlap reduces to a shared-prefix value check plus an integer length-range intersection.
  • Query patterns are inserted into a throwaway router via the real addRoute, and shapes are read back from tree entries (kind-tagged edges + paramsMap) — so queries and registered routes are classified by the exact same pipeline, including escaped literals (/a/\* is the static segment *). Registered-entry shapes are cached in a WeakMap (only the persistent ctx path benefits — the transient query router computes shapes directly, so it pays no cache overhead).
  • findOverlappingRoutes walks the tree in findAllRoutes order (least→most specific), prunes static subtrees the query can't reach, and collapses only genuine reference-duplicates (one route expanded by optional/group syntax into several entries sharing a data reference). Distinct routes with equal-or-absent primitive data are never merged — the dedup key is data identity, not value, so no scope is silently dropped.

Semantics (verified against the live router)

  • bare ** = 0+ segments, **:name = 1+, trailing bare * = 0-or-1, mid */:name = exactly 1.
  • static/static and static/regex are precise (/user/:id(\d+) does not overlap /user/abc); regex/regex and regex/any over-approximate to "overlaps" — the safe conservative default, since exact regex intersection is undecidable.
  • Groups/modifiers/escaping expand to variants; patterns overlap if any variant-pair does.

Also fixed

  • findAllRoutes (interpreter) matched **:name against paths with zero remaining segments, disagreeing with both findRoute and the compiled matchAll router. Fixed with a regression test; the core bundle budget moves 5.7k → 5.8k for this fix.
  • Compiler / root path /: p.split("/") turns / into ["", ""], a phantom trailing segment that made the compiled router match required root wildcards and params at / — /**:name and /:x both wrongly matched /, disagreeing with the (correct) interpreter and findRoute. Fixed by letting root collapse to "" in the compiled prologue (drop the || "/" so "".split("/") has no phantom) — a build-time change with zero added per-call runtime overhead. Regression covered by an interpreter-vs-compiled parity test.

Tests & docs

  • test/overlap.test.ts — table-driven routesOverlap (literals, ** zero-segment, * vs :param vs literal, nested **, disjoint siblings incl. /ab/** vs /a/**, regex precise + conservative, *.png, :x?/:x+/:x*, {s}?, escaping — all asserted symmetric) + findOverlappingRoutes (merged-config example, disjoint scopes, method handling, regex, escaped literals, reference-based dedup, and regressions proving distinct routes with equal-or-absent primitive data are not dropped, **:name boundary).
  • test/find-all.test.ts — named-wildcard boundary regression + root-path parity block (required root wildcard/param at /), all interpreter-vs-compiled.
  • README "Route Patterns" → new Pattern overlap section; AGENTS.md updated.

Full suite, lint, typecheck, and build all pass. Zero new runtime dependencies.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added routesOverlap and findOverlappingRoutes to detect whether two route patterns can match the same paths and to list overlapping registered routes.
  • Documentation
    • Updated “Pattern overlap” guides and import examples, plus API documentation for the new utilities.
  • Bug Fixes
    • Improved end-of-path handling for recursive wildcards and aligned root trailing-slash normalization across matching behavior.
  • Tests
    • Expanded overlap/matcher coverage and updated benchmark bundle-size thresholds.

Add routesOverlap() and findOverlappingRoutes() for reasoning about
patterns against patterns (do their match-sets intersect), using rou3's
own segment/radix semantics so behavior stays consistent with
findRoute/findAllRoutes.

- routesOverlap(a, b): pure, router-free boolean overlap check
- findOverlappingRoutes(ctx, method, pattern): findAllRoutes over a
  pattern scope, least->most specific
- src/_overlap.ts: RouteShape model + shape intersection; reuses the
  addRoute pipeline (getParamRegexp now exported from add.ts)
- static/static and static/regex precise; regex/regex and regex/any
  over-approximated to "overlaps" (conservative default)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pi0x
pi0x requested a review from pi0 as a code owner July 2, 2026 17:07
@coderabbitai

coderabbitai Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: de4ebd4c-a91d-424d-a17c-675416472226

📥 Commits

Reviewing files that changed from the base of the PR and between 432b29e and b467894.

📒 Files selected for processing (5)
  • src/compiler.ts
  • test/.snapshot/compiled-all.mjs
  • test/.snapshot/compiled-aot.mjs
  • test/.snapshot/compiled-jit.mjs
  • test/find-all.test.ts
✅ Files skipped from review due to trivial changes (2)
  • test/.snapshot/compiled-all.mjs
  • test/.snapshot/compiled-jit.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • test/.snapshot/compiled-aot.mjs
  • src/compiler.ts

📝 Walkthrough

Walkthrough

Adds route-pattern overlap detection with public API, docs, and tests. Separately, wildcard end-of-path matching now filters end-at-path wildcard matches by optionality, and compiler snapshots and root-path normalization were updated.

Changes

Route overlap feature

Layer / File(s) Summary
Route shape primitives and generation
src/_overlap.ts
Defines the RouteShape and Edge types and builds canonical shapes for a pattern by inserting it into a throwaway router and collecting radix-tree shapes.
Registered-route shaping and overlap comparison
src/_overlap.ts
Computes cached shapes for registered entries, compares shapes for shared concrete paths, and prunes static traversal with query reachability checks.
Public API functions and router traversal
src/operations/overlap.ts, src/index.ts
Adds route-pattern overlap queries, traverses router nodes in specificity order, applies method fallback and deduplication, and re-exports the new API from the package entrypoint.
Overlap test suites
test/overlap.test.ts
Adds tests for symmetric pattern overlap cases and for overlapping-route discovery across scope ordering, method buckets, deduplication, escaping, and regex constraints.
Documentation and bundle-size threshold update
AGENTS.md, README.md, test/bench/bundle.test.ts
Documents the new overlap API and pattern semantics in AGENTS.md and README.md, and raises the bundle-size benchmark thresholds.

Wildcard end-of-path matching fix

Layer / File(s) Summary
End-of-path wildcard filtering and tests
src/operations/find-all.ts, test/find-all.test.ts, src/compiler.ts, test/.snapshot/compiled-all.mjs, test/.snapshot/compiled-aot.mjs, test/.snapshot/compiled-jit.mjs
Changes wildcard match aggregation to honor optionality at path end and adds parity tests for named and root wildcard cases, with matching compiled snapshots and compiler normalization updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Suggested reviewers: pi0

Poem

I hopped through shapes and shady routey glades,
Where tails and wildcards cast their little shades.
Two paths may meet, or softly pass apart,
While root and slash now share a gentler heart.
Hooray for hops in matcher-land! 🐇

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding route pattern overlap utilities.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/overlap-util

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 2, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/_overlap.ts (2)

65-112: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider relocating this internal helper to the end of the file.

segmentsToShape is non-exported but sits between the exported routeToShapes and methodDataToShape. Repository convention is to keep internal helpers at the bottom.

As per coding guidelines: "Place non-exported/internal helpers at the end of the file."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/_overlap.ts` around lines 65 - 112, Move the internal helper
segmentsToShape out of the middle of the module and place it at the end of the
file so it follows the repository convention for non-exported helpers. Keep the
exported routeToShapes and methodDataToShape in their current order, and
relocate only the private segmentsToShape implementation without changing its
behavior or references.

Source: Coding guidelines


76-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prefer charCodeAt() over .startsWith() for the wildcard check.

To stay consistent with the router's performance-first style, use charCodeAt for this prefix test.

As per coding guidelines: "Use charCodeAt() instead of .startsWith() ... in performance-sensitive code."

♻️ Proposed tweak
-    if (seg.startsWith("**")) {
+    if (seg.charCodeAt(0) === 42 /* '*' */ && seg.charCodeAt(1) === 42) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/_overlap.ts` at line 76, The wildcard prefix check currently uses
seg.startsWith("**"), which should be replaced with a charCodeAt()-based test to
match the router’s performance-first style. Update the prefix logic in the
overlap handling path where seg is inspected so it uses direct character code
comparison instead of .startsWith(), keeping the same behavior for the "**" case
while avoiding the slower string helper.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@README.md`:
- Around line 173-190: The README example imports only routesOverlap and
findOverlappingRoutes but also uses createRouter and addRoute, so the snippet is
incomplete. Update the example import statement to include the router helpers
referenced in the snippet, using the existing symbols createRouter and addRoute
so the sample runs as written.

---

Nitpick comments:
In `@src/_overlap.ts`:
- Around line 65-112: Move the internal helper segmentsToShape out of the middle
of the module and place it at the end of the file so it follows the repository
convention for non-exported helpers. Keep the exported routeToShapes and
methodDataToShape in their current order, and relocate only the private
segmentsToShape implementation without changing its behavior or references.
- Line 76: The wildcard prefix check currently uses seg.startsWith("**"), which
should be replaced with a charCodeAt()-based test to match the router’s
performance-first style. Update the prefix logic in the overlap handling path
where seg is inspected so it uses direct character code comparison instead of
.startsWith(), keeping the same behavior for the "**" case while avoiding the
slower string helper.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 51f53b83-ab01-4a48-88ff-53722fdc5e16

📥 Commits

Reviewing files that changed from the base of the PR and between 346e491 and 67dd1ec.

📒 Files selected for processing (7)
  • AGENTS.md
  • README.md
  • src/_overlap.ts
  • src/index.ts
  • src/operations/add.ts
  • src/operations/overlap.ts
  • test/overlap.test.ts

Comment thread README.md Resolved
pi0 and others added 7 commits July 2, 2026 18:02
Interpreter findAllRoutes pushed wildcard methods unconditionally, matching
/a/**:rest against /a — disagreeing with both findRoute and the compiled
matchAll router, which require one segment for non-optional wildcards.
Bundle budget raised 5.7k->5.8k for the core fix.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the parallel shape decoders with a single path: query patterns go
through a throwaway addRoute, and shapes are read back from tree entries via
kind-tagged edges + paramsMap (cached per entry in a WeakMap). Fixes
escaped-literal routes (/a/\* stored as static "*") being classified as
dynamic, dedupes results of expanded routes (:x?, {s}?) by matched data
value, prunes unreachable static subtrees, deletes _overlap.ts, and reverts
the getParamRegexp export — core bundle is no longer affected (add.ts and
types.ts untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Keeps both modules under the 200 LoC guideline: _overlap.ts holds the shape
model (RouteShape, shapeOf, shapesOverlap), operations/overlap.ts the public
API and tree traversal.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`p.split("/")` turns "/" into ["",""], a phantom trailing segment that made
the compiled router match required root wildcards and params at "/":
`/**:name` and `/:x` both wrongly matched "/", disagreeing with the
interpreter and findRoute.

Let root collapse to "" in the compiled prologue (drop the `|| "/"` so
`"".split("/")` has no phantom segment). Build-time change, zero added
per-call runtime overhead. Covered by an interpreter-vs-compiled root-path
parity test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
test/overlap.test.ts (1)

159-169: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add exact-scope assertions for expanded optional routes.

/a/** overlaps both expanded shapes, so this test would still pass if the cached/computed shape were accidentally broadened. Add narrow checks like /a and /a/b to exercise both expansion entries independently.

Test hardening
     // `:x?` / `{/c}?` expand to several tree entries sharing one data reference.
+    expect(findOverlappingRoutes(router, "GET", "/a").map((m) => m.data)).toEqual([opt]);
+    expect(findOverlappingRoutes(router, "GET", "/a/b").map((m) => m.data)).toEqual([opt]);
     expect(findOverlappingRoutes(router, "GET", "/a/**").map((m) => m.data)).toEqual([opt]);
+    expect(findOverlappingRoutes(router, "GET", "/b").map((m) => m.data)).toEqual([grp]);
+    expect(findOverlappingRoutes(router, "GET", "/b/c").map((m) => m.data)).toEqual([grp]);
     expect(findOverlappingRoutes(router, "GET", "/b/**").map((m) => m.data)).toEqual([grp]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/overlap.test.ts` around lines 159 - 169, The optional-route overlap test
for findOverlappingRoutes only checks the broad /a/** and /b/** patterns, so it
can miss regressions where the expanded shapes are widened incorrectly. Update
the test around createRouter, addRoute, and findOverlappingRoutes to add
exact-scope assertions for the expanded entries, such as checking /a and /a/b
separately for the /a/:x? case (and the equivalent narrow paths for /b{/c}?) so
each expansion is exercised independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@test/overlap.test.ts`:
- Around line 159-169: The optional-route overlap test for findOverlappingRoutes
only checks the broad /a/** and /b/** patterns, so it can miss regressions where
the expanded shapes are widened incorrectly. Update the test around
createRouter, addRoute, and findOverlappingRoutes to add exact-scope assertions
for the expanded entries, such as checking /a and /a/b separately for the /a/:x?
case (and the equivalent narrow paths for /b{/c}?) so each expansion is
exercised independently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89a0c161-8ae0-465d-9edd-6c8c2fe1adb9

📥 Commits

Reviewing files that changed from the base of the PR and between 3e95c6b and 24f420e.

📒 Files selected for processing (10)
  • AGENTS.md
  • README.md
  • src/_overlap.ts
  • src/compiler.ts
  • src/operations/overlap.ts
  • test/.snapshot/compiled-all.mjs
  • test/.snapshot/compiled-aot.mjs
  • test/.snapshot/compiled-jit.mjs
  • test/find-all.test.ts
  • test/overlap.test.ts
✅ Files skipped from review due to trivial changes (3)
  • test/.snapshot/compiled-all.mjs
  • AGENTS.md
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/operations/overlap.ts

… root parity

Post-review follow-ups:
- compiler: root static now matches both "" and the once-stripped "//" so a
  root `/` route keeps interpreter/compiled parity on the doubled-slash path
  (root-only condition, no per-call cost on other routers). Broad sweep drops
  from 54 -> 18 interpreter-vs-compiled divergences; the remaining 18 are all
  pre-existing (malformed `//` + required root wildcard/param, and the
  `/:id/**` param-zero-segment findRoute quirk) — none newly introduced.
- overlap tests: add coverage for the regex-constrained query pruning branch,
  any-segment query fan-out across static siblings, tail reaching deep statics
  + trailing-`*` prune boundary, single-node wildcard->param->static->self
  ordering, and reference-identity dedup for function data.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/compiler.ts`:
- Around line 89-96: The compiled matcher in compiler.ts is still using the raw
path string, so root param/wildcard behavior can diverge from
findRoute/findAllRoutes. Update the matcher generation around the key/nk
condition to use splitPath() the same way the interpreter does, and then base
the compiled route comparison on that normalized path representation so "//" and
"/" resolve consistently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b0decce-f858-4459-9f78-c455c467caed

📥 Commits

Reviewing files that changed from the base of the PR and between 24f420e and 432b29e.

📒 Files selected for processing (2)
  • src/compiler.ts
  • test/overlap.test.ts

Comment thread src/compiler.ts Resolved
…y segments

Address CodeRabbit review: the compiled matcher counted a phantom trailing
segment for paths that keep a trailing empty after the slash strip ("//",
"/a//", ...), so required root/nested wildcards and params matched where the
interpreter's splitPath() rejects them. Drop the trailing empty segment in the
compiled tree walk (guarded by length>1 so the leading-empty root case keeps
0 real segments without over-collapsing). Root static still matches "//" via
the un-split ctx.static fast path, mirroring the interpreter's dual handling.

Broad interpreter-vs-compiled sweep now diverges only on the pre-existing
/:id/** findRoute bug (tracked in #184). Regression tests added via the
_findAllRoutes dual-validation helper.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pi0
pi0 merged commit 835332a into main Jul 2, 2026
6 checks passed
@pi0
pi0 deleted the feat/overlap-util branch July 2, 2026 20:17
@pi0x pi0x mentioned this pull request Aug 21, 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.

2 participants