@inerrata-corporation/errata 2.0.6-dev.2656 → 2.0.6-dev.2657

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.
@@ -16206,6 +16206,10 @@ function rowToEdge(r) {
16206
16206
  }
16207
16207
  var SqliteGraphStore = class {
16208
16208
  db;
16209
+ // Typed edge lookups build their SQL from the number of types asked for, and
16210
+ // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
16211
+ // 225k-node store was statement compilation alone. Keyed by the SQL text.
16212
+ typedEdgeStmts = /* @__PURE__ */ new Map();
16209
16213
  stmts;
16210
16214
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
16211
16215
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -16710,6 +16714,14 @@ var SqliteGraphStore = class {
16710
16714
  const rows = this.db.prepare(sql).all(params);
16711
16715
  return rows.map(rowToNode);
16712
16716
  }
16717
+ typedEdgeStmt(sql) {
16718
+ let stmt = this.typedEdgeStmts.get(sql);
16719
+ if (!stmt) {
16720
+ stmt = this.db.prepare(sql);
16721
+ this.typedEdgeStmts.set(sql, stmt);
16722
+ }
16723
+ return stmt;
16724
+ }
16713
16725
  outEdges(nodeId, types) {
16714
16726
  if (!types || types.length === 0) {
16715
16727
  const rows2 = this.stmts.outEdges.all(nodeId);
@@ -16720,7 +16732,7 @@ var SqliteGraphStore = class {
16720
16732
  types.forEach((t, i) => {
16721
16733
  params[`t${i}`] = t;
16722
16734
  });
16723
- const rows = this.db.prepare(
16735
+ const rows = this.typedEdgeStmt(
16724
16736
  `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
16725
16737
  ).all(params);
16726
16738
  return rows.map(rowToEdge);
@@ -16735,7 +16747,7 @@ var SqliteGraphStore = class {
16735
16747
  types.forEach((t, i) => {
16736
16748
  params[`t${i}`] = t;
16737
16749
  });
16738
- const rows = this.db.prepare(
16750
+ const rows = this.typedEdgeStmt(
16739
16751
  `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
16740
16752
  ).all(params);
16741
16753
  return rows.map(rowToEdge);
@@ -16813,6 +16825,15 @@ var SqliteGraphStore = class {
16813
16825
  const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
16814
16826
  return rows.map((r) => r.id);
16815
16827
  }
16828
+ labelFingerprint(labels) {
16829
+ if (labels.length === 0) return "0";
16830
+ const r = this.db.prepare(
16831
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
16832
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
16833
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
16834
+ ).get(...labels);
16835
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
16836
+ }
16816
16837
  /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
16817
16838
  * Closures matter as much as additions: a node that lost inflow is rank-dirty
16818
16839
  * while its own row never updated, so removal endpoints must seed the
@@ -18254,10 +18275,16 @@ function internalPackageNames(workspaceRoot) {
18254
18275
  return names;
18255
18276
  }
18256
18277
  var cache = /* @__PURE__ */ new Map();
18278
+ var cacheKeys = /* @__PURE__ */ new Map();
18257
18279
  function publicSymbolIndex(store, workspaceRoot, deps = {}) {
18258
18280
  const now = deps.nowMs ?? Date.now();
18281
+ const fileRows = store.labelFingerprint?.(["File"]);
18282
+ const key = fileRows === void 0 ? void 0 : `${fileRows}|${store.countNodesByLabel("Module")}`;
18259
18283
  const cached2 = cache.get(workspaceRoot);
18260
- if (cached2 && now - cached2.builtAt < INDEX_TTL_MS) return cached2;
18284
+ if (cached2) {
18285
+ const fresh = key !== void 0 ? cacheKeys.get(workspaceRoot) === key : now - cached2.builtAt < INDEX_TTL_MS;
18286
+ if (fresh) return cached2;
18287
+ }
18261
18288
  const readHead = deps.readHead ?? ((absPath) => {
18262
18289
  try {
18263
18290
  return readFileSync(absPath, "utf8").slice(0, HEAD_BYTES);
@@ -18294,6 +18321,8 @@ function publicSymbolIndex(store, workspaceRoot, deps = {}) {
18294
18321
  }
18295
18322
  const index = { symbols, builtAt: now };
18296
18323
  cache.set(workspaceRoot, index);
18324
+ if (key !== void 0) cacheKeys.set(workspaceRoot, key);
18325
+ else cacheKeys.delete(workspaceRoot);
18297
18326
  return index;
18298
18327
  }
18299
18328
  var PRESERVED_SYMBOLS_CAP = 8;
@@ -19082,7 +19111,16 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19082
19111
  ...profile.repoLocatorAliases ? { aliases: profile.repoLocatorAliases } : {},
19083
19112
  ...profile.aka ? { aka: profile.aka } : {}
19084
19113
  };
19085
- const fileLexicon = buildFileLexicon(store);
19114
+ const fileLexicon = opts.fileLexicon ?? buildFileLexicon(store);
19115
+ const nodeMemo = /* @__PURE__ */ new Map();
19116
+ const nodeOf = (id) => {
19117
+ let n = nodeMemo.get(id);
19118
+ if (n === void 0) {
19119
+ n = store.getNode(id) ?? opts.sharedStore?.getNode(id) ?? null;
19120
+ nodeMemo.set(id, n);
19121
+ }
19122
+ return n;
19123
+ };
19086
19124
  const shareable = (n) => {
19087
19125
  const preserved = symbolIndex ? preservedSymbolsFor(n.description, symbolIndex) : [];
19088
19126
  const fileRefs = generalizeFileRefs(n.description, fileLexicon, level);
@@ -19207,7 +19245,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19207
19245
  for (const sourceId of anchorSources) {
19208
19246
  const targets = [];
19209
19247
  for (const e of store.outEdges(sourceId, [...ANCHOR_EDGES])) {
19210
- const t = store.getNode(e.to);
19248
+ const t = nodeOf(e.to);
19211
19249
  if (!t || !ANCHOR_TARGET_LABELS.includes(t.label)) continue;
19212
19250
  if (e.type === "OCCURS_IN" && t.label !== "Language") continue;
19213
19251
  if (e.type === "DEPENDS_ON" && t.label !== "Package") continue;
@@ -19230,7 +19268,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19230
19268
  }
19231
19269
  }
19232
19270
  const dg = digest(targets.map(({ e, wireId }) => `${e.type}>${wireId}`).sort());
19233
- const sourceNode = store.getNode(sourceId);
19271
+ const sourceNode = nodeOf(sourceId);
19234
19272
  if (sourceNode?.attrs["anchorsContributedDigest"] === dg) continue;
19235
19273
  anchorDigests[sourceId] = dg;
19236
19274
  const wireFrom = seen.has(sourceId) || !sourceNode ? sourceId : contributedWireId(sourceNode);
@@ -19260,12 +19298,12 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19260
19298
  if (project) {
19261
19299
  const now = Date.now();
19262
19300
  for (const sourceId of anchorSources) {
19263
- const source = store.getNode(sourceId);
19301
+ const source = nodeOf(sourceId);
19264
19302
  if (!source) continue;
19265
19303
  const wireFrom = seen.has(sourceId) ? sourceId : contributedWireId(source);
19266
19304
  const planned = [];
19267
19305
  for (const e of store.outEdges(sourceId, ["ANCHORED_AT"])) {
19268
- const target = store.getNode(e.to);
19306
+ const target = nodeOf(e.to);
19269
19307
  if (!target) continue;
19270
19308
  if (!isAnchorLabelKind(target.label)) continue;
19271
19309
  const isFile = target.label === "File";
@@ -19342,7 +19380,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19342
19380
  const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
19343
19381
  if (outs.length === 0) continue;
19344
19382
  const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
19345
- const owner = store.getNode(sourceId) ?? opts.sharedStore?.getNode(sourceId);
19383
+ const owner = nodeOf(sourceId);
19346
19384
  const digestSkip = owner?.attrs["semanticEdgesContributedDigest"] === dg;
19347
19385
  if (!digestSkip) semanticEdgeDigests[sourceId] = dg;
19348
19386
  for (const { e, wireTo } of outs) {
@@ -19399,7 +19437,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19399
19437
  const heldOrphans = /* @__PURE__ */ new Set();
19400
19438
  for (const wn of nodes) {
19401
19439
  if (wn.label !== "RootCause" && wn.label !== "Solution") continue;
19402
- const src = store.getNode(wn.id) ?? opts.sharedStore?.getNode(wn.id);
19440
+ const src = nodeOf(wn.id);
19403
19441
  if (typeof src?.attrs["contributedAtSeq"] === "number") continue;
19404
19442
  if (bindingEndpoints.has(wn.id)) continue;
19405
19443
  heldOrphans.add(wn.id);
@@ -19444,6 +19482,21 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
19444
19482
  }
19445
19483
 
19446
19484
  // src/drain-build.ts
19485
+ var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
19486
+ function codeGraphKey(store) {
19487
+ const files = store.labelFingerprint?.(["File"]);
19488
+ if (files === void 0) return null;
19489
+ return [files, ...LEXICON_SYMBOL_LABELS.map((l) => store.countNodesByLabel(l))].join("|");
19490
+ }
19491
+ var lexiconCache = /* @__PURE__ */ new WeakMap();
19492
+ function lexiconsFor(store) {
19493
+ const key = codeGraphKey(store);
19494
+ const hit = key !== null ? lexiconCache.get(store) : void 0;
19495
+ if (hit && hit.key === key) return hit;
19496
+ const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
19497
+ if (key !== null) lexiconCache.set(store, { key, ...built });
19498
+ return built;
19499
+ }
19447
19500
  function buildDrainContext(store, input) {
19448
19501
  return buildContextIngest(store, input.profile, input.daemonVersion, input.ignorePatterns, {
19449
19502
  ...input.includePackages !== void 0 ? { includePackages: input.includePackages } : {}
@@ -19451,9 +19504,10 @@ function buildDrainContext(store, input) {
19451
19504
  }
19452
19505
  function buildDrainInstances(store, sharedStore, input) {
19453
19506
  let lexicon;
19507
+ let fileLexicon;
19454
19508
  let lexiconError;
19455
19509
  try {
19456
- lexicon = buildSymbolLexicon(store);
19510
+ ({ symbols: lexicon, files: fileLexicon } = lexiconsFor(store));
19457
19511
  } catch (err) {
19458
19512
  lexiconError = err instanceof Error ? err.message : String(err);
19459
19513
  }
@@ -19466,6 +19520,7 @@ function buildDrainInstances(store, sharedStore, input) {
19466
19520
  composition = c;
19467
19521
  },
19468
19522
  ...lexicon ? { lexicon } : {},
19523
+ ...fileLexicon ? { fileLexicon } : {},
19469
19524
  ...input.project ? { project: input.project } : {}
19470
19525
  });
19471
19526
  return {
package/errata.mjs CHANGED
@@ -17566,6 +17566,10 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
17566
17566
  `;
17567
17567
  SqliteGraphStore = class {
17568
17568
  db;
17569
+ // Typed edge lookups build their SQL from the number of types asked for, and
17570
+ // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
17571
+ // 225k-node store was statement compilation alone. Keyed by the SQL text.
17572
+ typedEdgeStmts = /* @__PURE__ */ new Map();
17569
17573
  stmts;
17570
17574
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
17571
17575
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -18070,6 +18074,14 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18070
18074
  const rows = this.db.prepare(sql).all(params);
18071
18075
  return rows.map(rowToNode);
18072
18076
  }
18077
+ typedEdgeStmt(sql) {
18078
+ let stmt = this.typedEdgeStmts.get(sql);
18079
+ if (!stmt) {
18080
+ stmt = this.db.prepare(sql);
18081
+ this.typedEdgeStmts.set(sql, stmt);
18082
+ }
18083
+ return stmt;
18084
+ }
18073
18085
  outEdges(nodeId, types) {
18074
18086
  if (!types || types.length === 0) {
18075
18087
  const rows2 = this.stmts.outEdges.all(nodeId);
@@ -18080,7 +18092,7 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18080
18092
  types.forEach((t, i2) => {
18081
18093
  params[`t${i2}`] = t;
18082
18094
  });
18083
- const rows = this.db.prepare(
18095
+ const rows = this.typedEdgeStmt(
18084
18096
  `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
18085
18097
  ).all(params);
18086
18098
  return rows.map(rowToEdge);
@@ -18095,7 +18107,7 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18095
18107
  types.forEach((t, i2) => {
18096
18108
  params[`t${i2}`] = t;
18097
18109
  });
18098
- const rows = this.db.prepare(
18110
+ const rows = this.typedEdgeStmt(
18099
18111
  `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
18100
18112
  ).all(params);
18101
18113
  return rows.map(rowToEdge);
@@ -18173,6 +18185,15 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
18173
18185
  const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
18174
18186
  return rows.map((r) => r.id);
18175
18187
  }
18188
+ labelFingerprint(labels) {
18189
+ if (labels.length === 0) return "0";
18190
+ const r = this.db.prepare(
18191
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
18192
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
18193
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
18194
+ ).get(...labels);
18195
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
18196
+ }
18176
18197
  /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
18177
18198
  * Closures matter as much as additions: a node that lost inflow is rank-dirty
18178
18199
  * while its own row never updated, so removal endpoints must seed the
@@ -78515,7 +78536,7 @@ function createWatchBreaker(deps) {
78515
78536
  }
78516
78537
 
78517
78538
  // src/engine.ts
78518
- var DAEMON_VERSION = true ? "2.0.6-dev.2656" : "2.0.0-alpha.0";
78539
+ var DAEMON_VERSION = true ? "2.0.6-dev.2657" : "2.0.0-alpha.0";
78519
78540
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
78520
78541
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
78521
78542
  var GIT_OP_MUTE_MS = 4e3;
@@ -81236,10 +81257,16 @@ function internalPackageNames(workspaceRoot) {
81236
81257
  return names;
81237
81258
  }
81238
81259
  var cache = /* @__PURE__ */ new Map();
81260
+ var cacheKeys = /* @__PURE__ */ new Map();
81239
81261
  function publicSymbolIndex(store, workspaceRoot, deps = {}) {
81240
81262
  const now = deps.nowMs ?? Date.now();
81263
+ const fileRows = store.labelFingerprint?.(["File"]);
81264
+ const key = fileRows === void 0 ? void 0 : `${fileRows}|${store.countNodesByLabel("Module")}`;
81241
81265
  const cached2 = cache.get(workspaceRoot);
81242
- if (cached2 && now - cached2.builtAt < INDEX_TTL_MS) return cached2;
81266
+ if (cached2) {
81267
+ const fresh = key !== void 0 ? cacheKeys.get(workspaceRoot) === key : now - cached2.builtAt < INDEX_TTL_MS;
81268
+ if (fresh) return cached2;
81269
+ }
81243
81270
  const readHead = deps.readHead ?? ((absPath) => {
81244
81271
  try {
81245
81272
  return readFileSync28(absPath, "utf8").slice(0, HEAD_BYTES);
@@ -81276,6 +81303,8 @@ function publicSymbolIndex(store, workspaceRoot, deps = {}) {
81276
81303
  }
81277
81304
  const index = { symbols, builtAt: now };
81278
81305
  cache.set(workspaceRoot, index);
81306
+ if (key !== void 0) cacheKeys.set(workspaceRoot, key);
81307
+ else cacheKeys.delete(workspaceRoot);
81279
81308
  return index;
81280
81309
  }
81281
81310
  var PRESERVED_SYMBOLS_CAP = 8;
@@ -81410,7 +81439,16 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81410
81439
  ...profile.repoLocatorAliases ? { aliases: profile.repoLocatorAliases } : {},
81411
81440
  ...profile.aka ? { aka: profile.aka } : {}
81412
81441
  };
81413
- const fileLexicon = buildFileLexicon(store);
81442
+ const fileLexicon = opts.fileLexicon ?? buildFileLexicon(store);
81443
+ const nodeMemo = /* @__PURE__ */ new Map();
81444
+ const nodeOf = (id) => {
81445
+ let n = nodeMemo.get(id);
81446
+ if (n === void 0) {
81447
+ n = store.getNode(id) ?? opts.sharedStore?.getNode(id) ?? null;
81448
+ nodeMemo.set(id, n);
81449
+ }
81450
+ return n;
81451
+ };
81414
81452
  const shareable = (n) => {
81415
81453
  const preserved = symbolIndex ? preservedSymbolsFor(n.description, symbolIndex) : [];
81416
81454
  const fileRefs = generalizeFileRefs(n.description, fileLexicon, level);
@@ -81535,7 +81573,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81535
81573
  for (const sourceId of anchorSources) {
81536
81574
  const targets = [];
81537
81575
  for (const e of store.outEdges(sourceId, [...ANCHOR_EDGES])) {
81538
- const t = store.getNode(e.to);
81576
+ const t = nodeOf(e.to);
81539
81577
  if (!t || !ANCHOR_TARGET_LABELS.includes(t.label)) continue;
81540
81578
  if (e.type === "OCCURS_IN" && t.label !== "Language") continue;
81541
81579
  if (e.type === "DEPENDS_ON" && t.label !== "Package") continue;
@@ -81558,7 +81596,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81558
81596
  }
81559
81597
  }
81560
81598
  const dg = digest(targets.map(({ e, wireId }) => `${e.type}>${wireId}`).sort());
81561
- const sourceNode = store.getNode(sourceId);
81599
+ const sourceNode = nodeOf(sourceId);
81562
81600
  if (sourceNode?.attrs["anchorsContributedDigest"] === dg) continue;
81563
81601
  anchorDigests[sourceId] = dg;
81564
81602
  const wireFrom = seen.has(sourceId) || !sourceNode ? sourceId : contributedWireId(sourceNode);
@@ -81588,12 +81626,12 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81588
81626
  if (project) {
81589
81627
  const now = Date.now();
81590
81628
  for (const sourceId of anchorSources) {
81591
- const source = store.getNode(sourceId);
81629
+ const source = nodeOf(sourceId);
81592
81630
  if (!source) continue;
81593
81631
  const wireFrom = seen.has(sourceId) ? sourceId : contributedWireId(source);
81594
81632
  const planned = [];
81595
81633
  for (const e of store.outEdges(sourceId, ["ANCHORED_AT"])) {
81596
- const target = store.getNode(e.to);
81634
+ const target = nodeOf(e.to);
81597
81635
  if (!target) continue;
81598
81636
  if (!isAnchorLabelKind(target.label)) continue;
81599
81637
  const isFile = target.label === "File";
@@ -81670,7 +81708,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81670
81708
  const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
81671
81709
  if (outs.length === 0) continue;
81672
81710
  const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
81673
- const owner = store.getNode(sourceId) ?? opts.sharedStore?.getNode(sourceId);
81711
+ const owner = nodeOf(sourceId);
81674
81712
  const digestSkip = owner?.attrs["semanticEdgesContributedDigest"] === dg;
81675
81713
  if (!digestSkip) semanticEdgeDigests[sourceId] = dg;
81676
81714
  for (const { e, wireTo } of outs) {
@@ -81727,7 +81765,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
81727
81765
  const heldOrphans = /* @__PURE__ */ new Set();
81728
81766
  for (const wn of nodes) {
81729
81767
  if (wn.label !== "RootCause" && wn.label !== "Solution") continue;
81730
- const src = store.getNode(wn.id) ?? opts.sharedStore?.getNode(wn.id);
81768
+ const src = nodeOf(wn.id);
81731
81769
  if (typeof src?.attrs["contributedAtSeq"] === "number") continue;
81732
81770
  if (bindingEndpoints.has(wn.id)) continue;
81733
81771
  heldOrphans.add(wn.id);
@@ -81811,8 +81849,58 @@ function buildCausalEdgeBackfill(store, profile, daemonVersion) {
81811
81849
  return { ...base, payloadDigest: digest(base) };
81812
81850
  }
81813
81851
 
81852
+ // src/live-sync.ts
81853
+ var LIVE_SYNC_DEFAULTS = {
81854
+ enabled: true,
81855
+ debounceMs: 1e4,
81856
+ maxWaitMs: 6e4
81857
+ };
81858
+ var BACKOFF_BASE_MS = 5 * 6e4;
81859
+ var BACKOFF_MAX_MS = 60 * 6e4;
81860
+ function positiveMs(raw2, fallback) {
81861
+ if (raw2 === void 0 || raw2.trim() === "") return fallback;
81862
+ const n = Number(raw2);
81863
+ return Number.isFinite(n) && n > 0 ? n : fallback;
81864
+ }
81865
+ function liveSyncConfig(env2 = process.env) {
81866
+ const flag = (env2["ERRATA_LIVE_SYNC"] ?? "").trim().toLowerCase();
81867
+ const debounceMs = positiveMs(env2["ERRATA_LIVE_SYNC_DEBOUNCE_MS"], LIVE_SYNC_DEFAULTS.debounceMs);
81868
+ return {
81869
+ enabled: !["0", "false", "off", "no"].includes(flag),
81870
+ debounceMs,
81871
+ // A ceiling below the debounce would make the debounce meaningless.
81872
+ maxWaitMs: Math.max(debounceMs, positiveMs(env2["ERRATA_LIVE_SYNC_MAX_WAIT_MS"], LIVE_SYNC_DEFAULTS.maxWaitMs))
81873
+ };
81874
+ }
81875
+ function liveSyncEnabled(env2 = process.env) {
81876
+ return liveSyncConfig(env2).enabled;
81877
+ }
81878
+ function liveShipDelay(now, dirtySince, cfg) {
81879
+ const ceiling = dirtySince + cfg.maxWaitMs - now;
81880
+ return Math.max(0, Math.min(cfg.debounceMs, ceiling));
81881
+ }
81882
+ function liveBackoffMs(failures) {
81883
+ if (failures <= 0) return 0;
81884
+ return Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** (failures - 1));
81885
+ }
81886
+
81814
81887
  // src/drain-build.ts
81815
81888
  init_generalize_graph();
81889
+ var LEXICON_SYMBOL_LABELS = ["Function", "Method", "Class", "Interface", "Const", "Comment"];
81890
+ function codeGraphKey(store) {
81891
+ const files = store.labelFingerprint?.(["File"]);
81892
+ if (files === void 0) return null;
81893
+ return [files, ...LEXICON_SYMBOL_LABELS.map((l) => store.countNodesByLabel(l))].join("|");
81894
+ }
81895
+ var lexiconCache = /* @__PURE__ */ new WeakMap();
81896
+ function lexiconsFor(store) {
81897
+ const key = codeGraphKey(store);
81898
+ const hit = key !== null ? lexiconCache.get(store) : void 0;
81899
+ if (hit && hit.key === key) return hit;
81900
+ const built = { symbols: buildSymbolLexicon(store), files: buildFileLexicon(store) };
81901
+ if (key !== null) lexiconCache.set(store, { key, ...built });
81902
+ return built;
81903
+ }
81816
81904
  function buildDrainContext(store, input) {
81817
81905
  return buildContextIngest(store, input.profile, input.daemonVersion, input.ignorePatterns, {
81818
81906
  ...input.includePackages !== void 0 ? { includePackages: input.includePackages } : {}
@@ -81820,9 +81908,10 @@ function buildDrainContext(store, input) {
81820
81908
  }
81821
81909
  function buildDrainInstances(store, sharedStore, input) {
81822
81910
  let lexicon;
81911
+ let fileLexicon;
81823
81912
  let lexiconError;
81824
81913
  try {
81825
- lexicon = buildSymbolLexicon(store);
81914
+ ({ symbols: lexicon, files: fileLexicon } = lexiconsFor(store));
81826
81915
  } catch (err2) {
81827
81916
  lexiconError = err2 instanceof Error ? err2.message : String(err2);
81828
81917
  }
@@ -81835,6 +81924,7 @@ function buildDrainInstances(store, sharedStore, input) {
81835
81924
  composition = c;
81836
81925
  },
81837
81926
  ...lexicon ? { lexicon } : {},
81927
+ ...fileLexicon ? { fileLexicon } : {},
81838
81928
  ...input.project ? { project: input.project } : {}
81839
81929
  });
81840
81930
  return {
@@ -85215,6 +85305,8 @@ async function startMultiDaemon(opts = {}) {
85215
85305
  let serverPending = null;
85216
85306
  const boundaryListeners = [];
85217
85307
  let boundaryFlushing = false;
85308
+ let pushRequested = false;
85309
+ let shippedFingerprint = "";
85218
85310
  let settleRunning = false;
85219
85311
  let boundaryFlushStartedAt = 0;
85220
85312
  let boundaryFlushStage = "";
@@ -85284,6 +85376,36 @@ async function startMultiDaemon(opts = {}) {
85284
85376
  boundaryFlushStage = "";
85285
85377
  endFlushPass?.();
85286
85378
  endFlushPass = null;
85379
+ if (pushRequested) {
85380
+ pushRequested = false;
85381
+ setImmediate(() => {
85382
+ void daemon.liveShip().catch(() => {
85383
+ });
85384
+ });
85385
+ }
85386
+ };
85387
+ const instanceFingerprint = () => {
85388
+ const cfg2 = loadConfig();
85389
+ const parts2 = [];
85390
+ for (const r of records) {
85391
+ if (!effectiveConsent(cfg2, r.engine.profile).sync) continue;
85392
+ parts2.push(`${r.id}=${r.engine.store.labelFingerprint?.(INSTANCE_LABELS) ?? `?${Date.now()}`}`);
85393
+ }
85394
+ if (parts2.length === 0) return "";
85395
+ parts2.push(`shared=${sharedStore.labelFingerprint?.(["RootCause"]) ?? `?${Date.now()}`}`);
85396
+ return parts2.join("|");
85397
+ };
85398
+ const runSettle = async () => {
85399
+ if (settleRunning) return;
85400
+ settleRunning = true;
85401
+ try {
85402
+ for (const r of records) {
85403
+ const donePass = markPass(`settle:${r.engine.profile.name}`);
85404
+ await r.engine.embedSettled().finally(donePass);
85405
+ }
85406
+ } finally {
85407
+ settleRunning = false;
85408
+ }
85287
85409
  };
85288
85410
  const fireBoundary = () => {
85289
85411
  for (const l of boundaryListeners) {
@@ -86565,22 +86687,17 @@ async function startMultiDaemon(opts = {}) {
86565
86687
  return { written, pruned: pruned2 };
86566
86688
  },
86567
86689
  async sessionBoundaryFlush() {
86568
- if (!settleRunning) {
86569
- settleRunning = true;
86570
- try {
86571
- for (const r of records) {
86572
- const donePass = markPass(`settle:${r.engine.profile.name}`);
86573
- await r.engine.embedSettled().finally(donePass);
86574
- }
86575
- } finally {
86576
- settleRunning = false;
86577
- }
86690
+ await runSettle();
86691
+ if (boundaryFlushing) {
86692
+ pushRequested = true;
86693
+ return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
86578
86694
  }
86579
- if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
86580
86695
  beginFlush("instances");
86581
86696
  try {
86582
86697
  flushStage("instances");
86698
+ const startedFrom = instanceFingerprint();
86583
86699
  const inst = await daemon.syncInstancesPublic();
86700
+ shippedFingerprint = startedFrom;
86584
86701
  flushStage("skills");
86585
86702
  const skills = await daemon.syncSkillsAll();
86586
86703
  return {
@@ -86593,6 +86710,36 @@ async function startMultiDaemon(opts = {}) {
86593
86710
  endFlush();
86594
86711
  }
86595
86712
  },
86713
+ pendingLiveFingerprint() {
86714
+ const fp = instanceFingerprint();
86715
+ return fp && fp !== shippedFingerprint ? fp : null;
86716
+ },
86717
+ async liveShip() {
86718
+ if (!liveSyncEnabled()) return { uploaded: 0, skipped: "disabled" };
86719
+ if (daemon.pendingLiveFingerprint() === null) return { uploaded: 0, skipped: "unchanged" };
86720
+ if (boundaryFlushing) {
86721
+ pushRequested = true;
86722
+ return { uploaded: 0, skipped: "in-flight" };
86723
+ }
86724
+ await runSettle();
86725
+ if (boundaryFlushing) {
86726
+ pushRequested = true;
86727
+ return { uploaded: 0, skipped: "in-flight" };
86728
+ }
86729
+ beginFlush("live");
86730
+ try {
86731
+ const startedFrom = instanceFingerprint();
86732
+ const inst = await daemon.syncInstancesPublic();
86733
+ shippedFingerprint = startedFrom;
86734
+ return {
86735
+ uploaded: inst.uploaded,
86736
+ ...inst.skipped ? { skipped: inst.skipped } : {},
86737
+ ...inst.errors ? { errors: inst.errors } : {}
86738
+ };
86739
+ } finally {
86740
+ endFlush();
86741
+ }
86742
+ },
86596
86743
  onSessionBoundary(cb) {
86597
86744
  boundaryListeners.push(cb);
86598
86745
  },
@@ -90059,8 +90206,53 @@ async function cmdDash(args2) {
90059
90206
  void flushed.finally(() => triggerConsolidation(force));
90060
90207
  };
90061
90208
  handle2.onSessionBoundary(() => settleThenConsolidate("session-end", true));
90209
+ const liveCfg = liveSyncConfig();
90210
+ let liveTimer = null;
90211
+ let liveDirtySince = 0;
90212
+ let liveSeenFingerprint = "";
90213
+ let liveFailures = 0;
90214
+ let liveBackoffUntil = 0;
90215
+ const fireLiveShip = () => {
90216
+ liveTimer = null;
90217
+ void handle2.liveShip().then((r) => {
90218
+ if (r.skipped === "in-flight") return;
90219
+ liveDirtySince = 0;
90220
+ if (r.errors && r.errors.length > 0) {
90221
+ liveFailures++;
90222
+ const pause = liveBackoffMs(liveFailures);
90223
+ liveBackoffUntil = Date.now() + pause;
90224
+ console.warn(
90225
+ `[live] push had ${r.errors.length} error(s); pausing the live push ${Math.round(pause / 6e4)}m (quiescence / session-end still flush)`
90226
+ );
90227
+ return;
90228
+ }
90229
+ liveFailures = 0;
90230
+ if (r.uploaded > 0) console.log(`[live] uploaded ${r.uploaded} node(s)/edge(s)`);
90231
+ }).catch(() => {
90232
+ });
90233
+ };
90234
+ const probeLivePush = () => {
90235
+ if (!liveCfg.enabled || Date.now() < liveBackoffUntil) return;
90236
+ let fp;
90237
+ try {
90238
+ fp = handle2.pendingLiveFingerprint();
90239
+ } catch {
90240
+ return;
90241
+ }
90242
+ if (fp === null) {
90243
+ liveSeenFingerprint = "";
90244
+ return;
90245
+ }
90246
+ if (fp === liveSeenFingerprint) return;
90247
+ liveSeenFingerprint = fp;
90248
+ const now = Date.now();
90249
+ if (!liveDirtySince) liveDirtySince = now;
90250
+ if (liveTimer) clearTimeout(liveTimer);
90251
+ liveTimer = setTimeout(fireLiveShip, liveShipDelay(now, liveDirtySince, liveCfg));
90252
+ };
90062
90253
  let ticking = false;
90063
90254
  const interval = setInterval(() => {
90255
+ probeLivePush();
90064
90256
  if (ticking) return;
90065
90257
  ticking = true;
90066
90258
  void handle2.tickAll().then((m) => {
@@ -90301,6 +90493,7 @@ shutting down\u2026 (${signal})`);
90301
90493
  clearInterval(interval);
90302
90494
  clearInterval(nightlyInterval);
90303
90495
  if (quiesceTimer) clearTimeout(quiesceTimer);
90496
+ if (liveTimer) clearTimeout(liveTimer);
90304
90497
  await handle2.stop();
90305
90498
  process.exit(0);
90306
90499
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.6-dev.2656",
3
+ "version": "2.0.6-dev.2657",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -24588,6 +24588,10 @@ function rowToEdge(r) {
24588
24588
  }
24589
24589
  var SqliteGraphStore = class {
24590
24590
  db;
24591
+ // Typed edge lookups build their SQL from the number of types asked for, and
24592
+ // used to prepare it on every call: ~600ms of a 2.5s drain build on a live
24593
+ // 225k-node store was statement compilation alone. Keyed by the SQL text.
24594
+ typedEdgeStmts = /* @__PURE__ */ new Map();
24591
24595
  stmts;
24592
24596
  /** Unified graph-mutation counter (see GraphStore.mutationCount). Bumped on
24593
24597
  * every node/edge upsert — code-index and capture alike — so the consolidation
@@ -25092,6 +25096,14 @@ var SqliteGraphStore = class {
25092
25096
  const rows = this.db.prepare(sql).all(params);
25093
25097
  return rows.map(rowToNode);
25094
25098
  }
25099
+ typedEdgeStmt(sql) {
25100
+ let stmt = this.typedEdgeStmts.get(sql);
25101
+ if (!stmt) {
25102
+ stmt = this.db.prepare(sql);
25103
+ this.typedEdgeStmts.set(sql, stmt);
25104
+ }
25105
+ return stmt;
25106
+ }
25095
25107
  outEdges(nodeId, types) {
25096
25108
  if (!types || types.length === 0) {
25097
25109
  const rows2 = this.stmts.outEdges.all(nodeId);
@@ -25102,7 +25114,7 @@ var SqliteGraphStore = class {
25102
25114
  types.forEach((t, i2) => {
25103
25115
  params[`t${i2}`] = t;
25104
25116
  });
25105
- const rows = this.db.prepare(
25117
+ const rows = this.typedEdgeStmt(
25106
25118
  `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
25107
25119
  ).all(params);
25108
25120
  return rows.map(rowToEdge);
@@ -25117,7 +25129,7 @@ var SqliteGraphStore = class {
25117
25129
  types.forEach((t, i2) => {
25118
25130
  params[`t${i2}`] = t;
25119
25131
  });
25120
- const rows = this.db.prepare(
25132
+ const rows = this.typedEdgeStmt(
25121
25133
  `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
25122
25134
  ).all(params);
25123
25135
  return rows.map(rowToEdge);
@@ -25195,6 +25207,15 @@ var SqliteGraphStore = class {
25195
25207
  const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
25196
25208
  return rows.map((r) => r.id);
25197
25209
  }
25210
+ labelFingerprint(labels) {
25211
+ if (labels.length === 0) return "0";
25212
+ const r = this.db.prepare(
25213
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
25214
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
25215
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
25216
+ ).get(...labels);
25217
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
25218
+ }
25198
25219
  /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
25199
25220
  * Closures matter as much as additions: a node that lost inflow is rank-dirty
25200
25221
  * while its own row never updated, so removal endpoints must seed the