Commit Graph
109 Commits
Author SHA1 Message Date
LynxBay eef798bff7 fix(store): distinguish transient SQLITE_BUSY from corruption before quarantine (#1206, #1037)
The quarantine path in resolve_store_internal() used the shallow
cbm_store_check_integrity() to decide whether to rename a project DB
to .corrupt and rebuild it. That check has two flaws that together
cause healthy databases to be destroyed under concurrent access:

1. It treats ANY sqlite3_prepare_v2 failure as corruption. Under
   concurrent MCP instances (#1206), a writer lock held by another
   instance makes the schema lookup return SQLITE_BUSY/LOCKED — a
   transient condition, not damage. The losing instance quarantines
   a perfectly healthy DB and rebuilds a sparse replacement.

2. It never runs PRAGMA quick_check, so it cannot catch real
   page-level corruption either. A DB with a torn node/edge btree
   but an intact projects table passes the check and keeps being
   served (#1037) — the inverse failure.

This change introduces cbm_store_check_integrity_verdict(), which
returns a tri-state (OK / CORRUPT / TRANSIENT) and runs the shallow
check plus PRAGMA quick_check, classifying BUSY/LOCKED/IO-lock codes
as TRANSIENT. The quarantine decision in mcp.c now quarantines ONLY
on a confirmed CORRUPT verdict; TRANSIENT releases the mutation lease
and retries on the next resolve, never touching the DB file.

The existing cbm_store_check_integrity() and _deep() are unchanged,
so all other call sites (hot opens, artifact import, maintenance)
keep their current behavior.

Verified: builds clean with -Wall -Wextra -Werror; cli list_projects
and index_status run correctly against the patched binary.

Signed-off-by: LynxBay <187708458+LynxBay@users.noreply.github.com>
2026-08-11 21:23:14 +08:00
Martin Vogel f36ab45f8b fix(store): normalize the Windows file-API path on every SQLite open
Root cause of the Cyrillic guard failure, found via the finalize diagnostics:
seal_existing rc=-1. The primary store open normalizes the path through
cbm_path_for_file_api (UTF-8 -> wide -> UTF-8, the form SQLite's Windows VFS
needs), but four maintenance opens passed the raw UTF-8 path:

  cbm_store_seal_existing_path_for_replace  (the one this path hits)
  cbm_store_prepare_path_for_replace
  cbm_store_backup_path                     (both source and staging)
  cbm_store_dump_to_file

So a generation was WRITTEN through the normalized path and then could not be
RE-OPENED through the raw one. On the second index into a cache the seal step
failed, finalize returned PERSIST_FAILED, and the user was told "Check repo_path
exists and contains source files" about a repo that had just indexed cleanly
(nodes=12 edges=21 in the same worker log).

On POSIX cbm_path_for_file_api is a no-op memcpy, which is why the full 3-OS
ladder and every non-Windows test passed -- the defect is Windows-only and only
manifests on a path the raw UTF-8 -> SQLite open mishandles. It surfaced on the
Cyrillic case first because the guard indexes the same repo TWICE (once for the
count, once on retry), making it the first case to re-open an existing
generation; the earlier ASCII/latin1 cases index into fresh caches.

All four now use cbm_path_for_file_api exactly as the primary open does. Also
upgrades dump_to_file from sqlite3_open to sqlite3_open_v2 with explicit flags.
The finalize.prepare_failed / rename_failed diagnostics from the previous commit
stay: a silent PERSIST_FAILED that blamed the repo cost a day of remote bisection
and must never be silent again.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-10 04:15:00 +02:00
Martin Vogel 4e99ddfb4a chore(store): strip the coverage-timing diagnostic scaffolding
The cov_timing_mark instrumentation existed to locate one block (it
found the shadow rebuild); the previous commit shipped with it still in
place, including a call after a return that cppcheck rightly flagged as
unreachable — that commit went out with the lint gate RED because the
push was chained without depending on the gate result. The scaffolding
is gone; the durable publish.timing brackets in pipeline.c remain.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 22:18:33 +02:00
Martin Vogel 87eafefe73 perf(pipeline): pin dependency-query join order; gate the coverage shadow rebuild
Two measured pathologies out of the delta path, both shared-code fixes
that help every route:

Join order. The inbound-edge snapshot and the dependent-files lookup
let the planner start from EDGES, walking every project edge through
the url_path index prefix — 14.6s for a one-file closure against the
kernel's 16.5M edges. CROSS JOIN pins nodes-first (idx_nodes_file →
idx_edges_target → primary key): measured 4ms for the same query, 23ms
for the full snapshot+purge step.

Coverage shadow graph. cbm_store_coverage_replace_ex rebuilt the
miss-graph shadow view wholesale inside every publish — wipe plus tens
of thousands of node/edge upserts probing the full-size nodes index,
23.4s at kernel scale — even when the failure-row set it derives from
was byte-identical. The rebuild is now gated on a sha256 fingerprint of
the failure rows persisted in store_meta: unchanged set, provable
no-op, skipped. Measured 84ms steady-state; the rebuild still fires
whenever the set actually changes, and the shadow output is untouched.

The benchmark probe also stops appending a trailing comment that
happened to break bootp.c's parse — a probe that mutates the failure
set on every run measures the shadow rebuild, not the repair. It now
edits inside the license-header comment.

Kernel one-file warm, steady state: 111.6s -> 85.3s wall (71.1s
worker). Remaining measured blocks: base-def rehydration ~35s, preseed
18.8s, repair 10.8s — the parallelization targets. 554 store/pipeline/
incremental tests green.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 22:08:17 +02:00
Martin Vogel f07c2e2f34 feat(pipeline): closure-repair incremental route
The routing follow-through on the +92% warm-reindex finding: a semantic
manifest delta no longer unconditionally rebuilds the world. The planner
recomputes exactly the changed files plus the recorded consumers of any
changed SURFACE, and the executor resolves them against cross registries
rehydrated from the persisted per-file surfaces -- the same registration
code a full build feeds from fresh parses, which is what makes the output
converge instead of drift.

Routing, in order: exact manifest match stays a no-op; a delta first
offers itself to the closure planner; every uncertain case declines to
the full rebuild that was yesterday's only behaviour. Declines: virtual/
config manifest entries, new files, ADDED definition names (yesterday's
graph cannot know who would resolve to a name that did not exist -- the
write-the-caller-first flow and shadowing both live here), missing or
undecodable surface rows, dependents outside discovery, and a budget of
30% of files with an 8-file floor (a percentage alone starves small
repos: 1 changed file in 3 is 33%).

Two structural facts carry the correctness argument. Per-file extraction
is a pure function of file content, so an unchanged dependent can never
be surface-changed in turn -- the closure is depth-1 by construction, no
fixpoint. And a body edit reserializes to the identical surface bytes,
so its closure is the file itself. The dependent set comes from one
indexed query over the previous generation's edges (structural
Folder/Project containment excluded -- a container is not a consumer).

The executor is the existing partial machinery, parameterized: re-parse
list = closure; inbound-edge snapshot/re-link keeps only sources OUTSIDE
the closure (sound because every referencer of a surface-changed file is
inside it by construction); cbm_parallel_resolve now receives real cross
registries built from stored-surface defs plus this run's fresh parses;
publication merges surviving surface rows with the re-parsed files'
fresh ones inside the same generation. The legacy test-only route
publishes no surface rows at all -- a stale row that satisfies a future
closure plan with yesterday's surface would be worse than the full
rebuild an empty table forces.

Tests pin route AND convergence together (route equality matters because
a full rebuild satisfies any convergence assertion vacuously): body edit
routes CLOSURE_REPAIR with node/edge/CALL_REFERENCE counts equal to a
fresh full index; REMOVING a definition keeps the closure route and
drops the dependent's stale CALL_REFERENCE -- the assertion the legacy
QN-keyed re-link could never pass; added-name, new-file and budget cases
decline; the existing Go content-change test now routes CLOSURE_REPAIR
with its convergence assertions unchanged, making it the Go-language
proof of the same machinery.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 18:41:49 +02:00
Martin Vogel 8d3685bfca feat(store): persist per-file LSP surfaces for closure-repair incremental
First piece of the closure-repair incremental route (the follow-through
on the +92% warm-reindex finding): a per-file lsp_surface row holding
the file's serialized cross-file definition set -- exactly what
pass_lsp_cross registration consumes -- plus the metadata the routing
decision needs: the surface sha (early-cutoff key: a body edit leaves it
unchanged, so no dependent recomputation is owed), a referenced-name
bloom (added-symbol trigger), and a governing-config context hash.

The store treats defs_json and the bloom as opaque; the codec lives with
pass_lsp_cross, which is their only writer and reader. A project with no
rows reads back as OK/0 -- callers treat that as "no surface data" and
route to a full rebuild, which is also how databases written before this
table existed upgrade themselves.

Table appears via the CREATE IF NOT EXISTS schema on store open, so the
raw dump writer needs no change: publication opens the staging DB with
the store right after the dump, which applies the schema.

Round-trip covered in store_nodes: batch upsert, ordering, binary bloom
with embedded NUL, NULL bloom, whole-row conflict replacement including
bloom removal, project-scoped delete, and the empty-project signal.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 17:23:37 +02:00
Martin Vogel 80afcd6bbe fix: close the memory-error paths the clang-analyzer lane surfaced
The memory-diagnostics report's priority-4 lane (path-sensitive clang-analyzer,
memory checks only) run over all 111 production files. 21 findings triaged;
the real ones, all cold-path (none can explain #581's per-query residual):

LEAKS
- mcp get_architecture: scope_path leaked on the missing-store early return
  (REQUIRE_STORE frees only `project`); allocate after the gate.
- pass_definitions: cancellation mid-extraction leaked the pass-owned result
  cache including already-extracted entries; mirror the end-of-pass cleanup.
- store package-boundary scan: the row-scan abort path freed the node arrays
  but not the boundary accumulators or their duplicated package strings.
- cbm quarantine set: a duplicate path line leaked the replaced value (and a
  fresh key copy -- the table borrows key pointers); a partial strdup failure
  leaked the surviving half. Reuse the stored key for duplicates.
- pass_githistory: unchecked malloc/strdup -- an OOM dereferenced NULL and a
  failed strdup leaked the index cell. Allocate before claiming the slot.

NULL/UB
- cli config subcommand: NULL argv with nonzero argc slipped the guard (the
  inner `argv &&` shielded only the help comparison) into argv[0].
- store bfs_multi: a negative max_results broke out before any row was
  written, then freed fields of an unwritten negative-index slot. Clamp.
- pass_calls emit_http_async_edge: the service-pattern call sites pass a NULL
  target behind a hand-duplicated URL predicate; a drift between the copies
  turned target->id into a null deref. The callee is now total.
- sqlite_writer: both leaf-array OOM paths left leaf_count stale with a NULL
  array, walking pb_finalize_* into leaves[0]; consistent empty state routes
  them to the existing root=0 failure return.

HARDENED (invariants true but invisible to path-sensitive analysis)
- Leiden CSR + aggregate arrays, SCC adjacency: calloc + endpoint guards, so
  a future degree/collection miscount degrades benignly instead of UB.
- SCC cycle fill: the ncyc==0 no-slot invariant made local.

RECORDED FALSE POSITIVES (no code change)
- yaml sequence starts (loop bound == alloc bound), cypher agg arrays (same
  count both sides), mcp read_message ch (assigned by fgetc each iteration),
  pkgmap clean buffer, mcp csize (Tarjan: ncomp>=1 when nverts>=1), vendored
  verstable x2.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

GATE + LANES (user decision: runner cost accepted)
- make lint-mem (local triage) and lint-mem-ci (gating: vendored-filtered,
  any remaining finding fails). The gate is green because every false
  positive above was restructured for provability -- calloc'd fill-cursor
  arrays, explicit Tarjan invariant, zeroed buffer tails, min-1-element
  allocations -- never suppressed.
- make diag: pinned newest-LLVM ASan/UBSan lane with straighter stacks.
- CI: lint-mem job (_lint.yml) and test-diag job (_test.yml), both on the
  pinned LLVM 22 apt toolchain. Cost disclosure: roughly +25-40 min and
  +25-60 min (ccache-warm) per push respectively.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-08-03 15:37:33 +02:00
Martin Vogel 029416fda6 Merge remote-tracking branch 'origin/main' into qa/call-reference-rebase
# Conflicts:
#	Makefile.cbm
#	internal/cbm/extract_defs.c
#	src/foundation/compat_fs.c
#	src/foundation/compat_fs.h
#	src/pipeline/pipeline.c
#	src/pipeline/pipeline_incremental.c
#	tests/test_main.c
#	tests/test_pipeline.c
#	tests/test_store_checkpoint.c
2026-08-01 01:29:20 +02:00
Martin Vogelandwin4r 63eda549eb fix(store): route label allowlists through the canonical type-like set
cbm.h documents cbm_label_is_type_like() as the single source of truth for
type-like labels, "so adding a new type-like label (e.g. Struct for
Rust/Go/Swift/D structs) updates them all at once instead of scattering
|| strcmp(label,"Struct")==0 across the tree".

