@inerrata-corporation/errata 2.0.22-dev.4665 → 2.0.22-dev.4676

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.
@@ -15402,6 +15402,48 @@ var LEADING_VERB = new RegExp(String.raw`^\s*(${VERB_WORDS_SOURCE})\s*:`, "i");
15402
15402
  var ANY_VERB_WORD = new RegExp(String.raw`\b(?:${VERB_WORDS_SOURCE})\s*:`, "gi");
15403
15403
  var QUOTED_TAG_OPENER = new RegExp(String.raw`\(\s*(?:(${VERB_WORDS_SOURCE})\s*:|(?=\[))`, "gi");
15404
15404
 
15405
+ // ../../packages/local-graph/src/wm-calibration.ts
15406
+ var WM_CALIBRATION_KEYS = [
15407
+ "shownCount",
15408
+ "evictedCount",
15409
+ "reinforceShownCount",
15410
+ "reinforceRecentCount",
15411
+ "reinforceEvictedCount",
15412
+ "reinforceUnshownCount",
15413
+ "reinforceUnshownQueryOnlyCount"
15414
+ ];
15415
+ var WM_COUNTED_PREDICATE = WM_CALIBRATION_KEYS.map((k) => `json_type(attrs_json, '$.${k}') IS NOT NULL`).join(" OR ");
15416
+ var wmCounterSql = (k) => `CASE WHEN json_type(attrs_json, '$.${k}') IN ('integer', 'real') THEN json_extract(attrs_json, '$.${k}') END`;
15417
+
15418
+ // ../../packages/local-graph/src/exposure-attrs.ts
15419
+ var EXPOSURE_ATTRS = [
15420
+ "shownCount",
15421
+ "evictedCount",
15422
+ "firstShownAt",
15423
+ "firstEvictedAt",
15424
+ "firstShownAtSeq",
15425
+ "lastShownAtSeq",
15426
+ "lastEvictedAtSeq",
15427
+ "shownSinceTouch",
15428
+ "shownSinceTouchAt",
15429
+ "exposureLog",
15430
+ "exposureLogStart",
15431
+ "exposureLogSince",
15432
+ "exposureFirst",
15433
+ "exposureTrimmed"
15434
+ ];
15435
+ var EXPOSURE_SCOPE = "exposure";
15436
+ var EXPOSURE_KEYS = new Set(EXPOSURE_ATTRS);
15437
+ var isExposureAttr = (key) => EXPOSURE_KEYS.has(key);
15438
+ function pickExposure(attrs) {
15439
+ const out = {};
15440
+ for (const k of EXPOSURE_ATTRS) if (attrs[k] !== void 0) out[k] = attrs[k];
15441
+ return out;
15442
+ }
15443
+ function exposureDocSql(key) {
15444
+ return `coalesce((SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id AND json_type(x.attrs_json, '$.${key}') IS NOT NULL), attrs_json)`;
15445
+ }
15446
+
15405
15447
  // ../../packages/local-graph/src/history-cache-floor.ts
15406
15448
  var HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
