@inerrata-corporation/errata 2.0.6-dev.2618 → 2.0.6-dev.2627
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 +51 -1
- package/errata.mjs +67 -15
- package/package.json +1 -1
- package/pass-worker.mjs +52 -2
package/consolidate-worker.mjs
CHANGED
|
@@ -16343,6 +16343,10 @@ var SqliteGraphStore = class {
|
|
|
16343
16343
|
countByLabel: this.db.prepare(
|
|
16344
16344
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
16345
16345
|
),
|
|
16346
|
+
// Attrs twin of findByLabel — same live rows, no embedding blob read.
|
|
16347
|
+
attrsByLabel: this.db.prepare(
|
|
16348
|
+
"SELECT id, created_at, created_at_seq, attrs_json FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
16349
|
+
),
|
|
16346
16350
|
// Identity twin of findByLabel — same live-row semantics, two attrs read in
|
|
16347
16351
|
// SQL, no attrs JSON.parse or embedding decode (PJ-anchor-reach).
|
|
16348
16352
|
identitiesByLabel: this.db.prepare(
|
|
@@ -16396,8 +16400,16 @@ var SqliteGraphStore = class {
|
|
|
16396
16400
|
closeEdge: this.db.prepare(
|
|
16397
16401
|
"UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
|
|
16398
16402
|
),
|
|
16403
|
+
// Same rows as `COALESCE(logical_id, id) = ?`, split so each branch can use
|
|
16404
|
+
// an index (nodes_logical, then the id primary key). The COALESCE form made
|
|
16405
|
+
// SQLite SCAN nodes: 0.4-0.9s per call on a 227k-node store, once per new
|
|
16406
|
+
// diagnostic via attributeRootCause (2026-09-24).
|
|
16399
16407
|
nodeHistory: this.db.prepare(
|
|
16400
|
-
|
|
16408
|
+
`SELECT * FROM (
|
|
16409
|
+
SELECT * FROM nodes WHERE logical_id = ?1
|
|
16410
|
+
UNION ALL
|
|
16411
|
+
SELECT * FROM nodes WHERE id = ?1 AND logical_id IS NULL
|
|
16412
|
+
) ORDER BY version ASC`
|
|
16401
16413
|
),
|
|
16402
16414
|
nodesAsOf: this.db.prepare(
|
|
16403
16415
|
"SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
|
|
@@ -16648,6 +16660,19 @@ var SqliteGraphStore = class {
|
|
|
16648
16660
|
countNodesByLabel(label) {
|
|
16649
16661
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
16650
16662
|
}
|
|
16663
|
+
/** Live rows for a label with attrs and creation stamps only — for scans that
|
|
16664
|
+
* read attrs and never the vector. `findNodesByLabel` decodes every embedding
|
|
16665
|
+
* blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
|
|
16666
|
+
* hold doing exactly that on a 227k-node store (2026-09-24). */
|
|
16667
|
+
findNodeAttrsByLabel(label) {
|
|
16668
|
+
const rows = this.stmts.attrsByLabel.all(label);
|
|
16669
|
+
return rows.map((r) => ({
|
|
16670
|
+
id: r.id,
|
|
16671
|
+
createdAt: r.created_at,
|
|
16672
|
+
...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
|
|
16673
|
+
attrs: JSON.parse(r.attrs_json)
|
|
16674
|
+
}));
|
|
16675
|
+
}
|
|
16651
16676
|
findNodeIdentitiesByLabel(label) {
|
|
16652
16677
|
const rows = this.stmts.identitiesByLabel.all(label);
|
|
16653
16678
|
return rows.map((r) => ({
|
|
@@ -16712,6 +16737,18 @@ var SqliteGraphStore = class {
|
|
|
16712
16737
|
).all(params);
|
|
16713
16738
|
return rows.map(rowToEdge);
|
|
16714
16739
|
}
|
|
16740
|
+
/** Live in-degree per target over `types`, in one grouped query — the counts
|
|
16741
|
+
* `inEdges(id, types).length` gives, for every node at once. A per-node
|
|
16742
|
+
* inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
|
|
16743
|
+
* hotspots vfile materialized (2026-09-24). */
|
|
16744
|
+
inDegreeByType(types) {
|
|
16745
|
+
if (types.length === 0) return /* @__PURE__ */ new Map();
|
|
16746
|
+
const placeholders = types.map(() => "?").join(",");
|
|
16747
|
+
const rows = this.db.prepare(
|
|
16748
|
+
`SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
|
|
16749
|
+
).all(...types);
|
|
16750
|
+
return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
|
|
16751
|
+
}
|
|
16715
16752
|
neighbors(nodeId, types) {
|
|
16716
16753
|
const out = [];
|
|
16717
16754
|
const edges = this.outEdges(nodeId, types);
|
|
@@ -16974,6 +17011,19 @@ var SqliteGraphStore = class {
|
|
|
16974
17011
|
const rows = this.stmts.nodesAsOf.all({ t });
|
|
16975
17012
|
return rows.map(rowToNode);
|
|
16976
17013
|
}
|
|
17014
|
+
/** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
|
|
17015
|
+
* SQL, so only the flagged rows are hydrated. The filter-after form hydrated
|
|
17016
|
+
* the whole graph for a handful of revisit flags: 6.7s per context render on
|
|
17017
|
+
* a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
|
|
17018
|
+
* so it must be a plain identifier. */
|
|
17019
|
+
nodesAsOfWhereTrue(t, attr) {
|
|
17020
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
|
|
17021
|
+
const rows = this.db.prepare(
|
|
17022
|
+
`SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
|
|
17023
|
+
AND json_type(attrs_json, '$.${attr}') = 'true'`
|
|
17024
|
+
).all({ t });
|
|
17025
|
+
return rows.map(rowToNode);
|
|
17026
|
+
}
|
|
16977
17027
|
edgesAsOf(t) {
|
|
16978
17028
|
const rows = this.stmts.edgesAsOf.all({ t });
|
|
16979
17029
|
return rows.map(rowToEdge);
|
package/errata.mjs
CHANGED
|
@@ -17703,6 +17703,10 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17703
17703
|
countByLabel: this.db.prepare(
|
|
17704
17704
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
17705
17705
|
),
|
|
17706
|
+
// Attrs twin of findByLabel — same live rows, no embedding blob read.
|
|
17707
|
+
attrsByLabel: this.db.prepare(
|
|
17708
|
+
"SELECT id, created_at, created_at_seq, attrs_json FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
17709
|
+
),
|
|
17706
17710
|
// Identity twin of findByLabel — same live-row semantics, two attrs read in
|
|
17707
17711
|
// SQL, no attrs JSON.parse or embedding decode (PJ-anchor-reach).
|
|
17708
17712
|
identitiesByLabel: this.db.prepare(
|
|
@@ -17756,8 +17760,16 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
17756
17760
|
closeEdge: this.db.prepare(
|
|
17757
17761
|
"UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
|
|
17758
17762
|
),
|
|
17763
|
+
// Same rows as `COALESCE(logical_id, id) = ?`, split so each branch can use
|
|
17764
|
+
// an index (nodes_logical, then the id primary key). The COALESCE form made
|
|
17765
|
+
// SQLite SCAN nodes: 0.4-0.9s per call on a 227k-node store, once per new
|
|
17766
|
+
// diagnostic via attributeRootCause (2026-09-24).
|
|
17759
17767
|
nodeHistory: this.db.prepare(
|
|
17760
|
-
|
|
17768
|
+
`SELECT * FROM (
|
|
17769
|
+
SELECT * FROM nodes WHERE logical_id = ?1
|
|
17770
|
+
UNION ALL
|
|
17771
|
+
SELECT * FROM nodes WHERE id = ?1 AND logical_id IS NULL
|
|
17772
|
+
) ORDER BY version ASC`
|
|
17761
17773
|
),
|
|
17762
17774
|
nodesAsOf: this.db.prepare(
|
|
17763
17775
|
"SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
|
|
@@ -18008,6 +18020,19 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
18008
18020
|
countNodesByLabel(label) {
|
|
18009
18021
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
18010
18022
|
}
|
|
18023
|
+
/** Live rows for a label with attrs and creation stamps only — for scans that
|
|
18024
|
+
* read attrs and never the vector. `findNodesByLabel` decodes every embedding
|
|
18025
|
+
* blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
|
|
18026
|
+
* hold doing exactly that on a 227k-node store (2026-09-24). */
|
|
18027
|
+
findNodeAttrsByLabel(label) {
|
|
18028
|
+
const rows = this.stmts.attrsByLabel.all(label);
|
|
18029
|
+
return rows.map((r) => ({
|
|
18030
|
+
id: r.id,
|
|
18031
|
+
createdAt: r.created_at,
|
|
18032
|
+
...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
|
|
18033
|
+
attrs: JSON.parse(r.attrs_json)
|
|
18034
|
+
}));
|
|
18035
|
+
}
|
|
18011
18036
|
findNodeIdentitiesByLabel(label) {
|
|
18012
18037
|
const rows = this.stmts.identitiesByLabel.all(label);
|
|
18013
18038
|
return rows.map((r) => ({
|
|
@@ -18072,6 +18097,18 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
18072
18097
|
).all(params);
|
|
18073
18098
|
return rows.map(rowToEdge);
|
|
18074
18099
|
}
|
|
18100
|
+
/** Live in-degree per target over `types`, in one grouped query — the counts
|
|
18101
|
+
* `inEdges(id, types).length` gives, for every node at once. A per-node
|
|
18102
|
+
* inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
|
|
18103
|
+
* hotspots vfile materialized (2026-09-24). */
|
|
18104
|
+
inDegreeByType(types) {
|
|
18105
|
+
if (types.length === 0) return /* @__PURE__ */ new Map();
|
|
18106
|
+
const placeholders = types.map(() => "?").join(",");
|
|
18107
|
+
const rows = this.db.prepare(
|
|
18108
|
+
`SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
|
|
18109
|
+
).all(...types);
|
|
18110
|
+
return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
|
|
18111
|
+
}
|
|
18075
18112
|
neighbors(nodeId, types) {
|
|
18076
18113
|
const out2 = [];
|
|
18077
18114
|
const edges = this.outEdges(nodeId, types);
|
|
@@ -18334,6 +18371,19 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
|
|
|
18334
18371
|
const rows = this.stmts.nodesAsOf.all({ t });
|
|
18335
18372
|
return rows.map(rowToNode);
|
|
18336
18373
|
}
|
|
18374
|
+
/** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
|
|
18375
|
+
* SQL, so only the flagged rows are hydrated. The filter-after form hydrated
|
|
18376
|
+
* the whole graph for a handful of revisit flags: 6.7s per context render on
|
|
18377
|
+
* a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
|
|
18378
|
+
* so it must be a plain identifier. */
|
|
18379
|
+
nodesAsOfWhereTrue(t, attr) {
|
|
18380
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
|
|
18381
|
+
const rows = this.db.prepare(
|
|
18382
|
+
`SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
|
|
18383
|
+
AND json_type(attrs_json, '$.${attr}') = 'true'`
|
|
18384
|
+
).all({ t });
|
|
18385
|
+
return rows.map(rowToNode);
|
|
18386
|
+
}
|
|
18337
18387
|
edgesAsOf(t) {
|
|
18338
18388
|
const rows = this.stmts.edgesAsOf.all({ t });
|
|
18339
18389
|
return rows.map(rowToEdge);
|
|
@@ -18740,7 +18790,7 @@ function clearRevisit(store, nodeId, ts) {
|
|
|
18740
18790
|
store.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
|
|
18741
18791
|
}
|
|
18742
18792
|
function listNeedsRevisit(store, asOf) {
|
|
18743
|
-
return store.
|
|
18793
|
+
return store.nodesAsOfWhereTrue(asOf, "revisit").map((n) => ({
|
|
18744
18794
|
id: n.id,
|
|
18745
18795
|
label: n.label,
|
|
18746
18796
|
description: n.description,
|
|
@@ -23458,7 +23508,7 @@ function evaluateFed(store, d) {
|
|
|
23458
23508
|
const column = COLUMN_INPUTS[d.fed.attr];
|
|
23459
23509
|
const fed = d.fed;
|
|
23460
23510
|
const has = (n) => column ? column(n) : n.attrs[fed.attr] !== void 0;
|
|
23461
|
-
const nodes = fed.labels.flatMap((label) => store.
|
|
23511
|
+
const nodes = fed.labels.flatMap((label) => store.findNodeAttrsByLabel(label));
|
|
23462
23512
|
let window2 = nodes;
|
|
23463
23513
|
if (fed.recent !== void 0) {
|
|
23464
23514
|
window2 = [...nodes].sort((a, b) => b.createdAt - a.createdAt).slice(0, fed.recent);
|
|
@@ -31722,16 +31772,17 @@ function diagnosticKey(relPath, d) {
|
|
|
31722
31772
|
function anchorByLine(store, workspaceId2, relPath, line) {
|
|
31723
31773
|
let best = null;
|
|
31724
31774
|
let bestRow = -1;
|
|
31725
|
-
|
|
31726
|
-
|
|
31727
|
-
|
|
31728
|
-
|
|
31729
|
-
|
|
31730
|
-
|
|
31731
|
-
|
|
31732
|
-
|
|
31733
|
-
|
|
31734
|
-
|
|
31775
|
+
const labelRank = { Function: 0, Method: 1, Class: 2 };
|
|
31776
|
+
for (const n of store.nodesByRelPath([relPath])) {
|
|
31777
|
+
const rank = labelRank[n.label];
|
|
31778
|
+
if (rank === void 0) continue;
|
|
31779
|
+
if (n.attrs["workspaceId"] !== workspaceId2) continue;
|
|
31780
|
+
if (n.attrs["relPath"] !== relPath) continue;
|
|
31781
|
+
const row = n.attrs["startRow"];
|
|
31782
|
+
if (typeof row !== "number" || row > line) continue;
|
|
31783
|
+
if (row > bestRow || row === bestRow && best !== null && rank < labelRank[best.label]) {
|
|
31784
|
+
bestRow = row;
|
|
31785
|
+
best = n;
|
|
31735
31786
|
}
|
|
31736
31787
|
}
|
|
31737
31788
|
return best ? best.logicalId ?? best.id : null;
|
|
@@ -42783,7 +42834,8 @@ var init_mcp = __esm({
|
|
|
42783
42834
|
items: dead.slice(0, limit)
|
|
42784
42835
|
};
|
|
42785
42836
|
}
|
|
42786
|
-
const
|
|
42837
|
+
const fanIn = store.inDegreeByType([...CODE_REACH_EDGES]);
|
|
42838
|
+
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);
|
|
42787
42839
|
return { kind: "fan-in", items: ranked };
|
|
42788
42840
|
}
|
|
42789
42841
|
},
|
|
@@ -78426,7 +78478,7 @@ function createWatchBreaker(deps) {
|
|
|
78426
78478
|
}
|
|
78427
78479
|
|
|
78428
78480
|
// src/engine.ts
|
|
78429
|
-
var DAEMON_VERSION = true ? "2.0.6-dev.
|
|
78481
|
+
var DAEMON_VERSION = true ? "2.0.6-dev.2627" : "2.0.0-alpha.0";
|
|
78430
78482
|
var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
|
|
78431
78483
|
var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
|
|
78432
78484
|
var GIT_OP_MUTE_MS = 4e3;
|
package/package.json
CHANGED
package/pass-worker.mjs
CHANGED
|
@@ -24725,6 +24725,10 @@ var SqliteGraphStore = class {
|
|
|
24725
24725
|
countByLabel: this.db.prepare(
|
|
24726
24726
|
"SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
24727
24727
|
),
|
|
24728
|
+
// Attrs twin of findByLabel — same live rows, no embedding blob read.
|
|
24729
|
+
attrsByLabel: this.db.prepare(
|
|
24730
|
+
"SELECT id, created_at, created_at_seq, attrs_json FROM nodes WHERE label = ? AND valid_to IS NULL"
|
|
24731
|
+
),
|
|
24728
24732
|
// Identity twin of findByLabel — same live-row semantics, two attrs read in
|
|
24729
24733
|
// SQL, no attrs JSON.parse or embedding decode (PJ-anchor-reach).
|
|
24730
24734
|
identitiesByLabel: this.db.prepare(
|
|
@@ -24778,8 +24782,16 @@ var SqliteGraphStore = class {
|
|
|
24778
24782
|
closeEdge: this.db.prepare(
|
|
24779
24783
|
"UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
|
|
24780
24784
|
),
|
|
24785
|
+
// Same rows as `COALESCE(logical_id, id) = ?`, split so each branch can use
|
|
24786
|
+
// an index (nodes_logical, then the id primary key). The COALESCE form made
|
|
24787
|
+
// SQLite SCAN nodes: 0.4-0.9s per call on a 227k-node store, once per new
|
|
24788
|
+
// diagnostic via attributeRootCause (2026-09-24).
|
|
24781
24789
|
nodeHistory: this.db.prepare(
|
|
24782
|
-
|
|
24790
|
+
`SELECT * FROM (
|
|
24791
|
+
SELECT * FROM nodes WHERE logical_id = ?1
|
|
24792
|
+
UNION ALL
|
|
24793
|
+
SELECT * FROM nodes WHERE id = ?1 AND logical_id IS NULL
|
|
24794
|
+
) ORDER BY version ASC`
|
|
24783
24795
|
),
|
|
24784
24796
|
nodesAsOf: this.db.prepare(
|
|
24785
24797
|
"SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
|
|
@@ -25030,6 +25042,19 @@ var SqliteGraphStore = class {
|
|
|
25030
25042
|
countNodesByLabel(label) {
|
|
25031
25043
|
return Number(this.stmts.countByLabel.get(label).n);
|
|
25032
25044
|
}
|
|
25045
|
+
/** Live rows for a label with attrs and creation stamps only — for scans that
|
|
25046
|
+
* read attrs and never the vector. `findNodesByLabel` decodes every embedding
|
|
25047
|
+
* blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
|
|
25048
|
+
* hold doing exactly that on a 227k-node store (2026-09-24). */
|
|
25049
|
+
findNodeAttrsByLabel(label) {
|
|
25050
|
+
const rows = this.stmts.attrsByLabel.all(label);
|
|
25051
|
+
return rows.map((r) => ({
|
|
25052
|
+
id: r.id,
|
|
25053
|
+
createdAt: r.created_at,
|
|
25054
|
+
...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
|
|
25055
|
+
attrs: JSON.parse(r.attrs_json)
|
|
25056
|
+
}));
|
|
25057
|
+
}
|
|
25033
25058
|
findNodeIdentitiesByLabel(label) {
|
|
25034
25059
|
const rows = this.stmts.identitiesByLabel.all(label);
|
|
25035
25060
|
return rows.map((r) => ({
|
|
@@ -25094,6 +25119,18 @@ var SqliteGraphStore = class {
|
|
|
25094
25119
|
).all(params);
|
|
25095
25120
|
return rows.map(rowToEdge);
|
|
25096
25121
|
}
|
|
25122
|
+
/** Live in-degree per target over `types`, in one grouped query — the counts
|
|
25123
|
+
* `inEdges(id, types).length` gives, for every node at once. A per-node
|
|
25124
|
+
* inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
|
|
25125
|
+
* hotspots vfile materialized (2026-09-24). */
|
|
25126
|
+
inDegreeByType(types) {
|
|
25127
|
+
if (types.length === 0) return /* @__PURE__ */ new Map();
|
|
25128
|
+
const placeholders = types.map(() => "?").join(",");
|
|
25129
|
+
const rows = this.db.prepare(
|
|
25130
|
+
`SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
|
|
25131
|
+
).all(...types);
|
|
25132
|
+
return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
|
|
25133
|
+
}
|
|
25097
25134
|
neighbors(nodeId, types) {
|
|
25098
25135
|
const out2 = [];
|
|
25099
25136
|
const edges = this.outEdges(nodeId, types);
|
|
@@ -25356,6 +25393,19 @@ var SqliteGraphStore = class {
|
|
|
25356
25393
|
const rows = this.stmts.nodesAsOf.all({ t });
|
|
25357
25394
|
return rows.map(rowToNode);
|
|
25358
25395
|
}
|
|
25396
|
+
/** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
|
|
25397
|
+
* SQL, so only the flagged rows are hydrated. The filter-after form hydrated
|
|
25398
|
+
* the whole graph for a handful of revisit flags: 6.7s per context render on
|
|
25399
|
+
* a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
|
|
25400
|
+
* so it must be a plain identifier. */
|
|
25401
|
+
nodesAsOfWhereTrue(t, attr) {
|
|
25402
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
|
|
25403
|
+
const rows = this.db.prepare(
|
|
25404
|
+
`SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
|
|
25405
|
+
AND json_type(attrs_json, '$.${attr}') = 'true'`
|
|
25406
|
+
).all({ t });
|
|
25407
|
+
return rows.map(rowToNode);
|
|
25408
|
+
}
|
|
25359
25409
|
edgesAsOf(t) {
|
|
25360
25410
|
const rows = this.stmts.edgesAsOf.all({ t });
|
|
25361
25411
|
return rows.map(rowToEdge);
|
|
@@ -25471,7 +25521,7 @@ function clearRevisit(store2, nodeId, ts) {
|
|
|
25471
25521
|
store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
|
|
25472
25522
|
}
|
|
25473
25523
|
function listNeedsRevisit(store2, asOf) {
|
|
25474
|
-
return store2.
|
|
25524
|
+
return store2.nodesAsOfWhereTrue(asOf, "revisit").map((n) => ({
|
|
25475
25525
|
id: n.id,
|
|
25476
25526
|
label: n.label,
|
|
25477
25527
|
description: n.description,
|