GitNexus/gitnexus/bench/cross-repo-trace/README.md
Gergő Magyar 49ffd8e316
feat(group): resolve cross-file named HTTP handlers (#2275) (#2277)
* feat(group): resolve cross-file named HTTP handlers via unique repo-wide lookup

U1 of #2275. When a provider's named handler is defined in a file other than its
route registration (e.g. router.get('/x', listUsers) with listUsers imported),
the registration file's symbols don't contain it, so resolution fell back to the
file-level boundary. Add a repo-wide name query (RESOLVE_BY_NAME_QUERY, the
label-union pattern from manifest-extractor) consulted only after the file-scoped
lookup misses, and honored ONLY when exactly one Function/Method/CodeElement
carries that name (zero/many → keep the file fallback, no wrong-symbol
attribution). Provider-only, cached by name. 4 unit tests; 743 group tests pass.

* test(bench): cross-file named handler scenario (end-to-end proof of #2275)

U2 of #2275. Adds a fifth bench scenario: a backend route whose handler
(listUsers) is imported from another file than its registration, with a frontend
consumer. Asserts the provider resolves to the handler via the repo-wide unique
name lookup (sym=listUsers, uid set) and that the cross-repo trace is symbol-
precise (no file-level fallback). verify.mjs now 12/12 on the real pipeline.

* fix(review): apply autofix feedback

ce-code-review (autofix) — no correctness/security findings; applied test-coverage
+ robustness fixes: repo-wide query throw -> empty (no exception); by-name lookup
cache fires once across same-named handlers; consumers never consult the repo-wide
lookup; same-file-wins now asserts the global path is bypassed; bench provider find
scoped by contractId; clarified the uniqueness-guard comment. 167 extractor tests.

* fix(group): tri-review fixes for cross-file handler resolution

Two-engine PR tri-review (Claude swarm+ce, Codex gpt-5.5 swarm+ce+adversarial)
on #2277. Correctness/security clean (injection refuted, bind-param). Fixes:

- Named-provider wrapper-attach (Codex swarm P1 + Claude ce-adversarial,
  cross-engine): a named handler that fails both name lookups no longer falls
  through to line-span containment, which attached the route to the enclosing
  registrar (e.g. a setupRoutes() wrapper) instead of leaving it empty.
  Containment now applies only to consumers and inline-arrow providers.
- CodeElement/ORM empty-file nodes (Claude ce-adversarial reproduced +
  ce-maintainability): RESOLVE_BY_NAME_QUERY gains 'AND n.filePath <> ""' so a
  handler name colliding with a synthetic ORM model node (orm.ts emits
  filePath:'') neither resolves to an edge-less node nor inflates the uniqueness
  count and masks the real handler; + a defensive empty-filePath guard in
  resolveSymbolByNameUnique. Added LIMIT 2 (Codex swarm P3 + ce-maintainability)
  to bound homonym materialization (count guard stays exact).
- Documented the aliased-import limitation (Codex adversarial): the route-site
  identifier is the local alias, fix deferred to #2275 import narrowing.
- README expected verdict 9/9 -> 12/12 (Codex swarm+ce P3).

Tests: +3 (wrapper-no-attach, empty-filePath reject, empty-registration-file
resolves) covering the cross-engine gaps. 170 extractor / 748 group+integration
pass; bench 12/12 end-to-end.

* feat(group): import-pinned handler resolution (fixes deferred alias case)

Resolves the tri-review's deferred item: cross-file named handlers are now pinned
to their import's target module instead of resolved by name alone, so aliases and
names that collide with a local symbol resolve correctly.

- node.ts builds a local-binding -> {declared name, module} map from the file's
  named imports; the express handler emits the DECLARED name + a handlerImport
  {name, module} (HttpDetection gains the optional field).
- resolveDetectionSymbol gains an imported-handler rung: resolveImportedSymbol
  pins to the import's target file via RESOLVE_IN_MODULE_QUERY
  (n.name= AND filePath STARTS WITH the resolved module path), unique-match
  only. An imported handler never uses file-scoped lookup (it is defined
  elsewhere); on a module miss it falls back to a unique repo-wide name match on
  the DECLARED name, then null. Relative imports only; bare/non-relative imports
  keep the repo-wide fallback. Cached by (module-prefix, name).
- Closes the Codex-adversarial alias finding: import { listUsers as handleUsers }
  + an unrelated handleUsers no longer mis-resolves — the route resolves to the
  imported listUsers in its module, and the alias is never looked up.
- Shared toResolvedSymbol helper (dedups the row->symbol + empty-filePath guard).

Tests: alias-resolves-to-declared-name + module-pin-resolves-ambiguous-name unit
tests; same-file-wins reworked to a genuinely LOCAL handler. Bench scenario 6
(aliased import with a decoy) proves it end-to-end. 172 extractor / 751
group+integration pass; bench 14/14.

* feat(group): import-pinned resolution for Python aliased handlers

Extends the JS/TS import-pinning to Python. The Python analog of express
router.get(path, handler) is Flask's imperative add_url_rule(view_func=...),
whose view is often an imported (aliased) symbol.

- New Flask add_url_rule provider pattern (path + view_func handler + methods;
  default GET, methods=[...] honored). High Flask-specificity keeps false
  positives low — unlike bare path()/Route(), which the plugin deliberately
  leaves to graph Route nodes.
- buildPythonImportMap resolves 'from .mod import name as alias' (and plain
  'from mod import name') to the declared name + raw module spec.
- resolveModuleBase generalized to two relative-import dialects: path-style
  (JS './h/users') and dotted (Python '.handlers.users', '..pkg.users' — leading
  dots are package levels). Bare/absolute imports keep the repo-wide fallback.
- Django stays graph-resolved (handlerSymbolId); FastAPI/Flask decorators stay
  same-file (decorated function). This only adds the imperative imported-view
  case Python lacked.

Tests: Flask aliased add_url_rule unit test (relative dotted module pinned, alias
never queried) + bench scenario 7 (end-to-end, 16/16). 173 extractor / 752
group+integration pass.
2026-06-23 12:12:49 +01:00

5.6 KiB
Raw Permalink Blame History

Cross-repo trace — end-to-end verification

Verifies the cross-repo trace MCP tool against the real pipeline (not hand-persisted graphs): runFullAnalysis(--pdg) on two repos → real syncGroup HTTP contract extraction + bridge build → callTool('trace', { repo: '@group' }).

Run from gitnexus/ (needs a current build for the parse worker):

node scripts/build.js
node bench/cross-repo-trace/verify.mjs

verify.mjs is self-contained — it generates each fixture inline, runs the real analyze → sync → trace/impact pipeline, and prints PASS/FAIL per assertion (exit non-zero on any failure). Expected verdict: 16/16 checks passed.

Cases covered (one scenario each)

  1. Named handlers, same file — a frontend with named fetch wrappers (fetchUsers, createUserReq) and a backend with named express handlers (listUsers, createUser) on /api/users GET/POST. Asserts: all four contracts resolve a symbolUid; trace is symbol-precise (the GET pair selects http::GET, the POST pair http::POST, no file-fallback note); the destination trace lands at listUsers.
  2. Anonymous handler — router.get('/api/ping', (req,res) => …). Asserts the provider contract has an empty symbolUid, and the destination trace (omit to) reaches it, reported as <http::GET::/api/ping handler> with an anonymous note.
  3. Cross-repo impact fan-out — impact @group on fetchUsers crosses the boundary (cross_repo_hits >= 1); the same symbolUid join was 0 before.
  4. Multi-language (Python) — a Flask provider + requests consumer; asserts the Python line wiring resolves the consumer and the cross-repo trace stitches fetch_items -> list_items.
  5. Cross-file named handler (#2275) — a route whose handler (listUsers) is imported from another file than its registration. Asserts the provider resolves to the handler via the import-pinned module lookup, and the trace is symbol-precise (no file-level fallback).
  6. Aliased cross-file import (#2275) — import { listUsers as handleUsers } with an unrelated decoy handleUsers elsewhere. Asserts the route resolves through the import to the declared listUsers (not the alias or the decoy), proving import-pinned resolution.
  7. Python aliased import (#2275) — a Flask add_url_rule('/api/users', view_func=handle_users) whose view is from .handlers.users import list_users as handle_users. Asserts the handler resolves through Python's dotted relative module to list_users, symbol-precise.

The ambiguous-destination (a file making several HTTP calls whose consumer contracts have no resolved uid) and degraded-member (a member DB that throws mid-resolution) paths need synthetic inputs the real analyzer cannot produce, so they live in the unit suite (test/unit/group/cross-trace.test.ts).

What it proves

  • analyze + syncGroup build the correct ContractLinks (exact HTTP match).
  • HTTP contracts carry a real symbolUid whenever the endpoint resolves — the extractor binds each detection to the function it lives in (the function CONTAINING the fetch; the named handler, or the inline handler by line-span containment, for a route). A handler/consumer that resolves to no named symbol (a fully anonymous handler, or a language plugin that does not yet set the call-site line) keeps an empty uid and degrades to the file/destination fallback. When resolved, contracts report extractionStrategy: 'source_scan_resolved' / 'graph_assisted' with a uid.
  • trace @group from=<calling fn> to=<handler fn> stitches the cross-repo path (fetchUsers → listUsers), reporting the CONTRACT_LINK hop and (with pdg:true) the data-flow enrichment, symbol-precise (GET pair → http::GET contract, POST → http::POST), with no file-fallback note.
  • The same symbolUid fix makes impact @group fan out across the boundary (it was 0 cross-repo hits before — both tools join crossings on symbolUid).

Resolution precedence & residual limits

The extractor resolves symbolUid in this order, falling through on a miss:

  1. Named handler — router.get('/x', listUsers) resolves listUsers by name.
  2. Containment — the innermost Function/Method whose line span encloses the call/registration line (consumers; inline-arrow providers).
  3. File-level boundary fallback (in cross-trace) — only when 1–2 leave the uid empty: if the user's from/to resolves into the contract's file, that endpoint anchors the boundary. A notes[] entry flags it as file-level, not symbol-precise.

The call-site line is set by all bundled language plugins (Node/TS, Python, Go, PHP, Kotlin, Java), and containment matches symbols by filePath across Function/Method/CodeElement, so it also resolves methods nested in classes (Java/Kotlin), not just top-level functions.

Anonymous handlers — the destination trace

A fully anonymous handler (router.get('/x', (req,res) => res.json(...))) has no symbol node at all, so it cannot be named as a to target. This is handled by the 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:

app/frontend:fetchUsers → app/backend:<http::GET::/api/users handler>   [CONTRACT_LINK]

To go deeper into an anonymous handler, trace to a named function it calls (the provider segment then resolves normally).