Commit Graph
715 Commits
Author SHA1 Message Date
Martin Vogel 0956f378b1 Refresh vendored tree-sitter grammars to pinned upstream revisions
Update 128 vendored grammars to the commits pinned by nvim-treesitter and
Helix (cross-verified canonical sources), normalize their tree_sitter
includes (angle-bracket -> quoted) and external-scanner deserialize
signatures, and restore missing upstream LICENSE files.

Add internal/cbm/vendored/grammars/MANIFEST.md as a provenance record:
upstream repo, pinned commit, current/latest ABI, and license status for
all 157 grammars. 12 first-party (self-maintained) grammars are marked and
excluded from upstream refresh; vhdl and fennel are kept at their prior
revisions (scanner ABI / added-scanner incompatibilities).

Adapt the shared extraction layer to node-shape changes in the refreshed
grammars: resolve declaration names from simple_identifier/type_identifier
children when the grammar drops the `name` field (kotlin, swift), and
recognize multiline_comment as a doc comment.
2026-06-03 01:32:57 +02:00
Martin Vogel d849aeaee7 Merge Java hybrid LSP
Integrates the type-aware Java semantic resolver from
worktree-adding-java-lsp-support (based on 673ac4e): java_lsp.c + generated
java.lang/util stdlib registry, wired into the per-file LSP pass, with 94
java_lsp tests + ~170 java_lsp_coverage tests.

Wiring conflicts (the shared files diverged 146 commits on main) resolved by
keeping main's existing LSP registrations (go/c/php/py/ts/cs) and adding the
Java entries in lsp_all.c, cbm.c (cbm_run_java_lsp dispatch), test_main.c and
Makefile.cbm.

Adapted to the current unity-build: renamed java_lsp.c's static helpers
node_text/return_type_of to java_node_text/java_return_type_of so they no
longer collide with ts_lsp.c inside the shared lsp_all.c translation unit.

4082 tests pass (incl. java_lsp + java_lsp_coverage); ASan/UBSan clean.
2026-06-02 19:06:02 +02:00
Martin Vogel a600e80cb1 feat(lsp): Java hybrid LSP resolver (type-aware calls, annotations, signatures)
Pure-C Java semantic resolver (java_lsp.c) + generated java.lang/util stdlib
registry, wired into the per-file LSP pass. Resolves direct/method/trait/
generic/lambda calls with confidence; conservative no-edge on unresolved.
Includes test_java_lsp.c + coverage suite.
2026-06-02 18:52:37 +02:00
Martin Vogel 12683b84a9 feat(graph): queryable computation-bottleneck metrics + indexing perf & fixes
Per-function complexity metadata is now stored on graph nodes and queryable,
alongside several indexing performance and correctness fixes developed and
validated together (3704 tests, ASan/UBSan clean).

Bottleneck metrics (query via query_graph):
- Tier A (in the extraction AST walk): cyclomatic (complexity), cognitive
  (nesting-weighted), loop_count, loop_depth (max nested-loop depth),
  param_count, max_access_depth.
- Tier B (new pre-dump pass, pass_complexity.c): transitive_loop_depth
  propagated along CALLS edges + a recursive flag (direct self-recursion and
  mutual-recursion cycles), plus the call-context signals linear_scan_in_loop,
  alloc_in_loop, recursion_in_loop and unguarded_recursion.
- query_graph and get_architecture tool descriptions document the metrics and
  the Leiden community clusters.

Cypher engine:
- node_prop exposes arbitrary persisted node properties to WHERE/RETURN.
- Fix projection aliasing: multi-property rows shared a single static buffer so
  every column returned the last value read; now per-column/rotating buffers.
- Fix a stack-use-after-scope in aggregate RETURN (caller-owned value buffers).

Indexing performance:
- Gate C/C++ #define Macro-node extraction to full mode (it is ~49% of nodes on
  the Linux kernel); moderate/fast skip it.
- Emit the complexity property block only for Function/Method nodes so the
  millions of Macro/Field/Variable/Class/Enum nodes no longer carry zeroed
  fields — large RAM reduction at scale.
- Classify node types via tree-sitter TSSymbol bitsets in cbm_kind_in_set
  instead of per-node strcmp scans (thread-local cache, strcmp fallback;
  behaviour-identical).
