Commit Graph
2796 Commits
Author SHA1 Message Date
Martin Vogel 7e47043acc fix(tests): make the lock-registry absolute-deadline fixture deterministic
lock_registry_absolute_deadline_survives_repeated_wakes has failed on the
test-unix (macos-15-intel) leg of three unrelated pull requests in one
week -- #1342 (08-28), #1811 (09-02) and #1819 (09-03, run 33799475629,
job 100823626965) -- always with the same signature:

  FAIL tests/test_lock_registry.c:1153: ASSERT(tail_queued)
  7648 passed / 1 failed

The registry is not racy. cbm_lock_registry_acquire enqueues the waiter
synchronously under the registry mutex before any wait, so waiter_count
and the attempting count are exact. The defect is in the fixture: it
raced two wall-clock windows against each other, both anchored to a
timestamp the observer thread took before the tail thread had even been
scheduled.

  deadline_start = cbm_now_ms();
  tail.deadline_ms = deadline_start + 200;  /* the tail's acquire deadline */
  queue_deadline   = deadline_start + 100;  /* the observer's budget */

Because both windows start before the tail runs, a loaded runner breaks
the fixture two different ways:

  * the tail is scheduled inside its deadline but after the observer's
    100 ms budget has expired -- the queued state existed and was simply
    no longer being looked at; or
  * the tail is scheduled more than 200 ms late, in which case its
    deadline has already passed when it finally calls acquire, the
    pre-registration deadline check in lock_registry_acquire_internal
    returns BUSY immediately, and the tail never enqueues at all -- the
    asserted state can then never occur, however long the observer waits.

lock_registry is in the parallel wave of run-tests-parallel.sh, not the
serial tail, so on a small CI runner it competes with a full wave of
sanitized suites -- exactly the scheduling delay both paths need.

Widening the observer's budget would only paper over the first path, and
the state it waits for is transient by construction: it exists only
between the tail's enqueue and the tail's own deadline. So the fixture is
rebuilt to observe states that cannot evaporate.

  * The tail anchors its absolute deadline itself, in its own thread,
    immediately before the acquire it bounds. Scheduling delay can no
    longer consume the deadline before the call starts, so the enqueue is
    unconditional, and elapsed is measured from the tail's own anchor --
    it now times the registry instead of timing the scheduler.

  * Enqueue is exposed as a monotonic counter,
    cbm_lock_registry_waiter_enqueue_count_for_test, next to the existing
    test_condition_wait_calls counter it is modelled on. Because the
    count only ever grows, the observer reads it once after the tail has
    returned rather than trying to catch a live queue depth: the polling
    loop, and with it the window, is gone.

  * The fixture's remaining 500 ms and 600 ms budgets become the file's
    LOCK_REGISTRY_TEST_TIMEOUT_MS backstop, and each loop exits on the
    state it waits for instead of on the clock, so the backstop only
    fires when the product is actually broken.

The contract is unchanged: the tail must still return at its absolute
deadline (150 <= elapsed < 350 ms for a 200 ms deadline) despite ~40
unrelated cancel broadcasts, still with BUSY and no lease, with the head
still holding the attempt. The broadcast loop now starts immediately
after the tail is released, so it overlaps the tail's wait at least as
much as it did before.

Verification, all on macOS arm64 with the sanitized runner:

  * The mechanism was reproduced locally by delaying only the tail thread
    after its start gate, with production untouched. A 120 ms delay (the
    state exists, outside the observer's budget) and a 250 ms delay (the
    tail never enqueues) each produced exactly one failure,
    ASSERT(tail_queued) -- the CI signature.
  * After the rebuild the same injections are green at 120, 250, 400 and
    900 ms: an arbitrary scheduling delay no longer decides the verdict.
  * lock_registry 30/30 green plain and 30/30 green under four CPU hogs;
    16/16 inside the saturated 18-job parallel wave.
  * private_file_lock, lock_registry, daemon, project_lock and the
    daemon_* suites: 222 passed, 1 skipped.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 15:42:15 +02:00
