@inerrata-corporation/errata 2.0.6-dev.2654 → 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.
- package/consolidate-worker.mjs +66 -11
- package/errata.mjs +223 -24
- package/package.json +1 -1
- package/pass-worker.mjs +23 -2
package/consolidate-worker.mjs
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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.
|
|
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,13 +78536,14 @@ function createWatchBreaker(deps) {
|
|
|
78515
78536
|
}
|
|
78516
78537
|
|
|
78517
78538
|
// src/engine.ts
|
|
78518
|
-
var DAEMON_VERSION = true ? "2.0.6-dev.
|
|
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;
|
|
78522
78543
|
var REINDEX_DEBOUNCE_MS = 200;
|
|
78523
78544
|
var IDENTITY_AUDIT_MAX_BYTES = 16 * 1024 * 1024;
|
|
78524
78545
|
var TURN_REPLAY_LOOKBACK_MS = 7 * 24 * 60 * 6e4;
|
|
78546
|
+
var INDUCE_DISABLED_RECHECK_MS = 6 * 60 * 6e4;
|
|
78525
78547
|
var CODEX_SWEEP_LOOKBACK_MS = 10 * 6e4;
|
|
78526
78548
|
function appendIdentityAudit(path2, record2, line) {
|
|
78527
78549
|
if (!record2.accepted && record2.score <= 0) return;
|
|
@@ -80434,11 +80456,16 @@ B: ${b}`,
|
|
|
80434
80456
|
}
|
|
80435
80457
|
});
|
|
80436
80458
|
}
|
|
80459
|
+
let induceDisabledUntil = 0;
|
|
80437
80460
|
const pullSkills = async () => {
|
|
80438
|
-
if ((opts.syncConsent ?? consentNow().sync) === true) {
|
|
80461
|
+
if ((opts.syncConsent ?? consentNow().sync) === true && Date.now() >= induceDisabledUntil) {
|
|
80439
80462
|
for (const req of communityInductionRequests(store)) {
|
|
80440
80463
|
try {
|
|
80441
80464
|
const r = await cloud.induceSkill({ members: req.members, context: req.context });
|
|
80465
|
+
if (r.status === "disabled") {
|
|
80466
|
+
induceDisabledUntil = Date.now() + INDUCE_DISABLED_RECHECK_MS;
|
|
80467
|
+
break;
|
|
80468
|
+
}
|
|
80442
80469
|
if (r.status === "induced" || r.status === "refreshed") {
|
|
80443
80470
|
console.log(`[errata] skill ${r.status} for community ${req.communityId}: ${r.title ?? r.skillId}`);
|
|
80444
80471
|
}
|
|
@@ -81230,10 +81257,16 @@ function internalPackageNames(workspaceRoot) {
|
|
|
81230
81257
|
return names;
|
|
81231
81258
|
}
|
|
81232
81259
|
var cache = /* @__PURE__ */ new Map();
|
|
81260
|
+
var cacheKeys = /* @__PURE__ */ new Map();
|
|
81233
81261
|
function publicSymbolIndex(store, workspaceRoot, deps = {}) {
|
|
81234
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")}`;
|
|
81235
81265
|
const cached2 = cache.get(workspaceRoot);
|
|
81236
|
-
if (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
|
+
}
|
|
81237
81270
|
const readHead = deps.readHead ?? ((absPath) => {
|
|
81238
81271
|
try {
|
|
81239
81272
|
return readFileSync28(absPath, "utf8").slice(0, HEAD_BYTES);
|
|
@@ -81270,6 +81303,8 @@ function publicSymbolIndex(store, workspaceRoot, deps = {}) {
|
|
|
81270
81303
|
}
|
|
81271
81304
|
const index = { symbols, builtAt: now };
|
|
81272
81305
|
cache.set(workspaceRoot, index);
|
|
81306
|
+
if (key !== void 0) cacheKeys.set(workspaceRoot, key);
|
|
81307
|
+
else cacheKeys.delete(workspaceRoot);
|
|
81273
81308
|
return index;
|
|
81274
81309
|
}
|
|
81275
81310
|
var PRESERVED_SYMBOLS_CAP = 8;
|
|
@@ -81404,7 +81439,16 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
|
|
|
81404
81439
|
...profile.repoLocatorAliases ? { aliases: profile.repoLocatorAliases } : {},
|
|
81405
81440
|
...profile.aka ? { aka: profile.aka } : {}
|
|
81406
81441
|
};
|
|
81407
|
-
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
|
+
};
|
|
81408
81452
|
const shareable = (n) => {
|
|
81409
81453
|
const preserved = symbolIndex ? preservedSymbolsFor(n.description, symbolIndex) : [];
|
|
81410
81454
|
const fileRefs = generalizeFileRefs(n.description, fileLexicon, level);
|
|
@@ -81529,7 +81573,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
|
|
|
81529
81573
|
for (const sourceId of anchorSources) {
|
|
81530
81574
|
const targets = [];
|
|
81531
81575
|
for (const e of store.outEdges(sourceId, [...ANCHOR_EDGES])) {
|
|
81532
|
-
const t =
|
|
81576
|
+
const t = nodeOf(e.to);
|
|
81533
81577
|
if (!t || !ANCHOR_TARGET_LABELS.includes(t.label)) continue;
|
|
81534
81578
|
if (e.type === "OCCURS_IN" && t.label !== "Language") continue;
|
|
81535
81579
|
if (e.type === "DEPENDS_ON" && t.label !== "Package") continue;
|
|
@@ -81552,7 +81596,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
|
|
|
81552
81596
|
}
|
|
81553
81597
|
}
|
|
81554
81598
|
const dg = digest(targets.map(({ e, wireId }) => `${e.type}>${wireId}`).sort());
|
|
81555
|
-
const sourceNode =
|
|
81599
|
+
const sourceNode = nodeOf(sourceId);
|
|
81556
81600
|
if (sourceNode?.attrs["anchorsContributedDigest"] === dg) continue;
|
|
81557
81601
|
anchorDigests[sourceId] = dg;
|
|
81558
81602
|
const wireFrom = seen.has(sourceId) || !sourceNode ? sourceId : contributedWireId(sourceNode);
|
|
@@ -81582,12 +81626,12 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
|
|
|
81582
81626
|
if (project) {
|
|
81583
81627
|
const now = Date.now();
|
|
81584
81628
|
for (const sourceId of anchorSources) {
|
|
81585
|
-
const source =
|
|
81629
|
+
const source = nodeOf(sourceId);
|
|
81586
81630
|
if (!source) continue;
|
|
81587
81631
|
const wireFrom = seen.has(sourceId) ? sourceId : contributedWireId(source);
|
|
81588
81632
|
const planned = [];
|
|
81589
81633
|
for (const e of store.outEdges(sourceId, ["ANCHORED_AT"])) {
|
|
81590
|
-
const target =
|
|
81634
|
+
const target = nodeOf(e.to);
|
|
81591
81635
|
if (!target) continue;
|
|
81592
81636
|
if (!isAnchorLabelKind(target.label)) continue;
|
|
81593
81637
|
const isFile = target.label === "File";
|
|
@@ -81664,7 +81708,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
|
|
|
81664
81708
|
const outs = [...dedup.values()].map((e) => ({ e, wireTo: semanticEndpoints.get(e.to) })).filter((x) => x.wireTo != null);
|
|
81665
81709
|
if (outs.length === 0) continue;
|
|
81666
81710
|
const dg = digest(outs.map(({ e, wireTo }) => `${e.type}>${wireTo}`).sort());
|
|
81667
|
-
const owner =
|
|
81711
|
+
const owner = nodeOf(sourceId);
|
|
81668
81712
|
const digestSkip = owner?.attrs["semanticEdgesContributedDigest"] === dg;
|
|
81669
81713
|
if (!digestSkip) semanticEdgeDigests[sourceId] = dg;
|
|
81670
81714
|
for (const { e, wireTo } of outs) {
|
|
@@ -81721,7 +81765,7 @@ function buildInstanceIngest(store, profile, daemonVersion, ignorePatterns = [],
|
|
|
81721
81765
|
const heldOrphans = /* @__PURE__ */ new Set();
|
|
81722
81766
|
for (const wn of nodes) {
|
|
81723
81767
|
if (wn.label !== "RootCause" && wn.label !== "Solution") continue;
|
|
81724
|
-
const src =
|
|
81768
|
+
const src = nodeOf(wn.id);
|
|
81725
81769
|
if (typeof src?.attrs["contributedAtSeq"] === "number") continue;
|
|
81726
81770
|
if (bindingEndpoints.has(wn.id)) continue;
|
|
81727
81771
|
heldOrphans.add(wn.id);
|
|
@@ -81805,8 +81849,58 @@ function buildCausalEdgeBackfill(store, profile, daemonVersion) {
|
|
|
81805
81849
|
return { ...base, payloadDigest: digest(base) };
|
|
81806
81850
|
}
|
|
81807
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
|
+
|
|
81808
81887
|
// src/drain-build.ts
|
|
81809
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
|
+
}
|
|
81810
81904
|
function buildDrainContext(store, input) {
|
|
81811
81905
|
return buildContextIngest(store, input.profile, input.daemonVersion, input.ignorePatterns, {
|
|
81812
81906
|
...input.includePackages !== void 0 ? { includePackages: input.includePackages } : {}
|
|
@@ -81814,9 +81908,10 @@ function buildDrainContext(store, input) {
|
|
|
81814
81908
|
}
|
|
81815
81909
|
function buildDrainInstances(store, sharedStore, input) {
|
|
81816
81910
|
let lexicon;
|
|
81911
|
+
let fileLexicon;
|
|
81817
81912
|
let lexiconError;
|
|
81818
81913
|
try {
|
|
81819
|
-
lexicon =
|
|
81914
|
+
({ symbols: lexicon, files: fileLexicon } = lexiconsFor(store));
|
|
81820
81915
|
} catch (err2) {
|
|
81821
81916
|
lexiconError = err2 instanceof Error ? err2.message : String(err2);
|
|
81822
81917
|
}
|
|
@@ -81829,6 +81924,7 @@ function buildDrainInstances(store, sharedStore, input) {
|
|
|
81829
81924
|
composition = c;
|
|
81830
81925
|
},
|
|
81831
81926
|
...lexicon ? { lexicon } : {},
|
|
81927
|
+
...fileLexicon ? { fileLexicon } : {},
|
|
81832
81928
|
...input.project ? { project: input.project } : {}
|
|
81833
81929
|
});
|
|
81834
81930
|
return {
|
|
@@ -85209,6 +85305,8 @@ async function startMultiDaemon(opts = {}) {
|
|
|
85209
85305
|
let serverPending = null;
|
|
85210
85306
|
const boundaryListeners = [];
|
|
85211
85307
|
let boundaryFlushing = false;
|
|
85308
|
+
let pushRequested = false;
|
|
85309
|
+
let shippedFingerprint = "";
|
|
85212
85310
|
let settleRunning = false;
|
|
85213
85311
|
let boundaryFlushStartedAt = 0;
|
|
85214
85312
|
let boundaryFlushStage = "";
|
|
@@ -85278,6 +85376,36 @@ async function startMultiDaemon(opts = {}) {
|
|
|
85278
85376
|
boundaryFlushStage = "";
|
|
85279
85377
|
endFlushPass?.();
|
|
85280
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
|
+
}
|
|
85281
85409
|
};
|
|
85282
85410
|
const fireBoundary = () => {
|
|
85283
85411
|
for (const l of boundaryListeners) {
|
|
@@ -86559,22 +86687,17 @@ async function startMultiDaemon(opts = {}) {
|
|
|
86559
86687
|
return { written, pruned: pruned2 };
|
|
86560
86688
|
},
|
|
86561
86689
|
async sessionBoundaryFlush() {
|
|
86562
|
-
|
|
86563
|
-
|
|
86564
|
-
|
|
86565
|
-
|
|
86566
|
-
const donePass = markPass(`settle:${r.engine.profile.name}`);
|
|
86567
|
-
await r.engine.embedSettled().finally(donePass);
|
|
86568
|
-
}
|
|
86569
|
-
} finally {
|
|
86570
|
-
settleRunning = false;
|
|
86571
|
-
}
|
|
86690
|
+
await runSettle();
|
|
86691
|
+
if (boundaryFlushing) {
|
|
86692
|
+
pushRequested = true;
|
|
86693
|
+
return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
|
|
86572
86694
|
}
|
|
86573
|
-
if (boundaryFlushing) return { uploaded: 0, written: 0, pruned: 0, skipped: "in-flight" };
|
|
86574
86695
|
beginFlush("instances");
|
|
86575
86696
|
try {
|
|
86576
86697
|
flushStage("instances");
|
|
86698
|
+
const startedFrom = instanceFingerprint();
|
|
86577
86699
|
const inst = await daemon.syncInstancesPublic();
|
|
86700
|
+
shippedFingerprint = startedFrom;
|
|
86578
86701
|
flushStage("skills");
|
|
86579
86702
|
const skills = await daemon.syncSkillsAll();
|
|
86580
86703
|
return {
|
|
@@ -86587,6 +86710,36 @@ async function startMultiDaemon(opts = {}) {
|
|
|
86587
86710
|
endFlush();
|
|
86588
86711
|
}
|
|
86589
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
|
+
},
|
|
86590
86743
|
onSessionBoundary(cb) {
|
|
86591
86744
|
boundaryListeners.push(cb);
|
|
86592
86745
|
},
|
|
@@ -90053,8 +90206,53 @@ async function cmdDash(args2) {
|
|
|
90053
90206
|
void flushed.finally(() => triggerConsolidation(force));
|
|
90054
90207
|
};
|
|
90055
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
|
+
};
|
|
90056
90253
|
let ticking = false;
|
|
90057
90254
|
const interval = setInterval(() => {
|
|
90255
|
+
probeLivePush();
|
|
90058
90256
|
if (ticking) return;
|
|
90059
90257
|
ticking = true;
|
|
90060
90258
|
void handle2.tickAll().then((m) => {
|
|
@@ -90295,6 +90493,7 @@ shutting down\u2026 (${signal})`);
|
|
|
90295
90493
|
clearInterval(interval);
|
|
90296
90494
|
clearInterval(nightlyInterval);
|
|
90297
90495
|
if (quiesceTimer) clearTimeout(quiesceTimer);
|
|
90496
|
+
if (liveTimer) clearTimeout(liveTimer);
|
|
90298
90497
|
await handle2.stop();
|
|
90299
90498
|
process.exit(0);
|
|
90300
90499
|
};
|
package/package.json
CHANGED
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.
|
|
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.
|
|
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
|