A SQL string literal cannot call it. Four queries in store.c and the BM25
ranking in mcp.c carried their own hardcoded label lists, so they silently
opted out of that contract and stopped matching the moment Struct, Interface,
Enum, Type and Trait began being emitted:

  store.c:5279  arch_boundaries         ('Function','Method','Class')
  store.c:5422  arch_packages_from_qn   ('Function','Method','Class')
  store.c:6689  arch_clusters           ('Function','Method','Class')
  store.c:7742  cbm_store_vector_search ('Function','Method','Class')
  mcp.c:2759    BM25 ranking            ('Class','Interface','Type','Enum')

Effect on main today: get_architecture (boundaries, packages, clusters) and
vector search drop every Struct, Interface, Enum, Type and Trait node in the
project, and search_code under-ranks structs -- for Rust, Go, Swift and D,
where struct is the primary type declaration. The BM25 list had drifted
differently again, omitting Struct and Trait but not Interface.

The fix is not a wider literal. CBM_SQL_TYPE_LIKE_LABELS and
CBM_SQL_CALLABLE_OR_TYPE_LABELS live next to each other in constants.h and are
pinned to cbm_label_is_type_like() by a test that checks both directions:
every label the C predicate accepts appears in the SQL fragment, and nothing it
rejects is smuggled in. Adding a type-like label without updating the SQL now
fails that test instead of quietly shrinking query results.

