Commit Graph
284 Commits
Author SHA1 Message Date
Martin Vogel d87cffe168 feat(arch): surface Leiden community clusters in get_architecture
Wire the Leiden detector into get_architecture via a new 'clusters' aspect.
arch_clusters loads Function/Method/Class nodes + CALLS edges (capped for very
large graphs), runs cbm_leiden, and reports the top-N communities compactly —
each with a dominant-package label, member count, cohesion (internal vs boundary
edge ratio), representative top nodes (by degree), and the packages spanned.
Singletons are skipped to avoid noise. The mcp.c serializer already emitted the
clusters array; it is now populated.

Tests: arch_clusters_basic (two cliques + a bridge -> >=2 connected clusters).
Generous smoke coverage added for the full Cypher surface (labels/type/id/keys/
properties/size/reverse/replace/left/coalesce/substring, NOT EXISTS dead-code,
CASE, unsupported-function error) plus the get_architecture clusters aspect.
2026-06-01 22:36:50 +02:00
Martin Vogel 08b62f03b9 feat(cypher): bounded EXISTS { } pattern predicate in WHERE
Add a single-hop, anchored existence predicate:
  WHERE NOT EXISTS { (f)<-[:CALLS]-() }   -- functions with no caller
  WHERE EXISTS { (f)-[:CALLS]->() }       -- functions that call something

Parsed via parse_exists_predicate (reusing parse_node/parse_rel) into a leaf
condition (op=EXISTS, anchor variable, edge type, direction); evaluated against
the bound node with cbm_store_find_edges_by_source_type / _by_target_type. This
is edge-type-specific, so it finds true orphans that in_degree/out_degree miss
(e.g. a node with only a DEFINES edge but no CALLS caller). Multi-hop / nested-
WHERE EXISTS is intentionally unsupported and errors clearly.

Tests cover the dead-code (NOT EXISTS) and has-outgoing (EXISTS) cases; smoke
coverage added. ASan-clean.

Refs: Cypher read suite (EXISTS predicate)
2026-06-01 21:17:56 +02:00
Martin Vogel c160465118 feat(cypher): multi-argument scalar functions (suite tier 2b)
Add an args-list (cbm_func_arg_t) to return items and parse comma-separated
arguments (var.prop or string/number literals), enabling multi-argument scalar
functions in projections:

- coalesce(a, b, ...)        first non-empty value
- substring(s, start[, len]) 0-indexed substring
- replace(s, from, to)       replace all occurrences
- left(s, n) / right(s, n)   leading/trailing n chars

Arguments are freed in free_return_clause (ASan-clean). Unit test
cypher_func_multiarg plus smoke coverage for substring + coalesce.

Refs: Cypher read suite (tier 2b)
2026-06-01 21:04:25 +02:00
Martin Vogel 67a7334e60 feat(cypher): string functions + fail loudly on unsupported syntax
Add single-argument string functions to projections — size, length, trim,
ltrim, rtrim, reverse — via the generic function recognizer.

More importantly, change the engine to FAIL LOUDLY on unsupported syntax instead
of silently projecting an empty column. An unknown function call (e.g.
split(...), coalesce(...)) or list indexing/slicing in RETURN/WITH now returns a
clear "unsupported function '<name>' (supported: ...)" error rather than a
valid-looking but blank result — the same silent-empty failure mode that hid the
labels() bug. This supersedes the #373 graceful-resync behaviour (its test is
flipped to assert the error; a new test covers an unknown function in RETURN).

Decision: full Cypher Tier 3 (lists/maps/paths/comprehensions/params — a value
data-model rewrite) is intentionally NOT implemented; clear errors on
unsupported features are the higher-value, lower-risk completeness win for the
read subset agents actually use.

Refs: Cypher read suite (tiers 1-2a + unsupported-syntax hardening)
2026-06-01 20:47:06 +02:00
Martin Vogel 71a6c5714e feat(cypher): scalar + entity-introspection functions (suite tier 1)
Add single-argument functions to RETURN/WITH projections, dispatched via a
generic IDENT-call recognizer so new functions no longer need a dedicated lexer
keyword:

- labels(n)      -> ["<label>"]
- type(r)        -> relationship type
- id(n)          -> node/edge identity
- keys(n)        -> JSON list of the node's non-null property keys
- properties(n)  -> the node/edge properties JSON object
- toInteger/toFloat/toBoolean(x) -> numeric/boolean casts (null on non-numeric)

Previously labels()/id()/keys()/etc. fell into the unknown-function path and
silently projected empty (labels(n) returned ""); they now evaluate correctly.
Regex =~ in WHERE was already supported. Unit tests for each function plus
smoke-test coverage via the live query_graph CLI.

