@inerrata-corporation/errata 2.0.2-dev.125 → 2.0.2-dev.1255

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",
@@ -205,7 +212,7 @@ var init_castalia = __esm({
205
212
  DECOMPOSITION_EDGES = ["SPLIT_INTO"];
206
213
  TRANSFER_EDGES = ["MAY_RESOLVE"];
207
214
  SOLUTION_STRUCTURE_EDGES = ["BUILDS_ON", "SUPERSEDES", "ALTERNATIVE_TO"];
208
- SUSPICION_EDGES = ["SUSPECTED_LINK"];
215
+ SUSPICION_EDGES = ["SUSPECTED_LINK", "REGRESSION_OF"];
209
216
  TAXONOMY_EDGES = ["IS_A"];
210
217
  TRIAGE_EDGES = ["TRIAGED_BY", "CONFIRMS", "INDICATES", "ROUTES_TO"];
211
218
  GIT_EDGES = ["POINTS_AT", "PARENT", "AUTHORED_BY"];
@@ -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",
@@ -266,14 +277,20 @@ var init_castalia = __esm({
266
277
  "ALTERNATIVE_TO",
267
278
  // symmetric peer marker — weight would let alternatives inflate each other
268
279
  // NB: BUILDS_ON is NOT excluded — it flows extension→base so foundational solutions rank up.
269
- "SUSPECTED_LINK"
280
+ "SUSPECTED_LINK",
270
281
  // Somnus hypothesis (PLAN_SOMNUS_V2 §4) — never flows rank; navigation-invisible.
282
+ "REGRESSION_OF"
283
+ // recurrence marker (RG-recognize) — a question about a close, not signal-flow.
271
284
  ]);
272
285
  EDGE_WEIGHT = {
273
286
  // Causal — high signal
274
287
  CAUSED_BY: 3,
275
288
  FIXED_BY: 3,
276
289
  SOLVED_BY: 3,
290
+ // Twin-face link (failure motif → remedy motif, KN-twin-faces) — causal-grade
291
+ // but a notch below witnessed FIXED_BY/SOLVED_BY: the pairing is an LLM
292
+ // judgment over descriptions, not an agent-witnessed resolution.
293
+ REMEDIED_BY: 2.5,
277
294
  MANIFESTS_AS: 2,
278
295
  ESCALATES_TO: 1.5,
279
296
  AFFECTS: 1.2,
@@ -376,6 +393,7 @@ var init_castalia = __esm({
376
393
  ALTERNATIVE_TO: 0,
377
394
  // Suspicion — a hypothesis, weightless + PageRank-excluded (never flows rank / EIG).
378
395
  SUSPECTED_LINK: 0,
396
+ REGRESSION_OF: 0,
379
397
  // Git — topology/authorship, weightless (not domain signal-flow)
380
398
  POINTS_AT: 0,
381
399
  PARENT: 0,
@@ -406,6 +424,13 @@ var init_castalia = __esm({
406
424
  }
407
425
  });
408
426
 
427
+ // ../../packages/shared/src/graph-events.ts
428
+ var init_graph_events = __esm({
429
+ "../../packages/shared/src/graph-events.ts"() {
430
+ "use strict";
431
+ }
432
+ });
433
+
409
434
  // ../../packages/shared/src/canonical/hash.ts
410
435
  import { createHash } from "node:crypto";
411
436
  function sha256(input) {
@@ -447,13 +472,18 @@ function resolveCanonical(label) {
447
472
  function resolveCanonicalId(label) {
448
473
  return resolveCanonical(label)?.id;
449
474
  }
475
+ function resolveUnambiguousCanonicalId(label) {
476
+ const key = label.trim().toLowerCase();
477
+ if (AMBIGUOUS_ALIASES.has(key)) return void 0;
478
+ return resolveCanonicalId(key);
479
+ }
450
480
  function aliasesLongestFirst() {
451
481
  const out2 = [];
452
482
  for (const e of REGISTRY) for (const a of e.aliases) out2.push({ alias: a.toLowerCase(), entity: e });
453
483
  out2.sort((x, y) => y.alias.length - x.alias.length);
454
484
  return out2;
455
485
  }
456
- var REGISTRY, BY_ALIAS;
486
+ var REGISTRY, AMBIGUOUS_ALIASES, BY_ALIAS;
457
487
  var init_taxonomy = __esm({
458
488
  "../../packages/shared/src/nlp/taxonomy.ts"() {
459
489
  "use strict";
@@ -502,6 +532,13 @@ var init_taxonomy = __esm({
502
532
  { id: "concept:retry", name: "retry", category: "concept", aliases: ["retry"] },
503
533
  { id: "concept:migration", name: "migration", category: "concept", aliases: ["migration"] }
504
534
  ];
535
+ AMBIGUOUS_ALIASES = /* @__PURE__ */ new Set([
536
+ "node",
537
+ "go",
538
+ "pool",
539
+ "spring",
540
+ "ws"
541
+ ]);
505
542
  BY_ALIAS = /* @__PURE__ */ new Map();
506
543
  for (const e of REGISTRY) {
507
544
  for (const a of e.aliases) {
@@ -563,7 +600,7 @@ function lemma(token) {
563
600
  }
564
601
  return token;
565
602
  }
566
- function conceptTokens(text) {
603
+ function tokenize(text, resolve) {
567
604
  const matches = text.normalize("NFC").toLowerCase().match(TOKEN_RE) ?? [];
568
605
  const out2 = /* @__PURE__ */ new Set();
569
606
  for (const raw of matches) {
@@ -573,11 +610,14 @@ function conceptTokens(text) {
573
610
  out2.add(token);
574
611
  continue;
575
612
  }
576
- const canonical = resolveCanonicalId(token);
613
+ const canonical = resolve(token);
577
614
  out2.add(canonical ?? lemma(token));
578
615
  }
579
616
  return [...out2].sort();
580
617
  }
618
+ function retrievalTokens(text) {
619
+ return tokenize(text, resolveUnambiguousCanonicalId);
620
+ }
581
621
  var STOPWORDS, NEGATION_TOKENS, TOKEN_RE;
582
622
  var init_concept_bag = __esm({
583
623
  "../../packages/shared/src/nlp/concept-bag.ts"() {
@@ -15058,7 +15098,12 @@ var init_edge_rules = __esm({
15058
15098
  // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
15059
15099
  IS_A: { from: ["Weakness"], to: ["Weakness"] },
15060
15100
  // ── Conceptual (v1 taxonomy.ts: instance → motif/pattern reference) ──
15061
- INSTANCE_OF: { to: ["Pattern", "Weakness", "Technique"] },
15101
+ // `AntiPattern` joined the target set 2026-08-02: it is one of the three motif
15102
+ // kinds (generalizer motifs.ts `layerOf`: pattern | technique | antipattern) and
15103
+ // the SUPPRESSED-close path mints `Problem ─INSTANCE_OF→ AntiPattern` as the
15104
+ // negative-knowledge binding ("silenced, not solved") — the original three-label
15105
+ // rule predates AntiPattern joining the motif layer and silently ate that edge.
15106
+ INSTANCE_OF: { to: ["Pattern", "AntiPattern", "Weakness", "Technique"] },
15062
15107
  IMPLEMENTS: { from: ["Solution", "Language", "Component"], to: ["Pattern", "Technique"] },
15063
15108
  MATCHES: { to: ["Pattern"] },
15064
15109
  // ── Artifact evidence (v1 taxonomy.ts artifact rows) ──
@@ -15159,6 +15204,23 @@ var init_wire = __esm({
15159
15204
  /** Canonical human-readable description (no raw paths — daemon scrubs; server rechecks). */
15160
15205
  description: external_exports.string().min(1).max(4e3),
15161
15206
  attrs: external_exports.record(external_exports.string(), external_exports.unknown()).default({}),
15207
+ /** One-way origin key of the SESSION that minted this node (`ws_…`, a
15208
+ * truncated digest — never the raw session id). Stamped onto the created
15209
+ * node as `authoringSession`, which is the independence unit for the
15210
+ * evidence channels: the session that authored a claim may not corroborate
15211
+ * or refute it, while a DIFFERENT session on the same checkout may (alyssa,
15212
+ * 2026-07-31 — the workspace key made every witness on a single-checkout
15213
+ * deployment a self-corroboration). Optional + additive: absent leaves the
15214
+ * gate fail-open for that node, exactly today's behaviour. */
15215
+ originSession: external_exports.string().min(3).max(64).optional(),
15216
+ /** Epoch ms of the ORIGINATING local node's creation — when the knowledge was
15217
+ * actually captured, as opposed to when its public twin reached the cloud.
15218
+ * Stored as `observedAt`; the network board's chronological view orders on
15219
+ * it. Without this the wire carried NO timestamp at all, so a node captured
15220
+ * days ago surfaced as "new" the moment it was generalized and published —
15221
+ * the board showed ingest order wearing a chronology's clothes. Optional +
15222
+ * additive: absent keeps ingest-time ordering for that node. */
15223
+ originCreatedAtMs: external_exports.number().int().positive().optional(),
15162
15224
  extractionSource: external_exports.enum(INGEST_EXTRACTION_SOURCES),
15163
15225
  validationSource: external_exports.enum(VALIDATION_SOURCES).optional(),
15164
15226
  /** Org-membrane (M2): the daemon's anchor tag — it owns the lockfile, so it
@@ -15270,6 +15332,7 @@ var init_src = __esm({
15270
15332
  "../../packages/shared/src/index.ts"() {
15271
15333
  "use strict";
15272
15334
  init_castalia();
15335
+ init_graph_events();
15273
15336
  init_identity();
15274
15337
  init_wire();
15275
15338
  init_edge_rules();
@@ -15331,6 +15394,51 @@ var init_review = __esm({
15331
15394
  }
15332
15395
  });
15333
15396
 
15397
+ // ../../packages/local-shared/src/anchorable.ts
15398
+ function anchorableExtensionAlternation() {
15399
+ return BY_LENGTH.map((e) => e.slice(1).replace(/[+.]/g, (c) => `\\${c}`)).join("|");
15400
+ }
15401
+ var ANCHORABLE_EXTENSIONS, BY_LENGTH;
15402
+ var init_anchorable = __esm({
15403
+ "../../packages/local-shared/src/anchorable.ts"() {
15404
+ "use strict";
15405
+ ANCHORABLE_EXTENSIONS = /* @__PURE__ */ new Set([
15406
+ // typescript provider
15407
+ ".ts",
15408
+ ".tsx",
15409
+ ".js",
15410
+ ".jsx",
15411
+ ".mjs",
15412
+ ".cjs",
15413
+ // python / go / rust / ruby / csharp providers
15414
+ ".py",
15415
+ ".go",
15416
+ ".rs",
15417
+ ".rb",
15418
+ ".cs",
15419
+ // cpp provider — every variant it parses, not just the three that were listed
15420
+ ".c",
15421
+ ".h",
15422
+ ".cpp",
15423
+ ".cc",
15424
+ ".cxx",
15425
+ ".c++",
15426
+ ".hpp",
15427
+ ".hh",
15428
+ ".hxx",
15429
+ ".h++",
15430
+ // CUDA — the cpp provider parses these too. Missed when this list was first
15431
+ // transcribed by hand; the parity test caught them on its very first run,
15432
+ // which is the argument for the test existing.
15433
+ ".cu",
15434
+ ".cuh",
15435
+ // no provider yet; the grammar ships. Inert, not wrong — see above.
15436
+ ".java"
15437
+ ]);
15438
+ BY_LENGTH = [...ANCHORABLE_EXTENSIONS].sort((a, b) => b.length - a.length);
15439
+ }
15440
+ });
15441
+
15334
15442
  // ../../packages/local-shared/src/sqlite-adapter.ts
15335
15443
  function openDatabase(path) {
15336
15444
  const db = new DatabaseSync(path);
@@ -15343,6 +15451,7 @@ function openDatabase(path) {
15343
15451
  db.exec("PRAGMA journal_mode = WAL");
15344
15452
  db.exec("PRAGMA synchronous = NORMAL");
15345
15453
  db.exec("PRAGMA busy_timeout = 5000");
15454
+ db.exec("PRAGMA journal_size_limit = 67108864");
15346
15455
  } catch {
15347
15456
  }
15348
15457
  }
@@ -15378,7 +15487,7 @@ function openDatabase(path) {
15378
15487
  return db.prepare(`PRAGMA ${key}`).get();
15379
15488
  },
15380
15489
  transaction(fn) {
15381
- db.exec("BEGIN");
15490
+ db.exec("BEGIN IMMEDIATE");
15382
15491
  try {
15383
15492
  const r = fn();
15384
15493
  db.exec("COMMIT");
@@ -15432,10 +15541,92 @@ var init_src2 = __esm({
15432
15541
  init_profile();
15433
15542
  init_daemon_wire();
15434
15543
  init_review();
15544
+ init_anchorable();
15435
15545
  init_sqlite_adapter();
15436
15546
  }
15437
15547
  });
15438
15548
 
15549
+ // ../../packages/indexer/src/parse-cache.ts
15550
+ import { createHash as createHash2 } from "node:crypto";
15551
+ import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
15552
+ import { homedir } from "node:os";
15553
+ import { join } from "node:path";
15554
+ function parseCacheDir() {
15555
+ return process.env["ERRATA_PARSE_CACHE"] ?? join(homedir(), ".errata", "parse-cache");
15556
+ }
15557
+ function parseCacheKey(source, providerId) {
15558
+ return createHash2("sha256").update(`${PARSE_CACHE_VERSION}:${providerId}:${source}`).digest("hex");
15559
+ }
15560
+ function entryPath(dir, key) {
15561
+ return join(dir, key.slice(0, 2), `${key.slice(2)}.json`);
15562
+ }
15563
+ function readParseCache(dir, key) {
15564
+ const file2 = entryPath(dir, key);
15565
+ try {
15566
+ const raw = readFileSync(file2, "utf8");
15567
+ const parsed = JSON.parse(raw);
15568
+ if (parsed.v !== PARSE_CACHE_VERSION || !Array.isArray(parsed.symbols)) return null;
15569
+ return { symbols: parsed.symbols, reExports: parsed.reExports ?? [] };
15570
+ } catch {
15571
+ return null;
15572
+ }
15573
+ }
15574
+ function writeParseCache(dir, key, providerId, value) {
15575
+ try {
15576
+ const envelope = {
15577
+ v: PARSE_CACHE_VERSION,
15578
+ provider: providerId,
15579
+ symbols: value.symbols,
15580
+ reExports: value.reExports
15581
+ };
15582
+ const body2 = JSON.stringify(envelope);
15583
+ if (body2.length > MAX_ENTRY_BYTES) return;
15584
+ const file2 = entryPath(dir, key);
15585
+ mkdirSync(join(dir, key.slice(0, 2)), { recursive: true });
15586
+ writeFileSync(file2, body2);
15587
+ } catch {
15588
+ }
15589
+ }
15590
+ function sweepParseCacheOnce(dir, now = Date.now()) {
15591
+ if (sweptThisProcess) return 0;
15592
+ sweptThisProcess = true;
15593
+ let removed = 0;
15594
+ try {
15595
+ if (!existsSync(dir)) return 0;
15596
+ for (const bucket of readdirSync(dir)) {
15597
+ const bucketDir = join(dir, bucket);
15598
+ let names;
15599
+ try {
15600
+ names = readdirSync(bucketDir);
15601
+ } catch {
15602
+ continue;
15603
+ }
15604
+ for (const name2 of names) {
15605
+ const file2 = join(bucketDir, name2);
15606
+ try {
15607
+ if (now - statSync(file2).mtimeMs > ENTRY_TTL_MS) {
15608
+ rmSync(file2, { force: true });
15609
+ removed++;
15610
+ }
15611
+ } catch {
15612
+ }
15613
+ }
15614
+ }
15615
+ } catch {
15616
+ }
15617
+ return removed;
15618
+ }
15619
+ var PARSE_CACHE_VERSION, ENTRY_TTL_MS, MAX_ENTRY_BYTES, sweptThisProcess;
15620
+ var init_parse_cache = __esm({
15621
+ "../../packages/indexer/src/parse-cache.ts"() {
15622
+ "use strict";
15623
+ PARSE_CACHE_VERSION = 1;
15624
+ ENTRY_TTL_MS = 30 * 24 * 60 * 60 * 1e3;
15625
+ MAX_ENTRY_BYTES = 2 * 1024 * 1024;
15626
+ sweptThisProcess = false;
15627
+ }
15628
+ });
15629
+
15439
15630
  // ../../packages/indexer/src/simhash.ts
15440
15631
  function fnv1a64(s) {
15441
15632
  let h = FNV_OFFSET;
@@ -15454,7 +15645,7 @@ function popcount(x) {
15454
15645
  }
15455
15646
  return c;
15456
15647
  }
15457
- function tokenize(text) {
15648
+ function tokenize2(text) {
15458
15649
  return text.match(/[A-Za-z_$][A-Za-z0-9_$]*|\d+|[^\s\w]/g) ?? [];
15459
15650
  }
15460
15651
  function shingle(tokens, n = SHINGLE_N) {
@@ -15483,7 +15674,7 @@ function simhashFeatures(features) {
15483
15674
  return out2;
15484
15675
  }
15485
15676
  function simhash(text) {
15486
- return simhashFeatures(shingle(tokenize(text)));
15677
+ return simhashFeatures(shingle(tokenize2(text)));
15487
15678
  }
15488
15679
  function hammingDistance(a, b) {
15489
15680
  return popcount((a ^ b) & MASK64);
@@ -15659,10 +15850,10 @@ var init_identity2 = __esm({
15659
15850
  });
15660
15851
 
15661
15852
  // ../../packages/indexer/src/pipeline.ts
15662
- import { appendFileSync, readFileSync, statSync } from "node:fs";
15853
+ import { appendFileSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
15663
15854
  import { readdir } from "node:fs/promises";
15664
- import { extname, join, relative, sep } from "node:path";
15665
- import { createHash as createHash2 } from "node:crypto";
15855
+ import { extname, join as join2, relative, sep } from "node:path";
15856
+ import { createHash as createHash3 } from "node:crypto";
15666
15857
  import { execFileSync } from "node:child_process";
15667
15858
  function nowTs() {
15668
15859
  return indexNow ?? Date.now();
@@ -15671,7 +15862,7 @@ function fingerprintOf(signature, bodyHash) {
15671
15862
  return `${signature ?? ""}|${bodyHash ?? ""}`;
15672
15863
  }
15673
15864
  function sha(s) {
15674
- return createHash2("sha256").update(s).digest("hex").slice(0, 16);
15865
+ return createHash3("sha256").update(s).digest("hex").slice(0, 16);
15675
15866
  }
15676
15867
  function fileNodeId(workspaceId, relPath) {
15677
15868
  return `file_${sha(workspaceId + ":" + relPath.replace(/\\/g, "/"))}`;
@@ -15791,10 +15982,11 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15791
15982
  };
15792
15983
  }
15793
15984
  const fileHashes = /* @__PURE__ */ new Map();
15985
+ const contentMovedPaths = /* @__PURE__ */ new Set();
15794
15986
  for (const rel of [...changedRelPaths]) {
15795
15987
  let h;
15796
15988
  try {
15797
- h = createHash2("sha256").update(readFileSync(join(rootPath, rel))).digest("hex");
15989
+ h = createHash3("sha256").update(readFileSync2(join2(rootPath, rel))).digest("hex");
15798
15990
  } catch {
15799
15991
  continue;
15800
15992
  }
@@ -15803,6 +15995,8 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15803
15995
  if (fnode && fnode.attrs["contentHash"] === h && !fileHasStrandedSymbol(store2, fnode.id)) {
15804
15996
  store2.updateNode(fnode.id, { lastUpdatedAt: t });
15805
15997
  changedRelPaths.delete(rel);
15998
+ } else if (!fnode || fnode.attrs["contentHash"] !== h) {
15999
+ contentMovedPaths.add(rel);
15806
16000
  }
15807
16001
  }
15808
16002
  if (changedRelPaths.size === 0) {
@@ -15841,13 +16035,13 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15841
16035
  let parsedFiles = 0;
15842
16036
  for (const rel of changedRelPaths) {
15843
16037
  if (parsedFiles++ > 0) await new Promise((r2) => setImmediate(r2));
15844
- const abs = join(rootPath, ...rel.split("/"));
16038
+ const abs = join2(rootPath, ...rel.split("/"));
15845
16039
  const ext = extname(abs).toLowerCase();
15846
16040
  const provider = providers.find((p) => p.fileExtensions.includes(ext));
15847
16041
  if (!provider) continue;
15848
16042
  let symbols;
15849
16043
  try {
15850
- symbols = provider.extractSymbols(readFileSync(abs, "utf8"), abs);
16044
+ symbols = provider.extractSymbols(readFileSync2(abs, "utf8"), abs);
15851
16045
  } catch {
15852
16046
  continue;
15853
16047
  }
@@ -15937,6 +16131,7 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15937
16131
  changedRelPaths
15938
16132
  });
15939
16133
  store2.transaction(() => {
16134
+ store2.reviveReobserved([...changedRelPaths], workspaceId, t);
15940
16135
  const versionedAndFile = /* @__PURE__ */ new Set([...VERSIONED_LABELS, "File"]);
15941
16136
  const ownedLive = [];
15942
16137
  for (const n of store2.nodesByRelPath([...changedRelPaths])) {
@@ -16045,7 +16240,14 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
16045
16240
  const h = fileHashes.get(rel);
16046
16241
  if (!h) continue;
16047
16242
  const fnode = store2.getNode(fileNodeId(workspaceId, rel));
16048
- if (fnode) store2.updateNode(fnode.id, { attrs: { ...fnode.attrs, contentHash: h } });
16243
+ if (!fnode) continue;
16244
+ store2.updateNode(fnode.id, {
16245
+ attrs: {
16246
+ ...fnode.attrs,
16247
+ contentHash: h,
16248
+ ...contentMovedPaths.has(rel) ? { contentChangedAt: t } : {}
16249
+ }
16250
+ });
16049
16251
  }
16050
16252
  return {
16051
16253
  filesReindexed: changedRelPaths.size,
@@ -16093,8 +16295,13 @@ async function runIndexer(store2, opts) {
16093
16295
  byLanguage: {},
16094
16296
  durationMs: 0,
16095
16297
  nodesPurged: 0,
16096
- edgesPurged: 0
16298
+ edgesPurged: 0,
16299
+ parseCacheHits: 0,
16300
+ parseCacheMisses: 0
16097
16301
  };
16302
+ const cacheDir = parseCacheDir();
16303
+ const parseCacheEnabled = cacheDir !== "";
16304
+ if (parseCacheEnabled) sweepParseCacheOnce(cacheDir);
16098
16305
  if (opts.clean) {
16099
16306
  const purge = purgeWorkspaceCodeGraph(store2, opts.workspaceId);
16100
16307
  report.nodesPurged = purge.nodes;
@@ -16148,25 +16355,38 @@ async function runIndexer(store2, opts) {
16148
16355
  }
16149
16356
  let source;
16150
16357
  try {
16151
- const st = statSync(f.absPath);
16358
+ const st = statSync2(f.absPath);
16152
16359
  if (st.size > maxBytes) {
16153
16360
  report.filesSkipped++;
16154
16361
  continue;
16155
16362
  }
16156
- source = readFileSync(f.absPath, "utf8");
16363
+ source = readFileSync2(f.absPath, "utf8");
16157
16364
  } catch {
16158
16365
  report.filesSkipped++;
16159
16366
  continue;
16160
16367
  }
16368
+ const cacheKey = parseCacheKey(source, f.provider.id);
16369
+ const cached2 = parseCacheEnabled ? readParseCache(cacheDir, cacheKey) : null;
16161
16370
  let symbols;
16162
- try {
16163
- symbols = f.provider.extractSymbols(source, f.absPath);
16164
- } catch {
16165
- report.filesSkipped++;
16166
- continue;
16371
+ let reExports;
16372
+ if (cached2) {
16373
+ symbols = cached2.symbols;
16374
+ reExports = cached2.reExports;
16375
+ report.parseCacheHits++;
16376
+ } else {
16377
+ try {
16378
+ symbols = f.provider.extractSymbols(source, f.absPath);
16379
+ } catch {
16380
+ report.filesSkipped++;
16381
+ continue;
16382
+ }
16383
+ reExports = scanReExports(source);
16384
+ report.parseCacheMisses++;
16385
+ if (parseCacheEnabled) {
16386
+ writeParseCache(cacheDir, cacheKey, f.provider.id, { symbols, reExports });
16387
+ }
16167
16388
  }
16168
16389
  symbolsByFile.set(f.relPath, symbols);
16169
- const reExports = scanReExports(source);
16170
16390
  if (reExports.length > 0) reExportsByFile.set(f.relPath, reExports);
16171
16391
  report.filesParsed++;
16172
16392
  report.byLanguage[f.provider.id] = (report.byLanguage[f.provider.id] ?? 0) + 1;
@@ -16211,7 +16431,7 @@ async function runIndexer(store2, opts) {
16211
16431
  const depth = fileNode.relPath.split("/").length;
16212
16432
  if (depth !== 3) continue;
16213
16433
  try {
16214
- const pkg = JSON.parse(readFileSync(fileNode.absPath, "utf8"));
16434
+ const pkg = JSON.parse(readFileSync2(fileNode.absPath, "utf8"));
16215
16435
  if (!pkg.name) continue;
16216
16436
  const pkgDir = fileNode.relPath.replace(/\/package\.json$/, "");
16217
16437
  const candidates = [
@@ -16593,7 +16813,7 @@ async function scan(root, current, ignores, providers, out2) {
16593
16813
  for (const ent of entries) {
16594
16814
  if (ignores.has(ent.name)) continue;
16595
16815
  if (ent.name.startsWith(".") && ent.name !== ".") continue;
16596
- const abs = join(current, ent.name);
16816
+ const abs = join2(current, ent.name);
16597
16817
  if (ent.isDirectory()) {
16598
16818
  await scan(root, abs, ignores, providers, out2);
16599
16819
  } else if (ent.isFile()) {
@@ -16602,7 +16822,7 @@ async function scan(root, current, ignores, providers, out2) {
16602
16822
  const rel = relative(root, abs).split(sep).join("/");
16603
16823
  let size = 0;
16604
16824
  try {
16605
- size = statSync(abs).size;
16825
+ size = statSync2(abs).size;
16606
16826
  } catch {
16607
16827
  continue;
16608
16828
  }
@@ -16616,7 +16836,7 @@ async function scan(root, current, ignores, providers, out2) {
16616
16836
  }
16617
16837
  }
16618
16838
  }
16619
- function gitListFiles(root, ignores, providers) {
16839
+ function gitListRelPaths(root, ignores) {
16620
16840
  let stdout;
16621
16841
  try {
16622
16842
  stdout = execFileSync(
@@ -16639,10 +16859,19 @@ function gitListFiles(root, ignores, providers) {
16639
16859
  if (segs.some((s) => ignores.has(s) || s.startsWith(".") && s.length > 1)) {
16640
16860
  continue;
16641
16861
  }
16642
- const abs = join(root, rel);
16862
+ out2.push(rel);
16863
+ }
16864
+ return out2;
16865
+ }
16866
+ function gitListFiles(root, ignores, providers) {
16867
+ const rels = gitListRelPaths(root, ignores);
16868
+ if (rels === null) return null;
16869
+ const out2 = [];
16870
+ for (const rel of rels) {
16871
+ const abs = join2(root, rel);
16643
16872
  let size;
16644
16873
  try {
16645
- size = statSync(abs).size;
16874
+ size = statSync2(abs).size;
16646
16875
  } catch {
16647
16876
  continue;
16648
16877
  }
@@ -16662,7 +16891,7 @@ function upsertFile(store2, id, f, workspaceId) {
16662
16891
  const now = nowTs();
16663
16892
  let contentHash;
16664
16893
  try {
16665
- contentHash = createHash2("sha256").update(readFileSync(f.absPath)).digest("hex");
16894
+ contentHash = createHash3("sha256").update(readFileSync2(f.absPath)).digest("hex");
16666
16895
  } catch {
16667
16896
  }
16668
16897
  const node = {
@@ -16861,6 +17090,7 @@ var init_pipeline = __esm({
16861
17090
  "../../packages/indexer/src/pipeline.ts"() {
16862
17091
  "use strict";
16863
17092
  init_src2();
17093
+ init_parse_cache();
16864
17094
  init_identity2();
16865
17095
  DEFAULT_IGNORES = /* @__PURE__ */ new Set([
16866
17096
  "node_modules",
@@ -21025,8 +21255,8 @@ ${JSON.stringify(symbolNames, null, 2)}`);
21025
21255
 
21026
21256
  // ../../packages/indexer/src/languages/tree-sitter-loader.ts
21027
21257
  import { fileURLToPath } from "node:url";
21028
- import { dirname, join as join2 } from "node:path";
21029
- import { existsSync, readdirSync } from "node:fs";
21258
+ import { dirname, join as join3 } from "node:path";
21259
+ import { existsSync as existsSync2, readdirSync as readdirSync2 } from "node:fs";
21030
21260
  import { createRequire } from "node:module";
21031
21261
  function entryDir() {
21032
21262
  try {
@@ -21040,27 +21270,27 @@ function entryDir() {
21040
21270
  }
21041
21271
  function findWasmDir() {
21042
21272
  const here = entryDir();
21043
- const seaWasm = join2(here, "resources", "wasm");
21044
- if (existsSync(join2(seaWasm, "tree-sitter-typescript.wasm"))) return seaWasm;
21273
+ const seaWasm = join3(here, "resources", "wasm");
21274
+ if (existsSync2(join3(seaWasm, "tree-sitter-typescript.wasm"))) return seaWasm;
21045
21275
  let dir = here;
21046
21276
  for (let i2 = 0; i2 < 6; i2++) {
21047
- const flat = join2(
21277
+ const flat = join3(
21048
21278
  dir,
21049
21279
  "node_modules",
21050
21280
  "@vscode",
21051
21281
  "tree-sitter-wasm",
21052
21282
  "wasm"
21053
21283
  );
21054
- if (existsSync(join2(flat, "tree-sitter-typescript.wasm"))) return flat;
21284
+ if (existsSync2(join3(flat, "tree-sitter-typescript.wasm"))) return flat;
21055
21285
  dir = dirname(dir);
21056
21286
  }
21057
21287
  let root = here;
21058
21288
  for (let i2 = 0; i2 < 6; i2++) {
21059
- const pnpmDir = join2(root, "node_modules", ".pnpm");
21060
- if (existsSync(pnpmDir)) {
21061
- for (const entry of readdirSync(pnpmDir)) {
21289
+ const pnpmDir = join3(root, "node_modules", ".pnpm");
21290
+ if (existsSync2(pnpmDir)) {
21291
+ for (const entry of readdirSync2(pnpmDir)) {
21062
21292
  if (entry.startsWith("@vscode+tree-sitter-wasm@")) {
21063
- const candidate = join2(
21293
+ const candidate = join3(
21064
21294
  pnpmDir,
21065
21295
  entry,
21066
21296
  "node_modules",
@@ -21068,7 +21298,7 @@ function findWasmDir() {
21068
21298
  "tree-sitter-wasm",
21069
21299
  "wasm"
21070
21300
  );
21071
- if (existsSync(join2(candidate, "tree-sitter-typescript.wasm"))) {
21301
+ if (existsSync2(join3(candidate, "tree-sitter-typescript.wasm"))) {
21072
21302
  return candidate;
21073
21303
  }
21074
21304
  }
@@ -21082,27 +21312,27 @@ function findWasmDir() {
21082
21312
  }
21083
21313
  function findRuntimeDir() {
21084
21314
  const here = entryDir();
21085
- const seaRuntime = join2(here, "resources", "wasm");
21086
- if (existsSync(join2(seaRuntime, "web-tree-sitter.wasm"))) return seaRuntime;
21315
+ const seaRuntime = join3(here, "resources", "wasm");
21316
+ if (existsSync2(join3(seaRuntime, "web-tree-sitter.wasm"))) return seaRuntime;
21087
21317
  let dir = here;
21088
21318
  for (let i2 = 0; i2 < 6; i2++) {
21089
- const flat = join2(dir, "node_modules", "web-tree-sitter");
21090
- if (existsSync(join2(flat, "web-tree-sitter.wasm"))) return flat;
21319
+ const flat = join3(dir, "node_modules", "web-tree-sitter");
21320
+ if (existsSync2(join3(flat, "web-tree-sitter.wasm"))) return flat;
21091
21321
  dir = dirname(dir);
21092
21322
  }
21093
21323
  let root = here;
21094
21324
  for (let i2 = 0; i2 < 6; i2++) {
21095
- const pnpmDir = join2(root, "node_modules", ".pnpm");
21096
- if (existsSync(pnpmDir)) {
21097
- for (const entry of readdirSync(pnpmDir)) {
21325
+ const pnpmDir = join3(root, "node_modules", ".pnpm");
21326
+ if (existsSync2(pnpmDir)) {
21327
+ for (const entry of readdirSync2(pnpmDir)) {
21098
21328
  if (entry.startsWith("web-tree-sitter@")) {
21099
- const candidate = join2(
21329
+ const candidate = join3(
21100
21330
  pnpmDir,
21101
21331
  entry,
21102
21332
  "node_modules",
21103
21333
  "web-tree-sitter"
21104
21334
  );
21105
- if (existsSync(join2(candidate, "web-tree-sitter.wasm"))) {
21335
+ if (existsSync2(join3(candidate, "web-tree-sitter.wasm"))) {
21106
21336
  return candidate;
21107
21337
  }
21108
21338
  }
@@ -21119,7 +21349,7 @@ async function loadWebTreeSitter() {
21119
21349
  void err2;
21120
21350
  }
21121
21351
  const here = entryDir();
21122
- const seaResourceBase = join2(here, "resources", "_resolve.js");
21352
+ const seaResourceBase = join3(here, "resources", "_resolve.js");
21123
21353
  const resourceRequire = createRequire(seaResourceBase);
21124
21354
  return resourceRequire("web-tree-sitter");
21125
21355
  }
@@ -21134,9 +21364,9 @@ async function ensureTreeSitterReady() {
21134
21364
  await Parser2.init({
21135
21365
  locateFile: (name2) => {
21136
21366
  if (name2 === "tree-sitter.wasm" || name2 === "web-tree-sitter.wasm") {
21137
- return join2(runtime, name2);
21367
+ return join3(runtime, name2);
21138
21368
  }
21139
- return join2(grammars, name2);
21369
+ return join3(grammars, name2);
21140
21370
  }
21141
21371
  });
21142
21372
  })();
@@ -21148,7 +21378,7 @@ async function loadGrammar(name2) {
21148
21378
  if (cached2) return cached2;
21149
21379
  if (!languageClass) throw new Error("tree-sitter not initialized");
21150
21380
  const grammars = findWasmDir();
21151
- const lang = await languageClass.load(join2(grammars, `${name2}.wasm`));
21381
+ const lang = await languageClass.load(join3(grammars, `${name2}.wasm`));
21152
21382
  grammarCache.set(name2, lang);
21153
21383
  return lang;
21154
21384
  }
@@ -21171,7 +21401,7 @@ var init_tree_sitter_loader = __esm({
21171
21401
  });
21172
21402
 
21173
21403
  // ../../packages/indexer/src/languages/typescript-treesitter.ts
21174
- import { createHash as createHash3 } from "node:crypto";
21404
+ import { createHash as createHash4 } from "node:crypto";
21175
21405
  function extractNode(node, source, containerQname, containerKind) {
21176
21406
  switch (node.type) {
21177
21407
  case "function_declaration":
@@ -21384,7 +21614,7 @@ function scopeRecord(kind, name2, qname, node, body2, source) {
21384
21614
  const sig = header.replace(/\s+/g, " ").trim();
21385
21615
  if (sig) rec.signature = sig;
21386
21616
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
21387
- rec.bodyHash = createHash3("sha256").update(bodyText).digest("hex").slice(0, 16);
21617
+ rec.bodyHash = createHash4("sha256").update(bodyText).digest("hex").slice(0, 16);
21388
21618
  rec.bodySimhash = toHex(simhash(bodyText));
21389
21619
  }
21390
21620
  }
@@ -21826,14 +22056,14 @@ var init_typescript_treesitter = __esm({
21826
22056
  });
21827
22057
 
21828
22058
  // ../../packages/indexer/src/languages/python-treesitter.ts
21829
- import { createHash as createHash4 } from "node:crypto";
22059
+ import { createHash as createHash5 } from "node:crypto";
21830
22060
  function sigAndHash(node, body2, source) {
21831
22061
  if (!body2) return {};
21832
22062
  const out2 = {};
21833
22063
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
21834
22064
  if (sig) out2.signature = sig;
21835
22065
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
21836
- out2.bodyHash = createHash4("sha256").update(bodyText).digest("hex").slice(0, 16);
22066
+ out2.bodyHash = createHash5("sha256").update(bodyText).digest("hex").slice(0, 16);
21837
22067
  out2.bodySimhash = toHex(simhash(bodyText));
21838
22068
  return out2;
21839
22069
  }
@@ -22246,7 +22476,7 @@ var init_python_treesitter = __esm({
22246
22476
  });
22247
22477
 
22248
22478
  // ../../packages/indexer/src/languages/cpp-treesitter.ts
22249
- import { createHash as createHash5 } from "node:crypto";
22479
+ import { createHash as createHash6 } from "node:crypto";
22250
22480
  function extractNode3(node, containerQname, containerIsClass, source) {
22251
22481
  switch (node.type) {
22252
22482
  case "function_definition":
@@ -22430,7 +22660,7 @@ function sigAndHash2(node, body2, source) {
22430
22660
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
22431
22661
  if (sig) out2.signature = sig;
22432
22662
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
22433
- out2.bodyHash = createHash5("sha256").update(bodyText).digest("hex").slice(0, 16);
22663
+ out2.bodyHash = createHash6("sha256").update(bodyText).digest("hex").slice(0, 16);
22434
22664
  out2.bodySimhash = toHex(simhash(bodyText));
22435
22665
  return out2;
22436
22666
  }
@@ -22591,7 +22821,7 @@ var init_cpp_treesitter = __esm({
22591
22821
  });
22592
22822
 
22593
22823
  // ../../packages/indexer/src/languages/go-treesitter.ts
22594
- import { createHash as createHash6 } from "node:crypto";
22824
+ import { createHash as createHash7 } from "node:crypto";
22595
22825
  function extractNode4(node, containerQname, source) {
22596
22826
  switch (node.type) {
22597
22827
  case "function_declaration": {
@@ -22753,7 +22983,7 @@ function sigAndHash3(node, body2, source) {
22753
22983
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
22754
22984
  if (sig) out2.signature = sig;
22755
22985
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
22756
- out2.bodyHash = createHash6("sha256").update(bodyText).digest("hex").slice(0, 16);
22986
+ out2.bodyHash = createHash7("sha256").update(bodyText).digest("hex").slice(0, 16);
22757
22987
  out2.bodySimhash = toHex(simhash(bodyText));
22758
22988
  return out2;
22759
22989
  }
@@ -22897,7 +23127,7 @@ var init_go_treesitter = __esm({
22897
23127
  });
22898
23128
 
22899
23129
  // ../../packages/indexer/src/languages/rust-treesitter.ts
22900
- import { createHash as createHash7 } from "node:crypto";
23130
+ import { createHash as createHash8 } from "node:crypto";
22901
23131
  function extractNode5(node, containerQname, containerIsClass, source) {
22902
23132
  switch (node.type) {
22903
23133
  case "function_item":
@@ -23112,7 +23342,7 @@ function sigAndHash4(node, body2, source) {
23112
23342
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
23113
23343
  if (sig) out2.signature = sig;
23114
23344
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
23115
- out2.bodyHash = createHash7("sha256").update(bodyText).digest("hex").slice(0, 16);
23345
+ out2.bodyHash = createHash8("sha256").update(bodyText).digest("hex").slice(0, 16);
23116
23346
  out2.bodySimhash = toHex(simhash(bodyText));
23117
23347
  return out2;
23118
23348
  }
@@ -23263,7 +23493,7 @@ var init_rust_treesitter = __esm({
23263
23493
  });
23264
23494
 
23265
23495
  // ../../packages/indexer/src/languages/ruby-treesitter.ts
23266
- import { createHash as createHash8 } from "node:crypto";
23496
+ import { createHash as createHash9 } from "node:crypto";
23267
23497
  function sigAndHash5(node, body2, source) {
23268
23498
  if (!body2) {
23269
23499
  const sig2 = source.slice(node.startIndex, node.endIndex).replace(/\s+/g, " ").trim();
@@ -23273,7 +23503,7 @@ function sigAndHash5(node, body2, source) {
23273
23503
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
23274
23504
  if (sig) out2.signature = sig;
23275
23505
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
23276
- out2.bodyHash = createHash8("sha256").update(bodyText).digest("hex").slice(0, 16);
23506
+ out2.bodyHash = createHash9("sha256").update(bodyText).digest("hex").slice(0, 16);
23277
23507
  out2.bodySimhash = toHex(simhash(bodyText));
23278
23508
  return out2;
23279
23509
  }
@@ -23619,7 +23849,7 @@ var init_ruby_treesitter = __esm({
23619
23849
  });
23620
23850
 
23621
23851
  // ../../packages/indexer/src/languages/csharp-treesitter.ts
23622
- import { createHash as createHash9 } from "node:crypto";
23852
+ import { createHash as createHash10 } from "node:crypto";
23623
23853
  function extractNode7(node, containerQname, containerIsType, source, eof) {
23624
23854
  switch (node.type) {
23625
23855
  case "method_declaration":
@@ -23821,7 +24051,7 @@ function sigAndHash6(node, body2, source) {
23821
24051
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
23822
24052
  if (sig) out2.signature = sig;
23823
24053
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
23824
- out2.bodyHash = createHash9("sha256").update(bodyText).digest("hex").slice(0, 16);
24054
+ out2.bodyHash = createHash10("sha256").update(bodyText).digest("hex").slice(0, 16);
23825
24055
  out2.bodySimhash = toHex(simhash(bodyText));
23826
24056
  return out2;
23827
24057
  }
@@ -23978,6 +24208,7 @@ var init_csharp_treesitter = __esm({
23978
24208
  // ../../packages/indexer/src/index.ts
23979
24209
  var src_exports = {};
23980
24210
  __export(src_exports, {
24211
+ DEFAULT_IGNORES: () => DEFAULT_IGNORES,
23981
24212
  DRIFT_FORK_RATIO: () => DRIFT_FORK_RATIO,
23982
24213
  TreeSitterCSharpProvider: () => TreeSitterCSharpProvider,
23983
24214
  TreeSitterCppProvider: () => TreeSitterCppProvider,
@@ -23994,6 +24225,7 @@ __export(src_exports, {
23994
24225
  findInnermostScope: () => findInnermostScope,
23995
24226
  folderNodeId: () => folderNodeId,
23996
24227
  fromHex: () => fromHex,
24228
+ gitListRelPaths: () => gitListRelPaths,
23997
24229
  hammingDistance: () => hammingDistance,
23998
24230
  hasForkedDrift: () => hasForkedDrift,
23999
24231
  incrementalReindex: () => incrementalReindex,
@@ -24006,7 +24238,7 @@ __export(src_exports, {
24006
24238
  simhashFeatures: () => simhashFeatures,
24007
24239
  symbolNodeId: () => symbolNodeId,
24008
24240
  toHex: () => toHex,
24009
- tokenize: () => tokenize
24241
+ tokenize: () => tokenize2
24010
24242
  });
24011
24243
  function defaultProviders() {
24012
24244
  if (providersCache) return providersCache;
@@ -24059,9 +24291,38 @@ var LOCAL_RULE_OVERRIDES = {
24059
24291
  to: [...CODE_NODE_LABELS, "Symbol"]
24060
24292
  },
24061
24293
  REVEALED_BY: null,
24062
- PRODUCED: null
24294
+ PRODUCED: null,
24295
+ // FIXED_BY locally ALSO carries the fix-provenance sense: `resolveProblem`
24296
+ // attributes a closed Problem to the fixing `Episode` (PLAN_PLASTICITY §2.1)
24297
+ // alongside the cloud's Problem→Solution knowledge claim. Same shape as
24298
+ // PRODUCED/REVEALED_BY above — an Episode is code-layer and never drains, so
24299
+ // the cloud door's `to: [Solution]` rule is untouched. The `from` constraint
24300
+ // stays: the reversed (Solution)-FIXED_BY->(...) splash bug is the reason
24301
+ // this rule exists at all.
24302
+ FIXED_BY: { from: EDGE_RULES["FIXED_BY"]?.from, to: ["Solution", "Episode"] },
24303
+ // RG-recognize: a NEW open Problem re-observing a RESOLVED one ("this is
24304
+ // back — the fix didn't hold"). Local-only for now: the instance drain's
24305
+ // INSTANCE_EDGES allowlist doesn't ship it, and the cloud door's matrix
24306
+ // doesn't know it — crossing the membrane is a deliberate later step, never
24307
+ // a side effect (the half-shipped-edge-type lesson).
24308
+ REGRESSION_OF: { from: ["Problem"], to: ["Problem"] }
24063
24309
  };
24064
- var SCHEMA_VERSION = 5;
24310
+ function localEdgeViolation(fromLabel, type, toLabel) {
24311
+ if (type in LOCAL_RULE_OVERRIDES) {
24312
+ const rule = LOCAL_RULE_OVERRIDES[type];
24313
+ if (!rule) return null;
24314
+ if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24315
+ return `${type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24316
+ }
24317
+ if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24318
+ return `${type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24319
+ }
24320
+ return null;
24321
+ }
24322
+ const verdict = isValidEdge(fromLabel, type, toLabel);
24323
+ return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24324
+ }
24325
+ var SCHEMA_VERSION = 6;
24065
24326
  var SCHEMA_SQL = `
24066
24327
  CREATE TABLE IF NOT EXISTS schema_version (
24067
24328
  version INTEGER PRIMARY KEY
@@ -24076,6 +24337,21 @@ CREATE TABLE IF NOT EXISTS store_meta (
24076
24337
  value TEXT NOT NULL
24077
24338
  );
24078
24339
 
24340
+ -- Durable ledger of edges the ontology gate REFUSED, keyed by edge type.
24341
+ -- Durable rather than in-memory for one specific reason: the status command runs
24342
+ -- in a SEPARATE process and opens its own store handle, so a counter living on
24343
+ -- the instance reads 0 there forever. That is exactly how a producer rejecting
24344
+ -- 100% of its output stayed invisible for 17 days. Persisting it also survives
24345
+ -- the daemon restart that would otherwise erase the evidence.
24346
+ -- Keyed by type because a systematic producer bug shows up as ONE type
24347
+ -- dominating; sample keeps the latest reason so the count is actionable.
24348
+ CREATE TABLE IF NOT EXISTS edge_rejections (
24349
+ type TEXT PRIMARY KEY,
24350
+ count INTEGER NOT NULL DEFAULT 0,
24351
+ last_at INTEGER NOT NULL,
24352
+ sample TEXT
24353
+ );
24354
+
24079
24355
  CREATE TABLE IF NOT EXISTS nodes (
24080
24356
  id TEXT PRIMARY KEY,
24081
24357
  label TEXT NOT NULL,
@@ -24452,6 +24728,16 @@ var SqliteGraphStore = class {
24452
24728
  if (!cols.has(name2)) this.db.exec(ddl);
24453
24729
  }
24454
24730
  }
24731
+ if (from < 6) {
24732
+ this.db.exec(`
24733
+ CREATE TABLE IF NOT EXISTS edge_rejections (
24734
+ type TEXT PRIMARY KEY,
24735
+ count INTEGER NOT NULL DEFAULT 0,
24736
+ last_at INTEGER NOT NULL,
24737
+ sample TEXT
24738
+ )
24739
+ `);
24740
+ }
24455
24741
  this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
24456
24742
  });
24457
24743
  }
@@ -24533,6 +24819,7 @@ var SqliteGraphStore = class {
24533
24819
  const violation = this.edgeRuleViolation(edge);
24534
24820
  if (violation) {
24535
24821
  this.rejectedEdgeCount++;
24822
+ this.recordEdgeRejection(edge.type, violation, edge.lastSeenAt || edge.createdAt || 0);
24536
24823
  console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
24537
24824
  return;
24538
24825
  }
@@ -24557,22 +24844,51 @@ var SqliteGraphStore = class {
24557
24844
  * overlay consulted first. Returns the reason string on a documented-
24558
24845
  * forbidden combination, else null. Point lookups on the id PK — negligible
24559
24846
  * next to the insert itself. */
24560
- edgeRuleViolation(edge) {
24561
- const fromLabel = this.getNode(edge.from)?.label;
24562
- const toLabel = this.getNode(edge.to)?.label;
24563
- if (edge.type in LOCAL_RULE_OVERRIDES) {
24564
- const rule = LOCAL_RULE_OVERRIDES[edge.type];
24565
- if (!rule) return null;
24566
- if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24567
- return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24568
- }
24569
- if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24570
- return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24571
- }
24572
- return null;
24847
+ /** Upsert one refusal into the durable ledger. Best-effort: a bookkeeping
24848
+ * failure must never turn a refused edge into a thrown write. */
24849
+ recordEdgeRejection(type, reason, at) {
24850
+ try {
24851
+ this.db.prepare(
24852
+ `INSERT INTO edge_rejections (type, count, last_at, sample) VALUES (?, 1, ?, ?)
24853
+ ON CONFLICT(type) DO UPDATE SET
24854
+ count = count + 1, last_at = excluded.last_at, sample = excluded.sample`
24855
+ ).run(type, at, reason.slice(0, 200));
24856
+ } catch {
24573
24857
  }
24574
- const verdict = isValidEdge(fromLabel, edge.type, toLabel);
24575
- return verdict.ok ? null : verdict.reason ?? "edge rule violation";
24858
+ }
24859
+ /** Refusals recorded by the ontology gate, per edge type, newest activity first.
24860
+ * Durable across restarts and readable from any process (see the table note). */
24861
+ edgeRejections() {
24862
+ try {
24863
+ return this.db.prepare(
24864
+ "SELECT type, count, last_at AS lastAt, sample FROM edge_rejections ORDER BY count DESC, last_at DESC"
24865
+ ).all();
24866
+ } catch {
24867
+ return [];
24868
+ }
24869
+ }
24870
+ /** Drop ledger entries whose last refusal predates `cutoff`. A still-misbehaving
24871
+ * producer keeps refreshing `last_at` and survives; a fixed one fades out. */
24872
+ pruneEdgeRejections(cutoff) {
24873
+ try {
24874
+ this.db.prepare("DELETE FROM edge_rejections WHERE last_at < ?").run(cutoff);
24875
+ } catch {
24876
+ }
24877
+ }
24878
+ /** Clear the ledger outright, whole or per type — operator escape hatch. */
24879
+ clearEdgeRejections(type) {
24880
+ try {
24881
+ if (type) this.db.prepare("DELETE FROM edge_rejections WHERE type = ?").run(type);
24882
+ else this.db.exec("DELETE FROM edge_rejections");
24883
+ } catch {
24884
+ }
24885
+ }
24886
+ edgeRuleViolation(edge) {
24887
+ return localEdgeViolation(
24888
+ this.getNode(edge.from)?.label,
24889
+ edge.type,
24890
+ this.getNode(edge.to)?.label
24891
+ );
24576
24892
  }
24577
24893
  updateEdge(id, patch) {
24578
24894
  this.stmts.updateEdge.run({
@@ -24709,6 +25025,163 @@ var SqliteGraphStore = class {
24709
25025
  const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
24710
25026
  return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
24711
25027
  }
25028
+ getMeta(key) {
25029
+ const r = this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key);
25030
+ return r?.value ?? null;
25031
+ }
25032
+ setMeta(key, value) {
25033
+ this.db.prepare(
25034
+ "INSERT INTO store_meta (key, value) VALUES (:key, :value) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
25035
+ ).run({ key, value });
25036
+ }
25037
+ dirtyNodeIdsSince(ts) {
25038
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
25039
+ return rows.map((r) => r.id);
25040
+ }
25041
+ /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
25042
+ * Closures matter as much as additions: a node that lost inflow is rank-dirty
25043
+ * while its own row never updated, so removal endpoints must seed the
25044
+ * incremental region or the stale inflow persists until the full backstop. */
25045
+ edgeEndpointsTouchedSince(ts) {
25046
+ const rows = this.db.prepare(
25047
+ `SELECT from_id, to_id FROM edges
25048
+ WHERE (valid_to IS NULL AND (created_at > :ts OR last_seen_at > :ts))
25049
+ OR (valid_to IS NOT NULL AND valid_to > :ts)`
25050
+ ).all({ ts });
25051
+ return rows.map((r) => ({ from: r.from_id, to: r.to_id }));
25052
+ }
25053
+ /** Lightweight out-edges for a SET of sources, chunked IN-lists — the region
25054
+ * assembly path for incremental PageRank (per-node outEdges() at region
25055
+ * scale re-creates the 106k-prepared-calls problem the batch scan solved). */
25056
+ outEdgesForMany(ids) {
25057
+ return this.edgesForMany(ids, "from_id");
25058
+ }
25059
+ inEdgesForMany(ids) {
25060
+ return this.edgesForMany(ids, "to_id");
25061
+ }
25062
+ /** Stored pageRank for a SET of ids (live rows only — a closed id is simply
25063
+ * absent, which is how incremental region assembly drops dead endpoints). */
25064
+ ranksForMany(ids) {
25065
+ const out2 = /* @__PURE__ */ new Map();
25066
+ const CHUNK = 400;
25067
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
25068
+ const chunk = ids.slice(i2, i2 + CHUNK);
25069
+ const placeholders = chunk.map(() => "?").join(", ");
25070
+ const rows = this.db.prepare(
25071
+ `SELECT id, page_rank FROM nodes WHERE id IN (${placeholders}) AND valid_to IS NULL`
25072
+ ).all(...chunk);
25073
+ for (const r of rows) out2.set(r.id, r.page_rank);
25074
+ }
25075
+ return out2;
25076
+ }
25077
+ /** The minimal row set the landmark sweep needs — landmark CANDIDATES
25078
+ * (Pattern/RootCause), everything currently flagged, and every
25079
+ * persistent-tier node (force-landmarks). A few thousand rows, so the
25080
+ * incremental path can refresh the GLOBAL landmark set without the 179k-row
25081
+ * scanLiveNodes materialization. */
25082
+ landmarkSweepRows() {
25083
+ const rows = this.db.prepare(
25084
+ `SELECT id, label, page_rank, is_landmark, memory_tier, extraction_source FROM nodes
25085
+ WHERE valid_to IS NULL
25086
+ AND (memory_tier = 'persistent' OR is_landmark = 1 OR label IN ('Pattern', 'RootCause'))`
25087
+ ).all();
25088
+ return rows.map((r) => ({
25089
+ id: r.id,
25090
+ label: r.label,
25091
+ pageRank: r.page_rank,
25092
+ isLandmark: r.is_landmark === 1,
25093
+ memoryTier: r.memory_tier,
25094
+ extractionSource: r.extraction_source
25095
+ }));
25096
+ }
25097
+ edgesForMany(ids, col) {
25098
+ const out2 = [];
25099
+ const CHUNK = 400;
25100
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
25101
+ const chunk = ids.slice(i2, i2 + CHUNK);
25102
+ const placeholders = chunk.map(() => "?").join(", ");
25103
+ const rows = this.db.prepare(
25104
+ `SELECT from_id, to_id, type FROM edges WHERE ${col} IN (${placeholders}) AND valid_to IS NULL`
25105
+ ).all(...chunk);
25106
+ for (const r of rows) out2.push({ from: r.from_id, to: r.to_id, type: r.type });
25107
+ }
25108
+ return out2;
25109
+ }
25110
+ checkpointWal() {
25111
+ try {
25112
+ const r = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
25113
+ return r ?? null;
25114
+ } catch {
25115
+ return null;
25116
+ }
25117
+ }
25118
+ reviveReobserved(relPaths, workspaceId, since) {
25119
+ let revived = 0;
25120
+ const CHUNK = 400;
25121
+ for (let i2 = 0; i2 < relPaths.length; i2 += CHUNK) {
25122
+ const slice = relPaths.slice(i2, i2 + CHUNK);
25123
+ if (slice.length === 0) continue;
25124
+ const r = this.db.prepare(
25125
+ `UPDATE nodes SET valid_to = NULL
25126
+ WHERE valid_to IS NOT NULL
25127
+ AND last_updated_at >= ?
25128
+ AND json_extract(attrs_json, '$.workspaceId') = ?
25129
+ AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})`
25130
+ ).run(since, workspaceId, ...slice);
25131
+ revived += Number(r.changes);
25132
+ }
25133
+ if (revived > 0) this.mutations++;
25134
+ return revived;
25135
+ }
25136
+ recentEdgeAttrCoverage(marker, attr, limit) {
25137
+ const r = this.db.prepare(
25138
+ `SELECT COUNT(*) AS total,
25139
+ COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
25140
+ FROM (SELECT attrs_json FROM edges
25141
+ WHERE valid_to IS NULL AND attrs_json LIKE ?
25142
+ ORDER BY created_at DESC LIMIT ?)`
25143
+ ).get(attr, `%${marker}%`, limit);
25144
+ return { count: Number(r.count), total: Number(r.total) };
25145
+ }
25146
+ liveNodeIds(ids) {
25147
+ const live = /* @__PURE__ */ new Set();
25148
+ const CHUNK = 900;
25149
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
25150
+ const slice = ids.slice(i2, i2 + CHUNK);
25151
+ if (slice.length === 0) continue;
25152
+ const rows = this.db.prepare(
25153
+ `SELECT id FROM nodes WHERE valid_to IS NULL AND id IN (${slice.map(() => "?").join(",")})`
25154
+ ).all(...slice);
25155
+ for (const r of rows) live.add(r.id);
25156
+ }
25157
+ return live;
25158
+ }
25159
+ /**
25160
+ * Live edges WITH their endpoint labels and ids, resolved in ONE join.
25161
+ *
25162
+ * The ontology sweep needs (id, type, fromLabel, toLabel) for every live edge.
25163
+ * Doing that as `scanLiveEdges()` + two `getNode()` calls is 2N node reads —
25164
+ * 550,000 on this store — and each one deserializes the node's embedding blob.
25165
+ * Measured: the sweep did not finish in 10 minutes. As a single join it is one
25166
+ * query over an index-covered scan. Labels only; nothing here touches embeddings.
25167
+ */
25168
+ scanLiveEdgeRows() {
25169
+ const rows = this.db.prepare(
25170
+ `SELECT e.id, e.from_id, e.to_id, e.type, a.label AS from_label, b.label AS to_label
25171
+ FROM edges e
25172
+ LEFT JOIN nodes a ON a.id = e.from_id AND a.valid_to IS NULL
25173
+ LEFT JOIN nodes b ON b.id = e.to_id AND b.valid_to IS NULL
25174
+ WHERE e.valid_to IS NULL`
25175
+ ).all();
25176
+ return rows.map((r) => ({
25177
+ id: r.id,
25178
+ from: r.from_id,
25179
+ to: r.to_id,
25180
+ type: r.type,
25181
+ fromLabel: r.from_label ?? void 0,
25182
+ toLabel: r.to_label ?? void 0
25183
+ }));
25184
+ }
24712
25185
  /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
24713
25186
  * expression index so the incremental reindex fetches only the changed files'
24714
25187
  * symbols instead of scanning every versioned node. */
@@ -24729,6 +25202,10 @@ var SqliteGraphStore = class {
24729
25202
  if (!live) return null;
24730
25203
  const version2 = live.version ?? 1;
24731
25204
  const frozenId = `${liveId}@v${version2}`;
25205
+ if (this.getNode(frozenId)) {
25206
+ this.stmts.advanceLive.run({ live_id: liveId, t });
25207
+ return frozenId;
25208
+ }
24732
25209
  this.stmts.freezeCopy.run({ frozen_id: frozenId, live_id: liveId, t });
24733
25210
  this.mergeEdge({
24734
25211
  id: `edge_superseded_${frozenId}`,
@@ -24787,6 +25264,7 @@ var CAUSAL_FAMILY = [
24787
25264
 
24788
25265
  // ../../packages/local-graph/src/justification.ts
24789
25266
  init_src();
25267
+ init_src2();
24790
25268
  function addDependency(store2, opts) {
24791
25269
  const id = digest({ from: opts.fromId, type: "DEPENDS_ON", to: opts.toId });
24792
25270
  const attrs = { relation: opts.relation };
@@ -24809,13 +25287,72 @@ function addDependency(store2, opts) {
24809
25287
  store2.mergeEdge(edge);
24810
25288
  return edge;
24811
25289
  }
24812
- function markRevisit(store2, node, reason, ts) {
25290
+ function markRevisit(store2, node, reason, ts, answeredWhen) {
24813
25291
  const fresh = store2.getNode(node.id) ?? node;
24814
25292
  store2.updateNode(node.id, {
24815
- attrs: { ...fresh.attrs, revisit: true, revisitReason: reason, revisitSinceTs: ts },
25293
+ attrs: {
25294
+ ...fresh.attrs,
25295
+ revisit: true,
25296
+ revisitReason: reason,
25297
+ revisitSinceTs: ts,
25298
+ revisitAnsweredWhen: answeredWhen
25299
+ },
24816
25300
  lastUpdatedAt: ts
24817
25301
  });
24818
25302
  }
25303
+ var LEGACY_AUTO_CLOSE_ASK = "auto-closed off a pre-provenance anchor";
25304
+ var KNOWN_CONDITIONS = {
25305
+ parentProblemOpen: true,
25306
+ dependentStale: true
25307
+ };
25308
+ function isRevisitCondition(v) {
25309
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(KNOWN_CONDITIONS, v);
25310
+ }
25311
+ function conditionOf(node) {
25312
+ const explicit = node.attrs["revisitAnsweredWhen"];
25313
+ if (isRevisitCondition(explicit)) return explicit;
25314
+ if (String(node.attrs["revisitReason"] ?? "").startsWith(LEGACY_AUTO_CLOSE_ASK)) {
25315
+ return "parentProblemOpen";
25316
+ }
25317
+ return void 0;
25318
+ }
25319
+ function clearAnsweredRevisits(store2, ts) {
25320
+ const report = { cleared: 0, standing: 0 };
25321
+ for (const label of SEMANTIC_NODE_LABELS) {
25322
+ for (const node of store2.findNodesByLabel(label)) {
25323
+ if (node.attrs["revisit"] !== true) continue;
25324
+ const condition = conditionOf(node);
25325
+ if (condition === void 0 || !isRevisitAnswered(store2, node, condition)) {
25326
+ report.standing++;
25327
+ continue;
25328
+ }
25329
+ clearRevisit(store2, node.id, ts);
25330
+ report.cleared++;
25331
+ }
25332
+ }
25333
+ return report;
25334
+ }
25335
+ function isRevisitAnswered(store2, node, condition) {
25336
+ switch (condition) {
25337
+ case "parentProblemOpen": {
25338
+ const parents = store2.inEdges(node.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.from)).filter((n) => n !== null && n.validTo == null);
25339
+ return parents.every((p) => p.attrs["resolvedAt"] == null);
25340
+ }
25341
+ case "dependentStale": {
25342
+ const since = Number(node.attrs["revisitSinceTs"] ?? 0);
25343
+ return since > 0 && node.lastUpdatedAt > since;
25344
+ }
25345
+ }
25346
+ }
25347
+ function clearRevisit(store2, nodeId, ts) {
25348
+ const n = store2.getNode(nodeId);
25349
+ if (!n) return;
25350
+ const attrs = { ...n.attrs };
25351
+ delete attrs["revisit"];
25352
+ delete attrs["revisitReason"];
25353
+ delete attrs["revisitSinceTs"];
25354
+ store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
25355
+ }
24819
25356
  function listNeedsRevisit(store2, asOf) {
24820
25357
  return store2.nodesAsOf(asOf).filter((n) => n.attrs["revisit"] === true).map((n) => ({
24821
25358
  id: n.id,
@@ -24948,7 +25485,6 @@ function markBoth(store2, a, b, patch, ts) {
24948
25485
  // ../../packages/local-graph/src/design-problem.ts
24949
25486
  init_src();
24950
25487
  init_src();
24951
- init_src2();
24952
25488
 
24953
25489
  // ../../packages/local-graph/src/problem-package-link.ts
24954
25490
  init_src();
@@ -25134,50 +25670,114 @@ function backfillProblemContext(store2, ts) {
25134
25670
  }
25135
25671
 
25136
25672
  // ../../packages/local-graph/src/design-problem.ts
25137
- function buildNode(id, label, description, ts, attrs) {
25138
- return {
25139
- id,
25140
- label,
25141
- description,
25142
- extractionConfidence: 0.4,
25143
- // provisional: below the error-path's 0.6+
25144
- extractionSource: "agent-observed",
25145
- embedding: [],
25146
- cumulativeSurprise: 0,
25147
- peakSurprise: 0,
25148
- cumulativeHits: 1,
25149
- lastUpdatedAt: ts,
25150
- createdAt: ts,
25151
- memoryTier: "short-term",
25152
- pageRank: 0,
25153
- isLandmark: false,
25154
- community: null,
25155
- stability: "unstable",
25156
- attrs
25157
- };
25673
+ init_src2();
25674
+
25675
+ // ../../packages/local-graph/src/problem-dedup.ts
25676
+ init_src();
25677
+ init_src();
25678
+ var REDIRECT_EDGES = [
25679
+ "CAUSED_BY",
25680
+ "SOLVED_BY",
25681
+ "FIXED_BY",
25682
+ "ANCHORED_AT",
25683
+ "MANIFESTED_IN",
25684
+ "EVIDENCED_BY"
25685
+ ];
25686
+ function corroborations(n) {
25687
+ return Number(n.attrs["corroborations"] ?? 0);
25158
25688
  }
25159
- function mergeEdge(store2, from, to, type, ts) {
25160
- store2.mergeEdge({
25161
- id: `edge_${digest({ from, type, to })}`.slice(0, 24),
25162
- from,
25163
- to,
25164
- type,
25165
- confidence: 0.4,
25166
- extractionSource: "agent-observed",
25167
- createdAt: ts,
25168
- lastSeenAt: ts,
25169
- navSuccesses: 0,
25170
- navFailures: 0,
25171
- attrs: { provisional: true }
25689
+ function overlap(a, b) {
25690
+ if (a.size === 0 || b.size === 0) return 0;
25691
+ let inter = 0;
25692
+ for (const x of a) if (b.has(x)) inter++;
25693
+ return inter / Math.min(a.size, b.size);
25694
+ }
25695
+ function mergeDuplicateProblems(store2, opts) {
25696
+ const minOverlap = opts.minOverlap ?? 0.85;
25697
+ const minTokens = opts.minTokens ?? 4;
25698
+ const report = { clusters: 0, merged: 0 };
25699
+ const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
25700
+ const tokens = /* @__PURE__ */ new Map();
25701
+ for (const p of open) tokens.set(p.id, new Set(retrievalTokens(p.description)));
25702
+ const consumed = /* @__PURE__ */ new Set();
25703
+ store2.transaction(() => {
25704
+ for (let i2 = 0; i2 < open.length; i2++) {
25705
+ const a = open[i2];
25706
+ if (consumed.has(a.id)) continue;
25707
+ const cluster = [a];
25708
+ const ta = tokens.get(a.id);
25709
+ for (let j = i2 + 1; j < open.length; j++) {
25710
+ const b = open[j];
25711
+ if (consumed.has(b.id)) continue;
25712
+ const tb = tokens.get(b.id);
25713
+ if (Math.min(ta.size, tb.size) < minTokens) continue;
25714
+ if (isConstraintProblem(a) !== isConstraintProblem(b)) continue;
25715
+ if (overlap(ta, tb) >= minOverlap) {
25716
+ cluster.push(b);
25717
+ consumed.add(b.id);
25718
+ }
25719
+ }
25720
+ if (cluster.length < 2) continue;
25721
+ report.clusters++;
25722
+ cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
25723
+ const survivor = cluster[0];
25724
+ for (const dup of cluster.slice(1)) {
25725
+ foldDuplicateProblem(store2, dup, survivor, opts.ts);
25726
+ report.merged++;
25727
+ }
25728
+ }
25172
25729
  });
25730
+ return report;
25173
25731
  }
25174
- var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
25732
+ function foldDuplicateProblem(store2, dup, survivor, ts) {
25733
+ const surv = store2.getNode(survivor.id);
25734
+ if (!surv) return;
25735
+ const sources = new Set(surv.attrs["sources"] ?? []);
25736
+ for (const s of dup.attrs["sources"] ?? []) sources.add(s);
25737
+ store2.updateNode(survivor.id, {
25738
+ attrs: {
25739
+ ...surv.attrs,
25740
+ sources: [...sources],
25741
+ corroborations: corroborations(surv) + corroborations(dup) + 1
25742
+ },
25743
+ cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
25744
+ lastUpdatedAt: ts
25745
+ });
25746
+ for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
25747
+ if (e.to === survivor.id) continue;
25748
+ const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
25749
+ if (store2.getEdge(id)) continue;
25750
+ const redirected = {
25751
+ ...e,
25752
+ id,
25753
+ from: survivor.id,
25754
+ createdAt: ts,
25755
+ lastSeenAt: ts
25756
+ };
25757
+ store2.mergeEdge(redirected);
25758
+ }
25759
+ store2.updateNode(dup.id, {
25760
+ attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
25761
+ lastUpdatedAt: ts
25762
+ });
25763
+ store2.closeNode(dup.id, ts);
25764
+ }
25765
+
25766
+ // ../../packages/local-graph/src/design-problem.ts
25767
+ function isConstraintProblem(node) {
25768
+ return node.attrs["kind"] === "constraint";
25769
+ }
25770
+ var RECURRENCE_DEBOUNCE_MS = 60 * 60 * 1e3;
25771
+ var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
25175
25772
  "Solution",
25176
25773
  "RootCause",
25177
25774
  "Pattern",
25178
25775
  "Technique",
25179
25776
  "AntiPattern"
25180
25777
  ]);
25778
+ function isAutoMinted(n) {
25779
+ return n.label === "Solution" && String(n.description ?? "").startsWith(AUTO_MINT_PREFIX);
25780
+ }
25181
25781
  function priorsForFile(store2, relPath) {
25182
25782
  const want = relPath.trim().replace(/^\.?\//, "");
25183
25783
  let file2 = null;
@@ -25189,35 +25789,53 @@ function priorsForFile(store2, relPath) {
25189
25789
  }
25190
25790
  if (!file2) return null;
25191
25791
  const openProblems = [];
25792
+ const constraints = [];
25793
+ const resolvedProblems = [];
25192
25794
  const seenProblem = /* @__PURE__ */ new Set();
25193
25795
  const related = /* @__PURE__ */ new Map();
25194
25796
  for (const e of store2.inEdges(file2.id, ["ANCHORED_AT"])) {
25195
25797
  const n = store2.getNode(e.from);
25196
25798
  if (!n) continue;
25197
25799
  if (n.label === "Problem") {
25198
- if (!n.attrs["resolvedAt"] && !seenProblem.has(n.id)) {
25199
- seenProblem.add(n.id);
25200
- openProblems.push(n);
25201
- }
25202
- } else if (CITABLE_PRIOR_LABELS.has(n.label)) {
25800
+ if (seenProblem.has(n.id)) continue;
25801
+ seenProblem.add(n.id);
25802
+ if (!n.attrs["resolvedAt"]) {
25803
+ (isConstraintProblem(n) ? constraints : openProblems).push(n);
25804
+ } else if (n.attrs["resolvedAs"] == null && n.attrs["mergedInto"] === void 0 && e.attrs?.["anchorProvenance"] !== "legacy") {
25805
+ resolvedProblems.push(n);
25806
+ }
25807
+ } else if (CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) {
25203
25808
  related.set(n.id, n);
25204
25809
  }
25205
25810
  }
25206
25811
  const solutionsByProblem = /* @__PURE__ */ new Map();
25207
- for (const p of openProblems) {
25812
+ for (const p of [...openProblems, ...constraints, ...resolvedProblems]) {
25208
25813
  for (const e of store2.outEdges(p.id, ["SOLVED_BY", "CAUSED_BY", "INSTANCE_OF"])) {
25209
25814
  const n = store2.getNode(e.to);
25210
- if (n && CITABLE_PRIOR_LABELS.has(n.label)) related.set(n.id, n);
25211
- if (n && n.label === "Solution" && e.type === "SOLVED_BY") {
25815
+ if (n && CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) related.set(n.id, n);
25816
+ if (n && n.label === "Solution" && e.type === "SOLVED_BY" && !isAutoMinted(n)) {
25212
25817
  const list = solutionsByProblem.get(p.id) ?? [];
25213
25818
  if (!list.some((s) => s.id === n.id)) list.push(n);
25214
25819
  solutionsByProblem.set(p.id, list);
25215
25820
  }
25216
25821
  }
25217
25822
  }
25218
- if (openProblems.length === 0 && related.size === 0) return null;
25219
- openProblems.sort((a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0));
25220
- return { file: file2, openProblems, related: [...related.values()], solutionsByProblem };
25823
+ if (openProblems.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
25824
+ return null;
25825
+ const recentFirst = (a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0);
25826
+ openProblems.sort(recentFirst);
25827
+ constraints.sort(recentFirst);
25828
+ resolvedProblems.sort(
25829
+ (a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)
25830
+ );
25831
+ return {
25832
+ file: file2,
25833
+ openProblems,
25834
+ constraints,
25835
+ resolvedProblems,
25836
+ related: [...related.values()],
25837
+ solutionsByProblem
25838
+ };
25221
25839
  }
25222
25840
  function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
25223
25841
  const p = store2.getNode(problemId);
@@ -25228,18 +25846,49 @@ function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
25228
25846
  });
25229
25847
  return true;
25230
25848
  }
25231
- function resolveDesignProblems(store2, t) {
25849
+ function reopenAutoClosedProblems(store2, t) {
25850
+ const report = { reopened: 0, alreadySuspect: 0, agentDescribed: 0 };
25851
+ for (const p of store2.findNodesByLabel("Problem")) {
25852
+ if (p.attrs["resolvedAt"] == null) continue;
25853
+ if (p.attrs["resolvedAs"] != null) continue;
25854
+ if (p.attrs["mergedInto"] !== void 0) continue;
25855
+ if (p.attrs["resolutionWitnessed"] === true) {
25856
+ report.agentDescribed++;
25857
+ continue;
25858
+ }
25859
+ const sols = store2.outEdges(p.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.to)).filter((n) => n !== null);
25860
+ if (sols.length === 0) continue;
25861
+ if (!sols.every((s) => String(s.description ?? "").startsWith(AUTO_MINT_PREFIX))) {
25862
+ report.agentDescribed++;
25863
+ continue;
25864
+ }
25865
+ if (p.attrs["resolutionSuspect"] === true) report.alreadySuspect++;
25866
+ const attrs = { ...p.attrs };
25867
+ delete attrs["resolvedAt"];
25868
+ attrs["reopenedFrom"] = "auto-close";
25869
+ attrs["reopenedAt"] = t;
25870
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
25871
+ report.reopened++;
25872
+ }
25873
+ return report;
25874
+ }
25875
+ var AUTO_MINT_PREFIX = "addressed by an edit to ";
25876
+ function markFixCandidates(store2, t) {
25232
25877
  let resolved = 0;
25233
25878
  for (const p of store2.findNodesByLabel("Problem")) {
25234
25879
  if (!p.id.startsWith("dprob_")) continue;
25235
25880
  if (p.attrs["resolvedAt"]) continue;
25881
+ if (isConstraintProblem(p)) continue;
25236
25882
  let symName = "";
25237
25883
  let symRelPath;
25238
25884
  let edited = false;
25885
+ const since = Math.max(p.createdAt, Number(p.attrs["fixCandidateClearedAt"] ?? 0));
25239
25886
  for (const e of store2.outEdges(p.id, ["ANCHORED_AT"])) {
25240
25887
  if (e.attrs?.["mention"] === true) continue;
25888
+ if (e.attrs?.["anchorProvenance"] === "legacy") continue;
25241
25889
  const sym = store2.getNode(e.to);
25242
- if (sym && sym.lastUpdatedAt > p.createdAt) {
25890
+ const changedAt = sym?.label === "File" ? Number(sym.attrs["contentChangedAt"] ?? 0) : sym?.lastUpdatedAt ?? 0;
25891
+ if (sym && changedAt > since) {
25243
25892
  edited = true;
25244
25893
  symName = sym.description;
25245
25894
  symRelPath = sym.attrs["relPath"] ?? void 0;
@@ -25247,36 +25896,172 @@ function resolveDesignProblems(store2, t) {
25247
25896
  }
25248
25897
  }
25249
25898
  if (!edited) continue;
25250
- if (store2.outEdges(p.id, ["SOLVED_BY"]).length === 0) {
25251
- const solId = `dfix_${digest({ problemId: p.id, edit: t })}`.slice(0, 56);
25252
- store2.mergeNode(
25253
- buildNode(solId, "Solution", `addressed by an edit to ${symName}`, t, {
25254
- source: "convo",
25255
- provisional: false,
25256
- // AC-resolution-attrs: the resolving symbol/file as STRUCTURED data, not
25257
- // just prose — the F2 join key downstream backfills and the viz read.
25258
- resolvedSymbols: [symName],
25259
- ...symRelPath ? { resolvedRelPath: symRelPath } : {}
25260
- })
25261
- );
25262
- mergeEdge(store2, p.id, solId, "SOLVED_BY", t);
25263
- for (const ae of store2.outEdges(p.id, ["ANCHORED_AT"]))
25264
- mergeEdge(store2, solId, ae.to, "ANCHORED_AT", t);
25265
- }
25899
+ if (p.attrs["fixCandidateAt"] !== void 0) continue;
25900
+ if (store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
25901
+ const s = store2.getNode(e.to);
25902
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
25903
+ }))
25904
+ continue;
25266
25905
  store2.updateNode(p.id, {
25267
- attrs: { ...p.attrs, provisional: false, resolvedAt: t },
25906
+ attrs: {
25907
+ ...p.attrs,
25908
+ // The cue, kept as structured data so the render can name the file it is
25909
+ // asking about. Deliberately NOT `resolvedAt` — this is a question.
25910
+ fixCandidateAt: t,
25911
+ fixCandidateSymbol: symName,
25912
+ ...symRelPath ? { fixCandidateFile: symRelPath } : {},
25913
+ // Snapshot the render-ledger counter so "shown since THIS ask" is
25914
+ // measurable. `shownCount` is the node's LIFETIME count across every
25915
+ // band, so comparing it raw would retire a fresh ask on a
25916
+ // frequently-surfaced problem before anyone had seen the question once.
25917
+ fixCandidateShownBase: Number(p.attrs["shownCount"] ?? 0)
25918
+ },
25268
25919
  lastUpdatedAt: t
25269
25920
  });
25270
25921
  resolved++;
25271
25922
  }
25272
25923
  return resolved;
25273
25924
  }
25925
+ var FIX_CANDIDATE_ASK_LIMIT = 5;
25926
+ function clearSettledFixCandidates(store2, t) {
25927
+ const report = { answered: 0, ignored: 0, unsubstantiated: 0, standing: 0 };
25928
+ for (const p of store2.findNodesByLabel("Problem")) {
25929
+ if (p.attrs["fixCandidateAt"] === void 0) continue;
25930
+ 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");
25931
+ const unsubstantiated = fileAnchors.length > 0 && fileAnchors.every((f) => f.attrs["contentChangedAt"] === void 0);
25932
+ const answered = p.attrs["resolvedAt"] != null || store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
25933
+ const s = store2.getNode(e.to);
25934
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
25935
+ });
25936
+ const base = Number(
25937
+ p.attrs["fixCandidateShownBase"] ?? p.attrs["shownCount"] ?? 0
25938
+ );
25939
+ const shownSinceAsk = Number(p.attrs["shownCount"] ?? 0) - base;
25940
+ const ignored = shownSinceAsk >= FIX_CANDIDATE_ASK_LIMIT;
25941
+ if (!answered && !ignored && !unsubstantiated) {
25942
+ report.standing++;
25943
+ continue;
25944
+ }
25945
+ const attrs = { ...p.attrs };
25946
+ delete attrs["fixCandidateAt"];
25947
+ delete attrs["fixCandidateSymbol"];
25948
+ delete attrs["fixCandidateFile"];
25949
+ attrs["fixCandidateClearedAt"] = t;
25950
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
25951
+ if (answered) report.answered++;
25952
+ else if (ignored) report.ignored++;
25953
+ else report.unsubstantiated++;
25954
+ }
25955
+ return report;
25956
+ }
25957
+
25958
+ // ../../packages/local-graph/src/intent.ts
25959
+ init_src();
25274
25960
 
25275
25961
  // ../../packages/local-graph/src/anchor-backfill.ts
25276
- var AUTO_MINT_PREFIX = "addressed by an edit to ";
25962
+ init_src();
25963
+ init_src2();
25277
25964
  function isLegacyAnchor(attrs) {
25278
25965
  return attrs?.["captureTime"] === true && attrs["anchorProvenance"] === void 0;
25279
25966
  }
25967
+ var STATEMENT_PATH_RE = new RegExp(
25968
+ String.raw`(?:[\w.+-]+\/)+[\w.+-]+\.(?:${anchorableExtensionAlternation()})`,
25969
+ "g"
25970
+ );
25971
+ function repairLegacyAnchorsFromStatement(store2, ts) {
25972
+ const report = {
25973
+ repaired: [],
25974
+ retiredGuesses: 0,
25975
+ noPathNamed: 0,
25976
+ pathUnknown: 0,
25977
+ alreadyCorrect: 0
25978
+ };
25979
+ const byPath = /* @__PURE__ */ new Map();
25980
+ for (const f of store2.findNodesByLabel("File")) {
25981
+ const path = String(f.attrs["relPath"] ?? f.description ?? "");
25982
+ if (path) byPath.set(path, { id: f.id, path });
25983
+ }
25984
+ const resolvePath = (named) => {
25985
+ const exact = byPath.get(named);
25986
+ if (exact) return exact;
25987
+ const hits = [...byPath.values()].filter(
25988
+ (f) => f.path.endsWith(`/${named}`) || named.endsWith(`/${f.path}`)
25989
+ );
25990
+ return hits.length === 1 ? hits[0] : null;
25991
+ };
25992
+ for (const p of store2.findNodesByLabel("Problem")) {
25993
+ try {
25994
+ const anchors = store2.outEdges(p.id, ["ANCHORED_AT"]);
25995
+ const legacy = anchors.filter((e) => e.attrs?.["anchorProvenance"] === "legacy");
25996
+ if (legacy.length === 0) continue;
25997
+ const better = anchors.filter((e) => {
25998
+ const prov = e.attrs?.["anchorProvenance"];
25999
+ return prov === "restated" || prov === "witnessed" || prov === "edited";
26000
+ });
26001
+ if (better.length > 0) {
26002
+ const betterTargets = new Set(better.map((e) => e.to));
26003
+ let retired = 0;
26004
+ for (const e of legacy) {
26005
+ if (!betterTargets.has(e.to)) {
26006
+ store2.closeEdge(e.id, ts);
26007
+ retired++;
26008
+ }
26009
+ }
26010
+ report.retiredGuesses += retired;
26011
+ continue;
26012
+ }
26013
+ const named = [...String(p.description ?? "").matchAll(STATEMENT_PATH_RE)].map((m) => m[0]);
26014
+ if (named.length === 0) {
26015
+ report.noPathNamed++;
26016
+ continue;
26017
+ }
26018
+ const guessedPaths = legacy.map((e) => {
26019
+ const f = store2.getNode(e.to);
26020
+ return String(f?.attrs["relPath"] ?? f?.description ?? "");
26021
+ });
26022
+ if (named.some((x) => guessedPaths.some((g) => g.endsWith(x) || x.endsWith(g)))) {
26023
+ report.alreadyCorrect++;
26024
+ continue;
26025
+ }
26026
+ const target = named.map(resolvePath).find((f) => f !== null);
26027
+ if (!target) {
26028
+ report.pathUnknown++;
26029
+ continue;
26030
+ }
26031
+ store2.mergeEdge({
26032
+ id: `edge_${digest({ from: p.id, type: "ANCHORED_AT", to: target.id })}`.slice(0, 24),
26033
+ from: p.id,
26034
+ to: target.id,
26035
+ type: "ANCHORED_AT",
26036
+ // Same confidence a capture-time anchor enters at: the source is the
26037
+ // agent's own statement either way, only the moment of reading differs.
26038
+ confidence: 0.4,
26039
+ extractionSource: "agent-observed",
26040
+ createdAt: ts,
26041
+ lastSeenAt: ts,
26042
+ navSuccesses: 0,
26043
+ navFailures: 0,
26044
+ attrs: {
26045
+ anchorProvenance: "restated",
26046
+ restatedFrom: guessedPaths[0] ?? "",
26047
+ restatedAt: ts
26048
+ }
26049
+ });
26050
+ for (const e of legacy) {
26051
+ if (e.to !== target.id) store2.closeEdge(e.id, ts);
26052
+ }
26053
+ report.retiredGuesses += legacy.filter((e) => e.to !== target.id).length;
26054
+ report.repaired.push({
26055
+ problemId: p.id,
26056
+ problem: p.description,
26057
+ guessed: guessedPaths[0] ?? "",
26058
+ restated: target.path
26059
+ });
26060
+ } catch {
26061
+ }
26062
+ }
26063
+ return report;
26064
+ }
25280
26065
  function backfillLegacyAnchors(store2, ts) {
25281
26066
  const report = {
25282
26067
  demotedEdges: 0,
@@ -25316,7 +26101,11 @@ function backfillLegacyAnchors(store2, ts) {
25316
26101
  store2,
25317
26102
  sol,
25318
26103
  `auto-closed off a pre-provenance anchor (guessed ${guessedAnchor}) \u2014 confirm the problem is really fixed, or reopen it`,
25319
- ts
26104
+ ts,
26105
+ // "or reopen it" is half the ask, and the nightly reopen takes that
26106
+ // branch — so record what would answer this, or the flag can never be
26107
+ // lowered and the band fills with settled questions.
26108
+ "parentProblemOpen"
25320
26109
  );
25321
26110
  store2.updateNode(p.id, {
25322
26111
  attrs: { ...p.attrs, resolutionSuspect: true },
@@ -25350,7 +26139,7 @@ function pagerank(input) {
25350
26139
  const ids = input.nodeIds;
25351
26140
  const n = ids.length;
25352
26141
  if (n === 0) {
25353
- return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
26142
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true, danglingRank: 0 };
25354
26143
  }
25355
26144
  const index = /* @__PURE__ */ new Map();
25356
26145
  for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
@@ -25422,8 +26211,12 @@ function pagerank(input) {
25422
26211
  }
25423
26212
  }
25424
26213
  const scores = /* @__PURE__ */ new Map();
25425
- for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
25426
- return { scores, iterations: iter, converged };
26214
+ let danglingRank = 0;
26215
+ for (let i2 = 0; i2 < n; i2++) {
26216
+ scores.set(ids[i2], score2[i2]);
26217
+ if (dangling[i2]) danglingRank += score2[i2];
26218
+ }
26219
+ return { scores, iterations: iter, converged, danglingRank };
25427
26220
  }
25428
26221
  function markLandmarks(scores, percentile = 0.1, filter) {
25429
26222
  const entries = [...scores.entries()];
@@ -25433,7 +26226,7 @@ function markLandmarks(scores, percentile = 0.1, filter) {
25433
26226
  }
25434
26227
  }
25435
26228
  if (entries.length === 0) return /* @__PURE__ */ new Set();
25436
- entries.sort((a, b) => b[1] - a[1]);
26229
+ entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
25437
26230
  const cutoff = Math.max(1, Math.floor(entries.length * percentile));
25438
26231
  const out2 = /* @__PURE__ */ new Set();
25439
26232
  for (let i2 = 0; i2 < cutoff; i2++) {
@@ -25671,8 +26464,86 @@ function motifDecision(input) {
25671
26464
  };
25672
26465
  }
25673
26466
 
25674
- // ../../packages/local-graph/src/community.ts
25675
- init_src2();
26467
+ // ../../packages/math/src/pagerank-local.ts
26468
+ function pagerankLocal(input) {
26469
+ const damping = input.damping ?? 0.85;
26470
+ const tol = input.tolerance ?? 1e-6;
26471
+ const maxIter = input.maxIterations ?? 100;
26472
+ const ids = input.regionIds;
26473
+ const n = ids.length;
26474
+ if (n === 0 || input.globalN === 0) {
26475
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
26476
+ }
26477
+ const index = /* @__PURE__ */ new Map();
26478
+ for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
26479
+ const base = (1 - damping) / input.globalN;
26480
+ const outSum = new Float64Array(n);
26481
+ const inflow = new Float64Array(n);
26482
+ let score2 = new Float64Array(n);
26483
+ for (let i2 = 0; i2 < n; i2++) {
26484
+ const id = ids[i2];
26485
+ outSum[i2] = input.outSum.get(id) ?? 0;
26486
+ inflow[i2] = input.boundaryInflow.get(id) ?? 0;
26487
+ score2[i2] = input.rank0.get(id) ?? base;
26488
+ }
26489
+ const rowLen = new Int32Array(n);
26490
+ let edgeCount = 0;
26491
+ for (const [from, edges] of input.out) {
26492
+ const fi = index.get(from);
26493
+ if (fi === void 0) continue;
26494
+ let inSet = 0;
26495
+ for (const e of edges) if (index.has(e.to)) inSet++;
26496
+ rowLen[fi] = inSet;
26497
+ edgeCount += inSet;
26498
+ }
26499
+ const rowStart = new Int32Array(n + 1);
26500
+ for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
26501
+ const colIdx = new Int32Array(edgeCount);
26502
+ const colW = new Float64Array(edgeCount);
26503
+ const cursor = rowStart.slice(0, n);
26504
+ for (const [from, edges] of input.out) {
26505
+ const fi = index.get(from);
26506
+ if (fi === void 0) continue;
26507
+ for (const e of edges) {
26508
+ const ti = index.get(e.to);
26509
+ if (ti === void 0) continue;
26510
+ const c = cursor[fi];
26511
+ cursor[fi] = c + 1;
26512
+ colIdx[c] = ti;
26513
+ colW[c] = e.weight;
26514
+ }
26515
+ }
26516
+ const externalDangling = input.externalDanglingRank ?? 0;
26517
+ let next = new Float64Array(n);
26518
+ let iter = 0;
26519
+ let converged = false;
26520
+ for (; iter < maxIter; iter++) {
26521
+ let internalDangling = 0;
26522
+ for (let i2 = 0; i2 < n; i2++) if (outSum[i2] <= 0) internalDangling += score2[i2];
26523
+ const danglingShare = damping * (internalDangling + externalDangling) / input.globalN;
26524
+ for (let i2 = 0; i2 < n; i2++) next[i2] = base + danglingShare + damping * inflow[i2];
26525
+ for (let i2 = 0; i2 < n; i2++) {
26526
+ const os2 = outSum[i2];
26527
+ if (os2 <= 0) continue;
26528
+ const f = damping * score2[i2] / os2;
26529
+ const end = rowStart[i2 + 1];
26530
+ for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
26531
+ }
26532
+ let diff = 0;
26533
+ for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
26534
+ const tmp = score2;
26535
+ score2 = next;
26536
+ next = tmp;
26537
+ if (diff < tol) {
26538
+ iter++;
26539
+ converged = true;
26540
+ break;
26541
+ }
26542
+ }
26543
+ const scores = /* @__PURE__ */ new Map();
26544
+ for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
26545
+ return { scores, iterations: iter, converged };
26546
+ }
25676
26547
 
25677
26548
  // ../../packages/local-graph/src/triage.ts
25678
26549
  init_src();
@@ -25681,95 +26552,8 @@ init_src();
25681
26552
  init_src2();
25682
26553
  var DRIFT_VALUES = new Set(Object.values(DRIFT_KIND));
25683
26554
 
25684
- // ../../packages/local-graph/src/problem-dedup.ts
25685
- init_src();
25686
- init_src();
25687
- var REDIRECT_EDGES = [
25688
- "CAUSED_BY",
25689
- "SOLVED_BY",
25690
- "FIXED_BY",
25691
- "ANCHORED_AT",
25692
- "MANIFESTED_IN",
25693
- "EVIDENCED_BY"
25694
- ];
25695
- function corroborations(n) {
25696
- return Number(n.attrs["corroborations"] ?? 0);
25697
- }
25698
- function overlap(a, b) {
25699
- if (a.size === 0 || b.size === 0) return 0;
25700
- let inter = 0;
25701
- for (const x of a) if (b.has(x)) inter++;
25702
- return inter / Math.min(a.size, b.size);
25703
- }
25704
- function mergeDuplicateProblems(store2, opts) {
25705
- const minOverlap = opts.minOverlap ?? 0.85;
25706
- const minTokens = opts.minTokens ?? 4;
25707
- const report = { clusters: 0, merged: 0 };
25708
- const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
25709
- const tokens = /* @__PURE__ */ new Map();
25710
- for (const p of open) tokens.set(p.id, new Set(conceptTokens(p.description)));
25711
- const consumed = /* @__PURE__ */ new Set();
25712
- store2.transaction(() => {
25713
- for (let i2 = 0; i2 < open.length; i2++) {
25714
- const a = open[i2];
25715
- if (consumed.has(a.id)) continue;
25716
- const cluster = [a];
25717
- const ta = tokens.get(a.id);
25718
- for (let j = i2 + 1; j < open.length; j++) {
25719
- const b = open[j];
25720
- if (consumed.has(b.id)) continue;
25721
- const tb = tokens.get(b.id);
25722
- if (Math.min(ta.size, tb.size) < minTokens) continue;
25723
- if (overlap(ta, tb) >= minOverlap) {
25724
- cluster.push(b);
25725
- consumed.add(b.id);
25726
- }
25727
- }
25728
- if (cluster.length < 2) continue;
25729
- report.clusters++;
25730
- cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
25731
- const survivor = cluster[0];
25732
- for (const dup of cluster.slice(1)) {
25733
- foldDuplicateProblem(store2, dup, survivor, opts.ts);
25734
- report.merged++;
25735
- }
25736
- }
25737
- });
25738
- return report;
25739
- }
25740
- function foldDuplicateProblem(store2, dup, survivor, ts) {
25741
- const surv = store2.getNode(survivor.id);
25742
- if (!surv) return;
25743
- const sources = new Set(surv.attrs["sources"] ?? []);
25744
- for (const s of dup.attrs["sources"] ?? []) sources.add(s);
25745
- store2.updateNode(survivor.id, {
25746
- attrs: {
25747
- ...surv.attrs,
25748
- sources: [...sources],
25749
- corroborations: corroborations(surv) + corroborations(dup) + 1
25750
- },
25751
- cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
25752
- lastUpdatedAt: ts
25753
- });
25754
- for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
25755
- if (e.to === survivor.id) continue;
25756
- const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
25757
- if (store2.getEdge(id)) continue;
25758
- const redirected = {
25759
- ...e,
25760
- id,
25761
- from: survivor.id,
25762
- createdAt: ts,
25763
- lastSeenAt: ts
25764
- };
25765
- store2.mergeEdge(redirected);
25766
- }
25767
- store2.updateNode(dup.id, {
25768
- attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
25769
- lastUpdatedAt: ts
25770
- });
25771
- store2.closeNode(dup.id, ts);
25772
- }
26555
+ // ../../packages/local-graph/src/community.ts
26556
+ init_src2();
25773
26557
 
25774
26558
  // ../../packages/local-graph/src/tools.ts
25775
26559
  init_src();
@@ -25786,6 +26570,9 @@ function rankToolsForHandles(store2, limit = 5) {
25786
26570
  // ../../packages/local-graph/src/principle-sync.ts
25787
26571
  init_src2();
25788
26572
 
26573
+ // ../../packages/local-graph/src/mechanism-liveness.ts
26574
+ var STALL_MIN_SPAN_MS = 60 * 60 * 1e3;
26575
+
25789
26576
  // ../../packages/generalizer/src/generalizer.ts
25790
26577
  init_src2();
25791
26578
  init_src();
@@ -25905,7 +26692,164 @@ function promoteMotifs(store2, t) {
25905
26692
  }
25906
26693
 
25907
26694
  // ../../packages/generalizer/src/nightly.ts
25908
- function runNightlyPipeline(store2) {
26695
+ var FULL_RESCORE_EVERY = 12;
26696
+ var INCREMENTAL_REGION_MAX_FRACTION = 0.2;
26697
+ var INCREMENTAL_HOPS = 3;
26698
+ function runGraphRescore(store2, opts = {}) {
26699
+ const lastRescoreAt = Number(store2.getMeta?.("lastRescoreAt") ?? 0);
26700
+ const sinceFull = Number(store2.getMeta?.("rescoresSinceFull") ?? 0);
26701
+ 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;
26702
+ if (incrementalCapable) {
26703
+ const started = Date.now();
26704
+ const inc = tryIncrementalRescore(store2, lastRescoreAt, started);
26705
+ if (inc) {
26706
+ store2.setMeta("lastRescoreAt", String(started));
26707
+ store2.setMeta("rescoresSinceFull", String(sinceFull + 1));
26708
+ return inc;
26709
+ }
26710
+ }
26711
+ const report = runFullRescore(store2);
26712
+ store2.setMeta?.("lastRescoreAt", String(report.startedAt));
26713
+ store2.setMeta?.("rescoresSinceFull", "0");
26714
+ store2.setMeta?.("danglingRankShare", String(report.danglingRank));
26715
+ const { startedAt: _drop, danglingRank: _drop2, ...rest } = report;
26716
+ return rest;
26717
+ }
26718
+ function tryIncrementalRescore(store2, lastRescoreAt, started) {
26719
+ const allowedTypes = new Set(pagerankEdgeTypes());
26720
+ const liveN = store2.nodeCount();
26721
+ const cap = Math.max(1e3, Math.floor(liveN * INCREMENTAL_REGION_MAX_FRACTION));
26722
+ const region = new Set(store2.dirtyNodeIdsSince(lastRescoreAt));
26723
+ for (const e of store2.edgeEndpointsTouchedSince(lastRescoreAt)) {
26724
+ region.add(e.from);
26725
+ region.add(e.to);
26726
+ }
26727
+ if (region.size > cap) return null;
26728
+ let frontier = [...region];
26729
+ for (let hop = 0; hop < INCREMENTAL_HOPS && frontier.length > 0; hop++) {
26730
+ const next = [];
26731
+ for (const e of store2.outEdgesForMany(frontier)) {
26732
+ if (!allowedTypes.has(e.type)) continue;
26733
+ if (!region.has(e.to)) {
26734
+ region.add(e.to);
26735
+ next.push(e.to);
26736
+ }
26737
+ }
26738
+ if (region.size > cap) return null;
26739
+ frontier = next;
26740
+ }
26741
+ const rank0 = store2.ranksForMany([...region]);
26742
+ const regionIds = [...rank0.keys()];
26743
+ if (regionIds.length === 0) {
26744
+ return {
26745
+ scoredNodes: 0,
26746
+ iterations: 0,
26747
+ converged: true,
26748
+ landmarks: 0,
26749
+ communities: 0,
26750
+ motifsPromoted: 0,
26751
+ techniques: 0,
26752
+ antipatterns: 0,
26753
+ pageRankWritten: 0,
26754
+ landmarkFlips: 0,
26755
+ mode: "incremental",
26756
+ regionSize: 0,
26757
+ durationMs: Date.now() - started
26758
+ };
26759
+ }
26760
+ const inRegion = new Set(regionIds);
26761
+ const out2 = /* @__PURE__ */ new Map();
26762
+ const outSum = /* @__PURE__ */ new Map();
26763
+ for (const e of store2.outEdgesForMany(regionIds)) {
26764
+ if (!allowedTypes.has(e.type)) continue;
26765
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26766
+ if (w <= 0) continue;
26767
+ outSum.set(e.from, (outSum.get(e.from) ?? 0) + w);
26768
+ if (!inRegion.has(e.to)) continue;
26769
+ const l = out2.get(e.from);
26770
+ if (l) l.push({ to: e.to, weight: w });
26771
+ else out2.set(e.from, [{ to: e.to, weight: w }]);
26772
+ }
26773
+ const boundaryEdges = store2.inEdgesForMany(regionIds).filter((e) => allowedTypes.has(e.type) && !inRegion.has(e.from) && (EDGE_WEIGHT[e.type] ?? 1) > 0);
26774
+ const boundarySources = [...new Set(boundaryEdges.map((e) => e.from))];
26775
+ if (boundarySources.length > cap) return null;
26776
+ const boundaryRanks = store2.ranksForMany(boundarySources);
26777
+ const boundaryOutSum = /* @__PURE__ */ new Map();
26778
+ for (const e of store2.outEdgesForMany(boundarySources)) {
26779
+ if (!allowedTypes.has(e.type)) continue;
26780
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26781
+ if (w > 0) boundaryOutSum.set(e.from, (boundaryOutSum.get(e.from) ?? 0) + w);
26782
+ }
26783
+ const boundaryInflow = /* @__PURE__ */ new Map();
26784
+ for (const e of boundaryEdges) {
26785
+ const r = boundaryRanks.get(e.from);
26786
+ const os2 = boundaryOutSum.get(e.from);
26787
+ if (r === void 0 || !os2) continue;
26788
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26789
+ boundaryInflow.set(e.to, (boundaryInflow.get(e.to) ?? 0) + r * w / os2);
26790
+ }
26791
+ const globalDangling = Number(store2.getMeta("danglingRankShare") ?? 0);
26792
+ let regionDangling = 0;
26793
+ for (const id of regionIds) {
26794
+ if ((outSum.get(id) ?? 0) <= 0) regionDangling += rank0.get(id) ?? 0;
26795
+ }
26796
+ const result = pagerankLocal({
26797
+ regionIds,
26798
+ rank0,
26799
+ out: out2,
26800
+ outSum,
26801
+ boundaryInflow,
26802
+ globalN: liveN,
26803
+ externalDanglingRank: Math.max(0, globalDangling - regionDangling),
26804
+ damping: 0.85,
26805
+ tolerance: 1e-6,
26806
+ maxIterations: 100
26807
+ });
26808
+ let pageRankWritten = 0;
26809
+ store2.transaction(() => {
26810
+ for (const [id, score2] of result.scores) {
26811
+ if (Math.abs(score2 - (rank0.get(id) ?? 0)) < 1e-9) continue;
26812
+ store2.setPageRank(id, score2);
26813
+ pageRankWritten++;
26814
+ }
26815
+ });
26816
+ const sweep = store2.landmarkSweepRows();
26817
+ const candidates = /* @__PURE__ */ new Map();
26818
+ for (const row of sweep) {
26819
+ if (row.label === "Pattern" || row.label === "RootCause") {
26820
+ candidates.set(row.id, result.scores.get(row.id) ?? row.pageRank);
26821
+ }
26822
+ }
26823
+ const want = markLandmarks(candidates, 0.1);
26824
+ for (const row of sweep) if (row.memoryTier === "persistent") want.add(row.id);
26825
+ let landmarkFlips = 0;
26826
+ store2.transaction(() => {
26827
+ for (const row of sweep) {
26828
+ if (row.extractionSource === "bko-inferred") continue;
26829
+ const should = want.has(row.id);
26830
+ if (row.isLandmark === should) continue;
26831
+ store2.setLandmark(row.id, should);
26832
+ landmarkFlips++;
26833
+ }
26834
+ });
26835
+ return {
26836
+ scoredNodes: regionIds.length,
26837
+ iterations: result.iterations,
26838
+ converged: result.converged,
26839
+ landmarks: want.size,
26840
+ communities: 0,
26841
+ // deferred to the full backstop — see mode docs
26842
+ motifsPromoted: 0,
26843
+ techniques: 0,
26844
+ antipatterns: 0,
26845
+ pageRankWritten,
26846
+ landmarkFlips,
26847
+ mode: "incremental",
26848
+ regionSize: regionIds.length,
26849
+ durationMs: Date.now() - started
26850
+ };
26851
+ }
26852
+ function runFullRescore(store2) {
25909
26853
  const started = Date.now();
25910
26854
  const allowedTypes = new Set(pagerankEdgeTypes());
25911
26855
  const nodeIds = [];
@@ -25931,10 +26875,12 @@ function runNightlyPipeline(store2) {
25931
26875
  maxIterations: 100
25932
26876
  });
25933
26877
  const scored = result.scores.size;
26878
+ let pageRankWritten = 0;
25934
26879
  store2.transaction(() => {
25935
26880
  for (const [id, score2] of result.scores) {
25936
26881
  if (Math.abs(score2 - (meta3.get(id)?.pageRank ?? 0)) < 1e-9) continue;
25937
26882
  store2.setPageRank(id, score2);
26883
+ pageRankWritten++;
25938
26884
  }
25939
26885
  });
25940
26886
  const landmarkCandidates = /* @__PURE__ */ new Map();
@@ -25949,11 +26895,13 @@ function runNightlyPipeline(store2) {
25949
26895
  for (const id of nodeIds) {
25950
26896
  if (meta3.get(id)?.memoryTier === "persistent") allLandmarks.add(id);
25951
26897
  }
26898
+ let landmarkFlips = 0;
25952
26899
  store2.transaction(() => {
25953
26900
  for (const id of nodeIds) {
25954
26901
  const want = allLandmarks.has(id);
25955
26902
  if ((meta3.get(id)?.isLandmark ?? false) === want) continue;
25956
26903
  store2.setLandmark(id, want);
26904
+ landmarkFlips++;
25957
26905
  }
25958
26906
  });
25959
26907
  const MOTIF_LABELS = /* @__PURE__ */ new Set(["Pattern", "Technique", "AntiPattern"]);
@@ -25988,8 +26936,6 @@ function runNightlyPipeline(store2) {
25988
26936
  for (const [id, c] of comm.community) store2.updateNode(id, { community: c });
25989
26937
  });
25990
26938
  const motifs = promoteMotifs(store2, started);
25991
- const designResolved = resolveDesignProblems(store2, started);
25992
- const cry = crystallize(store2, { ts: started });
25993
26939
  return {
25994
26940
  scoredNodes: scored,
25995
26941
  iterations: result.iterations,
@@ -25999,15 +26945,61 @@ function runNightlyPipeline(store2) {
25999
26945
  motifsPromoted: motifs.promoted,
26000
26946
  techniques: motifs.techniques,
26001
26947
  antipatterns: motifs.antipatterns,
26002
- skillsInduced: 0,
26003
- // skills are cloud-induced now (see above)
26004
- skillsRefreshed: 0,
26948
+ pageRankWritten,
26949
+ landmarkFlips,
26950
+ mode: "full",
26951
+ regionSize: 0,
26952
+ startedAt: started,
26953
+ danglingRank: result.danglingRank,
26954
+ durationMs: Date.now() - started
26955
+ };
26956
+ }
26957
+ function runSemanticMaintenance(store2) {
26958
+ const started = Date.now();
26959
+ reopenAutoClosedProblems(store2, started);
26960
+ const revisits = clearAnsweredRevisits(store2, started);
26961
+ const fixCandidates = clearSettledFixCandidates(store2, started);
26962
+ const designResolved = markFixCandidates(store2, started);
26963
+ const cry = crystallize(store2, { ts: started });
26964
+ return {
26005
26965
  designResolved,
26966
+ revisitsCleared: revisits.cleared,
26967
+ revisitsStanding: revisits.standing,
26968
+ fixCandidatesSettled: fixCandidates.answered + fixCandidates.ignored + fixCandidates.unsubstantiated,
26969
+ fixCandidates,
26006
26970
  claimsEvaluated: cry.evaluated,
26007
26971
  claimsDerived: cry.derived.length,
26008
26972
  durationMs: Date.now() - started
26009
26973
  };
26010
26974
  }
26975
+ function runNightlyPipeline(store2) {
26976
+ const started = Date.now();
26977
+ const rescore = runGraphRescore(store2);
26978
+ const semantic = runSemanticMaintenance(store2);
26979
+ return {
26980
+ scoredNodes: rescore.scoredNodes,
26981
+ iterations: rescore.iterations,
26982
+ converged: rescore.converged,
26983
+ landmarks: rescore.landmarks,
26984
+ communities: rescore.communities,
26985
+ motifsPromoted: rescore.motifsPromoted,
26986
+ techniques: rescore.techniques,
26987
+ antipatterns: rescore.antipatterns,
26988
+ skillsInduced: 0,
26989
+ // skills are cloud-induced now (see above)
26990
+ skillsRefreshed: 0,
26991
+ pageRankWritten: rescore.pageRankWritten,
26992
+ landmarkFlips: rescore.landmarkFlips,
26993
+ mode: rescore.mode,
26994
+ regionSize: rescore.regionSize,
26995
+ designResolved: semantic.designResolved,
26996
+ revisitsCleared: semantic.revisitsCleared,
26997
+ fixCandidatesSettled: semantic.fixCandidatesSettled,
26998
+ claimsEvaluated: semantic.claimsEvaluated,
26999
+ claimsDerived: semantic.claimsDerived,
27000
+ durationMs: Date.now() - started
27001
+ };
27002
+ }
26011
27003
  function codeAnchorProjection(store2, semanticIds, opts = {}) {
26012
27004
  const anchorToNodes = /* @__PURE__ */ new Map();
26013
27005
  for (const id of semanticIds) {
@@ -26052,13 +27044,27 @@ var AGENTS_POINTER_BODY = [
26052
27044
  init_src2();
26053
27045
  function buildSnapshot(opts) {
26054
27046
  const problems = opts.store.findNodesByLabel("Problem").filter((p) => p.attrs["mergedInto"] === void 0);
26055
- const allProblems = problems.filter((p) => p.attrs["resolvedAt"] == null);
27047
+ const stillOpen = problems.filter((p) => p.attrs["resolvedAt"] == null);
27048
+ const allProblems = stillOpen.filter((p) => !isConstraintProblem(p));
26056
27049
  allProblems.sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt);
26057
27050
  const recent = allProblems.slice(0, 8).map((p) => ({
26058
27051
  node: p,
26059
27052
  anchors: opts.store.outEdges(p.id, ["MANIFESTED_IN", "EVIDENCED_BY"])
26060
27053
  }));
26061
- 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) => {
27054
+ const recentConstraints = stillOpen.filter((p) => isConstraintProblem(p)).sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt).slice(0, 3);
27055
+ const regressions = problems.filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0).flatMap(
27056
+ (open) => opts.store.outEdges(open.id, ["REGRESSION_OF"]).flatMap((e) => {
27057
+ const closed = opts.store.getNode(e.to);
27058
+ if (!closed || closed.attrs["resolutionSuspect"] !== true || closed.attrs["resolvedAt"] == null) {
27059
+ return [];
27060
+ }
27061
+ const solEdge = opts.store.outEdges(closed.id, ["SOLVED_BY"])[0];
27062
+ const solution = solEdge ? opts.store.getNode(solEdge.to) ?? void 0 : void 0;
27063
+ return [{ open, closed, ...solution ? { solution } : {} }];
27064
+ })
27065
+ ).sort((a, b) => Number(b.closed.attrs["recurredAt"] ?? 0) - Number(a.closed.attrs["recurredAt"] ?? 0)).slice(0, 2);
27066
+ const regressionClosedIds = new Set(regressions.map((r) => r.closed.id));
27067
+ const recentResolved = problems.filter((p) => p.attrs["resolvedAt"] != null && p.attrs["resolvedAs"] == null).filter((p) => !regressionClosedIds.has(p.id)).sort((a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)).slice(0, 4).map((p) => {
26062
27068
  const solEdge = opts.store.outEdges(p.id, ["SOLVED_BY"])[0];
26063
27069
  const solution = solEdge ? opts.store.getNode(solEdge.to) ?? void 0 : void 0;
26064
27070
  return { node: p, ...solution ? { solution } : {} };
@@ -26085,10 +27091,13 @@ function buildSnapshot(opts) {
26085
27091
  episodeId,
26086
27092
  problems: problems2
26087
27093
  }));
26088
- const needsRevisit = listNeedsRevisit(
26089
- opts.store,
26090
- (opts.now ?? /* @__PURE__ */ new Date()).getTime()
26091
- ).slice(0, 10);
27094
+ const revisitSeen = /* @__PURE__ */ new Set();
27095
+ const needsRevisit = listNeedsRevisit(opts.store, (opts.now ?? /* @__PURE__ */ new Date()).getTime()).filter((r) => {
27096
+ const key = `${r.label}:${r.description.trim().toLowerCase()}`;
27097
+ if (revisitSeen.has(key)) return false;
27098
+ revisitSeen.add(key);
27099
+ return true;
27100
+ }).slice(0, 5);
26092
27101
  const norm = (x) => x.toLowerCase().replace(/[^a-z0-9]+/g, "");
26093
27102
  const pkgBase = (x) => {
26094
27103
  const at = x.lastIndexOf("@");
@@ -26124,7 +27133,10 @@ function buildSnapshot(opts) {
26124
27133
  profileContext,
26125
27134
  recentProblems: recent,
26126
27135
  recentResolved,
27136
+ recentConstraints,
27137
+ regressions,
26127
27138
  ...causalNudge ? { causalNudge } : {},
27139
+ ...opts.selfCiteSkips && opts.selfCiteSkips > 0 ? { selfCiteSkips: opts.selfCiteSkips } : {},
26128
27140
  ...domainNudge ? { domainNudge } : {},
26129
27141
  motifs,
26130
27142
  reviewCount: opts.reviewCount,
@@ -26175,10 +27187,16 @@ function sliceForFile(store2, relPath) {
26175
27187
  }
26176
27188
  const fp = priorsForFile(store2, relPath);
26177
27189
  const priors = fp ? {
26178
- openProblems: fp.openProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
27190
+ openProblems: fp.openProblems.slice(0, 3).map((p) => ({
27191
+ id: p.id,
27192
+ description: p.description,
27193
+ ...typeof p.attrs["fixCandidateFile"] === "string" ? { fixCandidateFile: p.attrs["fixCandidateFile"] } : {}
27194
+ })),
27195
+ constraints: fp.constraints.slice(0, 2).map((c) => ({ id: c.id, description: c.description })),
27196
+ resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
26179
27197
  related: fp.related.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description })),
26180
27198
  solutionsByProblem: Object.fromEntries(
26181
- fp.openProblems.slice(0, 3).map((p) => [
27199
+ [...fp.openProblems.slice(0, 3), ...fp.resolvedProblems.slice(0, 3)].map((p) => [
26182
27200
  p.id,
26183
27201
  (fp.solutionsByProblem.get(p.id) ?? []).slice(0, 4).map((s) => ({ id: s.id, description: s.description }))
26184
27202
  ])
@@ -26215,37 +27233,37 @@ init_src3();
26215
27233
  // ../../node_modules/.pnpm/env-paths@3.0.0/node_modules/env-paths/index.js
26216
27234
  import os from "node:os";
26217
27235
  import process3 from "node:process";
26218
- var homedir = os.homedir();
27236
+ var homedir2 = os.homedir();
26219
27237
  var tmpdir = os.tmpdir();
26220
27238
  var { env } = process3;
26221
27239
 
26222
27240
  // src/paths.ts
26223
- import { dirname as dirname2, join as join3 } from "node:path";
27241
+ import { dirname as dirname2, join as join4 } from "node:path";
26224
27242
  function workspaceDir(workspaceRoot) {
26225
- return join3(workspaceRoot, ".errata");
27243
+ return join4(workspaceRoot, ".errata");
26226
27244
  }
26227
27245
  function workspacePaths(root) {
26228
27246
  const dir = workspaceDir(root);
26229
27247
  return {
26230
27248
  root,
26231
27249
  configDir: dir,
26232
- workspaceJson: join3(dir, "workspace.json"),
26233
- eventLog: join3(dir, "eventlog.sqlite"),
26234
- castalia: join3(dir, "castalia.db"),
26235
- reviewQueue: join3(dir, "review-queue.json"),
26236
- outbox: join3(dir, "outbox"),
26237
- daemonLock: join3(dir, "daemon.lock"),
26238
- identityAudit: join3(dir, "identity-audit.log"),
26239
- skillsDir: join3(dir, "skills"),
26240
- skillsManifest: join3(dir, "skills.json")
27250
+ workspaceJson: join4(dir, "workspace.json"),
27251
+ eventLog: join4(dir, "eventlog.sqlite"),
27252
+ castalia: join4(dir, "castalia.db"),
27253
+ reviewQueue: join4(dir, "review-queue.json"),
27254
+ outbox: join4(dir, "outbox"),
27255
+ daemonLock: join4(dir, "daemon.lock"),
27256
+ identityAudit: join4(dir, "identity-audit.log"),
27257
+ skillsDir: join4(dir, "skills"),
27258
+ skillsManifest: join4(dir, "skills.json")
26241
27259
  };
26242
27260
  }
26243
27261
 
26244
27262
  // src/reconcile.ts
26245
27263
  init_src3();
26246
27264
  import { execFileSync as execFileSync2 } from "node:child_process";
26247
- import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
26248
- import { join as join4, relative as relative2, sep as sep2 } from "node:path";
27265
+ import { readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
27266
+ import { join as join5, relative as relative2, sep as sep2 } from "node:path";
26249
27267
  var IGNORED = /[\\/](?:\.git|node_modules|\.errata|dist|__pycache__)(?:[\\/]|$)/;
26250
27268
  var SOURCE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i;
26251
27269
  function gitSourceFiles(root) {
@@ -26262,7 +27280,7 @@ function gitSourceFiles(root) {
26262
27280
  const out2 = [];
26263
27281
  for (const rel of stdout.split("\0")) {
26264
27282
  if (!rel || !SOURCE_RE.test(rel)) continue;
26265
- const abs = join4(root, rel);
27283
+ const abs = join5(root, rel);
26266
27284
  if (IGNORED.test(abs)) continue;
26267
27285
  out2.push(abs);
26268
27286
  }
@@ -26271,13 +27289,13 @@ function gitSourceFiles(root) {
26271
27289
  function* walkSource(dir) {
26272
27290
  let entries;
26273
27291
  try {
26274
- entries = readdirSync2(dir, { withFileTypes: true });
27292
+ entries = readdirSync3(dir, { withFileTypes: true });
26275
27293
  } catch {
26276
27294
  return;
26277
27295
  }
26278
27296
  for (const e of entries) {
26279
27297
  const name2 = String(e.name);
26280
- const full = join4(dir, name2);
27298
+ const full = join5(dir, name2);
26281
27299
  if (IGNORED.test(full)) continue;
26282
27300
  if (e.isDirectory()) yield* walkSource(full);
26283
27301
  else if (SOURCE_RE.test(name2)) yield full;
@@ -26286,12 +27304,18 @@ function* walkSource(dir) {
26286
27304
  function listSourceFiles(root) {
26287
27305
  return gitSourceFiles(root) ?? walkSource(root);
26288
27306
  }
26289
- function findStaleFiles(store2, rootPath, workspaceId) {
27307
+ var SCAN_YIELD_EVERY = 100;
27308
+ var LIVENESS_CHUNK = 4e3;
27309
+ async function findStaleFiles(store2, rootPath, workspaceId) {
26290
27310
  const stale = [];
27311
+ const pending = [];
27312
+ const allTargets = [];
27313
+ let scanned = 0;
26291
27314
  for (const abs of listSourceFiles(rootPath)) {
27315
+ if (++scanned % SCAN_YIELD_EVERY === 0) await new Promise((r) => setImmediate(r));
26292
27316
  let mtimeMs;
26293
27317
  try {
26294
- mtimeMs = statSync2(abs).mtimeMs;
27318
+ mtimeMs = statSync3(abs).mtimeMs;
26295
27319
  } catch {
26296
27320
  continue;
26297
27321
  }
@@ -26299,32 +27323,52 @@ function findStaleFiles(store2, rootPath, workspaceId) {
26299
27323
  const fnode = store2.getNode(fileNodeId(workspaceId, rel));
26300
27324
  if (!fnode) {
26301
27325
  stale.push(abs);
27326
+ } else if ((fnode.validTo ?? null) !== null) {
27327
+ stale.push(abs);
26302
27328
  } else if (fnode.lastUpdatedAt < mtimeMs) {
26303
27329
  stale.push(abs);
26304
27330
  } else {
26305
27331
  const edges = store2.outEdges(fnode.id, ["DEFINES", "CONTAINS"]);
26306
27332
  if (edges.length === 0) {
26307
27333
  stale.push(abs);
26308
- } else if (edges.some((e) => !isLive(store2.getNode(e.to)))) {
26309
- stale.push(abs);
27334
+ } else {
27335
+ const targets = edges.map((e) => e.to);
27336
+ pending.push({ abs, targets });
27337
+ allTargets.push(...targets);
26310
27338
  }
26311
27339
  }
26312
27340
  }
27341
+ const live = /* @__PURE__ */ new Set();
27342
+ for (let i2 = 0; i2 < allTargets.length; i2 += LIVENESS_CHUNK) {
27343
+ for (const id of store2.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live.add(id);
27344
+ if (i2 + LIVENESS_CHUNK < allTargets.length) await new Promise((r) => setImmediate(r));
27345
+ }
27346
+ for (const { abs, targets } of pending) {
27347
+ if (targets.some((id) => !live.has(id))) stale.push(abs);
27348
+ }
26313
27349
  return stale;
26314
27350
  }
26315
- function isLive(n) {
26316
- return n != null && (n.validTo ?? null) === null;
26317
- }
26318
27351
  async function reconcileStaleFiles(store2, rootPath, workspaceId) {
26319
- const stale = findStaleFiles(store2, rootPath, workspaceId);
27352
+ const stale = await findStaleFiles(store2, rootPath, workspaceId);
26320
27353
  if (stale.length === 0) return 0;
26321
27354
  let total = 0;
27355
+ let failedBatches = 0;
26322
27356
  const BATCH = 20;
26323
27357
  for (let i2 = 0; i2 < stale.length; i2 += BATCH) {
26324
- const r = await incrementalReindex(store2, rootPath, workspaceId, stale.slice(i2, i2 + BATCH));
26325
- total += r.filesReindexed;
27358
+ try {
27359
+ const r = await incrementalReindex(store2, rootPath, workspaceId, stale.slice(i2, i2 + BATCH));
27360
+ total += r.filesReindexed;
27361
+ } catch (err2) {
27362
+ failedBatches++;
27363
+ console.warn(
27364
+ `[errata] reconcile: batch ${i2 / BATCH + 1} failed (${stale.slice(i2, i2 + BATCH).length} file(s) skipped): ${err2 instanceof Error ? err2.message : err2}`
27365
+ );
27366
+ }
26326
27367
  if (i2 + BATCH < stale.length) await new Promise((res) => setImmediate(res));
26327
27368
  }
27369
+ if (failedBatches > 0) {
27370
+ console.warn(`[errata] reconcile: ${failedBatches} batch(es) failed; ${total} file(s) reindexed`);
27371
+ }
26328
27372
  return total;
26329
27373
  }
26330
27374
 
@@ -26377,6 +27421,7 @@ function runNightly() {
26377
27421
  const a = backfillLegacyAnchors(store, Date.now());
26378
27422
  anchorsDemoted = a.demotedEdges;
26379
27423
  resolutionsSuspect = a.suspects.length;
27424
+ repairLegacyAnchorsFromStatement(store, Date.now());
26380
27425
  } catch {
26381
27426
  }
26382
27427
  const report = runNightlyPipeline(store);