Commit Graph
431 Commits
Author SHA1 Message Date
Martin Vogel 67099fa249 Merge pull request #736 from DeusData/fix/deprecate-raw-json-cli
feat(cli): deprecation-warn raw-JSON args + full flag-ops smoke coverage
2026-07-01 17:21:27 +02:00
Martin Vogel 5c92f5c63e feat(cli): deprecation-warn raw-JSON args + full flag-ops smoke coverage
Raw JSON (cli <tool> '{...}') still works for back-compat, but now emits a
deprecation warning to STDERR (stdout stays clean JSON for piping) nudging
users to flags / --args-file / stdin. Only the raw-JSON path warns.

Also make scripts/smoke-test.sh exercise the whole new CLI input surface
end-to-end against the built binary: every tool call migrated from raw JSON to
FLAG form (index_repository / search_graph / trace_path / get_graph_schema /
query_graph incl. all Cypher checks / get_architecture / search_code /
delete_project -- flag form passes the query as one argv token, so the old
JSON-escaping dance is gone), plus dedicated guards for an integer flag
(--limit / --depth), a bare boolean (--exclude-entry-points), a repeated array
flag (--semantic-query -> JSON array), piped stdin, --args-file, and per-tool
--help (RC 0 + shows a flag; unknown tool -> RC!=0). One raw-JSON call is kept
as the deprecation guard (asserts the warning fires; the flag form asserts it
does not).

Reproduce-first: reverting the warning makes the raw-JSON smoke assertion fail
("did not emit a deprecation warning"); restored -> green. Full smoke ALL
PASSED; C unit suite 5735/0 (unchanged -- only main.c + the smoke script).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Shane McCarron 64af0b8c7e feat(install): inject CMM code-discovery reminder into Claude subagents
Subagents spawned via the Agent tool do not fire SessionStart, so the
existing SessionStart reminder never reaches them — they start without the
codebase-memory-mcp code-discovery guidance and fall back to grep/file-read.

Register a Claude Code SubagentStart hook (matcher "*") that injects a leaner
variant of the protocol via JSON additionalContext, omitting the
index_repository step since the parent session has already indexed the
project. SubagentStart injects context only via a JSON object on stdout, not
plain text, so the generated script emits a static JSON literal — no runtime
escaping and no python3/jq dependency. Advisory only; installed and removed
alongside the SessionStart hook in install_claude_code_config.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Martin Vogel f7d6d34457 feat(diag): persistent memory trajectory + query-leak CI soak for #581
#581 (query-path memory growth) does not reproduce in our soak: a 10-min,
200-file query-only run shows RSS flat-to-shrinking and committed plateauing
(mimalloc already purges on free, purge_delay=0). Rather than ship speculative
mitigations for a leak we cannot measure, this adds the observability to find it
where it actually occurs, plus a continuous guard.

- diag: the diagnostics writer now appends a PERSISTENT NDJSON memory trajectory
  (one sample / 5s: rss, committed, peak fields, page_faults, fd, queries), kept
  on exit (rotates to a .1 generation past 8 MB) so users can send the time
  series post-mortem. The previous latest-snapshot file was overwritten every 5s
  and deleted on stop.
- ci: the soak now also runs a read-only query-leak leg (CBM_SOAK_MODE query-leak,
  no reindex/mutate) on every platform including Windows, so a regression that
  introduces a query-path leak is caught. soak-test.sh RESULTS_DIR is now
  env-overridable so both legs keep separate artifacts.
- docs: README Troubleshooting and Diagnostics section + a bug-report field
  explain the no-telemetry stance and how to capture and share the trajectory.

