Commit graph

2 commits

Author SHA1 Message Date
Gergő Magyar
698f5efc82
feat(group): resolve inline HTTP provider handlers via call-site line (#2276) (#2282)
Some checks are pending
CodeQL / Analyze (javascript-typescript) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
Gitleaks / gitleaks (push) Waiting to run
Publish / Classify release event (push) Waiting to run
Publish / RC guard (marker + release-PR skip) (push) Blocked by required conditions
Publish / ci (push) Blocked by required conditions
Publish / Publish to npm (push) Blocked by required conditions
Publish / Build & Push RC Docker images (push) Blocked by required conditions
Scorecard / Scorecard analysis (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-cli) (push) Waiting to run
Trivy Image Scan / Trivy (gitnexus-web) (push) Waiting to run
* feat(group): resolve Go inline provider handlers via line containment (#2276)

Widen the Go HandleFunc + framework-route handler capture to match
func literals and emit name:null + call-site line for them, so an
inline handler resolves to its containing/closure symbol instead of
file-level. Named identifier handlers keep resolving by name.

* feat(group): resolve Laravel closure provider handlers via line containment (#2276)

Capture the Laravel route handler argument; a closure (anonymous
function or arrow fn) now emits name:null + the registration line so it
resolves to its containing symbol (service-provider boot, controller
method) by containment. Named-controller routes keep the 'route' label.
File-scope closures stay file-level (PHP closures not yet indexed).

* feat(group): wire call-site line on FastAPI provider emits (#2276)

Set line on the FastAPI @app/@router provider detections (already
name:null) so the source-scan fallback resolves the decorated handler
by line-span containment. Best-effort: FastAPI routes are graph-backed
and the function span starts at def, so this lands the single-decorator
case. Flask add_url_rule already carried line.

* feat(group): wire call-site line on Kotlin/Java Spring provider emits (#2276)

Add line to the Kotlin and Java Spring @*Mapping provider detections
for parity with the consumer emits and a future inline DSL. Inert for
current resolution: a named Spring controller method resolves by name
and never falls through to line-span containment.

* fix(review): apply autofix feedback

Pin two documented limitations with tests: a file-scope Laravel closure
and a multi-decorator FastAPI handler both degrade to file-level rather
than mis-attributing (#2276 ce-code-review autofix).

* test(group): lock named gin framework-route resolves by name not registrar (#2276)

Reviewer verified named Go handlers still resolve by name across the
widened queries; the HandleFunc path was already pinned, this adds the
framework-route (gin/echo) path with a DB + enclosing registrar whose
span covers the registration line, proving the emitted line never
diverts a named provider to its registrar via containment.

* test(group): end-to-end inline Go provider resolution against real LadybugDB (#2276)

Closes the validation gap that all prior coverage mocked CONTAINING_QUERY:
runs the real pipeline over a Go file with an inline http.HandleFunc
func-literal handler, persists into a real LadybugDB, and runs the
production HttpRouteExtractor against the real executor — proving the
emitted call-site line lands inside main()'s real 0-based span and yields
source_scan_resolved, not the file-level fallback.

* fix(test): use fs.mkdtemp to satisfy CodeQL insecure-temporary-file gate (#2276)

The new integration test created its temp base via a predictable
os.tmpdir()+name join, which CodeQL flags as js/insecure-temporary-file
(1 high). Switch to fs.mkdtemp for an atomic, randomly-named base dir.

* fix(group): anchor Go provider @handler to the trailing argument (#2276)

The widened framework-route and HandleFunc handler captures
(`[(identifier) (func_literal)] @handler`) were unanchored, so a variadic
middleware route `r.GET("/x", mw, func(){})` produced two provider
detections — one for the middleware identifier and one for the closure.
The contractId-only merge then kept the middleware detection and
mis-attributed the route to it (and the pre-existing `mw, namedHandler`
shape had the same defect), silently neutralizing the inline-handler
containment resolution from #2276.

Add a trailing tree-sitter anchor (`@handler .`) so the handler binds the
LAST argument of the call, leaving middleware args before it unconstrained.
Verified against tree-sitter-go: the multi-arg shapes now yield exactly one
detection (the real handler) while every 2-arg case is unchanged. Adds two
regression tests pinning that a middleware + inline closure resolves to its
containing function and a middleware + named handler resolves by name.

* test(group): cover FastAPI @router inline-handler containment (#2276)

The @router/APIRouter provider emit gained a call-site `line` in #2276 but
only the @app path was tested; the existing @router tests call
`extract(null, …)` so the resolver/containment path never ran for @router.
Add two tests mirroring the @app cases: a single-decorator @router handler
resolves to its function via source_scan_resolved (which fails if `line` is
dropped), and a multi-decorator one degrades to file-level.

* fix(group): treat synthetic 'route' label as anonymous in cross-trace (#2276)

After #2276 an unresolved file-scope Laravel closure emits name:null, so its
persisted symbolName falls back to 'handler' — which providerLabel already
anonymizes to '<contractId handler>'. But an unresolved named-controller
route still carries the synthetic 'route' placeholder, which the sentinel
did NOT cover, so group_trace/group_cross_impact rendered it as the literal
'route' while equivalent closures showed '<... handler>'.

'route' is only ever the synthetic Laravel placeholder (php.ts), never a
resolved handler name, so add it to the unresolved-generic sentinel set
alongside 'handler'/'fetch'. The resolved branch is untouched, so a real
symbol genuinely named 'route' still displays its name. Adds a cross-trace
test pinning the anonymized label.

* fix(group): gate Spring provider line on a present method name (#2276)

The Java/Kotlin Spring @*Mapping provider emits set `line` unconditionally
while the method name is typed string|null. The 'a named provider never
reaches containment' guarantee held only because the grammar always captures
a method name — the type did not enforce it. A (grammar-impossible) null name
would emit name:null + line and resolve by containment to the enclosing class
body instead of staying file-level.

Emit `line` only when the method name is truthy, so a nameless provider
degrades to file-level (the safe no-mis-attribution outcome). Behavior is
unchanged for every real Spring route (name is always present), but the
inertness is now enforced rather than incidental.
2026-06-23 17:51:11 +01:00
Gergő Magyar
1a03c8527a
feat(group): cross-repo call trace using PDG (#2269)
* refactor(group): extract shared resolveBridgeNeighbors from cross-impact

Lift the uid-filtered consumer<->provider ContractLink join (direction +
queryBridge + row normalization + confidence sort) out of runGroupImpact's
inline Phase-2 block into an exported resolveBridgeNeighbors helper. Behavior
is unchanged for impact; the helper becomes the single shared bridge join so
the upcoming cross-repo trace path never forks its own copy of the neighbor
Cypher. Empty uid sets short-circuit without a DB round-trip.

Adds direct coverage (real bridge via writeBridge/openBridgeDbReadOnly) for
both directions plus the empty-set and unknown-uid edges.

* feat(group): cross-repo trace stitching (groupTrace + runGroupTrace)

Add GroupService.groupTrace and the pure runGroupTrace engine that stitches
per-repo CALLS/HAS_METHOD trace segments across one ContractLink boundary in
the group bridge:

  from --(local trace)--> consumer --(ContractLink)--> provider --(local trace)--> to

- Resolves from/to across all members (symbol node id == bridge symbolUid);
  same-repo endpoints delegate to a single local trace with no crossing.
- Single boundary crossing (MAX_SUPPORTED_CROSS_DEPTH); deeper crossDepth is
  clamped with a note, mirroring cross-impact.
- Discriminated GroupTraceResult union (ok|not_found|ambiguous|error) with
  per-hop repo tags, a typed crossings[] entry, and centralized degraded-state
  note constants (TRACE_NOTES). No .
- Trace-specific pair query (keeps BOTH crossing endpoints) lives in this
  module; the uid-filtered neighbor join (resolveBridgeNeighbors) is reused
  where it fits. ensureBridgeReady exported for reuse.
- New GroupToolPort methods (trace/resolveSymbol/pdgFlows) are optional so
  existing port mocks keep type-checking; runGroupTrace guards on presence.

PDG enrichment is wired as an opt-in hook (enrichSegment) — the port method is
stubbed until U4. Covered by unit tests over a real bridge + mocked port.

* feat(group): route trace tool to groupTrace on @group syntax

Wire the cross-repo trace through the existing @group dispatch:
- callTool routes trace with an @-prefixed repo to callToolAtGroupRepo, which
  forwards from/to/uid/file/maxDepth/includeTests plus the experimental
  pdg/crossDepth flags to GroupService.groupTrace. Member path in @group/path
  is advisory for trace (resolution is whole-group).
- Port gains trace/resolveSymbol/pdgFlows adapters. resolveSymbolForGroup wraps
  the shared resolveSymbolCandidates so groupTrace can locate the member repo
  and recover each endpoint node id (== bridge symbolUid). pdgFlowsForGroup is
  a degraded stub here (call-level only); U4 implements the REACHING_DEF walk.
- trace tool schema documents the @group entry point, pdg, and crossDepth.

Single-repo trace is untouched. Covered by dispatch-routing tests (@group ->
groupTrace, non-group stays local) and tool-schema assertions.

* feat(group): opt-in PDG data-flow enrichment for cross-repo trace

Implement _pdgFlowsForGroupImpl: the real REACHING_DEF anchor walk that backs
the port pdgFlows adapter (replacing the U3 call-level stub). When pdg:true and
the segment repo has a flows PDG layer, the boundary-adjacent segments carry
their intra-procedural def->use hops:

- Anchors by the boundary symbol UID (precise; avoids the by-name ambiguity the
  resolveBlockAnchor path can hit), then reuses the same span-anchored,
  bind-param-only flows query as pdg_query (BasicBlock id-prefix + [start+1,
  end+1] line window; no rel-property index, so the anchor IS the bound).
- Stays intra-procedural: data flow never crosses the repo boundary.
- pdgStampForMode probe: false -> available:false (degrade with note); the
  trace stays ok. Any query failure is swallowed (enrichment is auxiliary).

Covered by runGroupTrace enrichment tests: dataFlow attached on opt-in,
degraded note when no layer, and no pdgFlows call when pdg is omitted.

* test(group): evaluation-first cross-repo trace e2e (two real indexes)

End-to-end gate for the cross-repo trace: stands up two real LadybugDB indexes
(consumer 'frontend' + provider 'backend'), a real ContractLink bridge, and a
real LocalBackend with both repos registered, then drives the public
callTool('trace', { repo: '@grp', pdg: true }) and asserts:
  - the stitched checkout -> callUsers -(CONTRACT_LINK)-> handleUsers -> getUsers
    path, each hop tagged with its member repo
  - real REACHING_DEF data-flow enrichment of the consumer segment (userId)
  - a degraded 'No PDG layer in app/backend' note (provider has no PDG layer)
  - single-repo trace against one member is unchanged (no crossings)

Hand-persists the minimal real graph (deterministic; a full two-repo analyze is
heavier than this gate needs) and exercises real Cypher across
resolveSymbolCandidates, _traceImpl, the bridge pair query, and
_pdgFlowsForGroupImpl. Windows-skipped (describeReopen) and registered in the
cross-platform native-lbug set.

Scoped to a single @group call: opening bridge.lbug read-only a SECOND time in
one process currently fails (shared bridge open/close lifecycle, also affects
impact @group) — the pdg-omitted/clamp variants are unit-covered.

* docs(group): document cross-repo trace + PDG enrichment

ARCHITECTURE.md: trace is now group-aware; describe the @group cross-repo
stitch over a single ContractLink boundary (CONTRACT_LINK hop, crossings[],
crossDepth clamp), the opt-in experimental PDG REACHING_DEF enrichment of
boundary-adjacent segments, the symbolUid-grain join between the two stores,
and the deferred full cross-program (SDG-like) data flow. PIPELINE.md: add the
cross-trace consumer of the bridge with its pair-query rationale.

Does not touch gitnexus/CHANGELOG.md (release-owned).

* fix(review): apply autofix feedback

Apply safe_auto findings from ce-code-review (run 20260622-094243):
- local-backend.ts: drop (r: any) in _pdgFlowsForGroupImpl row map; coerce
  hop line via Number() so a nullish LadybugDB cell can't surface NaN.
- tools.ts: advertise the forwarded  param in the trace schema and add
  crossDepth maximum:10 (schema now matches what groupTrace reads).
- cross-trace.ts: parallelize per-member resolveSymbol/resolveRepo with
  order-preserving Promise.all (matches groupContext/groupQuery); add a note
  when pdg:true is passed to a same-repo trace (PDG only enriches at a
  cross-repo boundary).
- tests: remove  / tighten  (no-any rule).

Residual gated_auto/manual findings (unbounded crossing query + loop,
whole-file PDG widening on absent span, error-vs-no_path masking, top-level
try/catch parity, helper dedupe, branch-coverage gaps) are recorded in the run
artifact for the PR body.

* fix(group): skip CHECKPOINT on read-only bridge close so it can reopen

Root cause of the in-process bridge.lbug reopen failure (which broke repeated
@group impact/trace calls in a long-lived MCP server): closeBridgeDb issued
CHECKPOINT on EVERY handle, including read-only ones. A CHECKPOINT on a
read-only connection has nothing to flush but leaves a WAL/shadow lock artifact
that makes the next read-only open of the same path fail (openBridgeDbReadOnly
returns null -> 'Could not open bridge.lbug read-only'). Reproduced: open ->
query -> closeBridgeDb -> open again returned null only when the close ran
CHECKPOINT; a non-checkpoint close reopened fine, and the raw native
open/close cycle was never the problem.

Fix: tag read-only handles (BridgeHandle._readOnly, set by openBridgeDbReadOnly)
and skip CHECKPOINT for them in closeBridgeDb. Writable handles are unchanged
(they still flush before close). This is the shared bridge-db close path, so
impact @group benefits identically.

- Regression test in bridge-db.test.ts: open/query/close/open/query/open in one
  process now succeeds.
- Re-enabled the second @group call in cross-trace-e2e.test.ts (was scoped to a
  single call for this very limitation).

* fix(group): bring bridge-db close to parity with the core adapter safeClose

The bridge open/close cycle was less robust than the main graph DB's: closeBridgeDb
closed the connection/database but skipped the post-close steps the core adapter's
safeClose performs, so a rapid in-process reopen could race the OS handle release
(Windows) or an orphaned WAL sidecar. That gap is why the close-then-reopen tests
had to skip Windows.

closeBridgeDb now mirrors safeClose after closing the handle:
- waitForWindowsHandleRelease(dbPath): probe the file (+ .wal) until the residual
  Windows lock clears, so the next open does not race (warns if the budget is
  exhausted, matching the core adapter).
- finalizeLbugSidecarsAfterClose(dbPath): quarantine an orphaned WAL (shadow
  missing) so the next open replays a consistent file.

Both helpers are the same ones safeClose uses (Windows-proven via the core adapter
CI), and the bridge read open already retries transient locks. Combined with the
read-only CHECKPOINT skip, the bridge reopen is now robust on every platform, so
the close-then-reopen tests run on all platforms (Windows CI exercises them via the
cross-platform subset). No write-path behavior change; Linux/macOS unaffected.

* fix(group): bound cross-repo crossing fan-out (LIMIT + segment memoization)

Address the top review residual: the bridge crossing query was unbounded and the
crossing-selection loop could run an O(2*N) sequential trace-BFS over every
ContractLink between a repo pair.

- CY_CROSSINGS_BETWEEN now ORDERs BY confidence DESC and LIMITs to
  MAX_CROSSINGS_TO_TRY + 1; listCrossingsBetween slices to the cap and reports
  truncation. Exceeding the cap surfaces a note (no silent truncation), keeping
  the highest-confidence crossings. Aligns with the repo's anchored+LIMIT-bounded
  query discipline (LadybugDB has no rel-property index).
- The home-repo segment (from -> consumer) depends only on the consumer uid and
  the target-repo segment (provider -> to) only on the provider uid, so each is
  memoized by that uid. Many crossings sharing a consumer/provider (one client
  call linked to several providers) now cost one trace per distinct endpoint
  instead of one per crossing. A consumer whose segment already failed is skipped
  for every later crossing that shares it.

Test: two links sharing a consumer (first provider unreachable, second reachable)
assert the from->consumer segment is traced exactly once and the second crossing
wins.

* fix(group): restore Windows skip for bridge reopen tests; drop ineffective close-side probe

The previous commit flipped the bridge close-then-reopen tests to run on Windows,
betting that a close-side waitForWindowsHandleRelease + finalizeLbugSidecarsAfterClose
probe (mirroring the core adapter safeClose) would make the in-process reopen work
there. Windows CI proved otherwise: 4 writeBridge->openBridgeDbReadOnly tests fail
('expected null not to be null' — the read open returns null). The writable-close ->
read-open handoff plus writeBridge's atomic sidecar rename does not release the OS
file handle before the read open races, and the existing open-side LBUG_OPEN_RETRY
only retries lock-pattern errors, not the post-rename sidecar database-id mismatch.
macOS passes; the core adapter's own reopen also passes — this is bridge+Windows
specific.

- Revert itLbugReopen to the Windows skip (the pre-existing, correct state).
- Remove the close-side probe + finalize from closeBridgeDb: it did NOT close the
  Windows gap, and reviewers flagged it for hot-path latency (finalize ran on every
  close, all platforms) and safeClose duplication.
- KEEP the load-bearing fix — skipping CHECKPOINT on read-only handles — which fixed
  the reproduced Linux/macOS in-process reopen artifact (the real bug).

Net: Linux/macOS repeated @group impact/trace works in-process; Windows in-process
bridge reopen remains a documented limitation (unchanged from before this PR).

* fix(group): surface degraded members + cap truncation; honest crossDepth schema

Address the cross-engine-corroborated tri-review findings (Codex + Claude):
- resolveAcrossMembers / runGroupTrace now track member repos that could NOT be
  queried (resolveRepo or resolveSymbol threw) and, when the result is not_found,
  attach a degraded-member note. A transient/corrupt member DB is no longer
  silently reported as a clean 'symbol absent' not_found. (Codex B1+B3 + ce-reliability.)
- The cross-repo not_found now carries a programmatic truncated:true flag (and a
  clearer suggestion) when the MAX_CROSSINGS_TO_TRY cap was hit, so a consumer can
  distinguish 'no path' from 'cap may have hidden a connecting ContractLink'.
  (Codex B3 + ce-adversarial + ce-api-contract.)
- trace tool schema: crossDepth maximum 10 -> 1 to match the implementation's
  single-hop clamp (the schema previously advertised an unsupported 2-10 range).
  (ce-api-contract, conf 100.)

Test: a member whose resolveSymbol throws yields not_found WITH a degraded note
naming the unreachable repo (if-free responder map).

* docs(group): clarify trace @group/memberPath is advisory (resolves all members)

Tri-review (Codex ce, conf 100) caught a doc/impl inconsistency: ARCHITECTURE.md
lumped trace with query/context/impact as honoring @group/memberPath member
scoping, but cross-repo trace resolves from/to across ALL members (the member
path is advisory). Clarify the behavior and point to from_uid/to_uid for
disambiguating same-named symbols across members.

* feat(group): file-level boundary fallback so cross-repo trace works on HTTP contracts

Benchmark (bench/cross-repo-trace/) running the REAL pipeline (runFullAnalysis
--pdg -> real syncGroup -> trace @group) found that cross-repo trace returned
not_found for real HTTP links even though sync built the correct ContractLinks:
HTTP (and other source-scan) contracts hardcode symbolUid:'' (http-route-extractor),
and both cross-trace AND cross-impact join crossings by Contract.symbolUid, which
never matches an empty uid. (Pre-existing — impact @group has the same gap.)

Fix: when a crossing's symbolUid is empty, fall back to the contract's FILE — if
the user's from/to resolves into the contract file, that endpoint anchors the
boundary. CY_CROSSINGS_BETWEEN now returns consumer/provider filePath; a crossing
is kept if it can be anchored by uid OR file on each side; a fileBoundaryFallback
note flags that the boundary is file-level, not symbol-precise. This makes the
common 'trace from=<calling fn> to=<handler fn>' case work end-to-end (verified:
fetchUsers -> listUsers stitches with a CONTRACT_LINK hop + PDG enrichment, 2/2).

Limits (documented in the bench README + the note): anonymous handlers have no
named target; when several contracts share files the file fallback may attach the
wrong contractId to a correct path. The proper upstream fix is to populate
symbolUid in the HTTP extraction (benefits impact too) — the bench is its gate.

Adds a unit test pinning the empty-symbolUid file-fallback stitch.

* fix(group): resolve HTTP contract symbolUid by containment (fixes cross-repo trace + impact)

Addresses the root cause behind the cross-repo trace file-fallback: HTTP
contracts hardcoded symbolUid:'' (http-route-extractor), so both cross-trace and
cross-impact — which join crossings on Contract.symbolUid — could not traverse
HTTP links. (Also found: the pre-existing graph-assisted resolution queried the
wrong edge, CONTAINS instead of DEFINES, so it never resolved a uid either.)

Now the extractor resolves each detection to a real symbol:
- HttpDetection carries the call-site line (node.ts sets it on every express/
  fetch/axios/jquery/nest detection; express also captures the handler arg).
- resolveDetectionSymbol resolves the named handler first, else the innermost
  Function/Method whose line span encloses the call (consumer = the function
  containing the fetch; provider = the named/inline handler), over the correct
  File-[DEFINES]->symbol edge. Base-tolerant (0- vs 1-based startLine).
- Wired into both source-scan and graph-assisted provider/consumer paths.

Verified end-to-end (bench/cross-repo-trace): all 4 contracts now carry real
uids, trace is symbol-precise (GET pair -> http::GET, POST -> http::POST, no
file-fallback note), and impact @group fans out (cross_repo_hits 0 -> 1). The
cross-trace file-level fallback remains as the secondary path for truly
anonymous handlers. Adds 2 containment unit tests; 738 group/integration pass.

Languages other than JS/TS still resolve providers by handler name; their
consumers fall through to the file fallback until their plugins set the line.

* fix(group): extend HTTP symbolUid containment to all languages + nested methods

Completes the symbolUid resolution across every bundled HTTP plugin: Python, Go,
PHP, Kotlin and Java now set the call-site line on their consumer (and Feign/
named) detections, so their HTTP contracts resolve to the containing function
the same way Node/TS already did.

Also generalizes the containment query: it now matches Function/Method/CodeElement
by filePath (UNION ALL) instead of File-[DEFINES]->symbol. The DEFINES edge only
reaches a file's TOP-LEVEL symbols, so methods nested in classes (Java/Kotlin —
File defines the class, the class defines the method) were invisible; matching by
filePath reaches them. Verified against a real index (LadybugDB supports the
UNION); JS/TS still fully symbol-precise (bench 2/2), 709 group tests pass.

Residual is now only the inherent case — a fully anonymous handler with no named
callee — which keeps the cross-trace file-level fallback.

* feat(group): destination trace — follow a consumer to an anonymous handler

Handles the one inherent residual: an anonymous route handler
(`router.get('/x', (req,res) => …)`) has no symbol node at all (the file holds
only a Const + PDG BasicBlocks), so it can never be named as a trace `to`.

Adds a DESTINATION TRACE: omit to/to_uid/to_file on an @group trace and
`trace from=<consumer>` follows the consumer's outgoing HTTP call across the
bridge and reports where it lands — by route + file:line, with a notes[] entry
flagging the handler as anonymous. Implemented as a new branch in runGroupTrace
(p.destination) backed by CY_CROSSINGS_FROM (all ContractLinks leaving the
consumer repo) + stitchToDestination; the provider endpoint is labelled
'<METHOD /path handler>' when its symbolName is a generic token/file basename.

The MCP routing already omitted an absent `to`, so only the schema docs changed.
parseTraceParams now treats a missing `to` as a destination trace instead of an
error. Verified end-to-end: anonymous fixture reports
'app/frontend:fetchUsers -> app/backend:<http::GET::/api/users handler>'; named
fixture lands at the real function. Adds 2 unit tests; 915 group tests pass.

* fix(group): tri-review fixes for cross-repo trace + symbolUid resolution

Two-engine tri-review (Claude swarm+ce + Codex GPT-5.5 swarm+ce+adversarial)
surfaced these; cross-engine-corroborated unless noted.

Correctness (P1, all four lanes): destination trace reported the WRONG endpoint
— an empty-uid consumer made trace(from->from) trivially succeed, so the highest-
confidence same-file crossing won regardless of which call `from` makes.
stitchToDestination now collects ALL connecting crossings, prefers symbol-precise
hits, and returns `ambiguous` (with candidates) when it cannot disambiguate.

Correctness (P1, Codex): resolveDetectionSymbol early-returned null when
d.line==null, blocking NAME resolution for named providers that set no line
(Spring/Go/etc.). Name resolution now runs first; only containment needs a line.

Correctness (P2): resolveContainingSymbol OR-ed `line` and `line-1`, which could
mis-pick a one-line sibling. It now probes the base-correct `line-1` first and
falls back to `line` only if nothing matches.

Correctness (Codex): anonymous Express handlers emitted name:'handler' and could
attach to an unrelated fn literally named `handler`. node.ts now emits name:null
for non-identifier handlers (containment-only).

Robustness: drop the first-symbol-in-file pickSymbolUid guess from the graph
consumer/provider paths (a wrong uid would win the contractId merge); remove the
dead CONTAINS_QUERY fallback (CONTAINS is File->Folder, never a symbol) + the now
-unused pickSymbolUid/handlerName; seed destination notes with degraded-member
notes so a successful trace still surfaces them; providerLabel takes providerUid
so a resolved fn named `handler` is not mislabeled anonymous, and only true file
basenames (known extensions) — not any dotted name — count as anonymous.

API contract: a single-repo trace with no `to` now returns an actionable error
(destination trace is @group-only) instead of "symbol 'undefined' not found".

Maintainability/tests: narrow asLocalTrace per-field (drop as-unknown-as); fix the
PR's lone as-any (vi.mocked); if-free e2e teardown; qualify the bench README.

Adds ambiguous-destination, anonymous-handler-no-false-name, and single-repo-no-to
tests; redirects graph mocks CONTAINS->UNION ALL. 918 group/integration pass.

* fix(group): carry degraded-member notes through SUCCESSFUL group traces

A reviewer (koriyoshi2041, PR #2269) correctly flagged that degraded-member
resolution was surfaced only on not_found, not on a successful ok result. Group
trace resolves names across ALL members, so an ok is 'unique among the members
we could query' — if a member that threw during resolveSymbol also holds from/to,
the real answer could be ambiguous. The destination path already seeded the note
(prior commit); this extends it to the same-repo and cross-repo success paths by
seeding the dispatch notes with degradedNotes([...fromRes.degraded, ...toRes.degraded]).

Adds a regression test: reg-be throws while a same-repo trace succeeds in reg-fe;
the ok result now carries the 'could not be queried' degraded note (app/backend).

* test(bench): cover all implemented cross-repo trace cases in one runner

Replace the single named-handler script with a self-contained verify.mjs that
generates each fixture inline and exercises every implemented end-to-end case
against the real analyze -> sync -> trace/impact pipeline, asserting PASS/FAIL
(exit non-zero on failure). 10 checks across 4 scenarios:
- named handlers: 4/4 symbolUid resolved; symbol-precise GET vs POST crossing
  selection; destination trace lands at the named handler.
- anonymous handler: empty symbolUid; destination trace reports it by route with
  the anonymous note.
- impact @group fan-out (cross_repo_hits >= 1).
- multi-language (Python Flask + requests): link built, cross-repo trace stitches,
  and the file-level boundary fallback is exercised when the provider has no uid.

Ambiguous-destination and degraded-member paths need synthetic inputs the real
analyzer cannot produce, so they stay in the unit suite (documented in the README
+ script header). Removes verify-named.mjs + fixtures-named/ (folded inline).

* test(group): pin destination degraded-success + precise-tier ambiguity

Adds the two regression guards koriyoshi2041 requested on PR #2269 after the
degraded-on-success fix:
- destination trace success with a degraded member: reg-fe resolves from and
  follows the link to an anonymous handler while reg-be throws; the ok result
  carries the anonymous endpoint AND the 'could not be queried' degraded note, so
  the no-to path stays aligned with explicit to traces.
- multiple PRECISE destination hits: one from reaches two consumers with resolved
  uids linked to different routes; the result is ambiguous (role: to) with both
  route candidates. Distinct from the existing file-level ambiguous test, this
  pins the stronger precise tier against a future change silently picking the
  highest-confidence destination.

Both already pass against current behavior; 716 group tests pass.
2026-06-23 07:54:13 +01:00