68 Commits
Author SHA1 Message Date
Shane McCarron 5a666f2fa3 fix(mcp): match search_code file_pattern with a directory against the repo-relative path
search_code matched file_pattern against the file basename only, so any
pattern containing a directory (src/cli/*.c, apps/web/src/**/*.ts)
silently returned 0 results, while search_graph's file_pattern matches
the full repo-relative path.

A file_pattern containing '/' is now matched in C against the
repo-relative path of every indexed file, on all platforms, with
search_graph-equivalent semantics: '*' and '**' match across '/', '**/'
also matches zero directories, '?' matches one character, and a pattern
without wildcards is a substring match. Patterns without '/' keep the
existing basename behaviour. On Windows such patterns are scanned in
process from the already-filtered list instead of the PowerShell -like
post-filter. A search with a '/' pattern that returns nothing now carries
a hint explaining whole-path matching and pointing at path_filter, and
the schema describes both parameters.

Refs #2836

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-10-29 11:46:23 -05:00
Shane McCarron 349d80c643 perf(index): run supervised index workers at below-normal CPU priority
A full index saturates every core in the parallel extraction pass, and
interactive CLI/MCP queries (CPU-bound at start-up) were measured taking
twice their wall time behind it with flat CPU time: pure run-queue
contention. The index-worker role now lowers its own priority once,
before any indexing thread is created: nice 10 on POSIX,
BELOW_NORMAL_PRIORITY_CLASS on Windows. It never raises priority: a
process already nicer than 10 (or in the IDLE / BELOW_NORMAL class) is
left alone. Plain nice only, not macOS background QoS, which would pin
indexing to efficiency cores and throttle its I/O. Failure is logged and
ignored.

The test runs the helper in a forked child and checks both legs: a
normal-priority child ends at nice >= 10, and a child started nicer than
that is left where it was.

Refs #2834

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-10-29 11:46:19 -05:00
Shane McCarron 7f2fd1081d fix(hooks): tell subagents without MCP tools how to reach the graph via the CLI
The SubagentStart hook matches every agent type, but agents defined with an
explicit tool list (e.g. tools: Read, Grep, Glob, Bash) have no MCP tools, and
SubagentStart input carries no tool list to detect that. Append one sentence to
the SubagentStart context only (indexed and unindexed variants) pointing such
agents at `codebase-memory-mcp cli <tool> --help`. Longest variant is ~1.1 KB,
well inside the 2 KB context buffer.

Closes #2837

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-10-29 10:48:36 -05:00
Shane McCarronandClaude Opus 5.5 8cd2ee5f42 fix(daemon): address QA round 4
Reflow the host-loop seam call to clang-format and note that the fixture's
convenience start joins its own participant guard, unlike the host's
start_reserved.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbYKGdU7pqG6AEuMQuJxiF
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-25 14:39:51 -05:00
Shane McCarronandClaude Opus 5.5 b15e7f91b9 style(tests): drop unrelated reflow hunks
Restore main's formatting on pre-existing lines that a whole-file
clang-format pass had reflowed, so the diff carries only the #2178 change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbYKGdU7pqG6AEuMQuJxiF
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-25 14:24:26 -05:00
Shane McCarronandClaude Opus 5.5 fee73ced23 test(daemon): cover host loop stop on coordination file loss
Add cbm_daemon_host_wait_for_lifetime_for_test, which runs the production
lifetime loop over caller-held coordination handles, and a runtime test that
ages the claim marker, unlinks the cohort lifetime lock, and asserts the tick
refreshes the marker, logs daemon.lifetime_end reason=coordination_file_lost
file=cohort_lease, and stops the runtime. The seam runs an ephemeral
generation, so a missed loss ends at the initial client window instead of
hanging.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NbYKGdU7pqG6AEuMQuJxiF
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-25 14:24:26 -05:00
Shane McCarron 38fc6ea995 Merge remote-tracking branch 'origin/main' into fix/cohort-lock-tmp-cleaner 2026-09-22 13:41:32 -05:00
Shane McCarronandClaude Opus 5 1133ba3246 fix(daemon): address QA round 2
- Classify touch failures explicitly. A file is lost only on proof that
  the path no longer names it: ENOENT, st_nlink 0, another inode, a
  symlink or other non-regular entry, or a replaced lock directory. Any
  other syscall failure (EIO, ESTALE, ENOMEM, EACCES on reopen) is
  transient, so a flaky runtime volume no longer stops the daemon. The
  POSIX private-lock touch and the ipc lock/marker touches share this
  rule; the Windows touch reports a failed handle query as transient.
- The identity marker is classified by path (fstatat, no follow) before
  it is reopened, so replacement by a symlink is now loss, not a
  transient reopen failure.
- Tests: symlink replacement of the identity marker (lost); an
  unreadable marker is transient (-1) and recovers (1) once readable;
  the cohort touch test ages both held files and asserts they were
  refreshed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-22 11:27:26 -05:00
Shane McCarron 7f8d1ac252 Merge remote-tracking branch 'origin/main' into fix/cohort-lock-tmp-cleaner 2026-09-21 14:58:28 -05:00
Shane McCarronandClaude Opus 5 3cf2a6663f fix(daemon): address QA round 1
- Separate loss from transient failure. Touch helpers now report OK /
  lost / transient (version_cohort: OK/UNSAFE/IO; ipc: 1/0/-1). Only a
  proven unlink or replacement stops the daemon. A refresh failure on a
  still-valid file (EROFS, EPERM, EMFILE on the marker reopen) logs
  daemon.coordination_touch_failed once, retries every tick, and logs
  daemon.coordination_touch_recovered when it clears.
- Cohort and claim locks now prove the path still names the held inode
  (private_file_revalidate against the stored directory + name), not
  just st_nlink, so rename-away, hard-link+unlink and a replaced
  directory are caught like unlink.
- The runtime service's self-owned participant guard is refreshed by
  the listener heartbeat too.
- Tests: rename-away detection; cohort lease/claim touch (lease with
  only lifetime held is OK, each unlinked file reported independently);
  IPC listener touch now checks timestamps are actually refreshed and
  covers the participant guard and identity-marker replacement.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-21 13:02:07 -05:00
