@inerrata-corporation/errata 2.0.6-dev.2621 → 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.
@@ -16400,8 +16400,16 @@ var SqliteGraphStore = class {
16400
16400
  closeEdge: this.db.prepare(
16401
16401
  "UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
16402
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).
16403
16407
  nodeHistory: this.db.prepare(
16404
- "SELECT * FROM nodes WHERE COALESCE(logical_id, id) = ? ORDER BY version ASC"
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`
16405
16413
  ),
16406
16414
  nodesAsOf: this.db.prepare(
16407
16415
  "SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
@@ -16729,6 +16737,18 @@ var SqliteGraphStore = class {
16729
16737
  ).all(params);
16730
16738
  return rows.map(rowToEdge);
16731
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
+ }
16732
16752
  neighbors(nodeId, types) {
16733
16753
  const out = [];
16734
16754
  const edges = this.outEdges(nodeId, types);
@@ -16991,6 +17011,19 @@ var SqliteGraphStore = class {
16991
17011
  const rows = this.stmts.nodesAsOf.all({ t });
16992
17012
  return rows.map(rowToNode);
16993
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
+ }
16994
17027
  edgesAsOf(t) {
16995
17028
  const rows = this.stmts.edgesAsOf.all({ t });
16996
17029
  return rows.map(rowToEdge);
package/errata.mjs CHANGED
@@ -17760,8 +17760,16 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
17760
17760
  closeEdge: this.db.prepare(
17761
17761
  "UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
17762
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).
17763
17767
  nodeHistory: this.db.prepare(
17764
- "SELECT * FROM nodes WHERE COALESCE(logical_id, id) = ? ORDER BY version ASC"
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`
17765
17773
  ),
17766
17774
  nodesAsOf: this.db.prepare(
17767
17775
  "SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
@@ -18089,6 +18097,18 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
18089
18097
  ).all(params);
18090
18098
  return rows.map(rowToEdge);
18091
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
+ }
18092
18112
  neighbors(nodeId, types) {
18093
18113
  const out2 = [];
18094
18114
  const edges = this.outEdges(nodeId, types);
@@ -18351,6 +18371,19 @@ CREATE INDEX IF NOT EXISTS nodes_relpath ON nodes(json_extract(attrs_json, '$.re
18351
18371
  const rows = this.stmts.nodesAsOf.all({ t });
18352
18372
  return rows.map(rowToNode);
18353
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
+ }
18354
18387
  edgesAsOf(t) {
18355
18388
  const rows = this.stmts.edgesAsOf.all({ t });
18356
18389
  return rows.map(rowToEdge);
@@ -18757,7 +18790,7 @@ function clearRevisit(store, nodeId, ts) {
18757
18790
  store.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
18758
18791
  }
18759
18792
  function listNeedsRevisit(store, asOf) {
18760
- return store.nodesAsOf(asOf).filter((n) => n.attrs["revisit"] === true).map((n) => ({
18793
+ return store.nodesAsOfWhereTrue(asOf, "revisit").map((n) => ({
18761
18794
  id: n.id,
18762
18795
  label: n.label,
18763
18796
  description: n.description,
@@ -31739,16 +31772,17 @@ function diagnosticKey(relPath, d) {
31739
31772
  function anchorByLine(store, workspaceId2, relPath, line) {
31740
31773
  let best = null;
31741
31774
  let bestRow = -1;
31742
- for (const label of ["Function", "Method", "Class"]) {
31743
- for (const n of store.findNodesByLabel(label)) {
31744
- if (n.attrs["workspaceId"] !== workspaceId2) continue;
31745
- if (n.attrs["relPath"] !== relPath) continue;
31746
- const row = n.attrs["startRow"];
31747
- if (typeof row !== "number" || row > line) continue;
31748
- if (row > bestRow) {
31749
- bestRow = row;
31750
- best = n;
31751
- }
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;
31752
31786
  }
31753
31787
  }
31754
31788
  return best ? best.logicalId ?? best.id : null;
@@ -42800,7 +42834,8 @@ var init_mcp = __esm({
42800
42834
  items: dead.slice(0, limit)
42801
42835
  };
42802
42836
  }
42803
- const ranked = codeNodes.map((n) => ({ id: n.id, label: n.label, name: n.description, fanIn: store.inEdges(n.id, [...CODE_REACH_EDGES]).length })).sort((a, b) => b.fanIn - a.fanIn).slice(0, limit);
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);
42804
42839
  return { kind: "fan-in", items: ranked };
42805
42840
  }
42806
42841
  },
@@ -78443,7 +78478,7 @@ function createWatchBreaker(deps) {
78443
78478
  }
78444
78479
 
78445
78480
  // src/engine.ts
78446
- var DAEMON_VERSION = true ? "2.0.6-dev.2621" : "2.0.0-alpha.0";
78481
+ var DAEMON_VERSION = true ? "2.0.6-dev.2627" : "2.0.0-alpha.0";
78447
78482
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
78448
78483
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
78449
78484
  var GIT_OP_MUTE_MS = 4e3;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.6-dev.2621",
3
+ "version": "2.0.6-dev.2627",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -24782,8 +24782,16 @@ var SqliteGraphStore = class {
24782
24782
  closeEdge: this.db.prepare(
24783
24783
  "UPDATE edges SET valid_to = :t WHERE id = :id AND valid_to IS NULL"
24784
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).
24785
24789
  nodeHistory: this.db.prepare(
24786
- "SELECT * FROM nodes WHERE COALESCE(logical_id, id) = ? ORDER BY version ASC"
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`
24787
24795
  ),
24788
24796
  nodesAsOf: this.db.prepare(
24789
24797
  "SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)"
@@ -25111,6 +25119,18 @@ var SqliteGraphStore = class {
25111
25119
  ).all(params);
25112
25120
  return rows.map(rowToEdge);
25113
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
+ }
25114
25134
  neighbors(nodeId, types) {
25115
25135
  const out2 = [];
25116
25136
  const edges = this.outEdges(nodeId, types);
@@ -25373,6 +25393,19 @@ var SqliteGraphStore = class {
25373
25393
  const rows = this.stmts.nodesAsOf.all({ t });
25374
25394
  return rows.map(rowToNode);
25375
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
+ }
25376
25409
  edgesAsOf(t) {
25377
25410
  const rows = this.stmts.edgesAsOf.all({ t });
25378
25411
  return rows.map(rowToEdge);
@@ -25488,7 +25521,7 @@ function clearRevisit(store2, nodeId, ts) {
25488
25521
  store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
25489
25522
  }
25490
25523
  function listNeedsRevisit(store2, asOf) {
25491
- return store2.nodesAsOf(asOf).filter((n) => n.attrs["revisit"] === true).map((n) => ({
25524
+ return store2.nodesAsOfWhereTrue(asOf, "revisit").map((n) => ({
25492
25525
  id: n.id,
25493
25526
  label: n.label,
25494
25527
  description: n.description,