Commit Graph
387 Commits
Author SHA1 Message Date
Andrew Hundt bc73e8e889 test(arch): analytics work across a polyglot graph (#36)
Contract test confirming the graph-analytics chain (PageRank + architecture)
runs on a multi-"language" graph (nodes from .py/.ts/.go files with
cross-language call edges). Analytics are language-agnostic (graph-level), so
this complements the language-agnostic pagerank/arch unit tests + the
per-language LSP tests: it asserts the CHAIN runs across the polyglot case
(PageRank ranks all 6 nodes; architecture returns OK). language_count is not
asserted — that depends on language metadata populated at real-index time, not
the analytics. store_arch 53 pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 22:59:27 -04:00
Andrew Hundt 1ad7556fac fix(graph_buffer): retain DB on relative root_path (was false-deleted) (#57)
The post-dump verify (B1 mitigation, 63de254) used the strict
cbm_store_check_integrity, which treats a non-absolute root_path (e.g. a
relative repo_path like ".") as bad_root_path → the verify DELETED a valid DB.
Indexing with a relative path therefore failed (status=error) — caught by
self-indexing the repo (cbm cli index_repository repo_path="."); the same
index with an absolute path succeeded cleanly (14723 nodes).

Switch the verify to cbm_store_check_integrity_full with the path_only
out-param: a path-only defect (cosmetic root_path) is RETAINED, consistent
with #557 resolve_store — only genuine structural corruption (>5 project
rows) is deleted. Verified: relative-path index now returns status=indexed;
absolute-path index unchanged.

test_graph_buffer gbuf_dump_relative_root_path_retained locks this in.
graph_buffer 50 pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 22:37:03 -04:00
Andrew Hundt 29025375a6 test: validation — ranking across all sort modes + config-knob clamps (#49)
- store_prefix_ranks_project_above_dep_all_sort_modes: proves dep-last
  ranking holds for EVERY sort_by (relevance/name/degree/calls/linkrank),
  not just the default. Dep inserted first (lower id) so a missing dep-last
  in any mode would let the dep win the id tiebreak — a real per-mode check.
- pagerank_invalid_inputs_clamp_cleanly: negative/>1 damping, negative/zero
  epsilon, negative/zero max_iter all clamp to defaults and produce finite
  ranks summing to ~1 (guards the #21 config knobs against bad values, and
  complements the #44 NaN fix).

Green: tool_consolidation 88 / pagerank 45.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 21:58:00 -04:00
Andrew Hundt 76565f3592 feat(architecture): expose Leiden resolution as a config-tunable knob (#41)
cbm_store_get_architecture hardcoded Leiden resolution (gamma) = 1.0 at both
cbm_leiden call sites (arch_clusters + the cbm_louvain wrapper). gamma
controls cluster granularity (>1 -> smaller/more clusters, <1 -> larger).

- store: add a `double leiden_resolution` param to cbm_store_get_architecture
  (after hotspot_limit, matching its existing config-threaded-param pattern)
  and to arch_clusters; cbm_louvain keeps its 1.0 default (separate low-level
  API). NaN/non-positive clamps to 1.0 (same hardening as the PageRank NaN
  fix, #44).
- mcp: CBM_CONFIG_ARCH_RESOLUTION ("architecture_resolution") read via
  cbm_config_get_double (default 1.0) and passed through handle_get_architecture.
- test_store_arch: arch_clusters_resolution_knob verifies the knob is threaded
  (multiple resolutions succeed + valid output) and NaN clamps. 14 existing
  callers updated to pass 1.0 (compiler-enforced). store_arch 52 pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 21:27:12 -04:00
Andrew Hundt 1087dfd846 fix(pagerank): reject NaN damping/epsilon in the range clamp
cbm_pagerank_compute's clamp used `damping < 0 || damping > 1` and
`epsilon <= 0`, both of which IEEE-754 NaN bypasses (every NaN
comparison is false). Since damping/epsilon became config-tunable and
cbm_config_get_double uses strtod (which parses "nan"), a
`config set pagerank_damping nan` would let NaN propagate through the
power iteration: every rank becomes NaN, the convergence check
`delta < epsilon` is always false (runs full max_iter), and the pagerank
table is silently corrupted, poisoning the search relevance sort.

Switch to the inverted-range form `!(damping >= 0 && damping <= 1)` and
`!(epsilon > 0)`, which reject NaN because the inner comparison is
false. Found by the silent-failure review agent; verified independently
(strtod parses "nan"; NaN bypassed the old clamp).

test_pagerank.c pagerank_nan_damping_is_clamped: a NaN damping must
yield finite ranks summing to ~1. Green: pagerank 44 passed.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 19:39:26 -04:00
Andrew Hundt 3c619e767e feat(pagerank): expose damping + epsilon as config knobs
cbm_pagerank_compute_with_config already read max_iter and the 12
edge-weight keys from config, but damping (0.85) and epsilon (1e-6) were
passed as hard-coded #defines -- the two core convergence params were
not tunable.

- pagerank.h: CBM_CONFIG_PAGERANK_DAMPING / CBM_CONFIG_PAGERANK_EPSILON.
- pagerank.c: read both via cbm_config_get_double in
  cbm_pagerank_compute_with_config (defaults = existing #defines, so no
  behavior change unless a user tunes them).
- test_pagerank.c: pagerank_damping_epsilon_config_tunable asserts that
  tuning damping (0.5 vs 0.99) materially changes the hub rank value and
  ranks still sum to ~1 under a custom config.

Green: pagerank 43 passed.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 19:08:47 -04:00
Andrew Hundt be82cacbe2 feat(search): rank project symbols above dependencies; make it tunable
The dep-last tiebreak in cbm_store_search only fired for glob searches
(params->project_pattern), so the common prefix-match path
(params->project, which includes proj.dep.*) let a dependency symbol
outrank the project's own symbol of the same name -- the "Path" concern
(a stdlib Path fronting the user's own Path).

- store.c: hoist dep-last into one clause applied consistently across
  ALL sort modes (relevance/degree/calls/linkrank/name) whenever a
  search is scoped to a project; gated on the new param.
- store.h: cbm_search_params_t::disable_dep_ranking (default false =
  deps rank last; inverted sense so {0}-init preserves the safe default).
- mcp.c: search_disable_dep_ranking config key (bool, default false)
  wired through srv->config and documented in the search_graph schema,
  following the existing CBM_CONFIG_* pattern.
- test_tool_consolidation.c: store_prefix_ranks_project_above_dep
  (default ranks project above dep) and store_prefix_disable_dep_ranking_lets_dep_win
  (pure relevance when disabled).

Green: tool_consolidation 87 passed. Full suite unchanged except the 2
known B1 incremental failures.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 19:08:19 -04:00
Andrew Hundt 1d1f9da5f6 test: add B1 corruption-isolation probes + CBM_ONLY_SUITE filters
Two regression probes that isolate the non-deterministic .db corruption
(B1) to the extraction-population hop, each with a decisive negative:

- test_sqlite_writer.c sw_scale_root_path_integrity: variable-length
  properties_json (20-1500B, stressing every page boundary) through
  cbm_write_db. Clean 10/10 -> rules out the variable-record
  page-boundary hypothesis in the writer.
- test_graph_buffer.c gbuf_dump_pipeline_path_integrity: drives
  cbm_gbuf_dump_to_sqlite with a populated + merged gbuf (variable heap
  properties_json + cbm_gbuf_merge ID remap, 15k nodes / 50k edges).
  Clean 10/10 -> rules out the gbuf->dump handoff (build_dump_nodes /
  temp_to_final).

Both confirm the writer and the gbuf->dump handoff are correct on
realistic synthetic data; B1 must originate in real tree-sitter
extraction populating the gbuf (needs real source data to reproduce).

test_main.c: add CBM_ONLY_SUITE fast-filters for graph_buffer + pagerank.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-26 19:07:26 -04:00
Andrew Hundt 06b7bd257d test(sqlite_writer): add scale root_path integrity regression (B1 probe)
Adds sw_scale_root_path_integrity: writes a ~fastapi-scale DB in ISOLATION
(20k nodes / 200k unique edges) via cbm_write_db, then asserts root_path
round-trips exactly, PRAGMA integrity_check == "ok", and node/edge counts.

Purpose: isolate whether the B1 custom-writer corruption reproduces without
the pipeline. Result: PASSES 3/3 — the writer is sound at scale in isolation
(sw_minimal_data covers the tiny case). This rules out the writer's
serialization as the B1 cause and points at the pipeline/parallel handoff
(a data race — to be confirmed with TSan). Also a permanent regression guard
for the custom B-tree writer at scale.

Edges generated with unique (source,target) pairs to respect the table's
UNIQUE(source_id, target_id, type) constraint. Adds sqlite_writer to the
CBM_ONLY_SUITE filter.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-25 06:05:41 -04:00
Andrew Hundt 7cfcff9e68 test: add CBM_ONLY_SUITE env filter for targeted suite runs
When CBM_ONLY_SUITE is set (e.g. "incremental", "mcp", "store_nodes",
"tool_consolidation"), run only that suite then summarize and exit;
otherwise behavior is unchanged. Zero cost when unset. Speeds the
edit/build/test loop for focused work without spinning up the heavy
LSP/stdlib suites.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-25 05:11:40 -04:00
Andrew Hundt 4f71dfb4f4 fix(mcp): retain bad-root_path DBs instead of auto-deleting (#557 data loss)
resolve_store (mcp.c) auto-cleaned (deleted .db + WAL + SHM) whenever
cbm_store_check_integrity flagged the projects table. A single malformed
project root_path (e.g. a numeric value) is a cosmetic projects-row
defect — node/edge data stays intact and queries key off project name,
not root_path — yet it triggered a wholesale delete of the freshly-indexed
DB, losing all indexed data (upstream #557).

Add cbm_store_check_integrity_full(store, &path_only_failure) (store.h,
store.c) which classifies a bad-root_path-only defect (rows fine) apart
from genuine >5-row corruption. resolve_store now RETAINS path-only-defect
DBs (logs store.integrity_retain) and only auto-deletes genuine
corruption. The watcher registration is guarded against a bogus
(numeric/empty) root_path from a retained DB. cbm_store_check_integrity()
is kept as a thin wrapper so artifact.c, pipeline.c, and existing tests
are unchanged.

Mitigates #557 data loss and stabilizes incremental node-count reads (the
count no longer sporadically drops to 0 after a query). Does NOT fully
close the 2 pre-existing incremental-accuracy failures
(incr_db_deleted_recovery, incr_accuracy_vs_full), which stem from a
separate full-reindex-after-deletion node-count discrepancy — tracked
separately.

Verified: store_nodes 56/56 incl. new store_integrity_full_path_only_classification;
mcp suite green; incremental suite shows only the 2 pre-existing failures
(no regression; diff_pct=96 identical pre/post).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-25 05:11:12 -04:00
Andrew Hundt 74a5c63eff feat(mcp): split search_code_graph into focused tools (§4b)
Replace the consolidated search_code_graph mega-tool — which merged graph
search + source grep + cypher behind two nearly disjoint parameter
families — with the three existing focused tools as the DEFAULT surface:
search_graph (graph/structural), search_code (source grep), query_graph
(cypher). These are drawn from TOOLS[] (single schema source, DRY);
trace_call_path + get_code remain on the default surface. The
search_code_graph schema and its thin dispatch router are deleted; the
underlying handlers (handle_search_graph/search_code/query_graph) stay.

The _hidden_tools progressive-disclosure hint now lists 11 hidden tools
(was 14). Repointed three user-facing hint strings (handle_trace_call_path
missing-name and function-not-found; two unknown-tool path hints) from the
deleted tool to search_graph.

Tests updated to the new 5-tool surface:
- tests/test_mcp.c: mcp_tools_list + server_handle_tools_list assert the
  5 default tools present and search_code_graph absent.
- tests/test_tool_consolidation.c: renamed streamlined_mode_shows_3_tools
  -> ..._5_default_tools; repurposed the two search_code_graph dispatch
  tests into search_graph_dispatch / query_graph_dispatch (query_graph
  handler accepts both cypher= and query=); revised
  schema_compact_documented_as_graph_only (the graph-vs-source compact
  collision it warned about no longer exists post-split).
- tests/test_input_validation.c: remapped 16 dispatch calls by mode --
  cypher -> query_graph, source-grep -> search_code, graph/structural ->
  search_graph.
- tests/test_depindex.c: tool_index_dependencies_listed asserts
  search_graph (was search_code_graph).
- src/cli/cli.c: six config-description strings (search_limit, tool_mode,
  context_injection, compact, default_sort_by, default_include_dependencies).

Back-compat: blast radius is in-repo only (0 references in Go internal/cmd,
docs, or the CGO wrapper). External callers of search_code_graph must
switch to search_graph / search_code / query_graph, which are strictly
more capable. query_graph keeps its query= backward-compat alias.

Context -- of the three section-4 fixes in the merge plan, only section 4b
was a real change: section 4c (function<->class edges) was already
implemented (commit b950bf2 added the missing MEMBER_OF reverse-edge
coverage), and section 4a (passive resources) was already mitigated by
inject_context_once plus the get_graph_schema/get_architecture/index_status
tools -- both verified against code, not assumed.

Gate: 5981 passed / 2 failed (the 2 are the deferred B1 store-layer
incremental failures, unrelated to section 4b).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-24 22:00:24 -04:00
Andrew Hundt 950fa19d19 test(lang_contract): assert MEMBER_OF reverse edge (§4c bidirectional coverage)
contract_edge_defines_method asserted only the forward DEFINES_METHOD
(Class->Method) edge. The fork's reverse MEMBER_OF (Method->Class) edge —
emitted in pass_definitions.c:322 / pass_parallel.c / pass_normalize.c and
consumed by pagerank.c (member_rank_factor) — had ZERO test coverage
anywhere in the suite. Add the MEMBER_OF assertion so the function<->class
tie is locked in in both directions.

§4c investigation finding (evidence-based, refutes the plan's assumption
that a code fix was needed): the function<->class tie was ALREADY
implemented, not a merge regression. The Method-only gate
(pass_definitions.c:318) is identical in fork (3018b22:270), upstream
(34efbc0:315), and HEAD; DEFINES_METHOD is tested+passing across 9
languages (defines_method_go/rust/java/csharp/php/ruby/kotlin/ts/scala in
test_edge_types_probe.c); the documented "Go parent_class never derived"
bug is already fixed (extract_defs.c:2776-2783); and every class-scoped
callable is labeled Method with parent_class populated (push_method_def /
generic extractor / extract_rust_impl), so there is no class-scoped
Function gap to fix. Broadening the gate would change nothing. The only
real gap was the untested reverse edge, closed here.

Gate: 5981 passed / 2 failed (the 2 are the deferred B1 incremental
store-layer failures, unrelated to §4c).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-24 19:34:23 -04:00
Andrew Hundt aff5df33f9 test(mcp): cover classic-mode tools list (CBM_TOOL_MODE=classic)
The merged cbm_mcp_tools_list (src/mcp/mcp.c:868) defaults to streamlined mode
when srv is NULL, so tests/test_mcp.c:mcp_tools_list only exercised the 3
consolidated tools. The classic-mode path (CBM_TOOL_MODE=classic, mcp.c:876)
that emits all 15 TOOLS[] split tools had no test — the MERGE-TODO flagged at
tests/test_mcp.c:172.

Add mcp_tools_list_classic_mode: setenv("CBM_TOOL_MODE","classic"), capture the
list, then unsetenv BEFORE any ASSERT so a failed assertion cannot leak the
classic setting into sibling tests (which expect the streamlined default). Assert
the classic split tools (index_repository, search_graph, query_graph) are present
and the streamlined-only consolidated search_code_graph + the _hidden_tools
progressive-disclosure hint are absent. Registered in suite_mcp.

Full suite: 5981 passed / 2 failed (the 2 are the deferred B1 incremental races,
unrelated to this test).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-24 18:01:42 -04:00
Andrew Hundt e494cee91a fix(leak): unhang stack_overflow + lang_contract fork tests under leaks --atExit
tests/test_stack_overflow.c and tests/test_lang_contract.c each have a fork()
helper that runs cbm_extract_file on pathological input in a child process (to
verify no-crash), then bare-waitpids with waitpid(pid, &status, 0). Under
`leaks --atExit` (macOS) the forked child is SIGSTOP'd during heap inspection
and the bare waitpid never returns — the identical hang class already fixed in
b336466 (test_store_bulk.c, src/foundation/compat_fs.c) and 148b4d7
(test_dump_verify_io.c).

Replace the bare waitpid with the WUNTRACED+SIGCONT loop used elsewhere in the
suite (kill(child, SIGCONT) on WIFSTOPPED so leaks can finish and the child
proceeds to _exit), and add #include <signal.h> for kill() in both files. The
helpers keep their existing `return WIFSIGNALED(status)` contract.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-24 17:30:04 -04:00
Andrew Hundt ec206caa4b fix(leak): unhang dump_verify_io_fork_crash under leaks --atExit
dump_verify_io_fork_crash_uncommitted_discarded used bare
waitpid(pid, &status, 0), which never returns when `leaks --atExit` (macOS)
SIGSTOPs the forked child during heap inspection — the identical hang b336466
fixed in test_store_bulk.c (bulk_crash_recovery) and src/foundation/compat_fs.c
(cbm_exec_no_shell). This test was missed by that earlier fix.

Replace the bare waitpid with the WUNTRACED+SIGCONT loop that mirrors
test_store_bulk.c: on WIFSTOPPED, kill(child, SIGCONT) so leaks can finish
inspecting and the child proceeds to _exit(). Add #include <signal.h> for kill().

Unblocks `make test-leak` past this test (the run previously hung here
indefinitely).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-06-24 17:03:07 -04:00
Andrew Hundt 92f3b86f7a merge(upstream/main): integrate DeusData main into api-consolidation
Deep --no-ff merge of upstream DeusData/main (597 commits, merge-base 988d975)
into fork api-consolidation (HEAD 3018b22), producing a superset that combines
upstream breadth with the fork's surviving depth.

Upstream additions integrated: semantic/embedding search (vendored/nomic),
minhash/SimHash, cross-repo + cross-file LSP + route + pkgmap passes, artifact
import/export, path_alias, git_context, hook_augment, Windows support
(compat_fs/compat_thread), the UI layer, and ~140 expanded tree-sitter grammars.

Fork depth retained: PageRank/LinkRank ranking (src/pagerank/; sort_by=
relevance/linkrank/degree/calls), dependency indexing (src/depindex/;
CBM_INDEX_MODE=DEP), token reduction, and HTTP-endpoint discovery
(src/pipeline/httplink.c, pass_httplinks.c).

Conflict resolution (27 regions across the 39-file intersection):
- src/mcp/mcp.c: kept the fork superset handler pattern (resolve_project_store +
  expand_project_param + inject_context_once) as the single path; UNIONed the
  tool dispatch table; emit status:"indexed", excluded[], cross_service trace.
- src/store/store.c: upstream base + fork pagerank/linkrank/node_degree tables
  and sort_by plumbing; cbm_store_node_degree now excludes INHERITS
  (WHERE type != 'INHERITS'); removed the duplicate cbm_store_get_db.
- src/pipeline/pipeline.c: took upstream run_extraction_phase; grafted the fork
  passes (httplinks, normalize, flush_store dump branch); call
  cbm_pipeline_set_committed_counts before cbm_gbuf_dump_to_sqlite.
- internal/cbm/lang_specs.c: 158 entries combining the fork section_node_types
  with upstream ts_factory/embedded_imports; wired 13 orphaned type-arrays.
- src/depindex/depindex.h: renamed CBM_PATH_MAX -> CBM_DEP_PATH_MAX (it collided
  with constants.h enum CBM_PATH_MAX=1024) + 11 call sites in depindex.c.
- Makefile.cbm: upstream grammar/nomic units + fork CGO targets; added
  httplink.c + pass_httplinks.c to PIPELINE_SRCS; removed mongoose NOSAN
  machinery (mongoose was deleted upstream).
- httplinks restored (the merge silently deleted them because the fork had left
  them unchanged since the base): httplink.c/.h, pass_httplinks.c,
  tests/test_httplink.c; registered RUN_SUITE(httplink) in tests/test_main.c.
- CBM_INDEX_MODE enum unified to FULL=0, MODERATE=1, FAST=2, DEP=3.

Completeness (dual-parent check): 0 upstream files dropped from the merge tree;
6 fork files dropped, all intentional (upstream-superseded): CHANGELOG.md,
vendored/mimalloc/src/{arena-abandon,segment-map,segment}.c,
vendored/mongoose/{mongoose.c,mongoose.h}. Fork features (pagerank, depindex,
token_reduction, httplink) verified present and registered in tests/test_main.c.
src/pipeline/pass_parallel.c has an identical function set to upstream (49=49,
none dropped); the large line-deltas vs upstream are within-function divergence
of the fork extractor, not dropped functions.

Gate (run manually; hooks skip on merge commits): build green
(make -f Makefile.cbm cbm, exit 0); full C test suite 5980 passed / 2 failed /
0 crashes. The fork's pre-existing ASan crash in build_project_list_error_srv is
fixed by upstream's mcp.c rewrite, so the merge is strictly better than the fork
baseline on the crash axis.

Known issue, tracked separately (NOT a merge defect): 2 nondeterministic
failures in tests/test_incremental.c — incr_db_deleted_recovery:772 (diff_pct)
and incr_accuracy_vs_full:803 (full_nodes vs incr_nodes). Cause: the fork's
extractor (internal/cbm/extract_*.c) and parallel-resolve Route-node creation
are not thread-safe under the concurrent use of upstream's parallel pipeline
(run_parallel_pipeline), producing nondeterministic route/infra HANDLES edges
(~234k edge swing between runs). Sequential extraction (CBM_WORKERS=1) is
deterministic and correct. Root cause, bisect, and repro are documented in
notes/2026-06-24-fork-vs-upstream-divergence-assessment.md (section 8f); the fix
is deferred to a dedicated thread-safety audit (the make test-tsan target is
currently a stub and needs wiring).
2026-06-24 16:12:00 -04:00
Martin Vogel 170d5a82ab fix(ci): allow-list MSYS2 toolchain URL in binary string audit
The MSYS2 CLANG64 toolchain bakes its package-tracker URL
(https://github.com/msys2/MINGW-packages) into the static Windows .exe via
libc++/compiler-rt. The Layer 2 binary string audit flagged it as an
unauthorized URL, failing smoke-windows (both the standard and ui variants)
while Linux smoke stayed green. Add it to ALLOWED_URLS alongside the other
compiler/linker toolchain URLs (gcc.gnu.org, sourceware.org, launchpad).

Add tests/test_security_strings_allowlist.sh as a reproduce-first regression
guard: it reproduces the BLOCKED failure with a binary fixture and includes a
negative control proving a genuinely unauthorized URL is still rejected. Wired
into scripts/test.sh as Step 6.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-24 03:15:36 +02:00
Martin Vogel 3d4236e861 Merge pull request #486 from mvanhorn/fix/400-windows-persistence-artifact-silent-failure
fix: Surface a real error when persistence artifact export silently fails on Windows
2026-06-23 23:52:36 +02:00
Martin Vogel 090a1a72d5 Merge pull request #542 from mvanhorn/fix/499-global-core-excludesfile-ignore-source
fix: honor git global core.excludesFile as an ignore source
2026-06-23 23:46:24 +02:00
Martin Vogel f82c6aee68 Merge pull request #526 from jstar0/fix/snippet-valid-utf8
fix(mcp): return valid UTF-8 snippets
2026-06-23 23:46:20 +02:00
Martin Vogel 96c1fc38fb Merge pull request #465 from KerseyFabrications/fix/cypher-with-node-props
fix(cypher): preserve node properties through WITH aggregation
2026-06-23 23:46:16 +02:00
Martin Vogel bbaca2a848 Merge pull request #447 from isc-tdyar/fix/string-dispatch-dead-code
fix(extract): string-dispatch CALLS edges never emitted — detection was in dead code
2026-06-23 23:46:13 +02:00
Martin Vogel 56d36ba17e Merge pull request #572 from mvanhorn/fix/558-search-graph-degree-inherits-edges
fix: count INHERITS/IMPLEMENTS edges in search_graph degree
2026-06-23 23:08:13 +02:00
Martin Vogel 879c3cbe5b Merge pull request #566 from jsw6701/fix/kotlin-spring-routes
fix(extract): extract Spring routes from Kotlin controllers
2026-06-23 23:08:09 +02:00
Martin Vogel 008f6e3873 Merge pull request #506 from imateusdev/fix/route-param-canonicalization
fix(routes): canonicalize path parameters so client calls match handlers (#505)
2026-06-23 23:08:05 +02:00
Martin Vogel 4dd6ebd69e Merge pull request #456 from nguiaSoren/fix/334-committed-count-capture
fix(pipeline): make the #334 plausibility gate fire (capture committed_nodes before dump)
2026-06-23 23:08:01 +02:00
imateusdev 8b813675be fix(routes): canonicalize path parameters so client calls match handlers
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>
2026-06-23 13:52:08 -03:00
Matt Van Horn 0714e46346 fix: address self-review findings — keep node_degree CALLS-only
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>
2026-06-23 04:08:36 -07:00
Matt Van Horn 65b8425648 fix: count INHERITS/IMPLEMENTS edges in search_graph degree
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>
2026-06-23 04:08:36 -07:00
jsw6701 7aacf380dd fix(extract): extract Spring routes from Kotlin controllers
try_route_from_annotation read the annotation name via the "name" field
(ts_node_child_by_field_name). tree-sitter-kotlin annotation nodes have no
"name" field: the name lives in a nested type_identifier under (user_type)
or (constructor_invocation (user_type ...)). The lookup returned null, so
every Kotlin @RestController route was dropped (0 decorator-source routes)
while Java worked. Args are likewise under constructor_invocation as
value_arguments (each wrapped in value_argument), not Java argument_list.

Add annotation_name_node()/annotation_args_node() that fall back to the
Kotlin AST shape (Java path unchanged) and unwrap value_argument when
scanning for the route path. Kotlin @GetMapping/@PostMapping/etc. now yield
Route nodes and HANDLES edges. Adds handles_spring_kotlin regression test.

Signed-off-by: jsw6701 <86887824+jsw6701@users.noreply.github.com>
2026-06-23 03:59:45 +00:00
Thomas Dyar 0b94f44f96 fix(extract): string-dispatch calls in dead code path, never linked to target
Python calls of the form iris_obj.classMethodValue('Pkg.Class', 'Method')
emitted no CALLS edge. The detection existed in walk_calls, which is dead
code — the pipeline uses cbm_extract_unified → handle_calls, not walk_calls.

Move the IRIS Native API string-dispatch detection to handle_calls. When a
Python call's callee ends in .classMethodValue, .classMethodVoid,
.classMethodBoolean, or .classMethodObject, and both string-literal arguments
can be extracted, emit a synthetic CALLS edge to Arg1.Arg2.

The pattern generalizes beyond this specific API: any library that takes
class/method names as string literals follows the same shape.

Regression test: python_iris_classMethodValue in test_extraction.c.

Signed-off-by: Thomas Dyar <tdyar@intersystems.com>
2026-06-22 19:56:46 -04:00
Martin Vogel 5ea5579d2b fix(discover): make cbm_gitignore_merge atomic on allocation failure
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>
2026-06-22 23:51:49 +02:00
Matt Van Horn 44d5174511 fix: address review feedback on artifact export error surfacing
- 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>
2026-06-22 14:46:29 -07:00
soren a0a9c26809 fix(pipeline): capture committed_nodes before dump frees the index (#334)
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>
2026-06-23 06:39:49 +09:00
Matt Van Horn 9f66e428f8 fix: resolve core.excludesFile only from user-owned global git configs
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>
2026-06-22 14:39:26 -07:00
Sam Li 559993a853 Add post-dump plausibility gate returning status:degraded (#334)
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>
2026-06-23 06:38:45 +09:00
Martin Vogel acb7eaaa05 Merge pull request #528 from win4r/fix/incremental-inbound-cross-file-edges
fix(pipeline): preserve inbound cross-file edges on incremental reindex
2026-06-22 23:28:28 +02:00
Martin Vogel ff89931070 Merge pull request #475 from rainder/fix/args-json-stack-overflow
fix(pipeline): prevent stack-buffer-overflow in append_args_json
2026-06-22 23:28:10 +02:00
Martin Vogel 6e0f7f7926 Merge pull request #466 from KerseyFabrications/fix/name-resolution
fix(mcp): prefer definitions and report ambiguity in name resolution
2026-06-22 23:28:07 +02:00
Martin Vogel c3b5b00796 Merge pull request #464 from KerseyFabrications/fix/detect-changes-since
fix(mcp): honor detect_changes 'since' parameter
2026-06-22 23:28:03 +02:00
Martin Vogel 0ad4d2d029 Merge pull request #448 from isc-tdyar/fix/deploy-dir-always-skip
fix(discover): deploy/ directories indexed as duplicate source in full mode
2026-06-22 23:27:09 +02:00
Martin Vogel 97cbfce055 Merge pull request #412 from isc-tdyar/fix/cypher-label-filter-truncation
fix(cypher): label-filtered edge traversal silently truncates at 10 results
2026-06-22 23:26:43 +02:00
Pcristin 0618eaa99a fix: honor file_pattern in bm25 graph search (#555)
Signed-off-by: pcristin <xxxokzxxx@protonmail.com>
2026-06-22 22:05:29 +02:00
Kiril Klein, PhDandClaude Opus 4.8 df8bc9254d fix(mcp): trace_path cross_service follows CROSS_* edges (#522) (#538)
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>
2026-06-22 22:05:18 +02:00
Alex 8ba286324c fix(pipeline): persist call-site line on CALLS edges (#508)
* 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>
2026-06-22 22:05:09 +02:00
Shaurya 11009f2d94 fix(discover): honor .git/info/exclude during file discovery (#493)
* 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>
2026-06-22 22:04:42 +02:00
Matt Van Horn f5ea36cc64 fix: Reject filesystem paths in HTTP_CALLS route extraction so client routes can join server routes (#485)
Signed-off-by: mvanhorn <mvanhorn@gmail.com>
2026-06-22 22:04:31 +02:00
c8f9f94e08 fix(search): ignore empty-string label in search_graph name_pattern path (#483)
* 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>
2026-06-22 22:04:19 +02:00
cd660270cb fix(resolver): disambiguate qualified cross-file calls by full namespace tail (#479)
* 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>
2026-06-22 22:04:06 +02:00