Refs: full Cypher read suite (tier 1 of 3)
2026-06-01 20:28:53 +02:00
Martin Vogel 1b308ec864 feat(helm): Chart.yaml dependencies -> DEPENDS_ON edges + values.yaml top-level keys
Two more Helm semantic improvements for #338:

- Chart.yaml: parse the top-level name and the dependencies: list
  (cbm_parse_helm_chart) and, in the k8s pass, emit a Chart node per chart plus
  a DEPENDS_ON edge to a shared, per-project deduplicated Chart node for each
  declared dependency. So "which charts depend on postgresql" is now traversable.
- values.yaml: extract only the top-level keys as structured Variables instead
  of one node per nested leaf key — killing the node flood the issue described
  (86 nodes for a typical values.yaml down to a handful).

Generic-YAML one-node-per-file dedup (repo-wide behavior change) is intentionally
left as a separate follow-up. Tests cover the dependency parser (with/without
deps) and the values.yaml top-level-only extraction.

Closes #338
2026-06-01 19:43:15 +02:00
Martin Vogel a8fc901129 feat(helm): extract Helm/Go-template named templates and include calls
Helm chart helpers (.tpl) and Go templates produced no semantic graph — just a
flat key flood. Wire the existing gotemplate grammar for real structure:

- .tpl files map to the gotemplate language (Helm _helpers.tpl).
- {{ define "chart.fullname" }} becomes a Function node named after the
  template (the name is a string literal, not an identifier, so it's resolved
  in a dedicated walker branch).
- {{ include "x" . }} and {{ template "x" . }} resolve to the referenced
  template name instead of the bare `include` builtin, so they form CALLS
  edges to the define'd Function. template_action is added to the call set.

Lets you trace which charts include which named templates. Tests cover define
-> Function and include/template -> CALLS, plus the .tpl mapping.

Refs #338
2026-06-01 18:41:49 +02:00
Martin Vogel 1e1d40851c feat(lang): add CFML / ColdFusion support (cfscript + tag dialects)
Vendor the MIT-licensed cfmleditor/tree-sitter-cfml grammars and wire both
CFML dialects through the pipeline:

- .cfc components -> cfscript grammar (CBM_LANG_CFSCRIPT): a JS-like grammar, so
  function/method declarations, calls and imports extract with the shared JS/TS
  arrays. Components' functions become Function nodes.
- .cfm templates -> cfml tag grammar (CBM_LANG_CFML): tag-based (HTML-derived).
  Embedded <cfscript> functions extract via function_declaration; tag-based
  <cffunction name="..."> nodes are handled by a dedicated walker branch that
  reads the name attribute (cf_function_tag has no name field). Shared scanner
  headers are vendored at vendored/common/ so the grammar's ../../common/
  includes resolve without editing vendored sources.

Adds an ASCII case-insensitive compare for CFML's case-insensitive attributes.
Tests cover both dialects and the extension mapping. LICENSEs vendored.

Closes #38
2026-06-01 18:28:36 +02:00
Martin Vogel f16f9dcf01 feat(lang): add Qt QML language support
Vendor the MIT-licensed tree-sitter-qmljs grammar (a TypeScript superset with
declarative ui_* nodes) and wire .qml files through the full pipeline: language
enum, extension + display-name mapping, grammar shim, and a language spec that
reuses the JS/TS function/call/branch arrays and adds QML class/field/import
types (ui_inline_component, ui_property, ui_signal, ui_import).

QML files now produce Function nodes (incl. JS functions inside QML objects),
class/interface/enum nodes, imports and CALLS edges, so call chains through a
Qt project's .qml files can be traced. LICENSE vendored alongside the grammar.

Closes #42
2026-06-01 18:07:05 +02:00
Martin Vogel 955b87d3fd feat(arch): replace single-level Louvain with multi-level Leiden community detection
The previous community detection ran a single local-moving pass with no
aggregation or refinement, so it fragmented graphs into hundreds of tiny,
sometimes internally-disconnected clusters (e.g. 275 on a mid-size repo).

Implement the Leiden algorithm (Traag, Waltman & van Eck 2019,
arXiv:1810.08473): local moving (fast, queue-based) -> refinement -> graph
aggregation, repeated until the partition can no longer be coarsened. The
refinement phase re-derives each community bottom-up from singletons, merging
only along edges, which guarantees every reported community is internally
connected. Aggregation seeds each level from the prior move-phase partition so
coarse structure carries over. A resolution parameter controls granularity.

Internals use a CSR graph (cbm_lg_t); self-loops are folded into the weighted
degree rather than materialised, so the modularity gain uses degree for the
null model and the adjacency for connectivity. cbm_louvain is preserved as a
thin wrapper over cbm_leiden(resolution=1.0) for API compatibility.

Tests: multi-level collapse (32-node clique chain -> few connected
communities), resolution control (path graph granularity), and a connectivity
checker proving the refinement guarantee; existing Louvain tests still pass.