Build clean; unit suite 5714/0; trajectory verified to persist post-exit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 23:28:01 +02:00
Martin Vogel b7f1a71a6b style: apply clang-format to new mcp/ui/logging lines
The CI lint gate (scripts/lint.sh --ci, clang-format-20) flagged 7 files whose
new lines were not run through the project formatter. Reformat only those lines
(line wrapping / continuation alignment) — no behavior change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:49:17 +02:00
Martin Vogel 345425a1bb merge main into mcp protocol branch
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:34:18 +02:00
Martin Vogel d5414111b3 feat(ui): add i18n and index picker UX
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 21:21:32 +02:00
Martin Vogel 63ed6789df Merge pull request #656 from caioribeiroclw-pixel/fix/openclaw-mcp-config
Fix OpenClaw MCP config path
2026-06-28 21:05:38 +02:00
Martin Vogel d9d70656b4 fix(ui): cap graph rendering and index spawn
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 20:57:14 +02:00
Martin Vogel d30bccd772 fix(compat): include stdlib.h so getenv/_putenv_s compile on native ARM64 Windows
compat.h calls getenv (cbm_tmpdir) and _putenv_s (cbm_setenv/cbm_unsetenv) but
only included stddef.h and stdio.h. The x86-64 mingw toolchain pulled stdlib.h
in transitively, so it built there, but the aarch64 (CLANGARM64) include chain
does not — the calls became implicit declarations that conflict with the real
stdlib.h getenv type and fail to compile on native ARM64 Windows. Include
stdlib.h directly so the declarations are correct on every toolchain.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 14:59:25 +02:00
Martin Vogel 62a28fd36e fix(logging): use local log schema
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 13:05:05 +02:00
Martin Vogel 5b60045c55 fix(cpp): resolve implicit destructor, copy-ctor, and conversion-operator calls
Three C++ constructs invoke a member implicitly with no textual call node, so
the call walk produced nothing for the c-LSP to attach its resolution to
(callable=0, no CALLS edge):

  - delete p;        -> destructor   T::~T
  - T a = b;         -> copy/move ctor T::T
  - if (obj) { ... } -> conversion   T::operator bool

Synthesize a textual call sourced to the enclosing function at each trigger so
the lsp_{destructor,copy_constructor,conversion} resolution binds. The
destructor callee QN embeds the type (T.~T) which is not textually available
from `delete p`, so it joins via the reason gate (c_lsp stashes the operand
text); the other two join by callee short-name (the declared type / the
type-independent operator bool).

Two underlying defects surfaced and are fixed:

  - The conversion check evaluated the type of the `condition_clause` node
    (the `( expr )` wrapper, which has no type) instead of the inner
    expression, so `if (obj)` never resolved obj's type. Unwrap the clause.

  - A conversion operator's `operator_cast` node spans the full
    "operator bool() const" in this grammar (params + cv-qualifiers folded in),
    so the method was indexed under that malformed name and member lookups for
    "operator bool" missed. Add cbm_func_name_node_text to normalize it to
    "operator bool", shared by the defs and unified extractors so the def name
    and call-scope QN agree.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:55:27 +02:00
Martin Vogel f05f0df811 fix(logging): keep format selection local
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 09:42:03 +02:00
Martin Vogel 9b17927bf6 fix(php-lsp): resolve magic __call dispatch to the __call handler
$p->anything(1) on a class with a __call magic method emitted php_method_dynamic
to a synthetic <class>.anything target at confidence 0.20 — below the pipeline
join's 0.6 floor, and to a node that does not exist — so no CALLS edge ever
carried the strategy.

The call actually dispatches to the class's __call handler. Resolve to
<class>.__call (a real method node) at 0.85 (the dispatch is certain), and stash
the dynamic method name ("anything") in reason so the join binds it to the
textual call site whose callee differs from __call. Admit php_method_dynamic to
the reason-matched join (lsp_resolve.h), the same pattern used for func-pointer,
ctor-ref, and dict-dispatch resolutions.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 05:23:39 +02:00
Martin Vogel 9ef37e89be fix(py-lsp): resolve dict-dispatch calls funcs["a"](v) to the dispatch-table target
A Python call through a dict dispatch table, funcs["a"](v), produced no CALLS
edge at all: the call's function field is a subscript, which the textual
callee extractor did not handle, so no textual call existed for the py-LSP's
lsp_dict_dispatch resolution to attach to (callable=0, strategy absent).

