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

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
@@ -71163,7 +71184,7 @@ var init_report_render = __esm({
71163
71184
  init_src6();
71164
71185
  import { closeSync as closeSync2, existsSync as existsSync34, openSync as openSync2, readFileSync as readFileSync34, renameSync as renameSync4, statSync as statSync8 } from "node:fs";
71165
71186
  import { join as join39, resolve as pathResolve2 } from "node:path";
71166
- import { spawn as spawn3 } from "node:child_process";
71187
+ import { spawn as spawn4 } from "node:child_process";
71167
71188
 
71168
71189
  // src/daemon.ts
71169
71190
  import { existsSync as existsSync26, writeFileSync as writeFileSync22 } from "node:fs";
@@ -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.2674" : "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 {
@@ -82620,6 +82710,17 @@ function isGeneratedPath(path2) {
82620
82710
  function isReleaseSubject(subject) {
82621
82711
  return RELEASE_SUBJECT_RE.test(subject.trim());
82622
82712
  }
82713
+ var FORGE_MARK_RES = [/^See merge request \S+!\d+$/i, /^Merge pull request #\d+ from \S+$/i];
82714
+ var TRAILER_RES = [
82715
+ /^(Closes|Fixes|Resolves|Related to|Part of)\b.*$/i,
82716
+ /^(Co-Authored-By|Signed-off-by|Reviewed-by|Approved-by):/i
82717
+ ];
82718
+ function isForgeTrailerBody(body2) {
82719
+ const lines = body2.split("\n").map((l) => l.trim()).filter((l) => l.length > 0);
82720
+ if (!lines.some((l) => FORGE_MARK_RES.some((re) => re.test(l)))) return false;
82721
+ const own = lines.filter((l) => !FORGE_MARK_RES.some((re) => re.test(l)) && !TRAILER_RES.some((re) => re.test(l)));
82722
+ return own.length <= 1;
82723
+ }
82623
82724
  function classifySkip(subject, files, tags) {
82624
82725
  if (files.length > 0 && files.every((f) => f.binary || isLockfilePath(f.path) || isGeneratedPath(f.path))) {
82625
82726
  return "lockfile-only";
@@ -82628,6 +82729,7 @@ function classifySkip(subject, files, tags) {
82628
82729
  return void 0;
82629
82730
  }
82630
82731
  var GIT_OPTS = ["-c", "core.quotepath=false", "-c", "color.ui=never", "--no-pager"];
82732
+ var MERGE_SIDE_CONCURRENCY = 6;
82631
82733
  var META_FMT = ["%H", "%P", "%aI", "%s", "%b"].join("%x00") + "%x00";
82632
82734
  function parseMeta(head2) {
82633
82735
  const [sha2, parents, authorDate, subject, body2] = head2.split("\0");
@@ -82666,6 +82768,38 @@ async function readCommit(root, sha2) {
82666
82768
  const rest2 = parts2.slice(5).join("\0");
82667
82769
  return { ...parseMeta(head2), files: parseNumstat(rest2) };
82668
82770
  }
82771
+ async function readCommitsBulk(root, branch, since) {
82772
+ const out2 = await git(root, [
82773
+ ...GIT_OPTS,
82774
+ "log",
82775
+ "--numstat",
82776
+ "-M",
82777
+ "--diff-merges=first-parent",
82778
+ `--format=%x1e${META_FMT}`,
82779
+ ...since ? [`^${since}`] : [],
82780
+ branch
82781
+ ]);
82782
+ const map2 = /* @__PURE__ */ new Map();
82783
+ for (const rec of out2.split("")) {
82784
+ if (!rec) continue;
82785
+ const parts2 = rec.split("\0");
82786
+ const meta3 = parseMeta(parts2.slice(0, 5).join("\0"));
82787
+ if (meta3.sha) map2.set(meta3.sha, { ...meta3, files: parseNumstat(parts2.slice(5).join("\0")) });
82788
+ }
82789
+ return map2;
82790
+ }
82791
+ async function mapBounded(items, limit, fn) {
82792
+ const out2 = new Array(items.length);
82793
+ let next = 0;
82794
+ const worker = async () => {
82795
+ while (next < items.length) {
82796
+ const i2 = next++;
82797
+ out2[i2] = await fn(items[i2]);
82798
+ }
82799
+ };
82800
+ await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
82801
+ return out2;
82802
+ }
82669
82803
  async function firstParentLine(root, branch) {
82670
82804
  const out2 = await git(root, [...GIT_OPTS, "rev-list", "--first-parent", "--reverse", "--parents", branch]);
82671
82805
  return out2.split(/\r?\n/).filter((l) => l.trim().length > 0).map((l) => {
@@ -82705,11 +82839,18 @@ async function enumerateUnits(root, opts = {}) {
82705
82839
  }
82706
82840
  const tags = await tagsByCommit(root);
82707
82841
  const tagsOf = (sha2) => tags.get(sha2) ?? [];
82842
+ if (start2 >= line.length) return [];
82843
+ const commits = await readCommitsBulk(root, branch, start2 > 0 ? line[start2 - 1].sha : null);
82844
+ const commit = async (sha2) => commits.get(sha2) ?? await readCommit(root, sha2);
82845
+ const merges = line.slice(start2).filter((c) => c.parents.length >= 2);
82846
+ const sides = /* @__PURE__ */ new Map();
82847
+ const sideLists = await mapBounded(merges, MERGE_SIDE_CONCURRENCY, (m) => mergeSide(root, m.parents[0], m.parents[1]));
82848
+ merges.forEach((m, i2) => sides.set(m.sha, sideLists[i2]));
82708
82849
  const units = [];
82709
82850
  for (let index = start2; index < line.length; index++) {
82710
82851
  const { sha: sha2, parents } = line[index];
82711
82852
  if (parents.length < 2) {
82712
- const c = await readCommit(root, sha2);
82853
+ const c = await commit(sha2);
82713
82854
  const skip = classifySkip(c.subject, c.files, tagsOf(sha2));
82714
82855
  units.push({
82715
82856
  index,
@@ -82725,11 +82866,11 @@ async function enumerateUnits(root, opts = {}) {
82725
82866
  });
82726
82867
  continue;
82727
82868
  }
82728
- const side = await mergeSide(root, parents[0], parents[1]);
82869
+ const side = sides.get(sha2) ?? await mergeSide(root, parents[0], parents[1]);
82729
82870
  const group = [];
82730
82871
  for (const s of side) {
82731
82872
  if (s.parents.length >= 2) continue;
82732
- const c = await readCommit(root, s.sha);
82873
+ const c = await commit(s.sha);
82733
82874
  const skip = classifySkip(c.subject, c.files, tagsOf(s.sha));
82734
82875
  group.push({
82735
82876
  index,
@@ -82745,8 +82886,9 @@ async function enumerateUnits(root, opts = {}) {
82745
82886
  groupSha: sha2
82746
82887
  });
82747
82888
  }
82748
- const merge2 = await readCommit(root, sha2);
82749
- if (merge2.body.length > 0) {
82889
+ const merge2 = await commit(sha2);
82890
+ const hasMembers = group.some((g) => !g.skip);
82891
+ if (merge2.body.length > 0 && !(hasMembers && isForgeTrailerBody(merge2.body))) {
82750
82892
  const skip = classifySkip(merge2.subject, merge2.files, tagsOf(sha2));
82751
82893
  group.push({
82752
82894
  index,
@@ -83978,10 +84120,111 @@ async function buildUnitBundle(root, unit, opts = {}) {
83978
84120
  };
83979
84121
  }
83980
84122
 
84123
+ // src/git-batch.ts
84124
+ import { spawn as spawn3 } from "node:child_process";
84125
+ var GitBlobReader = class {
84126
+ constructor(root, maxCached = 4096) {
84127
+ this.root = root;
84128
+ this.maxCached = maxCached;
84129
+ }
84130
+ proc = null;
84131
+ buf = Buffer.alloc(0);
84132
+ queue = [];
84133
+ cache = /* @__PURE__ */ new Map();
84134
+ failed = null;
84135
+ start() {
84136
+ if (this.proc) return this.proc;
84137
+ const proc = spawn3("git", ["-C", this.root, "cat-file", "--batch"], { windowsHide: true });
84138
+ proc.stdout.on("data", (chunk) => {
84139
+ this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);
84140
+ this.drain();
84141
+ });
84142
+ const fail = (err2) => {
84143
+ this.failed = err2;
84144
+ this.proc = null;
84145
+ for (const p of this.queue.splice(0)) p.reject(err2);
84146
+ };
84147
+ proc.on("error", fail);
84148
+ proc.stdin.on("error", fail);
84149
+ proc.on("exit", (code) => {
84150
+ if (this.queue.length > 0) fail(new Error(`git cat-file --batch exited (${code}) with ${this.queue.length} reads pending`));
84151
+ this.proc = null;
84152
+ });
84153
+ this.proc = proc;
84154
+ return proc;
84155
+ }
84156
+ drain() {
84157
+ for (; ; ) {
84158
+ const nl = this.buf.indexOf(10);
84159
+ if (nl < 0 || this.queue.length === 0) return;
84160
+ const header = this.buf.subarray(0, nl).toString("utf8");
84161
+ if (header.endsWith(" missing") || header.endsWith(" ambiguous")) {
84162
+ this.buf = this.buf.subarray(nl + 1);
84163
+ this.queue.shift().resolve(null);
84164
+ continue;
84165
+ }
84166
+ const size = Number(header.slice(header.lastIndexOf(" ") + 1));
84167
+ if (!Number.isFinite(size)) {
84168
+ const err2 = new Error(`git cat-file --batch: unexpected header ${JSON.stringify(header.slice(0, 120))}`);
84169
+ this.queue.shift().reject(err2);
84170
+ this.buf = this.buf.subarray(nl + 1);
84171
+ continue;
84172
+ }
84173
+ if (this.buf.length < nl + 1 + size + 1) return;
84174
+ const body2 = this.buf.subarray(nl + 1, nl + 1 + size).toString("utf8");
84175
+ this.buf = this.buf.subarray(nl + 1 + size + 1);
84176
+ const oid = header.slice(0, header.indexOf(" "));
84177
+ this.remember(oid, body2);
84178
+ this.queue.shift().resolve(body2);
84179
+ }
84180
+ }
84181
+ remember(oid, text) {
84182
+ if (this.cache.size >= this.maxCached) {
84183
+ const first = this.cache.keys().next().value;
84184
+ if (first !== void 0) this.cache.delete(first);
84185
+ }
84186
+ this.cache.set(oid, text);
84187
+ }
84188
+ /** Read one object by blob id (served from cache when seen) or `<rev>:<path>`. */
84189
+ read(spec) {
84190
+ const hit = this.cache.get(spec);
84191
+ if (hit !== void 0) return Promise.resolve(hit);
84192
+ if (this.failed) return Promise.reject(this.failed);
84193
+ if (/[\r\n]/.test(spec)) return Promise.resolve(null);
84194
+ const proc = this.start();
84195
+ return new Promise((resolve7, reject) => {
84196
+ this.queue.push({ resolve: resolve7, reject });
84197
+ proc.stdin.write(`${spec}
84198
+ `);
84199
+ });
84200
+ }
84201
+ /**
84202
+ * End the process and wait for it to exit: on Windows a live cat-file keeps
84203
+ * the repo's pack files open, so a caller that removes or moves the checkout
84204
+ * right after the walk (EPERM) must not race it. Bounded, never throws.
84205
+ */
84206
+ close(timeoutMs = 5e3) {
84207
+ const proc = this.proc;
84208
+ this.proc = null;
84209
+ if (!proc || proc.exitCode !== null) return Promise.resolve();
84210
+ return new Promise((resolve7) => {
84211
+ const timer = setTimeout(() => {
84212
+ proc.kill();
84213
+ resolve7();
84214
+ }, timeoutMs);
84215
+ proc.once("exit", () => {
84216
+ clearTimeout(timer);
84217
+ resolve7();
84218
+ });
84219
+ proc.stdin.end();
84220
+ });
84221
+ }
84222
+ };
84223
+
83981
84224
  // src/project-history.ts
83982
84225
  init_profile2();
83983
84226
  var execFileAsync = promisify(execFile3);
83984
- var HISTORY_EXTRACTOR_VERSION = "history-1";
84227
+ var HISTORY_EXTRACTOR_VERSION = "history-2";
83985
84228
  var LEASE_POLL_MS = 2e4;
83986
84229
  var RECOVERY_DEADLINE_MS = 15 * 6e4;
83987
84230
  var MAX_ANCHORS_PER_TOKEN = 20;
@@ -83997,27 +84240,38 @@ function readIgnoreLines(root, profile) {
83997
84240
  }
83998
84241
  return [...lines, ...profile.ignoreExtra ?? []].map((l) => l.trim()).filter((l) => l && !l.startsWith("#"));
83999
84242
  }
84000
- async function spineForUnit(git3, root, unit) {
84001
- const listing = await git3(root, ["ls-tree", "-r", "--name-only", unit.sha]);
84002
- const manifestPaths2 = listing.split("\n").map((l) => l.trim()).filter((p) => p && isManifestPath(p));
84003
- if (manifestPaths2.length === 0) return void 0;
84004
- const texts = /* @__PURE__ */ new Map();
84005
- for (const p of manifestPaths2) {
84243
+ function readBlobViaGit(git3, root) {
84244
+ return async (spec) => {
84006
84245
  try {
84007
- texts.set(p, await git3(root, ["show", `${unit.sha}:${p}`]));
84246
+ return await git3(root, ["show", spec]);
84008
84247
  } catch {
84248
+ return null;
84009
84249
  }
84250
+ };
84251
+ }
84252
+ function manifestBlobs(listing) {
84253
+ const out2 = /* @__PURE__ */ new Map();
84254
+ for (const line of listing.split("\n")) {
84255
+ const tab = line.indexOf(" ");
84256
+ if (tab < 0) continue;
84257
+ const path2 = line.slice(tab + 1).trim();
84258
+ const [, type, oid] = line.slice(0, tab).split(" ");
84259
+ if (type === "blob" && oid && path2 && isManifestPath(path2)) out2.set(path2, oid);
84010
84260
  }
84261
+ return out2;
84262
+ }
84263
+ async function spineForUnit(git3, root, unit, readBlob) {
84264
+ const blobs = manifestBlobs(await git3(root, ["ls-tree", "-r", unit.sha]));
84265
+ if (blobs.size === 0) return void 0;
84266
+ const texts = /* @__PURE__ */ new Map();
84267
+ const read2 = await Promise.all([...blobs].map(async ([p, oid]) => [p, await readBlob(oid).catch(() => null)]));
84268
+ for (const [p, text] of read2) if (text !== null) texts.set(p, text);
84011
84269
  const spine = buildSpine(unit.sha, [...texts.keys()], (p) => texts.get(p) ?? "");
84012
84270
  const parentTexts = /* @__PURE__ */ new Map();
84013
84271
  const readParent = (p) => parentTexts.get(p) ?? null;
84014
84272
  for (const f of unit.files) {
84015
84273
  if (!isManifestPath(f.path) || parentTexts.has(f.path)) continue;
84016
- try {
84017
- parentTexts.set(f.path, await git3(root, ["show", `${unit.sha}^:${f.path}`]));
84018
- } catch {
84019
- parentTexts.set(f.path, null);
84020
- }
84274
+ parentTexts.set(f.path, await readBlob(`${unit.sha}^:${f.path}`).catch(() => null));
84021
84275
  }
84022
84276
  const rings = depsForUnit(spine, unit.files.map((f) => f.path), readParent);
84023
84277
  const wire = (d) => ({ ecosystem: d.ecosystem, name: d.name, version: d.version, kind: d.kind, manifest: d.manifest });
@@ -84060,16 +84314,36 @@ function stage(profile, patch, now) {
84060
84314
  headSha: patch.headSha !== void 0 ? patch.headSha : prev?.headSha ?? null,
84061
84315
  startedAt: prev?.startedAt ?? at,
84062
84316
  updatedAt: at,
84063
- ...patch.lastError !== void 0 ? { lastError: patch.lastError } : prev?.lastError ? { lastError: prev.lastError } : {},
84317
+ // `lastError: undefined` in the patch CLEARS it (a resumed walk must not
84318
+ // keep showing the error it recovered from); an absent key keeps it.
84319
+ ..."lastError" in patch ? patch.lastError ? { lastError: patch.lastError } : {} : prev?.lastError ? { lastError: prev.lastError } : {},
84064
84320
  ...patch.counts ? { counts: patch.counts } : prev?.counts ? { counts: prev.counts } : {}
84065
84321
  }
84066
84322
  };
84067
84323
  }
84324
+ function unitTiming(postStarted, prepMs, receipt) {
84325
+ const s = (ms) => `${(ms / 1e3).toFixed(1)}s`;
84326
+ const parts2 = [`local ${s(prepMs)}`];
84327
+ const g = receipt.gate;
84328
+ if (g) parts2.push(g.pRoutine === null ? `jev ${g.error ?? "n/a"}` : `jev ${s(g.ms)} p=${g.pRoutine}${g.skipped ? " skipped" : ""}`);
84329
+ if (receipt.timing?.modelMs !== void 0) parts2.push(`model ${s(receipt.timing.modelMs)}`);
84330
+ if (receipt.timing?.ingestMs !== void 0) parts2.push(`ingest ${s(receipt.timing.ingestMs)}`);
84331
+ return ` in ${s(Date.now() - postStarted + prepMs)} (${parts2.join(", ")})`;
84332
+ }
84068
84333
  function addCounts(a, b) {
84069
84334
  const sum = (k) => (a[k] ?? 0) + (b[k] ?? 0);
84070
84335
  return { commitsWalked: sum("commitsWalked"), changesExtracted: sum("changesExtracted"), pairs: sum("pairs"), claims: sum("claims"), anchors: sum("anchors"), packages: sum("packages") };
84071
84336
  }
84072
84337
  async function runHistoryWalk(root, input, client, opts = {}) {
84338
+ const reader = opts.git ? null : new GitBlobReader(root);
84339
+ const readBlob = reader ? (spec) => reader.read(spec) : readBlobViaGit(opts.git ?? defaultGit, root);
84340
+ try {
84341
+ return await walk10(root, input, client, opts, readBlob);
84342
+ } finally {
84343
+ await reader?.close();
84344
+ }
84345
+ }
84346
+ async function walk10(root, input, client, opts, readBlob) {
84073
84347
  const log = opts.log ?? ((line) => console.warn(line));
84074
84348
  const now = opts.now ?? (() => /* @__PURE__ */ new Date());
84075
84349
  const sleep = opts.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
@@ -84124,6 +84398,7 @@ async function runHistoryWalk(root, input, client, opts = {}) {
84124
84398
  log(`[errata] history ${label} skipped (${u.skip})`);
84125
84399
  continue;
84126
84400
  }
84401
+ const prepStarted = Date.now();
84127
84402
  const bundle = await buildUnitBundle(root, u, {
84128
84403
  ignorePatterns: ignoreLines,
84129
84404
  narrative: narrative(u, u.isMerge ? group : void 0)
@@ -84145,7 +84420,7 @@ async function runHistoryWalk(root, input, client, opts = {}) {
84145
84420
  ...opts.frame ? { frame: opts.frame } : {},
84146
84421
  ...dryRun ? { dryRun: true } : {}
84147
84422
  };
84148
- const spine = await spineForUnit(git3, root, u);
84423
+ const spine = await spineForUnit(git3, root, u, readBlob);
84149
84424
  if (spine) body2.spine = spine;
84150
84425
  const anchors = anchorsFor(bundle.pathTokens, ignoreLines, projectId, opts.anchors);
84151
84426
  if (anchors) {
@@ -84161,6 +84436,7 @@ async function runHistoryWalk(root, input, client, opts = {}) {
84161
84436
  await reportHistoryStage(client, profile, log);
84162
84437
  return { profile, processed, state: "refused" };
84163
84438
  }
84439
+ const prepMs = Date.now() - prepStarted;
84164
84440
  const started2 = Date.now();
84165
84441
  let outcome = null;
84166
84442
  let attempt = 0;
@@ -84206,7 +84482,7 @@ async function runHistoryWalk(root, input, client, opts = {}) {
84206
84482
  }
84207
84483
  const packages = new Set(outcome.result.nodes.filter((n) => n.decision !== "rejected" && n.canonicalId.startsWith("pkg:")).map((n) => n.canonicalId)).size;
84208
84484
  counts = addCounts(counts, { commitsWalked: 1, changesExtracted: 1, pairs: outcome.receipt.keptPairs, claims: outcome.receipt.claims, anchors: outcome.receipt.anchors, packages });
84209
- log(`[errata] history ${label}: ${outcome.result.nodes.length} nodes, ${outcome.result.edges.length} edges (${outcome.receipt.keptPairs} pairs, ${outcome.receipt.claims} claims)`);
84485
+ log(`[errata] history ${label}: ${outcome.result.nodes.length} nodes, ${outcome.result.edges.length} edges (${outcome.receipt.keptPairs} pairs, ${outcome.receipt.claims} claims)${unitTiming(started2, prepMs, outcome.receipt)}`);
84210
84486
  break;
84211
84487
  }
84212
84488
  case "replayed": {
@@ -84217,7 +84493,7 @@ async function runHistoryWalk(root, input, client, opts = {}) {
84217
84493
  case "empty":
84218
84494
  case "dry-run":
84219
84495
  counts = addCounts(counts, { commitsWalked: 1, changesExtracted: outcome.status === "dry-run" ? 1 : 0, pairs: outcome.receipt.keptPairs, claims: outcome.receipt.claims });
84220
- log(`[errata] history ${label}: ${outcome.status} (${outcome.receipt.keptPairs} pairs, dropped ${outcome.receipt.droppedByEvidence} by evidence, ${outcome.receipt.droppedByAnchor} by anchor)`);
84496
+ log(`[errata] history ${label}: ${outcome.status} (${outcome.receipt.keptPairs} pairs, dropped ${outcome.receipt.droppedByEvidence} by evidence, ${outcome.receipt.droppedByAnchor} by anchor)${unitTiming(started2, prepMs, outcome.receipt)}`);
84221
84497
  break;
84222
84498
  case "refused":
84223
84499
  profile = stage(profile, { state: "refused", lastError: `unit ${label} refused: ${outcome.reason}` }, now);
@@ -84299,12 +84575,12 @@ function applyStopRequest(profile, now = () => /* @__PURE__ */ new Date()) {
84299
84575
  }
84300
84576
  };
84301
84577
  }
84302
- async function startHistoryStage(root, input, client, walk10 = {}) {
84578
+ async function startHistoryStage(root, input, client, walk11 = {}) {
84303
84579
  let profile = input;
84304
84580
  if (!profile.projectId) return { started: false, reason: "no-project", profile };
84305
84581
  const response = await reportHistoryStage(client, profile);
84306
- if (response?.consents && walk10.applyConsents) {
84307
- profile = walk10.applyConsents(profile, response.consents);
84582
+ if (response?.consents && walk11.applyConsents) {
84583
+ profile = walk11.applyConsents(profile, response.consents);
84308
84584
  }
84309
84585
  const consents = historyConsents(profile);
84310
84586
  if (response?.stopRequested && profile.historyStage && profile.historyStage.state !== "refused") {
@@ -84313,7 +84589,7 @@ async function startHistoryStage(root, input, client, walk10 = {}) {
84313
84589
  return { started: false, reason: "stopped", profile: stopped };
84314
84590
  }
84315
84591
  if (!consents.mineHistory) return { started: false, reason: "consent-pending", profile };
84316
- const outcome = await runHistoryWalk(root, profile, client, walk10);
84592
+ const outcome = await runHistoryWalk(root, profile, client, walk11);
84317
84593
  return { started: true, profile: outcome.profile };
84318
84594
  }
84319
84595
 
@@ -85215,6 +85491,8 @@ async function startMultiDaemon(opts = {}) {
85215
85491
  let serverPending = null;
85216
85492
  const boundaryListeners = [];
85217
85493
  let boundaryFlushing = false;
85494
+ let pushRequested = false;
85495
+ let shippedFingerprint = "";
85218
85496
  let settleRunning = false;
85219
85497
  let boundaryFlushStartedAt = 0;
85220
85498
  let boundaryFlushStage = "";
@@ -85284,6 +85562,36 @@ async function startMultiDaemon(opts = {}) {
85284
85562
  boundaryFlushStage = "";
85285
85563
  endFlushPass?.();
85286
85564
  endFlushPass = null;
85565
+ if (pushRequested) {
85566
+ pushRequested = false;
85567
+ setImmediate(() => {
85568
+ void daemon.liveShip().catch(() => {
85569
+ });
85570
+ });
85571
+ }
85572
+ };
85573
+ const instanceFingerprint = () => {
85574
+ const cfg2 = loadConfig();
85575
+ const parts2 = [];
85576
+ for (const r of records) {
85577
+ if (!effectiveConsent(cfg2, r.engine.profile).sync) continue;
85578
+ parts2.push(`${r.id}=${r.engine.store.labelFingerprint?.(INSTANCE_LABELS) ?? `?${Date.now()}`}`);
85579
+ }
85580
+ if (parts2.length === 0) return "";
85581
+ parts2.push(`shared=${sharedStore.labelFingerprint?.(["RootCause"]) ?? `?${Date.now()}`}`);
85582
+ return parts2.join("|");
85583
+ };
85584
+ const runSettle = async () => {
85585
+ if (settleRunning) return;
85586
+ settleRunning = true;
85587
+ try {
85588
+ for (const r of records) {
85589
+ const donePass = markPass(`settle:${r.engine.profile.name}`);
85590
+ await r.engine.embedSettled().finally(donePass);
85591
+ }
85592
+ } finally {
85593
+ settleRunning = false;
85594
+ }
85287
85595
  };
85288
85596
  const fireBoundary = () => {
85289
85597
  for (const l of boundaryListeners) {
@@ -86565,22 +86873,17 @@ async function startMultiDaemon(opts = {}) {
86565
86873
  return { written, pruned: pruned2 };
86566
86874
  },
86567
86875
  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
- }
86876
+ await runSettle();
86877
+ if (boundaryFlushing) {
86878
+ pushRequested = true;
86879
+ return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
86578
86880
  }
86579
- if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
86580
86881
  beginFlush("instances");
86581
86882
  try {
86582
86883
  flushStage("instances");
86884
+ const startedFrom = instanceFingerprint();
86583
86885
  const inst = await daemon.syncInstancesPublic();
86886
+ shippedFingerprint = startedFrom;
86584
86887
  flushStage("skills");
86585
86888
  const skills = await daemon.syncSkillsAll();
86586
86889
  return {
@@ -86593,6 +86896,36 @@ async function startMultiDaemon(opts = {}) {
86593
86896
  endFlush();
86594
86897
  }
86595
86898
  },
86899
+ pendingLiveFingerprint() {
86900
+ const fp = instanceFingerprint();
86901
+ return fp && fp !== shippedFingerprint ? fp : null;
86902
+ },
86903
+ async liveShip() {
86904
+ if (!liveSyncEnabled()) return { uploaded: 0, skipped: "disabled" };
86905
+ if (daemon.pendingLiveFingerprint() === null) return { uploaded: 0, skipped: "unchanged" };
86906
+ if (boundaryFlushing) {
86907
+ pushRequested = true;
86908
+ return { uploaded: 0, skipped: "in-flight" };
86909
+ }
86910
+ await runSettle();
86911
+ if (boundaryFlushing) {
86912
+ pushRequested = true;
86913
+ return { uploaded: 0, skipped: "in-flight" };
86914
+ }
86915
+ beginFlush("live");
86916
+ try {
86917
+ const startedFrom = instanceFingerprint();
86918
+ const inst = await daemon.syncInstancesPublic();
86919
+ shippedFingerprint = startedFrom;
86920
+ return {
86921
+ uploaded: inst.uploaded,
86922
+ ...inst.skipped ? { skipped: inst.skipped } : {},
86923
+ ...inst.errors ? { errors: inst.errors } : {}
86924
+ };
86925
+ } finally {
86926
+ endFlush();
86927
+ }
86928
+ },
86596
86929
  onSessionBoundary(cb) {
86597
86930
  boundaryListeners.push(cb);
86598
86931
  },
@@ -86953,9 +87286,9 @@ function deriveAuthBase(verificationUri) {
86953
87286
  }
86954
87287
  function openInBrowser(url2) {
86955
87288
  try {
86956
- void import("node:child_process").then(({ spawn: spawn4 }) => {
87289
+ void import("node:child_process").then(({ spawn: spawn5 }) => {
86957
87290
  const [cmd2, args2] = process.platform === "win32" ? ["cmd", ["/c", "start", "", url2]] : process.platform === "darwin" ? ["open", [url2]] : ["xdg-open", [url2]];
86958
- spawn4(cmd2, args2, { detached: true, stdio: "ignore" }).unref();
87291
+ spawn5(cmd2, args2, { detached: true, stdio: "ignore" }).unref();
86959
87292
  });
86960
87293
  } catch {
86961
87294
  }
@@ -89598,7 +89931,7 @@ function spawnDaemonDetached(options = {}) {
89598
89931
  out2 = openSync2(logPath, "a");
89599
89932
  } catch {
89600
89933
  }
89601
- spawn3(cmd2, args2, {
89934
+ spawn4(cmd2, args2, {
89602
89935
  detached: true,
89603
89936
  stdio: ["ignore", out2, out2],
89604
89937
  windowsHide: true,
@@ -90059,8 +90392,53 @@ async function cmdDash(args2) {
90059
90392
  void flushed.finally(() => triggerConsolidation(force));
90060
90393
  };
90061
90394
  handle2.onSessionBoundary(() => settleThenConsolidate("session-end", true));
90395
+ const liveCfg = liveSyncConfig();
90396
+ let liveTimer = null;
90397
+ let liveDirtySince = 0;
90398
+ let liveSeenFingerprint = "";
90399
+ let liveFailures = 0;
90400
+ let liveBackoffUntil = 0;
90401
+ const fireLiveShip = () => {
90402
+ liveTimer = null;
90403
+ void handle2.liveShip().then((r) => {
90404
+ if (r.skipped === "in-flight") return;
90405
+ liveDirtySince = 0;
90406
+ if (r.errors && r.errors.length > 0) {
90407
+ liveFailures++;
90408
+ const pause = liveBackoffMs(liveFailures);
90409
+ liveBackoffUntil = Date.now() + pause;
90410
+ console.warn(
90411
+ `[live] push had ${r.errors.length} error(s); pausing the live push ${Math.round(pause / 6e4)}m (quiescence / session-end still flush)`
90412
+ );
90413
+ return;
90414
+ }
90415
+ liveFailures = 0;
90416
+ if (r.uploaded > 0) console.log(`[live] uploaded ${r.uploaded} node(s)/edge(s)`);
90417
+ }).catch(() => {
90418
+ });
90419
+ };
90420
+ const probeLivePush = () => {
90421
+ if (!liveCfg.enabled || Date.now() < liveBackoffUntil) return;
90422
+ let fp;
90423
+ try {
90424
+ fp = handle2.pendingLiveFingerprint();
90425
+ } catch {
90426
+ return;
90427
+ }
90428
+ if (fp === null) {
90429
+ liveSeenFingerprint = "";
90430
+ return;
90431
+ }
90432
+ if (fp === liveSeenFingerprint) return;
90433
+ liveSeenFingerprint = fp;
90434
+ const now = Date.now();
90435
+ if (!liveDirtySince) liveDirtySince = now;
90436
+ if (liveTimer) clearTimeout(liveTimer);
90437
+ liveTimer = setTimeout(fireLiveShip, liveShipDelay(now, liveDirtySince, liveCfg));
90438
+ };
90062
90439
  let ticking = false;
90063
90440
  const interval = setInterval(() => {
90441
+ probeLivePush();
90064
90442
  if (ticking) return;
90065
90443
  ticking = true;
90066
90444
  void handle2.tickAll().then((m) => {
@@ -90301,6 +90679,7 @@ shutting down\u2026 (${signal})`);
90301
90679
  clearInterval(interval);
90302
90680
  clearInterval(nightlyInterval);
90303
90681
  if (quiesceTimer) clearTimeout(quiesceTimer);
90682
+ if (liveTimer) clearTimeout(liveTimer);
90304
90683
  await handle2.stop();
90305
90684
  process.exit(0);
90306
90685
  };
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.2674",
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