@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.
@@ -16825,6 +16825,48 @@ import { parentPort } from "node:worker_threads";
16825
16825
  init_src2();
16826
16826
  init_src();
16827
16827
 
16828
+ // ../../packages/local-graph/src/wm-calibration.ts
16829
+ var WM_CALIBRATION_KEYS = [
16830
+ "shownCount",
16831
+ "evictedCount",
16832
+ "reinforceShownCount",
16833
+ "reinforceRecentCount",
16834
+ "reinforceEvictedCount",
16835
+ "reinforceUnshownCount",
16836
+ "reinforceUnshownQueryOnlyCount"
16837
+ ];
16838
+ var WM_COUNTED_PREDICATE = WM_CALIBRATION_KEYS.map((k) => `json_type(attrs_json, '$.${k}') IS NOT NULL`).join(" OR ");
16839
+ var wmCounterSql = (k) => `CASE WHEN json_type(attrs_json, '$.${k}') IN ('integer', 'real') THEN json_extract(attrs_json, '$.${k}') END`;
16840
+
16841
+ // ../../packages/local-graph/src/exposure-attrs.ts
16842
+ var EXPOSURE_ATTRS = [
16843
+ "shownCount",
16844
+ "evictedCount",
16845
+ "firstShownAt",
16846
+ "firstEvictedAt",
16847
+ "firstShownAtSeq",
16848
+ "lastShownAtSeq",
16849
+ "lastEvictedAtSeq",
16850
+ "shownSinceTouch",
16851
+ "shownSinceTouchAt",
16852
+ "exposureLog",
16853
+ "exposureLogStart",
16854
+ "exposureLogSince",
16855
+ "exposureFirst",
16856
+ "exposureTrimmed"
16857
+ ];
16858
+ var EXPOSURE_SCOPE = "exposure";
16859
+ var EXPOSURE_KEYS = new Set(EXPOSURE_ATTRS);
16860
+ var isExposureAttr = (key) => EXPOSURE_KEYS.has(key);
16861
+ function pickExposure(attrs) {
16862
+ const out = {};
16863
+ for (const k of EXPOSURE_ATTRS) if (attrs[k] !== void 0) out[k] = attrs[k];
16864
+ return out;
16865
+ }
16866
+ function exposureDocSql(key) {
16867
+ 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)`;
16868
+ }
16869
+
16828
16870
  // ../../packages/local-graph/src/history-cache-floor.ts
16829
16871
  var HISTORY_CACHE_FLOOR_KEY = "history-cache-withdrawal-floor-3";
16830
16872
  function installHistoryCacheFloor(db) {
@@ -17109,25 +17151,6 @@ function textIndexReady(db) {
17109
17151
  return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
17110
17152
  }
17111
17153
 
17112
- // ../../packages/local-graph/src/exposure-attrs.ts
17113
- var EXPOSURE_ATTRS = [
17114
- "shownCount",
17115
- "evictedCount",
17116
- "firstShownAt",
17117
- "firstEvictedAt",
17118
- "firstShownAtSeq",
17119
- "lastShownAtSeq",
17120
- "lastEvictedAtSeq",
17121
- "shownSinceTouch",
17122
- "shownSinceTouchAt",
17123
- "exposureLog",
17124
- "exposureLogStart",
17125
- "exposureLogSince",
17126
- "exposureFirst",
17127
- "exposureTrimmed"
17128
- ];
17129
- var EXPOSURE_SCOPE = "exposure";
17130
-
17131
17154
  // ../../packages/local-graph/src/change-feed.ts
17132
17155
  var CHANGE_FEED_KEY = "change-feed";
17133
17156
  var ANY_CHANGE = "*";
@@ -17191,6 +17214,16 @@ function readChangedNodeIds(db, sinceSeq, labels) {
17191
17214
  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);
17192
17215
  return rows.map((r) => r.id);
17193
17216
  }
17217
+ function readChangedAmong(db, sinceSeq, ids) {
17218
+ if (!changeLogCovers(db, sinceSeq)) return null;
17219
+ const out = [];
17220
+ for (let i = 0; i < ids.length; i += 500) {
17221
+ const chunk = ids.slice(i, i + 500);
17222
+ 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);
17223
+ for (const r of rows) out.push(r.id);
17224
+ }
17225
+ return out;
17226
+ }
17194
17227
  function readLineageChangedFiles(db, sinceSeq) {
17195
17228
  if (!changeLogCovers(db, sinceSeq)) return null;
17196
17229
  const rows = db.prepare(LINEAGE_CHANGED_FILES_SQL).all({ s: sinceSeq });
@@ -17665,8 +17698,11 @@ function compileNodeWhere(q) {
17665
17698
  }
17666
17699
  return { where: conds.join(" AND "), params };
17667
17700
  }
17668
- function attrValueSql(p) {
17669
- 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`;
17701
+ function attrValueSql(p, doc = "attrs_json") {
17702
+ return `CASE json_type(${doc}, ${p}) WHEN 'integer' THEN CAST(json_extract(${doc}, ${p}) AS REAL) ELSE json_extract(${doc}, ${p}) END`;
17703
+ }
17704
+ function attrDocSql(key) {
17705
+ return isExposureAttr(key) ? exposureDocSql(key) : "attrs_json";
17670
17706
  }
