Commit Graph
2769 Commits
Author SHA1 Message Date
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 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 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 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 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 Richter c4e902849f fix(extraction): reach Swift call arguments through call_suffix
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>
2026-09-02 20:26:17 -04:00
Martin Vogel c38aa35c27 Merge pull request #1777 from ocochard/feature/freebsd-pkg-managed-detection
fix(freebsd): treat ${LOCALBASE}/bin as pkg-managed; resolve self via sysctl
2026-09-03 00:07:06 +02:00
Martin Vogel 192f29609a Merge pull request #2006 from ilyabrykau-orca/fix/go-field-ref-member-signal
fix(pipeline): key the Go Field guard on the recorded selector shape, not reference text
2026-09-02 23:59:55 +02:00
Martin Vogel 01bf5f23e3 Merge pull request #2009 from CaptainMittens/fix/route-handler-last-argument
fix(routes): take a route's handler from the last argument, not the first
2026-09-02 23:53:32 +02:00
Martin Vogel e6a34fcea0 Merge pull request #1824 from The1nk/feat/blazor-razor-routes
feat(extract): discover Blazor .razor components and emit Route nodes for @page
2026-09-02 23:48:14 +02:00
Martin Vogel 2a0e9b9b80 Merge pull request #1876 from ertankucukoglu/fix/windows-unicode-session-context
Fix Unicode daemon session roots on Windows
2026-09-02 23:06:32 +02:00
Martin Vogel 7910de591b Merge pull request #1877 from CaptainMittens/fix/confidence-parse-sentinel
fix(confidence): stop an unreadable confidence from reading as 0.0
2026-09-02 23:04:20 +02:00
Martin Vogel 4bae9bd67f Merge pull request #1875 from CaptainMittens/fix/cypher-reject-trailing-tokens
fix(cypher): refuse a query the parser did not fully read
2026-09-02 23:04:04 +02:00
Martin Vogel 9639803175 Merge pull request #1836 from ertankucukoglu/perf/search-code-scope-suffix
perf(mcp): filter suffix globs while building search scope
2026-09-02 23:03:46 +02:00
Martin Vogel 620613ede6 Merge pull request #1986 from CaptainMittens/fix/swift-scanner-try-bang-shift
fix(grammar): stop the Swift try-bang entry shifting past its own type
2026-09-02 22:51:46 +02:00
Martin Vogel aefd2a1838 Merge pull request #2008 from CaptainMittens/fix/decorator-arg-scan-limit
fix(routes): find a route path wherever it sits in a decorator's arguments
2026-09-02 22:19:03 +02:00
Rares Popa e0785b353a feat(ui): show serving binary version
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>
2026-09-02 20:50:31 +02:00
Martin Vogel 4d8ac9e1ce Merge pull request #1802 from rarepops/fix/install-clients-registry-1798
fix(cli): apply client selector to registry integrations (#1798)
2026-09-02 19:22:45 +02:00
Martin Vogel c895513f2d Merge pull request #1800 from kilianpaquier/fix/clients-cli-help
fix(help): add --clients to CLI help
2026-09-02 19:22:20 +02:00
Martin Vogel 1424934980 Merge pull request #1799 from rarepops/fix/nanosleep-eintr-1616
fix(foundation): resume spawn backoff after EINTR (#1616)
2026-09-02 19:12:10 +02:00
Pcristin 3f08fac513 Merge branch 'main' into fix/issue-1682-dotted-ts-imports 2026-09-02 20:04:12 +03:00
Martin Vogel b44e58490f Merge pull request #1779 from rarepops/fix/autoindex-skip-limit-value-1466
fix(mcp): log the numeric auto_index_limit in autoindex.skip (#1466)
2026-09-02 19:01:10 +02:00
Rares Popa 572725e61a fix(extraction): treat trailing blanks at EOF as an absent newline, not a miss
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>
2026-09-02 18:42:40 +02:00