From 14491e9f211d411d5e22f84cbaaab55b75ea2161 Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Wed, 26 Aug 2026 19:22:10 +0000 Subject: [PATCH] refactor(ingestion): reuse the shared modifier reader, and report the drop where it is decided MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four cleanup passes over the fix commits. No behaviour change except where noted; every mutation that guarded these paths still bites. Reuse — `isRequestHandler` tested `child.type === 'static'`. The repo already exports `hasKeyword` for exactly this question, uses it at three other sites, and documents why the type spelling is wrong: `static` reaches the tree as an anonymous token in some grammar versions and a keyword node in others, so a type test "silently stops firing on a grammar bump". Here that would have readmitted precisely the phantom routes 72596b0d0 removes, with the suite green. `hasKeyword` also skips the `name` field, so a method called `get()` or `static()` is not misread. Fourth divergent spelling of one predicate, gone. Altitude — the drop diagnostic moves up to `controllerPrefix`, the single funnel every whole-controller drop already returns through. It was attached to one refusal out of four, which made the loudest signal fire for the rarest cause and left `@Controller(ROUTES.VENUES)` — the case this module's own header calls motivating — silent. One call site now covers all four. This DELETES `refuseUnprovableObject`, its constant, and the `filePath` threading through `literalPaths` and `decoratorLiteralPaths`: better coverage from less code. The structurally right home is a counted diagnostic that survives the parse cache, summarized in the routes phase beside the existing duplicate-URL line. That needs a SCHEMA_BUMP of its own and is a follow-up, not this commit. Simplification — the three pre-seed sites hand-paired `set` with `preSeededKeys.add`, making the invariant a convention three call sites had to remember; a fourth that forgot the `add` would reopen #3049 as silently as the source-set it replaced. One `preSeed` helper now writes both, which is the shape `claim()` in call-processor.ts uses and which that comment already cited. The test file grew three copies of one controller wrapper and two of one fixture. Now one `memberUrls` helper and one `STATIC_THEN_INSTANCE` constant; the standalone instance-method control became a row of the table it controls for, where it reads as the contrast it is. The ledger paragraph traded ~6 lines re-litigating scan methodology for the thing a future reader actually needs: why this is still a hand-picked number when `SCHEMA_FINGERPRINT` next door is a derived digest that cannot collide. The derivation exists — `resolveAnalyzerRunnerIdentity` computes a build digest every run — and is rejected because it moves on any build change, including a comment-only one, forcing a full re-parse for every dev and CI build. That is the real trade and it was nowhere written down. The residual cost is named too: a capture change with no bump ships inert and no check can see it, which #2860 does not close. Declined: merging the two object-refusal tables (they document different rules — unreadable path value vs unprovable object — and the row labels are the documentation); extracting a shared object walk into data-route-table.ts (its `routeFromObject` dedupes only route-token keys and runs `containsExecutingExpression`, so a shared strict walk would change ITS behaviour, and a third primitive is not a subtraction). --- .../core/ingestion/pipeline-phases/routes.ts | 34 +++--- .../core/ingestion/route-extractors/nest.ts | 108 ++++++++---------- gitnexus/src/storage/parse-cache.ts | 27 ++++- .../test/unit/nest-decorator-routes.test.ts | 81 +++++-------- 4 files changed, 117 insertions(+), 133 deletions(-) diff --git a/gitnexus/src/core/ingestion/pipeline-phases/routes.ts b/gitnexus/src/core/ingestion/pipeline-phases/routes.ts index ba09263bc..e93fa033c 100644 --- a/gitnexus/src/core/ingestion/pipeline-phases/routes.ts +++ b/gitnexus/src/core/ingestion/pipeline-phases/routes.ts @@ -256,35 +256,33 @@ export const routesPhase: PipelinePhase = { } } + // One writer for every pre-seeded route, so recording membership cannot be + // forgotten. Inlining `has` / `set` / `add` at each site made the invariant + // a convention three call sites had to remember — and a fourth source that + // forgot the `add` would reopen #3049 exactly as silently as the source-set + // it replaced. This is the shape `claim()` in call-processor.ts uses for the + // same reason: one helper writes the collection and its key set together. + const preSeed = (url: string, entry: Omit): boolean => { + if (routeRegistry.has(url)) return false; + routeRegistry.set(url, { ...entry, url }); + preSeededKeys.add(url); + return true; + }; + for (const p of allPaths) { if (expoAppPaths.has(p)) { const expoURL = expoFileToRouteURL(p); - if (expoURL && !routeRegistry.has(expoURL)) { - routeRegistry.set(expoURL, { - filePath: p, - source: 'expo-filesystem-route', - url: expoURL, - }); - preSeededKeys.add(expoURL); + if (expoURL && preSeed(expoURL, { filePath: p, source: 'expo-filesystem-route' })) { continue; } } const nextjsURL = nextjsFileToRouteURL(p); - if (nextjsURL && !routeRegistry.has(nextjsURL)) { - routeRegistry.set(nextjsURL, { - filePath: p, - source: 'nextjs-filesystem-route', - url: nextjsURL, - }); - preSeededKeys.add(nextjsURL); + if (nextjsURL && preSeed(nextjsURL, { filePath: p, source: 'nextjs-filesystem-route' })) { continue; } if (p.endsWith('.php')) { const phpURL = phpFileToRouteURL(p); - if (phpURL && !routeRegistry.has(phpURL)) { - routeRegistry.set(phpURL, { filePath: p, source: 'php-file-route', url: phpURL }); - preSeededKeys.add(phpURL); - } + if (phpURL) preSeed(phpURL, { filePath: p, source: 'php-file-route' }); } } diff --git a/gitnexus/src/core/ingestion/route-extractors/nest.ts b/gitnexus/src/core/ingestion/route-extractors/nest.ts index e1b90124f..73d34ddcd 100644 --- a/gitnexus/src/core/ingestion/route-extractors/nest.ts +++ b/gitnexus/src/core/ingestion/route-extractors/nest.ts @@ -44,6 +44,7 @@ import type Parser from 'tree-sitter'; import type { ExtractedDecoratorRoute } from '../workers/parse-worker.js'; import { plainString, propertyName } from './data-route-table.js'; +import { hasKeyword } from '../field-extractors/configs/helpers.js'; import { isDev } from '../utils/env.js'; import { logger } from '../../logger.js'; @@ -131,10 +132,7 @@ function decoratorName(decorator: Parser.SyntaxNode): string | null { * `:id(d+)`, and `@Get('/v\u0069ews')` came out as `/vews`. Both are paths the * app never serves, i.e. the wrong-URL outcome the paragraph above forbids. */ -function decoratorLiteralPaths( - decorator: Parser.SyntaxNode, - filePath: string, -): readonly string[] | null { +function decoratorLiteralPaths(decorator: Parser.SyntaxNode): readonly string[] | null { const call = decorator.namedChild(0); // A bare `@Injectable` with no call, or `@Get()` with no argument — legal, // and both mean "no path segment of my own". @@ -146,42 +144,7 @@ function decoratorLiteralPaths( // Reading it as a route would mint a URL the app never serves, which is the // invented fact this module refuses; an unreadable shape drops instead. if (first.type === 'object' && decoratorName(decorator) !== 'Controller') return null; - return literalPaths(first, filePath); -} - -/** - * How much of a refused options object to quote in the dev line. Enough to - * recognise the shape, bounded because an options object is arbitrary source. - */ -const REFUSED_OBJECT_LOG_LIMIT = 160; - -/** - * Decline an options object whose `path` is not provable, and say so. - * - * Gated on `isDev` exactly as the routes phase gates its own registry line - * (`pipeline-phases/routes.ts`), and emitted at `info` rather than `debug` - * because the logger's base level IS `info`: an `isDev`-gated `logger.debug` - * would be gated twice and stay silent in the very dev run it exists for. - * - * Names the FILE, not just the shape — a controller dropped without a path to - * look at is only marginally louder than one dropped in silence, and this - * refusal costs every route on the class. The line number is deliberately - * absent: `lineOffset` (a Vue SFC `