Commit Graph
439 Commits
Author SHA1 Message Date
Martin VogelandGen Li 87091edd24 fix(extract): receiver-aware self-recursion detection
The recursion detector flagged any call whose short name matched the
enclosing def as self-recursion, so super().save() inside save,
axios.get() inside get, and console.error() inside error were all
false positives feeding is_recursive, recursion_in_loop and
unguarded_recursion.

Add is_self_receiver() and AND it into the short-name comparison: a
qualified callee only counts when its whole receiver chain (everything
before the last '.') names the same object — self/this/cls/@self, or
the enclosing def's own receiver identifier parsed from
CBMDefinition.receiver (Go: s in 'func (s *Store) save()'). Bare names
keep the prior behavior. Matching the full chain keeps self.obj.recur()
(a field's same-named method) out, and the dynamic receiver whitelist
keeps Go s.save() detected.

Distilled from PR #699 with two corrections: the receiver chain is
matched via the last dot instead of the first segment (self.obj.recur
was still a false positive), and the enclosing-receiver whitelist so
Go method self-recursion is not lost.

Closes #599

Co-authored-by: Gen Li <lg320531124@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-03 20:20:49 +02:00
Martin Vogel 09148ab121 Merge pull request #780 from SS-42/fix/adr-path-normalization
Normalize project paths for ADR lookups
2026-07-03 18:38:18 +02:00
Martin Vogel 62594c6bd8 Merge pull request #772 from jonghyunlee-29cm/fix/spring-kotlin-route-prefix
fix(extract): compose Spring controller route prefixes
2026-07-03 18:36:54 +02:00
Martin Vogel eaf2ca58b0 Merge pull request #771 from apappas1129/fix/767-imports-import-targetable-label
fix(pipeline): reject non-file-like IMPORTS targets in fallback strategies
2026-07-03 18:36:50 +02:00
Martin Vogel 6a6c7a98ca Merge pull request #766 from apappas1129/fix/730-tsconfig-alias-dotdot-climb
fix(pipeline): resolve ../ climbs in tsconfig path alias targets
2026-07-03 18:36:45 +02:00
Martin Vogel fa12a38c22 Merge pull request #757 from SyntaxSawdust/codex/issue-731-vs-schema
fix(mcp): tighten ingest traces schema for strict clients
2026-07-03 18:36:20 +02:00
Martin Vogel 3a1387edc7 Merge pull request #774 from minorityre/codex/sync-mcp-server-version
Sync MCP server version with binary version
2026-07-03 18:36:16 +02:00
Martin Vogel 23e4bce030 Merge pull request #537 from Golevka2001/main
fix: unwatch deleted projects to prevent zombie reindex
2026-07-03 18:35:39 +02:00
Martin Vogel b97d814788 Merge pull request #781 from SS-42/fix/parent-watchdog-startup
Exit promptly after parent death
2026-07-03 18:35:35 +02:00
jonghyun.lee a19fe033ff fix(extract): compose Spring controller route prefixes
Spring class-level RequestMapping prefixes were not folded into method mapping route_path values, so the graph could contain partial Route nodes like /orders instead of the actual endpoint /api/orders even while HANDLES edges existed.

Combine Java/Kotlin class mapping prefixes with method-level mappings during method definition extraction, and scan bounded annotation-argument subtrees so named or array-valued Spring annotations can provide the path literal.

Strengthen edge_types_probe to assert exact Spring Route node names for Java and Kotlin, not just HANDLES edge counts.

Signed-off-by: jonghyun.lee <jonghyun.lee@musinsa.com>
2026-07-03 10:08:01 +09:00
Martin Vogel 9cb3cabf76 Merge pull request #779 from DeusData/feat/lsp-walk-depth-caps
fix(lsp): cap resolve-walker recursion depth in py/go/php/kotlin (Stage 2/B1)
2026-07-03 00:12:45 +02:00
Martin Vogel e89aa776a7 feat(index): report per-file indexing failures via skipped[] + logfile (Stage 2/B2-B4)
index_repository silently dropped files that failed to index: CBMFileResult.has_error
was set but never read, oversized files (>100 MB) were dropped with no signal, and
read/extract failures only bumped a logged-only counter — a file that couldn't be
indexed just vanished from the graph.

Collect and surface them:
- New retained {path, reason, phase} error list on struct cbm_pipeline (mirrors the
  excluded_dirs pattern) + accessor + a back-pointer on cbm_pipeline_ctx_t so both
  extraction paths can append. Wired in BOTH the sequential (pass_definitions) and
  parallel (per-worker, merged lock-free) paths — small repos take the sequential
  path, so wiring only one would leave the guard vacuous.
- Feeds: read-fail, extract-fail, the newly-CONSUMED has_error (parse timeout /
  parse failed, with error_msg), and oversized. The cross_lsp phase is reserved for
  the crash supervisor (Track C) and not fed here (the cross-LSP passes are
  best-effort with no failure return; feeding the no-source case would be false
  positives).
- MCP/CLI response gains top-level "skipped_count" (0 on clean) and, when >0, a
  capped "skipped":{files[<=50],count,truncated} + "logfile". Status stays "indexed"
  — a reported skip is the expected, handled outcome, not a degradation.
- Per-run logfile (full uncapped list) written ONLY when >=1 file is skipped:
  $CBM_INDEX_LOG override else <cache_dir>/logs/<project>-<epoch>.log.
- Generous env-configurable caps (src/foundation/limits.c): CBM_MAX_FILE_BYTES,
  default raised 100 MB -> 512 MiB; over-cap files are REPORTED (phase oversized) +
  WARNed, never silently dropped.

Reproduce-first: tests/test_index_resilience.c (gating) — an oversized file (cap
lowered via env) must appear in skipped[] with the 2 good files still indexed and a
logfile written; a clean run has skipped_count 0 and no logfile. Genuine guard:
no-op'ing the recorder makes the oversized file silently vanish (RED). Full suite
5750/0, no ASan/UBSan.

Part of the resilient-indexing effort (Track B surfacing layer). Refs #668.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 22:11:00 +02:00
SS-42 273a3af008 fix(mcp): normalize project paths for ADR lookups
Signed-off-by: SS-42 <noreply@incogni.to>
2026-07-02 22:27:49 +03:00
SS-42 f607c0fc32 fix(main): exit promptly after parent death
Signed-off-by: SS-42 <noreply@incogni.to>
2026-07-02 21:43:07 +03:00
Martin Vogel f47e8b5bf5 fix(lsp): cap resolve-walker recursion depth in py/go/php/kotlin (Stage 2/B1)
The per-language "resolve calls in AST node" walkers recurse once per nesting
level. Four of them — Python (py_resolve_calls_in), Go (resolve_calls_in_node),
PHP (php_resolve_calls_in_node), Kotlin (kt_resolve_calls_in_node) — had no depth
guard, so a deeply-nested or cyclic file could drive them into a native stack
overflow (SIGSEGV) that takes down the whole index run. C and Java already guard
this (C_LSP_MAX_WALK_DEPTH / JAVA_LSP_MAX_WALK_DEPTH).

Add the same wrapper/inner depth guard to all four, backed by a shared,
env-configurable cap: CBM_LSP_MAX_WALK_DEPTH (default 512) in scope.h. Past the
cap the wrapper skips the subtree — those calls stay unresolved (graceful
degradation, not a crash). The walk_depth-- runs after the inner body returns, so
early returns can't leak the counter.

Reproduce-first: lsp_{python,go,php,kotlin}_deep_nesting_no_crash (30000-deep
nested calls, run under a forked child + WIFSIGNALED). RED without the cap (the
child SIGSEGVs — verified by raising CBM_LSP_MAX_WALK_DEPTH to 100M), GREEN with
it. Full suite 5752/0, no ASan/leak.

Part of the resilient-indexing effort (Track B prevention layer). Refs #668.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 20:15:11 +02:00
Martin Vogel 174e56b447 fix(extract): heap-allocate walk_defs frame stack — fix overflow + >4096 truncation (#668)
walk_defs used a fixed `walk_defs_frame_t stack[4096]` — a single ~160 KB frame on
the C stack. Two problems:
- On small thread stacks (notably the pre-2026-03 Windows 1 MB main thread) that
  huge frame overflowed the stack during the definitions pass — the 0xC00000FD
  crash a reporter hit indexing a large SQL file (#668). Reproduced locally by
  running extraction under a constrained stack (~208 KB) + ASan.
- The `top < 4096` push guards SILENTLY DROPPED every top-level definition past
  4096, so a file with more than 4096 top-level defs was only partially indexed.

Replace the fixed array with a growable heap stack (wd_stack_t + wd_push): a
256-frame initial buffer that doubles on demand, bounded by a generous,
env-configurable ceiling (CBM_WALK_DEFS_MAX, default 8M frames) that WARNs once
instead of dropping, and returns cleanly on realloc OOM (no NULL deref). A normal
file now uses ~10 KB of heap instead of 160 KB of stack, and files with thousands
of top-level defs are fully extracted. The frame stack is a pure index-addressed
LIFO (frames copied by value on pop), so heap-backing is semantically identical.

Reproduce-first: walk_defs_no_truncation_over_4096_issue668 — 5000 top-level defs
must all be extracted (RED on the old 4096 cap, GREEN after). Full suite 5748/0,
no ASan/UBSan reports on the new alloc.

Root cause was pinned by reproduction (constrained-stack + ASan), refuting earlier
grammar/scanner hypotheses — extraction and the vendored tree-sitter tree ops are
otherwise iterative. The separate error-recovery assertion mode
(symbol<token_count) was not locally reproducible and is addressed by the
forthcoming general crash-containment layer.

Refs #668

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 18:39:17 +02:00
armand 6c56b8fd91 Sync MCP server version with binary version
Signed-off-by: armand <armandhell123@gmail.com>
2026-07-02 16:40:25 +02:00
Alexandros Pappas fe2b072d02 fix(pipeline): reject non-file-like IMPORTS targets in fallback strategies
A wildcard tsconfig path alias (e.g. "@lib/*" -> "./src/lib/*") matches
any import specifier sharing that prefix, including an unrelated scoped
npm package meant to resolve from node_modules. The direct match
already failed cleanly (no node exists at the substituted path), but
cbm_pipeline_resolve_import_node's Strategy 4 - a generic "strip a
trailing path segment and retry" fallback applied to every language,
not just its intended Rust crate::/self::/super:: case - re-resolves
the truncated import string and can land on the tsconfig's OTHER, bare
alias entry that maps straight to a directory. The resulting QN
collides with that directory's real Folder node (cbm_pipeline_fqn_module
and cbm_pipeline_fqn_folder both dot-join path segments with no type
tag), and Strategy 4 accepted whatever node it found there.

Strategy 3 (symbol-name fallback) already guards its matches with
import_targetable_label(), which excludes Folder/Project/etc. Strategy
1b (resolve_sibling_file, used only for markup/build-tool sibling
imports - SCSS/Just/BitBake/Meson/Pony, none of which are directory-
module languages) and Strategy 4 lacked the same guard; both now use
it too.

Strategy 1 (the primary, direct resolution path) intentionally keeps
no such filter: Go and Java are directory-module languages in this
pipeline (cbm_lang_module_is_dir), so a bare package import correctly
and deliberately resolves straight to a Folder node there - adding the
guard to Strategy 1 broke that (verified via the Go/Java integration
tests) before being scoped down to just the fallback strategies where
the collision actually originates.

Closes #767.

Signed-off-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
2026-07-02 21:20:22 +08:00
Alexandros Pappas 70377035ae Merge branch 'main' into fix/730-tsconfig-alias-dotdot-climb 2026-07-02 19:51:36 +08:00
Martin Vogel b9b7905309 Merge pull request #749 from DeusData/distill/672-git-canonical-root
fix(git): correct canonical_root for worktrees and subdirectory projects
2026-07-02 12:33:01 +02:00
Alexandros Pappas 8cc1d1e9fa fix(pipeline): resolve ../ climbs in tsconfig path alias targets
tsconfig.json `paths` entries that climb out of their config's directory
(e.g. a tsconfig at apps/web/tsconfig.json mapping "@shared/*" to
"../../packages/shared/src/*", the standard monorepo layout) resolved to
a broken path: resolve_target_relative() only stripped a single leading
"./" and otherwise concatenated dir_prefix and target verbatim, leaving
literal ".." segments in the result. Those never match a real module's
FQN, since cbm_pipeline_fqn_module tokenizes on '/' without collapsing
them - so every cross-package import through such an alias looked like
dead code.

resolve_target_relative now walks the target's path segments and pops
a directory off dir_prefix for each "..", the same normalization
cbm_pipeline_resolve_relative_import already does for plain relative
imports.

Closes #730.

Signed-off-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
2026-07-02 17:41:22 +08:00
Dustin Persek 540ca5f5c9 fix(mcp): tighten ingest traces schema
Signed-off-by: Dustin Persek <dustin.persek@protonmail.com>
2026-07-01 22:57:50 -04:00
Martin Vogelandlg320531124 3dd24719b0 fix(search): scoped search must handle repo paths containing spaces
search_code's scoped grep writes an "<root>/<file>" filelist and pipes it to
xargs. With a newline-separated filelist + plain `xargs`, a space anywhere in
the absolute path splits one file into several bogus arguments, so grep finds
nothing and scoped search returns 0 matches for any repo indexed under a path
containing a space (#687).

Fix: write NUL-separated records and consume them with `xargs -0`, so each path
stays a single argument regardless of spaces. On Windows the scoped path uses
PowerShell `Get-Content | Select-String -LiteralPath`, which reads line-by-line
and already handles spaces, so the filelist keeps newline separators there.

write_scoped_filelist writes each record piece-by-piece with fwrite/fputc (no
fixed-size stack buffer), so arbitrarily long absolute paths cannot overflow.

Reproduce-first: search_code_scoped_path_with_spaces_issue687 indexes a repo
rooted at "<tmp>/my project" and asserts total_grep_matches > 0 (RED on the old
newline + plain-xargs code -> 0 matches; GREEN after). Full suite green.

Distilled from #693 (thanks @lg320531124). Keeps that PR's correct NUL/xargs-0
idea and its Windows newline branch, but replaces its char buf[2048] + unbounded
memcpy (a stack buffer overflow reachable via long indexed paths, PATH_MAX 4096)
with the direct piece-by-piece write, and adds the reproduce-first guard it
lacked.

Closes #687

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: lg320531124 <lg320531124@users.noreply.github.com>
2026-07-02 01:24:29 +02:00
Martin Vogel abda9641b4 Merge pull request #748 from DeusData/distill/650-trace-ambiguity
fix(mcp): trace_path must not conflate distinct same-named functions
2026-07-02 00:48:59 +02:00
Martin Vogel 722ace679f fix(test): guard git_context git helpers behind #ifndef _WIN32
On Windows every git_context test SKIP_PLATFORMs, leaving git_run/make_git_repo
as unused static functions -> -Wunused-function under -Werror failed the Windows
build. Guard the two helpers (only called from the non-Windows test bodies) so
the file compiles clean on Windows while the tests still run on Linux/macOS.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-02 00:37:52 +02:00
Martin Vogel 46c11ff189 Merge pull request #683 from Naam/fix/worktree-info-exclude
fix(discover): honor info/exclude and core.excludesFile in linked worktrees
2026-07-02 00:31:29 +02:00
Martin Vogelandanivaryam 354befe557 fix(git): correct canonical_root for worktrees and subdirectory projects
detect_changes returned an empty impacted_symbols set for projects indexed
inside a git worktree or from a repo subdirectory (#659): canonical_root was
computed wrong. `git rev-parse --git-common-dir` emits a path relative to the
directory passed via -C (input_path), NOT to worktree_root, and the "/.git"
suffix was stripped textually without resolving ".." components. So e.g.
"../.git" joined against worktree_root, or "/ws/scripts/../.git" strip, left an
unresolved ".." and never matched the project's stored root path.

Fix (src/git/git_context.c): join the relative --git-common-dir against
input_path (the -C dir) instead of worktree_root, and realpath()/_fullpath()-
normalize the result BEFORE stripping "/.git" (the .git dir always exists for a
valid repo, so resolution succeeds). This is consistent with the existing
realpath'd root_path used for prefix matching (mcp.c).

Reproduce-first: tests/test_git_context.c. The subdirectory case is the genuine
guard — a repo indexed from a subdir yields a *relative* --git-common-dir, so the
unfixed code leaves an un-normalized "<root>/subdir/.." (verified RED; GREEN after
the realpath fix). The linked-worktree case is a supporting invariant, not the
#659 reproducer: git that emits an *absolute* worktree --git-common-dir (e.g.
2.48.x) doesn't manifest the bug, so it passes either way; it still enforces the
worktree->main-root invariant. All three git-based tests SKIP_PLATFORM on Windows
(the CI shell there cannot init a repo via system()).

Distilled from #672 (thanks @anivaryam); that PR's production fix is taken
verbatim. Its repo_root/subdir tests ran git unconditionally and failed the
Windows CI leg ("failed to init git repo"); here all three git-based tests are
Windows-skipped so ci-ok is green cross-platform. Supersedes #696, which only
realpath'd after the strip without changing the join base and so still yielded
the workspace parent for the subdirectory case.

Closes #659

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: anivaryam <anivaryam.dev@gmail.com>
2026-07-02 00:19:16 +02:00
Martin Vogel f42c06651d Merge pull request #746 from DeusData/distill/647-drf-viewset-actions
feat(extract): Route+HANDLES for Django REST Framework @action methods
2026-07-01 23:59:52 +02:00
Martin VogelandMatt Van Horn 7d6d52b9d7 fix(mcp): trace_path must not conflate distinct same-named functions
The #546 fix (bfs_union_same_name) BFSs from every graph node sharing the
resolved name and merges the results, so a symbol split across a real
implementation and a body-less ambient .d.ts stub reports a complete caller
set. But pick_resolved_node flagged ambiguity only on an exact score tie, and
node_resolution_score adds the body span, so two genuinely-DIFFERENT functions
that share a name but differ in body length score differently, dodge the tie,
and get their caller sets unioned into one confidently-conflated answer -- the
mirror of the #546 under-report.

Complete the ambiguity rule: two or more *real* callable definitions sharing a
name (callable label + non-empty body span) are ambiguous and routed to the
existing disambiguation (snippet_suggestions) path. A body-less span==start node
is an ambient declaration / signature stub -- a fragment of one logical symbol
-- so a real implementation plus its .d.ts stub stays non-ambiguous and still
unions, preserving the #546 behavior.

Reproduce-first: tool_trace_call_path_distinct_defs_not_over_unioned (two
unrelated same-named functions with different spans -> merged caller set on the
old code = RED; ambiguous on the fix = GREEN) plus
tool_trace_call_path_dts_stub_unions_with_impl guarding that the stub+impl union
still works. Full suite green.

Distilled from #650 (thanks @mvanhorn); its trace_-prefixed reimplementation
collided with main's already-landed bfs_union_same_name and could not merge.

Refs #546, #650

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-01 23:37:56 +02:00
Martin Vogelandmuskiteer 024c0c5f76 feat(extract): add Route+HANDLES for Django REST Framework @action methods
Django REST Framework ViewSets expose extra endpoints through @action-decorated
methods, e.g. @action(detail=True, methods=["post"], url_path="approve"). On
main these methods carried no route_path, so Phase 2a created no Route node and
no HANDLES edge for them -- the endpoint was invisible in the graph (#603).

Add try_drf_action_decorator (internal/cbm/extract_defs.c): when a method's
decorator identifier is "action", read the methods / url_path / detail kwargs
and set *out_path/*out_method. The existing decorator-route pipeline
(ensure_one_decorator_route, Phase 2a) then creates the Route + handler->Route
HANDLES edge in the established direction. Fallbacks: method name for url_path,
"GET" for methods; detail=True -> /{pk}/<seg>, else /<seg>.

Reproduce-first guard: handles_drf_action_python in the edge_types_probe suite
-- a DRF @action method must produce >=1 HANDLES edge (RED with 0 edges before
the extractor, GREEN after).

Known limitation: multi-method actions (methods=["get","post"]) capture only the
first method -> a single Route.

Distilled from #647 (thanks @muskiteer). Drops that PR's unrelated project-wide
HANDLES edge-direction reversal, which was unnecessary for the fix and broke the
handler->Route convention relied on by pass_cross_repo.c's find_route_handler.

Closes #603

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: muskiteer <agarwal.vansh3110@gmail.com>
2026-07-01 22:49:01 +02:00
Martin VogelandMatt Van Horn db20e53459 fix(fqn): cap derived project-name length to avoid ENAMETOOLONG
cbm_project_name_from_path hex-encodes non-ASCII path bytes (#571), but had no
length cap -- a deep CJK / non-ASCII path triples in length past the
filesystem's 255-byte filename-component limit, so <cache>/<name>.db becomes
un-openable (ENAMETOOLONG).

Cap the derived name at FQN_MAX_NAME_LEN (200): names within the cap are
returned UNCHANGED (no drift for already-indexed projects), while a longer name
keeps its first (CAP-9) bytes plus an FNV-1a hash of the full name as a "-%08x"
suffix, so distinct long paths stay distinct. The hex encoding scheme is
unchanged.

Distilled from #624 by Matt Van Horn (the ENAMETOOLONG catch); the rest of that
PR was redundant with the #571 fix already on main and would have swapped the
encoding scheme, causing project-name drift.

Reproduce-first: project_name_length_capped_issue624 builds a deep CJK path
(~376-byte raw name) and asserts the result is <= 200, validator-safe, and that
two long paths differing only in the trailing segment map to different names; a
short CJK path stays byte-identical (no drift). RED (376 > 200) -> GREEN. Full
suite 5738/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-Authored-By: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-07-01 21:23:12 +02:00
Martin Vogel 7cf1e4d213 Merge pull request #619 from catsmonster/fix/hook-augment-windows-paths
fix(hook): make hook-augment work on Windows drive-letter paths
2026-07-01 20:59:52 +02:00
catsmonster 4bb9effe4c fix(hook): make hook-augment work on Windows drive-letter paths
hook-augment's PreToolUse Grep/Glob augmenter required POSIX-style
'/'-prefixed absolute paths in two guards, so a Windows cwd (C:\repo or
C:/repo) always failed: the _WIN32 cwd check returned early, and the
walk-up loop guard (dir[0] == '/') never iterated. The result was a
structural no-op on Windows — zero additionalContext for every Grep/Glob,
even for an exactly-indexed symbol.

Add cbm_hook_path_is_abs(), accepting POSIX "/..." and Windows "X:/..." roots;
normalize backslashes to forward slashes on the Windows cwd; and stop the
parent-climb at the drive root as well as the POSIX root. POSIX behavior
is unchanged.

Adds a cli_hook_augment_path_is_abs regression test (the predicate is
exposed as cbm_hook_path_is_abs via cli.h) covering POSIX and Windows
drive roots — it fails on the pre-fix POSIX-only guard and passes after.

Verified with that test plus a standalone walk self-test (full path
resolves first, then climbs to one level below the drive root, symmetric
to POSIX); clang-format clean on all three files; clang -fsyntax-only on
the translation unit. Not built through the full toolchain locally (no
make/zlib) — relying on Windows CI to confirm the build.

Fixes #618

Signed-off-by: catsmonster <shaked.brand@gmail.com>
2026-07-01 06:19:30 -06:00
Martin Vogel d4c0b6bdbc fix(mcp): keep available_projects error JSON valid when many projects (#235)
collect_db_project_names built the "project not found or not indexed" error's
available_projects list into a fixed 4 KB buffer and, on overflow, let snprintf
truncate the last name mid-token then clamped offset to the buffer end -- so the
error response was malformed, unterminated JSON (e.g. ...,"tmp-cb],"count":50})
whenever enough projects were indexed to overflow the buffer. A client parsing
the error would fail. Found while exercising the CLI against a cache with 280
indexed projects.

Write element-boundary instead: before each name, require the whole element
(optional leading comma + the quoted name + NUL) to fit in the remaining buffer;
otherwise stop at that boundary. The array then always holds only complete names
and count == its length, so the JSON is always valid (just fewer names when the
buffer is full -- callers can run list_projects for the complete set).

Reproduce-first: tool_bad_project_error_valid_json_issue235 builds 50 dbs with
~120-char internal names (well over 4 KB), triggers the error, and asserts the
response parses as JSON and available_projects is an array with length == count.
RED before (yyjson_read returns NULL on the truncated buffer), GREEN after. The
existing tool_bad_project_name_no_overflow_issue235 (buffer bounds) still passes.
Full suite 5736/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-01 12:58:32 +02:00
Martin Vogel 4a422856e3 Merge pull request #719 from DeusData/fix/readonly-query-open
fix(store): open query stores READONLY so queries never mutate dbs
2026-06-30 18:54:07 +02:00
Martin Vogel ad422bc59a Merge pull request #717 from DeusData/fix/704-store-resolution
fix(mcp): resolve projects by internal name + filter ghost dbs (#704)
2026-06-30 18:53:45 +02:00
Martin Vogel c11e659047 Merge pull request #716 from DeusData/fix/680-cli-flags
feat(cli): accept flags, stdin, and --args-file for tools + per-tool --help (#680)
2026-06-30 18:52:46 +02:00
Martin Vogel 8ec7523a30 fix(store): open query stores READONLY so queries never mutate dbs
Query tools opened the project db with SQLITE_OPEN_READWRITE and ran write
pragmas (PRAGMA journal_mode=WAL + wal_checkpoint), so every query MUTATED the
db (rewrote the header, created -wal/-shm) and FAILED outright when the db or
its directory was read-only (the WAL pragma cannot create -shm on a read-only
filesystem -> resolve_store returned NULL -> "project not found"). Latent
data-integrity bug found while diagnosing #704 (a query grew a 0-byte shadow
db to 100 KB).

- cbm_store_open_path_query now opens SQLITE_OPEN_READONLY. configure_pragmas
  gains a read_only mode that runs only the non-writing pragmas (foreign_keys,
  temp_store, busy_timeout, mmap_size) and skips journal_mode / wal_checkpoint
  / synchronous. The read-write open (cbm_store_open_path) is unchanged.
- Read-only-filesystem fallback: SQLite opens lazily, so a RO-FS failure
  surfaces on first access; we probe with SELECT 1 and, only if the file
  exists, retry once with a `file:<path>?immutable=1` URI (URI-special
  characters percent-encoded) which bypasses WAL and reads the main db
  directly. The fallback is gated to that failure path so writable-fs readers
  still see uncheckpointed WAL frames via -shm.
- manage_adr is the only resolve_store caller that writes; it now opens its own
  read-write handle (cbm_store_open_path) for ADR storage (in-memory/embedded
  stores still write through the resolved store). All 12 resolve_store callers
  audited; query_graph is read-only (the Cypher engine rejects writes).

Reproduce-first (non-vacuous, no real read-only mount needed):
- readonly_query_does_not_mutate_db: snapshot a rollback-journal db's bytes,
  run search_graph, assert the file is byte-identical + no -wal created. RED
  before (the WAL pragma rewrote the header).
- readonly_query_succeeds_on_readonly_fs: chmod the containing DIRECTORY 0555
  (the real read-only-fs symptom -- a 0444 file alone is vacuous since SQLite
  downgrades the open), run search_graph, assert it succeeds. RED before (WAL
  pragma could not create -shm -> "project not found"); exercises the immutable
  fallback.
Full suite 5724/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 16:48:40 +02:00
Martin Vogel 1d1772a24f fix(mcp): resolve projects by internal name + filter ghost dbs (#704)
Project resolution was filename-addressed but internal-name-validated:
resolve_store opened <cache>/<passed>.db and required the internal
projects.name row to equal the passed name, while list_projects advertised
the .db FILENAME. When a db's filename differed from its internal name (a
copied/renamed db, or a legacy '.'-vs-'-' username twin from a past path
sanitization), it showed in list_projects but every query returned "project
not found or not indexed". Node rows are keyed on the internal name, so just
opening the file is not enough -- queries must use the internal name too. The
intermittency ("works now, fails 10 min later") was a warm in-process store
cache masking a broken cold resolve; a fresh cli call is always cold.

Key list + resolve on the INTERNAL project name, with no file renames or
mutation:
- list_projects / collect_db_project_names / build_project_json_entry now
  advertise each db's internal projects.name (via db_internal_project_name)
  and skip ghost/empty/corrupt dbs (0-byte, no project row) so the listed
  names are exactly the ones that resolve.
- resolve_store keeps the fast path (open <passed>.db, get_project -- the
  common case where filename == internal name). On a miss it runs a bounded
  cache-dir scan, adopting the db whose sole internal project name equals the
  passed name (gated on exactly one project row so it never serves another
  project's data). No match -> NULL (a typo stays not-found). The scan runs
  only on the fallback.

Reproduce-first: tool_resolve_store_by_internal_name_issue704 builds an
isolated CBM_CACHE_DIR with a control db, a drifted db (filename != internal
name), and a 0-byte ghost; asserts list advertises the internal name (not the
filename), the drifted project resolves via the scan, and the ghost is neither
listed nor resolvable. RED (113/1, list lacked the internal name) -> GREEN
(114/0). Full suite 5723/0.

Also hardened tool_bad_project_name_no_overflow_issue235: its fixture used
1-byte non-SQLite files that the new ghost filter would skip, making the
buffer-overflow guard vacuous; rebuilt it with valid dbs carrying long
internal names so the guard still fires.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 16:11:07 +02:00
Martin Vogel 3c2c12006f feat(cli): accept flags, stdin, and --args-file for tools + per-tool --help (#680)
The CLI only accepted a single inline JSON argument (cbm cli <tool> '<json>'),
which PowerShell mangles on Windows and which made `cli <tool> --help` return
"repo_path is required" -- the "--help" token was parsed as the JSON args, and
there was no per-tool help at all.

run_cli now resolves tool arguments by precedence: --args-file <path>, a raw
JSON object (back-compat), --flag / --flag=value / bare-boolean flags, piped
stdin, then empty {}. Flags are converted to a JSON object via the tool's
input_schema: kebab-case keys map to snake_case, values are typed
(string/integer/boolean), and repeated flags accumulate into array-typed
properties (--semantic-query a --semantic-query b -> ["a","b"]). stdin and
--args-file are UTF-8-clean and bypass shell quoting entirely (the robust path
on Windows). `cli <tool> --help` prints usage + the tool's flags, generated
from the same input_schema the MCP tools/list advertises -- no hand-maintained
table.

cbm_mcp_tool_input_schema() exposes a tool's schema; cbm_cli_build_args_json()
and cbm_cli_print_tool_help() do the conversion and help.

Reproduce-first: 8 cli tests (string / integer-not-string / bare-boolean /
repeated-array / kebab->snake / --key=value with spaces / bad-positional-errors
/ help) -- RED on a stub, GREEN after. Full suite 5730/0.

Note: true CJK-path support on Windows still needs a UTF-8/wmain entry point
(argv is ANSI-codepage-mangled upstream of parsing); stdin/--args-file mitigate
it and are tracked separately.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 15:44:36 +02:00
Martin Vogel 8b56c52231 fix(mcp): accept project_name alias + clearer no-store error (#640)
Query tools resolved the project only via the exact "project" key, but
list_projects surfaces the field as "name" and the not-found hint said
"pass the project name", so callers naturally pass "project_name". With no
alias the arg was NULL and resolve_store bailed before opening any .db, so
every query returned "project not found or not indexed" even though the
project was indexed -- the reporter's strace shows the cache dir scanned but
no .db opened. Reproduced on macOS; not Linux-specific.

Add get_project_arg() accepting project | project_name | project_id |
projectName across the 11 query handlers (not bare "name" -- index_repository
uses it for an explicit project-name override). Split the no-store error so a
missing argument reports "missing required argument: project" instead of
masquerading as "not indexed", and name the literal "project" key in the
not-found hint.

Reproduce-first: tool_get_architecture_accepts_project_name_alias_issue640
and tool_search_graph_accepts_project_name_alias_issue640 (RED before ->
GREEN after). Full suite 5724/0, no regressions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 12:51:49 +02:00
Shane McCarron 5255939a95 fix(install): address QA round 1
QA round 1 (PR #632) flagged that the SubagentStart hook uses matcher "*",
the same matcher a user is most likely to pick for their own catch-all
SubagentStart hook. Because is_cmm_hook_entry keyed ownership on the matcher
string alone, install would remove the user's "*" entry and replace it with
ours, and uninstall could remove the wrong one.

Add an optional `match_command_substr` to the upsert/remove args, threaded into
is_cmm_hook_entry: when set, an entry must ALSO carry a hooks[].command
containing that substring to be claimed as ours. The Claude SubagentStart hook
passes "cbm-subagent-reminder"; all existing callers leave it NULL, preserving
their matcher-only behavior unchanged. Install/uninstall now only ever touch
CMM's own entry and never clobber a foreign "*" hook.

Add cli_claude_subagent_hook_preserves_user_entry covering the case: a
pre-existing user "*" SubagentStart hook survives both install and uninstall.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Shane McCarron 3466ba02e8 test(install): cover SubagentStart hook registration
Verify the Claude SubagentStart reminder: install writes a SubagentStart
entry with a match-all matcher pointing at cbm-subagent-reminder, a second
upsert is idempotent (no duplicate entry), and removal leaves no SubagentStart
key behind.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Nahim El Atmani f1b93829dc fix(discover): honor info/exclude and excludesfile in linked worktrees
In a linked worktree, <repo>/.git is a regular file ("gitdir: <path>")
rather than a directory. cbm_discover_ex gated all per-clone ignore
sources on .git being a directory, so inside a worktree it skipped
.git/info/exclude and the global core.excludesfile entirely and walked
into directories the user had excluded there (e.g. build/, twister-out/).
Committed .gitignore still applied via the issue #510 path, which is why
the gap was easy to miss.

Resolve the git common dir up front: when .git is a gitlink file, parse
'gitdir:', then follow <gitdir>/commondir to the shared dir where git
keeps info/exclude and config (falling back to the gitdir when no
commondir file is present). info/exclude and config are then read from
that common dir, so a worktree behaves like an ordinary checkout.

Adds discover_worktree_info_exclude (fails before this change) and
discover_worktree_committed_gitignore.

Signed-off-by: Nahim El Atmani <2959826+Naam@users.noreply.github.com>
2026-06-28 21:05:07 -07:00
Martin Vogel d10701dced fix(lsp): seal shared Tier-2 cross-registry against O(n^2) resolve hang
The per-file C cross-LSP resolver mutated the finalized, project-wide cross
registry that is shared read-only across the parallel resolve workers. Each
post-finalize add landed in a tail the hash index does not cover, so every
lookup linear-scanned an ever-growing tail -> O(files*defs) on large C
codebases. The Linux-kernel full index hung at "[4/9] Resolving" on 11 cores
for >6 min and never finished, plus a heap data race across workers.

Seal every shared cross-registry: CBMTypeRegistry gains a read_only flag, set
by all five {c,py,cs,ts,go}_build_cross_registry builders right after
finalize, and cbm_registry_add_func/_type no-op on a sealed registry. One
chokepoint guards every language, robust to any resolver mutation site. The
plain-C function-registration site is also skipped directly when the registry
is shared.

Add six cross-language invariant tests (c, c++, py, c#, ts, go): resolving
against a finalized shared registry must leave func_count/type_count
unchanged -- RED before the fix (C +1, C++ +2 post-finalize adds), GREEN
after.

Verified: full suite 5720 passed / 0 failed; Linux-kernel full index now
completes all 9 phases in 643s (4.88M nodes, 11.9M edges) instead of hanging
at phase 4.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-29 01:33:40 +02:00
Martin Vogel 4ab00df63f Merge pull request #657 from DeusData/feat/mcp-protocol-2025-11-25
feat(mcp): add structured tool metadata
2026-06-28 22:08:57 +02:00
Martin Vogel b7f1a71a6b style: apply clang-format to new mcp/ui/logging lines
The CI lint gate (scripts/lint.sh --ci, clang-format-20) flagged 7 files whose
new lines were not run through the project formatter. Reformat only those lines
(line wrapping / continuation alignment) — no behavior change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:49:17 +02:00
Martin Vogel 345425a1bb merge main into mcp protocol branch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:34:18 +02:00
Martin Vogel aedb979f55 Merge pull request #673 from DeusData/qa/bug-repro-suite
qa: Python-builtin + kotlin.Any LSP fixes; false-guard audit (honest #581 known-red)
2026-06-28 21:29:03 +02:00
Martin Vogel d5414111b3 feat(ui): add i18n and index picker UX
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:21:32 +02:00