- Subsample frequent (Zipfian) tokens in the semantic co-occurrence finalize;
  ~14x faster finalize on the kernel, output unchanged.
- pass_lsp_cross: replace O(n^2) linear dedup with hash-set dedup.

Windows:
- Canonicalize drive-letter case during path normalization so "c:/repo" and
  "C:/repo" derive the same project key and cache file (#394/#227/#367).

Tests: extraction, pipeline and cypher regressions covering all of the above.
2026-06-02 18:50:55 +02:00
Martin Vogel 64be28083e fix(deps): bump vitest 3 -> 4.1.8 (GHSA-5xrq-8626-4rwp)
Resolves the critical Dependabot/Scorecard alert for vitest <4.1.0 (UI server
arbitrary file read/exec). graph-ui has no test files, so the major bump is
risk-free here (CI uses 'vitest run'); lockfile regenerated, vitest 4.1.8.
2026-06-01 22:57:54 +02:00
Martin Vogel 09e3f71bf6 fix(search_code): accept '&' in paths, warn on literal-'|' trap, report timing
#272: '&' is neutralised by the single-/double-quoting of the grep/Select-String/
git commands, so it no longer fails validation. A relaxed validate_search_path_arg
(cbm_validate_shell_arg minus '&') is used for search_code + detect_changes
root_path/file_pattern; all other shell metacharacters stay rejected and
base_branch keeps the strict check.

#282: search_code defaulted regex=false, so 'foo|bar' matched the literal pipe
and silently returned 0 results. The result now carries a warnings[] array that
flags a literal '|' under regex=false (with the regex=true hint), so the trap is
visible instead of looking like a legitimate no-match. (The related invalid-regex
silent-empty case was already fixed in #283.)

Also: every search_code result now reports elapsed_ms, and a perf warning fires
when a search exceeds 5s (also logged as search.slow) so slow calls are visible.

Tests for both issues + the multi-word/invalid-regex no-regression; generous
search_code smoke coverage (basic+elapsed_ms, literal-| warning, '&' acceptance).

Closes #272
Closes #282
2026-06-01 22:53:17 +02:00
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 023d92cd8e docs(cypher): document the openCypher read subset accurately
The Supported-Cypher section was stale (listed WITH/COLLECT/OPTIONAL MATCH as
unsupported — all now work). Rewrite it to list the actual clauses, patterns,
WHERE operators (incl. =~ and EXISTS{}), aggregates and scalar functions, and
note that anything outside the subset now fails with a clear 'unsupported'
error instead of returning empty.
2026-06-01 21:18:54 +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 efec2ceffe docs: rebuild landing page, update facts and add GEO/SEO + live releases
- Update to current capabilities: 158 languages, 11 agents, 14 MCP tools,
  Hybrid LSP, infrastructure-as-code indexing, single static C binary
- Add dedicated Hybrid LSP section and per-language resolution table
- Remove the GitNexus comparison
- Restructure with answer-first, question-based headings for AI answer engines
- Add JSON-LD structured data (SoftwareApplication, Organization, WebSite,
  BreadcrumbList, FAQPage) with arXiv preprint citation
- Add a live GitHub releases widget (client-side, graceful fallback)
- Add prominent research-preprint callout under the hero
- Centered nav, "by DeusData" subtitle, inline SVG favicon
- Welcome AI crawlers in robots.txt, refresh sitemap, add llms.txt
- Fix broken benchmark link
2026-06-01 21:12:06 +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 600fe9033b docs: bump language count to 158 (add QML, CFML) and grammar count to 157
New languages added this round: Qt QML (.qml), CFML/ColdFusion (.cfc script +
.cfm tag). Update README, npm README, and chocolatey description to the
accurate distinct-language count (158) and vendored-grammar count (157).
2026-06-01 18:45:14 +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 2cad2fe0c3 feat(extract): fold HCL block labels into node names
HCL/Terraform blocks were named by their bare keyword, so every resource
collapsed to a single "resource" node and blocks were indistinguishable in the
graph. Fold the quoted labels into the name via the block's string_lit ->
template_literal children, so resource "aws_instance" "web" becomes
resource.aws_instance.web (and variable.region, module.vpc, etc). Each block is
now a distinct, query-friendly Class node. The dedicated terraform parser
(cbm_parse_terraform_source) is unchanged.

Closes #337
2026-06-01 17:52:29 +02:00
Martin Vogel f42d77244a feat(extract): verify GDScript/PowerShell/Luau extraction + fix PowerShell function names
GDScript (.gd), PowerShell (.ps1/.psm1/.psd1) and Luau (.luau) were wired into
the grammar table, lang specs and extension map but had no extraction coverage,
so the AST->graph visitor was never proven for them. Add extraction tests that
assert Function/Class nodes and CALLS are produced from representative source.

PowerShell's function_statement has no `name` field — the name is a
`function_name` child node — so resolve_func_name returned null and no Function
node was emitted. Resolve the name from that child, mirroring the Swift path.

Closes #186
Closes #35
Closes #39
2026-06-01 01:16:48 +02:00
Martin Vogel 10ce652152 feat(extract): extract C/C++ preprocessor macros as Macro nodes
#define directives were not represented in the graph. Object-like macros
(preproc_def) and function-like macros (preproc_function_def) are now emitted
as Macro nodes with a DEFINES edge from the file. A function-like macro keeps
its parameter list as the signature; the macro body is not descended into.
Applies to all C-preprocessor languages (C, C++, CUDA, GLSL, Objective-C, ISPC).

Closes #375
2026-06-01 00:59:36 +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 eb50569bed fix(extract): capture arrow methods returned from JS/TS factory functions
walk_defs stopped descending into a function body once the function itself
was extracted, so arrow methods of an object literal returned from a factory
(the Zustand actions-slice pattern) were never visited. Let JS/TS/TSX descend
into function bodies (as Wolfram already did) and resolve an arrow's name from
a 'pair' parent's key, so { addItem: (...) => {...} } is captured as a Function.
Anonymous nested arrows have no resolvable name and are skipped, so no noise.

Closes #341
2026-05-31 23:45:35 +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 d67329efbf test(smoke): update Antigravity E2E to 2026 CLI layout
The Phase 8 agent-install E2E checked the pre-unification Antigravity paths
(~/.gemini/antigravity/), which broke after the config-path fix. Point the
stub setup at ~/.gemini/antigravity-cli/ and assert the MCP server lands in
the shared ~/.gemini/config/mcp_config.json with AGENTS.md under
~/.gemini/antigravity-cli/. Verified end-to-end (smoke ALL PASSED).
2026-05-31 21:42:42 +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 f5573f17ce test(discover): guard vendor/ root-gitignore exclusion (#234)
The v0.6.0 behavior where a vendor/ directory in the root .gitignore was
still indexed is fixed on the v0.7.0 discovery rework. Add a faithful
regression test (PHP project, untracked vendor/ in root .gitignore) asserting
nothing under vendor/ is discovered, so it cannot regress.
2026-05-31 17:35:52 +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 7fad5bc87b ci(dry-run): stop scarce Intel-macOS runner from gating smoke/soak
GitHub is sunsetting Intel-macOS runners, so the macos-15-intel build leg is
repeatedly cancelled before it can be allocated, turning the aggregate build
result non-success and skipping the smoke + soak jobs even though every other
build/test passed.

Mark only the macos-15-intel matrix leg continue-on-error, and relax the
smoke/soak gate to run unless builds were skipped or a leg genuinely failed
(tolerating a cancelled aggregate caused solely by the non-blocking Intel leg).
All other platforms remain required.
2026-05-31 16:06:06 +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 9dcc0e3c59 ci: remove temporary Windows repro harness
#394 group triaged: #266/#274/#331/#347/#348 verified fixed on v0.7.0 in
Windows CI; #227/#367 (SMB) pending reporter retest. Removing the throwaway
workflow.
2026-05-31 12:59:09 +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 d198403e21 ci(temp): Windows repro harness for #394 group
Indexes a repo at a C:\ drive-letter path (subdirs + .cbmignore +
.mjs/.js) on windows-latest and dumps diagnostics for #347/#227/#367/
#331/#266/#274/#348. Temporary — delete after the run.
2026-05-31 12:51:20 +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