Commit Graph
247 Commits
Author SHA1 Message Date
Andrew Hundt a86d3d5659 fix(mcp): 6 bugs found in post-implementation review
store.h:108 — comment typo: "AND" → "OR" for pattern OR-search field

mcp.c:1754 — indentation bug in mode-error free() block: free(file_pattern)
  had 1-space indent instead of 8 and a spurious blank line

mcp.c:2422 — OOM leak in QN lookup: when cbm_store_find_node_by_qn succeeds
  but calloc fails, qn_tmp heap fields (name/project/label/etc.) leaked.
  Fix: free_node_contents(&qn_tmp) in else branch. Added forward declaration
  of free_node_contents before handle_trace_call_path so it resolves.

mcp.c:2464 — "function not found" hint was wrong when only qualified_name
  passed. Old hint said "use name_pattern" but user passed a QN. New hint
  says "use pattern= to find the correct qualified_name".

mcp.c:472 — search_in schema description now documents the response format
  difference: graph mode returns {total,results:[{qualified_name,...}]},
  source mode returns {matches:[{file,line,content}],count}.

tests: 4 new TDD tests covering:
  - pattern glob wildcard auto-convert (*foo* → .*foo.*)
  - pattern invalid regex returns error with hint
  - trace: qualified_name takes priority over function_name when both given
  - trace: QN-not-found returns specific hint mentioning qualified_name
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-04-06 02:04:27 -04:00
Andrew Hundt 6da71c7973 fix(tests): correct 2 test regressions from api-consolidation
test_tool_consolidation:539 — resource_client_never_gets_context_across_multiple_calls
  - Cold-start fix (resolve_project_store) now opens the real indexed DB, so
    search results include node names containing "_context" as a substring
    (e.g. "inject_context_once", "gets_context_across_multiple_calls").
  - strstr(r, "_context") was a false positive: matched substrings in node
    names, not the injected "_context" JSON key.
  - Fix: check for "\"_context\":" (quoted JSON key + colon) which only
    matches the actual injected context object, not substrings in results.
  - Same fix applied to legacy_client_gets_context_only_on_first_call test.

test_input_validation:695 — trace_accepts_qualified_name_param
  - QN lookup calls cbm_store_find_node_by_qn(store, NULL, qn, &node).
    SQL "WHERE project = NULL" never matches (SQL null semantics).
  - Test setup uses cbm_mcp_server_set_project("validation-test") which sets
    current_project but NOT session_project, so cold-start project fix does
    not fire, leaving pe.value = NULL.
  - Fix: pass project="validation-test" in test args so pe.value is set.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-04-06 01:40:36 -04:00
Andrew Hundt 9b0537519f feat(mcp): add pattern OR-search+search_in+case_sensitive to search_code_graph; qualified_name to trace; compact/sort/include_deps config; cold-start session fix
- search_code_graph: pattern param (OR-search name+qn), search_in='source' dispatch,
  case_sensitive, summary=bool alias, compact/sort_by/include_deps config defaults
- trace_call_path: qualified_name param with QN-first lookup via B-tree,
  compact config default, accepts function_name OR qualified_name
- get_code: compact config default
- store: pattern OR clause in SQL builder, pattern field in search params
- inject_context_once: auto_indexing status + actionable guidance
- REQUIRE_STORE: improved auto-index failure error message
- schemas: added search_in, pattern, case_sensitive, summary, max_rows to search;
  qualified_name to trace; compact to get_code
- config registry: compact, default_sort_by, default_include_dependencies entries
- tests: 9 new TDD tests (pattern_or, case_sensitive, source_search, summary_alias,
  config_compact, config_sort_by, trace_qualified_name, regression)

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-04-05 00:40:56 -04:00
Andrew Hundt ab58abf0f8 mcp.c: add missing parameter descriptions for name_pattern, qn_pattern, sort_by, function_name
Previous behavior: name_pattern and qn_pattern had no JSON schema description
in either classic or streamlined tool mode — AI clients saw only
{"type":"string"} with no hint that the field accepts regex or auto-converts
glob wildcards. sort_by in streamlined mode had the enum but no description
of what each value means. function_name in trace_call_path said only
"Function name to trace" with no mention of case-insensitive fallback.

What changed:
- src/mcp/mcp.c TOOLS[] (classic mode, line 287): name_pattern and qn_pattern
  gain description: "Regex pattern on symbol name. Glob wildcards (*tool*,
  foo?) auto-convert to regex."
- src/mcp/mcp.c TOOLS[] (classic mode, line 329): trace_call_path tool
  description adds "Matches exact name first, then falls back to
  case-insensitive search if not found." function_name param gains
  "Case-insensitive fallback if exact match not found."
- src/mcp/mcp.c STREAMLINED_TOOLS[] (line 447): same name_pattern/qn_pattern
  descriptions. sort_by gains description: "Sort order: relevance (PageRank,
  default), name, degree (edge weight), calls (function calls in+out),
  linkrank (link-based rank)."
- src/mcp/mcp.c STREAMLINED_TOOLS[] (line 472): same trace_call_path
  description and function_name param updates.

Why: After fixing three bugs (sort_by validation, glob auto-convert,
trace_call_path fuzzy fallback) in commit 77352a7, the tool schemas served
to AI clients did not describe the new behaviors. AI agents would still
write .*tool.* instead of *tool* (not knowing glob works), not know what
sort_by values mean in streamlined mode, and not know that function_name
matching is case-insensitive.

Testable: codebase-memory-mcp cli tools/list shows updated descriptions.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-04-04 01:44:37 -04:00
Andrew Hundt 6026fa6647 fix(mcp): accept sort_by calls/linkrank, auto-convert glob wildcards, fuzzy trace_call_path
Three bugs fixed in MCP tool validation layer:

1. sort_by: validator only checked 3 of 5 valid values (relevance/name/degree).
   Added missing strcmp for 'calls' and 'linkrank' at mcp.c:1625-1627.

2. name_pattern/qn_pattern: glob wildcards (*tool*) rejected as invalid regex.
   On regex compile failure, glob_to_regex() converts bare * -> .* and ? -> .
   before retrying. Valid regex patterns have zero overhead (no conversion).
   Applied to both name_pattern and qn_pattern validation blocks.

3. trace_call_path: exact case-sensitive name match too strict.
   On node_count==0, fallback to cbm_store_search with case_sensitive=false
   and min/max_degree=-1 (no degree filter). Uses found result's project for
   the re-query to handle NULL project correctly. Zero overhead on exact hit.

TDD: 10 new tests in test_input_validation.c covering all three bugs:
- f6_sort_by_calls_accepted, f6_sort_by_linkrank_accepted
- f9_glob_star_autoconverted, f9_glob_question_autoconverted,
  f9_valid_regex_still_works, f9_truly_invalid_pattern_still_errors,
  f9_qn_pattern_glob_autoconverted
- trace_case_mismatch_finds_via_fallback, trace_exact_match_still_works,
  trace_truly_missing_still_errors

All 2252 tests pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-04-03 21:15:01 -04:00
Andrew Hundt 30dfc1f9a1 test(watcher): add TDD coverage for CPU bug fixes
Add two tests that validate both watcher CPU bug fixes are permanent:

- watcher_dirty_content_change_retriggered: proves hash-based dirty
  detection is bidirectional — same porcelain output blocks reindex
  (hash stable), but a genuinely new dirty state (second file modified,
  different porcelain output → different djb2 hash) correctly fires a
  second reindex. Guards against regression of Fix 1 (dirty-tree loop).

- watcher_watch_path_change_resets_state: proves cbm_watcher_watch()
  with a different path for the same project name replaces state and
  starts fresh baseline on the new path, while same-path calls remain
  idempotent. Guards against regression of Fix 2 (state reset on switch).

All 2242 tests pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-31 00:13:47 -04:00
Andrew Hundt 230b49014a fix(watcher): eliminate continuous CPU usage from dirty-tree reindex loop
Root cause 1: git_is_dirty() always returns true for dirty working trees,
causing check_changes() to trigger a full reindex on every poll (every 5-60s)
even when nothing had changed since the last reindex.

