@inerrata-corporation/errata 2.0.2-dev.70 → 2.0.2-dev.704

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/pass-worker.mjs CHANGED
@@ -144,7 +144,14 @@ var init_castalia = __esm({
144
144
  "SOLVED_BY",
145
145
  "MITIGATES",
146
146
  "REPORTED_FAILURE",
147
- "CONTRADICTS"
147
+ "CONTRADICTS",
148
+ // Motif twin-faces (KN-twin-faces): failure-face motif → remedy-face motif
149
+ // across layers (AntiPattern/Weakness ↔ Technique/Pattern). Same
150
+ // problem→fix direction as FIXED_BY/SOLVED_BY. Minted by the nightly LLM
151
+ // twin-face pass — these are the near-identical cross-layer pairs the
152
+ // polarity gate (finding 5) correctly refuses to FUSE; the link carries
153
+ // what fusion can't.
154
+ "REMEDIED_BY"
148
155
  ];
149
156
  CONCEPTUAL_EDGES = [
150
157
  "INSTANCE_OF",
@@ -259,6 +266,10 @@ var init_castalia = __esm({
259
266
  // git topology, not signal-flow
260
267
  "AUTHORED_BY",
261
268
  // git authorship, not signal-flow
269
+ "PRODUCED",
270
+ // edit-episode provenance (Episode→symbol), not signal-flow — the family
271
+ // above was excluded together but this member slipped the net (LC-produced-pagerank);
272
+ // at 43% of all live edges it was the largest single edge population flowing rank
262
273
  "CONTRIBUTED",
263
274
  // agent attribution (Agent → knowledge), not signal-flow
264
275
  "SUPERSEDES",
@@ -274,6 +285,10 @@ var init_castalia = __esm({
274
285
  CAUSED_BY: 3,
275
286
  FIXED_BY: 3,
276
287
  SOLVED_BY: 3,
288
+ // Twin-face link (failure motif → remedy motif, KN-twin-faces) — causal-grade
289
+ // but a notch below witnessed FIXED_BY/SOLVED_BY: the pairing is an LLM
290
+ // judgment over descriptions, not an agent-witnessed resolution.
291
+ REMEDIED_BY: 2.5,
277
292
  MANIFESTS_AS: 2,
278
293
  ESCALATES_TO: 1.5,
279
294
  AFFECTS: 1.2,
@@ -447,13 +462,18 @@ function resolveCanonical(label) {
447
462
  function resolveCanonicalId(label) {
448
463
  return resolveCanonical(label)?.id;
449
464
  }
465
+ function resolveUnambiguousCanonicalId(label) {
466
+ const key = label.trim().toLowerCase();
467
+ if (AMBIGUOUS_ALIASES.has(key)) return void 0;
468
+ return resolveCanonicalId(key);
469
+ }
450
470
  function aliasesLongestFirst() {
451
471
  const out2 = [];
452
472
  for (const e of REGISTRY) for (const a of e.aliases) out2.push({ alias: a.toLowerCase(), entity: e });
453
473
  out2.sort((x, y) => y.alias.length - x.alias.length);
454
474
  return out2;
455
475
  }
456
- var REGISTRY, BY_ALIAS;
476
+ var REGISTRY, AMBIGUOUS_ALIASES, BY_ALIAS;
457
477
  var init_taxonomy = __esm({
458
478
  "../../packages/shared/src/nlp/taxonomy.ts"() {
459
479
  "use strict";
@@ -502,6 +522,13 @@ var init_taxonomy = __esm({
502
522
  { id: "concept:retry", name: "retry", category: "concept", aliases: ["retry"] },
503
523
  { id: "concept:migration", name: "migration", category: "concept", aliases: ["migration"] }
504
524
  ];
525
+ AMBIGUOUS_ALIASES = /* @__PURE__ */ new Set([
526
+ "node",
527
+ "go",
528
+ "pool",
529
+ "spring",
530
+ "ws"
531
+ ]);
505
532
  BY_ALIAS = /* @__PURE__ */ new Map();
506
533
  for (const e of REGISTRY) {
507
534
  for (const a of e.aliases) {
@@ -563,7 +590,7 @@ function lemma(token) {
563
590
  }
564
591
  return token;
565
592
  }
566
- function conceptTokens(text) {
593
+ function tokenize(text, resolve) {
567
594
  const matches = text.normalize("NFC").toLowerCase().match(TOKEN_RE) ?? [];
568
595
  const out2 = /* @__PURE__ */ new Set();
569
596
  for (const raw of matches) {
@@ -573,11 +600,14 @@ function conceptTokens(text) {
573
600
  out2.add(token);
574
601
  continue;
575
602
  }
576
- const canonical = resolveCanonicalId(token);
603
+ const canonical = resolve(token);
577
604
  out2.add(canonical ?? lemma(token));
578
605
  }
579
606
  return [...out2].sort();
580
607
  }
608
+ function retrievalTokens(text) {
609
+ return tokenize(text, resolveUnambiguousCanonicalId);
610
+ }
581
611
  var STOPWORDS, NEGATION_TOKENS, TOKEN_RE;
582
612
  var init_concept_bag = __esm({
583
613
  "../../packages/shared/src/nlp/concept-bag.ts"() {
@@ -15049,13 +15079,21 @@ var init_edge_rules = __esm({
15049
15079
  // NOT ruled here — the type pre-exists with broader extractor senses, and a
15050
15080
  // new rule on an old type would reject legitimate live flows (reject-never-flip
15051
15081
  // cuts both ways: only rule types you introduce or senses that are documented).
15052
- CONCERNS: { from: ["Problem", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool"] },
15082
+ // `Component` joined the target set with OM-agent-anchors: an agent-named
15083
+ // component ("React Router") is the same knowledge→named-unit anchor shape as
15084
+ // a Tool — the knowledge is ABOUT it, not dependent on it.
15085
+ CONCERNS: { from: ["Problem", "Solution", "RootCause", "Claim", "Pattern"], to: ["Tool", "Component"] },
15053
15086
  OPERATES_ON: { from: ["Algorithm"], to: ["DataStructure"] },
15054
15087
  INVOLVES: { from: ["Problem", "Solution"], to: ["DataStructure"] },
15055
15088
  // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
15056
15089
  IS_A: { from: ["Weakness"], to: ["Weakness"] },
15057
15090
  // ── Conceptual (v1 taxonomy.ts: instance → motif/pattern reference) ──
15058
- INSTANCE_OF: { to: ["Pattern", "Weakness", "Technique"] },
15091
+ // `AntiPattern` joined the target set 2026-08-02: it is one of the three motif
15092
+ // kinds (generalizer motifs.ts `layerOf`: pattern | technique | antipattern) and
15093
+ // the SUPPRESSED-close path mints `Problem ─INSTANCE_OF→ AntiPattern` as the
15094
+ // negative-knowledge binding ("silenced, not solved") — the original three-label
15095
+ // rule predates AntiPattern joining the motif layer and silently ate that edge.
15096
+ INSTANCE_OF: { to: ["Pattern", "AntiPattern", "Weakness", "Technique"] },
15059
15097
  IMPLEMENTS: { from: ["Solution", "Language", "Component"], to: ["Pattern", "Technique"] },
15060
15098
  MATCHES: { to: ["Pattern"] },
15061
15099
  // ── Artifact evidence (v1 taxonomy.ts artifact rows) ──
@@ -15156,13 +15194,35 @@ var init_wire = __esm({
15156
15194
  /** Canonical human-readable description (no raw paths — daemon scrubs; server rechecks). */
15157
15195
  description: external_exports.string().min(1).max(4e3),
15158
15196
  attrs: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
15197
+ /** One-way origin key of the SESSION that minted this node (`ws_…`, a
15198
+ * truncated digest — never the raw session id). Stamped onto the created
15199
+ * node as `authoringSession`, which is the independence unit for the
15200
+ * evidence channels: the session that authored a claim may not corroborate
15201
+ * or refute it, while a DIFFERENT session on the same checkout may (alyssa,
15202
+ * 2026-07-31 — the workspace key made every witness on a single-checkout
15203
+ * deployment a self-corroboration). Optional + additive: absent leaves the
15204
+ * gate fail-open for that node, exactly today's behaviour. */
15205
+ originSession: external_exports.string().min(3).max(64).optional(),
15206
+ /** Epoch ms of the ORIGINATING local node's creation — when the knowledge was
15207
+ * actually captured, as opposed to when its public twin reached the cloud.
15208
+ * Stored as `observedAt`; the network board's chronological view orders on
15209
+ * it. Without this the wire carried NO timestamp at all, so a node captured
15210
+ * days ago surfaced as "new" the moment it was generalized and published —
15211
+ * the board showed ingest order wearing a chronology's clothes. Optional +
15212
+ * additive: absent keeps ingest-time ordering for that node. */
15213
+ originCreatedAtMs: external_exports.number().int().positive().optional(),
15159
15214
  extractionSource: external_exports.enum(INGEST_EXTRACTION_SOURCES),
15160
15215
  validationSource: external_exports.enum(VALIDATION_SOURCES).optional(),
15161
15216
  /** Org-membrane (M2): the daemon's anchor tag — it owns the lockfile, so it
15162
15217
  * knows which packages are private. The door does NOT trust a `public` claim
15163
15218
  * blindly (it re-confirms on the spine); absent ⇒ unknown ⇒ fail-closed to
15164
15219
  * org-private. Accepted-but-ignored until ORG_MEMBRANE_ENABLED flips. */
15165
- anchorVisibility: external_exports.enum(["public", "private"]).optional()
15220
+ anchorVisibility: external_exports.enum(["public", "private"]).optional(),
15221
+ /** The spine-resolvable anchor id backing a `public` tag (OM-anchor-tag): a
15222
+ * Package purl or `languageCanonicalId`. The door validates THIS on the
15223
+ * public spine (falling back to `canonicalId` when absent — context stubs
15224
+ * are self-anchored). Never trusted without spine confirmation. */
15225
+ anchor: external_exports.string().min(1).max(300).optional()
15166
15226
  });
15167
15227
  RouteContextCountWireSchema = external_exports.object({
15168
15228
  confirmed: external_exports.number().int().min(0),
@@ -15323,6 +15383,51 @@ var init_review = __esm({
15323
15383
  }
15324
15384
  });
15325
15385
 
15386
+ // ../../packages/local-shared/src/anchorable.ts
15387
+ function anchorableExtensionAlternation() {
15388
+ return BY_LENGTH.map((e) => e.slice(1).replace(/[+.]/g, (c) => `\\${c}`)).join("|");
15389
+ }
15390
+ var ANCHORABLE_EXTENSIONS, BY_LENGTH;
15391
+ var init_anchorable = __esm({
15392
+ "../../packages/local-shared/src/anchorable.ts"() {
15393
+ "use strict";
15394
+ ANCHORABLE_EXTENSIONS = /* @__PURE__ */ new Set([
15395
+ // typescript provider
15396
+ ".ts",
15397
+ ".tsx",
15398
+ ".js",
15399
+ ".jsx",
15400
+ ".mjs",
15401
+ ".cjs",
15402
+ // python / go / rust / ruby / csharp providers
15403
+ ".py",
15404
+ ".go",
15405
+ ".rs",
15406
+ ".rb",
15407
+ ".cs",
15408
+ // cpp provider — every variant it parses, not just the three that were listed
15409
+ ".c",
15410
+ ".h",
15411
+ ".cpp",
15412
+ ".cc",
15413
+ ".cxx",
15414
+ ".c++",
15415
+ ".hpp",
15416
+ ".hh",
15417
+ ".hxx",
15418
+ ".h++",
15419
+ // CUDA — the cpp provider parses these too. Missed when this list was first
15420
+ // transcribed by hand; the parity test caught them on its very first run,
15421
+ // which is the argument for the test existing.
15422
+ ".cu",
15423
+ ".cuh",
15424
+ // no provider yet; the grammar ships. Inert, not wrong — see above.
15425
+ ".java"
15426
+ ]);
15427
+ BY_LENGTH = [...ANCHORABLE_EXTENSIONS].sort((a, b) => b.length - a.length);
15428
+ }
15429
+ });
15430
+
15326
15431
  // ../../packages/local-shared/src/sqlite-adapter.ts
15327
15432
  function openDatabase(path) {
15328
15433
  const db = new DatabaseSync(path);
@@ -15335,6 +15440,7 @@ function openDatabase(path) {
15335
15440
  db.exec("PRAGMA journal_mode = WAL");
15336
15441
  db.exec("PRAGMA synchronous = NORMAL");
15337
15442
  db.exec("PRAGMA busy_timeout = 5000");
15443
+ db.exec("PRAGMA journal_size_limit = 67108864");
15338
15444
  } catch {
15339
15445
  }
15340
15446
  }
@@ -15370,7 +15476,7 @@ function openDatabase(path) {
15370
15476
  return db.prepare(`PRAGMA ${key}`).get();
15371
15477
  },
15372
15478
  transaction(fn) {
15373
- db.exec("BEGIN");
15479
+ db.exec("BEGIN IMMEDIATE");
15374
15480
  try {
15375
15481
  const r = fn();
15376
15482
  db.exec("COMMIT");
@@ -15424,6 +15530,7 @@ var init_src2 = __esm({
15424
15530
  init_profile();
15425
15531
  init_daemon_wire();
15426
15532
  init_review();
15533
+ init_anchorable();
15427
15534
  init_sqlite_adapter();
15428
15535
  }
15429
15536
  });
@@ -15446,7 +15553,7 @@ function popcount(x) {
15446
15553
  }
15447
15554
  return c;
15448
15555
  }
15449
- function tokenize(text) {
15556
+ function tokenize2(text) {
15450
15557
  return text.match(/[A-Za-z_$][A-Za-z0-9_$]*|\d+|[^\s\w]/g) ?? [];
15451
15558
  }
15452
15559
  function shingle(tokens, n = SHINGLE_N) {
@@ -15475,7 +15582,7 @@ function simhashFeatures(features) {
15475
15582
  return out2;
15476
15583
  }
15477
15584
  function simhash(text) {
15478
- return simhashFeatures(shingle(tokenize(text)));
15585
+ return simhashFeatures(shingle(tokenize2(text)));
15479
15586
  }
15480
15587
  function hammingDistance(a, b) {
15481
15588
  return popcount((a ^ b) & MASK64);
@@ -15783,6 +15890,7 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15783
15890
  };
15784
15891
  }
15785
15892
  const fileHashes = /* @__PURE__ */ new Map();
15893
+ const contentMovedPaths = /* @__PURE__ */ new Set();
15786
15894
  for (const rel of [...changedRelPaths]) {
15787
15895
  let h;
15788
15896
  try {
@@ -15795,6 +15903,8 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15795
15903
  if (fnode && fnode.attrs["contentHash"] === h && !fileHasStrandedSymbol(store2, fnode.id)) {
15796
15904
  store2.updateNode(fnode.id, { lastUpdatedAt: t });
15797
15905
  changedRelPaths.delete(rel);
15906
+ } else if (!fnode || fnode.attrs["contentHash"] !== h) {
15907
+ contentMovedPaths.add(rel);
15798
15908
  }
15799
15909
  }
15800
15910
  if (changedRelPaths.size === 0) {
@@ -15929,6 +16039,7 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15929
16039
  changedRelPaths
15930
16040
  });
15931
16041
  store2.transaction(() => {
16042
+ store2.reviveReobserved([...changedRelPaths], workspaceId, t);
15932
16043
  const versionedAndFile = /* @__PURE__ */ new Set([...VERSIONED_LABELS, "File"]);
15933
16044
  const ownedLive = [];
15934
16045
  for (const n of store2.nodesByRelPath([...changedRelPaths])) {
@@ -16037,7 +16148,14 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
16037
16148
  const h = fileHashes.get(rel);
16038
16149
  if (!h) continue;
16039
16150
  const fnode = store2.getNode(fileNodeId(workspaceId, rel));
16040
- if (fnode) store2.updateNode(fnode.id, { attrs: { ...fnode.attrs, contentHash: h } });
16151
+ if (!fnode) continue;
16152
+ store2.updateNode(fnode.id, {
16153
+ attrs: {
16154
+ ...fnode.attrs,
16155
+ contentHash: h,
16156
+ ...contentMovedPaths.has(rel) ? { contentChangedAt: t } : {}
16157
+ }
16158
+ });
16041
16159
  }
16042
16160
  return {
16043
16161
  filesReindexed: changedRelPaths.size,
@@ -16608,7 +16726,7 @@ async function scan(root, current, ignores, providers, out2) {
16608
16726
  }
16609
16727
  }
16610
16728
  }
16611
- function gitListFiles(root, ignores, providers) {
16729
+ function gitListRelPaths(root, ignores) {
16612
16730
  let stdout;
16613
16731
  try {
16614
16732
  stdout = execFileSync(
@@ -16631,6 +16749,15 @@ function gitListFiles(root, ignores, providers) {
16631
16749
  if (segs.some((s) => ignores.has(s) || s.startsWith(".") && s.length > 1)) {
16632
16750
  continue;
16633
16751
  }
16752
+ out2.push(rel);
16753
+ }
16754
+ return out2;
16755
+ }
16756
+ function gitListFiles(root, ignores, providers) {
16757
+ const rels = gitListRelPaths(root, ignores);
16758
+ if (rels === null) return null;
16759
+ const out2 = [];
16760
+ for (const rel of rels) {
16634
16761
  const abs = join(root, rel);
16635
16762
  let size;
16636
16763
  try {
@@ -23970,6 +24097,7 @@ var init_csharp_treesitter = __esm({
23970
24097
  // ../../packages/indexer/src/index.ts
23971
24098
  var src_exports = {};
23972
24099
  __export(src_exports, {
24100
+ DEFAULT_IGNORES: () => DEFAULT_IGNORES,
23973
24101
  DRIFT_FORK_RATIO: () => DRIFT_FORK_RATIO,
23974
24102
  TreeSitterCSharpProvider: () => TreeSitterCSharpProvider,
23975
24103
  TreeSitterCppProvider: () => TreeSitterCppProvider,
@@ -23986,6 +24114,7 @@ __export(src_exports, {
23986
24114
  findInnermostScope: () => findInnermostScope,
23987
24115
  folderNodeId: () => folderNodeId,
23988
24116
  fromHex: () => fromHex,
24117
+ gitListRelPaths: () => gitListRelPaths,
23989
24118
  hammingDistance: () => hammingDistance,
23990
24119
  hasForkedDrift: () => hasForkedDrift,
23991
24120
  incrementalReindex: () => incrementalReindex,
@@ -23998,7 +24127,7 @@ __export(src_exports, {
23998
24127
  simhashFeatures: () => simhashFeatures,
23999
24128
  symbolNodeId: () => symbolNodeId,
24000
24129
  toHex: () => toHex,
24001
- tokenize: () => tokenize
24130
+ tokenize: () => tokenize2
24002
24131
  });
24003
24132
  function defaultProviders() {
24004
24133
  if (providersCache) return providersCache;
@@ -24051,9 +24180,32 @@ var LOCAL_RULE_OVERRIDES = {
24051
24180
  to: [...CODE_NODE_LABELS, "Symbol"]
24052
24181
  },
24053
24182
  REVEALED_BY: null,
24054
- PRODUCED: null
24183
+ PRODUCED: null,
24184
+ // FIXED_BY locally ALSO carries the fix-provenance sense: `resolveProblem`
24185
+ // attributes a closed Problem to the fixing `Episode` (PLAN_PLASTICITY §2.1)
24186
+ // alongside the cloud's Problem→Solution knowledge claim. Same shape as
24187
+ // PRODUCED/REVEALED_BY above — an Episode is code-layer and never drains, so
24188
+ // the cloud door's `to: [Solution]` rule is untouched. The `from` constraint
24189
+ // stays: the reversed (Solution)-FIXED_BY->(...) splash bug is the reason
24190
+ // this rule exists at all.
24191
+ FIXED_BY: { from: EDGE_RULES["FIXED_BY"]?.from, to: ["Solution", "Episode"] }
24055
24192
  };
24056
- var SCHEMA_VERSION = 5;
24193
+ function localEdgeViolation(fromLabel, type, toLabel) {
24194
+ if (type in LOCAL_RULE_OVERRIDES) {
24195
+ const rule = LOCAL_RULE_OVERRIDES[type];
24196
+ if (!rule) return null;
24197
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24198
+ return `${type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24199
+ }
24200
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24201
+ return `${type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24202
+ }
24203
+ return null;
24204
+ }
24205
+ const verdict = isValidEdge(fromLabel, type, toLabel);
24206
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24207
+ }
24208
+ var SCHEMA_VERSION = 6;
24057
24209
  var SCHEMA_SQL = `
24058
24210
  CREATE TABLE IF NOT EXISTS schema_version (
24059
24211
  version INTEGER PRIMARY KEY
@@ -24068,6 +24220,21 @@ CREATE TABLE IF NOT EXISTS store_meta (
24068
24220
  value TEXT NOT NULL
24069
24221
  );
24070
24222
 
24223
+ -- Durable ledger of edges the ontology gate REFUSED, keyed by edge type.
24224
+ -- Durable rather than in-memory for one specific reason: the status command runs
24225
+ -- in a SEPARATE process and opens its own store handle, so a counter living on
24226
+ -- the instance reads 0 there forever. That is exactly how a producer rejecting
24227
+ -- 100% of its output stayed invisible for 17 days. Persisting it also survives
24228
+ -- the daemon restart that would otherwise erase the evidence.
24229
+ -- Keyed by type because a systematic producer bug shows up as ONE type
24230
+ -- dominating; sample keeps the latest reason so the count is actionable.
24231
+ CREATE TABLE IF NOT EXISTS edge_rejections (
24232
+ type TEXT PRIMARY KEY,
24233
+ count INTEGER NOT NULL DEFAULT 0,
24234
+ last_at INTEGER NOT NULL,
24235
+ sample TEXT
24236
+ );
24237
+
24071
24238
  CREATE TABLE IF NOT EXISTS nodes (
24072
24239
  id TEXT PRIMARY KEY,
24073
24240
  label TEXT NOT NULL,
@@ -24330,6 +24497,13 @@ var SqliteGraphStore = class {
24330
24497
  findByLabel: this.db.prepare(
24331
24498
  "SELECT * FROM nodes WHERE label = ? AND valid_to IS NULL"
24332
24499
  ),
24500
+ // Scalar count twin of findByLabel — same live-row semantics, no row
24501
+ // hydration. Exists because status surfaces (daemon `/` + `/health`) used
24502
+ // findNodesByLabel(...).length, materializing every row's attrs JSON and
24503
+ // embedding blob per request — seconds of synchronous loop-hold per poll.
24504
+ countByLabel: this.db.prepare(
24505
+ "SELECT COUNT(*) AS n FROM nodes WHERE label = ? AND valid_to IS NULL"
24506
+ ),
24333
24507
  // ALL versions of a label — incl frozen/closed (valid_to set). Used by the
24334
24508
  // clean-reindex purge so a true wipe removes history too, not just live rows.
24335
24509
  findByLabelAll: this.db.prepare("SELECT * FROM nodes WHERE label = ?"),
@@ -24437,6 +24611,16 @@ var SqliteGraphStore = class {
24437
24611
  if (!cols.has(name2)) this.db.exec(ddl);
24438
24612
  }
24439
24613
  }
24614
+ if (from < 6) {
24615
+ this.db.exec(`
24616
+ CREATE TABLE IF NOT EXISTS edge_rejections (
24617
+ type TEXT PRIMARY KEY,
24618
+ count INTEGER NOT NULL DEFAULT 0,
24619
+ last_at INTEGER NOT NULL,
24620
+ sample TEXT
24621
+ )
24622
+ `);
24623
+ }
24440
24624
  this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
24441
24625
  });
24442
24626
  }
@@ -24518,6 +24702,7 @@ var SqliteGraphStore = class {
24518
24702
  const violation = this.edgeRuleViolation(edge);
24519
24703
  if (violation) {
24520
24704
  this.rejectedEdgeCount++;
24705
+ this.recordEdgeRejection(edge.type, violation, edge.lastSeenAt || edge.createdAt || 0);
24521
24706
  console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
24522
24707
  return;
24523
24708
  }
@@ -24542,22 +24727,51 @@ var SqliteGraphStore = class {
24542
24727
  * overlay consulted first. Returns the reason string on a documented-
24543
24728
  * forbidden combination, else null. Point lookups on the id PK — negligible
24544
24729
  * next to the insert itself. */
24545
- edgeRuleViolation(edge) {
24546
- const fromLabel = this.getNode(edge.from)?.label;
24547
- const toLabel = this.getNode(edge.to)?.label;
24548
- if (edge.type in LOCAL_RULE_OVERRIDES) {
24549
- const rule = LOCAL_RULE_OVERRIDES[edge.type];
24550
- if (!rule) return null;
24551
- if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24552
- return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24553
- }
24554
- if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24555
- return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24556
- }
24557
- return null;
24730
+ /** Upsert one refusal into the durable ledger. Best-effort: a bookkeeping
24731
+ * failure must never turn a refused edge into a thrown write. */
24732
+ recordEdgeRejection(type, reason, at) {
24733
+ try {
24734
+ this.db.prepare(
24735
+ `INSERT INTO edge_rejections (type, count, last_at, sample) VALUES (?, 1, ?, ?)
24736
+ ON CONFLICT(type) DO UPDATE SET
24737
+ count = count + 1, last_at = excluded.last_at, sample = excluded.sample`
24738
+ ).run(type, at, reason.slice(0, 200));
24739
+ } catch {
24740
+ }
24741
+ }
24742
+ /** Refusals recorded by the ontology gate, per edge type, newest activity first.
24743
+ * Durable across restarts and readable from any process (see the table note). */
24744
+ edgeRejections() {
24745
+ try {
24746
+ return this.db.prepare(
24747
+ "SELECT type, count, last_at AS lastAt, sample FROM edge_rejections ORDER BY count DESC, last_at DESC"
24748
+ ).all();
24749
+ } catch {
24750
+ return [];
24558
24751
  }
24559
- const verdict = isValidEdge(fromLabel, edge.type, toLabel);
24560
- return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24752
+ }
24753
+ /** Drop ledger entries whose last refusal predates `cutoff`. A still-misbehaving
24754
+ * producer keeps refreshing `last_at` and survives; a fixed one fades out. */
24755
+ pruneEdgeRejections(cutoff) {
24756
+ try {
24757
+ this.db.prepare("DELETE FROM edge_rejections WHERE last_at < ?").run(cutoff);
24758
+ } catch {
24759
+ }
24760
+ }
24761
+ /** Clear the ledger outright, whole or per type — operator escape hatch. */
24762
+ clearEdgeRejections(type) {
24763
+ try {
24764
+ if (type) this.db.prepare("DELETE FROM edge_rejections WHERE type = ?").run(type);
24765
+ else this.db.exec("DELETE FROM edge_rejections");
24766
+ } catch {
24767
+ }
24768
+ }
24769
+ edgeRuleViolation(edge) {
24770
+ return localEdgeViolation(
24771
+ this.getNode(edge.from)?.label,
24772
+ edge.type,
24773
+ this.getNode(edge.to)?.label
24774
+ );
24561
24775
  }
24562
24776
  updateEdge(id, patch) {
24563
24777
  this.stmts.updateEdge.run({
@@ -24583,6 +24797,13 @@ var SqliteGraphStore = class {
24583
24797
  const rows = this.stmts.findByLabel.all(label);
24584
24798
  return rows.map(rowToNode);
24585
24799
  }
24800
+ /** Live-row count for a label — `findNodesByLabel(label).length` without the
24801
+ * per-row hydration (attrs JSON + embedding blob). Status surfaces poll this
24802
+ * per project per request; the materializing form held the daemon's event
24803
+ * loop for seconds at scale. */
24804
+ countNodesByLabel(label) {
24805
+ return Number(this.stmts.countByLabel.get(label).n);
24806
+ }
24586
24807
  findAllVersionsByLabel(label) {
24587
24808
  const rows = this.stmts.findByLabelAll.all(label);
24588
24809
  return rows.map(rowToNode);
@@ -24687,6 +24908,163 @@ var SqliteGraphStore = class {
24687
24908
  const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
24688
24909
  return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
24689
24910
  }
24911
+ getMeta(key) {
24912
+ const r = this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key);
24913
+ return r?.value ?? null;
24914
+ }
24915
+ setMeta(key, value) {
24916
+ this.db.prepare(
24917
+ "INSERT INTO store_meta (key, value) VALUES (:key, :value) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
24918
+ ).run({ key, value });
24919
+ }
24920
+ dirtyNodeIdsSince(ts) {
24921
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
24922
+ return rows.map((r) => r.id);
24923
+ }
24924
+ /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
24925
+ * Closures matter as much as additions: a node that lost inflow is rank-dirty
24926
+ * while its own row never updated, so removal endpoints must seed the
24927
+ * incremental region or the stale inflow persists until the full backstop. */
24928
+ edgeEndpointsTouchedSince(ts) {
24929
+ const rows = this.db.prepare(
24930
+ `SELECT from_id, to_id FROM edges
24931
+ WHERE (valid_to IS NULL AND (created_at > :ts OR last_seen_at > :ts))
24932
+ OR (valid_to IS NOT NULL AND valid_to > :ts)`
24933
+ ).all({ ts });
24934
+ return rows.map((r) => ({ from: r.from_id, to: r.to_id }));
24935
+ }
24936
+ /** Lightweight out-edges for a SET of sources, chunked IN-lists — the region
24937
+ * assembly path for incremental PageRank (per-node outEdges() at region
24938
+ * scale re-creates the 106k-prepared-calls problem the batch scan solved). */
24939
+ outEdgesForMany(ids) {
24940
+ return this.edgesForMany(ids, "from_id");
24941
+ }
24942
+ inEdgesForMany(ids) {
24943
+ return this.edgesForMany(ids, "to_id");
24944
+ }
24945
+ /** Stored pageRank for a SET of ids (live rows only — a closed id is simply
24946
+ * absent, which is how incremental region assembly drops dead endpoints). */
24947
+ ranksForMany(ids) {
24948
+ const out2 = /* @__PURE__ */ new Map();
24949
+ const CHUNK = 400;
24950
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
24951
+ const chunk = ids.slice(i2, i2 + CHUNK);
24952
+ const placeholders = chunk.map(() => "?").join(", ");
24953
+ const rows = this.db.prepare(
24954
+ `SELECT id, page_rank FROM nodes WHERE id IN (${placeholders}) AND valid_to IS NULL`
24955
+ ).all(...chunk);
24956
+ for (const r of rows) out2.set(r.id, r.page_rank);
24957
+ }
24958
+ return out2;
24959
+ }
24960
+ /** The minimal row set the landmark sweep needs — landmark CANDIDATES
24961
+ * (Pattern/RootCause), everything currently flagged, and every
24962
+ * persistent-tier node (force-landmarks). A few thousand rows, so the
24963
+ * incremental path can refresh the GLOBAL landmark set without the 179k-row
24964
+ * scanLiveNodes materialization. */
24965
+ landmarkSweepRows() {
24966
+ const rows = this.db.prepare(
24967
+ `SELECT id, label, page_rank, is_landmark, memory_tier, extraction_source FROM nodes
24968
+ WHERE valid_to IS NULL
24969
+ AND (memory_tier = 'persistent' OR is_landmark = 1 OR label IN ('Pattern', 'RootCause'))`
24970
+ ).all();
24971
+ return rows.map((r) => ({
24972
+ id: r.id,
24973
+ label: r.label,
24974
+ pageRank: r.page_rank,
24975
+ isLandmark: r.is_landmark === 1,
24976
+ memoryTier: r.memory_tier,
24977
+ extractionSource: r.extraction_source
24978
+ }));
24979
+ }
24980
+ edgesForMany(ids, col) {
24981
+ const out2 = [];
24982
+ const CHUNK = 400;
24983
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
24984
+ const chunk = ids.slice(i2, i2 + CHUNK);
24985
+ const placeholders = chunk.map(() => "?").join(", ");
24986
+ const rows = this.db.prepare(
24987
+ `SELECT from_id, to_id, type FROM edges WHERE ${col} IN (${placeholders}) AND valid_to IS NULL`
24988
+ ).all(...chunk);
24989
+ for (const r of rows) out2.push({ from: r.from_id, to: r.to_id, type: r.type });
24990
+ }
24991
+ return out2;
24992
+ }
24993
+ checkpointWal() {
24994
+ try {
24995
+ const r = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
24996
+ return r ?? null;
24997
+ } catch {
24998
+ return null;
24999
+ }
25000
+ }
25001
+ reviveReobserved(relPaths, workspaceId, since) {
25002
+ let revived = 0;
25003
+ const CHUNK = 400;
25004
+ for (let i2 = 0; i2 < relPaths.length; i2 += CHUNK) {
25005
+ const slice = relPaths.slice(i2, i2 + CHUNK);
25006
+ if (slice.length === 0) continue;
25007
+ const r = this.db.prepare(
25008
+ `UPDATE nodes SET valid_to = NULL
25009
+ WHERE valid_to IS NOT NULL
25010
+ AND last_updated_at >= ?
25011
+ AND json_extract(attrs_json, '$.workspaceId') = ?
25012
+ AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})`
25013
+ ).run(since, workspaceId, ...slice);
25014
+ revived += Number(r.changes);
25015
+ }
25016
+ if (revived > 0) this.mutations++;
25017
+ return revived;
25018
+ }
25019
+ recentEdgeAttrCoverage(marker, attr, limit) {
25020
+ const r = this.db.prepare(
25021
+ `SELECT COUNT(*) AS total,
25022
+ COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
25023
+ FROM (SELECT attrs_json FROM edges
25024
+ WHERE valid_to IS NULL AND attrs_json LIKE ?
25025
+ ORDER BY created_at DESC LIMIT ?)`
25026
+ ).get(attr, `%${marker}%`, limit);
25027
+ return { count: Number(r.count), total: Number(r.total) };
25028
+ }
25029
+ liveNodeIds(ids) {
25030
+ const live = /* @__PURE__ */ new Set();
25031
+ const CHUNK = 900;
25032
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
25033
+ const slice = ids.slice(i2, i2 + CHUNK);
25034
+ if (slice.length === 0) continue;
25035
+ const rows = this.db.prepare(
25036
+ `SELECT id FROM nodes WHERE valid_to IS NULL AND id IN (${slice.map(() => "?").join(",")})`
25037
+ ).all(...slice);
25038
+ for (const r of rows) live.add(r.id);
25039
+ }
25040
+ return live;
25041
+ }
25042
+ /**
25043
+ * Live edges WITH their endpoint labels and ids, resolved in ONE join.
25044
+ *
25045
+ * The ontology sweep needs (id, type, fromLabel, toLabel) for every live edge.
25046
+ * Doing that as `scanLiveEdges()` + two `getNode()` calls is 2N node reads —
25047
+ * 550,000 on this store — and each one deserializes the node's embedding blob.
25048
+ * Measured: the sweep did not finish in 10 minutes. As a single join it is one
25049
+ * query over an index-covered scan. Labels only; nothing here touches embeddings.
25050
+ */
25051
+ scanLiveEdgeRows() {
25052
+ const rows = this.db.prepare(
25053
+ `SELECT e.id, e.from_id, e.to_id, e.type, a.label AS from_label, b.label AS to_label
25054
+ FROM edges e
25055
+ LEFT JOIN nodes a ON a.id = e.from_id AND a.valid_to IS NULL
25056
+ LEFT JOIN nodes b ON b.id = e.to_id AND b.valid_to IS NULL
25057
+ WHERE e.valid_to IS NULL`
25058
+ ).all();
25059
+ return rows.map((r) => ({
25060
+ id: r.id,
25061
+ from: r.from_id,
25062
+ to: r.to_id,
25063
+ type: r.type,
25064
+ fromLabel: r.from_label ?? void 0,
25065
+ toLabel: r.to_label ?? void 0
25066
+ }));
25067
+ }
24690
25068
  /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
24691
25069
  * expression index so the incremental reindex fetches only the changed files'
24692
25070
  * symbols instead of scanning every versioned node. */
@@ -24707,6 +25085,10 @@ var SqliteGraphStore = class {
24707
25085
  if (!live) return null;
24708
25086
  const version2 = live.version ?? 1;
24709
25087
  const frozenId = `${liveId}@v${version2}`;
25088
+ if (this.getNode(frozenId)) {
25089
+ this.stmts.advanceLive.run({ live_id: liveId, t });
25090
+ return frozenId;
25091
+ }
24710
25092
  this.stmts.freezeCopy.run({ frozen_id: frozenId, live_id: liveId, t });
24711
25093
  this.mergeEdge({
24712
25094
  id: `edge_superseded_${frozenId}`,
@@ -24765,6 +25147,7 @@ var CAUSAL_FAMILY = [
24765
25147
 
24766
25148
  // ../../packages/local-graph/src/justification.ts
24767
25149
  init_src();
25150
+ init_src2();
24768
25151
  function addDependency(store2, opts) {
24769
25152
  const id = digest({ from: opts.fromId, type: "DEPENDS_ON", to: opts.toId });
24770
25153
  const attrs = { relation: opts.relation };
@@ -24787,13 +25170,72 @@ function addDependency(store2, opts) {
24787
25170
  store2.mergeEdge(edge);
24788
25171
  return edge;
24789
25172
  }
24790
- function markRevisit(store2, node, reason, ts) {
25173
+ function markRevisit(store2, node, reason, ts, answeredWhen) {
24791
25174
  const fresh = store2.getNode(node.id) ?? node;
24792
25175
  store2.updateNode(node.id, {
24793
- attrs: { ...fresh.attrs, revisit: true, revisitReason: reason, revisitSinceTs: ts },
25176
+ attrs: {
25177
+ ...fresh.attrs,
25178
+ revisit: true,
25179
+ revisitReason: reason,
25180
+ revisitSinceTs: ts,
25181
+ revisitAnsweredWhen: answeredWhen
25182
+ },
24794
25183
  lastUpdatedAt: ts
24795
25184
  });
24796
25185
  }
25186
+ var LEGACY_AUTO_CLOSE_ASK = "auto-closed off a pre-provenance anchor";
25187
+ var KNOWN_CONDITIONS = {
25188
+ parentProblemOpen: true,
25189
+ dependentStale: true
25190
+ };
25191
+ function isRevisitCondition(v) {
25192
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(KNOWN_CONDITIONS, v);
25193
+ }
25194
+ function conditionOf(node) {
25195
+ const explicit = node.attrs["revisitAnsweredWhen"];
25196
+ if (isRevisitCondition(explicit)) return explicit;
25197
+ if (String(node.attrs["revisitReason"] ?? "").startsWith(LEGACY_AUTO_CLOSE_ASK)) {
25198
+ return "parentProblemOpen";
25199
+ }
25200
+ return void 0;
25201
+ }
25202
+ function clearAnsweredRevisits(store2, ts) {
25203
+ const report = { cleared: 0, standing: 0 };
25204
+ for (const label of SEMANTIC_NODE_LABELS) {
25205
+ for (const node of store2.findNodesByLabel(label)) {
25206
+ if (node.attrs["revisit"] !== true) continue;
25207
+ const condition = conditionOf(node);
25208
+ if (condition === void 0 || !isRevisitAnswered(store2, node, condition)) {
25209
+ report.standing++;
25210
+ continue;
25211
+ }
25212
+ clearRevisit(store2, node.id, ts);
25213
+ report.cleared++;
25214
+ }
25215
+ }
25216
+ return report;
25217
+ }
25218
+ function isRevisitAnswered(store2, node, condition) {
25219
+ switch (condition) {
25220
+ case "parentProblemOpen": {
25221
+ const parents = store2.inEdges(node.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.from)).filter((n) => n !== null && n.validTo == null);
25222
+ return parents.every((p) => p.attrs["resolvedAt"] == null);
25223
+ }
25224
+ case "dependentStale": {
25225
+ const since = Number(node.attrs["revisitSinceTs"] ?? 0);
25226
+ return since > 0 && node.lastUpdatedAt > since;
25227
+ }
25228
+ }
25229
+ }
25230
+ function clearRevisit(store2, nodeId, ts) {
25231
+ const n = store2.getNode(nodeId);
25232
+ if (!n) return;
25233
+ const attrs = { ...n.attrs };
25234
+ delete attrs["revisit"];
25235
+ delete attrs["revisitReason"];
25236
+ delete attrs["revisitSinceTs"];
25237
+ store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
25238
+ }
24797
25239
  function listNeedsRevisit(store2, asOf) {
24798
25240
  return store2.nodesAsOf(asOf).filter((n) => n.attrs["revisit"] === true).map((n) => ({
24799
25241
  id: n.id,
@@ -24926,7 +25368,6 @@ function markBoth(store2, a, b, patch, ts) {
24926
25368
  // ../../packages/local-graph/src/design-problem.ts
24927
25369
  init_src();
24928
25370
  init_src();
24929
- init_src2();
24930
25371
 
24931
25372
  // ../../packages/local-graph/src/problem-package-link.ts
24932
25373
  init_src();
@@ -25112,42 +25553,102 @@ function backfillProblemContext(store2, ts) {
25112
25553
  }
25113
25554
 
25114
25555
  // ../../packages/local-graph/src/design-problem.ts
25115
- function buildNode(id, label, description, ts, attrs) {
25116
- return {
25117
- id,
25118
- label,
25119
- description,
25120
- extractionConfidence: 0.4,
25121
- // provisional: below the error-path's 0.6+
25122
- extractionSource: "agent-observed",
25123
- embedding: [],
25124
- cumulativeSurprise: 0,
25125
- peakSurprise: 0,
25126
- cumulativeHits: 1,
25127
- lastUpdatedAt: ts,
25128
- createdAt: ts,
25129
- memoryTier: "short-term",
25130
- pageRank: 0,
25131
- isLandmark: false,
25132
- community: null,
25133
- stability: "unstable",
25134
- attrs
25135
- };
25556
+ init_src2();
25557
+
25558
+ // ../../packages/local-graph/src/problem-dedup.ts
25559
+ init_src();
25560
+ init_src();
25561
+ var REDIRECT_EDGES = [
25562
+ "CAUSED_BY",
25563
+ "SOLVED_BY",
25564
+ "FIXED_BY",
25565
+ "ANCHORED_AT",
25566
+ "MANIFESTED_IN",
25567
+ "EVIDENCED_BY"
25568
+ ];
25569
+ function corroborations(n) {
25570
+ return Number(n.attrs["corroborations"] ?? 0);
25136
25571
  }
25137
- function mergeEdge(store2, from, to, type, ts) {
25138
- store2.mergeEdge({
25139
- id: `edge_${digest({ from, type, to })}`.slice(0, 24),
25140
- from,
25141
- to,
25142
- type,
25143
- confidence: 0.4,
25144
- extractionSource: "agent-observed",
25145
- createdAt: ts,
25146
- lastSeenAt: ts,
25147
- navSuccesses: 0,
25148
- navFailures: 0,
25149
- attrs: { provisional: true }
25572
+ function overlap(a, b) {
25573
+ if (a.size === 0 || b.size === 0) return 0;
25574
+ let inter = 0;
25575
+ for (const x of a) if (b.has(x)) inter++;
25576
+ return inter / Math.min(a.size, b.size);
25577
+ }
25578
+ function mergeDuplicateProblems(store2, opts) {
25579
+ const minOverlap = opts.minOverlap ?? 0.85;
25580
+ const minTokens = opts.minTokens ?? 4;
25581
+ const report = { clusters: 0, merged: 0 };
25582
+ const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
25583
+ const tokens = /* @__PURE__ */ new Map();
25584
+ for (const p of open) tokens.set(p.id, new Set(retrievalTokens(p.description)));
25585
+ const consumed = /* @__PURE__ */ new Set();
25586
+ store2.transaction(() => {
25587
+ for (let i2 = 0; i2 < open.length; i2++) {
25588
+ const a = open[i2];
25589
+ if (consumed.has(a.id)) continue;
25590
+ const cluster = [a];
25591
+ const ta = tokens.get(a.id);
25592
+ for (let j = i2 + 1; j < open.length; j++) {
25593
+ const b = open[j];
25594
+ if (consumed.has(b.id)) continue;
25595
+ const tb = tokens.get(b.id);
25596
+ if (Math.min(ta.size, tb.size) < minTokens) continue;
25597
+ if (isConstraintProblem(a) !== isConstraintProblem(b)) continue;
25598
+ if (overlap(ta, tb) >= minOverlap) {
25599
+ cluster.push(b);
25600
+ consumed.add(b.id);
25601
+ }
25602
+ }
25603
+ if (cluster.length < 2) continue;
25604
+ report.clusters++;
25605
+ cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
25606
+ const survivor = cluster[0];
25607
+ for (const dup of cluster.slice(1)) {
25608
+ foldDuplicateProblem(store2, dup, survivor, opts.ts);
25609
+ report.merged++;
25610
+ }
25611
+ }
25150
25612
  });
25613
+ return report;
25614
+ }
25615
+ function foldDuplicateProblem(store2, dup, survivor, ts) {
25616
+ const surv = store2.getNode(survivor.id);
25617
+ if (!surv) return;
25618
+ const sources = new Set(surv.attrs["sources"] ?? []);
25619
+ for (const s of dup.attrs["sources"] ?? []) sources.add(s);
25620
+ store2.updateNode(survivor.id, {
25621
+ attrs: {
25622
+ ...surv.attrs,
25623
+ sources: [...sources],
25624
+ corroborations: corroborations(surv) + corroborations(dup) + 1
25625
+ },
25626
+ cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
25627
+ lastUpdatedAt: ts
25628
+ });
25629
+ for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
25630
+ if (e.to === survivor.id) continue;
25631
+ const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
25632
+ if (store2.getEdge(id)) continue;
25633
+ const redirected = {
25634
+ ...e,
25635
+ id,
25636
+ from: survivor.id,
25637
+ createdAt: ts,
25638
+ lastSeenAt: ts
25639
+ };
25640
+ store2.mergeEdge(redirected);
25641
+ }
25642
+ store2.updateNode(dup.id, {
25643
+ attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
25644
+ lastUpdatedAt: ts
25645
+ });
25646
+ store2.closeNode(dup.id, ts);
25647
+ }
25648
+
25649
+ // ../../packages/local-graph/src/design-problem.ts
25650
+ function isConstraintProblem(node) {
25651
+ return node.attrs["kind"] === "constraint";
25151
25652
  }
25152
25653
  var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
25153
25654
  "Solution",
@@ -25156,6 +25657,9 @@ var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
25156
25657
  "Technique",
25157
25658
  "AntiPattern"
25158
25659
  ]);
25660
+ function isAutoMinted(n) {
25661
+ return n.label === "Solution" && String(n.description ?? "").startsWith(AUTO_MINT_PREFIX);
25662
+ }
25159
25663
  function priorsForFile(store2, relPath) {
25160
25664
  const want = relPath.trim().replace(/^\.?\//, "");
25161
25665
  let file2 = null;
@@ -25167,35 +25671,53 @@ function priorsForFile(store2, relPath) {
25167
25671
  }
25168
25672
  if (!file2) return null;
25169
25673
  const openProblems = [];
25674
+ const constraints = [];
25675
+ const resolvedProblems = [];
25170
25676
  const seenProblem = /* @__PURE__ */ new Set();
25171
25677
  const related = /* @__PURE__ */ new Map();
25172
25678
  for (const e of store2.inEdges(file2.id, ["ANCHORED_AT"])) {
25173
25679
  const n = store2.getNode(e.from);
25174
25680
  if (!n) continue;
25175
25681
  if (n.label === "Problem") {
25176
- if (!n.attrs["resolvedAt"] && !seenProblem.has(n.id)) {
25177
- seenProblem.add(n.id);
25178
- openProblems.push(n);
25179
- }
25180
- } else if (CITABLE_PRIOR_LABELS.has(n.label)) {
25682
+ if (seenProblem.has(n.id)) continue;
25683
+ seenProblem.add(n.id);
25684
+ if (!n.attrs["resolvedAt"]) {
25685
+ (isConstraintProblem(n) ? constraints : openProblems).push(n);
25686
+ } else if (n.attrs["resolvedAs"] == null && n.attrs["mergedInto"] === void 0 && e.attrs?.["anchorProvenance"] !== "legacy") {
25687
+ resolvedProblems.push(n);
25688
+ }
25689
+ } else if (CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) {
25181
25690
  related.set(n.id, n);
25182
25691
  }
25183
25692
  }
25184
25693
  const solutionsByProblem = /* @__PURE__ */ new Map();
25185
- for (const p of openProblems) {
25694
+ for (const p of [...openProblems, ...constraints, ...resolvedProblems]) {
25186
25695
  for (const e of store2.outEdges(p.id, ["SOLVED_BY", "CAUSED_BY", "INSTANCE_OF"])) {
25187
25696
  const n = store2.getNode(e.to);
25188
- if (n && CITABLE_PRIOR_LABELS.has(n.label)) related.set(n.id, n);
25189
- if (n && n.label === "Solution" && e.type === "SOLVED_BY") {
25697
+ if (n && CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) related.set(n.id, n);
25698
+ if (n && n.label === "Solution" && e.type === "SOLVED_BY" && !isAutoMinted(n)) {
25190
25699
  const list = solutionsByProblem.get(p.id) ?? [];
25191
25700
  if (!list.some((s) => s.id === n.id)) list.push(n);
25192
25701
  solutionsByProblem.set(p.id, list);
25193
25702
  }
25194
25703
  }
25195
25704
  }
25196
- if (openProblems.length === 0 && related.size === 0) return null;
25197
- openProblems.sort((a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0));
25198
- return { file: file2, openProblems, related: [...related.values()], solutionsByProblem };
25705
+ if (openProblems.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
25706
+ return null;
25707
+ const recentFirst = (a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0);
25708
+ openProblems.sort(recentFirst);
25709
+ constraints.sort(recentFirst);
25710
+ resolvedProblems.sort(
25711
+ (a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)
25712
+ );
25713
+ return {
25714
+ file: file2,
25715
+ openProblems,
25716
+ constraints,
25717
+ resolvedProblems,
25718
+ related: [...related.values()],
25719
+ solutionsByProblem
25720
+ };
25199
25721
  }
25200
25722
  function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
25201
25723
  const p = store2.getNode(problemId);
@@ -25206,18 +25728,49 @@ function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
25206
25728
  });
25207
25729
  return true;
25208
25730
  }
25209
- function resolveDesignProblems(store2, t) {
25731
+ function reopenAutoClosedProblems(store2, t) {
25732
+ const report = { reopened: 0, alreadySuspect: 0, agentDescribed: 0 };
25733
+ for (const p of store2.findNodesByLabel("Problem")) {
25734
+ if (p.attrs["resolvedAt"] == null) continue;
25735
+ if (p.attrs["resolvedAs"] != null) continue;
25736
+ if (p.attrs["mergedInto"] !== void 0) continue;
25737
+ if (p.attrs["resolutionWitnessed"] === true) {
25738
+ report.agentDescribed++;
25739
+ continue;
25740
+ }
25741
+ const sols = store2.outEdges(p.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.to)).filter((n) => n !== null);
25742
+ if (sols.length === 0) continue;
25743
+ if (!sols.every((s) => String(s.description ?? "").startsWith(AUTO_MINT_PREFIX))) {
25744
+ report.agentDescribed++;
25745
+ continue;
25746
+ }
25747
+ if (p.attrs["resolutionSuspect"] === true) report.alreadySuspect++;
25748
+ const attrs = { ...p.attrs };
25749
+ delete attrs["resolvedAt"];
25750
+ attrs["reopenedFrom"] = "auto-close";
25751
+ attrs["reopenedAt"] = t;
25752
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
25753
+ report.reopened++;
25754
+ }
25755
+ return report;
25756
+ }
25757
+ var AUTO_MINT_PREFIX = "addressed by an edit to ";
25758
+ function markFixCandidates(store2, t) {
25210
25759
  let resolved = 0;
25211
25760
  for (const p of store2.findNodesByLabel("Problem")) {
25212
25761
  if (!p.id.startsWith("dprob_")) continue;
25213
25762
  if (p.attrs["resolvedAt"]) continue;
25763
+ if (isConstraintProblem(p)) continue;
25214
25764
  let symName = "";
25215
25765
  let symRelPath;
25216
25766
  let edited = false;
25767
+ const since = Math.max(p.createdAt, Number(p.attrs["fixCandidateClearedAt"] ?? 0));
25217
25768
  for (const e of store2.outEdges(p.id, ["ANCHORED_AT"])) {
25218
25769
  if (e.attrs?.["mention"] === true) continue;
25770
+ if (e.attrs?.["anchorProvenance"] === "legacy") continue;
25219
25771
  const sym = store2.getNode(e.to);
25220
- if (sym && sym.lastUpdatedAt > p.createdAt) {
25772
+ const changedAt = sym?.label === "File" ? Number(sym.attrs["contentChangedAt"] ?? 0) : sym?.lastUpdatedAt ?? 0;
25773
+ if (sym && changedAt > since) {
25221
25774
  edited = true;
25222
25775
  symName = sym.description;
25223
25776
  symRelPath = sym.attrs["relPath"] ?? void 0;
@@ -25225,36 +25778,172 @@ function resolveDesignProblems(store2, t) {
25225
25778
  }
25226
25779
  }
25227
25780
  if (!edited) continue;
25228
- if (store2.outEdges(p.id, ["SOLVED_BY"]).length === 0) {
25229
- const solId = `dfix_${digest({ problemId: p.id, edit: t })}`.slice(0, 56);
25230
- store2.mergeNode(
25231
- buildNode(solId, "Solution", `addressed by an edit to ${symName}`, t, {
25232
- source: "convo",
25233
- provisional: false,
25234
- // AC-resolution-attrs: the resolving symbol/file as STRUCTURED data, not
25235
- // just prose — the F2 join key downstream backfills and the viz read.
25236
- resolvedSymbols: [symName],
25237
- ...symRelPath ? { resolvedRelPath: symRelPath } : {}
25238
- })
25239
- );
25240
- mergeEdge(store2, p.id, solId, "SOLVED_BY", t);
25241
- for (const ae of store2.outEdges(p.id, ["ANCHORED_AT"]))
25242
- mergeEdge(store2, solId, ae.to, "ANCHORED_AT", t);
25243
- }
25781
+ if (p.attrs["fixCandidateAt"] !== void 0) continue;
25782
+ if (store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
25783
+ const s = store2.getNode(e.to);
25784
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
25785
+ }))
25786
+ continue;
25244
25787
  store2.updateNode(p.id, {
25245
- attrs: { ...p.attrs, provisional: false, resolvedAt: t },
25788
+ attrs: {
25789
+ ...p.attrs,
25790
+ // The cue, kept as structured data so the render can name the file it is
25791
+ // asking about. Deliberately NOT `resolvedAt` — this is a question.
25792
+ fixCandidateAt: t,
25793
+ fixCandidateSymbol: symName,
25794
+ ...symRelPath ? { fixCandidateFile: symRelPath } : {},
25795
+ // Snapshot the render-ledger counter so "shown since THIS ask" is
25796
+ // measurable. `shownCount` is the node's LIFETIME count across every
25797
+ // band, so comparing it raw would retire a fresh ask on a
25798
+ // frequently-surfaced problem before anyone had seen the question once.
25799
+ fixCandidateShownBase: Number(p.attrs["shownCount"] ?? 0)
25800
+ },
25246
25801
  lastUpdatedAt: t
25247
25802
  });
25248
25803
  resolved++;
25249
25804
  }
25250
25805
  return resolved;
25251
25806
  }
25807
+ var FIX_CANDIDATE_ASK_LIMIT = 5;
25808
+ function clearSettledFixCandidates(store2, t) {
25809
+ const report = { answered: 0, ignored: 0, unsubstantiated: 0, standing: 0 };
25810
+ for (const p of store2.findNodesByLabel("Problem")) {
25811
+ if (p.attrs["fixCandidateAt"] === void 0) continue;
25812
+ const fileAnchors = store2.outEdges(p.id, ["ANCHORED_AT"]).filter((e) => e.attrs?.["mention"] !== true && e.attrs?.["anchorProvenance"] !== "legacy").map((e) => store2.getNode(e.to)).filter((n) => n !== null && n.label === "File");
25813
+ const unsubstantiated = fileAnchors.length > 0 && fileAnchors.every((f) => f.attrs["contentChangedAt"] === void 0);
25814
+ const answered = p.attrs["resolvedAt"] != null || store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
25815
+ const s = store2.getNode(e.to);
25816
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
25817
+ });
25818
+ const base = Number(
25819
+ p.attrs["fixCandidateShownBase"] ?? p.attrs["shownCount"] ?? 0
25820
+ );
25821
+ const shownSinceAsk = Number(p.attrs["shownCount"] ?? 0) - base;
25822
+ const ignored = shownSinceAsk >= FIX_CANDIDATE_ASK_LIMIT;
25823
+ if (!answered && !ignored && !unsubstantiated) {
25824
+ report.standing++;
25825
+ continue;
25826
+ }
25827
+ const attrs = { ...p.attrs };
25828
+ delete attrs["fixCandidateAt"];
25829
+ delete attrs["fixCandidateSymbol"];
25830
+ delete attrs["fixCandidateFile"];
25831
+ attrs["fixCandidateClearedAt"] = t;
25832
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
25833
+ if (answered) report.answered++;
25834
+ else if (ignored) report.ignored++;
25835
+ else report.unsubstantiated++;
25836
+ }
25837
+ return report;
25838
+ }
25839
+
25840
+ // ../../packages/local-graph/src/intent.ts
25841
+ init_src();
25252
25842
 
25253
25843
  // ../../packages/local-graph/src/anchor-backfill.ts
25254
- var AUTO_MINT_PREFIX = "addressed by an edit to ";
25844
+ init_src();
25845
+ init_src2();
25255
25846
  function isLegacyAnchor(attrs) {
25256
25847
  return attrs?.["captureTime"] === true && attrs["anchorProvenance"] === void 0;
25257
25848
  }
25849
+ var STATEMENT_PATH_RE = new RegExp(
25850
+ String.raw`(?:[\w.+-]+\/)+[\w.+-]+\.(?:${anchorableExtensionAlternation()})`,
25851
+ "g"
25852
+ );
25853
+ function repairLegacyAnchorsFromStatement(store2, ts) {
25854
+ const report = {
25855
+ repaired: [],
25856
+ retiredGuesses: 0,
25857
+ noPathNamed: 0,
25858
+ pathUnknown: 0,
25859
+ alreadyCorrect: 0
25860
+ };
25861
+ const byPath = /* @__PURE__ */ new Map();
25862
+ for (const f of store2.findNodesByLabel("File")) {
25863
+ const path = String(f.attrs["relPath"] ?? f.description ?? "");
25864
+ if (path) byPath.set(path, { id: f.id, path });
25865
+ }
25866
+ const resolvePath = (named) => {
25867
+ const exact = byPath.get(named);
25868
+ if (exact) return exact;
25869
+ const hits = [...byPath.values()].filter(
25870
+ (f) => f.path.endsWith(`/${named}`) || named.endsWith(`/${f.path}`)
25871
+ );
25872
+ return hits.length === 1 ? hits[0] : null;
25873
+ };
25874
+ for (const p of store2.findNodesByLabel("Problem")) {
25875
+ try {
25876
+ const anchors = store2.outEdges(p.id, ["ANCHORED_AT"]);
25877
+ const legacy = anchors.filter((e) => e.attrs?.["anchorProvenance"] === "legacy");
25878
+ if (legacy.length === 0) continue;
25879
+ const better = anchors.filter((e) => {
25880
+ const prov = e.attrs?.["anchorProvenance"];
25881
+ return prov === "restated" || prov === "witnessed" || prov === "edited";
25882
+ });
25883
+ if (better.length > 0) {
25884
+ const betterTargets = new Set(better.map((e) => e.to));
25885
+ let retired = 0;
25886
+ for (const e of legacy) {
25887
+ if (!betterTargets.has(e.to)) {
25888
+ store2.closeEdge(e.id, ts);
25889
+ retired++;
25890
+ }
25891
+ }
25892
+ report.retiredGuesses += retired;
25893
+ continue;
25894
+ }
25895
+ const named = [...String(p.description ?? "").matchAll(STATEMENT_PATH_RE)].map((m) => m[0]);
25896
+ if (named.length === 0) {
25897
+ report.noPathNamed++;
25898
+ continue;
25899
+ }
25900
+ const guessedPaths = legacy.map((e) => {
25901
+ const f = store2.getNode(e.to);
25902
+ return String(f?.attrs["relPath"] ?? f?.description ?? "");
25903
+ });
25904
+ if (named.some((x) => guessedPaths.some((g) => g.endsWith(x) || x.endsWith(g)))) {
25905
+ report.alreadyCorrect++;
25906
+ continue;
25907
+ }
25908
+ const target = named.map(resolvePath).find((f) => f !== null);
25909
+ if (!target) {
25910
+ report.pathUnknown++;
25911
+ continue;
25912
+ }
25913
+ store2.mergeEdge({
25914
+ id: `edge_${digest({ from: p.id, type: "ANCHORED_AT", to: target.id })}`.slice(0, 24),
25915
+ from: p.id,
25916
+ to: target.id,
25917
+ type: "ANCHORED_AT",
25918
+ // Same confidence a capture-time anchor enters at: the source is the
25919
+ // agent's own statement either way, only the moment of reading differs.
25920
+ confidence: 0.4,
25921
+ extractionSource: "agent-observed",
25922
+ createdAt: ts,
25923
+ lastSeenAt: ts,
25924
+ navSuccesses: 0,
25925
+ navFailures: 0,
25926
+ attrs: {
25927
+ anchorProvenance: "restated",
25928
+ restatedFrom: guessedPaths[0] ?? "",
25929
+ restatedAt: ts
25930
+ }
25931
+ });
25932
+ for (const e of legacy) {
25933
+ if (e.to !== target.id) store2.closeEdge(e.id, ts);
25934
+ }
25935
+ report.retiredGuesses += legacy.filter((e) => e.to !== target.id).length;
25936
+ report.repaired.push({
25937
+ problemId: p.id,
25938
+ problem: p.description,
25939
+ guessed: guessedPaths[0] ?? "",
25940
+ restated: target.path
25941
+ });
25942
+ } catch {
25943
+ }
25944
+ }
25945
+ return report;
25946
+ }
25258
25947
  function backfillLegacyAnchors(store2, ts) {
25259
25948
  const report = {
25260
25949
  demotedEdges: 0,
@@ -25294,7 +25983,11 @@ function backfillLegacyAnchors(store2, ts) {
25294
25983
  store2,
25295
25984
  sol,
25296
25985
  `auto-closed off a pre-provenance anchor (guessed ${guessedAnchor}) \u2014 confirm the problem is really fixed, or reopen it`,
25297
- ts
25986
+ ts,
25987
+ // "or reopen it" is half the ask, and the nightly reopen takes that
25988
+ // branch — so record what would answer this, or the flag can never be
25989
+ // lowered and the band fills with settled questions.
25990
+ "parentProblemOpen"
25298
25991
  );
25299
25992
  store2.updateNode(p.id, {
25300
25993
  attrs: { ...p.attrs, resolutionSuspect: true },
@@ -25328,7 +26021,7 @@ function pagerank(input) {
25328
26021
  const ids = input.nodeIds;
25329
26022
  const n = ids.length;
25330
26023
  if (n === 0) {
25331
- return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
26024
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true, danglingRank: 0 };
25332
26025
  }
25333
26026
  const index = /* @__PURE__ */ new Map();
25334
26027
  for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
@@ -25400,8 +26093,12 @@ function pagerank(input) {
25400
26093
  }
25401
26094
  }
25402
26095
  const scores = /* @__PURE__ */ new Map();
25403
- for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
25404
- return { scores, iterations: iter, converged };
26096
+ let danglingRank = 0;
26097
+ for (let i2 = 0; i2 < n; i2++) {
26098
+ scores.set(ids[i2], score2[i2]);
26099
+ if (dangling[i2]) danglingRank += score2[i2];
26100
+ }
26101
+ return { scores, iterations: iter, converged, danglingRank };
25405
26102
  }
25406
26103
  function markLandmarks(scores, percentile = 0.1, filter) {
25407
26104
  const entries = [...scores.entries()];
@@ -25411,7 +26108,7 @@ function markLandmarks(scores, percentile = 0.1, filter) {
25411
26108
  }
25412
26109
  }
25413
26110
  if (entries.length === 0) return /* @__PURE__ */ new Set();
25414
- entries.sort((a, b) => b[1] - a[1]);
26111
+ entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
25415
26112
  const cutoff = Math.max(1, Math.floor(entries.length * percentile));
25416
26113
  const out2 = /* @__PURE__ */ new Set();
25417
26114
  for (let i2 = 0; i2 < cutoff; i2++) {
@@ -25649,8 +26346,86 @@ function motifDecision(input) {
25649
26346
  };
25650
26347
  }
25651
26348
 
25652
- // ../../packages/local-graph/src/community.ts
25653
- init_src2();
26349
+ // ../../packages/math/src/pagerank-local.ts
26350
+ function pagerankLocal(input) {
26351
+ const damping = input.damping ?? 0.85;
26352
+ const tol = input.tolerance ?? 1e-6;
26353
+ const maxIter = input.maxIterations ?? 100;
26354
+ const ids = input.regionIds;
26355
+ const n = ids.length;
26356
+ if (n === 0 || input.globalN === 0) {
26357
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
26358
+ }
26359
+ const index = /* @__PURE__ */ new Map();
26360
+ for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
26361
+ const base = (1 - damping) / input.globalN;
26362
+ const outSum = new Float64Array(n);
26363
+ const inflow = new Float64Array(n);
26364
+ let score2 = new Float64Array(n);
26365
+ for (let i2 = 0; i2 < n; i2++) {
26366
+ const id = ids[i2];
26367
+ outSum[i2] = input.outSum.get(id) ?? 0;
26368
+ inflow[i2] = input.boundaryInflow.get(id) ?? 0;
26369
+ score2[i2] = input.rank0.get(id) ?? base;
26370
+ }
26371
+ const rowLen = new Int32Array(n);
26372
+ let edgeCount = 0;
26373
+ for (const [from, edges] of input.out) {
26374
+ const fi = index.get(from);
26375
+ if (fi === void 0) continue;
26376
+ let inSet = 0;
26377
+ for (const e of edges) if (index.has(e.to)) inSet++;
26378
+ rowLen[fi] = inSet;
26379
+ edgeCount += inSet;
26380
+ }
26381
+ const rowStart = new Int32Array(n + 1);
26382
+ for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
26383
+ const colIdx = new Int32Array(edgeCount);
26384
+ const colW = new Float64Array(edgeCount);
26385
+ const cursor = rowStart.slice(0, n);
26386
+ for (const [from, edges] of input.out) {
26387
+ const fi = index.get(from);
26388
+ if (fi === void 0) continue;
26389
+ for (const e of edges) {
26390
+ const ti = index.get(e.to);
26391
+ if (ti === void 0) continue;
26392
+ const c = cursor[fi];
26393
+ cursor[fi] = c + 1;
26394
+ colIdx[c] = ti;
26395
+ colW[c] = e.weight;
26396
+ }
26397
+ }
26398
+ const externalDangling = input.externalDanglingRank ?? 0;
26399
+ let next = new Float64Array(n);
26400
+ let iter = 0;
26401
+ let converged = false;
26402
+ for (; iter < maxIter; iter++) {
26403
+ let internalDangling = 0;
26404
+ for (let i2 = 0; i2 < n; i2++) if (outSum[i2] <= 0) internalDangling += score2[i2];
26405
+ const danglingShare = damping * (internalDangling + externalDangling) / input.globalN;
26406
+ for (let i2 = 0; i2 < n; i2++) next[i2] = base + danglingShare + damping * inflow[i2];
26407
+ for (let i2 = 0; i2 < n; i2++) {
26408
+ const os2 = outSum[i2];
26409
+ if (os2 <= 0) continue;
26410
+ const f = damping * score2[i2] / os2;
26411
+ const end = rowStart[i2 + 1];
26412
+ for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
26413
+ }
26414
+ let diff = 0;
26415
+ for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
26416
+ const tmp = score2;
26417
+ score2 = next;
26418
+ next = tmp;
26419
+ if (diff < tol) {
26420
+ iter++;
26421
+ converged = true;
26422
+ break;
26423
+ }
26424
+ }
26425
+ const scores = /* @__PURE__ */ new Map();
26426
+ for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
26427
+ return { scores, iterations: iter, converged };
26428
+ }
25654
26429
 
25655
26430
  // ../../packages/local-graph/src/triage.ts
25656
26431
  init_src();
@@ -25659,95 +26434,8 @@ init_src();
25659
26434
  init_src2();
25660
26435
  var DRIFT_VALUES = new Set(Object.values(DRIFT_KIND));
25661
26436
 
25662
- // ../../packages/local-graph/src/problem-dedup.ts
25663
- init_src();
25664
- init_src();
25665
- var REDIRECT_EDGES = [
25666
- "CAUSED_BY",
25667
- "SOLVED_BY",
25668
- "FIXED_BY",
25669
- "ANCHORED_AT",
25670
- "MANIFESTED_IN",
25671
- "EVIDENCED_BY"
25672
- ];
25673
- function corroborations(n) {
25674
- return Number(n.attrs["corroborations"] ?? 0);
25675
- }
25676
- function overlap(a, b) {
25677
- if (a.size === 0 || b.size === 0) return 0;
25678
- let inter = 0;
25679
- for (const x of a) if (b.has(x)) inter++;
25680
- return inter / Math.min(a.size, b.size);
25681
- }
25682
- function mergeDuplicateProblems(store2, opts) {
25683
- const minOverlap = opts.minOverlap ?? 0.85;
25684
- const minTokens = opts.minTokens ?? 4;
25685
- const report = { clusters: 0, merged: 0 };
25686
- const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
25687
- const tokens = /* @__PURE__ */ new Map();
25688
- for (const p of open) tokens.set(p.id, new Set(conceptTokens(p.description)));
25689
- const consumed = /* @__PURE__ */ new Set();
25690
- store2.transaction(() => {
25691
- for (let i2 = 0; i2 < open.length; i2++) {
25692
- const a = open[i2];
25693
- if (consumed.has(a.id)) continue;
25694
- const cluster = [a];
25695
- const ta = tokens.get(a.id);
25696
- for (let j = i2 + 1; j < open.length; j++) {
25697
- const b = open[j];
25698
- if (consumed.has(b.id)) continue;
25699
- const tb = tokens.get(b.id);
25700
- if (Math.min(ta.size, tb.size) < minTokens) continue;
25701
- if (overlap(ta, tb) >= minOverlap) {
25702
- cluster.push(b);
25703
- consumed.add(b.id);
25704
- }
25705
- }
25706
- if (cluster.length < 2) continue;
25707
- report.clusters++;
25708
- cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
25709
- const survivor = cluster[0];
25710
- for (const dup of cluster.slice(1)) {
25711
- foldDuplicateProblem(store2, dup, survivor, opts.ts);
25712
- report.merged++;
25713
- }
25714
- }
25715
- });
25716
- return report;
25717
- }
25718
- function foldDuplicateProblem(store2, dup, survivor, ts) {
25719
- const surv = store2.getNode(survivor.id);
25720
- if (!surv) return;
25721
- const sources = new Set(surv.attrs["sources"] ?? []);
25722
- for (const s of dup.attrs["sources"] ?? []) sources.add(s);
25723
- store2.updateNode(survivor.id, {
25724
- attrs: {
25725
- ...surv.attrs,
25726
- sources: [...sources],
25727
- corroborations: corroborations(surv) + corroborations(dup) + 1
25728
- },
25729
- cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
25730
- lastUpdatedAt: ts
25731
- });
25732
- for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
25733
- if (e.to === survivor.id) continue;
25734
- const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
25735
- if (store2.getEdge(id)) continue;
25736
- const redirected = {
25737
- ...e,
25738
- id,
25739
- from: survivor.id,
25740
- createdAt: ts,
25741
- lastSeenAt: ts
25742
- };
25743
- store2.mergeEdge(redirected);
25744
- }
25745
- store2.updateNode(dup.id, {
25746
- attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
25747
- lastUpdatedAt: ts
25748
- });
25749
- store2.closeNode(dup.id, ts);
25750
- }
26437
+ // ../../packages/local-graph/src/community.ts
26438
+ init_src2();
25751
26439
 
25752
26440
  // ../../packages/local-graph/src/tools.ts
25753
26441
  init_src();
@@ -25764,6 +26452,9 @@ function rankToolsForHandles(store2, limit = 5) {
25764
26452
  // ../../packages/local-graph/src/principle-sync.ts
25765
26453
  init_src2();
25766
26454
 
26455
+ // ../../packages/local-graph/src/mechanism-liveness.ts
26456
+ var STALL_MIN_SPAN_MS = 60 * 60 * 1e3;
26457
+
25767
26458
  // ../../packages/generalizer/src/generalizer.ts
25768
26459
  init_src2();
25769
26460
  init_src();
@@ -25883,7 +26574,164 @@ function promoteMotifs(store2, t) {
25883
26574
  }
25884
26575
 
25885
26576
  // ../../packages/generalizer/src/nightly.ts
25886
- function runNightlyPipeline(store2) {
26577
+ var FULL_RESCORE_EVERY = 12;
26578
+ var INCREMENTAL_REGION_MAX_FRACTION = 0.2;
26579
+ var INCREMENTAL_HOPS = 3;
26580
+ function runGraphRescore(store2, opts = {}) {
26581
+ const lastRescoreAt = Number(store2.getMeta?.("lastRescoreAt") ?? 0);
26582
+ const sinceFull = Number(store2.getMeta?.("rescoresSinceFull") ?? 0);
26583
+ const incrementalCapable = !opts.forceFull && lastRescoreAt > 0 && sinceFull < FULL_RESCORE_EVERY - 1 && !!store2.getMeta && !!store2.setMeta && !!store2.dirtyNodeIdsSince && !!store2.edgeEndpointsTouchedSince && !!store2.outEdgesForMany && !!store2.inEdgesForMany && !!store2.ranksForMany && !!store2.landmarkSweepRows;
26584
+ if (incrementalCapable) {
26585
+ const started = Date.now();
26586
+ const inc = tryIncrementalRescore(store2, lastRescoreAt, started);
26587
+ if (inc) {
26588
+ store2.setMeta("lastRescoreAt", String(started));
26589
+ store2.setMeta("rescoresSinceFull", String(sinceFull + 1));
26590
+ return inc;
26591
+ }
26592
+ }
26593
+ const report = runFullRescore(store2);
26594
+ store2.setMeta?.("lastRescoreAt", String(report.startedAt));
26595
+ store2.setMeta?.("rescoresSinceFull", "0");
26596
+ store2.setMeta?.("danglingRankShare", String(report.danglingRank));
26597
+ const { startedAt: _drop, danglingRank: _drop2, ...rest } = report;
26598
+ return rest;
26599
+ }
26600
+ function tryIncrementalRescore(store2, lastRescoreAt, started) {
26601
+ const allowedTypes = new Set(pagerankEdgeTypes());
26602
+ const liveN = store2.nodeCount();
26603
+ const cap = Math.max(1e3, Math.floor(liveN * INCREMENTAL_REGION_MAX_FRACTION));
26604
+ const region = new Set(store2.dirtyNodeIdsSince(lastRescoreAt));
26605
+ for (const e of store2.edgeEndpointsTouchedSince(lastRescoreAt)) {
26606
+ region.add(e.from);
26607
+ region.add(e.to);
26608
+ }
26609
+ if (region.size > cap) return null;
26610
+ let frontier = [...region];
26611
+ for (let hop = 0; hop < INCREMENTAL_HOPS && frontier.length > 0; hop++) {
26612
+ const next = [];
26613
+ for (const e of store2.outEdgesForMany(frontier)) {
26614
+ if (!allowedTypes.has(e.type)) continue;
26615
+ if (!region.has(e.to)) {
26616
+ region.add(e.to);
26617
+ next.push(e.to);
26618
+ }
26619
+ }
26620
+ if (region.size > cap) return null;
26621
+ frontier = next;
26622
+ }
26623
+ const rank0 = store2.ranksForMany([...region]);
26624
+ const regionIds = [...rank0.keys()];
26625
+ if (regionIds.length === 0) {
26626
+ return {
26627
+ scoredNodes: 0,
26628
+ iterations: 0,
26629
+ converged: true,
26630
+ landmarks: 0,
26631
+ communities: 0,
26632
+ motifsPromoted: 0,
26633
+ techniques: 0,
26634
+ antipatterns: 0,
26635
+ pageRankWritten: 0,
26636
+ landmarkFlips: 0,
26637
+ mode: "incremental",
26638
+ regionSize: 0,
26639
+ durationMs: Date.now() - started
26640
+ };
26641
+ }
26642
+ const inRegion = new Set(regionIds);
26643
+ const out2 = /* @__PURE__ */ new Map();
26644
+ const outSum = /* @__PURE__ */ new Map();
26645
+ for (const e of store2.outEdgesForMany(regionIds)) {
26646
+ if (!allowedTypes.has(e.type)) continue;
26647
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26648
+ if (w <= 0) continue;
26649
+ outSum.set(e.from, (outSum.get(e.from) ?? 0) + w);
26650
+ if (!inRegion.has(e.to)) continue;
26651
+ const l = out2.get(e.from);
26652
+ if (l) l.push({ to: e.to, weight: w });
26653
+ else out2.set(e.from, [{ to: e.to, weight: w }]);
26654
+ }
26655
+ const boundaryEdges = store2.inEdgesForMany(regionIds).filter((e) => allowedTypes.has(e.type) && !inRegion.has(e.from) && (EDGE_WEIGHT[e.type] ?? 1) > 0);
26656
+ const boundarySources = [...new Set(boundaryEdges.map((e) => e.from))];
26657
+ if (boundarySources.length > cap) return null;
26658
+ const boundaryRanks = store2.ranksForMany(boundarySources);
26659
+ const boundaryOutSum = /* @__PURE__ */ new Map();
26660
+ for (const e of store2.outEdgesForMany(boundarySources)) {
26661
+ if (!allowedTypes.has(e.type)) continue;
26662
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26663
+ if (w > 0) boundaryOutSum.set(e.from, (boundaryOutSum.get(e.from) ?? 0) + w);
26664
+ }
26665
+ const boundaryInflow = /* @__PURE__ */ new Map();
26666
+ for (const e of boundaryEdges) {
26667
+ const r = boundaryRanks.get(e.from);
26668
+ const os2 = boundaryOutSum.get(e.from);
26669
+ if (r === void 0 || !os2) continue;
26670
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26671
+ boundaryInflow.set(e.to, (boundaryInflow.get(e.to) ?? 0) + r * w / os2);
26672
+ }
26673
+ const globalDangling = Number(store2.getMeta("danglingRankShare") ?? 0);
26674
+ let regionDangling = 0;
26675
+ for (const id of regionIds) {
26676
+ if ((outSum.get(id) ?? 0) <= 0) regionDangling += rank0.get(id) ?? 0;
26677
+ }
26678
+ const result = pagerankLocal({
26679
+ regionIds,
26680
+ rank0,
26681
+ out: out2,
26682
+ outSum,
26683
+ boundaryInflow,
26684
+ globalN: liveN,
26685
+ externalDanglingRank: Math.max(0, globalDangling - regionDangling),
26686
+ damping: 0.85,
26687
+ tolerance: 1e-6,
26688
+ maxIterations: 100
26689
+ });
26690
+ let pageRankWritten = 0;
26691
+ store2.transaction(() => {
26692
+ for (const [id, score2] of result.scores) {
26693
+ if (Math.abs(score2 - (rank0.get(id) ?? 0)) < 1e-9) continue;
26694
+ store2.setPageRank(id, score2);
26695
+ pageRankWritten++;
26696
+ }
26697
+ });
26698
+ const sweep = store2.landmarkSweepRows();
26699
+ const candidates = /* @__PURE__ */ new Map();
26700
+ for (const row of sweep) {
26701
+ if (row.label === "Pattern" || row.label === "RootCause") {
26702
+ candidates.set(row.id, result.scores.get(row.id) ?? row.pageRank);
26703
+ }
26704
+ }
26705
+ const want = markLandmarks(candidates, 0.1);
26706
+ for (const row of sweep) if (row.memoryTier === "persistent") want.add(row.id);
26707
+ let landmarkFlips = 0;
26708
+ store2.transaction(() => {
26709
+ for (const row of sweep) {
26710
+ if (row.extractionSource === "bko-inferred") continue;
26711
+ const should = want.has(row.id);
26712
+ if (row.isLandmark === should) continue;
26713
+ store2.setLandmark(row.id, should);
26714
+ landmarkFlips++;
26715
+ }
26716
+ });
26717
+ return {
26718
+ scoredNodes: regionIds.length,
26719
+ iterations: result.iterations,
26720
+ converged: result.converged,
26721
+ landmarks: want.size,
26722
+ communities: 0,
26723
+ // deferred to the full backstop — see mode docs
26724
+ motifsPromoted: 0,
26725
+ techniques: 0,
26726
+ antipatterns: 0,
26727
+ pageRankWritten,
26728
+ landmarkFlips,
26729
+ mode: "incremental",
26730
+ regionSize: regionIds.length,
26731
+ durationMs: Date.now() - started
26732
+ };
26733
+ }
26734
+ function runFullRescore(store2) {
25887
26735
  const started = Date.now();
25888
26736
  const allowedTypes = new Set(pagerankEdgeTypes());
25889
26737
  const nodeIds = [];
@@ -25909,10 +26757,12 @@ function runNightlyPipeline(store2) {
25909
26757
  maxIterations: 100
25910
26758
  });
25911
26759
  const scored = result.scores.size;
26760
+ let pageRankWritten = 0;
25912
26761
  store2.transaction(() => {
25913
26762
  for (const [id, score2] of result.scores) {
25914
26763
  if (Math.abs(score2 - (meta3.get(id)?.pageRank ?? 0)) < 1e-9) continue;
25915
26764
  store2.setPageRank(id, score2);
26765
+ pageRankWritten++;
25916
26766
  }
25917
26767
  });
25918
26768
  const landmarkCandidates = /* @__PURE__ */ new Map();
@@ -25927,11 +26777,13 @@ function runNightlyPipeline(store2) {
25927
26777
  for (const id of nodeIds) {
25928
26778
  if (meta3.get(id)?.memoryTier === "persistent") allLandmarks.add(id);
25929
26779
  }
26780
+ let landmarkFlips = 0;
25930
26781
  store2.transaction(() => {
25931
26782
  for (const id of nodeIds) {
25932
26783
  const want = allLandmarks.has(id);
25933
26784
  if ((meta3.get(id)?.isLandmark ?? false) === want) continue;
25934
26785
  store2.setLandmark(id, want);
26786
+ landmarkFlips++;
25935
26787
  }
25936
26788
  });
25937
26789
  const MOTIF_LABELS = /* @__PURE__ */ new Set(["Pattern", "Technique", "AntiPattern"]);
@@ -25966,8 +26818,6 @@ function runNightlyPipeline(store2) {
25966
26818
  for (const [id, c] of comm.community) store2.updateNode(id, { community: c });
25967
26819
  });
25968
26820
  const motifs = promoteMotifs(store2, started);
25969
- const designResolved = resolveDesignProblems(store2, started);
25970
- const cry = crystallize(store2, { ts: started });
25971
26821
  return {
25972
26822
  scoredNodes: scored,
25973
26823
  iterations: result.iterations,
@@ -25977,15 +26827,61 @@ function runNightlyPipeline(store2) {
25977
26827
  motifsPromoted: motifs.promoted,
25978
26828
  techniques: motifs.techniques,
25979
26829
  antipatterns: motifs.antipatterns,
25980
- skillsInduced: 0,
25981
- // skills are cloud-induced now (see above)
25982
- skillsRefreshed: 0,
26830
+ pageRankWritten,
26831
+ landmarkFlips,
26832
+ mode: "full",
26833
+ regionSize: 0,
26834
+ startedAt: started,
26835
+ danglingRank: result.danglingRank,
26836
+ durationMs: Date.now() - started
26837
+ };
26838
+ }
26839
+ function runSemanticMaintenance(store2) {
26840
+ const started = Date.now();
26841
+ reopenAutoClosedProblems(store2, started);
26842
+ const revisits = clearAnsweredRevisits(store2, started);
26843
+ const fixCandidates = clearSettledFixCandidates(store2, started);
26844
+ const designResolved = markFixCandidates(store2, started);
26845
+ const cry = crystallize(store2, { ts: started });
26846
+ return {
25983
26847
  designResolved,
26848
+ revisitsCleared: revisits.cleared,
26849
+ revisitsStanding: revisits.standing,
26850
+ fixCandidatesSettled: fixCandidates.answered + fixCandidates.ignored + fixCandidates.unsubstantiated,
26851
+ fixCandidates,
25984
26852
  claimsEvaluated: cry.evaluated,
25985
26853
  claimsDerived: cry.derived.length,
25986
26854
  durationMs: Date.now() - started
25987
26855
  };
25988
26856
  }
26857
+ function runNightlyPipeline(store2) {
26858
+ const started = Date.now();
26859
+ const rescore = runGraphRescore(store2);
26860
+ const semantic = runSemanticMaintenance(store2);
26861
+ return {
26862
+ scoredNodes: rescore.scoredNodes,
26863
+ iterations: rescore.iterations,
26864
+ converged: rescore.converged,
26865
+ landmarks: rescore.landmarks,
26866
+ communities: rescore.communities,
26867
+ motifsPromoted: rescore.motifsPromoted,
26868
+ techniques: rescore.techniques,
26869
+ antipatterns: rescore.antipatterns,
26870
+ skillsInduced: 0,
26871
+ // skills are cloud-induced now (see above)
26872
+ skillsRefreshed: 0,
26873
+ pageRankWritten: rescore.pageRankWritten,
26874
+ landmarkFlips: rescore.landmarkFlips,
26875
+ mode: rescore.mode,
26876
+ regionSize: rescore.regionSize,
26877
+ designResolved: semantic.designResolved,
26878
+ revisitsCleared: semantic.revisitsCleared,
26879
+ fixCandidatesSettled: semantic.fixCandidatesSettled,
26880
+ claimsEvaluated: semantic.claimsEvaluated,
26881
+ claimsDerived: semantic.claimsDerived,
26882
+ durationMs: Date.now() - started
26883
+ };
26884
+ }
25989
26885
  function codeAnchorProjection(store2, semanticIds, opts = {}) {
25990
26886
  const anchorToNodes = /* @__PURE__ */ new Map();
25991
26887
  for (const id of semanticIds) {
@@ -26030,12 +26926,14 @@ var AGENTS_POINTER_BODY = [
26030
26926
  init_src2();
26031
26927
  function buildSnapshot(opts) {
26032
26928
  const problems = opts.store.findNodesByLabel("Problem").filter((p) => p.attrs["mergedInto"] === void 0);
26033
- const allProblems = problems.filter((p) => p.attrs["resolvedAt"] == null);
26929
+ const stillOpen = problems.filter((p) => p.attrs["resolvedAt"] == null);
26930
+ const allProblems = stillOpen.filter((p) => !isConstraintProblem(p));
26034
26931
  allProblems.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
26035
26932
  const recent = allProblems.slice(0, 8).map((p) => ({
26036
26933
  node: p,
26037
26934
  anchors: opts.store.outEdges(p.id, ["MANIFESTED_IN", "EVIDENCED_BY"])
26038
26935
  }));
26936
+ const recentConstraints = stillOpen.filter((p) => isConstraintProblem(p)).sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt).slice(0, 3);
26039
26937
  const recentResolved = problems.filter((p) => p.attrs["resolvedAt"] != null && p.attrs["resolvedAs"] == null).sort((a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)).slice(0, 4).map((p) => {
26040
26938
  const solEdge = opts.store.outEdges(p.id, ["SOLVED_BY"])[0];
26041
26939
  const solution = solEdge ? opts.store.getNode(solEdge.to) ?? void 0 : void 0;
@@ -26063,10 +26961,13 @@ function buildSnapshot(opts) {
26063
26961
  episodeId,
26064
26962
  problems: problems2
26065
26963
  }));
26066
- const needsRevisit = listNeedsRevisit(
26067
- opts.store,
26068
- (opts.now ?? /* @__PURE__ */ new Date()).getTime()
26069
- ).slice(0, 10);
26964
+ const revisitSeen = /* @__PURE__ */ new Set();
26965
+ const needsRevisit = listNeedsRevisit(opts.store, (opts.now ?? /* @__PURE__ */ new Date()).getTime()).filter((r) => {
26966
+ const key = `${r.label}:${r.description.trim().toLowerCase()}`;
26967
+ if (revisitSeen.has(key)) return false;
26968
+ revisitSeen.add(key);
26969
+ return true;
26970
+ }).slice(0, 5);
26070
26971
  const norm = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, "");
26071
26972
  const pkgBase = (x) => {
26072
26973
  const at = x.lastIndexOf("@");
@@ -26102,7 +27003,9 @@ function buildSnapshot(opts) {
26102
27003
  profileContext,
26103
27004
  recentProblems: recent,
26104
27005
  recentResolved,
27006
+ recentConstraints,
26105
27007
  ...causalNudge ? { causalNudge } : {},
27008
+ ...opts.selfCiteSkips && opts.selfCiteSkips > 0 ? { selfCiteSkips: opts.selfCiteSkips } : {},
26106
27009
  ...domainNudge ? { domainNudge } : {},
26107
27010
  motifs,
26108
27011
  reviewCount: opts.reviewCount,
@@ -26153,10 +27056,16 @@ function sliceForFile(store2, relPath) {
26153
27056
  }
26154
27057
  const fp = priorsForFile(store2, relPath);
26155
27058
  const priors = fp ? {
26156
- openProblems: fp.openProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
27059
+ openProblems: fp.openProblems.slice(0, 3).map((p) => ({
27060
+ id: p.id,
27061
+ description: p.description,
27062
+ ...typeof p.attrs["fixCandidateFile"] === "string" ? { fixCandidateFile: p.attrs["fixCandidateFile"] } : {}
27063
+ })),
27064
+ constraints: fp.constraints.slice(0, 2).map((c) => ({ id: c.id, description: c.description })),
27065
+ resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
26157
27066
  related: fp.related.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description })),
26158
27067
  solutionsByProblem: Object.fromEntries(
26159
- fp.openProblems.slice(0, 3).map((p) => [
27068
+ [...fp.openProblems.slice(0, 3), ...fp.resolvedProblems.slice(0, 3)].map((p) => [
26160
27069
  p.id,
26161
27070
  (fp.solutionsByProblem.get(p.id) ?? []).slice(0, 4).map((s) => ({ id: s.id, description: s.description }))
26162
27071
  ])
@@ -26264,9 +27173,15 @@ function* walkSource(dir) {
26264
27173
  function listSourceFiles(root) {
26265
27174
  return gitSourceFiles(root) ?? walkSource(root);
26266
27175
  }
26267
- function findStaleFiles(store2, rootPath, workspaceId) {
27176
+ var SCAN_YIELD_EVERY = 100;
27177
+ var LIVENESS_CHUNK = 4e3;
27178
+ async function findStaleFiles(store2, rootPath, workspaceId) {
26268
27179
  const stale = [];
27180
+ const pending = [];
27181
+ const allTargets = [];
27182
+ let scanned = 0;
26269
27183
  for (const abs of listSourceFiles(rootPath)) {
27184
+ if (++scanned % SCAN_YIELD_EVERY === 0) await new Promise((r) => setImmediate(r));
26270
27185
  let mtimeMs;
26271
27186
  try {
26272
27187
  mtimeMs = statSync2(abs).mtimeMs;
@@ -26277,32 +27192,52 @@ function findStaleFiles(store2, rootPath, workspaceId) {
26277
27192
  const fnode = store2.getNode(fileNodeId(workspaceId, rel));
26278
27193
  if (!fnode) {
26279
27194
  stale.push(abs);
27195
+ } else if ((fnode.validTo ?? null) !== null) {
27196
+ stale.push(abs);
26280
27197
  } else if (fnode.lastUpdatedAt < mtimeMs) {
26281
27198
  stale.push(abs);
26282
27199
  } else {
26283
27200
  const edges = store2.outEdges(fnode.id, ["DEFINES", "CONTAINS"]);
26284
27201
  if (edges.length === 0) {
26285
27202
  stale.push(abs);
26286
- } else if (edges.some((e) => !isLive(store2.getNode(e.to)))) {
26287
- stale.push(abs);
27203
+ } else {
27204
+ const targets = edges.map((e) => e.to);
27205
+ pending.push({ abs, targets });
27206
+ allTargets.push(...targets);
26288
27207
  }
26289
27208
  }
26290
27209
  }
27210
+ const live = /* @__PURE__ */ new Set();
27211
+ for (let i2 = 0; i2 < allTargets.length; i2 += LIVENESS_CHUNK) {
27212
+ for (const id of store2.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live.add(id);
27213
+ if (i2 + LIVENESS_CHUNK < allTargets.length) await new Promise((r) => setImmediate(r));
27214
+ }
27215
+ for (const { abs, targets } of pending) {
27216
+ if (targets.some((id) => !live.has(id))) stale.push(abs);
27217
+ }
26291
27218
  return stale;
26292
27219
  }
26293
- function isLive(n) {
26294
- return n != null && (n.validTo ?? null) === null;
26295
- }
26296
27220
  async function reconcileStaleFiles(store2, rootPath, workspaceId) {
26297
- const stale = findStaleFiles(store2, rootPath, workspaceId);
27221
+ const stale = await findStaleFiles(store2, rootPath, workspaceId);
26298
27222
  if (stale.length === 0) return 0;
26299
27223
  let total = 0;
27224
+ let failedBatches = 0;
26300
27225
  const BATCH = 20;
26301
27226
  for (let i2 = 0; i2 < stale.length; i2 += BATCH) {
26302
- const r = await incrementalReindex(store2, rootPath, workspaceId, stale.slice(i2, i2 + BATCH));
26303
- total += r.filesReindexed;
27227
+ try {
27228
+ const r = await incrementalReindex(store2, rootPath, workspaceId, stale.slice(i2, i2 + BATCH));
27229
+ total += r.filesReindexed;
27230
+ } catch (err2) {
27231
+ failedBatches++;
27232
+ console.warn(
27233
+ `[errata] reconcile: batch ${i2 / BATCH + 1} failed (${stale.slice(i2, i2 + BATCH).length} file(s) skipped): ${err2 instanceof Error ? err2.message : err2}`
27234
+ );
27235
+ }
26304
27236
  if (i2 + BATCH < stale.length) await new Promise((res) => setImmediate(res));
26305
27237
  }
27238
+ if (failedBatches > 0) {
27239
+ console.warn(`[errata] reconcile: ${failedBatches} batch(es) failed; ${total} file(s) reindexed`);
27240
+ }
26306
27241
  return total;
26307
27242
  }
26308
27243
 
@@ -26355,6 +27290,7 @@ function runNightly() {
26355
27290
  const a = backfillLegacyAnchors(store, Date.now());
26356
27291
  anchorsDemoted = a.demotedEdges;
26357
27292
  resolutionsSuspect = a.suspects.length;
27293
+ repairLegacyAnchorsFromStatement(store, Date.now());
26358
27294
  } catch {
26359
27295
  }
26360
27296
  const report = runNightlyPipeline(store);