No new labels are introduced. Every label added here is one the extractors
already emit.

Reported by @win4r in #568, whose audit found four of the five sites. Their
patch widened the literals in place and added Actor, a label main does not
emit; this routes through the canonical set instead and also covers Interface,
Type and Trait, which that patch still omitted.

Co-Authored-By: win4r <win4r@users.noreply.github.com>
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-31 14:37:34 +02:00
Martin Vogel 65796ec779 Merge pull request #1175 from SEPURI-SAI-KRISHNA/fix/adr-render-overflow
fix(store): clamp ADR render cursor to prevent stack overflow
2026-07-31 01:52:03 +02:00
Martin Vogel 43fd20337c fix(store): drop the SQLite page-cache slab that faults on arm64
The slab was a static char array handed to SQLITE_CONFIG_PAGECACHE. A static
char array carries no alignment guarantee and SQLite requires 8-byte-aligned
page-cache memory: x86_64 tolerates the unaligned access, arm64 faults. Every
suite that opens a store died instantly on windows-11-arm with
STATUS_ILLEGAL_INSTRUCTION (pass=0, secs=0) while pure-logic suites in the same
wave passed, and both arm64 shards were green on the commit before the merge.

It was also never the fix it was added for: measurement showed the slab
installed correctly and #581's growth unchanged, because the cause was thread
heaps never being released. Removing it rather than aligning it -- an unverified
optimisation has no claim on a release gate.

Note PR CI does not build the windows-11-arm leg, so this class of defect can
only surface in the dry run.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-26 15:34:07 +02:00
Martin Vogel a46627d6ee fix(ci): restore vendored mimalloc, hoist enum out of struct, format
Three CI failures, one cause each:

- Vendored integrity flagged a MISMATCH on the Windows mimalloc primitives.
  The decommit counters added there were diagnostic and have served their
  purpose -- 16k calls, zero failures, which retired the theory that purge was
  silently failing. Restored to pristine, and the census fields they fed are
  gone with them.
