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>
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>
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>
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>
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>
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>
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>
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>
#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>
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>
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>
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>
$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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
#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>
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>
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>
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>
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>
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>
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>
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>