Root cause 2: cbm_watcher_watch() destroyed and recreated project_state_t on
every call, resetting baseline_done/last_head/last_dirty_hash and triggering
redundant git commands on each project switch.

Fix 1 (src/watcher/watcher.c):
- Replace git_is_dirty() with git_dirty_hash() which computes a djb2 hash of
  the full `git status --porcelain` output
- Add last_dirty_hash[17] field to project_state_t
- check_changes() now compares new hash to last_dirty_hash — only returns true
  when the dirty content actually changed, not just when the tree is dirty
- poll_project() refreshes last_dirty_hash after a successful reindex so the
  same uncommitted edits don't retrigger on the very next poll

Fix 2 (src/watcher/watcher.c):
- cbm_watcher_watch() is now idempotent: early-returns when project is already
  watched at the same path, preserving all accumulated state

Tests (tests/test_watcher.c):
- watcher_continued_dirty: update assertion from 2→1 (same dirty hash = no
  extra reindex); update comment to document correct post-fix behavior
- watcher_dirty_hash_stable: new test — 4 polls with identical dirty content
  all produce index_call_count==1 (no infinite loop)
- watcher_watch_idempotent: new test — re-watching same project+path preserves
  state; redundant watch does not reset baseline or dirty hash

2240 tests pass (was 2213 before this branch's earlier work; +27 new tests
added across the branch including these 2 new watcher tests).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-30 23:26:07 -04:00
Andrew Hundt ce6d970f1b fix(autotune): fix CWD bug, race condition, and update CBM ground truth
Three bugs fixed in autotune.py:

1. CWD bug: binary uses getcwd() (not rootUri) for session_root.
   _send_batch now passes cwd=repo_root to subprocess so architecture
   queries return results for the correct project.

2. Race condition: delete_project_db() before an MCP session triggered
   the background autoindex thread which raced with index_repository,
   causing isError=true. Removed the deletion — index_repository forces
   a full reindex by itself.

3. CBM ground truth was wrong: used C in-degree (cbm_arena_alloc in-degree
   21) but PageRank is nearly uniform for CBM's C graph (~0.000035 for all).
   Updated to functions verified high by PageRank: cbm_go_stdlib_register,
   cbm_extract_imports, cbm_levenshtein_distance, cbm_path_match_score, etc.

Added exclude_ui_tests and calls_boost_excl_tests experiments.

Final scores (9/30 vs 5/30 before fixes):
  rtk:    5/10 — tokenize, resolved_command, check_for_hook, try_route_native_command
  autorun: 2/10 — session_state, SessionStateManager (Path stdlib bug inflates top slots)
  cbm:    1-2/10 — architecture IS improved, C/Go code shown instead of TypeScript UI

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 05:37:40 -04:00
Andrew Hundt 52d2445f8b autotune.py: set DEFAULTS to best experiment results (calls_boost_excl_tests)
Previous defaults: edge_weight_calls=1.0, edge_weight_usage=0.7,
key_functions_exclude="" (no exclusions).

What changed:
- scripts/autotune.py DEFAULTS: edge_weight_calls 1.0 → 2.0 (call edges
  are the strongest signal for production importance)
- scripts/autotune.py DEFAULTS: edge_weight_usage 0.7 → 0.3 (type-reference
  edges add noise, dampening improves ranking signal)
