cocoindex-code 0.2.36__tar.gz → 0.2.37__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/PKG-INFO +36 -5
  2. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/README.md +31 -0
  3. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/pyproject.toml +5 -5
  4. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/_version.py +2 -2
  5. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/cli.py +75 -0
  6. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/daemon.py +3 -10
  7. cocoindex_code-0.2.36/src/cocoindex_code/indexer.py → cocoindex_code-0.2.37/src/cocoindex_code/file_walk.py +63 -118
  8. cocoindex_code-0.2.37/src/cocoindex_code/grep.py +411 -0
  9. cocoindex_code-0.2.37/src/cocoindex_code/indexer.py +123 -0
  10. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/.gitignore +0 -0
  11. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/LICENSE +0 -0
  12. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/__init__.py +0 -0
  13. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/__main__.py +0 -0
  14. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/_daemon_paths.py +0 -0
  15. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/chunking.py +0 -0
  16. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/client.py +0 -0
  17. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/embedder_defaults.py +0 -0
  18. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/embedder_params.py +0 -0
  19. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/litellm_embedder.py +0 -0
  20. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/project.py +0 -0
  21. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/protocol.py +0 -0
  22. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/query.py +0 -0
  23. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/schema.py +0 -0
  24. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/server.py +0 -0
  25. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/settings.py +0 -0
  26. {cocoindex_code-0.2.36 → cocoindex_code-0.2.37}/src/cocoindex_code/shared.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cocoindex-code
3
- Version: 0.2.36
3
+ Version: 0.2.37
4
4
  Summary: MCP server for indexing and querying codebases using CocoIndex
5
5
  Project-URL: Homepage, https://github.com/cocoindex-io/cocoindex-code
6
6
  Project-URL: Repository, https://github.com/cocoindex-io/cocoindex-code
@@ -17,7 +17,7 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
19
  Requires-Python: >=3.11
20
- Requires-Dist: cocoindex[litellm]<1.1.0,>=1.0.6
20
+ Requires-Dist: cocoindex[litellm]<1.1.0,>=1.0.13
21
21
  Requires-Dist: einops>=0.8.2
22
22
  Requires-Dist: mcp>=1.0.0
23
23
  Requires-Dist: msgspec>=0.19.0
@@ -29,7 +29,7 @@ Requires-Dist: questionary>=2.0.0
29
29
  Requires-Dist: sqlite-vec>=0.1.0
30
30
  Requires-Dist: typer>=0.9.0
31
31
  Provides-Extra: dev
32
- Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.6; extra == 'dev'
32
+ Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.13; extra == 'dev'
33
33
  Requires-Dist: mypy>=1.0.0; extra == 'dev'
34
34
  Requires-Dist: prek>=0.1.0; extra == 'dev'
35
35
  Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
@@ -37,9 +37,9 @@ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
37
37
  Requires-Dist: pytest>=7.0.0; extra == 'dev'
38
38
  Requires-Dist: ruff>=0.1.0; extra == 'dev'
39
39
  Provides-Extra: embeddings-local
40
- Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.6; extra == 'embeddings-local'
40
+ Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.13; extra == 'embeddings-local'
41
41
  Provides-Extra: full
42
- Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.6; extra == 'full'
42
+ Requires-Dist: cocoindex[sentence-transformers]<1.1.0,>=1.0.13; extra == 'full'
43
43
  Description-Content-Type: text/markdown
44
44
 
45
45
  <p align="center">
@@ -224,6 +224,7 @@ The background daemon starts automatically on first use.
224
224
  | `ccc init` | Initialize a project — creates settings files, adds `.cocoindex_code/` to `.gitignore` |
225
225
  | `ccc index` | Build or update the index (auto-inits if needed). Shows streaming progress. |
226
226
  | `ccc search <query>` | Semantic search across the codebase |
227
+ | `ccc grep <pattern> [path]` | Structural code search by example (no index needed) |
227
228
  | `ccc status` | Show index stats (chunk count, file count, language breakdown) |
228
229
  | `ccc mcp` | Run as MCP server in stdio mode |
229
230
  | `ccc doctor` | Run diagnostics — checks settings, daemon, model, file matching, and index health |
@@ -244,6 +245,36 @@ ccc search --refresh database schema # update index first, then
244
245
 
245
246
  By default, `ccc search` scopes results to your current working directory (relative to the project root). Use `--path` to override.
246
247
 
