Commit Graph
2914 Commits
Author SHA1 Message Date
Martin VogelandClaude Fable 5.1 ecfb9642e1 test(daemon): assert fast-fail by mechanism, not wall-clock, in the bootstrap ENOSPC test (#2113, O9)
The #1828 fail-fast test asserted `elapsed_ms < 5000`, a wall-clock budget
that flaked on the ASan test-diag lane under CI contention while the fail-fast
mechanism itself engaged correctly: the whole daemon_bootstrap suite ran in
11 s, so the test never reached the 30 s slow-timeout path -- it only overran
the 5 s wall-clock budget. Per O9 a gate never asserts a transient timing
window, and a rerun is not a fix.

Drop the wall-clock measurement and rely on the deterministic mechanism the
test already observes: the client surfaces the recorded ENOSPC cause verbatim
("failed to start" + errno + path), the slow "active or starting" timeout
wording is absent, and exactly one daemon is spawned. That "failed to start"
message is produced only on the fast-fail break
(cbm_daemon_bootstrap_start_failure_format), never on the 30 s deadline path,
so these assertions bind the fast-fail contract without any timing.

Verified RED-on-revert: defeating the fast-fail detection makes the client
hang to the 30 s deadline and report "active or starting", tripping
ASSERT_FALSE(stale_wording). Full daemon_bootstrap suite green (28/28) under
the ASan/UBSan build.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 23:45:24 +02:00
Martin VogelandClaude Fable 5.1 0567dc70a0 fix(daemon): guard the POSIX listen-failure recorder so Windows does not see it unused
The rich errno+path recorder ipc_listen_failed (and ipc_listen_failure_reset)
are defined before the POSIX '#ifndef _WIN32' listener block but only called
inside it, so the Windows build compiled them dead and failed under
-Werror,-Wunused-function (pr-smoke windows-latest). Wrap both in the same
'#ifndef _WIN32'; the failure struct and its cross-platform accessor
cbm_daemon_ipc_listen_failure_detail stay compiled on every platform.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 20:13:17 +02:00
Martin VogelandClaude Fable 5.1 00060cec85 fix(daemon): fail the client fast when the daemon cannot start
A daemon that died at listener publication (a full /tmp) took the
lifetime reservation, failed, and released it; the waiting client saw the
generation vanish, respawned it, and repeated that for its whole 30 s
deadline before reporting "CBM daemon is active or starting but could not
accept this client" -- the opposite of what happened, with no cause and
no path. MCP hosts then dropped the server silently (#1828).

A detached daemon has no stderr and no channel back to its launcher, so
the host now appends one start-failure record per failed start to
<cache>/logs/cbm-daemon.start-failures.log -- next to cbm-daemon.log,
through the same owner-only private-log open. Deliberately NOT the
runtime directory: that is exactly what a full /tmp cannot write, and a
new artifact there would entangle the stale-generation cleanup that
#1894 just hardened. The record carries the listener detail from the
previous commit (component, stage, errno, path) plus the runtime
directory and a wall-clock stamp, and is written while the lifetime
reservation is still held, so a client that watches the generation
vanish already finds the cause.

The client bootstrap loop reads that record (every 50 ms after its own
spawn, and unconditionally before any relaunch), accepts only records for
its endpoint not older than its spawn instant (2 s skew), and stops at
once:

    error: CBM daemon failed to start: pending_publication failed with
    ENOSPC (No space left on device) at /tmp/cbm-daemon-1000/cbm-<key>.sock.pending.tmp;
    see ~/.cache/codebase-memory-mcp/logs/cbm-daemon.log

The read is an optional bootstrap op (unit contracts with fake ops are
untouched); the production op resolves the cache log directory. Every
other wait state -- a live or stopping generation, a mute holder, a
concurrent starter -- keeps its existing semantics and diagnostics.

Tests (daemon_bootstrap):
- daemon_bootstrap_fails_fast_when_daemon_dies_at_publication: the
  production bootstrap (real cohort, probe, startup lock, handoff) with a
  seam-only spawn override that forks a REAL daemon host whose record
  writes fail with ENOSPC. RED before: 34.7 s and "active or starting";
  GREEN after: FAILED in well under 5 s naming ENOSPC and the temp path,
  one child spawned, daemon log carries errno=ENOSPC path=... .
- daemon_bootstrap_start_failure_record_round_trip: record/read/format
  contract, a stale record and another endpoint's record are ignored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 02:01:33 +02:00
Martin VogelandClaude Fable 5.1 33c4fd80ef fix(daemon): name errno and path in daemon.ipc.listen_failed
A full /tmp made every daemon start die at pending publication, and the
only durable trace was `daemon.ipc.listen_failed stage=pending_publication`
-- no syscall, no errno, no path. The reporter needed hours (and a `df` on
the wrong mount) to find the cause (#1828).

Every listener failure now records the stage, the errno the failing step
reported, and the artifact path it was operating on, and the log line
carries all three:

    daemon.ipc.listen_failed stage=pending_publication errno=ENOSPC
        path=/tmp/cbm-daemon-1000/cbm-<key>.pending.tmp

The record publication helper notes which step refused (create, write,
link, unlink, sync, close, read-back) and on which artifact, so the
listener can name it. The bind failure keeps its errno but joins the same
shape (stage=socket_bind errno=... path=...) instead of a bare numeric
bind_errno. cbm_errno_name() maps the portable errno set to its symbol.
cbm_daemon_ipc_listen_failure_detail() exposes the same triple to the
daemon host for the fail-fast record that follows.

Test: daemon_ipc_listen_failure_names_errno_and_path forces the record
write to fail with ENOSPC through a narrow inert seam (the socket binds,
the file is created, only its data write is refused -- the reporter's
exact shape) and asserts the log line names the errno and the temp path,
and that the namespace is left clean.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-08 01:35:48 +02:00
Martin Vogel 29dbf0bfa1 Merge pull request #2079 from DeusData/fix/complexity-pass-deterministic-order
fix(pipeline): make the complexity pass independent of worker id order
2026-09-08 00:53:07 +02:00
Martin Vogel 760c08f453 Merge pull request #1894 from mvanhorn/fix/1760-stale-rendezvous-recovery
fix: recover markerless stale rendezvous sockets during activation
2026-09-07 23:50:52 +02:00
Martin Vogel 5e5405c6e6 Merge pull request #1874 from LazyXuan/fix/windows-parent-watchdog
fix(main): exit Windows stdio servers when the spawning client dies (#914)
2026-09-07 23:49:58 +02:00
Martin Vogel 8ec859bb89 Merge pull request #2083 from DeusData/fix/semantic-edges-qsort-null-guard
fix(pipeline): skip the function sort when the graph has no functions
2026-09-07 23:48:53 +02:00
Martin Vogel 421d9d2299 Merge pull request #1998 from CaptainMittens/fix/bound-pattern-variable-count
fix(cypher): refuse a query naming more variables than a binding holds
2026-09-07 23:48:13 +02:00
Martin Vogel 98b94a24a3 Merge pull request #1971 from CaptainMittens/fix/coverage-range-duplicate-and-eof
fix(coverage): stop reporting a duplicate range and a line past EOF (#963)
2026-09-07 23:47:39 +02:00
Joshua RichterandClaude Opus 5 e5c17de621 fix(coverage): stop reporting a duplicate range and a line past EOF (#963)
scripts/setup-windows.ps1 has 326 lines and its parse-coverage report read
"113-113,113-113,245-327" — the same line named twice, and an end line that
does not exist. Two separate faults, both in cbm_error_regions_push.

Past-EOF end line. A tree-sitter node that ends at column 0 stopped right
after the previous line's newline, so it holds no text on the row it points
at. Adding 1 to that row named a line past the end of the file whenever the
region ran to EOF. The node here is start=(244,2) end=(326,0). Clamp the end
to the row above when the end column is 0 and the node spans more than one
row.

Duplicate range. Line 113 carries two separate ERROR nodes, at columns 25-29
and 31-32, and each pushed its own range. A line range says nothing new the
second time. Drop a range that exactly repeats the one already open. The
drop runs BEFORE the cap check, so a repeat is never miscounted as a range
the cap threw away.

Only an EXACT repeat is dropped, never a range that merely overlaps. Each
range is judged separately afterwards by cbm_region_is_recovered, which asks
whether definitions starting inside that range cover it. Two ranges holding
the same numbers always get the same verdict, so dropping the repeat changes
nothing. Two different ranges do not. Merging 3-3 into 2-3 hands the wider
range's covering definition to an error that definition does not explain, and
a real parse failure then vanishes from the report.

That is not hypothetical. An earlier version of this commit merged on overlap
and broke perl_malformed_source_remains_partial_issue1838, the test added
with the Perl grammar refresh in 17b5a432. The malformed fixture produces two
ERROR nodes, at lines 2-3 and 3-3. Merged, the 2-3 range looks fully covered
by before_error and is removed, so parse_incomplete comes back false on a file
that plainly does not parse. That test now pins this boundary.

The real file reports "113-113,245-326".

Two tests, both proved RED first with the exact expected text:
  coverage_repeated_error_line_reports_one_range_issue963  "2-2,2-2" != "2-2"
  coverage_range_never_ends_past_the_last_line_issue963    "1-5"    != "1-4"

Suites run on this change: parse_coverage 34, extraction 325, pipeline 264,
mcp 246, index_resilience 7 — all passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-06 23:57:52 -04:00
Martin Vogel 4d3d06ac2a Merge branch 'main' into fix/1760-stale-rendezvous-recovery 2026-09-07 03:55:44 +02:00
Matt Van Horn 088fedb2d2 test(cli): restore suite activation runtime after recovery test
The markerless stale-rendezvous recovery test reset the activation runtime
parent to NULL instead of restoring g_cli_suite_runtime_parent, so every
later cli test ran the activation probe against the developer's real
/tmp/cbm-daemon-<uid>. Restore the suite value per the contract at
tests/test_cli.c:61-65 and the sibling at :1310.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-09-06 18:54:28 -07:00
Martin Vogel 4ef5672c53 Merge branch 'main' into fix/windows-parent-watchdog 2026-09-07 02:54:17 +02:00
Martin VogelandYusuf S. Demirci 77a0c7d941 fix(pipeline): skip the function sort when the graph has no functions
phase1_scan_functions() in pass_semantic_edges.c sorts the collected
Function/Method node pointers so the func order is deterministic. When
the graph has no Function or Method node at all -- a struct-only or
config-only project -- node_ptrs is never allocated and the call becomes
qsort(NULL, 0, ...). The func_count <= 0 early-out lives in phase 1b,
which runs only after phase 1 has returned, so it never guards this call.

A NULL qsort base is undefined behaviour. glibc declares the parameter
nonnull, so UBSan on the Linux leg reports "null pointer passed as
argument 1, which is declared to never be null" at the call. The macOS
SDK carries no such attribute, so neither the default sanitizer build nor
the Homebrew LLVM diag lane can flag it there. The existing
pipeline_docstring_go_class test already reaches the call on Linux; the
harness never gated on recoverable UBSan reports, so it went unnoticed.

Guard the sort on func_count > 0. The new test
pipeline_semantic_edges_no_functions indexes a Go struct-only fixture
through the full pipeline and pins the fixture to what it claims
(Struct >= 1, Function = Method = 0). On the Linux leg with
UBSAN_OPTIONS=halt_on_error=1 the pipeline suite aborts at the qsort
call before the fix and completes with it.

Distilled from #1810 with co-author credit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Yusuf S. Demirci <ysf-samet@outlook.com>
2026-09-07 02:21:44 +02:00
Martin Vogel a143b0882d Merge branch 'main' into fix/bound-pattern-variable-count 2026-09-07 01:16:22 +02:00
Martin Vogel dab58c0701 Merge branch 'main' into fix/complexity-pass-deterministic-order 2026-09-07 01:16:20 +02:00
Martin Vogel aa44c28ea5 Merge pull request #1941 from CaptainMittens/fix/parse-coverage-honest-ranges
fix(coverage): narrow parse-error ranges, and stop the report hiding what it dropped (#963)
2026-09-07 01:15:37 +02:00
Martin Vogel 994990893c Merge pull request #2074 from Tokarzewski/fix/721-vb6-frm-cls-disambiguation
fix(discover): keep VB6 .frm/.cls files out of the FORM and Apex grammars
2026-09-07 01:14:12 +02:00
Martin Vogel a6aea0bc07 Merge pull request #2059 from Balneario-de-Cofrentes/fix/yaml-remove-absent-config
fix(cli): YAML removers answer OK for an absent config, so uninstall no longer refuses
2026-09-07 01:13:46 +02:00
Martin Vogel 971111c7a9 Merge pull request #2070 from DeusData/fix/daemon-session-root-separators
fix(daemon): keep one separator spelling for the session root on Windows
2026-09-07 01:12:24 +02:00
Joshua Richter 98899ba5d6 fix(cypher): count the unnamed head node against the variable cap
The head of the first pattern is bound whether the query names it or not:
execute_single falls back to the synthetic name "_n0". The capacity check
counted only names, so an unnamed head plus 16 named nodes passed it and
binding_set then dropped the 16th name without a word — the same silence
this guard exists to remove.

Count that slot, and name the synthetic variable once instead of twice.

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-06 18:54:43 -04:00
Martin VogelandZhiyu e410c86f27 fix(pipeline): make the complexity pass independent of worker id order
The complexity pass (transitive_loop_depth / recursive) walked Function and
Method nodes in temp-id order and each node's CALLS targets in adjacency
order. Under parallel extraction the temp ids come from one shared atomic
counter, so id order is worker-scheduling order and differs run to run. The
cycle guard flags whichever member of a mutual-recursion cycle the DFS enters
first, so `recursive` flipped between the members of a cycle across otherwise
identical multi-worker runs while the CALLS edge set stayed identical -- a
violation of the MT-byte-identical invariant.

Evidence: on a 66-file Python fixture of equal-sized mutual-recursion cycles
(24 pairs + 6 triangles, adjacent in the size-ordered work queue) a probe saw
3 distinct outputs in 5 runs at 4 workers. The new regression test is RED on
main 5/5 (a 4-worker run diverges from the single-threaded run on the first
cycle every time) and GREEN with this change.

Fix: order the traversal by content only -- seeds sorted by (qualified_name,
file_path, start_line), and each node's CALLS targets sorted the same way
before recursing, through one bump-stack scratch sized to the CALLS edge
count (a single upfront allocation that bails the way the existing calloc
path does). Semantics are unchanged: the DFS entry node of a cycle is the one
flagged, as before; which member that is now depends on the inputs alone,
identically for sequential and parallel indexing. SCC-based marking of every
cycle member is out of scope here.

Test: pipeline_complexity_props_independent_of_worker_order indexes the
fixture once single-threaded and six times with CBM_WORKERS=4 through the
same harness as the sequential/parallel parity test, collects every
Function's complexity props sorted by qualified_name, and asserts all
parallel runs are identical to the sequential run.

Distilled from #1925 with co-author credit.

Verification: build/c/test-runner pipeline complexity extraction (ASan/UBSan)
617 passed, 0 failed; revert-check RED 5/5 on main, GREEN with the fix;
make -f Makefile.cbm lint-ci clean; scripts/check-no-test-skips.sh OK.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: Zhiyu <zhiyuzhang001@gmail.com>
2026-09-07 00:32:02 +02:00
Martin Vogel e2d538afd0 Merge pull request #2041 from CaptainMittens/test/cli-uninstall-path-isolation
test(cli): point PATH at the fixture so uninstall isolation holds on any machine
2026-09-07 00:10:42 +02:00
Martin Vogel 2b030a5b60 Merge pull request #2056 from bmcnaboe/fix/svelte-html-astro-embedded-structure
fix(extract): extract structure from Svelte/HTML/Astro inline scripts
2026-09-07 00:10:22 +02:00
Martin Vogel c8ea1cad0d Merge branch 'main' into fix/bound-pattern-variable-count 2026-09-06 18:11:36 +02:00
TokarzewskiandClaude Opus 5 6db22aadb6 fix(discover): keep VB6 .frm/.cls files out of the FORM and Apex grammars
Visual Basic 6 shares .frm with FORM and .cls with Apex/ObjectScript, so VB6
forms were parsed by the FORM grammar (0 defs) and class modules by the Apex
grammar (junk nodes, extract timeouts). Recognise the VB6 export header
(VERSION x.yy / VERSION 1.0 CLASS / Begin VB. / Attribute VB_Name) and report
such files as unsupported (CBM_LANG_COUNT) until a Visual Basic grammar exists:
new cbm_disambiguate_frm(), VB6 check added to cbm_disambiguate_cls() ahead of
its ObjectScript scan. Defaults on any doubt are unchanged (FORM / Apex).

Short-term step agreed in #721; the grammar follows separately.

Refs #721

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Tokarzewski <bartlomiej.tokarzewski@gmail.com>
2026-09-06 17:41:47 +02:00
Martin Vogel 9846c3f171 fix(daemon): compare session roots separator-equivalently without respelling the policy root
The first cut on this branch (c1b9c451) respelled the session root and
the allowed root with forward slashes on the way into the session policy
(cbm_mcp_server_set_session_context) and at the three application.c sites
that canonicalize a root. CI's Windows shard then failed two tests:

  tests/test_daemon_application.c:2256: ASSERT(sensitive_blocked)
  tests/test_daemon_application.c:2380: ASSERT(sensitive_blocked)

Both set HOME to the cbm_canonical_path spelling of a directory - the
platform's native form, backslashes on Windows - and expect a session
rooted there to be refused for auto-index and for watch. The sensitive-root
and allowed-root containment walk (ws_is_ancestor_or_equal in
src/foundation/workspace.c) compares prefixes byte-exact, so a root that
now read C:/Users/... no longer matched a HOME of C:\Users\...: the
sensitive refusal was lost and $HOME was admitted. Respelling the policy
root was the wrong layer.

This commit keeps the policy roots in their canonical native spelling
(mcp.c and the three application.c sites are back to what main runs) and
folds the separator spelling at the one comparison the original defect
lives in: application_index_args_equal, where the running job's args are
compared with an explicit index_repository request's. The auto-index job
spells repo_path the way the session policy holds it; the handler spells
it with forward slashes. On Windows the two never matched, and the request
was refused as OPTIONS_CONFLICT instead of joining the job already running
for its root. The fold acts on the parsed copies only (yyjson_mut_obj_replace
with a cbm_normalize_path_sep'd string), so nothing the daemon stores
changes spelling and every other option stays exact.

Why this is correct on Windows without a local Windows run: the policy
spelling is untouched, so the containment path is byte-for-byte what main
runs there today; cbm_normalize_path_sep folds backslashes on every
platform, so the new fold is exercised by the new test wherever the suite
runs.

Tests: daemon_session_context_keeps_one_spelling_of_a_root asserted that
the policy respells a root with forward slashes. That was the wrong
contract: the policy's containment checks compare its spelling byte-exact
against HOME and the granted roots, so a respelled root is exactly what
stops matching them. It is replaced by
daemon_session_context_keeps_the_policy_spelling_of_a_root, which binds
the opposite: the policy stores the root as given. The comparison is exposed through
application_internal.h as cbm_daemon_application_index_args_equal_for_test
and bound by daemon_application_index_args_compare_repo_path_separator_equivalently:
a backslash and a forward-slash repo_path compare equal; a different path,
a sub-path, and a different mode still do not.

Refs #1726

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-06 16:23:45 +02:00
Martin Vogel 306c02d44e Merge branch 'main' into fix/yaml-remove-absent-config 2026-09-06 16:10:07 +02:00
Martin Vogel ddb6533cd9 Merge branch 'main' into fix/parse-coverage-honest-ranges 2026-09-06 16:10:04 +02:00
Martin Vogel 99fa463023 Merge branch 'main' into test/cli-uninstall-path-isolation 2026-09-06 15:54:57 +02:00
Martin Vogel 9cb2e9dd1c Merge branch 'main' into fix/svelte-html-astro-embedded-structure 2026-09-06 15:53:50 +02:00
Martin Vogel 745794df80 Merge pull request #2069 from DeusData/fix/test-wave-atomic-barrier-writes
test: write scheduler barrier files atomically
2026-09-06 15:52:43 +02:00
Martin Vogel 4efedf257b Merge pull request #1822 from AmirF194/fix/1815-symlink-skip-not-reported
fix(discover): report a symlink skip through the ignored-files taxonomy
2026-09-06 15:50:19 +02:00
Martin Vogel 86c031a661 Merge pull request #2066 from kavish-19/fix-grep-record-alignment
test(mcp): over-long grep records never invent matches (#2011)
2026-09-06 15:49:07 +02:00
周文瑄 a3500040e6 test(watchdog): guard the ps -W pipeline for POSIX cleanup
CI caught the macOS run exiting 1 AFTER the watchdog assertion passed
('ok: child exited after parent death' followed by exit code 1): the
kill_hard helper resolved the Windows pid with 'ps -W', an MSYS-only
flag. Under 'set -euo pipefail' the failing ps aborts the helper, the
'[[ -n ]] && kill_hard' statement inherits that status, and the EXIT
trap turns a passing test into a red job. Linux ps would fail the same
way. Guard the pipeline (empty winpid on POSIX, where the flag does not
exist) and restore the bare '|| true' tail on every cleanup kill, which
the original script had and my refactor dropped.

Signed-off-by: 周文瑄 <zhouwx1997@126.com>
2026-09-06 13:12:20 +08:00
周文瑄 92725a5eea fix(main): exit Windows stdio servers when the spawning client dies (#914)
The POSIX parent-death watchdog (#407) was excluded on Windows with the
comment "job objects handle this". They do not: the KILL_ON_JOB_CLOSE
job in subprocess.c only wraps processes CBM spawns itself. An MCP stdio
server is spawned BY the client as its child, and Windows never
propagates a parent's termination to its children, so a force-killed
client (editor crash, task manager, CI timeout) leaves the server
lingering forever blocked on stdin. The orphan pins SQLite WAL read
locks, blocking checkpoints (#1083) and feeding the delete_project
permission-denied chain from #914.

Windows has no reparenting to poll for, so instead of getppid polling
the watchdog opens a SYNCHRONIZE handle to the parent at startup and
waits on it: the kernel signals a process handle exactly once on
termination, and the held handle pins the process object, so PID reuse
cannot fool the wait. The 500 ms loop timeout exists only to re-check
g_shutdown, mirroring the POSIX poll cadence. On a signaled parent the
thread takes the same deliberate _exit(0) as POSIX: after the owning
client is gone, kernel handle reclamation is the only trustworthy
release for the daemon connection, file locks and the WAL read lock.

When no trustworthy parent signal exists at startup (snapshot failed,
reserved PID, parent already exited, or the handle cannot be opened),
the client keeps running and leans on the stdin EOF path - the same
fail-open choice as the POSIX initial_ppid <= 1 bail-out - while a
watchdog thread creation failure stays fail-closed. Workers are
unchanged: they are spawned inside CBM's own kill-on-close job, which
is the containment the old comment assumed everyone had.

The parent-watchdog shell test now runs on MSYS2 instead of skipping:
an MSYS bash wrapper launches the native server over an anonymous pipe
(an MSYS FIFO is not readable by native binaries), a writer helper
holds the write end open so the exit can only come from the watchdog,
and the kill resolves the child's Windows-physical parent (the pipeline
subshell, not the wrapper script process - the watchdog watches the
Toolhelp ParentProcessId) and TerminateProcesses exactly that one,
because MSYS kill -9 does not reliably terminate the Windows process
behind an MSYS pid.

Verified against the pre-fix binary: same test, same tree, the old
build leaves the server running after the parent dies; this build exits
within one watchdog tick.

Signed-off-by: 周文瑄 <zhouwx1997@126.com>
2026-09-06 13:12:19 +08:00
Joshua RichterandClaude Opus 5 874a972fb7 test(cli): point PATH at the fixture so uninstall isolation holds on any machine
cli_uninstall_quiesces_active_cohort_before_removing_binary_and_index moved
HOME and CBM_CACHE_DIR into a temporary directory but left PATH alone. Agent
detection reads PATH first, so a real agent binary on the developer's machine
was found even though HOME pointed at the empty fixture. Uninstall then tried
to edit that agent's config file inside the fixture, failed because the fixture
never created one, and stopped before removing the binary and the index. The
test measures exactly that removal, so it failed.

On this machine the agent is Goose, at /Users/<me>/go/bin/goose. Same tree and
same built runner, only PATH differs:

  goose on PATH        291 passed, 1 failed
  goose off PATH       292 passed, 0 failed

With this change the suite reports 292 passed, 0 failed while goose is still on
PATH, so the fix is the redirect and not a quieter environment.

This only stops the suite from depending on what the developer has installed.
The abort it exposed is a real defect and is reported in #1954: one agent config
that cannot be edited stops teardown for every other agent and for the
executable and indexes.

Eleven other tests that call cli_activation_save_env leave PATH open the same
way. They pass today, so I left them alone rather than change what they
exercise without evidence.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-06 00:43:49 -04:00
Joshua Richter 04ba2fa368 fix(cypher): refuse a query naming more variables than a binding holds
check_projection_scope models declared names in a fixed 32-entry array
and skipped the check entirely when a query declared more. Nothing
bounded how many variables a pattern could declare, so the out-of-scope
refusal added by #1922 switched itself off on a wide query: the same
undeclared name was refused at 10 declared names and quietly accepted at
35, answering a column of empty strings.

Skipping was deliberate — a wrong refusal costs the caller a working
query, which is worse than the silence. The premise was wrong. A binding
holds CYP_MAX_VARS node variables and CYP_MAX_EDGE_VARS edge variables in
plain arrays, and binding_set and binding_set_edge drop anything past
those without a word, so a query naming more cannot be answered at all.
Its extra names bind to nothing and project as blanks, which reads as
"the graph holds no such data". The choice was never refuse-or-stay-quiet;
it was refuse, or answer wrong.

So bound the input. check_pattern_var_capacity counts distinct node and
edge variables across every pattern — they share one binding, and an
OPTIONAL MATCH pattern sits in the same array — and refuses beyond what a
binding holds, naming the limit and how to get under it. Leaving the name
off a node frees its slot, because an unnamed node takes none.

Bounding the input rather than each consumer also makes three other silent
drops unreachable: binding_set past 16, binding_set_edge past 8, and the
column collection in execute_default_projection. Those would otherwise
need an error path threaded up through every caller, since both binding
setters return void.

The two `< 0` branches in check_projection_scope stay, with comments
saying they can no longer fire. They keep the guard standing if either
bound ever moves.

Fixes #1995

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-05 23:51:50 -04:00
Martin Vogeland刘冲 c1b9c451c7 fix(daemon): keep one separator spelling for the session root on Windows
On Windows cbm_canonical_path answers in the platform's native backslash
form, and the daemon stored that spelling verbatim for the client's
session_root and allowed_root (cbm_mcp_server_set_session_context) and for
the auto-index job's repo_path (application.c canonicalizes with the bare
call at three sites). Every tool handler, however, normalizes separators
after canonicalizing. One directory therefore had two names, and the exact
comparison in application_index_args_equal never matched: an explicit
index_repository request for the session root was refused as
OPTIONS_CONFLICT instead of joining the auto-index job already running for
it.

Normalize the separator spelling of both session roots in
set_session_context, and canonicalize plus normalize at the three
application.c sites through one helper, application_canonical_root().
Separators fold on every platform, so the new test
daemon_session_context_keeps_one_spelling_of_a_root binds wherever the
suite runs; it fails on main with "C:\repos\cbm" != "C:/repos/cbm".

Distilled from #1726 with co-author credit.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
Co-authored-by: 刘冲 <mail@liuchong.dev>
2026-09-06 02:48:16 +02:00
Martin Vogel 7da8bf1583 Merge branch 'main' into fix-grep-record-alignment 2026-09-06 02:28:04 +02:00
Martin Vogel f895152425 Merge branch 'main' into fix/test-wave-atomic-barrier-writes 2026-09-06 02:28:02 +02:00
Martin Vogel eb3bd818b5 Merge branch 'main' into fix/1815-symlink-skip-not-reported 2026-09-06 02:27:53 +02:00
Martin Vogel e384f88f90 Merge branch 'main' into fix/svelte-html-astro-embedded-structure 2026-09-06 02:27:51 +02:00
Martin Vogel 7b0f553cba Merge pull request #2067 from DeusData/fix/semantic-search-residuals-938
fix(mcp): harden semantic search inputs and store-error handling
2026-09-06 02:26:48 +02:00
David Gil 3007dbf842 fix(cli): YAML removers answer OK for an absent config, so uninstall no longer refuses
`uninstall` failed with rc 1 on any macOS where Hermes is installed:
Hermes is detected by its binary on PATH, not by its home, so a HOME
without `.hermes/` (every test HOME, and a fresh user's) reached the
Hermes uninstall ops with `~/.hermes/config.yaml` absent. The JSON
removers already treat a missing config as "nothing to remove"; the YAML
removers acquired the lock first, and the lock directory is created
beside the target — with the parent directory absent that failed, the
adapter recorded

  error: agent_config agent=Hermes op=pre_llm_hook_uninstall ... (target: does not exist or cannot be inspected)
  error: agent_config agent=Hermes op=mcp_uninstall ...

and uninstall refused to remove the executable and the indexes. This is
what made cli_uninstall_quiesces_active_cohort_before_removing_binary_and_index
fail on such a machine (7837 passed, 1 failed on a pristine main).

All four YAML removers short-circuit on a genuinely absent path and
create nothing: lstat + ENOENT on POSIX, and CreateFileW with
FILE_FLAG_OPEN_REPARSE_POINT on Windows, mirroring toml_read_file.
Opening the reparse point rather than following it keeps a dangling
symlink out of the short-circuit on both platforms, so it still reaches
the reader and its byte-identical rejection.

The regression test covers the four removers and runs on every platform:
it asserts "nothing was created" through cbm_path_info_utf8 rather than
lstat, so the Windows branch is exercised rather than compiled out.

Signed-off-by: David Gil <dgilperez@gmail.com>
2026-09-05 23:30:00 +02:00
Joshua RichterandClaude Opus 5 b04f54506a perf(coverage): find a line by table lookup, not by rescanning the file (#963)
cbm_refine_regions_with_pp_lines asks cbm_line_is_toplevel_macro_call about
every preprocessor-parsed line in a region. That check walked the source from
byte 0 to find the line, so the refinement cost bytes times lines — on exactly
the whole-file-ERROR C shape the refinement exists to narrow.

Build one line-offset table per file, and hand the byte span to the macro
check. The line-based form stays for the two callers that ask about a whole
region, where a single walk is cheaper than a table. A file with no table (the
allocation failed) still gets the old walk, so the answer never changes.

Measured on one C file whose file-scope brace is split by an #ifndef, padded to
5k / 10k / 20k / 40k lines. Release build, one machine, seconds:

  lines     main   branch   this commit
   5000     0.98     1.14     0.98
  10000     3.30     4.02     3.41
  20000    11.64    14.77    12.56
  40000    44.04    58.58    46.90

The branch ran 33% over main at 40k lines and now runs 6.5% over. The reported
ranges do not change: 8-8, where main reports the whole file as 5-40000.

A profile of the 40k run no longer reaches the macro check. What is left is
cbm_line_contains and cbm_span_contains_callable_def, which main carries too
and this branch does not touch. The fixture holds 40k definitions, which makes
those two cost more here than on a smaller file.

parse_coverage, extraction, pipeline, mcp and index_resilience: 964 passed,
4 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-05 16:24:25 -04:00
Joshua RichterandClaude Opus 5 f871178b8c fix(coverage): name the whole-file number for what it is, a range end (#963)
Review follow-up on this branch.

Each parse_unusable entry carries one number, and the field was called
"lines". That reads as the length of the file, and the two are not the same
number: a grammar can end an error node past the last line, which this repo
has already met — scripts/setup-windows.ps1 has 326 lines and its range ends
at 327. A report whose whole thesis is honest reporting should not name that
number after the wrong thing.

"lines" also already means something else in this same response. Every search
result carries a "lines" field holding a definition's line span. One word, two
meanings, one document.

The field is now "range_end", at both places that emit it — add_coverage_report
reading the persisted rows, and add_parse_unusable_summary reading the per-run
errors. The comment beside each one says the number can exceed the file, so the
next reader does not have to rediscover it.

Deriving the real file length instead was the other option and is not
available here: neither cbm_file_error_t nor cbm_coverage_row_t carries it,
only the path and the range string.

One test, proved RED first — "range_end is NULL" against the old field name.
It reads the end line from the persisted coverage row rather than a constant,
so it states the property and not a measurement, and it asserts the old name is
gone rather than kept beside the new one.

index_resilience, parse_coverage and mcp: 283 passed, 4 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-05 16:24:08 -04:00
Joshua RichterandClaude Opus 5 219b348263 test(coverage): pin the truncation marker, the partial ceiling and the grammar limit (#963)
Phase 5. Four test groups, each checked RED before it was kept.

- The Studio Export range join puts ONE ",+<N>" marker at the end with the
  summed drop count. A marker left mid-string makes every reader stop there
  and silently lose the ranges after it. Reaching the join through the
  pipeline needs an export file with 256+ error regions across two <Class>
  elements, so it goes through a test seam, following the pattern already in
  this repo (CBM_COVERAGE_MARKER_TEST_API).
- check_index_coverage emits every range in front of a marker, never turns
  the marker's digits into a range, and reports "truncated" from BOTH caps —
  the producer's and its own 256 limit.
- test_index_resilience now has a ceiling beside its floor: exactly one of
  the two fixture files is flagged, the clean neighbour is absent, and the
  range does not cover the whole file.
- The three _Thread_local forms are pinned as measured. Only the array form
  fails today; the plan's Phase 0 also listed the pointer form, and that is
  wrong on the grammar shipped now.

Also fixes 13 clang-format violations the earlier commits on this branch left
in cbm.c, mcp.c and pass_definitions.c. `make -f Makefile.cbm lint-format`
would have failed CI. The changes are whitespace only — the two reflowed tool
descriptions concatenate byte-identically, so no output moved.

Full suite: 7735 passed, 28 failed, 7 skipped. The 28 are the pre-existing
cli install/uninstall failures, identical at clean HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-05 16:02:17 -04:00
Joshua RichterandClaude Opus 5 a953509275 fix(coverage): stop the report hiding what it dropped, and name whole-file failures (#963)
Two silent failures in the parse-coverage report, both made visible by the
Phase 2 range refinement that came before this.

Two caps sat in series and both returned early without saying anything:

    CBM_MAX_ERROR_REGIONS = 64   internal/cbm/cbm.c
    COVERAGE_RANGE_MAX   = 128   src/mcp/mcp.c

Raising only the first would have moved the clip from 64 to 128, so both move
to 256. This was live behaviour, not a theoretical limit: after Phase 2 split
one whole-file range into many small ones, src/cli/cli.c and tests/test_cli.c
both reported exactly 64 ranges — the cap binding, dead-on, twice. Every
coverage figure measured before this change was a floor. With the cap at 256
the true numbers are cli.c 13.9% (not 9.8%) and test_cli.c 3.1%, and the
longest list in the repo is 85 ranges.

A raised cap is still a cap, so the report now says when it clipped:

- cbm_error_regions_t gained a `dropped` counter, and cbm_collect_error_regions
  walks to the end instead of stopping at the cap, so the count is exact rather
  than a lower bound. That costs little — the walk never descends into an ERROR
  subtree.
- cbm_error_ranges_str appends ",+<N>" when N ranges were thrown away.
- coverage_add_ranges reads that marker and sets "truncated": true, and also
  sets it when its own limit stops the loop. Before this the marker was
  invisible: the parser stopped at the '+' with no error and no leftover, so a
  clipped list arrived looking complete.
- objectscript_export_append_error_ranges strips markers off both operands
  before joining two Studio Export parts and adds one back at the end. A marker
  left mid-string would make every reader stop there and silently lose every
  range after it.

"Look at lines 1 to 13047" of a 13046-line file tells a reader nothing. Those
files now carry their own kind rather than being described as partially
covered.

New `parse_unusable` field in CBMFileResult, set when one range covers 80% or
more of the file. Its customers are non-C languages: the Phase 2 refinement
that narrows a whole-file range using the preprocessed parse only runs for C,
C++ and CUDA, so a Python, Java, Ruby or TypeScript file whose root node is
ERROR still reports 1-N. Verified against real files in all four.

The kind is `parse_unusable`, not `parse_failed`. index_coverage.kind already
means one of two things — indexed-but-partial, or a skip phase saying the file
was never indexed at all — and `parse_failed` reads as the second when it is
the first. The store.c schema comment, which is the only written record of this
vocabulary, now describes all three classes and says why.

Two places would have mislabelled the new kind as "skipped", which is exactly
that confusion: coverage_status fell through to its catch-all pass, and
add_coverage_report fell into its else branch. A reader who finds a file under
"skipped" believes it is absent from the graph, when it was indexed. Both now
have explicit branches. index_status gained parse_unusable_count so a CI gate
can read it without parsing anything else, get_code_snippet says "read the
source directly" instead of naming useless ranges, and the three tool
descriptions that listed two coverage kinds now list three.

Seven added. The cap test moved from 64 to 256; a new test asserts the marker
carries a real drop count and that nothing follows it; an inverse test asserts
an under-cap file carries no marker at all. For the new kind: a Python file
whose root is ERROR is unusable, a file with a local parse failure stays
partial, a clean file is neither, and — the one that matters most — the
that last one ever flips, the Phase 2 refinement has stopped working.

Full suite: 7732 passed, 28 failed, 7 skipped. The 28 are pre-existing
agent-client install/uninstall failures in the cli suite, identical in count
and identity at clean HEAD.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-05 16:01:05 -04:00