Closes #57
2026-06-01 00:48:38 +02:00
Martin Vogel 1a636f1962 feat(r): box::use/library imports + module\$fn calls (#218, #219)
R had no import extraction at all (not in the import dispatch), so library(),
require(), source() AND box::use() all produced zero IMPORTS edges; and
module\$fn() calls were dropped because the call-callee resolver did not handle
R extract_operator function nodes.

#218: add parse_r_imports — recursively scan for call nodes and emit IMPORTS for
library/require/requireNamespace/loadNamespace/source (first arg) and for each
box::use(mod[syms], pkg/path[syms], ...) argument (module path = the spec text
before the [symbols] list). AST node types confirmed via a tree-sitter-R probe.

#219: handle extract_operator (R \$) as a call function node in
extract_callee_from_fields → emit "module.fn" so module\$fn() yields a CALLS
edge like other member calls.

Tests cover box::use + library + source imports and the \$-call; r_collect_imports
added to the recursion whitelist.
2026-05-31 23:07:50 +02:00
Martin Vogel db6e99c0ea fix(discover): map .blade.php to Blade by default (#258)
Laravel Blade templates (*.blade.php) fell through to the single-extension
lookup and were mis-classified as PHP, so the PHP extractor tried to parse
Blade syntax. Add a built-in compound-extension table (checked before the
single-extension fallback and alongside user config) mapping .blade.php to
CBM_LANG_BLADE, so Blade files are discovered and parsed with the Blade
grammar. Combined with the now-non-blocking discovery gate (c29e6d5), this
resolves the "agent stuck on Blade files" report. Plain .php is unaffected.
2026-05-31 21:15:57 +02:00
Martin Vogel 13bd10812a fix(install): correct Antigravity CLI config paths (2026 unification)
Antigravity support targeted the pre-unification layout, so it never actually
configured the CLI:
- detection probed ~/.gemini/antigravity/ — the CLI now installs under
  ~/.gemini/antigravity-cli/ (brain/, mcp/, settings.json)
- MCP config was written to ~/.gemini/antigravity/mcp_config.json — Antigravity
  reads the SHARED ~/.gemini/config/mcp_config.json (mcpServers, command/args)
- the SessionStart hook + AGENTS.md targeted the stale dir

Point detection at ~/.gemini/antigravity-cli/, write the MCP server to the
shared ~/.gemini/config/mcp_config.json (creating ~/.gemini/config if needed),
and place AGENTS.md + the SessionStart reminder under ~/.gemini/antigravity-cli/.
Install, uninstall, install --plan, the detection test, and the README table
updated.
2026-05-31 21:02:51 +02:00
Martin Vogel 3be9ace6d5 fix(cypher): drop dead-store assignments flagged by cppcheck
The buffer-full guards in parse_node (label alternation) and with_proj_key
assigned to ll/kl right before break — never read again. cppcheck 2.20.0
(--ci) flags these as unreadVariable and fails the lint gate. Replace with a
bare break. No behavior change.
2026-05-31 21:02:51 +02:00
Martin Vogel a2cd1e7c07 fix(cypher): resync on unsupported WITH/RETURN expression (#373)
split(f.path,'/')[0] and similar unknown function-call / index
expressions left their trailing tokens unconsumed, desyncing the parser into
a misleading default star projection (columns f.name/f.qualified_name/
f.label, one blank row per node). parse_return_item now consumes the balanced
parentheses/brackets of an unknown call so the parser stays in sync: the
requested RETURN/WITH column shape is preserved (aliases intact), sibling
aggregates like count(*) still compute, and the unsupported expression simply
projects empty instead of corrupting the whole projection. Existing scalar
forms (labels(), toInteger()) are unaffected.
2026-05-31 20:40:55 +02:00
Martin Vogel 869341c709 feat(cypher): support COUNT(DISTINCT x) (#239)
count(DISTINCT x) was a parse error ("expected token type 85, got 9").
parse_aggregate_item now consumes an optional DISTINCT inside the call and
records it on the return item. Both aggregation paths track the set of unique
values per group and emit its size for COUNT(DISTINCT): the RETURN path reuses
the existing collect_lists machinery; the WITH path gains distinct_lists/
distinct_n fields. Plain COUNT and the other aggregates are unchanged.

Unit test: COUNT(DISTINCT f.label)=1 (all same label), count(f.label)=4
(non-distinct), COUNT(DISTINCT f.name)=4 (unique names). Smoke suite adds a
count(DISTINCT) query_graph check.
2026-05-31 20:22:55 +02:00
Martin Vogel 2a5515e586 feat(cypher): support label alternation MATCH (n:A|B) (#242)
openCypher node-label alternation was unsupported (parse error after the
first label). parse_node now consumes `:A|B|C` into a single "A|B|C" label
string (mirroring the existing relationship-type alternation). The seed path
unions per-label results (scan_alternation_labels), and target/hop label
filters use label_alt_matches, which ORs over the alternatives. Single-label
matching is unchanged.

Unit test (Function|Module → 5 rows; Function|Class → 4) plus a query_graph
alternation check in the smoke suite. Relationship-type alternation tests
remain green.
2026-05-31 20:11:12 +02:00
Martin Vogel b30e6d6443 feat(cypher): support label tests in WHERE — WHERE n:Label (#241)
openCypher label predicates in WHERE clauses were a parse error
("expected token type 67, got 73"). Parse `var:Label` in the WHERE leaf as
a condition with op=HAS_LABEL/value=Label, and evaluate it against the bound
node label in eval_condition (honoring NOT). Composes with AND/OR and works
in count(...) queries.

Unit test covers true/false/negated label predicates; smoke suite adds a
query_graph WHERE-label check.
2026-05-31 20:02:23 +02:00
Martin Vogel 3014867cb3 feat(cypher): apply WITH DISTINCT deduplication (#238)
The DISTINCT keyword on a WITH clause was parsed (r->distinct) but never
applied, so WITH DISTINCT silently returned duplicate rows. Add a dedup pass
in execute_with_clause that drops projected rows whose full value tuple
duplicates an earlier one (first occurrence kept), gated on wc->distinct.
No-op for the aggregation path (which already collapses per group).

Unit test (4 same-label functions collapse to 1 row; control without DISTINCT
keeps 4) plus a query_graph WITH DISTINCT check added to the smoke suite.
2026-05-31 19:53:25 +02:00
Martin Vogel 6c71244e63 feat(hooks): SessionStart reminders for Codex, Gemini CLI, Antigravity (#330)
Codex, Gemini CLI, and Antigravity all gained lifecycle-hook support, so give
them the same non-blocking SessionStart reminder Claude Code has — stdout is
injected as session context, nudging the agent to use codebase-memory-mcp
graph tools before grep.

- Codex: append a sentinel-delimited [[hooks.SessionStart]] block to
  ~/.codex/config.toml (idempotent upsert, preserves other content, removable).
- Gemini CLI: SessionStart hook in ~/.gemini/settings.json, reusing the shared
  JSON hook upsert (alongside the existing BeforeTool reminder).
- Antigravity: same JSON hook in ~/.gemini/antigravity/settings.json (it shares
  Gemini hook semantics).

The reminder command is written to be valid both as a TOML single-quoted
literal and a JSON string (no single quotes, no newlines). Wired into install,
uninstall, and the install --plan receipt. remove_hooks_json now prunes an
emptied event key so removal leaves no stale "<Event>": [] cruft. README
Multi-Agent table updated. Tests cover Codex upsert/idempotency/removal and
Gemini parity.
2026-05-31 19:39:31 +02:00
Martin Vogel 4c38c5a275 feat(install): add install --plan machine-readable receipt (#388)
An installer that configures MCP entries, instruction files, Skills, and hooks
across many agents should let callers see what it will mutate before it does.
`--dry-run` already lists planned writes in human-readable form; this adds the
machine-readable `agent.install.plan.v1` JSON receipt the issue asked for.

`install --plan` runs the real install dispatch in record-only mode — a global
g_install_plan recorder that each write-site function appends to (at the same
point it would write) while all mutations are disabled — then emits JSON with
agents_detected, config_files_planned, instruction_files_planned,
hooks_planned, writes_started:false, network_after_install:false, and
next_safe_command. Because the plan is recorded on the actual install code
path, it cannot drift from real behavior. No config is written, no index
deleted, no network used.

cbm_build_install_plan_json is exposed for testing; the test asserts the
receipt content AND that building it creates no config files.
2026-05-31 18:15:40 +02:00
Martin Vogel 70e3ed70ac fix(adr): unify manage_adr onto the SQLite store shared with the UI (#256)
manage_adr (MCP/CLI) wrote/read ADRs as a file at <root>/.codebase-memory/adr.md,
while the UI /api/adr endpoints used the SQLite project_summaries table via
cbm_store_adr_get/store. Writes through one interface were invisible to the other.

Route manage_adr through cbm_store_adr_get/store (the same backend the UI uses),
so update/get/sections all operate on the shared store. Add a one-time migration:
if the store has no ADR but the legacy adr.md file exists, import it on first
access so nothing is lost on upgrade. The legacy file reader is retained solely
for that migration.
2026-05-31 17:59:05 +02:00
Martin Vogel 6657044026 fix(ui): warn on stderr when --ui requested without embedded UI (#350)
A binary built without the embedded frontend (CBM_EMBEDDED_FILE_COUNT == 0)
accepted --ui=true, persisted it to config, but never started the HTTP server
— the only signal was a ui.no_assets warning routed to the log-file sink, which
a user running `--ui=true` on a terminal never sees. Detect an explicit
--ui=true on a non-UI binary and print a clear stderr message pointing to the
UI release asset / cbm-with-ui build, so the silent no-op is explained.
2026-05-31 17:43:24 +02:00
Martin Vogel 5595ce47f9 fix(naming): derive only validator-safe project names (#349)
cbm_project_name_from_path replaced only "/" and ":" with "-", leaving
spaces, "@", "+", unicode, etc. intact. resolve_store (via project_db_path)
gates on cbm_validate_project_name, which allows only [A-Za-z0-9._-]. So a
repo like "/home/u/my project" was indexed and shown by list_projects (which
opens the .db file directly), yet index_status/search_graph reported
project-not-found because resolve_store rejected the space.

Normalize every rejected character to "-", collapse consecutive dots (the
validator forbids ".."), and strip leading dots, so a derived name always
satisfies cbm_validate_project_name and round-trips through resolve_store.
2026-05-31 17:22:02 +02:00
Martin Vogel 3bc1d586d6 fix(search_code): error on invalid regex instead of silent empty (#283)
With regex=true, a syntactically invalid pattern (e.g. unclosed group) made
the underlying grep fail, which handle_search_code reported as an empty result
set — indistinguishable from a legitimate no-match. Validate the user pattern
with cbm_regcomp up front and return an explicit "invalid regex" error so
callers can tell a broken pattern from zero matches. regex=false is unaffected.
2026-05-31 16:22:31 +02:00
Martin Vogel 30dfa952d9 fix(install): emit fish-native PATH syntax for fish configs (#319)
cbm_ensure_path appended `export PATH="<dir>:$PATH"` to every shell rc,
including config.fish — which is a syntax error in fish and broke users
fish config. Detect a .fish target and emit `fish_add_path <dir>` instead
(idempotent, prepends only if absent). POSIX shells are unchanged.
2026-05-31 16:13:47 +02:00
Martin Vogel 5f3847c0e5 fix(grpc): anchor Route emission on stub suffix; preserve service name (#294)
emit_grpc_edge produced two classes of bad output:

1. Wrong service name: the suffix list stripped "ServiceClient"/"ServiceGrpc"
   before "Client"/"Grpc", so FooServiceClient collapsed to "Foo" instead of
   the proto-declared "FooService" — breaking cross-repo Route matching.

2. Phantom Routes: extract_grpc_service_method returned true for any
   "<recv>.<method>" call, so ordinary receiver vars (_provider.GetGroup,
   _builder.AddSomeService) became __grpc__provider/... Routes matching no
   .proto anywhere.

Strip only the trailing stub/client token (Client/Stub/Grpc/BlockingStub/…),
preserving "Service", and require that a recognized suffix was actually present
before emitting — the suffix match is the gRPC stub-type signal. Plain receiver
vars no longer yield Routes.

extract_grpc_service_method is no longer static (declared in pipeline_internal.h)
so it is unit-testable.
2026-05-31 14:44:54 +02:00
Martin Vogel 61453e928f test(hooks): guard PreToolUse gate shim against predictable temp file (#384)
The symlink-attack vector (predictable /tmp/cbm-code-discovery-gate-$PPID)
was removed in c29e6d5 when the blocking bash gate became a stateless
compiled augmenter writing to stdout only. Add a regression guard so a
predictable temp/state file can never be reintroduced: assert the emitted
gate shim contains no /tmp path and no $PPID, and delegates to hook-augment.

Export cbm_install_hook_gate_script (was static) so the security property
is directly testable.
2026-05-31 13:32:08 +02:00
Martin Vogel f88d0315ac fix(install): detect Cursor IDE and register MCP config (#222)
Cursor was never in the editor-detection table, so install/update only
ever wrote the VS Code config and silently skipped ~/.cursor/mcp.json,
even when Cursor was present. Uninstall removed the Cursor entry but a
subsequent install never re-created it.

Detect ~/.cursor/ as the Cursor agent, register the MCP server in
~/.cursor/mcp.json (mcpServers format, same as Windsurf/Gemini) on
install, remove it on uninstall, and list Cursor in detected agents.
2026-05-31 13:21:31 +02:00
Martin Vogel 4abcaa43e7 fix(mcp): preserve JSON-RPC string ids instead of coercing to int (#253)
JSON-RPC 2.0 §4 permits string request ids, and several MCP clients
(including Claude Desktop) send a string id for "initialize". The parser
coerced string ids via strtol, yielding 0 for any non-numeric id, so the
response echoed the wrong id and the client could not correlate it.

Carry the string id verbatim through parse -> dispatch -> response
(new id_str field on the request/response structs) and emit it as a JSON
string. Numeric ids are unchanged. The method-not-found error path now
echoes the original id too.
2026-05-31 13:10:52 +02:00
Martin Vogel 0485d3f3b1 fix(cli): probe Windows PATHEXT variants in find_in_path (#221)
Agent CLIs on Windows are installed as .cmd/.ps1/.exe shims (e.g. opencode
via mise/npm), but find_in_path only probed the bare name, so they weren't
detected. Try .exe/.cmd/.bat/.ps1 per PATH entry on Windows.

Relates to #221 (opencode not detected). The install-time taskkill 'eq'
error from the same report is addressed separately — cbm_kill_other_instances
now uses _spawnvp with an argv array rather than a shell string.
2026-05-31 12:54:58 +02:00
Martin Vogel c1dabc988b fix(store): accept lowercase drive letters in root_path integrity check
The projects.root_path integrity check only accepted '/' or an uppercase
'A'-'Z' first character. On Windows, drive letters are commonly lowercase
(c:/repo, y:/share), so such a path was flagged store.corrupt and the DB
was auto-deleted on open — a likely cause of the mapped-drive DB deletion
in #227/#367. Accept 'a'-'z' as well.

Relates to #227, #367.
2026-05-31 12:50:19 +02:00
Martin Vogel 2de984bdb6 fix(discover): map .mjs/.cjs (JS) and .mts/.cts (TS) extensions (#197)
.mjs (ES modules) and .cjs (CommonJS) were absent from the extension
table, so those files were never assigned a language — they weren't
indexed (no graph nodes) and search_code returned 0 for any pattern in
them. Add .mjs/.cjs → JavaScript and .mts/.cts → TypeScript.

Verified end-to-end: a function in a .mjs file is now extracted and found
by search_code. Closes #197.
2026-05-31 12:10:29 +02:00
Martin Vogel 53be053d3f fix(search): treat wildcard-free file_pattern as a path substring (#200)
search_graph file_pattern always returned 0 for a bare name like
"offer-server": cbm_glob_to_like produced the literal "offer-server",
so `file_path LIKE 'offer-server'` only matched a path equal to it, never
"src/offer-server/x.js". When the pattern contains no glob wildcards
(* or ?), wrap the LIKE as %pattern% so it matches anywhere in the path.
Explicit globs keep their semantics.

Closes #200.
2026-05-31 11:48:25 +02:00
Martin Vogel bcf73b29e8 fix(pkgmap): make repo-wide manifest walk POSIX-only (Windows hang)
The full-repo manifest walk added for workspace-import resolution hung
the Windows CI test-runner (pipeline_structure_nodes): pkgmap_walk_dir
uses plain stat() on Windows (no lstat/S_ISLNK), so directory junctions
in the walked tree can be followed into cycles, never terminating.

Make cbm_pkgmap_scan_repo a no-op on Windows and compile pkgmap_walk_dir
only on POSIX. Windows falls back to the files[]-based pkgmap (the
pre-batch baseline behavior). Workspace-import resolution from
discovery-ignored manifests (package.json) is therefore Linux/macOS-only
until the walk is made junction-safe on Windows (follow-up).
2026-05-30 21:03:48 +02:00
Martin Vogel fcf98acf8d fix(portability+format): Windows pkgmap stat; clang-format distilled passes
- pass_pkgmap: lstat/S_ISLNK are POSIX-only and undeclared on the MSYS2
  Windows build. Guard them under #ifndef _WIN32 (plain stat on Windows,
  where symlinks aren't a concern here). Fixes the Windows build break
  introduced by the workspace-manifest walker.
- Run clang-format-20 over str_util.c, store.c, mcp.c, pipeline.c,
  pass_route_nodes.c, pass_pkgmap.c so the recently-landed pkgmap / route /
  security passes satisfy the lint gate.
2026-05-30 19:34:27 +02:00
Ivan Porto Carrero 9e2bb92895 fix(arena): NULL-arena guard in cbm_arena_alloc; skip vendored/ dirs
- cbm_arena_alloc returns NULL on a NULL arena (both arena.c copies)
  instead of dereferencing it — defense-in-depth against the NULL-arena
  type-allocation path that could crash the LSP type layer.
- discover: add "vendored" to the always-skip directory list.

Distilled from #374, taking the parts not already on main. The PR's
ts_lsp tuple-arena allocation, the C/C++ template formal-count clamp,
and the type_rep unbound-param preservation it also carried all landed
independently in v0.7.0 / via #322 / #360, so only these two
defensive improvements remain. Relates to #390.
2026-05-30 18:26:44 +02:00
utilia-ai-wox 9bcfaabe4c fix(pkgmap+routes): resolve workspace imports + extract SvelteKit routes
Two structural fixes for TS monorepos (pnpm/yarn workspaces) and
SvelteKit projects, measured to take a 885-file SvelteKit+Turborepo repo
from 319 edges to 31802 (100x) with no fixture regressions.

- pkgmap: package.json / composer.json live in IGNORED_JSON_FILES, so the
  manifest-driven pkgmap builders never saw them and every
  '@my/pkg' workspace import was silently dropped. Add a symlink-safe,
  skip-dir-aware filesystem walker (cbm_pkgmap_scan_repo /
  cbm_pkgmap_build_from_repo) that harvests manifests directly from the
  repo regardless of the discovery filter. Sequential and parallel paths
  both feed it now.
- definitions: extract every file's defs (creating Module nodes) BEFORE
  resolving imports, so a workspace import in the first file can resolve
  against the complete in-memory graph (two-phase, cache-backed).
- routes: synthesise Route nodes + HANDLES edges from SvelteKit's
  filesystem layout (+server / +page.server / +layout.server), which has
  no app.get(...) call site for pass_calls to pick up.

Distilled from #369 onto current main. The PR's CBM_DISABLE_LSP_CROSS
escape hatch was rebased onto the v0.7.0 fused cross-LSP architecture:
it now gates run_cross_lsp (NULL all_defs makes the fused resolver no-op
cross-file resolution) instead of wrapping the removed standalone
pass_lsp_cross call. Relates to #271 / #56.
2026-05-30 18:15:47 +02:00
jjserenity 2389d82214 fix(windows): use wide-char API for non-ASCII path support (#386)
Replace ANSI Windows APIs (FindFirstFileA, CreateFileA, etc.) with
wide-char (W) counterparts. Add UTF-8/UTF-16 conversion helpers.
Fix stat() calls in discover pipeline to use _wstat64.

Fixes #357
2026-05-30 18:05:03 +02:00
jjserenityandHC Cheng a6ad4011ed fix(store): checkpoint WAL on close and startup to prevent orphan accumulation (#387)
Add WAL checkpoint on store close and after WAL-mode enable at startup.
This ensures graceful shutdown leaves a clean WAL, and crash recovery
merges stale WAL on next open. Both use PASSIVE mode (non-blocking,
no ftruncate). Best-effort — silently skip if concurrent reader holds
a lock.

Fixes #277

Co-authored-by: HC Cheng <hz.chang@pm.me>
2026-05-30 17:54:52 +02:00
Sam Li a5a3d1d60a feat(platform): cgroup-aware CPU/memory detection in detect_system_linux
Inside a container, sysconf(_SC_NPROCESSORS_ONLN) and sysinfo() report
the host's CPU count and RAM, not the cgroup's effective quota — so
cbm_default_worker_count over-provisions workers and the mmap budget can
exceed the cgroup memory cap, driving OOMKills (#363).

detect_system_linux now reads the cgroup limits:
- cbm_detect_cgroup_cpus: cgroup v2 cpu.max, then v1 cpu.cfs_quota_us /
  cpu.cfs_period_us → ceil(quota/period); -1 when unlimited.
- cbm_detect_cgroup_mem: cgroup v2 memory.max, then v1
  memory.limit_in_bytes; 0 when unlimited or the v1 near-ULLONG_MAX
  sentinel.
- Effective value is min(cgroup, host), guarding against mis-mounted
  cgroups that report more than the host.

Helpers read via a bounded read_small_file (fopen "re", capped fread)
and are exposed through system_info_internal.h for unit tests that drive
them against a fake cgroup tree. The test teardown uses opendir/unlink/
rmdir (no shell spawn).

Distilled from #365 onto current main (unioned the test additions with
the CBM_WORKERS tests from #364, and replaced the test-cleanup
system("rm -rf") with a shell-free recursive remove). Closes #363
together with #364.
2026-05-30 17:47:18 +02:00
yangsec888 d95223817e feat(platform): CBM_WORKERS env override for cbm_default_worker_count
Adds a CBM_WORKERS env knob (clamped to [1, 256], invalid values warned
and ignored) that explicitly sets the parallel-indexing worker count.
In containers, sysconf(_SC_NPROCESSORS_ONLN) reports host CPUs rather
than the cgroup's effective quota, so a 1-vCPU pod on a 16-core node
otherwise spawns ~16 workers — the dominant OOMKill driver. When unset,
behaviour is unchanged.

From #364.
2026-05-30 17:32:47 +02:00
Code Analyzer 21c73b57f1 fix: security and correctness hardening from codebase audit
- str_util: add cbm_validate_project_name() — strict allowlist
  ([A-Za-z0-9._-], no leading dot, no '..', no path separators) so a
  project name can never escape the cache directory.
- store/mcp/http_server: validate project names (fail-closed) before
  interpolating them into .db file paths (path-traversal hardening;
  defense-in-depth for the wrong-path class in #331).
- store: fix use-after-free in the LIKE pool — like_pool_add() frees its
  argument when the pool is full, so capture pool_was_full before the add
  and skip the subsequent bind that would touch the freed pointer (two
  call sites). Also fix the find_edges_by_url_path LIKE pattern (stray
  double-percent) and add NULL guards to cbm_store_checkpoint /
  cbm_store_node_degree / cbm_store_restore_from.
- mcp: emit "result": null when a JSON-RPC response would otherwise
  carry neither result nor error (2.0 compliance); bounds-check the
  project-name accumulator against truncation.
- http_server: JSON-escape directory and parent names in the browse
  endpoint to prevent stored-XSS/JSON injection via crafted names.
- cypher: expr_free now recurses on EXPR_FREE_STACK overflow instead of
  silently dropping subtrees (memory leak).

Distilled from #352 onto current main (store.c like-pool hunk re-resolved
against the v0.7.0 search refactor; restored cypher.c EOF newline).
2026-05-30 17:19:56 +02:00
Christof Meerwald 9d6e28bccf feat: add support for MCP ping (#354) 2026-05-30 17:10:03 +02:00
Alexey Z df38e331ca fix(windows): use cmd.exe-compatible syntax for git-log history pass
The git-history popen command used `cd '%s' && git log ... 2>/dev/null`,
which fails on Windows cmd.exe: single quotes are not recognized and
/dev/null is not a valid path, producing "The filename, directory name,
or volume label syntax is incorrect" at the git-history step of a full
index. Switch to `git -C "%s" log ... 2>NUL` (POSIX: 2>/dev/null),
which is valid on both cmd.exe and POSIX shells. Double-quoting the path
is safe because cbm_validate_shell_arg already rejects ", $, backtick,
backslash and the other active metacharacters before the command is built.

Distilled from #325 onto current main (preserves the :%%ct commit-time
field added since the PR branch). Closes #324.
2026-05-30 15:51:09 +02:00
Shane McCarron dedd33d975 fix(install): respect $CLAUDE_CONFIG_DIR in install/uninstall/update
Route Claude Code config paths (skills, .mcp.json, .claude.json,
settings.json, hook scripts) and agent detection through
CLAUDE_CONFIG_DIR-aware helpers, falling back to ~/.claude. Hook command
strings written to settings.json keep the legacy tilde form when the env
var is unset, so existing configs stay portable across HOME values. Prints
a one-line migration nudge when CLAUDE_CONFIG_DIR is set and a legacy
~/.claude tree still exists.

Adds cli_detect_agents_finds_claude_via_env and isolates CLAUDE_CONFIG_DIR
in the existing detection tests so the runner env can't leak in.

Distilled from #321 onto current main (adapts to the v0.7.0 non-blocking
augmenter hook signature, which the original branch predated). Closes #320.
2026-05-30 15:22:37 +02:00
Martin Vogel 82d5c841e6 fix(parallel): destroy thread parser before slab reclaim in LSP-cross loop
The LSP-cross loop in parallel_resolve called cbm_slab_reclaim() without
first destroying the thread-local tree-sitter parser, violating the
documented slab contract. The parser's lexer holds slab-allocated state
(notably lexer.included_ranges, a 24-byte allocation that lands in the
≤64B slab bucket); reclaiming the slab leaves that pointer dangling.

Workers don't trip on it because they don't reparse afterward, but the
main thread participates in cbm_parallel_for and any later cbm_extract_file
on the main thread — including the sequential pass_definitions in the
incremental pipeline — hits a heap-use-after-free in ts_lexer_goto on
macOS-ASan.

Two-part fix:
- pass_parallel.c: destroy the thread parser before cbm_slab_reclaim() in
  the per-file LSP-cross loop, matching the worker extract loop.
- pass_definitions.c: defensively drop the thread parser at pass entry so
  the incremental sequential path cannot inherit stale state from any
  prior run.
2026-05-29 23:11:52 +02:00
Martin Vogel 90e85bb10c fix: pad source read buffers in all parse passes (tree-sitter lookahead)
Same fix as read_file in pass_definitions: the calls/usages/semantic/k8s/lsp_cross passes each read a source file into a malloc'd buffer and re-parse it, so they hit the same tree-sitter lexer over-read past EOF under ASan. Over-allocate by 16 and zero the tail in every reader.
2026-05-29 06:00:44 +02:00
Martin Vogel c023931014 fix: pad source read buffer for tree-sitter lookahead (ASan over-read)
read_file allocated size+1, but tree-sitter's lexer peeks a few bytes past the final UTF-8 char (ts_lexer_goto), reading past the buffer end. Under ASan this is a heap-buffer-overflow (intermittently flagged in the incremental suite on macOS); in production it is a benign over-read of adjacent heap. Over-allocate by 16 bytes and zero the tail so the lookahead stays in-bounds.
2026-05-29 05:57:19 +02:00
Martin Vogel ef25ca0727 style: wrap cbm_strndup call in path_alias.c for clang-format-20
The strndup->cbm_strndup rename pushed the line past the column limit; reflow to satisfy the clang-format-20 lint check.
2026-05-29 02:54:00 +02:00