Three coordinated changes, the same reason-field pattern used for func_ptr and
ctor refs:
- extract_calls: for a Python call whose function field is a subscript with an
  identifier base and a literal string key, emit the base identifier ("funcs")
  as the textual callee so a callable-sourced CALLS edge exists. Gated to that
  exact shape so other subscript calls (arr[i]()) are unaffected.
- py_lsp: the dict-dispatch resolution stashes the subscript base identifier in
  reason (the textual callee differs from the resolved target "foo"), via a new
  py_emit_resolved_call_reason; py_emit_resolved_call delegates with NULL.
- lsp_resolve: admit lsp_dict_dispatch into the reason-matched join so the
  resolution binds to the textual call site.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 04:34:54 +02:00
Martin Vogel c18e3dc304 fix(lsp): resolve Java constructor references via class node + reason-match
A constructor reference `ClassName::new` is extracted as a textual call to
`new`, but the LSP resolved it to a synthetic constructor QN with no graph
node, and the join keys on the callee short-name (`new` != the class/ctor
name), so both lsp_method_ref_ctor and lsp_method_ref_ctor_synth dropped
(callable=0).

Resolve the reference to the ClassName CLASS node (which exists, mirroring
the constructor-call handling) and stash the original textual callee `new`
in the resolved call's `reason` field; the join matches the textual `new`
call site on it, gated to the ctor-ref strategies. No struct/ABI change.
java_cs LSP reproductions 19->21.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 03:07:49 +02:00
Martin Vogel bef9ab08ff fix(lsp): resolve C/C++ function-pointer calls via the join (#func_ptr)
A function-pointer call `fp(args)` has textual callee `fp` (the pointer
variable), but c_lsp resolves it by data-flow to the pointed-to function
`target`. The pipeline join keys on the callee short-name, so `fp` != `target`
dropped the lsp_func_ptr/lsp_dll_resolve resolution and no edge formed.

Stash the original textual callee (`fp`) in the resolved call's `reason`
field (unused and NULL for resolved calls; already copied across all
cross-file paths) and have the join match the call site on it, gated to the
func_ptr/dll strategies so `reason` is never misread as an unresolved-call
diagnostic. No struct/ABI change. c_cpp LSP reproductions 11->12.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:38:49 +02:00
Martin Vogel 59b836e441 feat(logging): add operational server logs
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:32:05 +02:00
Martin Vogel beaa776ef1 fix(pipeline): emit HTTP_CALLS for external client calls masked by registry mis-resolution (#523)
A client call to an external HTTP library (`requests.get("/api/orders/{id}")`)
carries its service-pattern signal in the callee_name. But the registry
mis-resolves `requests.get` to a spurious builtin short-name match
(`builtins.dict.get` via "get", strategy unique_name) -- a non-empty,
non-HTTP QN. Both the empty-resolution fallback (checks the callee) and the
resolved-QN service check (checks the resolved QN) therefore miss it, the call
is dropped, no HTTP_CALLS edge is written, and cross-repo matching finds
nothing to link.

Detect the HTTP/ASYNC service pattern on the callee_name FIRST (before the
registry can mask it) and emit the edge to a synthesized route node. Applied
to both pipeline paths (pass_calls.c sequential + pass_parallel.c
resolve_file_calls). Cross-repo HTTP linking now works for external client
libraries whose source is not indexed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:04:36 +02:00
Martin Vogel 0d3e24c77a feat(mcp): add tool schemas and pagination
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 02:02:29 +02:00
Martin Vogel 04697c632c fix(pipeline): deny-wins-by-value for infra Route extraction (#521)
A config/upstream URL is often extracted as several string_refs at
different key_path granularities (full path, leaf key, flat/null). Because
the Route node is keyed by the URL VALUE, a value was minted if ANY
granularity passed the per-ref guard -- a denied full path
`registries.terraform-registry.url` was defeated by a sibling leaf `url`,
and a healthcheck `curl ... || exit 1` minted under the leaf key `test`.

Restructure infra-route extraction into two passes: pass 1 collects every
URL value denied under ANY of its refs (whitespace value = command, NULL
key = context-less, or upstream/config/healthcheck key); pass 2 mints only
values never denied. A config-only file now yields zero spurious routes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 01:46:51 +02:00
Martin Vogel 800476f284 feat(mcp): add structured tool metadata
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-28 01:37:41 +02:00
Caio Ribeiro 2358b3c452 Fix OpenClaw MCP config install path
Signed-off-by: Caio Ribeiro <caio.ribeiro.clw@gmail.com>
2026-06-27 23:17:49 +00:00
Martin Vogel e51c93554f fix(fqn): keep structural nodes from dir-module QN collision; correct dotfile module QN
A directory-based-module language (Go/Java) derives a file's module QN
from its directory: a root file's module == the project name and a file
in pkg/ has module proj.pkg -- exactly the QNs of the "Project" root and
the "pkg/" Folder structural nodes. The per-file Module def then clobbered
those nodes' labels through the gbuf upsert (last-write-wins on label),
turning Project/Folder into Module. Guard the upsert so a "Module" never
downgrades a "Project" or "Folder" -- the directory node is the package
container. One guard covers both the sequential upsert and the parallel
local-gbuf merge.

Separately, a dotfile such as .env had its leading dot stripped as an
"extension", leaving an empty stem whose module QN also collided with the
project root. Treat a leading dot as a dotfile marker (not an extension)
and drop it from the QN segment, so .env -> proj.env (a distinct module).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 23:13:01 +02:00
Martin Vogel b3b3a48998 fix(fqn): directory-based module QN for Java/Go (de-double) + nested-class enclosing
Java/Go QNs doubled the top-level class/package (proj.Outer.Outer.run; Go module
included the filename). Add cbm_fqn_module_source_lang / cbm_fqn_compute_source_lang
(dir-based module for Java/Go; legacy cbm_fqn_compute UNCHANGED for import paths),
route the single authority cbm.c:569 result->module_qn + the def/calls/pipeline
module derivations through them. Also fix unified compute_class_qn to prefix the
enclosing class for nested classes (was ignored) so def-QN == LSP caller_qn ==
calls-enclosing → lsp_outer_dispatch joins. Reproduce-first tests added; legacy
test_fqn.c/test_registry.c (generic helper) preserved. Per external second-opinion plan.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 21:12:07 +02:00
Martin Vogel 0849f28c3f fix: bulk parallel — Rust cross-crate, Python builtins, #607 reinstall, grammar reds
Four file-disjoint problem groups fixed in parallel, deployed together:
- G1 (lsp): wire the dead Rust cross-file LSP into pass_lsp_cross.c (CBM_LANG_RUST
  in has_cross_lsp + run_one, CBMLSPDef->CBMRustLSPDef conversion, Cargo manifest
  via cbm_cargo_parse threaded through) -> 8 in-crate Rust strategy reds + #56
  cross-crate (lsp_cross_crate disambiguation).
- G2 (lsp): the py typeshed registry already resolved builtins but no builtins.*
  graph node existed to target -> add py_builtins.c injecting builtin def nodes
  (len/str/list/dict + str.upper/list.append/...). Plus rescope the genuinely
  no-edge LSP markers (lsp_unresolved/_unindexed/external-import) to assert the
  accurate no-edge invariant (new inv_no_calls_edge_to_qn helper).
- G3 (cli): #607 — install now PRESERVES existing indexes by default + a
  --reset-indexes opt-in (cbm_install_handle_existing_indexes helper); uninstall/
  update unchanged; repro_issue607 retargeted to the helper.
- G4 (grammar): Swift struct (Swift uses class_declaration+declaration_kind),
  Nix function defs + a discovered Nix calls-extraction bug, Teal/Magma fixtures
  corrected. LLVM/NASM confirmed already green. (Meson/Nim flagged needs-decision.)

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 19:15:47 +02:00
Martin Vogel 49aab71489 fix(lint): clear cppcheck errors gating make-test (NULL-target guard + go.mod var shadow)
The CI lint job (cppcheck) failed, which SKIPS the gated make-test job:
- pass_parallel.c: emit_config_edge/emit_normal_calls_edge dereference target->id,
  but emit_service_edge can be reached with target==NULL on the #523 HTTP/ASYNC
  external-client bypass. cppcheck (ctunullpointer) flagged the potential NULL
  deref. Guard both emitters — a CONFIGURES/CALLS edge needs a real target.
- extract_defs.c: the go.mod require_spec local 'spec' shadowed the CBMLangSpec
  *spec parameter (cppcheck shadowArgument, treated as error here). Rename to
  req_spec. Full scripts/lint.sh --ci (clang-format-20 + cppcheck) now passes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 15:04:27 +02:00
Martin Vogel 9cbb1aa90f style: clang-format-20 sweep (marathon-touched files)
The CI lint uses clang-format-20 (scripts/lint.sh --ci CLANG_FORMAT=clang-format-20).
Grammar/LSP/struct/issue edits across these files weren't formatted with cf-20,
failing lint and gating the make-test job. Format with cf-20.1.8 to match CI.
No logic change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 14:34:57 +02:00
Martin Vogel ad33c9b359 fix(issues): #521 YAML config-URL Route dedup, #523 sequential external HTTP edge, #520 fixture project arg
#521: YAML leaf URLs were pushed to string_refs twice (once with NULL key_path
in handle_string_refs, once via the key_path-aware YAML walk); the NULL-key_path
copy bypassed the upstream/config-URL deny check and minted spurious Route nodes.
Skip YAML in handle_string_refs so the key_path walk is the sole authority.
#523: the sequential pass dropped calls to external/unindexed HTTP client libs
(requests.get) before the service-pattern check; classify on the raw callee_name
in the empty-resolution branch and emit the HTTP/ASYNC edge (target=NULL), mirroring
the parallel path, so cross-repo matching can link it.
#520: fixture omitted the required project arg to detect_changes (production
correct); pass project derived from the tmpdir.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 14:31:17 +02:00
Martin Vogel 554b171cf7 fix(graph): precise Struct label for Rust/Swift/D/Go structs (Option B)
Struct definitions were labelled 'Class', conflating a distinct kind. Emit
the precise 'Struct' label for Rust (struct_item), Swift/D (struct_declaration)
and Go (type_spec struct_type); C/C++/Obj-C struct_specifier stays 'Class'.

To avoid regressing type resolution, a struct must still behave as a type
everywhere a class does. Added a centralized predicate cbm_label_is_type_like()
(Class/Struct/Interface/Enum/Type/Trait) and routed every type-like consumer
through it: registry seeding (pass_definitions/pass_parallel/pipeline_incremental
kept in sync), class-resolution (pass_semantic/pass_parallel resolve_as_class),
Go interface satisfaction (collects Struct nodes), the cross-file LSP label map,
and the Go/Rust LSP type registrars. Class-only sites (protobuf services, K8s
kinds, the C-family LSPs) are left unchanged.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 13:54:51 +02:00
Martin Vogel 781456818f style: clang-format issue-batch additions (cypher/mcp)
Fixes the dry-run lint failure: the #627 cypher and #514/#520/#546 mcp
additions had lines exceeding the column limit. clang-format 22.1.1
(project version) wraps them; no logic change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 13:13:26 +02:00
Martin Vogel fc1b6cc72d fix(lsp): join type-aware method calls (C++ ::/->, Go receiver QN, templates, ctor)
The pipeline join (cbm_pipeline_find_lsp_resolution) dropped LSP strategies
when the textual callee carried a member/scope separator the bare-segment
helper did not recognise:

- C++ '::' (Math::square) and '->' (p->run): bare-segment split only on '.',
  so the textual callee never matched the LSP's dotted callee_qn short name.
  Split on the last of '.', ':' or '>' (the latter only when closing '->'),
  recovering the method on both QN and textual sides.
- C++ explicit template calls f<T>(): the 'function' field is a
  template_function whose 'name' child is the bare callee; emit it so a
  textual callee (hence a CALLS edge for lsp_template) exists.
- Go methods: enclosing_func_qn was package.Method, but the textual extractor
  and registry qualify methods as package.Type.Method, so caller_qn never
  matched and every in-method call lost its type-aware strategy. Derive the
  receiver type (unwrapping *Type) and build package.Type.Method.
- C++ 'new Foo()': resolve to the existing Foo class node when the class has
  no user-declared constructor (no Foo.Foo node), mirroring java_lsp.c, so a
  CALLS edge carrying lsp_constructor still forms.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 12:26:46 +02:00
Martin Vogel 3f754cf449 fix: 17 reproduce-first issue + new-bug fixes (per-case verified)
Each was RED (reproduce-first). Verdicts: production bug unless noted.

- #510 nested .gitignore: load root .gitignore unconditionally (discover.c).
- #403 IDE dir: add "Antigravity" to ALWAYS_SKIP_DIRS.
- #571 CJK project name: transliterate non-ASCII bytes to hex in fqn.c (was -> "-").
- #520 detect_changes: include untracked files (git status --porcelain -uno) (mcp.c).
- #363 CBM_MEM_BUDGET_MB env override (mem.c).
- #570 Codex dual-hook: write the hook to hooks.json when present (cli.c).
- #431 VSCode per-profile mcp.json (cli.c).
- #521 routes from config URLs: deny upstream/jwks/healthcheck/... keys (pipeline.c).
- #548 D:\ browse: normalize sep before is_dir + drive-root self-parent (http_server.c);
  TEST A was a broken fixture (called is_dir on backslash) -> corrected to the
  production sequence; assertion unchanged.
- #495 Rust cfg-gated twins: fold #[cfg(...)] into the QN (extract_defs.c).
- new_py_tuple_unpack: handle pattern_list/tuple_pattern LHS -> Variable per id.
- new_ts_class_field_arrow: class arrow-field -> Method + scope (extract_defs.c).
- repro_extraction/#554 C++ out-of-line: compute_func_qn class-qualifies the call's
  enclosing QN via cbm_cpp_out_of_line_parent_class (extract_unified.c + helpers.h).
- #523 cross-repo HTTP_CALLS: emit service-pattern edge with target=NULL when the
  client lib is unindexed (pass_calls.c, pass_parallel.c).
- #514 data_flow arg expressions: serialize "args" on CALLS edges + propagate
  through store BFS + mcp data_flow (pass_calls.c, store.h/.c, mcp.c).
- #627 query_graph: size_t malloc + NULL-check; OPTIONAL MATCH from bound terminal
  so WHERE c IS NULL returns orphans correctly (cypher.c).
- #546/#480 trace_path: union BFS across same-name nodes (.d.ts dup) (mcp.c).

DEFERRED (need a maintainer decision, untouched): #56 cross-crate Rust (needs Cargo
workspace plumbing / Rust cross-LSP — architectural), #607 reinstall-deletes-index
(cbm_remove_indexes is shared with uninstall/update where deletion is intended).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 11:59:59 +02:00
Martin Vogel 9ebddb718a fix(cypher): LIMIT 0 returns 0 rows instead of all rows
The return-clause `limit` was zero-initialized (calloc) and every guard tested
`limit > 0`, so an explicit `LIMIT 0` was indistinguishable from "no LIMIT
clause" and returned ALL rows. Standard Cypher treats `LIMIT N` as an upper
bound, so `LIMIT 0` must return zero rows.