Shane McCarronandClaude Opus 5 9e793df87b fix(daemon): keep held runtime files fresh and exit when one is lost
macOS com.apple.tmp_cleaner deletes /tmp files whose atime, mtime and
ctime are all older than three days. The daemon's long-held lock files
are empty and never written, so under a daemon that lives past three
days the cleaner unlinks them while they are held. The next participant
then creates and locks a fresh inode: forked index workers see the
cohort daemon marker as free, read the live daemon as uncoordinated and
exit 1 on every reindex, and a lost IPC lifetime lock or cohort lifetime
file stops protecting the generation at all. A lost socket identity
marker additionally makes listener close leave the socket pair behind.

The host lifetime loop now runs a heartbeat every 60 s over every file
the generation holds (cohort daemon claim and lease, participant guard,
IPC lifetime reservation, identity marker). Each file is refreshed with
futimens through its held descriptor, only after proving the path still
names that descriptor, so the cleaner never selects it. If any file is
already lost, the daemon logs daemon.lifetime_end
reason=coordination_file_lost and stops cleanly; the next client starts
a coordinated generation. Re-claiming in place would race that client.

Windows needs no refresh (no age-based cleaner; held lock handles deny
deletion), so its entry points only validate the handles.

Closes #2178

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-21 11:48:06 -05:00
Shane McCarronandClaude Opus 5 086422020f fix: address QA round 3
Documentation and claim accuracy only — no behavioural change. Two of the
three findings are corrections to inaccurate statements the round-2 commit
(61ccd905) introduced.

The gating test's framing was overstated, and this time the overstatement was
measured rather than argued. Replacing the backoff call with the pre-change
`ctx->now + interval_ms * US_PER_MS` leaves the watcher suite green at 82/82:
the index_backoff_* tests pass because they exercise the pure function
directly, which the reverted scheduler no longer calls, and the streak tests
pass because the counter is untouched. So no test in this suite fails when
this change's behaviour is removed. The test now records that, with the
figure, instead of claiming it demonstrates the backoff. It is a regression
guard on a deadline being assigned at all — real, but narrower than round 2
said.

Closing that gap needs either a controllable clock or a further accessor
exposing next_poll_ns. Both were judged out of scope: a third exported symbol
purely for testing, on a change already carrying two, is the API widening
earlier rounds flagged. The gap is recorded in the test, the PR description
and the round-3 note rather than papered over.

The memory-visibility comment claimed index_failures follows "the same
discipline every other per-project field in this struct already follows".
That is false: active_git is serialized by projects_lock and registered is an
atomic_bool. Narrowed to the poll-mutated fields, which are now enumerated.
The round-2 decision to document rather than lock still stands — the panel
verified the underlying premise — but it was resting on an overbroad claim.

watcher.h said the delay "doubles up to a fixed ceiling", which skips the
shift cap: the delay plateaus at interval_ms << INDEX_FAIL_SHIFT_MAX, and
that reaches the ceiling only for interval_ms >= 4688 ms. Every interval this
watcher generates is >= POLL_BASE_MS so the ceiling is always reached in
practice, but the function is exported and a caller may pass less.

Refs #2015

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-02 15:28:21 -05:00
Shane McCarronandClaude Opus 5 61ccd90511 fix: address QA round 2
Retracts an incorrect justification from the round-1 commit (4fc5999a) and
closes the coverage it was used to excuse.

MAJOR — the delay wire-up had no coverage, and the reason given for that was
wrong. Round 1 claimed the gating half was "structurally unobservable without
an injectable clock". It is not. cbm_watcher_touch zeroes next_poll_ns, so a
wire-up that never sets a deadline leaves it at zero and the callback fires on
every poll: simply NOT calling touch between polls observes gating
deterministically, no clock involved. Only the delay's MAGNITUDE needs one.

watcher_index_failure_backoff_gates_repolling_issue2015 covers it — one hard
failure, then five polls without touch asserting no re-fork, then a touch
proving the retry still gets through (so the test cannot pass by observing a
dead watcher). Mutation-verified: deleting the wire-up yields
"failing_index_calls == 6, expected 1".

MINOR — round 1's F1 was half-fixed. Of the two single-token mutations that
finding named, only the streak reset was covered; widening the
sustained-failure "==" to ">=" still passed. watcher_sustained_failure_logs_
once_issue2015 drives 14 consecutive failures through a cbm_log_set_sink and
asserts exactly one emission. Mutation-verified: ">=" yields
"sustained_log_hits == 5, expected 1".

All three mutations F1 named are now demonstrated caught rather than argued
about: reset removal (round 1), wire-up deletion, and the "==" widening.

MINOR — cbm_watcher_index_failure_count reads index_failures under
projects_lock while poll_project writes it lock-free from a state snapshot.
That is the discipline every other per-project field here already follows, so
this documents the visibility contract rather than making one field atomic in
isolation: the accessor is a diagnostic and test seam, not a synchronisation
point, and must not carry scheduling decisions without atomic accessors first.

MINOR — watcher.h still described the pre-F3 backoff contract. It now states
the clamp is a floor as well as a cap, why the floor exists for an exported
function whose reachable inputs cannot currently hit it, and that negative
inputs are treated as zero.

Still uncovered, now stated precisely rather than hand-waved: the delay's
MAGNITUDE. Nothing asserts that ten failures produce a five-minute delay
rather than a five-second one. Gating is proven; the numbers are not, and
that does need a controllable clock.

Refs #2015

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-02 14:56:05 -05:00
Shane McCarronandClaude Opus 5 4fc5999a85 fix: address QA round 1
F1 (major) — the failure state machine the backoff feeds had no test
coverage. Deleting the streak reset, or widening the sustained-failure
comparison, left all 7788 tests green: the index_backoff_* tests cover the
delay arithmetic, and nothing covered the counter that selects which delay
applies. A streak that never increments silently degrades the backoff to the
unbounded retry it exists to prevent.