- scripts/autotune.py DEFAULTS: key_functions_exclude "" → "graph-ui/**,
  tools/**,scripts/**,tests/**" (excluding non-production paths surfaces core
  library functions instead of test helpers)

Why: autotune run on 2026-03-26 scored calls_boost_excl_tests at 6/30 across
3 repos (codebase-memory-mcp, autorun, rtk), best of 8 experiments. Baseline
scored 0/30. These defaults are now the baseline that experiments diverge from,
so future autotune runs search the config space around the current best.

  Testable: python3 scripts/autotune.py  (baseline_25 now starts from these values)

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 05:31:05 -04:00
Andrew Hundt 2c255f0430 autotune.py: fix PageRank recompute, persistent MCP session, JSON results, CLI params
Previous behavior: autotune set config keys but never triggered PageRank recompute
between experiments — all experiments read stale stored scores, producing identical
results. The binary also got SIGKILL'd on macOS 25+ due to invalidated ad-hoc
signature after `cp` during install.

What changed:
- scripts/autotune.py: replace query_architecture() (async REQUIRE_STORE reindex)
  with index_and_query_architecture() — opens one persistent stdio MCP session per
  repo per experiment sending 3 sequential messages: initialize → tools/call
  index_repository (synchronous, blocks until full pipeline+PageRank completes with
  current edge weights) → resources/read codebase://architecture
- scripts/autotune.py: add project_name_from_path() mirroring cbm_project_name_from_path()
  from src/pipeline/fqn.c, and delete_project_db() to remove stale DBs
- scripts/autotune.py: add _send_batch() env+cwd params; pass CBM_TOOL_MODE=classic
  so index_repository tool is available in MCP session
- scripts/autotune.py: add --top-matches (default 10) and --key-count (default 25)
  CLI params; show matched expected names + top-N per repo in output
- scripts/autotune.py: default timeout 60s → 1200s (indexing takes ~40s per repo)
- scripts/autotune.py: add exclude_ui_tests experiment; rename calls_boost_excl →
  calls_boost_excl_tests with tests/** added to exclude list
- scripts/autotune.py: save every run to scripts/autotune_results.json (appended,
  with timestamp/binary/repos/experiments/best fields)
- scripts/autotune.py: show progress bar (█/░) and ◀ BEST marker in final report
- .gitignore: add scripts/autotune_results.json (generated artifact, not tracked)

Why: edge weights and PageRank iterations are only applied at index time via
cbm_pagerank_compute_with_config(); querying a DB indexed with old weights produces
wrong rankings regardless of config changes. Full reindex per experiment is required.
Also fixes macOS 25+ SIGKILL by rebuilding binary (Makefile.cbm re-signs with
codesign --force --sign - after install).

First run result: calls_boost_excl_tests scores 6/30 (best), baseline 0/30.
  Testable: python3 scripts/autotune.py

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 05:30:26 -04:00
Andrew Hundt 1646f96988 feat(config,ranking): parameterize limits, enrich config docs, add autotune
Fixes codebase://architecture returning only 10 results all from graph-ui
by wiring hardcoded limits through the config system and raising defaults to 25.

Key changes:
- mcp.c: add key_functions_count config (default 25); wire into
  build_key_functions_sql (was hardcoded LIMIT 10 at line 4317) and
  build_resource_architecture call site
- mcp.c: add arch_hotspot_limit config (default 25); wire into classic
  get_architecture tool handler
- store.c/store.h: raise CBM_ARCH_HOTSPOT_DEFAULT_LIMIT 10->25; add
  hotspot_limit param to cbm_store_get_architecture
- store.c/store.h: add sort_by=calls (ORDER BY calls_in+calls_out DESC)
  and sort_by=linkrank (ORDER BY linkrank_in DESC) dispatch cases;
  add degree_mode config (weighted|unweighted|calls_only) for
  min_degree/max_degree filter column selection
- watcher.c/watcher.h: add poll_base_ms/poll_max_ms to struct cbm_watcher;
  change cbm_watcher_run and cbm_watcher_poll_interval_ms signatures to
  accept base_ms/max_ms params (0=defaults); wire watcher_poll_base_ms and
  watcher_poll_max_ms config keys through main.c
- cli.h: extend cbm_config_entry_t with range and guidance fields (5->7)
- cli.c: replace entire CBM_CONFIG_REGISTRY with 7-field entries for all 32
  config keys with broadest feasible ranges and actionable guidance strings;
  update config list/get/help display to print [range] + guidance per entry
- scripts/autotune.py: new standalone Python 3.9+ script that sends JSON-RPC
  directly to the binary via stdin/stdout, tries 7 experiments, scores
  against expected top-10 ground truth for 3 repos, resets config on exit
- tests: update all callers of cbm_store_get_architecture (pass 0 for
  hotspot_limit) and cbm_watcher_poll_interval_ms (pass 0,0 for defaults)

All 2238 tests pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 04:40:13 -04:00
Andrew Hundt c5a13eea1c docs(mcp): update streamlined tool + resource descriptions for accuracy
search_code_graph: add auto-index on first query, cypher filter ignore
note, summary mode results_suppressed behavior.

trace_call_path: add auto-index, depth<1 clamped to 1, invalid
direction returns error.

get_code: add Module metadata-only note with auto_resolve hint.

codebase://status resource: add indexing state, project name field,
action_required hint, auto-index failure detail.

_hidden_tools: add auto-index note, list all 4 status states.

All 2238 tests pass. Installed to ~/.local/bin/.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 02:51:55 -04:00
Andrew Hundt dba9570150 fix(mcp): re-apply Phase 3 DRY resolve_project_store + Phase 8 IX-1/2/3 indexing status
Phase 3 — DRY project resolution in 5 handlers:
  handle_get_graph_schema, handle_index_status, handle_get_architecture,
  handle_get_code_snippet: resolve_store → resolve_project_store
  handle_index_dependencies: expand raw_project before resolve_store
  Forward declaration added for resolve_project_store (needed by
  handle_get_graph_schema which precedes the definition)

Phase 8 — Indexing pathway status state machine:
  IX-1: autoindex_failed flag in server struct. REQUIRE_STORE captures
  pipeline_run return code — on failure sets flag + logs error. Error
  response includes "auto-indexing failed" with detail and fix hint.
  IX-2: build_resource_status checks autoindex_active → "indexing" state
  with timing hint. Not-indexed path shows failure detail or action_required.
  Empty store path shows hint about no recognized source files.
  IX-3: just_autoindexed flag set on successful auto-index in REQUIRE_STORE.

All 2238 tests pass. Installed to ~/.local/bin/.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 02:20:35 -04:00
Andrew Hundt ab1ddd5a37 chore: add .clangd config, gitignore runtime artifacts
.clangd: mirrors Makefile.cbm CFLAGS_COMMON include paths so clangd
resolves headers without compile_commands.json.

.gitignore: add .worktrees/, session_project, project, conductor/,
with — runtime/session artifacts from Claude Code subagents.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 02:04:59 -04:00
Andrew Hundt a775040358 fix(mcp,store,pagerank,pipeline): 18 bugs fixed, DF-1 degree precompute, pass_normalize, 11 TDD tests
Phases 1-8 from comprehensive plan (notes/2026-03-26-0013-plan-*.md):

Phase 1 — Input validation (F1,F4,F6,F7,F9,F10,F15):
  mcp.c: empty label→NULL, limit≤0→default, sort_by/mode enum validation,
  regex pre-validation via cbm_regcomp, depth clamp, direction validation

Phase 2 — B7 Cypher param fix + CQ-2 project expansion:
  mcp.c:handle_query_graph reads "cypher" first with "query" fallback,
  uses resolve_project_store for "self"/"dep"/path shortcuts

Phase 3 — DRY resolve_project_store in 5 handlers:
  handle_get_graph_schema, handle_index_status, handle_get_architecture,
  handle_get_code_snippet, handle_index_dependencies

Phase 4 — DF-1 degree precompute (100× faster queries):
  store.c: node_degree table DDL, search SELECT uses LEFT JOIN with
  HC-6 fallback to edge COUNT, cbm_store_node_degree reads precomputed
  table, arch_hotspots uses nd.calls_in, arch_boundaries adds behavioral types
  pagerank.c: is_calls field, degree accumulation during edge iteration,
  node_degree batch INSERT after LinkRank, OOM-safe allocations

Phase 5 — B2/B5 name-based caller fallback:
  pass_calls.c: 3-step resolution (exact QN → shared helper → Module)
  graph_buffer.c: cbm_gbuf_resolve_by_name_in_file DRY helper (HC-1)

Phase 6 — B17/B13 class-method edge repair:
  NEW pass_normalize.c: enforces I2 (Method→Class) and I3 (Field→Class)
  invariants via QN prefix + name+label+file fallback. O(M+F) runtime.
  pipeline.c: normalize pass before dump. Makefile.cbm updated.

Phase 7 — CBMLangSpec section_node_types field:
  lang_specs.h: added section_node_types (17th field)
  lang_specs.c: all 64 language specs updated with NULL initializer

Phase 8 — IX-1..3 indexing pathway fixes:
  mcp.c: autoindex_failed + just_autoindexed flags in server struct,
  REQUIRE_STORE captures pipeline return code, build_resource_status
  shows "indexing" state + failure detail + action_required hints

Additional fixes:
  G1: summary mode adds results=[] + results_suppressed=true
  CQ-3: Cypher + filter params produces warning

Tests: 2238 pass (11 new in test_input_validation.c covering F1,F6,F9,
F10,F15 edge cases, G1, CQ-3, IX-2). Updated test_store_nodes.c for
total degree. Updated test_token_reduction.c for G1 results key.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-26 01:59:37 -04:00
Andrew Hundt 4780675b8d docs: add memory leak test instructions to CLAUDE.md and CONTRIBUTING.md
CLAUDE.md (new): project-level developer notes for Claude with concrete
commands for make test, make test-leak, make test-analyze, and explanation
of why macOS requires test-runner-nosan (ASan replaces malloc, blocking
leaks --atExit from walking the heap).

CONTRIBUTING.md: added "Run C Server Tests" section after the Go test
section. Covers make -f Makefile.cbm test/test-leak/test-analyze, the
macOS vs Linux difference in leak detection approach, and the expected
clean-run output ("0 leaks for 0 total leaked bytes").

Makefile.cbm HOW TO USE block (committed previously) already documents
the commands inline — these docs surface the same info for contributors
who read CONTRIBUTING.md first.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-25 21:53:19 -04:00
Andrew Hundt de3dcdcd8d fix(leaks,depindex,mcp): fix 206 heap leaks, expand ecosystem detection to 17 managers, improve compact output
Memory leaks fixed (0 leaks confirmed via leaks --atExit):

- mcp.c resolve_store: cbm_project_free_fields was gated on
  proj.root_path[0] — empty string paths silently skipped free.
  Separated free from the watcher call; now always frees after
  successful cbm_store_get_project.

- mcp.c handle_index_status: cbm_store_search_free skipped when
  dep_out.count==0 — cbm_store_search allocates even for empty results.
  Restructured to free whenever search succeeds. Same fix for
  cbm_project_free_fields call in ecosystem detection path.

- pagerank.c: node_labels leaked on two early return paths (N==0 and
  id_map_init failure). Both paths now free node_ids and node_labels
  (with per-element free for strdup'd entries before the N==0 branch
  assigns any).

- pass_envscan.c: 8 static regexes compiled once by compile_patterns()
  were never freed. Added cbm_envscan_free_patterns() that calls
  cbm_regfree on each and resets patterns_compiled=0.

- pipeline.h/pipeline.c: public cbm_pipeline_global_cleanup() wraps
  cbm_envscan_free_patterns(). Called in main.c after ALL server
  threads joined (HTTP + stdio) to avoid racing with autoindex threads.
  Also called in run_cli() path and test_pipeline.c teardown.

Ecosystem detection expanded from 8 to 17 package managers:

- depindex.h: added CBM_PKG_MAKE, CBM_PKG_CMAKE, CBM_PKG_MESON,
  CBM_PKG_CONAN (C/C++ build systems). Expanded CBM_MANIFEST_FILES
  with build.gradle.kts, bun.lockb, global.json, Directory.Build.props,
  NuGet.Config, Makefile, GNUmakefile, Makefile.cbm, CMakeLists.txt,
  meson.build, conanfile.txt, conanfile.py, vcpkg.json.

- depindex.c: rewrote cbm_detect_ecosystem to cover all 17 managers
  using CHECK() macro for exact filename matches and dir_contains_suffix()
  for wildcard patterns (*.csproj, *.fsproj). Added has_vendored_deps_dir()
  helper. Added discover_vendored_deps() which scans vendor/ vendored/
  third_party/ thirdparty/ deps/ external/ ext/ contrib/ lib/ _vendor/
  submodules/ for C/C++ and CBM_PKG_CUSTOM build systems.

dep search hint in handle_search_graph:

- When a dep project search (project:"dep", expanded to prefix
  "<session>.dep") returns 0 results, emits a "hint" field with an
  ecosystem-aware actionable message. If cbm_detect_ecosystem succeeds,
  the hint names the detected build system and instructs to re-run
  index_repository. If no ecosystem detected, lists all 17 supported
  manifest file types.

Compact output improvements in mcp.c:

- handle_search_graph: skip emitting "name" when it equals the last
  segment of qualified_name (ends_with_segment check) or when empty.
- handle_trace_call_path: same fix for both outbound (callees) and
  inbound (callers) node arrays. Added callers_total emission to match
  callees_total (was documented in tool description but never emitted).
- build_snippet_response: skip empty name, label, and file_path fields.
  Compact param now wired through all six call sites in handle_get_code.
  Zero-value numeric fields skipped in compact mode.
- handle_get_architecture / build_resource_architecture: skip redundant
  name (when equals last qualified_name segment) and empty label/fp
  in key_functions arrays.

Test coverage:

- test_token_reduction.c: 504-line new file covering compact suppression
  of redundant name/label/empty fields, callers_total presence,
  get_code compact param propagation, architecture key_functions,
  and dep search hint emission.
- test_mcp.c, test_pipeline.c: minor additions for new behaviors.

Makefile.cbm:

- Added nosan build (CFLAGS_NOSAN, LDFLAGS_NOSAN, MONGOOSE_CFLAGS_NOSAN,
  per-object NOSAN variants for sqlite3/lsp/grammar/ts_runtime/mongoose).
- Added test-leak target: macOS uses leaks --atExit on test-runner-nosan;
  Linux uses ASAN_OPTIONS=detect_leaks=1 on regular test-runner.
- Added test-analyze target: Clang --analyze on production + test sources
  (skipped with message when IS_GCC=yes).
- Updated .PHONY with test-leak, test-analyze, test-runner-nosan.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-25 21:40:11 -04:00
Andrew Hundt 2ef29ace6d mcp.json: use sh -c exec \$HOME/... for cross-machine portability
Replace hardcoded /Users/martinvogel path (and intermediate ~ which
MCP clients don't expand) with sh -c "exec \$HOME/.local/bin/..." so
the shell expands \$HOME at launch time on any machine.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-25 19:21:23 -04:00
Andrew Hundt 7711572c61 mcp: rewrite tool description strings for clarity, completeness, and token efficiency
search_graph compact: enumerate all omitted fields explicitly (name, empty
label/file_path, zero degrees) with concrete example and absent-field defaults,
replacing ambiguous "Absent:" footnote that didn't connect omission to compact.

search_graph include_dependencies: remove redundant "Default: true" restatement
(already in schema) and duplicate "dep sub-projects" mention.

trace_call_path compact: add missing omission condition (name == qualified_name
last segment) and example, replacing unexplained "redundant" jargon.

query_graph max_rows: tighten prose without losing the "default: unlimited" fact
(absent from schema) or the scanned-vs-returned distinction.

search_code case_sensitive: consolidate into single clause
"Match case-sensitively (default: case-insensitive)."

Also includes (from prior commits on this branch):
- search_graph: omit empty label/file_path fields instead of emitting ""
- search_graph: omit zero in_degree/out_degree instead of emitting 0
- trace_call_path candidates: omit empty file_path instead of emitting ""

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-25 19:12:20 -04:00
Andrew Hundt 42f5ae7fd3 mcp,store,tests: wire 5 search_graph params + trace edge_types that were silently ignored
Previous behavior: search_graph accepted qn_pattern, relationship, exclude_entry_points,
include_connected, and include_dependencies in its JSON schema but never extracted or
applied them — all 5 were silently ignored. trace_call_path hardcoded edge_types=["CALLS"]
regardless of user input, and its compact default (true) disagreed with the schema (false).
include_dependencies schema default was false, opposite to the prefix-match behavior that
already included dep sub-projects by default.

What changed:
- src/mcp/mcp.c: extract qn_pattern and relationship in handle_search_graph Phase 1
  (after name_pattern, before file_pattern); extract exclude_entry_points, include_connected,
  include_dependencies as bools after max_degree; wire all 5 into cbm_search_params_t;
  add include_dependencies=false guard: sets project_exact=true when project is set without
  glob pattern, scoping results to exact project name (excludes .dep.* sub-projects);
  add free(qn_pattern) and free(relationship) to cleanup block
- src/mcp/mcp.c: replace hardcoded edge_types[]={"CALLS"} in handle_trace_call_path with
  user-supplied edge_types array extracted after all three early-return guards (lines 2062,
  2069, 2086) to avoid memory leaks on those paths; use free_string_array() for cleanup;
  fix compact default from false to true (matches schema); fix include_dependencies schema
  default from false to true with updated description
- src/store/store.c: add qn_pattern REGEXP/iregexp dual-branch WHERE clause after
  name_pattern block (same pattern as name_pattern at lines 1835-1844); add relationship
  EXISTS filter using local rel_cond[128] (exceeds bind_buf[64]) with both edge directions
  (source OR target); merge exclude_entry_points "in_deg > 0" condition into the existing
  degree-filter subquery block to avoid double subquery nesting; fix has_degree_wrap to
  include exclude_entry_points so ORDER BY uses bare column names in the outer wrapped query
- tests/test_token_reduction.c: add setup_sp_server() fixture (4 nodes: main, process_request,
  fetch_data, dep_helper; 2 edges: CALLS main->process_request, HTTP_CALLS fetch_data->process_request);
  add 12 new parameterization accuracy tests in token_reduction suite covering qn_pattern filter,
  relationship filter, exclude_entry_points, include_dependencies=true/false, compact default,
  edge_types traversal

Why: parameters declared in the MCP schema but not implemented silently accept user input
and return wrong results — AI agents and users passing these params get misleading output.
The include_dependencies schema default disagreed with actual behavior. The trace edge_types
hardcoding prevented traversal of non-CALLS relationships (HTTP_CALLS, IMPORTS, etc.).

Testable: make -f Makefile.cbm test (2213 passed, 0 failed)
  search_graph '{"qn_pattern":".*handlers.*","project":"sp-test"}' returns only handlers
  search_graph '{"relationship":"HTTP_CALLS","project":"sp-test"}' returns nodes with HTTP edges
  search_graph '{"exclude_entry_points":true}' removes nodes with in_deg=0 (CALLS)
  search_graph '{"include_dependencies":false,"project":"myapp"}' excludes myapp.dep.* nodes
  trace_call_path '{"function_name":"f","edge_types":["HTTP_CALLS"]}' follows HTTP edges
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-25 16:22:56 -04:00
Andrew Hundt 9d6c11ec98 pagerank: MEMBER_OF reverse edges + tuned edge weights
MEMBER_OF edges (Method→Class):
- Pipeline inserts MEMBER_OF reverse edge alongside each DEFINES_METHOD
  edge in both parallel (pass_parallel.c) and sequential (pass_definitions.c)
  paths. PageRank power iteration naturally propagates member importance
  to parent classes via the graph structure — no post-hoc hacks.
- Config: edge_weight_member_of (default 0.5, 0=disabled)

Edge weight tuning:
- USAGE: 0.2→0.7 (type refs dominant in Python/JS)
- DEFINES: 0.5→0.1 (structural noise)
- DEFINES_METHOD: 0.8→0.5
- default_weight: 0.3→0.1
- New explicit: TESTS=0.05, WRITES=0.15, DECORATES=0.2

Result on autorun (no hacks, pure algorithm):
  EventContext #5, SessionStateManager #4, classes throughout top 10
  Test functions dampened, structural noise reduced

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 08:24:23 -04:00
Andrew Hundt 3178176a1e fix: SIGBUS crash in auto-index background thread (stack overflow)
Root cause: pass_configlink.c allocated ~4.2MB on the stack:
- config_entries[4096] × 520 bytes = 2.0MB
- code_entries[8192] × 264 bytes = 2.1MB
- deps[2048] × 264 bytes = 0.5MB
Background threads get 512KB stack (macOS default) → SIGBUS.

Fix: heap-allocate all three arrays with calloc, free on every return
path. Verified: autorun repo (311 files, 6766 nodes) completes in 409ms.

Also fix: main.c shutdown order — join autoindex thread BEFORE freeing
watcher and watch_store. Previously watcher was freed while autoindex
thread still had a reference to srv->watcher, causing use-after-free.

Tested: CBM_AUTO_INDEX=true on ~/.claude/autorun — clean completion,
no SIGBUS, no hang. 2201 tests pass.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 07:37:30 -04:00
Andrew Hundt 1418b36d8c cli: add config registry with 25 keys, env var overrides, grouped help
Config registry (CBM_CONFIG_REGISTRY in cli.c):
- 25 config keys across 5 categories: Indexing, Search, Tools, PageRank,
  Dependencies. Each entry has key, default, env var name, category,
  description.
- All defaults verified against code-level #define values.

cbm_config_get_effective(): priority chain env > DB > default.
- Checks registry for env var name, reads env first, falls back to DB.
- Used by config get CLI and auto_index in maybe_auto_index.

Env var overrides for key settings:
- CBM_AUTO_INDEX (bool), CBM_AUTO_INDEX_LIMIT (int)
- CBM_REINDEX_ON_STARTUP (bool)
- CBM_KEY_FUNCTIONS_EXCLUDE (comma-separated globs)
- CBM_TOOL_MODE (streamlined/classic)

config list output:
- Grouped by category with [Category] headers
- Shows (env) when env var is active, (set) when DB value differs from default
- All 25 keys visible (was: only 2)

config help:
- Shows storage location (~/.cache/codebase-memory-mcp/_config.db)
- Priority explanation (env > config set > default)
- Examples for config set and env var usage
- Keys grouped by category with [env: VAR_NAME] annotation

Fixed: auto_dep_limit default 5→20, dep_max_files default 5000→1000
to match code-level CBM_DEFAULT_AUTO_DEP_LIMIT and CBM_DEFAULT_DEP_MAX_FILES.
Fixed: hint message provides complete commands, not fragments.
Improved: dependency config descriptions explain what packages/files mean.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 07:19:13 -04:00
Andrew Hundt e42067d3b5 mcp: add exclude param, config-driven key_functions, auto_index default true
exclude param (search_code_graph, trace_call_path, get_architecture):
- Accepts array of glob patterns to filter results by file_path
- Converted to SQL NOT LIKE via cbm_glob_to_like in store.c
- New cbm_search_params_t.exclude_paths field (NULL-terminated array)
- Helper: cbm_mcp_get_string_array_arg() parses JSON array → C string array
- 4 TDD tests: filters paths, empty array no-op, exclude-all, schema presence

Config-driven key_functions (get_architecture tool + codebase://architecture):
- build_key_functions_sql() shared helper: builds PageRank query with
  config + param exclude patterns applied via NOT LIKE clauses
- CBM_CONFIG_KEY_FUNCTIONS_EXCLUDE: comma-separated globs persisted in config
  (e.g. "scripts/**,tools/**,tests/**") — no hardcoded path assumptions
- Labels filtered to Function/Class/Method/Interface (code entities only)
- Both get_architecture tool and build_resource_architecture use same helper

auto_index default changed from false to true:
- maybe_auto_index() now indexes on first startup by default
- Ensures codebase://schema/architecture/status resources have data at first read
- Configurable: set auto_index=false to disable for large repos

Tests: 2197 → 2201 (4 new exclude param tests)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 07:00:02 -04:00
Andrew Hundt 5429352d45 discover: skip vendored/third-party dirs in all modes, not just FAST
Move "third_party", "thirdparty", "3rdparty", "external" from
FAST_SKIP_DIRS to ALWAYS_SKIP_DIRS so they're excluded in FULL mode too.
Add "vendored" (new) to ALWAYS_SKIP_DIRS.

Add prefix-based matching via has_vendored_prefix() for naming variations
like "vendored_libs", "vendor-bundle", "third_party_deps". Matches
vendor*, 3rdparty*, third_party*, thirdparty* followed by separator or
end-of-string.

Before: FULL mode indexed vendored grammars → 22,935 nodes, PageRank
dominated by vendored scanner functions (eof, seq, View.size).
After: 5,300 nodes, PageRank correctly shows core pipeline/store/mcp
functions at the top.

No entries removed from skip lists — 4 entries promoted FAST→ALWAYS,
1 entry added. DEP mode unaffected (has its own minimal skip list).

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 06:33:19 -04:00
Andrew Hundt 719cdfe57f mcp: fix 6 bugs + token optimization + empty DB reindex + 4 TDD tests
Bugs fixed:
- _hidden_tools dispatch: returns tool list instead of "unknown tool" error
- trace_call_path: accepts project paths via expand_project_param (DRY
  resolve_project_store helper shared with search_code_graph)
- Resources scoping: codebase://schema/architecture/status now reflect the
  most-recently-queried project (active_project_name) instead of always
  returning the empty session CWD project
- compact default: search/trace default to compact=true via new
  cbm_mcp_get_bool_arg_default() — omits redundant name field
- PageRank precision: add_pagerank_val() writes raw JSON via %.4g format
  (e.g. 4.755e-05) instead of 17-digit doubles, no float round-trip
- Empty DB skip: maybe_auto_index now checks db_has_content() (SELECT 1
  FROM nodes LIMIT 1) instead of just stat(). Empty DBs trigger reindex.

New features:
- db_is_stale(): compares DB mtime vs git HEAD commit time, with
  configurable max_age_seconds (reindex_stale_seconds config key)
- reindex_on_startup config: when true + stale DB, triggers reindex at
  server start. Default false for large project safety.

DRY refactors:
- resolve_project_store(): extracted from handle_search_graph, reused by
  handle_trace_call_path. Handles expand_project_param + DB selection +
  prefix collision avoidance + auto-index on first use.

Tests (2193 → 2197):
- hidden_tools_returns_info_not_error
- compact_defaults_to_true
- pagerank_output_has_limited_precision
- empty_db_not_treated_as_indexed

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 05:49:38 -04:00
Andrew Hundt 47bbe5cd51 watcher: register all session-accessed projects for auto-reindex
Previously only the CWD project at startup was watched for file changes.
Now any project the AI session interacts with gets registered:

- handle_index_repository: call cbm_watcher_watch() after successful
  index+pagerank, so explicitly indexed projects get auto-reindexed on
  file changes (same pattern as auto-index thread at lines 3503-3505)

- resolve_store: call cbm_store_get_project() to get root_path from DB,
  then cbm_watcher_watch() when a new store is opened. Only runs on the
  new-store path (early-return skips already-cached projects). Covers
  all data-access tool paths: search_code_graph, trace_call_path, get_code.

TDD: 3 new tests in test_tool_consolidation.c (all pass, 2193 total):
  watcher_registered_after_index_repository
  watcher_registered_on_resolve_store
  watcher_not_registered_for_unknown_path
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 04:21:17 -04:00
Andrew Hundt afb07d69c1 Makefile.cbm: add integrated codesign + install target for macOS 25+
macOS 25+ enforces ad-hoc code signatures. Copying a binary with cp
invalidates the existing signature and the binary gets SIGKILL at startup.

Changes:
- Detect platform with UNAME_S := $(shell uname -s) at Makefile top
- codesign_binary() make function: calls codesign --force --sign - on macOS,
  prints warning if codesign not found, no-op + informational message on Linux
- cbm target: calls codesign_binary after linking (build/c/... is always signed)
- install target: new target — builds, copies to INSTALL_DIR (~/.local/bin),
  re-signs the copy (required because cp invalidates the build signature)
- Clear status lines on every outcome:
    ✓ signed <path> (ad-hoc, macOS 25+ compatible)
    ✗ WARNING: codesign failed — <path> may crash on macOS 25+
    ✗ WARNING: codesign not found — install Xcode CLT
    (signing skipped — not macOS)
- Updated usage comment with install target and macOS signing note

To install: make -f Makefile.cbm install

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 03:09:27 -04:00
Andrew Hundt ec99ec27f7 mcp: fix get_code returning ambiguous with 1 match + cold-start project detection
Three bugs fixed in handle_get_code_snippet:

Bug A (root cause): Tiers 1-3 all use WHERE project = ?1 AND ... in SQL.
When project param is NULL, SQLite binds NULL and the comparison is always
false — all exact/suffix/name lookups silently return 0 rows.

Bug B: Tier 4 fuzzy search found 1 result but snippet_suggestions() always
sets status=ambiguous regardless of count.

Bug C: Dedup block with cand_count==1 after dedup fell through to ambiguous
instead of resolving.

Fixes:
1. extract_project_from_qn(): scans each dot-prefix of the QN and tests for
   a matching ~/.cache/codebase-memory-mcp/{prefix}.db file (O(n), ~5-15
   access() calls). Returns longest matching prefix — the QN is
   self-describing so this works even on cold start (no prior search call).
   Single malloc+memcpy pattern: best_end offset avoids repeated strdup.
2. handle_get_code_snippet: when project param is NULL, calls
   extract_project_from_qn(qn) and opens the correct DB via resolve_store.
   Falls back to srv->current_project if no DB found. Assigns result into
   project (was NULL) so all existing free(project) exit paths own the memory.
3. Tier 4 fuzzy: fuzzy_count==1 now resolves directly (build_snippet_response)
   instead of calling snippet_suggestions.
4. Dedup block: cand_count==1 after dedup now resolves directly.

Tests added (3 new, total 2190):
- get_code_no_project_uses_open_store_tier1: after search_graph opens a
  store, get_code without project resolves via Tier 1 exact QN + eff_project
- get_code_single_fuzzy_result_resolves_not_ambiguous: wrong-prefix QN forces
  Tier 4 fuzzy; single result must not return status=ambiguous
- get_code_cold_start_parses_project_from_qn: fresh server, no prior call,
  extract_project_from_qn finds the DB and resolves the symbol

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 02:58:20 -04:00
Andrew Hundt 94b4c9632a tests: add 8 prefix collision regression tests for cross-project DB selection
Tests the fix for the bug where session "Users-athundt-.claude" matched
project "Users-athundt-.claude-codebase-memory-mcp-..." due to strncmp
prefix match without checking the separator character.

Tests cover:
- cross_project_search_not_confused_by_prefix: core bug regression
- session_dep_search_uses_session_store: "myapp.dep.lib" → session DB
- exact_session_name_uses_session_store: "myapp" → session DB
- prefix_collision_dash_after_session_name: "myapp-v2" → NOT session
- prefix_collision_underscore_after_session_name: "myapp_test" → NOT session
- prefix_collision_longer_name_with_dot_not_dep: "myapp.config" → session (by design)
- prefix_collision_completely_different_project: "other-project" → NOT session
- prefix_collision_session_is_substring_of_project: "ab" vs "abc" → NOT session

All tests clean up DB files created by resolve_store via unlink().
Total: 2187 tests passing.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 01:45:17 -04:00
Andrew Hundt 643cc87c1e mcp: fix cross-project search prefix collision in DB selection
Root cause: handle_search_graph used strncmp(pe.value, session_project, sp_len)
to decide whether to use the session DB. When session is "Users-athundt-.claude"
and the requested project is "Users-athundt-.claude-codebase-memory-mcp-...",
the first 22 chars match (shared path prefix), so search incorrectly opened
the empty session DB instead of the requested project's 22K-node DB.

Fix: after strncmp, also check that pe.value[sp_len] is '.' (dep separator)
or '\0' (exact match). This prevents "myapp" from matching "myapp-other-project"
while still correctly matching "myapp.dep.pandas".

Found by dogfooding: search_code_graph with explicit project name returned 0
results despite DB having 22828 nodes. Binary test from the same CWD worked
because session_project matched the target project.

2179 tests passing.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 01:40:29 -04:00
Andrew Hundt bdd1d3a9ca mcp: fix _hidden_tools inputSchema string→object — unblocks Claude Code tool discovery
Root cause: _hidden_tools entry used yyjson_mut_obj_add_str for inputSchema,
producing a JSON string value instead of a JSON object. MCP spec requires
inputSchema to be a JSON Schema object. Claude Code validated the tools/list
response and rejected the ENTIRE list when one tool had a malformed schema,
making all 3 real tools (search_code_graph, trace_call_path, get_code)
invisible to the AI.

Fix: build inputSchema as a proper yyjson object (yyjson_mut_obj with
"type":"object" and empty "properties":{}), matching the pattern used by
emit_tool() for real tools.

Found by: dogfooding — server showed "connected" in /mcp but ToolSearch
returned nothing. Binary testing confirmed inputSchema was str not dict.
MCP best practices reference (memory/mcp-best-practices.md) confirmed
the spec requirement.

Test: all_tools_have_object_inputSchema — parses tools/list JSON response
and asserts every tool's inputSchema is yyjson_is_obj (not string/null/array).
This test would have caught this bug immediately.

Total: 2179 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 01:09:23 -04:00
Andrew Hundt fcff8e3de4 tests: fix test collision — use unique DB name + cleanup in dep_search test
dep_search_explicit_dep_project_name created nonexistent.db via
resolve_store → cbm_store_open_path(SQLITE_OPEN_CREATE), causing
tool_delete_project_not_found to find the file and return "deleted"
instead of "not_found".

Fix: use unique name "_tc_deptest_proj_" + unlink cleanup after test.

2178 tests passing.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-23 00:05:52 -04:00
Andrew Hundt e4500fd506 mcp: fix 4 dep search bugs + add 5 TDD regression tests
Bugs found via binary dogfooding (index project + deps → search):

1. resolve_store opened wrong DB for dep projects
   "myapp.dep.pandas" → opened myapp.dep.pandas.db (empty) instead of myapp.db
   Fix: parent_project_for_db() strips .dep.* to find parent DB

2. store.c prefix match was actually exact match
   search(project="myapp") used "n.project = ?" → missed dep nodes
   Fix: "(n.project = ? OR n.project LIKE ?||'.%')" includes deps

3. cbm_is_dep_project failed for cross-project deps
   "otherapp.dep.pandas" with session "myapp" → false (early return)
   Fix: Fall through to generic .dep. strstr when session prefix mismatches

4. Package name extraction used session_project offset
   Wrong offset when session != indexed project → truncated package names
   Fix: Use strstr(".dep.") to find separator position directly

Tests (5 new, 2178 total):
- dep_search_explicit_dep_project_name: resolve_store routes to parent DB
- store_prefix_match_includes_deps: prefix returns project + dep nodes
- store_exact_match_excludes_deps: exact match returns project only
- is_dep_project_cross_project_detection: .dep. detected across projects
- e2e_dep_search_returns_project_and_dep_results: full workflow with tags

Binary verified: 18 results (9 project + 9 dependency), correct source tags,
correct package:"testlib" extraction.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 23:56:09 -04:00
Andrew Hundt 3fd21f8a23 mcp: fix 3 dep search bugs found by binary dogfooding
Bug 1: resolve_store opens wrong DB for dep projects
  Root cause: resolve_store("myapp.dep.pandas") opens myapp.dep.pandas.db
  instead of myapp.db where deps actually live (same-db design).
  Fix: parent_project_for_db() strips .dep.* suffix to find parent DB.

Bug 2: store.c prefix match was actually exact match
  Root cause: cbm_store_search with params->project set (non-exact mode)
  used "n.project = ?" — exact match, not prefix. Deps invisible.
  Fix: SQL now uses "(n.project = ? OR n.project LIKE ?||'.%')" for
  prefix mode, so search_graph(project="myapp") returns project + deps.

Bug 3: cbm_is_dep_project fails for cross-project deps
  Root cause: Early return when session_project doesn't match prefix.
  "otherapp.dep.pandas" with session "myapp" → false (should be true).
  Fix: Fall through to generic .dep. strstr check when session prefix
  doesn't match. Any project containing ".dep." is a dependency.

Bug 4: Package name extraction used wrong offset
  Root cause: Used strlen(session_project) as offset into project name,
  but session_project is CWD-detected, not the indexed project.
  Fix: Use strstr(project, ".dep.") to find separator position directly.

Binary verification (all confirmed working):
- search_graph(project="myapp") → 18 results (9 project + 9 dep)
- source:"project" vs source:"dependency" correctly tagged
- package:"testlib" correctly extracted
- Multiple deps in one index_dependencies call works

Tests: 2173 passing (updated test_depindex cross-project assertion).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 23:51:34 -04:00
Andrew Hundt f6f0767748 mcp: fix resource error double-wrapping + add 6 JSON-RPC structure e2e tests
Root cause: handle_resources_read returned a pre-formatted JSON-RPC error
(via cbm_jsonrpc_format_error), but cbm_mcp_server_handle wrapped it again
in cbm_jsonrpc_format_response. Result: {result: {jsonrpc, id:0, error: {...}}}
instead of the correct {error: {...}}.

Fix: handle_resources_read now takes req_id + err_out params. On error, sets
*err_out to a properly-formatted JSON-RPC error with the correct request id.
The dispatch code returns err_out directly, bypassing the result wrapper.
On success, returns raw result JSON for normal wrapping.

Also: resolve_resource_store() opens the session project DB on demand so
resources work even before any tool call.

New tests (6):
- resource_error_is_top_level_not_nested_in_result: verifies error at top
  level with correct request id (the exact bug that was found)
- resource_error_missing_uri_is_top_level: same for missing uri
- resource_error_no_params_is_top_level: same for no params
- resource_success_has_result_not_error: complement — success has "result"
- resource_schema_returns_real_data_when_indexed: schema has node_labels
- resource_status_returns_not_indexed_when_no_store: fresh server status

Total: 2173 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 23:18:35 -04:00
Andrew Hundt 462f064e72 mcp: fix resources/read returning empty {} — orphan content_obj never attached to doc root
Bug: handle_resources_read created content_obj = yyjson_mut_obj(doc) and passed
it to build_resource_{schema,architecture,status}, but content_obj was never
added to the document root. yy_doc_to_str(doc) serialized the empty root → "{}".

Fix: pass root directly to builders instead of the orphan content_obj. Also add
resolve_resource_store() helper that opens the session project DB on demand so
resources return data even before any tool call (resources/read can be the first
call after initialize).

Verified with real indexed codebase (22,828 nodes):
- codebase://status → {"project":"...","status":"ready","nodes":22828,"edges":50639}
- codebase://schema → {"node_labels":[{"label":"Function","count":12695},...]}
- codebase://architecture → {"total_nodes":22828,"total_edges":50639,...}

Tests: 2167 passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 23:02:08 -04:00
Andrew Hundt 819ab6a890 mcp: cross-reference tools ↔ resources in descriptions for AI discoverability
Per MCP best practices (modelcontextprotocol.io/specification/2025-06-18/server/tools):
tool descriptions are the primary way LLMs discover capabilities. Descriptions must
be self-sufficient — an AI reading just the tool list should know the full workflow.

Tool description changes:
- search_code_graph: now says "Read codebase://schema for available node labels
  and edge types before writing Cypher queries"
- trace_call_path: now says "Read codebase://architecture for key functions
  to start tracing from"
- get_code: now says "Get qualified_name values from search_code_graph results"
- _hidden_tools hint: now lists all 3 resource URIs with usage guidance

Resource description changes (more actionable):
- codebase://schema: lists example labels, says "Read this before writing Cypher"
- codebase://architecture: mentions PageRank, says "Read this first to understand
  codebase structure and find important entry points"
- codebase://status: lists all fields, says "Read this to check if project is indexed"

Tests: 2 new tests verify tool descriptions reference resources and _hidden_tools
hint mentions all 3 resource URIs. Total: 2167 tests passing.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 22:45:33 -04:00
Andrew Hundt d87d408447 mcp: fix 18 vague error messages + add 16 behavioral/spec compliance tests
Error messages: Every error now includes:
- WHAT failed (the specific input that caused the error)
- HOW to fix it (actionable "hint" field with next step)
- WHERE to look (tool names, param examples, valid options)

Fixed errors (18):
- "no project loaded" (3x) → + hint:"Run index_repository..."
- "function not found" → includes searched function name + hint
- "symbol not found" → includes searched qualified_name + hint
- "query is required" → + hint with Cypher syntax example
- "function_name is required" → + hint with param example
- "qualified_name is required" → + hint with format + "Use search_code_graph"
- "pattern is required" → + hint about regex vs literal
- "repo_path is required" → + hint about absolute path
- "project_name is required" → + hint:"Use list_projects"
- "project not found" (2x) → + hint:"Run index_repository or list_projects"
- "project not found or not indexed" → + hint with both options
- "failed to create pipeline" → + hint about path/permissions
- "search failed: temp file" → + hint about /tmp disk space
- "search failed" → + hint about grep installation
- "git diff failed" → + hint about git installation
- "missing tool name" → + lists available tools + "Use tools/list"
- "unknown tool: X" → + lists available tools + "Use tools/list"

New tests (16):
- MCP spec compliance: protocol version, subscribe:false, listChanged:true,
  resources/list fields, resources/read contents array, missing uri, no params
- Client behavioral differences: resource client never gets _context (3 calls),
  legacy client gets _context only first call, empty resources:{} counts as
  support, no-initialize defaults to legacy
- Error message quality: hint field present on no-project, function-not-found
  includes name, symbol-not-found includes qn, all required-param errors have
  hints, unknown-tool lists valid options, resource -32002 is actionable

Total: 2165 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 22:29:06 -04:00
Andrew Hundt 7adaf00851 mcp: fix 3 MCP resources spec compliance issues
Audited against https://modelcontextprotocol.io/docs/concepts/resources

F1: notifications/resources/updated → notifications/resources/list_changed
    We declared listChanged:true in server capabilities, not subscribe:true.
    list_changed is for data changes; updated is for per-resource subscriptions.

F2: Error code -32602 → -32002 for unknown resource URI
    MCP spec Error Handling section specifies -32002 for "Resource not found".
    -32602 is "Invalid params" which is wrong — the URI param is valid, the
    resource just doesn't exist.

F3: Error message now actionable — includes the bad URI and lists all 3
    valid resource URIs (codebase://schema, codebase://architecture,
    codebase://status) with hint to use resources/list.

Tests: 2149 passing (assertions updated for new error code and message).
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 22:11:23 -04:00
Andrew Hundt 8b2f2a75cc mcp: add MCP resources (resources/list + resources/read) with fallback context injection
Phase 10: Replace one-shot static context injection with persistent MCP resources
for clients that support them (Claude Code, VS Code Copilot, OpenCode).

Resources exposed:
- codebase://schema — node labels and edge types with counts
- codebase://architecture — graph size, key functions by PageRank, relationship patterns
- codebase://status — index status, PageRank stats, ecosystem, dependencies

Implementation:
- resources/list returns 3 resource URIs with descriptions
- resources/read dispatches by URI to build_resource_{schema,architecture,status}
- Server advertises resources capability in initialize response (listChanged:true)
- Client capabilities.resources parsed from initialize params (client_has_resources flag)
- inject_context_once skipped when client supports resources (0 token overhead)
- notifications/resources/updated sent after index_repository, index_dependencies, autoindex
- Fallback: legacy clients without resources support still get _context injection

Tests: 8 new tests (resources_list, resources_read x4, initialize_advertises,
client_capability_parsing, fallback_injection). Total: 2149 tests passing.

Also: add .claude/ to .gitignore for local project memory.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 21:26:54 -04:00
Andrew Hundt 4834ea90b7 mcp: auto-index on first use + auto-context injection + use-after-free fix
Auto-index on first use (REQUIRE_STORE + search_graph):
  When store is NULL and session_root is a valid directory:
  1. If autoindex_active: join background thread, re-resolve store
  2. If still NULL: run cbm_pipeline_run() synchronously, then
     cbm_dep_auto_index() + cbm_pagerank_compute_with_config()
  Handles all 3 paths: CWD (detect_session), explicit path (Rule 0),
  MCP roots (future). access(session_root, F_OK) guard prevents
  triggering on non-existent paths in tests.

inject_context_once(): auto-provide architecture/schema on first response.
  First tool response gets _context header with: status, nodes, edges,
  node_labels, edge_types, ranked_nodes, pagerank_computed_at,
  detected_ecosystem. Subsequent responses only get session_project.

Fix: use-after-free in inject_context_once (ASAN crash at mcp.c:937).
  cbm_store_schema_free() freed label/type strings while yyjson still
  held borrowed pointers. Fix: yyjson_mut_obj_add_strcpy() copies strings
  into yyjson's allocator before schema is freed.

Fix: _hidden_tools count corrected from "12" to "14" (14 tools hidden).

Tests: 2 new (first_response_has_context_header, context_has_schema_info).
Total: 2141 tests passing.
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 05:33:25 -04:00
Andrew Hundt c7aaef853c mcp: progressive disclosure + env var override + session_project in all handlers
Progressive disclosure: _hidden_tools entry in streamlined tool list tells AI
  which 12 tools are hidden and how to enable them (CBM_TOOL_MODE=classic env
  var or config set tool_mode classic or per-tool config set tool_<name> true).
  Hidden tools still dispatch normally — AI can call them after discovery.

Env var override: CBM_TOOL_MODE env var takes precedence over config for
  tool_mode. Enables backwards compat without needing a config store.

Session context: add session_project to trace_call_path and get_architecture
  responses. Now all major tool responses include session_project so AI always
  knows which project it's working with.

Tests: 4 new in test_tool_consolidation.c:
  - streamlined_mode_has_hidden_tools_hint
  - hidden_tools_still_dispatch
  - search_graph_has_session_project
  - index_status_has_session_project

Total: 2139 tests passing (13 new Phase 9 tests total)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 03:54:55 -04:00
Andrew Hundt 46e15c4593 mcp: fix gaps — config-backed PageRank callers, Phase 9 test suite (9 tests)
G1: Wire cbm_pagerank_compute_with_config(store, project, srv->config) into
    handle_index_repository and autoindex_thread (2 callers in mcp.c).
    Edge weight config keys now actually used at runtime.
G5: Create tests/test_tool_consolidation.c with 9 tests covering:
    - streamlined_mode_shows_3_tools (NULL srv → 3 consolidated tools)
    - classic_mode_shows_all_15_tools (via server_handle)
    - search_code_graph_structured_dispatch (name_pattern → search_graph)
    - search_code_graph_cypher_dispatch (cypher → query_graph)
    - get_code_dispatch (→ get_code_snippet)
    - old_tool_names_still_dispatch (backwards compat)
    - project_param_path_detection (expand_project_param Rule 0)
    - unknown_tool_returns_error
    - null_tool_name_returns_error
Register suite in test_main.c + Makefile.cbm.

Total: 2135 tests passing (9 new)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 03:50:06 -04:00
Andrew Hundt ff833362f6 mcp: Phase 9 API consolidation — 15 tools to 3 streamlined + config-based visibility
STREAMLINED_TOOLS[]: search_code_graph (merges search_graph + query_graph via cypher param),
  trace_call_path (unchanged), get_code (alias for get_code_snippet)
cbm_mcp_tools_list(srv): filters by tool_mode config (streamlined=3, classic=all 15)
  Per-tool re-enable via config set tool_<name> true
Dispatch: search_code_graph routes to handle_query_graph when cypher param present,
  otherwise handle_search_graph. get_code routes to handle_get_code_snippet.
expand_project_param Rule 0: detects paths (/, ~, ./) and converts via
  cbm_project_name_from_path(). Enables project="/path/to/repo".
Server struct: add context_injected field for Phase 9 auto-context (future).
mcp.h: forward-declare cbm_mcp_server_t at top, cbm_mcp_tools_list takes srv param.
Tests: updated for streamlined mode (3 tools default, old names hidden).

Total: 2126 tests passing
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 03:26:13 -04:00
Andrew Hundt cecf6278cb mcp: apply Phase 8.5 refinements to merged branch
mcp.c: key_functions (top 10 by PageRank) in get_architecture response
mcp.c: pagerank stats (ranked_nodes, computed_at) in index_status response
mcp.c: conditional in_degree/out_degree (only when PageRank not computed)
pagerank.c: cbm_pagerank_compute_with_config() for config-backed edge weights
pagerank.h: 9 CBM_CONFIG_EDGE_WEIGHT_* config key constants + forward decl
cli.c: cbm_config_get_double() for double config values
test_pagerank.c: 7 Phase 8.5 tests (key_functions, config, stats, streamlining)

Total: 2126 tests passing (7 new over merged baseline of 2119)
Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-22 03:09:11 -04:00
Andrew Hundt 1885d13802 merge pagerank-ranking: add PageRank node + LinkRank edge ranking
Merge pagerank-ranking branch into combined token-reduction +
reference-api-indexing branch. Resolves conflicts in mcp.c
(sort_by coexists with compact/summary/token-reduction params),
Makefile.cbm (restore TEST_TOKEN_REDUCTION_SRCS lost in merge).

All 2119 tests pass (35 PageRank + 22 token-reduction + 2062 existing).
2026-03-21 23:29:31 -04:00
Andrew Hundt ef76a7c125 pagerank: add PageRank node ranking + LinkRank edge ranking (Phase 8)
Implement PageRank (power iteration, d=0.85, weighted edges) and
LinkRank (Kim et al. 2010) to rank nodes by structural importance
and edges by traversal probability. References: aider repomap,
NetworkX, RepoGraph (peer-reviewed).

New files:
  src/pagerank/pagerank.{h,c} — algorithm + API (~380 lines)
  tests/test_pagerank.c — 35 tests (core, edge cases, LinkRank)

Schema: pagerank + linkrank tables with indexes (store.c)
Store: conditional LEFT JOIN pagerank on search, sort_by dispatch
  (relevance/name/degree), pr_rank in BFS visited, lr_rank in edges
MCP: sort_by param on search_graph, pagerank in response JSON,
  pagerank scores in trace_call_path callees/callers
Index: compute after pipeline in all 3 paths (handler, watcher,
  autoindex) + index_dependencies

Edge weights: CALLS=1.0 DEFINES_METHOD=0.8 DEFINES=0.5 IMPORTS=0.3
  USAGE=0.2 CONFIGURES=0.1 HTTP_CALLS=0.5 ASYNC_CALLS=0.8

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-21 23:22:07 -04:00
Andrew Hundt 14cf8a8714 tests: add trace/snippet source tagging and cross-edges null safety tests
3 new tests:
- test_trace_results_have_source_field: verifies trace_call_path
  results include source:"project" tagging
- test_snippet_has_source_field: verifies get_code_snippet results
  include source:"project" provenance for project nodes
- test_cross_edges_null_safety: verifies cbm_dep_link_cross_edges
  handles NULL store, NULL project_name, empty string, nonexistent
  project without crashing (returns 0)

Also adds #include <depindex/depindex.h> for cbm_dep_link_cross_edges.

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-21 20:22:13 -04:00
Andrew Hundt 61394569a3 mcp: close remaining gaps — trace/snippet source tagging, cross-edges
Gap 3 (trace boundary tagging): trace_call_path now tags each caller
and callee with source:"project"|"dependency" and read_only:true for
dep nodes. Uses cbm_is_dep_project() for consistent tagging.

Gap 4 (snippet provenance): build_snippet_response adds source and
read_only fields so get_code_snippet results indicate whether code
is from the project or a dependency.

Cross-edges: cbm_dep_link_cross_edges implemented — searches project
Variable nodes, looks for matching Module nodes in dep projects
(project.dep.%), creates IMPORTS edges to link them. Enables
trace_call_path to follow imports across project/dep boundary.

Gap 1 (watcher dep re-index) was already done in prior commit.

Files: src/mcp/mcp.c (trace + snippet tagging),
src/depindex/depindex.c (cross-edge implementation)

Signed-off-by: Andrew Hundt <ATHundt@gmail.com>
2026-03-21 20:17:09 -04:00