Martin Vogel ffb5ebe9e8 Merge pull request #1721 from henry-hz/upstream-pr/elixir-call-arguments
fix(elixir): populate first_string_arg for Elixir calls
2026-09-04 15:38:50 +02:00
Martin Vogel 8e1de0403e Merge pull request #1709 from benrfairless/fix/ruby-superclass-base-classes
fix(extraction): keep Ruby superclass names bare
2026-09-04 15:36:08 +02:00
Martin Vogel 13b7d06701 Merge pull request #2047 from DeusData/fix/2046-cohort-conflict-retry-until-deadline
fix(daemon): retry a mismatched cohort holder until the caller's deadline
2026-09-04 15:35:39 +02:00
Martin Vogel 184a00a442 Merge pull request #2048 from DeusData/ci/msan-image-network-retry
ci(msan): retry transient network failures while building the MSan image
2026-09-04 15:35:23 +02:00
Martin Vogel f404dfbc7a ci(msan): retry transient network failures while building the MSan image
The test-msan lane dies whenever the buildx layer cache misses and the
builder's resolver hiccups. PR-CI runs 33816056229 and 33818108315 both
failed in "Build MSan image (cached layers)" with:

    #7 26.77   Could not resolve 'apt.llvm.org'
    #7 27.34 W: Failed to fetch http://apt.llvm.org/noble/dists/\
        llvm-toolchain-noble-22/InRelease  Could not resolve 'apt.llvm.org'
    ERROR: failed to build: failed to solve: process "/bin/sh -c apt-get \
        update && ..." did not complete successfully

The wget of the signing key from that SAME host had succeeded seconds
earlier in the same layer, so this is a transient DNS failure inside
buildkit, not a wrong URL or a dead mirror. 2 of the last 15 PR runs.

Every network-touching step in Dockerfile.msan now runs under a small
POSIX-sh helper written into the image before the first layer that needs
it: /usr/local/bin/retry makes 5 attempts with growing backoff
(5s/10s/20s/40s) and still exits non-zero once they are exhausted, so a
genuine breakage keeps failing the build instead of being swallowed.
Wrapped: both apt-get update/install pairs (also given
-o Acquire::Retries=3), the wget of the LLVM signing key, and the
llvm-project and zlib clones. Two shapes needed care:

- the key is fetched with `wget -O <file>` instead of `-qO- > <file>`,
  because a redirect is opened once for all attempts and a partial write
  from a failed attempt would be prepended to a later successful one;
- each clone clears its destination first, or a half-finished clone makes
  every later attempt fail on "destination path already exists".

The pinned base digest, the pinned llvmorg-22.1.0 / v1.3.1 tags,
--no-install-recommends and the layer structure are unchanged; on a
healthy network the image is byte-for-byte the same work as before.

Verified on Colima:
- `docker buildx build --check -f test-infrastructure/Dockerfile.msan
  test-infrastructure/` -> "Check complete, no warnings found."
- scratch image = FROM + helper + the real apt.llvm.org layer, built
  --no-cache: layer completes, clang-22/llvm-22 installed.
- scratch image where the wrapped command fails twice then succeeds:
  attempts 1 and 2 logged with 5s/10s backoff, attempt 3 succeeds,
  build exit 0.
- scratch image where the wrapped command always fails: 5 attempts,
  "retry: giving up after 5 attempts", retry_exit=1, build exit 1.

