Skip to content

fix(mcp): match search_code file_pattern with a directory against the repo-relative path - #2841

Merged
DeusData merged 2 commits into
DeusData:mainfrom
halindrome:fix/search-code-file-pattern-path
Oct 31, 2026
Merged

DeusData merged 2 commits into
DeusData:mainfrom
halindrome:fix/search-code-file-pattern-path

Conversation

@halindrome

Copy link
Copy Markdown
Contributor

Summary

search_code matched file_pattern against the file basename only. Any pattern containing a directory, such as src/cli/*.c or apps/web/src/**/*.ts, silently returned 0 results, while search_graph matches the same pattern against the full repo-relative path.

Closes #2836

Change (src/mcp/mcp.c)

  • New portable matcher, search_code_path_glob_match. It runs in linear time and doesn't use fnmatch, which Windows lacks.
    • * and ** match across /, and **/ also matches zero directories.
    • ? matches one character.
    • A pattern with no wildcards is a substring match, the same as search_graph.
  • write_scoped_filelist: a file_pattern containing / is matched against the repo-relative path on every platform. Windows separators are normalised first. Patterns without / keep the existing basename behaviour.
  • Windows: / patterns are scanned in process from the already-filtered list, not by the PowerShell -like post-filter. cbm_search_code_file_pattern_can_prefilter already rejects /, so nothing is filtered twice.
  • When a search with a / pattern returns no results, the response carries a hint, in both tree and JSON output. The hint explains that the pattern must match the whole repo-relative path and points at path_filter.
  • The schema now describes file_pattern ("Glob on file name; with '/', on repo-relative path (** = any dirs).") and path_filter ("Regex on repo-relative path.").

Acceptance criteria

  1. A file_pattern containing / is matched against the repo-relative path, with ** crossing directories (including zero directories).
  2. A file_pattern without / keeps the current basename semantics.
  3. The behaviour is the same on POSIX and Windows for indexed (scoped) searches.
  4. A / pattern that yields 0 results returns a hint pointing at path_filter.
  5. The tool schema describes both file_pattern and path_filter.
  6. Existing mcp suite tests stay green.

Known limits

  • Unscoped searches (an unindexed root) still use find -name, so a / pattern there returns 0. The hint still fires.
  • * crosses /, and matching is case-sensitive. Both are consistent with search_graph and with the existing basename match.

Testing

  • New search_code_file_pattern_matches_repo_relative_path_issue2836 in tests/test_mcp.c, with fixture files src/main.c and src/cli/watcher.c:
    • src/cli/*.c → 1 match
    • src/**/*.c → 2
    • **/cli/*.c → 1
    • src/cli/ → 1
    • watcher.c → 1
    • *.c → 2
    • lib/*.c → 0, with the hint
  • Red-first check: with the original mcp.c, the new test fails, because the four directory patterns return 0 and there is no hint.
  • scripts/test.sh --suites mcp: 397 passed, 4 skipped, 0 failed.
  • The Windows path was changed by reading only; it hasn't been run.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CDDUoWEZMNZ9fLvzp2pPGT

… repo-relative path

search_code matched file_pattern against the file basename only, so any
pattern containing a directory (src/cli/*.c, apps/web/src/**/*.ts)
silently returned 0 results, while search_graph's file_pattern matches
the full repo-relative path.

A file_pattern containing '/' is now matched in C against the
repo-relative path of every indexed file, on all platforms, with
search_graph-equivalent semantics: '*' and '**' match across '/', '**/'
also matches zero directories, '?' matches one character, and a pattern
without wildcards is a substring match. Patterns without '/' keep the
existing basename behaviour. On Windows such patterns are scanned in
process from the already-filtered list instead of the PowerShell -like
post-filter. A search with a '/' pattern that returns nothing now carries
a hint explaining whole-path matching and pointing at path_filter, and
the schema describes both parameters.

Refs DeusData#2836

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
@halindrome

Copy link
Copy Markdown
Contributor Author