Default `limit` to the sentinel -1 ("no limit"); the parser still sets 0 for an
explicit `LIMIT 0`. Change the truncation guards (rb_apply_skip_limit,
bindings_skip_limit) and the apply call to `limit >= 0`, so limit 0 truncates to
0 while -1 returns all. The proj_cap optimization stays `> 0` (LIMIT 0 falls
through to full projection, then the final truncation yields 0).

Reproduced by repro_new_cypher_limit_zero (was RED): `RETURN ... LIMIT 0` must
yield row_count == 0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 02:20:47 +02:00
Martin Vogel f8a7e8b715 fix(lsp): join method-dispatch resolutions by bare callee short-name
The LSP-resolution join compared the LSP's class-qualified callee short-name
(e.g. "inc" from "Counter.inc") against the call's RAW callee_name, which is
receiver-qualified for method/qualified calls ("c.inc", "A.Helper", "c.Add").
That comparison always missed for method dispatch, so every type-aware LSP
resolution (lsp_type_dispatch, cs_static_typed, lsp_method_dispatch, ...) was
silently dropped and the call fell through to the weaker textual registry
resolver — the LSP strategy never reached the CALLS edge's properties_json.

Reduce the call's callee_name to its bare last dot-separated segment before the
comparison, mirroring what is already done to callee_qn. The LSP has already
performed receiver->type resolution, so matching bare method names is correct;
free-function calls (already bare) are unaffected. This lets the more accurate
type-aware LSP resolution win over textual registry matching for method calls.