248
+ ### Structural Search (`ccc grep`)
249
+
250
+ `ccc grep` finds code by **structure**, not text — you write a by-example pattern
251
+ and it matches the syntax tree (via cocoindex's `code_match`), so formatting,
252
+ whitespace, and intervening tokens don't matter. It runs entirely locally: no
253
+ index, daemon, or embeddings required.
254
+
255
+ ```bash
256
+ ccc grep 'def \NAME(\(ARGS*\)):' # every Python function def under the cwd
257
+ ccc grep 'foo(\(ARGS*\))' src/ # calls to foo(...) anywhere under src/
258
+ ccc grep 'fn \NAME(\(A*\))' --lang rust # restrict to one language
259
+ ccc grep 'class \NAME:' --path 'tests/**' # restrict to a path glob
260
+ ccc grep 'TODO(\(A*\))' path/to/file.py # a single file
261
+ ```
262
+
263
+ Metavariables use the `\` sigil: `\NAME` captures one node, `\(NAME*\)` a run of
264
+ siblings, `\_`/`\*` match anonymously. The pattern is matched per language, so a
265
+ single invocation scans every supported source file (others are skipped). Inside
266
+ an initialized project, `ccc grep` honors the project's include/exclude patterns
267
+ and `.gitignore`; otherwise it scans all supported source files under the path.
268
+
269
+ Results stream to the terminal file-by-file as each match is found (in completion
270
+ order, since files are matched in parallel) rather than all at once at the end.
271
+ Each matching file shows its matched line range; under a TTY the path is colored,
272
+ line numbers are dimmed, and the unmatched context around a match is dimmed so the
273
+ match stands out.
274
+
275
+ > **Note:** `ccc grep` relies on cocoindex's structural `code_match` feature.
276
+ > Until it ships in a released cocoindex, run against a local cocoindex build.
277
+
247
278
  ## Docker
248
279
 
249
280
  A Docker image is available for teams who want a reproducible, dependency-free
@@ -180,6 +180,7 @@ The background daemon starts automatically on first use.
180
180
  | `ccc init` | Initialize a project — creates settings files, adds `.cocoindex_code/` to `.gitignore` |
181
181
  | `ccc index` | Build or update the index (auto-inits if needed). Shows streaming progress. |
182
182
  | `ccc search <query>` | Semantic search across the codebase |
183
+ | `ccc grep <pattern> [path]` | Structural code search by example (no index needed) |
183
184
  | `ccc status` | Show index stats (chunk count, file count, language breakdown) |
184
185
  | `ccc mcp` | Run as MCP server in stdio mode |
185
186
  | `ccc doctor` | Run diagnostics — checks settings, daemon, model, file matching, and index health |
@@ -200,6 +201,36 @@ ccc search --refresh database schema # update index first, then
200
201
 
201
202
  By default, `ccc search` scopes results to your current working directory (relative to the project root). Use `--path` to override.
202
203
 
204
+ ### Structural Search (`ccc grep`)
205
+
206
+ `ccc grep` finds code by **structure**, not text — you write a by-example pattern
207
+ and it matches the syntax tree (via cocoindex's `code_match`), so formatting,
208
+ whitespace, and intervening tokens don't matter. It runs entirely locally: no
209
+ index, daemon, or embeddings required.
210
+
211
+ ```bash
212
+ ccc grep 'def \NAME(\(ARGS*\)):' # every Python function def under the cwd
213
+ ccc grep 'foo(\(ARGS*\))' src/ # calls to foo(...) anywhere under src/
214
+ ccc grep 'fn \NAME(\(A*\))' --lang rust # restrict to one language
215
+ ccc grep 'class \NAME:' --path 'tests/**' # restrict to a path glob
216
+ ccc grep 'TODO(\(A*\))' path/to/file.py # a single file
217
+ ```
218
+
219
+ Metavariables use the `\` sigil: `\NAME` captures one node, `\(NAME*\)` a run of
220
+ siblings, `\_`/`\*` match anonymously. The pattern is matched per language, so a
221
+ single invocation scans every supported source file (others are skipped). Inside
222
+ an initialized project, `ccc grep` honors the project's include/exclude patterns
223
+ and `.gitignore`; otherwise it scans all supported source files under the path.
224
+
225
+ Results stream to the terminal file-by-file as each match is found (in completion
226
+ order, since files are matched in parallel) rather than all at once at the end.
227
+ Each matching file shows its matched line range; under a TTY the path is colored,
228
+ line numbers are dimmed, and the unmatched context around a match is dimmed so the
229
+ match stands out.
230
+
231
+ > **Note:** `ccc grep` relies on cocoindex's structural `code_match` feature.
232
+ > Until it ships in a released cocoindex, run against a local cocoindex build.
233
+
203
234
  ## Docker
204
235
 
205
236
  A Docker image is available for teams who want a reproducible, dependency-free
@@ -23,7 +23,7 @@ classifiers = [
23
23
 
24
24
  dependencies = [
25
25
  "mcp>=1.0.0",
26
- "cocoindex[litellm]>=1.0.6,<1.1.0",
26
+ "cocoindex[litellm]>=1.0.13,<1.1.0",
27
27
  "sqlite-vec>=0.1.0",
28
28
  "pydantic>=2.0.0",
29
29
  "numpy>=1.24.0",
@@ -39,7 +39,7 @@ dependencies = [
39
39
  # `embeddings-local` is the primary feature extra: it pulls in
40
40
  # `sentence-transformers` (via cocoindex) so local embeddings work without
41
41
  # an API key.
42
- embeddings-local = ["cocoindex[sentence-transformers]>=1.0.6,<1.1.0"]
42
+ embeddings-local = ["cocoindex[sentence-transformers]>=1.0.13,<1.1.0"]
43
43
  # `full` is the umbrella "batteries-included" alias. Today it's just
44
44
  # `embeddings-local`, but we expect to bundle more optional niceties under
45
45
  # it over time — users who want everything can keep using `[full]` and pick
@@ -47,7 +47,7 @@ embeddings-local = ["cocoindex[sentence-transformers]>=1.0.6,<1.1.0"]
47
47
  # `:full` image variant for consistency across install paths. Contents are
48
48
  # inlined rather than self-referencing `cocoindex-code[embeddings-local]`
49
49
  # to avoid resolver edge cases with older pip.
50
- full = ["cocoindex[sentence-transformers]>=1.0.6,<1.1.0"]
50
+ full = ["cocoindex[sentence-transformers]>=1.0.13,<1.1.0"]
51
51
  dev = [
52
52
  "pytest>=7.0.0",
53
53
  "pytest-asyncio>=0.21.0",
@@ -55,7 +55,7 @@ dev = [
55
55
  "ruff>=0.1.0",
56
56
  "mypy>=1.0.0",
57
57
  "prek>=0.1.0",
58
- "cocoindex[sentence-transformers]>=1.0.6,<1.1.0",
58
+ "cocoindex[sentence-transformers]>=1.0.13,<1.1.0",
59
59
  ]
60
60
 
61
61
  [project.scripts]
@@ -89,7 +89,7 @@ dev = [
89
89
  "mypy>=1.0.0",
90
90
  "prek>=0.1.0",
91
91
  "types-pyyaml>=6.0.12.20250915",
92
- "cocoindex[sentence-transformers]>=1.0.7,<1.1.0",
92
+ "cocoindex[sentence-transformers]>=1.0.13,<1.1.0",
93
93
  ]
94
94
 
95
95
  [tool.ruff]
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '0.2.36'
22
- __version_tuple__ = version_tuple = (0, 2, 36)
21
+ __version__ = version = '0.2.37'
22
+ __version_tuple__ = version_tuple = (0, 2, 37)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import functools
6
6
  import os
7
7
  import sys
8
+ import threading
8
9
  from collections.abc import Callable
9
10
  from pathlib import Path
10
11
  from typing import TYPE_CHECKING, TypeVar
@@ -12,6 +13,7 @@ from typing import TYPE_CHECKING, TypeVar
12
13
  import typer as _typer
13
14
 
14
15
  if TYPE_CHECKING:
16
+ from .grep import FileMatches, GrepWarning
15
17
  from .protocol import (
16
18
  DoctorCheckResult,
17
19
  IndexingProgress,
@@ -657,6 +659,79 @@ def search(
657
659
  print_search_results(resp)
658
660
 
659
661
 
662
+ @app.command()
663
+ def grep(
664
+ pattern: str = _typer.Argument(
665
+ ...,
666
+ help=r"By-example structural pattern; use \ for metavariables, "
667
+ r"e.g. 'def \NAME(\(ARGS*\)):' or 'foo(\(ARGS*\))'.",
668
+ ),
669
+ path: str = _typer.Argument(
670
+ ".", help="File or directory to search (default: current directory)."
671
+ ),
672
+ lang: list[str] = _typer.Option(
673
+ [], "--lang", help="Only match files of these languages (e.g. python, rust, cpp)."
674
+ ),
675
+ path_glob: str | None = _typer.Option(
676
+ None, "--path", help="Only match files whose path matches this glob (globset syntax)."
677
+ ),
678
+ no_color: bool = _typer.Option(False, "--no-color", help="Disable colored output."),
679
+ ) -> None:
680
+ r"""Structurally grep code by example (no index or daemon required).
681
+
682
+ Compiles the pattern per language and matches every supported source file
683
+ under PATH in parallel. Inside an initialized project it honors the project's
684
+ include/exclude and .gitignore rules; otherwise it scans all supported source
685
+ files.
686
+ """
687
+ from . import grep as _grep
688
+
689
+ target = Path(path)
690
+ if not target.exists():
691
+ _typer.echo(f"Error: path not found: {path}", err=True)
692
+ raise _typer.Exit(code=1)
693
+
694
+ req = _grep.GrepRequest(
695
+ pattern=pattern,
696
+ root=target,
697
+ languages=frozenset(lang_name.lower() for lang_name in lang) or None,
698
+ path_glob=path_glob,
699
+ )
700
+ use_color = not no_color and sys.stdout.isatty() and not os.environ.get("NO_COLOR")
701
+ grep_run = _grep.Grep(req)
702
+ matched = 0
703
+ # `run` calls `_emit` from several worker threads at once; the lock keeps one
704
+ # file's output (and the `matched` bookkeeping) from interleaving with another's.
705
+ output_lock = threading.Lock()
706
+
707
+ def _emit(item: FileMatches | GrepWarning) -> None:
708
+ nonlocal matched
709
+ if isinstance(item, _grep.GrepWarning):
710
+ with output_lock:
711
+ _typer.echo(f"warning: {item.message}", err=True)
712
+ return
713
+ block = _grep.render_file(item, color=use_color) # render outside the lock
714
+ with output_lock:
715
+ if matched:
716
+ _typer.echo() # blank line between files
717
+ _typer.echo(block)
718
+ matched += 1
719
+
720
+ grep_run.run(_emit)
721
+
722
+ # The "unusable everywhere" verdict needs the whole walk, so it's known only
723
+ # once the run is done — report it before exiting.
724
+ if grep_run.unusable:
725
+ langs = ", ".join(grep_run.failed_languages)
726
+ _typer.echo(
727
+ f"Error: the pattern did not compile for any of the languages found ({langs}).",
728
+ err=True,
729
+ )
730
+ raise _typer.Exit(code=1)
731
+ if matched == 0:
732
+ _typer.echo("No matches found.")
733
+
734
+
660
735
  @app.command()
661
736
  @_catch_daemon_start_error
662
737
  def status() -> None:
@@ -372,10 +372,8 @@ async def _check_file_walk(project_root_str: str) -> DoctorCheckResult:
372
372
  """Walk project files and report counts + gitignore paths."""
373
373
  from pathlib import PurePath
374
374
 
375
- from cocoindex.resources.file import PatternFilePathMatcher
376
-
377
- from .indexer import GitignoreAwareMatcher
378
- from .settings import load_gitignore_spec, load_project_settings
375
+ from .file_walk import build_matcher
376
+ from .settings import load_project_settings
379
377
 
380
378
  project_root = Path(project_root_str)
381
379
  try:
@@ -383,12 +381,7 @@ async def _check_file_walk(project_root_str: str) -> DoctorCheckResult:
383
381
  except FileNotFoundError as e:
384
382
  return DoctorCheckResult(name="File Walk", ok=False, details=[], errors=[str(e)])
385
383
 
386
- gitignore_spec = load_gitignore_spec(project_root)
387
- base_matcher = PatternFilePathMatcher(
388
- included_patterns=ps.include_patterns,
389
- excluded_patterns=ps.exclude_patterns,
390
- )
391
- matcher = GitignoreAwareMatcher(base_matcher, gitignore_spec, project_root)
384
+ matcher = build_matcher(project_root, ps.include_patterns, ps.exclude_patterns)
392
385
 
393
386
  counts_by_ext: dict[str, int] = {}
394
387
  gitignore_dirs: list[str] = []
@@ -1,36 +1,22 @@
1
- """CocoIndex app for indexing codebases."""
1
+ """Shared source-file walking: pattern + .gitignore matching, reused by the
2
+ indexer, the daemon's doctor file-walk, and ``ccc grep``.
3
+
4
+ The matcher (include/exclude globs + nested ``.gitignore`` awareness) is the
5
+ single source of truth for "which files count as part of the project". The
6
+ indexer feeds it to CocoIndex's incremental file source; the daemon and ``ccc
7
+ grep`` drive a plain :func:`os.walk` over it via :func:`iter_included_files`.
8
+ """
2
9
 
3
10
  from __future__ import annotations
4
11
 
5
- from collections.abc import Iterable
12
+ import os
13
+ from collections.abc import Iterable, Iterator
6
14
  from pathlib import Path, PurePath
7
15
 
8
- import cocoindex as coco
9
- from cocoindex.connectors import localfs, sqlite
10
- from cocoindex.connectors.sqlite import Vec0TableDef
11
- from cocoindex.ops.text import RecursiveSplitter, detect_code_language
12
- from cocoindex.resources.chunk import Chunk
13
16
  from cocoindex.resources.file import FilePathMatcher, PatternFilePathMatcher
14
- from cocoindex.resources.id import IdGenerator
15
17
  from pathspec import GitIgnoreSpec
16
18
 
17
- from .chunking import CHUNKER_REGISTRY
18
- from .settings import load_gitignore_spec, load_project_settings
19
- from .shared import (
20
- CODEBASE_DIR,
21
- EMBEDDER,
22
- INDEXING_EMBED_PARAMS,
23
- SQLITE_DB,
24
- CodeChunk,
25
- )
26
-
27
- # Chunking configuration
28
- CHUNK_SIZE = 1000
29
- MIN_CHUNK_SIZE = 250
30
- CHUNK_OVERLAP = 150
31
-
32
- # Chunking splitter (stateless, can be module-level)
33
- splitter = RecursiveSplitter()
19
+ from .settings import load_gitignore_spec
34
20
 
35
21
 
36
22
  def _normalize_gitignore_lines(lines: Iterable[str], directory: PurePath) -> list[str]:
@@ -134,98 +120,57 @@ class GitignoreAwareMatcher(FilePathMatcher):
134
120
  return self._delegate.is_file_included(path)
135
121
 
136
122
 
137
- @coco.fn(memo=True)
138
- async def process_file(
139
- file: localfs.File,
140
- table: sqlite.TableTarget[CodeChunk],
141
- ) -> None:
142
- """Process a single file: chunk, embed, and store."""
143
- embedder = coco.use_context(EMBEDDER)
144
- indexing_params = coco.use_context(INDEXING_EMBED_PARAMS)
145
-
146
- try:
147
- content = await file.read_text()
148
- except UnicodeDecodeError:
149
- return
150
-
151
- if not content.strip():
152
- return
153
-
154
- suffix = file.file_path.path.suffix
155
- project_root = coco.use_context(CODEBASE_DIR)
156
- ps = load_project_settings(project_root)
157
- ext_lang_map = {f".{lo.ext}": lo.lang for lo in ps.language_overrides}
158
- language = (
159
- ext_lang_map.get(suffix)
160
- or detect_code_language(filename=file.file_path.path.name)
161
- or "text"
162
- )
163
-
164
- chunker_registry = coco.use_context(CHUNKER_REGISTRY)
165
- chunker = chunker_registry.get(suffix)
166
- if chunker is not None:
167
- language_override, chunks = chunker(Path(file.file_path.path), content)
168
- if language_override is not None:
169
- language = language_override
170
- else:
171
- chunks = splitter.split(
172
- content,
173
- chunk_size=CHUNK_SIZE,
174
- min_chunk_size=MIN_CHUNK_SIZE,
175
- chunk_overlap=CHUNK_OVERLAP,
176
- language=language,
177
- )
178
-
179
- id_gen = IdGenerator()
180
-
181
- async def process(chunk: Chunk) -> None:
182
- table.declare_row(
183
- row=CodeChunk(
184
- id=await id_gen.next_id(chunk.text),
185
- file_path=file.file_path.path.as_posix(),
186
- language=language,
187
- content=chunk.text,
188
- start_line=chunk.start.line,
189
- end_line=chunk.end.line,
190
- embedding=await embedder.embed(chunk.text, **indexing_params),
191
- )
192
- )
193
-
194
- await coco.map(process, chunks)
195
-
196
-
197
- @coco.fn
198
- async def indexer_main() -> None:
199
- """Main indexing function - walks files and processes each."""
200
- project_root = coco.use_context(CODEBASE_DIR)
201
- ps = load_project_settings(project_root)
202
- gitignore_spec = load_gitignore_spec(project_root)
203
-
204
- table = await sqlite.mount_table_target(
205
- db=SQLITE_DB,
206
- table_name="code_chunks_vec",
207
- table_schema=await sqlite.TableSchema.from_class(
208
- CodeChunk,
209
- primary_key=["id"],
210
- ),
211
- virtual_table_def=Vec0TableDef(
212
- partition_key_columns=["language"],
213
- auxiliary_columns=["file_path", "content", "start_line", "end_line"],
214
- ),
215
- )
216
-
123
+ def find_git_root(start: Path) -> Path | None:
124
+ """Walk up from ``start`` to the nearest directory holding a ``.git`` entry — a
125
+ directory for a normal repo, or a *file* for a submodule or linked worktree.
126
+ Returns that directory, or ``None`` if ``start`` is not inside a git repo.
127
+
128
+ Used to anchor ``.gitignore`` resolution at the real repo root when grepping a
129
+ subdirectory that isn't inside an initialized cocoindex project."""
130
+ current = start.resolve()
131
+ while True:
132
+ if (current / ".git").exists():
133
+ return current
134
+ if current.parent == current:
135
+ return None
136
+ current = current.parent
137
+
138
+
139
+ def build_matcher(
140
+ project_root: Path,
141
+ included_patterns: list[str],
142
+ excluded_patterns: list[str],
143
+ ) -> FilePathMatcher:
144
+ """Build the project's file matcher: include/exclude globs plus nested
145
+ ``.gitignore`` awareness anchored at ``project_root``."""
217
146
  base_matcher = PatternFilePathMatcher(
218
- included_patterns=ps.include_patterns,
219
- excluded_patterns=ps.exclude_patterns,
220
- )
221
- matcher: FilePathMatcher = GitignoreAwareMatcher(base_matcher, gitignore_spec, project_root)
222
-
223
- files = localfs.walk_dir(
224
- CODEBASE_DIR,
225
- recursive=True,
226
- path_matcher=matcher,
227
- )
228
-
229
- await coco.mount_each(
230
- coco.component_subpath(coco.Symbol("process_file")), process_file, files.items(), table
147
+ included_patterns=included_patterns,
148
+ excluded_patterns=excluded_patterns,
231
149
  )
150
+ return GitignoreAwareMatcher(base_matcher, load_gitignore_spec(project_root), project_root)
151
+
152
+
153
+ def iter_included_files(
154
+ start: Path,
155
+ base: Path,
156
+ matcher: FilePathMatcher,
157
+ ) -> Iterator[tuple[Path, PurePath]]:
158
+ """Walk ``start`` recursively, yielding ``(absolute_path, path_relative_to_base)``
159
+ for every file ``matcher`` includes, pruning excluded directories.
160
+
161
+ ``base`` anchors the relative paths the matcher sees (the project root, so
162
+ its patterns line up); ``start`` is where traversal begins and may be a
163
+ subdirectory of ``base``. Both must be absolute. Traversal is deterministic
164
+ (directories and files are visited in sorted order).
165
+ """
166
+ for dirpath_str, dirnames, filenames in os.walk(start):
167
+ dirpath = Path(dirpath_str)
168
+ rel_dir = PurePath(dirpath.relative_to(base))
169
+ if rel_dir != PurePath(".") and not matcher.is_dir_included(rel_dir):
170
+ dirnames.clear()
171
+ continue
172
+ dirnames.sort()
173
+ for fname in sorted(filenames):
174
+ rel_path = rel_dir / fname if rel_dir != PurePath(".") else PurePath(fname)
175
+ if matcher.is_file_included(rel_path):
176
+ yield dirpath / fname, rel_path
@@ -0,0 +1,411 @@
1
+ r"""``ccc grep`` — by-example structural code search over files.
2
+
3
+ Unlike ``ccc search`` (semantic, needs the index + daemon + embeddings), ``grep``
4
+ runs entirely locally: it compiles a structural pattern (cocoindex ``code_match``)
5
+ once per language, walks the matching source files, and matches them in parallel.
6
+ No index or daemon is required.
7
+
8
+ Patterns use the ``\`` sigil for metavariables, e.g. ``def \NAME(\(ARGS*\)):`` or
9
+ ``foo(\(ARGS*\))``. See the cocoindex code_match design for the full syntax.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import functools
15
+ from collections.abc import Callable, Iterator
16
+ from concurrent.futures import ThreadPoolExecutor
17
+ from dataclasses import dataclass
18
+ from pathlib import Path
19
+
20
+ import click
21
+ from cocoindex.ops.code import CodeMatch, CodePattern
22
+ from cocoindex.ops.text import detect_code_language
23
+ from cocoindex.resources.file import PatternFilePathMatcher
24
+
25
+ from .file_walk import build_matcher, find_git_root, iter_included_files
26
+ from .settings import (
27
+ DEFAULT_EXCLUDED_PATTERNS,
28
+ DEFAULT_INCLUDED_PATTERNS,
29
+ find_project_root,
30
+ load_project_settings,
31
+ )
32
+
33
+ # A trivial, always-valid pattern (a bare identifier) used to probe whether a
34
+ # language is structurally matchable, independent of the user's pattern.
35
+ _PROBE_PATTERN = "x"
36
+
37
+
38
+ @dataclass(frozen=True, slots=True)
39
+ class GrepWarning:
40
+ """A non-fatal problem surfaced during a grep — a file that couldn't be read,
41
+ or a supported language the pattern failed to compile for. grep keeps going;
42
+ the CLI prints these to stderr."""
43
+
44
+ message: str
45
+
46
+ language: str | None = None
47
+ """Set when this is a per-language pattern-compile failure (so :class:`Grep` can
48
+ tell whether the pattern is unusable for *every* language it tried)."""
49
+
50
+
51
+ @dataclass(frozen=True, slots=True)
52
+ class FileMatches:
53
+ """Every match found in one file."""
54
+
55
+ path: str
56
+ """Display path, mirroring the root passed to grep (e.g. ``src/a.py`` or
57
+ ``/tmp/x/a.py``)."""
58
+
59
+ source: str
60
+ """Full file content — kept so the renderer can show context around each match."""
61
+
62
+ matches: list[CodeMatch]
63
+ """Matches in source order (at least one)."""
64
+
65
+
66
+ @dataclass(frozen=True, slots=True)
67
+ class GrepRequest:
68
+ """A grep invocation."""
69
+
70
+ pattern: str
71
+ root: Path
72
+ """File or directory to search."""
73
+
74
+ languages: frozenset[str] | None = None
75
+ """Restrict to these languages (lowercased canonical names); ``None`` = all."""
76
+
77
+ path_glob: str | None = None
78
+ """Extra include glob (globset syntax) on the project-relative path; ``None`` = all."""
79
+
80
+
81
+ @dataclass(frozen=True, slots=True)
82
+ class _Target:
83
+ path: Path
84
+ display: str
85
+ pattern: CodePattern
86
+
87
+
88
+ # ---------------------------------------------------------------------------
89
+ # Per-language pattern compilation
90
+ # ---------------------------------------------------------------------------
91
+
92
+
93
+ @functools.cache
94
+ def _is_match_supported(language: str) -> bool:
95
+ """Whether code_match can structurally match ``language``.
96
+
97
+ Probes with a trivial always-valid pattern so the answer doesn't depend on
98
+ the user's pattern. Cached across the process.
99
+ """
100
+ try:
101
+ CodePattern(_PROBE_PATTERN, language=language)
102
+ return True
103
+ except ValueError:
104
+ return False
105
+
106
+
107
+ class _PatternCompiler:
108
+ r"""Compiles one pattern per language on demand, caching the result.
109
+
110
+ code_match patterns are language-bound (compiled against a grammar's token
111
+ table), so we keep one compiled :class:`CodePattern` per language and reuse it
112
+ across every file of that language. A language maps to ``None`` (its files are
113
+ skipped) when either code_match can't match it at all, or the pattern won't
114
+ compile for it — the latter records a :class:`GrepWarning` once for that
115
+ language, so the user learns why those files were skipped instead of the whole
116
+ run aborting.
117
+ """
118
+
119
+ def __init__(self, pattern: str) -> None:
120
+ self._pattern = pattern
121
+ self._cache: dict[str, CodePattern | None] = {}
122
+ self.warnings: list[GrepWarning] = []
123
+
124
+ def for_language(self, language: str) -> CodePattern | None:
125
+ if language not in self._cache:
126
+ self._cache[language] = self._compile(language)
127
+ return self._cache[language]
128
+
129
+ def _compile(self, language: str) -> CodePattern | None:
130
+ try:
131
+ return CodePattern(self._pattern, language=language)
132
+ except ValueError as e:
133
+ # A *supported* language that won't compile the pattern is a real
134
+ # problem to surface (once per language); an *unsupported* language is
135
+ # an expected silent skip. `_is_match_supported` tells them apart,
136
+ # independent of the user's pattern.
137
+ if _is_match_supported(language):
138
+ self.warnings.append(
139
+ GrepWarning(f"pattern invalid for {language}: {e}", language=language)
140
+ )
141
+ return None
142
+
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Target collection (which files to match, with which compiled pattern)
146
+ # ---------------------------------------------------------------------------
147
+
148
+
149
+ def _detect_language(path: Path, ext_overrides: dict[str, str]) -> str | None:
150
+ """Language for ``path``: project extension override first, then auto-detect."""
151
+ return ext_overrides.get(path.suffix) or detect_code_language(filename=path.name)
152
+
153
+
154
+ def _ext_overrides(project_root: Path | None) -> dict[str, str]:
155
+ if project_root is None:
156
+ return {}
157
+ ps = load_project_settings(project_root)
158
+ return {f".{lo.ext}": lo.lang for lo in ps.language_overrides}
159
+
160
+
161
+ def _target_for_file(
162
+ abs_path: Path,
163
+ display: str,
164
+ ext_overrides: dict[str, str],
165
+ req: GrepRequest,
166
+ compiler: _PatternCompiler,
167
+ ) -> _Target | None:
168
+ language = _detect_language(abs_path, ext_overrides)
169
+ if language is None:
170
+ return None
171
+ if req.languages is not None and language.lower() not in req.languages:
172
+ return None
173
+ cp = compiler.for_language(language)
174
+ if cp is None:
175
+ return None
176
+ return _Target(path=abs_path, display=display, pattern=cp)
177
+
178
+
179
+ def _resolve_file(
180
+ abs_path: Path,
181
+ display: str,
182
+ ext_overrides: dict[str, str],
183
+ req: GrepRequest,
184
+ compiler: _PatternCompiler,
185
+ ) -> Iterator[_Target | GrepWarning]:
186
+ """Yield any compile warning newly raised for this file's language, then the
187
+ file's target if the pattern compiled for that language."""
188
+ before = len(compiler.warnings)
189
+ target = _target_for_file(abs_path, display, ext_overrides, req, compiler)
190
+ yield from compiler.warnings[before:]
191
+ if target is not None:
192
+ yield target
193
+
194
+
195
+ def _iter_targets(req: GrepRequest, compiler: _PatternCompiler) -> Iterator[_Target | GrepWarning]:
196
+ """Lazily resolve a request into the files to match, yielded as they're
197
+ discovered (so matching can begin before the walk finishes), interleaved with
198
+ each new per-language compile warning.
199
+
200
+ Single source of truth with the indexer: the same include/exclude patterns and
201
+ ``.gitignore`` rules decide which files belong to the project. Outside a project
202
+ we fall back to the default source-file patterns.
203
+ """
204
+ root = req.root.resolve()
205
+
206
+ if root.is_file():
207
+ project_root = find_project_root(root.parent)
208
+ yield from _resolve_file(
209
+ root, req.root.as_posix(), _ext_overrides(project_root), req, compiler
210
+ )
211
+ return
212
+
213
+ project_root = find_project_root(root)
214
+ if project_root is not None:
215
+ ps = load_project_settings(project_root)
216
+ included, excluded = ps.include_patterns, ps.exclude_patterns
217
+ ext_overrides = {f".{lo.ext}": lo.lang for lo in ps.language_overrides}
218
+ base = project_root
219
+ else:
220
+ included = list(DEFAULT_INCLUDED_PATTERNS)
221
+ excluded = list(DEFAULT_EXCLUDED_PATTERNS)
222
+ ext_overrides = {}
223
+ # Anchor at the enclosing git repo so grepping a subdirectory still honors the
224
+ # repo-root (and intervening) .gitignore; fall back to the target dir itself.
225
+ base = find_git_root(root) or root
226
+
227
+ matcher = build_matcher(base, included, excluded)
228
+ path_filter = (
229
+ PatternFilePathMatcher(included_patterns=[req.path_glob]) if req.path_glob else None
230
+ )
231
+
232
+ for abs_path, rel in iter_included_files(root, base, matcher):
233
+ if path_filter is not None and not path_filter.is_file_included(rel):
234
+ continue
235
+ # Display paths mirror the root the user gave (e.g. "src/a.py", "/tmp/x/a.py"),
236
+ # rather than always being cwd-relative.
237
+ display = (req.root / abs_path.relative_to(root)).as_posix()
238
+ yield from _resolve_file(abs_path, display, ext_overrides, req, compiler)
239
+
240
+
241
+ # ---------------------------------------------------------------------------
242
+ # Matching
243
+ # ---------------------------------------------------------------------------
244
+
245
+
246
+ def _match_file(target: _Target) -> FileMatches | GrepWarning | None:
247
+ """Match one file via code_match's ``match_file``: it reads the file and runs
248
+ the prefilter in Rust — skipping the parse (and the Python-side read) for files
249
+ that can't match — all with the GIL released, so a worker-thread pool scans many
250
+ files truly in parallel.
251
+
252
+ Returns ``None`` for a file that's binary, prefilter-rejected, or has no matches
253
+ (a silent skip, like ``grep -I``); a :class:`GrepWarning` for an unreadable file.
254
+ """
255
+ try:
256
+ fm = target.pattern.match_file(str(target.path))
257
+ except OSError as e:
258
+ return GrepWarning(f"cannot read {target.display}: {e}")
259
+ if fm is None:
260
+ return None # binary, prefilter-rejected, or no matches
261
+ matches = sorted(fm.matches, key=lambda m: m.chunks[0].start.byte_offset if m.chunks else 0)
262
+ return FileMatches(path=target.display, source=fm.ast.source, matches=matches)
263
+
264
+
265
+ class Grep:
266
+ """A single grep run. :meth:`run` matches each file as it's listed and streams
267
+ results as they complete. After ``run`` is exhausted, :attr:`unusable` /
268
+ :attr:`failed_languages` report the compile verdict."""
269
+
270
+ def __init__(self, req: GrepRequest) -> None:
271
+ self._req = req
272
+ self._compiler = _PatternCompiler(req.pattern)
273
+ self._target_count = 0
274
+
275
+ def run(self, emit: Callable[[FileMatches | GrepWarning], object]) -> None:
276
+ """Match the request, calling ``emit`` with each file's matches and each
277
+ compile warning the moment it's ready — *while the walk is still running*.
278
+
279
+ Fully synchronous, no event loop: the walk runs on the calling thread and
280
+ submits each file's match to a thread pool. ``match_file`` reads + prefilters
281
+ + parses + matches in Rust with the GIL released, so the pool threads run
282
+ truly in parallel — with the walk and with each other — and hand each result
283
+ straight to ``emit`` as it finishes. ``run`` returns once the walk and every
284
+ match are done; afterwards :attr:`unusable` / :attr:`failed_languages` hold the
285
+ compile verdict.
286
+
287
+ ``emit`` is called concurrently from the pool's worker threads (plus this
288
+ thread, for warnings), so a consumer that does I/O must serialize it itself.
289
+ """
290
+
291
+ def _match(item: _Target) -> None:
292
+ result = _match_file(item)
293
+ if result is not None: # None = binary / prefiltered / no match (skip)
294
+ emit(result)
295
+
296
+ with ThreadPoolExecutor() as pool:
297
+ for item in _iter_targets(self._req, self._compiler):
298
+ if isinstance(item, GrepWarning):
299
+ emit(item)
300
+ else:
301
+ self._target_count += 1
302
+ pool.submit(_match, item)
303
+ # ThreadPoolExecutor.__exit__ waits for every submitted match to finish.
304
+
305
+ @property
306
+ def failed_languages(self) -> list[str]:
307
+ """Supported languages the pattern would not compile for (valid once
308
+ :meth:`run` is exhausted)."""
309
+ return [w.language for w in self._compiler.warnings if w.language is not None]
310
+
311
+ @property
312
+ def unusable(self) -> bool:
313
+ """The pattern compiled for *none* of the languages actually encountered — a
314
+ supported language was found but every one rejected the pattern, so there was
315
+ nothing to match. A target exists only for a file whose language compiled, so
316
+ zero targets plus ≥1 failed language means unusable everywhere it was tried —
317
+ distinct from "no matchable files found" (no failures). Valid once
318
+ :meth:`run` is exhausted."""
319
+ return bool(self.failed_languages) and self._target_count == 0
320
+
321
+
322
+ # ---------------------------------------------------------------------------
323
+ # Rendering
324
+ # ---------------------------------------------------------------------------
325
+
326
+
327
+ def _line_char_offsets(source: str) -> list[int]:
328
+ """Char offset of the start of each line (index ``line - 1``)."""
329
+ offsets = [0]
330
+ for i, ch in enumerate(source):
331
+ if ch == "\n":
332
+ offsets.append(i + 1)
333
+ return offsets
334
+
335
+
336
+ def _paint(text: str, color: bool, **style: object) -> str:
337
+ if not color or not text:
338
+ return text
339
+ return click.style(text, **style) # type: ignore[arg-type]
340
+
341
+
342
+ def _render_code_line(
343
+ line_no: int,
344
+ text: str,
345
+ dim_pre_end: int,
346
+ dim_post_start: int,
347
+ width: int,
348
+ color: bool,
349
+ ) -> str:
350
+ """One code line: dimmed ``line_no| `` gutter, then the text with the leading
351
+ (before ``dim_pre_end``) and trailing (from ``dim_post_start``) context dimmed
352
+ and the matched span shown normally."""
353
+ pre = max(0, min(dim_pre_end, len(text)))
354
+ post = max(pre, min(dim_post_start, len(text)))
355
+ gutter = _paint(f"{line_no:>{width}}| ", color, fg="bright_black")
356
+ if not color:
357
+ return f"{gutter}{text}"
358
+ before = _paint(text[:pre], color, dim=True)
359
+ matched = text[pre:post]
360
+ after = _paint(text[post:], color, dim=True)
361
+ return f"{gutter}{before}{matched}{after}"
362
+
363
+
364
+ def _render_match(
365
+ src_lines: list[str],
366
+ offsets: list[int],
367
+ match: CodeMatch,
368
+ width: int,
369
+ color: bool,
370
+ ) -> list[str]:
371
+ chunk = match.chunks[0]
372
+ s_off, e_off = chunk.start.char_offset, chunk.end.char_offset
373
+ s_line, e_line = chunk.start.line, chunk.end.line
374
+ out: list[str] = []
375
+ for line_no in range(s_line, e_line + 1):
376
+ idx = line_no - 1
377
+ text = src_lines[idx] if 0 <= idx < len(src_lines) else ""
378
+ line_start = offsets[idx] if 0 <= idx < len(offsets) else 0
379
+ dim_pre_end = (s_off - line_start) if line_no == s_line else 0
380
+ dim_post_start = (e_off - line_start) if line_no == e_line else len(text)
381
+ out.append(_render_code_line(line_no, text, dim_pre_end, dim_post_start, width, color))
382
+ return out
383
+
384
+
385
+ def render_file(fm: FileMatches, *, color: bool) -> str:
386
+ """Render one file's matches: the path, then each match's line range, with
387
+ matches separated by a ``---`` line."""
388
+ # Split on "\n" to keep line numbers aligned with the offsets below (which
389
+ # count "\n"), then drop the trailing "\r" that CRLF files leave on each line.
390
+ src_lines = [line.rstrip("\r") for line in fm.source.split("\n")]
391
+ offsets = _line_char_offsets(fm.source)
392
+ max_line = max((m.chunks[0].end.line for m in fm.matches if m.chunks), default=1)
393
+ width = len(str(max_line))
394
+
395
+ parts = [_paint(fm.path, color, fg="magenta", bold=True)]
396
+ emitted = False
397
+ for match in fm.matches:
398
+ if not match.chunks:
399
+ continue
400
+ if emitted:
401
+ parts.append(_paint("---", color, fg="bright_black"))
402
+ parts.extend(_render_match(src_lines, offsets, match, width, color))
403
+ emitted = True
404
+ return "\n".join(parts)
405
+
406
+
407
+ def render_results(results: list[FileMatches], *, color: bool) -> str:
408
+ """Render a list of per-file matches in the ``ccc grep`` output format, files
409
+ separated by a blank line. The CLI streams with :func:`render_file` instead;
410
+ this is the batch form (used in tests)."""
411
+ return "\n\n".join(render_file(fm, color=color) for fm in results)
@@ -0,0 +1,123 @@
1
+ """CocoIndex app for indexing codebases."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import cocoindex as coco
8
+ from cocoindex.connectors import localfs, sqlite
9
+ from cocoindex.connectors.sqlite import Vec0TableDef
10
+ from cocoindex.ops.text import RecursiveSplitter, detect_code_language
11
+ from cocoindex.resources.chunk import Chunk
12
+ from cocoindex.resources.id import IdGenerator
13
+
14
+ from .chunking import CHUNKER_REGISTRY
15
+ from .file_walk import build_matcher
16
+ from .settings import load_project_settings
17
+ from .shared import (
18
+ CODEBASE_DIR,
19
+ EMBEDDER,
20
+ INDEXING_EMBED_PARAMS,
21
+ SQLITE_DB,
22
+ CodeChunk,
23
+ )
24
+
25
+ # Chunking configuration
26
+ CHUNK_SIZE = 1000
27
+ MIN_CHUNK_SIZE = 250
28
+ CHUNK_OVERLAP = 150
29
+
30
+ # Chunking splitter (stateless, can be module-level)
31
+ splitter = RecursiveSplitter()
32
+
33
+
34
+ @coco.fn(memo=True)
35
+ async def process_file(
36
+ file: localfs.File,
37
+ table: sqlite.TableTarget[CodeChunk],
38
+ ) -> None:
39
+ """Process a single file: chunk, embed, and store."""
40
+ embedder = coco.use_context(EMBEDDER)
41
+ indexing_params = coco.use_context(INDEXING_EMBED_PARAMS)
42
+
43
+ try:
44
+ content = await file.read_text()
45
+ except UnicodeDecodeError:
46
+ return
47
+
48
+ if not content.strip():
49
+ return
50
+
51
+ suffix = file.file_path.path.suffix
52
+ project_root = coco.use_context(CODEBASE_DIR)
53
+ ps = load_project_settings(project_root)
54
+ ext_lang_map = {f".{lo.ext}": lo.lang for lo in ps.language_overrides}
55
+ language = (
56
+ ext_lang_map.get(suffix)
57
+ or detect_code_language(filename=file.file_path.path.name)
58
+ or "text"
59
+ )
60
+
61
+ chunker_registry = coco.use_context(CHUNKER_REGISTRY)
62
+ chunker = chunker_registry.get(suffix)
63
+ if chunker is not None:
64
+ language_override, chunks = chunker(Path(file.file_path.path), content)
65
+ if language_override is not None:
66
+ language = language_override
67
+ else:
68
+ chunks = splitter.split(
69
+ content,
70
+ chunk_size=CHUNK_SIZE,
71
+ min_chunk_size=MIN_CHUNK_SIZE,
72
+ chunk_overlap=CHUNK_OVERLAP,
73
+ language=language,
74
+ )
75
+
76
+ id_gen = IdGenerator()
77
+
78
+ async def process(chunk: Chunk) -> None:
79
+ table.declare_row(
80
+ row=CodeChunk(
81
+ id=await id_gen.next_id(chunk.text),
82
+ file_path=file.file_path.path.as_posix(),
83
+ language=language,
84
+ content=chunk.text,
85
+ start_line=chunk.start.line,
86
+ end_line=chunk.end.line,
87
+ embedding=await embedder.embed(chunk.text, **indexing_params),
88
+ )
89
+ )
90
+
91
+ await coco.map(process, chunks)
92
+
93
+
94
+ @coco.fn
95
+ async def indexer_main() -> None:
96
+ """Main indexing function - walks files and processes each."""
97
+ project_root = coco.use_context(CODEBASE_DIR)
98
+ ps = load_project_settings(project_root)
99
+
100
+ table = await sqlite.mount_table_target(
101
+ db=SQLITE_DB,
102
+ table_name="code_chunks_vec",
103
+ table_schema=await sqlite.TableSchema.from_class(
104
+ CodeChunk,
105
+ primary_key=["id"],
106
+ ),
107
+ virtual_table_def=Vec0TableDef(
108
+ partition_key_columns=["language"],
109
+ auxiliary_columns=["file_path", "content", "start_line", "end_line"],
110
+ ),
111
+ )
112
+
113
+ matcher = build_matcher(project_root, ps.include_patterns, ps.exclude_patterns)
114
+
115
+ files = localfs.walk_dir(
116
+ CODEBASE_DIR,
117
+ recursive=True,
118
+ path_matcher=matcher,
119
+ )
120
+
121
+ await coco.mount_each(
122
+ coco.component_subpath(coco.Symbol("process_file")), process_file, files.items(), table
123
+ )
File without changes