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
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).
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
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
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
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
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#186Closes#35Closes#39
#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
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
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
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
#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.
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.
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.
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.
Confirmed working in the current query_graph evaluator and locked in:
- #240 labels() function with AS alias
- #237 DISTINCT applied before ORDER BY + LIMIT
- #252 toInteger()
- #305 count(*) with AS alias
Closes#240. Closes#237. Closes#252. Closes#305.
.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.
#220: C++ CALLS edges originated from the Module node instead of the
enclosing Function, breaking function-level trace_path. The cross-LSP
resolver now sets caller_qn = enclosing_func_qn; the test asserts a call
(doWork→helper) is attributed to doWork, not the module.
Closes#220.
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.
#382 — Java Method nodes now carry decorators (@GetMapping) and a non-empty
signature; verified by extract_java_method_annotations_issue382.
#213 — a 256 KB / 4000-function TS file now yields Function nodes instead of
a childless File node (fixed by the tree-sitter read-buffer padding);
verified by extract_large_ts_has_functions_issue213.
Closes#382. Closes#213.
#228: a class/static method invoked on a cross-file imported class name
(ActionRecordX.build_from_text(...)) produced no CALLS edge, so the
method looked like dead code (in/out degree 0). The v0.7.0 py_lsp
cross-file resolver handles this; the test feeds the exact pattern through
cbm_run_py_lsp_cross and asserts the call resolves.
Closes#228.
The #336 (Icinga2/Linux) and #385 (KiCad/Windows) reproductions are done —
both index cleanly on v0.7.0 on their reported platforms. Removing the
throwaway workflow.
The reused _build.yml builds all platforms; a cancelled macos-15-intel
runner skipped the icinga2/kicad index jobs. Build inline in each job so
the Linux (#336) and Windows (#385) repros don't depend on unrelated
platforms. Still temporary — delete after the run.
Issues #344/#340 reported a scale-dependent SIGSEGV in the TS cross-file
LSP pass once the collected def array exceeded ~1189 entries. The v0.7.0
cross-LSP rewrite registers defs through the arena (no fixed-size def
array), so the threshold no longer exists. This test drives
cbm_run_ts_lsp_cross with a 4000-entry def array under -fsanitize=address
and resolves cleanly, locking in the fix.
Closes#344. Closes#340.
Temporary workflow_dispatch harness: builds current-main binaries and
indexes Icinga2 on Linux (fast) and KiCad on Windows (moderate) to check
whether the reported crashes still reproduce on v0.7.0. To be removed
after the run.
Issue #355 reported a segfault extracting a macro-heavy xxhash C header
(dhw/xx_hash.h, not available to us). Add a guard that runs the vendored
xxhash.h (~7.5k lines, same macro-dense family) through C extraction under
ASan. It passes on current main — the C-LSP crash hardening (#322/#323/
#360) appears to cover this class. Kept as a regression guard; #355 stays
open pending the reporter's confirmation/file.
Passing an unrecognised project name triggered a buffer overflow while
building the available-projects error list: collect_db_project_names did
snprintf(out+offset, out_sz-offset, ...) with no bound, so once the
listed names exceeded the 4 KB projects buffer, (out_sz - offset)
underflowed (unsigned) to a huge size and snprintf wrote past the buffer.
Fixed in 21c73b5 (PR #352) with an offset>=out_sz bounds check + clamp.
This test fills a temp CBM_CACHE_DIR with 40 long-named .db files
(~5 KB of names) and issues a bad-project search_graph call; the test
build runs under -fsanitize=address so the old underflow would abort here.
Closes#235.
A C++ function template with a defaulted type param and an `auto`
parameter, called with matching arg count, previously segfaulted in the
definitions pass (unbound `U` flowed through type substitution as NULL).
Fixed by the cbm_type_substitute NULL-preservation guard; this locks in
a reproduction so it can't regress.
Closes#312.