Reproduced by the repro_lsp_* per-pass strategy suites (java/cs/go/rust/kotlin/
php/ts), which asserted the LSP strategy is present on a CALLS edge and were RED.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 01:40:41 +02:00
Martin Vogel 639bac9c5d fix(mcp): back up corrupt DB instead of silently deleting it (#557)
Data-loss fix: resolve_store() unlink()'d a project DB (+WAL/SHM) when
cbm_store_check_integrity() failed, destroying the user's graph with no recovery.
Rename the DB to <path>.corrupt instead (clearing any prior backup first so rename
succeeds on Windows; fall back to unlink only if rename fails). Re-index rebuilds a
fresh DB. Flips repro_issue557 green (DB preserved / backup exists).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-26 23:49:33 +02:00
Martin Vogel 9c17b79aba fix(discover): skip .claude-worktrees during indexing
QUALITY_ANALYSIS gap #1: .claude-worktrees was indexed (nearly tripling this
repo's indexed surface + polluting queries with stale duplicate symbols).
Discovery skipped .claude and .worktrees but not .claude-worktrees. Add it to
ALWAYS_SKIP_DIRS. Flips the discovery-hygiene invariant green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-26 22:10:02 +02:00
Martin Vogel 66b3e3004b Merge pull request #614 from linze0721/berserk/fd632085-2b69-4b14-930b-47025d80d973
fix: get_architecture should honor path scope
2026-06-25 21:18:49 +02:00
Martin Vogel 677582ccf8 Merge pull request #539 from RithvikReddy0-0/fix/516-adr-persistence
fix(pipeline): preserve ADR across full re-index (#516)fix(pipeline): preserve ADR across full re-index (#516)
2026-06-25 21:18:33 +02:00
saurav kumar 078e9e2d85 Merge branch 'main' into fix/json-escape 2026-06-24 20:36:59 +05:30
Saurav Kumar fdcafbb53f fix(foundation): properly escape JSON control characters as \u00XX
Signed-off-by: Saurav Kumar <sauravsk2507@gmail.com>
2026-06-24 20:33:52 +05:30