The previous commit's clang-format -i rewrote all of
test_edge_types_probe.c (+732/-358 for a 48-line test addition) - my
local formatter's whole-file drift, not real non-conformance (lint-ci
passes on the restored file). Restore the file to main's formatting
with only the two #952 tests added.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Real Laravel apps register routes through the Illuminate facade, whose
class lives in vendor/ and is never indexed - and for that style the
graph minted ZERO Route nodes, flat or grouped. Three stacked defects:
1. The callee of Route::get(...) (scoped_call_expression) was
extracted as bare 'get' via generic field resolution, so the
empty-resolution route fallback ('::get' suffix table) could never
engage. The extractor now emits the qualified 'Route::get' form,
gated to the literal Route scope AND a route-method table match so
Cache::get / Config::get keep bare names and can never mint junk
(guarded by an inverse test; the path gate additionally requires a
leading '/').
2. The sequential pass had no ROUTE_REG empty-resolution fallback at
all (its comment claimed parity with the parallel path, which has
one) - unresolvable route registrations were dropped entirely.
Added, mirroring the parallel callee_suffix fallback.
3. Nothing composed enclosing prefix()->group() chains (same class as
Spring's #734). At extraction - the only place the AST still exists
- each route call inside group closures now walks its ancestors:
every enclosing closure that is the argument of ->group(...)
contributes the prefix('...') found on that group call's receiver
chain, outer-to-inner for nested groups; middleware/name/domain
chain links are skipped.
The existing handles_laravel_php test kept passing throughout because
its fixture defines an in-tree Route class, resolving by QN - which is
exactly how this class stayed unseen.
Closes#952
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Layer 2 of #996, forced by the new guard going RED on the Windows CI
leg with the writer fix alone: _environ holds ANSI-code-page bytes,
not UTF-8, so a non-ASCII USERPROFILE or CBM_CACHE_DIR arrives either
mojibake'd or with unrepresentable characters replaced by '?' - which
is invalid in Windows paths, so every downstream wide-safe file API
fails no matter how correct it is. cbm_safe_getenv now reads the value
via GetEnvironmentVariableW and converts to genuine UTF-8
(cbm_wide_to_utf8), matching the UTF-8-path convention the rest of the
codebase assumes; missing variables honor the fallback as before, and
conversion trouble falls back to the ANSI scan rather than failing.
POSIX untouched.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The 8e gate compared uname -s to the bare literal MINGW64_NT, which
never matches the real value (MINGW64_NT-10.0-...), so the POSIX
branch silently ran on Windows and looked for the extensionless shim
that #929 no longer installs. Branch by platform prefix: Windows
expects the .cmd shim and the absence of the legacy extensionless
twin; POSIX unchanged. Content checks (never blocks, delegates to
hook-augment) now run on both.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_writer_open used a raw ANSI-CP fopen for the hand-rolled SQLite
writer - the one file-creating call on the dump chain without a
UTF-8-to-wide conversion. On a non-ASCII Windows profile
(C:\Users\Kovacs Janos) extract and resolve succeeded but the dump
failed with pipeline.err phase=dump: fopen could not create graph.db
under the profile-derived cache dir while cbm_mkdir_p (already wide)
had happily created the directory around it. Routed through cbm_fopen,
matching #700 (pass readers) and #973 (canonicalization); POSIX is
byte-transparent.
New Windows guard test_non_ascii_cache_dump.py indexes an ASCII repo
into a non-ASCII CBM_CACHE_DIR and requires a query_graph readback
with Function count > 0 - proving the DB was written to and reopened
from the non-ASCII cache, not merely that no error surfaced. The
existing non-ASCII guard only varied the repo path against an ASCII
cache, which is why this class went unseen.
The USERPROFILE-sourced env encoding question (Layer 2 - _environ is
ANSI-CP, not UTF-8) is deliberately not bundled; it needs verification
on a real Windows box and will be filed separately if the guard alone
does not settle it.
Closes#996
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Extensionless bash shims under %USERPROFILE%\.claude\hooks triggered
the Windows 'How do you want to open this file?' dialog whenever an
editor (Cursor) scanned the hooks dir, and could not execute without
bash anyway. All three Claude hook scripts (discovery gate, session
reminder, subagent reminder) now install as .cmd files with cmd syntax
on Windows - @echo off bodies, caret-escaped pipes in the reminder
text, 2>/dev/null redirects - and the install removes the pre-existing
extensionless twin so upgrades stop triggering the dialog. The
settings.json command paths follow automatically since they are built
from the same script-name macros. POSIX behavior is byte-identical and
guarded (no .cmd twin).
Closes#929
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
ha_deadline_ms was defined unconditionally but only used by the POSIX
ha_arm_deadline - unused-function under -Werror broke every Windows
leg. The knob, its constants and the doc comment now live inside the
same #ifndef _WIN32 block as the deadline machinery they configure;
Windows continues to rely solely on the settings.json hook timeout.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Aider has no MCP support - it reads CONVENTIONS.md but can only run
shell commands. The installer wrote the shared MCP-tool-centric
instructions there, telling the model to call search_graph(...) etc.,
tools Aider cannot invoke; a reporter reasonably asked how that was
ever supposed to work (#1032).
Aider now gets a dedicated variant: the same discovery priority
expressed as runnable codebase-memory-mcp cli commands (usable via
/run), including first-index and list_projects bootstrapping, with the
no-MCP constraint stated explicitly.
Closes#1032
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The 300ms in-process SIGALRM budget in hook-augment self-terminated
with a silent _exit(0) on real cold starts (SQLite/mmap open under
load), so PreToolUse augmentation never appeared in real sessions
(reporter: 0/24 observed) while warm manual invocations worked - and a
fired deadline was indistinguishable from a legitimate no-match run.
Default budget is now 2000ms (the settings.json hook timeout stays the
outer backstop), overridable via CBM_HOOK_DEADLINE_MS (clamped
50..10000). When the deadline fires, the handler write()s a
pre-formatted breadcrumb (deadline, pid, the env knob to raise) to
~/.cache/codebase-memory-mcp/logs/hook-augment-timeouts.log before
exiting - fd and message are prepared at arm time so the handler stays
async-signal-safe; CBM_HOOK_TIMEOUT_LOG overrides the path for tests.
Windows keeps relying on the settings.json timeout (unchanged no-op).
Deterministic reproduction: stdin is a pipe whose writer never sends,
so the read blocks past a 60ms deadline - the timer must fire, exit 0
(fail-open), and leave the breadcrumb. RED before (no breadcrumb),
GREEN after.
Closes#858
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Agents naturally pass the repo folder name (codebase-memory-mcp), but
indexed project names derive from the full path (E:\project\graph\x
-> E-project-graph-x), so every graph tool answered 'project not found
or not indexed' while list_projects clearly showed the project.
get_project_arg now falls back to a cache-dir filename scan when no
<project>.db exists: a passed name matching exactly ONE indexed
project as a segment-aligned tail (-<name>.db) adopts that project's
full name (logged); zero or several matches keep the original name so
the existing not-found error with the candidate list fires. Exact
names never enter the scan, and internal-name drift stays #704's
fallback in resolve_store.
Closes#1025
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_pipeline_fqn_compute strips the file extension, so a C/C++ header
and its same-stem source collide on both the module QN and the File
QN. Node upserts match by QN, so the header's File node is merged into
the source's - the header has no node of its own and can never receive
an inbound IMPORTS edge; the reporter's zero-inbound query flags 194
of 214 files.
The module-QN unification is (at least partly) load-bearing for C/C++
declaration/definition call joins, and the File-node creation sites
overlap PR #995's pending fix for #994, so the fix is deferred to land
on top of that. This RED reproduction (non-gating repro tier) keeps
the bug tracked until then. Refs #964
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A function whose body braces are split across #ifdef/#else branches
(one open brace per branch, a single shared close) parses with an
ERROR region on the raw source - both branches are present at once, so
the braces are unbalanced - and the defs walk silently dropped it
while its callers stayed as unresolved names (cbm_path_within_root,
handle_process_kill).
The preprocessed second pass (simplecpp) already exists for macro-
hidden CALLS; it evaluates the conditionals, so one branch is chosen
and the expanded tree parses clean, with same-file token lines aligned
to the original. Recover definitions from it, adopting only defs that
(a) intersect a raw-tree ERROR region, (b) have their name visible on
the corresponding original source line (rejects header-inlined defs
whose physical expanded lines alias unrelated raw lines when
compile_commands include paths resolve), and (c) are absent by
qualified name from the raw pass.
Verified on the real instances: handle_process_kill (580-647) and
cbm_path_within_root (5070-5085) both index with correct line ranges.
Closes#961
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The #599 receiver-aware narrowing (87091ed) shipped without coverage
for the delegation/store bucket reported in #876: same-named calls
whose receiver is a call result (_get_store().get inside get), a
module singleton (_default.check inside check), or a loop-local cursor
(cur.execute inside execute). All three are clean on current main;
this guard pins the bucket so it cannot regress. Refs #876
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
cbm_store_find_node_by_id heap-allocates the node's string fields;
the #871 helper left them unfreed, tripping LSan on the Linux legs.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A persistently dirty or untracked worktree re-triggered a full reindex
(and its DB/artifact rewrite) on every poll cycle - git_is_dirty()
answered 'is the tree dirty' where the watcher needs 'did the tree
CHANGE'. Idle repos with one uncommitted file produced sustained disk
write amplification (reported at 1 TB/day across several repos).
The watcher now tracks a dirty-state signature: FNV-1a over the
entries of git status --porcelain -uall -z plus each listed path's
(size, mtime_ns). Same signature -> no reindex; editing a dirty file,
adding/removing one, or reverting the tree to clean each produce a new
signature and trigger exactly one reindex. Submodule status is folded
in on POSIX, mirroring the old submodule dirty probe. 0 is reserved
for a clean tree; a dirty-at-baseline tree still reindexes once (the
watcher cannot know whether that state reached the DB, e.g. after a
server restart with a stale artifact).
Baselines (HEAD and signature) are now committed only after a
SUCCESSFUL reindex: check_changes stages observations in pending_*
fields, and busy-skips (index_fn > 0, e.g. pipeline lock held) or
failed runs leave the baselines untouched so the change is retried on
the next poll instead of being recorded as seen and silently lost -
the old code stored the new HEAD at check time, dropping any commit
that arrived while the pipeline was busy.
Closes#937
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
const foo = require('./foo') emitted a Variable definition for the
binding, so calls to foo() resolved to that same-module Variable (an
import alias, not a definition) instead of following the recorded
import to the exported function - which stayed at zero inbound CALLS
and surfaced as a disconnected Variable node. ESM import bindings never
materialize as Variables; the identifier-bound require form now behaves
identically: extract_js_vars skips the Variable def exactly when the
declarator value is a require() call with a string-literal path (the
form extract_imports records with local_name = the identifier), letting
resolution fall through to the import map. Destructured requires keep
their Variable defs since the import row only records the module.
Closes#871
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>