`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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
handle_calls reads a call's arguments through one tree-sitter field
lookup, ts_node_child_by_field_name(node, "arguments"). The vendored
Swift grammar declares no "arguments" field at all -- its own
ts_field_names[] table has zero occurrences, against one in Go and two
in TypeScript. Swift models a call as a target expression plus a
call_suffix, and the arguments hang off the suffix as value_arguments.
So args was null for every Swift call ever parsed, first_string_arg was
never populated, and no Swift HTTP call could raise an HTTP_CALLS edge
or a Route node. Alamofire, Moya and URLSession have been in the
service-pattern table the whole time and match the callee text
correctly; the URL simply never arrived.
Three changes, all in extract_calls.c:
- swift_call_args() reaches the argument list through call_suffix, in
the same shape as the existing objectscript_call_args() fallback and
used from the same place. A trailing closure has no value_arguments,
so it returns a null node and that call behaves as before.
- extract_url_or_topic_arg() unwraps Swift's per-argument
value_argument node, stepping past a leading value_argument_label.
Without it, dataTask(with: "/api/v1/widgets") yields the label "with"
rather than the path. PHP and C# already had the same unwrap for
their own "argument" node.
- is_string_like() gains line_string_literal, which is what Swift calls
an ordinary "..." literal. The list already held raw_string_literal,
so only Swift's common case was missing.
This is the layer underneath #1892 rather than the whole of it. A
literal URL argument now arrives; a literal nested inside a
constructor, as in URLSession.shared.data(from: URL(string: "...")!),
still does not, because extract_positional_url reads a literal, a
template string, a concatenation or a named constant and that shape is
none of them.
Reproduce-first: all three tests fail without the fix, two on a null
first_string_arg and one on HTTP_CALLS being 0.
Fixes#1892
Signed-off-by: Joshua Richter <jrichter5781@gmail.com>
Expose the compiled server version through the existing UI config endpoint and render it beside the product name. Keep older or unavailable config responses compatible by hiding the label, with focused frontend and native endpoint coverage.
Fixes#1820
Signed-off-by: Rares Popa <2606875+rarepops@users.noreply.github.com>
A file whose final line lacks its newline leaves the grammar's mandatory
terminator MISSING at EOF. #1610 suppressed that phantom parse_partial on the
grounds that the node is ZERO-WIDTH: the parser consumed no source for it, so
by construction nothing was dropped.
That suppression tested `end == source_len` exactly. Trailing blanks are extras
owned by no node, so `ENTRYPOINT ["a"] ` + EOF parks the zero-width terminator
one byte short of source_len and the check missed it. This is why neither a
trailing blank nor an absent final newline flagged on its own -- only the pair
did, exactly as the reporter's byte-exact control matrix showed.
Treat "at EOF" as EOF modulo a trailing blank run. Every blank except newline
qualifies, deliberately not a hand-picked subset: a form feed is no more
content than a space, and #1610 exists precisely because whether a file got
flagged used to hinge on such incidentals. Newline stays excluded because a
terminated final line produces no MISSING terminator at all.
The rule remains ZERO-WIDTH ONLY. A width-bearing MISSING/ERROR at EOF is a
genuine loss and is still flagged (a Makefile's unterminated final recipe
really does vanish), as is any failure earlier in the file; both are pinned by
guard tests alongside the existing #1610 guards.
Verified end to end: the issue's own `cli index_repository` reproduction goes
from parse_partial_count 1 to 0 with node count unchanged, so the phantom flag
is gone without anything dropping out of the graph.
Fixes#1746
Signed-off-by: Rares Popa <2606875+rarepops@users.noreply.github.com>