@inerrata-corporation/errata 2.0.23-dev.4734 → 2.0.23-dev.4779

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.
@@ -17613,7 +17613,8 @@ var SELECT_INDEXES = [
17613
17613
  "nodes_live_fold_candidate",
17614
17614
  "nodes_live_resolved",
17615
17615
  "nodes_live_fix_candidate",
17616
- "nodes_live_anchor_hint"
17616
+ "nodes_live_anchor_hint",
17617
+ "nodes_live_created_v2"
17617
17618
  ];
17618
17619
  var EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
17619
17620
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -17934,7 +17935,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
17934
17935
  `;
17935
17936
  var TEMPORAL_INDEX_SQL = `
17936
17937
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
17937
- CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
17938
+ -- Dropped: a prefix of nodes_live_page_rank (valid_to, page_rank), which serves
17939
+ -- the live count and the "any live node?" check as a covering index too. It was
17940
+ -- re-keyed on every close and revive: ~11 KB of a symbol flip's ~80 KB.
17941
+ DROP INDEX IF EXISTS nodes_live_count;
17938
17942
  -- Problems by recency (the only label read newest-first). Over every live row,
17939
17943
  -- each write that stamps last_updated_at moved a code symbol's entry from a
17940
17944
  -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
@@ -17946,7 +17950,13 @@ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_update
17946
17950
  -- walks nodes_live and reads every live row of the label to test created_at:
17947
17951
  -- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
17948
17952
  -- is a key column so the planner prefers this over nodes_live (one more term).
17949
- CREATE INDEX IF NOT EXISTS nodes_live_created ON nodes(label, valid_to, created_at) WHERE valid_to IS NULL;
17953
+ -- last_updated_at rides along for errata.recent on code labels, which names this
17954
+ -- index: the filter runs on index entries, not on every row of the label (79 ms
17955
+ -- \u2192 3 ms for Function on a 1.1 GB store). It costs no extra writes: a close or
17956
+ -- revive already re-keys this index, and mergeNodeIfChanged leaves a symbol's
17957
+ -- last_updated_at alone when only its position moved.
17958
+ DROP INDEX IF EXISTS nodes_live_created;
17959
+ CREATE INDEX IF NOT EXISTS nodes_live_created_v2 ON nodes(label, valid_to, created_at, last_updated_at) WHERE valid_to IS NULL;
17950
17960
  -- Community members (the skills pull reads them twice per session boundary):
17951
17961
  -- ~15% of semantic nodes carry a localCommunity, and without this every live
17952
17962
  -- semantic row was read to find them (168 MB \u2192 18 MB per read).
@@ -18131,6 +18141,12 @@ function sameSqlValue(a, b) {
18131
18141
  if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
18132
18142
  return false;
18133
18143
  }
18144
+ var POSITION_ATTRS = ["startByte", "endByte", "startRow"];
18145
+ function attrsJsonWithoutPositions(attrs) {
18146
+ const rest = { ...attrs };
18147
+ for (const k of POSITION_ATTRS) delete rest[k];
18148
+ return JSON.stringify(rest);
18149
+ }
18134
18150
  var PRIOR_TAG_MARKER = '"priorTag":true';