QA round 1 for this change. It ran on the fork PR (halindrome#7) before the commits were squashed into 5a666f2, so any commit SHAs below refer to the pre-squash history. The QA was performed by Claude Opus 5.5 (Anthropic) in Claude Code.

QA Round 1

Scope: origin/main..HEAD on fix/search-code-file-pattern-path (closes #2836). The panel had 3 lenses: contract-security, regression-edges and test-quality. All 3 landed.

Contract Verification

# Criterion Verdict Evidence
1 A file_pattern containing / is matched against the repo-relative path, with ** crossing directories (zero included), consistent with cbm_glob_to_like ✅ pass search_code_path_glob_match (src/mcp/mcp.c:14826) is applied in write_scoped_filelist (mcp.c:16503-16511). */** cross /, **/ matches zero dirs, and a pattern with no wildcard is a substring match, the same as search_where_basic (#200). Test cases: src/cli/*.c, src/**/*.c, **/cli/*.c, src/cli/. Minor divergences that do not break the criterion: the match is case-sensitive where LIKE ignores ASCII case, and it is slightly stricter around /**.
2 A file_pattern without / keeps basename semantics ✅ pass The basename fnmatch (mcp.c:16519) and the Windows prefilter are unchanged when !path_glob. Test cases: watcher.c, *.c.
3 Behaviour is the same on POSIX and Windows for scoped searches ✅ pass The path glob runs in C before the #ifdef split, after separator normalisation. On Windows a / pattern is kept on the in-process scan (mcp.c:17536-17538), and the test is not platform-skipped. Not run on Windows in this review.
4 A / pattern yielding 0 results returns a path_filter hint ✅ pass metrics.hint_path_filter (mcp.c:17710) is rendered in both the JSON and toon outputs, and the lib/*.c case asserts it. See Finding 1 for the wording.
5 The tool schema describes file_pattern and path_filter ✅ pass mcp.c:732-734.
6 Existing mcp suite stays green; new tests are red without the change ⚠ partial Red-without-change was confirmed by reading the code: basename fnmatch returns 0 for every / case and never emits the hint. Green was not verified, because lenses do not run the suite. The CI result is the evidence for this half.
7 Accepted limit: unscoped searches still use find -name ✅ pass cbm_search_code_build_grep_cmd's unscoped branch is unchanged.

Finding 1 — path_filter hint claims file_pattern "matched nothing" when files matched but the search text did not

[claude:contract-security|regression-edges|test-quality] · minor · confirmed · src/mcp/mcp.c:17710

  • Problem: metrics.hint_path_filter = file_pattern && strchr(file_pattern, '/') && gm_count == 0; is keyed on grep matches, not on whether the glob selected any files. SEARCH_CODE_PATH_HINT (mcp.c:14816) tells the caller the file_pattern "matched nothing" and points at path_filter. With a correct glob such as src/cli/*.c and a needle that does not occur, the caller is told to rewrite a correct filter.
  • Expected: Fire the hint only when the / glob selected no files (scoped && scoped_written == 0, plus the unscoped find -name case), or reword it so it does not claim the glob matched nothing.
  • Impact: A misleading diagnostic that wastes an agent round-trip. Results are never wrong, and the AC wording is literally met. About a one-line fix. All three lenses found this independently.

Finding 2 — Hint rendering in the default tree/toon output is untested

[claude:test-quality] · minor · confirmed · tests/test_mcp.c:11724-11735

  • Problem: Every case in search_code_file_pattern_matches_repo_relative_path_issue2836 sends "format":"json". The new hint branch in assemble_search_output_toon (mcp.c:15479-15481) handles the default output format and is never exercised, so deleting it leaves the suite green.
  • Expected: One tree-format case in the existing loop.
  • Impact: Low. The branch is a 3-line mirror of the JSON one.

Summary

Severity Confirmed
Critical 0
Major 0
Minor 2

No blocking findings. Contract: 6 criteria pass. 1 is partial, because suite-green is left to CI.

Observations

None.


SAST review not applicable

Target default has security_stage: false in the qa-cycle config. No CI
security stage is wired for this target, so no SAST/SCA delta is computed.


QA performed by Claude Code (claude-opus-5-5), manager + 3-lens panel

Dispositions (orchestrator, Step 3B)

  • Fixed (minor): the hint said the / pattern "matched nothing", even when it had selected files that simply didn't contain the search text. The wording now reads: "no results; a file_pattern with '/' must match the whole repo-relative path (e.g. src/**/*.c); to scope by folder you can also use path_filter …". That is accurate in both cases. The hint still keys on zero matches, because the unscoped find path has no file count to key on.
  • Not fixed (carried forward, minor): no test covers the hint in the tree (toon) output format. Both renderers read the same metrics.hint_path_filter flag, and the JSON branch is tested.

Verification

CC=/usr/bin/clang CXX=/usr/bin/clang++ SDKROOT=… scripts/test.sh --suites mcp → 397 passed, 4 skipped, 0 failed (exit 0). The existing test asserts the hint is present without pinning its wording, so it stays green. This is narrower than CI's default leg; the local default scripts/test.sh stops at contract Step 0t, which unmodified main also fails in this environment.

QA-Fix-Commit: 73ff216


Posted by the developer account (QA-agent token unavailable). Review by Claude Opus 5.5 (Anthropic) in Claude Code via qa-panel.

@halindrome

Copy link
Copy Markdown
Contributor Author

QA round 2 for this change. It ran on the fork PR (halindrome#7) before the commits were squashed into 5a666f2, so any commit SHAs below refer to the pre-squash history. The QA was performed by Claude Opus 5.5 (Anthropic) in Claude Code.

QA Round 2

Scope: origin/main..HEAD on fix/search-code-file-pattern-path, at 73ff216f. That includes the round-1 fix commit 73ff216f, which rewords the path_filter hint. The PR closes #2836. The panel had 3 lenses: contract-security, regression-edges and test-quality. All 3 landed, and all ran on claude-opus-5-5 with CMM + context-mode navigation. The working tree was unchanged across the panel.

Contract Verification

# Criterion Verdict Evidence
1 A file_pattern containing / is matched against the repo-relative path, with ** crossing directories (zero included), consistent with cbm_glob_to_like ✅ pass search_code_path_glob_match (src/mcp/mcp.c ~14826) is applied in write_scoped_filelist when the pattern contains /. */** cross /, **/ matches zero dirs, and a pattern with no wildcard is a substring match, the same as search_where_basic. It is equal to or slightly stricter than cbm_glob_to_like (src/store/store.c:6910) around /**. Test cases: src/cli/*.c, src/**/*.c, **/cli/*.c, src/cli/.
2 A file_pattern without / keeps basename semantics ✅ pass The basename fnmatch is gated only on !path_glob. The Windows prefilter (cbm_search_code_file_pattern_can_prefilter) is unchanged. Test cases: watcher.c, *.c.
3 Behaviour is the same on POSIX and Windows for scoped searches ✅ pass The path glob runs before the #ifdef split, after separator normalisation. On Windows, handle_search_code keeps / patterns on the in-process scan. The test is not platform-skipped. Verified by reading; not run on Windows.
4 A / pattern yielding 0 results returns a path_filter hint ✅ pass metrics.hint_path_filter is rendered in both the JSON (mcp.c:15722) and toon (mcp.c:15480) outputs, with no duplicate hint key. After the round-1 rewording, SEARCH_CODE_PATH_HINT (mcp.c:14816) is accurate whether the glob selected no files or the selected files lacked the text. The lib/*.c case asserts use path_filter is present, and the other cases assert it is absent.
5 The tool schema describes file_pattern and path_filter ✅ pass mcp.c ~732-734.
6 Existing mcp suite stays green; new tests are red without the change ✅ pass Red without the change: confirmed by reading the code. Basename fnmatch rejects every / case and the old code emits no hint. Green: lenses do not run the suite. The orchestrator ran scripts/test.sh --suites mcp at this head: 397 passed, 4 skipped, 0 failed.
7 Accepted limit: unscoped searches still use find -name ✅ pass The unscoped branch of cbm_search_code_build_grep_cmd is unchanged. A / pattern there yields 0 results and the hint fires, which is the documented limit.

Summary

Severity Confirmed
Critical 0
Major 0
Minor 0

No new findings. The round-1 fix (73ff216f) was reviewed as part of the full diff and introduced no regressions. All 7 contract criteria pass.

Observations

  • TOON-format path_filter hint branch has no test (known, deferred from round 1 Finding 2) · minor · tests/test_mcp.c:11729-11733. Every case sends "format":"json", so the hint in assemble_search_output_toon (mcp.c:15480), the default format, is not exercised. This is low risk because it mirrors the JSON branch, which reads the same flag. It was raised again by test-quality and is listed here as carried forward, not as a new finding.

SAST review not applicable

Target default has security_stage: false in the qa-cycle config. No CI
security stage is wired for this target, so no SAST/SCA delta is computed.


QA performed by Claude Code (claude-opus-5-5), manager + 3-lens panel

Cycle outcome: round 2 is clean. There are no findings, all contract criteria pass, and the round-1 fix is reviewed. Carried forward: no tree/toon-format test for the hint. Not approved: no QA-agent token is available, and approval is the human reviewer's call.


Posted by the developer account (QA-agent token unavailable). Review by Claude Opus 5.5 (Anthropic) in Claude Code via qa-panel.

@github-actions

Copy link
Copy Markdown

Thanks for opening this — it has been seen, and it is queued.

This note is automated, but it is not a brush-off: it exists so you know where your PR stands instead of having to guess from silence.

Current review status: working through a backlog. 0.9.1-rc.1 is out, so the release freeze that held reviews is over — but it left a large queue of open pull requests behind it, and we are reading through them oldest-first. The background is in discussion #1144.

What that means for this PR, concretely:

  • It will not be closed for inactivity. No stale bot touches pull requests here.
  • It may still sit a while before a human reads it. That is on us, not on you.
  • Older PRs are read first, so a recent one is not being skipped — it is behind a queue.

Things that will genuinely speed it up whenever review does happen:

  • Keep it rebased on main — the tree is moving quickly right now, and a conflicting branch cannot be reviewed as the diff you intended.
  • Get CI green, or say which failures you believe are pre-existing.
  • Keep the change to one claim. Bundled features and refactors get split before they get merged, which costs you a round trip.
  • Every commit needs a sign-off (git commit -s) — CI enforces DCO.

If this fixes a bug, a reproduction we can run is worth more than a description of the symptom.

Thanks for contributing, and sorry in advance for the wait.

@halindrome
halindrome marked this pull request as ready for review October 29, 2026 16:51
@halindrome
halindrome requested a review from DeusData as a code owner October 29, 2026 16:51
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
@DeusData

Copy link
Copy Markdown
Owner

Thank you, Shane! The directory-aware file_pattern matching is a careful fix for #2836: it mirrors search_graph's path semantics (including ** spanning zero directories and the no-wildcard substring rule), keeps basename matching for patterns without '/', and routes '/' patterns through one portable matcher on every platform. The zero-result hint and the two short schema descriptions should save agents a wasted round trip.

One maintainer change so CI covers the current tree: we merged current main into your branch (merge commit 6f63e38, signed off). Main had moved on in src/mcp/mcp.c with an unrelated change, so the earlier green no longer covered the combined tree; the merge was clean and changes nothing in your diff. A fresh CI run is on its way, and we will follow up here once it is through. Please pull before pushing again so your branch keeps this commit.

@DeusData
DeusData merged commit 30f1fbf into DeusData:main Oct 31, 2026
26 checks passed
@DeusData

Copy link
Copy Markdown
Owner

Merged as 30f1fbf - thank you, Shane! A search_code file_pattern that contains '/' now matches the repo-relative path with the same semantics as search_graph, patterns without '/' keep matching the basename, and a zero-result search says why. Thank you for the careful matcher, the tests, and for filing #2836 first.

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.

search_code: file_pattern silently matches basenames only (src/cli/*.c returns 0 results)

2 participants