Commit Graph
339 Commits
Author SHA1 Message Date
Kiril Klein, PhDandClaude Opus 4.8 df8bc9254d fix(mcp): trace_path cross_service follows CROSS_* edges (#522) (#538)
trace_path mode=cross_service resolved its default edge-type list to
{HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, CALLS} — none of the CROSS_* edge
types the cross-repo pass actually writes. Since cbm_store_bfs filters
edges with an exact `WHERE e.type IN (...)` clause, cross-service traces
silently skipped every cross hop even when the edge was present and
visible via query_graph.

Add the six CROSS_* types (CROSS_HTTP_CALLS, CROSS_ASYNC_CALLS,
CROSS_CHANNEL, CROSS_GRPC_CALLS, CROSS_GRAPHQL_CALLS, CROSS_TRPC_CALLS)
to the cross_service default list — exactly the set pass_cross_repo.c
emits and cross_repo_delete_edges() cleans up. The count is now computed
from the array size (sizeof) so it can't drift; the dead MCP_N_DEFAULTS_4
constant is removed. Tool description updated to match.

Adds integ_mcp_trace_path_cross_service: seeds a CROSS_HTTP_CALLS edge
between two indexed functions with no CALLS relationship and asserts
cross_service surfaces the hop while calls mode does not.

Signed-off-by: kirilklein <kikl@di.ku.dk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:05:18 +02:00
Alex 8ba286324c fix(pipeline): persist call-site line on CALLS edges (#508)
* fix(pipeline): persist call-site line on CALLS edges

Persist CBMCall.start_line as a "line" property on CALLS edges in both resolution paths (finalize_and_emit + calls_emit_edge), so call-site lines are queryable instead of needing grep. Scoped to CALLS only: route/config edge props feed full-only predump passes the incremental path does not re-run, so changing them desyncs full vs incremental indexing. Verified: full suite matches unpatched main; line present on CALLS both paths.

Fixes #503

Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>

* test(pipeline): assert CALLS edge carries call-site line

Indexes a Go fixture where Helper() is called on line 8 and asserts the Main->Helper CALLS edge persists a line property (line:8) in its JSON props.

Refs #503.

Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>

---------

Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
2026-06-22 22:05:09 +02:00
Shaurya 11009f2d94 fix(discover): honor .git/info/exclude during file discovery (#493)
* fix(discover): honor .git/info/exclude during file discovery

index_repository only loaded .gitignore when building the exclusion
matcher. .git/info/exclude — the per-clone exclude file that Git treats
as authoritative — was never read, so paths excluded only there were
walked in full. On repos with Sandcastle worktrees this caused the
indexer to traverse 661K files / 170 GB and OOM (issue #489).

Add cbm_gitignore_merge() to append patterns from one matcher into
another. In cbm_discover_ex, after loading .gitignore, also load
.git/info/exclude and merge its patterns in. If only the exclude file
exists, use it directly. No downstream call paths change.

Add three unit tests for cbm_gitignore_merge and two integration tests
that reproduce the issue scenario (exclude-only and exclude-stacked-
with-gitignore).

Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>

* style(discover): fix clang-format alignment in cbm_gitignore_merge

Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>

---------

Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
2026-06-22 22:04:42 +02:00
Matt Van Horn f5ea36cc64 fix: Reject filesystem paths in HTTP_CALLS route extraction so client routes can join server routes (#485)
Signed-off-by: mvanhorn <mvanhorn@gmail.com>
2026-06-22 22:04:31 +02:00
c8f9f94e08 fix(search): ignore empty-string label in search_graph name_pattern path (#483)
* fix(search): ignore empty-string label in search_graph name_pattern path

search_where_basic guarded the label filter on NULL only (`if (params->label)`),
so a search passing label="" appended a literal `n.label = ''` clause that
matches no node — silently returning zero results. The BM25 query path already
treats an empty label as absent, so name_pattern/qn_pattern searches and BM25
searches behaved inconsistently for the same `"label":""` payload (issue #481):
agents fell back to grep because structural class/service discovery returned
nothing.

Guard the clause on `params->label && params->label[0]` so an empty-string
label is a no-op, identical to an omitted label and to the BM25 path.

Adds store_search_empty_label_ignored: name_pattern + label="" returns the same
match as name_pattern alone, while a non-empty label still filters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* test(search): address QA round 1 — cover qn_pattern + empty label

QA round 1 noted store_search_empty_label_ignored asserted only the
name_pattern path though the comment referenced qn_pattern too. Add a
qn_pattern + label="" assertion so a future change that special-cased the
qn_pattern branch can't silently reintroduce the empty-label bug. Both paths
share search_where_basic, so this locks in the shared guard.

Test-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

---------

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:04:19 +02:00
cd660270cb fix(resolver): disambiguate qualified cross-file calls by full namespace tail (#479)
* fix(resolver): disambiguate qualified cross-file calls by full namespace tail

cbm_registry_resolve reduced a package/namespace-qualified callee
(Foo::Bar::sub) to its bare simple name before the multi-candidate name
lookup. When that bare name was defined in several packages, the bare-name
scorer (best_by_import_distance) picked one winner deterministically and
routed every caller to it — collapsing distinct packages' same-named subs
onto a single definition and orphaning the rest. Impact is heaviest for Perl,
which has no cross-file LSP and relies entirely on this generic resolver, but
the defect applies to any "::"- or "."-qualified callee.

Add qualified_suffix_match: when a callee is qualified and its bare name has
multiple candidates, pick the sole candidate whose qualified tail matches at a
segment boundary (normalizing "::" to "."). Returns the unique match with a new
high-confidence "qualified_suffix" strategy; falls through to the existing
bare-name scoring when zero or several candidates match. Language agnostic —
bare callees contain no separator and are unaffected.

Adds resolve_qualified_disambiguates_same_name covering per-package routing of
a name defined in three packages, plus the bare-call (no-disambiguation) case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* test(resolver): address QA round 1 (qualified-suffix edge coverage)

Harden resolve_qualified_disambiguates_same_name and add a new test per the
QA round 1 advisory notes, guarding the fall-through paths against silent
regression:
- dotted callee form (App.Beta.save) resolves via qualified_suffix
- qualified callee matching no candidate tail falls through (not qualified_suffix)
- resolve_qualified_ambiguous_tail_falls_through: two candidates sharing the
  same qualified tail are ambiguous → fall through to bare-name scoring

Test-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

---------

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:04:06 +02:00
92b4506cd5 fix(perl): eliminate false-positive Perl CALLS edges (builtins, framework method calls, config strings) (#477)
* fix(perl): stop extracting config strings as call targets

The Perl branch of extract_scripting_callee blindly returned the text of
child(0) of every call node. In config-heavy Perl (.cgi/.pl with embedded
log4perl-style config), tree-sitter-perl misparses dotted config tokens
(e.g. "log4perl.appender.File.utf8") into call-shaped nodes, and that
dotted string was emitted as a callee_name, later matched by the generic
short-name resolver to unrelated project subs.

Now the Perl branch pulls the real name token (method/function field, else
child(0)) and validates it as a bare Perl sub/method identifier via
perl_is_identifier_callee: must start with a letter or '_' and contain only
[A-Za-z0-9_:] (allowing the '::' package separator). Any '.', whitespace,
quote, or '/' disqualifies it and NULL is returned so no CALLS edge forms.
Gated to CBM_LANG_PERL; other languages are untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* fix(resolver): don't match Perl builtins to project subs

Perl builtins (push/shift/keys/sprintf/...) carry no language or call-kind
awareness through the generic name-matcher in cbm_registry_resolve. When a
project defines a sub whose name collides with a builtin, an invocation of
the builtin was wired to that sub by same-module / suffix matching - a
false-positive CALLS edge.

Adds cbm_perl_is_builtin (curated, sorted bsearch set of 94 perlfunc core
builtins) and applies it in both call-resolution passes (sequential
resolve_single_call and parallel resolve_file_calls), gated on the file
language == CBM_LANG_PERL and only AFTER LSP resolution has declined, so a
genuine LSP-resolved call is never suppressed. The file language is threaded
into both resolvers via a new trailing CBMLanguage parameter; every other
language reaches cbm_registry_resolve unchanged (byte-identical behavior).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* fix(resolver): suppress generic short-name matching for Perl method calls

A Perl method call ($obj->m / Class->m) carries no receiver type at the
structural tier, so the generic short-name matcher in cbm_registry_resolve
would wire $dbh->commit / $cgi->param / $logger->log to any project sub
sharing the bare method name - the dominant source of false-positive CALLS
edges in CPAN/framework-heavy Perl. Resolving such a call correctly is the
LSP's job, not the bare-name matcher's.

Adds a CBMCall.is_method flag (zero-init false, so all other languages and
existing call sites are unaffected). method_call_expression is added to the
Perl call node set and handle_calls sets is_method=true only for that node
type when the file language is Perl. Both call-resolution passes then skip
generic resolution for Perl method calls (combined with the builtin guard
from the prior commit). Genuine intra-project function calls (non-method,
non-builtin) still resolve as before. LSP-resolved method calls are
unaffected because the guard runs only after LSP resolution declines.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* test(perl): cover call-graph noise fixes (builtins, methods, config strings)

Hermetic tests for the three Perl call-graph noise fixes:

test_extraction.c (extraction tier):
  - config string is never emitted as a callee; genuine call still extracted
  - builtin calls (push/keys) extracted but never flagged is_method
  - arrow/method calls ($self->commit / $dbh->commit) set is_method=true,
    while the genuine function call (helper) does not
  - a JS method call never sets is_method (flag is Perl-only — other
    languages unaffected)

test_registry.c (resolver tier):
  - cbm_perl_is_builtin recognizes core builtins (incl. first/last of the
    sorted set) and rejects project subs, case variants, empty, and NULL

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* fix(perl): address QA round 1

Round 1 (Claude panel + DO DeepSeek) findings:
- Rework the Perl noise guard so it suppresses only WEAK generic matches
  (suffix_match/unique_name) and KEEPS high-confidence same_module/import_map.
  The prior guard ran before cbm_registry_resolve and dropped genuine same-file
  calls to builtin-named subs (e.g. a project sub log/index/open called as a
  bare function) that pre-PR resolved via same_module. Extracted the decision
  into pure, unit-tested cbm_perl_suppress_generic_match() shared by the
  sequential (pass_calls.c) and parallel (pass_parallel.c) resolvers; corrected
  the inaccurate comments (Perl has no LSP/textual stage before the guard).
- Tighten perl_is_identifier_callee to require '::' pairs (reject a lone ':',
  ':::', or trailing '::').
- Add resolver-contract tests covering weak-match suppression, same_module/
  import_map retention, genuine-call survival, non-Perl no-op, and NULL strategy.

Verified on a real Perl monorepo: .cgi builtin/CPAN/config-string noise stays
eliminated while same_module edges to builtin-named subs are recovered.

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

* fix(perl): address QA round 2

Round 2 (Claude panel) caught a regression introduced by the round-1 refactor:
cbm_perl_suppress_generic_match whitelisted only the exact strategies
"same_module" and "import_map", but resolve_import_map can also return
"import_map_suffix" (confidence 0.85 — a genuine import-based resolution, not a
weak short-name guess). A '::'-qualified Perl builtin/method call resolved via
the import-suffix fallback was therefore dropped, contradicting the helper's
documented contract and partially missing acceptance criterion (d).

Add import_map_suffix to the kept (high-confidence) set so only the weak
short-name strategies (suffix_match / unique_name) are suppressed; update the
doc comment and add a unit-test case asserting import_map_suffix is retained.

Deferred as advisory (non-blocking, noted on the PR): a hypothetical leading-'::'
(main:: shorthand) under-extraction in perl_is_identifier_callee, and a
colon-edge-case coverage gap (logic correct by inspection).

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

---------

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:03:53 +02:00
b63aa9dc39 fix(cli): copy and sign the built binary on install instead of re-signing the stale target (#473)
* fix(cli): copy and sign the built binary on install instead of re-signing the stale target

`install` (especially with --force) only ad-hoc-signed whatever already sat at
~/.local/bin/codebase-memory-mcp and never copied the binary the operator ran
install from. An operator who built from source and ran `install -y --force`
was silently left with the OLD binary at the target — running stale code while
believing they had upgraded (#472).

Install now mirrors `update`: detect the running binary, copy it to the
canonical target (skipping the copy when we already ARE the target, which would
otherwise truncate the running file), make it executable, ad-hoc sign the
TARGET, and point all agent configs at the target. A different pre-existing
binary is only replaced with --force or interactive confirmation; --dry-run
reports the intended swap without touching anything.

The copy+sign also resolves the secondary report: a hand-copied clang build is
linker-signed (flags=0x20002) and gets Killed:9 when spawned by an MCP host —
re-signing ad-hoc (flags=0x2) makes it launchable, which install now does for
the placed binary.

Adds cbm_copy_binary_to_target() (with a same-file guard) plus regression tests
covering the binary swap and the self-copy guard.

Closes #472

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* fix(cli): address QA round 1

Extend cli_install_same_file_guard_issue472 with a symlink case: two distinct
path strings resolving to the same inode (which a non-canonical
cbm_detect_self_path vs the hardcoded target can produce) must also be detected
as same-file and skipped, not truncated. POSIX-only (#ifndef _WIN32).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* test(cli): guard install copy exec-bit assertion to POSIX

cli_install_copies_binary_to_target_issue472 asserted the executable bit on the
copied target, but cbm_copy_binary_to_target only chmods on POSIX (the exec bit
is not meaningful on Windows, and MinGW stat() derives it from the file
extension). Wrap the stat/S_IXUSR assertion in #ifndef _WIN32 so test-windows
passes; the content and creation assertions still run on all platforms.

Refs #472

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

---------

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:03:37 +02:00
pcristin 1519f8619a feat(git): add worktree branch roots
Signed-off-by: pcristin <xxxokzxxx@protonmail.com>
2026-06-19 13:56:17 +02:00
Martin Vogel 3ee9d5d112 Drop the nim grammar and refresh language counts
Removes the heaviest vendored grammar (66 MB) along with its language
wiring and test fixtures; the supported-language count moves to 158
across README, site, and manifest. The grammar manifest additionally
records the canonical-source decisions for the five
registry-disagreement grammars and the per-directory license files
restored earlier this week.
2026-06-12 02:17:39 +02:00
Martin Vogel 630bd40a90 Replace the graph-UI HTTP server with a first-party implementation
The web server behind the graph UI is now a purpose-built in-house
module (src/ui/httpd.c): localhost-only listener, strict HTTP/1.1
parsing with fixed request caps, a per-connection receive deadline,
and Connection: close semantics. http_server.c keeps the routing and
handlers, rewritten against the new transport API; the public server
API and main.c are unchanged. The previously vendored third-party
server is removed entirely.

Comes with a new 28-test transport + routing suite (tests/test_httpd.c)
covering parsing edge cases (strict CRLF, Content-Length limits, raw
path matching, percent-decode rules) and live-socket behavior (CORS
policy, RPC dispatch, receive deadline, clean shutdown). The security
audit scripts now check the new file layout and treat any network call
in vendored code as a failure.
2026-06-12 02:17:25 +02:00
Martin Vogel 2b67abfd54 Probe the by-name index in import_map_suffix instead of scanning the whole registry
The import_map_suffix fallback ran cbm_ht_foreach over the ENTIRE exact-QN table (with a strlen per entry) for every call the direct candidate missed - on elasticsearch that is a 700k-entry scan per unresolved call site, and sampling showed it at 94% of resolve-phase CPU (cbm_registry_resolve -> cbm_ht_foreach -> ims_scan -> strlen). Any QN matching 'resolved.*.suffix' necessarily has the suffix's last segment as its own last segment, so the existing by_name index already holds the only possible candidates: probe it and prefix/tail-check the handful of entries. elasticsearch full index: 640 s -> 71 s, nodes identical, edges within 0.001% (iteration-order tiebreaks among equal-confidence candidates); strategy semantics covered by resolve_import_map_suffix and registry_confidence_import_map_suffix.
2026-06-11 15:06:50 +02:00
Martin Vogel 5706bf52f0 Extract url_path for the dump index via real JSON parsing
The dump path sliced url_path out of the properties string with strstr/strchr, returning ESCAPED bytes and cutting at embedded escaped quotes, while the index's backing column is GENERATED AS json_extract(properties,'$.url_path') and yields the unescaped value. The mismatch left edges 'missing from index idx_edges_url_path' under PRAGMA integrity_check once the formerly-malformed route props became valid JSON. url_path is now parsed with yyjson so the indexed value matches json_extract exactly.
2026-06-10 18:25:08 +02:00
Martin Vogel 8666a99e29 Close the remaining malformed edge-props producers
Three stragglers after 6c04ab7/7f78f93, found by edge json_valid audits on elasticsearch: (1) the parallel emit_route_registration twin still interpolated callee/route_path raw into CALLS props; (2) HANDLES props buffers were smaller than escaped handler QNs plus the JSON wrapper, so snprintf truncated the closing brace (escaping alone cannot prevent wrapper truncation - value buffers now 512B inside 1KB props); (3) DATA_FLOWS assembles sliced handler_params/args_json fragments that can break in ways escaping does not cover - the assembled props are now validated with yyjson and fall back to a minimal valid envelope. Verified by reindexing elasticsearch: edge json_valid violations 47 -> expected 0.
2026-06-10 17:07:10 +02:00
Martin Vogel 7f78f936ec Spill oversized index keys to overflow pages; escape remaining edge-props text
The hand-rolled dump writer stored index cells fully inline regardless of payload size. SQLite's format spills index payloads above the max-local threshold (16,422 bytes at the 64 KB page size) to overflow pages, so a long key written inline makes the reader interpret key bytes as an overflow page number: PRAGMA integrity_check reported 'invalid page number 0x43654C6C' (ASCII from the key text) in idx_nodes_name on elasticsearch (very long Section names), and name lookups on that index silently returned nothing. write_index_btree now rewrites oversized cells (varint + local prefix + overflow chain via write_overflow_pages) before page building; promoted separators carry their chains with them. Reproduce-first: sw_long_index_keys_overflow (20 KB name; integrity_check failed pre-fix, ok post-fix). Also escapes the remaining raw text interpolations in DATA_FLOWS route props and HANDLES handler QNs (the ~51-edge malformed tail after 6c04ab7).
2026-06-10 16:42:20 +02:00
Martin Vogel 6c04ab7f61 Escape sliced source text in edge properties JSON
Edge properties interpolated raw source-text slices into JSON via snprintf without escaping: decorator text (quotes + raw newlines, e.g. @register.tag("block") or multi-line @override_settings) in DECORATES props - django produced 3826 malformed edges - plus usage ref_name and route-registration callee/url in USAGE/CALLS props. Malformed edge JSON aborts every json_extract consumer, including the url_path_gen generated-column evaluation that runs during PRAGMA integrity_check. All such sites now route through cbm_json_escape (DECORATES twins in pass_parallel.c/pass_semantic.c, usage emit twins, route-registration calls). Regression: pipeline_edge_props_valid_json (register.tag fixture; real-repo red recorded on the django index).
2026-06-10 13:44:09 +02:00
Martin Vogel 359f37cd71 Escape control bytes in properties-JSON array items (raw newlines from multi-line C params)
The array appenders (param_types/param_names/decorators/base_classes) in both props-builder twins escaped only quote and backslash, so items sliced from multi-line C parameter declarations carried raw newline/tab bytes — invalid inside JSON strings (the remaining 118 malformed kernel rows after the truncation fix; e.g. param_types:["struct\n\t\t..."]). Array items now go through the full escape function, and both escapers degrade any other raw control byte (e.g. form feed) to a space. Reproduced via the prod CLI on a multi-line-param fixture (nl_fn json_valid=0 pre-fix); the sweep regression test gained that fixture (61st case).
2026-06-09 23:54:25 +02:00
Martin Vogel 9f465e5109 Apply atomic properties-JSON appends to the parallel pipeline twin
The parallel extraction path (taken above MIN_FILES_FOR_PARALLEL=50 files — i.e. every real repo) has its own copy of the props appenders in pass_parallel.c, so the previous fix in pass_definitions.c did not cover it: a kernel reindex still produced the 135 malformed rows and get_architecture's hotspots aspect returned empty (its json scan aborts on the first malformed row). Same atomic-append change as the serial twin. The reproduce-first sweep test now writes one function per file (60 files) to force the parallel pipeline; RED via pass_parallel pre-fix, GREEN now.
2026-06-09 23:30:51 +02:00
Martin Vogel 4b317c8779 Make definition-properties JSON appends atomic (no mid-value truncation)
build_def_props serialized into a fixed 2KB buffer and cut fields mid-value when full: the closing quote/brace got skipped, leaving malformed properties JSON (Linux kernel: 135 nodes, 50-param functions cut at 2047 bytes inside param_types — '["enum'). Malformed properties abort every json_extract()-based consumer (arch_entry_points, user Cypher on properties) mid-scan. Appends are now atomic: a field is emitted only when its whole serialized form fits (closing-brace reserve included); oversized optional fields are dropped whole and the JSON stays valid. Reproduce-first: pipeline_def_props_valid_json_when_oversized (param-count sweep deterministically hits the truncation window; RED 2047-byte malformed pre-fix, GREEN post-fix).
2026-06-09 22:59:33 +02:00
Martin Vogel 08115fdfb5 get_architecture: skip JSON property-key discovery (counts-only schema)
handle_get_architecture called cbm_store_get_schema, whose per-label/per-type property-key discovery runs json_each() over EVERY node and edge row (~10s of the 23s on a 4.8M-node kernel graph) — yet the handler only renders label/type counts. Added cbm_store_get_schema_counts (same impl, with_props=false) and switched the handler to it. The full get_graph_schema tool is unchanged. A partial expression index on is_entry_point was tried and reverted: json_extract in an index WHERE aborts store open on rows with malformed properties JSON (pre-existing in older DBs; see next commit).
2026-06-09 22:59:32 +02:00
Martin Vogel 66bf33a003 Cap name-resolution candidate lists at 256 (unresolvably ambiguous)
resolve_name_lookup walked the full by_name candidate array (reachability check + score per candidate, re-done per file via the per-file caches) for every cache-miss name. On identifier-dense repos this dominated usage resolution: the Linux kernel has 274 names with >256 registered definitions (list_head 7188, flags 5520, dev 4374, ...) and resolve_usages burned 987s CPU. candidate_count_penalty already floors confidence to ~3/count (<=0.006 at 256), so these matches were noise edges. Resolution now bails out as unresolved when a name has >256 candidates (same cap in fuzzy_resolve); same-module and import-map strategies still resolve such names exactly. Kernel re-measure (same files): resolve_usages CPU 987s->177s, parallel_resolve 134s->53s, wall 325s->251s, edges -485k noise (-5%); suite 5568/0.
2026-06-09 21:53:26 +02:00
Martin Vogel b904cf0ab5 Fix O(E*N) scan in arch_boundaries: binary-search the id->package map
get_architecture's arch_boundaries looked up the package of every CALLS edge endpoint with a linear scan over all Function/Method/Class nodes (lookup_pkg over parallel arrays) — O(edges * nodes). On the Linux kernel graph (~1.4M defs, ~1.4M CALLS edges) the boundaries aspect spun >10 minutes at 100% CPU; latent until C call extraction was fixed (the kernel previously produced almost no CALLS edges). The node query now orders by id and lookup_pkg binary-searches: 80k nodes/160k edges 16.6s -> 207ms in the regression test; kernel-graph get_architecture(all) >10min -> 23s. Reproduce-first: arch_boundaries_no_quadratic_scan (red 16566ms pre-fix vs 8000ms bound, green 207ms post-fix).
2026-06-09 21:32:16 +02:00
Martin Vogel 69c16cada4 fix: deliver the static -portable linux binary on all install/update paths
The standard linux release binary dynamically links glibc 2.38+ and
GLIBCXX_3.4.32, so it fails to start on Debian 11, Ubuntu 20.04/22.04,
RHEL/Rocky 8/9, Amazon Linux 2, etc. — yet install.sh, the npm and PyPI
wrappers, and the binary's own self-update all fetched it by default,
contradicting the "single static binary" promise.

Point every linux install + self-update path at the fully-static
"-portable" asset (gcc -static), which has no glibc floor. macOS/Windows
are unaffected and unchanged.

- install.sh, pkg/npm/install.js, pkg/pypi _cli.py: select -portable on linux
- src/cli/cli.c: self-update download URL AND checksum archive name both use
  -portable on linux (they must match or the update fails checksum verify)
- scripts/smoke-test.sh: assert linux self-update targets the -portable asset
- scripts/ci/check-glibc-compat.sh: new guard — runs the binary inside
  debian:bullseye (glibc 2.31) and asserts it starts
- _smoke.yml: run the guard on the portable binary in smoke-linux-portable

Reproduced: standard binary -> "GLIBC_2.38 not found" on glibc 2.31;
portable binary runs cleanly.
2026-06-09 11:36:00 +02:00
Martin Vogel 332bea6b42 Bind sqlite3 and libgit2 to mimalloc explicitly in prod build (#424 follow-up)
Defense-in-depth follow-up to the tree-sitter bind: route sqlite3 and libgit2
allocations through mimalloc explicitly so a correct binary never depends on the
fragile MI_OVERRIDE symbol override (where the Windows static-MinGW link can
resolve a library's malloc to mimalloc but its free to the CRT, corrupting the
heap). Prod-only, guarded by the existing CBM_BIND_TS_ALLOCATOR; the CRT+ASan
test build compiles these as no-ops and stays unchanged.

New cbm_alloc_init() consolidates all three binds behind a static idempotency
guard. main() calls it as its very first statement — before cbm_mcp_server_new
opens the in-memory store — because SQLITE_CONFIG_MALLOC must run before the
first sqlite3_open*/sqlite3_initialize, else sqlite3_config returns SQLITE_MISUSE
silently and the bind is ignored (the return code is asserted). cbm_init() also
calls cbm_alloc_init() so pipeline-pass entry points get the binds too; the
ts_set_allocator bind moved out of cbm_init into cbm_alloc_init.

sqlite3: a static sqlite3_mem_methods backed by mi_malloc/mi_free/mi_realloc,
xSize via mi_usable_size, xRoundup to an 8-byte boundary, xInit returning
SQLITE_OK, xShutdown a no-op (int-sized sqlite signatures wrapped with size_t
casts). libgit2: a static git_allocator (gmalloc/grealloc/gfree → mi_*) installed
via git_libgit2_opts(GIT_OPT_SET_ALLOCATOR). The allocator is set BEFORE any
git_libgit2_init: git_allocator_global_init keeps a custom allocator if one is
already set (git__allocator.gmalloc != git_failalloc_malloc), the pre-init global
is the fail-allocator, and there is no allocator reset on git_libgit2_shutdown —
so binding once at startup survives pass_githistory's per-call init/shutdown
pairs. libgit2 code is additionally guarded by HAVE_LIBGIT2.

Test build 5564/0 ASan-clean; prod build clean with CBM_BIND_TS_ALLOCATOR=1; the
HAVE_LIBGIT2 branch type-checked against the libgit2 sys/alloc.h struct shape;
lint-ci clean.
2026-06-09 11:25:56 +02:00
Martin Vogel 0314a216c6 Rename k8s enum PATH_MAX to avoid system-macro collision on macOS-15/Windows
PATH_MAX is defined by limits.h on the macOS-15 SDK (1024) and MSYS2 clang64 (260), so 'enum { PATH_MAX = 12 }' expanded to 'enum { 1024 = 12 }' → 'expected identifier' build failure on those platforms (Linux/macOS-14 didn't pull the macro into this TU). Renamed to K8S_PATH_DEPTH.
2026-06-09 00:59:19 +02:00
Martin Vogel 632af52f26 Make namespace import resolution deterministic across platforms
The namespace map kept only the first file per package (by platform-dependent iteration order). A same-package wildcard import (import com.example.* from a file itself in com.example) could resolve to the importer → self-edge suppressed → 0 IMPORTS on amd64 but 1 on arm64 (ei_kotlin_wildcard_import). Now store ALL files per namespace as a delimited list and resolve to the first non-importer sibling — deterministic on amd64/arm64. Re-key via the stored key pointer to avoid a use-after-free.
2026-06-09 00:01:36 +02:00
Martin Vogel 6f3b24df4f Fix CI lint: drop debug scaffolding, make lisp import walker iterative, apply clang-format
Remove leftover getenv(CBM_DBG_*) debug fprintf blocks in extract_calls.c/pass_calls.c (cppcheck always-true). Convert parse_lisp_imports to a stack-based walk (no recursion → no NOLINT, avoids deep-nesting overflow). Apply Homebrew clang-format to all touched sources. Local suite 5562/0, lint-ci clean.
2026-06-08 22:55:23 +02:00
Martin Vogel b30fe13de1 Emit INFRA_MAPS for HCL scheduler + k8s selectors, ASYNC_CALLS for SQS struct args
extract_calls: recover SQS/SNS queue identity from Go composite-literal struct fields (QueueUrl/TopicArn) so emit_http_async_edge forms ASYNC_CALLS. extract_unified: HCL value/body descent + cloud_scheduler_job source synthesis. pipeline: self-create topic Route for standalone-manifest infra bindings + run infra-route passes in the sequential pipeline. pass_k8s: cross-manifest Service-selector -> workload label/name matching -> INFRA_MAPS.
2026-06-08 22:55:23 +02:00
Martin Vogel de2c54ac1f Emit DECORATES to synthetic nodes for external decorators + python subscript-dunder CALLS
pass_semantic + pass_parallel: when a decorator name resolves to no local def, upsert a synthetic '<decorator:NAME>' node (label Decorator) and emit DECORATES+USAGE — fixes rust #[derive]/#[allow], swift @discardableResult, scala @deprecated (deduped per project; pipeline-level so extraction histograms unaffected). py_lsp: inject a synthetic CBMCall for resolved subscript/operator dunders on typed receivers so they reach the CALLS pipeline — fixes python s[0]->Seq.__getitem__.
2026-06-08 22:55:23 +02:00
Martin Vogel 35ac60c544 Resolve C# field writes + Kotlin cross-file calls
lang_specs: C# postfix/prefix_unary_expression as assignments (x++ -> WRITES). extract_semantic: resolve_write_lhs_node for unary inc/dec write targets. helpers: dedicated kotlin_keywords (Kotlin doesn't reserve double/int/... so fun double()/call double() now extract). kotlin_lsp + pass_lsp_cross: cbm_run_kotlin_lsp_cross + sole-definer top-level fallback. java_lsp: sole-definer type fallback for cross-file static calls. Fixes convergence 571, edge_structural 374, lsp_resolution 1210.
2026-06-08 22:55:23 +02:00
Martin Vogel 0d87a86e54 Resolve IMPORTS edges for 12 grammar-only languages
extract_imports: recursive lisp walker (Clojure ns/:require, Common Lisp defpackage/:use, Fennel nested require) + parsers for elm/move/smali(descriptor-demangle)/tlaplus(EXTENDS)/vhdl(use work)/wit/smithy(#->.)/hyprlang(source). lang_specs: astro frontmatter JS re-parse for component imports. pass_pkgmap: basename sibling-file candidate for prefixed include paths. All sound resolution via namespace-map / sibling / symbol-fallback / relative-import.
2026-06-08 22:55:23 +02:00
Martin Vogel 791b768c34 Emit IMPLEMENTS (not INHERITS) when a base resolves to an Interface
pass_semantic process_def: choose edge type by the resolved base node's label — Interface -> IMPLEMENTS (Java implements, C# : IFace, TS implements), Class/Type/Enum -> INHERITS. Fixes es_implements_samefile_java/csharp.
2026-06-08 17:46:32 +02:00
Martin Vogel 65ee5ef7b3 Add HANDLES for Spring/ASP.NET/Laravel routes + Guzzle/RestSharp HTTP_CALLS
extract_defs: Java/Spring + C#/ASP.NET attribute-based route mapping → HANDLES. extract_calls: accept Laravel string/array handler forms; preserve PHP scoped-call namespace QN so Guzzle HTTP_CALLS pattern matches. pass_calls: route C# client calls through service-pattern matching (RestSharp).
2026-06-08 17:24:40 +02:00
Martin Vogel 3c4e7b4eac Extract DECORATES for C#/PHP8/Scala/TS attributes + TS type-position USAGE
find_jvm_modifiers handles attribute_list (C#/PHP8); extract_decorators scans direct children (Scala annotation) and skips anon tokens (TS @Decorator before export); pass_semantic normalizes attribute syntaxes + C# Name->NameAttribute. USAGE: export_statement wrapping a declaration no longer suppresses type-annotation usages. py_lsp: operator/subscript dunder-call resolution for typed receivers.
2026-06-08 16:16:34 +02:00
Martin Vogel 861b9d917e Resolve grammar-only IMPORTS edges for sibling-file imports
pass_pkgmap.c: sibling-file resolution (direct, SCSS _partial, meson subdir/meson.build). language.c: map .just/.inc extensions. extract_imports.c: strip quotes from path imports (pony/func), parse_meson_imports (subdir()), generalized lisp parser for emacslisp/fennel/commonlisp node kinds. Fixes scss/just/bitbake/func/pony/emacslisp/meson import edges.
2026-06-08 14:24:39 +02:00
Martin Vogel 6afb45427c Add grammar-only import parsers + pipeline edges (WRITES/CONFIGURES/DEPENDS_ON)
Imports: per-grammar parsers for hare/pascal/powershell/scheme/racket/starlark/tcl/teal/zsh + css/html/cmake/bitbake/kconfig/gn/just/nix/jsonnet/pkl/nickel/thrift/capnp/d/tablegen/crystal/fsharp/ada; node-extraction for D/PowerShell/Pascal/Julia/F#; INHERITS for those. Pipeline edges: register Variable/Field defs for WRITES/READS resolution; CONFIGURES pass consuming env_accesses; DEPENDS_ON from go.mod/requirements.txt; SQS dot-form QN. Golden updates: julia +Class, just +Function (correct new extraction).
2026-06-08 13:04:12 +02:00
Martin Vogel 7275be3062 Add IMPORTS extraction+edge resolution and Go interface IMPLEMENTS/OVERRIDE
Imports: per-language extraction (PHP namespace_use, C# alias/static, Python wildcard/__future__, ES re-export) + generic import_node_types consumption for grammar-only langs; shared cross-pass import resolver (module-QN, namespace-map, symbol-name fallback, Rust crate-relative) in pass_pkgmap.c creates IMPORTS edges for all 9 hybrid langs + many grammar-only. Go: match interface methods via parent_class DEFINES_METHOD edges so IMPLEMENTS/OVERRIDE resolve. Kotlin throws via jump_expression detection.
2026-06-08 11:07:34 +02:00
Martin Vogel 2d772b01bf Fix extraction/resolution across languages: routing, Go methods, cross-LSP, inheritance, constructors, node kinds
WIP checkpoint (local). File-index routing for dotenv/gitattributes/regex; Go receiver-method parent_class; cross-LSP dispatch + :: QN split (Rust/Java/C#/Kotlin); base_classes for TS/PHP/Kotlin/Python + Java/C++ qualified; new T() constructors (java/c#/php/scala/ruby/ts); per-grammar node extraction for ~17 struct/record/type grammars. Test corrections: cpp qualified-base :: guards, gitignore ignore-rules contract, julia struct golden.
2026-06-07 22:56:12 +02:00
Martin Vogel 586729bdfe Resolve package.json workspaces cross-package IMPORTS on Windows (#408)
cbm_pkgmap_scan_repo was a no-op on Windows and pkgmap_walk_dir was POSIX-only (raw opendir/readdir), so package.json manifests were never read on Windows -> the pkgmap was empty -> bare @org/pkg workspace imports never resolved (cross-package IMPORTS = 0). The walk was disabled on Windows because a naive recursive descent hung on directory-junction cycles. Port it to the portable cbm_opendir/cbm_readdir API (as discover.c), guarantee termination with a hard PKGMAP_WALK_MAX_DEPTH=64 recursion bound (cannot hang regardless of junctions/symlink cycles), skip Windows reparse points and POSIX symlinks before descending, and remove the Windows gates. Verified: full macOS suite 4705 passed / 0 failed (contract_edge_workspaces_imports_issue408 now resolves on the POSIX path; Windows verified by CI).
2026-06-07 00:00:11 +02:00
Martin Vogel 118c327ff7 Fix cppcheck findings in distilled log + excluded summary (#411, #413)
log.c: check the buffer bound before reading raw[i] (arrayIndexThenCheck). mcp.c: compare excluded_count to INDEX_EXCLUDED_DIR_CAP directly for the 'truncated' flag (equivalent to >shown, but avoids cppcheck's knownConditionTrueFalse).
2026-06-06 22:59:48 +02:00
Martin Vogel bd77424648 Apply clang-format to the distilled watchdog in main.c (#406) 2026-06-06 22:53:58 +02:00
Martin Vogel de1d58ab9e Distill PR #407 (#406) and PR #414 (#413): parent-death watchdog + CBM_LOG_LEVEL
Two reviewed external-fork contributions, re-implemented as clean maintainer code.

#406 / PR #407 (thanks @nvt-pankajsharma): a POSIX parent-death watchdog so the stdio MCP server exits when its launching parent dies instead of lingering on stdin. Refactors signal_handler into an idempotent request_shutdown(); a watchdog thread polls getppid() (500ms) and shuts down + exit(0) once the initial ppid (>1) changes; Windows unaffected (#ifndef _WIN32). The fork's getppid()<=1 startup early-exit was dropped (it could wrongly kill a legitimately-launched server during reparent races / in container launchers; the initial_ppid>1 guard already no-ops safely), and thread-create failure is now non-fatal (matches the watcher/HTTP background-thread policy). Adds tests/test_parent_watchdog.sh + scripts/test.sh wiring.

#413 / PR #414 (thanks @santanusinha): runtime log-level control via CBM_LOG_LEVEL (case-insensitive debug|info|warn|error|none, or numeric 0..4; unset/unknown leaves the level unchanged). getenv() before threads start (no race). Adds tests/test_log.c cases + README env-var row.
2026-06-06 22:30:38 +02:00
Martin Vogel ba52d6ba56 Report excluded subtrees in the index_repository result (#411)
index_repository silently dropped entire subtrees (ALWAYS_SKIP dirs in all modes; FAST_SKIP dirs like tools/scripts/bin in moderate/fast) with no record — users couldn't tell what wasn't indexed. The discover walk now records skipped directory rel-paths; cbm_discover_ex returns them; the pipeline holds + exposes them; and the index response carries a compact summary: "excluded":{"dirs":[up to 25],"count":N,"truncated":bool}. Behavior-neutral for indexing. Adds a reproduction in test_integration.c.
2026-06-06 22:16:04 +02:00
Martin Vogel a2c9804397 Make the watcher git helpers work under Windows cmd.exe
The watcher shells out to git via cbm_popen, which runs through cmd.exe on Windows. The commands used POSIX-only constructs: single-quoted paths ('%s'), which cmd.exe does NOT strip — so git received a literal-quoted path, failed with 'cannot find the path', and the watcher fell back to strategy=none (no change detection); plus /dev/null and '| wc -l'. Use double-quoted paths (stripped by cmd.exe and POSIX sh alike), a platform null device (NUL / dev/null), count ls-files lines in C, and gate the submodule-foreach enhancement (whose inner shell command cmd.exe cannot pass) to POSIX. The watcher's git change-detection now works on Windows; this is what the (newly un-skipped) watcher tests exercise.
2026-06-06 19:05:40 +02:00
Martin Vogel c993c8cf84 Normalize cbm_mkdtemp output to forward slashes on Windows
The Windows cbm_mkdtemp built paths with backslashes (%TEMP%\\template). Tests embed that path in the JSON repo_path (where \\t/\\a are invalid escapes, so index_repository fails) and pass it to git -C. Convert to forward slashes — accepted by Windows file APIs, JSON, and git — fixing integration_setup, incremental, and the lang contracts on Windows.
2026-06-06 14:57:29 +02:00
Martin Vogel d8d114747c Free the thread-local node-type cache to satisfy LeakSanitizer
cbm_kind_in_set builds a thread-local cache of calloc'd symbol bitsets (ks_cache) that was never freed, so LeakSanitizer (Linux x64) reported the per-worker-thread caches as leaks once the previously-empty grammars gained real node-type sets. Add cbm_kind_in_set_free_cache() and call it at worker-thread teardown (extract_worker, beside cbm_slab_destroy_thread) and main-thread exit (test runner, beside sqlite3_shutdown).
2026-06-06 12:34:59 +02:00
Martin Vogel 8a5c992bd2 Satisfy lint for the new extraction recursion helpers
Whitelist find_first_descendant_by_kind and find_first_descendant_of in recursion_whitelist.h (bounded AST-depth DFS, like the existing r_collect_imports entry), and narrow two cppcheck-flagged variable scopes in sqlite_writer.c: use w->project at its single use, and fclose the value-copied wc.fp after free(w) instead of a pre-captured fp.
2026-06-06 01:26:06 +02:00
Martin Vogel 863bb70c94 Apply clang-format-20 across flagged sources
The CI lint (clang-format-20) flagged formatting violations across these files (pre-existing drift plus the new extraction code). Reformat to satisfy the check; no logic changes.
2026-06-06 00:20:47 +02:00
Martin Vogel 42dc17ead5 Add a star request to the update-available notice
Append a short "leave a star" prompt with the repo URL to the startup
update notice, so users who are behind a release see it on the first
tool call alongside the upgrade hint. Fits the existing 256-byte notice
buffer.
2026-06-05 18:48:30 +02:00
Martin Vogel 7b67cf9a22 Add memory back-pressure to parallel extraction
The extraction workers parse files concurrently; with the file list
sorted largest-first, they can collectively exceed the RSS budget on
very large repositories. Add a back-pressure gate at the top of the
worker loop: when over budget, a worker reclaims and naps (bounded
spins) before pulling another file so peers can finish and return
memory. Self-disabling when the budget is unset or RSS is under budget,
so small repos and the test suite are unaffected.

Also add per-phase RSS logging (mem.phase) at the registry / cross-LSP /
resolve boundaries for memory profiling.
2026-06-04 10:16:34 +02:00