pi-context-engine 0.1.4 → 0.3.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 +63 -0
- package/docs/IMPLEMENTATION-PLAN.md +30 -13
- package/package.json +83 -81
- package/src/compression/compressor.ts +119 -20
- package/src/config.ts +63 -3
- package/src/engine.ts +238 -13
- package/src/extension.ts +120 -9
- package/src/guidance.ts +1 -0
- package/src/indexer/chunker.ts +321 -21
- package/src/indexer/context-header.ts +86 -0
- package/src/indexer/embedder.ts +32 -5
- package/src/indexer/manifest.ts +41 -19
- package/src/indexer/pipeline.ts +435 -197
- package/src/memory/db.ts +166 -53
- package/src/retrieval/confidence.ts +53 -0
- package/src/retrieval/query-parser.ts +33 -0
- package/src/retrieval/reranker.ts +112 -0
- package/src/retrieval/retriever.ts +351 -14
- package/src/storage/fts-store.ts +48 -6
- package/src/storage/graph-store.ts +160 -4
- package/src/storage/local-backend.ts +36 -4
- package/src/storage/vector-store.ts +35 -29
- package/src/utils.ts +13 -0
|
@@ -24,6 +24,10 @@ CREATE TABLE IF NOT EXISTS edges (
|
|
|
24
24
|
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges (source_id);
|
|
25
25
|
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges (target_id);
|
|
26
26
|
CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes (file_path);
|
|
27
|
+
CREATE TABLE IF NOT EXISTS pagerank (
|
|
28
|
+
file_path TEXT PRIMARY KEY,
|
|
29
|
+
score REAL NOT NULL
|
|
30
|
+
);
|
|
27
31
|
`;
|
|
28
32
|
|
|
29
33
|
type Sqlite = SqliteHandle;
|
|
@@ -85,6 +89,64 @@ export class GraphStore {
|
|
|
85
89
|
return rows.map(rowToNode);
|
|
86
90
|
}
|
|
87
91
|
|
|
92
|
+
/**
|
|
93
|
+
* PR #7 review fix (incremental call graph): every function/class node
|
|
94
|
+
* across ALL indexed files, so calls-edge derivation on an incremental
|
|
95
|
+
* run still sees symbols owned by files that did not change this run.
|
|
96
|
+
*/
|
|
97
|
+
listSymbolNodes(): GraphNode[] {
|
|
98
|
+
const rows = this.db
|
|
99
|
+
.prepare(
|
|
100
|
+
"SELECT id, node_type, name, file_path, properties FROM nodes WHERE node_type IN ('function','class')",
|
|
101
|
+
)
|
|
102
|
+
.all() as Record<string, unknown>[];
|
|
103
|
+
return rows.map(rowToNode);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* R5: structured symbol lookup for the multi-path retriever. Matches a
|
|
108
|
+
* chunk-level node whose name EQUALS a queried identifier, ENDS with
|
|
109
|
+
* `.identifier` (so `charge` finds `PaymentGateway.charge`), or STARTS with
|
|
110
|
+
* `identifier.` (so `PaymentGateway` surfaces its methods). Exact matches
|
|
111
|
+
* rank first; results are deterministic (name length as the tiebreak) so
|
|
112
|
+
* RRF fusion is reproducible.
|
|
113
|
+
*/
|
|
114
|
+
findSymbols(names: string[], limit = 20): GraphNode[] {
|
|
115
|
+
if (names.length === 0) return [];
|
|
116
|
+
const conds: string[] = [];
|
|
117
|
+
const params: unknown[] = [];
|
|
118
|
+
for (const raw of names) {
|
|
119
|
+
const name = raw.trim().toLowerCase();
|
|
120
|
+
if (name.length < 3) continue;
|
|
121
|
+
conds.push("(lower(name) = ? OR lower(name) LIKE ? OR lower(name) LIKE ?)");
|
|
122
|
+
params.push(name, `%.${name}`, `${name}.%`);
|
|
123
|
+
}
|
|
124
|
+
if (conds.length === 0) return [];
|
|
125
|
+
params.push(Math.max(limit * 3, 30)); // overfetch: JS ranks exact-first, then cuts
|
|
126
|
+
let rows: Record<string, unknown>[];
|
|
127
|
+
try {
|
|
128
|
+
rows = this.db
|
|
129
|
+
.prepare(
|
|
130
|
+
`SELECT id, node_type, name, file_path, properties FROM nodes
|
|
131
|
+
WHERE node_type IN ('function', 'class', 'module', 'doc')
|
|
132
|
+
AND (${conds.join(" OR ")})
|
|
133
|
+
LIMIT ?`,
|
|
134
|
+
)
|
|
135
|
+
.all(...params) as Record<string, unknown>[];
|
|
136
|
+
} catch {
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
const exact = new Set(names.map((n) => n.trim().toLowerCase()));
|
|
140
|
+
const nodes = rows.map(rowToNode);
|
|
141
|
+
nodes.sort((a, b) => {
|
|
142
|
+
const ax = exact.has(a.name.toLowerCase()) ? 0 : 1;
|
|
143
|
+
const bx = exact.has(b.name.toLowerCase()) ? 0 : 1;
|
|
144
|
+
if (ax !== bx) return ax - bx;
|
|
145
|
+
return a.name.length - b.name.length;
|
|
146
|
+
});
|
|
147
|
+
return nodes.slice(0, limit);
|
|
148
|
+
}
|
|
149
|
+
|
|
88
150
|
neighborsForFiles(filePaths: string[], edgeTypes: EdgeType[], nodeTypes?: NodeType[]): GraphNode[] {
|
|
89
151
|
if (filePaths.length === 0 || edgeTypes.length === 0) return [];
|
|
90
152
|
const fpPh = filePaths.map(() => "?").join(",");
|
|
@@ -107,6 +169,86 @@ export class GraphStore {
|
|
|
107
169
|
return rows.map(rowToNode);
|
|
108
170
|
}
|
|
109
171
|
|
|
172
|
+
/**
|
|
173
|
+
* R7: PageRank over the file-level projection of the graph (imports +
|
|
174
|
+
* calls edges collapsed to file→file). Scores are normalised so the
|
|
175
|
+
* best-connected file is 1.0; the retriever applies them as a bounded
|
|
176
|
+
* multiplicative prior. Hub files genuinely central to a codebase —
|
|
177
|
+
* types, shared utils, the engine — consistently rank above leaves, which
|
|
178
|
+
* is exactly the tie-break signal fusion needs when lexical and vector
|
|
179
|
+
* evidence disagree.
|
|
180
|
+
*/
|
|
181
|
+
computePageRank(): void {
|
|
182
|
+
const nodeFile = new Map<string, string>();
|
|
183
|
+
for (const row of this.db.prepare("SELECT id, file_path FROM nodes").all() as {
|
|
184
|
+
id: string;
|
|
185
|
+
file_path: string;
|
|
186
|
+
}[]) {
|
|
187
|
+
nodeFile.set(row.id, row.file_path);
|
|
188
|
+
}
|
|
189
|
+
const edgeRows = this.db
|
|
190
|
+
.prepare("SELECT source_id, target_id FROM edges WHERE edge_type IN ('imports', 'calls')")
|
|
191
|
+
.all() as { source_id: string; target_id: string }[];
|
|
192
|
+
|
|
193
|
+
const nodes = new Set<string>();
|
|
194
|
+
for (const fp of nodeFile.values()) if (fp) nodes.add(fp);
|
|
195
|
+
const out = new Map<string, Set<string>>();
|
|
196
|
+
for (const e of edgeRows) {
|
|
197
|
+
const s = nodeFile.get(e.source_id);
|
|
198
|
+
const tgt = nodeFile.get(e.target_id);
|
|
199
|
+
if (!s || !tgt || s === tgt) continue;
|
|
200
|
+
let set = out.get(s);
|
|
201
|
+
if (!set) out.set(s, (set = new Set()));
|
|
202
|
+
set.add(tgt);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const run = this.db.transaction(() => {
|
|
206
|
+
this.db.exec("DELETE FROM pagerank");
|
|
207
|
+
if (nodes.size === 0) return;
|
|
208
|
+
const d = 0.85;
|
|
209
|
+
const n = nodes.size;
|
|
210
|
+
const incoming = new Map<string, string[]>();
|
|
211
|
+
for (const [s, targets] of out) {
|
|
212
|
+
for (const tgt of targets) {
|
|
213
|
+
let arr = incoming.get(tgt);
|
|
214
|
+
if (!arr) incoming.set(tgt, (arr = []));
|
|
215
|
+
arr.push(s);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
let rank = new Map<string, number>();
|
|
219
|
+
for (const node of nodes) rank.set(node, 1 / n);
|
|
220
|
+
for (let iter = 0; iter < 25; iter++) {
|
|
221
|
+
const next = new Map<string, number>();
|
|
222
|
+
let dangling = 0;
|
|
223
|
+
for (const node of nodes) {
|
|
224
|
+
const o = out.get(node);
|
|
225
|
+
if (!o || o.size === 0) dangling += rank.get(node) ?? 0;
|
|
226
|
+
}
|
|
227
|
+
for (const node of nodes) {
|
|
228
|
+
let s = (1 - d) / n + (d * dangling) / n;
|
|
229
|
+
for (const src of incoming.get(node) ?? []) {
|
|
230
|
+
s += (d * (rank.get(src) ?? 0)) / (out.get(src)?.size ?? 1);
|
|
231
|
+
}
|
|
232
|
+
next.set(node, s);
|
|
233
|
+
}
|
|
234
|
+
rank = next;
|
|
235
|
+
}
|
|
236
|
+
const max = Math.max(...rank.values(), 1e-12);
|
|
237
|
+
const ins = this.db.prepare("INSERT OR REPLACE INTO pagerank (file_path, score) VALUES (?, ?)");
|
|
238
|
+
for (const [fp, s] of rank) ins.run(fp, s / max);
|
|
239
|
+
});
|
|
240
|
+
run();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Read of the PageRank table (file_path → score in [0,1]). */
|
|
244
|
+
pageRanks(): Map<string, number> {
|
|
245
|
+
const rows = this.db.prepare("SELECT file_path, score FROM pagerank").all() as {
|
|
246
|
+
file_path: string;
|
|
247
|
+
score: number;
|
|
248
|
+
}[];
|
|
249
|
+
return new Map(rows.map((r) => [r.file_path, r.score]));
|
|
250
|
+
}
|
|
251
|
+
|
|
110
252
|
getNodesByType(nodeType: NodeType): GraphNode[] {
|
|
111
253
|
const rows = this.db
|
|
112
254
|
.prepare("SELECT id, node_type, name, file_path, properties FROM nodes WHERE node_type = ?")
|
|
@@ -126,15 +268,28 @@ export class GraphStore {
|
|
|
126
268
|
const nodeIds = (
|
|
127
269
|
this.db.prepare(`SELECT id FROM nodes WHERE file_path IN (${ph})`).all(...batch) as {
|
|
128
270
|
id: string;
|
|
129
|
-
|
|
271
|
+
}[]
|
|
130
272
|
).map((r) => r.id);
|
|
273
|
+
// R7: file_* node ids are STABLE across re-ingest of the same
|
|
274
|
+
// file, so inbound imports edges from other files stay valid.
|
|
275
|
+
// Only delete edges pointing at this file's chunk-level nodes
|
|
276
|
+
// (their ids change when content changes). Without this,
|
|
277
|
+
// re-indexing b.ts silently deleted every a.ts→b.ts imports
|
|
278
|
+
// edge unless a.ts changed too.
|
|
279
|
+
const chunkNodeIds = nodeIds.filter((id) => !id.startsWith("file_"));
|
|
131
280
|
for (const idBatch of batchedParams(nodeIds)) {
|
|
132
281
|
const idPh = idBatch.map(() => "?").join(",");
|
|
133
282
|
this.db
|
|
134
|
-
.prepare(`DELETE FROM
|
|
135
|
-
|
|
136
|
-
|
|
283
|
+
.prepare(`DELETE FROM nodes WHERE id IN (${idPh})`).run(...idBatch);
|
|
284
|
+
}
|
|
285
|
+
const srcPh = nodeIds.map(() => "?").join(",");
|
|
286
|
+
this.db.prepare("DELETE FROM edges WHERE source_id IN (" + srcPh + ")").run(...nodeIds);
|
|
287
|
+
if (chunkNodeIds.length > 0) {
|
|
288
|
+
const tgtPh = chunkNodeIds.map(() => "?").join(",");
|
|
289
|
+
this.db.prepare("DELETE FROM edges WHERE target_id IN (" + tgtPh + ")").run(...chunkNodeIds);
|
|
137
290
|
}
|
|
291
|
+
const filePh = batch.map(() => "?").join(",");
|
|
292
|
+
this.db.prepare("DELETE FROM pagerank WHERE file_path IN (" + filePh + ")").run(...batch);
|
|
138
293
|
}
|
|
139
294
|
});
|
|
140
295
|
run([...batchedParams(filePaths)]);
|
|
@@ -143,6 +298,7 @@ export class GraphStore {
|
|
|
143
298
|
clear(): void {
|
|
144
299
|
this.db.exec("DELETE FROM edges");
|
|
145
300
|
this.db.exec("DELETE FROM nodes");
|
|
301
|
+
this.db.exec("DELETE FROM pagerank");
|
|
146
302
|
}
|
|
147
303
|
|
|
148
304
|
close(): void {
|
|
@@ -11,6 +11,8 @@ export class LocalBackend {
|
|
|
11
11
|
private vectorStore: VectorStore;
|
|
12
12
|
private ftsStore: FTSStore;
|
|
13
13
|
private graphStore: GraphStore;
|
|
14
|
+
/** R7: cached PageRank table; refreshed by recomputePriors(). */
|
|
15
|
+
private pageRankCache: Map<string, number> | null = null;
|
|
14
16
|
|
|
15
17
|
constructor(basePath: string) {
|
|
16
18
|
this.vectorStore = new VectorStore(path.join(basePath, "vectors"));
|
|
@@ -18,10 +20,6 @@ export class LocalBackend {
|
|
|
18
20
|
this.graphStore = new GraphStore(path.join(basePath, "graph"));
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
get metricRebuilt(): boolean {
|
|
22
|
-
return this.vectorStore.metricRebuilt;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
23
|
ingest(chunks: Chunk[], nodes: GraphNode[], edges: GraphEdge[]): void {
|
|
26
24
|
this.vectorStore.ingest(chunks);
|
|
27
25
|
this.ftsStore.ingest(chunks);
|
|
@@ -56,6 +54,38 @@ export class LocalBackend {
|
|
|
56
54
|
return this.vectorStore.getById(chunkId);
|
|
57
55
|
}
|
|
58
56
|
|
|
57
|
+
/** R7: recompute file PageRank after indexing; retrieval reads it as a prior. */
|
|
58
|
+
recomputePriors(): void {
|
|
59
|
+
this.graphStore.computePageRank();
|
|
60
|
+
this.pageRankCache = null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** R7: cached file→[0,1] PageRank (empty until first recomputePriors). */
|
|
64
|
+
pageRanks(): Map<string, number> {
|
|
65
|
+
if (!this.pageRankCache) this.pageRankCache = this.graphStore.pageRanks();
|
|
66
|
+
return this.pageRankCache;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** R5: symbol lookup over graph node names (exact / `.suffix`). */
|
|
70
|
+
findSymbols(names: string[], limit = 20): GraphNode[] {
|
|
71
|
+
return this.graphStore.findSymbols(names, limit);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** R9: graph nodes of one file (sibling-symbol hints). */
|
|
75
|
+
getNodesByFile(filePath: string): GraphNode[] {
|
|
76
|
+
return this.graphStore.getNodesByFile(filePath);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** PR #7 review: all function/class nodes, for incremental call-graph derivation. */
|
|
80
|
+
listSymbolNodes(): GraphNode[] {
|
|
81
|
+
return this.graphStore.listSymbolNodes();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** R5: chunks of one file in source order. */
|
|
85
|
+
chunksByFile(filePath: string, limit = 10): Chunk[] {
|
|
86
|
+
return this.vectorStore.byFile(filePath, limit);
|
|
87
|
+
}
|
|
88
|
+
|
|
59
89
|
getChunksByIds(chunkIds: string[]): Chunk[] {
|
|
60
90
|
return this.vectorStore.getChunksByIds(chunkIds);
|
|
61
91
|
}
|
|
@@ -69,6 +99,7 @@ export class LocalBackend {
|
|
|
69
99
|
this.vectorStore.deleteByFiles(filePaths);
|
|
70
100
|
this.ftsStore.deleteByFiles(filePaths);
|
|
71
101
|
this.graphStore.deleteByFiles(filePaths);
|
|
102
|
+
this.pageRankCache = null;
|
|
72
103
|
}
|
|
73
104
|
|
|
74
105
|
countChunks(): number {
|
|
@@ -91,6 +122,7 @@ export class LocalBackend {
|
|
|
91
122
|
this.vectorStore.clear();
|
|
92
123
|
this.ftsStore.clear();
|
|
93
124
|
this.graphStore.clear();
|
|
125
|
+
this.pageRankCache = null;
|
|
94
126
|
}
|
|
95
127
|
|
|
96
128
|
close(): void {
|
|
@@ -13,6 +13,7 @@ import fs from "node:fs";
|
|
|
13
13
|
import path from "node:path";
|
|
14
14
|
import { openDatabase, type SqliteHandle } from "../sqlite.js";
|
|
15
15
|
import { batchedParams } from "../utils.js";
|
|
16
|
+
import { parseContextHeaderUnit } from "../indexer/context-header.js";
|
|
16
17
|
import type { Chunk, ChunkType } from "../types.js";
|
|
17
18
|
|
|
18
19
|
const MAX_CONTENT_CHARS = 5_000;
|
|
@@ -26,8 +27,6 @@ type Sqlite = SqliteHandle;
|
|
|
26
27
|
export class VectorStore {
|
|
27
28
|
private db: Sqlite;
|
|
28
29
|
private dim: number | null = null;
|
|
29
|
-
/** Set when a legacy L2 vec table was wiped — pipeline forces a full reindex. */
|
|
30
|
-
metricRebuilt = false;
|
|
31
30
|
constructor(dbPath: string) {
|
|
32
31
|
fs.mkdirSync(dbPath, { recursive: true });
|
|
33
32
|
// Without sqlite-vec the store still keeps chunk metadata; search()
|
|
@@ -36,10 +35,6 @@ export class VectorStore {
|
|
|
36
35
|
this.ensureTables();
|
|
37
36
|
}
|
|
38
37
|
|
|
39
|
-
private get vecAvailable(): boolean {
|
|
40
|
-
return this.dim !== null || this.vecTableExists();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
38
|
private vecTableExists(): boolean {
|
|
44
39
|
const row = this.db
|
|
45
40
|
.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='chunks_vec'")
|
|
@@ -60,10 +55,6 @@ export class VectorStore {
|
|
|
60
55
|
modified_ts REAL
|
|
61
56
|
)
|
|
62
57
|
`);
|
|
63
|
-
const cols = (this.db.pragma("table_info(chunks)") as { name: string }[]).map((c) => c.name);
|
|
64
|
-
if (!cols.includes("modified_ts")) {
|
|
65
|
-
this.db.exec("ALTER TABLE chunks ADD COLUMN modified_ts REAL");
|
|
66
|
-
}
|
|
67
58
|
this.db.exec("CREATE INDEX IF NOT EXISTS idx_chunks_file_path ON chunks(file_path)");
|
|
68
59
|
this.db.exec(`
|
|
69
60
|
CREATE TABLE IF NOT EXISTS chunk_compressions (
|
|
@@ -74,25 +65,19 @@ export class VectorStore {
|
|
|
74
65
|
)
|
|
75
66
|
`);
|
|
76
67
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (r) {
|
|
91
|
-
const len = this.db
|
|
92
|
-
.prepare("SELECT vec_length(embedding) AS len FROM chunks_vec LIMIT 1")
|
|
93
|
-
.get() as { len: number };
|
|
94
|
-
this.dim = len.len;
|
|
95
|
-
}
|
|
68
|
+
if (this.vecTableExists()) {
|
|
69
|
+
// The table exists — read its dimension. There is no migration path
|
|
70
|
+
// for tables this package did not write (older metric/column shapes):
|
|
71
|
+
// an unrecognized shape cannot occur from our own DDL, and foreign
|
|
72
|
+
// stores are unsupported by design.
|
|
73
|
+
const r = this.db.prepare("SELECT rowid FROM chunks_vec LIMIT 1").get() as
|
|
74
|
+
| { rowid: number }
|
|
75
|
+
| undefined;
|
|
76
|
+
if (r) {
|
|
77
|
+
const len = this.db
|
|
78
|
+
.prepare("SELECT vec_length(embedding) AS len FROM chunks_vec LIMIT 1")
|
|
79
|
+
.get() as { len: number };
|
|
80
|
+
this.dim = len.len;
|
|
96
81
|
}
|
|
97
82
|
}
|
|
98
83
|
}
|
|
@@ -144,6 +129,12 @@ export class VectorStore {
|
|
|
144
129
|
};
|
|
145
130
|
if (row.modified_ts != null) chunk.metadata["modified_ts"] = row.modified_ts;
|
|
146
131
|
if (distance !== undefined) chunk.metadata["_distance"] = distance;
|
|
132
|
+
// Restore `unit` from the R1 context header line so served chunks carry
|
|
133
|
+
// their symbol (refine hints filter siblings by it; the header is the
|
|
134
|
+
// single source of truth for indexed content). No header → no unit,
|
|
135
|
+
// same as before.
|
|
136
|
+
const unit = parseContextHeaderUnit(chunk.content);
|
|
137
|
+
if (unit) chunk.metadata["unit"] = unit;
|
|
147
138
|
return chunk;
|
|
148
139
|
}
|
|
149
140
|
|
|
@@ -327,6 +318,21 @@ export class VectorStore {
|
|
|
327
318
|
}
|
|
328
319
|
}
|
|
329
320
|
|
|
321
|
+
/** R5: chunks of one file in source order (file-hint retrieval path). */
|
|
322
|
+
byFile(filePath: string, limit = 10): Chunk[] {
|
|
323
|
+
try {
|
|
324
|
+
const rows = this.db
|
|
325
|
+
.prepare(
|
|
326
|
+
`SELECT id, content, chunk_type, file_path, start_line, end_line, language, modified_ts
|
|
327
|
+
FROM chunks WHERE file_path = ? ORDER BY start_line ASC LIMIT ?`,
|
|
328
|
+
)
|
|
329
|
+
.all(filePath, limit) as Record<string, unknown>[];
|
|
330
|
+
return rows.map((r) => this.rowToChunk(r));
|
|
331
|
+
} catch {
|
|
332
|
+
return [];
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
330
336
|
close(): void {
|
|
331
337
|
this.db.close();
|
|
332
338
|
}
|
package/src/utils.ts
CHANGED
|
@@ -79,6 +79,19 @@ export function epochSeconds(): number {
|
|
|
79
79
|
return Math.floor(Date.now() / 1000);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Cooperative macrotask yield for long-running indexing work. ONNX
|
|
84
|
+
* inference, tokenization and tree-sitter parsing all run on the main
|
|
85
|
+
* thread, so a long stretch of synchronous work starves pi's UI loop
|
|
86
|
+
* (measured by scripts/bench-event-loop.mts: ~0.5-0.9 s stalls per 64-text
|
|
87
|
+
* embed batch). setImmediate keeps the yield cheap — same loop turn's check
|
|
88
|
+
* phase — while still letting pending timers and I/O (keystrokes, renders)
|
|
89
|
+
* run between units of work.
|
|
90
|
+
*/
|
|
91
|
+
export function yieldToEventLoop(): Promise<void> {
|
|
92
|
+
return new Promise((resolve) => setImmediate(resolve));
|
|
93
|
+
}
|
|
94
|
+
|
|
82
95
|
/**
|
|
83
96
|
* Fractional seconds. Session-capture ordering needs sub-second resolution:
|
|
84
97
|
* several decisions are commonly recorded within the same wall-clock second,
|