@nanonets/graft 0.13.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -10
- package/dist/ai/crux.d.ts +14 -1
- package/dist/ai/crux.d.ts.map +1 -1
- package/dist/ai/crux.js +39 -51
- package/dist/ai/crux.js.map +1 -1
- package/dist/ai/failure.d.ts +13 -2
- package/dist/ai/failure.d.ts.map +1 -1
- package/dist/ai/failure.js +16 -4
- package/dist/ai/failure.js.map +1 -1
- package/dist/ai/llm/factory.d.ts +10 -1
- package/dist/ai/llm/factory.d.ts.map +1 -1
- package/dist/ai/llm/factory.js +16 -0
- package/dist/ai/llm/factory.js.map +1 -1
- package/dist/ai/llm/litellm.d.ts +34 -0
- package/dist/ai/llm/litellm.d.ts.map +1 -0
- package/dist/ai/llm/litellm.js +45 -0
- package/dist/ai/llm/litellm.js.map +1 -0
- package/dist/ai/llm/openai.d.ts.map +1 -1
- package/dist/ai/llm/openai.js +66 -7
- package/dist/ai/llm/openai.js.map +1 -1
- package/dist/ai/llm/orcarouter.d.ts +34 -0
- package/dist/ai/llm/orcarouter.d.ts.map +1 -0
- package/dist/ai/llm/orcarouter.js +45 -0
- package/dist/ai/llm/orcarouter.js.map +1 -0
- package/dist/ai/llm/recover-tool.d.ts +19 -0
- package/dist/ai/llm/recover-tool.d.ts.map +1 -0
- package/dist/ai/llm/recover-tool.js +109 -0
- package/dist/ai/llm/recover-tool.js.map +1 -0
- package/dist/ai/providers.d.ts.map +1 -1
- package/dist/ai/providers.js +15 -3
- package/dist/ai/providers.js.map +1 -1
- package/dist/ai/synthesize.d.ts.map +1 -1
- package/dist/ai/synthesize.js +19 -5
- package/dist/ai/synthesize.js.map +1 -1
- package/dist/app/checkout.d.ts +28 -0
- package/dist/app/checkout.d.ts.map +1 -0
- package/dist/app/checkout.js +100 -0
- package/dist/app/checkout.js.map +1 -0
- package/dist/app/events.d.ts +37 -0
- package/dist/app/events.d.ts.map +1 -0
- package/dist/app/events.js +59 -0
- package/dist/app/events.js.map +1 -0
- package/dist/app/identity.d.ts +54 -0
- package/dist/app/identity.d.ts.map +1 -0
- package/dist/app/identity.js +96 -0
- package/dist/app/identity.js.map +1 -0
- package/dist/app/main.d.ts +2 -0
- package/dist/app/main.d.ts.map +1 -0
- package/dist/app/main.js +56 -0
- package/dist/app/main.js.map +1 -0
- package/dist/app/pages.d.ts +39 -0
- package/dist/app/pages.d.ts.map +1 -0
- package/dist/app/pages.js +86 -0
- package/dist/app/pages.js.map +1 -0
- package/dist/app/queue.d.ts +40 -0
- package/dist/app/queue.d.ts.map +1 -0
- package/dist/app/queue.js +68 -0
- package/dist/app/queue.js.map +1 -0
- package/dist/app/review.d.ts +23 -0
- package/dist/app/review.d.ts.map +1 -0
- package/dist/app/review.js +139 -0
- package/dist/app/review.js.map +1 -0
- package/dist/app/server.d.ts +40 -0
- package/dist/app/server.d.ts.map +1 -0
- package/dist/app/server.js +117 -0
- package/dist/app/server.js.map +1 -0
- package/dist/ask/ask.d.ts +35 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +427 -63
- package/dist/ask/ask.js.map +1 -1
- package/dist/ask/file-rank.d.ts +72 -0
- package/dist/ask/file-rank.d.ts.map +1 -0
- package/dist/ask/file-rank.js +160 -0
- package/dist/ask/file-rank.js.map +1 -0
- package/dist/ask/file-selection.d.ts +4 -0
- package/dist/ask/file-selection.d.ts.map +1 -1
- package/dist/ask/file-selection.js +25 -0
- package/dist/ask/file-selection.js.map +1 -1
- package/dist/ask/fuse.d.ts +16 -0
- package/dist/ask/fuse.d.ts.map +1 -1
- package/dist/ask/fuse.js +21 -4
- package/dist/ask/fuse.js.map +1 -1
- package/dist/ask/graphrank.d.ts +18 -0
- package/dist/ask/graphrank.d.ts.map +1 -1
- package/dist/ask/graphrank.js +65 -19
- package/dist/ask/graphrank.js.map +1 -1
- package/dist/blast/blast-cli.d.ts +5 -0
- package/dist/blast/blast-cli.d.ts.map +1 -1
- package/dist/blast/blast-cli.js +21 -30
- package/dist/blast/blast-cli.js.map +1 -1
- package/dist/blast/blast.d.ts +9 -0
- package/dist/blast/blast.d.ts.map +1 -1
- package/dist/blast/blast.js.map +1 -1
- package/dist/blast/name.d.ts +13 -0
- package/dist/blast/name.d.ts.map +1 -1
- package/dist/blast/name.js +36 -0
- package/dist/blast/name.js.map +1 -1
- package/dist/blast/owners.d.ts +85 -0
- package/dist/blast/owners.d.ts.map +1 -0
- package/dist/blast/owners.js +272 -0
- package/dist/blast/owners.js.map +1 -0
- package/dist/blast/render.d.ts.map +1 -1
- package/dist/blast/render.js +105 -0
- package/dist/blast/render.js.map +1 -1
- package/dist/blast/viz.d.ts.map +1 -1
- package/dist/blast/viz.js +10 -0
- package/dist/blast/viz.js.map +1 -1
- package/dist/claude/format.js +1 -1
- package/dist/claude/format.js.map +1 -1
- package/dist/claude/hooks.d.ts.map +1 -1
- package/dist/claude/hooks.js +132 -22
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/init.d.ts +1 -0
- package/dist/claude/init.d.ts.map +1 -1
- package/dist/claude/init.js +1 -1
- package/dist/claude/init.js.map +1 -1
- package/dist/claude/session-metrics.d.ts +63 -0
- package/dist/claude/session-metrics.d.ts.map +1 -0
- package/dist/claude/session-metrics.js +181 -0
- package/dist/claude/session-metrics.js.map +1 -0
- package/dist/claude/settings-merge.d.ts +18 -1
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +70 -27
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/state.d.ts +25 -0
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +14 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/statusline.d.ts +5 -3
- package/dist/claude/statusline.d.ts.map +1 -1
- package/dist/claude/statusline.js +6 -4
- package/dist/claude/statusline.js.map +1 -1
- package/dist/claude/tally.d.ts +23 -0
- package/dist/claude/tally.d.ts.map +1 -0
- package/dist/claude/tally.js +138 -0
- package/dist/claude/tally.js.map +1 -0
- package/dist/cli.js +145 -4
- package/dist/cli.js.map +1 -1
- package/dist/context/build.d.ts +11 -0
- package/dist/context/build.d.ts.map +1 -1
- package/dist/context/build.js +49 -9
- package/dist/context/build.js.map +1 -1
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +5 -12
- package/dist/context/check.js.map +1 -1
- package/dist/context/node-file.d.ts +2 -1
- package/dist/context/node-file.d.ts.map +1 -1
- package/dist/context/node-file.js +70 -3
- package/dist/context/node-file.js.map +1 -1
- package/dist/context/savings.d.ts +8 -0
- package/dist/context/savings.d.ts.map +1 -1
- package/dist/context/savings.js +14 -0
- package/dist/context/savings.js.map +1 -1
- package/dist/engine.d.ts +4 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +2 -0
- package/dist/engine.js.map +1 -1
- package/dist/graph/bindings.d.ts +6 -5
- package/dist/graph/bindings.d.ts.map +1 -1
- package/dist/graph/bindings.js +117 -5
- package/dist/graph/bindings.js.map +1 -1
- package/dist/graph/build.d.ts +4 -0
- package/dist/graph/build.d.ts.map +1 -1
- package/dist/graph/build.js +7 -4
- package/dist/graph/build.js.map +1 -1
- package/dist/graph/cards.d.ts +4 -3
- package/dist/graph/cards.d.ts.map +1 -1
- package/dist/graph/cards.js +34 -6
- package/dist/graph/cards.js.map +1 -1
- package/dist/graph/check.d.ts.map +1 -1
- package/dist/graph/check.js +36 -6
- package/dist/graph/check.js.map +1 -1
- package/dist/graph/enrich.d.ts +4 -4
- package/dist/graph/enrich.d.ts.map +1 -1
- package/dist/graph/enrich.js +32 -6
- package/dist/graph/enrich.js.map +1 -1
- package/dist/graph/extract.d.ts +9 -2
- package/dist/graph/extract.d.ts.map +1 -1
- package/dist/graph/extract.js +356 -12
- package/dist/graph/extract.js.map +1 -1
- package/dist/graph/fingerprint.d.ts +6 -1
- package/dist/graph/fingerprint.d.ts.map +1 -1
- package/dist/graph/fingerprint.js +7 -3
- package/dist/graph/fingerprint.js.map +1 -1
- package/dist/graph/generic.d.ts +10 -0
- package/dist/graph/generic.d.ts.map +1 -1
- package/dist/graph/generic.js +20 -3
- package/dist/graph/generic.js.map +1 -1
- package/dist/graph/invariants.d.ts +3 -2
- package/dist/graph/invariants.d.ts.map +1 -1
- package/dist/graph/invariants.js +6 -3
- package/dist/graph/invariants.js.map +1 -1
- package/dist/graph/refresh.d.ts.map +1 -1
- package/dist/graph/refresh.js +6 -2
- package/dist/graph/refresh.js.map +1 -1
- package/dist/graph/resolve.d.ts +1 -1
- package/dist/graph/resolve.d.ts.map +1 -1
- package/dist/graph/resolve.js +106 -5
- package/dist/graph/resolve.js.map +1 -1
- package/dist/graph/scopes.d.ts.map +1 -1
- package/dist/graph/scopes.js +2 -1
- package/dist/graph/scopes.js.map +1 -1
- package/dist/graph/source-files.d.ts +7 -2
- package/dist/graph/source-files.d.ts.map +1 -1
- package/dist/graph/source-files.js +19 -6
- package/dist/graph/source-files.js.map +1 -1
- package/dist/graph/workspace-cli.d.ts +2 -0
- package/dist/graph/workspace-cli.d.ts.map +1 -1
- package/dist/graph/workspace-cli.js +3 -0
- package/dist/graph/workspace-cli.js.map +1 -1
- package/dist/graph/workspace.d.ts +2 -0
- package/dist/graph/workspace.d.ts.map +1 -1
- package/dist/graph/workspace.js +173 -6
- package/dist/graph/workspace.js.map +1 -1
- package/dist/hosts/antigravity.d.ts +2 -2
- package/dist/hosts/antigravity.d.ts.map +1 -1
- package/dist/hosts/codex-hooks.d.ts +2 -6
- package/dist/hosts/codex-hooks.d.ts.map +1 -1
- package/dist/hosts/codex-hooks.js +7 -30
- package/dist/hosts/codex-hooks.js.map +1 -1
- package/dist/hosts/config-write.d.ts +36 -0
- package/dist/hosts/config-write.d.ts.map +1 -0
- package/dist/hosts/config-write.js +63 -0
- package/dist/hosts/config-write.js.map +1 -0
- package/dist/hosts/cursor-hooks.d.ts +18 -0
- package/dist/hosts/cursor-hooks.d.ts.map +1 -0
- package/dist/hosts/cursor-hooks.js +100 -0
- package/dist/hosts/cursor-hooks.js.map +1 -0
- package/dist/hosts/init.d.ts +2 -2
- package/dist/hosts/init.d.ts.map +1 -1
- package/dist/hosts/init.js +8 -2
- package/dist/hosts/init.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts +15 -5
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +51 -15
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/plan.d.ts.map +1 -1
- package/dist/hosts/plan.js +2 -0
- package/dist/hosts/plan.js.map +1 -1
- package/dist/hosts/retract.d.ts +47 -0
- package/dist/hosts/retract.d.ts.map +1 -0
- package/dist/hosts/retract.js +473 -0
- package/dist/hosts/retract.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/ingest/fs.d.ts +39 -5
- package/dist/ingest/fs.d.ts.map +1 -1
- package/dist/ingest/fs.js +104 -11
- package/dist/ingest/fs.js.map +1 -1
- package/dist/mcp/tool-names.d.ts +20 -0
- package/dist/mcp/tool-names.d.ts.map +1 -0
- package/dist/mcp/tool-names.js +36 -0
- package/dist/mcp/tool-names.js.map +1 -0
- package/dist/mcp/tools.d.ts +11 -2
- package/dist/mcp/tools.d.ts.map +1 -1
- package/dist/mcp/tools.js +9 -21
- package/dist/mcp/tools.js.map +1 -1
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +17 -3
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/flush.d.ts.map +1 -1
- package/dist/telemetry/flush.js +1 -0
- package/dist/telemetry/flush.js.map +1 -1
- package/dist/telemetry/identity.d.ts +7 -0
- package/dist/telemetry/identity.d.ts.map +1 -1
- package/dist/telemetry/identity.js.map +1 -1
- package/dist/telemetry/index.d.ts +2 -2
- package/dist/telemetry/index.d.ts.map +1 -1
- package/dist/telemetry/index.js +2 -2
- package/dist/telemetry/index.js.map +1 -1
- package/dist/telemetry/sessions.d.ts +19 -2
- package/dist/telemetry/sessions.d.ts.map +1 -1
- package/dist/telemetry/sessions.js +57 -34
- package/dist/telemetry/sessions.js.map +1 -1
- package/dist/telemetry/track.d.ts +22 -3
- package/dist/telemetry/track.d.ts.map +1 -1
- package/dist/telemetry/track.js +34 -5
- package/dist/telemetry/track.js.map +1 -1
- package/dist/upkeep-run.js +1 -1
- package/dist/upkeep-run.js.map +1 -1
- package/dist/upkeep.d.ts +7 -4
- package/dist/upkeep.d.ts.map +1 -1
- package/dist/upkeep.js +2 -1
- package/dist/upkeep.js.map +1 -1
- package/dist/util/state.d.ts +12 -2
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js +4 -0
- package/dist/util/state.js.map +1 -1
- package/dist/viewer/app.js +2 -1
- package/dist/viewer/style.css +14 -0
- package/dist/viz/assemble.d.ts +17 -0
- package/dist/viz/assemble.d.ts.map +1 -1
- package/dist/viz/assemble.js.map +1 -1
- package/package.json +12 -4
- package/scripts/graph-quality.mjs +3 -3
- package/scripts/postinstall.mjs +42 -3
- package/scripts/run-tests.mjs +14 -0
- package/scripts/tally-audit.mjs +140 -0
- package/dist/ai/llm/claude-cli.d.ts +0 -60
- package/dist/ai/llm/claude-cli.d.ts.map +0 -1
- package/dist/ai/llm/claude-cli.js +0 -417
- package/dist/ai/llm/claude-cli.js.map +0 -1
- package/dist/util/num.d.ts +0 -3
- package/dist/util/num.d.ts.map +0 -1
- package/dist/util/num.js +0 -20
- package/dist/util/num.js.map +0 -1
package/dist/ask/ask.js
CHANGED
|
@@ -22,9 +22,10 @@ import { loadGraphCached, loadAskIndexCached } from "../graph/load.js";
|
|
|
22
22
|
import { assertPrefixIndexed, pathUnderPrefix, scopeLabel, scopeOf, scopesHereClause, scopesOfGraph, } from "../graph/scopes.js";
|
|
23
23
|
import { normalizePathPrefix } from "../util/paths.js";
|
|
24
24
|
import { resolveSymbol } from "../graph/traverse.js";
|
|
25
|
-
import { rankScopesAndFuse } from "./fuse.js";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { combineComparableScopes, rankScopesAndFuse, } from "./fuse.js";
|
|
26
|
+
import { rankFilesBounded, } from "./file-rank.js";
|
|
27
|
+
import { fileFirstRoundRobin, roundRobinQueues } from "./file-selection.js";
|
|
28
|
+
import { personalizedPageRank, personalizedPageRankPrepared, preparePageRankPartitions, } from "./graphrank.js";
|
|
28
29
|
import { readSourceFile } from "../util/source.js";
|
|
29
30
|
import { counts, tokenize } from "./index-file.js";
|
|
30
31
|
/** Max source lines to inline per hit — a definition longer than this is
|
|
@@ -141,6 +142,25 @@ function bm25(query, doc, idf, dl, avgdl) {
|
|
|
141
142
|
}
|
|
142
143
|
return s;
|
|
143
144
|
}
|
|
145
|
+
/** Exact query terms awarded by the existing lexical fields. Bounded ranking uses
|
|
146
|
+
* presence only: raw name/path/BM25 magnitudes cannot enter the file residual. */
|
|
147
|
+
function matchedLexicalTerms(query, name, path, body) {
|
|
148
|
+
const terms = new Set();
|
|
149
|
+
for (const term of query.keys())
|
|
150
|
+
if (name.has(term) || path.has(term) || body.has(term))
|
|
151
|
+
terms.add(term);
|
|
152
|
+
return terms;
|
|
153
|
+
}
|
|
154
|
+
/** NAME-only query terms behind file-level strong coverage. This intentionally
|
|
155
|
+
* mirrors {@link strongShare}'s plural folding while excluding file nodes at
|
|
156
|
+
* the call site. */
|
|
157
|
+
function matchedStrongTerms(query, name) {
|
|
158
|
+
const terms = new Set();
|
|
159
|
+
for (const term of query.keys())
|
|
160
|
+
if (hasTerm(name, term))
|
|
161
|
+
terms.add(term);
|
|
162
|
+
return terms;
|
|
163
|
+
}
|
|
144
164
|
// ── Structural intent ──────────────────────────────────────────────────────
|
|
145
165
|
const INCOMING = /\b(caller|callers|calls?\s+into|who\s+calls|what\s+calls|called\s+by|used\s+by|uses)\b/;
|
|
146
166
|
const OUTGOING = /\b(callee|callees|what\s+does\s+\w+\s+call|calls\s+what|imports?|depends\s+on)\b/;
|
|
@@ -283,7 +303,18 @@ function matchedIdfShare(q, fields, idf, dfltIdf) {
|
|
|
283
303
|
}
|
|
284
304
|
return total > 0 ? matched / total : 0;
|
|
285
305
|
}
|
|
286
|
-
|
|
306
|
+
/** Normalized query-term IDF mass used by file-level union coverage. Presence
|
|
307
|
+
* comes from the exact term-contribution maps, so plural folding cannot
|
|
308
|
+
* manufacture evidence that the lexical scorer did not award. */
|
|
309
|
+
function normalizedQueryWeights(q, idf, dfltIdf) {
|
|
310
|
+
let total = 0;
|
|
311
|
+
for (const t of q.keys())
|
|
312
|
+
total += idf.get(t) ?? dfltIdf;
|
|
313
|
+
if (total <= 0)
|
|
314
|
+
return new Map();
|
|
315
|
+
return new Map([...q.keys()].map((term) => [term, (idf.get(term) ?? dfltIdf) / total]));
|
|
316
|
+
}
|
|
317
|
+
function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true, fileComplement = false, fileTopLock = false, includeRankingMetadata = false) {
|
|
287
318
|
// Binary query terms: a word repeated in a pasted issue body counts once, so a
|
|
288
319
|
// ranty description can't linearly amplify an incidental word.
|
|
289
320
|
const q = new Map([...counts(tokenize(query)).keys()].map((t) => [t, 1]));
|
|
@@ -421,6 +452,34 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
421
452
|
};
|
|
422
453
|
const docsById = new Map(symbolDocs.map((d) => [d.n.id, d]));
|
|
423
454
|
const symbolHits = [];
|
|
455
|
+
const baselineSymbolHits = [];
|
|
456
|
+
const baselineHitById = new Map();
|
|
457
|
+
const fileQueueHitsByGroup = new Map();
|
|
458
|
+
const baselineQueueHitsByGroup = new Map();
|
|
459
|
+
const fileGroups = [];
|
|
460
|
+
const needsFileQueues = fileComplement && (fileTopLock || includeRankingMetadata);
|
|
461
|
+
const makeSymbolHit = (id, hitScore, scope) => {
|
|
462
|
+
const n = byId.get(id);
|
|
463
|
+
if (!n)
|
|
464
|
+
return null;
|
|
465
|
+
const hit = {
|
|
466
|
+
kind: "symbol",
|
|
467
|
+
title: `${n.name} · ${n.kind}`,
|
|
468
|
+
pointer: n.kind === "file" ? n.path : `${n.path}:${n.span}`,
|
|
469
|
+
snippet: n.summary?.split("\n")[0].trim() ?? n.signature ?? "",
|
|
470
|
+
score: hitScore,
|
|
471
|
+
...(scope === undefined ? {} : { scope }),
|
|
472
|
+
};
|
|
473
|
+
const d = docsById.get(id);
|
|
474
|
+
matchedOf.set(hit, d ? matchedIdfShare(q, [d.name, d.path, d.body], idf, dfltIdf) : 0);
|
|
475
|
+
matchedStrongOf.set(hit, d ? strongShare(q, n, d, idf, dfltIdf) : 0);
|
|
476
|
+
selectionGroupOf.set(hit, `file:${n.path}`);
|
|
477
|
+
return hit;
|
|
478
|
+
};
|
|
479
|
+
const symbolTitle = (id) => {
|
|
480
|
+
const n = byId.get(id);
|
|
481
|
+
return n ? `${n.name} · ${n.kind}` : id;
|
|
482
|
+
};
|
|
424
483
|
// The single-vs-multi-scope branch keys on `scopesOfGraph` ONLY: one scope
|
|
425
484
|
// (or no graph) keeps the original local scoring path.
|
|
426
485
|
const scopes = graph ? scopesOfGraph(graph) : null;
|
|
@@ -435,6 +494,18 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
435
494
|
: symbolDocs.length
|
|
436
495
|
? symbolDocs.reduce((a, d) => a + bodyLen(d.body), 0) / symbolDocs.length
|
|
437
496
|
: 0;
|
|
497
|
+
// Populated only for bounded file ranking. The baseline path pays no allocation
|
|
498
|
+
// or matched-term tracking cost, and their scalar lexical path remains exact.
|
|
499
|
+
const rawLexicalById = new Map();
|
|
500
|
+
const matchedTermsById = new Map();
|
|
501
|
+
const matchedStrongTermsById = new Map();
|
|
502
|
+
const fileQueryWeights = fileComplement
|
|
503
|
+
? normalizedQueryWeights(q, idf, dfltIdf)
|
|
504
|
+
: new Map();
|
|
505
|
+
const spanStart = (span) => {
|
|
506
|
+
const match = span.match(/^L(\d+)-L\d+$/);
|
|
507
|
+
return match ? Number(match[1]) : Number.POSITIVE_INFINITY;
|
|
508
|
+
};
|
|
438
509
|
if (scopes && scopes.length > 1) {
|
|
439
510
|
// ── Multi-scope repo: partition by sub-project and walk each subgraph
|
|
440
511
|
// separately — so the big scope can't drown the small one — but score every
|
|
@@ -442,14 +513,25 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
442
513
|
// can be ordered by score. Same scoring functions and blend as the
|
|
443
514
|
// single-scope path; the combination itself lives in fuse.ts. ──
|
|
444
515
|
const byScope = new Map();
|
|
516
|
+
const scopeById = new Map();
|
|
445
517
|
for (const d of symbolDocs) {
|
|
446
518
|
const s = scopeOf(d.n.path, scopes).prefix;
|
|
519
|
+
scopeById.set(d.n.id, s);
|
|
447
520
|
const list = byScope.get(s);
|
|
448
521
|
if (list)
|
|
449
522
|
list.push(d);
|
|
450
523
|
else
|
|
451
524
|
byScope.set(s, [d]);
|
|
452
525
|
}
|
|
526
|
+
// Lazily build every scope's PageRank topology in one node/edge pass. The
|
|
527
|
+
// old nodeFilter path rebuilt the full repo adjacency once per matching
|
|
528
|
+
// scope; on large multi-project repos that turned one query into hundreds
|
|
529
|
+
// of full graph scans before the participation gate could run. Waiting for
|
|
530
|
+
// the first walk keeps no-match and graphRank:false queries edge-scan free.
|
|
531
|
+
let pageRankByScope;
|
|
532
|
+
let collapsedComponents = [];
|
|
533
|
+
let collapsedComponentById;
|
|
534
|
+
let collapsedFileByRepresentative;
|
|
453
535
|
const fusion = rankScopesAndFuse([...byScope.keys()].sort(), {
|
|
454
536
|
lex: (s) => {
|
|
455
537
|
const docs = byScope.get(s);
|
|
@@ -461,12 +543,20 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
461
543
|
// per-scope now; the statistics are global.
|
|
462
544
|
const out = new Map();
|
|
463
545
|
for (const { n, name, path, body } of docs) {
|
|
546
|
+
const factor = testFactor(n.path);
|
|
464
547
|
const total = (score(q, name, idf) * 3 +
|
|
465
548
|
score(q, path, idf) * 2 +
|
|
466
549
|
bm25(q, body, idf, bodyLen(body), avgBodyLen)) *
|
|
467
|
-
|
|
468
|
-
if (total > 0)
|
|
550
|
+
factor;
|
|
551
|
+
if (total > 0) {
|
|
469
552
|
out.set(n.id, total);
|
|
553
|
+
if (fileComplement) {
|
|
554
|
+
rawLexicalById.set(n.id, total);
|
|
555
|
+
matchedTermsById.set(n.id, matchedLexicalTerms(q, name, path, body));
|
|
556
|
+
if (n.kind !== "file")
|
|
557
|
+
matchedStrongTermsById.set(n.id, matchedStrongTerms(q, name));
|
|
558
|
+
}
|
|
559
|
+
}
|
|
470
560
|
}
|
|
471
561
|
return out;
|
|
472
562
|
},
|
|
@@ -478,36 +568,121 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
478
568
|
return true;
|
|
479
569
|
return false;
|
|
480
570
|
}),
|
|
481
|
-
walk: (s, seeds) =>
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
571
|
+
walk: (s, seeds) => {
|
|
572
|
+
if (!graphRank)
|
|
573
|
+
return new Map();
|
|
574
|
+
pageRankByScope ??= preparePageRankPartitions(graph, (id) => scopeById.get(id));
|
|
575
|
+
const topology = pageRankByScope?.get(s);
|
|
576
|
+
return topology
|
|
577
|
+
? personalizedPageRankPrepared(topology, seeds)
|
|
578
|
+
: new Map();
|
|
579
|
+
},
|
|
489
580
|
rankFactor: (_s, id) => testFactor(byId.get(id)?.path ?? ""),
|
|
581
|
+
collapseCandidates: fileComplement
|
|
582
|
+
? (candidates) => {
|
|
583
|
+
collapsedComponents = [...candidates];
|
|
584
|
+
collapsedComponentById = new Map(candidates.map((candidate) => [candidate.id, candidate]));
|
|
585
|
+
const fileCandidates = candidates.map((candidate) => {
|
|
586
|
+
const n = byId.get(candidate.id);
|
|
587
|
+
const rawLexical = rawLexicalById.get(candidate.id) ?? 0;
|
|
588
|
+
return {
|
|
589
|
+
id: candidate.id,
|
|
590
|
+
file: n?.path ?? "",
|
|
591
|
+
kind: n?.kind === "file" ? "file" : "symbol",
|
|
592
|
+
rawLexical,
|
|
593
|
+
lexical: candidate.lexical,
|
|
594
|
+
graph: candidate.graph,
|
|
595
|
+
rankFactor: candidate.rankFactor,
|
|
596
|
+
baselineScore: candidate.score,
|
|
597
|
+
baselineTieKey: symbolTitle(candidate.id),
|
|
598
|
+
matchedTerms: matchedTermsById.get(candidate.id) ?? new Set(),
|
|
599
|
+
matchedStrongTerms: matchedStrongTermsById.get(candidate.id) ?? new Set(),
|
|
600
|
+
eligible: Boolean(n && n.kind !== "file" && rawLexical > 0),
|
|
601
|
+
spanStart: n ? spanStart(n.span) : undefined,
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
const rankedFiles = rankFilesBounded(fileCandidates, fileQueryWeights);
|
|
605
|
+
collapsedFileByRepresentative = new Map(rankedFiles.map((file) => [file.representative.id, file]));
|
|
606
|
+
return rankedFiles.map((file) => {
|
|
607
|
+
const component = collapsedComponentById.get(file.representative.id);
|
|
608
|
+
return {
|
|
609
|
+
id: file.representative.id,
|
|
610
|
+
scope: component.scope,
|
|
611
|
+
score: file.score,
|
|
612
|
+
};
|
|
613
|
+
});
|
|
614
|
+
}
|
|
615
|
+
: undefined,
|
|
490
616
|
}, GRAPH_WEIGHT, RESCUE_FLOOR);
|
|
491
|
-
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
617
|
+
if (needsFileQueues) {
|
|
618
|
+
const baselineFusion = combineComparableScopes(collapsedComponents.map(({ id, scope, score }) => ({ id, scope, score })));
|
|
619
|
+
const baselineScoreById = new Map(baselineFusion.ranked.map((ranked) => [ranked.id, ranked.score]));
|
|
620
|
+
const baselineDisplayOrder = (a, b) => b.score - a.score || symbolTitle(a.id).localeCompare(symbolTitle(b.id));
|
|
621
|
+
let baselineTopRanked = baselineFusion.ranked[0];
|
|
622
|
+
for (const ranked of baselineFusion.ranked.slice(1))
|
|
623
|
+
if (baselineTopRanked && baselineDisplayOrder(ranked, baselineTopRanked) < 0)
|
|
624
|
+
baselineTopRanked = ranked;
|
|
625
|
+
const baselineHitsToBuild = includeRankingMetadata
|
|
626
|
+
? [...baselineFusion.ranked].sort(baselineDisplayOrder)
|
|
627
|
+
: baselineTopRanked
|
|
628
|
+
? [baselineTopRanked]
|
|
629
|
+
: [];
|
|
630
|
+
for (const ranked of baselineHitsToBuild) {
|
|
631
|
+
const hit = makeSymbolHit(ranked.id, ranked.score, ranked.scope);
|
|
632
|
+
if (hit) {
|
|
633
|
+
baselineSymbolHits.push(hit);
|
|
634
|
+
baselineHitById.set(ranked.id, hit);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
for (const ranked of fusion.ranked) {
|
|
638
|
+
const file = collapsedFileByRepresentative?.get(ranked.id);
|
|
639
|
+
if (!file)
|
|
640
|
+
continue;
|
|
641
|
+
const materializeFileQueue = () => file.queue.flatMap((member, index) => {
|
|
642
|
+
const component = collapsedComponentById?.get(member.id);
|
|
643
|
+
if (index > 0) {
|
|
644
|
+
const baselineHit = baselineHitById.get(member.id);
|
|
645
|
+
if (baselineHit)
|
|
646
|
+
return [baselineHit];
|
|
647
|
+
}
|
|
648
|
+
const hit = makeSymbolHit(member.id, index === 0
|
|
649
|
+
? ranked.score
|
|
650
|
+
: baselineScoreById.get(member.id) ?? member.baselineScore, component?.scope ?? ranked.scope);
|
|
651
|
+
return hit ? [hit] : [];
|
|
652
|
+
});
|
|
653
|
+
const materializeBaselineQueue = () => file.queue.flatMap((member) => {
|
|
654
|
+
const hit = makeSymbolHit(member.id, baselineScoreById.get(member.id) ?? member.baselineScore, collapsedComponentById?.get(member.id)?.scope ?? ranked.scope);
|
|
655
|
+
return hit ? [hit] : [];
|
|
656
|
+
});
|
|
657
|
+
const hits = includeRankingMetadata
|
|
658
|
+
? materializeFileQueue()
|
|
659
|
+
: (() => {
|
|
660
|
+
const leader = file.queue[0];
|
|
661
|
+
if (!leader)
|
|
662
|
+
return [];
|
|
663
|
+
const hit = makeSymbolHit(leader.id, ranked.score, collapsedComponentById?.get(leader.id)?.scope ?? ranked.scope);
|
|
664
|
+
return hit ? [hit] : [];
|
|
665
|
+
})();
|
|
666
|
+
if (hits.length === 0)
|
|
667
|
+
continue;
|
|
668
|
+
const group = {
|
|
669
|
+
key: `file:${file.file}`,
|
|
670
|
+
hits,
|
|
671
|
+
coverage: file.unionCoverage,
|
|
672
|
+
coverageStrong: file.unionStrongCoverage,
|
|
673
|
+
};
|
|
674
|
+
fileQueueHitsByGroup.set(group.key, materializeFileQueue);
|
|
675
|
+
baselineQueueHitsByGroup.set(group.key, materializeBaselineQueue);
|
|
676
|
+
fileGroups.push(group);
|
|
677
|
+
symbolHits.push(hits[0]);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
for (const ranked of fusion.ranked) {
|
|
682
|
+
const hit = makeSymbolHit(ranked.id, ranked.score, ranked.scope);
|
|
683
|
+
if (hit)
|
|
684
|
+
symbolHits.push(hit);
|
|
685
|
+
}
|
|
511
686
|
}
|
|
512
687
|
// Label + footer only when federation actually happened (or a scope was
|
|
513
688
|
// gated out and is worth mentioning) — a query matching one scope of a
|
|
@@ -525,13 +700,20 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
525
700
|
for (const { n, name, path, body } of symbolDocs) {
|
|
526
701
|
// Name and path are short identifiers → plain idf-weighted overlap; the body
|
|
527
702
|
// is length-normalized via BM25 so long definitions don't win on bulk.
|
|
703
|
+
const factor = testFactor(n.path);
|
|
528
704
|
const total = (score(q, name, idf) * 3 +
|
|
529
705
|
score(q, path, idf) * 2 +
|
|
530
706
|
bm25(q, body, idf, bodyLen(body), avgBodyLen)) *
|
|
531
|
-
|
|
707
|
+
factor;
|
|
532
708
|
if (total > 0) {
|
|
533
709
|
lex.set(n.id, total);
|
|
534
710
|
maxLex = Math.max(maxLex, total);
|
|
711
|
+
if (fileComplement) {
|
|
712
|
+
rawLexicalById.set(n.id, total);
|
|
713
|
+
matchedTermsById.set(n.id, matchedLexicalTerms(q, name, path, body));
|
|
714
|
+
if (n.kind !== "file")
|
|
715
|
+
matchedStrongTermsById.set(n.id, matchedStrongTerms(q, name));
|
|
716
|
+
}
|
|
535
717
|
}
|
|
536
718
|
}
|
|
537
719
|
// ── Graph-rank: let connectivity to the matched set reorder and rescue ──
|
|
@@ -547,31 +729,115 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
547
729
|
for (const [id, p] of pr)
|
|
548
730
|
if (p >= RESCUE_FLOOR)
|
|
549
731
|
candidates.add(id);
|
|
732
|
+
const singleCandidates = [];
|
|
550
733
|
for (const id of candidates) {
|
|
551
734
|
const n = byId.get(id);
|
|
552
735
|
if (!n)
|
|
553
736
|
continue;
|
|
554
|
-
const
|
|
737
|
+
const lexical = maxLex > 0 ? (lex.get(id) ?? 0) / maxLex : 0;
|
|
738
|
+
const graphScore = pr.get(id) ?? 0;
|
|
739
|
+
const rankFactor = testFactor(n.path);
|
|
555
740
|
// Apply the test penalty after normalization too. Applying it only to the
|
|
556
741
|
// raw lexical score is not enough: if a test is still the strongest raw
|
|
557
742
|
// match, dividing by maxLex restores it to 1.0 and erases the de-rank.
|
|
558
|
-
const
|
|
559
|
-
if (
|
|
743
|
+
const baseline = (lexical + GRAPH_WEIGHT * graphScore) * rankFactor;
|
|
744
|
+
if (baseline <= 0)
|
|
560
745
|
continue;
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
746
|
+
singleCandidates.push({ id, n, lexical, graph: graphScore, rankFactor, baseline });
|
|
747
|
+
}
|
|
748
|
+
const candidateById = fileComplement
|
|
749
|
+
? new Map(singleCandidates.map((candidate) => [candidate.id, candidate]))
|
|
750
|
+
: undefined;
|
|
751
|
+
const rankedFiles = fileComplement
|
|
752
|
+
? rankFilesBounded(singleCandidates.map((candidate) => {
|
|
753
|
+
const rawLexical = rawLexicalById.get(candidate.id) ?? 0;
|
|
754
|
+
return {
|
|
755
|
+
id: candidate.id,
|
|
756
|
+
file: candidate.n.path,
|
|
757
|
+
kind: candidate.n.kind === "file" ? "file" : "symbol",
|
|
758
|
+
rawLexical,
|
|
759
|
+
lexical: candidate.lexical,
|
|
760
|
+
graph: candidate.graph,
|
|
761
|
+
rankFactor: candidate.rankFactor,
|
|
762
|
+
baselineScore: candidate.baseline,
|
|
763
|
+
baselineTieKey: symbolTitle(candidate.id),
|
|
764
|
+
matchedTerms: matchedTermsById.get(candidate.id) ?? new Set(),
|
|
765
|
+
matchedStrongTerms: matchedStrongTermsById.get(candidate.id) ?? new Set(),
|
|
766
|
+
eligible: candidate.n.kind !== "file" && rawLexical > 0,
|
|
767
|
+
spanStart: spanStart(candidate.n.span),
|
|
768
|
+
};
|
|
769
|
+
}), fileQueryWeights)
|
|
770
|
+
: undefined;
|
|
771
|
+
if (needsFileQueues && rankedFiles) {
|
|
772
|
+
const baselineCandidateOrder = (a, b) => b.baseline - a.baseline ||
|
|
773
|
+
`${a.n.name} · ${a.n.kind}`.localeCompare(`${b.n.name} · ${b.n.kind}`);
|
|
774
|
+
let baselineTopCandidate = singleCandidates[0];
|
|
775
|
+
for (const candidate of singleCandidates.slice(1))
|
|
776
|
+
if (baselineTopCandidate && baselineCandidateOrder(candidate, baselineTopCandidate) < 0)
|
|
777
|
+
baselineTopCandidate = candidate;
|
|
778
|
+
const baselineCandidatesToBuild = includeRankingMetadata
|
|
779
|
+
? [...singleCandidates].sort(baselineCandidateOrder)
|
|
780
|
+
: baselineTopCandidate
|
|
781
|
+
? [baselineTopCandidate]
|
|
782
|
+
: [];
|
|
783
|
+
for (const candidate of baselineCandidatesToBuild) {
|
|
784
|
+
const hit = makeSymbolHit(candidate.id, candidate.baseline);
|
|
785
|
+
if (hit) {
|
|
786
|
+
baselineSymbolHits.push(hit);
|
|
787
|
+
baselineHitById.set(candidate.id, hit);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
for (const file of rankedFiles) {
|
|
791
|
+
const materializeFileQueue = () => file.queue.flatMap((member, index) => {
|
|
792
|
+
if (index > 0) {
|
|
793
|
+
const baselineHit = baselineHitById.get(member.id);
|
|
794
|
+
if (baselineHit)
|
|
795
|
+
return [baselineHit];
|
|
796
|
+
}
|
|
797
|
+
const hit = makeSymbolHit(member.id, index === 0 ? file.score : member.baselineScore);
|
|
798
|
+
return hit ? [hit] : [];
|
|
799
|
+
});
|
|
800
|
+
const materializeBaselineQueue = () => file.queue.flatMap((member) => {
|
|
801
|
+
const hit = makeSymbolHit(member.id, member.baselineScore);
|
|
802
|
+
return hit ? [hit] : [];
|
|
803
|
+
});
|
|
804
|
+
const hits = includeRankingMetadata
|
|
805
|
+
? materializeFileQueue()
|
|
806
|
+
: (() => {
|
|
807
|
+
const leader = file.queue[0];
|
|
808
|
+
if (!leader)
|
|
809
|
+
return [];
|
|
810
|
+
const hit = makeSymbolHit(leader.id, file.score);
|
|
811
|
+
return hit ? [hit] : [];
|
|
812
|
+
})();
|
|
813
|
+
if (hits.length === 0)
|
|
814
|
+
continue;
|
|
815
|
+
const group = {
|
|
816
|
+
key: `file:${file.file}`,
|
|
817
|
+
hits,
|
|
818
|
+
coverage: file.unionCoverage,
|
|
819
|
+
coverageStrong: file.unionStrongCoverage,
|
|
820
|
+
};
|
|
821
|
+
fileQueueHitsByGroup.set(group.key, materializeFileQueue);
|
|
822
|
+
baselineQueueHitsByGroup.set(group.key, materializeBaselineQueue);
|
|
823
|
+
fileGroups.push(group);
|
|
824
|
+
symbolHits.push(hits[0]);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
else if (rankedFiles) {
|
|
828
|
+
for (const file of rankedFiles) {
|
|
829
|
+
const candidate = candidateById?.get(file.representative.id);
|
|
830
|
+
const hit = candidate ? makeSymbolHit(candidate.id, file.score) : null;
|
|
831
|
+
if (hit)
|
|
832
|
+
symbolHits.push(hit);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
else {
|
|
836
|
+
for (const candidate of singleCandidates) {
|
|
837
|
+
const hit = makeSymbolHit(candidate.id, candidate.baseline);
|
|
838
|
+
if (hit)
|
|
839
|
+
symbolHits.push(hit);
|
|
840
|
+
}
|
|
575
841
|
}
|
|
576
842
|
}
|
|
577
843
|
// Concepts and symbols live on comparable 0..~1.5 scales so they merge fairly:
|
|
@@ -581,17 +847,100 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
581
847
|
h.score = maxConcept > 0 ? h.score / maxConcept : 0;
|
|
582
848
|
const scored = [...conceptHits, ...symbolHits];
|
|
583
849
|
scored.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
// A workspace child defers this step until cross-repo fusion has established
|
|
587
|
-
// the final authoritative ranking.
|
|
588
|
-
const selected = fileFirst
|
|
589
|
-
? fileFirstRoundRobin(scored.map((hit, index) => ({
|
|
590
|
-
group: selectionGroupOf.get(hit) ?? `ungrouped:${index}`,
|
|
591
|
-
value: hit,
|
|
592
|
-
})), limit)
|
|
850
|
+
const baselineScored = needsFileQueues
|
|
851
|
+
? [...conceptHits, ...baselineSymbolHits].sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
|
|
593
852
|
: scored;
|
|
853
|
+
const conceptGroups = conceptHits.map((hit, index) => ({
|
|
854
|
+
key: selectionGroupOf.get(hit) ?? `concept:${index}`,
|
|
855
|
+
hits: [hit],
|
|
856
|
+
coverage: matchedOf.get(hit) ?? 0,
|
|
857
|
+
coverageStrong: matchedStrongOf.get(hit) ?? 0,
|
|
858
|
+
}));
|
|
859
|
+
const unlockedGroups = [...conceptGroups, ...fileGroups].sort((a, b) => b.hits[0].score - a.hits[0].score ||
|
|
860
|
+
a.hits[0].title.localeCompare(b.hits[0].title));
|
|
861
|
+
const sameHit = (a, b) => a.kind === b.kind && a.pointer === b.pointer && a.title === b.title;
|
|
862
|
+
let projectedGroups = unlockedGroups;
|
|
863
|
+
let lockedGroupKey;
|
|
864
|
+
if (fileTopLock && fileFirst && baselineScored[0]) {
|
|
865
|
+
const baselineTop = baselineScored[0];
|
|
866
|
+
const key = selectionGroupOf.get(baselineTop) ?? "locked:top";
|
|
867
|
+
lockedGroupKey = key;
|
|
868
|
+
const existing = unlockedGroups.find((group) => group.key === key);
|
|
869
|
+
const baselineQueue = includeRankingMetadata || limit > unlockedGroups.length
|
|
870
|
+
? baselineQueueHitsByGroup.get(key)?.() ?? baselineScored.filter((hit) => (selectionGroupOf.get(hit) ?? "") === key)
|
|
871
|
+
: [];
|
|
872
|
+
const locked = existing
|
|
873
|
+
? {
|
|
874
|
+
...existing,
|
|
875
|
+
hits: [
|
|
876
|
+
baselineTop,
|
|
877
|
+
...baselineQueue.filter((hit) => !sameHit(hit, baselineTop)),
|
|
878
|
+
],
|
|
879
|
+
}
|
|
880
|
+
: {
|
|
881
|
+
key,
|
|
882
|
+
hits: baselineQueue.length ? baselineQueue : [baselineTop],
|
|
883
|
+
coverage: matchedOf.get(baselineTop) ?? 0,
|
|
884
|
+
coverageStrong: matchedStrongOf.get(baselineTop) ?? 0,
|
|
885
|
+
};
|
|
886
|
+
projectedGroups = [
|
|
887
|
+
locked,
|
|
888
|
+
...unlockedGroups.filter((group) => group.key !== key),
|
|
889
|
+
];
|
|
890
|
+
}
|
|
891
|
+
// Ranking always sees the complete candidate universe, but a normal bounded
|
|
892
|
+
// request usually needs only one leader per file. Materialize secondary span
|
|
893
|
+
// queues only when the requested output can actually reach a second round.
|
|
894
|
+
if (fileTopLock &&
|
|
895
|
+
fileFirst &&
|
|
896
|
+
!includeRankingMetadata &&
|
|
897
|
+
limit > projectedGroups.length) {
|
|
898
|
+
projectedGroups = projectedGroups.map((group) => {
|
|
899
|
+
if (group.key === lockedGroupKey)
|
|
900
|
+
return group;
|
|
901
|
+
const materialize = fileQueueHitsByGroup.get(group.key);
|
|
902
|
+
return materialize ? { ...group, hits: materialize() } : group;
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
// File-aware ranking feeds latent groups to the existing file-first frontier.
|
|
906
|
+
// Leaders are listed first so every file/concept emits once before any
|
|
907
|
+
// secondary span; each group's remaining queue retains baseline order.
|
|
908
|
+
const selected = fileTopLock && fileFirst
|
|
909
|
+
? roundRobinQueues(projectedGroups.map((group) => group.hits), limit)
|
|
910
|
+
: fileFirst
|
|
911
|
+
? fileFirstRoundRobin(scored.map((hit, index) => ({
|
|
912
|
+
group: selectionGroupOf.get(hit) ?? `ungrouped:${index}`,
|
|
913
|
+
value: hit,
|
|
914
|
+
})), limit)
|
|
915
|
+
: scored;
|
|
594
916
|
const top = selected[0] ?? scored[0];
|
|
917
|
+
if (fileTopLock && top?.scope && scopeMeta) {
|
|
918
|
+
scopeMeta = {
|
|
919
|
+
federated: [...new Set([top.scope, ...scopeMeta.federated])],
|
|
920
|
+
alsoMatched: scopeMeta.alsoMatched.filter((match) => match.scope !== top.scope),
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
const ranking = includeRankingMetadata && needsFileQueues
|
|
924
|
+
? {
|
|
925
|
+
groups: unlockedGroups.slice(0, limit).map((group) => ({
|
|
926
|
+
...group,
|
|
927
|
+
hits: group.hits.slice(0, limit),
|
|
928
|
+
baselineHits: baselineScored
|
|
929
|
+
.filter((hit) => selectionGroupOf.get(hit) === group.key)
|
|
930
|
+
.slice(0, limit),
|
|
931
|
+
})),
|
|
932
|
+
baseline: baselineScored.slice(0, limit).map((hit, index) => ({
|
|
933
|
+
group: selectionGroupOf.get(hit) ?? `ungrouped:${index}`,
|
|
934
|
+
hit,
|
|
935
|
+
})),
|
|
936
|
+
baselineCoverage: baselineScored[0]
|
|
937
|
+
? matchedOf.get(baselineScored[0]) ?? 0
|
|
938
|
+
: undefined,
|
|
939
|
+
baselineCoverageStrong: baselineScored[0]
|
|
940
|
+
? matchedStrongOf.get(baselineScored[0]) ?? 0
|
|
941
|
+
: undefined,
|
|
942
|
+
}
|
|
943
|
+
: undefined;
|
|
595
944
|
return {
|
|
596
945
|
query,
|
|
597
946
|
mode: scored.length ? "lexical" : "empty",
|
|
@@ -599,6 +948,7 @@ function lexical(query, corpus, limit, graphRank, inPrefix, fileFirst = true) {
|
|
|
599
948
|
scopes: scopeMeta,
|
|
600
949
|
coverage: top && q.size > 0 ? matchedOf.get(top) ?? 0 : undefined,
|
|
601
950
|
coverageStrong: top && q.size > 0 ? matchedStrongOf.get(top) ?? 0 : undefined,
|
|
951
|
+
...(ranking ? { ranking } : {}),
|
|
602
952
|
// Zero hits on a genuinely multi-scope graph names the scopes that exist,
|
|
603
953
|
// so a query that missed everywhere still tells the caller where to look.
|
|
604
954
|
note: scored.length
|
|
@@ -693,6 +1043,12 @@ export function ask(dir, query, opts = {}) {
|
|
|
693
1043
|
const limit = opts.limit ?? 8;
|
|
694
1044
|
const corpus = loadCorpus(outDir);
|
|
695
1045
|
const graphRank = opts.graphRank ?? true;
|
|
1046
|
+
const fileFirst = opts.fileFirst ?? true;
|
|
1047
|
+
// The production path uses bounded file scoring plus an exact baseline top lock.
|
|
1048
|
+
// Internal/raw callers can still request the baseline by disabling
|
|
1049
|
+
// file-first selection. A top lock is projected only on a file-first list.
|
|
1050
|
+
const fileTopLock = opts.fileTopLock ?? fileFirst;
|
|
1051
|
+
const fileComplement = (opts.fileComplement ?? false) || fileTopLock;
|
|
696
1052
|
// Normalized ONCE, up front, and this value used everywhere below
|
|
697
1053
|
// (validation, filtering, structural). `ask --in frontend/` must work exactly
|
|
698
1054
|
// like `--in frontend` — `scopeLabel`/the footer's own `--in <scope>/`
|
|
@@ -710,7 +1066,7 @@ export function ask(dir, query, opts = {}) {
|
|
|
710
1066
|
result = outcome.result;
|
|
711
1067
|
}
|
|
712
1068
|
else {
|
|
713
|
-
result = lexical(query, corpus, limit, graphRank, inPrefix, opts.
|
|
1069
|
+
result = lexical(query, corpus, limit, graphRank, inPrefix, fileFirst, fileComplement, fileTopLock, opts.includeRankingMetadata ?? false);
|
|
714
1070
|
// A structural-intent query that couldn't be answered structurally still
|
|
715
1071
|
// gets a prominent note on the lexical fallback result — never silent.
|
|
716
1072
|
if (outcome && "fallthroughNote" in outcome) {
|
|
@@ -719,10 +1075,18 @@ export function ask(dir, query, opts = {}) {
|
|
|
719
1075
|
}
|
|
720
1076
|
}
|
|
721
1077
|
else {
|
|
722
|
-
result = lexical(query, corpus, limit, graphRank, inPrefix, opts.
|
|
1078
|
+
result = lexical(query, corpus, limit, graphRank, inPrefix, fileFirst, fileComplement, fileTopLock, opts.includeRankingMetadata ?? false);
|
|
723
1079
|
}
|
|
724
1080
|
if (opts.source) {
|
|
725
1081
|
inlineSource(root, result.hits, corpus.graph, opts.full ?? false);
|
|
1082
|
+
if (result.ranking) {
|
|
1083
|
+
const internalHits = [
|
|
1084
|
+
...result.ranking.groups.flatMap((group) => group.hits),
|
|
1085
|
+
...result.ranking.groups.flatMap((group) => group.baselineHits ?? []),
|
|
1086
|
+
...result.ranking.baseline.map((entry) => entry.hit),
|
|
1087
|
+
];
|
|
1088
|
+
inlineSource(root, [...new Set(internalHits)], corpus.graph, opts.full ?? false);
|
|
1089
|
+
}
|
|
726
1090
|
// The pack is truly substitutive only in retriever mode (spans inlined), so
|
|
727
1091
|
// the "vs reading whole files" estimate is only honest here.
|
|
728
1092
|
result.saved = baselineFor(result.hits, corpus.graph);
|