The full MSan image was deliberately not built locally (it compiles
libc++ with MSan); CI builds it on the branch.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 12:42:50 +02:00
Martin Vogel 8f64c36d85 Merge pull request #1918 from CaptainMittens/fix/return-star-respects-with-scope
fix(cypher): RETURN * must read the live scope, not the query pattern
2026-09-04 12:27:44 +02:00
Martin Vogel 54a434743e Merge pull request #1880 from CaptainMittens/fix/env-int-parse-strict
fix(env): stop an unreadable environment setting from picking a wrong default
2026-09-04 12:27:18 +02:00
Martin Vogel b95b33af86 Merge pull request #2029 from DeusData/ci/smoke-8al-node-pi-host-modules
smoke(8al-node): load the Pi extension with the modules the Pi host provides
2026-09-04 12:26:40 +02:00
Martin Vogel c803525258 Merge pull request #2028 from DeusData/ci/pin-llvm22-lsan-macos
ci(lsan-macos): pin the Homebrew LLVM toolchain to llvm@22
2026-09-04 12:26:09 +02:00
Martin Vogel 44fa3aeb21 Merge pull request #2026 from DeusData/fix/windows-guard-daemon-isolation
test(windows): isolate the daemon guards' rendezvous and stop hiding their failures
2026-09-04 12:25:34 +02:00
Martin Vogel dc703dad4a Merge branch 'main' into upstream-pr/elixir-call-arguments 2026-09-04 09:49:54 +02:00
Martin Vogel 2efb7a6323 Merge branch 'main' into fix/ruby-superclass-base-classes 2026-09-04 09:49:50 +02:00
Martin Vogel 3bbabf187c Merge pull request #1811 from pcristin/fix/issue-1682-dotted-ts-imports
fix(pipeline): preserve dotted TypeScript import basenames
2026-09-04 09:45:57 +02:00
Martin Vogel 9ed01a478c Merge pull request #2024 from DeusData/feat/cshtml-razor-pages
feat(extract): discover Razor Pages / MVC views (.cshtml) and route their @page
2026-09-04 09:45:53 +02:00
Martin Vogel 91a1723175 Merge pull request #1897 from CaptainMittens/fix/resolve-receiver-chain-mismatch
fix(registry): refuse a name-only match the receiver chain contradicts
2026-09-04 09:45:49 +02:00
Martin Vogel 9d0c6a1f1a Merge pull request #1881 from CaptainMittens/fix/span-duration-unreadable-timestamps
fix(traces): stop an unreadable span timestamp from becoming a real duration
2026-09-04 09:45:45 +02:00
Martin Vogel 9104feb67f fix(daemon): retry a mismatched cohort holder until the caller's deadline
A participant admitted to the version cohort holds the cohort lifetime lock
SH until its lease is released at exit. An internal daemon that has just
lost its last stdio client keeps that lock through its teardown, so a local
CLI (`config get`, `index_status`, ...) arriving in that few-hundred-ms
window met a holder whose cache root differed and was refused immediately
with "active account daemon uses a different cache directory" — a pure
lifecycle race, previously masked by the slow scalar self-hash and exposed
as soon as the hash got fast (#1767 pr-smoke on macOS).

cbm_version_cohort_acquire now retries a CONFLICT until the caller's finite
deadline_ms, holding no guard between attempts, exactly as host.c already
waits out the same handoff for the daemon claim marker. Every production
caller passes a finite deadline; UINT64_MAX keeps failing fast so a genuine
conflict against a long-lived peer is never waited on indefinitely.

Tests (deterministic, no timing assertions on transient windows): one proves
the retry runs until the deadline and a UINT64_MAX caller still fails
immediately; one proves a waiter that met the mismatched holder is admitted
the moment the holder releases. Both fail with the retry removed.

Fixes #2046

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-04 09:41:30 +02:00
Martin Vogel 2f9828d6de Merge pull request #1697 from angusgastle/docs/team-artifact-lfs-guidance
docs: recommend Git LFS for team-shared graph artifact
2026-09-03 22:09:06 +02:00
Martin Vogel f47cd47afe Merge branch 'main' into fix/issue-1682-dotted-ts-imports 2026-09-03 21:58:07 +02:00
Martin Vogel 380b1c5af1 Merge branch 'main' into feat/cshtml-razor-pages 2026-09-03 21:58:04 +02:00
Martin Vogel 429141261d Merge branch 'main' into fix/resolve-receiver-chain-mismatch 2026-09-03 21:57:58 +02:00
Martin Vogel 4d4af28672 Merge branch 'main' into fix/span-duration-unreadable-timestamps 2026-09-03 21:57:55 +02:00
Martin Vogel e63292a8d4 Merge branch 'main' into fix/env-int-parse-strict 2026-09-03 21:57:52 +02:00
Martin Vogel 74700067c4 Merge branch 'main' into fix/return-star-respects-with-scope 2026-09-03 21:57:49 +02:00
Martin Vogel 951e02e2f9 Merge pull request #2027 from DeusData/fix/posix-probe-counter-unused-but-set
fix(cli): drop the POSIX probe-counter store that Clang 23 rejects
2026-09-03 21:47:36 +02:00
Martin Vogel be66a2d2c6 smoke(8al-node): load the Pi extension with the modules the Pi host provides
The 8al-node probe imports the generated Pi extension in bare Node with an
empty node_modules. That was a sound stand-in for the Pi host only while
the extension imported nothing but node: builtins; the first package
import (`typebox`, Pi's schema library for tool parameters — a real Pi
dependency, verified against pi-coding-agent 0.84.4 on #1809) can never
resolve there, so the probe fails on a correct extension and would pass a
wrong one just as readily.

Give the probe what the host gives the extension: a Proxy-backed
`typebox` stub in the probe's node_modules (any Type.X(...) returns a
descriptor, so the probe never tracks which helpers the emitter uses — it
only exercises the tool lifecycle), plus an explicit allowlist: every
import specifier in the generated file must be a node: builtin or a
package on PI_HOST_PACKAGES, otherwise the probe fails naming the
specifier. That turns the guarantee the empty node_modules gave by
accident (no surprise dependency) into an asserted contract.

Verified locally by running only the 8al-node block against extensions
generated by two binaries:
  main@5802ccdd extension, old probe: OK; new probe: OK
  #1809 extension, old probe: ERR_MODULE_NOT_FOUND typebox (the CI red)
  #1809 extension, new probe: OK
  #1809 extension + an injected 'lodash' import, new probe:
    FAIL 8al-node: ... packages the Pi host does not provide: lodash

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-03 16:16:51 +02:00
Martin Vogel 1d38be3331 ci(lsan-macos): pin the Homebrew LLVM toolchain to llvm@22
The test-lsan-macos leg installs the unversioned `llvm` formula, which
tracks Homebrew's current stable. That moved from 22.1.8 to 23.1.0 today,
so the leg's compiler now depends on which bottle the runner image draws:
run 33745916176 (PR #1811) poured 23.1.0 and died at compile on a new
Clang 23 diagnostic (-Wunused-but-set-global under -Werror); the run an
hour earlier poured 22.1.8 and built fine. Same code, two verdicts.

Pin the formula (and the `brew --prefix` lookup) to llvm@22 — the version
every green run of this leg has used so far. Moving to a newer major
becomes a deliberate edit of these two lines instead of an accident of
the runner image.

The diagnostic itself is fixed independently in #2027 so main compiles
under both majors; this change is about determinism of the leg, not the
warning.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-03 16:07:42 +02:00
Martin Vogel 0ac290e2e2 fix(cli): drop the POSIX probe-counter store that Clang 23 rejects
`g_mcp_command_path_probe_counter` is only ever read inside
`cbm_json_mcp_probe_command_path`, which is compiled for Windows alone.
On POSIX test builds the setter stored a pointer nothing read, and Clang
23 (Homebrew LLVM 23.1.0, which `brew install llvm` now pours on the
macOS LSan leg) flags that as `-Wunused-but-set-global`; with `-Werror`
the whole test-runner build dies at `src/cli/cli.c:1768:21`.

Keep the counter where a probe can happen (Windows) and make the POSIX
setter an explicit no-op with the reason spelled out. The three tests
that install a counter keep asserting it stays at zero on every platform,
which on POSIX is exactly what "no classifier" means.

Verified in the failing arena: `origin/main` `src/cli/cli.c` fails under
clang 23.1.1 with the CI diagnostic; with this change it compiles clean,
as do all 105 built `src` TUs and all 155 test + extraction TUs under the
same flags. `cli` suite: 292 passed with Homebrew LLVM 22.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-03 15:48:59 +02:00
Martin Vogel 5802ccdd77 Merge pull request #1896 from CaptainMittens/fix/swift-call-arguments
fix(extraction): reach Swift call arguments through call_suffix
2026-09-03 15:12:27 +02:00
Martin Vogel 73f4a432e4 test(windows): isolate the daemon guards' rendezvous and stop hiding their failures
Every Windows guard shared ONE daemon. test_daemon_stability.py said each
section "runs under its OWN cache directory: daemon coordination is
cache-scoped" -- it is not. The rendezvous key is FNV-1a over a compile-time
domain string (service.c: cbm_daemon_rendezvous_key) under a machine-global
parent (win_default_runtime_parent -> SHGetFolderPathW(CSIDL_LOCAL_APPDATA),
which ignores the LOCALAPPDATA variable). So the guard's own `daemon stop`
and kill -9 sections, the sibling guards, and anything else on the runner
all met the same daemon, and a daemon wedged anywhere failed every check
after it. That is the shape of this week's test-windows-guards red on
#1874: lifecycle's `daemon start` connected, the UI-config handshake never
answered, and stability / hook_augment / ui_drive / cli_non_ascii went red
in a row with an orphan daemon killed at cleanup -- on a diff and a base that
were each green elsewhere.

CBM_RUNTIME_DIR is the only relocation hook (bootstrap.c, resolved in
cbm_daemon_bootstrap_endpoint_new for every product endpoint). Each stability
section now gets a private runtime parent next to its cache, created before
the binary runs -- the endpoint validates the parent and never creates it --
and the daemon inherits it from the client that spawns it. Proven on macOS:
a daemon on the default rendezvous keeps its pid across the whole guard
(three stops and a kill -9 inside it).

The reporting was hiding the cause as well:
* every SETUP FAIL printed nothing at all, and every RED printed a 300-400
  character HEAD slice, which is the allocator preamble, never the error.
  Excerpts now show the tail (1500 chars); setup paths print the daemon's
  full output.
* a subprocess timeout raised on a parallel-client worker thread was lost
  and the client reported as "rc=none" with no output; it is now kept as a
  failed result with its partial output. A section-level timeout is a
  named RED with the command and output, not a bare traceback.
* the MCP session's stderr went to DEVNULL; it now goes to a file that is
  printed when initialize never answers -- and that case is a RED against a
  running daemon, not a "setup" skip.
* exit 2 was documented and never produced. Only `daemon start` failing
  raises it now (test_daemon_lifecycle.py owns that surface as a
  regression, so the board loses nothing); main() runs every section and
  names all of the failing ones.
* test_hook_augment.py / test_cli_non_ascii_arg.py print full stdout AND
  stderr on their index-setup failures instead of 300 chars of stdout.

On POSIX (local iteration only) the namespace lives under /tmp: sun_path
caps the socket at 104 bytes on macOS and TMPDIR there is ~50 bytes deep.

Out of scope, noted: test_daemon_lifecycle.py, test_non_ascii_path.py and
test_ui_drive_listing.py still use the machine-global rendezvous; a too-long
CBM_RUNTIME_DIR on POSIX is refused (ipc.c sun_path check) with no
validation detail.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-03 14:56:42 +02:00
Pcristin b64e217000 Merge branch 'main' into fix/issue-1682-dotted-ts-imports 2026-09-03 14:52:02 +03:00
Martin Vogel bac8edc58d Merge pull request #2019 from CaptainMittens/test/cli-registry-plan-machine-independent
test(cli): stop the registry plan check depending on the developer's machine
2026-09-03 13:51:07 +02:00
Pcristin bbecc9b9e9 Merge branch 'main' into fix/issue-1682-dotted-ts-imports 2026-09-03 13:44:15 +03:00
Martin Vogel f37a47eb01 Merge pull request #1871 from LazyXuan/fix/query-tools-readonly-annotations
fix(mcp): non-mutating query-only store resolve; annotate the ten read-only tools
2026-09-03 11:46:06 +02:00
Martin Vogel 1770d68a2c feat(extract): discover Razor Pages / MVC views (.cshtml) and route their @page
Follow-up to #1824, which mapped Blazor `.razor` to the C# host and extracted
`@page` routes from it. `.cshtml` is the same Razor syntax with the same C#
host and was equally unmapped: an ASP.NET Core MVC or Razor Pages application
produced no nodes at all for its entire view tree.

`@page` matters more here than it does for components. On a Blazor component
the directive is an optional annotation; in Razor Pages it is the directive
that *defines* a page, so the routable surface of a Razor Pages app lives
exclusively in this file type.

That is why the extension mapping alone is not enough. `cbm_path_is_razor()`
hardcoded a single `.razor` suffix, so a `.cshtml` file would have been
discovered and parsed but skipped by the directive scan -- present in the
graph, with none of its routes. Widened to a suffix table covering both.

Scope is deliberately the Razor family and nothing else:

  - `.vbhtml` is excluded because there is no VB.NET language in this repo,
    so there is no host to map it to.
  - `.aspx` / `.ascx` are excluded because Web Forms is a different templating
    syntax (`<%@ %>`, `runat="server"`) with no `@page` directive, so neither
    the C# error recovery nor the directive scan applies.

Best-effort on the same terms as `.razor`: the C# grammar recovers the `@{ }`
and `@functions` blocks, and the surrounding markup lands in ERROR regions
reported through parse_partial.

Tests: the extension mapping; a Razor Page asserting the extracted route path
and method; and a negative case for a layout with no `@page`, since most
`.cshtml` files are layouts and partials and a spurious Route node per view
would be worse than the missing ones this fixes.

Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
2026-09-03 11:42:52 +02:00
Pcristin e79a7d73b4 Merge branch 'main' into fix/issue-1682-dotted-ts-imports 2026-09-03 12:36:08 +03:00
Martin Vogel 6cca768816 Merge pull request #1773 from rarepops/fix/dockerfile-trailing-blank-eof-1746
fix(extraction): treat trailing blanks at EOF as an absent newline, not a miss (#1746)
2026-09-03 11:26:47 +02:00
Pcristin da09b73bc0 Merge branch 'main' into fix/issue-1682-dotted-ts-imports 2026-09-03 11:20:48 +03:00
Martin Vogel cc3e2638da Merge pull request #1912 from DeusData/feat/python-bare-local-binding
fix(python): suppress bare calls shadowed by an enclosing parameter
2026-09-03 09:39:46 +02:00
周文瑄 adb49b3a28 test(mcp): snapshot the query-branch DB state before suite teardown
CI's tsan and lsan-macos runs failed the mutation-guard suite on
ASSERT(cbm_file_exists(db_path)) at the guard-balance test, while the
behavior suite passed everywhere. The assertion ran after the test's
own teardown: cleanup_project_db() unlinks the same db path before the
assertion block, and the manage_adr branch above replants and
quarantines that very path, so the late file check observed the
test's cleanup, not the query-only resolve it was meant to pin.
Whether it tripped depended on unlink/visibility timing, which is why
it surfaced only under some schedulers and never in the single-suite
local runs.

Capture cbm_file_exists() into query_db_left_in_place right after the
query branch completes, before anything replants or removes the file,
and assert on that snapshot.

Signed-off-by: 周文瑄 <zhouwx1997@126.com>
2026-09-03 15:33:57 +08:00
周文瑄 240dc8156b style(mcp): keep the upstream pointer-to-array declaration spacing
The local clang-format 15 that produced the previous commit removes the
space in 'char (*mods)[CBM_SZ_128]' (pointer-to-array declaration),
while the CI formatter keeps it; 15 also rewrote those two pre-existing
detect_changes lines even though they were untouched by this PR. Restore
the upstream spelling so the CI format gate passes and the PR diff no
longer touches lines it has no reason to touch.

Signed-off-by: 周文瑄 <zhouwx1997@126.com>
2026-09-03 15:33:57 +08:00
周文瑄 2d8721a603 style(mcp): clang-format the query-only resolve and error builder changes
The CI lint gate flagged 18 clang-format violations in the lines the
previous commit touched (the read-only verdict branch inside
resolve_store_internal, the REQUIRE_STORE macro, and the two call sites
that switched to build_no_store_error_checked). No semantic change:
whitespace and line wrapping only; the mcp suite still passes 202/0/7.

Signed-off-by: 周文瑄 <zhouwx1997@126.com>
2026-09-03 15:33:57 +08:00
周文瑄 93d2ee59ea fix(mcp): non-mutating query-only store resolve; annotate the ten read-only tools
search_graph, query_graph, trace_path, get_code_snippet, get_graph_schema,
get_architecture, search_code, index_status, check_index_coverage and
detect_changes were annotated readOnlyHint=false / destructiveHint=true
because resolve_store()'s corrupt-store recovery quarantines database
files - a mutation. That conservatism locked every plan-mode client out
of the read-only analysis surface (#1100 calls the same tools "eleven
read-only analysis tools"), since spec-compliant plan gates refuse tools
that are not declared read-only.

This closes the gap the annotation comment pointed at. The query-only
resolve now classifies a failed integrity check without mutating: no
mutation lease, no quarantine. A confirmed-corrupt database is reported
(a new STORE_RECOVERY_CORRUPT status; the tool reply names the corruption
and points at index_repository instead of a misleading "project not
found") and left in place for a write-side open to quarantine and
rebuild, exactly as before. Transient verdicts stay retryable. Write-side
opens (index_repository, manage_adr updates) keep the existing blocking
recovery, quarantine, and generation re-check semantics unchanged.

The ten query tools' annotations flip to readOnlyHint=true /
destructiveHint=false, making them callable under plan mode in Claude
Code, Codex, ZCode, and every other client that gates on the hints.

Tests: the annotations expectation table is flipped; the query branch of
the guard-balance and guard-denial tests now assert zero leases taken and
byte-identical files; the ghost-db test (#704) expects the corruption
message; the four write-side recovery tests (generation re-check after
lease wait, unique backup naming, publish failure, published-WAL
completeness) are driven through manage_adr update - the write path where
that recovery now lives.

Signed-off-by: 周文瑄 <zhouwx1997@126.com>
2026-09-03 15:33:57 +08:00
Martin Vogel db2f104a1f Merge pull request #2013 from shafty023/fix/1997-extract-scratch-arena
fix(extract): AST traversal stacks no longer outlive the file they walk
2026-09-03 08:14:16 +02:00
Martin Vogel 0e9ed75991 Merge pull request #2016 from rarepops/feat/ui-server-version
feat(ui): show serving binary version (#1820)
2026-09-03 08:13:57 +02:00
Joshua RichterandClaude Opus 5 ad04339030 test(cli): stop the registry plan check depending on the developer's machine
The test built a fixture with its own HOME and PATH, then asserted that
the whole install plan contained no path with "/plugins/" in it. Agent
detection does not stay inside HOME and PATH: cbm_find_cli also looks in
/usr/local/bin and, on macOS, /opt/homebrew/bin. A developer with OpenCode
installed there has it detected inside the fixture, and OpenCode ships a
real plugin file, so the plan legitimately carried a "/plugins/" path and
the assertion failed. On a machine without OpenCode the same assertion
passed, which is why CI never saw it.

Name the two directories the check actually meant — the fixture's Qoder
and Pi directories, neither of which has a plugin directory, so a planned
path under one would be invented. The check now tests the same thing
regardless of what the developer has installed.

No production behaviour changed. Searching well-known install directories
is deliberate, so that install finds an agent whose command is not on the
current PATH.

Fixes the failure at tests/test_cli.c:6859.
Local cli suite: 293 passed, 0 failed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-02 22:20:54 -04:00
Joshua Richter 2c76563a13 fix(registry): refuse a name-only match the receiver chain contradicts
A dotted callee whose first segment starts upper-case names a type. That
receiver chain is evidence, and the two name-only strategies threw it away:
they matched on the final segment alone, so Foundation's
URLSession.shared.data bound to a project's own PickedFile.data — at 0.75
confidence, with nothing in the graph to show the answer was wrong.

receiver_chain_admits() now requires the candidate's own parent segment to
appear somewhere in that chain. Calendar.utcGregorian.startOfDayUTC still
resolves to AuthDTOs.Calendar.startOfDayUTC, because the project really does
extend Calendar and Calendar is in the chain.

Three shapes pass through untouched:

  - a callee with no separator, which has no chain to judge;
  - a lower-case root, which names a value whose declared type the chain does
    not show (vm.load, http.Get, os.path.join);
  - a name in capitals with underscores, which is a constant holding a value
    rather than a type. Measured: without this carve-out the gate refused
    ISO_4217_URL.lower -> builtins.str.lower, which is correct. JSON and URL
    carry no underscore and stay guarded.

The gate applies only at the two name-only exits of resolve_name_lookup.
import_map, same_module and qualified_suffix already carry real evidence and
are left alone.

Language agnostic by design: the registry holds no language, and every
language that writes receiver chains gains the same protection.

Fixes #1893

Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-02 21:55:31 -04:00
Joshua RichterandClaude Opus 5 57ef0a6d86 fix(traces): stop an unreadable span timestamp from becoming a real duration
strtoll answers 0 for text it cannot read, and cbm_parse_duration handed that
0 straight into its subtraction. The report of this said the result was a
false duration of zero. It is worse than that. An unreadable START time reads
as 0, so the span reports the whole END time as its duration:

  FAIL tests/test_traces.c:340: info.duration_ns == 1050000000,
    expected CBM_DURATION_UNKNOWN == -1

That is 1.05 seconds of measured time for a span whose start was never read.
Nothing downstream can tell that number from a real one.

The fix keeps cbm_parse_duration exactly as it is. It is public, it is
declared in traces.h, and eight tests pin its answers -- 0 for a NULL
argument, 0 for an end at or before the start. None of that moves.

A companion carries the extra answer instead:

  int64_t cbm_parse_duration_checked(const char *start, const char *end, bool *ok);

Both reads go through one small reader that follows src/main.c:1104 -- an end
pointer, errno, and a check that nothing was left over -- plus a refusal of a
leading blank, which strtoll would otherwise step over.

cbm_extract_http_info now uses the companion and writes CBM_DURATION_UNKNOWN
(-1) when the timestamps do not read. It still returns true, because the
method and path on that span are still good data and dropping them would
punish them for a fault they had no part in. -1 as "not recorded" is the
sentinel this codebase already uses for the same question, in
CBM_EDGE_CONF_ABSENT. The duration_ns field comment in traces.h now says so,
so a reader who meets -1 has something to read.

Scope note: cbm_extract_http_info has no production caller on this tree. rg
finds it only in src/traces/traces.c, src/traces/traces.h and tests, and
handle_ingest_traces in src/mcp/mcp.c never touches a timestamp. So this is a
defect in a public, tested function rather than one putting bad rows in a
graph today.

Two tests come with the change. The one that pins the behaviour was seen
failing first -- the FAIL line above is from that run. After the fix,
TEST_SUITES="traces" reports 32 passed, 0 failed, exit 0.

The full suite reports 7633 passed, 2 failed. Both failures are in
tests/test_cli.c (lines 1749 and 6725), print "error: one or more agent
cleanup operations failed", and reproduce on a clean tree without this
change. make -f Makefile.cbm lint-ci passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
2026-09-02 21:55:30 -04:00
Joshua RichterandClaude Opus 5 6b801eec68 fix(env): stop an unreadable environment setting from picking a wrong default
atoi and atol answer 0 for text they cannot read, and 0 is a real setting at
three places in this project. So a typo, a trailing unit such as "30s", or a
stray space silently chose a value nobody asked for, and nothing on screen
said the setting had been dropped.

  src/mcp/index_supervisor.c  CBM_INDEX_WORKER_TIMEOUT_S
  src/cli/hook_augment.c      CBM_HOOK_DEADLINE_MS
  src/mcp/mcp.c               CBM_INDEX_MAX_RESTARTS

CBM_HOOK_DEADLINE_MS was the worst of the three. atoi answered 0, 0 is below
HA_DEADLINE_MIN_MS, and the clamp then handed back 50 ms -- the SHORTEST
deadline the setting allows, for a setting whose only purpose is to give the
hook more room. The comment above that function records a hunt for hook runs
that never finished, 0 of 24 real sessions, which is the exact symptom a
silently-shortened deadline produces.

CBM_INDEX_MAX_RESTARTS lost twice. A typo kept the default of 100, and
CBM_INDEX_MAX_RESTARTS=0 -- which reads as "do not restart" to anybody who
sets it -- also kept 100. The setting did the opposite of the request.

CBM_INDEX_WORKER_TIMEOUT_S fell through to the 15-minute default, so a test
set to give up after 30 seconds hung for 15 minutes with nothing to explain
why.

The fix adds one helper rather than three copies of the same check:

  bool cbm_env_long(const char *name, long *out);

It answers true only when the variable is set, is not empty, and reads
cleanly from its first character to its last. It holds no policy -- no
minimum, no maximum, no default -- because the three sites disagree on all
three, and a helper that guessed would be wrong at two of them. The shape is
the one src/main.c:1104 already uses: an end pointer, errno, and a check that
nothing was left over. It also refuses a leading blank, which strtol would
otherwise step over, so " 5" is a slip rather than the number 5.

Each site keeps its own rule:

  worker timeout    an unreadable value keeps the 15-minute default AND logs
                    the value it dropped
  restart cap       0 now means no restarts; an unreadable value keeps 100
                    AND logs the value it dropped
  hook deadline     an unreadable value now yields HA_DEADLINE_DEFAULT_MS,
                    not the floor. This one stays silent on purpose: the file
                    includes no log header and writes no stderr, because its
                    output is hook protocol.

The restart-cap parse was lifted out of a very large function into a named
index_restart_cap(), so it can be read and reached on its own.

Four tests come with the change. The two that pin the user-visible behaviour
were seen failing before the fix:

  FAIL tests/test_cli.c:402: ms == 50, expected HOOK_DEADLINE_DEFAULT == 2000
    (with "unreadable value \"abc\" gave 50 ms" printed above it)
  FAIL tests/test_cli.c:434: cbm_index_restart_cap_for_testing() == 100,
    expected 0 == 0

After the fix, TEST_SUITES="platform cli mcp" reports 518 passed, 2 failed.
The full suite reports 7635 passed, 2 failed. Both failures are in
tests/test_cli.c (lines 1826 and 6802), print "error: one or more agent
cleanup operations failed", and reproduce on a clean tree without this
change -- they depend on the coding agents installed on the machine.
make -f Makefile.cbm lint-ci passes.

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