* fix(routes): link base-URL fetch calls made through wrapper chains (#3488)
GitNexus's own web client calls `fetchWithTimeout(`${_backendUrl}/api/repos`)`,
which reaches fetch through `resilientFetch` and a local `fetchImpl` alias.
`api_impact` found none of those consumers, for three independent reasons:
- normalizeFetchURL rejected any URL that did not start with `/`, so a
leading base-URL binding dropped the call. It now strips a leading
`${...}` run followed by `/`, with the same rules as the group layer's
consumer normalizer (#3111); the helper moves to route-path.ts so both
layers share it. A path whose every segment is a param (`${base}/${id}`,
`${base}/${id}.json`) is rejected, since it would match every route of
that depth. Interpolations are collapsed brace-balanced before the query
split, so `${repo ? `?repo=${repo}` : ''}` no longer ends the path, and
one glued to the last static segment is treated as a query or suffix.
- The wrapper call collector required a `/`-leading literal, so the same
template never became a wrapper candidate.
- Wrapper detection only looked five AST levels deep for a call to an
identifier named `fetch`. Arrow-const wrappers sit deeper than that, and
`resilientFetch` calls an alias of `globalThis.fetch`. Detection now walks
the whole definition and accepts `globalThis`/`window`/`self.fetch` and
aliases whose value names fetch (through `??`, `||`, `?:`), read from the
AST rather than the text. It records whether the first parameter reaches
fetch's URL argument and which callees receive that parameter. The routes
phase closes the wrapper set over that forwarding: `fetchWithTimeout` is a
wrapper because it forwards its URL to `resilientFetch`. A callee declared
in the caller's file or imported from a relative module must carry a URL
in exactly that module; a package-imported name is followed only when
every recorded function of that name carries one. A fetch caller whose
first parameter is not the URL (`track(event)`) or a same-named function
elsewhere (an IPC `request(channel)`) lends nothing.
When `[param]` matches several sibling routes (`/api/analyze/upload` and
`/api/analyze/:jobId`), the fetch now links to the route whose segments
match by kind instead of the first one registered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(routes): match wrapper calls by declaring module and decline ambiguous fetch matches
Review follow-ups for #3488:
- A propagated wrapper's name went into a repo-wide set, so a literal call
to an unrelated same-named function in another module was taken as a
fetch. Wrapper call candidates now carry the module declaring a bare
callee when the file declares or imports it relatively, and must reach a
wrapper defined in exactly that module. Package and member callees keep
the name match.
- An alias declared in a nested helper (`const fetchImpl = globalThis.fetch`)
marked an outer call to a same-named parameter as a fetch. An alias now
counts only where its declaring function encloses the call.
- A first parameter redeclared inside the function (`{ const url = ...;
fetch(url) }`) was taken as the URL. A shadowed parameter is no longer
followed.
- A fetch whose `[param]` ties between sibling static routes
(`/api/kinds/foo`, `/api/kinds/bar`) linked to the first registered one.
A tie now links none.
- The fixture's `resilientFetch` returns the last response instead of
rejecting with `undefined`, and its client encodes the repo query value.
The release tool-accuracy gate now passes `api.template-wrapper`, so its
known-gap allowance is removed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(routes): keep call-site wrapper names and ignore helpers that never run
- A wrapper call through a renamed relative import (`import { request as
send }`) stored the exported name, so a configured `fetchWrappers` entry
naming the call-site `send` no longer matched. The call keeps its
call-site name and carries the declared name separately for the module
lookup.
- A nested helper bound to a local that is never referenced again (`const
later = () => resilientFetch(url)`) made its enclosing function forward
its URL. Calls inside such a helper are ignored; a helper that is called
or passed on, such as a retry callback, still counts.
- The fixture's `resilientFetch` no longer retries after a timeout or abort.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(routes): let a nested parameter shadow a fetch alias
A function parameter between a fetch alias and a call shadows the alias
there (`function send(fetchImpl) { return fetchImpl(event) }` beside an
outer `const fetchImpl = globalThis.fetch`), so that call no longer marks
the enclosing function as a fetch wrapper. The fixture's retry helper also
stops returning a stale 5xx response after a later attempt throws.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(routes): drop fetch aliases whose name is redeclared with another value
A block that redeclares an alias name with a non-fetch value (`{ const
fetchImpl = other; fetchImpl('/x') }`) let the inner call read as a fetch.
A name declared anywhere in the function with a value that does not name
fetch is no longer an alias, which covers every block-scoped shadow without
lexical scope tracking.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(routes): ignore uncalled nested declarations and shadowed global objects
- An uncalled nested `function helper() { fetch('/x') }` made its enclosing
function a fetch wrapper. Nested function declarations that are never
referenced are now ignored, as unused function-valued locals already were.
- `window.fetch(url)` through a parameter or local named `window` (or
`self`, `globalThis`) was read as the platform fetch. Such a root is now
that value's method. A bare `fetch` parameter is still taken as fetch,
since injecting a fetch implementation is the common reason for it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test(bench): refresh the receiver-resolution baseline for the fetch fixture
The new `fetch-base-url-wrapper` fixture in `test/fixtures/lang-resolution`
adds one unresolved TypeScript chain-field call to the committed check
corpus (127 to 128 call drops). Regenerated with
`measure.mjs --update-baseline` after a fresh build and cleared parse caches;
no other counter moved.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
* feat: configure prettier with pre-commit hook integration
Add prettier, lint-staged, and prettier-plugin-tailwindcss at the repo
root with husky pre-commit hook integration. Moves husky from
gitnexus/ to root package.json for reliable hook installation.
- Root package.json with prepare/format/format:check scripts
- .prettierrc with endOfLine:lf and tailwindStylesheet for TW v4
- .prettierignore excluding fixtures, vendor, generated, *.d.ts, *.md
- .gitattributes enforcing LF line endings for Windows consistency
- Pre-commit hook uses direct node_modules/.bin/ paths (no npx)
* style: apply prettier formatting to entire codebase
One-time bulk format. No logic changes.
Use .git-blame-ignore-revs to skip this commit in git blame.
* chore: add .git-blame-ignore-revs for prettier format commit
* perf: pre-commit hook runs only tests related to staged files
Use vitest --related to scope test execution to tests that import
the changed files, instead of running the full suite on every commit.
* perf: remove vitest from pre-commit hook, keep in CI only
Pre-commit now runs lint-staged + tsc only. Tests run in CI
(ci-tests.yml) where they belong — keeps commits fast.
* ci: add prettier format check to quality workflow
PRs will now fail if code isn't formatted with prettier.
* feat: add PHP response shape extraction for json_encode patterns
Adds extractPHPResponseShapes() to detect response keys from PHP
json_encode() calls with associative array literals. Supports:
- Short array syntax: json_encode(['key' => value])
- Long array syntax: json_encode(array('key' => value))
- Error classification via http_response_code() and header() status
- exit;/die; boundary detection to prevent cross-block status leaking
- Nested array filtering (only top-level keys extracted)
Pipeline integration dispatches PHP files to the new extractor.
Verified on collector project: 10 PHP routes now show responseKeys.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review — exit boundary, die; offset, CGI Status header
- Replace lastIndexOf('exit;')/lastIndexOf('die;') with regex that
matches exit(N), exit(0), die('msg'), die($var) as boundaries
- Fixes die; off-by-one (was slicing at +5 for a 4-char keyword)
- Add header('Status: NNN') CGI/FastCGI format detection
- Add 3 regression tests for the fixed bugs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: extract shared helpers, remove duplicate test block
- Extract lastMatchGroup() and buildShapeResult() to eliminate repeated
patterns in both JS/TS and PHP extractors
- Simplify detectPHPStatusCode to use ?? chaining with lastMatchGroup
- Remove duplicate 9-test PHP describe block (kept the 12-test version)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test: add PHP response shape integration tests
Adds a PHP fixture (api/items.php, api/submit.php) with multiple
json_encode patterns and a pipeline integration test verifying:
- Route nodes created for PHP endpoints
- responseKeys/errorKeys correctly extracted and separated
- exit(N)/die() boundaries respected
- HANDLES_ROUTE edges point to correct PHP handler files
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>