@inerrata-corporation/errata 2.0.6-dev.2621 → 2.0.6-dev.2632
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/consolidate-worker.mjs +52 -1
- package/errata.mjs +98 -26
- package/package.json +1 -1
- package/pass-worker.mjs +53 -2
package/consolidate-worker.mjs
CHANGED
|
@@ -16143,6 +16143,9 @@ CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
|
|
|
16143
16143
|
-- incremental reindex can fetch just the changed files' symbols instead of
|
|
16144
16144
|
-- scanning every versioned node. Partial: only live rows are ever queried.
|
|
16145
16145
|
CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.relPath')) WHERE valid_to IS NULL;
|
|
16146
|
+
-- Live rows by name, for looking up the few symbols a statement mentions
|
|
16147
|
+
-- instead of hydrating every code symbol per minted problem.
|
|
16148
|
+
CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
|
|
16146
16149
|
`;
|
|
16147
16150
|
function encodeEmbedding(emb) {
|
|
16148
16151
|
if (!emb || emb.length === 0) return null;
|
|
@@ -16400,8 +16403,16 @@ var SqliteGraphStore = class {
|
|
|
16400
16403
|
closeEdge: this.db.prepare(
|
|
16401
16404
|
"UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
|
|
16402
16405
|
),
|
|
16406
|
+
// Same rows as `COALESCE(logical_id, id) = ?`, split so each branch can use
|
|
16407
|
+
// an index (nodes_logical, then the id primary key). The COALESCE form made
|
|
16408
|
+
// SQLite SCAN nodes: 0.4-0.9s per call on a 227k-node store, once per new
|
|
16409
|
+
// diagnostic via attributeRootCause (2026-09-24).
|
|
16403
16410
|
nodeHistory: this.db.prepare(
|
|
16404
|
-
|
|
16411
|
+
`SELECT * FROM (
|
|
16412
|
+
SELECT * FROM nodes WHERE logical_id = ?1
|
|
16413
|
+
UNION ALL
|
|
16414
|
+
SELECT * FROM nodes WHERE id = ?1 AND logical_id IS NULL
|
|
16415
|
+
) ORDER BY version ASC`
|
|
16405
16416
|
),
|
|
16406
16417
|
nodesAsOf: this.db.prepare(
|
|
16407
16418
|
"SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
|
|
@@ -16729,6 +16740,18 @@ var SqliteGraphStore = class {
|
|
|
16729
16740
|
).all(params);
|
|
16730
16741
|
return rows.map(rowToEdge);
|
|
16731
16742
|
}
|
|
16743
|
+
/** Live in-degree per target over `types`, in one grouped query — the counts
|
|
16744
|
+
* `inEdges(id, types).length` gives, for every node at once. A per-node
|
|
16745
|
+
* inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
|
|
16746
|
+
* hotspots vfile materialized (2026-09-24). */
|
|
16747
|
+
inDegreeByType(types) {
|
|
16748
|
+
if (types.length === 0) return /* @__PURE__ */ new Map();
|
|
16749
|
+
const placeholders = types.map(() => "?").join(",");
|
|
16750
|
+
const rows = this.db.prepare(
|
|
16751
|
+
`SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
|
|
16752
|
+
).all(...types);
|
|
16753
|
+
return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
|
|
16754
|
+
}
|
|
16732
16755
|
neighbors(nodeId, types) {
|
|
16733
16756
|
const out = [];
|
|
16734
16757
|
const edges = this.outEdges(nodeId, types);
|
|
@@ -16937,6 +16960,21 @@ var SqliteGraphStore = class {
|
|
|
16937
16960
|
/** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
|
|
16938
16961
|
* expression index so the incremental reindex fetches only the changed files'
|
|
16939
16962
|
* symbols instead of scanning every versioned node. */
|
|
16963
|
+
/** Live nodes of `labels` whose description is exactly one of `names`, via
|
|
16964
|
+
* the nodes_live_description index. Chunked under the bound-parameter cap. */
|
|
16965
|
+
liveNodesByDescription(names, labels) {
|
|
16966
|
+
if (names.length === 0 || labels.length === 0) return [];
|
|
16967
|
+
const out = [];
|
|
16968
|
+
const labelPh = labels.map(() => "?").join(",");
|
|
16969
|
+
for (let i = 0; i < names.length; i += 400) {
|
|
16970
|
+
const chunk = names.slice(i, i + 400);
|
|
16971
|
+
const rows = this.db.prepare(
|
|
16972
|
+
`SELECT * FROM nodes WHERE valid_to IS NULL AND description IN (${chunk.map(() => "?").join(",")}) AND label IN (${labelPh})`
|
|
16973
|
+
).all(...chunk, ...labels);
|
|
16974
|
+
for (const r of rows) out.push(rowToNode(r));
|
|
16975
|
+
}
|
|
16976
|
+
return out;
|
|
16977
|
+
}
|
|
16940
16978
|
nodesByRelPath(relPaths) {
|
|
16941
16979
|
if (relPaths.length === 0) return [];
|
|
16942
16980
|
const placeholders = relPaths.map(() => "?").join(",");
|
|
@@ -16991,6 +17029,19 @@ var SqliteGraphStore = class {
|
|
|
16991
17029
|
const rows = this.stmts.nodesAsOf.all({ t });
|
|
16992
17030
|
return rows.map(rowToNode);
|
|
16993
17031
|
}
|
|
17032
|
+
/** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
|
|
17033
|
+
* SQL, so only the flagged rows are hydrated. The filter-after form hydrated
|
|
17034
|
+
* the whole graph for a handful of revisit flags: 6.7s per context render on
|
|
17035
|
+
* a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
|
|
17036
|
+
* so it must be a plain identifier. */
|
|
17037
|
+
nodesAsOfWhereTrue(t, attr) {
|
|
17038
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
|
|
17039
|
+
const rows = this.db.prepare(
|
|
17040
|
+
`SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
|
|
17041
|
+
AND json_type(attrs_json, '$.${attr}') = 'true'`
|
|
17042
|
+
).all({ t });
|
|
17043
|
+
return rows.map(rowToNode);
|
|
17044
|
+
}
|
|
16994
17045
|
edgesAsOf(t) {
|
|
16995
17046
|
const rows = this.stmts.edgesAsOf.all({ t });
|
|
16996
17047
|
return rows.map(rowToEdge);
|
package/errata.mjs
CHANGED
|
@@ -17560,6 +17560,9 @@ CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
|
|
|
17560
17560
|
-- incremental reindex can fetch just the changed files' symbols instead of
|
|
17561
17561
|
-- scanning every versioned node. Partial: only live rows are ever queried.
|
|
17562
17562
|
CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.relPath')) WHERE valid_to IS NULL;
|
|
17563
|
+
-- Live rows by name, for looking up the few symbols a statement mentions
|
|
17564
|
+
-- instead of hydrating every code symbol per minted problem.
|
|
17565
|
+
CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
|
|
17563
17566
|
`;
|
|
17564
17567
|
SqliteGraphStore = class {
|
|
17565
17568
|
db;
|
|
@@ -17760,8 +17763,16 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17760
17763
|
closeEdge: this.db.prepare(
|
|
17761
17764
|
"UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
|
|
17762
17765
|
),
|
|
17766
|
+
// Same rows as `COALESCE(logical_id, id) = ?`, split so each branch can use
|
|
17767
|
+
// an index (nodes_logical, then the id primary key). The COALESCE form made
|
|
17768
|
+
// SQLite SCAN nodes: 0.4-0.9s per call on a 227k-node store, once per new
|
|
17769
|
+
// diagnostic via attributeRootCause (2026-09-24).
|
|
17763
17770
|
nodeHistory: this.db.prepare(
|
|
17764
|
-
|
|
17771
|
+
`SELECT * FROM (
|
|
17772
|
+
SELECT * FROM nodes WHERE logical_id = ?1
|
|
17773
|
+
UNION ALL
|
|
17774
|
+
SELECT * FROM nodes WHERE id = ?1 AND logical_id IS NULL
|
|
17775
|
+
) ORDER BY version ASC`
|
|
17765
17776
|
),
|
|
17766
17777
|
nodesAsOf: this.db.prepare(
|
|
17767
17778
|
"SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
|
|
@@ -18089,6 +18100,18 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
18089
18100
|
).all(params);
|
|
18090
18101
|
return rows.map(rowToEdge);
|
|
18091
18102
|
}
|
|
18103
|
+
/** Live in-degree per target over `types`, in one grouped query — the counts
|
|
18104
|
+
* `inEdges(id, types).length` gives, for every node at once. A per-node
|
|
18105
|
+
* inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
|
|
18106
|
+
* hotspots vfile materialized (2026-09-24). */
|
|
18107
|
+
inDegreeByType(types) {
|
|
18108
|
+
if (types.length === 0) return /* @__PURE__ */ new Map();
|
|
18109
|
+
const placeholders = types.map(() => "?").join(",");
|
|
18110
|
+
const rows = this.db.prepare(
|
|
18111
|
+
`SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
|
|
18112
|
+
).all(...types);
|
|
18113
|
+
return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
|
|
18114
|
+
}
|
|
18092
18115
|
neighbors(nodeId, types) {
|
|
18093
18116
|
const out2 = [];
|
|
18094
18117
|
const edges = this.outEdges(nodeId, types);
|
|
@@ -18297,6 +18320,21 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
18297
18320
|
/** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
|
|
18298
18321
|
* expression index so the incremental reindex fetches only the changed files'
|
|
18299
18322
|
* symbols instead of scanning every versioned node. */
|
|
18323
|
+
/** Live nodes of `labels` whose description is exactly one of `names`, via
|
|
18324
|
+
* the nodes_live_description index. Chunked under the bound-parameter cap. */
|
|
18325
|
+
liveNodesByDescription(names, labels) {
|
|
18326
|
+
if (names.length === 0 || labels.length === 0) return [];
|
|
18327
|
+
const out2 = [];
|
|
18328
|
+
const labelPh = labels.map(() => "?").join(",");
|
|
18329
|
+
for (let i2 = 0; i2 < names.length; i2 += 400) {
|
|
18330
|
+
const chunk = names.slice(i2, i2 + 400);
|
|
18331
|
+
const rows = this.db.prepare(
|
|
18332
|
+
`SELECT * FROM nodes WHERE valid_to IS NULL AND description IN (${chunk.map(() => "?").join(",")}) AND label IN (${labelPh})`
|
|
18333
|
+
).all(...chunk, ...labels);
|
|
18334
|
+
for (const r of rows) out2.push(rowToNode(r));
|
|
18335
|
+
}
|
|
18336
|
+
return out2;
|
|
18337
|
+
}
|
|
18300
18338
|
nodesByRelPath(relPaths) {
|
|
18301
18339
|
if (relPaths.length === 0) return [];
|
|
18302
18340
|
const placeholders = relPaths.map(() => "?").join(",");
|
|
@@ -18351,6 +18389,19 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
18351
18389
|
const rows = this.stmts.nodesAsOf.all({ t });
|
|
18352
18390
|
return rows.map(rowToNode);
|
|
18353
18391
|
}
|
|
18392
|
+
/** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
|
|
18393
|
+
* SQL, so only the flagged rows are hydrated. The filter-after form hydrated
|
|
18394
|
+
* the whole graph for a handful of revisit flags: 6.7s per context render on
|
|
18395
|
+
* a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
|
|
18396
|
+
* so it must be a plain identifier. */
|
|
18397
|
+
nodesAsOfWhereTrue(t, attr) {
|
|
18398
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
|
|
18399
|
+
const rows = this.db.prepare(
|
|
18400
|
+
`SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
|
|
18401
|
+
AND json_type(attrs_json, '$.${attr}') = 'true'`
|
|
18402
|
+
).all({ t });
|
|
18403
|
+
return rows.map(rowToNode);
|
|
18404
|
+
}
|
|
18354
18405
|
edgesAsOf(t) {
|
|
18355
18406
|
const rows = this.stmts.edgesAsOf.all({ t });
|
|
18356
18407
|
return rows.map(rowToEdge);
|
|
@@ -18757,7 +18808,7 @@ function clearRevisit(store, nodeId, ts) {
|
|
|
18757
18808
|
store.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
|
|
18758
18809
|
}
|
|
18759
18810
|
function listNeedsRevisit(store, asOf) {
|
|
18760
|
-
return store.
|
|
18811
|
+
return store.nodesAsOfWhereTrue(asOf, "revisit").map((n) => ({
|
|
18761
18812
|
id: n.id,
|
|
18762
18813
|
label: n.label,
|
|
18763
18814
|
description: n.description,
|
|
@@ -19270,6 +19321,33 @@ function buildSymbolIndex(store) {
|
|
|
19270
19321
|
}
|
|
19271
19322
|
return idx;
|
|
19272
19323
|
}
|
|
19324
|
+
function symbolCandidatesInText(text) {
|
|
19325
|
+
const out2 = /* @__PURE__ */ new Set();
|
|
19326
|
+
for (const run3 of text.match(/[\w$.]+/g) ?? []) {
|
|
19327
|
+
const segs = run3.split(".");
|
|
19328
|
+
for (let i2 = 0; i2 < segs.length; i2++) {
|
|
19329
|
+
if (!segs[i2]) continue;
|
|
19330
|
+
let name2 = "";
|
|
19331
|
+
for (let j = i2; j < segs.length && segs[j]; j++) {
|
|
19332
|
+
name2 = j === i2 ? segs[j] : `${name2}.${segs[j]}`;
|
|
19333
|
+
if (isShapedSymbol(name2)) out2.add(name2);
|
|
19334
|
+
}
|
|
19335
|
+
}
|
|
19336
|
+
}
|
|
19337
|
+
return [...out2];
|
|
19338
|
+
}
|
|
19339
|
+
function symbolIndexForText(store, text) {
|
|
19340
|
+
const counts = /* @__PURE__ */ new Map();
|
|
19341
|
+
for (const n of store.liveNodesByDescription(symbolCandidatesInText(text), SYMBOL_LABELS)) {
|
|
19342
|
+
const name2 = n.description.trim();
|
|
19343
|
+
const list = counts.get(name2);
|
|
19344
|
+
if (list) list.push(n);
|
|
19345
|
+
else counts.set(name2, [n]);
|
|
19346
|
+
}
|
|
19347
|
+
const idx = /* @__PURE__ */ new Map();
|
|
19348
|
+
for (const [name2, nodes] of counts) if (nodes.length === 1) idx.set(name2, nodes[0]);
|
|
19349
|
+
return idx;
|
|
19350
|
+
}
|
|
19273
19351
|
function matchSymbolsInText(text, index) {
|
|
19274
19352
|
const out2 = [];
|
|
19275
19353
|
for (const [name2, node2] of index) {
|
|
@@ -19282,19 +19360,9 @@ function matchSymbolsInText(text, index) {
|
|
|
19282
19360
|
function linkProblemToSymbols(store, problemId, text, ts, index) {
|
|
19283
19361
|
const p = store.getNode(problemId);
|
|
19284
19362
|
if (!p || p.label !== "Problem") return [];
|
|
19285
|
-
const idx = index ??
|
|
19363
|
+
const idx = index ?? symbolIndexForText(store, text);
|
|
19286
19364
|
if (idx.size === 0) return [];
|
|
19287
|
-
|
|
19288
|
-
const fileFor2 = (relPath) => {
|
|
19289
|
-
if (!fileByPath) {
|
|
19290
|
-
fileByPath = /* @__PURE__ */ new Map();
|
|
19291
|
-
for (const f of store.findNodesByLabel("File")) {
|
|
19292
|
-
const rp = String(f.attrs["relPath"] ?? f.description ?? "");
|
|
19293
|
-
if (rp) fileByPath.set(rp, f);
|
|
19294
|
-
}
|
|
19295
|
-
}
|
|
19296
|
-
return fileByPath.get(relPath) ?? null;
|
|
19297
|
-
};
|
|
19365
|
+
const fileFor2 = (relPath) => store.nodesByRelPath([relPath]).find((f) => f.label === "File") ?? null;
|
|
19298
19366
|
const already = new Set(store.outEdges(problemId, ["ANCHORED_AT"]).map((e) => e.to));
|
|
19299
19367
|
const linked = [];
|
|
19300
19368
|
for (const sym of matchSymbolsInText(text, idx)) {
|
|
@@ -24019,6 +24087,8 @@ __export(src_exports2, {
|
|
|
24019
24087
|
stampObservedOs: () => stampObservedOs,
|
|
24020
24088
|
statementIdentityKey: () => statementIdentityKey,
|
|
24021
24089
|
staticConductance: () => staticConductance,
|
|
24090
|
+
symbolCandidatesInText: () => symbolCandidatesInText,
|
|
24091
|
+
symbolIndexForText: () => symbolIndexForText,
|
|
24022
24092
|
toolNodeId: () => toolNodeId,
|
|
24023
24093
|
toolPriorsFor: () => toolPriorsFor,
|
|
24024
24094
|
triageOf: () => triageOf,
|
|
@@ -31739,16 +31809,17 @@ function diagnosticKey(relPath, d) {
|
|
|
31739
31809
|
function anchorByLine(store, workspaceId2, relPath, line) {
|
|
31740
31810
|
let best = null;
|
|
31741
31811
|
let bestRow = -1;
|
|
31742
|
-
|
|
31743
|
-
|
|
31744
|
-
|
|
31745
|
-
|
|
31746
|
-
|
|
31747
|
-
|
|
31748
|
-
|
|
31749
|
-
|
|
31750
|
-
|
|
31751
|
-
|
|
31812
|
+
const labelRank = { Function: 0, Method: 1, Class: 2 };
|
|
31813
|
+
for (const n of store.nodesByRelPath([relPath])) {
|
|
31814
|
+
const rank = labelRank[n.label];
|
|
31815
|
+
if (rank === void 0) continue;
|
|
31816
|
+
if (n.attrs["workspaceId"] !== workspaceId2) continue;
|
|
31817
|
+
if (n.attrs["relPath"] !== relPath) continue;
|
|
31818
|
+
const row = n.attrs["startRow"];
|
|
31819
|
+
if (typeof row !== "number" || row > line) continue;
|
|
31820
|
+
if (row > bestRow || row === bestRow && best !== null && rank < labelRank[best.label]) {
|
|
31821
|
+
bestRow = row;
|
|
31822
|
+
best = n;
|
|
31752
31823
|
}
|
|
31753
31824
|
}
|
|
31754
31825
|
return best ? best.logicalId ?? best.id : null;
|
|
@@ -42800,7 +42871,8 @@ var init_mcp = __esm({
|
|
|
42800
42871
|
items: dead.slice(0, limit)
|
|
42801
42872
|
};
|
|
42802
42873
|
}
|
|
42803
|
-
const
|
|
42874
|
+
const fanIn = store.inDegreeByType([...CODE_REACH_EDGES]);
|
|
42875
|
+
const ranked = codeNodes.map((n) => ({ id: n.id, label: n.label, name: n.description, fanIn: fanIn.get(n.id) ?? 0 })).sort((a, b) => b.fanIn - a.fanIn).slice(0, limit);
|
|
42804
42876
|
return { kind: "fan-in", items: ranked };
|
|
42805
42877
|
}
|
|
42806
42878
|
},
|
|
@@ -78443,7 +78515,7 @@ function createWatchBreaker(deps) {
|
|
|
78443
78515
|
}
|
|
78444
78516
|
|
|
78445
78517
|
// src/engine.ts
|
|
78446
|
-
var DAEMON_VERSION = true ? "2.0.6-dev.
|
|
78518
|
+
var DAEMON_VERSION = true ? "2.0.6-dev.2632" : "2.0.0-alpha.0";
|
|
78447
78519
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
78448
78520
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
78449
78521
|
var GIT_OP_MUTE_MS = 4e3;
|
package/package.json
CHANGED
package/pass-worker.mjs
CHANGED
|
@@ -24525,6 +24525,9 @@ CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
|
|
|
24525
24525
|
-- incremental reindex can fetch just the changed files' symbols instead of
|
|
24526
24526
|
-- scanning every versioned node. Partial: only live rows are ever queried.
|
|
24527
24527
|
CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.relPath')) WHERE valid_to IS NULL;
|
|
24528
|
+
-- Live rows by name, for looking up the few symbols a statement mentions
|
|
24529
|
+
-- instead of hydrating every code symbol per minted problem.
|
|
24530
|
+
CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) WHERE valid_to IS NULL;
|
|
24528
24531
|
`;
|
|
24529
24532
|
function encodeEmbedding(emb) {
|
|
24530
24533
|
if (!emb || emb.length === 0) return null;
|
|
@@ -24782,8 +24785,16 @@ var SqliteGraphStore = class {
|
|
|
24782
24785
|
closeEdge: this.db.prepare(
|
|
24783
24786
|
"UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
|
|
24784
24787
|
),
|
|
24788
|
+
// Same rows as `COALESCE(logical_id, id) = ?`, split so each branch can use
|
|
24789
|
+
// an index (nodes_logical, then the id primary key). The COALESCE form made
|
|
24790
|
+
// SQLite SCAN nodes: 0.4-0.9s per call on a 227k-node store, once per new
|
|
24791
|
+
// diagnostic via attributeRootCause (2026-09-24).
|
|
24785
24792
|
nodeHistory: this.db.prepare(
|
|
24786
|
-
|
|
24793
|
+
`SELECT * FROM (
|
|
24794
|
+
SELECT * FROM nodes WHERE logical_id = ?1
|
|
24795
|
+
UNION ALL
|
|
24796
|
+
SELECT * FROM nodes WHERE id = ?1 AND logical_id IS NULL
|
|
24797
|
+
) ORDER BY version ASC`
|
|
24787
24798
|
),
|
|
24788
24799
|
nodesAsOf: this.db.prepare(
|
|
24789
24800
|
"SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
|
|
@@ -25111,6 +25122,18 @@ var SqliteGraphStore = class {
|
|
|
25111
25122
|
).all(params);
|
|
25112
25123
|
return rows.map(rowToEdge);
|
|
25113
25124
|
}
|
|
25125
|
+
/** Live in-degree per target over `types`, in one grouped query — the counts
|
|
25126
|
+
* `inEdges(id, types).length` gives, for every node at once. A per-node
|
|
25127
|
+
* inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
|
|
25128
|
+
* hotspots vfile materialized (2026-09-24). */
|
|
25129
|
+
inDegreeByType(types) {
|
|
25130
|
+
if (types.length === 0) return /* @__PURE__ */ new Map();
|
|
25131
|
+
const placeholders = types.map(() => "?").join(",");
|
|
25132
|
+
const rows = this.db.prepare(
|
|
25133
|
+
`SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
|
|
25134
|
+
).all(...types);
|
|
25135
|
+
return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
|
|
25136
|
+
}
|
|
25114
25137
|
neighbors(nodeId, types) {
|
|
25115
25138
|
const out2 = [];
|
|
25116
25139
|
const edges = this.outEdges(nodeId, types);
|
|
@@ -25319,6 +25342,21 @@ var SqliteGraphStore = class {
|
|
|
25319
25342
|
/** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
|
|
25320
25343
|
* expression index so the incremental reindex fetches only the changed files'
|
|
25321
25344
|
* symbols instead of scanning every versioned node. */
|
|
25345
|
+
/** Live nodes of `labels` whose description is exactly one of `names`, via
|
|
25346
|
+
* the nodes_live_description index. Chunked under the bound-parameter cap. */
|
|
25347
|
+
liveNodesByDescription(names, labels) {
|
|
25348
|
+
if (names.length === 0 || labels.length === 0) return [];
|
|
25349
|
+
const out2 = [];
|
|
25350
|
+
const labelPh = labels.map(() => "?").join(",");
|
|
25351
|
+
for (let i2 = 0; i2 < names.length; i2 += 400) {
|
|
25352
|
+
const chunk = names.slice(i2, i2 + 400);
|
|
25353
|
+
const rows = this.db.prepare(
|
|
25354
|
+
`SELECT * FROM nodes WHERE valid_to IS NULL AND description IN (${chunk.map(() => "?").join(",")}) AND label IN (${labelPh})`
|
|
25355
|
+
).all(...chunk, ...labels);
|
|
25356
|
+
for (const r of rows) out2.push(rowToNode(r));
|
|
25357
|
+
}
|
|
25358
|
+
return out2;
|
|
25359
|
+
}
|
|
25322
25360
|
nodesByRelPath(relPaths) {
|
|
25323
25361
|
if (relPaths.length === 0) return [];
|
|
25324
25362
|
const placeholders = relPaths.map(() => "?").join(",");
|
|
@@ -25373,6 +25411,19 @@ var SqliteGraphStore = class {
|
|
|
25373
25411
|
const rows = this.stmts.nodesAsOf.all({ t });
|
|
25374
25412
|
return rows.map(rowToNode);
|
|
25375
25413
|
}
|
|
25414
|
+
/** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
|
|
25415
|
+
* SQL, so only the flagged rows are hydrated. The filter-after form hydrated
|
|
25416
|
+
* the whole graph for a handful of revisit flags: 6.7s per context render on
|
|
25417
|
+
* a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
|
|
25418
|
+
* so it must be a plain identifier. */
|
|
25419
|
+
nodesAsOfWhereTrue(t, attr) {
|
|
25420
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
|
|
25421
|
+
const rows = this.db.prepare(
|
|
25422
|
+
`SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
|
|
25423
|
+
AND json_type(attrs_json, '$.${attr}') = 'true'`
|
|
25424
|
+
).all({ t });
|
|
25425
|
+
return rows.map(rowToNode);
|
|
25426
|
+
}
|
|
25376
25427
|
edgesAsOf(t) {
|
|
25377
25428
|
const rows = this.stmts.edgesAsOf.all({ t });
|
|
25378
25429
|
return rows.map(rowToEdge);
|
|
@@ -25488,7 +25539,7 @@ function clearRevisit(store2, nodeId, ts) {
|
|
|
25488
25539
|
store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
|
|
25489
25540
|
}
|
|
25490
25541
|
function listNeedsRevisit(store2, asOf) {
|
|
25491
|
-
return store2.
|
|
25542
|
+
return store2.nodesAsOfWhereTrue(asOf, "revisit").map((n) => ({
|
|
25492
25543
|
id: n.id,
|
|
25493
25544
|
label: n.label,
|
|
25494
25545
|
description: n.description,
|