@nanonets/graft 0.12.1 → 0.15.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 +20 -7
- package/dist/ai/crux.d.ts.map +1 -1
- package/dist/ai/crux.js +22 -1
- package/dist/ai/crux.js.map +1 -1
- package/dist/ai/llm/factory.d.ts +5 -1
- package/dist/ai/llm/factory.d.ts.map +1 -1
- package/dist/ai/llm/factory.js +8 -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/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 +2 -0
- 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 +129 -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 +42 -0
- package/dist/ask/ask.d.ts.map +1 -1
- package/dist/ask/ask.js +442 -57
- 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 +22 -0
- package/dist/ask/file-selection.d.ts.map +1 -0
- package/dist/ask/file-selection.js +66 -0
- package/dist/ask/file-selection.js.map +1 -0
- 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 +16 -1
- 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/evidence.d.ts +51 -0
- package/dist/blast/evidence.d.ts.map +1 -0
- package/dist/blast/evidence.js +162 -0
- package/dist/blast/evidence.js.map +1 -0
- 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 +11 -2
- package/dist/blast/render.d.ts.map +1 -1
- package/dist/blast/render.js +125 -4
- package/dist/blast/render.js.map +1 -1
- package/dist/blast/viz.d.ts +20 -0
- package/dist/blast/viz.d.ts.map +1 -1
- package/dist/blast/viz.js +13 -157
- 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 +99 -11
- package/dist/claude/hooks.js.map +1 -1
- package/dist/claude/settings-merge.d.ts +11 -0
- package/dist/claude/settings-merge.d.ts.map +1 -1
- package/dist/claude/settings-merge.js +24 -10
- package/dist/claude/settings-merge.js.map +1 -1
- package/dist/claude/state.d.ts +14 -1
- package/dist/claude/state.d.ts.map +1 -1
- package/dist/claude/state.js +1 -1
- package/dist/claude/state.js.map +1 -1
- package/dist/claude/stats.d.ts.map +1 -1
- package/dist/claude/stats.js +2 -1
- package/dist/claude/stats.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/sync-run.d.ts.map +1 -1
- package/dist/claude/sync-run.js +10 -2
- package/dist/claude/sync-run.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 +138 -1
- package/dist/cli.js.map +1 -1
- package/dist/context/build.d.ts.map +1 -1
- package/dist/context/build.js +23 -4
- package/dist/context/build.js.map +1 -1
- package/dist/context/check.d.ts.map +1 -1
- package/dist/context/check.js +2 -1
- 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 +37 -2
- package/dist/context/node-file.js.map +1 -1
- package/dist/engine.d.ts +2 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +1 -0
- package/dist/engine.js.map +1 -1
- package/dist/graph/bindings.d.ts +25 -3
- package/dist/graph/bindings.d.ts.map +1 -1
- package/dist/graph/bindings.js +168 -2
- 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/check.d.ts +3 -0
- package/dist/graph/check.d.ts.map +1 -1
- package/dist/graph/check.js +58 -11
- package/dist/graph/check.js.map +1 -1
- package/dist/graph/enrich.d.ts.map +1 -1
- package/dist/graph/enrich.js +29 -9
- package/dist/graph/enrich.js.map +1 -1
- package/dist/graph/extract.d.ts +22 -2
- package/dist/graph/extract.d.ts.map +1 -1
- package/dist/graph/extract.js +1444 -44
- 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.map +1 -1
- package/dist/graph/generic.js +39 -7
- 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/queries/dart.scm +42 -0
- package/dist/graph/queries/lua.scm +36 -0
- package/dist/graph/queries/nix.scm +32 -0
- 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 +211 -26
- 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/traverse-cli.d.ts +13 -2
- package/dist/graph/traverse-cli.d.ts.map +1 -1
- package/dist/graph/traverse-cli.js +25 -4
- package/dist/graph/traverse-cli.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.map +1 -1
- package/dist/graph/workspace.js +26 -2
- package/dist/graph/workspace.js.map +1 -1
- package/dist/hosts/mcp-config.d.ts +12 -0
- package/dist/hosts/mcp-config.d.ts.map +1 -1
- package/dist/hosts/mcp-config.js +54 -5
- package/dist/hosts/mcp-config.js.map +1 -1
- package/dist/hosts/registry.d.ts.map +1 -1
- package/dist/hosts/registry.js +20 -0
- package/dist/hosts/registry.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/ingest/fs.d.ts +19 -5
- package/dist/ingest/fs.d.ts.map +1 -1
- package/dist/ingest/fs.js +35 -9
- package/dist/ingest/fs.js.map +1 -1
- package/dist/telemetry/contract.d.ts.map +1 -1
- package/dist/telemetry/contract.js +7 -2
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/telemetry/sessions.d.ts.map +1 -1
- package/dist/telemetry/sessions.js +6 -0
- package/dist/telemetry/sessions.js.map +1 -1
- package/dist/util/state.d.ts +25 -2
- package/dist/util/state.d.ts.map +1 -1
- package/dist/util/state.js +24 -2
- 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 +19 -4
- package/scripts/graph-quality.mjs +3 -3
- 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,8 +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 {
|
|
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";
|
|
27
29
|
import { readSourceFile } from "../util/source.js";
|
|
28
30
|
import { counts, tokenize } from "./index-file.js";
|
|
29
31
|
/** Max source lines to inline per hit — a definition longer than this is
|
|
@@ -140,6 +142,25 @@ function bm25(query, doc, idf, dl, avgdl) {
|
|
|
140
142
|
}
|
|
141
143
|
return s;
|
|
142
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
|
+
}
|
|
143
164
|
// ── Structural intent ──────────────────────────────────────────────────────
|
|
144
165
|
const INCOMING = /\b(caller|callers|calls?\s+into|who\s+calls|what\s+calls|called\s+by|used\s+by|uses)\b/;
|
|
145
166
|
const OUTGOING = /\b(callee|callees|what\s+does\s+\w+\s+call|calls\s+what|imports?|depends\s+on)\b/;
|
|
@@ -282,7 +303,18 @@ function matchedIdfShare(q, fields, idf, dfltIdf) {
|
|
|
282
303
|
}
|
|
283
304
|
return total > 0 ? matched / total : 0;
|
|
284
305
|
}
|
|
285
|
-
|
|
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) {
|
|
286
318
|
// Binary query terms: a word repeated in a pasted issue body counts once, so a
|
|
287
319
|
// ranty description can't linearly amplify an incidental word.
|
|
288
320
|
const q = new Map([...counts(tokenize(query)).keys()].map((t) => [t, 1]));
|
|
@@ -382,6 +414,10 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
382
414
|
// Parallel to matchedOf, but over NAME+PATH only (body dropped) — the
|
|
383
415
|
// match-strength signal exported as `coverageStrong`.
|
|
384
416
|
const matchedStrongOf = new Map();
|
|
417
|
+
// Final listwise selection groups code hits by exact source path. Concepts
|
|
418
|
+
// use singleton pseudo-groups so they remain ordinary ranked candidates
|
|
419
|
+
// without masquerading as every file listed in their sources.
|
|
420
|
+
const selectionGroupOf = new Map();
|
|
385
421
|
// ── Concepts (prose nodes; not in the wiring graph) ──
|
|
386
422
|
const conceptHits = [];
|
|
387
423
|
let maxConcept = 0;
|
|
@@ -399,6 +435,10 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
399
435
|
};
|
|
400
436
|
matchedOf.set(hit, matchedIdfShare(q, [name, body], idf, dfltIdf));
|
|
401
437
|
matchedStrongOf.set(hit, matchedIdfShare(q, [name], idf, dfltIdf)); // concepts have no path field
|
|
438
|
+
// A concept is one singleton partition even if two source entries reuse
|
|
439
|
+
// the same slug; include its stable encounter index to prevent accidental
|
|
440
|
+
// grouping by frontmatter alone.
|
|
441
|
+
selectionGroupOf.set(hit, `concept:${c.slug}:${conceptHits.length}`);
|
|
402
442
|
conceptHits.push(hit);
|
|
403
443
|
}
|
|
404
444
|
}
|
|
@@ -412,9 +452,36 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
412
452
|
};
|
|
413
453
|
const docsById = new Map(symbolDocs.map((d) => [d.n.id, d]));
|
|
414
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
|
+
};
|
|
415
483
|
// The single-vs-multi-scope branch keys on `scopesOfGraph` ONLY: one scope
|
|
416
|
-
// (or no graph)
|
|
417
|
-
// branch is a byte-level regression guarantee for single-scope repos.
|
|
484
|
+
// (or no graph) keeps the original local scoring path.
|
|
418
485
|
const scopes = graph ? scopesOfGraph(graph) : null;
|
|
419
486
|
let scopeMeta;
|
|
420
487
|
// The BM25 length prior, corpus-global like `idf`/`dfltIdf` above. Hoisted out
|
|
@@ -427,6 +494,18 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
427
494
|
: symbolDocs.length
|
|
428
495
|
? symbolDocs.reduce((a, d) => a + bodyLen(d.body), 0) / symbolDocs.length
|
|
429
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
|
+
};
|
|
430
509
|
if (scopes && scopes.length > 1) {
|
|
431
510
|
// ── Multi-scope repo: partition by sub-project and walk each subgraph
|
|
432
511
|
// separately — so the big scope can't drown the small one — but score every
|
|
@@ -434,14 +513,25 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
434
513
|
// can be ordered by score. Same scoring functions and blend as the
|
|
435
514
|
// single-scope path; the combination itself lives in fuse.ts. ──
|
|
436
515
|
const byScope = new Map();
|
|
516
|
+
const scopeById = new Map();
|
|
437
517
|
for (const d of symbolDocs) {
|
|
438
518
|
const s = scopeOf(d.n.path, scopes).prefix;
|
|
519
|
+
scopeById.set(d.n.id, s);
|
|
439
520
|
const list = byScope.get(s);
|
|
440
521
|
if (list)
|
|
441
522
|
list.push(d);
|
|
442
523
|
else
|
|
443
524
|
byScope.set(s, [d]);
|
|
444
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;
|
|
445
535
|
const fusion = rankScopesAndFuse([...byScope.keys()].sort(), {
|
|
446
536
|
lex: (s) => {
|
|
447
537
|
const docs = byScope.get(s);
|
|
@@ -453,12 +543,20 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
453
543
|
// per-scope now; the statistics are global.
|
|
454
544
|
const out = new Map();
|
|
455
545
|
for (const { n, name, path, body } of docs) {
|
|
546
|
+
const factor = testFactor(n.path);
|
|
456
547
|
const total = (score(q, name, idf) * 3 +
|
|
457
548
|
score(q, path, idf) * 2 +
|
|
458
549
|
bm25(q, body, idf, bodyLen(body), avgBodyLen)) *
|
|
459
|
-
|
|
460
|
-
if (total > 0)
|
|
550
|
+
factor;
|
|
551
|
+
if (total > 0) {
|
|
461
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
|
+
}
|
|
462
560
|
}
|
|
463
561
|
return out;
|
|
464
562
|
},
|
|
@@ -470,35 +568,121 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
470
568
|
return true;
|
|
471
569
|
return false;
|
|
472
570
|
}),
|
|
473
|
-
walk: (s, seeds) =>
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
+
},
|
|
481
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,
|
|
482
616
|
}, GRAPH_WEIGHT, RESCUE_FLOOR);
|
|
483
|
-
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
+
}
|
|
502
686
|
}
|
|
503
687
|
// Label + footer only when federation actually happened (or a scope was
|
|
504
688
|
// gated out and is worth mentioning) — a query matching one scope of a
|
|
@@ -516,13 +700,20 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
516
700
|
for (const { n, name, path, body } of symbolDocs) {
|
|
517
701
|
// Name and path are short identifiers → plain idf-weighted overlap; the body
|
|
518
702
|
// is length-normalized via BM25 so long definitions don't win on bulk.
|
|
703
|
+
const factor = testFactor(n.path);
|
|
519
704
|
const total = (score(q, name, idf) * 3 +
|
|
520
705
|
score(q, path, idf) * 2 +
|
|
521
706
|
bm25(q, body, idf, bodyLen(body), avgBodyLen)) *
|
|
522
|
-
|
|
707
|
+
factor;
|
|
523
708
|
if (total > 0) {
|
|
524
709
|
lex.set(n.id, total);
|
|
525
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
|
+
}
|
|
526
717
|
}
|
|
527
718
|
}
|
|
528
719
|
// ── Graph-rank: let connectivity to the matched set reorder and rescue ──
|
|
@@ -538,46 +729,226 @@ function lexical(query, corpus, limit, graphRank, inPrefix) {
|
|
|
538
729
|
for (const [id, p] of pr)
|
|
539
730
|
if (p >= RESCUE_FLOOR)
|
|
540
731
|
candidates.add(id);
|
|
732
|
+
const singleCandidates = [];
|
|
541
733
|
for (const id of candidates) {
|
|
542
734
|
const n = byId.get(id);
|
|
543
735
|
if (!n)
|
|
544
736
|
continue;
|
|
545
|
-
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);
|
|
546
740
|
// Apply the test penalty after normalization too. Applying it only to the
|
|
547
741
|
// raw lexical score is not enough: if a test is still the strongest raw
|
|
548
742
|
// match, dividing by maxLex restores it to 1.0 and erases the de-rank.
|
|
549
|
-
const
|
|
550
|
-
if (
|
|
743
|
+
const baseline = (lexical + GRAPH_WEIGHT * graphScore) * rankFactor;
|
|
744
|
+
if (baseline <= 0)
|
|
551
745
|
continue;
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
+
}
|
|
565
841
|
}
|
|
566
842
|
}
|
|
567
843
|
// Concepts and symbols live on comparable 0..~1.5 scales so they merge fairly:
|
|
568
|
-
// concept scores are normalized to their own max
|
|
844
|
+
// concept scores are normalized to their own max; symbol scores use the
|
|
569
845
|
// lexical-normalized + graph-weighted blend.
|
|
570
846
|
for (const h of conceptHits)
|
|
571
847
|
h.score = maxConcept > 0 ? h.score / maxConcept : 0;
|
|
572
848
|
const scored = [...conceptHits, ...symbolHits];
|
|
573
849
|
scored.sort((a, b) => b.score - a.score || a.title.localeCompare(b.title));
|
|
850
|
+
const baselineScored = needsFileQueues
|
|
851
|
+
? [...conceptHits, ...baselineSymbolHits].sort((a, b) => b.score - a.score || a.title.localeCompare(b.title))
|
|
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;
|
|
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;
|
|
574
944
|
return {
|
|
575
945
|
query,
|
|
576
946
|
mode: scored.length ? "lexical" : "empty",
|
|
577
|
-
hits:
|
|
947
|
+
hits: selected.slice(0, limit),
|
|
578
948
|
scopes: scopeMeta,
|
|
579
|
-
coverage:
|
|
580
|
-
coverageStrong:
|
|
949
|
+
coverage: top && q.size > 0 ? matchedOf.get(top) ?? 0 : undefined,
|
|
950
|
+
coverageStrong: top && q.size > 0 ? matchedStrongOf.get(top) ?? 0 : undefined,
|
|
951
|
+
...(ranking ? { ranking } : {}),
|
|
581
952
|
// Zero hits on a genuinely multi-scope graph names the scopes that exist,
|
|
582
953
|
// so a query that missed everywhere still tells the caller where to look.
|
|
583
954
|
note: scored.length
|
|
@@ -672,6 +1043,12 @@ export function ask(dir, query, opts = {}) {
|
|
|
672
1043
|
const limit = opts.limit ?? 8;
|
|
673
1044
|
const corpus = loadCorpus(outDir);
|
|
674
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;
|
|
675
1052
|
// Normalized ONCE, up front, and this value used everywhere below
|
|
676
1053
|
// (validation, filtering, structural). `ask --in frontend/` must work exactly
|
|
677
1054
|
// like `--in frontend` — `scopeLabel`/the footer's own `--in <scope>/`
|
|
@@ -689,7 +1066,7 @@ export function ask(dir, query, opts = {}) {
|
|
|
689
1066
|
result = outcome.result;
|
|
690
1067
|
}
|
|
691
1068
|
else {
|
|
692
|
-
result = lexical(query, corpus, limit, graphRank, inPrefix);
|
|
1069
|
+
result = lexical(query, corpus, limit, graphRank, inPrefix, fileFirst, fileComplement, fileTopLock, opts.includeRankingMetadata ?? false);
|
|
693
1070
|
// A structural-intent query that couldn't be answered structurally still
|
|
694
1071
|
// gets a prominent note on the lexical fallback result — never silent.
|
|
695
1072
|
if (outcome && "fallthroughNote" in outcome) {
|
|
@@ -698,10 +1075,18 @@ export function ask(dir, query, opts = {}) {
|
|
|
698
1075
|
}
|
|
699
1076
|
}
|
|
700
1077
|
else {
|
|
701
|
-
result = lexical(query, corpus, limit, graphRank, inPrefix);
|
|
1078
|
+
result = lexical(query, corpus, limit, graphRank, inPrefix, fileFirst, fileComplement, fileTopLock, opts.includeRankingMetadata ?? false);
|
|
702
1079
|
}
|
|
703
1080
|
if (opts.source) {
|
|
704
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
|
+
}
|
|
705
1090
|
// The pack is truly substitutive only in retriever mode (spans inlined), so
|
|
706
1091
|
// the "vs reading whole files" estimate is only honest here.
|
|
707
1092
|
result.saved = baselineFor(result.hits, corpus.graph);
|