Adds cbm_watcher_index_failure_count(), alongside the existing
cbm_watcher_watch_count() in watcher.h's "Introspection (for testing)"
section, and a test that drives a real git repo through fail/fail/succeed on
the failing_index_callback seam already used by the #937 test, asserting the
streak goes 0 -> 1 -> 2 -> 0. Verified by mutation: removing the reset turns
it red at the reset assertion while every index_backoff_* test stays green.

The delay-GATING half stays uncovered on purpose. Every integration test
calls cbm_watcher_touch first, which zeroes next_poll_ns, so whether the
scheduler honours the computed deadline is unobservable without an
injectable clock the watcher does not have. That harness is a larger change
than this fix.

F3 (minor) — cbm_watcher_index_backoff_ms was non-monotonic for an interval
above the ceiling: zero failures returned the interval, one failure returned
the smaller ceiling. Unreachable today (POLL_MAX_MS < the ceiling), but the
function is exported, so the clamp is now a floor as well as a cap and
backing off can never schedule sooner than the project's own cadence.

F2 (minor) needed no code change: a busy-skip following an unresolved
failure streak keeps the backed-off delay, which is deliberate — only a
success clears the streak, and treating rc > 0 as recovery would let a
project alternating fail/busy never back off at all. The PR description's
over-strong "busy-skip is behaviourally unchanged" claim is corrected there.

Refs #2015

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-02 14:18:02 -05:00
Shane McCarronandClaude Opus 5 76e54f7525 fix(watcher): back off re-forking a hard-failing index worker
A hard index failure (index_fn < 0) left the retry cadence untouched, so a
persistently failing project re-forked an index worker every poll for as
long as the daemon lived. Observed on 0.10.8/macOS: 2233 consecutive failed
workers over 4h43m, ~8/min, every one dying at the same worker-side
coordination seal. Read queries stayed healthy throughout, so nothing
surfaced to the user while no project could be indexed at all.

#937 deliberately leaves the baseline uncommitted on a failed reindex so the
change is retried rather than lost. That guarantee is kept; what changes is
the cadence. Each consecutive hard failure doubles the delay, capped at five
minutes, taking a permanently failing project from ~480 attempts/hour to
~12 while still recovering on its own once the cause clears. Busy-skip
(rc > 0) and success paths are untouched.

The delay arithmetic is a pure exported helper so it is unit-testable
without a clock. watcher.index.err now carries rc and the consecutive-
failure count, and a distinct watcher.index.sustained_failure names a
project whose failures are clearly not transient.

Note: itoa_buf returns a single shared per-thread buffer, so the two-value
log line uses local buffers rather than two itoa_buf calls.

