pi-supernova 0.8.1 → 0.9.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 +244 -54
- package/docs/CHANGELOG.md +111 -1
- package/docs/TOKEN_COSTS.md +38 -0
- package/index.js +10 -175
- package/package.json +2 -1
- package/src/adapters/bash.js +23 -33
- package/src/adapters/errors.js +1 -9
- package/src/adapters/read-focus.js +98 -0
- package/src/adapters/read-image.js +51 -0
- package/src/adapters/read-json.js +42 -0
- package/src/adapters/read-text.js +71 -0
- package/src/adapters/read.js +66 -635
- package/src/bridge/catalog.js +3 -2
- package/src/bridge/host-bridge.js +35 -167
- package/src/bridge/tool-registry.js +104 -0
- package/src/bridge/trace.js +41 -0
- package/src/context/evidence-graph.js +249 -0
- package/src/context/evidence-rank.js +153 -0
- package/src/context/evidence.js +10 -424
- package/src/context/query.js +71 -0
- package/src/context/repo-index.js +8 -162
- package/src/context/search-files.js +19 -0
- package/src/context/search.js +2 -24
- package/src/context/snap-search.js +202 -0
- package/src/context/snap.js +5 -266
- package/src/context/source-entry.js +112 -0
- package/src/contract/bash.js +18 -2
- package/src/contract/program.js +36 -0
- package/src/contract/read.js +8 -53
- package/src/fs/check.js +1 -1
- package/src/fs/commit.js +161 -0
- package/src/fs/diff.js +11 -15
- package/src/fs/directory.js +79 -0
- package/src/fs/file-io.js +100 -0
- package/src/fs/glob.js +54 -0
- package/src/fs/json-size.js +54 -0
- package/src/fs/lines.js +117 -0
- package/src/fs/read-window.js +74 -0
- package/src/fs/session-resource.js +50 -0
- package/src/fs/text-ops.js +7 -242
- package/src/fs/vfs.js +5 -239
- package/src/fs/workspace.js +7 -2
- package/src/output/bottleneck.js +13 -67
- package/src/output/final.js +114 -0
- package/src/output/format.js +94 -5
- package/src/output/outcome.js +91 -0
- package/src/runtime/batch-input.js +68 -0
- package/src/runtime/guest-api.js +281 -0
- package/src/runtime/guest-worker.js +62 -333
- package/src/runtime/parallel.js +41 -39
- package/src/runtime/program-batch.js +35 -68
- package/src/runtime/program-file.js +3 -11
- package/src/runtime/program.js +141 -0
- package/src/runtime/reference.js +11 -10
- package/src/runtime/runtime.js +94 -261
- package/src/runtime/worker-pool.js +91 -0
- package/src/shared/decode.js +22 -8
- package/src/shared/image-worker.js +30 -0
- package/src/shared/image.js +78 -0
- package/src/shared/png.js +57 -0
- package/src/shared/result.js +77 -0
- package/src/shared/syntax-context.js +61 -3
- package/src/ui/host-render.js +104 -0
- package/src/ui/progress.js +51 -0
- package/src/ui/render.js +21 -421
- package/src/ui/trace.js +277 -0
package/src/context/evidence.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import {pickEvidence} from "./evidence-rank.js";
|
|
2
|
+
import {pendingInScope,overlaySearchEntry} from './search-files.js';
|
|
1
3
|
import * as fs from "node:fs/promises";
|
|
2
4
|
import * as path from "node:path";
|
|
3
5
|
import { WorkspaceIndex } from "./repo-index.js";
|
|
4
|
-
import { tokenizeQuery, scorePathTopology, stem } from "./
|
|
5
|
-
import { isTestPath } from "../fs/workspace.js";
|
|
6
|
+
import { tokenizeQuery, scorePathTopology, stem } from "./query.js";
|
|
6
7
|
|
|
7
8
|
// Zero-token evidence selection over source code, after Zero-Mem (arXiv:2607.29377).
|
|
8
9
|
// The codebase is the interaction history H; declared spans are the context units;
|
|
@@ -38,15 +39,7 @@ const IDENT = /[A-Za-z_$][\w$]*/g;
|
|
|
38
39
|
// Verb forms only: "call sites" is a concept, "who calls X" is a usage question.
|
|
39
40
|
const RELATION_WORDS = new Set(["calls", "called", "caller", "callers", "uses", "usages", "used", "using", "imports", "imported", "depends", "references", "referenced", "invokes", "invoked"]);
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const HUB_MIN = 8;
|
|
44
|
-
|
|
45
|
-
export { stem } from "./snap.js";
|
|
46
|
-
|
|
47
|
-
function splitIdentifier(name) {
|
|
48
|
-
return name.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
49
|
-
}
|
|
42
|
+
export { stem } from "./query.js";
|
|
50
43
|
|
|
51
44
|
/** eq.6: query profile with subjects, keywords/stems, answer type, test/doc flags, route. */
|
|
52
45
|
export function profileQuery(query) {
|
|
@@ -81,314 +74,6 @@ function spansOf(entry, filePath, maxSpanLines) {
|
|
|
81
74
|
return declared.map((s, i) => ({ ...base, ...s, sourceEnd: s.end, id: filePath + ":" + s.start, end: Math.min(s.end, s.start + maxSpanLines - 1), index: i }));
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
function spanLines(span) {
|
|
85
|
-
return span.lower.slice(span.start - 1, span.end);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// ---- eq.3 / eq.4: entity–context graph over candidate spans ----
|
|
89
|
-
|
|
90
|
-
function countSpanEntities(span, entityNames) {
|
|
91
|
-
const counts = new Map();
|
|
92
|
-
let total = 0;
|
|
93
|
-
const { idents } = span.lines;
|
|
94
|
-
|
|
95
|
-
for (let li = span.start - 1; li < span.end; li++) {
|
|
96
|
-
for (const word of idents[li]) {
|
|
97
|
-
if (!entityNames.has(word)) continue;
|
|
98
|
-
counts.set(word, (counts.get(word) || 0) + 1);
|
|
99
|
-
total += 1;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return { counts, total };
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function weightsFromCounts(counts, total, entitySpans, spanId) {
|
|
107
|
-
const weights = new Map();
|
|
108
|
-
|
|
109
|
-
for (const [e, c] of counts) {
|
|
110
|
-
weights.set(e, c / total); // eq.4
|
|
111
|
-
|
|
112
|
-
if (!entitySpans.has(e)) entitySpans.set(e, new Set());
|
|
113
|
-
entitySpans.get(e).add(spanId);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return weights;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function hubEntities(entitySpans, spans) {
|
|
120
|
-
// Entities present in a large share of spans (isString, path, …) carry no query signal; keep them out of propagation.
|
|
121
|
-
const hubLimit = Math.max(HUB_MIN, Math.floor(spans.length * HUB_FRACTION));
|
|
122
|
-
const hubs = new Set();
|
|
123
|
-
|
|
124
|
-
for (const [entity, ids] of entitySpans) {
|
|
125
|
-
if (ids.size > hubLimit) hubs.add(entity);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return hubs;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function buildGraph(spans) {
|
|
132
|
-
const entityNames = new Set(spans.map((s) => s.name).filter((n) => n && n.length > 2));
|
|
133
|
-
const spanEntities = new Map(); // span.id → Map(entity → w(d,e))
|
|
134
|
-
const entitySpans = new Map(); // entity → Set(span.id)
|
|
135
|
-
|
|
136
|
-
for (const span of spans) {
|
|
137
|
-
const { counts, total } = countSpanEntities(span, entityNames);
|
|
138
|
-
spanEntities.set(span.id, weightsFromCounts(counts, total, entitySpans, span.id));
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return { entityNames, spanEntities, entitySpans, hubs: hubEntities(entitySpans, spans), byId: new Map(spans.map((s) => [s.id, s])) };
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// ---- eq.8 / eq.9: entity activation and one propagation step ----
|
|
145
|
-
|
|
146
|
-
function lexicalSim(a, b) {
|
|
147
|
-
const ta = new Set(splitIdentifier(a));
|
|
148
|
-
const tb = new Set(splitIdentifier(b));
|
|
149
|
-
|
|
150
|
-
if (ta.size === 0 || tb.size === 0) return 0;
|
|
151
|
-
let inter = 0;
|
|
152
|
-
|
|
153
|
-
for (const t of ta) if (tb.has(t)) inter++;
|
|
154
|
-
|
|
155
|
-
return inter / (ta.size + tb.size - inter);
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
function activateEntities(profile, graph) {
|
|
159
|
-
const eta = new Map();
|
|
160
|
-
const anchors = profile.subjects.length ? profile.subjects : profile.stems;
|
|
161
|
-
|
|
162
|
-
for (const anchor of anchors) {
|
|
163
|
-
let best = null;
|
|
164
|
-
let bestSim = 0;
|
|
165
|
-
|
|
166
|
-
for (const e of graph.entityNames) {
|
|
167
|
-
const sim = e.toLowerCase() === anchor.toLowerCase() ? 1 : lexicalSim(e, anchor);
|
|
168
|
-
|
|
169
|
-
if (sim > bestSim) {
|
|
170
|
-
bestSim = sim;
|
|
171
|
-
best = e;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (best && bestSim >= 0.5) eta.set(best, Math.max(eta.get(best) || 0, bestSim)); // eq.8
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return eta;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function querySim(profile, lowerLine) {
|
|
182
|
-
let hits = 0;
|
|
183
|
-
|
|
184
|
-
for (const t of profile.stems) if (lowerLine.includes(t)) hits++;
|
|
185
|
-
|
|
186
|
-
return profile.stems.length ? hits / profile.stems.length : 0;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** Co-occurring entities on one query-relevant line receive act·sim(q,z)·idf (eq.9, IDF-damped). */
|
|
190
|
-
function activateCooccurring(e, line, weight, spanId, graph, eta1) {
|
|
191
|
-
for (const [other] of graph.spanEntities.get(spanId)) {
|
|
192
|
-
if (other === e || graph.hubs.has(other) || !line.includes(other.toLowerCase())) continue;
|
|
193
|
-
const idf = 1 / Math.log2(1 + graph.entitySpans.get(other).size);
|
|
194
|
-
eta1.set(other, (eta1.get(other) || 0) + weight * idf);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function propagateFrom(e, act, graph, profile, eta1) {
|
|
199
|
-
const eLower = e.toLowerCase();
|
|
200
|
-
|
|
201
|
-
for (const spanId of graph.entitySpans.get(e) || []) {
|
|
202
|
-
for (const line of spanLines(graph.byId.get(spanId))) {
|
|
203
|
-
if (!line.includes(eLower)) continue;
|
|
204
|
-
const sim = querySim(profile, line);
|
|
205
|
-
|
|
206
|
-
if (sim > 0) activateCooccurring(e, line, act * sim, spanId, graph, eta1);
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function propagate(eta0, spans, graph, profile) {
|
|
212
|
-
const eta1 = new Map(eta0);
|
|
213
|
-
|
|
214
|
-
for (const [e, act] of eta0) propagateFrom(e, act, graph, profile, eta1);
|
|
215
|
-
|
|
216
|
-
return eta1;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// ---- eq.10: personalized PageRank over spans ----
|
|
220
|
-
|
|
221
|
-
function resetDistribution(spans, graph, eta, prior) {
|
|
222
|
-
const n = spans.length;
|
|
223
|
-
const reset = new Float64Array(n);
|
|
224
|
-
let sum = 0;
|
|
225
|
-
|
|
226
|
-
for (let i = 0; i < n; i++) {
|
|
227
|
-
let r = prior[i];
|
|
228
|
-
|
|
229
|
-
for (const [e, w] of graph.spanEntities.get(spans[i].id)) if (!graph.hubs.has(e)) r += (eta.get(e) || 0) * w;
|
|
230
|
-
reset[i] = r;
|
|
231
|
-
sum += r;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (sum > 0) for (let i = 0; i < n; i++) reset[i] /= sum;
|
|
235
|
-
|
|
236
|
-
return { reset, sum };
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Transition structure d → d' = Σ_e w(d,e)·w(d',e) over shared non-hub entities plus 0.5 per in-file
|
|
241
|
-
* neighbour (Edd). Kept factored through the entity layer so an iteration costs O(nnz), never O(n²).
|
|
242
|
-
*/
|
|
243
|
-
function transitionStructure(spans, graph) {
|
|
244
|
-
const n = spans.length;
|
|
245
|
-
const entities = [];
|
|
246
|
-
|
|
247
|
-
for (const [entity, ids] of graph.entitySpans) {
|
|
248
|
-
if (ids.size >= 2 && !graph.hubs.has(entity)) entities.push(entity);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
const eIndex = new Map(entities.map((e, i) => [e, i]));
|
|
252
|
-
|
|
253
|
-
const spanTerms = spans.map((s) => {
|
|
254
|
-
const terms = [];
|
|
255
|
-
|
|
256
|
-
for (const [e, w] of graph.spanEntities.get(s.id)) if (eIndex.has(e)) terms.push([eIndex.get(e), w]);
|
|
257
|
-
|
|
258
|
-
return terms;
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const entityMass = new Float64Array(entities.length);
|
|
262
|
-
|
|
263
|
-
for (let i = 0; i < n; i++) for (const [ei, w] of spanTerms[i]) entityMass[ei] += w;
|
|
264
|
-
const neighbours = spans.map((s, i) => [i - 1, i + 1].filter((j) => j >= 0 && j < n && spans[j].path === s.path));
|
|
265
|
-
const outWeight = new Float64Array(n);
|
|
266
|
-
|
|
267
|
-
for (let i = 0; i < n; i++) {
|
|
268
|
-
let out = 0.5 * neighbours[i].length;
|
|
269
|
-
|
|
270
|
-
for (const [ei, w] of spanTerms[i]) out += w * (entityMass[ei] - w);
|
|
271
|
-
outWeight[i] = out;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
return { spanTerms, entityCount: entities.length, neighbours, outWeight };
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function pushStep(pi, next, acc, structure, gamma) {
|
|
278
|
-
const { spanTerms, neighbours, outWeight } = structure;
|
|
279
|
-
let dangling = 0;
|
|
280
|
-
|
|
281
|
-
for (let i = 0; i < pi.length; i++) {
|
|
282
|
-
if (outWeight[i] === 0) {
|
|
283
|
-
dangling += pi[i];
|
|
284
|
-
continue;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const flow = (gamma * pi[i]) / outWeight[i];
|
|
288
|
-
|
|
289
|
-
for (const [ei, w] of spanTerms[i]) {
|
|
290
|
-
acc[ei] += flow * w;
|
|
291
|
-
next[i] -= flow * w * w; // remove the d → d self term
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
for (const j of neighbours[i]) next[j] += flow * 0.5;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
return dangling;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function pageRank(spans, graph, eta, prior, { gamma, pprIterations }) {
|
|
301
|
-
const n = spans.length;
|
|
302
|
-
const { reset, sum } = resetDistribution(spans, graph, eta, prior);
|
|
303
|
-
|
|
304
|
-
if (sum === 0) return reset;
|
|
305
|
-
const structure = transitionStructure(spans, graph);
|
|
306
|
-
const acc = new Float64Array(structure.entityCount);
|
|
307
|
-
let pi = Float64Array.from(reset);
|
|
308
|
-
|
|
309
|
-
for (let iter = 0; iter < pprIterations; iter++) {
|
|
310
|
-
const next = new Float64Array(n);
|
|
311
|
-
acc.fill(0);
|
|
312
|
-
const dangling = pushStep(pi, next, acc, structure, gamma);
|
|
313
|
-
|
|
314
|
-
for (let i = 0; i < n; i++) {
|
|
315
|
-
for (const [ei, w] of structure.spanTerms[i]) next[i] += acc[ei] * w;
|
|
316
|
-
next[i] += (1 - gamma) * reset[i] + gamma * dangling * reset[i]; // dangling mass follows the reset
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
pi = next;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
return pi;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
// ---- eq.11: hierarchical view (file → span → line) ----
|
|
326
|
-
|
|
327
|
-
function nameDefinitionScore(span, profile, usage) {
|
|
328
|
-
if (usage) return 0; // eq.15 Rank_ϕ: a usage question is answered by callers, not the definer
|
|
329
|
-
const nameTokens = splitIdentifier(span.name || "");
|
|
330
|
-
let def = 0;
|
|
331
|
-
|
|
332
|
-
for (const t of profile.stems) if (nameTokens.some((n) => n.startsWith(t))) def += 40;
|
|
333
|
-
|
|
334
|
-
return def;
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
function mentionScore(span, profile, usage, skipDeclaration) {
|
|
338
|
-
const perHit = usage ? 15 : 5;
|
|
339
|
-
let mentions = 0;
|
|
340
|
-
let bestLine = span.start;
|
|
341
|
-
let bestHits = 0;
|
|
342
|
-
const lines = spanLines(span);
|
|
343
|
-
|
|
344
|
-
for (let i = skipDeclaration ? 1 : 0; i < lines.length; i++) {
|
|
345
|
-
let hits = 0;
|
|
346
|
-
|
|
347
|
-
for (const t of profile.stems) if (lines[i].includes(t)) hits++;
|
|
348
|
-
|
|
349
|
-
if (hits > bestHits) {
|
|
350
|
-
bestHits = hits;
|
|
351
|
-
bestLine = span.start + i;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
mentions += hits * hits * perHit; // several query stems on one line is strong evidence
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return { mentions, bestLine };
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
function hierarchicalScores(spans, fileScores, profile) {
|
|
361
|
-
const usage = profile.answerType === "usage";
|
|
362
|
-
|
|
363
|
-
return spans.map((span) => {
|
|
364
|
-
const definesSubject = profile.subjects.includes(span.name);
|
|
365
|
-
const def = nameDefinitionScore(span, profile, usage);
|
|
366
|
-
const { mentions, bestLine } = mentionScore(span, profile, usage, usage && definesSubject);
|
|
367
|
-
span.bestLine = bestLine;
|
|
368
|
-
span.support = def + mentions; // span-level lexical evidence; file-level bonuses do not count
|
|
369
|
-
const fileScore = Math.max(0, fileScores.get(span.path) || 0);
|
|
370
|
-
|
|
371
|
-
return fileScore / 2 + def + Math.min(mentions, usage ? 200 : 120) + (span.isExport ? 10 : 0);
|
|
372
|
-
});
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// ---- eq.12 / eq.13 ----
|
|
376
|
-
|
|
377
|
-
function normalize(scores) {
|
|
378
|
-
let min = Infinity;
|
|
379
|
-
let max = -Infinity;
|
|
380
|
-
|
|
381
|
-
for (const s of scores) {
|
|
382
|
-
if (s < min) min = s;
|
|
383
|
-
|
|
384
|
-
if (s > max) max = s;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
if (!(max > min)) return scores.map(() => 1);
|
|
388
|
-
|
|
389
|
-
return scores.map((s) => (s - min) / (max - min));
|
|
390
|
-
}
|
|
391
|
-
|
|
392
77
|
// ---- candidate files (boundary + topology + entity hits) ----
|
|
393
78
|
|
|
394
79
|
function topologyScored(files, profile) {
|
|
@@ -440,48 +125,6 @@ function candidateFiles(files, profile, index, limit, overlayText) {
|
|
|
440
125
|
return { files: [...chosen], fileScores: new Map(scored.map(({ f, s }) => [f, s])) };
|
|
441
126
|
}
|
|
442
127
|
|
|
443
|
-
// ---- eq.14 / eq.15 ----
|
|
444
|
-
|
|
445
|
-
function bridgesFor(i, spans, graph, fused, definers, chosen) {
|
|
446
|
-
const byId = graph.byId;
|
|
447
|
-
const bridges = [];
|
|
448
|
-
|
|
449
|
-
for (const [e, w] of graph.spanEntities.get(spans[i].id)) {
|
|
450
|
-
const defId = definers.get(e);
|
|
451
|
-
|
|
452
|
-
if (!defId || graph.hubs.has(e) || chosen.has(defId) || defId === spans[i].id || w < 0.15) continue;
|
|
453
|
-
const definer = byId.get(defId);
|
|
454
|
-
|
|
455
|
-
if (definer.end - definer.start < 2) continue; // one-line helpers add no understanding
|
|
456
|
-
bridges.push({ id: defId, w: w * fused[definer.index0] });
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
return bridges.sort((a, b) => b.w - a.w).slice(0, 2);
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function closure(main, spans, graph, fused, k) {
|
|
463
|
-
spans.forEach((s, i) => (s.index0 = i));
|
|
464
|
-
const chosen = new Map(main.map((i) => [spans[i].id, { i, why: "main" }]));
|
|
465
|
-
const definers = new Map();
|
|
466
|
-
|
|
467
|
-
for (const s of spans) if (s.name) definers.set(s.name, s.id);
|
|
468
|
-
const neighboursOf = (i) => [i - 1, i + 1].filter((j) => j >= 0 && j < spans.length && spans[j].path === spans[i].path);
|
|
469
|
-
|
|
470
|
-
for (const i of main) {
|
|
471
|
-
// Ng: spans that define identifiers this span uses (relational bridges).
|
|
472
|
-
for (const b of bridgesFor(i, spans, graph, fused, definers, chosen)) chosen.set(b.id, { i: graph.byId.get(b.id).index0, why: "bridge" });
|
|
473
|
-
|
|
474
|
-
// Nh: in-file neighbours that still carry query signal.
|
|
475
|
-
for (const j of neighboursOf(i)) {
|
|
476
|
-
if (fused[j] > 0.2 && !chosen.has(spans[j].id)) chosen.set(spans[j].id, { i: j, why: "neighbor" });
|
|
477
|
-
}
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
const supports = [...chosen.values()].filter((c) => c.why !== "main").sort((a, b) => fused[b.i] - fused[a.i]).slice(0, k);
|
|
481
|
-
|
|
482
|
-
return [...main.map((i) => ({ i, why: "main" })), ...supports];
|
|
483
|
-
}
|
|
484
|
-
|
|
485
128
|
function render(spans, picks, fused, opts, root) {
|
|
486
129
|
const out = [];
|
|
487
130
|
let budget = opts.maxChars;
|
|
@@ -505,16 +148,16 @@ function render(spans, picks, fused, opts, root) {
|
|
|
505
148
|
const lastLine = span.start + text.split("\n").length - 1;
|
|
506
149
|
const truncated = lastLine < span.sourceEnd;
|
|
507
150
|
budget -= text.length;
|
|
508
|
-
|
|
151
|
+
const rendered = {
|
|
509
152
|
path: path.relative(root, span.path) || span.path,
|
|
510
153
|
lines: [span.start, lastLine],
|
|
511
|
-
truncated: truncated || undefined,
|
|
512
|
-
nextOffset: truncated ? lastLine + 1 : undefined,
|
|
513
154
|
name: span.name,
|
|
514
155
|
kind: span.kind,
|
|
515
156
|
why,
|
|
516
157
|
text,
|
|
517
|
-
}
|
|
158
|
+
};
|
|
159
|
+
if (truncated) { rendered.truncated = true; rendered.nextOffset = lastLine+1; }
|
|
160
|
+
out.push(rendered);
|
|
518
161
|
|
|
519
162
|
if (budget <= 0) break;
|
|
520
163
|
}
|
|
@@ -522,14 +165,6 @@ function render(spans, picks, fused, opts, root) {
|
|
|
522
165
|
return out;
|
|
523
166
|
}
|
|
524
167
|
|
|
525
|
-
function stagedInRoot(searchRoot, pendingPaths) {
|
|
526
|
-
return pendingPaths.filter(file => {
|
|
527
|
-
const relative = path.relative(searchRoot, file);
|
|
528
|
-
|
|
529
|
-
return relative !== ".." && !relative.startsWith(".." + path.sep) && !path.isAbsolute(relative);
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
|
|
533
168
|
function statCatch(error) {
|
|
534
169
|
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") return null;
|
|
535
170
|
throw error;
|
|
@@ -547,26 +182,18 @@ async function diskFilesAt(searchRoot, index) {
|
|
|
547
182
|
|
|
548
183
|
async function listedFiles(root, searchDir, pendingPaths, index) {
|
|
549
184
|
const searchRoot = path.resolve(searchDir || root);
|
|
550
|
-
const files = [...new Set([...await diskFilesAt(searchRoot, index), ...
|
|
185
|
+
const files = [...new Set([...await diskFilesAt(searchRoot, index), ...pendingInScope(searchRoot, pendingPaths)])];
|
|
551
186
|
|
|
552
187
|
if (files.length === 0) throw new Error(`no files found to search in ${searchDir || root}`);
|
|
553
188
|
|
|
554
189
|
return files;
|
|
555
190
|
}
|
|
556
191
|
|
|
557
|
-
function overlayEntry(f, overlayText, index) {
|
|
558
|
-
const pending = overlayText(f);
|
|
559
|
-
|
|
560
|
-
return pending === undefined
|
|
561
|
-
? index.entry(f)
|
|
562
|
-
: Buffer.byteLength(pending, "utf8") <= 512 * 1024 ? WorkspaceIndex.fromText(f, pending) : null;
|
|
563
|
-
}
|
|
564
|
-
|
|
565
192
|
function collectSpans(chosenFiles, overlayText, index, maxSpanLines) {
|
|
566
193
|
const spans = [];
|
|
567
194
|
|
|
568
195
|
for (const f of chosenFiles) {
|
|
569
|
-
const entry =
|
|
196
|
+
const entry = overlaySearchEntry(index, f, overlayText);
|
|
570
197
|
|
|
571
198
|
if (!entry) continue;
|
|
572
199
|
spans.push(...spansOf(entry, f, maxSpanLines));
|
|
@@ -592,47 +219,6 @@ function usageSpans(spans, profile, maxSpanLines) {
|
|
|
592
219
|
});
|
|
593
220
|
}
|
|
594
221
|
|
|
595
|
-
function fuseScores(profile, graphNorm, hierNorm, rho) {
|
|
596
|
-
const [primary, secondary] = profile.route === "relational" ? [graphNorm, hierNorm] : [hierNorm, graphNorm];
|
|
597
|
-
|
|
598
|
-
return primary.map((p, i) => rho * p + (1 - rho) * secondary[i]); // eq.13
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
function spanAdmissible(span, profile) {
|
|
602
|
-
const p = span.path;
|
|
603
|
-
const isDoc = /\.(md|mdx|rst|txt)$/i.test(p);
|
|
604
|
-
|
|
605
|
-
return span.support > 0
|
|
606
|
-
&& (profile.flags.wantsTest || !isTestPath(p))
|
|
607
|
-
&& (profile.flags.wantsDoc || !isDoc);
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
function compareFused(spans, fused) {
|
|
611
|
-
return (a, b) => fused[b] - fused[a] || spans[a].path.localeCompare(spans[b].path) || spans[a].start - spans[b].start;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function dampUsageDefiners(spans, profile, fused, admissible) {
|
|
615
|
-
const usage = profile.answerType === "usage";
|
|
616
|
-
|
|
617
|
-
for (const i of admissible) {
|
|
618
|
-
if (usage && profile.subjects.includes(spans[i].name)) fused[i] *= 0.5; // a usage question is answered by callers
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
function pickEvidence(spans, fileScores, profile, opts) {
|
|
623
|
-
const graph = buildGraph(spans);
|
|
624
|
-
const hierNorm = normalize(hierarchicalScores(spans, fileScores, profile));
|
|
625
|
-
const eta = propagate(activateEntities(profile, graph), spans, graph, profile);
|
|
626
|
-
const pi = pageRank(spans, graph, eta, hierNorm.map((s) => s * 0.5), opts);
|
|
627
|
-
const fused = fuseScores(profile, normalize([...pi]), hierNorm, opts.rho);
|
|
628
|
-
// eq.15 Filter: boundary/type hard constraints and lexical support; Rank_ϕ: answer-type compatibility.
|
|
629
|
-
const admissible = spans.flatMap((span, i) => spanAdmissible(span, profile) ? [i] : []);
|
|
630
|
-
dampUsageDefiners(spans, profile, fused, admissible);
|
|
631
|
-
const main = admissible.sort(compareFused(spans, fused)).slice(0, opts.k);
|
|
632
|
-
|
|
633
|
-
return { picks: closure(main, spans, graph, fused, opts.k), fused };
|
|
634
|
-
}
|
|
635
|
-
|
|
636
222
|
/**
|
|
637
223
|
* R(q): top-K provenance-bearing source spans for a concept query, selected without any model call.
|
|
638
224
|
* @returns {{ route: string, spans: Array<{path, lines, name, kind, why, text}> }}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import {isString} from '../shared/decode.js';
|
|
3
|
+
import {isTestPath} from '../fs/workspace.js';
|
|
4
|
+
|
|
5
|
+
const STOP_WORDS = new Set([
|
|
6
|
+
"the", "a", "an", "and", "or", "in", "on", "at", "to", "for", "of", "with",
|
|
7
|
+
"by", "from", "is", "it", "this", "that", "where", "how", "what", "which",
|
|
8
|
+
"file", "code", "function", "class", "method", "find", "get", "look", "are", "does", "do",
|
|
9
|
+
]);
|
|
10
|
+
|
|
11
|
+
const SOURCE_EXT = new Set([".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".rs", ".py", ".go"]);
|
|
12
|
+
|
|
13
|
+
const TYPED_EXT = new Set([".ts", ".tsx", ".rs", ".go"]);
|
|
14
|
+
|
|
15
|
+
const MAX_NEEDLE_CHARS = 128;
|
|
16
|
+
|
|
17
|
+
/** Light suffix stripping so "terminated" ⊇ "terminat" matches "terminate"; deterministic, no dictionary. */
|
|
18
|
+
export function stem(token) {
|
|
19
|
+
if (token.length < 5) return token;
|
|
20
|
+
|
|
21
|
+
return token.replace(/(ations?|ings?|ed|es|e|s|ly|ers?)$/, (m) => (token.length - m.length >= 4 ? "" : m));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function tokenizeQuery(query) {
|
|
25
|
+
if (!isString(query) || !query.trim()) return { tokens: [], wantsTest: false, wantsType: false, wantsDoc: false };
|
|
26
|
+
const words = query.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-zA-Z0-9_]+/);
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
tokens: [...new Set(words.filter(word => word.length > 1 && !STOP_WORDS.has(word)))],
|
|
30
|
+
wantsTest: words.some(word => ["test", "tests", "testing", "spec", "specs"].includes(word)),
|
|
31
|
+
wantsType: words.some(word => ["type", "types", "interface", "interfaces", "schema", "schemas"].includes(word)),
|
|
32
|
+
wantsDoc: words.some(word => ["doc", "docs", "documentation", "readme"].includes(word)),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function tokenPathScore(base, words, normalized, tokens) {
|
|
37
|
+
let score = 0;
|
|
38
|
+
|
|
39
|
+
for (const token of tokens) {
|
|
40
|
+
if (base === token || base.startsWith(token + ".")) score += 60;
|
|
41
|
+
else if (base.includes(token)) score += 30;
|
|
42
|
+
else if (words.includes(token)) score += 15;
|
|
43
|
+
else if (normalized.includes(token)) score += 5;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return score;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function topologyPenalty(normalized, flags) {
|
|
50
|
+
const parts = normalized.split("/");
|
|
51
|
+
|
|
52
|
+
if (parts.some(part => ["node_modules", "dist", "target"].includes(part))) return -100;
|
|
53
|
+
const test = isTestPath(normalized);
|
|
54
|
+
|
|
55
|
+
if (test && !flags.wantsTest) return -50;
|
|
56
|
+
if (!test && flags.wantsTest) return -20;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function scorePathTopology(filePath, tokens, flags) {
|
|
60
|
+
const normalized = filePath.replaceAll("\\", "/").toLowerCase();
|
|
61
|
+
const penalty = topologyPenalty(normalized, flags);
|
|
62
|
+
|
|
63
|
+
if (penalty !== undefined) return penalty;
|
|
64
|
+
const ext = path.extname(normalized);
|
|
65
|
+
let score = SOURCE_EXT.has(ext) && !flags.wantsDoc ? 5 : 0;
|
|
66
|
+
|
|
67
|
+
if (flags.wantsType && TYPED_EXT.has(ext)) score += 10;
|
|
68
|
+
|
|
69
|
+
return score + tokenPathScore(path.basename(normalized), normalized.split(/[^a-zA-Z0-9]+/), normalized, tokens);
|
|
70
|
+
}
|
|
71
|
+
export { SOURCE_EXT, MAX_NEEDLE_CHARS };
|