17671
17707
  function nodeSelectFrom(q) {
17672
17708
  if (q.indexedBy) return `nodes INDEXED BY ${selectIndex(q.indexedBy)}`;
@@ -17679,7 +17715,7 @@ function nodeSelectFrom(q) {
17679
17715
  function compileNodeSelect(q) {
17680
17716
  const { where, params } = compileNodeWhere(q);
17681
17717
  const attrs = q.attrs ?? [];
17682
- const attrCols = attrs.map((k, i) => `, ${attrValueSql(path(k))} AS a${i}, json_type(attrs_json, ${path(k)}) AS t${i}`).join("");
17718
+ const attrCols = attrs.map((k, i) => `, ${attrValueSql(path(k), attrDocSql(k))} AS a${i}, json_type(${attrDocSql(k)}, ${path(k)}) AS t${i}`).join("");
17683
17719
  let order;
17684
17720
  if (q.orderBy && "column" in q.orderBy) {
17685
17721
  order = `${column(q.orderBy.column)} ${q.orderBy.desc ? "DESC" : "ASC"}, rowid`;
@@ -17796,6 +17832,7 @@ function localEdgeViolation(fromLabel, type, toLabel) {
17796
17832
  }
17797
17833
  var SCHEMA_VERSION = 6;
17798
17834
  var SESSION_REPAIR_KEY = "session-evidence-v1";
17835
+ var EXPOSURE_SIDE_KEY = "exposure-side-rows-1";
17799
17836
  var SESSION_EVIDENCE_PREDICATE = SESSION_EVIDENCE_FIELDS.map((field) => `instr(attrs_json, '${JSON.stringify(field)}') > 0`).join(" OR ");
17800
17837
  var SCHEMA_SQL = `
17801
17838
  CREATE TABLE IF NOT EXISTS schema_version (
@@ -17859,8 +17896,13 @@ CREATE TABLE IF NOT EXISTS nodes (
17859
17896
  );
17860
17897
 
17861
17898
  CREATE INDEX IF NOT EXISTS nodes_label ON nodes(label);
17862
- CREATE INDEX IF NOT EXISTS nodes_memory_tier ON nodes(memory_tier);
17863
- CREATE INDEX IF NOT EXISTS nodes_page_rank ON nodes(page_rank DESC);
17899
+ -- Dropped: no query plans through them (every prepared statement across the
17900
+ -- local-graph, indexer, context-writer, generalizer and daemon suites was
17901
+ -- planned with and without them, 2026-10-21), and each write that touches
17902
+ -- memory_tier / page_rank (every node upsert, every rescore) paid to maintain
17903
+ -- them. nodes_live_page_rank serves the live page-rank orderings.
17904
+ DROP INDEX IF EXISTS nodes_memory_tier;
17905
+ DROP INDEX IF EXISTS nodes_page_rank;
17864
17906
 
17865
17907
  CREATE TABLE IF NOT EXISTS edges (
17866
17908
  id TEXT PRIMARY KEY,
@@ -17882,7 +17924,10 @@ CREATE TABLE IF NOT EXISTS edges (
17882
17924
  FOREIGN KEY (to_id) REFERENCES nodes(id) ON DELETE CASCADE
17883
17925
  );
17884
17926
 
17885
- CREATE INDEX IF NOT EXISTS edges_from ON edges(from_id, type);
17927
+ -- Dropped: a prefix of edges_triple (from_id, type, to_id), which every
17928
+ -- from-side lookup and foreign-key check already plans through; it cost 47 MB
17929
+ -- and an index write on every edge write.
17930
+ DROP INDEX IF EXISTS edges_from;
17886
17931
  CREATE INDEX IF NOT EXISTS edges_to ON edges(to_id, type);
17887
17932
  CREATE INDEX IF NOT EXISTS edges_type ON edges(type);
17888
17933
  CREATE UNIQUE INDEX IF NOT EXISTS edges_triple ON edges(from_id, type, to_id);
@@ -17904,6 +17949,12 @@ CREATE INDEX IF NOT EXISTS nodes_live_local_community ON nodes(label, valid_to,
17904
17949
  -- Problems parked for the fold judge (read every settle boundary): ~1 in 5 open
17905
17950
  -- Problems; without it every live Problem was read to find them (56 MB).
17906
17951
  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;
17952
+ -- Render impressions, one row per node id (exposure-attrs.ts): a render bumps
17953
+ -- ~40 of these, which in the node row rewrote each whole row and its indexes.
17954
+ CREATE TABLE IF NOT EXISTS node_exposure (
17955
+ id TEXT PRIMARY KEY,
17956
+ attrs_json TEXT NOT NULL DEFAULT '{}'
17957
+ ) WITHOUT ROWID;
17907
17958
  -- The semantic-maintenance sweeps' own filters (closed, asked, hinted): each read
17908
17959
  -- every Problem row (~30 MB on a 1.1 GB store) for the few hundred that match,
17909
17960
  -- several times a run, and a run follows most writes (2026-10-20).
@@ -17911,6 +17962,9 @@ CREATE INDEX IF NOT EXISTS nodes_live_resolved ON nodes(label, valid_to) WHERE v
17911
17962
  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;
17912
17963
  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;
17913
17964
  CREATE INDEX IF NOT EXISTS nodes_session_evidence ON nodes(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
17965
+ -- Rows carrying a calibration counter, any version: the logical nodes wm
17966
+ -- calibration can count (~3k), found without reading every citable row.
17967
+ 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};
17914
17968
  CREATE INDEX IF NOT EXISTS edges_session_evidence ON edges(id) WHERE ${SESSION_EVIDENCE_PREDICATE};
17915
17969
  -- Covering + partial index for labelFingerprint over the instance labels: the
17916
17970
  -- live-push probe runs it per workspace on a timer, and each hit used to read
@@ -18064,6 +18118,7 @@ var SqliteGraphStore = class {
18064
18118
  constructor(opts) {
18065
18119
  this.db = openDatabase(opts.path);
18066
18120
  this.db.exec("PRAGMA cache_size = -32768");
18121
+ this.db.exec("PRAGMA mmap_size = 2147483648");
18067
18122
  this.db.exec(SCHEMA_SQL);
18068
18123
  const versionRow = this.db.prepare("SELECT version FROM schema_version LIMIT 1").get();
18069
18124
  if (!versionRow) {
@@ -18127,8 +18182,17 @@ var SqliteGraphStore = class {
18127
18182
  last_reinforced_at_seq = excluded.last_reinforced_at_seq,
18128
18183
  attrs_json = excluded.attrs_json
18129
18184
  `),
18130
- getNode: this.db.prepare("SELECT * FROM nodes WHERE id = ?"),
18185
+ 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 = ?"),
18186
+ exposureOf: this.db.prepare("SELECT attrs_json FROM node_exposure WHERE id = ?"),
18187
+ writeExposure: this.db.prepare("INSERT INTO node_exposure(id, attrs_json) VALUES (?, ?) ON CONFLICT(id) DO UPDATE SET attrs_json = excluded.attrs_json"),
18188
+ lastUpdatedOf: this.db.prepare("SELECT last_updated_at AS t, attrs_json FROM nodes WHERE id = ?"),
18131
18189
  labelOf: this.db.prepare("SELECT label FROM nodes WHERE id = ?"),
18190
+ nodeContent: this.db.prepare(
18191
+ "SELECT label, description, extraction_confidence, extraction_source, stability, valid_to, attrs_json FROM nodes WHERE id = ?"
18192
+ ),
18193
+ edgeContent: this.db.prepare(
18194
+ "SELECT confidence, valid_to, attrs_json FROM edges WHERE from_id = ? AND type = ? AND to_id = ?"
18195
+ ),
18132
18196
  // Single-column fast paths for the nightly pass — writing 100k pageRank
18133
18197
  // values through the 18-column COALESCE updateNode took ~30s; these are ~10x
18134
18198
  // cheaper (one bound column, no COALESCE evaluation per row).
@@ -18285,6 +18349,7 @@ var SqliteGraphStore = class {
18285
18349
  "INSERT INTO store_meta (key, value) VALUES ('ingest_seq', :v) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
18286
18350
  )
18287
18351
  };
18352
+ this.moveExposureToSideRows();
18288
18353
  const seqRow = this.db.prepare("SELECT value FROM store_meta WHERE key = 'ingest_seq'").get();
18289
18354
  this.ingestSeq = seqRow ? Number(seqRow.value) : 0;
18290
18355
  }
@@ -18340,6 +18405,41 @@ var SqliteGraphStore = class {
18340
18405
  ORDER BY label, created_at, rowid`
18341
18406
  ).all(label);
18342
18407
  }
18408
+ wmCalibrationRows(labels) {
18409
+ const counted = this.db.prepare(
18410
+ `SELECT rowid AS rid, logical_id, version, valid_to, label, ${WM_CALIBRATION_KEYS.map((k, i) => `${wmCounterSql(k)} AS c${i}`).join(", ")}
18411
+ FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`
18412
+ ).all();
18413
+ const exposed = this.db.prepare(
18414
+ `SELECT n.rowid AS rid, n.logical_id, n.version, n.valid_to, n.label, x.attrs_json AS exposure
18415
+ FROM node_exposure x JOIN nodes n ON n.id = x.id`
18416
+ ).all();
18417
+ const others = this.db.prepare(
18418
+ `SELECT rowid AS rid, logical_id, version, valid_to, label FROM nodes WHERE rowid IN (
18419
+ SELECT n.rowid FROM nodes AS n INDEXED BY nodes_logical
18420
+ WHERE n.logical_id IN (SELECT logical_id FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
18421
+ UNION SELECT n2.logical_id FROM node_exposure x2 JOIN nodes n2 ON n2.id = x2.id)
18422
+ EXCEPT SELECT rowid FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}
18423
+ EXCEPT SELECT n3.rowid FROM node_exposure x3 JOIN nodes n3 ON n3.id = x3.id)`
18424
+ ).all();
18425
+ const byRid = /* @__PURE__ */ new Map();
18426
+ for (const r of [...counted, ...others]) {
18427
+ const attrs = {};
18428
+ WM_CALIBRATION_KEYS.forEach((k, i) => {
18429
+ const v = r[`c${i}`];
18430
+ if (typeof v === "number") attrs[k] = v;
18431
+ });
18432
+ byRid.set(r.rid, { row: r, attrs });
18433
+ }
18434
+ for (const r of exposed) {
18435
+ const side = JSON.parse(r.exposure);
18436
+ const entry = byRid.get(r.rid) ?? { row: r, attrs: {} };
18437
+ for (const k of WM_CALIBRATION_KEYS) if (typeof side[k] === "number" && Number.isFinite(side[k])) entry.attrs[k] = side[k];
18438
+ byRid.set(r.rid, entry);
18439
+ }
18440
+ const order = new Map(labels.map((l, i) => [l, i]));
18441
+ 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 }));
18442
+ }
18343
18443
  liveCommentsAttachedTo(name) {
18344
18444
  return this.db.prepare(
18345
18445
  `SELECT json_extract(attrs_json, '$.kind') AS kind, json_extract(attrs_json, '$.text') AS text, description FROM nodes INDEXED BY nodes_live_attached_to
@@ -18476,9 +18576,22 @@ var SqliteGraphStore = class {
18476
18576
  logical_id: node.logicalId ?? node.id,
18477
18577
  created_at_seq: node.createdAtSeq ?? null,
18478
18578
  last_reinforced_at_seq: node.lastReinforcedAtSeq ?? null,
18479
- attrs_json: JSON.stringify(node.attrs ?? {})
18579
+ attrs_json: this.nodeAttrsJson(node.id, node.attrs ?? {})
18480
18580
  });
