The #832 supervisor keystone added a second watcher-registration site in
autoindex_thread's supervised-success branch, but wired it as a direct
cbm_watcher_watch() guarded only by `if (srv->watcher)`. srv->watcher is set
unconditionally (cbm_mcp_server_set_watcher), so that guard does NOT honour the
auto_watch config. PR #849 had routed ALL watcher registration through
register_watcher_if_enabled() (which checks auto_watch_enabled() and logs
watcher.register.skipped reason=auto_watch_off). On a supervisor-marked prod
host with a fresh (unindexed) project and `config set auto_watch false`, the
supervised path therefore registered a background watcher against the user's
explicit opt-out — defeating #849's multi-project containment. The in-process
twin ~20 lines below already used register_watcher_if_enabled() correctly.
Fix: replace the ungated block in the supervised-success branch with
register_watcher_if_enabled(srv), matching the in-process branch. Surrounding
supervised-success logging is unchanged.
Reproduce-first: mcp_auto_watch_false_skips_supervised_autoindex_issue853 drives
the real public entry initialize -> maybe_auto_index -> autoindex_thread on a
supervisor-marked host (kill switch off) with a FRESH project and
auto_watch=false, in a fork+alarm-bounded child (like the #832/#845 gate tests).
cbm_mcp_server_free() joins the autoindex thread so the registration decision has
run before the watch count is read. It asserts spawn_count advanced (so the probe
cannot pass vacuously by skipping the supervised branch) and watch_count == 0.
RED on the unfixed code (watch_count == 1, exit 61); GREEN after the fix
(register_watcher_if_enabled skips → watcher.register.skipped reason=auto_watch_off).
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distilled from #685 (nguyentamdat) rebased onto current main, plus two
research-driven refinements and a genuine reproduce-first guard.
The parallel extract retains each file's source text so the fused cross-file
LSP resolve can re-parse it. That retention is transient but a peak-RSS driver.
On main the caps are flat (100 MiB/file, 2 GiB total) and a file over the cap is
silently unretained AND its cross-file resolution is skipped -- a graph-quality
gap. This change bounds retention AND keeps every cross-file edge.
- Source-text cap as a FLOOR, not just a ceiling: retention total defaults to
min(cbm_mem_budget()/8, 1 GiB), per-file min(32 MiB, total). Following the
rust-analyzer memory model, the RAM-derived default is clamped to a small
absolute ceiling so a huge-RAM host does not hold tens of GB it would re-read
cheaply. Both caps env-overridable via CBM_RETAIN_TOTAL_MB /
CBM_RETAIN_PER_FILE_MB (limits.c convention); ceilings bound only the
auto-derived default, never an explicit operator/caller choice. A dropped file
emits one index.retain_capped WARN per run.
- Bounded re-read fallback (the correctness guarantee): resolve_worker re-reads
an unretained file's source on demand (bounded, freed immediately) instead of
skipping resolution, wired at every cross-LSP site that consumes source. The
cap now only trades retained RAM for a bounded re-read, never a lost edge.
- cbm_parallel_extract_ex + opts struct (cbm_parallel_extract is now a wrapper
passing NULL -> env-derived defaults); malloc/calloc NULL-check hardening.
Reproduce-first: parallel_cross_file_reread_preserves_unretained_edges uses a
Java<->Kotlin pair whose cross-file lsp edges are genuinely source-dependent;
the edges are lost when the caller is unretained and the fallback is absent
(RED), present with it (GREEN), with a retained CONTROL scenario proving
non-vacuity. #685's original Python red test was a false guard (per-file py_lsp
already resolves those calls) and is replaced. Peak-bound guards
(retained_bytes <= total_cap; retain_sources=false retains nothing) in test_mem.c.
Verify: make -f Makefile.cbm cbm && make -f Makefile.cbm lint-ci; test-runner
parallel pipeline incremental py_lsp ts_lsp java_lsp kotlin_lsp c_lsp cs_lsp
go_lsp rust_lsp mem -> 2323 passed.
Co-authored-by: nguyentamdat <nguyentamdat@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The long-lived MCP server ran the full index pipeline in-process on two
background paths -- the session auto-index (autoindex_thread, mcp.c) and the
watcher re-index (watcher_index_fn, main.c). The parallel pipeline's worker
threads abandon their mimalloc pages at thread exit, and mimalloc v3 defaults
page_reclaim_on_free=0, so those pages are never reclaimed when the main thread
later frees their blocks (mi_collect cannot touch abandoned pages either). RSS
then ratchets across re-index cycles and never comes back down (#832).
The index_repository tool path already avoided this by running the index in a
supervised worker subprocess: on child exit the kernel reclaims 100% of the
child's RSS. Route the two background paths through that same supervised runner:
- supervisor_invalidate_store() is now NULL-safe (the watcher path has no MCP
server / cached store).
- New static index_run_supervised_path() builds {"repo_path": root} and reuses
the existing spawn + skip-and-continue recovery loop (no duplication). New
public cbm_mcp_index_run_supervised_path() is the srv-less entry main.c calls.
- autoindex_thread and watcher_index_fn gate on cbm_index_supervisor_should_wrap():
supervised on a marked host with the kill switch off, else fall through to the
unchanged in-process pipeline (kill switch / spawn-failure degrade). The
watcher keeps its non-blocking try_lock gate + g_shutdown check exactly; the
lock still serialises re-indexes. cbm_mem_collect() stays on the in-process
branch only.
Also fold in the in-process allocator fallback: cbm_mem_init sets
mi_option_page_reclaim_on_free=1, restoring v2 behaviour so any path that stays
in-process (kill switch, degrade, embedders) also reclaims abandoned worker
pages. The subprocess is the primary cure (child exit returns RSS every cycle);
this is the fallback.
Tests: a deterministic gating guard (test_mcp.c) proves the background paths now
spawn a worker child and index the fixture (RED when routed in-process:
spawn_count unchanged; GREEN after). The test-runner acts as a real in-process
worker when spawned as `cli --index-worker` (public APIs only, no production
seam). A non-gating prod-binary RSS reproduction (tests/repro/issue832_rss.py)
shows the supervised server flat at ~10 MB vs ~131 MB in-process.
Scope: JobObject, the #841 spurious-trigger fix, retention caps, budget tiering
and the backpressure busy-spin are follow-ups.
Refs #832, #841.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The index supervisor gate (cbm_index_supervisor_should_wrap) wrapped ANY
embedder of cbm_mcp_handle_tool: it resolved the CURRENT executable and
re-invoked it as '<self> cli --index-worker index_repository ...'. In a
test binary 'self' is the test runner, which ignores those args and
re-runs its suites instead, producing recursive spawn chains (observed
11-minute hangs, and multiplied kernel VM-map/memory load during the
2026-07-04 host panics). The gate is now opt-in: only a process that
called cbm_index_supervisor_mark_host() — the real binary's main(),
before any subcommand dispatch — may spawn workers. Embedders index
in-process by default; the test mains additionally pin
CBM_INDEX_SUPERVISOR=0 as belt-and-suspenders. Guarded by
index_supervisor_gate_requires_marked_host_issue845 (fork+alarm harness,
new spawn-count test hook; red on the old gate, green without the env
kill switch on the new one).
A supervised worker whose supervisor died also kept indexing as an
orphan: the parent-death watchdog only ran in MCP-server mode. run_cli
now starts the same watchdog (detached; on ppid change it only writes
to stderr and _exit(0)s, so it is safe outside server mode) when it
detects --index-worker. Guarded by tests/test_worker_watchdog.sh
(scripts/test.sh Step 5b): a CBM_TEST_HANG_ON busy-spin pins the worker
mid-index, the wrapper parent is killed, and the worker must self-exit
within 15s — red without the wiring, green with it.
Fixes#845.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Add the auto_watch config key (default: true) and route both session
watcher-registration sites in mcp.c (autoindex completion and the
already-indexed connect path) through a register_watcher_if_enabled()
helper. When disabled, the skip is logged as a short structured line
(watcher.register.skipped reason=auto_watch_off).
Default is TRUE (opt-out): existing users keep background auto-sync
unchanged; the knob exists for multi-project users who want each
session contained to explicit indexing.
Distilled from the auto_watch half of PR #625. The PR's bundled
project-index dedup commit is excluded here — it belongs to #754's
own review. Unlike the original PR, no watch registration is added
to the index_repository handler (none exists on main; adding one
would be new behavior beyond the gate), and the default is flipped
from off to on to preserve current behavior.
Tests: default-unset registers the watcher on connect (guards the
no-behavior-change promise); auto_watch=false registers nothing —
verified RED against the ungated production code, GREEN with the gate.
Co-authored-by: Andy11-cpu <canada11@duck.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Coincident (or sub-ULP-separated) bodies made octree_insert subdivide
forever in the graph-UI 3D layout, calloc-ing one cell per level until
the process crashed (stack overflow) or froze the machine allocating
(the 34GB-swap reports). Guard distilled from #821: octree_insert now
carries a depth and stops at depth 26 or half_size < 1e-4f, folding the
body into the cell as a mass-weighted centroid aggregate
(body_index = -1). octree_repulse already clamps d to 0.01 before the
dx/d division, so folded coincident bodies get exactly zero force and
no NaN.
The default-cap raise bundled in #821 (DEFAULT_MAX_NODES and
GRAPH_RENDER_NODE_LIMIT 2000 -> 60000) is a UX policy change deferred
to its own discussion per review; HARD_MAX_NODES is raised to 200000 so
opt-in CBM_UI_MAX_RENDER_NODES users get the new ceiling.
Guard test layout_coincident_nodes_bounded drives the public layout API
with same-file nodes whose distinct qualified names share one 32-bit
FNV-1a hash (bit-identical coincident positions on every platform), in
a fork+alarm child so the unfixed runaway cannot take down the runner.
Refs #498, #726, #402
Co-authored-by: Ljove02 <135197334+Ljove02@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Windows cbm_popen wrapped _popen, whose CreateProcess(bInheritHandles=TRUE)
leaks every inheritable handle - including the UI listening socket and
Winsock/AFD helpers - into git children. Git-for-Windows' MSYS2 runtime
walks inherited handles with NtQueryObject at startup and deadlocks on
socket/AFD handles, wedging the single-threaded UI server (#798).
Read-mode cbm_popen now spawns via CreateProcessW with STARTUPINFOEXW and
PROC_THREAD_ATTRIBUTE_HANDLE_LIST restricted to the stdout pipe write end
and a NUL handle for stdin/stderr; cbm_pclose reaps the child via a handle
table + WaitForSingleObject + GetExitCodeProcess.
Distilled from #799 with three corrections:
- no silent fallback to _popen: a failed isolated spawn logs a structured
warning (compat.popen_isolated_failed stage/gle/errno) and returns NULL
(all call sites handle NULL); the composed command line is heap-allocated
instead of a fixed 2048-byte buffer whose overflow forced the fallback
- cmd.exe resolved explicitly from %COMSPEC% (GetSystemDirectoryW\cmd.exe
fallback) and passed as lpApplicationName - no search-path lookup
- GetExitCodeProcess failure reports -1 instead of success; a failed NUL
open fails the spawn instead of feeding INVALID_HANDLE_VALUE into
STARTF_USESTDHANDLES
Adds two windows-only regression tests (test_security.c) driving the
cbm_popen/cbm_pclose round-trip against real git and asserting the isolated
path was taken via the cbm_popen_last_was_isolated() hook.
Closes#798
Co-authored-by: Flipper <jacobphilipp@ymail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Since #667 every file in a Java/Go package shares a directory-based module
QN that collides with the pipeline's Folder/Project node. Class-level
references resolved their enclosing scope to that shared node, whose
file_path was clobbered by each file's always-emitted Module def — last
writer wins (sequential upsert) or last-merged worker wins (parallel merge)
— so USAGE-edge sources conflated across same-package files and varied run
to run.
Three coordinated changes:
- cbm_gbuf_upsert_node: a Module def colliding with a Project/Folder node
no longer updates ANY field (the #667 guard only protected the label).
- merge_update_existing: same guard on the parallel worker-gbuf merge path;
the ID remap stays outside the guard so worker edges still redirect onto
the canonical node.
- Source-node finders (pass_usages, pass_parallel, pass_calls) treat a
lookup landing on a Folder/Project container as a miss and fall back to
the per-file File node (new cbm_pipeline_node_is_dir_container helper).
Adds tests/repro/repro_issue787.c: exact-source-set, 5-run stability, and
>50-file parallel-path guards (red on the unfixed code in both sequential
and parallel flavors).
Distilled from #828: the author's commits lacked DCO sign-off trailers and
could not be cherry-picked; content applied verbatim plus one clang-format
fix in graph_buffer.c.
Closes#787.
Co-authored-by: Tommy Corbett <spde89@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
DELETE /api/project removed the project's .db file but never told the
watcher; the zombie watch kept polling the repo and the next change
silently resurrected the deleted DB (#803). Wire the watcher into the
UI HTTP server (non-owned ref; created before the server, freed after
it) and unwatch after a successful unlink and on ENOENT (clears an
already-orphaned watch, still 404). Invalid name stays a plain 404
with no unwatch; a non-ENOENT unlink failure stays 500 and keeps the
watch. The cbm_file_exists pre-check is replaced by unlink-errno
handling, removing the TOCTOU window.
Distilled from #830, rebased over the defer_state_free refactor
(#817): the OOM hardening of the deferred-free path moves into
defer_state_free itself so BOTH unwatch and stale-root prune get it.
On pending-free realloc failure the helper no longer frees the state
immediately (a potential use-after-free against an in-flight poll_once
snapshot) — it logs watcher.unwatch.oom and returns failure, and the
callers keep the entry registered; the prune path retries on the next
poll cycle.
Carries six httpd regression tests from #830 driving the live UI
server with a real watcher wired in.
Closes#803.
Co-authored-by: Dustin Persek <dustin.persek@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A member call `x.foo()` reaches the registry's weak textual cascade only when
the TS-LSP could not resolve the receiver type — type-resolved calls win via
lsp_* strategies before the registry runs. Binding such a call to a project
symbol by a weak short-name strategy fabricates a CALLS edge (`re.test()` ->
SalesforceRestClient.test, `date.toISOString()` -> any project toISOString).
On a 6k-file monorepo this produced ~21.6k false edges (~12.6% of CALLS).
Add cbm_tsjs_suppress_weak_method_match: for a TS/JS/TSX member call flagged
is_method, the weak strategies (suffix_match, unique_name, field_type_hint,
fuzzy) are noise. It uses an EXPLICIT drop-list, not keep-list + default-drop,
because the resolver runs lsp_* through the same code path — a default-drop
would silently kill lsp_ts_method. Gated on the file language so no other
language is affected.
The suppression is applied at the PLAIN-CALLS emission point, not before it:
emit_classified_edge (sequential) and emit_service_edge (parallel) take a
suppress_plain_calls flag and skip ONLY the final plain CALLS fall-through
(and emit_http_async_edge's no-URL fall-through). Every service classification
that runs first — the #523 callee-name HTTP/ASYNC bypass, route registration,
gRPC/GraphQL/tRPC/CONFIG, and emit_service_edge's unconditional
detect_url_in_args — is therefore byte-identical to main by construction. This
replaces an earlier attempt that dropped the call at a language guard and tried
to re-derive "is this a service edge?" with a predicate: that predicate drifted
from the emit path's classification and lost ~399 HTTP_CALLS (verb-suffix
clients like api.patch / page.goto / request(app).get, whose HTTP signal is not
a library name in the callee) plus ~63 CONFIGURES on the monorepo. Suppressing
at the emit point cannot drift.
Tests: unit tests pin the keep/drop split (including lsp_* -> keep). A
reproduce-first sequential E2E fixture asserts the regex-receiver false edge is
gone while a typed-receiver call (lsp_ts_method) and a bare local call survive.
A >=50-file parallel fixture (CBM_WORKERS forces the parallel resolver) asserts
that axios.get and dev.load('/api/data') keep their HTTP_CALLS (the latter via
detect_url_in_args — the class the old predicate lost), that api.patch,
request(app).get and router.get keep their Route registrations, and that the
regex and dev.load weak plain-CALLS edges stay suppressed. The ts/S6
inherited-method probe, which used to pass via a fragile unique_name fallback,
now asserts exactly zero CALLS with a store-opened guard (the INHERITS edge IS
extracted; the gap is ts_lsp_cross's cross-file resolution, which the guard
keeps once wired).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Hitoshi Okazaki <okazaki.hitoshi@tecnos.co.jp>
- format the include trailing comments and wrap the over-long reap log line
(clang-format-20 violations flagged by lint / lint)
- allowlist subprocess.c's fork with justification (security-static): the
supervisor primitive forks and execs immediately; no code runs in the
forked image
- move bin into the fork-child block alongside argv (cppcheck variableScope
consistency)
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The hardcoded-URL security audit flags http:// literals in src/; the comment
only illustrates the shape of stored url_path values.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Apply path_filter while writing the scoped filelist instead of only after
grep, so large indexed projects do not scan files whose hits the post-grep
filter would discard anyway. The prefilter runs the SAME compiled regex via
the same cbm_regexec call against the same root-relative path (Windows
separators normalized first) as the post-grep filter in collect_grep_matches,
which is kept as belt-and-suspenders — results-preserving by construction.
Guard the edge the prefilter introduces: a path_filter matching zero indexed
files now short-circuits with the normal empty result instead of invoking
xargs on an empty filelist (GNU execs grep once with no operands, BSD skips).
Tests: positive guard (filter matching the hit's file still returns it,
out-of-filter file excluded) and zero-match guard (clean empty response,
grep subprocess skipped). Both green pre-change too — perf-only change.
Distilled from #756 rebased onto the post-#751 filelist writer.
Co-authored-by: yutianyu111602-glitch <yutianyu111602@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Mixed Gradle/Maven source roots (src/main/java + src/main/kotlin) share
JVM packages that path-derived module QNs cannot join: cross-file LSP
emitted package-shaped names while graph nodes stayed path-derived, and
same-package defs across roots were filtered out, so Java<->Kotlin calls
fell back to weak or wrong textual edges.
The declared package is JVM ground truth. Normalize JVM defs to their
declared package (inferring it from conventional source roots only when
no package was extracted), index defs by declared namespace alongside
the path module so same-package defs survive per-file filtering, restrict
JVM callers to JVM defs, register cross-def return-type signatures for
Kotlin receiver typing, and consume qualified member-call overrides by
leaf name in the parallel lsp_idx fast path.
Distilled from #684 with one design change: the new unique-Class.method
tail-match fallbacks in lsp_resolve.h are gated to JVM callers
(cbm_pipeline_lsp_allow_tail_match, Java/Kotlin only). Tail-matching by
unique leaf is safe where class-per-file package semantics hold; in
other languages a single wrong-module coincidence would fabricate a
CALLS edge. The leaf-key lsp_idx lookup stays global: it aligns the
fast path with the build-side key that already used leaf names.
Tests: carried mixed Java/Kotlin regression (red on main, green here)
plus a gate guard asserting the tail fallbacks stay off for non-JVM
callers and on for JVM.
Co-authored-by: nguyentamdat <nguyentamdat@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distilled from #738: the watcher now prunes a watched project whose root
directory has genuinely disappeared - deleting the cached DB (+wal/shm,
validated name, cache-dir-only paths) and removing the watch entry - so
vanished worktrees stop being watched forever (#286).
Hardened beyond the original PR to remove a data-loss hazard (the cached
DB can hold user-authored data such as the ADR, unrecoverable once
deleted):
- Only ENOENT/ENOTDIR stat failures count as missing. Any other failure
(EACCES, EIO, transient mounts, macOS TCC revocation) resets the
streak and logs watcher.root_stat_error with the errno; Windows
(mingw/UCRT) maps not-found to ENOENT so the check holds there.
- Pruning requires BOTH >=3 consecutive missing polls AND a sustained-
absence grace window since the streak's first miss (default 600s,
override via CBM_WATCHER_PRUNE_GRACE_S), tracked with monotonic
cbm_now_ms.
- Root reappearance resets streak + timestamp (watcher.root_restored).
- The pruned entry is released via the deferred-free list poll_once
drains (one freeing model shared with cbm_watcher_unwatch).
Limitation: only currently-watched projects are pruned; stale DBs left
by older sessions are out of scope.
Co-authored-by: pcristin <xxxokzxxx@protonmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distills PR #770 (graph-buffer dedup key) and completes the edge-uniqueness
contract it left partial: the contract lives in THREE places, and changing
only the buffer ships databases that violate their own UNIQUE constraint
(PRAGMA integrity_check: non-unique entry in sqlite_autoindex_edges_1).
A single 'import { A, B } from ./lib' produced ONE IMPORTS edge: the graph
buffer dedups edges on (source_id, target_id, type) and merge-replaces
properties on collision, so the second symbol silently overwrote the first.
pass_calls.c, pass_usages.c, pass_semantic.c and pass_lsp_cross.c parse one
local_name per IMPORTS edge for cross-file resolution, so the dropped
symbol's calls failed to resolve too — not just "who imports X" queries.
Changes, kept in sync across all three sites:
- graph_buffer.c (from #770): make_edge_key() folds local_name into the
dedup key for IMPORTS edges only; all three key call-sites updated.
Hardened beyond #770: EDGE_KEY_BUF bumped to 256 and oversized
local_names are re-keyed with an FNV-1a hash of the full name instead
of being silently truncated (two long names sharing a prefix must not
collide back into one edge).
- store.c: edges gains local_name_gen, a VIRTUAL generated column
(IMPORTS -> coalesce(json_extract(properties,'$.local_name'),''),
else '' — NOT NULL because NULLs never conflict in a UNIQUE index);
uniqueness widened to UNIQUE(source_id, target_id, type,
local_name_gen) and the insert upsert's conflict target matches.
init_schema probes pre-#768 DBs (no local_name_gen) and fails the
open: SQLite cannot ALTER a table constraint in place, and an
unopenable DB already takes the existing repair path — full index
deletes + rebuilds, artifact import refuses and falls back to a
reindex. Read-only query opens skip init_schema and keep working.
- sqlite_writer.c/.h: dump DDL matches the widened schema; the hand-built
sqlite_autoindex_edges_1 comparator and entry builder include the
local_name column; CBMDumpEdge carries local_name extracted via real
JSON parsing (yyjson) so index entries match json_extract's unescaped
values exactly, per the idx_edges_url_path precedent.
- artifact.h: CBM_ARTIFACT_SCHEMA_VERSION 1 -> 2 so old binaries refuse
artifacts carrying the widened schema (their 3-column conflict target
can no longer prepare against it).
Tests (reproduce-first, all red on the unfixed code):
- gbuf tier: multi-symbol import -> 2 IMPORTS edges; long-local_name
truncation guard.
- store tier: distinct local_name coexists as 2 rows, same local_name
still upserts, non-IMPORTS dedup unchanged.
- writer tier: dumped DB passes integrity_check with 2 sibling imports
and exposes matching local_name_gen values.
- end-to-end: TS fixture through the real pipeline -> 2 queryable
IMPORTS edges AND integrity_check ok; with only the buffer half of
the fix this test still fails on integrity_check, proving the schema
half is required.
Closes#768.
Co-authored-by: Alexandros Pappas <11921291+apappas1129@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distilled from PR #702 (fixes#697):
- install.ps1: replace the try/catch that swallowed `codebase-memory-mcp
install` failures with a $LASTEXITCODE check that reports the exit
code and fails the installer, instead of silently leaving no coding
agent configured.
- README: document Unblock-File for the Mark-of-the-Web restriction
that blocks the downloaded install.ps1, plus the execution-policy
escape hatch.
- cbm_exec_no_shell (Windows): switch from _spawnvp, whose CRT does not
quote arguments containing spaces (the taskkill filter "IMAGENAME eq
codebase-memory-mcp.exe" arrived as three bare tokens), to
CreateProcessW over a two-pass MSVC-convention quoted command line
(cbm_build_cmdline, exposed for tests via compat_fs_internal.h).
- UTF-8-correct widening: the quoted command line is assembled in UTF-8
bytes and converted once via cbm_utf8_to_wide, so non-ASCII arguments
(e.g. a non-ASCII %USERPROFILE%) survive intact instead of being
byte-widened as Latin-1 mojibake.
- tests: regression guard for the #697 taskkill filter, MSVC quoting
edge cases (empty arg, embedded quote, trailing backslashes), UTF-8
widening guards (2-byte sequence with explicit code points; mixed
2-/3-byte round-trip via cbm_utf8_to_wide), and live CreateProcessW
spawn tests. All Windows-only, exercised by the Windows CI leg.
Co-authored-by: ShauryaaSharma <shauryasofficial27@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
On NixOS (and other non-FHS systems) /bin/bash does not exist, so
scripts with an absolute shebang fail to run. Switch the remaining
holdouts to /usr/bin/env bash: eleven scripts/*.sh,
test-infrastructure/run.sh, and the three Claude Code hook scripts
emitted by src/cli/cli.c (gate, session reminder, subagent reminder).
Distilled from PR #674, with parser-test coverage preserved: the
infra_parse_shell* fixtures in tests/test_pipeline.c intentionally keep
#!/bin/bash so absolute-path shebang extraction stays covered, and
tests/repro fixtures are untouched.
Also replace the GitHub-PAT-shaped fixture string flagged in the #674
thread with an obviously fake placeholder (ghp_FAKE...) that still
matches the ghp_ + 36-alnum secret detector.
Co-authored-by: Sandro Jäckel <sandro.jaeckel@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The truncation check `header_end + (int)comp_size > data_len` was
bypassable for comp_size >= 2^31: the int cast turns the size negative,
the sum drops below data_len, and the entry is accepted -- authorizing
inflate to read up to ~4GB past the downloaded archive buffer during
self-update ZIP extraction. Replace it with
`header_end > data_len || comp_size > (uint32_t)(data_len - header_end)`,
which is overflow-safe and strictly tighter.
Regression guard cli_extract_binary_from_zip_rejects_truncated_deflate_size_over_int_max
builds a 52-byte archive whose entry claims comp_size=0xFFFF0000 with a
self-terminating DEFLATE stream: the old check admits it and extraction
returns non-NULL (verified red pre-fix); the new check rejects it.
Secondary cppcheck cleanups from the same review:
- cli.c: extract zip_read_u16le/zip_read_u32le little-endian helpers
(behavior-identical) and widen zip_extract_entry sizes to size_t with
an explicit UINT_MAX guard before the zlib uInt narrowing.
- cypher.c: drop unreachable IS NULL / IS NOT NULL null guards in
eval_condition (resolve_condition_value result is checked earlier).
Distilled from PR #784.
Co-authored-by: SS-42 <noreply@incogni.to>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
The auxiliary filesystem walks (pkgmap manifest scan, tsconfig/jsconfig
path-alias discovery, env-URL scan) ignored the directory subtrees that
discovery had already excluded (gitignore + skip dirs). On a huge
monorepo with a gitignored vendored tree this kept the pkgmap walk busy
for ~15 minutes re-traversing directories the index never uses (#792).
Distills PR #793 by Patrick (@Sowiedu): a shared root-anchored,
'/'-boundary exclusion predicate (cbm_pipeline_relpath_is_excluded) in
pipeline_internal.h, exclusion parameters threaded through
cbm_pkgmap_scan_repo / cbm_pkgmap_build_from_repo, find_alias_files /
cbm_load_path_aliases_excluded and cbm_scan_project_env_urls_excluded,
NULL-exclusion wrappers preserving the old signatures, and the borrowed
excluded_dirs/excluded_count pair on cbm_pipeline_ctx_t.
Beyond #793:
- Thread the excluded list into pipeline_incremental.c's extract ctx
and its path-alias load as well, so incremental runs stop walking
excluded trees too (#804).
- Add regression tests: boundary semantics of the exclusion predicate,
plus walk-level exclusion tests for pkgmap, path-alias, and envscan
(each with an unexcluded control run so they cannot pass vacuously).
All are red against the unfixed sources.
- Correct the envscan narrative: cbm_scan_project_env_urls has no
production callers today; the exclusion plumbing is kept for
consistency and exercised by tests.
Closes#792. Closes#804.
Co-authored-by: Patrick <11910229+Sowiedu@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distills the pass_cross_repo.c half of #536; the pass_calls.c half landed
separately as beaa776 (broader: covers the parallel path too).
match_http_routes could not match a stored HTTP_CALLS url_path that was a
full URL (scheme://host:port/path is stored raw from the call's first
string argument), never matched a concrete client path against a templated
route QN, and only ran consumer->provider, so a provider-side run found
nothing - and worse, destroyed previously recorded links: delete_cross_edges
wiped the provider's reverse edges and nothing recreated them.
- cr_url_path(): strip scheme://host[:port] before route-QN construction;
a bare base URL maps to the root route path.
- cr_path_matches_template() + find_route_handler_fuzzy(): segment-wise
fallback so /v2/orders/123 matches /v2/orders/{} (method-gated, only for
edges the exact lookup missed; O(calls x routes) per project pair,
bounded by CR_MAX_EDGES).
- Reverse-direction match_http_routes run so provider-side invocations
find the consumer's HTTP_CALLS and re-create the wiped reverse edges.
Row dedup needs no insert guard: the edges table upserts on its
(source_id, target_id, type) UNIQUE key.
Four reproduce-first tests in tests/repro/repro_issue523.c (scheme-strip,
template fuzzy match, provider-side run, bidirectional convergence), all
red on the previous code, green with the fix.
Refs #523
Co-authored-by: RithvikReddy0-0 <rithvikreddymukkara@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
A .cbmignore negation pattern (e.g. "!obj/") whose rule is the last
match for a directory now un-skips built-in skip-list dirs, so projects
can index sources that live under names like obj/, target/, or docs/
(issue #500). Distills the design and test intents of PR #543 onto the
current 3-state cbm_gitignore_match_result API: instead of a second
inverted-polarity matcher, should_skip_directory consults the existing
last-match-wins result (<0 = negated) inside the built-in branch and
falls through so .gitignore/global/local rules still apply.
Non-negatable safety core: .git, node_modules, and the worktree-internal
dirs (.worktrees, .claude-worktrees) can never be un-skipped — a
repo-committed .cbmignore must not defeat OOM/safety skips (#489, #802).
Un-skipped dirs are walked normally and therefore no longer appear in
the excluded-subtree list (#411 coherence).
Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Distills the design from PR #560 onto current main:
- "overview" now expands to every aspect except file_tree (the per-file
listing that alone can push the payload past the MCP output cap).
Previously the token matched nothing and silently degraded to just
{total_nodes,total_edges}. Both aspect gates (want_aspect in store.c,
aspect_wanted in mcp.c) share one predicate,
cbm_store_arch_aspect_in_overview, declared in store.h so the two
sites cannot drift.
- The aspects items schema gains an enum of the 13 valid tokens
(advisory, client-side), mirroring VALID_ASPECTS.
- Server-side validation (authoritative): unknown aspect tokens now
return an isError result listing the valid values instead of a
silent near-empty response. Bounded snprintf; respects the existing
16-token aspects cap.
Reproduce-first tests: overview-subset and unknown-aspect tests fail on
the unfixed code; a parsed tools/list guard pins the schema enum.
Co-authored-by: Kirchlive <roberto.livebox@gmail.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>