Refs #2015

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FbrM52pmB1m2SR1vRRoNnf
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-09-02 13:02:34 -05:00
Shane McCarron da046da59f fix(ts-runtime): bound GLR stack-merge recursion to prevent stack overflow
tree-sitter's GLR parser merges ambiguous parse-stack heads recursively in
stack_node_add_link (internal/cbm/vendored/ts_runtime/src/stack.c), once per
nesting level. On pathologically nested input whose grammar is ambiguous
(Perl's paren-optional calls, f(f(f(...)))), that recursion overflows the
native stack during the parse — a small ~1 MB Windows thread stack, and even
an 8 MB POSIX stack at extreme depth — before any extraction runs. Grammars
that are unambiguous here (Java, C++) never trigger the recursive merge.

Cap the recursive merge at CBM_TS_STACK_MERGE_MAX_DEPTH (512) via a
depth-tracked inner worker; the public stack_node_add_link is a thin wrapper
seeding depth 0, so every existing call site is unchanged. Past the cap the
ambiguity is left on the GLR stack instead of merged — a valid parse, never a
wrong one. 512 frames is well within a 1 MB stack while far exceeding any
realistic source nesting.

Add perl_glr_deep_parse_recursion_capped: a crash-isolating regression that
parses deep ambiguous Perl directly (a forked child on POSIX, in-process on
Windows), bypassing cbm_extract_file's CBM_PERL_MAX_PARSE_NESTING pre-parse
guard so the vendored cap itself is exercised rather than the guard.

Narrow vendored-runtime change; no other parsing behavior is affected. The
higher-level Perl pre-parse guard (issue #913 context) stays in place and can
be retired in a follow-up once this cap is proven in the field.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-10 07:58:04 -05:00
Shane McCarron 0815d78377 fix(perl-lsp): iterate wide AST children via cursor, batch method-table build
Two O(n^2) patterns in the Perl LSP, the class the ARM
extract_wide_flat_file_is_linear guard flags:

- Every child-iteration loop used indexed ts_node_child(node, i), which is
  O(i) per call and O(n^2) over a wide flat node. Route them all through a new
  perl_collect_children() helper mirroring wd_collect_children() in
  extract_defs.c: a forward TSTreeCursor collect for wide nodes (>= 64
  children), NULL (cheap indexed access) for the small common case.

- perl_attach_methods rebuilt each package's whole NULL-terminated method
  array on every sub via perl_type_add_method, O(subs^2) on a single-package
  file. Replace with a collect -> qsort-by-package -> build-once batch
  (O(S log S)); source order within a package is preserved so first-defined
  still wins on redefinition.

No behavior change: all perllsp_* resolution tests and
lsp_perl_deep_expression_no_crash pass; check-lsp-originality.sh reports clean.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:47:33 -05:00
Shane McCarron 89aecfd2fc fix(perl-lsp): guard pathologically nested Perl before tree-sitter parse
A deeply nested Perl call chain f(f(f(...))) overflowed the stack while
parsing on small-stack platforms: tree-sitter's GLR parser recurses once
per nesting level in stack_node_add_link (vendored ts_runtime/src/stack.c)
merging the ambiguous parse-stack heads that Perl's f(...) grammar produces.
The overflow happens during ts_parser_parse, before any LSP walk runs, so
the CBM_LSP_PERL_MAX_WALK_DEPTH guards can never fire. This crashed
test-windows (CLANG64) with an AddressSanitizer stack-overflow and hung
test-unix (ubuntu-24.04-arm) to a 4h timeout on lsp_perl_deep_expression_no_crash.

Add cbm_source_nesting_exceeds(): an O(n) early-exit bracket-depth scan, and
skip extraction (clean has_error, zero edges) for Perl input whose nesting
exceeds CBM_PERL_MAX_PARSE_NESTING (128) before it reaches tree-sitter.
Scoped to Perl because only its ambiguous call grammar drives the GLR
recursion to the nesting depth; C/Java/Python parse the same shape with a
single stack head. 128 is ~10x above any real Perl nesting and well below
the depth where the parser overflows a 1MB stack.

Correct the lsp_perl_deep_expression_no_crash comment to describe the actual
mechanism (parser recursion, not the LSP walk).

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 868c4097f5 docs(perl-lsp): document Perl in the Hybrid LSP section
Perl gained a full Hybrid LSP tier in this PR but was absent from the Hybrid LSP docs. Add a capabilities row to the "Languages with full Hybrid LSP" table, list Perl in the intro + features language lists, and bump the badge from 9 to 10 languages. Perl was already in the general language-support list and the resolution-quality tier (grammar-level); this covers the new type-resolution tier.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron a0571049b7 fix(perl-lsp): address QA round 2 follow-ups
Optional, non-blocking follow-ups from the #461 review:

- perl_eval_function_call_type: split qualified static calls on the LAST
  "::" (mirroring perl_resolve_function_call) so return-type chaining on
  Foo::Bar::sub() resolves instead of yielding unknown.
- cbm_run_perl_lsp: finalize the type registry into a per-call scratch
  arena (as php_lsp/java_lsp do) before resolution, so method/func/symbol
  lookups use O(1) hash buckets instead of a per-file linear scan.
- perl_infer_self_type: match the `shift` invocant idiom at word
  boundaries and accept `$_[0]`, so `my $self = shifty();` no longer
  mis-binds the receiver (tightens the zero-edge guarantee).

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron f3e6bcf713 chore(planning): drop bundled VBW planning summary from PR
Remove .vbw-planning/phases/22-perl-lsp-semantic-resolution/22-03-SUMMARY.md
per maintainer request — internal planning/scratch doc that should not land
in the repo.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron a100820243 fix(perl-lsp): split qualified calls on last "::" so multi-level packages resolve
perl_resolve_function_call split a package-qualified call name on the FIRST
"::" via strstr. For multi-level packages (Foo::Bar::sub) this yielded
pkg "Foo" and a sub name still containing "::" ("Bar::sub"), which never
resolved. The call then fell through to the generic bare-name fallback, which
matches on the bare sub name only — collapsing distinct packages' same-named
subs onto a single winner and orphaning the rest. Single-level calls
(Foo::sub) were unaffected, which is why the existing test missed it.

Split on the LAST "::" instead so the full package name reaches
perl_lookup_method, which resolves to the correct fully-qualified sub.

Adds perllsp_static_multilevel_package_call covering the multi-level form.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron aa97ec55f2 fix(perl-lsp): address QA round 1
F1: dot the Exporter import target so seeded CPAN exported subs resolve.
perl_collect_qw_imports built colon-form targets (Scalar::Util::blessed)
but the stdlib registry keys curated CPAN subs in dotted form
(Scalar.Util.blessed) and lookup is exact-match. Wire in perl_pkg_to_dot
to dot the module portion and drop the now-unnecessary (void) cast.

F2: add a recursion-depth guard (CBM_LSP_PERL_MAX_WALK_DEPTH=512) to both
AST walkers (perl_resolve_calls_in_node, perl_pass1_scan) via a depth-
guarded wrapper + inner split, mirroring java_lsp's JAVA_LSP_MAX_WALK_DEPTH.
Past the cap a subtree is skipped (graceful degradation, no wrong edge),
preventing stack overflow on pathologically nested input.

F3: lock the shared last-"::"-segment normalization in lsp_resolve.h with a
direct regression test over cbm_pipeline_find_lsp_resolution: a qualified
static call still resolves AND the cross-namespace mis-attribution edge case
is bounded by caller-QN equality + the confidence floor.

F4: implement SUPER:: dispatch. Populate enclosing_parent_qn from the
enclosing package's first @ISA parent and resolve $self->SUPER::method() to
that parent's method (strategy perl_method_super). No known parent or
unresolved method emits no edge (zero-edge guarantee preserved).

Tests: perllsp_cpan_exported_function, perllsp_super_dispatch,
perllsp_super_no_parent_no_edge, lsp_perl_deep_expression_no_crash,
lsp_resolve_qualified_static_call_normalizes_colons,
lsp_resolve_misattribution_is_bounded.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 83f907a84c test(perl-lsp): add Perl LSP resolution suite mirroring test_php_lsp.c
Replace the plan-01 placeholder with the full Perl LSP test suite covering the
ten foundational resolution scenarios from 22-RESEARCH.md, plus the extract_perl
/ find_resolved / require_resolved / find_resolved_with_strategy helpers cloned
from test_php_lsp.c:

  1. method via bless-assignment      6. use parent MRO
  2. constructor class-method type    7. use base MRO
  3. static package call              8. Exporter import (use Mod qw(f))
  4. $self method dispatch            9. require fallback
  5. @ISA inheritance               10. unresolvable receiver -> zero edges

Assertions match the resolver's actual QN scheme (module_qn.subname, dotted —
the Perl package governs dispatch, not the emitted QN), and the negative test
confirms the zero-edge guarantee for untyped scalar and unindexed package
receivers. Register suite_perl_lsp in test_main.c so scripts/test.sh runs it.

All 10 perl_lsp tests pass (3563 passed, 1 pre-existing unrelated failure:
search_code_multi_word in tests/test_mcp.c).

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 7e487ecca5 fix(perl-lsp): collect @ISA parents from parenthesized RHS siblings
perl_collect_isa_assignment relied on the assignment's `right` field, but
tree-sitter-perl flattens a parenthesized RHS (`our @ISA = ('Base')`) so the
`right` field points at the `(` token while the parent string literals are
sibling children of the assignment. The single form `@ISA = 'Base'` worked but
the common parenthesized form silently collected zero parents, so @ISA
inheritance never populated embedded_types and method dispatch could not walk
the MRO.

Scan every named child after the `=` instead of only the `right` field, covering
both `@ISA = 'Base'` and `@ISA = ('Base', 'Other')`. use parent / use base were
already handled via a separate path and are unaffected.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 9c6b26e55d fix(perl-lsp): surface resolved method/static calls as graph CALLS edges
- Add method_call_expression to perl_call_types in lang_specs.c so the
  structural tier emits a method-call edge for the LSP bridge to refine
  (parity with PHP member_call_expression). callee_name is the bare method
  via the field-based extractor's `method` branch.
- Normalize the textual callee in cbm_pipeline_find_lsp_resolution to its
  last "::"-separated segment so qualified static Pkg::sub() calls match the
  resolved sub's dotted short-name (parity with PHP scoped_call_expression).
- Zero-edge guarantee preserved: untyped receivers still emit no edge.

Resolves DEVN-04 from plan 22-03. Verified on a Base/Derived/main fixture:
run_typed->{greet,describe}, run_static->helper, run_classcall->greet,
describe->greet (inherited), run_untyped->none.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron f0aa57c1c6 feat(perl-lsp): implement core semantic resolution (package/sub/method/ISA/bless/exports)
Replace the plan-01 no-op walk in perl_lsp.c with the full Perl resolver,
mirroring php_lsp.c's architecture. Touches ONLY perl_lsp.c.

- Two-pass perl_lsp_process_file: PASS 1 collects package_statement context
  (packages may switch mid-file), @ISA / use parent / use base inheritance,
  and Exporter `use Foo qw(...)` import maps; PASS 2 walks
  subroutine_declaration_statement bodies.
- process_subroutine: pushes a scope, sets enclosing_func_qn (module_qn.sub --
  the structural QN scheme, verified via helpers.c cbm_enclosing_func_qn), and
  binds the $self/$class invocant (my $X = shift idiom) to the package type.
- perl_eval_expr_type: sigil-aware scalar scope lookup, method/function call
  dispatch, bless($r,'Class') literal (0.95) + ref($class)||$class inferred
  (0.75), assignment RHS propagation, ClassName->new => ClassName; recursion-
  guarded via eval_depth (cap 8, mirrors php).
- perl_find_isa via @ISA assignment, use parent, use base; perl_lookup_method
  walks the @ISA chain (embedded_types) bounded by CBM_LSP_MAX_LOOKUP_DEPTH.
- Call/method dispatch + emit: Package::sub() static, bare/imported func(),
  and typed-receiver $obj->m / Class->m / $self->m emit CBMResolvedCall.
  Unresolvable receivers emit NO edge (zero-edge guarantee); symbol-table
  aliasing ignored.

Tree-sitter-perl node/field names verified against the vendored compiled
grammar (parser.c ts_symbol_names/ts_field_names): method_call_expression uses
fields invocant+method; package_statement uses name; use_statement uses module;
variable_declaration target is field `variable` (singular). Documented in a
file-header comment.

Build green (scripts/build.sh); scripts/test.sh 3553 passed / 1 pre-existing
unrelated failure (search_code_multi_word). clang-format clean.

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 69c8155335 feat(perl-lsp): seed common CPAN OOP modules in perl_stdlib_data.c
- Register module-qualified exported subs under dotted QNs so an
  Exporter import map (plan 22-03) can resolve `use Foo::Bar qw(...)`:
  - Scalar::Util (blessed, reftype, weaken)
  - List::Util (sum, max, min, first, reduce)
  - Carp (croak, carp, confess, cluck)
  - POSIX, Storable, Data::Dumper entry points
- Moose meta stubs deferred (Open Question #4)

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 13340267a7 feat(perl-lsp): seed perlfunc core builtins in perl_stdlib_data.c
- Replace plan-01 placeholder with ~28 perlfunc core builtins
  (print, push, shift, map, sort, keys, bless, defined, exists, ...)
- Register as global, package-less functions via REG_BUILTIN
- Add REG_FUNC macro for upcoming module-qualified CPAN seed

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 447338bd9b feat(perl-lsp): wire dispatch, unity build, and Makefile
- cbm.c: include lsp/perl_lsp.h and dispatch CBM_LANG_PERL -> cbm_run_perl_lsp
- lsp_all.c: unity-include perl_lsp.c + generated/perl_stdlib_data.c
- Makefile.cbm: register TEST_PERL_LSP_SRCS and append to ALL_TEST_SRCS
- tests/test_perl_lsp.c: placeholder suite so the Makefile var resolves
  (full suite + test_main.c registration land in plan 22-04)

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 06d14a6ef8 feat(perl-lsp): add stub perl_stdlib_data.c
- Define cbm_perl_stdlib_register with a REG_BUILTIN macro (php_stdlib shape)
- Register placeholder builtins (print, bless, ref) so the symbol links
- TODO(plan 22-02): full perlfunc + CPAN seed

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 38db990a51 feat(perl-lsp): add perl_lsp.c skeleton with no-op phases A/B/C
- perl_node_text + perl_pkg_to_dot (Foo::Bar -> Foo.Bar) helpers
- perl_lsp_init zeroes context (arena/source/registry/current_package_qn)
- cbm_run_perl_lsp runs phases A (stdlib register), B (file-def Function/Method
  registration, return types unknown), C (init + empty walk)
- Emits zero resolved-call edges; real resolution lands in plan 22-03

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 0add9d2ab8 feat(perl-lsp): add perl_lsp.h header with PerlLSPContext
- Declare PerlLSPContext mirroring PHPLSPContext (package/@ISA/bless/export maps)
- Public decls: init, add_use, process_file, eval_expr_type, resolve_package_name,
  lookup_method, cbm_run_perl_lsp entry, cbm_perl_stdlib_register
- Stub-declare cbm_run_perl_lsp_cross for a later cross-file plan

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-07-09 14:09:39 -05:00
Shane McCarron 5255939a95 fix(install): address QA round 1
QA round 1 (PR #632) flagged that the SubagentStart hook uses matcher "*",
the same matcher a user is most likely to pick for their own catch-all
SubagentStart hook. Because is_cmm_hook_entry keyed ownership on the matcher
string alone, install would remove the user's "*" entry and replace it with
ours, and uninstall could remove the wrong one.

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

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

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Shane McCarron 3466ba02e8 test(install): cover SubagentStart hook registration
Verify the Claude SubagentStart reminder: install writes a SubagentStart
entry with a match-all matcher pointing at cbm-subagent-reminder, a second
upsert is idempotent (no duplicate entry), and removal leaves no SubagentStart
key behind.

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

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

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
2026-06-29 08:42:36 -05:00
Shane McCarron dedd33d975 fix(install): respect $CLAUDE_CONFIG_DIR in install/uninstall/update
Route Claude Code config paths (skills, .mcp.json, .claude.json,
settings.json, hook scripts) and agent detection through
CLAUDE_CONFIG_DIR-aware helpers, falling back to ~/.claude. Hook command
strings written to settings.json keep the legacy tilde form when the env
var is unset, so existing configs stay portable across HOME values. Prints
a one-line migration nudge when CLAUDE_CONFIG_DIR is set and a legacy
~/.claude tree still exists.

Adds cli_detect_agents_finds_claude_via_env and isolates CLAUDE_CONFIG_DIR
in the existing detection tests so the runner env can't leak in.

Distilled from #321 onto current main (adapts to the v0.7.0 non-blocking
augmenter hook signature, which the original branch predated). Closes #320.
2026-05-30 15:22:37 +02:00
Shane McCarronandClaude Sonnet 4.6 822a56f0df fix(mcp): fix use-after-free in handle_manage_adr get path
yyjson_mut_obj_add_str stores the raw pointer without copying. Freeing
buf before yy_doc_to_str serializes the document causes yyjson to read
freed heap memory, producing garbage JSON. cbm_jsonrpc_format_response
then fails to parse the result field (res_doc == NULL), so "result" is
omitted from the JSON-RPC response and the MCP client hangs indefinitely.

Fix: hoist adr_buf to function scope (initialized NULL), remove the
premature free, and free it after yy_doc_to_str has serialized the doc.

Adds regression test tool_manage_adr_get_with_existing_adr that FAILS
before this fix and PASSES after it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-23 08:25:42 -05:00
Shane McCarronandClaude Sonnet 4.6 a3f11bd7fa fix(mcp,store): address QA round 2
- Fix handle_get_code_snippet inline store-NULL check to return the same
  JSON error format as REQUIRE_STORE and the other inline checks:
  {"error":"no project loaded"} instead of a plain string
- Fix smoke_guard.sh query_graph invocation: pass "query" parameter
  (not "cypher") to match what handle_query_graph actually reads; the
  wrong key caused the handler to early-return before reaching the guard
- Remove extra blank line in handle_get_graph_schema guard block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:33:10 -05:00
Shane McCarronandClaude Sonnet 4.6 2791d0d8af fix(mcp,store): address QA round 1
- Update stale comment in verify_project_indexed: resolve_store now uses
  cbm_store_open_path_query (no SQLITE_OPEN_CREATE), so store is NULL for
  missing files; the helper catches the empty-but-present .db case
- Guard state updates in resolve_store behind successful open check: only
  set owns_store=true and update current_project when store is non-NULL,
  preventing misleading state when an unknown project is queried
- Expand smoke_guard.sh to test all 5 guarded handlers (search_graph,
  query_graph, get_graph_schema, trace_call_path, get_code_snippet) instead
  of search_graph only; each checks both the guard error and no-ghost-file
  invariant

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:28:27 -05:00
Shane McCarronandClaude Sonnet 4.6 e6717457d7 test(smoke): add smoke_guard.sh for guard and ghost-file invariants
Asserts that query handlers return a guard error for unknown projects
and that no ghost .db file is created in the cache directory.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:18:21 -05:00
Shane McCarronandClaude Sonnet 4.6 a109e9753e fix(store,mcp): prevent ghost .db file creation for unknown projects
- Add cbm_store_open_path_query() that opens with SQLITE_OPEN_READWRITE
  only (no SQLITE_OPEN_CREATE); returns NULL when file is absent
- Declare cbm_store_open_path_query() in store.h
- Change resolve_store() in mcp.c to call cbm_store_open_path_query
  so querying a nonexistent project never creates a ghost .db file
- Indexing path (cbm_store_open_path) retains SQLITE_OPEN_CREATE

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:18:21 -05:00
Shane McCarronandClaude Sonnet 4.6 172d3a658a fix(mcp): extend project-not-indexed guard to all query handlers
- Add static helper verify_project_indexed() before handle_get_graph_schema
- Replace inline guard in handle_get_architecture with helper call
- Apply guard to handle_search_graph, handle_get_graph_schema,
  handle_trace_call_path, handle_get_code_snippet, handle_query_graph
- All five query handlers now return {"error":"project not indexed —
  run index_repository first"} instead of silently returning empty results

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 18:18:21 -05:00
Shane McCarronandClaude Sonnet 4.6 804d680c0d fix(store): set busy_timeout before journal_mode=WAL to prevent immediate SQLITE_BUSY on lock contention
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-22 14:42:03 -05:00
Shane McCarronandClaude Sonnet 4.6 1a7c6ae8d2 fix(k8s): add k8s pass to parallel pipeline path
The parallel execution path (used for repos above MIN_FILES_FOR_PARALLEL)
was missing the cbm_pipeline_pass_k8s() call. The pass existed only in
the sequential fallback path, so any repo large enough to trigger parallel
indexing produced zero k8s Resource and kustomize Module nodes.

Discovered via live testing against a 954-file gitops repo: the parallel
path was taken, k8s pass was silently skipped, and search_graph returned
0 Resource nodes despite 213 kustomize overlays and hundreds of manifests.

Add the pass after cbm_parallel_resolve() in the parallel branch, matching
the same pattern (fail-open, cancel check, timing log) as the sequential path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 15:08:42 -05:00
Shane McCarronandClaude Sonnet 4.6 94882c26f9 fix(k8s): address QA round 3 findings
- Check return value of cbm_pipeline_pass_k8s() in incremental path;
  log warning on failure (fail-open, matches full pipeline behaviour)
- Add comment documenting pass ordering and the two known structural
  limitations: File→Resource DEFINES edges and cross-file kustomize
  IMPORTS edges are not emitted in incremental (gbuf only contains
  nodes for changed files; File nodes from pass_structure are absent)
- Fix clang-format violation in incremental_kustomize_module_indexed
  test (single-line if condition)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 14:59:12 -05:00
Shane McCarronandClaude Sonnet 4.6 a90ab5d37b fix(k8s): add k8s pass to incremental pipeline
cbm_pipeline_pass_k8s() was called in the full pipeline (pipeline.c)
but absent from pipeline_incremental.c. This meant k8s Resource nodes
and kustomize Module nodes were never created or updated during
incremental re-indexing — only after a fresh full index.

Add the pass after the semantic pass, following the same timing-log
pattern as the other incremental passes. Pass changed_files (not the
full file list) so only modified/added YAML files are re-processed.

Add two regression tests:
- incremental_k8s_manifest_indexed: full index + add manifest via
  incremental, verifies Resource node appears in the DB
- incremental_kustomize_module_indexed: same for kustomization.yaml
  Module node

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 14:38:41 -05:00
Shane McCarronandClaude Sonnet 4.6 2af9e60738 fix(cbm): replace O(N²) indexed loop with O(N) cursor in parse_generic_imports
ts_node_child(root, i) and ts_node_next_sibling() both call
ts_node_child_with_descendant() internally, making sibling iteration
O(N²) total on roots with many children (e.g. generated TypeScript
.d.ts files or large Perl files with 5,000–50,000 top-level nodes).

Replace with TSTreeCursor: ts_tree_cursor_goto_next_sibling() maintains
traversal state across calls and is O(1) per step, reducing total
complexity from O(N²) to O(N).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 13:10:51 -05:00
Shane McCarronandClaude Sonnet 4.6 5b534ce465 fix(mcp): address QA round 2 findings
- Add explicit fallback path when fcntl(F_GETFL) fails: skip the FILE*
  peek and fall through directly to blocking poll so idle eviction still
  fires on timeout (Finding 1)
- Strengthen C unit test: verify id:1 (initialize) and id:2 (tools/list)
  response IDs are both present, not just a substring match on "tools"
  (Finding 2)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 12:47:11 -05:00
Shane McCarronandClaude Sonnet 4.6 5ca5f3291f fix(mcp): address QA round 1 findings
- Use O_NONBLOCK + clearerr() in Phase 2 fgetc probe to preserve the
  60s idle eviction timeout when both kernel fd and FILE* buffer are
  empty (fgetc on a blocking fd would otherwise block indefinitely,
  bypassing Phase 3 poll timeout and preventing cbm_mcp_server_evict_idle)
- Add #include <fcntl.h> for fcntl()/O_NONBLOCK
- Fix comment: "two-phase" → "three-phase" (implementation has 3 phases)
- Improve Python integration test: verify id:1 (initialize) and id:2
  (tools/list) response IDs are both present, not just "tools" substring

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 12:37:28 -05:00
Shane McCarron 16552befdd test(mcp): add rapid-init integration test for poll/getline buffering fix 2026-03-21 12:03:52 -05:00
Shane McCarron b8b2dd356b fix(mcp): resolve poll/getline FILE* buffering mismatch causing tools/list hang 2026-03-21 12:00:41 -05:00
Shane McCarron 2704fdf734 chore(branch): create fix/mcp-stdio-buffering off main 2026-03-21 11:59:40 -05:00
Shane McCarronandClaude Sonnet 4.6 340862c619 fix(k8s): address QA round 2 findings
- CONTRIBUTING.md: fix CBMLanguage enum file reference from
  non-existent cbm_language.h to internal/cbm/cbm.h
- CONTRIBUTING.md: rephrase "do not use tree-sitter grammars" to
  "do not require a new tree-sitter grammar", clarifying they reuse
  the existing YAML grammar
- pass_k8s.c: correct header comment from "per top-level resource
  document" to "first document only — multi-document YAML is not
  yet supported"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 94841e0542 fix(k8s): address QA round 1 findings
- [Major 1+2] pass_k8s: always re-extract K8s manifests with CBM_LANG_K8S,
  discarding any cached YAML result; pass already-read source buffer to
  handle_k8s_manifest to eliminate the double file read
- [Minor 3] CONTRIBUTING.md: fix extractor path to internal/cbm/extract_k8s.c
  and clarify tree-sitter YAML grammar usage
- [Minor 4] language.c: add comment documenting intentional case-sensitive
  FILENAME_TABLE and case-insensitive cbm_is_kustomize_file() split behaviour
- [Minor 5] test_pipeline.c: add k8s_extract_manifest_multidoc test pinning
  single-document-per-file extraction behaviour
- [Minor 6] pass_infrascan: replace strlen with strnlen(content, 4096) in
  cbm_is_k8s_manifest to bound the scan
- [Minor 7] extract_k8s: add "crds" to is_kustomize_list_key
- [Nit 8] extract_k8s: remove dead second block_node unwrap in
  extract_k8s_scalars metadata descent
- [Nit 10] extract_k8s: use cbm_arena_strdup for def.label "Resource"

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 e696fcdf80 style(k8s): apply clang-format to k8s and infra-pass source files
- Fix alignment violations in extract_k8s.c, pass_k8s.c, pass_infrascan.c
- Fix spacing in language.c filename table rows (kustomization.yml entry)
- Fix alignment in cbm.h CBMLanguage enum comments
- Fix pipeline.c empty-body brace style

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 5a663b9c84 docs(k8s): document infra-pass pattern in CONTRIBUTING.md
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 e8030fe638 docs(k8s): add Resource to Node Labels list in README
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 337694dc65 test(k8s): tests for detection helpers, extractors, and pipeline pass
- Add TEST(infra_is_kustomize_file): positive/negative/NULL cases
- Add TEST(infra_is_k8s_manifest): apiVersion present/absent, kustomize
  file returns false, NULL guards
- Add TEST(k8s_extract_kustomize): asserts 2 imports (deployment.yaml,
  service.yaml) from Kustomization resources list
- Add TEST(k8s_extract_manifest): asserts Resource def with label
  "Resource" and name containing "Deployment"
- Add TEST(k8s_extract_manifest_no_name): no crash, has_error==false
- Fix extract_k8s.c get_scalar_text() to unwrap flow_node wrappers
  (tree-sitter YAML grammar wraps plain_scalar in flow_node)
- Fix pass_k8s.c missing #include "foundation/compat.h" for CBM_TLS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 bf3af55bf2 feat(k8s): pipeline pass for k8s manifest parsing and graph node emission
- Add pass_k8s.c: cbm_pipeline_pass_k8s() iterates files, classifies
  kustomize overlays via cbm_is_kustomize_file() and k8s manifests via
  cbm_is_k8s_manifest(), emits Module/Resource nodes and IMPORTS/DEFINES edges
- Kustomize files emit Module node (cbm_infra_qn) + IMPORTS edges per resources entry
- K8s manifest files emit Resource nodes per top-level document with DEFINES edge
- Falls back to file re-read + re-extraction when result_cache is unavailable
- Declare cbm_pipeline_pass_k8s() prototype in pipeline_internal.h
- Add pass_k8s.c to PIPELINE_SRCS in Makefile.cbm
- Call pass after definitions pass in pipeline.c sequential path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 88ddb63e8b feat(k8s): K8s and Kustomize YAML extractors in the CBM layer
- lang_specs.c: add LangSpec entries for CBM_LANG_KUSTOMIZE and CBM_LANG_K8S
  (both reuse tree_sitter_yaml()); add cbm_ts_language() switch cases
- extract_k8s.c: new file implementing cbm_extract_k8s(); kustomize path
  walks block_sequence items under resources/bases/patches/components/
  patchesStrategicMerge and emits CBMImport per scalar; k8s path extracts
  apiVersion/kind/metadata.name and emits CBMDefinition with label "Resource"
  and name "Kind/metadata-name"; malformed manifests (missing kind or name)
  produce zero definitions
- cbm.h: declare cbm_extract_k8s() alongside other sub-extractor entry points
- cbm.c: call cbm_extract_k8s() after unified extraction for the two new langs
- Makefile.cbm: add extract_k8s.c to EXTRACTION_SRCS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 ee769d5806 feat(k8s): detection helpers, CBMLanguage enum, and language table entries
- Add CBM_LANG_KUSTOMIZE and CBM_LANG_K8S to CBMLanguage enum (before CBM_LANG_COUNT)
- Add kustomization.yaml/yml to FILENAME_TABLE mapped to CBM_LANG_KUSTOMIZE
- Add Kustomize and Kubernetes entries to LANG_NAMES
- Implement cbm_is_kustomize_file() with to_lower+strcmp pattern
- Implement cbm_is_k8s_manifest() scanning first 4KB for apiVersion: via ci_strstr()
- Declare both helpers in pipeline_internal.h Infrascan helpers section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:29:22 -05:00
Shane McCarronandClaude Sonnet 4.6 57e08e616b fix(discover): address QA round 2 findings
- Defensively free p->userconfig in cbm_pipeline_free in case run() was
  never called (e.g. pipeline created but not executed before free)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:25:33 -05:00
Shane McCarronandClaude Sonnet 4.6 1bfe473a76 fix(discover): address QA round 1 findings
- pipeline.c: inline userconfig cleanup on early-exit paths (discover
  failure and cancel check) so userconfig is always freed, fixing the
  resource leak when pipeline exits before reaching the cleanup label
- userconfig.c: free partially-built entries array on global config
  parse failure to fix alloc-failure memory leak
- language.c: probe user config for compound extensions (e.g.
  ".blade.php") by scanning from the first dot to the last, falling back
  to built-in single-extension lookup — preserves built-in behavior
  while correctly resolving user-defined compound extensions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:25:33 -05:00
Shane McCarronandClaude Sonnet 4.6 1cdb98319a feat(discover): add user-defined extension→language mappings via config files
Read extra_extensions from ~/.config/codebase-memory-mcp/config.json
(global) and {repo}/.codebase-memory.json (project), merging them with
project config taking precedence. Unknown language values warn and skip.
Missing files are silently ignored (fail-open).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 17:25:33 -05:00
Shane McCarronandClaude Sonnet 4.6 a0e809d425 fix(store): address QA round 2 findings
- Validate child exit status (WIFEXITED + WEXITSTATUS) after waitpid so
  the test fails fast if the child couldn't open the store, rather than
  passing vacuously due to the "crashed" row never being written

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:17:55 -05:00
Shane McCarronandClaude Sonnet 4.6 675fdf341e fix(store): address QA round 1 findings
- Wrap bulk_crash_recovery test and its POSIX includes with #ifndef _WIN32
  guards to fix compilation failure on Windows (fork/waitpid unavailable)
- Add negative assertion that the uncommitted "crashed" row is absent
  after crash recovery, completing the test's correctness verification

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 18:07:17 -05:00
Shane McCarronandClaude Sonnet 4.6 8dd490b47c fix(store): keep WAL journal mode during bulk write to prevent DB corruption on crash
cbm_store_begin_bulk() was switching the SQLite journal mode from WAL to
MEMORY for write throughput.  If the process crashed mid-bulk-write the
in-memory rollback journal was lost, leaving the database file in a
partially-written, unrecoverable state.

WAL mode is inherently crash-safe: uncommitted WAL entries are discarded
on the next open.  The performance benefit of bulk mode is preserved via
synchronous=OFF and an enlarged cache_size, both of which are safe under
WAL.

Remove the PRAGMA journal_mode = MEMORY from cbm_store_begin_bulk and
the matching PRAGMA journal_mode = WAL from cbm_store_end_bulk.  Update
the header comments to reflect the new invariant.

Add tests/test_store_bulk.c with three tests:
- bulk_pragma_wal_invariant: asserts journal_mode remains "wal" after
  cbm_store_begin_bulk via an independent read-only connection
- bulk_pragma_end_wal_invariant: asserts journal_mode remains "wal" after
  cbm_store_end_bulk
- bulk_crash_recovery: forks a child that enters bulk mode, opens an
  explicit transaction, writes data, then calls _exit() without committing;
  the parent verifies the database opens cleanly and baseline data survives

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-19 17:21:48 -05:00