18481
18581
  }
18582
+ /** mergeNode, unless the live row already holds this content. Content is what
18583
+ * the columns a merge rewrites from the producer (label, description, attrs,
18584
+ * confidence, stability);
18585
+ * rank, community, landmark, surprise, hits, tier and the vector belong to
18586
+ * the passes that compute them, and a skipped write leaves them as they are
18587
+ * where a merge would have reset them to the producer's defaults. Returns
18588
+ * whether it wrote. */
18589
+ mergeNodeIfChanged(node) {
18590
+ const row = this.stmts.nodeContent.get(node.id);
18591
+ 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;
18592
+ this.mergeNode(node);
18593
+ return true;
18594
+ }
18482
18595
  updateNode(id, patch) {
18483
18596
  const hasEmbedding = patch.embedding !== void 0 && patch.embedding.length > 0;
18484
18597
  this.stmts.updateNode.run({
@@ -18502,7 +18615,37 @@ var SqliteGraphStore = class {
18502
18615
  logical_id: patch.logicalId ?? null,
18503
18616
  created_at_seq: patch.createdAtSeq ?? null,
18504
18617
  last_reinforced_at_seq: patch.lastReinforcedAtSeq ?? null,
18505
- attrs_json: patch.attrs ? JSON.stringify(patch.attrs) : null
18618
+ attrs_json: patch.attrs ? this.nodeAttrsJson(id, patch.attrs) : null
18619
+ });
18620
+ }
18621
+ /**
18622
+ * The node-row attrs of a write, after handing any exposure keys to the side
18623
+ * row (exposure-attrs.ts). `getNode` returns exposure merged into attrs, so a
18624
+ * read-modify-write carries it; kept in the row it would re-bloat the row and
18625
+ * drift from the side row. Unchanged exposure costs no write.
18626
+ */
18627
+ nodeAttrsJson(id, attrs) {
18628
+ if (!EXPOSURE_ATTRS.some((k) => attrs[k] !== void 0)) return JSON.stringify(attrs);
18629
+ const current = this.stmts.exposureOf.get(id);
18630
+ const next = JSON.stringify({ ...current ? JSON.parse(current.attrs_json) : {}, ...pickExposure(attrs) });
18631
+ if (current?.attrs_json !== next) this.stmts.writeExposure.run(id, next);
18632
+ const rest = { ...attrs };
18633
+ for (const k of EXPOSURE_ATTRS) delete rest[k];
18634
+ return JSON.stringify(rest);
18635
+ }
18636
+ /** One-time: exposure keys written to node rows before the side table existed
18637
+ * move to it. Only rows carrying a counter can carry exposure, and those are
18638
+ * what nodes_wm_counted holds, so this reads the index, not the table. */
18639
+ moveExposureToSideRows() {
18640
+ if (this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(EXPOSURE_SIDE_KEY)) return;
18641
+ const rows = this.db.prepare(`SELECT id, attrs_json FROM nodes INDEXED BY nodes_wm_counted WHERE ${WM_COUNTED_PREDICATE}`).all();
18642
+ const move = this.db.prepare("UPDATE nodes SET attrs_json = ? WHERE id = ?");
18643
+ this.db.transaction(() => {
18644
+ for (const r of rows) {
18645
+ const stripped = this.nodeAttrsJson(r.id, JSON.parse(r.attrs_json));
18646
+ if (stripped !== r.attrs_json) move.run(stripped, r.id);
18647
+ }
18648
+ this.db.prepare("INSERT OR REPLACE INTO store_meta(key, value) VALUES (?, 'done')").run(EXPOSURE_SIDE_KEY);
18506
18649
  });
18507
18650
  }
18508
18651
  /** Fast single-column writes for the nightly pass (avoid the 18-column COALESCE
@@ -18584,6 +18727,21 @@ var SqliteGraphStore = class {
18584
18727
  attrs_json: JSON.stringify(edge.attrs ?? {})
18585
18728
  });
18586
18729
  }
18730
+ /** mergeEdge, unless the live edge already holds this content: same attrs, and
18731
+ * a confidence the upsert's MAX would keep. Its navigation posteriors and
18732
+ * last-seen stamp stay as they are where a merge would have reset them. The
18733
+ * ontology gate runs first either way, so refusals are recorded as before.
18734
+ * Returns whether it wrote (or refused). */
18735
+ mergeEdgeIfChanged(edge) {
18736
+ if (!this.edgeRuleViolation(edge)) {
18737
+ const row = this.stmts.edgeContent.get(edge.from, edge.type, edge.to);
18738
+ if (row && row.valid_to === null && row.confidence >= edge.confidence && row.attrs_json === JSON.stringify(edge.attrs ?? {})) {
18739
+ return false;
18740
+ }
18741
+ }
18742
+ this.mergeEdge(edge);
18743
+ return true;
18744
+ }
18587
18745
  /** EDGE_RULES check against the stored endpoints' labels, with the LOCAL
18588
18746
  * overlay consulted first. Returns the reason string on a documented-
18589
18747
  * forbidden combination, else null. Point lookups on the id PK — negligible
@@ -18650,7 +18808,27 @@ var SqliteGraphStore = class {
18650
18808
  }
18651
18809
  getNode(id) {
18652
18810
  const row = this.stmts.getNode.get(id);
18653
- return row ? rowToNode(row) : null;
18811
+ if (!row) return null;
18812
+ const node = rowToNode(row);
18813
+ if (row.exposure_json) Object.assign(node.attrs, JSON.parse(row.exposure_json));
18814
+ return node;
18815
+ }
18816
+ /** A node's render exposure: the side row's keys over the node row's own
18817
+ * (exposure-attrs.ts), plus the node's `lastUpdatedAt` (the "shown since
18818
+ * touch" counter is relative to it) — without reading the node's attrs. */
18819
+ exposureOf(id) {
18820
+ const row = this.stmts.lastUpdatedOf.get(id);
18821
+ if (!row) return null;
18822
+ const side = this.stmts.exposureOf.get(id);
18823
+ return {
18824
+ attrs: { ...pickExposure(JSON.parse(row.attrs_json)), ...side ? JSON.parse(side.attrs_json) : {} },
18825
+ lastUpdatedAt: row.t
18826
+ };
18827
+ }
18828
+ /** Replace a node's exposure (only EXPOSURE_ATTRS keys belong here). Touches
18829
+ * neither the node row nor its indexes, nor the change feed. */
18830
+ writeExposure(id, attrs) {
18831
+ this.stmts.writeExposure.run(id, JSON.stringify(attrs));
18654
18832
  }
18655
18833
  nodeLabels(ids) {
18656
18834
  const width = NODE_LABELS_CHUNK;
@@ -18701,7 +18879,7 @@ var SqliteGraphStore = class {
18701
18879
  findNodeStampsByLabel(label, attrKey) {
18702
18880
  if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attrKey)) throw new Error(`findNodeStampsByLabel: bad attr key ${attrKey}`);
18703
18881
  const rows = this.db.prepare(
18704
- `SELECT id, created_at, created_at_seq, json_type(attrs_json, '$.${attrKey}') IS NOT NULL AS has,
18882
+ `SELECT id, created_at, created_at_seq, json_type(${attrDocSql(attrKey)}, '$.${attrKey}') IS NOT NULL AS has,
18705
18883
  COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
18706
18884
  FROM nodes WHERE label = ? AND valid_to IS NULL`
18707
18885
  ).all(label);
@@ -18718,7 +18896,7 @@ var SqliteGraphStore = class {
18718
18896
  const keys = [...new Set(attrKeys)];
18719
18897
  const rows = this.db.prepare(
18720
18898
  `SELECT id, created_at, created_at_seq,
18721
- ${keys.map((k, i) => `json_type(attrs_json, '$.${k}') IS NOT NULL AS h${i}`).join(", ")}${keys.length ? "," : ""}
18899
+ ${keys.map((k, i) => `json_type(${attrDocSql(k)}, '$.${k}') IS NOT NULL AS h${i}`).join(", ")}${keys.length ? "," : ""}
18722
18900
  COALESCE(json_extract(attrs_json, '$.source') = 'cloud', 0) AS cloud
18723
18901
  FROM nodes WHERE label = ? AND valid_to IS NULL`
18724
18902
  ).all(label);
@@ -18978,6 +19156,9 @@ var SqliteGraphStore = class {
18978
19156
  changedNodeIds(sinceSeq, labels) {
18979
19157
  return readChangedNodeIds(this.db, sinceSeq, labels);
18980
19158
  }
19159
+ changedAmong(sinceSeq, ids) {
19160
+ return readChangedAmong(this.db, sinceSeq, ids);
19161
+ }
18981
19162
  pruneChangeLog() {
18982
19163
  return pruneChangeLog(this.db);
18983
19164
  }