From 53da6dcd0ce9febcec474a22993bb7e84d8e05c0 Mon Sep 17 00:00:00 2001 From: Aaron Queen Date: Sun, 6 Sep 2026 00:14:16 -0600 Subject: [PATCH] fix(explore): a damped declaration file is a candidate, not a walk start MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CG-28: on a prose flow query, a declaration-only file outranks the implementation it declares. The damage enters through the RWR restart vector, not through connectivity. `contains` is not a RANK_EDGE, so a declared member is near-isolated and carries almost no walk mass of its own — but the restart vector is uniform over seeds, so since #1638 a platform `.d.ts` contributes one seed per member, and those member names (`body`, `stream`, `metadata`) are exactly what a prose flow query matches. Every such seed divides the restart mass the implementation files are competing for. That is what halves an implementation file's graph mass while the shim's holds steady. Filter the damped files out of the seed set only. They stay candidates, stay reachable, and keep their `score` contribution; this changes where the walk starts and nothing else. The predicate is `isDampedDeclaration` rather than a bare ambient test because it already exempts a file whose declared type the query named — so the counter-case holds: on a query about the declared type the shim still ranks first, at mass 1.0. Fixture (ambient-decls-ts), flow query: storage/metadata.ts 0.137461 -> 0.504025 rank 2 -> 1 storage/stream.ts 0.058601 -> 0.214871 rank 4 -> 2 platform-shims.d.ts 0.184398 -> 0.009459 rank 1 -> 3, still named __tests__/explore-declaration-only.test.ts: 12 passed, 0 failed. Full suite against this base: 31 failed -> 30 failed, and the CG-28 gate is the only difference in the failing set. Verified on vitejs/vite (1,719 files, 13,793 nodes, 32,822 edges), one index shared across arms so ranking is the only variable: zero changed rows against the unpatched base on four prose flow queries, and the type counter-case keeps types/hmrPayload.d.ts at rank 1 (mass 0.185539). The change is inert where it is not needed. --- src/mcp/tools.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/mcp/tools.ts b/src/mcp/tools.ts index 9e1987e67..06345f1ba 100644 --- a/src/mcp/tools.ts +++ b/src/mcp/tools.ts @@ -3819,8 +3819,33 @@ export class ToolHandler { // (org-user.storage.ts, call-connected to the matches) accrues mass; a lone // text match (LensSwitcher.swift, matched "switch" but calls nothing in the // flow) gets only its restart probability → ~0, and is dropped by the gate. + // + // A file the ambient-declaration penalty has already damped is a candidate, + // but not a place a walk STARTS. The restart vector is uniform over seeds, + // so every seed divides the restart mass the implementation files compete + // for — and since #1638 a platform `.d.ts` contributes one seed per member, + // whose names (`body`, `stream`, `metadata`) are exactly what a prose flow + // query matches. That is what halves an implementation file's graph mass + // while the shim's holds steady: dilution of the restart vector, not + // connectivity. `contains` is not a RANK_EDGE, so these members carry almost + // no walk mass of their own; seeding is the whole of their effect on rank. + // + // `isDampedDeclaration` and not a bare ambient test: it already exempts a + // file whose declared type the query NAMED, so a query genuinely about the + // declared type keeps its seeds and the shim still ranks first. Damped files + // stay in the candidate set, stay reachable, and keep their `score` + // contribution — this changes only where the walk starts. + const rwrSeedIds = new Set(); + for (const id of entryNodeIds) { + const seed = subgraph.nodes.get(id); + if (seed && isDampedDeclaration(seed.filePath)) continue; + rwrSeedIds.add(id); + } const nodeRwr = this.computeGraphRelevance( - [...subgraph.nodes.keys()], subgraph.edges, entryNodeIds, + // Fall back to the unfiltered seeds when EVERY seed is damped: the walk + // must not lose its restart vector and return all-uniform. + [...subgraph.nodes.keys()], subgraph.edges, + rwrSeedIds.size > 0 ? rwrSeedIds : entryNodeIds, ); // // Carries `rankPenalty` too, so generated/low-value files are demoted on the