- GCC rejected an anonymous enum declared inside a struct ('declaration does
  not declare anything'); clang accepted it, so it only broke the Ubuntu and
  Windows smoke builds. Hoisted to file scope.
- clang-format across the files touched this session.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-26 01:05:19 +02:00
Martin Vogel 5929f6a2e6 fix(store): install the SQLite page-cache slab before SQLite initialises
SQLITE_CONFIG_PAGECACHE is only accepted before sqlite3_initialize, so calling
it from the store open paths was always refused (rc=misuse) and every
request-scoped store fell back to allocating its page cache from the general
allocator. Each request therefore took a fresh multi-megabyte run of arena
slices and left the previous one fragmented behind mimalloc's commit frontier,
which is the per-request retention behind #581.

Hoisted to process init, immediately after the allocator binding and before
anything can open a store. The existing call sites stay as an idempotent
backstop.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-25 19:07:52 +02:00
Martin Vogel 22e917db52 wip: shared dense sqlite page-cache slab
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-25 16:38:42 +02:00
Martin Vogel 0baddf7887 daemon: permanent lifecycle, daemon-backed CLI/hooks, real-Windows hardening, long-path launcher transactions
Daemon lifecycle and Windows correctness, verified on a real Windows 11
ARM64 VM through the maintained test-infrastructure/vm drivers, plus the
macOS and Linux arm64 suites and the container lint gate.

Daemon lifecycle:

- daemon start/stop/status subcommands. `daemon start` launches a
  PERMANENT daemon (spawn shape is byte-exact argv; survives idle
  periods and session ends) and reports an already-active daemon
  instead of failing. Permanence is honored at every stop latch:
  last-committed-client disconnect, host initial-client window,
  coordinator release, and application final-session close — a
  permanent daemon also keeps admitting new sessions after its last
  one closes.
- daemon stop refuses while sessions are active and lists the blocking
  peers (pid/role) that must finish first; an idle daemon drains
  through the activation-shutdown machinery with the ACK ordered after
  connection interrupts. A second stop is idempotent. The wire ops are
  no-cohort first-frame requests with peer fingerprint authentication,
  so stop/status never conflict with an exact-build admission gate.
- One-shot CLI commands now execute through the daemon (index workers
  keep their local supervised path). A cold CLI run that had to spawn a
  temporary daemon prints a hint that `daemon start` removes the
  per-command startup tax; a warm daemon is recycled silently.
- Hooks are connect-only fail-open: with no daemon present the hook
  emits a visible, rate-limited notice (Claude-dialect systemMessage
  plus stderr for other dialects) and always exits 0 — augmentation is
  never allowed to block the caller's tool use.
- Version skew: a newer-build client automatically drains an
  older-build permanent daemon (strict semantic-version triples only;
  dev builds never auto-drain) and the build-conflict message names
  `cbm daemon stop` as the manual escape hatch.

Windows IPC/runtime (real-VM verified):

- ipc(win): persistent pending overlapped ConnectNamedPipe. The accept
  path used to destroy its listening pipe instance on every 20 ms poll
  timeout; a client attaching in the teardown window was severed or left
  on an orphaned pipe object whose HELLO no server handle could ever
  read, absorbing the connect until the client's own timeout expired.
  The pending connect now survives poll timeouts and nothing is
  destroyed while a client could be attaching.
- ipc(win): drain-before-close for final responses. Closing a named-pipe
  server handle can discard a just-sent response before the peer reads
  it (POSIX stream sockets never lose buffered data on close). A bounded
  cbm_daemon_ipc_connection_drain (read-until-peer-EOF; no-op on POSIX,
  immediate on interrupted connections) now precedes close in
  runtime_worker_finish and runtime_reject_inline, so hello-conflict,
  capacity and disconnect acknowledgements reliably reach the peer.
- runtime: CLOSE_INTENT wire frame. A Windows named-pipe client has no
  transport half-close, so close_begin now announces departure with an
  explicit frame (ordered after APPLICATION_CANCEL, before the local
  interrupt); the server releases the client's admission on receipt
  instead of waiting for the handle to close. Admission-drop timing is
  now identical to POSIX shutdown() semantics on every platform.
- runtime(win): client close cancellation. close_begin serializes with
  request publication under the send lock, best-effort sends the active
  token's APPLICATION_CANCEL frame, then interrupts local I/O; the
  server cancels MCP/subprocess work promptly. Contract tests accept
  both correct outcomes (interrupted transport or decoded CANCELLED).
- runtime: activation acknowledgement ordering. The activation ACK is
  the requester's license to act on "snapshotted and draining", so every
  connection interrupt is now initiated before the ACK is sent; a
  session could previously get one more request serviced after the
  requester observed the ACK.
- service(win): deadline-bounded private-file prepare. The conflict-log
  prepare retry loop (100 x Sleep(2), which rounds up to the ~16 ms
  timer granularity) burned ~1.6 s against permanently obstructed paths,
  stalling hello rejections past the client's timeout. The retry budget
  is now a 250 ms deadline; transient share collisions still retry.
- subprocess(win): cmd.exe /C payload encoder quotes metacharacters
  correctly (root cause of the git-on-Windows failure cluster).
- watcher: SHA-256 buffer sizing (CBM_SZ_64 -> CBM_SZ_128) and a native
  Windows stop/unwatch cancellation test with exact-image verification.
- httpd: send_all writes in bounded 64 KiB slices. A single giant
  nonblocking send() on Windows is absorbed wholesale into AFD kernel
  buffering regardless of SO_SNDBUF, so send deadlines and interrupts
  could never engage against a slow peer (and the full payload was
  pinned in nonpaged pool). Slicing restores a deterministic
  backpressure point; a test hook pins SO_SNDBUF for the deadline and
  interrupt tests.
- ui/http: shutdown lifecycle — interrupt checks, response-wide send
  deadline, explicit connection states, refusal to free a server while
  a listener-owned connection is active.

Windows long-path support:

- Central path-aware wide conversion (canonicalize via GetFullPathNameW
  and prepend the extended-length prefix for absolute paths >=240) at
  the compat chokepoints (cbm_fopen/compat_fs/mkstemp/mkdtemp), sqlite
  store opens, and the daemon build-fingerprint/log paths. Deep managed
  installs (a 64-hex generation directory routinely exceeds MAX_PATH)
  now index, stage and activate correctly.
- activation transaction: its own file APIs and the component-walking
  ancestry validators now operate in the extended-length namespace;
  the launcher path is canonicalized (and prefixed when deep) once at
  entry so every downstream exact-string comparison stays
  form-consistent.
- Executable self-resolution uses the wide APIs (GetModuleFileNameW,
  GetFileAttributesW) so non-ASCII install paths survive argv[0]
  resolution.

Windows launcher install/uninstall transaction:

- FileRenameInfoEx names are NUL-terminated in an over-allocated
  buffer. FileNameLength governs per the contract, but filter drivers
  read FileName as NUL-terminated and appended adjacent heap bytes to
  created names — a flaky, garbage-suffixed rename target. Both the CLI
  and the launcher rename helpers are fixed.
- Uninstall retires state via rename-aside (.cbm ->
  .cbm-retired-v1-<tag>-<pid>) with the retired tag shortened to 16 hex
  chars so the bare rename target stays under the FileRenameInfoEx
  NT-conversion ceiling at guard depths; 64 bits still uniquely
  identify the generation.
- When the running launcher's mapped generation backings pin .cbm
  against rename, the backings are relocated to activation-<pid>-N
  .retired tombstones beside the install (a mapped image may be renamed,
  never deleted; the launcher's liveness-guarded sweep reclaims stale
  tombstones). Every relocation is recorded, and a FAILED uninstall
  reverses the moves after restoring .cbm — via MoveFileExW with
  extended-length paths on both arguments, since the deep generation
  target is beyond the handle-based rename's bare-path reach — so a
  restored install keeps its generation backings and stays runnable.
- After a committed uninstall the retired tree's backings are relocated
  out so the tree is shallow enough for the detached cleanup's rd, and
  the cleanup's working directory strips the extended-length prefix
  (CreateProcessW lpCurrentDirectory silently ignores prefixed paths).
- Files created under Administrators-default-owner directories
  (CopyFileW destinations, CREATE_NEW tombstones, probe directories)
  are explicitly owner-stamped so the exact-owner validators hold on
  runner images; guard fixtures stamp hand-built trees the same way.

Diagnostics, tests and infra:

- diagnostics: discovery is now an always-delivered JSON control record
  (new cbm_log_control) that survives CBM_LOG_LEVEL suppression and
  paths containing spaces; placement honors $TMPDIR with /tmp fallback
  via a diagnostics-local helper; the soak parser reads the JSON record;
  documented in docs/CONFIGURATION.md. Red-first coverage for suppressed
  log levels, TMPDIR-with-spaces, and native Windows output-contract
  assertions.
- tests(win): daemon_ipc/daemon_frontend fixtures now build endpoint
  parents with production-shaped ancestry (LocalAppData on Windows, via
  th_secure_runtime_parent_new) — the runtime ancestry validation
  correctly refuses temp roots whose ancestors grant mutation rights to
  Authenticated Users (C:/msys64/tmp, GitHub-runner work dirs) — and
  drive the documented startup-owner publication flow before reading
  generation-bound endpoint addresses. This turns the 26 Windows
  failures previously visible in CI's full-test job green without
  weakening any validation.
- tests(win): the launcher guard covers the full permanent-launcher
  contract including failed-uninstall restore and immediate reinstall
  after uninstall; new daemon lifecycle and reworked hook-augment
  guards run the start/recycle/stop flow end to end.
- tests: CBM_SKIP_PERF is now actually consumed by the test runner
  (it was set by CI but never read, so perf suites ran everywhere);
  four throughput/bench suites are classified as perf, the heavy
  store_arch suite moved to the slow-timeout tier, and two
  wall-clock-sensitive assertions were rewritten as invariant checks
  with coarse hang-detector backstops.
- build/test infra: build-dir safety contract, UI dev-proxy security
  contract, soak daemon-recovery contract, path-safety helper, the
  Windows VM worktree-sync contract wired into scripts/test.sh, and
  vm/win.sh guards building its clean embedded-UI product in an
  isolated BUILD_DIR so it cannot clobber the incremental test build.
  provision-windows.sh now installs Node.js for the guards UI build.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-21 15:32:15 +02:00
SEPURI-SAI-KRISHNA 13d523de48 fix(store): clamp ADR render cursor to prevent stack overflow
Signed-off-by: SEPURI-SAI-KRISHNA <saik20533@gmail.com>
2026-07-19 12:35:58 +05:30
Martin Vogel 87a0e3f74f fix: preserve semantic graph relationships
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 15:52:50 +02:00
Martin Vogel 5d8a9421fb Merge remote-tracking branch 'origin/main' into feat/shared-coordination-daemon
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>

# Conflicts:
#	src/mcp/mcp.c
#	tests/test_cli.c
2026-07-18 13:48:06 +02:00
Martin Vogel dd87676865 Merge origin/main into feat/shared-coordination-daemon
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 01:29:45 +02:00
Martin Vogel 130a82bd10 feat(mcp): real impact analysis + cycle detection (multi-source BFS, SCC)
Phase 3 of the graph-analysis overhaul. Two new capabilities on a shared
multi-source traversal substrate, plus a context-bomb guard.

detect_changes — rebuilt into real blast-radius analysis:
  The old handler parsed and echoed depth but never traversed: impacted was
  just the symbols DEFINED in changed files. It now resolves the git diff to
  those symbols, then runs ONE multi-source BFS to the transitive impact set.
  - default direction inbound = the blast radius (transitive callers that may
    need review); outbound/both available.
  - seeds are excluded from the result, and a changed file reached from
    another changed file is not counted as extra impact (that is not
    downstream fallout).
  - merge_base SHA pins the exact diff base for reproducibility.
  - impacted_modules: a 2-segment (src/mcp, tests/...) rollup — a quotient
    view of where the radius lands, always complete (overflow bucketed, never
    silently dropped). Per-symbol rows cap at limit (default 200, nearest
    hops first) with impacted_total exact and impacted_omitted surfaced.
  On a 7-file core diff: 1469 impacted symbols, 64KB unclipped -> 10.7KB with
  the rollup + cap. format:json returns the same model as structured JSON.

cbm_store_bfs_multi (store): one recursive CTE anchored on ALL seeds via a
  temp table — never seed_count separate walks over overlapping hubs.
  Seed-exclusion NOT IN, MIN(hop) across the set, canonical (hop,id) order,
  a memory-safety ceiling that reports *truncated instead of silently
  capping. Reproduce-first: seed-exclusion + min-hop, and truncation.

get_architecture aspects:[cycles] — SCC condensation:
  Iterative Tarjan (heap stacks, no recursion overflow) over the CALLS graph
  reports circular dependencies (strongly-connected components of size > 1).
  cbm_store_fetch_call_edges feeds it with a 400k-edge scan budget that
  reports partial coverage rather than truncating silently. Opt-in ONLY —
  never via a default or 'all' call, since it scans the whole call graph.
  On this repo it surfaces 26 real cycles (e.g. the C-LSP type-evaluation
  recursion) in ~5KB. Reproduce-first: a 3-node cycle is found, an acyclic
  chain is not, and a default call does not run the scan.

get_code_snippet: cap the returned line span at 500 (a structural Module/File
  node spans its whole file — an unclipped read returned ~400KB in one call).
  source_clipped flags it; start_line/end_line still allow a targeted re-read.

Full suite 6,372 passed / 0 failed; lint clean.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-18 00:13:50 +02:00
Martin Vogel c5bffb7fe6 feat(mcp): tree-format tool output, exactly-once pagination, lean defaults
Round-2 overhaul of the tool-output surface, built eval-first (byte harness +
no-context field-eval agents, always vs the v0.9.0 release binary):

Format — tree syntax everywhere:
- compact_out emits tree tables: 'key: N  (cols: a b c)' headers, indented
  space-delimited rows, '-' placeholders, space-aware quoting. Every table
  in every tool converts mechanically.
- Grouped prefix-factored output is the DEFAULT for search_graph and
  trace_path: rows grouped under their shared (qn-prefix, file) printed
  once, row qn = group prefix + '.' + name (stated in-band). Measured:
  192-hit search 2,929B (was 4,811 TOON / 238,466 v0.9.0), 82-row trace
  2,256B (was 4,234 / 8,094). Field eval: 5/5 answers correct, QN
  reconstruction verified, no comprehension loss.
- detail:'ids' tier: bare-qn enumeration for wide sweeps (-44%).

Pagination — exactly-once, stateless:
- store_meta generation (per-DB random uid + mutation counter bumped in
  cbm_store_upsert_project — every index run's choke point): cursors can
  never silently validate against a rebuilt DB.
- trace cursors: opaque token (leg, generation, params-hash, (hop,id)
  watermark); pages drain callees then callers with exact
  callees_total/callers_total on every page; teaching errors for
  params-mismatch and staleness. Guard: 12 callees paged 5+5+2, every row
  on exactly one page.
- deterministic ordering: BFS ORDER BY hop,id; canonical (hop,id) sort +
  min-hop dedup across same-name seeds (hop fed risk_labels wrong before);
  search (name,id)/(rank,id) tie-breaks make offset pages stable.

Lean defaults + teaching errors:
- get_graph_schema no longer advertises blocked internal fields (fp/sp/bt).
- index_repository coverage lists: counts + 5 examples + one-line pointer
  notes (full record was always in the logfile); 6.0KB -> 1.5KB here,
  53KB observed on a large repo. index_status gates the git context block
  behind verbose:true.
- trace limit param (<=5000) + truncated:true — never a silent truncation;
  invalid direction now errors with the valid values; requesting core
  columns via fields hints instead of emitting empty cells; cypher
  resolves f.file/f.qn/f.path aliases.

Trace-correctness fixes (reproduce-first, RED verified):
- bfs_collect_edges 4KB id-string overflow: past ~1000 visited nodes the
  edge SQL failed to prepare and EVERY trace edge silently vanished; now a
  per-connection temp-table join (star guard: 1200/1200 edges).
- bfs_union_same_name: min-hop across seeds + edge dedup.

Guards: gating tool-output regression suite (context-explosion detector:
blocked-field leaks, repeated-key envelopes, oversized notes + calibrated
byte ceilings + semantic floors). Full suite 6,367 passed / 0 failed.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-17 22:32:53 +02:00
Martin Vogel 9dfe52f16e fix(store): warn when a checkpoint can't reset a large WAL (starvation)
Add a checkpoint-starvation warning to cbm_store_checkpoint: read the
wal_checkpoint_v2 out-params (frames in the WAL vs frames reclaimed) and, when
the WAL is large (>= ~1 GiB of frames) but the PASSIVE checkpoint couldn't reset
it because readers hold marks, log store.wal.starving. This is the operator
visibility the reclaim limit can't provide on its own — it surfaces the
starvation (and its scale) before the -wal file fills the disk. Portable
(no file stat / no path handling), fires only under genuine starvation.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-16 19:39:15 +02:00
Martin Vogel 0e00ef5702 feat: coordinate concurrent CBM sessions
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-16 19:20:46 +02:00
Martin Vogel 4a3a9b745f fix(store): bound the WAL with journal_size_limit so a starved log is reclaimed
On-disk write connections set no journal_size_limit, so the -wal file is never
physically shrunk: all our checkpoints are PASSIVE (TRUNCATE is deliberately
avoided because its ftruncate(fd,0) can SIGBUS a sibling mmap'd process on
macOS), and PASSIVE never ftruncates. Under checkpoint starvation the WAL then
grows without bound (reported: 115 GB, filled the drive) and even a later
successful checkpoint leaves the file at its peak size (#1083).

Set journal_size_limit = 256 MiB on write connections so SQLite truncates the
-wal back to that bound whenever a checkpoint can finally reset the log —
reclaiming disk opportunistically during a session, not only on clean close.
256 MiB is far above the healthy WAL (~4 MiB under the default 1000-page
autocheckpoint), so normal indexing never triggers truncate/regrow churn; the
limit only fires after abnormal growth. No TRUNCATE checkpoint is introduced.

Scope: this is the *reclaim* half of #1083. It does not prevent the peak under
continuous reader contention (a checkpoint can't reset the log while a reader
holds a mark) — the write-amplification driver (concurrent full re-indexes of
one repo) is serialized by the cross-process pipeline lock in #892.

Test: a file-backed write store now reports journal_size_limit = 256 MiB
(RED on main: -1 / unlimited).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-16 18:53:35 +02:00
Martin Vogel d6c9d28bf5 style: apply current C formatting
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-13 02:00:32 +02:00
Martin Vogel f0db224140 feat: add coverage-aware agent integrations
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-13 01:50:03 +02:00
Martin Vogel 37880b62e8 fix(store): reject null search handles
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-13 01:48:29 +02:00
Martin Vogel 73a3c347d9 fix(cypher): shortest-path var-length semantics + advertised depth clamp
Three of #797's four findings fixed (the fourth — repeated-variable
unification — already holds on main and is now pinned by the test):

- Walk padding: the BFS recursive CTE deduped (node, hop) PAIRS, so a
  single self-loop minted every hop level for every node it could
  reach — *k..k matched fabricated walks of arbitrary length (a
  100-hop 'longest chain' artifact) and the row set exploded to
  nodes x depth (the reported crash). The traversal now returns each
  node once at its MINIMAL hop (MIN/GROUP BY): variable-length matches
  nodes whose shortest path lies in [min,max]. This is deliberately
  shortest-path semantics, not openCypher trail enumeration — right
  for reachability/depth audits on call graphs and linear instead of
  exponential; trace_path shares the traversal and now also reports
  each node once on cyclic graphs.
- Silent 100-hop cap: a clamped hop range now surfaces as
  result->warning ('clamped ... an empty result may mean clamped, not
  no such path'), emitted by query_graph in both TOON and JSON forms,
  on top of the existing log WARN.

Reproduce-first: cypher_exec_varlength_path_semantics_issue797 —
self-loop + real 2-chain fixture; RED on the walk semantics (*2..2
returned 3 endpoints instead of 1), GREEN now, incl. variable
unification, zero rows for nonexistent path lengths, and the clamp
warning assertion.

Closes #797

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 20:58:28 +02:00
Martin Vogel 1a38d0a2e8 Merge pull request #1016 from DeusData/fix/stale-wal-sidecars
fix(store): remove destination WAL/SHM sidecars before installing a fresh DB
2026-07-11 05:36:24 +02:00
Martin Vogel d7e6082be7 Merge pull request #1019 from DeusData/fix/artifact-torn-export
fix(artifact): snapshot exports consistently and refuse corrupt imports
2026-07-11 04:51:02 +02:00
Martin Vogel 43d84ee6f2 fix(store): replace-on-rename for dump installs on Windows
The new stale-sidecar guard exposed a pre-existing Windows gap in the
same code path: rename() fails with EEXIST when the destination
exists, so cbm_store_dump_to_file could never install over a previous
DB generation on Windows. New cbm_rename_replace uses
MoveFileExW(MOVEFILE_REPLACE_EXISTING) with wide paths (raw
MoveFileExA would re-mangle non-ASCII cache paths); POSIX keeps plain
atomic rename.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 04:14:04 +02:00
Martin Vogel b79c6da931 fix(artifact): snapshot exports consistently and refuse corrupt imports
The FAST export path (the watcher/incremental auto-update) read the
raw main-file bytes of a live WAL-mode store: committed rows still in
the -wal were missing entirely and mid-checkpoint reads produced torn
snapshots (field artifact: four short indexes + btreeInitPage error 11
across ~12 nodes-table pages). Import then installed the torn file
byte-for-byte — the integrity check only sanity-checked the projects
table.

- Export: both quality levels now snapshot via VACUUM INTO (the
  consistent copy); only the index-stripping remains BEST-only, FAST
  keeps zstd-3. The fast path pays a full-copy VACUUM INTO now —
  correctness over speed; a torn team-shared artifact costs far more.
- Import: new cbm_store_check_integrity_deep (shallow check + PRAGMA
  quick_check) — used only on this rare path, hot-path callers keep
  the cheap shallow check.

Reproduce-first: artifact_fast_export_snapshots_live_wal_store
exports FAST while the writer connection is open — RED on the old
code with 0 of 60 nodes surviving the round-trip (all rows lived in
the WAL); GREEN now. store_deep_integrity_detects_page_corruption
pins the import guard both directions (healthy passes, zero-banded
pages refused).

Closes #895

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 03:28:32 +02:00
Martin Vogel b422a16889 fix(store): surface SQLITE_CORRUPT from row scans instead of truncating
Every row-scan loop discarded the terminal sqlite3_step code, so
SQLITE_CORRUPT mid-scan was indistinguishable from a clean end of
results: against a page-corrupted DB, counts answered from covering
indexes stayed correct while row fetches died at the first corrupt
table page — every query surface returned plausible truncated/empty
answers with no error (search total=379 with results=[], Cypher counts
diverging from schema counts, BM25 total=0, empty schema sections).

All 31 scan loops now capture the terminal code and fail loudly with
CBM_STORE_ERR + a store error message when a scan aborts, with
per-site cleanup (partial results freed or handed to the caller's
normal free path). Two deliberate soft spots, both enrichment-only and
documented inline: include_connected neighbor names (no store handle)
and the cluster adjacency scan (partial adjacency; primary surfaces
already fail loudly). Best-effort schema property-name discovery and
two capped helper scans keep their by-design early-exit semantics.

Reproduce-first: corrupt_page_scan_returns_error_not_truncation
zeroes a 40-page mid-file band per the issue's dd recipe and asserts
scans return CBM_STORE_ERR. RED on the old code with the exact
reported symptom (OK with 264 of 2000 rows silently truncated); GREEN
now. Fixture-health guards make a healthy-scan pass impossible to
mistake for a fix.

Closes #896

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 03:13:45 +02:00
Martin Vogel 394198e8e9 fix(store): remove destination WAL/SHM sidecars before installing a fresh DB
Three code paths install a brand-new DB file where a previous
generation lived without removing the destination's -wal/-shm:
the pipeline page writer (cbm_writer_open), artifact import (which
cleaned the TMP file's sidecars but not the destination's), and
cbm_store_dump_to_file (whose comment wrongly asserted stale sidecars
'become stale and get recreated'). SQLite validates a WAL against the
sidecar's own header/checksums — not the main file — so a crashed
session's leftover WAL is replayed ON TOP of the freshly installed
file at the next open, splicing old-generation pages into it.

New shared helper cbm_remove_db_sidecars (compat_fs, wide-path safe on
Windows) called at all three install points.

Reproduce-first: dump_install_ignores_stale_wal_sidecar follows the
issue's recipe (hot-copy a live WAL, clean close, restore the copy,
install generation 2, reopen). RED on the unguarded code — the replay
clobbered the fresh generation entirely (fresh row unqueryable);
GREEN with the cleanup. Non-vacuous: asserts the hot WAL is non-empty
before the copy.

Closes #897

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-11 02:40:54 +02:00
Martin Vogel 2ba1107921 feat(index): report deliberately-ignored files as a by-design coverage class
Completes the #963 coverage taxonomy: alongside the best-effort FAILURE
signals (parse_partial, skipped), the report now lists what was
deliberately NOT indexed — a deterministic, by-design class, never mixed
into the failure surfaces.

- discover: file skips are classified by reason (gitignore / cbmignore /
  skip-list / ignored-suffix / fast-pattern / size-cap) and collected via
  the new cbm_discover_ex2 (per-file entries capped at 2000 with the
  uncapped total reported — truncation is explicit, never silent; whole
  excluded subtrees stay exhaustive as before). Unsupported-extension
  files are deliberately NOT recorded: no grammar for an extension is
  not an ignore decision, and listing every README/asset would drown
  the signal
- pipeline: ignored files captured per run (full + incremental) and
  persisted into the index_coverage table as not_indexed_file rows,
  excluded dirs as not_indexed_dir; incremental runs rewrite the
  by-design set fresh from discovery instead of carrying old rows
- store: the deleted-file prune exempts not_indexed_* (deliberately
  unindexed paths never have hash rows); the missed shadow graph keeps
  showing FAILURES only — the UI's report-an-edge-case callout can
  never fire for gitignored paths, and a project whose only coverage
  rows are by-design gets no miss graph at all
- mcp: index_repository response gains not_indexed_files {files, count,
  truncated, note} (dirs stay under excluded); index_status's coverage
  report gains a not_indexed section (dirs + files + note) and no longer
  lumps by-design rows into skipped; tool descriptions updated — the
  by-design class is presented as deliberate and deterministic (change
  the ignore rules and re-index), with no best-effort hedging
- tests: e2e guard (gitignored file + dir reported with reasons on both
  surfaces, failure surfaces stay clean, missed graph excludes them,
  rows survive an incremental re-index, ignored constructs genuinely
  absent from the graph); store unit test covers the prune exemption
  and shadow-graph exclusion

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-09 07:51:45 +02:00
Martin Vogel 664313e4ac feat(index): missed graph in UI + read-time coverage hook + recovery subtraction
Four refinements to the coverage signal:

- extraction: subtract DEFINITE recovery before flagging. Tree-sitter
  error recovery plus the ERROR-descending def walker often re-extract
  constructs inside a failed region (verified: `def broken(:` comes
  back as a def); a region whose every line is covered by definitions
  that START inside it is not a miss, and a fully recovered file is not
  flagged at all. Container defs (Module/Package) don't count as
  evidence, and partially covered regions stay flagged — the
  #ifdef-split case keeps its flag because the first branch's function
  is genuinely lost
- naming: the query_graph option is graph="missed" (the graph shows
  ONLY misses — "coverage" was misleading); shadow project renamed to
  "<name>::missed"; tool descriptions updated so agents discover both
  the option and its semantics
- hook: the CLI-installed PreToolUse augmenter now also matches Read
  and injects a coverage note when the file being read is listed as not
  fully indexed ("line ranges X-Y could not be parsed — the file
  content you are reading is ground truth"). Safe against the old
  issue-362 hazard: the augmenter is structurally non-blocking (always
  exit 0, additionalContext only), mirroring the Gemini matcher that
  already includes read_file; matcher upgrade bookkeeping updated
- ui: "Missed files" toggle in the graph sidebar renders the miss
  graph as a second graph option — /api/layout gains graph=missed
  (same db file, shadow-project scoping; base project name validated
  as before)

Tests: recovery-subtraction cases (recovered def unflagged, garbage
region flagged, trailing recovered defs keep the flag), CLI matcher
tests updated, e2e resilience fixtures switched to an unrecovered
miss; verified end-to-end: hook emits the note for a flagged file and
stays silent otherwise, /api/layout?graph=missed serves the miss graph
and the code graph is unchanged, frontend build + tests green.

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-08 19:13:25 +02:00
Martin Vogel f9d7f4aa61 feat(index): persist coverage in separate table + queryable miss graph
Persist the parse-coverage signal outside the graph and make the misses
queryable both as a list and as a graph.

- store: new index_coverage table (project, rel_path, kind, detail) —
  coverage is metadata ABOUT the graph and never mixes into the graph
  tables; replace/get/free APIs with a deleted-file prune keyed off
  file_hashes (the live-file set)
- store: materialize a derived miss graph under the shadow project
  "<project>::coverage" (Project -> Folder chain -> File{kind,detail},
  CONTAINS_FOLDER/CONTAINS_FILE edges), rebuilt from the table inside
  the same transaction on every replace; invisible to list_projects
  (which scans the cache directory, not the projects table)
- pipeline: coverage is written in the same persist step as every other
  post-dump SQL write (hashes, ADR, FTS). Full runs replace the whole
  set; incremental runs merge rows surviving from files not re-extracted
  with this run's fresh entries, so a fixed file's flag clears and
  deleted files prune automatically
- mcp: new get_index_coverage tool (per-project parse_partial + skipped
  lists with the best-effort note); query_graph gains graph="coverage"
  to run the same cypher against the miss graph; the File-node property
  marker from the previous commit is dropped in favor of the separate
  table — the code graph gains no coverage rows
- wording: constructs in flagged ranges MAY be missing — tree-sitter
  error recovery still salvages some (verified: a function inside a
  flagged #ifdef region and even a broken def were still extracted)
- tests: store round-trip/prune/shadow-graph unit test; e2e asserts the
  response fields, the advertised coverage-graph query, code-graph
  purity, and that the flag clears after the file is fixed (incremental
  route)

Refs #963

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-07-08 17:47:59 +02:00
Martin Vogel bb2510eac3 Merge pull request #806 from DeusData/distill/560-architecture-overview
fix(mcp): get_architecture overview subset, aspects enum + validation (+ .cbmignore how-to)
2026-07-04 03:44:29 +02:00
Martin VogelandAlexandros Pappas 6012f4626e fix(graph): IMPORTS edges keyed by local_name across buffer, store and dump
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>
2026-07-03 20:16:39 +02:00
Martin VogelandKirchlive 22f83b2179 fix(mcp): get_architecture overview subset, aspects enum + validation
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>
2026-07-03 19:08:47 +02:00
Martin Vogel 8ec7523a30 fix(store): open query stores READONLY so queries never mutate dbs
Query tools opened the project db with SQLITE_OPEN_READWRITE and ran write
pragmas (PRAGMA journal_mode=WAL + wal_checkpoint), so every query MUTATED the
db (rewrote the header, created -wal/-shm) and FAILED outright when the db or
its directory was read-only (the WAL pragma cannot create -shm on a read-only
filesystem -> resolve_store returned NULL -> "project not found"). Latent
data-integrity bug found while diagnosing #704 (a query grew a 0-byte shadow
db to 100 KB).

- cbm_store_open_path_query now opens SQLITE_OPEN_READONLY. configure_pragmas
  gains a read_only mode that runs only the non-writing pragmas (foreign_keys,
  temp_store, busy_timeout, mmap_size) and skips journal_mode / wal_checkpoint
  / synchronous. The read-write open (cbm_store_open_path) is unchanged.
- Read-only-filesystem fallback: SQLite opens lazily, so a RO-FS failure
  surfaces on first access; we probe with SELECT 1 and, only if the file
  exists, retry once with a `file:<path>?immutable=1` URI (URI-special
  characters percent-encoded) which bypasses WAL and reads the main db
  directly. The fallback is gated to that failure path so writable-fs readers
  still see uncheckpointed WAL frames via -shm.
- manage_adr is the only resolve_store caller that writes; it now opens its own
  read-write handle (cbm_store_open_path) for ADR storage (in-memory/embedded
  stores still write through the resolved store). All 12 resolve_store callers
  audited; query_graph is read-only (the Cypher engine rejects writes).

Reproduce-first (non-vacuous, no real read-only mount needed):
- readonly_query_does_not_mutate_db: snapshot a rollback-journal db's bytes,
  run search_graph, assert the file is byte-identical + no -wal created. RED
  before (the WAL pragma rewrote the header).
- readonly_query_succeeds_on_readonly_fs: chmod the containing DIRECTORY 0555
  (the real read-only-fs symptom -- a 0444 file alone is vacuous since SQLite
  downgrades the open), run search_graph, assert it succeeds. RED before (WAL
  pragma could not create -shm -> "project not found"); exercises the immutable
  fallback.
Full suite 5724/0.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-30 16:48:40 +02:00
Martin Vogel 9cbb1aa90f style: clang-format-20 sweep (marathon-touched files)
The CI lint uses clang-format-20 (scripts/lint.sh --ci CLANG_FORMAT=clang-format-20).
Grammar/LSP/struct/issue edits across these files weren't formatted with cf-20,
failing lint and gating the make-test job. Format with cf-20.1.8 to match CI.
No logic change.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 14:34:57 +02:00
Martin Vogel 3f754cf449 fix: 17 reproduce-first issue + new-bug fixes (per-case verified)
Each was RED (reproduce-first). Verdicts: production bug unless noted.

- #510 nested .gitignore: load root .gitignore unconditionally (discover.c).
- #403 IDE dir: add "Antigravity" to ALWAYS_SKIP_DIRS.
- #571 CJK project name: transliterate non-ASCII bytes to hex in fqn.c (was -> "-").
- #520 detect_changes: include untracked files (git status --porcelain -uno) (mcp.c).
- #363 CBM_MEM_BUDGET_MB env override (mem.c).
- #570 Codex dual-hook: write the hook to hooks.json when present (cli.c).
- #431 VSCode per-profile mcp.json (cli.c).
- #521 routes from config URLs: deny upstream/jwks/healthcheck/... keys (pipeline.c).
- #548 D:\ browse: normalize sep before is_dir + drive-root self-parent (http_server.c);
  TEST A was a broken fixture (called is_dir on backslash) -> corrected to the
  production sequence; assertion unchanged.
- #495 Rust cfg-gated twins: fold #[cfg(...)] into the QN (extract_defs.c).
- new_py_tuple_unpack: handle pattern_list/tuple_pattern LHS -> Variable per id.
- new_ts_class_field_arrow: class arrow-field -> Method + scope (extract_defs.c).
- repro_extraction/#554 C++ out-of-line: compute_func_qn class-qualifies the call's
  enclosing QN via cbm_cpp_out_of_line_parent_class (extract_unified.c + helpers.h).
- #523 cross-repo HTTP_CALLS: emit service-pattern edge with target=NULL when the
  client lib is unindexed (pass_calls.c, pass_parallel.c).
- #514 data_flow arg expressions: serialize "args" on CALLS edges + propagate
  through store BFS + mcp data_flow (pass_calls.c, store.h/.c, mcp.c).
- #627 query_graph: size_t malloc + NULL-check; OPTIONAL MATCH from bound terminal
  so WHERE c IS NULL returns orphans correctly (cypher.c).
- #546/#480 trace_path: union BFS across same-name nodes (.d.ts dup) (mcp.c).

DEFERRED (need a maintainer decision, untouched): #56 cross-crate Rust (needs Cargo
workspace plumbing / Rust cross-LSP — architectural), #607 reinstall-deletes-index
(cbm_remove_indexes is shared with uninstall/update where deletion is intended).

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-06-27 11:59:59 +02:00
Berserk Agent e270ceb8a4 style(store,mcp): clang-format-20 for CI lint (#604)
Signed-off-by: Berserk Agent <berserk@agent.local>
2026-06-24 13:54:33 +00:00
Berserk Agent 55058c05f0 fix(arch): normalize path trailing slashes; align MCP path_scoped with store (#604)
Signed-off-by: Berserk Agent <berserk@agent.local>
2026-06-24 12:54:45 +00:00
Berserk Agent 9332593b26 Store path scoping for get_architecture
Signed-off-by: Berserk Agent <berserk@agent.local>
2026-06-24 12:38:11 +00:00
Matt Van Horn 0714e46346 fix: address self-review findings — keep node_degree CALLS-only
cbm_store_node_degree backs the MCP get_code_snippet callers/callees
counts and the Cypher in_degree/out_degree virtual property, which are
deliberately CALLS-scoped. Restore it to CALLS-only and scope the
INHERITS/IMPLEMENTS broadening to cbm_store_search alone, which is the
search_graph surface issue #558 is about. Adjust the new tests so
node_degree assertions expect CALLS-only counts while the search_graph
result assertions expect the broadened structural degree.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-23 04:08:36 -07:00
Matt Van Horn 65b8425648 fix: count INHERITS/IMPLEMENTS edges in search_graph degree
search_graph reported in_degree/out_degree of 0 for Class nodes that
have real INHERITS edges, because the degree computation in both
cbm_store_node_degree (Cypher virtual properties) and cbm_store_search
(result population and min_degree/max_degree filter) only counted CALLS
(and USAGE) edges. A min_degree filter therefore silently excluded
well-connected classes.

Broaden the edge-type predicate in both surfaces to
IN ('CALLS', 'USAGE', 'INHERITS', 'IMPLEMENTS') so search_graph and
Cypher report the same edge-type-agnostic degree and stay consistent.

Add regression tests covering: a parent class with N INHERITS children,
combined CALLS+INHERITS degree without double counting, min_degree now
including inheritance-only classes, and isolated nodes still reporting
zero.

Signed-off-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
2026-06-23 04:08:36 -07:00
c8f9f94e08 fix(search): ignore empty-string label in search_graph name_pattern path (#483)
* fix(search): ignore empty-string label in search_graph name_pattern path

search_where_basic guarded the label filter on NULL only (`if (params->label)`),
so a search passing label="" appended a literal `n.label = ''` clause that
matches no node — silently returning zero results. The BM25 query path already
treats an empty label as absent, so name_pattern/qn_pattern searches and BM25
searches behaved inconsistently for the same `"label":""` payload (issue #481):
agents fell back to grep because structural class/service discovery returned
nothing.

Guard the clause on `params->label && params->label[0]` so an empty-string
label is a no-op, identical to an omitted label and to the BM25 path.

Adds store_search_empty_label_ignored: name_pattern + label="" returns the same
match as name_pattern alone, while a non-empty label still filters.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

* test(search): address QA round 1 — cover qn_pattern + empty label

QA round 1 noted store_search_empty_label_ignored asserted only the
name_pattern path though the comment referenced qn_pattern too. Add a
qn_pattern + label="" assertion so a future change that special-cased the
qn_pattern branch can't silently reintroduce the empty-label bug. Both paths
share search_where_basic, so this locks in the shared guard.

Test-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>

---------

Signed-off-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Shane McCarron <shane.mccarron@corvexconnect.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:04:19 +02:00
Martin Vogel 08115fdfb5 get_architecture: skip JSON property-key discovery (counts-only schema)
handle_get_architecture called cbm_store_get_schema, whose per-label/per-type property-key discovery runs json_each() over EVERY node and edge row (~10s of the 23s on a 4.8M-node kernel graph) — yet the handler only renders label/type counts. Added cbm_store_get_schema_counts (same impl, with_props=false) and switched the handler to it. The full get_graph_schema tool is unchanged. A partial expression index on is_entry_point was tried and reverted: json_extract in an index WHERE aborts store open on rows with malformed properties JSON (pre-existing in older DBs; see next commit).
2026-06-09 22:59:32 +02:00