18135
18151
  var SqliteGraphStore = class {
18136
18152
  db;
@@ -18231,7 +18247,7 @@ var SqliteGraphStore = class {
18231
18247
  lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
18232
18248
  labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
18233
18249
  nodeContent: this.db.prepare(
18234
- "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
18250
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, last_updated_at, attrs_json FROM nodes WHERE id = ?"
18235
18251
  ),
18236
18252
  edgeContent: this.db.prepare(
18237
18253
  "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
@@ -18644,11 +18660,23 @@ var SqliteGraphStore = class {
18644
18660
  * rank, community, landmark, surprise, hits, tier and the vector belong to
18645
18661
  * the passes that compute them, and a skipped write leaves them as they are
18646
18662
  * where a merge would have reset them to the producer's defaults. Returns
18647
- * whether it wrote. */
18663
+ * whether it wrote.
18664
+ *
18665
+ * A live row whose only change is its position keeps its lastUpdatedAt, so
18666
+ * that stamp means "this symbol's content changed": what errata.recent lists
18667
+ * and the fix-candidate ask reads, and what lets nodes_live_created_v2 carry
18668
+ * it without being re-keyed by every edit above a symbol. A closed row is
18669
+ * always stamped: reviveReobserved reopens what this pass stamped. */
18648
18670
  mergeNodeIfChanged(node) {
18649
18671
  const row = this.stmts.nodeContent.get(node.id);
18650
- if (row && row.valid_to === null && row.label === node.label && row.description === node.description && row.extraction_confidence === node.extractionConfidence && row.stability === node.stability && !EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0) && row.attrs_json === JSON.stringify(node.attrs ?? {})) return false;
18651
- this.mergeNode(node);
18672
+ if (!row || row.valid_to !== null || row.label !== node.label || row.description !== node.description || row.extraction_confidence !== node.extractionConfidence || row.stability !== node.stability || EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0)) {
18673
+ this.mergeNode(node);
18674
+ return true;
18675
+ }
18676
+ const attrs = node.attrs ?? {};
18677
+ if (row.attrs_json === JSON.stringify(attrs)) return false;
18678
+ const moved = attrsJsonWithoutPositions(JSON.parse(row.attrs_json)) === attrsJsonWithoutPositions(attrs);
18679
+ this.mergeNode(moved ? { ...node, lastUpdatedAt: row.last_updated_at } : node);
18652
18680
  return true;
18653
18681
  }
18654
18682
  updateNode(id, patch) {
@@ -18728,8 +18756,8 @@ var SqliteGraphStore = class {
18728
18756
  stmt2 = this.db.prepare(
18729
18757
  // Ordered, not left to the planner: readers take "first row wins" (the
18730
18758
  // symbol lexicon's name winner and doc pick), and the order otherwise
18731
- // followed whichever partial index the planner chose. nodes_live_created
18732
- // serves this order without a sort.
18759
+ // followed whichever partial index the planner chose. nodes_live_created_v2
18760
+ // serves it, sorting rowids only within an equal created_at.
18733
18761
  `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
18734
18762
  );
18735
18763
  this.fieldStmts.set(cacheKey, stmt2);
package/errata.mjs CHANGED
@@ -20645,7 +20645,8 @@ var init_node_select = __esm({
20645
20645
  "nodes_live_fold_candidate",
20646
20646
  "nodes_live_resolved",
20647
20647
  "nodes_live_fix_candidate",
20648
- "nodes_live_anchor_hint"
20648
+ "nodes_live_anchor_hint",
20649
+ "nodes_live_created_v2"
20649
20650
  ];
20650
20651
  EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
20651
20652
  KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -20761,10 +20762,15 @@ function sameSqlValue(a, b) {
20761
20762
  if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
20762
20763
  return false;
20763
20764
  }
20765
+ function attrsJsonWithoutPositions(attrs) {
20766
+ const rest2 = { ...attrs };
20767
+ for (const k of POSITION_ATTRS) delete rest2[k];
20768
+ return JSON.stringify(rest2);
20769
+ }
20764
20770
  function openGraphStore(opts) {
20765
20771
  return new SqliteGraphStore(opts);
20766
20772
  }
20767
- var PAGE_RANK_SCALE_KEY, TEXT_SEARCH_COLUMNS, LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, EXPOSURE_SIDE_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, NODE_LABELS_CHUNK, NODE_UPSERT_COLUMNS, EDGE_UPSERT_COLUMNS, PRIOR_TAG_MARKER, SqliteGraphStore;
20773
+ var PAGE_RANK_SCALE_KEY, TEXT_SEARCH_COLUMNS, LOCAL_RULE_OVERRIDES, SCHEMA_VERSION, SESSION_REPAIR_KEY, EXPOSURE_SIDE_KEY, SESSION_EVIDENCE_PREDICATE, SCHEMA_SQL, TEMPORAL_INDEX_SQL, FINGERPRINT_INDEXED_LABELS, NODE_LABELS_CHUNK, NODE_UPSERT_COLUMNS, EDGE_UPSERT_COLUMNS, POSITION_ATTRS, PRIOR_TAG_MARKER, SqliteGraphStore;
20768
20774
  var init_store = __esm({
20769
20775
  "../../packages/local-graph/src/store.ts"() {
20770
20776
  "use strict";
@@ -20918,7 +20924,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
20918
20924
  `;
20919
20925
  TEMPORAL_INDEX_SQL = `
20920
20926
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
20921
- CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
20927
+ -- Dropped: a prefix of nodes_live_page_rank (valid_to, page_rank), which serves
20928
+ -- the live count and the "any live node?" check as a covering index too. It was
20929
+ -- re-keyed on every close and revive: ~11 KB of a symbol flip's ~80 KB.
20930
+ DROP INDEX IF EXISTS nodes_live_count;
20922
20931
  -- Problems by recency (the only label read newest-first). Over every live row,
20923
20932
  -- each write that stamps last_updated_at moved a code symbol's entry from a
20924
20933
  -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
@@ -20930,7 +20939,13 @@ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_update
20930
20939
  -- walks nodes_live and reads every live row of the label to test created_at:
20931
20940
  -- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
20932
20941
  -- is a key column so the planner prefers this over nodes_live (one more term).
20933
- CREATE INDEX IF NOT EXISTS nodes_live_created ON nodes(label, valid_to, created_at) WHERE valid_to IS NULL;
20942
+ -- last_updated_at rides along for errata.recent on code labels, which names this
20943
+ -- index: the filter runs on index entries, not on every row of the label (79 ms
20944
+ -- \u2192 3 ms for Function on a 1.1 GB store). It costs no extra writes: a close or
20945
+ -- revive already re-keys this index, and mergeNodeIfChanged leaves a symbol's
20946
+ -- last_updated_at alone when only its position moved.
20947
+ DROP INDEX IF EXISTS nodes_live_created;
20948
+ CREATE INDEX IF NOT EXISTS nodes_live_created_v2 ON nodes(label, valid_to, created_at, last_updated_at) WHERE valid_to IS NULL;
20934
20949
  -- Community members (the skills pull reads them twice per session boundary):
20935
20950
  -- ~15% of semantic nodes carry a localCommunity, and without this every live
20936
20951
  -- semantic row was read to find them (168 MB \u2192 18 MB per read).
@@ -21053,6 +21068,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21053
21068
  "attrs_json"
21054
21069
  ];
21055
21070
  EDGE_UPSERT_COLUMNS = ["confidence", "last_seen_at", "nav_successes", "nav_failures", "valid_to", "attrs_json"];
21071
+ POSITION_ATTRS = ["startByte", "endByte", "startRow"];
21056
21072
  PRIOR_TAG_MARKER = '"priorTag":true';
21057
21073
  SqliteGraphStore = class {
21058
21074
  db;
@@ -21153,7 +21169,7 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21153
21169
  lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
21154
21170
  labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
21155
21171
  nodeContent: this.db.prepare(
21156
- "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
21172
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, last_updated_at, attrs_json FROM nodes WHERE id = ?"
21157
21173
  ),
21158
21174
  edgeContent: this.db.prepare(
21159
21175
  "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
@@ -21566,11 +21582,23 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21566
21582
  * rank, community, landmark, surprise, hits, tier and the vector belong to
21567
21583
  * the passes that compute them, and a skipped write leaves them as they are
21568
21584
  * where a merge would have reset them to the producer's defaults. Returns
21569
- * whether it wrote. */
21585
+ * whether it wrote.
21586
+ *
21587
+ * A live row whose only change is its position keeps its lastUpdatedAt, so
21588
+ * that stamp means "this symbol's content changed": what errata.recent lists
21589
+ * and the fix-candidate ask reads, and what lets nodes_live_created_v2 carry
21590
+ * it without being re-keyed by every edit above a symbol. A closed row is
21591
+ * always stamped: reviveReobserved reopens what this pass stamped. */
21570
21592
  mergeNodeIfChanged(node2) {
21571
21593
  const row = this.stmts.nodeContent.get(node2.id);
21572
- if (row && row.valid_to === null && row.label === node2.label && row.description === node2.description && row.extraction_confidence === node2.extractionConfidence && row.stability === node2.stability && !EXPOSURE_ATTRS.some((k) => node2.attrs?.[k] !== void 0) && row.attrs_json === JSON.stringify(node2.attrs ?? {})) return false;
21573
- this.mergeNode(node2);
21594
+ if (!row || row.valid_to !== null || row.label !== node2.label || row.description !== node2.description || row.extraction_confidence !== node2.extractionConfidence || row.stability !== node2.stability || EXPOSURE_ATTRS.some((k) => node2.attrs?.[k] !== void 0)) {
21595
+ this.mergeNode(node2);
21596
+ return true;
21597
+ }
21598
+ const attrs = node2.attrs ?? {};
21599
+ if (row.attrs_json === JSON.stringify(attrs)) return false;
21600
+ const moved = attrsJsonWithoutPositions(JSON.parse(row.attrs_json)) === attrsJsonWithoutPositions(attrs);
21601
+ this.mergeNode(moved ? { ...node2, lastUpdatedAt: row.last_updated_at } : node2);
21574
21602
  return true;
21575
21603
  }
21576
21604
  updateNode(id, patch) {
@@ -21650,8 +21678,8 @@ CREATE INDEX IF NOT EXISTS nodes_revisit_true ON nodes(valid_from) WHERE json_ty
21650
21678
  stmt2 = this.db.prepare(
21651
21679
  // Ordered, not left to the planner: readers take "first row wins" (the
21652
21680
  // symbol lexicon's name winner and doc pick), and the order otherwise
21653
- // followed whichever partial index the planner chose. nodes_live_created
21654
- // serves this order without a sort.
21681
+ // followed whichever partial index the planner chose. nodes_live_created_v2
21682
+ // serves it, sorting rowids only within an equal created_at.
21655
21683
  `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
21656
21684
  );
21657
21685
  this.fieldStmts.set(cacheKey2, stmt2);
@@ -23199,10 +23227,12 @@ function crystallize(store, opts = { ts: 0 }) {
23199
23227
  const s = support(c) + Math.max(0, sourcesCount(c) - 1);
23200
23228
  const f = contradictions;
23201
23229
  const confidence = (1 + s) / (2 + s + f);
23202
- store.updateNode(c.id, {
23203
- attrs: { ...c.attrs, confidence },
23204
- lastUpdatedAt: opts.ts
23205
- });
23230
+ if (c.attrs["confidence"] !== confidence) {
23231
+ store.updateNode(c.id, {
23232
+ attrs: { ...c.attrs, confidence },
23233
+ lastUpdatedAt: opts.ts
23234
+ });
23235
+ }
23206
23236
  const sup = support(c);
23207
23237
  let tier = c.memoryTier;
23208
23238
  if (sup >= PERSISTENT_SUPPORT && confidence >= PERSISTENT_CONFIDENCE) tier = "persistent";
@@ -23270,7 +23300,10 @@ function mergeTruthKind(a, b) {
23270
23300
  function markBoth(store, a, b, patch, ts) {
23271
23301
  for (const n of [a, b]) {
23272
23302
  const fresh = store.getNode(n.id);
23273
- store.updateNode(n.id, { attrs: { ...fresh.attrs, ...patch }, lastUpdatedAt: ts });
23303
+ const next = { ...patch };
23304
+ if (fresh.attrs["revisit"] === true && fresh.attrs["revisitReason"] === next["revisitReason"]) delete next["revisitSinceTs"];
23305
+ if (Object.entries(next).every(([k, v]) => fresh.attrs[k] === v)) continue;
23306
+ store.updateNode(n.id, { attrs: { ...fresh.attrs, ...next }, lastUpdatedAt: ts });
23274
23307
  }
23275
23308
  }
23276
23309
  var LONG_TERM_SUPPORT, PERSISTENT_SUPPORT, PERSISTENT_CONFIDENCE, SCOPE_AXES;
@@ -49322,7 +49355,14 @@ var init_mcp = __esm({
49322
49355
  total += count;
49323
49356
  groups[label] = {
49324
49357
  count,
49325
- items: store.selectNodes({ ...q, orderBy: { column: col, desc: true }, limit: sliceEnd(limit) }).map((n) => ({ id: n.id, description: n.description.slice(0, 160), at: col === "last_updated_at" ? n.lastUpdatedAt : n.createdAt }))
49358
+ items: store.selectNodes({
49359
+ ...q,
49360
+ orderBy: { column: col, desc: true },
49361
+ limit: sliceEnd(limit),
49362
+ // Unnamed, the planner reads every row of a code label through
49363
+ // nodes_live to test last_updated_at; this index carries it.
49364
+ ...col === "last_updated_at" ? { indexedBy: "nodes_live_created_v2" } : {}
49365
+ }).map((n) => ({ id: n.id, description: n.description.slice(0, 160), at: col === "last_updated_at" ? n.lastUpdatedAt : n.createdAt }))
49326
49366
  };
49327
49367
  }
49328
49368
  }
@@ -85606,7 +85646,7 @@ var OntologyValidationError = class extends Error {
85606
85646
  }
85607
85647
  };
85608
85648
  function phrase(text, article) {
85609
- const capitalized = text.length === 0 ? text : text.charAt(0).toUpperCase() + text.slice(1);
85649
+ const capitalized = firstAlphaCharUpper(text);
85610
85650
  return { text, article, textCapitalized: capitalized };
85611
85651
  }
85612
85652
  var DEFAULT_ONTOLOGY2 = {
@@ -85729,7 +85769,7 @@ function expectedArticleKind(text) {
85729
85769
  return "bare";
85730
85770
  }
85731
85771
  function firstAlphaCharUpper(s) {
85732
- return s.length === 0 ? s : s.charAt(0).toUpperCase() + s.slice(1);
85772
+ return s.charAt(0).toUpperCase() + s.slice(1);
85733
85773
  }
85734
85774
  function phraseTriggersLayer1(text) {
85735
85775
  for (const pat of ALL_PATTERNS) {
@@ -87480,7 +87520,7 @@ function createWatchBreaker(deps) {
87480
87520
  }
87481
87521
 
87482
87522
  // src/engine.ts
87483
- var DAEMON_VERSION = true ? "2.0.23-dev.4734" : "2.0.0-alpha.0";
87523
+ var DAEMON_VERSION = true ? "2.0.23-dev.4779" : "2.0.0-alpha.0";
87484
87524
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
87485
87525
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
87486
87526
  var GIT_OP_MUTE_MS = 4e3;
@@ -16010,7 +16010,8 @@ var SELECT_INDEXES = [
16010
16010
  "nodes_live_fold_candidate",
16011
16011
  "nodes_live_resolved",
16012
16012
  "nodes_live_fix_candidate",
16013
- "nodes_live_anchor_hint"
16013
+ "nodes_live_anchor_hint",
16014
+ "nodes_live_created_v2"
16014
16015
  ];
16015
16016
  var EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
16016
16017
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -16331,7 +16332,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
16331
16332
  `;
16332
16333
  var TEMPORAL_INDEX_SQL = `
16333
16334
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
16334
- CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
16335
+ -- Dropped: a prefix of nodes_live_page_rank (valid_to, page_rank), which serves
16336
+ -- the live count and the "any live node?" check as a covering index too. It was
16337
+ -- re-keyed on every close and revive: ~11 KB of a symbol flip's ~80 KB.
16338
+ DROP INDEX IF EXISTS nodes_live_count;
16335
16339
  -- Problems by recency (the only label read newest-first). Over every live row,
16336
16340
  -- each write that stamps last_updated_at moved a code symbol's entry from a
16337
16341
  -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
@@ -16343,7 +16347,13 @@ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_update
16343
16347
  -- walks nodes_live and reads every live row of the label to test created_at:
16344
16348
  -- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
16345
16349
  -- is a key column so the planner prefers this over nodes_live (one more term).
16346
- CREATE INDEX IF NOT EXISTS nodes_live_created ON nodes(label, valid_to, created_at) WHERE valid_to IS NULL;
16350
+ -- last_updated_at rides along for errata.recent on code labels, which names this
16351
+ -- index: the filter runs on index entries, not on every row of the label (79 ms
16352
+ -- \u2192 3 ms for Function on a 1.1 GB store). It costs no extra writes: a close or
16353
+ -- revive already re-keys this index, and mergeNodeIfChanged leaves a symbol's
16354
+ -- last_updated_at alone when only its position moved.
16355
+ DROP INDEX IF EXISTS nodes_live_created;
16356
+ CREATE INDEX IF NOT EXISTS nodes_live_created_v2 ON nodes(label, valid_to, created_at, last_updated_at) WHERE valid_to IS NULL;
16347
16357
  -- Community members (the skills pull reads them twice per session boundary):
16348
16358
  -- ~15% of semantic nodes carry a localCommunity, and without this every live
16349
16359
  -- semantic row was read to find them (168 MB \u2192 18 MB per read).
@@ -16528,6 +16538,12 @@ function sameSqlValue(a, b) {
16528
16538
  if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
16529
16539
  return false;
16530
16540
  }
16541
+ var POSITION_ATTRS = ["startByte", "endByte", "startRow"];
16542
+ function attrsJsonWithoutPositions(attrs) {
16543
+ const rest = { ...attrs };
16544
+ for (const k of POSITION_ATTRS) delete rest[k];
16545
+ return JSON.stringify(rest);
16546
+ }
16531
16547
  var PRIOR_TAG_MARKER = '"priorTag":true';
16532
16548
  var SqliteGraphStore = class {
16533
16549
  db;
@@ -16628,7 +16644,7 @@ var SqliteGraphStore = class {
16628
16644
  lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
16629
16645
  labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
16630
16646
  nodeContent: this.db.prepare(
16631
- "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
16647
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, last_updated_at, attrs_json FROM nodes WHERE id = ?"
16632
16648
  ),
16633
16649
  edgeContent: this.db.prepare(
16634
16650
  "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
@@ -17041,11 +17057,23 @@ var SqliteGraphStore = class {
17041
17057
  * rank, community, landmark, surprise, hits, tier and the vector belong to
17042
17058
  * the passes that compute them, and a skipped write leaves them as they are
17043
17059
  * where a merge would have reset them to the producer's defaults. Returns
17044
- * whether it wrote. */
17060
+ * whether it wrote.
17061
+ *
17062
+ * A live row whose only change is its position keeps its lastUpdatedAt, so
17063
+ * that stamp means "this symbol's content changed": what errata.recent lists
17064
+ * and the fix-candidate ask reads, and what lets nodes_live_created_v2 carry
17065
+ * it without being re-keyed by every edit above a symbol. A closed row is
17066
+ * always stamped: reviveReobserved reopens what this pass stamped. */
17045
17067
  mergeNodeIfChanged(node) {
17046
17068
  const row = this.stmts.nodeContent.get(node.id);
17047
- if (row && row.valid_to === null && row.label === node.label && row.description === node.description && row.extraction_confidence === node.extractionConfidence && row.stability === node.stability && !EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0) && row.attrs_json === JSON.stringify(node.attrs ?? {})) return false;
17048
- this.mergeNode(node);
17069
+ if (!row || row.valid_to !== null || row.label !== node.label || row.description !== node.description || row.extraction_confidence !== node.extractionConfidence || row.stability !== node.stability || EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0)) {
17070
+ this.mergeNode(node);
17071
+ return true;
17072
+ }
17073
+ const attrs = node.attrs ?? {};
17074
+ if (row.attrs_json === JSON.stringify(attrs)) return false;
17075
+ const moved = attrsJsonWithoutPositions(JSON.parse(row.attrs_json)) === attrsJsonWithoutPositions(attrs);
17076
+ this.mergeNode(moved ? { ...node, lastUpdatedAt: row.last_updated_at } : node);
17049
17077
  return true;
17050
17078
  }
17051
17079
  updateNode(id, patch) {
@@ -17125,8 +17153,8 @@ var SqliteGraphStore = class {
17125
17153
  stmt2 = this.db.prepare(
17126
17154
  // Ordered, not left to the planner: readers take "first row wins" (the
17127
17155
  // symbol lexicon's name winner and doc pick), and the order otherwise
17128
- // followed whichever partial index the planner chose. nodes_live_created
17129
- // serves this order without a sort.
17156
+ // followed whichever partial index the planner chose. nodes_live_created_v2
17157
+ // serves it, sorting rowids only within an equal created_at.
17130
17158
  `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
17131
17159
  );
17132
17160
  this.fieldStmts.set(cacheKey, stmt2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.23-dev.4734",
3
+ "version": "2.0.23-dev.4779",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -26982,7 +26982,8 @@ var SELECT_INDEXES = [
26982
26982
  "nodes_live_fold_candidate",
26983
26983
  "nodes_live_resolved",
26984
26984
  "nodes_live_fix_candidate",
26985
- "nodes_live_anchor_hint"
26985
+ "nodes_live_anchor_hint",
26986
+ "nodes_live_created_v2"
26986
26987
  ];
26987
26988
  var EMBEDDING_DIM_SQL = "(CASE WHEN length(embedding_blob) >= 4 THEN length(embedding_blob) / 4 ELSE COALESCE(json_array_length(embedding_json), 0) END)";
26988
26989
  var KEY = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*$/;
@@ -27303,7 +27304,10 @@ CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
27303
27304
  `;
27304
27305
  var TEMPORAL_INDEX_SQL = `
27305
27306
  CREATE INDEX IF NOT EXISTS nodes_live ON nodes(label, valid_to);
27306
- CREATE INDEX IF NOT EXISTS nodes_live_count ON nodes(valid_to);
27307
+ -- Dropped: a prefix of nodes_live_page_rank (valid_to, page_rank), which serves
27308
+ -- the live count and the "any live node?" check as a covering index too. It was
27309
+ -- re-keyed on every close and revive: ~11 KB of a symbol flip's ~80 KB.
27310
+ DROP INDEX IF EXISTS nodes_live_count;
27307
27311
  -- Problems by recency (the only label read newest-first). Over every live row,
27308
27312
  -- each write that stamps last_updated_at moved a code symbol's entry from a
27309
27313
  -- scattered leaf: ~19 KB per position-only symbol rewrite on a 1.1 GB store.
@@ -27315,7 +27319,13 @@ CREATE INDEX IF NOT EXISTS nodes_live_problem_recent ON nodes(label, last_update
27315
27319
  -- walks nodes_live and reads every live row of the label to test created_at:
27316
27320
  -- 56 MB per guard on a 1.1 GB store, to learn there was nothing to do. valid_to
27317
27321
  -- is a key column so the planner prefers this over nodes_live (one more term).
27318
- CREATE INDEX IF NOT EXISTS nodes_live_created ON nodes(label, valid_to, created_at) WHERE valid_to IS NULL;
27322
+ -- last_updated_at rides along for errata.recent on code labels, which names this
27323
+ -- index: the filter runs on index entries, not on every row of the label (79 ms
27324
+ -- \u2192 3 ms for Function on a 1.1 GB store). It costs no extra writes: a close or
27325
+ -- revive already re-keys this index, and mergeNodeIfChanged leaves a symbol's
27326
+ -- last_updated_at alone when only its position moved.
27327
+ DROP INDEX IF EXISTS nodes_live_created;
27328
+ CREATE INDEX IF NOT EXISTS nodes_live_created_v2 ON nodes(label, valid_to, created_at, last_updated_at) WHERE valid_to IS NULL;
27319
27329
  -- Community members (the skills pull reads them twice per session boundary):
27320
27330
  -- ~15% of semantic nodes carry a localCommunity, and without this every live
27321
27331
  -- semantic row was read to find them (168 MB \u2192 18 MB per read).
@@ -27500,6 +27510,12 @@ function sameSqlValue(a, b) {
27500
27510
  if (a instanceof Uint8Array && b instanceof Uint8Array) return Buffer.compare(a, b) === 0;
27501
27511
  return false;
27502
27512
  }
27513
+ var POSITION_ATTRS = ["startByte", "endByte", "startRow"];
27514
+ function attrsJsonWithoutPositions(attrs) {
27515
+ const rest = { ...attrs };
27516
+ for (const k of POSITION_ATTRS) delete rest[k];
27517
+ return JSON.stringify(rest);
27518
+ }
27503
27519
  var PRIOR_TAG_MARKER = '"priorTag":true';
27504
27520
  var SqliteGraphStore = class {
27505
27521
  db;
@@ -27600,7 +27616,7 @@ var SqliteGraphStore = class {
27600
27616
  lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
27601
27617
  labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
27602
27618
  nodeContent: this.db.prepare(
27603
- "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
27619
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, last_updated_at, attrs_json FROM nodes WHERE id = ?"
27604
27620
  ),
27605
27621
  edgeContent: this.db.prepare(
27606
27622
  "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
@@ -28013,11 +28029,23 @@ var SqliteGraphStore = class {
28013
28029
  * rank, community, landmark, surprise, hits, tier and the vector belong to
28014
28030
  * the passes that compute them, and a skipped write leaves them as they are
28015
28031
  * where a merge would have reset them to the producer's defaults. Returns
28016
- * whether it wrote. */
28032
+ * whether it wrote.
28033
+ *
28034
+ * A live row whose only change is its position keeps its lastUpdatedAt, so
28035
+ * that stamp means "this symbol's content changed": what errata.recent lists
28036
+ * and the fix-candidate ask reads, and what lets nodes_live_created_v2 carry
28037
+ * it without being re-keyed by every edit above a symbol. A closed row is
28038
+ * always stamped: reviveReobserved reopens what this pass stamped. */
28017
28039
  mergeNodeIfChanged(node) {
28018
28040
  const row = this.stmts.nodeContent.get(node.id);
28019
- if (row && row.valid_to === null && row.label === node.label && row.description === node.description && row.extraction_confidence === node.extractionConfidence && row.stability === node.stability && !EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0) && row.attrs_json === JSON.stringify(node.attrs ?? {})) return false;
28020
- this.mergeNode(node);
28041
+ if (!row || row.valid_to !== null || row.label !== node.label || row.description !== node.description || row.extraction_confidence !== node.extractionConfidence || row.stability !== node.stability || EXPOSURE_ATTRS.some((k) => node.attrs?.[k] !== void 0)) {
28042
+ this.mergeNode(node);
28043
+ return true;
28044
+ }
28045
+ const attrs = node.attrs ?? {};
28046
+ if (row.attrs_json === JSON.stringify(attrs)) return false;
28047
+ const moved = attrsJsonWithoutPositions(JSON.parse(row.attrs_json)) === attrsJsonWithoutPositions(attrs);
28048
+ this.mergeNode(moved ? { ...node, lastUpdatedAt: row.last_updated_at } : node);
28021
28049
  return true;
28022
28050
  }
28023
28051
  updateNode(id, patch) {
@@ -28097,8 +28125,8 @@ var SqliteGraphStore = class {
28097
28125
  stmt2 = this.db.prepare(
28098
28126
  // Ordered, not left to the planner: readers take "first row wins" (the
28099
28127
  // symbol lexicon's name winner and doc pick), and the order otherwise
28100
- // followed whichever partial index the planner chose. nodes_live_created
28101
- // serves this order without a sort.
28128
+ // followed whichever partial index the planner chose. nodes_live_created_v2
28129
+ // serves it, sorting rowids only within an equal created_at.
28102
28130
  `SELECT id, description, community, extraction_confidence${cols} FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY created_at, rowid`
28103
28131
  );
28104
28132
  this.fieldStmts.set(cacheKey, stmt2);
@@ -29130,10 +29158,12 @@ function crystallize(store2, opts = { ts: 0 }) {
29130
29158
  const s = support(c) + Math.max(0, sourcesCount(c) - 1);
29131
29159
  const f = contradictions;
29132
29160
  const confidence = (1 + s) / (2 + s + f);
29133
- store2.updateNode(c.id, {
29134
- attrs: { ...c.attrs, confidence },
29135
- lastUpdatedAt: opts.ts
29136
- });
29161
+ if (c.attrs["confidence"] !== confidence) {
29162
+ store2.updateNode(c.id, {
29163
+ attrs: { ...c.attrs, confidence },
29164
+ lastUpdatedAt: opts.ts
29165
+ });
29166
+ }
29137
29167
  const sup = support(c);
29138
29168
  let tier = c.memoryTier;
29139
29169
  if (sup >= PERSISTENT_SUPPORT && confidence >= PERSISTENT_CONFIDENCE) tier = "persistent";
@@ -29201,7 +29231,10 @@ function mergeTruthKind(a, b) {
29201
29231
  function markBoth(store2, a, b, patch, ts) {
29202
29232
  for (const n of [a, b]) {
29203
29233
  const fresh = store2.getNode(n.id);
29204
- store2.updateNode(n.id, { attrs: { ...fresh.attrs, ...patch }, lastUpdatedAt: ts });
29234
+ const next = { ...patch };
29235
+ if (fresh.attrs["revisit"] === true && fresh.attrs["revisitReason"] === next["revisitReason"]) delete next["revisitSinceTs"];
29236
+ if (Object.entries(next).every(([k, v]) => fresh.attrs[k] === v)) continue;
29237
+ store2.updateNode(n.id, { attrs: { ...fresh.attrs, ...next }, lastUpdatedAt: ts });
29205
29238
  }
29206
29239
  }
29207
29240
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-context",
3
- "version": "2.0.23-dev.4734",
3
+ "version": "2.0.23-dev.4779",
4
4
  "description": "A minimal 3D view of loaded Errata priors and recent graph bursts.",
5
5
  "author": {
6
6
  "name": "inErrata"
@@ -1,4 +1,4 @@
1
- 1cf9b23c192fdd174ce8e6758c456f5406439ff585d252e1765cdd7707583657 .codex-plugin/plugin.json
1
+ 8c2ed59e34694f0fbec381b67731aae53d87a289c92222a279d038f65e3bcad0 .codex-plugin/plugin.json
2
2
  a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
3
3
  fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
4
4
  73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js