Addresses review on #539.
- Free p->saved_adr in cbm_pipeline_free so it is not leaked on error
paths that exit before the restore in dump_and_persist_hashes (e.g. a
cbm_gbuf_dump_to_sqlite failure).
- Check cbm_store_adr_store's return value on restore and log an error
instead of silently dropping the ADR (the original #516 symptom).
- Add reproduce-first test pipeline_adr_survives_full_reindex: index,
store an ADR, force a full re-index by adding files, assert the ADR
survives unchanged. Passes against the fix.
Signed-off-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
manage_adr stores ADRs in project_summaries, but a full re-index
(triggered by file changes or new files) deletes the DB in
try_incremental_or_delete_db and rebuilds it from the graph buffer,
which writes an empty project_summaries table. file_hashes were
re-persisted after the rebuild but project_summaries were not, so the
ADR was silently lost.
Fix: capture the ADR before the DB is unlinked, stash it on the
pipeline struct, and restore it after the rebuilt DB is reopened in
dump_and_persist_hashes. The incremental path is unaffected (it never
rewrites the DB). Verified: ADR now survives a full re-index.
Signed-off-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
Under -D_GNU_SOURCE with optimization, glibc's string.h macros make
strstr() on a `const char *` (with a constant needle) yield a
`const char *`. gcc 15 then rejects assigning that to `char *` with
-Werror=discarded-qualifiers, breaking the build:
src/cli/cli.c:1503: error: initialization discards 'const'
qualifier from pointer target type [-Werror=discarded-qualifiers]
Minimal repro (gcc 15.2):
const char *c = "hello";
char *p = strstr(c, "ell"); // gcc -D_GNU_SOURCE -O2 -Werror=discarded-qualifiers
codex_hook_strip only reads through begin/end/cut (offset math and
byte comparisons, never writes), so making them `const char *` is the
correct fix and preserves the function's existing const contract
(`content` is already const). The malloc'd output buffer stays char *.
No behavior change; build succeeds on gcc 15.2.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Jeremy Maserang <gothuncc@gothaunty.com>
Route QNs (__route__METHOD__/path) are the rendezvous key for joining a client
HTTP_CALLS to the server handler that serves it, but they were built from the raw
path. Frameworks write path parameters with different placeholder syntaxes
(":id" Express/clients, "{id}" Axum/Spring/OpenAPI, "<id>" Flask/Rocket), so a
client call and its handler produced different QNs and never joined. Static
routes joined; parameterized routes (most of a REST surface) did not.
Add cbm_route_canon_path() which collapses each parameter token (":name",
"{name}", "<name>", "${...}") to a single "{}" token, and apply it to the path
at every HTTP-route QN construction site (pass_route_nodes, pass_parallel,
pass_calls, pass_cross_repo). Broker/async/infra QNs are left untouched. Only the
QN (identity/dedup key) is canonicalized; the Route node display name keeps the
original path. Parameter names are intentionally discarded so the same logical
endpoint matches across services that name the variable differently.
Adds tests/test_route_canon.c (11 cases). Fixes#505.
Signed-off-by: imateusdev <imateusdev@gmail.com>
On Windows struct stat::st_size is a 32-bit long, so any .db larger than 2 GB
makes stat() fail with EOVERFLOW. Three call sites in src/mcp/mcp.c checked
.db files this way and silently mishandled the > 2 GB case: list_projects
dropped the project from the UI list, maybe_auto_index re-indexed it on every
server start, and try_artifact_bootstrap re-imported it from the artifact.
Replace all three with the existing cbm_file_size (platform.c), which is
wide-char + 64-bit clean on Windows (GetFileAttributesExW) and returns int64_t
on both platforms, so the size never rides through the 32-bit st_size.
Signed-off-by: Dusk_NM02 <dusk.11th@outlook.com>
cbm_store_node_degree backs the MCP get_code_snippet callers/callees
counts and the Cypher in_degree/out_degree virtual property, which are
deliberately CALLS-scoped. Restore it to CALLS-only and scope the
INHERITS/IMPLEMENTS broadening to cbm_store_search alone, which is the
search_graph surface issue #558 is about. Adjust the new tests so
node_degree assertions expect CALLS-only counts while the search_graph
result assertions expect the broadened structural degree.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
search_graph reported in_degree/out_degree of 0 for Class nodes that
have real INHERITS edges, because the degree computation in both
cbm_store_node_degree (Cypher virtual properties) and cbm_store_search
(result population and min_degree/max_degree filter) only counted CALLS
(and USAGE) edges. A min_degree filter therefore silently excluded
well-connected classes.
Broaden the edge-type predicate in both surfaces to
IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS') so search_graph and
Cypher report the same edge-type-agnostic degree and stay consistent.
Add regression tests covering: a parent class with N INHERITS children,
combined CALLS+INHERITS degree without double counting, min_degree now
including inheritance-only classes, and isolated nodes still reporting
zero.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
cbm_gitignore_merge copied src patterns into dst one strdup at a time and
returned void on failure, leaving dst with a partial set of patterns and no
signal to the caller. If the dropped pattern was the critical exclude (the
.git/info/exclude entry that prevents an OOM walk), the bug could silently
recur with some patterns applied and others dropped.
Make the merge atomic: on any allocation failure roll back the partial copies
so dst is left exactly as it was, and return bool so callers can tell. The
.gitignore caller now degrades gracefully (exclude patterns skipped, as if the
file were absent) instead of ending up with a half-merged matcher.
Adds a reproduce-first test that injects a strdup failure mid-merge via a test
seam and asserts dst is unchanged; it is red without the rollback.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
- Declare cbm_artifact_export_last_error() in artifact.h and drop the ad-hoc
forward declarations in pipeline.c and test_artifact.c so there is a single
shared declaration.
- Document that dump_and_persist_hashes now intentionally propagates a failed
artifact export as a pipeline error (previously silently ignored), so the
behavior change is recorded in-tree.
- Use MoveFileExA(MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH) for the
Windows replace in write_file_atomic instead of unlink+rename; it replaces
the destination atomically (credit @Ayush7Ranjan, from #492).
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
The #334 plausibility gate compares committed (extracted) node count
against persisted rows, but committed_nodes was read AFTER
cbm_gbuf_dump_to_sqlite(), which calls release_gbuf_indexes() and frees
node_by_qn. cbm_gbuf_node_count() therefore returned 0, committed_nodes
fell at/below CBM_DUMP_VERIFY_MIN_FLOOR, and the gate never fired (a
healthy index reported expected_nodes:0 while expected_edges was correct,
since edges.count survives the dump).
- pipeline.c: capture committed node/edge counts before the dump.
- pipeline_incremental.c: set committed counts on the incremental path too
(via new cbm_pipeline_set_committed_counts), so the gate covers
incremental/reparse reindexes — the scenario #334 actually reproduces.
- test_pipeline.c: drive the real pipeline and assert committed_nodes > 0
and == persisted (the existing dump_verify tests fed synthetic counts and
could not catch the capture-ordering bug).
Signed-off-by: soren <nguiasoren@gmail.com>
Drop the repo-local .git/config pass in resolve_global_excludes_path() so
an untrusted repo cannot point core.excludesFile at an arbitrary file (e.g.
~/.ssh/id_rsa) and have cbm open and read it merely by being indexed. git is
safe here because it only trusts a repo's .git/config after a deliberate
clone/init; the indexer has no such trust step. Global excludes now resolve
only from the user-owned ~/.gitconfig and $XDG_CONFIG_HOME/git/config. A
repo's own ignores remain handled by .gitignore / .cbmignore.
Add a guard test asserting a repo-local .git/config whose excludesFile points
outside the repo is ignored, not read.
Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Compare persisted SQLite node counts to in-memory dump counts after
index_repository completes so partial WAL/durability loss surfaces as
status:"degraded" instead of silent indexed.
Signed-off-by: Sam Li <yangsec888@gmail.com>
trace_path mode=cross_service resolved its default edge-type list to
{HTTP_CALLS, ASYNC_CALLS, DATA_FLOWS, CALLS} — none of the CROSS_* edge
types the cross-repo pass actually writes. Since cbm_store_bfs filters
edges with an exact `WHERE e.type IN (...)` clause, cross-service traces
silently skipped every cross hop even when the edge was present and
visible via query_graph.
Add the six CROSS_* types (CROSS_HTTP_CALLS, CROSS_ASYNC_CALLS,
CROSS_CHANNEL, CROSS_GRPC_CALLS, CROSS_GRAPHQL_CALLS, CROSS_TRPC_CALLS)
to the cross_service default list — exactly the set pass_cross_repo.c
emits and cross_repo_delete_edges() cleans up. The count is now computed
from the array size (sizeof) so it can't drift; the dead MCP_N_DEFAULTS_4
constant is removed. Tool description updated to match.
Adds integ_mcp_trace_path_cross_service: seeds a CROSS_HTTP_CALLS edge
between two indexed functions with no CALLS relationship and asserts
cross_service surfaces the hop while calls mode does not.
Signed-off-by: kirilklein <kikl@di.ku.dk>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(pipeline): persist call-site line on CALLS edges
Persist CBMCall.start_line as a "line" property on CALLS edges in both resolution paths (finalize_and_emit + calls_emit_edge), so call-site lines are queryable instead of needing grep. Scoped to CALLS only: route/config edge props feed full-only predump passes the incremental path does not re-run, so changing them desyncs full vs incremental indexing. Verified: full suite matches unpatched main; line present on CALLS both paths.
Fixes#503
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
* test(pipeline): assert CALLS edge carries call-site line
Indexes a Go fixture where Helper() is called on line 8 and asserts the Main->Helper CALLS edge persists a line property (line:8) in its JSON props.
Refs #503.
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
---------
Signed-off-by: alexisperinger-ux <alexis.peringer@iss-stoxx.com>
* fix(discover): honor .git/info/exclude during file discovery
index_repository only loaded .gitignore when building the exclusion
matcher. .git/info/exclude — the per-clone exclude file that Git treats
as authoritative — was never read, so paths excluded only there were
walked in full. On repos with Sandcastle worktrees this caused the
indexer to traverse 661K files / 170 GB and OOM (issue #489).
Add cbm_gitignore_merge() to append patterns from one matcher into
another. In cbm_discover_ex, after loading .gitignore, also load
.git/info/exclude and merge its patterns in. If only the exclude file
exists, use it directly. No downstream call paths change.
Add three unit tests for cbm_gitignore_merge and two integration tests
that reproduce the issue scenario (exclude-only and exclude-stacked-
with-gitignore).
Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
* style(discover): fix clang-format alignment in cbm_gitignore_merge
Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
---------
Signed-off-by: ShauryaaSharma <shauryasofficial27@gmail.com>
* fix(search): ignore empty-string label in search_graph name_pattern path
search_where_basic guarded the label filter on NULL only (`if (params->label)`),
so a search passing label="" appended a literal `n.label = ''` clause that
matches no node — silently returning zero results. The BM25 query path already
treats an empty label as absent, so name_pattern/qn_pattern searches and BM25
searches behaved inconsistently for the same `"label":""` payload (issue #481):
agents fell back to grep because structural class/service discovery returned
nothing.
Guard the clause on `params->label && params->label[0]` so an empty-string
label is a no-op, identical to an omitted label and to the BM25 path.
Adds store_search_empty_label_ignored: name_pattern + label="" returns the same
match as name_pattern alone, while a non-empty label still filters.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(search): address QA round 1 — cover qn_pattern + empty label
QA round 1 noted store_search_empty_label_ignored asserted only the
name_pattern path though the comment referenced qn_pattern too. Add a
qn_pattern + label="" assertion so a future change that special-cased the
qn_pattern branch can't silently reintroduce the empty-label bug. Both paths
share search_where_basic, so this locks in the shared guard.
Test-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(resolver): disambiguate qualified cross-file calls by full namespace tail
cbm_registry_resolve reduced a package/namespace-qualified callee
(Foo::Bar::sub) to its bare simple name before the multi-candidate name
lookup. When that bare name was defined in several packages, the bare-name
scorer (best_by_import_distance) picked one winner deterministically and
routed every caller to it — collapsing distinct packages' same-named subs
onto a single definition and orphaning the rest. Impact is heaviest for Perl,
which has no cross-file LSP and relies entirely on this generic resolver, but
the defect applies to any "::"- or "."-qualified callee.
Add qualified_suffix_match: when a callee is qualified and its bare name has
multiple candidates, pick the sole candidate whose qualified tail matches at a
segment boundary (normalizing "::" to "."). Returns the unique match with a new
high-confidence "qualified_suffix" strategy; falls through to the existing
bare-name scoring when zero or several candidates match. Language agnostic —
bare callees contain no separator and are unaffected.
Adds resolve_qualified_disambiguates_same_name covering per-package routing of
a name defined in three packages, plus the bare-call (no-disambiguation) case.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(resolver): address QA round 1 (qualified-suffix edge coverage)
Harden resolve_qualified_disambiguates_same_name and add a new test per the
QA round 1 advisory notes, guarding the fall-through paths against silent
regression:
- dotted callee form (App.Beta.save) resolves via qualified_suffix
- qualified callee matching no candidate tail falls through (not qualified_suffix)
- resolve_qualified_ambiguous_tail_falls_through: two candidates sharing the
same qualified tail are ambiguous → fall through to bare-name scoring
Test-only; no behavior change.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(perl): stop extracting config strings as call targets
The Perl branch of extract_scripting_callee blindly returned the text of
child(0) of every call node. In config-heavy Perl (.cgi/.pl with embedded
log4perl-style config), tree-sitter-perl misparses dotted config tokens
(e.g. "log4perl.appender.File.utf8") into call-shaped nodes, and that
dotted string was emitted as a callee_name, later matched by the generic
short-name resolver to unrelated project subs.
Now the Perl branch pulls the real name token (method/function field, else
child(0)) and validates it as a bare Perl sub/method identifier via
perl_is_identifier_callee: must start with a letter or '_' and contain only
[A-Za-z0-9_:] (allowing the '::' package separator). Any '.', whitespace,
quote, or '/' disqualifies it and NULL is returned so no CALLS edge forms.
Gated to CBM_LANG_PERL; other languages are untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(resolver): don't match Perl builtins to project subs
Perl builtins (push/shift/keys/sprintf/...) carry no language or call-kind
awareness through the generic name-matcher in cbm_registry_resolve. When a
project defines a sub whose name collides with a builtin, an invocation of
the builtin was wired to that sub by same-module / suffix matching - a
false-positive CALLS edge.
Adds cbm_perl_is_builtin (curated, sorted bsearch set of 94 perlfunc core
builtins) and applies it in both call-resolution passes (sequential
resolve_single_call and parallel resolve_file_calls), gated on the file
language == CBM_LANG_PERL and only AFTER LSP resolution has declined, so a
genuine LSP-resolved call is never suppressed. The file language is threaded
into both resolvers via a new trailing CBMLanguage parameter; every other
language reaches cbm_registry_resolve unchanged (byte-identical behavior).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(resolver): suppress generic short-name matching for Perl method calls
A Perl method call ($obj->m / Class->m) carries no receiver type at the
structural tier, so the generic short-name matcher in cbm_registry_resolve
would wire $dbh->commit / $cgi->param / $logger->log to any project sub
sharing the bare method name - the dominant source of false-positive CALLS
edges in CPAN/framework-heavy Perl. Resolving such a call correctly is the
LSP's job, not the bare-name matcher's.
Adds a CBMCall.is_method flag (zero-init false, so all other languages and
existing call sites are unaffected). method_call_expression is added to the
Perl call node set and handle_calls sets is_method=true only for that node
type when the file language is Perl. Both call-resolution passes then skip
generic resolution for Perl method calls (combined with the builtin guard
from the prior commit). Genuine intra-project function calls (non-method,
non-builtin) still resolve as before. LSP-resolved method calls are
unaffected because the guard runs only after LSP resolution declines.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(perl): cover call-graph noise fixes (builtins, methods, config strings)
Hermetic tests for the three Perl call-graph noise fixes:
test_extraction.c (extraction tier):
- config string is never emitted as a callee; genuine call still extracted
- builtin calls (push/keys) extracted but never flagged is_method
- arrow/method calls ($self->commit / $dbh->commit) set is_method=true,
while the genuine function call (helper) does not
- a JS method call never sets is_method (flag is Perl-only — other
languages unaffected)
test_registry.c (resolver tier):
- cbm_perl_is_builtin recognizes core builtins (incl. first/last of the
sorted set) and rejects project subs, case variants, empty, and NULL
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(perl): address QA round 1
Round 1 (Claude panel + DO DeepSeek) findings:
- Rework the Perl noise guard so it suppresses only WEAK generic matches
(suffix_match/unique_name) and KEEPS high-confidence same_module/import_map.
The prior guard ran before cbm_registry_resolve and dropped genuine same-file
calls to builtin-named subs (e.g. a project sub log/index/open called as a
bare function) that pre-PR resolved via same_module. Extracted the decision
into pure, unit-tested cbm_perl_suppress_generic_match() shared by the
sequential (pass_calls.c) and parallel (pass_parallel.c) resolvers; corrected
the inaccurate comments (Perl has no LSP/textual stage before the guard).
- Tighten perl_is_identifier_callee to require '::' pairs (reject a lone ':',
':::', or trailing '::').
- Add resolver-contract tests covering weak-match suppression, same_module/
import_map retention, genuine-call survival, non-Perl no-op, and NULL strategy.
Verified on a real Perl monorepo: .cgi builtin/CPAN/config-string noise stays
eliminated while same_module edges to builtin-named subs are recovered.
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(perl): address QA round 2
Round 2 (Claude panel) caught a regression introduced by the round-1 refactor:
cbm_perl_suppress_generic_match whitelisted only the exact strategies
"same_module" and "import_map", but resolve_import_map can also return
"import_map_suffix" (confidence 0.85 — a genuine import-based resolution, not a
weak short-name guess). A '::'-qualified Perl builtin/method call resolved via
the import-suffix fallback was therefore dropped, contradicting the helper's
documented contract and partially missing acceptance criterion (d).
Add import_map_suffix to the kept (high-confidence) set so only the weak
short-name strategies (suffix_match / unique_name) are suppressed; update the
doc comment and add a unit-test case asserting import_map_suffix is retained.
Deferred as advisory (non-blocking, noted on the PR): a hypothetical leading-'::'
(main:: shorthand) under-extraction in perl_is_identifier_callee, and a
colon-edge-case coverage gap (logic correct by inspection).
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): copy and sign the built binary on install instead of re-signing the stale target
`install` (especially with --force) only ad-hoc-signed whatever already sat at
~/.local/bin/codebase-memory-mcp and never copied the binary the operator ran
install from. An operator who built from source and ran `install -y --force`
was silently left with the OLD binary at the target — running stale code while
believing they had upgraded (#472).
Install now mirrors `update`: detect the running binary, copy it to the
canonical target (skipping the copy when we already ARE the target, which would
otherwise truncate the running file), make it executable, ad-hoc sign the
TARGET, and point all agent configs at the target. A different pre-existing
binary is only replaced with --force or interactive confirmation; --dry-run
reports the intended swap without touching anything.
The copy+sign also resolves the secondary report: a hand-copied clang build is
linker-signed (flags=0x20002) and gets Killed:9 when spawned by an MCP host —
re-signing ad-hoc (flags=0x2) makes it launchable, which install now does for
the placed binary.
Adds cbm_copy_binary_to_target() (with a same-file guard) plus regression tests
covering the binary swap and the self-copy guard.
Closes#472
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* fix(cli): address QA round 1
Extend cli_install_same_file_guard_issue472 with a symlink case: two distinct
path strings resolving to the same inode (which a non-canonical
cbm_detect_self_path vs the hardcoded target can produce) must also be detected
as same-file and skipped, not truncated. POSIX-only (#ifndef _WIN32).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
* test(cli): guard install copy exec-bit assertion to POSIX
cli_install_copies_binary_to_target_issue472 asserted the executable bit on the
copied target, but cbm_copy_binary_to_target only chmods on POSIX (the exec bit
is not meaningful on Windows, and MinGW stat() derives it from the file
extension). Wrap the stat/S_IXUSR assertion in #ifndef _WIN32 so test-windows
passes; the content and creation assertions still run on all platforms.
Refs #472
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
---------
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Incremental re-indexing dropped inbound cross-file edges (CALLS, USAGE,
...) whose source lived in an unchanged file. cbm_pipeline_run_incremental
purges a changed file's nodes -- the cascade deletes every edge referencing
them, including inbound edges from unchanged callers -- but only re-resolves
the changed files, so those edges were never regenerated. The graph
silently diverged from a full reindex on every edit (and degraded under the
auto-sync watcher); e.g. a cross-file `Serve -> Help` CALLS edge vanished
when only the callee's file was edited.
Fix (two parts):
- Snapshot inbound cross-file edges into changed files before the purge,
keyed by endpoint qualified_name (stable across re-parse), and re-link
them after re-resolution + post-passes. insert_edge dedups; a target
whose qn no longer exists (deleted/renamed) is dropped, matching
full-reindex semantics. Edge types recomputed by post-passes that do not
run incrementally (SIMILAR_TO, SEMANTICALLY_RELATED, FILE_CHANGES_WITH,
DATA_FLOWS) are skipped so a stale snapshot cannot introduce an edge a
full reindex would not produce.
- registry_visitor now seeds the resolver with only the definition labels
the full index seeds -- {Function, Method, Class, Interface, Variable,
Field} (mirrors pass_definitions.c) -- instead of also seeding container
nodes (File/Module/Folder). Previously a type usage like `Word` resolved
to the same-named Module node instead of the Class node, diverging from
full.
Verified: an incremental reindex now produces a byte-identical structural
edge set to a fresh full reindex across 8 edit shapes (comment, body,
caller-file, two-file, add-function, model, view, 4-round iterative). Adds
a reproduce-first regression test that fails before this change (the
cross-file Serve -> Help edge is dropped) and passes after.
Signed-off-by: win4r <win4r@outlook.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A call carrying enough long arguments drove append_args_json()'s running
position past the fixed CBM_SZ_2K `props` stack buffer in
emit_normal_calls_edge(): format_call_arg() returns snprintf's *untruncated*
length, so `pos += (size_t)n` could exceed `bufsize`, after which the
trailing `buf[pos] = '\0'` (and `buf[pos++] = ']'`) wrote out of bounds. The
stack canary caught it as SIGABRT, so full-repo indexing of large TypeScript
codebases crashed the server in the parallel resolve pass
(emit_service_edge -> emit_normal_calls_edge -> finalize_and_emit ->
append_args_json). Confirmed with AddressSanitizer:
stack-buffer-overflow WRITE at pass_parallel.c:1124, 'props' (2048 B).
Fix: when an argument does not fully fit, roll back to before its separator
and stop appending (atomic field, matching append_json_string's behaviour),
so `pos` can never advance past the buffer.
Add regression test parallel_args_json_no_overflow: indexes a fixture whose
single call carries 60 long string args (args JSON well past 2 KB); under the
ASan test build it aborts without this fix and passes with it.
Signed-off-by: Andrius Skerla <1492322+rainder@users.noreply.github.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>