Skip to content

feat: add optional scope path-prefix filter to list_files and list CLI (#165)#167

Merged
shinpr merged 15 commits into
mainfrom
feature/list-files-scope-filter
Jul 9, 2026
Merged

feat: add optional scope path-prefix filter to list_files and list CLI (#165)#167
shinpr merged 15 commits into
mainfrom
feature/list-files-scope-filter

Conversation

@shinpr

@shinpr shinpr commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an optional scope path-prefix filter to the list_files MCP tool and the list CLI subcommand, resolving the large-volume timeout reported in #165. scope is one absolute path prefix or a list (unioned), matched exact-or-descendant on each file's on-disk scan path — reusing the semantics locked for query_documents in #146.

The origin is a real timeout: on a volume with several thousand files, list_files enumerates the whole tree. Rather than filter the result after the fact (which would leave the walk cost intact), the scope predicate is pushed into the BFS traversal — a directory outside every prefix is never readdir'd — so a scoped listing costs work proportional to the scoped subtree instead of the whole volume.

Minor release: 0.15.40.16.0. Fully backward compatible — omitting scope (and calling list_files with no arguments) behaves exactly as before.

Behavior

  • files[] is restricted to entries whose scan path is equal to or under a prefix; boundary-safe (/docs/api matches /docs/api/x.md, not /docs/apiv2).
  • sources: ingest_data (raw-data) sources are always listed regardless of scope; real-file source entries respect scope. This split is applied by a shared classifyIngestedSources helper used by both surfaces.
  • Non-absolute prefix: matches nothing (documented), and now surfaces a non-fatal warning (MCP: a warning content block; CLI: a stderr Warning, exit 0) so an empty result is explained rather than silent.
  • Empty/whitespace scope: rejected at the boundary (MCP McpError(InvalidParams); CLI non-zero exit).
  • No-arg list_files (arguments omitted / {}) still returns the full listing — no regression.

Path basis

scope is guaranteed over the on-disk reachable (scan) path, which makes the traversal pushdown exact. Under a symlinked/aliased root the returned filePath keeps its stored spelling while scope matches the reachable path; this edge is defined and fixture-covered.

Implementation

  • src/utils/scope-match.ts (new) — pure boundary-safe matcher (isUnderOrEqual, matchesAnyScope, shouldVisitDir, isInScope, nonAbsolutePrefixes), the JS counterpart of the SQL buildPrefixPredicate.
  • src/utils/list-sources.ts (new) — pure classifyIngestedSources shared by the MCP handler and the CLI (removes a prior byte-for-byte duplication).
  • Scope pushdown threaded into both BFS walkers (scanBaseDir, bfsCollectSupportedFiles) with a root gate before the first readdir.
  • MCP boundary: ListFilesInput + parseListFilesInput (accepts undefined/{} as no-scope), scope on the list_files schema with scan-path-basis wording, warning content block.
  • CLI: repeatable --scope, threading, and HELP_TEXT.
  • Tool-schema description clarified so an MCP client learns from the in-protocol description alone that sources are ingest_data items and that scope filters files (not ingest_data sources).

Tests

  • Unit: scope-match, list-sources, parseListFilesInput, schema.
  • Integration (real LanceDB + real-FS fixtures):
    • INT-2 — walker pushdown proven directly on both walkers via a readdir EACCES mock/spy (a scope-outside path is never visited), discriminating real pruning from a post-scan filter.
    • INT-1 / INT-3 — MCP handler / CLI scoped listing, sources split, and a scanned-file realpath spy.
    • Non-absolute-prefix warning tests on both surfaces.
  • E2E: a service-integration-e2e spawning the real CLI list --scope against a real fixture.
  • pnpm run check:all green (1047 tests); cross-platform-sensitive fixtures (symlink alias) skip on unsupported platforms rather than fail.

Closes #165.

🤖 Generated with Claude Code

shinpr and others added 12 commits July 9, 2026 10:22
Pure JS helper (isUnderOrEqual/matchesAnyScope) mirroring the SQL
buildPrefixPredicate contract: separator derived from the prefix
(default node:path.sep), trailing-separator equivalence, boundary-safe
exact-or-descendant matching. Foundation for list_files/list scope
(Phase 1 T1, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Thread an optional scope into scanBaseDir (3rd param) and
bfsCollectSupportedFiles (4th param, after maxDepth). A shouldVisitDir
gate (matchesAnyScope OR ancestor-of-scope) is applied at the root
enqueue and before each subdir enqueue, so a non-intersecting baseDir
is skipped with zero readdir; files are collected only when in scope.
Scope-absent behavior is byte-for-byte unchanged (Phase 1 T2, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Walker-layer integration test run against both scanBaseDir and
bfsCollectSupportedFiles: a readdir EACCES-mock/spy records visited
dirs and asserts a scope-outside path is never visited under scope
(with a no-scope sentinel proving the probe fires) — discriminating a
post-scan filter from real pruning. Covers boundary, root gate,
ancestor descent, order, cross-platform separators, and AC10 warning
semantics (Phase 1 T3, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add the MCP-boundary type and validator for list_files scope.
parseListFilesInput preserves the no-argument contract: undefined/{}
short-circuit to no-scope before asRecord (AC12), and a present scope
is validated via the existing module-private normalizeScope. Not yet
wired into the dispatcher (Phase 2 T1, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fier

Wire the list_files MCP dispatch through parseListFilesInput and thread
the normalized scope into scanBaseDir. Extract the sources
classification into a new pure, surface-independent helper
classifyIngestedSources (src/utils/list-sources.ts): raw-data sources
are always emitted; real-file entries are scope-filtered so a
scope-outside ingested real file drops from both files[] and sources[].
Scope-absent behavior is byte-for-byte unchanged. Adds INT-1 handler
integration tests (real LanceDB + FS; scanned-file realpath spy proving
pushdown; symlink-alias contract) and list-sources unit tests
(Phase 2 T2, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an optional scope property (oneOf string|array) to the list_files
inputSchema with bespoke scan-path-basis wording — files reachable at a
path equal to or under a prefix within the base directories — instead
of copying the query_documents "filePath equal to or under" phrasing,
which would contradict the scan-path guarantee under aliased roots
(Phase 3 T1, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Parse a repeatable --scope <prefix> (empty/whitespace rejected with a
non-zero exit), thread it into scanRoot -> bfsCollectSupportedFiles, and
reuse the shared classifyIngestedSources so the CLI and MCP surfaces
apply an identical raw-data/real-file sources split. Document --scope in
HELP_TEXT. Adds INT-3 integration tests (real LanceDB + FS; scanned-file
realpath spy pushdown proof; platform-guarded alias) (Phase 3 T2, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a service-integration-e2e (E2E-1) that spawns the real CLI
`list --scope` against a real LanceDB + FS fixture and asserts the
stdout JSON files[] are scope-restricted, raw-data sources retained,
and repeated --scope unions. Document list_files / list --scope
(scan-path basis) in README, SKILL.md, and cli-reference.md
(Final Phase, #165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the duplicated scope-integration composition (shouldVisitDir /
file-collect guard) from both BFS walkers into pure shouldVisitDir and
isInScope helpers in scope-match.ts. Removes the drift-prone core of the
walker duplication (code-review finding #1) so a boundary-semantics
change lives in one place. Behavior-preserving; INT-2 covers both
walkers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A non-absolute scope prefix matches nothing under the exact-or-descendant
contract, which previously produced a silent empty result. Both surfaces
now surface a non-fatal warning naming each non-absolute prefix (MCP: a
warning content block; CLI: a stderr Warning, exit 0 preserved) so the
"why is it empty?" gap is closed. Result semantics unchanged; the
existing empty/whitespace --scope rejection is untouched
(code-review finding #2).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The list_files schema — the only description an MCP client reads at
call time — under-documented sources and scope's effect on them, which
read as a bug during MCP debugging. Name sources as ingest_data content
(dropping the imprecise "outside the base dirs"), and state positively
that scope filters files by their scan path while ingest_data sources
(which have no base-directory path) are always listed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Minor release: adds the optional scope path-prefix filter to the
list_files tool and the list CLI subcommand (#165).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shinpr shinpr self-assigned this Jul 9, 2026
shinpr and others added 3 commits July 9, 2026 15:35
SKILL.md's `Search: Core Rules > Scope` section documents
query_documents scope; the appended list_files/CLI paragraph belonged
elsewhere. CLI flags live in references/cli-reference.md, and the
scope-vs-sources behavior is now stated in the list_files tool schema
description itself, so the supplement is redundant.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the restated boundary example and verbose phrasing so the list
--scope row mirrors the query --scope entry's density, keeping only the
two real deltas: scan-path basis and ingest_data sources always listed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /a/b/ and /a/b// variants appended a hardcoded "/", producing a
mixed "\...\a\b/" prefix on Windows that the matcher reads as \-style
(and never strips), so the case returned empty. Build the variants with
the OS separator instead. Test-only; production is unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@shinpr
shinpr merged commit 343d908 into main Jul 9, 2026
16 of 18 checks passed
@shinpr
shinpr deleted the feature/list-files-scope-filter branch July 9, 2026 07:17
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.

Feature request: optional filter on list_files

1 participant