diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1bfdd8b6b..4d77d921b 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -200,6 +200,10 @@ without a verb, and any regex it cannot translate exactly are all dropped rather than guessed at. A missing route is a coverage limit; an invented one is a lie. Express `app.get(RegExp, handler)` registrations are likewise omitted: a regex matcher does not have one finite URL to use as a Route node key. +Express `app.use(path, middleware)` is middleware attachment, not an endpoint; +it does not create a Route node. Dispatch-guard inference also ignores stored +`route.routePath` metadata: comparing a recorded route name with a literal +does not prove that the request path is being dispatched. Two rules there need more than one comparison to decide, and are worth knowing about before changing either: diff --git a/gitnexus/src/storage/parse-cache.ts b/gitnexus/src/storage/parse-cache.ts index c4a56ed56..7be46bc4a 100644 --- a/gitnexus/src/storage/parse-cache.ts +++ b/gitnexus/src/storage/parse-cache.ts @@ -858,7 +858,10 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid // and original graph positions. Both stores must re-extract v135 captures. // v137: Python module-accessor syntax proofs and exact assigned-call provenance // join the existing subtype capture payload. Re-extract v136 in both stores. -const SCHEMA_BUMP = 137; +// v138 (#3561): Express middleware registrations no longer emit endpoints, +// and dispatch guards ignore stored route metadata. Warm v137 shards retain +// those phantom routes, so both parse stores must re-extract unchanged files. +const SCHEMA_BUMP = 138; const GITNEXUS_PKG_VERSION = (() => { try { // package.json sits at gitnexus/package.json — two levels up from diff --git a/gitnexus/test/fixtures/lang-resolution/express-route-priority/src/server.ts b/gitnexus/test/fixtures/lang-resolution/express-route-priority/src/server.ts index 27fb6dd56..0fe8140e9 100644 --- a/gitnexus/test/fixtures/lang-resolution/express-route-priority/src/server.ts +++ b/gitnexus/test/fixtures/lang-resolution/express-route-priority/src/server.ts @@ -24,3 +24,7 @@ counts.get('/ghost-block-comment' /* cached key */); counts.get( '/ghost-line-comment', // cached key ); + +function inspectRoute(route) { + return route.routePath === '/comparison-ghost'; +} diff --git a/gitnexus/test/integration/resolvers/express-routes.test.ts b/gitnexus/test/integration/resolvers/express-routes.test.ts index d59728b89..e2948c100 100644 --- a/gitnexus/test/integration/resolvers/express-routes.test.ts +++ b/gitnexus/test/integration/resolvers/express-routes.test.ts @@ -11,6 +11,7 @@ import { } from '../../../src/storage/parse-cache.js'; import { getDurableParsedFileDir, + loadDurableParsedFileIndex, pruneAndSaveDurableParsedFileStore, } from '../../../src/storage/parsedfile-store.js'; import { @@ -113,6 +114,10 @@ describe('Express route identity and source priority', () => { }, ); + it('does not turn stored route metadata into a dispatch guard', () => { + expect(getNodesByLabel(result, 'Route')).not.toContain('/comparison-ghost'); + }); + it.each([ ['/api/chained', ['GET']], ['/api/chained-post', ['POST']], @@ -238,4 +243,52 @@ describe('Express route identity and source priority', () => { fs.rmSync(tempDir, { recursive: true, force: true }); } }, 120_000); + + it('reparses route captures from a same-package v137 cache on upgrade', async () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-express-route-upgrade-')); + const repoDir = path.join(tempDir, 'repo'); + const storageDir = path.join(tempDir, 'storage'); + const previousVersion = `137+${PARSE_CACHE_VERSION.split('+').slice(1).join('+')}`; + try { + fs.cpSync(path.join(FIXTURES, 'express-route-priority'), repoDir, { recursive: true }); + const cache: ParseCache = { + version: PARSE_CACHE_VERSION, + entries: new Map(), + usedKeys: new Set(), + storagePath: storageDir, + onDiskKeys: new Set(), + }; + const initial = await runPipelineFromRepo(repoDir, () => {}, { + parseCache: cache, + workerPoolSize: 1, + }); + expect(initial.reparsedFileCount).toBe(2); + pruneCache(cache, cache.usedKeys); + cache.version = previousVersion; + const savedKeys = await saveParseCache(storageDir, cache); + expect(savedKeys).toHaveLength(2); + const durableDir = getDurableParsedFileDir(storageDir); + await pruneAndSaveDurableParsedFileStore(durableDir, previousVersion, new Set(savedKeys)); + expect((await loadDurableParsedFileIndex(durableDir, previousVersion)).size).toBe(2); + + const upgraded = await loadParseCache(storageDir); + expect(upgraded.onDiskKeys?.size ?? 0).toBe(0); + expect(await loadDurableParsedFileIndex(durableDir, PARSE_CACHE_VERSION)).toEqual(new Map()); + const result = await runPipelineFromRepo(repoDir, () => {}, { + parseCache: upgraded, + workerPoolSize: 1, + }); + expect(result.reparsedFileCount).toBe(2); + expect(result.parseCacheHitFileCount).toBe(0); + const routes = getNodesByLabel(result, 'Route'); + expect(routes).toContain('/api/info'); + expect(routes).not.toContain('/ghost'); + expect(routes).not.toContain('/comparison-ghost'); + expect( + getNodesByLabelFull(result, 'Route').filter((node) => node.name === '/api/mcp'), + ).toHaveLength(1); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }, 120_000); }); diff --git a/gitnexus/test/unit/incremental-parse-cache.test.ts b/gitnexus/test/unit/incremental-parse-cache.test.ts index 9680bfde4..8edd96f6b 100644 --- a/gitnexus/test/unit/incremental-parse-cache.test.ts +++ b/gitnexus/test/unit/incremental-parse-cache.test.ts @@ -319,8 +319,9 @@ describe('PARSE_CACHE_VERSION', () => { // Moved 134 -> 135 for guarded-import and mutable binding review fixes (#3532). // Moved 135 -> 136 for parameter annotations and Vue embedded script fixes (#3532). // Moved 136 -> 137 for Python module-accessor and call-result assignment captures (#3497). - it('pins SCHEMA_BUMP to 137 so concurrent bumps cannot silently collide (#2766, #3015, #3088, #2885, #3128, #2865, #3130, #1432, #3161, #3179, #3219, #3190, #3253, #3273, #3339, #3354, #3371, #2965, #3390, #3398, #3396, #3394, #3399, #3414, #3408, #3402, #3446, #3450, #3499, #3502, #3487, #3505, #3504, #3497)', () => { - expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(137); + // Moved 137 -> 138 for the route-capture corrections in #3561. + it('pins SCHEMA_BUMP to 138 so concurrent bumps cannot silently collide (#2766, #3015, #3088, #2885, #3128, #2865, #3130, #1432, #3161, #3179, #3219, #3190, #3253, #3273, #3339, #3354, #3371, #2965, #3390, #3398, #3396, #3394, #3399, #3414, #3408, #3402, #3446, #3450, #3499, #3502, #3487, #3505, #3504, #3497, #3561)', () => { + expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).toBe(138); expect(PARSE_CACHE_BUCKET_COUNT).toBe(128); // The PREVIOUS version must fail the reuse gate, not merely differ from the // current one — a hardcoded number outside the conflict hunk rebases cleanly @@ -330,7 +331,7 @@ describe('PARSE_CACHE_VERSION', () => { 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, - 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, + 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, ]) { expect(Number(PARSE_CACHE_VERSION.split('+', 1)[0])).not.toBe(taken); }