@inerrata-corporation/errata 2.0.2-dev.133 → 2.0.2-dev.1411

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
@@ -212,7 +212,7 @@ var init_castalia = __esm({
212
212
  DECOMPOSITION_EDGES = ["SPLIT_INTO"];
213
213
  TRANSFER_EDGES = ["MAY_RESOLVE"];
214
214
  SOLUTION_STRUCTURE_EDGES = ["BUILDS_ON", "SUPERSEDES", "ALTERNATIVE_TO"];
215
- SUSPICION_EDGES = ["SUSPECTED_LINK"];
215
+ SUSPICION_EDGES = ["SUSPECTED_LINK", "REGRESSION_OF"];
216
216
  TAXONOMY_EDGES = ["IS_A"];
217
217
  TRIAGE_EDGES = ["TRIAGED_BY", "CONFIRMS", "INDICATES", "ROUTES_TO"];
218
218
  GIT_EDGES = ["POINTS_AT", "PARENT", "AUTHORED_BY"];
@@ -266,6 +266,10 @@ var init_castalia = __esm({
266
266
  // git topology, not signal-flow
267
267
  "AUTHORED_BY",
268
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
269
273
  "CONTRIBUTED",
270
274
  // agent attribution (Agent → knowledge), not signal-flow
271
275
  "SUPERSEDES",
@@ -273,8 +277,10 @@ var init_castalia = __esm({
273
277
  "ALTERNATIVE_TO",
274
278
  // symmetric peer marker — weight would let alternatives inflate each other
275
279
  // NB: BUILDS_ON is NOT excluded — it flows extension→base so foundational solutions rank up.
276
- "SUSPECTED_LINK"
280
+ "SUSPECTED_LINK",
277
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.
278
284
  ]);
279
285
  EDGE_WEIGHT = {
280
286
  // Causal — high signal
@@ -387,6 +393,7 @@ var init_castalia = __esm({
387
393
  ALTERNATIVE_TO: 0,
388
394
  // Suspicion — a hypothesis, weightless + PageRank-excluded (never flows rank / EIG).
389
395
  SUSPECTED_LINK: 0,
396
+ REGRESSION_OF: 0,
390
397
  // Git — topology/authorship, weightless (not domain signal-flow)
391
398
  POINTS_AT: 0,
392
399
  PARENT: 0,
@@ -417,6 +424,13 @@ var init_castalia = __esm({
417
424
  }
418
425
  });
419
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
+
420
434
  // ../../packages/shared/src/canonical/hash.ts
421
435
  import { createHash } from "node:crypto";
422
436
  function sha256(input) {
@@ -458,13 +472,18 @@ function resolveCanonical(label) {
458
472
  function resolveCanonicalId(label) {
459
473
  return resolveCanonical(label)?.id;
460
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
+ }
461
480
  function aliasesLongestFirst() {
462
481
  const out2 = [];
463
482
  for (const e of REGISTRY) for (const a of e.aliases) out2.push({ alias: a.toLowerCase(), entity: e });
464
483
  out2.sort((x, y) => y.alias.length - x.alias.length);
465
484
  return out2;
466
485
  }
467
- var REGISTRY, BY_ALIAS;
486
+ var REGISTRY, AMBIGUOUS_ALIASES, BY_ALIAS;
468
487
  var init_taxonomy = __esm({
469
488
  "../../packages/shared/src/nlp/taxonomy.ts"() {
470
489
  "use strict";
@@ -513,6 +532,13 @@ var init_taxonomy = __esm({
513
532
  { id: "concept:retry", name: "retry", category: "concept", aliases: ["retry"] },
514
533
  { id: "concept:migration", name: "migration", category: "concept", aliases: ["migration"] }
515
534
  ];
535
+ AMBIGUOUS_ALIASES = /* @__PURE__ */ new Set([
536
+ "node",
537
+ "go",
538
+ "pool",
539
+ "spring",
540
+ "ws"
541
+ ]);
516
542
  BY_ALIAS = /* @__PURE__ */ new Map();
517
543
  for (const e of REGISTRY) {
518
544
  for (const a of e.aliases) {
@@ -574,7 +600,7 @@ function lemma(token) {
574
600
  }
575
601
  return token;
576
602
  }
577
- function conceptTokens(text) {
603
+ function tokenize(text, resolve) {
578
604
  const matches = text.normalize("NFC").toLowerCase().match(TOKEN_RE) ?? [];
579
605
  const out2 = /* @__PURE__ */ new Set();
580
606
  for (const raw of matches) {
@@ -584,11 +610,14 @@ function conceptTokens(text) {
584
610
  out2.add(token);
585
611
  continue;
586
612
  }
587
- const canonical = resolveCanonicalId(token);
613
+ const canonical = resolve(token);
588
614
  out2.add(canonical ?? lemma(token));
589
615
  }
590
616
  return [...out2].sort();
591
617
  }
618
+ function retrievalTokens(text) {
619
+ return tokenize(text, resolveUnambiguousCanonicalId);
620
+ }
592
621
  var STOPWORDS, NEGATION_TOKENS, TOKEN_RE;
593
622
  var init_concept_bag = __esm({
594
623
  "../../packages/shared/src/nlp/concept-bag.ts"() {
@@ -15069,7 +15098,12 @@ var init_edge_rules = __esm({
15069
15098
  // ── Taxonomy (Track 4 Stream C: MITRE CWE `ChildOf` → `Weakness —IS_A→ Weakness`) ──
15070
15099
  IS_A: { from: ["Weakness"], to: ["Weakness"] },
15071
15100
  // ── Conceptual (v1 taxonomy.ts: instance → motif/pattern reference) ──
15072
- 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"] },
15073
15107
  IMPLEMENTS: { from: ["Solution", "Language", "Component"], to: ["Pattern", "Technique"] },
15074
15108
  MATCHES: { to: ["Pattern"] },
15075
15109
  // ── Artifact evidence (v1 taxonomy.ts artifact rows) ──
@@ -15179,6 +15213,14 @@ var init_wire = __esm({
15179
15213
  * deployment a self-corroboration). Optional + additive: absent leaves the
15180
15214
  * gate fail-open for that node, exactly today's behaviour. */
15181
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(),
15182
15224
  extractionSource: external_exports.enum(INGEST_EXTRACTION_SOURCES),
15183
15225
  validationSource: external_exports.enum(VALIDATION_SOURCES).optional(),
15184
15226
  /** Org-membrane (M2): the daemon's anchor tag — it owns the lockfile, so it
@@ -15290,6 +15332,7 @@ var init_src = __esm({
15290
15332
  "../../packages/shared/src/index.ts"() {
15291
15333
  "use strict";
15292
15334
  init_castalia();
15335
+ init_graph_events();
15293
15336
  init_identity();
15294
15337
  init_wire();
15295
15338
  init_edge_rules();
@@ -15351,6 +15394,51 @@ var init_review = __esm({
15351
15394
  }
15352
15395
  });
15353
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
+
15354
15442
  // ../../packages/local-shared/src/sqlite-adapter.ts
15355
15443
  function openDatabase(path) {
15356
15444
  const db = new DatabaseSync(path);
@@ -15363,6 +15451,7 @@ function openDatabase(path) {
15363
15451
  db.exec("PRAGMA journal_mode = WAL");
15364
15452
  db.exec("PRAGMA synchronous = NORMAL");
15365
15453
  db.exec("PRAGMA busy_timeout = 5000");
15454
+ db.exec("PRAGMA journal_size_limit = 67108864");
15366
15455
  } catch {
15367
15456
  }
15368
15457
  }
@@ -15398,7 +15487,7 @@ function openDatabase(path) {
15398
15487
  return db.prepare(`PRAGMA ${key}`).get();
15399
15488
  },
15400
15489
  transaction(fn) {
15401
- db.exec("BEGIN");
15490
+ db.exec("BEGIN IMMEDIATE");
15402
15491
  try {
15403
15492
  const r = fn();
15404
15493
  db.exec("COMMIT");
@@ -15452,10 +15541,92 @@ var init_src2 = __esm({
15452
15541
  init_profile();
15453
15542
  init_daemon_wire();
15454
15543
  init_review();
15544
+ init_anchorable();
15455
15545
  init_sqlite_adapter();
15456
15546
  }
15457
15547
  });
15458
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
+
15459
15630
  // ../../packages/indexer/src/simhash.ts
15460
15631
  function fnv1a64(s) {
15461
15632
  let h = FNV_OFFSET;
@@ -15474,7 +15645,7 @@ function popcount(x) {
15474
15645
  }
15475
15646
  return c;
15476
15647
  }
15477
- function tokenize(text) {
15648
+ function tokenize2(text) {
15478
15649
  return text.match(/[A-Za-z_$][A-Za-z0-9_$]*|\d+|[^\s\w]/g) ?? [];
15479
15650
  }
15480
15651
  function shingle(tokens, n = SHINGLE_N) {
@@ -15503,7 +15674,7 @@ function simhashFeatures(features) {
15503
15674
  return out2;
15504
15675
  }
15505
15676
  function simhash(text) {
15506
- return simhashFeatures(shingle(tokenize(text)));
15677
+ return simhashFeatures(shingle(tokenize2(text)));
15507
15678
  }
15508
15679
  function hammingDistance(a, b) {
15509
15680
  return popcount((a ^ b) & MASK64);
@@ -15679,10 +15850,10 @@ var init_identity2 = __esm({
15679
15850
  });
15680
15851
 
15681
15852
  // ../../packages/indexer/src/pipeline.ts
15682
- import { appendFileSync, readFileSync, statSync } from "node:fs";
15853
+ import { appendFileSync, readFileSync as readFileSync2, statSync as statSync2 } from "node:fs";
15683
15854
  import { readdir } from "node:fs/promises";
15684
- import { extname, join, relative, sep } from "node:path";
15685
- 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";
15686
15857
  import { execFileSync } from "node:child_process";
15687
15858
  function nowTs() {
15688
15859
  return indexNow ?? Date.now();
@@ -15691,7 +15862,7 @@ function fingerprintOf(signature, bodyHash) {
15691
15862
  return `${signature ?? ""}|${bodyHash ?? ""}`;
15692
15863
  }
15693
15864
  function sha(s) {
15694
- return createHash2("sha256").update(s).digest("hex").slice(0, 16);
15865
+ return createHash3("sha256").update(s).digest("hex").slice(0, 16);
15695
15866
  }
15696
15867
  function fileNodeId(workspaceId, relPath) {
15697
15868
  return `file_${sha(workspaceId + ":" + relPath.replace(/\\/g, "/"))}`;
@@ -15811,10 +15982,11 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15811
15982
  };
15812
15983
  }
15813
15984
  const fileHashes = /* @__PURE__ */ new Map();
15985
+ const contentMovedPaths = /* @__PURE__ */ new Set();
15814
15986
  for (const rel of [...changedRelPaths]) {
15815
15987
  let h;
15816
15988
  try {
15817
- h = createHash2("sha256").update(readFileSync(join(rootPath, rel))).digest("hex");
15989
+ h = createHash3("sha256").update(readFileSync2(join2(rootPath, rel))).digest("hex");
15818
15990
  } catch {
15819
15991
  continue;
15820
15992
  }
@@ -15823,6 +15995,8 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15823
15995
  if (fnode && fnode.attrs["contentHash"] === h && !fileHasStrandedSymbol(store2, fnode.id)) {
15824
15996
  store2.updateNode(fnode.id, { lastUpdatedAt: t });
15825
15997
  changedRelPaths.delete(rel);
15998
+ } else if (!fnode || fnode.attrs["contentHash"] !== h) {
15999
+ contentMovedPaths.add(rel);
15826
16000
  }
15827
16001
  }
15828
16002
  if (changedRelPaths.size === 0) {
@@ -15861,13 +16035,13 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15861
16035
  let parsedFiles = 0;
15862
16036
  for (const rel of changedRelPaths) {
15863
16037
  if (parsedFiles++ > 0) await new Promise((r2) => setImmediate(r2));
15864
- const abs = join(rootPath, ...rel.split("/"));
16038
+ const abs = join2(rootPath, ...rel.split("/"));
15865
16039
  const ext = extname(abs).toLowerCase();
15866
16040
  const provider = providers.find((p) => p.fileExtensions.includes(ext));
15867
16041
  if (!provider) continue;
15868
16042
  let symbols;
15869
16043
  try {
15870
- symbols = provider.extractSymbols(readFileSync(abs, "utf8"), abs);
16044
+ symbols = provider.extractSymbols(readFileSync2(abs, "utf8"), abs);
15871
16045
  } catch {
15872
16046
  continue;
15873
16047
  }
@@ -15957,6 +16131,7 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
15957
16131
  changedRelPaths
15958
16132
  });
15959
16133
  store2.transaction(() => {
16134
+ store2.reviveReobserved([...changedRelPaths], workspaceId, t);
15960
16135
  const versionedAndFile = /* @__PURE__ */ new Set([...VERSIONED_LABELS, "File"]);
15961
16136
  const ownedLive = [];
15962
16137
  for (const n of store2.nodesByRelPath([...changedRelPaths])) {
@@ -16065,7 +16240,14 @@ async function incrementalReindex(store2, rootPath, workspaceId, changedAbsPaths
16065
16240
  const h = fileHashes.get(rel);
16066
16241
  if (!h) continue;
16067
16242
  const fnode = store2.getNode(fileNodeId(workspaceId, rel));
16068
- 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
+ });
16069
16251
  }
16070
16252
  return {
16071
16253
  filesReindexed: changedRelPaths.size,
@@ -16113,8 +16295,13 @@ async function runIndexer(store2, opts) {
16113
16295
  byLanguage: {},
16114
16296
  durationMs: 0,
16115
16297
  nodesPurged: 0,
16116
- edgesPurged: 0
16298
+ edgesPurged: 0,
16299
+ parseCacheHits: 0,
16300
+ parseCacheMisses: 0
16117
16301
  };
16302
+ const cacheDir = parseCacheDir();
16303
+ const parseCacheEnabled = cacheDir !== "";
16304
+ if (parseCacheEnabled) sweepParseCacheOnce(cacheDir);
16118
16305
  if (opts.clean) {
16119
16306
  const purge = purgeWorkspaceCodeGraph(store2, opts.workspaceId);
16120
16307
  report.nodesPurged = purge.nodes;
@@ -16168,25 +16355,38 @@ async function runIndexer(store2, opts) {
16168
16355
  }
16169
16356
  let source;
16170
16357
  try {
16171
- const st = statSync(f.absPath);
16358
+ const st = statSync2(f.absPath);
16172
16359
  if (st.size > maxBytes) {
16173
16360
  report.filesSkipped++;
16174
16361
  continue;
16175
16362
  }
16176
- source = readFileSync(f.absPath, "utf8");
16363
+ source = readFileSync2(f.absPath, "utf8");
16177
16364
  } catch {
16178
16365
  report.filesSkipped++;
16179
16366
  continue;
16180
16367
  }
16368
+ const cacheKey = parseCacheKey(source, f.provider.id);
16369
+ const cached2 = parseCacheEnabled ? readParseCache(cacheDir, cacheKey) : null;
16181
16370
  let symbols;
16182
- try {
16183
- symbols = f.provider.extractSymbols(source, f.absPath);
16184
- } catch {
16185
- report.filesSkipped++;
16186
- 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
+ }
16187
16388
  }
16188
16389
  symbolsByFile.set(f.relPath, symbols);
16189
- const reExports = scanReExports(source);
16190
16390
  if (reExports.length > 0) reExportsByFile.set(f.relPath, reExports);
16191
16391
  report.filesParsed++;
16192
16392
  report.byLanguage[f.provider.id] = (report.byLanguage[f.provider.id] ?? 0) + 1;
@@ -16231,7 +16431,7 @@ async function runIndexer(store2, opts) {
16231
16431
  const depth = fileNode.relPath.split("/").length;
16232
16432
  if (depth !== 3) continue;
16233
16433
  try {
16234
- const pkg = JSON.parse(readFileSync(fileNode.absPath, "utf8"));
16434
+ const pkg = JSON.parse(readFileSync2(fileNode.absPath, "utf8"));
16235
16435
  if (!pkg.name) continue;
16236
16436
  const pkgDir = fileNode.relPath.replace(/\/package\.json$/, "");
16237
16437
  const candidates = [
@@ -16613,7 +16813,7 @@ async function scan(root, current, ignores, providers, out2) {
16613
16813
  for (const ent of entries) {
16614
16814
  if (ignores.has(ent.name)) continue;
16615
16815
  if (ent.name.startsWith(".") && ent.name !== ".") continue;
16616
- const abs = join(current, ent.name);
16816
+ const abs = join2(current, ent.name);
16617
16817
  if (ent.isDirectory()) {
16618
16818
  await scan(root, abs, ignores, providers, out2);
16619
16819
  } else if (ent.isFile()) {
@@ -16622,7 +16822,7 @@ async function scan(root, current, ignores, providers, out2) {
16622
16822
  const rel = relative(root, abs).split(sep).join("/");
16623
16823
  let size = 0;
16624
16824
  try {
16625
- size = statSync(abs).size;
16825
+ size = statSync2(abs).size;
16626
16826
  } catch {
16627
16827
  continue;
16628
16828
  }
@@ -16636,7 +16836,7 @@ async function scan(root, current, ignores, providers, out2) {
16636
16836
  }
16637
16837
  }
16638
16838
  }
16639
- function gitListFiles(root, ignores, providers) {
16839
+ function gitListRelPaths(root, ignores) {
16640
16840
  let stdout;
16641
16841
  try {
16642
16842
  stdout = execFileSync(
@@ -16659,10 +16859,19 @@ function gitListFiles(root, ignores, providers) {
16659
16859
  if (segs.some((s) => ignores.has(s) || s.startsWith(".") && s.length > 1)) {
16660
16860
  continue;
16661
16861
  }
16662
- 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);
16663
16872
  let size;
16664
16873
  try {
16665
- size = statSync(abs).size;
16874
+ size = statSync2(abs).size;
16666
16875
  } catch {
16667
16876
  continue;
16668
16877
  }
@@ -16682,7 +16891,7 @@ function upsertFile(store2, id, f, workspaceId) {
16682
16891
  const now = nowTs();
16683
16892
  let contentHash;
16684
16893
  try {
16685
- contentHash = createHash2("sha256").update(readFileSync(f.absPath)).digest("hex");
16894
+ contentHash = createHash3("sha256").update(readFileSync2(f.absPath)).digest("hex");
16686
16895
  } catch {
16687
16896
  }
16688
16897
  const node = {
@@ -16881,6 +17090,7 @@ var init_pipeline = __esm({
16881
17090
  "../../packages/indexer/src/pipeline.ts"() {
16882
17091
  "use strict";
16883
17092
  init_src2();
17093
+ init_parse_cache();
16884
17094
  init_identity2();
16885
17095
  DEFAULT_IGNORES = /* @__PURE__ */ new Set([
16886
17096
  "node_modules",
@@ -21045,8 +21255,8 @@ ${JSON.stringify(symbolNames, null, 2)}`);
21045
21255
 
21046
21256
  // ../../packages/indexer/src/languages/tree-sitter-loader.ts
21047
21257
  import { fileURLToPath } from "node:url";
21048
- import { dirname, join as join2 } from "node:path";
21049
- 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";
21050
21260
  import { createRequire } from "node:module";
21051
21261
  function entryDir() {
21052
21262
  try {
@@ -21060,27 +21270,27 @@ function entryDir() {
21060
21270
  }
21061
21271
  function findWasmDir() {
21062
21272
  const here = entryDir();
21063
- const seaWasm = join2(here, "resources", "wasm");
21064
- 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;
21065
21275
  let dir = here;
21066
21276
  for (let i2 = 0; i2 < 6; i2++) {
21067
- const flat = join2(
21277
+ const flat = join3(
21068
21278
  dir,
21069
21279
  "node_modules",
21070
21280
  "@vscode",
21071
21281
  "tree-sitter-wasm",
21072
21282
  "wasm"
21073
21283
  );
21074
- if (existsSync(join2(flat, "tree-sitter-typescript.wasm"))) return flat;
21284
+ if (existsSync2(join3(flat, "tree-sitter-typescript.wasm"))) return flat;
21075
21285
  dir = dirname(dir);
21076
21286
  }
21077
21287
  let root = here;
21078
21288
  for (let i2 = 0; i2 < 6; i2++) {
21079
- const pnpmDir = join2(root, "node_modules", ".pnpm");
21080
- if (existsSync(pnpmDir)) {
21081
- 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)) {
21082
21292
  if (entry.startsWith("@vscode+tree-sitter-wasm@")) {
21083
- const candidate = join2(
21293
+ const candidate = join3(
21084
21294
  pnpmDir,
21085
21295
  entry,
21086
21296
  "node_modules",
@@ -21088,7 +21298,7 @@ function findWasmDir() {
21088
21298
  "tree-sitter-wasm",
21089
21299
  "wasm"
21090
21300
  );
21091
- if (existsSync(join2(candidate, "tree-sitter-typescript.wasm"))) {
21301
+ if (existsSync2(join3(candidate, "tree-sitter-typescript.wasm"))) {
21092
21302
  return candidate;
21093
21303
  }
21094
21304
  }
@@ -21102,27 +21312,27 @@ function findWasmDir() {
21102
21312
  }
21103
21313
  function findRuntimeDir() {
21104
21314
  const here = entryDir();
21105
- const seaRuntime = join2(here, "resources", "wasm");
21106
- 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;
21107
21317
  let dir = here;
21108
21318
  for (let i2 = 0; i2 < 6; i2++) {
21109
- const flat = join2(dir, "node_modules", "web-tree-sitter");
21110
- 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;
21111
21321
  dir = dirname(dir);
21112
21322
  }
21113
21323
  let root = here;
21114
21324
  for (let i2 = 0; i2 < 6; i2++) {
21115
- const pnpmDir = join2(root, "node_modules", ".pnpm");
21116
- if (existsSync(pnpmDir)) {
21117
- 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)) {
21118
21328
  if (entry.startsWith("web-tree-sitter@")) {
21119
- const candidate = join2(
21329
+ const candidate = join3(
21120
21330
  pnpmDir,
21121
21331
  entry,
21122
21332
  "node_modules",
21123
21333
  "web-tree-sitter"
21124
21334
  );
21125
- if (existsSync(join2(candidate, "web-tree-sitter.wasm"))) {
21335
+ if (existsSync2(join3(candidate, "web-tree-sitter.wasm"))) {
21126
21336
  return candidate;
21127
21337
  }
21128
21338
  }
@@ -21139,7 +21349,7 @@ async function loadWebTreeSitter() {
21139
21349
  void err2;
21140
21350
  }
21141
21351
  const here = entryDir();
21142
- const seaResourceBase = join2(here, "resources", "_resolve.js");
21352
+ const seaResourceBase = join3(here, "resources", "_resolve.js");
21143
21353
  const resourceRequire = createRequire(seaResourceBase);
21144
21354
  return resourceRequire("web-tree-sitter");
21145
21355
  }
@@ -21154,9 +21364,9 @@ async function ensureTreeSitterReady() {
21154
21364
  await Parser2.init({
21155
21365
  locateFile: (name2) => {
21156
21366
  if (name2 === "tree-sitter.wasm" || name2 === "web-tree-sitter.wasm") {
21157
- return join2(runtime, name2);
21367
+ return join3(runtime, name2);
21158
21368
  }
21159
- return join2(grammars, name2);
21369
+ return join3(grammars, name2);
21160
21370
  }
21161
21371
  });
21162
21372
  })();
@@ -21168,7 +21378,7 @@ async function loadGrammar(name2) {
21168
21378
  if (cached2) return cached2;
21169
21379
  if (!languageClass) throw new Error("tree-sitter not initialized");
21170
21380
  const grammars = findWasmDir();
21171
- const lang = await languageClass.load(join2(grammars, `${name2}.wasm`));
21381
+ const lang = await languageClass.load(join3(grammars, `${name2}.wasm`));
21172
21382
  grammarCache.set(name2, lang);
21173
21383
  return lang;
21174
21384
  }
@@ -21191,7 +21401,7 @@ var init_tree_sitter_loader = __esm({
21191
21401
  });
21192
21402
 
21193
21403
  // ../../packages/indexer/src/languages/typescript-treesitter.ts
21194
- import { createHash as createHash3 } from "node:crypto";
21404
+ import { createHash as createHash4 } from "node:crypto";
21195
21405
  function extractNode(node, source, containerQname, containerKind) {
21196
21406
  switch (node.type) {
21197
21407
  case "function_declaration":
@@ -21404,7 +21614,7 @@ function scopeRecord(kind, name2, qname, node, body2, source) {
21404
21614
  const sig = header.replace(/\s+/g, " ").trim();
21405
21615
  if (sig) rec.signature = sig;
21406
21616
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
21407
- rec.bodyHash = createHash3("sha256").update(bodyText).digest("hex").slice(0, 16);
21617
+ rec.bodyHash = createHash4("sha256").update(bodyText).digest("hex").slice(0, 16);
21408
21618
  rec.bodySimhash = toHex(simhash(bodyText));
21409
21619
  }
21410
21620
  }
@@ -21846,14 +22056,14 @@ var init_typescript_treesitter = __esm({
21846
22056
  });
21847
22057
 
21848
22058
  // ../../packages/indexer/src/languages/python-treesitter.ts
21849
- import { createHash as createHash4 } from "node:crypto";
22059
+ import { createHash as createHash5 } from "node:crypto";
21850
22060
  function sigAndHash(node, body2, source) {
21851
22061
  if (!body2) return {};
21852
22062
  const out2 = {};
21853
22063
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
21854
22064
  if (sig) out2.signature = sig;
21855
22065
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
21856
- out2.bodyHash = createHash4("sha256").update(bodyText).digest("hex").slice(0, 16);
22066
+ out2.bodyHash = createHash5("sha256").update(bodyText).digest("hex").slice(0, 16);
21857
22067
  out2.bodySimhash = toHex(simhash(bodyText));
21858
22068
  return out2;
21859
22069
  }
@@ -22266,7 +22476,7 @@ var init_python_treesitter = __esm({
22266
22476
  });
22267
22477
 
22268
22478
  // ../../packages/indexer/src/languages/cpp-treesitter.ts
22269
- import { createHash as createHash5 } from "node:crypto";
22479
+ import { createHash as createHash6 } from "node:crypto";
22270
22480
  function extractNode3(node, containerQname, containerIsClass, source) {
22271
22481
  switch (node.type) {
22272
22482
  case "function_definition":
@@ -22450,7 +22660,7 @@ function sigAndHash2(node, body2, source) {
22450
22660
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
22451
22661
  if (sig) out2.signature = sig;
22452
22662
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
22453
- out2.bodyHash = createHash5("sha256").update(bodyText).digest("hex").slice(0, 16);
22663
+ out2.bodyHash = createHash6("sha256").update(bodyText).digest("hex").slice(0, 16);
22454
22664
  out2.bodySimhash = toHex(simhash(bodyText));
22455
22665
  return out2;
22456
22666
  }
@@ -22611,7 +22821,7 @@ var init_cpp_treesitter = __esm({
22611
22821
  });
22612
22822
 
22613
22823
  // ../../packages/indexer/src/languages/go-treesitter.ts
22614
- import { createHash as createHash6 } from "node:crypto";
22824
+ import { createHash as createHash7 } from "node:crypto";
22615
22825
  function extractNode4(node, containerQname, source) {
22616
22826
  switch (node.type) {
22617
22827
  case "function_declaration": {
@@ -22773,7 +22983,7 @@ function sigAndHash3(node, body2, source) {
22773
22983
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
22774
22984
  if (sig) out2.signature = sig;
22775
22985
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
22776
- out2.bodyHash = createHash6("sha256").update(bodyText).digest("hex").slice(0, 16);
22986
+ out2.bodyHash = createHash7("sha256").update(bodyText).digest("hex").slice(0, 16);
22777
22987
  out2.bodySimhash = toHex(simhash(bodyText));
22778
22988
  return out2;
22779
22989
  }
@@ -22917,7 +23127,7 @@ var init_go_treesitter = __esm({
22917
23127
  });
22918
23128
 
22919
23129
  // ../../packages/indexer/src/languages/rust-treesitter.ts
22920
- import { createHash as createHash7 } from "node:crypto";
23130
+ import { createHash as createHash8 } from "node:crypto";
22921
23131
  function extractNode5(node, containerQname, containerIsClass, source) {
22922
23132
  switch (node.type) {
22923
23133
  case "function_item":
@@ -23132,7 +23342,7 @@ function sigAndHash4(node, body2, source) {
23132
23342
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
23133
23343
  if (sig) out2.signature = sig;
23134
23344
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
23135
- out2.bodyHash = createHash7("sha256").update(bodyText).digest("hex").slice(0, 16);
23345
+ out2.bodyHash = createHash8("sha256").update(bodyText).digest("hex").slice(0, 16);
23136
23346
  out2.bodySimhash = toHex(simhash(bodyText));
23137
23347
  return out2;
23138
23348
  }
@@ -23283,7 +23493,7 @@ var init_rust_treesitter = __esm({
23283
23493
  });
23284
23494
 
23285
23495
  // ../../packages/indexer/src/languages/ruby-treesitter.ts
23286
- import { createHash as createHash8 } from "node:crypto";
23496
+ import { createHash as createHash9 } from "node:crypto";
23287
23497
  function sigAndHash5(node, body2, source) {
23288
23498
  if (!body2) {
23289
23499
  const sig2 = source.slice(node.startIndex, node.endIndex).replace(/\s+/g, " ").trim();
@@ -23293,7 +23503,7 @@ function sigAndHash5(node, body2, source) {
23293
23503
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
23294
23504
  if (sig) out2.signature = sig;
23295
23505
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
23296
- out2.bodyHash = createHash8("sha256").update(bodyText).digest("hex").slice(0, 16);
23506
+ out2.bodyHash = createHash9("sha256").update(bodyText).digest("hex").slice(0, 16);
23297
23507
  out2.bodySimhash = toHex(simhash(bodyText));
23298
23508
  return out2;
23299
23509
  }
@@ -23639,7 +23849,7 @@ var init_ruby_treesitter = __esm({
23639
23849
  });
23640
23850
 
23641
23851
  // ../../packages/indexer/src/languages/csharp-treesitter.ts
23642
- import { createHash as createHash9 } from "node:crypto";
23852
+ import { createHash as createHash10 } from "node:crypto";
23643
23853
  function extractNode7(node, containerQname, containerIsType, source, eof) {
23644
23854
  switch (node.type) {
23645
23855
  case "method_declaration":
@@ -23841,7 +24051,7 @@ function sigAndHash6(node, body2, source) {
23841
24051
  const sig = source.slice(node.startIndex, body2.startIndex).replace(/\s+/g, " ").trim();
23842
24052
  if (sig) out2.signature = sig;
23843
24053
  const bodyText = source.slice(body2.startIndex, body2.endIndex);
23844
- out2.bodyHash = createHash9("sha256").update(bodyText).digest("hex").slice(0, 16);
24054
+ out2.bodyHash = createHash10("sha256").update(bodyText).digest("hex").slice(0, 16);
23845
24055
  out2.bodySimhash = toHex(simhash(bodyText));
23846
24056
  return out2;
23847
24057
  }
@@ -23998,6 +24208,7 @@ var init_csharp_treesitter = __esm({
23998
24208
  // ../../packages/indexer/src/index.ts
23999
24209
  var src_exports = {};
24000
24210
  __export(src_exports, {
24211
+ DEFAULT_IGNORES: () => DEFAULT_IGNORES,
24001
24212
  DRIFT_FORK_RATIO: () => DRIFT_FORK_RATIO,
24002
24213
  TreeSitterCSharpProvider: () => TreeSitterCSharpProvider,
24003
24214
  TreeSitterCppProvider: () => TreeSitterCppProvider,
@@ -24014,6 +24225,7 @@ __export(src_exports, {
24014
24225
  findInnermostScope: () => findInnermostScope,
24015
24226
  folderNodeId: () => folderNodeId,
24016
24227
  fromHex: () => fromHex,
24228
+ gitListRelPaths: () => gitListRelPaths,
24017
24229
  hammingDistance: () => hammingDistance,
24018
24230
  hasForkedDrift: () => hasForkedDrift,
24019
24231
  incrementalReindex: () => incrementalReindex,
@@ -24026,7 +24238,7 @@ __export(src_exports, {
24026
24238
  simhashFeatures: () => simhashFeatures,
24027
24239
  symbolNodeId: () => symbolNodeId,
24028
24240
  toHex: () => toHex,
24029
- tokenize: () => tokenize
24241
+ tokenize: () => tokenize2
24030
24242
  });
24031
24243
  function defaultProviders() {
24032
24244
  if (providersCache) return providersCache;
@@ -24079,9 +24291,38 @@ var LOCAL_RULE_OVERRIDES = {
24079
24291
  to: [...CODE_NODE_LABELS, "Symbol"]
24080
24292
  },
24081
24293
  REVEALED_BY: null,
24082
- 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"] }
24083
24309
  };
24084
- 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;
24085
24326
  var SCHEMA_SQL = `
24086
24327
  CREATE TABLE IF NOT EXISTS schema_version (
24087
24328
  version INTEGER PRIMARY KEY
@@ -24096,6 +24337,21 @@ CREATE TABLE IF NOT EXISTS store_meta (
24096
24337
  value TEXT NOT NULL
24097
24338
  );
24098
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
+
24099
24355
  CREATE TABLE IF NOT EXISTS nodes (
24100
24356
  id TEXT PRIMARY KEY,
24101
24357
  label TEXT NOT NULL,
@@ -24472,6 +24728,16 @@ var SqliteGraphStore = class {
24472
24728
  if (!cols.has(name2)) this.db.exec(ddl);
24473
24729
  }
24474
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
+ }
24475
24741
  this.db.prepare("UPDATE schema_version SET version = ?").run(SCHEMA_VERSION);
24476
24742
  });
24477
24743
  }
@@ -24553,6 +24819,7 @@ var SqliteGraphStore = class {
24553
24819
  const violation = this.edgeRuleViolation(edge);
24554
24820
  if (violation) {
24555
24821
  this.rejectedEdgeCount++;
24822
+ this.recordEdgeRejection(edge.type, violation, edge.lastSeenAt || edge.createdAt || 0);
24556
24823
  console.warn(`[local-graph] rejected edge ${edge.from}-[:${edge.type}]->${edge.to}: ${violation}`);
24557
24824
  return;
24558
24825
  }
@@ -24577,22 +24844,51 @@ var SqliteGraphStore = class {
24577
24844
  * overlay consulted first. Returns the reason string on a documented-
24578
24845
  * forbidden combination, else null. Point lookups on the id PK — negligible
24579
24846
  * next to the insert itself. */
24580
- edgeRuleViolation(edge) {
24581
- const fromLabel = this.getNode(edge.from)?.label;
24582
- const toLabel = this.getNode(edge.to)?.label;
24583
- if (edge.type in LOCAL_RULE_OVERRIDES) {
24584
- const rule = LOCAL_RULE_OVERRIDES[edge.type];
24585
- if (!rule) return null;
24586
- if (fromLabel && rule.from && !rule.from.includes(fromLabel)) {
24587
- return `${edge.type} cannot originate from ${fromLabel} locally (allowed: ${rule.from.join(", ")})`;
24588
- }
24589
- if (toLabel && rule.to && !rule.to.includes(toLabel)) {
24590
- return `${edge.type} cannot target ${toLabel} locally (allowed: ${rule.to.join(", ")})`;
24591
- }
24592
- 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 {
24593
24857
  }
24594
- const verdict = isValidEdge(fromLabel, edge.type, toLabel);
24595
- 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
+ );
24596
24892
  }
24597
24893
  updateEdge(id, patch) {
24598
24894
  this.stmts.updateEdge.run({
@@ -24729,6 +25025,163 @@ var SqliteGraphStore = class {
24729
25025
  const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
24730
25026
  return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
24731
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
+ }
24732
25185
  /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
24733
25186
  * expression index so the incremental reindex fetches only the changed files'
24734
25187
  * symbols instead of scanning every versioned node. */
@@ -24749,6 +25202,10 @@ var SqliteGraphStore = class {
24749
25202
  if (!live) return null;
24750
25203
  const version2 = live.version ?? 1;
24751
25204
  const frozenId = `${liveId}@v${version2}`;
25205
+ if (this.getNode(frozenId)) {
25206
+ this.stmts.advanceLive.run({ live_id: liveId, t });
25207
+ return frozenId;
25208
+ }
24752
25209
  this.stmts.freezeCopy.run({ frozen_id: frozenId, live_id: liveId, t });
24753
25210
  this.mergeEdge({
24754
25211
  id: `edge_superseded_${frozenId}`,
@@ -24807,6 +25264,7 @@ var CAUSAL_FAMILY = [
24807
25264
 
24808
25265
  // ../../packages/local-graph/src/justification.ts
24809
25266
  init_src();
25267
+ init_src2();
24810
25268
  function addDependency(store2, opts) {
24811
25269
  const id = digest({ from: opts.fromId, type: "DEPENDS_ON", to: opts.toId });
24812
25270
  const attrs = { relation: opts.relation };
@@ -24829,13 +25287,72 @@ function addDependency(store2, opts) {
24829
25287
  store2.mergeEdge(edge);
24830
25288
  return edge;
24831
25289
  }
24832
- function markRevisit(store2, node, reason, ts) {
25290
+ function markRevisit(store2, node, reason, ts, answeredWhen) {
24833
25291
  const fresh = store2.getNode(node.id) ?? node;
24834
25292
  store2.updateNode(node.id, {
24835
- 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
+ },
24836
25300
  lastUpdatedAt: ts
24837
25301
  });
24838
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
+ }
24839
25356
  function listNeedsRevisit(store2, asOf) {
24840
25357
  return store2.nodesAsOf(asOf).filter((n) => n.attrs["revisit"] === true).map((n) => ({
24841
25358
  id: n.id,
@@ -24968,7 +25485,6 @@ function markBoth(store2, a, b, patch, ts) {
24968
25485
  // ../../packages/local-graph/src/design-problem.ts
24969
25486
  init_src();
24970
25487
  init_src();
24971
- init_src2();
24972
25488
 
24973
25489
  // ../../packages/local-graph/src/problem-package-link.ts
24974
25490
  init_src();
@@ -25154,46 +25670,109 @@ function backfillProblemContext(store2, ts) {
25154
25670
  }
25155
25671
 
25156
25672
  // ../../packages/local-graph/src/design-problem.ts
25157
- function isConstraintProblem(node) {
25158
- return node.attrs["kind"] === "constraint";
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);
25159
25688
  }
25160
- function buildNode(id, label, description, ts, attrs) {
25161
- return {
25162
- id,
25163
- label,
25164
- description,
25165
- extractionConfidence: 0.4,
25166
- // provisional: below the error-path's 0.6+
25167
- extractionSource: "agent-observed",
25168
- embedding: [],
25169
- cumulativeSurprise: 0,
25170
- peakSurprise: 0,
25171
- cumulativeHits: 1,
25172
- lastUpdatedAt: ts,
25173
- createdAt: ts,
25174
- memoryTier: "short-term",
25175
- pageRank: 0,
25176
- isLandmark: false,
25177
- community: null,
25178
- stability: "unstable",
25179
- attrs
25180
- };
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);
25181
25694
  }
25182
- function mergeEdge(store2, from, to, type, ts) {
25183
- store2.mergeEdge({
25184
- id: `edge_${digest({ from, type, to })}`.slice(0, 24),
25185
- from,
25186
- to,
25187
- type,
25188
- confidence: 0.4,
25189
- extractionSource: "agent-observed",
25190
- createdAt: ts,
25191
- lastSeenAt: ts,
25192
- navSuccesses: 0,
25193
- navFailures: 0,
25194
- attrs: { provisional: true }
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
+ }
25195
25729
  });
25730
+ return report;
25731
+ }
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
+ const dupSeq = dup.attrs["createdAtSeq"] ?? store2.currentIngestSeq();
25738
+ const exposure = priorExposure(store2, survivor.id, dupSeq);
25739
+ const expField = `reinforce${exposure[0].toUpperCase()}${exposure.slice(1)}Count`;
25740
+ store2.updateNode(survivor.id, {
25741
+ attrs: {
25742
+ ...surv.attrs,
25743
+ sources: [...sources],
25744
+ corroborations: corroborations(surv) + corroborations(dup) + 1,
25745
+ [expField]: (surv.attrs[expField] ?? 0) + 1,
25746
+ lastReinforceExposure: exposure
25747
+ },
25748
+ cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
25749
+ lastUpdatedAt: ts
25750
+ });
25751
+ for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
25752
+ if (e.to === survivor.id) continue;
25753
+ const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
25754
+ if (store2.getEdge(id)) continue;
25755
+ const redirected = {
25756
+ ...e,
25757
+ id,
25758
+ from: survivor.id,
25759
+ createdAt: ts,
25760
+ lastSeenAt: ts
25761
+ };
25762
+ store2.mergeEdge(redirected);
25763
+ }
25764
+ store2.updateNode(dup.id, {
25765
+ attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
25766
+ lastUpdatedAt: ts
25767
+ });
25768
+ store2.closeNode(dup.id, ts);
25769
+ }
25770
+
25771
+ // ../../packages/local-graph/src/design-problem.ts
25772
+ function isConstraintProblem(node) {
25773
+ return node.attrs["kind"] === "constraint";
25196
25774
  }
25775
+ var RECURRENCE_DEBOUNCE_MS = 60 * 60 * 1e3;
25197
25776
  var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
25198
25777
  "Solution",
25199
25778
  "RootCause",
@@ -25201,6 +25780,9 @@ var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
25201
25780
  "Technique",
25202
25781
  "AntiPattern"
25203
25782
  ]);
25783
+ function isAutoMinted(n) {
25784
+ return n.label === "Solution" && String(n.description ?? "").startsWith(AUTO_MINT_PREFIX);
25785
+ }
25204
25786
  function priorsForFile(store2, relPath) {
25205
25787
  const want = relPath.trim().replace(/^\.?\//, "");
25206
25788
  let file2 = null;
@@ -25213,37 +25795,52 @@ function priorsForFile(store2, relPath) {
25213
25795
  if (!file2) return null;
25214
25796
  const openProblems = [];
25215
25797
  const constraints = [];
25798
+ const resolvedProblems = [];
25216
25799
  const seenProblem = /* @__PURE__ */ new Set();
25217
25800
  const related = /* @__PURE__ */ new Map();
25218
25801
  for (const e of store2.inEdges(file2.id, ["ANCHORED_AT"])) {
25219
25802
  const n = store2.getNode(e.from);
25220
25803
  if (!n) continue;
25221
25804
  if (n.label === "Problem") {
25222
- if (!n.attrs["resolvedAt"] && !seenProblem.has(n.id)) {
25223
- seenProblem.add(n.id);
25805
+ if (seenProblem.has(n.id)) continue;
25806
+ seenProblem.add(n.id);
25807
+ if (!n.attrs["resolvedAt"]) {
25224
25808
  (isConstraintProblem(n) ? constraints : openProblems).push(n);
25809
+ } else if (n.attrs["resolvedAs"] == null && n.attrs["mergedInto"] === void 0 && e.attrs?.["anchorProvenance"] !== "legacy") {
25810
+ resolvedProblems.push(n);
25225
25811
  }
25226
- } else if (CITABLE_PRIOR_LABELS.has(n.label)) {
25812
+ } else if (CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) {
25227
25813
  related.set(n.id, n);
25228
25814
  }
25229
25815
  }
25230
25816
  const solutionsByProblem = /* @__PURE__ */ new Map();
25231
- for (const p of [...openProblems, ...constraints]) {
25817
+ for (const p of [...openProblems, ...constraints, ...resolvedProblems]) {
25232
25818
  for (const e of store2.outEdges(p.id, ["SOLVED_BY", "CAUSED_BY", "INSTANCE_OF"])) {
25233
25819
  const n = store2.getNode(e.to);
25234
- if (n && CITABLE_PRIOR_LABELS.has(n.label)) related.set(n.id, n);
25235
- if (n && n.label === "Solution" && e.type === "SOLVED_BY") {
25820
+ if (n && CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) related.set(n.id, n);
25821
+ if (n && n.label === "Solution" && e.type === "SOLVED_BY" && !isAutoMinted(n)) {
25236
25822
  const list = solutionsByProblem.get(p.id) ?? [];
25237
25823
  if (!list.some((s) => s.id === n.id)) list.push(n);
25238
25824
  solutionsByProblem.set(p.id, list);
25239
25825
  }
25240
25826
  }
25241
25827
  }
25242
- if (openProblems.length === 0 && constraints.length === 0 && related.size === 0) return null;
25828
+ if (openProblems.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
25829
+ return null;
25243
25830
  const recentFirst = (a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0);
25244
25831
  openProblems.sort(recentFirst);
25245
25832
  constraints.sort(recentFirst);
25246
- return { file: file2, openProblems, constraints, related: [...related.values()], solutionsByProblem };
25833
+ resolvedProblems.sort(
25834
+ (a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)
25835
+ );
25836
+ return {
25837
+ file: file2,
25838
+ openProblems,
25839
+ constraints,
25840
+ resolvedProblems,
25841
+ related: [...related.values()],
25842
+ solutionsByProblem
25843
+ };
25247
25844
  }
25248
25845
  function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
25249
25846
  const p = store2.getNode(problemId);
@@ -25254,8 +25851,34 @@ function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
25254
25851
  });
25255
25852
  return true;
25256
25853
  }
25854
+ function reopenAutoClosedProblems(store2, t) {
25855
+ const report = { reopened: 0, alreadySuspect: 0, agentDescribed: 0 };
25856
+ for (const p of store2.findNodesByLabel("Problem")) {
25857
+ if (p.attrs["resolvedAt"] == null) continue;
25858
+ if (p.attrs["resolvedAs"] != null) continue;
25859
+ if (p.attrs["mergedInto"] !== void 0) continue;
25860
+ if (p.attrs["resolutionWitnessed"] === true) {
25861
+ report.agentDescribed++;
25862
+ continue;
25863
+ }
25864
+ const sols = store2.outEdges(p.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.to)).filter((n) => n !== null);
25865
+ if (sols.length === 0) continue;
25866
+ if (!sols.every((s) => String(s.description ?? "").startsWith(AUTO_MINT_PREFIX))) {
25867
+ report.agentDescribed++;
25868
+ continue;
25869
+ }
25870
+ if (p.attrs["resolutionSuspect"] === true) report.alreadySuspect++;
25871
+ const attrs = { ...p.attrs };
25872
+ delete attrs["resolvedAt"];
25873
+ attrs["reopenedFrom"] = "auto-close";
25874
+ attrs["reopenedAt"] = t;
25875
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
25876
+ report.reopened++;
25877
+ }
25878
+ return report;
25879
+ }
25257
25880
  var AUTO_MINT_PREFIX = "addressed by an edit to ";
25258
- function resolveDesignProblems(store2, t) {
25881
+ function markFixCandidates(store2, t) {
25259
25882
  let resolved = 0;
25260
25883
  for (const p of store2.findNodesByLabel("Problem")) {
25261
25884
  if (!p.id.startsWith("dprob_")) continue;
@@ -25264,10 +25887,13 @@ function resolveDesignProblems(store2, t) {
25264
25887
  let symName = "";
25265
25888
  let symRelPath;
25266
25889
  let edited = false;
25890
+ const since = Math.max(p.createdAt, Number(p.attrs["fixCandidateClearedAt"] ?? 0));
25267
25891
  for (const e of store2.outEdges(p.id, ["ANCHORED_AT"])) {
25268
25892
  if (e.attrs?.["mention"] === true) continue;
25893
+ if (e.attrs?.["anchorProvenance"] === "legacy") continue;
25269
25894
  const sym = store2.getNode(e.to);
25270
- if (sym && sym.lastUpdatedAt > p.createdAt) {
25895
+ const changedAt = sym?.label === "File" ? Number(sym.attrs["contentChangedAt"] ?? 0) : sym?.lastUpdatedAt ?? 0;
25896
+ if (sym && changedAt > since) {
25271
25897
  edited = true;
25272
25898
  symName = sym.description;
25273
25899
  symRelPath = sym.attrs["relPath"] ?? void 0;
@@ -25275,35 +25901,182 @@ function resolveDesignProblems(store2, t) {
25275
25901
  }
25276
25902
  }
25277
25903
  if (!edited) continue;
25278
- if (store2.outEdges(p.id, ["SOLVED_BY"]).length === 0) {
25279
- const solId = `dfix_${digest({ problemId: p.id, edit: t })}`.slice(0, 56);
25280
- store2.mergeNode(
25281
- buildNode(solId, "Solution", `${AUTO_MINT_PREFIX}${symName}`, t, {
25282
- source: "convo",
25283
- provisional: false,
25284
- // AC-resolution-attrs: the resolving symbol/file as STRUCTURED data, not
25285
- // just prose — the F2 join key downstream backfills and the viz read.
25286
- resolvedSymbols: [symName],
25287
- ...symRelPath ? { resolvedRelPath: symRelPath } : {}
25288
- })
25289
- );
25290
- mergeEdge(store2, p.id, solId, "SOLVED_BY", t);
25291
- for (const ae of store2.outEdges(p.id, ["ANCHORED_AT"]))
25292
- mergeEdge(store2, solId, ae.to, "ANCHORED_AT", t);
25293
- }
25904
+ if (p.attrs["fixCandidateAt"] !== void 0) continue;
25905
+ if (store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
25906
+ const s = store2.getNode(e.to);
25907
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
25908
+ }))
25909
+ continue;
25294
25910
  store2.updateNode(p.id, {
25295
- attrs: { ...p.attrs, provisional: false, resolvedAt: t },
25911
+ attrs: {
25912
+ ...p.attrs,
25913
+ // The cue, kept as structured data so the render can name the file it is
25914
+ // asking about. Deliberately NOT `resolvedAt` — this is a question.
25915
+ fixCandidateAt: t,
25916
+ fixCandidateSymbol: symName,
25917
+ ...symRelPath ? { fixCandidateFile: symRelPath } : {},
25918
+ // Snapshot the render-ledger counter so "shown since THIS ask" is
25919
+ // measurable. `shownCount` is the node's LIFETIME count across every
25920
+ // band, so comparing it raw would retire a fresh ask on a
25921
+ // frequently-surfaced problem before anyone had seen the question once.
25922
+ fixCandidateShownBase: Number(p.attrs["shownCount"] ?? 0)
25923
+ },
25296
25924
  lastUpdatedAt: t
25297
25925
  });
25298
25926
  resolved++;
25299
25927
  }
25300
25928
  return resolved;
25301
25929
  }
25930
+ var FIX_CANDIDATE_ASK_LIMIT = 5;
25931
+ function clearSettledFixCandidates(store2, t) {
25932
+ const report = { answered: 0, ignored: 0, unsubstantiated: 0, standing: 0 };
25933
+ for (const p of store2.findNodesByLabel("Problem")) {
25934
+ if (p.attrs["fixCandidateAt"] === void 0) continue;
25935
+ 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");
25936
+ const unsubstantiated = fileAnchors.length > 0 && fileAnchors.every((f) => f.attrs["contentChangedAt"] === void 0);
25937
+ const answered = p.attrs["resolvedAt"] != null || store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
25938
+ const s = store2.getNode(e.to);
25939
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
25940
+ });
25941
+ const base = Number(
25942
+ p.attrs["fixCandidateShownBase"] ?? p.attrs["shownCount"] ?? 0
25943
+ );
25944
+ const shownSinceAsk = Number(p.attrs["shownCount"] ?? 0) - base;
25945
+ const ignored = shownSinceAsk >= FIX_CANDIDATE_ASK_LIMIT;
25946
+ if (!answered && !ignored && !unsubstantiated) {
25947
+ report.standing++;
25948
+ continue;
25949
+ }
25950
+ const attrs = { ...p.attrs };
25951
+ delete attrs["fixCandidateAt"];
25952
+ delete attrs["fixCandidateSymbol"];
25953
+ delete attrs["fixCandidateFile"];
25954
+ attrs["fixCandidateClearedAt"] = t;
25955
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
25956
+ if (answered) report.answered++;
25957
+ else if (ignored) report.ignored++;
25958
+ else report.unsubstantiated++;
25959
+ }
25960
+ return report;
25961
+ }
25962
+ function priorExposure(store2, nodeId, atSeq) {
25963
+ const n = store2.getNode(nodeId);
25964
+ if (!n || !n.attrs) return "unshown";
25965
+ const shownSeq = n.attrs["lastShownAtSeq"];
25966
+ if (shownSeq !== void 0 && shownSeq <= atSeq) return "shown";
25967
+ const evictedSeq = n.attrs["lastEvictedAtSeq"];
25968
+ if (evictedSeq !== void 0 && evictedSeq <= atSeq) return "evicted";
25969
+ if ((n.attrs["evictedCount"] ?? 0) > 0) return "evicted";
25970
+ return "unshown";
25971
+ }
25972
+
25973
+ // ../../packages/local-graph/src/intent.ts
25974
+ init_src();
25302
25975
 
25303
25976
  // ../../packages/local-graph/src/anchor-backfill.ts
25977
+ init_src();
25978
+ init_src2();
25304
25979
  function isLegacyAnchor(attrs) {
25305
25980
  return attrs?.["captureTime"] === true && attrs["anchorProvenance"] === void 0;
25306
25981
  }
25982
+ var STATEMENT_PATH_RE = new RegExp(
25983
+ String.raw`(?:[\w.+-]+\/)+[\w.+-]+\.(?:${anchorableExtensionAlternation()})`,
25984
+ "g"
25985
+ );
25986
+ function repairLegacyAnchorsFromStatement(store2, ts) {
25987
+ const report = {
25988
+ repaired: [],
25989
+ retiredGuesses: 0,
25990
+ noPathNamed: 0,
25991
+ pathUnknown: 0,
25992
+ alreadyCorrect: 0
25993
+ };
25994
+ const byPath = /* @__PURE__ */ new Map();
25995
+ for (const f of store2.findNodesByLabel("File")) {
25996
+ const path = String(f.attrs["relPath"] ?? f.description ?? "");
25997
+ if (path) byPath.set(path, { id: f.id, path });
25998
+ }
25999
+ const resolvePath = (named) => {
26000
+ const exact = byPath.get(named);
26001
+ if (exact) return exact;
26002
+ const hits = [...byPath.values()].filter(
26003
+ (f) => f.path.endsWith(`/${named}`) || named.endsWith(`/${f.path}`)
26004
+ );
26005
+ return hits.length === 1 ? hits[0] : null;
26006
+ };
26007
+ for (const p of store2.findNodesByLabel("Problem")) {
26008
+ try {
26009
+ const anchors = store2.outEdges(p.id, ["ANCHORED_AT"]);
26010
+ const legacy = anchors.filter((e) => e.attrs?.["anchorProvenance"] === "legacy");
26011
+ if (legacy.length === 0) continue;
26012
+ const better = anchors.filter((e) => {
26013
+ const prov = e.attrs?.["anchorProvenance"];
26014
+ return prov === "restated" || prov === "witnessed" || prov === "edited";
26015
+ });
26016
+ if (better.length > 0) {
26017
+ const betterTargets = new Set(better.map((e) => e.to));
26018
+ let retired = 0;
26019
+ for (const e of legacy) {
26020
+ if (!betterTargets.has(e.to)) {
26021
+ store2.closeEdge(e.id, ts);
26022
+ retired++;
26023
+ }
26024
+ }
26025
+ report.retiredGuesses += retired;
26026
+ continue;
26027
+ }
26028
+ const named = [...String(p.description ?? "").matchAll(STATEMENT_PATH_RE)].map((m) => m[0]);
26029
+ if (named.length === 0) {
26030
+ report.noPathNamed++;
26031
+ continue;
26032
+ }
26033
+ const guessedPaths = legacy.map((e) => {
26034
+ const f = store2.getNode(e.to);
26035
+ return String(f?.attrs["relPath"] ?? f?.description ?? "");
26036
+ });
26037
+ if (named.some((x) => guessedPaths.some((g) => g.endsWith(x) || x.endsWith(g)))) {
26038
+ report.alreadyCorrect++;
26039
+ continue;
26040
+ }
26041
+ const target = named.map(resolvePath).find((f) => f !== null);
26042
+ if (!target) {
26043
+ report.pathUnknown++;
26044
+ continue;
26045
+ }
26046
+ store2.mergeEdge({
26047
+ id: `edge_${digest({ from: p.id, type: "ANCHORED_AT", to: target.id })}`.slice(0, 24),
26048
+ from: p.id,
26049
+ to: target.id,
26050
+ type: "ANCHORED_AT",
26051
+ // Same confidence a capture-time anchor enters at: the source is the
26052
+ // agent's own statement either way, only the moment of reading differs.
26053
+ confidence: 0.4,
26054
+ extractionSource: "agent-observed",
26055
+ createdAt: ts,
26056
+ lastSeenAt: ts,
26057
+ navSuccesses: 0,
26058
+ navFailures: 0,
26059
+ attrs: {
26060
+ anchorProvenance: "restated",
26061
+ restatedFrom: guessedPaths[0] ?? "",
26062
+ restatedAt: ts
26063
+ }
26064
+ });
26065
+ for (const e of legacy) {
26066
+ if (e.to !== target.id) store2.closeEdge(e.id, ts);
26067
+ }
26068
+ report.retiredGuesses += legacy.filter((e) => e.to !== target.id).length;
26069
+ report.repaired.push({
26070
+ problemId: p.id,
26071
+ problem: p.description,
26072
+ guessed: guessedPaths[0] ?? "",
26073
+ restated: target.path
26074
+ });
26075
+ } catch {
26076
+ }
26077
+ }
26078
+ return report;
26079
+ }
25307
26080
  function backfillLegacyAnchors(store2, ts) {
25308
26081
  const report = {
25309
26082
  demotedEdges: 0,
@@ -25343,7 +26116,11 @@ function backfillLegacyAnchors(store2, ts) {
25343
26116
  store2,
25344
26117
  sol,
25345
26118
  `auto-closed off a pre-provenance anchor (guessed ${guessedAnchor}) \u2014 confirm the problem is really fixed, or reopen it`,
25346
- ts
26119
+ ts,
26120
+ // "or reopen it" is half the ask, and the nightly reopen takes that
26121
+ // branch — so record what would answer this, or the flag can never be
26122
+ // lowered and the band fills with settled questions.
26123
+ "parentProblemOpen"
25347
26124
  );
25348
26125
  store2.updateNode(p.id, {
25349
26126
  attrs: { ...p.attrs, resolutionSuspect: true },
@@ -25377,7 +26154,7 @@ function pagerank(input) {
25377
26154
  const ids = input.nodeIds;
25378
26155
  const n = ids.length;
25379
26156
  if (n === 0) {
25380
- return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
26157
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true, danglingRank: 0 };
25381
26158
  }
25382
26159
  const index = /* @__PURE__ */ new Map();
25383
26160
  for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
@@ -25449,8 +26226,12 @@ function pagerank(input) {
25449
26226
  }
25450
26227
  }
25451
26228
  const scores = /* @__PURE__ */ new Map();
25452
- for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
25453
- return { scores, iterations: iter, converged };
26229
+ let danglingRank = 0;
26230
+ for (let i2 = 0; i2 < n; i2++) {
26231
+ scores.set(ids[i2], score2[i2]);
26232
+ if (dangling[i2]) danglingRank += score2[i2];
26233
+ }
26234
+ return { scores, iterations: iter, converged, danglingRank };
25454
26235
  }
25455
26236
  function markLandmarks(scores, percentile = 0.1, filter) {
25456
26237
  const entries = [...scores.entries()];
@@ -25460,7 +26241,7 @@ function markLandmarks(scores, percentile = 0.1, filter) {
25460
26241
  }
25461
26242
  }
25462
26243
  if (entries.length === 0) return /* @__PURE__ */ new Set();
25463
- entries.sort((a, b) => b[1] - a[1]);
26244
+ entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
25464
26245
  const cutoff = Math.max(1, Math.floor(entries.length * percentile));
25465
26246
  const out2 = /* @__PURE__ */ new Set();
25466
26247
  for (let i2 = 0; i2 < cutoff; i2++) {
@@ -25698,8 +26479,86 @@ function motifDecision(input) {
25698
26479
  };
25699
26480
  }
25700
26481
 
25701
- // ../../packages/local-graph/src/community.ts
25702
- init_src2();
26482
+ // ../../packages/math/src/pagerank-local.ts
26483
+ function pagerankLocal(input) {
26484
+ const damping = input.damping ?? 0.85;
26485
+ const tol = input.tolerance ?? 1e-6;
26486
+ const maxIter = input.maxIterations ?? 100;
26487
+ const ids = input.regionIds;
26488
+ const n = ids.length;
26489
+ if (n === 0 || input.globalN === 0) {
26490
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
26491
+ }
26492
+ const index = /* @__PURE__ */ new Map();
26493
+ for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
26494
+ const base = (1 - damping) / input.globalN;
26495
+ const outSum = new Float64Array(n);
26496
+ const inflow = new Float64Array(n);
26497
+ let score2 = new Float64Array(n);
26498
+ for (let i2 = 0; i2 < n; i2++) {
26499
+ const id = ids[i2];
26500
+ outSum[i2] = input.outSum.get(id) ?? 0;
26501
+ inflow[i2] = input.boundaryInflow.get(id) ?? 0;
26502
+ score2[i2] = input.rank0.get(id) ?? base;
26503
+ }
26504
+ const rowLen = new Int32Array(n);
26505
+ let edgeCount = 0;
26506
+ for (const [from, edges] of input.out) {
26507
+ const fi = index.get(from);
26508
+ if (fi === void 0) continue;
26509
+ let inSet = 0;
26510
+ for (const e of edges) if (index.has(e.to)) inSet++;
26511
+ rowLen[fi] = inSet;
26512
+ edgeCount += inSet;
26513
+ }
26514
+ const rowStart = new Int32Array(n + 1);
26515
+ for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
26516
+ const colIdx = new Int32Array(edgeCount);
26517
+ const colW = new Float64Array(edgeCount);
26518
+ const cursor = rowStart.slice(0, n);
26519
+ for (const [from, edges] of input.out) {
26520
+ const fi = index.get(from);
26521
+ if (fi === void 0) continue;
26522
+ for (const e of edges) {
26523
+ const ti = index.get(e.to);
26524
+ if (ti === void 0) continue;
26525
+ const c = cursor[fi];
26526
+ cursor[fi] = c + 1;
26527
+ colIdx[c] = ti;
26528
+ colW[c] = e.weight;
26529
+ }
26530
+ }
26531
+ const externalDangling = input.externalDanglingRank ?? 0;
26532
+ let next = new Float64Array(n);
26533
+ let iter = 0;
26534
+ let converged = false;
26535
+ for (; iter < maxIter; iter++) {
26536
+ let internalDangling = 0;
26537
+ for (let i2 = 0; i2 < n; i2++) if (outSum[i2] <= 0) internalDangling += score2[i2];
26538
+ const danglingShare = damping * (internalDangling + externalDangling) / input.globalN;
26539
+ for (let i2 = 0; i2 < n; i2++) next[i2] = base + danglingShare + damping * inflow[i2];
26540
+ for (let i2 = 0; i2 < n; i2++) {
26541
+ const os2 = outSum[i2];
26542
+ if (os2 <= 0) continue;
26543
+ const f = damping * score2[i2] / os2;
26544
+ const end = rowStart[i2 + 1];
26545
+ for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
26546
+ }
26547
+ let diff = 0;
26548
+ for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
26549
+ const tmp = score2;
26550
+ score2 = next;
26551
+ next = tmp;
26552
+ if (diff < tol) {
26553
+ iter++;
26554
+ converged = true;
26555
+ break;
26556
+ }
26557
+ }
26558
+ const scores = /* @__PURE__ */ new Map();
26559
+ for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
26560
+ return { scores, iterations: iter, converged };
26561
+ }
25703
26562
 
25704
26563
  // ../../packages/local-graph/src/triage.ts
25705
26564
  init_src();
@@ -25708,96 +26567,8 @@ init_src();
25708
26567
  init_src2();
25709
26568
  var DRIFT_VALUES = new Set(Object.values(DRIFT_KIND));
25710
26569
 
25711
- // ../../packages/local-graph/src/problem-dedup.ts
25712
- init_src();
25713
- init_src();
25714
- var REDIRECT_EDGES = [
25715
- "CAUSED_BY",
25716
- "SOLVED_BY",
25717
- "FIXED_BY",
25718
- "ANCHORED_AT",
25719
- "MANIFESTED_IN",
25720
- "EVIDENCED_BY"
25721
- ];
25722
- function corroborations(n) {
25723
- return Number(n.attrs["corroborations"] ?? 0);
25724
- }
25725
- function overlap(a, b) {
25726
- if (a.size === 0 || b.size === 0) return 0;
25727
- let inter = 0;
25728
- for (const x of a) if (b.has(x)) inter++;
25729
- return inter / Math.min(a.size, b.size);
25730
- }
25731
- function mergeDuplicateProblems(store2, opts) {
25732
- const minOverlap = opts.minOverlap ?? 0.85;
25733
- const minTokens = opts.minTokens ?? 4;
25734
- const report = { clusters: 0, merged: 0 };
25735
- const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
25736
- const tokens = /* @__PURE__ */ new Map();
25737
- for (const p of open) tokens.set(p.id, new Set(conceptTokens(p.description)));
25738
- const consumed = /* @__PURE__ */ new Set();
25739
- store2.transaction(() => {
25740
- for (let i2 = 0; i2 < open.length; i2++) {
25741
- const a = open[i2];
25742
- if (consumed.has(a.id)) continue;
25743
- const cluster = [a];
25744
- const ta = tokens.get(a.id);
25745
- for (let j = i2 + 1; j < open.length; j++) {
25746
- const b = open[j];
25747
- if (consumed.has(b.id)) continue;
25748
- const tb = tokens.get(b.id);
25749
- if (Math.min(ta.size, tb.size) < minTokens) continue;
25750
- if (isConstraintProblem(a) !== isConstraintProblem(b)) continue;
25751
- if (overlap(ta, tb) >= minOverlap) {
25752
- cluster.push(b);
25753
- consumed.add(b.id);
25754
- }
25755
- }
25756
- if (cluster.length < 2) continue;
25757
- report.clusters++;
25758
- cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
25759
- const survivor = cluster[0];
25760
- for (const dup of cluster.slice(1)) {
25761
- foldDuplicateProblem(store2, dup, survivor, opts.ts);
25762
- report.merged++;
25763
- }
25764
- }
25765
- });
25766
- return report;
25767
- }
25768
- function foldDuplicateProblem(store2, dup, survivor, ts) {
25769
- const surv = store2.getNode(survivor.id);
25770
- if (!surv) return;
25771
- const sources = new Set(surv.attrs["sources"] ?? []);
25772
- for (const s of dup.attrs["sources"] ?? []) sources.add(s);
25773
- store2.updateNode(survivor.id, {
25774
- attrs: {
25775
- ...surv.attrs,
25776
- sources: [...sources],
25777
- corroborations: corroborations(surv) + corroborations(dup) + 1
25778
- },
25779
- cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
25780
- lastUpdatedAt: ts
25781
- });
25782
- for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
25783
- if (e.to === survivor.id) continue;
25784
- const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
25785
- if (store2.getEdge(id)) continue;
25786
- const redirected = {
25787
- ...e,
25788
- id,
25789
- from: survivor.id,
25790
- createdAt: ts,
25791
- lastSeenAt: ts
25792
- };
25793
- store2.mergeEdge(redirected);
25794
- }
25795
- store2.updateNode(dup.id, {
25796
- attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
25797
- lastUpdatedAt: ts
25798
- });
25799
- store2.closeNode(dup.id, ts);
25800
- }
26570
+ // ../../packages/local-graph/src/community.ts
26571
+ init_src2();
25801
26572
 
25802
26573
  // ../../packages/local-graph/src/tools.ts
25803
26574
  init_src();
@@ -25814,6 +26585,9 @@ function rankToolsForHandles(store2, limit = 5) {
25814
26585
  // ../../packages/local-graph/src/principle-sync.ts
25815
26586
  init_src2();
25816
26587
 
26588
+ // ../../packages/local-graph/src/mechanism-liveness.ts
26589
+ var STALL_MIN_SPAN_MS = 60 * 60 * 1e3;
26590
+
25817
26591
  // ../../packages/generalizer/src/generalizer.ts
25818
26592
  init_src2();
25819
26593
  init_src();
@@ -25933,7 +26707,164 @@ function promoteMotifs(store2, t) {
25933
26707
  }
25934
26708
 
25935
26709
  // ../../packages/generalizer/src/nightly.ts
25936
- function runNightlyPipeline(store2) {
26710
+ var FULL_RESCORE_EVERY = 12;
26711
+ var INCREMENTAL_REGION_MAX_FRACTION = 0.2;
26712
+ var INCREMENTAL_HOPS = 3;
26713
+ function runGraphRescore(store2, opts = {}) {
26714
+ const lastRescoreAt = Number(store2.getMeta?.("lastRescoreAt") ?? 0);
26715
+ const sinceFull = Number(store2.getMeta?.("rescoresSinceFull") ?? 0);
26716
+ 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;
26717
+ if (incrementalCapable) {
26718
+ const started = Date.now();
26719
+ const inc = tryIncrementalRescore(store2, lastRescoreAt, started);
26720
+ if (inc) {
26721
+ store2.setMeta("lastRescoreAt", String(started));
26722
+ store2.setMeta("rescoresSinceFull", String(sinceFull + 1));
26723
+ return inc;
26724
+ }
26725
+ }
26726
+ const report = runFullRescore(store2);
26727
+ store2.setMeta?.("lastRescoreAt", String(report.startedAt));
26728
+ store2.setMeta?.("rescoresSinceFull", "0");
26729
+ store2.setMeta?.("danglingRankShare", String(report.danglingRank));
26730
+ const { startedAt: _drop, danglingRank: _drop2, ...rest } = report;
26731
+ return rest;
26732
+ }
26733
+ function tryIncrementalRescore(store2, lastRescoreAt, started) {
26734
+ const allowedTypes = new Set(pagerankEdgeTypes());
26735
+ const liveN = store2.nodeCount();
26736
+ const cap = Math.max(1e3, Math.floor(liveN * INCREMENTAL_REGION_MAX_FRACTION));
26737
+ const region = new Set(store2.dirtyNodeIdsSince(lastRescoreAt));
26738
+ for (const e of store2.edgeEndpointsTouchedSince(lastRescoreAt)) {
26739
+ region.add(e.from);
26740
+ region.add(e.to);
26741
+ }
26742
+ if (region.size > cap) return null;
26743
+ let frontier = [...region];
26744
+ for (let hop = 0; hop < INCREMENTAL_HOPS && frontier.length > 0; hop++) {
26745
+ const next = [];
26746
+ for (const e of store2.outEdgesForMany(frontier)) {
26747
+ if (!allowedTypes.has(e.type)) continue;
26748
+ if (!region.has(e.to)) {
26749
+ region.add(e.to);
26750
+ next.push(e.to);
26751
+ }
26752
+ }
26753
+ if (region.size > cap) return null;
26754
+ frontier = next;
26755
+ }
26756
+ const rank0 = store2.ranksForMany([...region]);
26757
+ const regionIds = [...rank0.keys()];
26758
+ if (regionIds.length === 0) {
26759
+ return {
26760
+ scoredNodes: 0,
26761
+ iterations: 0,
26762
+ converged: true,
26763
+ landmarks: 0,
26764
+ communities: 0,
26765
+ motifsPromoted: 0,
26766
+ techniques: 0,
26767
+ antipatterns: 0,
26768
+ pageRankWritten: 0,
26769
+ landmarkFlips: 0,
26770
+ mode: "incremental",
26771
+ regionSize: 0,
26772
+ durationMs: Date.now() - started
26773
+ };
26774
+ }
26775
+ const inRegion = new Set(regionIds);
26776
+ const out2 = /* @__PURE__ */ new Map();
26777
+ const outSum = /* @__PURE__ */ new Map();
26778
+ for (const e of store2.outEdgesForMany(regionIds)) {
26779
+ if (!allowedTypes.has(e.type)) continue;
26780
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26781
+ if (w <= 0) continue;
26782
+ outSum.set(e.from, (outSum.get(e.from) ?? 0) + w);
26783
+ if (!inRegion.has(e.to)) continue;
26784
+ const l = out2.get(e.from);
26785
+ if (l) l.push({ to: e.to, weight: w });
26786
+ else out2.set(e.from, [{ to: e.to, weight: w }]);
26787
+ }
26788
+ const boundaryEdges = store2.inEdgesForMany(regionIds).filter((e) => allowedTypes.has(e.type) && !inRegion.has(e.from) && (EDGE_WEIGHT[e.type] ?? 1) > 0);
26789
+ const boundarySources = [...new Set(boundaryEdges.map((e) => e.from))];
26790
+ if (boundarySources.length > cap) return null;
26791
+ const boundaryRanks = store2.ranksForMany(boundarySources);
26792
+ const boundaryOutSum = /* @__PURE__ */ new Map();
26793
+ for (const e of store2.outEdgesForMany(boundarySources)) {
26794
+ if (!allowedTypes.has(e.type)) continue;
26795
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26796
+ if (w > 0) boundaryOutSum.set(e.from, (boundaryOutSum.get(e.from) ?? 0) + w);
26797
+ }
26798
+ const boundaryInflow = /* @__PURE__ */ new Map();
26799
+ for (const e of boundaryEdges) {
26800
+ const r = boundaryRanks.get(e.from);
26801
+ const os2 = boundaryOutSum.get(e.from);
26802
+ if (r === void 0 || !os2) continue;
26803
+ const w = EDGE_WEIGHT[e.type] ?? 1;
26804
+ boundaryInflow.set(e.to, (boundaryInflow.get(e.to) ?? 0) + r * w / os2);
26805
+ }
26806
+ const globalDangling = Number(store2.getMeta("danglingRankShare") ?? 0);
26807
+ let regionDangling = 0;
26808
+ for (const id of regionIds) {
26809
+ if ((outSum.get(id) ?? 0) <= 0) regionDangling += rank0.get(id) ?? 0;
26810
+ }
26811
+ const result = pagerankLocal({
26812
+ regionIds,
26813
+ rank0,
26814
+ out: out2,
26815
+ outSum,
26816
+ boundaryInflow,
26817
+ globalN: liveN,
26818
+ externalDanglingRank: Math.max(0, globalDangling - regionDangling),
26819
+ damping: 0.85,
26820
+ tolerance: 1e-6,
26821
+ maxIterations: 100
26822
+ });
26823
+ let pageRankWritten = 0;
26824
+ store2.transaction(() => {
26825
+ for (const [id, score2] of result.scores) {
26826
+ if (Math.abs(score2 - (rank0.get(id) ?? 0)) < 1e-9) continue;
26827
+ store2.setPageRank(id, score2);
26828
+ pageRankWritten++;
26829
+ }
26830
+ });
26831
+ const sweep = store2.landmarkSweepRows();
26832
+ const candidates = /* @__PURE__ */ new Map();
26833
+ for (const row of sweep) {
26834
+ if (row.label === "Pattern" || row.label === "RootCause") {
26835
+ candidates.set(row.id, result.scores.get(row.id) ?? row.pageRank);
26836
+ }
26837
+ }
26838
+ const want = markLandmarks(candidates, 0.1);
26839
+ for (const row of sweep) if (row.memoryTier === "persistent") want.add(row.id);
26840
+ let landmarkFlips = 0;
26841
+ store2.transaction(() => {
26842
+ for (const row of sweep) {
26843
+ if (row.extractionSource === "bko-inferred") continue;
26844
+ const should = want.has(row.id);
26845
+ if (row.isLandmark === should) continue;
26846
+ store2.setLandmark(row.id, should);
26847
+ landmarkFlips++;
26848
+ }
26849
+ });
26850
+ return {
26851
+ scoredNodes: regionIds.length,
26852
+ iterations: result.iterations,
26853
+ converged: result.converged,
26854
+ landmarks: want.size,
26855
+ communities: 0,
26856
+ // deferred to the full backstop — see mode docs
26857
+ motifsPromoted: 0,
26858
+ techniques: 0,
26859
+ antipatterns: 0,
26860
+ pageRankWritten,
26861
+ landmarkFlips,
26862
+ mode: "incremental",
26863
+ regionSize: regionIds.length,
26864
+ durationMs: Date.now() - started
26865
+ };
26866
+ }
26867
+ function runFullRescore(store2) {
25937
26868
  const started = Date.now();
25938
26869
  const allowedTypes = new Set(pagerankEdgeTypes());
25939
26870
  const nodeIds = [];
@@ -25959,10 +26890,12 @@ function runNightlyPipeline(store2) {
25959
26890
  maxIterations: 100
25960
26891
  });
25961
26892
  const scored = result.scores.size;
26893
+ let pageRankWritten = 0;
25962
26894
  store2.transaction(() => {
25963
26895
  for (const [id, score2] of result.scores) {
25964
26896
  if (Math.abs(score2 - (meta3.get(id)?.pageRank ?? 0)) < 1e-9) continue;
25965
26897
  store2.setPageRank(id, score2);
26898
+ pageRankWritten++;
25966
26899
  }
25967
26900
  });
25968
26901
  const landmarkCandidates = /* @__PURE__ */ new Map();
@@ -25977,11 +26910,13 @@ function runNightlyPipeline(store2) {
25977
26910
  for (const id of nodeIds) {
25978
26911
  if (meta3.get(id)?.memoryTier === "persistent") allLandmarks.add(id);
25979
26912
  }
26913
+ let landmarkFlips = 0;
25980
26914
  store2.transaction(() => {
25981
26915
  for (const id of nodeIds) {
25982
26916
  const want = allLandmarks.has(id);
25983
26917
  if ((meta3.get(id)?.isLandmark ?? false) === want) continue;
25984
26918
  store2.setLandmark(id, want);
26919
+ landmarkFlips++;
25985
26920
  }
25986
26921
  });
25987
26922
  const MOTIF_LABELS = /* @__PURE__ */ new Set(["Pattern", "Technique", "AntiPattern"]);
@@ -26016,8 +26951,6 @@ function runNightlyPipeline(store2) {
26016
26951
  for (const [id, c] of comm.community) store2.updateNode(id, { community: c });
26017
26952
  });
26018
26953
  const motifs = promoteMotifs(store2, started);
26019
- const designResolved = resolveDesignProblems(store2, started);
26020
- const cry = crystallize(store2, { ts: started });
26021
26954
  return {
26022
26955
  scoredNodes: scored,
26023
26956
  iterations: result.iterations,
@@ -26027,15 +26960,61 @@ function runNightlyPipeline(store2) {
26027
26960
  motifsPromoted: motifs.promoted,
26028
26961
  techniques: motifs.techniques,
26029
26962
  antipatterns: motifs.antipatterns,
26030
- skillsInduced: 0,
26031
- // skills are cloud-induced now (see above)
26032
- skillsRefreshed: 0,
26963
+ pageRankWritten,
26964
+ landmarkFlips,
26965
+ mode: "full",
26966
+ regionSize: 0,
26967
+ startedAt: started,
26968
+ danglingRank: result.danglingRank,
26969
+ durationMs: Date.now() - started
26970
+ };
26971
+ }
26972
+ function runSemanticMaintenance(store2) {
26973
+ const started = Date.now();
26974
+ reopenAutoClosedProblems(store2, started);
26975
+ const revisits = clearAnsweredRevisits(store2, started);
26976
+ const fixCandidates = clearSettledFixCandidates(store2, started);
26977
+ const designResolved = markFixCandidates(store2, started);
26978
+ const cry = crystallize(store2, { ts: started });
26979
+ return {
26033
26980
  designResolved,
26981
+ revisitsCleared: revisits.cleared,
26982
+ revisitsStanding: revisits.standing,
26983
+ fixCandidatesSettled: fixCandidates.answered + fixCandidates.ignored + fixCandidates.unsubstantiated,
26984
+ fixCandidates,
26034
26985
  claimsEvaluated: cry.evaluated,
26035
26986
  claimsDerived: cry.derived.length,
26036
26987
  durationMs: Date.now() - started
26037
26988
  };
26038
26989
  }
26990
+ function runNightlyPipeline(store2) {
26991
+ const started = Date.now();
26992
+ const rescore = runGraphRescore(store2);
26993
+ const semantic = runSemanticMaintenance(store2);
26994
+ return {
26995
+ scoredNodes: rescore.scoredNodes,
26996
+ iterations: rescore.iterations,
26997
+ converged: rescore.converged,
26998
+ landmarks: rescore.landmarks,
26999
+ communities: rescore.communities,
27000
+ motifsPromoted: rescore.motifsPromoted,
27001
+ techniques: rescore.techniques,
27002
+ antipatterns: rescore.antipatterns,
27003
+ skillsInduced: 0,
27004
+ // skills are cloud-induced now (see above)
27005
+ skillsRefreshed: 0,
27006
+ pageRankWritten: rescore.pageRankWritten,
27007
+ landmarkFlips: rescore.landmarkFlips,
27008
+ mode: rescore.mode,
27009
+ regionSize: rescore.regionSize,
27010
+ designResolved: semantic.designResolved,
27011
+ revisitsCleared: semantic.revisitsCleared,
27012
+ fixCandidatesSettled: semantic.fixCandidatesSettled,
27013
+ claimsEvaluated: semantic.claimsEvaluated,
27014
+ claimsDerived: semantic.claimsDerived,
27015
+ durationMs: Date.now() - started
27016
+ };
27017
+ }
26039
27018
  function codeAnchorProjection(store2, semanticIds, opts = {}) {
26040
27019
  const anchorToNodes = /* @__PURE__ */ new Map();
26041
27020
  for (const id of semanticIds) {
@@ -26088,7 +27067,19 @@ function buildSnapshot(opts) {
26088
27067
  anchors: opts.store.outEdges(p.id, ["MANIFESTED_IN", "EVIDENCED_BY"])
26089
27068
  }));
26090
27069
  const recentConstraints = stillOpen.filter((p) => isConstraintProblem(p)).sort((a, b) => b.lastUpdatedAt - a.lastUpdatedAt).slice(0, 3);
26091
- 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) => {
27070
+ const regressions = problems.filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0).flatMap(
27071
+ (open) => opts.store.outEdges(open.id, ["REGRESSION_OF"]).flatMap((e) => {
27072
+ const closed = opts.store.getNode(e.to);
27073
+ if (!closed || closed.attrs["resolutionSuspect"] !== true || closed.attrs["resolvedAt"] == null) {
27074
+ return [];
27075
+ }
27076
+ const solEdge = opts.store.outEdges(closed.id, ["SOLVED_BY"])[0];
27077
+ const solution = solEdge ? opts.store.getNode(solEdge.to) ?? void 0 : void 0;
27078
+ return [{ open, closed, ...solution ? { solution } : {} }];
27079
+ })
27080
+ ).sort((a, b) => Number(b.closed.attrs["recurredAt"] ?? 0) - Number(a.closed.attrs["recurredAt"] ?? 0)).slice(0, 2);
27081
+ const regressionClosedIds = new Set(regressions.map((r) => r.closed.id));
27082
+ 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) => {
26092
27083
  const solEdge = opts.store.outEdges(p.id, ["SOLVED_BY"])[0];
26093
27084
  const solution = solEdge ? opts.store.getNode(solEdge.to) ?? void 0 : void 0;
26094
27085
  return { node: p, ...solution ? { solution } : {} };
@@ -26158,7 +27149,9 @@ function buildSnapshot(opts) {
26158
27149
  recentProblems: recent,
26159
27150
  recentResolved,
26160
27151
  recentConstraints,
27152
+ regressions,
26161
27153
  ...causalNudge ? { causalNudge } : {},
27154
+ ...opts.selfCiteSkips && opts.selfCiteSkips > 0 ? { selfCiteSkips: opts.selfCiteSkips } : {},
26162
27155
  ...domainNudge ? { domainNudge } : {},
26163
27156
  motifs,
26164
27157
  reviewCount: opts.reviewCount,
@@ -26209,11 +27202,16 @@ function sliceForFile(store2, relPath) {
26209
27202
  }
26210
27203
  const fp = priorsForFile(store2, relPath);
26211
27204
  const priors = fp ? {
26212
- openProblems: fp.openProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
27205
+ openProblems: fp.openProblems.slice(0, 3).map((p) => ({
27206
+ id: p.id,
27207
+ description: p.description,
27208
+ ...typeof p.attrs["fixCandidateFile"] === "string" ? { fixCandidateFile: p.attrs["fixCandidateFile"] } : {}
27209
+ })),
26213
27210
  constraints: fp.constraints.slice(0, 2).map((c) => ({ id: c.id, description: c.description })),
27211
+ resolvedProblems: fp.resolvedProblems.slice(0, 3).map((p) => ({ id: p.id, description: p.description })),
26214
27212
  related: fp.related.slice(0, 4).map((n) => ({ id: n.id, label: n.label, description: n.description })),
26215
27213
  solutionsByProblem: Object.fromEntries(
26216
- fp.openProblems.slice(0, 3).map((p) => [
27214
+ [...fp.openProblems.slice(0, 3), ...fp.resolvedProblems.slice(0, 3)].map((p) => [
26217
27215
  p.id,
26218
27216
  (fp.solutionsByProblem.get(p.id) ?? []).slice(0, 4).map((s) => ({ id: s.id, description: s.description }))
26219
27217
  ])
@@ -26250,37 +27248,37 @@ init_src3();
26250
27248
  // ../../node_modules/.pnpm/env-paths@3.0.0/node_modules/env-paths/index.js
26251
27249
  import os from "node:os";
26252
27250
  import process3 from "node:process";
26253
- var homedir = os.homedir();
27251
+ var homedir2 = os.homedir();
26254
27252
  var tmpdir = os.tmpdir();
26255
27253
  var { env } = process3;
26256
27254
 
26257
27255
  // src/paths.ts
26258
- import { dirname as dirname2, join as join3 } from "node:path";
27256
+ import { dirname as dirname2, join as join4 } from "node:path";
26259
27257
  function workspaceDir(workspaceRoot) {
26260
- return join3(workspaceRoot, ".errata");
27258
+ return join4(workspaceRoot, ".errata");
26261
27259
  }
26262
27260
  function workspacePaths(root) {
26263
27261
  const dir = workspaceDir(root);
26264
27262
  return {
26265
27263
  root,
26266
27264
  configDir: dir,
26267
- workspaceJson: join3(dir, "workspace.json"),
26268
- eventLog: join3(dir, "eventlog.sqlite"),
26269
- castalia: join3(dir, "castalia.db"),
26270
- reviewQueue: join3(dir, "review-queue.json"),
26271
- outbox: join3(dir, "outbox"),
26272
- daemonLock: join3(dir, "daemon.lock"),
26273
- identityAudit: join3(dir, "identity-audit.log"),
26274
- skillsDir: join3(dir, "skills"),
26275
- skillsManifest: join3(dir, "skills.json")
27265
+ workspaceJson: join4(dir, "workspace.json"),
27266
+ eventLog: join4(dir, "eventlog.sqlite"),
27267
+ castalia: join4(dir, "castalia.db"),
27268
+ reviewQueue: join4(dir, "review-queue.json"),
27269
+ outbox: join4(dir, "outbox"),
27270
+ daemonLock: join4(dir, "daemon.lock"),
27271
+ identityAudit: join4(dir, "identity-audit.log"),
27272
+ skillsDir: join4(dir, "skills"),
27273
+ skillsManifest: join4(dir, "skills.json")
26276
27274
  };
26277
27275
  }
26278
27276
 
26279
27277
  // src/reconcile.ts
26280
27278
  init_src3();
26281
27279
  import { execFileSync as execFileSync2 } from "node:child_process";
26282
- import { readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
26283
- import { join as join4, relative as relative2, sep as sep2 } from "node:path";
27280
+ import { readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
27281
+ import { join as join5, relative as relative2, sep as sep2 } from "node:path";
26284
27282
  var IGNORED = /[\\/](?:\.git|node_modules|\.errata|dist|__pycache__)(?:[\\/]|$)/;
26285
27283
  var SOURCE_RE = /\.(ts|tsx|js|jsx|mjs|cjs|py)$/i;
26286
27284
  function gitSourceFiles(root) {
@@ -26297,7 +27295,7 @@ function gitSourceFiles(root) {
26297
27295
  const out2 = [];
26298
27296
  for (const rel of stdout.split("\0")) {
26299
27297
  if (!rel || !SOURCE_RE.test(rel)) continue;
26300
- const abs = join4(root, rel);
27298
+ const abs = join5(root, rel);
26301
27299
  if (IGNORED.test(abs)) continue;
26302
27300
  out2.push(abs);
26303
27301
  }
@@ -26306,13 +27304,13 @@ function gitSourceFiles(root) {
26306
27304
  function* walkSource(dir) {
26307
27305
  let entries;
26308
27306
  try {
26309
- entries = readdirSync2(dir, { withFileTypes: true });
27307
+ entries = readdirSync3(dir, { withFileTypes: true });
26310
27308
  } catch {
26311
27309
  return;
26312
27310
  }
26313
27311
  for (const e of entries) {
26314
27312
  const name2 = String(e.name);
26315
- const full = join4(dir, name2);
27313
+ const full = join5(dir, name2);
26316
27314
  if (IGNORED.test(full)) continue;
26317
27315
  if (e.isDirectory()) yield* walkSource(full);
26318
27316
  else if (SOURCE_RE.test(name2)) yield full;
@@ -26321,12 +27319,18 @@ function* walkSource(dir) {
26321
27319
  function listSourceFiles(root) {
26322
27320
  return gitSourceFiles(root) ?? walkSource(root);
26323
27321
  }
26324
- function findStaleFiles(store2, rootPath, workspaceId) {
27322
+ var SCAN_YIELD_EVERY = 100;
27323
+ var LIVENESS_CHUNK = 4e3;
27324
+ async function findStaleFiles(store2, rootPath, workspaceId) {
26325
27325
  const stale = [];
27326
+ const pending = [];
27327
+ const allTargets = [];
27328
+ let scanned = 0;
26326
27329
  for (const abs of listSourceFiles(rootPath)) {
27330
+ if (++scanned % SCAN_YIELD_EVERY === 0) await new Promise((r) => setImmediate(r));
26327
27331
  let mtimeMs;
26328
27332
  try {
26329
- mtimeMs = statSync2(abs).mtimeMs;
27333
+ mtimeMs = statSync3(abs).mtimeMs;
26330
27334
  } catch {
26331
27335
  continue;
26332
27336
  }
@@ -26334,32 +27338,52 @@ function findStaleFiles(store2, rootPath, workspaceId) {
26334
27338
  const fnode = store2.getNode(fileNodeId(workspaceId, rel));
26335
27339
  if (!fnode) {
26336
27340
  stale.push(abs);
27341
+ } else if ((fnode.validTo ?? null) !== null) {
27342
+ stale.push(abs);
26337
27343
  } else if (fnode.lastUpdatedAt < mtimeMs) {
26338
27344
  stale.push(abs);
26339
27345
  } else {
26340
27346
  const edges = store2.outEdges(fnode.id, ["DEFINES", "CONTAINS"]);
26341
27347
  if (edges.length === 0) {
26342
27348
  stale.push(abs);
26343
- } else if (edges.some((e) => !isLive(store2.getNode(e.to)))) {
26344
- stale.push(abs);
27349
+ } else {
27350
+ const targets = edges.map((e) => e.to);
27351
+ pending.push({ abs, targets });
27352
+ allTargets.push(...targets);
26345
27353
  }
26346
27354
  }
26347
27355
  }
27356
+ const live = /* @__PURE__ */ new Set();
27357
+ for (let i2 = 0; i2 < allTargets.length; i2 += LIVENESS_CHUNK) {
27358
+ for (const id of store2.liveNodeIds(allTargets.slice(i2, i2 + LIVENESS_CHUNK))) live.add(id);
27359
+ if (i2 + LIVENESS_CHUNK < allTargets.length) await new Promise((r) => setImmediate(r));
27360
+ }
27361
+ for (const { abs, targets } of pending) {
27362
+ if (targets.some((id) => !live.has(id))) stale.push(abs);
27363
+ }
26348
27364
  return stale;
26349
27365
  }
26350
- function isLive(n) {
26351
- return n != null && (n.validTo ?? null) === null;
26352
- }
26353
27366
  async function reconcileStaleFiles(store2, rootPath, workspaceId) {
26354
- const stale = findStaleFiles(store2, rootPath, workspaceId);
27367
+ const stale = await findStaleFiles(store2, rootPath, workspaceId);
26355
27368
  if (stale.length === 0) return 0;
26356
27369
  let total = 0;
27370
+ let failedBatches = 0;
26357
27371
  const BATCH = 20;
26358
27372
  for (let i2 = 0; i2 < stale.length; i2 += BATCH) {
26359
- const r = await incrementalReindex(store2, rootPath, workspaceId, stale.slice(i2, i2 + BATCH));
26360
- total += r.filesReindexed;
27373
+ try {
27374
+ const r = await incrementalReindex(store2, rootPath, workspaceId, stale.slice(i2, i2 + BATCH));
27375
+ total += r.filesReindexed;
27376
+ } catch (err2) {
27377
+ failedBatches++;
27378
+ console.warn(
27379
+ `[errata] reconcile: batch ${i2 / BATCH + 1} failed (${stale.slice(i2, i2 + BATCH).length} file(s) skipped): ${err2 instanceof Error ? err2.message : err2}`
27380
+ );
27381
+ }
26361
27382
  if (i2 + BATCH < stale.length) await new Promise((res) => setImmediate(res));
26362
27383
  }
27384
+ if (failedBatches > 0) {
27385
+ console.warn(`[errata] reconcile: ${failedBatches} batch(es) failed; ${total} file(s) reindexed`);
27386
+ }
26363
27387
  return total;
26364
27388
  }
26365
27389
 
@@ -26412,6 +27436,7 @@ function runNightly() {
26412
27436
  const a = backfillLegacyAnchors(store, Date.now());
26413
27437
  anchorsDemoted = a.demotedEdges;
26414
27438
  resolutionsSuspect = a.suspects.length;
27439
+ repairLegacyAnchorsFromStatement(store, Date.now());
26415
27440
  } catch {
26416
27441
  }
26417
27442
  const report = runNightlyPipeline(store);