15407
15449
  function installHistoryCacheFloor(db) {
@@ -15683,25 +15725,6 @@ function textIndexReady(db) {
15683
15725
  return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
15684
15726
  }
15685
15727
 
15686
- // ../../packages/local-graph/src/exposure-attrs.ts
15687
- var EXPOSURE_ATTRS = [
15688
- "shownCount",
15689
- "evictedCount",
15690
- "firstShownAt",
15691
- "firstEvictedAt",
15692
- "firstShownAtSeq",
15693
- "lastShownAtSeq",
15694
- "lastEvictedAtSeq",
15695
- "shownSinceTouch",
15696
- "shownSinceTouchAt",
15697
- "exposureLog",
15698
- "exposureLogStart",
15699
- "exposureLogSince",
15700
- "exposureFirst",
15701
- "exposureTrimmed"
15702
- ];
15703
- var EXPOSURE_SCOPE = "exposure";
15704
-
15705
15728
  // ../../packages/local-graph/src/change-feed.ts
15706
15729
  var CHANGE_FEED_KEY = "change-feed";
15707
15730
  var ANY_CHANGE = "*";
@@ -15765,6 +15788,16 @@ function readChangedNodeIds(db, sinceSeq, labels) {
15765
15788
  const rows = db.prepare(`SELECT DISTINCT id FROM change_log WHERE change_seq > ? AND kind = 'node' AND label IN (${labels.map(() => "?").join(",")})`).all(sinceSeq, ...labels);
15766
15789
  return rows.map((r) => r.id);
15767
15790
  }
15791
+ function readChangedAmong(db, sinceSeq, ids) {
15792
+ if (!changeLogCovers(db, sinceSeq)) return null;
15793
+ const out = [];
15794
+ for (let i = 0; i < ids.length; i += 500) {
15795
+ const chunk = ids.slice(i, i + 500);
15796
+ const rows = db.prepare(`SELECT DISTINCT id FROM change_log WHERE change_seq > ? AND kind = 'node' AND id IN (${chunk.map(() => "?").join(",")})`).all(sinceSeq, ...chunk);
15797
+ for (const r of rows) out.push(r.id);
15798
+ }
15799
+ return out;
15800
+ }
15768
15801
  function readLineageChangedFiles(db, sinceSeq) {
15769
15802
  if (!changeLogCovers(db, sinceSeq)) return null;
15770
15803
  const rows = db.prepare(LINEAGE_CHANGED_FILES_SQL).all({ s: sinceSeq });
@@ -16062,8 +16095,11 @@ function compileNodeWhere(q) {
16062
16095
  }
16063
16096
  return { where: conds.join(" AND "), params };
16064
16097
  }
16065
- function attrValueSql(p) {
16066
- return `CASE json_type(attrs_json, ${p}) WHEN 'integer' THEN CAST(json_extract(attrs_json, ${p}) AS REAL) ELSE json_extract(attrs_json, ${p}) END`;
16098
+ function attrValueSql(p, doc = "attrs_json") {
16099
+ return `CASE json_type(${doc}, ${p}) WHEN 'integer' THEN CAST(json_extract(${doc}, ${p}) AS REAL) ELSE json_extract(${doc}, ${p}) END`;
16100
+ }
16101
+ function attrDocSql(key) {
16102
+ return isExposureAttr(key) ? exposureDocSql(key) : "attrs_json";
16067
16103
  }
16068
16104
  function nodeSelectFrom(q) {
16069
16105
  if (q.indexedBy) return `nodes INDEXED BY ${selectIndex(q.indexedBy)}`;
@@ -16076,7 +16112,7 @@ function nodeSelectFrom(q) {
16076
16112
  function compileNodeSelect(q) {
16077
16113
  const { where, params } = compileNodeWhere(q);
16078
16114
  const attrs = q.attrs ?? [];
16079
- const attrCols = attrs.map((k, i) => `, ${attrValueSql(path(k))} AS a${i}, json_type(attrs_json, ${path(k)}) AS t${i}`).join("");
16115
+ const attrCols = attrs.map((k, i) => `, ${attrValueSql(path(k), attrDocSql(k))} AS a${i}, json_type(${attrDocSql(k)}, ${path(k)}) AS t${i}`).join("");
16080
16116
  let order;
16081
16117
  if (q.orderBy && "column" in q.orderBy) {
16082
16118
  order = `${column(q.orderBy.column)} ${q.orderBy.desc ? "DESC" : "ASC"}, rowid`;
@@ -16193,6 +16229,7 @@ function localEdgeViolation(fromLabel, type, toLabel) {
16193
16229
  }
16194
16230
  var SCHEMA_VERSION = 6;
16195
16231
  var SESSION_REPAIR_KEY = "session-evidence-v1";
16232
+ var EXPOSURE_SIDE_KEY = "exposure-side-rows-1";
16196
16233
  var SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
16197
16234
  var SCHEMA_SQL = `
16198
16235
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -16256,8 +16293,13 @@ CREATE TABLE IF NOT EXISTS nodes (
16256
16293
  );
16257
16294
 
16258
16295
  CREATE INDEX IF NOT EXISTS nodes_label ON nodes(label);
16259
- CREATE INDEX IF NOT EXISTS nodes_memory_tier ON nodes(memory_tier);
16260
- CREATE INDEX IF NOT EXISTS nodes_page_rank ON nodes(page_rank DESC);
16296
+ -- Dropped: no query plans through them (every prepared statement across the
16297
+ -- local-graph, indexer, context-writer, generalizer and daemon suites was
16298
+ -- planned with and without them, 2026-10-21), and each write that touches
16299
+ -- memory_tier / page_rank (every node upsert, every rescore) paid to maintain
16300
+ -- them. nodes_live_page_rank serves the live page-rank orderings.
16301
+ DROP INDEX IF EXISTS nodes_memory_tier;
16302
+ DROP INDEX IF EXISTS nodes_page_rank;
16261
16303
 
16262
16304
  CREATE TABLE IF NOT EXISTS edges (
16263
16305
  id TEXT PRIMARY KEY,
@@ -16279,7 +16321,10 @@ CREATE TABLE IF NOT EXISTS edges (
16279
16321
  FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE
16280
16322
  );
16281
16323
 
16282
- CREATE INDEX IF NOT EXISTS edges_from ON edges(from_id, type);
16324
+ -- Dropped: a prefix of edges_triple (from_id, type, to_id), which every
16325
+ -- from-side lookup and foreign-key check already plans through; it cost 47 MB
16326
+ -- and an index write on every edge write.
16327
+ DROP INDEX IF EXISTS edges_from;
16283
16328
  CREATE INDEX IF NOT EXISTS edges_to ON edges(to_id, type);
16284
16329
  CREATE INDEX IF NOT EXISTS edges_type ON edges(type);
16285
16330
  CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
@@ -16301,6 +16346,12 @@ CREATE INDEX IF NOT EXISTS nodes_live_local_community ON nodes(label, valid_to,
16301
16346
  -- Problems parked for the fold judge (read every settle boundary): ~1 in 5 open
16302
16347
  -- Problems; without it every live Problem was read to find them (56 MB).
16303
16348
  CREATE INDEX IF NOT EXISTS nodes_live_fold_candidate ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_type(attrs_json, '$.foldCandidate') IS NOT NULL;
16349
+ -- Render impressions, one row per node id (exposure-attrs.ts): a render bumps
16350
+ -- ~40 of these, which in the node row rewrote each whole row and its indexes.
16351
+ CREATE TABLE IF NOT EXISTS node_exposure (
16352
+ id TEXT PRIMARY KEY,
16353
+ attrs_json TEXT NOT NULL DEFAULT '{}'
16354
+ ) WITHOUT ROWID;
16304
16355
  -- The semantic-maintenance sweeps' own filters (closed, asked, hinted): each read
16305
16356
  -- every Problem row (~30 MB on a 1.1 GB store) for the few hundred that match,
16306
16357
  -- several times a run, and a run follows most writes (2026-10-20).
@@ -16308,6 +16359,9 @@ CREATE INDEX IF NOT EXISTS nodes_live_resolved ON nodes(label, valid_to) WHERE v
16308
16359
  CREATE INDEX IF NOT EXISTS nodes_live_fix_candidate ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_type(attrs_json, '$.fixCandidateAt') IS NOT NULL;
16309
16360
  CREATE INDEX IF NOT EXISTS nodes_live_anchor_hint ON nodes(label, valid_to) WHERE valid_to IS NULL AND json_extract(attrs_json, '$.anchorHint') IS NOT NULL;
16310
16361
  CREATE INDEX IF NOT EXISTS nodes_session_evidence ON nodes(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
16362
+ -- Rows carrying a calibration counter, any version: the logical nodes wm
16363
+ -- calibration can count (~3k), found without reading every citable row.
16364
+ CREATE INDEX IF NOT EXISTS nodes_wm_counted ON nodes(logical_id, version, valid_to, label, ${WM_CALIBRATION_KEYS.map(wmCounterSql).join(", ")}) WHERE ${WM_COUNTED_PREDICATE};
16311
16365
  CREATE INDEX IF NOT EXISTS edges_session_evidence ON edges(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
16312
16366
  -- Covering + partial index for labelFingerprint over the instance labels: the
16313
16367
  -- live-push probe runs it per workspace on a timer, and each hit used to read
@@ -16461,6 +16515,7 @@ var SqliteGraphStore = class {
16461
16515
  constructor(opts) {
16462
16516
  this.db = openDatabase(opts.path);
16463
16517
  this.db.exec("PRAGMA cache_size = -32768");
16518
+ this.db.exec("PRAGMA mmap_size = 2147483648");
16464
16519
  this.db.exec(SCHEMA_SQL);
16465
16520
  const versionRow = this.db.prepare("SELECT version FROM schema_version LIMIT 1").get();
16466
16521
  if (!versionRow) {
@@ -16524,8 +16579,17 @@ var SqliteGraphStore = class {
16524
16579
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
16525
16580
  attrs_json = excluded.attrs_json
16526
16581
  `),
16527
- getNode: this.db.prepare("SELECT * FROM nodes WHERE id = ?"),
16582
+ getNode: this.db.prepare("SELECT nodes.*, (SELECT x.attrs_json FROM node_exposure x WHERE x.id = nodes.id) AS exposure_json FROM nodes WHERE id = ?"),
16583
+ exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
16584
+ writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
16585
+ lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
16528
16586
  labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
16587
+ nodeContent: this.db.prepare(
16588
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
16589
+ ),
16590
+ edgeContent: this.db.prepare(
16591
+ "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
16592
+ ),
16529
16593
  // Single-column fast paths for the nightly pass — writing 100k pageRank
16530
16594
  // values through the 18-column COALESCE updateNode took ~30s; these are ~10x
16531
16595
  // cheaper (one bound column, no COALESCE evaluation per row).
@@ -16682,6 +16746,7 @@ var SqliteGraphStore = class {
16682
16746
  "INSERT INTO store_meta (key, value) VALUES ('ingest_seq', :v) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
16683
16747
  )
16684
16748
  };
16749
+ this.moveExposureToSideRows();
16685
16750
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
16686
16751
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
16687
16752
  }
@@ -16737,6 +16802,41 @@ var SqliteGraphStore = class {
16737
16802
  ORDER BY label, created_at, rowid`
16738
16803
  ).all(label);
16739
16804
  }
16805
+ wmCalibrationRows(labels) {
16806
+ const counted = this.db.prepare(
16807
+ `SELECT rowid AS rid, logical_id, version, valid_to, label, ${WM_CALIBRATION_KEYS.map((k, i) => `${wmCounterSql(k)} AS c${i}`).join(", ")}
16808
+ FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`
16809
+ ).all();
16810
+ const exposed = this.db.prepare(
16811
+ `SELECT n.rowid AS rid, n.logical_id, n.version, n.valid_to, n.label, x.attrs_json AS exposure
16812
+ FROM node_exposure x JOIN nodes n ON n.id = x.id`
16813
+ ).all();
16814
+ const others = this.db.prepare(
16815
+ `SELECT rowid AS rid, logical_id, version, valid_to, label FROM nodes WHERE rowid IN (
16816
+ SELECT n.rowid FROM nodes AS n INDEXED BY nodes_logical
16817
+ WHERE n.logical_id IN (SELECT logical_id FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
16818
+ UNION SELECT n2.logical_id FROM node_exposure x2 JOIN nodes n2 ON n2.id = x2.id)
16819
+ EXCEPT SELECT rowid FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
16820
+ EXCEPT SELECT n3.rowid FROM node_exposure x3 JOIN nodes n3 ON n3.id = x3.id)`
16821
+ ).all();
16822
+ const byRid = /* @__PURE__ */ new Map();
16823
+ for (const r of [...counted, ...others]) {
16824
+ const attrs = {};
16825
+ WM_CALIBRATION_KEYS.forEach((k, i) => {
16826
+ const v = r[`c${i}`];
16827
+ if (typeof v === "number") attrs[k] = v;
16828
+ });
16829
+ byRid.set(r.rid, { row: r, attrs });
16830
+ }
16831
+ for (const r of exposed) {
16832
+ const side = JSON.parse(r.exposure);
16833
+ const entry = byRid.get(r.rid) ?? { row: r, attrs: {} };
16834
+ for (const k of WM_CALIBRATION_KEYS) if (typeof side[k] === "number" && Number.isFinite(side[k])) entry.attrs[k] = side[k];
16835
+ byRid.set(r.rid, entry);
16836
+ }
16837
+ const order = new Map(labels.map((l, i) => [l, i]));
16838
+ return [...byRid.values()].filter(({ row }) => order.has(row.label)).sort((a, b) => order.get(a.row.label) - order.get(b.row.label) || a.row.rid - b.row.rid).map(({ row, attrs }) => ({ logicalId: row.logical_id, version: Number(row.version), validTo: row.valid_to ?? null, attrs }));
16839
+ }
16740
16840
  liveCommentsAttachedTo(name) {
16741
16841
  return this.db.prepare(
16742
16842
  `SELECT json_extract(attrs_json, '$.kind') AS kind, json_extract(attrs_json, '$.text') AS text, description FROM nodes INDEXED BY nodes_live_attached_to
@@ -16873,9 +16973,22 @@ var SqliteGraphStore = class {
16873
16973
  logical_id: node.logicalId ?? node.id,
16874
16974
  created_at_seq: node.createdAtSeq ?? null,
16875
16975
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
16876
- attrs_json: JSON.stringify(node.attrs ?? {})
16976
+ attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
16877
16977
  });
16878
16978
  }
16979
+ /** mergeNode, unless the live row already holds this content. Content is what
16980
+ * the columns a merge rewrites from the producer (label, description, attrs,
16981
+ * confidence, stability);
16982
+ * rank, community, landmark, surprise, hits, tier and the vector belong to
16983
+ * the passes that compute them, and a skipped write leaves them as they are
16984
+ * where a merge would have reset them to the producer's defaults. Returns
16985
+ * whether it wrote. */
16986
+ mergeNodeIfChanged(node) {
16987
+ const row = this.stmts.nodeContent.get(node.id);
16988
+ 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;
16989
+ this.mergeNode(node);
16990
+ return true;
16991
+ }
16879
16992
  updateNode(id, patch) {
16880
16993
  const hasEmbedding = patch.embedding !== void 0 && patch.embedding.length > 0;
16881
16994
  this.stmts.updateNode.run({
@@ -16899,7 +17012,37 @@ var SqliteGraphStore = class {
16899
17012
  logical_id: patch.logicalId ?? null,
16900
17013
  created_at_seq: patch.createdAtSeq ?? null,
16901
17014
  last_reinforced_at_seq: patch.lastReinforcedAtSeq ?? null,
16902
- attrs_json: patch.attrs ? JSON.stringify(patch.attrs) : null
17015
+ attrs_json: patch.attrs ? this.nodeAttrsJson(id, patch.attrs) : null
17016
+ });
17017
+ }
17018
+ /**
17019
+ * The node-row attrs of a write, after handing any exposure keys to the side
17020
+ * row (exposure-attrs.ts). `getNode` returns exposure merged into attrs, so a
17021
+ * read-modify-write carries it; kept in the row it would re-bloat the row and
17022
+ * drift from the side row. Unchanged exposure costs no write.
17023
+ */
17024
+ nodeAttrsJson(id, attrs) {
17025
+ if (!EXPOSURE_ATTRS.some((k) => attrs[k] !== void 0)) return JSON.stringify(attrs);
17026
+ const current = this.stmts.exposureOf.get(id);
17027
+ const next = JSON.stringify({ ...current ? JSON.parse(current.attrs_json) : {}, ...pickExposure(attrs) });
17028
+ if (current?.attrs_json !== next) this.stmts.writeExposure.run(id, next);
17029
+ const rest = { ...attrs };
17030
+ for (const k of EXPOSURE_ATTRS) delete rest[k];
17031
+ return JSON.stringify(rest);
17032
+ }
17033
+ /** One-time: exposure keys written to node rows before the side table existed
17034
+ * move to it. Only rows carrying a counter can carry exposure, and those are
17035
+ * what nodes_wm_counted holds, so this reads the index, not the table. */
17036
+ moveExposureToSideRows() {
17037
+ if (this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(EXPOSURE_SIDE_KEY)) return;
17038
+ const rows = this.db.prepare(`SELECT id, attrs_json FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`).all();
17039
+ const move = this.db.prepare("UPDATE nodes SET attrs_json = ? WHERE id = ?");
17040
+ this.db.transaction(() => {
17041
+ for (const r of rows) {
17042
+ const stripped = this.nodeAttrsJson(r.id, JSON.parse(r.attrs_json));
17043
+ if (stripped !== r.attrs_json) move.run(stripped, r.id);
17044
+ }
17045
+ this.db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, 'done')").run(EXPOSURE_SIDE_KEY);
16903
17046
  });
16904
17047
  }
16905
17048
  /** Fast single-column writes for the nightly pass (avoid the 18-column COALESCE
@@ -16981,6 +17124,21 @@ var SqliteGraphStore = class {
16981
17124
  attrs_json: JSON.stringify(edge.attrs ?? {})
16982
17125
  });
16983
17126
  }
17127
+ /** mergeEdge, unless the live edge already holds this content: same attrs, and
17128
+ * a confidence the upsert's MAX would keep. Its navigation posteriors and
17129
+ * last-seen stamp stay as they are where a merge would have reset them. The
17130
+ * ontology gate runs first either way, so refusals are recorded as before.
17131
+ * Returns whether it wrote (or refused). */
17132
+ mergeEdgeIfChanged(edge) {
17133
+ if (!this.edgeRuleViolation(edge)) {
17134
+ const row = this.stmts.edgeContent.get(edge.from, edge.type, edge.to);
17135
+ if (row && row.valid_to === null && row.confidence >= edge.confidence && row.attrs_json === JSON.stringify(edge.attrs ?? {})) {
17136
+ return false;
17137
+ }
17138
+ }
17139
+ this.mergeEdge(edge);
17140
+ return true;
17141
+ }
16984
17142
  /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
16985
17143
  * overlay consulted first. Returns the reason string on a documented-
16986
17144
  * forbidden combination, else null. Point lookups on the id PK — negligible
@@ -17047,7 +17205,27 @@ var SqliteGraphStore = class {
17047
17205
  }
17048
17206
  getNode(id) {
17049
17207
  const row = this.stmts.getNode.get(id);
17050
- return row ? rowToNode(row) : null;
17208
+ if (!row) return null;
17209
+ const node = rowToNode(row);
17210
+ if (row.exposure_json) Object.assign(node.attrs, JSON.parse(row.exposure_json));
17211
+ return node;
17212
+ }
17213
+ /** A node's render exposure: the side row's keys over the node row's own
17214
+ * (exposure-attrs.ts), plus the node's `lastUpdatedAt` (the "shown since
17215
+ * touch" counter is relative to it) — without reading the node's attrs. */
17216
+ exposureOf(id) {
17217
+ const row = this.stmts.lastUpdatedOf.get(id);
17218
+ if (!row) return null;
17219
+ const side = this.stmts.exposureOf.get(id);
17220
+ return {
17221
+ attrs: { ...pickExposure(JSON.parse(row.attrs_json)), ...side ? JSON.parse(side.attrs_json) : {} },
17222
+ lastUpdatedAt: row.t
17223
+ };
17224
+ }
17225
+ /** Replace a node's exposure (only EXPOSURE_ATTRS keys belong here). Touches
17226
+ * neither the node row nor its indexes, nor the change feed. */
17227
+ writeExposure(id, attrs) {
17228
+ this.stmts.writeExposure.run(id, JSON.stringify(attrs));
17051
17229
  }
17052
17230
  nodeLabels(ids) {
17053
17231
  const width = NODE_LABELS_CHUNK;
@@ -17098,7 +17276,7 @@ var SqliteGraphStore = class {
17098
17276
  findNodeStampsByLabel(label, attrKey) {
17099
17277
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
17100
17278
  const rows = this.db.prepare(
17101
- `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has,
17279
+ `SELECT id, created_at, created_at_seq, json_type(${attrDocSql(attrKey)}, '$.${attrKey}') IS NOT NULL AS has,
17102
17280
  COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
17103
17281
  FROM nodes WHERE label = ? AND valid_to IS NULL`
17104
17282
  ).all(label);
@@ -17115,7 +17293,7 @@ var SqliteGraphStore = class {
17115
17293
  const keys = [...new Set(attrKeys)];
17116
17294
  const rows = this.db.prepare(
17117
17295
  `SELECT id, created_at, created_at_seq,
17118
- ${keys.map((k, i) => `json_type(attrs_json, '$.${k}') IS NOT NULL AS h${i}`).join(", ")}${keys.length ? "," : ""}
17296
+ ${keys.map((k, i) => `json_type(${attrDocSql(k)}, '$.${k}') IS NOT NULL AS h${i}`).join(", ")}${keys.length ? "," : ""}
17119
17297
  COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
17120
17298
  FROM nodes WHERE label = ? AND valid_to IS NULL`
17121
17299
  ).all(label);
@@ -17375,6 +17553,9 @@ var SqliteGraphStore = class {
17375
17553
  changedNodeIds(sinceSeq, labels) {
17376
17554
  return readChangedNodeIds(this.db, sinceSeq, labels);
17377
17555
  }
17556
+ changedAmong(sinceSeq, ids) {
17557
+ return readChangedAmong(this.db, sinceSeq, ids);
17558
+ }
17378
17559
  pruneChangeLog() {
17379
17560
  return pruneChangeLog(this.db);
17380
17561
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.22-dev.4665",
3
+ "version": "2.0.22-dev.4676",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {