@inerrata-corporation/errata 2.0.21-dev.3745 → 2.0.21-dev.3763

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
@@ -16146,10 +16146,12 @@ function historyReadPolicy(node, options = {}) {
16146
16146
  notice: `History-derived${at}; source ${String(source)}; applicability ${current ? String(applicability) : "unknown at this checkout"}; ${environmentKnown ? "dependency context checked" : "dependency applicability unverified"}; ${independentlyValidated ? "separate validation recorded" : "not independently validated"}.`
16147
16147
  };
16148
16148
  }
16149
+ var CLOUD_ATTR_DUPLICATE_KEYS;
16149
16150
  var init_history_read = __esm({
16150
16151
  "../../packages/shared/src/history-read.ts"() {
16151
16152
  "use strict";
16152
16153
  init_history_dependencies();
16154
+ CLOUD_ATTR_DUPLICATE_KEYS = ["attrsJson", "embedding"];
16153
16155
  }
16154
16156
  });
16155
16157
 
@@ -25425,7 +25427,474 @@ function installEditPlaceholderRetirement(db) {
25425
25427
  });
25426
25428
  }
25427
25429
 
25430
+ // ../../packages/local-graph/src/cloud-attr-compaction.ts
25431
+ init_src2();
25432
+ var CLOUD_ATTR_COMPACTION_KEY = "cloud-attr-compaction-1";
25433
+ var DUPES = CLOUD_ATTR_DUPLICATE_KEYS.map((k) => `'${k}'`).join(",");
25434
+ function compacted(row) {
25435
+ const packed = `json_extract(${row}.attrs_json, '$.attrsJson')`;
25436
+ const nested = `CASE WHEN json_type(${row}.attrs_json, '$.attrsJson') = 'text' AND json_valid(${packed})
25437
+ AND json_type(${packed}) = 'object' THEN ${packed} ELSE '{}' END`;
25438
+ const value = `CASE WHEN type IN ('array','object') THEN json(value) WHEN type IN ('true','false') THEN json(type) ELSE value END`;
25439
+ return `(SELECT json_group_object(key, ${value}) FROM (
25440
+ SELECT key, value, type, min(pri) FROM (
25441
+ SELECT key, value, type, 0 AS pri FROM json_each(${row}.attrs_json)
25442
+ UNION ALL
25443
+ SELECT key, value, type, 1 AS pri FROM json_each(${nested})
25444
+ ) WHERE key NOT IN (${DUPES}) GROUP BY key))`;
25445
+ }
25446
+ function dirty(row) {
25447
+ const has = CLOUD_ATTR_DUPLICATE_KEYS.map((k) => `json_type(${row}.attrs_json, '$.${k}') IS NOT NULL`).join(" OR ");
25448
+ return `json_extract(${row}.attrs_json, '$.source') = 'cloud' AND (${has})`;
25449
+ }
25450
+ function installCloudAttrCompaction(db) {
25451
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(CLOUD_ATTR_COMPACTION_KEY)) return;
25452
+ db.transaction(() => {
25453
+ for (const operation of ["INSERT", "UPDATE"]) {
25454
+ db.exec(`CREATE TRIGGER IF NOT EXISTS cloud_attr_compact_${operation.toLowerCase()}
25455
+ AFTER ${operation} ON nodes WHEN ${dirty("NEW")}
25456
+ BEGIN UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE id = NEW.id; END;`);
25457
+ }
25458
+ db.exec(`UPDATE nodes SET attrs_json = ${compacted("nodes")} WHERE ${dirty("nodes")}`);
25459
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(CLOUD_ATTR_COMPACTION_KEY, "installed");
25460
+ });
25461
+ }
25462
+
25463
+ // ../../packages/math/src/pagerank.ts
25464
+ function pagerank(input) {
25465
+ const damping = input.damping ?? 0.85;
25466
+ const tol = input.tolerance ?? 1e-6;
25467
+ const maxIter = input.maxIterations ?? 100;
25468
+ const ids = input.nodeIds;
25469
+ const n = ids.length;
25470
+ if (n === 0) {
25471
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true, danglingRank: 0 };
25472
+ }
25473
+ const index = /* @__PURE__ */ new Map();
25474
+ for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
25475
+ const outSum = new Float64Array(n);
25476
+ const dangling = new Uint8Array(n).fill(1);
25477
+ const rowLen = new Int32Array(n);
25478
+ let edgeCount = 0;
25479
+ for (const [from, edges] of input.out) {
25480
+ const fi = index.get(from);
25481
+ if (fi === void 0) continue;
25482
+ let sum = 0;
25483
+ let inSet = 0;
25484
+ for (const e of edges) {
25485
+ sum += e.weight;
25486
+ if (index.has(e.to)) inSet++;
25487
+ }
25488
+ if (sum > 0) {
25489
+ outSum[fi] = sum;
25490
+ dangling[fi] = 0;
25491
+ }
25492
+ rowLen[fi] = inSet;
25493
+ edgeCount += inSet;
25494
+ }
25495
+ const rowStart = new Int32Array(n + 1);
25496
+ for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
25497
+ const colIdx = new Int32Array(edgeCount);
25498
+ const colW = new Float64Array(edgeCount);
25499
+ const cursor = rowStart.slice(0, n);
25500
+ for (const [from, edges] of input.out) {
25501
+ const fi = index.get(from);
25502
+ if (fi === void 0) continue;
25503
+ for (const e of edges) {
25504
+ const ti = index.get(e.to);
25505
+ if (ti === void 0) continue;
25506
+ const c = cursor[fi];
25507
+ cursor[fi] = c + 1;
25508
+ colIdx[c] = ti;
25509
+ colW[c] = e.weight;
25510
+ }
25511
+ }
25512
+ let score2 = new Float64Array(n).fill(1 / n);
25513
+ let next = new Float64Array(n);
25514
+ const base = (1 - damping) / n;
25515
+ let iter = 0;
25516
+ let converged = false;
25517
+ for (; iter < maxIter; iter++) {
25518
+ next.fill(base);
25519
+ let danglingMass = 0;
25520
+ for (let i2 = 0; i2 < n; i2++) if (dangling[i2]) danglingMass += score2[i2];
25521
+ for (let i2 = 0; i2 < n; i2++) {
25522
+ if (dangling[i2]) continue;
25523
+ const f = damping * score2[i2] / outSum[i2];
25524
+ const end = rowStart[i2 + 1];
25525
+ for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
25526
+ }
25527
+ if (danglingMass > 0) {
25528
+ const share = damping * danglingMass / n;
25529
+ for (let i2 = 0; i2 < n; i2++) next[i2] += share;
25530
+ }
25531
+ let diff = 0;
25532
+ for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
25533
+ const tmp = score2;
25534
+ score2 = next;
25535
+ next = tmp;
25536
+ if (diff < tol) {
25537
+ iter++;
25538
+ converged = true;
25539
+ break;
25540
+ }
25541
+ }
25542
+ const scores = /* @__PURE__ */ new Map();
25543
+ let danglingRank = 0;
25544
+ for (let i2 = 0; i2 < n; i2++) {
25545
+ scores.set(ids[i2], score2[i2]);
25546
+ if (dangling[i2]) danglingRank += score2[i2];
25547
+ }
25548
+ return { scores, iterations: iter, converged, danglingRank };
25549
+ }
25550
+ function markLandmarks(scores, percentile = 0.1, filter) {
25551
+ const entries = [...scores.entries()];
25552
+ if (filter) {
25553
+ for (let i2 = entries.length - 1; i2 >= 0; i2--) {
25554
+ if (!filter(entries[i2][0])) entries.splice(i2, 1);
25555
+ }
25556
+ }
25557
+ if (entries.length === 0) return /* @__PURE__ */ new Set();
25558
+ entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
25559
+ const cutoff = Math.max(1, Math.floor(entries.length * percentile));
25560
+ const out2 = /* @__PURE__ */ new Set();
25561
+ for (let i2 = 0; i2 < cutoff; i2++) {
25562
+ out2.add(entries[i2][0]);
25563
+ }
25564
+ return out2;
25565
+ }
25566
+
25567
+ // ../../packages/math/src/relevance.ts
25568
+ function pageRankScale(ranks) {
25569
+ const positive = [...ranks].filter((r) => Number.isFinite(r) && r > 0).sort((a, b) => a - b);
25570
+ return positive.length ? Math.max(1e-9, positive[Math.floor((positive.length - 1) * 0.95)]) : 1;
25571
+ }
25572
+
25573
+ // ../../packages/math/src/leiden.ts
25574
+ function buildGraph(nodeIds, adj) {
25575
+ const nbr = /* @__PURE__ */ new Map();
25576
+ const deg = /* @__PURE__ */ new Map();
25577
+ const present = new Set(nodeIds);
25578
+ for (const id of nodeIds) {
25579
+ nbr.set(id, /* @__PURE__ */ new Map());
25580
+ deg.set(id, 0);
25581
+ }
25582
+ let m2 = 0;
25583
+ for (const id of nodeIds) {
25584
+ for (const { to, weight } of adj.get(id) ?? []) {
25585
+ if (!present.has(to)) continue;
25586
+ const row = nbr.get(id);
25587
+ row.set(to, (row.get(to) ?? 0) + weight);
25588
+ deg.set(id, (deg.get(id) ?? 0) + weight);
25589
+ m2 += weight;
25590
+ }
25591
+ }
25592
+ return { nbr, deg, m2 };
25593
+ }
25594
+ function localMoving(nodeIds, g) {
25595
+ const community = new Map(nodeIds.map((n) => [n, n]));
25596
+ const comTot = new Map(nodeIds.map((n) => [n, g.deg.get(n) ?? 0]));
25597
+ const order = [...nodeIds].sort();
25598
+ const m2 = g.m2 > 0 ? g.m2 : 1;
25599
+ let improved = true;
25600
+ let iter = 0;
25601
+ while (improved && iter < 100) {
25602
+ improved = false;
25603
+ iter++;
25604
+ for (const node of order) {
25605
+ const ci = community.get(node);
25606
+ const ki = g.deg.get(node) ?? 0;
25607
+ comTot.set(ci, (comTot.get(ci) ?? 0) - ki);
25608
+ const wToComm = /* @__PURE__ */ new Map();
25609
+ for (const [nb, w] of g.nbr.get(node) ?? []) {
25610
+ if (nb === node) continue;
25611
+ const c = community.get(nb);
25612
+ wToComm.set(c, (wToComm.get(c) ?? 0) + w);
25613
+ }
25614
+ let bestC = ci;
25615
+ let bestGain = (wToComm.get(ci) ?? 0) - (comTot.get(ci) ?? 0) * ki / m2;
25616
+ for (const [c, kiIn] of [...wToComm.entries()].sort(
25617
+ (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0
25618
+ )) {
25619
+ const gain = kiIn - (comTot.get(c) ?? 0) * ki / m2;
25620
+ if (gain > bestGain) {
25621
+ bestGain = gain;
25622
+ bestC = c;
25623
+ }
25624
+ }
25625
+ community.set(node, bestC);
25626
+ comTot.set(bestC, (comTot.get(bestC) ?? 0) + ki);
25627
+ if (bestC !== ci) improved = true;
25628
+ }
25629
+ }
25630
+ return community;
25631
+ }
25632
+ function refineConnectivity(community, g) {
25633
+ const members = /* @__PURE__ */ new Map();
25634
+ for (const [id, c] of community) {
25635
+ const l = members.get(c) ?? [];
25636
+ l.push(id);
25637
+ members.set(c, l);
25638
+ }
25639
+ const refined = /* @__PURE__ */ new Map();
25640
+ for (const [, group] of members) {
25641
+ const inGroup = new Set(group);
25642
+ const seen = /* @__PURE__ */ new Set();
25643
+ for (const start2 of [...group].sort()) {
25644
+ if (seen.has(start2)) continue;
25645
+ const comp = [];
25646
+ const stack = [start2];
25647
+ seen.add(start2);
25648
+ while (stack.length) {
25649
+ const cur = stack.pop();
25650
+ comp.push(cur);
25651
+ for (const [nb] of g.nbr.get(cur) ?? []) {
25652
+ if (inGroup.has(nb) && !seen.has(nb)) {
25653
+ seen.add(nb);
25654
+ stack.push(nb);
25655
+ }
25656
+ }
25657
+ }
25658
+ const anchor = [...comp].sort()[0];
25659
+ for (const id of comp) refined.set(id, anchor);
25660
+ }
25661
+ }
25662
+ return refined;
25663
+ }
25664
+ function normalize(assignment) {
25665
+ const groups = /* @__PURE__ */ new Map();
25666
+ for (const [id, c] of assignment) {
25667
+ const l = groups.get(c) ?? [];
25668
+ l.push(id);
25669
+ groups.set(c, l);
25670
+ }
25671
+ const community = /* @__PURE__ */ new Map();
25672
+ for (const grp of groups.values()) {
25673
+ const anchor = [...grp].sort()[0];
25674
+ const cid = `community_${anchor}`;
25675
+ for (const m of grp) community.set(m, cid);
25676
+ }
25677
+ return { community, count: groups.size, iterations: 1 };
25678
+ }
25679
+ function causalComponents(nodeIds, pairs) {
25680
+ const parent = new Map(nodeIds.map((n) => [n, n]));
25681
+ const find = (x) => {
25682
+ let r = x;
25683
+ while (parent.get(r) !== r) r = parent.get(r);
25684
+ let c = x;
25685
+ while (parent.get(c) !== r) {
25686
+ const next = parent.get(c);
25687
+ parent.set(c, r);
25688
+ c = next;
25689
+ }
25690
+ return r;
25691
+ };
25692
+ for (const [a, b] of pairs) {
25693
+ const ra = find(a);
25694
+ const rb = find(b);
25695
+ if (ra !== rb) parent.set(ra < rb ? rb : ra, ra < rb ? ra : rb);
25696
+ }
25697
+ return find;
25698
+ }
25699
+ function detectCommunitiesLeiden(input, opts = {}) {
25700
+ const idSet = new Set(input.nodeIds);
25701
+ const protect = [...opts.protect ?? []].filter(
25702
+ ([a, b]) => a !== b && idSet.has(a) && idSet.has(b)
25703
+ );
25704
+ if (protect.length === 0) {
25705
+ const g = buildGraph(input.nodeIds, input.adj);
25706
+ return normalize(refineConnectivity(localMoving(input.nodeIds, g), g));
25707
+ }
25708
+ const rep = causalComponents(input.nodeIds, protect);
25709
+ const superIds = [...new Set(input.nodeIds.map(rep))];
25710
+ const superAdj = /* @__PURE__ */ new Map();
25711
+ const acc = /* @__PURE__ */ new Map();
25712
+ for (const id of input.nodeIds) {
25713
+ const ra = rep(id);
25714
+ for (const { to, weight } of input.adj.get(id) ?? []) {
25715
+ if (!idSet.has(to)) continue;
25716
+ const rb = rep(to);
25717
+ if (ra === rb) continue;
25718
+ const row = acc.get(ra) ?? /* @__PURE__ */ new Map();
25719
+ row.set(rb, (row.get(rb) ?? 0) + weight);
25720
+ acc.set(ra, row);
25721
+ }
25722
+ }
25723
+ for (const [a, row] of acc) superAdj.set(a, [...row].map(([to, weight]) => ({ to, weight })));
25724
+ const sg = buildGraph(superIds, superAdj);
25725
+ const superComm = refineConnectivity(localMoving(superIds, sg), sg);
25726
+ const assignment = /* @__PURE__ */ new Map();
25727
+ for (const id of input.nodeIds) assignment.set(id, superComm.get(rep(id)) ?? rep(id));
25728
+ return normalize(assignment);
25729
+ }
25730
+
25731
+ // ../../packages/math/src/bko/a1-stable-motif.ts
25732
+ var MIN_INSTANCES = 5;
25733
+ var MIN_SURVIVED_RUNS = 2;
25734
+ function a1StableMotif(input) {
25735
+ if (input.instanceCount >= MIN_INSTANCES && input.survivedRuns >= MIN_SURVIVED_RUNS) {
25736
+ return {
25737
+ shouldMarkStable: true,
25738
+ confidence: Math.min(1, input.instanceCount / 20),
25739
+ rule: "A1"
25740
+ };
25741
+ }
25742
+ return { shouldMarkStable: false, confidence: 0, rule: "A1" };
25743
+ }
25744
+
25745
+ // ../../packages/math/src/bko/a2-motif-quality.ts
25746
+ var MIN_INSTANCES2 = 3;
25747
+ var MIN_QUALITY = 0.65;
25748
+ var MAX_FAILURE_RATE = 0.15;
25749
+ var QUALITY_BOOST = 1.2;
25750
+ function a2MotifQuality(input) {
25751
+ if (input.instances.length === 0) {
25752
+ return {
25753
+ qualityScore: 0,
25754
+ qualityFailureRate: 0,
25755
+ qualified: false,
25756
+ qualityBoost: 1,
25757
+ rule: "A2"
25758
+ };
25759
+ }
25760
+ const mean = input.instances.reduce((s, i2) => s + i2.confidence, 0) / input.instances.length;
25761
+ const failures = input.instances.filter((i2) => i2.failed).length;
25762
+ const failureRate = failures / input.instances.length;
25763
+ const qualified = input.instances.length >= MIN_INSTANCES2 && mean >= MIN_QUALITY && failureRate <= MAX_FAILURE_RATE;
25764
+ return {
25765
+ qualityScore: mean,
25766
+ qualityFailureRate: failureRate,
25767
+ qualified,
25768
+ qualityBoost: qualified ? QUALITY_BOOST : 1,
25769
+ rule: "A2"
25770
+ };
25771
+ }
25772
+
25773
+ // ../../packages/math/src/motif-gate.ts
25774
+ var MOTIF_MIN_MEMBERS = 5;
25775
+ var MOTIF_MIN_SURVIVED = 2;
25776
+ function motifDecision(input) {
25777
+ if (input.memberCount < MOTIF_MIN_MEMBERS) {
25778
+ return {
25779
+ isCandidate: false,
25780
+ survivedRuns: input.priorSurvivedRuns,
25781
+ stable: false,
25782
+ qualified: false,
25783
+ promoted: false,
25784
+ motifConfidence: 0
25785
+ };
25786
+ }
25787
+ const survivedRuns = input.priorSurvivedRuns + 1;
25788
+ const a2 = a2MotifQuality({ instances: input.instances });
25789
+ const a1 = a1StableMotif({ instanceCount: input.instanceCount, survivedRuns });
25790
+ const stable = a1.shouldMarkStable;
25791
+ const promoted = survivedRuns >= MOTIF_MIN_SURVIVED && stable && a2.qualified;
25792
+ return {
25793
+ isCandidate: true,
25794
+ survivedRuns,
25795
+ stable,
25796
+ qualified: a2.qualified,
25797
+ promoted,
25798
+ motifConfidence: Math.round(a2.qualityScore * 100) / 100
25799
+ };
25800
+ }
25801
+
25802
+ // ../../packages/math/src/pagerank-local.ts
25803
+ function pagerankLocal(input) {
25804
+ const damping = input.damping ?? 0.85;
25805
+ const tol = input.tolerance ?? 1e-6;
25806
+ const maxIter = input.maxIterations ?? 100;
25807
+ const ids = input.regionIds;
25808
+ const n = ids.length;
25809
+ if (n === 0 || input.globalN === 0) {
25810
+ return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
25811
+ }
25812
+ const index = /* @__PURE__ */ new Map();
25813
+ for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
25814
+ const base = (1 - damping) / input.globalN;
25815
+ const outSum = new Float64Array(n);
25816
+ const inflow = new Float64Array(n);
25817
+ let score2 = new Float64Array(n);
25818
+ for (let i2 = 0; i2 < n; i2++) {
25819
+ const id = ids[i2];
25820
+ outSum[i2] = input.outSum.get(id) ?? 0;
25821
+ inflow[i2] = input.boundaryInflow.get(id) ?? 0;
25822
+ score2[i2] = input.rank0.get(id) ?? base;
25823
+ }
25824
+ const rowLen = new Int32Array(n);
25825
+ let edgeCount = 0;
25826
+ for (const [from, edges] of input.out) {
25827
+ const fi = index.get(from);
25828
+ if (fi === void 0) continue;
25829
+ let inSet = 0;
25830
+ for (const e of edges) if (index.has(e.to)) inSet++;
25831
+ rowLen[fi] = inSet;
25832
+ edgeCount += inSet;
25833
+ }
25834
+ const rowStart = new Int32Array(n + 1);
25835
+ for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
25836
+ const colIdx = new Int32Array(edgeCount);
25837
+ const colW = new Float64Array(edgeCount);
25838
+ const cursor = rowStart.slice(0, n);
25839
+ for (const [from, edges] of input.out) {
25840
+ const fi = index.get(from);
25841
+ if (fi === void 0) continue;
25842
+ for (const e of edges) {
25843
+ const ti = index.get(e.to);
25844
+ if (ti === void 0) continue;
25845
+ const c = cursor[fi];
25846
+ cursor[fi] = c + 1;
25847
+ colIdx[c] = ti;
25848
+ colW[c] = e.weight;
25849
+ }
25850
+ }
25851
+ const externalDangling = input.externalDanglingRank ?? 0;
25852
+ let next = new Float64Array(n);
25853
+ let iter = 0;
25854
+ let converged = false;
25855
+ for (; iter < maxIter; iter++) {
25856
+ let internalDangling = 0;
25857
+ for (let i2 = 0; i2 < n; i2++) if (outSum[i2] <= 0) internalDangling += score2[i2];
25858
+ const danglingShare = damping * (internalDangling + externalDangling) / input.globalN;
25859
+ for (let i2 = 0; i2 < n; i2++) next[i2] = base + danglingShare + damping * inflow[i2];
25860
+ for (let i2 = 0; i2 < n; i2++) {
25861
+ const os2 = outSum[i2];
25862
+ if (os2 <= 0) continue;
25863
+ const f = damping * score2[i2] / os2;
25864
+ const end = rowStart[i2 + 1];
25865
+ for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
25866
+ }
25867
+ let diff = 0;
25868
+ for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
25869
+ const tmp = score2;
25870
+ score2 = next;
25871
+ next = tmp;
25872
+ if (diff < tol) {
25873
+ iter++;
25874
+ converged = true;
25875
+ break;
25876
+ }
25877
+ }
25878
+ const scores = /* @__PURE__ */ new Map();
25879
+ for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
25880
+ return { scores, iterations: iter, converged };
25881
+ }
25882
+
25428
25883
  // ../../packages/local-graph/src/store.ts
25884
+ var PAGE_RANK_SCALE_KEY = "pageRankScale.p95.v1";
25885
+ var TEXT_SEARCH_COLUMNS = `id, label, description, page_rank,
25886
+ json_object('test', json(CASE WHEN json_type(attrs_json, '$.test') = 'true' THEN 'true' ELSE 'false' END),
25887
+ 'relPath', json_extract(attrs_json, '$.relPath'),
25888
+ 'resolutionSuspect', json(CASE WHEN json_type(attrs_json, '$.resolutionSuspect') = 'true' THEN 'true' ELSE 'false' END),
25889
+ 'resolvedAt', json_extract(attrs_json, '$.resolvedAt'),
25890
+ 'refutedAt', json_extract(attrs_json, '$.refutedAt'),
25891
+ 'refuteReasons', json_extract(attrs_json, '$.refuteReasons'),
25892
+ 'reopenedFrom', json_extract(attrs_json, '$.reopenedFrom'),
25893
+ 'resolvedAs', json_extract(attrs_json, '$.resolvedAs')) AS search_attrs`;
25894
+ function textSearchNode(row) {
25895
+ const attrs = JSON.parse(row.search_attrs);
25896
+ return { id: row.id, label: row.label, description: row.description, pageRank: row.page_rank, attrs };
25897
+ }
25429
25898
  var LOCAL_RULE_OVERRIDES = {
25430
25899
  ANCHORED_AT: {
25431
25900
  from: EDGE_RULES["ANCHORED_AT"]?.from,
@@ -25569,6 +26038,9 @@ CREATE INDEX IF NOT EXISTS edges_session_evidence ON edges(id) WHERE ${SESSION_E
25569
26038
  DROP INDEX IF EXISTS nodes_live_fingerprint;
25570
26039
  DROP INDEX IF EXISTS nodes_instance_fingerprint;
25571
26040
  CREATE INDEX IF NOT EXISTS nodes_instance_fingerprint_v2 ON nodes(label, last_updated_at, last_reinforced_at_seq, created_at_seq) WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause');
26041
+ -- Calibrate rank from a covering scalar index; the global page-rank index
26042
+ -- requires a table lookup per node to exclude frozen versions.
26043
+ CREATE INDEX IF NOT EXISTS nodes_live_page_rank ON nodes(valid_to, page_rank);
25572
26044
  CREATE INDEX IF NOT EXISTS nodes_logical ON nodes(logical_id, version);
25573
26045
  CREATE INDEX IF NOT EXISTS edges_live ON edges(valid_to);
25574
26046
  -- Expression index on the per-node relPath (stored in attrs_json) so the
@@ -25581,6 +26053,12 @@ CREATE INDEX IF NOT EXISTS nodes_live_description ON nodes(description, label) W
25581
26053
  -- Problems by the file their anchor hint names: the harvest upgrades hints for
25582
26054
  -- every edited file of every turn, and a scan parses every Problem's attrs.
25583
26055
  CREATE INDEX IF NOT EXISTS nodes_anchor_hint_path ON nodes(json_extract(attrs_json, '$.anchorHint.path')) WHERE valid_to IS NULL;
26056
+ -- Problems by the command / checker / file that opened them. The generalizer
26057
+ -- looked these up per shell or tool EVENT by hydrating every Problem: ~12s of
26058
+ -- every minute of daemon main-thread time on a 3.4k-Problem store (2026-10-05).
26059
+ CREATE INDEX IF NOT EXISTS nodes_command_signature ON nodes(json_extract(attrs_json, '$.commandSignature')) WHERE valid_to IS NULL;
26060
+ CREATE INDEX IF NOT EXISTS nodes_diag_command ON nodes(json_extract(attrs_json, '$.diagCommand')) WHERE valid_to IS NULL;
26061
+ CREATE INDEX IF NOT EXISTS nodes_lsp_rel_path ON nodes(json_extract(attrs_json, '$.lspRelPath')) WHERE valid_to IS NULL;
25584
26062
  -- Nodes still waiting on a root-cause ask. The MCP server checks this on EVERY
25585
26063
  -- tool response (the enrichment piggyback), so it must not parse every
25586
26064
  -- Solution's and Problem's attrs per call. Not partial: the Problem pull reads
@@ -25667,6 +26145,7 @@ var SqliteGraphStore = class {
25667
26145
  * ALL upserts incl. code reindex) — this counter is the knowledge-arrival axis
25668
26146
  * Δseq is measured over, so code reindex churn must NOT advance it. */
25669
26147
  ingestSeq = 0;
26148
+ initialPageRankScale;
25670
26149
  constructor(opts) {
25671
26150
  this.db = openDatabase(opts.path);
25672
26151
  this.db.exec(SCHEMA_SQL);
@@ -25683,6 +26162,7 @@ var SqliteGraphStore = class {
25683
26162
  this.db.exec(TEMPORAL_INDEX_SQL);
25684
26163
  installHistoryCacheFloor(this.db);
25685
26164
  installEditPlaceholderRetirement(this.db);
26165
+ installCloudAttrCompaction(this.db);
25686
26166
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
25687
26167
  this.db.transaction(() => {
25688
26168
  this.repairSessionAliases();
@@ -26180,1999 +26660,1613 @@ var SqliteGraphStore = class {
26180
26660
  * read attrs and never the vector. `findNodesByLabel` decodes every embedding
26181
26661
  * blob; the liveness watch's coverage pass spent 3.4s of a 4.3s main-thread
26182
26662
  * hold doing exactly that on a 227k-node store (2026-09-24). */
26183
- findNodeAttrsByLabel(label) {
26184
- const rows = this.stmts.attrsByLabel.all(label);
26185
- return rows.map((r) => ({
26186
- id: r.id,
26187
- createdAt: r.created_at,
26188
- ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
26189
- attrs: JSON.parse(r.attrs_json)
26190
- }));
26191
- }
26192
- *iterateNodeIdentitiesByLabel(label, pageSize = 2e3) {
26193
- const page = this.db.prepare(
26194
- "SELECT rowid AS rid, id, json_extract(attrs_json, '$.relPath') AS relPath, json_extract(attrs_json, '$.qname') AS qname FROM nodes WHERE rowid > ? AND label = ? AND valid_to IS NULL ORDER BY rowid LIMIT ?"
26195
- );
26196
- let after = 0;
26197
- for (; ; ) {
26198
- const rows = page.all(after, label, pageSize);
26199
- for (const r of rows) {
26200
- yield {
26201
- id: r.id,
26202
- relPath: typeof r.relPath === "string" ? r.relPath : null,
26203
- qname: typeof r.qname === "string" ? r.qname : null
26204
- };
26205
- }
26206
- if (rows.length < pageSize) return;
26207
- after = rows[rows.length - 1].rid;
26208
- }
26209
- }
26210
- recentNodeIds(label, limit) {
26211
- const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
26212
- return rows.map((r) => r.id);
26213
- }
26214
- recentOpenProblemCandidates(opts) {
26215
- if (!Number.isSafeInteger(opts.limit) || opts.limit < 0) {
26216
- throw new RangeError("candidate limit must be a non-negative safe integer");
26217
- }
26218
- return this.db.prepare(`SELECT id, description FROM nodes
26219
- WHERE label = 'Problem' AND valid_to IS NULL
26220
- AND json_extract(attrs_json, '$.resolvedAt') IS NULL
26221
- AND json_type(attrs_json, '$.mergedInto') IS NULL
26222
- AND (? IS NULL OR id != ?)
26223
- AND (json_type(attrs_json, '$.workspaceId') IS NULL OR json_extract(attrs_json, '$.workspaceId') = ?)
26224
- AND COALESCE(json_extract(attrs_json, '$.kind') = 'constraint', 0) = ?
26225
- ORDER BY last_updated_at DESC, rowid ASC LIMIT ?`).all(
26226
- opts.excludeId ?? null,
26227
- opts.excludeId ?? null,
26228
- opts.workspaceId,
26229
- opts.constraint ? 1 : 0,
26230
- opts.limit
26231
- );
26232
- }
26233
- hasLiveNodes() {
26234
- const r = this.db.prepare("SELECT EXISTS(SELECT 1 FROM nodes WHERE valid_to IS NULL) AS e").get();
26235
- return r.e === 1;
26236
- }
26237
- findNodeIdentitiesByLabel(label) {
26238
- const rows = this.stmts.identitiesByLabel.all(label);
26239
- return rows.map((r) => ({
26240
- id: r.id,
26241
- relPath: typeof r.relPath === "string" ? r.relPath : null,
26242
- qname: typeof r.qname === "string" ? r.qname : null
26243
- }));
26244
- }
26245
- findAllVersionsByLabel(label, attribute) {
26246
- if (attribute && !/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(attribute.path)) {
26247
- throw new Error(`invalid attr path: ${attribute.path}`);
26248
- }
26249
- const rows = attribute ? this.db.prepare(`SELECT * FROM nodes WHERE json_extract(attrs_json, '$.${attribute.path}') = ? AND label = ?`).all(attribute.value, label) : this.stmts.findByLabelAll.all(label);
26250
- return rows.map(rowToNode);
26251
- }
26252
- searchNodes(opts) {
26253
- const where = [];
26254
- const params = {};
26255
- if (opts.labels && opts.labels.length > 0) {
26256
- const placeholders = opts.labels.map((_, i2) => `:label${i2}`).join(", ");
26257
- where.push(`label IN (${placeholders})`);
26258
- opts.labels.forEach((l, i2) => {
26259
- params[`label${i2}`] = l;
26260
- });
26261
- }
26262
- if (opts.descriptionLike) {
26263
- where.push("description LIKE :descPat");
26264
- params["descPat"] = `%${opts.descriptionLike}%`;
26265
- }
26266
- where.push("valid_to IS NULL");
26267
- const limit = Math.max(1, opts.limit ?? 50);
26268
- params["limit"] = limit;
26269
- const whereSql = where.length > 0 ? "WHERE " + where.join(" AND ") : "";
26270
- const sql = `SELECT * FROM nodes ${whereSql} ORDER BY page_rank DESC, last_updated_at DESC LIMIT :limit`;
26271
- const rows = this.db.prepare(sql).all(params);
26272
- return rows.map(rowToNode);
26273
- }
26274
- searchDescriptionTerms(terms, limit) {
26275
- if (terms.length === 0) return [];
26276
- const score2 = terms.map(() => "(description LIKE ?)").join(" + ");
26277
- const rows = this.db.prepare(
26278
- `SELECT n.*, h.m AS matched
26279
- FROM (SELECT rowid AS rid, ${score2} AS m
26280
- FROM nodes INDEXED BY nodes_live_description
26281
- WHERE valid_to IS NULL) h
26282
- JOIN nodes n ON n.rowid = h.rid
26283
- WHERE h.m > 0
26284
- ORDER BY h.m DESC, n.page_rank DESC, n.last_updated_at DESC
26285
- LIMIT ?`
26286
- ).all(...terms.map((t) => `%${t}%`), Math.max(1, limit));
26287
- return rows.map((r) => ({ node: rowToNode(r), matched: Number(r.matched) }));
26288
- }
26289
- typedEdgeStmt(sql) {
26290
- let stmt = this.typedEdgeStmts.get(sql);
26291
- if (!stmt) {
26292
- stmt = this.db.prepare(sql);
26293
- this.typedEdgeStmts.set(sql, stmt);
26294
- }
26295
- return stmt;
26296
- }
26297
- outEdges(nodeId, types) {
26298
- if (!types || types.length === 0) {
26299
- const rows2 = this.stmts.outEdges.all(nodeId);
26300
- return rows2.map(rowToEdge);
26301
- }
26302
- const placeholders = types.map((_, i2) => `:t${i2}`).join(",");
26303
- const params = { from_id: nodeId };
26304
- types.forEach((t, i2) => {
26305
- params[`t${i2}`] = t;
26306
- });
26307
- const rows = this.typedEdgeStmt(
26308
- `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
26309
- ).all(params);
26310
- return rows.map(rowToEdge);
26311
- }
26312
- inEdges(nodeId, types) {
26313
- if (!types || types.length === 0) {
26314
- const rows2 = this.stmts.inEdges.all(nodeId);
26315
- return rows2.map(rowToEdge);
26316
- }
26317
- const placeholders = types.map((_, i2) => `:t${i2}`).join(",");
26318
- const params = { to_id: nodeId };
26319
- types.forEach((t, i2) => {
26320
- params[`t${i2}`] = t;
26321
- });
26322
- const rows = this.typedEdgeStmt(
26323
- `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
26324
- ).all(params);
26325
- return rows.map(rowToEdge);
26326
- }
26327
- /** Live in-degree per target over `types`, in one grouped query — the counts
26328
- * `inEdges(id, types).length` gives, for every node at once. A per-node
26329
- * inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
26330
- * hotspots vfile materialized (2026-09-24). */
26331
- inDegreeByType(types) {
26332
- if (types.length === 0) return /* @__PURE__ */ new Map();
26333
- const placeholders = types.map(() => "?").join(",");
26334
- const rows = this.db.prepare(
26335
- `SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
26336
- ).all(...types);
26337
- return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
26338
- }
26339
- neighbors(nodeId, types) {
26340
- const out2 = [];
26341
- const edges = this.outEdges(nodeId, types);
26342
- for (const e of edges) {
26343
- const n = this.getNode(e.to);
26344
- if (n) out2.push({ edgeType: e.type, confidence: e.confidence, node: n });
26345
- }
26346
- const inE = this.inEdges(nodeId, types);
26347
- for (const e of inE) {
26348
- const n = this.getNode(e.from);
26349
- if (n) out2.push({ edgeType: e.type, confidence: e.confidence, node: n });
26350
- }
26351
- return out2;
26352
- }
26353
- nodeCount() {
26354
- return Number(this.stmts.nodeCount.get().n);
26355
- }
26356
- mutationCount() {
26357
- return this.mutations;
26358
- }
26359
- currentIngestSeq() {
26360
- return this.ingestSeq;
26361
- }
26362
- nextIngestSeq() {
26363
- this.ingestSeq += 1;
26364
- this.stmts.setIngestSeq.run({ v: String(this.ingestSeq) });
26365
- return this.ingestSeq;
26366
- }
26367
- edgeCount() {
26368
- return Number(this.stmts.edgeCount.get().n);
26369
- }
26370
- // Lightweight whole-graph scans — select only the columns the caller needs so a
26371
- // 100k-node pass skips the per-row attrs JSON.parse + embedding decode rowToNode
26372
- // does. Live rows only (valid_to IS NULL), matching findNodesByLabel/outEdges.
26373
- scanLiveNodes() {
26374
- const rows = this.db.prepare("SELECT id, label, memory_tier, page_rank, is_landmark FROM nodes WHERE valid_to IS NULL").all();
26663
+ findNodeAttrsByLabel(label) {
26664
+ const rows = this.stmts.attrsByLabel.all(label);
26375
26665
  return rows.map((r) => ({
26376
26666
  id: r.id,
26377
- label: r.label,
26378
- memoryTier: r.memory_tier,
26379
- pageRank: r.page_rank,
26380
- isLandmark: r.is_landmark === 1
26667
+ createdAt: r.created_at,
26668
+ ...r.created_at_seq != null ? { createdAtSeq: r.created_at_seq } : {},
26669
+ attrs: JSON.parse(r.attrs_json)
26381
26670
  }));
26382
26671
  }
26383
- scanLiveEdges() {
26384
- const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
26385
- return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
26386
- }
26387
- getMeta(key) {
26388
- const r = this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key);
26389
- return r?.value ?? null;
26390
- }
26391
- setMeta(key, value) {
26392
- this.db.prepare(
26393
- "INSERT INTO store_meta (key, value) VALUES (:key, :value) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
26394
- ).run({ key, value });
26672
+ *iterateNodeIdentitiesByLabel(label, pageSize = 2e3) {
26673
+ const page = this.db.prepare(
26674
+ "SELECT rowid AS rid, id, json_extract(attrs_json, '$.relPath') AS relPath, json_extract(attrs_json, '$.qname') AS qname FROM nodes WHERE rowid > ? AND label = ? AND valid_to IS NULL ORDER BY rowid LIMIT ?"
26675
+ );
26676
+ let after = 0;
26677
+ for (; ; ) {
26678
+ const rows = page.all(after, label, pageSize);
26679
+ for (const r of rows) {
26680
+ yield {
26681
+ id: r.id,
26682
+ relPath: typeof r.relPath === "string" ? r.relPath : null,
26683
+ qname: typeof r.qname === "string" ? r.qname : null
26684
+ };
26685
+ }
26686
+ if (rows.length < pageSize) return;
26687
+ after = rows[rows.length - 1].rid;
26688
+ }
26395
26689
  }
26396
- dirtyNodeIdsSince(ts) {
26397
- const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
26690
+ recentNodeIds(label, limit) {
26691
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE label = ? AND valid_to IS NULL ORDER BY last_updated_at DESC LIMIT ?").all(label, Math.max(0, limit));
26398
26692
  return rows.map((r) => r.id);
26399
26693
  }
26400
- labelFingerprint(labels) {
26401
- if (labels.length === 0) return "0";
26402
- if (labels.length === FINGERPRINT_INDEXED_LABELS.length && FINGERPRINT_INDEXED_LABELS.every((l) => labels.includes(l))) {
26403
- const r2 = this.db.prepare(
26404
- `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
26405
- MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
26406
- FROM nodes INDEXED BY nodes_instance_fingerprint_v2
26407
- WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause')`
26408
- ).get();
26409
- return `${r2.n}:${r2.t ?? 0}:${r2.s ?? 0}`;
26694
+ recentOpenProblemCandidates(opts) {
26695
+ if (!Number.isSafeInteger(opts.limit) || opts.limit < 0) {
26696
+ throw new RangeError("candidate limit must be a non-negative safe integer");
26410
26697
  }
26411
- const r = this.db.prepare(
26412
- `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
26413
- MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
26414
- FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
26415
- ).get(...labels);
26416
- return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
26417
- }
26418
- /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
26419
- * Closures matter as much as additions: a node that lost inflow is rank-dirty
26420
- * while its own row never updated, so removal endpoints must seed the
26421
- * incremental region or the stale inflow persists until the full backstop. */
26422
- edgeEndpointsTouchedSince(ts) {
26423
- const rows = this.db.prepare(
26424
- `SELECT from_id, to_id FROM edges
26425
- WHERE (valid_to IS NULL AND (created_at > :ts OR last_seen_at > :ts))
26426
- OR (valid_to IS NOT NULL AND valid_to > :ts)`
26427
- ).all({ ts });
26428
- return rows.map((r) => ({ from: r.from_id, to: r.to_id }));
26429
- }
26430
- /** Lightweight out-edges for a SET of sources, chunked IN-lists — the region
26431
- * assembly path for incremental PageRank (per-node outEdges() at region
26432
- * scale re-creates the 106k-prepared-calls problem the batch scan solved). */
26433
- outEdgesForMany(ids) {
26434
- return this.edgesForMany(ids, "from_id");
26435
- }
26436
- inEdgesForMany(ids) {
26437
- return this.edgesForMany(ids, "to_id");
26698
+ return this.db.prepare(`SELECT id, description FROM nodes
26699
+ WHERE label = 'Problem' AND valid_to IS NULL
26700
+ AND json_extract(attrs_json, '$.resolvedAt') IS NULL
26701
+ AND json_type(attrs_json, '$.mergedInto') IS NULL
26702
+ AND (? IS NULL OR id != ?)
26703
+ AND (json_type(attrs_json, '$.workspaceId') IS NULL OR json_extract(attrs_json, '$.workspaceId') = ?)
26704
+ AND COALESCE(json_extract(attrs_json, '$.kind') = 'constraint', 0) = ?
26705
+ ORDER BY last_updated_at DESC, rowid ASC LIMIT ?`).all(
26706
+ opts.excludeId ?? null,
26707
+ opts.excludeId ?? null,
26708
+ opts.workspaceId,
26709
+ opts.constraint ? 1 : 0,
26710
+ opts.limit
26711
+ );
26438
26712
  }
26439
- /** Stored pageRank for a SET of ids (live rows only — a closed id is simply
26440
- * absent, which is how incremental region assembly drops dead endpoints). */
26441
- ranksForMany(ids) {
26442
- const out2 = /* @__PURE__ */ new Map();
26443
- const CHUNK = 400;
26444
- for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
26445
- const chunk = ids.slice(i2, i2 + CHUNK);
26446
- const placeholders = chunk.map(() => "?").join(", ");
26447
- const rows = this.db.prepare(
26448
- `SELECT id, page_rank FROM nodes WHERE id IN (${placeholders}) AND valid_to IS NULL`
26449
- ).all(...chunk);
26450
- for (const r of rows) out2.set(r.id, r.page_rank);
26451
- }
26452
- return out2;
26713
+ hasLiveNodes() {
26714
+ const r = this.db.prepare("SELECT EXISTS(SELECT 1 FROM nodes WHERE valid_to IS NULL) AS e").get();
26715
+ return r.e === 1;
26453
26716
  }
26454
- /** The minimal row set the landmark sweep needs — landmark CANDIDATES
26455
- * (Pattern/RootCause), everything currently flagged, and every
26456
- * persistent-tier node (force-landmarks). A few thousand rows, so the
26457
- * incremental path can refresh the GLOBAL landmark set without the 179k-row
26458
- * scanLiveNodes materialization. */
26459
- landmarkSweepRows() {
26460
- const rows = this.db.prepare(
26461
- `SELECT id, label, page_rank, is_landmark, memory_tier, extraction_source FROM nodes
26462
- WHERE valid_to IS NULL
26463
- AND (memory_tier = 'persistent' OR is_landmark = 1 OR label IN ('Pattern', 'RootCause'))`
26464
- ).all();
26717
+ findNodeIdentitiesByLabel(label) {
26718
+ const rows = this.stmts.identitiesByLabel.all(label);
26465
26719
  return rows.map((r) => ({
26466
26720
  id: r.id,
26467
- label: r.label,
26468
- pageRank: r.page_rank,
26469
- isLandmark: r.is_landmark === 1,
26470
- memoryTier: r.memory_tier,
26471
- extractionSource: r.extraction_source
26721
+ relPath: typeof r.relPath === "string" ? r.relPath : null,
26722
+ qname: typeof r.qname === "string" ? r.qname : null
26472
26723
  }));
26473
26724
  }
26474
- edgesForMany(ids, col) {
26475
- const out2 = [];
26476
- const CHUNK = 400;
26477
- for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
26478
- const chunk = ids.slice(i2, i2 + CHUNK);
26479
- const placeholders = chunk.map(() => "?").join(", ");
26480
- const rows = this.db.prepare(
26481
- `SELECT from_id, to_id, type FROM edges WHERE ${col} IN (${placeholders}) AND valid_to IS NULL`
26482
- ).all(...chunk);
26483
- for (const r of rows) out2.push({ from: r.from_id, to: r.to_id, type: r.type });
26725
+ findAllVersionsByLabel(label, attribute) {
26726
+ if (attribute && !/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(attribute.path)) {
26727
+ throw new Error(`invalid attr path: ${attribute.path}`);
26484
26728
  }
26485
- return out2;
26729
+ const rows = attribute ? this.db.prepare(`SELECT * FROM nodes WHERE json_extract(attrs_json, '$.${attribute.path}') = ? AND label = ?`).all(attribute.value, label) : this.stmts.findByLabelAll.all(label);
26730
+ return rows.map(rowToNode);
26486
26731
  }
26487
- checkpointWal() {
26488
- try {
26489
- const r = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
26490
- return r ?? null;
26491
- } catch {
26492
- return null;
26732
+ searchNodes(opts) {
26733
+ const where = [];
26734
+ const params = {};
26735
+ if (opts.labels && opts.labels.length > 0) {
26736
+ const placeholders = opts.labels.map((_, i2) => `:label${i2}`).join(", ");
26737
+ where.push(`label IN (${placeholders})`);
26738
+ opts.labels.forEach((l, i2) => {
26739
+ params[`label${i2}`] = l;
26740
+ });
26493
26741
  }
26494
- }
26495
- reviveReobserved(relPaths, workspaceId, since) {
26496
- let revived = 0;
26497
- const CHUNK = 400;
26498
- for (let i2 = 0; i2 < relPaths.length; i2 += CHUNK) {
26499
- const slice = relPaths.slice(i2, i2 + CHUNK);
26500
- if (slice.length === 0) continue;
26501
- const r = this.db.prepare(
26502
- `UPDATE nodes SET valid_to = NULL
26503
- WHERE valid_to IS NOT NULL
26504
- AND last_updated_at >= ?
26505
- AND json_extract(attrs_json, '$.workspaceId') = ?
26506
- AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})`
26507
- ).run(since, workspaceId, ...slice);
26508
- revived += Number(r.changes);
26742
+ if (opts.descriptionLike) {
26743
+ where.push("description LIKE :descPat");
26744
+ params["descPat"] = `%${opts.descriptionLike}%`;
26509
26745
  }
26510
- if (revived > 0) this.mutations++;
26511
- return revived;
26746
+ where.push("valid_to IS NULL");
26747
+ const limit = Math.max(1, opts.limit ?? 50);
26748
+ params["limit"] = limit;
26749
+ const whereSql = where.length > 0 ? "WHERE " + where.join(" AND ") : "";
26750
+ const sql = `SELECT * FROM nodes ${whereSql} ORDER BY page_rank DESC, last_updated_at DESC LIMIT :limit`;
26751
+ const rows = this.db.prepare(sql).all(params);
26752
+ return rows.map(rowToNode);
26512
26753
  }
26513
- recentEdgeAttrCoverage(marker, attr, limit) {
26514
- const r = this.db.prepare(
26515
- `SELECT COUNT(*) AS total,
26516
- COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
26517
- FROM (SELECT attrs_json FROM edges
26518
- WHERE valid_to IS NULL AND attrs_json LIKE ?
26519
- ORDER BY created_at DESC LIMIT ?)`
26520
- ).get(attr, `%${marker}%`, limit);
26521
- return { count: Number(r.count), total: Number(r.total) };
26754
+ searchDescriptionTerms(terms, limit) {
26755
+ if (terms.length === 0) return [];
26756
+ const score2 = terms.map(() => "(description LIKE ?)").join(" + ");
26757
+ const rows = this.db.prepare(
26758
+ `SELECT n.*, h.m AS matched
26759
+ FROM (SELECT rowid AS rid, ${score2} AS m
26760
+ FROM nodes INDEXED BY nodes_live_description
26761
+ WHERE valid_to IS NULL) h
26762
+ JOIN nodes n ON n.rowid = h.rid
26763
+ WHERE h.m > 0
26764
+ ORDER BY h.m DESC, n.page_rank DESC, n.last_updated_at DESC
26765
+ LIMIT ?`
26766
+ ).all(...terms.map((t) => `%${t}%`), Math.max(1, limit));
26767
+ return rows.map((r) => ({ node: rowToNode(r), matched: Number(r.matched) }));
26522
26768
  }
26523
- liveNodeIds(ids) {
26524
- const live = /* @__PURE__ */ new Set();
26525
- const CHUNK = 900;
26526
- for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
26527
- const slice = ids.slice(i2, i2 + CHUNK);
26528
- if (slice.length === 0) continue;
26529
- const rows = this.db.prepare(
26530
- `SELECT id FROM nodes WHERE valid_to IS NULL AND id IN (${slice.map(() => "?").join(",")})`
26531
- ).all(...slice);
26532
- for (const r of rows) live.add(r.id);
26533
- }
26534
- return live;
26769
+ searchTextCandidates(terms) {
26770
+ if (terms.length === 0) return [];
26771
+ const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
26772
+ SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
26773
+ WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
26774
+ GROUP BY n.id
26775
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, matches.matched FROM nodes
26776
+ JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
26777
+ return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
26778
+ }
26779
+ getTextSearchNode(id) {
26780
+ const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
26781
+ return row ? textSearchNode(row) : null;
26782
+ }
26783
+ readPageRankScale() {
26784
+ const rows = this.db.prepare("SELECT page_rank FROM nodes WHERE valid_to IS NULL AND page_rank > 0").all();
26785
+ return pageRankScale(rows.map((row) => row.page_rank));
26786
+ }
26787
+ pageRankScale() {
26788
+ const value = Number(this.getMeta(PAGE_RANK_SCALE_KEY));
26789
+ if (Number.isFinite(value) && value > 0) return value;
26790
+ return this.initialPageRankScale ??= this.readPageRankScale();
26791
+ }
26792
+ refreshPageRankScale() {
26793
+ const scale = this.readPageRankScale();
26794
+ this.setMeta(PAGE_RANK_SCALE_KEY, String(scale));
26795
+ return scale;
26535
26796
  }
26536
- /**
26537
- * Live edges WITH their endpoint labels and ids, resolved in ONE join.
26538
- *
26539
- * The ontology sweep needs (id, type, fromLabel, toLabel) for every live edge.
26540
- * Doing that as `scanLiveEdges()` + two `getNode()` calls is 2N node reads —
26541
- * 550,000 on this store — and each one deserializes the node's embedding blob.
26542
- * Measured: the sweep did not finish in 10 minutes. As a single join it is one
26543
- * query over an index-covered scan. Labels only; nothing here touches embeddings.
26544
- */
26545
- scanLiveEdgeRows() {
26546
- const rows = this.db.prepare(
26547
- `SELECT e.id, e.from_id, e.to_id, e.type, a.label AS from_label, b.label AS to_label
26548
- FROM edges e
26549
- LEFT JOIN nodes a ON a.id = e.from_id AND a.valid_to IS NULL
26550
- LEFT JOIN nodes b ON b.id = e.to_id AND b.valid_to IS NULL
26551
- WHERE e.valid_to IS NULL`
26552
- ).all();
26553
- return rows.map((r) => ({
26554
- id: r.id,
26555
- from: r.from_id,
26556
- to: r.to_id,
26557
- type: r.type,
26558
- fromLabel: r.from_label ?? void 0,
26559
- toLabel: r.to_label ?? void 0
26560
- }));
26797
+ typedEdgeStmt(sql) {
26798
+ let stmt = this.typedEdgeStmts.get(sql);
26799
+ if (!stmt) {
26800
+ stmt = this.db.prepare(sql);
26801
+ this.typedEdgeStmts.set(sql, stmt);
26802
+ }
26803
+ return stmt;
26561
26804
  }
26562
- /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
26563
- * expression index so the incremental reindex fetches only the changed files'
26564
- * symbols instead of scanning every versioned node. */
26565
- /** Live nodes of `labels` whose description is exactly one of `names`, via
26566
- * the nodes_live_description index. Chunked under the bound-parameter cap. */
26567
- liveNodesByDescription(names, labels) {
26568
- if (names.length === 0 || labels.length === 0) return [];
26569
- const out2 = [];
26570
- const labelPh = labels.map(() => "?").join(",");
26571
- for (let i2 = 0; i2 < names.length; i2 += 400) {
26572
- const chunk = names.slice(i2, i2 + 400);
26573
- const rows = this.db.prepare(
26574
- `SELECT * FROM nodes WHERE valid_to IS NULL AND description IN (${chunk.map(() => "?").join(",")}) AND label IN (${labelPh})`
26575
- ).all(...chunk, ...labels);
26576
- for (const r of rows) out2.push(rowToNode(r));
26805
+ outEdges(nodeId, types) {
26806
+ if (!types || types.length === 0) {
26807
+ const rows2 = this.stmts.outEdges.all(nodeId);
26808
+ return rows2.map(rowToEdge);
26577
26809
  }
26578
- return out2;
26810
+ const placeholders = types.map((_, i2) => `:t${i2}`).join(",");
26811
+ const params = { from_id: nodeId };
26812
+ types.forEach((t, i2) => {
26813
+ params[`t${i2}`] = t;
26814
+ });
26815
+ const rows = this.typedEdgeStmt(
26816
+ `SELECT * FROM edges WHERE from_id = :from_id AND type IN (${placeholders}) AND valid_to IS NULL`
26817
+ ).all(params);
26818
+ return rows.map(rowToEdge);
26579
26819
  }
26580
- nodesByRelPath(relPaths) {
26581
- if (relPaths.length === 0) return [];
26582
- const placeholders = relPaths.map(() => "?").join(",");
26583
- const rows = this.db.prepare(
26584
- `SELECT * FROM nodes WHERE valid_to IS NULL AND json_extract(attrs_json, '$.relPath') IN (${placeholders})`
26585
- ).all(...relPaths);
26586
- return rows.map(rowToNode);
26820
+ inEdges(nodeId, types) {
26821
+ if (!types || types.length === 0) {
26822
+ const rows2 = this.stmts.inEdges.all(nodeId);
26823
+ return rows2.map(rowToEdge);
26824
+ }
26825
+ const placeholders = types.map((_, i2) => `:t${i2}`).join(",");
26826
+ const params = { to_id: nodeId };
26827
+ types.forEach((t, i2) => {
26828
+ params[`t${i2}`] = t;
26829
+ });
26830
+ const rows = this.typedEdgeStmt(
26831
+ `SELECT * FROM edges WHERE to_id = :to_id AND type IN (${placeholders}) AND valid_to IS NULL`
26832
+ ).all(params);
26833
+ return rows.map(rowToEdge);
26587
26834
  }
26588
- findLiveNodesByAttr(label, attrPath, value) {
26589
- if (!/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(attrPath)) throw new Error(`invalid attr path: ${attrPath}`);
26835
+ /** Live in-degree per target over `types`, in one grouped query — the counts
26836
+ * `inEdges(id, types).length` gives, for every node at once. A per-node
26837
+ * inEdges loop over ~48k code nodes held a PreToolUse hook 12.6s when the
26838
+ * hotspots vfile materialized (2026-09-24). */
26839
+ inDegreeByType(types) {
26840
+ if (types.length === 0) return /* @__PURE__ */ new Map();
26841
+ const placeholders = types.map(() => "?").join(",");
26590
26842
  const rows = this.db.prepare(
26591
- `SELECT * FROM nodes WHERE json_extract(attrs_json, '$.${attrPath}') = ? AND valid_to IS NULL AND label = ?`
26592
- ).all(value, label);
26593
- return rows.map(rowToNode);
26843
+ `SELECT to_id, COUNT(*) AS n FROM edges WHERE type IN (${placeholders}) AND valid_to IS NULL GROUP BY to_id`
26844
+ ).all(...types);
26845
+ return new Map(rows.map((r) => [r.to_id, Number(r.n)]));
26594
26846
  }
26595
- // ── Bi-temporal versioning ───────────────────────────────────────────
26596
- // NOTE: the adapter's transaction() is non-reentrant (raw BEGIN/COMMIT), and
26597
- // the indexer calls freezeNode inside its own store.transaction(). So this
26598
- // method does NOT open a transaction — callers needing atomicity wrap it.
26599
- freezeNode(liveId, t) {
26600
- const live = this.getNode(liveId);
26601
- if (!live) return null;
26602
- const version3 = live.version ?? 1;
26603
- const frozenId = `${liveId}@v${version3}`;
26604
- if (this.getNode(frozenId)) {
26605
- this.stmts.advanceLive.run({ live_id: liveId, t });
26606
- return frozenId;
26847
+ neighbors(nodeId, types) {
26848
+ const out2 = [];
26849
+ const edges = this.outEdges(nodeId, types);
26850
+ for (const e of edges) {
26851
+ const n = this.getNode(e.to);
26852
+ if (n) out2.push({ edgeType: e.type, confidence: e.confidence, node: n });
26607
26853
  }
26608
- this.stmts.freezeCopy.run({ frozen_id: frozenId, live_id: liveId, t });
26609
- this.mergeEdge({
26610
- id: `edge_superseded_${frozenId}`,
26611
- from: frozenId,
26612
- to: liveId,
26613
- type: "SUPERSEDED_BY",
26614
- confidence: 1,
26615
- extractionSource: "daemon-extracted",
26616
- createdAt: t,
26617
- lastSeenAt: t,
26618
- navSuccesses: 0,
26619
- navFailures: 0,
26620
- validFrom: t,
26621
- validTo: null,
26622
- attrs: {}
26623
- });
26624
- this.stmts.advanceLive.run({ live_id: liveId, t });
26625
- return frozenId;
26854
+ const inE = this.inEdges(nodeId, types);
26855
+ for (const e of inE) {
26856
+ const n = this.getNode(e.from);
26857
+ if (n) out2.push({ edgeType: e.type, confidence: e.confidence, node: n });
26858
+ }
26859
+ return out2;
26626
26860
  }
26627
- closeNode(id, t) {
26628
- this.stmts.closeNode.run({ id, t });
26861
+ nodeCount() {
26862
+ return Number(this.stmts.nodeCount.get().n);
26629
26863
  }
26630
- closeEdge(id, t) {
26631
- this.stmts.closeEdge.run({ id, t });
26864
+ mutationCount() {
26865
+ return this.mutations;
26632
26866
  }
26633
- nodeHistory(logicalId) {
26634
- const rows = this.stmts.nodeHistory.all(logicalId);
26635
- return rows.map(rowToNode);
26867
+ currentIngestSeq() {
26868
+ return this.ingestSeq;
26636
26869
  }
26637
- nodesAsOf(t) {
26638
- const rows = this.stmts.nodesAsOf.all({ t });
26639
- return rows.map(rowToNode);
26870
+ nextIngestSeq() {
26871
+ this.ingestSeq += 1;
26872
+ this.stmts.setIngestSeq.run({ v: String(this.ingestSeq) });
26873
+ return this.ingestSeq;
26640
26874
  }
26641
- /** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
26642
- * SQL, so only the flagged rows are hydrated. The filter-after form hydrated
26643
- * the whole graph for a handful of revisit flags: 6.7s per context render on
26644
- * a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
26645
- * so it must be a plain identifier. */
26646
- nodesAsOfWhereTrue(t, attr) {
26647
- if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
26648
- const rows = this.db.prepare(
26649
- `SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
26650
- AND json_type(attrs_json, '$.${attr}') = 'true'`
26651
- ).all({ t });
26652
- return rows.map(rowToNode);
26875
+ edgeCount() {
26876
+ return Number(this.stmts.edgeCount.get().n);
26653
26877
  }
26654
- edgesAsOf(t) {
26655
- const rows = this.stmts.edgesAsOf.all({ t });
26656
- return rows.map(rowToEdge);
26878
+ // Lightweight whole-graph scans — select only the columns the caller needs so a
26879
+ // 100k-node pass skips the per-row attrs JSON.parse + embedding decode rowToNode
26880
+ // does. Live rows only (valid_to IS NULL), matching findNodesByLabel/outEdges.
26881
+ scanLiveNodes() {
26882
+ const rows = this.db.prepare("SELECT id, label, memory_tier, page_rank, is_landmark FROM nodes WHERE valid_to IS NULL").all();
26883
+ return rows.map((r) => ({
26884
+ id: r.id,
26885
+ label: r.label,
26886
+ memoryTier: r.memory_tier,
26887
+ pageRank: r.page_rank,
26888
+ isLandmark: r.is_landmark === 1
26889
+ }));
26657
26890
  }
26658
- transaction(fn) {
26659
- return this.db.transaction(fn);
26891
+ scanLiveEdges() {
26892
+ const rows = this.db.prepare("SELECT from_id, to_id, type FROM edges WHERE valid_to IS NULL").all();
26893
+ return rows.map((r) => ({ from: r.from_id, to: r.to_id, type: r.type }));
26660
26894
  }
26661
- close() {
26662
- this.db.close();
26895
+ getMeta(key) {
26896
+ const r = this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key);
26897
+ return r?.value ?? null;
26663
26898
  }
26664
- };
26665
- function openGraphStore(opts) {
26666
- return new SqliteGraphStore(opts);
26667
- }
26668
-
26669
- // ../../packages/local-graph/src/traversal.ts
26670
- init_src2();
26671
- var MAX_EDGE_WEIGHT = Math.max(...Object.values(EDGE_WEIGHT));
26672
- var CAUSAL_FAMILY = [
26673
- ...CAUSAL_EDGES,
26674
- ...RESOLUTION_EDGES
26675
- ];
26676
-
26677
- // ../../packages/local-graph/src/justification.ts
26678
- init_src();
26679
- init_src2();
26680
- function addDependency(store2, opts) {
26681
- const id = digest({ from: opts.fromId, type: "DEPENDS_ON", to: opts.toId });
26682
- const attrs = { relation: opts.relation };
26683
- if (opts.introduced !== void 0) attrs.introduced = opts.introduced;
26684
- if (opts.fixed !== void 0) attrs.fixed = opts.fixed;
26685
- if (opts.evidence !== void 0) attrs.evidence = opts.evidence;
26686
- const edge = {
26687
- id,
26688
- from: opts.fromId,
26689
- to: opts.toId,
26690
- type: "DEPENDS_ON",
26691
- confidence: 1,
26692
- extractionSource: opts.extractionSource ?? "agent-observed",
26693
- createdAt: opts.ts,
26694
- lastSeenAt: opts.ts,
26695
- navSuccesses: 0,
26696
- navFailures: 0,
26697
- attrs
26698
- };
26699
- store2.mergeEdge(edge);
26700
- return edge;
26701
- }
26702
- function markRevisit(store2, node, reason, ts, answeredWhen) {
26703
- const fresh = store2.getNode(node.id) ?? node;
26704
- if (fresh.validTo != null || fresh.attrs["retiredReason"]) return;
26705
- store2.updateNode(node.id, {
26706
- attrs: {
26707
- ...fresh.attrs,
26708
- revisit: true,
26709
- revisitReason: reason,
26710
- revisitSinceTs: ts,
26711
- revisitAnsweredWhen: answeredWhen
26712
- },
26713
- lastUpdatedAt: ts
26714
- });
26715
- }
26716
- var LEGACY_AUTO_CLOSE_ASK = "auto-closed off a pre-provenance anchor";
26717
- var KNOWN_CONDITIONS = {
26718
- parentProblemOpen: true,
26719
- dependentStale: true
26720
- };
26721
- function isRevisitCondition(v) {
26722
- return typeof v === "string" && Object.prototype.hasOwnProperty.call(KNOWN_CONDITIONS, v);
26723
- }
26724
- function conditionOf(node) {
26725
- const explicit = node.attrs["revisitAnsweredWhen"];
26726
- if (isRevisitCondition(explicit)) return explicit;
26727
- if (String(node.attrs["revisitReason"] ?? "").startsWith(LEGACY_AUTO_CLOSE_ASK)) {
26728
- return "parentProblemOpen";
26899
+ setMeta(key, value) {
26900
+ this.db.prepare(
26901
+ "INSERT INTO store_meta (key, value) VALUES (:key, :value) ON CONFLICT(key) DO UPDATE SET value = excluded.value"
26902
+ ).run({ key, value });
26729
26903
  }
26730
- return void 0;
26731
- }
26732
- function clearAnsweredRevisits(store2, ts) {
26733
- const report = { cleared: 0, standing: 0 };
26734
- for (const label of SEMANTIC_NODE_LABELS) {
26735
- for (const node of store2.findNodesByLabel(label)) {
26736
- if (node.attrs["revisit"] !== true) continue;
26737
- const condition = conditionOf(node);
26738
- if (condition === void 0 || !isRevisitAnswered(store2, node, condition)) {
26739
- report.standing++;
26740
- continue;
26741
- }
26742
- clearRevisit(store2, node.id, ts);
26743
- report.cleared++;
26744
- }
26904
+ dirtyNodeIdsSince(ts) {
26905
+ const rows = this.db.prepare("SELECT id FROM nodes WHERE valid_to IS NULL AND last_updated_at > ?").all(ts);
26906
+ return rows.map((r) => r.id);
26745
26907
  }
26746
- return report;
26747
- }
26748
- function isRevisitAnswered(store2, node, condition) {
26749
- switch (condition) {
26750
- case "parentProblemOpen": {
26751
- const parents = store2.inEdges(node.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.from)).filter((n) => n !== null && n.validTo == null);
26752
- return parents.every((p) => p.attrs["resolvedAt"] == null);
26753
- }
26754
- case "dependentStale": {
26755
- const since = Number(node.attrs["revisitSinceTs"] ?? 0);
26756
- return since > 0 && node.lastUpdatedAt > since;
26908
+ labelFingerprint(labels) {
26909
+ if (labels.length === 0) return "0";
26910
+ if (labels.length === FINGERPRINT_INDEXED_LABELS.length && FINGERPRINT_INDEXED_LABELS.every((l) => labels.includes(l))) {
26911
+ const r2 = this.db.prepare(
26912
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
26913
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
26914
+ FROM nodes INDEXED BY nodes_instance_fingerprint_v2
26915
+ WHERE valid_to IS NULL AND label IN ('Problem','Vulnerability','Solution','RootCause')`
26916
+ ).get();
26917
+ return `${r2.n}:${r2.t ?? 0}:${r2.s ?? 0}`;
26757
26918
  }
26919
+ const r = this.db.prepare(
26920
+ `SELECT COUNT(*) AS n, MAX(last_updated_at) AS t,
26921
+ MAX(COALESCE(last_reinforced_at_seq, created_at_seq, 0)) AS s
26922
+ FROM nodes WHERE valid_to IS NULL AND label IN (${labels.map(() => "?").join(",")})`
26923
+ ).get(...labels);
26924
+ return `${r.n}:${r.t ?? 0}:${r.s ?? 0}`;
26758
26925
  }
26759
- }
26760
- function clearRevisit(store2, nodeId, ts) {
26761
- const n = store2.getNode(nodeId);
26762
- if (!n) return;
26763
- const attrs = { ...n.attrs };
26764
- delete attrs["revisit"];
26765
- delete attrs["revisitReason"];
26766
- delete attrs["revisitSinceTs"];
26767
- store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
26768
- }
26769
- function listNeedsRevisit(store2, asOf) {
26770
- return store2.nodesAsOfWhereTrue(asOf, "revisit").filter((n) => !n.attrs["retiredReason"]).map((n) => ({
26771
- id: n.id,
26772
- label: n.label,
26773
- description: n.description,
26774
- reason: String(n.attrs["revisitReason"] ?? "")
26775
- }));
26776
- }
26777
-
26778
- // ../../packages/local-graph/src/credit.ts
26779
- init_src2();
26780
- var CREDIT_FAMILY = [
26781
- ...CAUSAL_EDGES,
26782
- ...RESOLUTION_EDGES,
26783
- ...JUSTIFICATION_EDGES
26784
- ];
26785
-
26786
- // ../../packages/local-graph/src/claims.ts
26787
- init_src();
26788
- init_src();
26789
-
26790
- // ../../packages/local-graph/src/crystallize.ts
26791
- var LONG_TERM_SUPPORT = 2;
26792
- var PERSISTENT_SUPPORT = 4;
26793
- var PERSISTENT_CONFIDENCE = 0.85;
26794
- var SCOPE_AXES = ["lang", "versionRange", "codebase", "regime"];
26795
- function splittingAxis(a, b) {
26796
- for (const ax of SCOPE_AXES) {
26797
- if (a[ax] !== void 0 && b[ax] !== void 0 && a[ax] !== b[ax]) return ax;
26926
+ /** Endpoints of edges TOUCHED since `ts` — created, re-seen, or CLOSED.
26927
+ * Closures matter as much as additions: a node that lost inflow is rank-dirty
26928
+ * while its own row never updated, so removal endpoints must seed the
26929
+ * incremental region or the stale inflow persists until the full backstop. */
26930
+ edgeEndpointsTouchedSince(ts) {
26931
+ const rows = this.db.prepare(
26932
+ `SELECT from_id, to_id FROM edges
26933
+ WHERE (valid_to IS NULL AND (created_at > :ts OR last_seen_at > :ts))
26934
+ OR (valid_to IS NOT NULL AND valid_to > :ts)`
26935
+ ).all({ ts });
26936
+ return rows.map((r) => ({ from: r.from_id, to: r.to_id }));
26798
26937
  }
26799
- return null;
26800
- }
26801
- function support(n) {
26802
- return n.attrs["groundedSupport"] ?? 0;
26803
- }
26804
- function sourcesCount(n) {
26805
- const s = n.attrs["sources"];
26806
- return Array.isArray(s) ? s.length : 0;
26807
- }
26808
- function crystallize(store2, opts = { ts: 0 }) {
26809
- const claims = store2.findNodesByLabel("Claim");
26810
- const result = {
26811
- evaluated: claims.length,
26812
- promoted: [],
26813
- derived: [],
26814
- plural: [],
26815
- discriminators: [],
26816
- contested: []
26817
- };
26818
- for (const c of claims) {
26819
- const contradictions = store2.outEdges(c.id, ["CONTRADICTS"]).concat(store2.inEdges(c.id, ["CONTRADICTS"])).length;
26820
- const s = support(c) + Math.max(0, sourcesCount(c) - 1);
26821
- const f = contradictions;
26822
- const confidence = (1 + s) / (2 + s + f);
26823
- store2.updateNode(c.id, {
26824
- attrs: { ...c.attrs, confidence },
26825
- lastUpdatedAt: opts.ts
26826
- });
26827
- const sup = support(c);
26828
- let tier = c.memoryTier;
26829
- if (sup >= PERSISTENT_SUPPORT && confidence >= PERSISTENT_CONFIDENCE) tier = "persistent";
26830
- else if (sup >= LONG_TERM_SUPPORT) tier = "long-term";
26831
- if (tier !== c.memoryTier) {
26832
- store2.updateNode(c.id, { attrs: { ...store2.getNode(c.id).attrs }, memoryTier: tier, lastUpdatedAt: opts.ts });
26833
- result.promoted.push(c.id);
26938
+ /** Lightweight out-edges for a SET of sources, chunked IN-lists — the region
26939
+ * assembly path for incremental PageRank (per-node outEdges() at region
26940
+ * scale re-creates the 106k-prepared-calls problem the batch scan solved). */
26941
+ outEdgesForMany(ids) {
26942
+ return this.edgesForMany(ids, "from_id");
26943
+ }
26944
+ inEdgesForMany(ids) {
26945
+ return this.edgesForMany(ids, "to_id");
26946
+ }
26947
+ /** Stored pageRank for a SET of ids (live rows only — a closed id is simply
26948
+ * absent, which is how incremental region assembly drops dead endpoints). */
26949
+ ranksForMany(ids) {
26950
+ const out2 = /* @__PURE__ */ new Map();
26951
+ const CHUNK = 400;
26952
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
26953
+ const chunk = ids.slice(i2, i2 + CHUNK);
26954
+ const placeholders = chunk.map(() => "?").join(", ");
26955
+ const rows = this.db.prepare(
26956
+ `SELECT id, page_rank FROM nodes WHERE id IN (${placeholders}) AND valid_to IS NULL`
26957
+ ).all(...chunk);
26958
+ for (const r of rows) out2.set(r.id, r.page_rank);
26834
26959
  }
26960
+ return out2;
26835
26961
  }
26836
- const seenPair = /* @__PURE__ */ new Set();
26837
- for (const c of claims) {
26838
- for (const e of store2.outEdges(c.id, ["CONTRADICTS"])) {
26839
- const key = [e.from, e.to].sort().join("|");
26840
- if (seenPair.has(key)) continue;
26841
- seenPair.add(key);
26842
- const a = store2.getNode(e.from);
26843
- const b = store2.getNode(e.to);
26844
- if (!a || !b) continue;
26845
- const kind = mergeTruthKind(
26846
- a.attrs["truthKind"],
26847
- b.attrs["truthKind"]
26848
- );
26849
- if (kind === "objective") {
26850
- markBoth(store2, a, b, { needsDiscriminator: true }, opts.ts);
26851
- result.discriminators.push(a.id, b.id);
26852
- continue;
26853
- }
26854
- if (kind === "taste") {
26855
- markBoth(store2, a, b, { crystallized: "plural" }, opts.ts);
26856
- result.plural.push(a.id, b.id);
26857
- continue;
26858
- }
26859
- const axis = splittingAxis(
26860
- a.attrs["scope"] ?? {},
26861
- b.attrs["scope"] ?? {}
26862
- );
26863
- if (axis && support(a) > 0 && support(b) > 0) {
26864
- markBoth(store2, a, b, { crystallized: "derived", splitAxis: axis }, opts.ts);
26865
- result.derived.push(a.id, b.id);
26866
- } else if (axis) {
26867
- markBoth(store2, a, b, { crystallized: "hypothesis" }, opts.ts);
26868
- } else {
26869
- markBoth(
26870
- store2,
26871
- a,
26872
- b,
26873
- {
26874
- revisit: true,
26875
- revisitReason: "contested: contradictory contextual claim at the same scope, no splitting axis",
26876
- revisitSinceTs: opts.ts
26877
- },
26878
- opts.ts
26879
- );
26880
- result.contested.push(a.id, b.id);
26881
- }
26962
+ /** The minimal row set the landmark sweep needs — landmark CANDIDATES
26963
+ * (Pattern/RootCause), everything currently flagged, and every
26964
+ * persistent-tier node (force-landmarks). A few thousand rows, so the
26965
+ * incremental path can refresh the GLOBAL landmark set without the 179k-row
26966
+ * scanLiveNodes materialization. */
26967
+ landmarkSweepRows() {
26968
+ const rows = this.db.prepare(
26969
+ `SELECT id, label, page_rank, is_landmark, memory_tier, extraction_source FROM nodes
26970
+ WHERE valid_to IS NULL
26971
+ AND (memory_tier = 'persistent' OR is_landmark = 1 OR label IN ('Pattern', 'RootCause'))`
26972
+ ).all();
26973
+ return rows.map((r) => ({
26974
+ id: r.id,
26975
+ label: r.label,
26976
+ pageRank: r.page_rank,
26977
+ isLandmark: r.is_landmark === 1,
26978
+ memoryTier: r.memory_tier,
26979
+ extractionSource: r.extraction_source
26980
+ }));
26981
+ }
26982
+ edgesForMany(ids, col) {
26983
+ const out2 = [];
26984
+ const CHUNK = 400;
26985
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
26986
+ const chunk = ids.slice(i2, i2 + CHUNK);
26987
+ const placeholders = chunk.map(() => "?").join(", ");
26988
+ const rows = this.db.prepare(
26989
+ `SELECT from_id, to_id, type FROM edges WHERE ${col} IN (${placeholders}) AND valid_to IS NULL`
26990
+ ).all(...chunk);
26991
+ for (const r of rows) out2.push({ from: r.from_id, to: r.to_id, type: r.type });
26882
26992
  }
26993
+ return out2;
26883
26994
  }
26884
- return result;
26885
- }
26886
- function mergeTruthKind(a, b) {
26887
- if (a === "objective" || b === "objective") return "objective";
26888
- if (a === "contextual" || b === "contextual") return "contextual";
26889
- return "taste";
26890
- }
26891
- function markBoth(store2, a, b, patch, ts) {
26892
- for (const n of [a, b]) {
26893
- const fresh = store2.getNode(n.id);
26894
- store2.updateNode(n.id, { attrs: { ...fresh.attrs, ...patch }, lastUpdatedAt: ts });
26995
+ checkpointWal() {
26996
+ try {
26997
+ const r = this.db.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
26998
+ return r ?? null;
26999
+ } catch {
27000
+ return null;
27001
+ }
26895
27002
  }
26896
- }
26897
-
26898
- // ../../packages/local-graph/src/design-problem.ts
26899
- init_src();
26900
- init_src();
26901
-
26902
- // ../../packages/local-graph/src/problem-package-link.ts
26903
- init_src();
26904
- var NAME_SEPARATOR = /[@/\-._]/;
26905
- var MIN_NAME_LEN = 3;
26906
- function buildPackageIndex(store2) {
26907
- const idx = /* @__PURE__ */ new Map();
26908
- for (const n of store2.findNodesByLabel("Package")) {
26909
- const name2 = String(n.attrs["name"] ?? "").trim();
26910
- if (!name2) continue;
26911
- const key = name2.toLowerCase();
26912
- const prev = idx.get(key);
26913
- if (!prev || n.attrs["resolved"] === true && prev.attrs["resolved"] !== true) {
26914
- idx.set(key, n);
27003
+ reviveReobserved(relPaths, workspaceId, since) {
27004
+ let revived = 0;
27005
+ const CHUNK = 400;
27006
+ for (let i2 = 0; i2 < relPaths.length; i2 += CHUNK) {
27007
+ const slice = relPaths.slice(i2, i2 + CHUNK);
27008
+ if (slice.length === 0) continue;
27009
+ const r = this.db.prepare(
27010
+ `UPDATE nodes SET valid_to = NULL
27011
+ WHERE valid_to IS NOT NULL
27012
+ AND last_updated_at >= ?
27013
+ AND json_extract(attrs_json, '$.workspaceId') = ?
27014
+ AND json_extract(attrs_json, '$.relPath') IN (${slice.map(() => "?").join(",")})`
27015
+ ).run(since, workspaceId, ...slice);
27016
+ revived += Number(r.changes);
26915
27017
  }
27018
+ if (revived > 0) this.mutations++;
27019
+ return revived;
26916
27020
  }
26917
- return idx;
26918
- }
26919
- function escapeRe2(s) {
26920
- return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26921
- }
26922
- function matchPackagesInText(text, index) {
26923
- const hay = text.toLowerCase();
26924
- const out2 = [];
26925
- const seen = /* @__PURE__ */ new Set();
26926
- for (const [key, node] of index) {
26927
- if (key.length < MIN_NAME_LEN) continue;
26928
- if (!hay.includes(key)) continue;
26929
- const esc2 = escapeRe2(key);
26930
- const bounded = new RegExp(`(?<![\\w@/.\\-])${esc2}(?![\\w])`);
26931
- if (!bounded.test(hay)) continue;
26932
- const verMatch = new RegExp(`${esc2}\\s*@?\\s*v?(\\d+(?:\\.\\d+){0,2})\\b`).exec(hay);
26933
- const hasSep = NAME_SEPARATOR.test(key);
26934
- if (!hasSep && !verMatch) continue;
26935
- if (seen.has(node.id)) continue;
26936
- seen.add(node.id);
26937
- out2.push(verMatch ? { node, version: verMatch[1] } : { node });
27021
+ recentEdgeAttrCoverage(marker, attr, limit) {
27022
+ const r = this.db.prepare(
27023
+ `SELECT COUNT(*) AS total,
27024
+ COALESCE(SUM(CASE WHEN json_extract(attrs_json, '$.' || ?) = 1 THEN 1 ELSE 0 END), 0) AS count
27025
+ FROM (SELECT attrs_json FROM edges
27026
+ WHERE valid_to IS NULL AND attrs_json LIKE ?
27027
+ ORDER BY created_at DESC LIMIT ?)`
27028
+ ).get(attr, `%${marker}%`, limit);
27029
+ return { count: Number(r.count), total: Number(r.total) };
26938
27030
  }
26939
- return out2;
26940
- }
26941
- function linkProblemToPackages(store2, problemId, text, ts, index) {
26942
- const p = store2.getNode(problemId);
26943
- if (!p || p.label !== "Problem") return { linked: [] };
26944
- const idx = index ?? buildPackageIndex(store2);
26945
- if (idx.size === 0) return { linked: [] };
26946
- const already = new Set(store2.outEdges(problemId, ["DEPENDS_ON"]).map((e) => e.to));
26947
- const linked = [];
26948
- for (const { node, version: version3 } of matchPackagesInText(text, idx)) {
26949
- if (already.has(node.id)) continue;
26950
- addDependency(store2, {
26951
- fromId: problemId,
26952
- toId: node.id,
26953
- relation: "depends-on",
26954
- evidence: version3 ? `named in problem (@${version3})` : "named in problem statement",
26955
- ts,
26956
- extractionSource: "daemon-extracted"
26957
- });
26958
- already.add(node.id);
26959
- linked.push(node.id);
27031
+ liveNodeIds(ids) {
27032
+ const live = /* @__PURE__ */ new Set();
27033
+ const CHUNK = 900;
27034
+ for (let i2 = 0; i2 < ids.length; i2 += CHUNK) {
27035
+ const slice = ids.slice(i2, i2 + CHUNK);
27036
+ if (slice.length === 0) continue;
27037
+ const rows = this.db.prepare(
27038
+ `SELECT id FROM nodes WHERE valid_to IS NULL AND id IN (${slice.map(() => "?").join(",")})`
27039
+ ).all(...slice);
27040
+ for (const r of rows) live.add(r.id);
27041
+ }
27042
+ return live;
26960
27043
  }
26961
- return { linked };
26962
- }
26963
- function problemText(store2, p) {
26964
- const parts2 = [p.description];
26965
- for (const e of store2.outEdges(p.id, ["CAUSED_BY"])) {
26966
- const c = store2.getNode(e.to);
26967
- if (c) parts2.push(c.description);
27044
+ /**
27045
+ * Live edges WITH their endpoint labels and ids, resolved in ONE join.
27046
+ *
27047
+ * The ontology sweep needs (id, type, fromLabel, toLabel) for every live edge.
27048
+ * Doing that as `scanLiveEdges()` + two `getNode()` calls is 2N node reads —
27049
+ * 550,000 on this store — and each one deserializes the node's embedding blob.
27050
+ * Measured: the sweep did not finish in 10 minutes. As a single join it is one
27051
+ * query over an index-covered scan. Labels only; nothing here touches embeddings.
27052
+ */
27053
+ scanLiveEdgeRows() {
27054
+ const rows = this.db.prepare(
27055
+ `SELECT e.id, e.from_id, e.to_id, e.type, a.label AS from_label, b.label AS to_label
27056
+ FROM edges e
27057
+ LEFT JOIN nodes a ON a.id = e.from_id AND a.valid_to IS NULL
27058
+ LEFT JOIN nodes b ON b.id = e.to_id AND b.valid_to IS NULL
27059
+ WHERE e.valid_to IS NULL`
27060
+ ).all();
27061
+ return rows.map((r) => ({
27062
+ id: r.id,
27063
+ from: r.from_id,
27064
+ to: r.to_id,
27065
+ type: r.type,
27066
+ fromLabel: r.from_label ?? void 0,
27067
+ toLabel: r.to_label ?? void 0
27068
+ }));
26968
27069
  }
26969
- return parts2.join(" \n ");
26970
- }
26971
- function backfillProblemPackageLinks(store2, ts) {
26972
- const report = { problems: 0, edges: 0 };
26973
- const idx = buildPackageIndex(store2);
26974
- if (idx.size === 0) return report;
26975
- for (const p of store2.findNodesByLabel("Problem")) {
26976
- if (p.attrs["resolvedAs"] === "false_positive") continue;
26977
- const { linked } = linkProblemToPackages(store2, p.id, problemText(store2, p), ts, idx);
26978
- if (linked.length > 0) {
26979
- report.problems++;
26980
- report.edges += linked.length;
27070
+ /** Live nodes whose attrs.relPath is one of `relPaths`. Uses the nodes_relpath
27071
+ * expression index so the incremental reindex fetches only the changed files'
27072
+ * symbols instead of scanning every versioned node. */
27073
+ /** Live nodes of `labels` whose description is exactly one of `names`, via
27074
+ * the nodes_live_description index. Chunked under the bound-parameter cap. */
27075
+ liveNodesByDescription(names, labels) {
27076
+ if (names.length === 0 || labels.length === 0) return [];
27077
+ const out2 = [];
27078
+ const labelPh = labels.map(() => "?").join(",");
27079
+ for (let i2 = 0; i2 < names.length; i2 += 400) {
27080
+ const chunk = names.slice(i2, i2 + 400);
27081
+ const rows = this.db.prepare(
27082
+ `SELECT * FROM nodes WHERE valid_to IS NULL AND description IN (${chunk.map(() => "?").join(",")}) AND label IN (${labelPh})`
27083
+ ).all(...chunk, ...labels);
27084
+ for (const r of rows) out2.push(rowToNode(r));
26981
27085
  }
27086
+ return out2;
26982
27087
  }
26983
- return report;
26984
- }
26985
- function langNodeId(name2) {
26986
- return `lang:${name2.trim().toLowerCase()}`;
26987
- }
26988
- function specToPackageName(spec) {
26989
- const s = spec.trim();
26990
- if (!s || s.startsWith(".") || s.startsWith("/") || s.startsWith("node:")) return null;
26991
- const parts2 = s.split("/");
26992
- return (s.startsWith("@") ? parts2.slice(0, 2).join("/") : parts2[0]).toLowerCase();
26993
- }
26994
- function mergeContextEdge(store2, from, to, type, ts) {
26995
- store2.mergeEdge({
26996
- id: `edge_${digest({ from, type, to })}`.slice(0, 24),
26997
- from,
26998
- to,
26999
- type,
27000
- confidence: 0.4,
27001
- extractionSource: "daemon-extracted",
27002
- createdAt: ts,
27003
- lastSeenAt: ts,
27004
- navSuccesses: 0,
27005
- navFailures: 0,
27006
- attrs: { provisional: true, structural: true }
27007
- });
27008
- }
27009
- function deriveContextFromAnchors(store2, problemId, ts, pkgIndex) {
27010
- const report = { languages: [], packages: [] };
27011
- const p = store2.getNode(problemId);
27012
- if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return report;
27013
- const files = store2.outEdges(problemId, ["ANCHORED_AT"]).map((e) => store2.getNode(e.to)).filter((n) => !!n && n.label === "File");
27014
- if (files.length === 0) return report;
27015
- const idx = pkgIndex ?? buildPackageIndex(store2);
27016
- const haveLang = new Set(store2.outEdges(problemId, ["OCCURS_IN"]).map((e) => e.to));
27017
- const havePkg = new Set(store2.outEdges(problemId, ["DEPENDS_ON"]).map((e) => e.to));
27018
- for (const file2 of files) {
27019
- const lang = String(file2.attrs["language"] ?? "").trim();
27020
- if (lang) {
27021
- const langId = langNodeId(lang);
27022
- if (!store2.getNode(langId)) {
27023
- store2.mergeNode({
27024
- id: langId,
27025
- label: "Language",
27026
- description: lang,
27027
- extractionConfidence: 1,
27028
- extractionSource: "daemon-extracted",
27029
- embedding: [],
27030
- cumulativeSurprise: 0,
27031
- peakSurprise: 0,
27032
- cumulativeHits: 0,
27033
- lastUpdatedAt: ts,
27034
- createdAt: ts,
27035
- memoryTier: "short-term",
27036
- pageRank: 0,
27037
- isLandmark: false,
27038
- community: null,
27039
- stability: "stable",
27040
- attrs: { name: lang, workspaceId: file2.attrs["workspaceId"] }
27041
- });
27042
- }
27043
- if (!haveLang.has(langId)) {
27044
- mergeContextEdge(store2, problemId, langId, "OCCURS_IN", ts);
27045
- haveLang.add(langId);
27046
- report.languages.push(langId);
27047
- }
27048
- }
27049
- for (const e of store2.outEdges(file2.id, ["IMPORTS"])) {
27050
- const mod = store2.getNode(e.to);
27051
- if (!mod) continue;
27052
- const name2 = specToPackageName(mod.description);
27053
- if (!name2) continue;
27054
- const pkg = idx.get(name2);
27055
- if (!pkg || havePkg.has(pkg.id)) continue;
27056
- addDependency(store2, {
27057
- fromId: problemId,
27058
- toId: pkg.id,
27059
- relation: "depends-on",
27060
- evidence: "imported by the anchored code",
27061
- ts,
27062
- extractionSource: "daemon-extracted"
27063
- });
27064
- havePkg.add(pkg.id);
27065
- report.packages.push(pkg.id);
27088
+ nodesByRelPath(relPaths) {
27089
+ if (relPaths.length === 0) return [];
27090
+ const placeholders = relPaths.map(() => "?").join(",");
27091
+ const rows = this.db.prepare(
27092
+ `SELECT * FROM nodes WHERE valid_to IS NULL AND json_extract(attrs_json, '$.relPath') IN (${placeholders})`
27093
+ ).all(...relPaths);
27094
+ return rows.map(rowToNode);
27095
+ }
27096
+ findLiveNodesByAttr(label, attrPath, value) {
27097
+ if (!/^[A-Za-z0-9_]+(\.[A-Za-z0-9_]+)*$/.test(attrPath)) throw new Error(`invalid attr path: ${attrPath}`);
27098
+ const rows = this.db.prepare(
27099
+ `SELECT * FROM nodes WHERE json_extract(attrs_json, '$.${attrPath}') = ? AND valid_to IS NULL AND label = ?`
27100
+ ).all(value, label);
27101
+ return rows.map(rowToNode);
27102
+ }
27103
+ // ── Bi-temporal versioning ───────────────────────────────────────────
27104
+ // NOTE: the adapter's transaction() is non-reentrant (raw BEGIN/COMMIT), and
27105
+ // the indexer calls freezeNode inside its own store.transaction(). So this
27106
+ // method does NOT open a transaction — callers needing atomicity wrap it.
27107
+ freezeNode(liveId, t) {
27108
+ const live = this.getNode(liveId);
27109
+ if (!live) return null;
27110
+ const version3 = live.version ?? 1;
27111
+ const frozenId = `${liveId}@v${version3}`;
27112
+ if (this.getNode(frozenId)) {
27113
+ this.stmts.advanceLive.run({ live_id: liveId, t });
27114
+ return frozenId;
27066
27115
  }
27116
+ this.stmts.freezeCopy.run({ frozen_id: frozenId, live_id: liveId, t });
27117
+ this.mergeEdge({
27118
+ id: `edge_superseded_${frozenId}`,
27119
+ from: frozenId,
27120
+ to: liveId,
27121
+ type: "SUPERSEDED_BY",
27122
+ confidence: 1,
27123
+ extractionSource: "daemon-extracted",
27124
+ createdAt: t,
27125
+ lastSeenAt: t,
27126
+ navSuccesses: 0,
27127
+ navFailures: 0,
27128
+ validFrom: t,
27129
+ validTo: null,
27130
+ attrs: {}
27131
+ });
27132
+ this.stmts.advanceLive.run({ live_id: liveId, t });
27133
+ return frozenId;
27067
27134
  }
27068
- return report;
27069
- }
27070
- function backfillProblemContext(store2, ts) {
27071
- const report = { problems: 0, edges: 0 };
27072
- const idx = buildPackageIndex(store2);
27073
- for (const p of store2.findNodesByLabel("Problem")) {
27074
- if (p.attrs["resolvedAs"] === "false_positive") continue;
27075
- const r = deriveContextFromAnchors(store2, p.id, ts, idx);
27076
- const n = r.languages.length + r.packages.length;
27077
- if (n > 0) {
27078
- report.problems++;
27079
- report.edges += n;
27080
- }
27135
+ closeNode(id, t) {
27136
+ this.stmts.closeNode.run({ id, t });
27081
27137
  }
27082
- return report;
27138
+ closeEdge(id, t) {
27139
+ this.stmts.closeEdge.run({ id, t });
27140
+ }
27141
+ nodeHistory(logicalId) {
27142
+ const rows = this.stmts.nodeHistory.all(logicalId);
27143
+ return rows.map(rowToNode);
27144
+ }
27145
+ nodesAsOf(t) {
27146
+ const rows = this.stmts.nodesAsOf.all({ t });
27147
+ return rows.map(rowToNode);
27148
+ }
27149
+ /** `nodesAsOf(t).filter((n) => n.attrs[attr] === true)` with the attr test in
27150
+ * SQL, so only the flagged rows are hydrated. The filter-after form hydrated
27151
+ * the whole graph for a handful of revisit flags: 6.7s per context render on
27152
+ * a 227k-node store (2026-09-24). `attr` is interpolated into a JSON path,
27153
+ * so it must be a plain identifier. */
27154
+ nodesAsOfWhereTrue(t, attr) {
27155
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(attr)) throw new Error(`nodesAsOfWhereTrue: bad attr ${JSON.stringify(attr)}`);
27156
+ const rows = this.db.prepare(
27157
+ `SELECT * FROM nodes WHERE valid_from <= :t AND (valid_to IS NULL OR valid_to > :t)
27158
+ AND json_type(attrs_json, '$.${attr}') = 'true'`
27159
+ ).all({ t });
27160
+ return rows.map(rowToNode);
27161
+ }
27162
+ edgesAsOf(t) {
27163
+ const rows = this.stmts.edgesAsOf.all({ t });
27164
+ return rows.map(rowToEdge);
27165
+ }
27166
+ transaction(fn) {
27167
+ return this.db.transaction(fn);
27168
+ }
27169
+ close() {
27170
+ this.db.close();
27171
+ }
27172
+ };
27173
+ function openGraphStore(opts) {
27174
+ return new SqliteGraphStore(opts);
27083
27175
  }
27084
27176
 
27085
- // ../../packages/local-graph/src/design-problem.ts
27177
+ // ../../packages/local-graph/src/traversal.ts
27086
27178
  init_src2();
27087
-
27088
- // ../../packages/local-graph/src/problem-dedup.ts
27089
- init_src();
27090
- init_src();
27091
- var REDIRECT_EDGES = [
27092
- "CAUSED_BY",
27093
- "SOLVED_BY",
27094
- "FIXED_BY",
27095
- "ANCHORED_AT",
27096
- "MANIFESTED_IN",
27097
- "EVIDENCED_BY"
27179
+ var MAX_EDGE_WEIGHT = Math.max(...Object.values(EDGE_WEIGHT));
27180
+ var CAUSAL_FAMILY = [
27181
+ ...CAUSAL_EDGES,
27182
+ ...RESOLUTION_EDGES
27098
27183
  ];
27099
- function corroborations(n) {
27100
- return Number(n.attrs["corroborations"] ?? 0);
27101
- }
27102
- function overlap(a, b) {
27103
- if (a.size === 0 || b.size === 0) return 0;
27104
- let inter = 0;
27105
- for (const x of a) if (b.has(x)) inter++;
27106
- return inter / Math.min(a.size, b.size);
27107
- }
27108
- function mergeDuplicateProblems(store2, opts) {
27109
- const minOverlap = opts.minOverlap ?? 0.85;
27110
- const minTokens = opts.minTokens ?? 4;
27111
- const report = { clusters: 0, merged: 0 };
27112
- const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
27113
- const tokens = /* @__PURE__ */ new Map();
27114
- for (const p of open) tokens.set(p.id, new Set(retrievalTokens(p.description)));
27115
- const consumed = /* @__PURE__ */ new Set();
27116
- store2.transaction(() => {
27117
- for (let i2 = 0; i2 < open.length; i2++) {
27118
- const a = open[i2];
27119
- if (consumed.has(a.id)) continue;
27120
- const cluster = [a];
27121
- const ta = tokens.get(a.id);
27122
- for (let j = i2 + 1; j < open.length; j++) {
27123
- const b = open[j];
27124
- if (consumed.has(b.id)) continue;
27125
- const tb = tokens.get(b.id);
27126
- if (Math.min(ta.size, tb.size) < minTokens) continue;
27127
- if (isConstraintProblem(a) !== isConstraintProblem(b)) continue;
27128
- if (overlap(ta, tb) >= minOverlap) {
27129
- cluster.push(b);
27130
- consumed.add(b.id);
27131
- }
27132
- }
27133
- if (cluster.length < 2) continue;
27134
- report.clusters++;
27135
- cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
27136
- const survivor = cluster[0];
27137
- for (const dup of cluster.slice(1)) {
27138
- foldDuplicateProblem(store2, dup, survivor, opts.ts);
27139
- report.merged++;
27140
- }
27141
- }
27142
- });
27143
- return report;
27144
- }
27145
- function foldDuplicateProblem(store2, dup, survivor, ts) {
27146
- const surv = store2.getNode(survivor.id);
27147
- if (!surv) return;
27148
- const sources = new Set(surv.attrs["sources"] ?? []);
27149
- for (const s of dup.attrs["sources"] ?? []) sources.add(s);
27150
- const dupSeq = dup.attrs["createdAtSeq"] ?? store2.currentIngestSeq();
27151
- const exposure = priorExposure(store2, survivor.id, dupSeq);
27152
- const expField = `reinforce${exposure[0].toUpperCase()}${exposure.slice(1)}Count`;
27153
- store2.updateNode(survivor.id, {
27154
- attrs: {
27155
- ...surv.attrs,
27156
- sources: [...sources],
27157
- corroborations: corroborations(surv) + corroborations(dup) + 1,
27158
- [expField]: (surv.attrs[expField] ?? 0) + 1,
27159
- lastReinforceExposure: exposure
27160
- },
27161
- cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
27162
- lastUpdatedAt: ts
27163
- });
27164
- for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
27165
- if (e.to === survivor.id) continue;
27166
- const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
27167
- if (store2.getEdge(id)) continue;
27168
- const redirected = {
27169
- ...e,
27170
- id,
27171
- from: survivor.id,
27172
- createdAt: ts,
27173
- lastSeenAt: ts
27174
- };
27175
- store2.mergeEdge(redirected);
27176
- }
27177
- store2.updateNode(dup.id, {
27178
- attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
27179
- lastUpdatedAt: ts
27180
- });
27181
- store2.closeNode(dup.id, ts);
27182
- }
27183
27184
 
27184
- // ../../packages/local-graph/src/design-problem.ts
27185
+ // ../../packages/local-graph/src/justification.ts
27185
27186
  init_src();
27186
- function isConstraintProblem(node) {
27187
- return node.attrs["kind"] === "constraint";
27188
- }
27189
- var RECURRENCE_DEBOUNCE_MS = 60 * 60 * 1e3;
27190
- function resolveFileNode(store2, path, workspaceId) {
27191
- const want = path.trim().replace(/^\.?\//, "");
27192
- for (const n of store2.findNodesByLabel("File")) {
27193
- if (n.attrs["workspaceId"] && n.attrs["workspaceId"] !== workspaceId) continue;
27194
- if (n.attrs["relPath"] === want || n.description === want) return n;
27195
- }
27196
- return null;
27197
- }
27198
- var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
27199
- "Solution",
27200
- "RootCause",
27201
- "Pattern",
27202
- "Technique",
27203
- "AntiPattern"
27204
- ]);
27205
- function isAutoMinted(n) {
27206
- return isEditPlaceholderSolution(n);
27207
- }
27208
- function priorsForFile(store2, relPath) {
27209
- const want = relPath.trim().replace(/^\.?\//, "");
27210
- let file2 = null;
27211
- for (const n of store2.findNodesByLabel("File")) {
27212
- if (n.description === want || n.attrs["relPath"] === want) {
27213
- file2 = n;
27214
- break;
27215
- }
27216
- }
27217
- if (!file2) return null;
27218
- const openProblems = [];
27219
- const constraints = [];
27220
- const resolvedProblems = [];
27221
- const seenProblem = /* @__PURE__ */ new Set();
27222
- const related = /* @__PURE__ */ new Map();
27223
- const anchorEdges = [
27224
- ...store2.inEdges(file2.id, ["ANCHORED_AT"]),
27225
- ...store2.outEdges(file2.id, ["DEFINES"]).flatMap((d) => store2.inEdges(d.to, ["ANCHORED_AT"]))
27226
- ];
27227
- for (const e of anchorEdges) {
27228
- const n = store2.getNode(e.from);
27229
- if (!n) continue;
27230
- if (n.label === "Problem" || n.label === "Vulnerability") {
27231
- if (seenProblem.has(n.id)) continue;
27232
- seenProblem.add(n.id);
27233
- if (!n.attrs["resolvedAt"]) {
27234
- (isConstraintProblem(n) ? constraints : openProblems).push(n);
27235
- } else if (n.attrs["resolvedAs"] == null && n.attrs["mergedInto"] === void 0 && e.attrs?.["anchorProvenance"] !== "legacy") {
27236
- resolvedProblems.push(n);
27237
- }
27238
- } else if (CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) {
27239
- related.set(n.id, n);
27240
- }
27241
- }
27242
- const solutionsByProblem = /* @__PURE__ */ new Map();
27243
- for (const p of [...openProblems, ...constraints, ...resolvedProblems]) {
27244
- for (const e of store2.outEdges(p.id, ["SOLVED_BY", "CAUSED_BY", "INSTANCE_OF"])) {
27245
- const n = store2.getNode(e.to);
27246
- if (n && CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) related.set(n.id, n);
27247
- if (n && n.label === "Solution" && e.type === "SOLVED_BY" && !isAutoMinted(n)) {
27248
- const list = solutionsByProblem.get(p.id) ?? [];
27249
- if (!list.some((s) => s.id === n.id)) list.push(n);
27250
- solutionsByProblem.set(p.id, list);
27251
- }
27252
- }
27253
- }
27254
- if (openProblems.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
27255
- return null;
27256
- const recentFirst = (a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0);
27257
- openProblems.sort(recentFirst);
27258
- constraints.sort(recentFirst);
27259
- resolvedProblems.sort(
27260
- (a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)
27261
- );
27262
- return {
27263
- file: file2,
27264
- openProblems,
27265
- constraints,
27266
- resolvedProblems,
27267
- related: [...related.values()],
27268
- solutionsByProblem
27187
+ init_src2();
27188
+ function addDependency(store2, opts) {
27189
+ const id = digest({ from: opts.fromId, type: "DEPENDS_ON", to: opts.toId });
27190
+ const attrs = { relation: opts.relation };
27191
+ if (opts.introduced !== void 0) attrs.introduced = opts.introduced;
27192
+ if (opts.fixed !== void 0) attrs.fixed = opts.fixed;
27193
+ if (opts.evidence !== void 0) attrs.evidence = opts.evidence;
27194
+ const edge = {
27195
+ id,
27196
+ from: opts.fromId,
27197
+ to: opts.toId,
27198
+ type: "DEPENDS_ON",
27199
+ confidence: 1,
27200
+ extractionSource: opts.extractionSource ?? "agent-observed",
27201
+ createdAt: opts.ts,
27202
+ lastSeenAt: opts.ts,
27203
+ navSuccesses: 0,
27204
+ navFailures: 0,
27205
+ attrs
27269
27206
  };
27207
+ store2.mergeEdge(edge);
27208
+ return edge;
27270
27209
  }
27271
- function anchorProblemToWorkingFile(store2, problemId, relPath, workspaceId, ts, provenance = "edited", opts = {}) {
27272
- const p = store2.getNode(problemId);
27273
- if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
27274
- if (p.label === "Vulnerability" && provenance !== "witnessed") return false;
27275
- if (!isAnchorableCodePath(relPath)) return false;
27276
- const file2 = resolveFileNode(store2, relPath, workspaceId);
27277
- if (!file2) return false;
27278
- const already = new Set(store2.outEdges(problemId, ["ANCHORED_AT"]).map((e) => e.to));
27279
- const confidence = provenance === "witnessed" ? 0.4 : 0.3;
27280
- if (!already.has(file2.id)) {
27281
- store2.mergeEdge({
27282
- id: `edge_${digest({ from: problemId, type: "ANCHORED_AT", to: file2.id })}`.slice(0, 24),
27283
- from: problemId,
27284
- to: file2.id,
27285
- type: "ANCHORED_AT",
27286
- confidence,
27287
- extractionSource: "agent-observed",
27288
- createdAt: ts,
27289
- lastSeenAt: ts,
27290
- navSuccesses: 0,
27291
- navFailures: 0,
27292
- attrs: { provisional: true, captureTime: true, anchorProvenance: provenance }
27293
- });
27210
+ function markRevisit(store2, node, reason, ts, answeredWhen) {
27211
+ const fresh = store2.getNode(node.id) ?? node;
27212
+ if (fresh.validTo != null || fresh.attrs["retiredReason"]) return;
27213
+ store2.updateNode(node.id, {
27214
+ attrs: {
27215
+ ...fresh.attrs,
27216
+ revisit: true,
27217
+ revisitReason: reason,
27218
+ revisitSinceTs: ts,
27219
+ revisitAnsweredWhen: answeredWhen
27220
+ },
27221
+ lastUpdatedAt: ts
27222
+ });
27223
+ }
27224
+ var LEGACY_AUTO_CLOSE_ASK = "auto-closed off a pre-provenance anchor";
27225
+ var KNOWN_CONDITIONS = {
27226
+ parentProblemOpen: true,
27227
+ dependentStale: true
27228
+ };
27229
+ function isRevisitCondition(v) {
27230
+ return typeof v === "string" && Object.prototype.hasOwnProperty.call(KNOWN_CONDITIONS, v);
27231
+ }
27232
+ function conditionOf(node) {
27233
+ const explicit = node.attrs["revisitAnsweredWhen"];
27234
+ if (isRevisitCondition(explicit)) return explicit;
27235
+ if (String(node.attrs["revisitReason"] ?? "").startsWith(LEGACY_AUTO_CLOSE_ASK)) {
27236
+ return "parentProblemOpen";
27294
27237
  }
27295
- if (opts.line !== void 0 && opts.fileText !== void 0) {
27296
- const sym = resolveEnclosingSymbol(store2, file2.id, opts.line, opts.fileText);
27297
- if (sym && !already.has(sym.id)) {
27298
- store2.mergeEdge({
27299
- id: `edge_${digest({ from: problemId, type: "ANCHORED_AT", to: sym.id })}`.slice(0, 24),
27300
- from: problemId,
27301
- to: sym.id,
27302
- type: "ANCHORED_AT",
27303
- confidence,
27304
- extractionSource: "agent-observed",
27305
- createdAt: ts,
27306
- lastSeenAt: ts,
27307
- navSuccesses: 0,
27308
- navFailures: 0,
27309
- attrs: {
27310
- provisional: true,
27311
- captureTime: true,
27312
- anchorProvenance: provenance,
27313
- anchorLine: opts.line,
27314
- enclosingSymbol: true,
27315
- // PJ-anchor-version: WHICH version of the symbol this was witnessed
27316
- // against. `bodyHash` is the indexer's 16-hex sha256 of the body;
27317
- // `headSha` is the checkout's HEAD at capture. Both opaque, both
27318
- // cross the membrane on the anchor edge, and a teammate's reanchor
27319
- // compares the hash with their own symbol to tell "current" from
27320
- // "pre-change" — the branch hazard where a fix witnessed on feat/x
27321
- // would otherwise read as a fact about the unchanged symbol on dev.
27322
- ...typeof sym.attrs["bodyHash"] === "string" ? { symbolContentHash: sym.attrs["bodyHash"] } : {},
27323
- ...opts.headSha ? { observedAtCommit: opts.headSha } : {}
27324
- }
27325
- });
27238
+ return void 0;
27239
+ }
27240
+ function clearAnsweredRevisits(store2, ts) {
27241
+ const report = { cleared: 0, standing: 0 };
27242
+ for (const label of SEMANTIC_NODE_LABELS) {
27243
+ for (const node of store2.findNodesByLabel(label)) {
27244
+ if (node.attrs["revisit"] !== true) continue;
27245
+ const condition = conditionOf(node);
27246
+ if (condition === void 0 || !isRevisitAnswered(store2, node, condition)) {
27247
+ report.standing++;
27248
+ continue;
27249
+ }
27250
+ clearRevisit(store2, node.id, ts);
27251
+ report.cleared++;
27326
27252
  }
27327
27253
  }
27328
- return true;
27254
+ return report;
27329
27255
  }
27330
- var ENCLOSING_SYMBOL_KINDS = /* @__PURE__ */ new Set([
27331
- "Function",
27332
- "Class",
27333
- "Method",
27334
- "Module",
27335
- "Interface",
27336
- "Const",
27337
- "Enum",
27338
- "TypeAlias",
27339
- "Namespace",
27340
- "Property"
27341
- ]);
27342
- var ENCLOSING_WALK_DEPTH = 8;
27343
- function resolveEnclosingSymbol(store2, fileId, line, fileText) {
27344
- if (!Number.isFinite(line) || line < 1) return null;
27345
- const row = Math.floor(line) - 1;
27346
- const lineStarts = [0];
27347
- for (let i2 = 0; i2 < fileText.length; i2++) if (fileText.charCodeAt(i2) === 10) lineStarts.push(i2 + 1);
27348
- const rowOf = (index) => {
27349
- let lo = 0;
27350
- let hi = lineStarts.length - 1;
27351
- while (lo < hi) {
27352
- const mid = lo + hi + 1 >> 1;
27353
- if (lineStarts[mid] <= index) lo = mid;
27354
- else hi = mid - 1;
27256
+ function isRevisitAnswered(store2, node, condition) {
27257
+ switch (condition) {
27258
+ case "parentProblemOpen": {
27259
+ const parents = store2.inEdges(node.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.from)).filter((n) => n !== null && n.validTo == null);
27260
+ return parents.every((p) => p.attrs["resolvedAt"] == null);
27355
27261
  }
27356
- return lo;
27357
- };
27358
- let best = null;
27359
- let bestStart = -1;
27360
- let bestSpan = Number.POSITIVE_INFINITY;
27361
- const seen = /* @__PURE__ */ new Set([fileId]);
27362
- let frontier = [fileId];
27363
- for (let depth = 0; depth < ENCLOSING_WALK_DEPTH && frontier.length > 0; depth++) {
27364
- const next = [];
27365
- for (const id of frontier) {
27366
- for (const e of store2.outEdges(id, ["DEFINES"])) {
27367
- if (seen.has(e.to)) continue;
27368
- seen.add(e.to);
27369
- const n = store2.getNode(e.to);
27370
- if (!n) continue;
27371
- next.push(n.id);
27372
- if (!ENCLOSING_SYMBOL_KINDS.has(n.label)) continue;
27373
- const startRow = n.attrs["startRow"];
27374
- const startByte = n.attrs["startByte"];
27375
- const endByte = n.attrs["endByte"];
27376
- if (typeof startRow !== "number" || typeof startByte !== "number" || typeof endByte !== "number") continue;
27377
- if (startRow < 0 || endByte < startByte) continue;
27378
- const endRow = rowOf(Math.max(startByte, Math.min(endByte, fileText.length) - 1));
27379
- if (row < startRow || row > endRow) continue;
27380
- const span = endByte - startByte;
27381
- if (startRow > bestStart || startRow === bestStart && span < bestSpan) {
27382
- best = n;
27383
- bestStart = startRow;
27384
- bestSpan = span;
27385
- }
27386
- }
27262
+ case "dependentStale": {
27263
+ const since = Number(node.attrs["revisitSinceTs"] ?? 0);
27264
+ return since > 0 && node.lastUpdatedAt > since;
27387
27265
  }
27388
- frontier = next;
27389
27266
  }
27390
- return best;
27391
- }
27392
- function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
27393
- const p = store2.getNode(problemId);
27394
- if (!p || p.label !== "Problem") return false;
27395
- if (p.attrs["anchorHint"]) return false;
27396
- store2.updateNode(problemId, {
27397
- attrs: { ...p.attrs, anchorHint: { path: relPath, provenance, ts } }
27398
- });
27399
- return true;
27400
27267
  }
27401
- function openAnchorHint(p) {
27402
- const h = p.attrs["anchorHint"];
27403
- if (!h || typeof h.path !== "string" || h.upgradedAt) return null;
27404
- return { path: h.path, ts: typeof h.ts === "number" ? h.ts : 0 };
27268
+ function clearRevisit(store2, nodeId, ts) {
27269
+ const n = store2.getNode(nodeId);
27270
+ if (!n) return;
27271
+ const attrs = { ...n.attrs };
27272
+ delete attrs["revisit"];
27273
+ delete attrs["revisitReason"];
27274
+ delete attrs["revisitSinceTs"];
27275
+ store2.updateNode(nodeId, { attrs, lastUpdatedAt: ts });
27405
27276
  }
27406
- function consumeAnchorHint(store2, p, ts) {
27407
- const hint = p.attrs["anchorHint"];
27408
- store2.updateNode(p.id, { attrs: { ...p.attrs, anchorHint: { ...hint, upgradedAt: ts } } });
27409
- deriveContextFromAnchors(store2, p.id, ts);
27277
+ function listNeedsRevisit(store2, asOf) {
27278
+ return store2.nodesAsOfWhereTrue(asOf, "revisit").filter((n) => !n.attrs["retiredReason"]).map((n) => ({
27279
+ id: n.id,
27280
+ label: n.label,
27281
+ description: n.description,
27282
+ reason: String(n.attrs["revisitReason"] ?? "")
27283
+ }));
27410
27284
  }
27411
- function upgradeAnchorHints(store2, workspaceId, ts) {
27412
- let upgraded = 0;
27413
- let standing = 0;
27414
- let unresolvable = 0;
27415
- let blocked = 0;
27416
- const filesByPath = /* @__PURE__ */ new Map();
27417
- for (const f of store2.findNodesByLabel("File")) {
27418
- if (f.attrs["workspaceId"] && f.attrs["workspaceId"] !== workspaceId) continue;
27419
- const rel = typeof f.attrs["relPath"] === "string" ? f.attrs["relPath"] : f.description;
27420
- if (rel && !filesByPath.has(rel)) filesByPath.set(rel, f);
27421
- }
27422
- for (const p of store2.findNodesByLabel("Problem")) {
27423
- if (p.validTo != null || p.attrs["resolvedAt"]) continue;
27424
- const hint = openAnchorHint(p);
27425
- if (!hint) continue;
27426
- const file2 = filesByPath.get(hint.path.trim().replace(/^\.?\//, ""));
27427
- if (!file2 || !isAnchorableCodePath(hint.path)) {
27428
- unresolvable++;
27429
- continue;
27430
- }
27431
- standing++;
27432
- const changedAt = Number(file2.attrs["contentChangedAt"] ?? 0);
27433
- if (changedAt <= hint.ts) continue;
27434
- if (anchorProblemToWorkingFile(store2, p.id, hint.path, workspaceId, ts, "hint-confirmed")) {
27435
- consumeAnchorHint(store2, p, ts);
27436
- upgraded++;
27437
- standing--;
27438
- } else {
27439
- blocked++;
27440
- }
27285
+
27286
+ // ../../packages/local-graph/src/credit.ts
27287
+ init_src2();
27288
+ var CREDIT_FAMILY = [
27289
+ ...CAUSAL_EDGES,
27290
+ ...RESOLUTION_EDGES,
27291
+ ...JUSTIFICATION_EDGES
27292
+ ];
27293
+
27294
+ // ../../packages/local-graph/src/claims.ts
27295
+ init_src();
27296
+ init_src();
27297
+
27298
+ // ../../packages/local-graph/src/crystallize.ts
27299
+ var LONG_TERM_SUPPORT = 2;
27300
+ var PERSISTENT_SUPPORT = 4;
27301
+ var PERSISTENT_CONFIDENCE = 0.85;
27302
+ var SCOPE_AXES = ["lang", "versionRange", "codebase", "regime"];
27303
+ function splittingAxis(a, b) {
27304
+ for (const ax of SCOPE_AXES) {
27305
+ if (a[ax] !== void 0 && b[ax] !== void 0 && a[ax] !== b[ax]) return ax;
27441
27306
  }
27442
- return { upgraded, standing, unresolvable, blocked };
27307
+ return null;
27308
+ }
27309
+ function support(n) {
27310
+ return n.attrs["groundedSupport"] ?? 0;
27443
27311
  }
27444
- function reopenAutoClosedProblems(store2, t) {
27445
- const report = { reopened: 0, alreadySuspect: 0, agentDescribed: 0 };
27446
- for (const p of store2.findNodesByLabel("Problem")) {
27447
- if (p.attrs["resolvedAt"] == null) continue;
27448
- if (p.attrs["resolvedAs"] != null) continue;
27449
- if (p.attrs["mergedInto"] !== void 0) continue;
27450
- if (p.attrs["resolutionWitnessed"] === true) {
27451
- report.agentDescribed++;
27452
- continue;
27453
- }
27454
- const sols = store2.outEdges(p.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.to)).filter((n) => n !== null);
27455
- if (sols.length === 0 && p.attrs["retiredEditPlaceholder"] !== true) continue;
27456
- if (!sols.every((s) => String(s.description ?? "").startsWith(AUTO_MINT_PREFIX))) {
27457
- report.agentDescribed++;
27458
- continue;
27312
+ function sourcesCount(n) {
27313
+ const s = n.attrs["sources"];
27314
+ return Array.isArray(s) ? s.length : 0;
27315
+ }
27316
+ function crystallize(store2, opts = { ts: 0 }) {
27317
+ const claims = store2.findNodesByLabel("Claim");
27318
+ const result = {
27319
+ evaluated: claims.length,
27320
+ promoted: [],
27321
+ derived: [],
27322
+ plural: [],
27323
+ discriminators: [],
27324
+ contested: []
27325
+ };
27326
+ for (const c of claims) {
27327
+ const contradictions = store2.outEdges(c.id, ["CONTRADICTS"]).concat(store2.inEdges(c.id, ["CONTRADICTS"])).length;
27328
+ const s = support(c) + Math.max(0, sourcesCount(c) - 1);
27329
+ const f = contradictions;
27330
+ const confidence = (1 + s) / (2 + s + f);
27331
+ store2.updateNode(c.id, {
27332
+ attrs: { ...c.attrs, confidence },
27333
+ lastUpdatedAt: opts.ts
27334
+ });
27335
+ const sup = support(c);
27336
+ let tier = c.memoryTier;
27337
+ if (sup >= PERSISTENT_SUPPORT && confidence >= PERSISTENT_CONFIDENCE) tier = "persistent";
27338
+ else if (sup >= LONG_TERM_SUPPORT) tier = "long-term";
27339
+ if (tier !== c.memoryTier) {
27340
+ store2.updateNode(c.id, { attrs: { ...store2.getNode(c.id).attrs }, memoryTier: tier, lastUpdatedAt: opts.ts });
27341
+ result.promoted.push(c.id);
27459
27342
  }
27460
- if (p.attrs["resolutionSuspect"] === true) report.alreadySuspect++;
27461
- const attrs = { ...p.attrs };
27462
- delete attrs["resolvedAt"];
27463
- attrs["reopenedFrom"] = "auto-close";
27464
- attrs["reopenedAt"] = t;
27465
- store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
27466
- report.reopened++;
27467
27343
  }
27468
- return report;
27469
- }
27470
- function markFixCandidates(store2, t) {
27471
- let resolved = 0;
27472
- for (const p of store2.findNodesByLabel("Problem")) {
27473
- if (!p.id.startsWith("dprob_")) continue;
27474
- if (p.attrs["resolvedAt"]) continue;
27475
- if (isConstraintProblem(p)) continue;
27476
- let symName = "";
27477
- let symRelPath;
27478
- let edited = false;
27479
- const since = Math.max(p.createdAt, Number(p.attrs["fixCandidateClearedAt"] ?? 0));
27480
- for (const e of store2.outEdges(p.id, ["ANCHORED_AT"])) {
27481
- if (e.attrs?.["mention"] === true) continue;
27482
- if (e.attrs?.["anchorProvenance"] === "legacy") continue;
27483
- const sym = store2.getNode(e.to);
27484
- const changedAt = sym?.label === "File" ? Number(sym.attrs["contentChangedAt"] ?? 0) : sym?.lastUpdatedAt ?? 0;
27485
- if (sym && changedAt > since) {
27486
- edited = true;
27487
- symName = sym.description;
27488
- symRelPath = sym.attrs["relPath"] ?? void 0;
27489
- break;
27344
+ const seenPair = /* @__PURE__ */ new Set();
27345
+ for (const c of claims) {
27346
+ for (const e of store2.outEdges(c.id, ["CONTRADICTS"])) {
27347
+ const key = [e.from, e.to].sort().join("|");
27348
+ if (seenPair.has(key)) continue;
27349
+ seenPair.add(key);
27350
+ const a = store2.getNode(e.from);
27351
+ const b = store2.getNode(e.to);
27352
+ if (!a || !b) continue;
27353
+ const kind = mergeTruthKind(
27354
+ a.attrs["truthKind"],
27355
+ b.attrs["truthKind"]
27356
+ );
27357
+ if (kind === "objective") {
27358
+ markBoth(store2, a, b, { needsDiscriminator: true }, opts.ts);
27359
+ result.discriminators.push(a.id, b.id);
27360
+ continue;
27490
27361
  }
27491
- }
27492
- if (!edited) continue;
27493
- if (p.attrs["fixCandidateAt"] !== void 0) continue;
27494
- if (store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
27495
- const s = store2.getNode(e.to);
27496
- return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
27497
- }))
27498
- continue;
27499
- store2.updateNode(p.id, {
27500
- attrs: {
27501
- ...p.attrs,
27502
- // The cue, kept as structured data so the render can name the file it is
27503
- // asking about. Deliberately NOT `resolvedAt` — this is a question.
27504
- fixCandidateAt: t,
27505
- fixCandidateSymbol: symName,
27506
- ...symRelPath ? { fixCandidateFile: symRelPath } : {},
27507
- // Snapshot the render-ledger counter so "shown since THIS ask" is
27508
- // measurable. `shownCount` is the node's LIFETIME count across every
27509
- // band, so comparing it raw would retire a fresh ask on a
27510
- // frequently-surfaced problem before anyone had seen the question once.
27511
- fixCandidateShownBase: Number(p.attrs["shownCount"] ?? 0)
27362
+ if (kind === "taste") {
27363
+ markBoth(store2, a, b, { crystallized: "plural" }, opts.ts);
27364
+ result.plural.push(a.id, b.id);
27365
+ continue;
27366
+ }
27367
+ const axis = splittingAxis(
27368
+ a.attrs["scope"] ?? {},
27369
+ b.attrs["scope"] ?? {}
27370
+ );
27371
+ if (axis && support(a) > 0 && support(b) > 0) {
27372
+ markBoth(store2, a, b, { crystallized: "derived", splitAxis: axis }, opts.ts);
27373
+ result.derived.push(a.id, b.id);
27374
+ } else if (axis) {
27375
+ markBoth(store2, a, b, { crystallized: "hypothesis" }, opts.ts);
27376
+ } else {
27377
+ markBoth(
27378
+ store2,
27379
+ a,
27380
+ b,
27381
+ {
27382
+ revisit: true,
27383
+ revisitReason: "contested: contradictory contextual claim at the same scope, no splitting axis",
27384
+ revisitSinceTs: opts.ts
27385
+ },
27386
+ opts.ts
27387
+ );
27388
+ result.contested.push(a.id, b.id);
27512
27389
  }
27513
- // No `lastUpdatedAt`: raising a question is bookkeeping, not a new
27514
- // observation of the problem, and the passive block's recent-problems band
27515
- // sorts on that field (see clearSettledFixCandidates).
27516
- });
27517
- resolved++;
27518
- }
27519
- return resolved;
27520
- }
27521
- var FIX_CANDIDATE_ASK_LIMIT = 5;
27522
- function clearSettledFixCandidates(store2, t) {
27523
- const report = { answered: 0, ignored: 0, unsubstantiated: 0, standing: 0 };
27524
- for (const p of store2.findNodesByLabel("Problem")) {
27525
- if (p.attrs["fixCandidateAt"] === void 0) continue;
27526
- 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");
27527
- const unsubstantiated = fileAnchors.length > 0 && fileAnchors.every((f) => f.attrs["contentChangedAt"] === void 0);
27528
- const answered = p.attrs["resolvedAt"] != null || store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
27529
- const s = store2.getNode(e.to);
27530
- return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
27531
- });
27532
- const base = Number(
27533
- p.attrs["fixCandidateShownBase"] ?? p.attrs["shownCount"] ?? 0
27534
- );
27535
- const shownSinceAsk = Number(p.attrs["shownCount"] ?? 0) - base;
27536
- const ignored = shownSinceAsk >= FIX_CANDIDATE_ASK_LIMIT;
27537
- if (!answered && !ignored && !unsubstantiated) {
27538
- report.standing++;
27539
- continue;
27540
27390
  }
27541
- const attrs = { ...p.attrs };
27542
- delete attrs["fixCandidateAt"];
27543
- delete attrs["fixCandidateSymbol"];
27544
- delete attrs["fixCandidateFile"];
27545
- attrs["fixCandidateClearedAt"] = t;
27546
- store2.updateNode(p.id, { attrs });
27547
- if (answered) report.answered++;
27548
- else if (ignored) report.ignored++;
27549
- else report.unsubstantiated++;
27550
27391
  }
27551
- return report;
27392
+ return result;
27552
27393
  }
27553
- function priorExposure(store2, nodeId, atSeq) {
27554
- const n = store2.getNode(nodeId);
27555
- if (!n || !n.attrs) return "unshown";
27556
- const shownSeq = n.attrs["lastShownAtSeq"];
27557
- if (shownSeq !== void 0 && shownSeq <= atSeq) return "shown";
27558
- const evictedSeq = n.attrs["lastEvictedAtSeq"];
27559
- if (evictedSeq !== void 0 && evictedSeq <= atSeq) return "evicted";
27560
- if ((n.attrs["evictedCount"] ?? 0) > 0) return "evicted";
27561
- return "unshown";
27394
+ function mergeTruthKind(a, b) {
27395
+ if (a === "objective" || b === "objective") return "objective";
27396
+ if (a === "contextual" || b === "contextual") return "contextual";
27397
+ return "taste";
27398
+ }
27399
+ function markBoth(store2, a, b, patch, ts) {
27400
+ for (const n of [a, b]) {
27401
+ const fresh = store2.getNode(n.id);
27402
+ store2.updateNode(n.id, { attrs: { ...fresh.attrs, ...patch }, lastUpdatedAt: ts });
27403
+ }
27562
27404
  }
27563
27405
 
27564
- // ../../packages/local-graph/src/intent.ts
27406
+ // ../../packages/local-graph/src/design-problem.ts
27407
+ init_src();
27565
27408
  init_src();
27566
27409
 
27567
- // ../../packages/local-graph/src/anchor-backfill.ts
27410
+ // ../../packages/local-graph/src/problem-package-link.ts
27568
27411
  init_src();
27569
- init_src2();
27570
- function isLegacyAnchor(attrs) {
27571
- return attrs?.["captureTime"] === true && attrs["anchorProvenance"] === void 0;
27412
+ var NAME_SEPARATOR = /[@/\-._]/;
27413
+ var MIN_NAME_LEN = 3;
27414
+ function buildPackageIndex(store2) {
27415
+ const idx = /* @__PURE__ */ new Map();
27416
+ for (const n of store2.findNodesByLabel("Package")) {
27417
+ const name2 = String(n.attrs["name"] ?? "").trim();
27418
+ if (!name2) continue;
27419
+ const key = name2.toLowerCase();
27420
+ const prev = idx.get(key);
27421
+ if (!prev || n.attrs["resolved"] === true && prev.attrs["resolved"] !== true) {
27422
+ idx.set(key, n);
27423
+ }
27424
+ }
27425
+ return idx;
27426
+ }
27427
+ function escapeRe2(s) {
27428
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27429
+ }
27430
+ function matchPackagesInText(text, index) {
27431
+ const hay = text.toLowerCase();
27432
+ const out2 = [];
27433
+ const seen = /* @__PURE__ */ new Set();
27434
+ for (const [key, node] of index) {
27435
+ if (key.length < MIN_NAME_LEN) continue;
27436
+ if (!hay.includes(key)) continue;
27437
+ const esc2 = escapeRe2(key);
27438
+ const bounded = new RegExp(`(?<![\\w@/.\\-])${esc2}(?![\\w])`);
27439
+ if (!bounded.test(hay)) continue;
27440
+ const verMatch = new RegExp(`${esc2}\\s*@?\\s*v?(\\d+(?:\\.\\d+){0,2})\\b`).exec(hay);
27441
+ const hasSep = NAME_SEPARATOR.test(key);
27442
+ if (!hasSep && !verMatch) continue;
27443
+ if (seen.has(node.id)) continue;
27444
+ seen.add(node.id);
27445
+ out2.push(verMatch ? { node, version: verMatch[1] } : { node });
27446
+ }
27447
+ return out2;
27448
+ }
27449
+ function linkProblemToPackages(store2, problemId, text, ts, index) {
27450
+ const p = store2.getNode(problemId);
27451
+ if (!p || p.label !== "Problem") return { linked: [] };
27452
+ const idx = index ?? buildPackageIndex(store2);
27453
+ if (idx.size === 0) return { linked: [] };
27454
+ const already = new Set(store2.outEdges(problemId, ["DEPENDS_ON"]).map((e) => e.to));
27455
+ const linked = [];
27456
+ for (const { node, version: version3 } of matchPackagesInText(text, idx)) {
27457
+ if (already.has(node.id)) continue;
27458
+ addDependency(store2, {
27459
+ fromId: problemId,
27460
+ toId: node.id,
27461
+ relation: "depends-on",
27462
+ evidence: version3 ? `named in problem (@${version3})` : "named in problem statement",
27463
+ ts,
27464
+ extractionSource: "daemon-extracted"
27465
+ });
27466
+ already.add(node.id);
27467
+ linked.push(node.id);
27468
+ }
27469
+ return { linked };
27572
27470
  }
27573
- var STATEMENT_PATH_RE = new RegExp(
27574
- String.raw`(?:[\w.+-]+\/)+[\w.+-]+\.(?:${anchorableExtensionAlternation()})`,
27575
- "g"
27576
- );
27577
- function filePathResolver(store2) {
27578
- const byPath = /* @__PURE__ */ new Map();
27579
- for (const f of store2.findNodesByLabel("File")) {
27580
- const path = String(f.attrs["relPath"] ?? f.description ?? "");
27581
- if (path) byPath.set(path, { id: f.id, path });
27471
+ function problemText(store2, p) {
27472
+ const parts2 = [p.description];
27473
+ for (const e of store2.outEdges(p.id, ["CAUSED_BY"])) {
27474
+ const c = store2.getNode(e.to);
27475
+ if (c) parts2.push(c.description);
27582
27476
  }
27583
- return (named) => {
27584
- const exact = byPath.get(named);
27585
- if (exact) return exact;
27586
- const hits = [...byPath.values()].filter(
27587
- (f) => f.path.endsWith(`/${named}`) || named.endsWith(`/${f.path}`)
27588
- );
27589
- return hits.length === 1 ? hits[0] : null;
27590
- };
27477
+ return parts2.join(" \n ");
27591
27478
  }
27592
- function statementTarget(statement, resolvePath) {
27593
- for (const m of statement.matchAll(STATEMENT_PATH_RE)) {
27594
- const f = resolvePath(m[0]);
27595
- if (f) return f;
27479
+ function backfillProblemPackageLinks(store2, ts) {
27480
+ const report = { problems: 0, edges: 0 };
27481
+ const idx = buildPackageIndex(store2);
27482
+ if (idx.size === 0) return report;
27483
+ for (const p of store2.findNodesByLabel("Problem")) {
27484
+ if (p.attrs["resolvedAs"] === "false_positive") continue;
27485
+ const { linked } = linkProblemToPackages(store2, p.id, problemText(store2, p), ts, idx);
27486
+ if (linked.length > 0) {
27487
+ report.problems++;
27488
+ report.edges += linked.length;
27489
+ }
27596
27490
  }
27597
- return null;
27491
+ return report;
27598
27492
  }
27599
- function restatedAnchor(problemId, fileId, restatedFrom, ts) {
27600
- return {
27601
- id: `edge_${digest({ from: problemId, type: "ANCHORED_AT", to: fileId })}`.slice(0, 24),
27602
- from: problemId,
27603
- to: fileId,
27604
- type: "ANCHORED_AT",
27605
- // Same confidence a capture-time anchor enters at: the source is the
27606
- // agent's own statement either way, only the moment of reading differs.
27493
+ function langNodeId(name2) {
27494
+ return `lang:${name2.trim().toLowerCase()}`;
27495
+ }
27496
+ function specToPackageName(spec) {
27497
+ const s = spec.trim();
27498
+ if (!s || s.startsWith(".") || s.startsWith("/") || s.startsWith("node:")) return null;
27499
+ const parts2 = s.split("/");
27500
+ return (s.startsWith("@") ? parts2.slice(0, 2).join("/") : parts2[0]).toLowerCase();
27501
+ }
27502
+ function mergeContextEdge(store2, from, to, type, ts) {
27503
+ store2.mergeEdge({
27504
+ id: `edge_${digest({ from, type, to })}`.slice(0, 24),
27505
+ from,
27506
+ to,
27507
+ type,
27607
27508
  confidence: 0.4,
27608
- extractionSource: "agent-observed",
27509
+ extractionSource: "daemon-extracted",
27609
27510
  createdAt: ts,
27610
27511
  lastSeenAt: ts,
27611
27512
  navSuccesses: 0,
27612
27513
  navFailures: 0,
27613
- attrs: { anchorProvenance: "restated", restatedFrom, restatedAt: ts }
27614
- };
27514
+ attrs: { provisional: true, structural: true }
27515
+ });
27615
27516
  }
27616
- function repairLegacyAnchorsFromStatement(store2, ts) {
27617
- const report = {
27618
- repaired: [],
27619
- retiredGuesses: 0,
27620
- noPathNamed: 0,
27621
- pathUnknown: 0,
27622
- alreadyCorrect: 0
27623
- };
27624
- const resolvePath = filePathResolver(store2);
27625
- for (const p of store2.findNodesByLabel("Problem")) {
27626
- try {
27627
- const anchors = store2.outEdges(p.id, ["ANCHORED_AT"]);
27628
- const legacy = anchors.filter((e) => e.attrs?.["anchorProvenance"] === "legacy");
27629
- if (legacy.length === 0) continue;
27630
- const better = anchors.filter((e) => {
27631
- const prov = e.attrs?.["anchorProvenance"];
27632
- return prov === "restated" || prov === "witnessed" || prov === "edited";
27633
- });
27634
- if (better.length > 0) {
27635
- const betterTargets = new Set(better.map((e) => e.to));
27636
- let retired = 0;
27637
- for (const e of legacy) {
27638
- if (!betterTargets.has(e.to)) {
27639
- store2.closeEdge(e.id, ts);
27640
- retired++;
27641
- }
27642
- }
27643
- report.retiredGuesses += retired;
27644
- continue;
27645
- }
27646
- const named = [...String(p.description ?? "").matchAll(STATEMENT_PATH_RE)].map((m) => m[0]);
27647
- if (named.length === 0) {
27648
- report.noPathNamed++;
27649
- continue;
27650
- }
27651
- const guessedPaths = legacy.map((e) => {
27652
- const f = store2.getNode(e.to);
27653
- return String(f?.attrs["relPath"] ?? f?.description ?? "");
27654
- });
27655
- if (named.some((x) => guessedPaths.some((g) => g.endsWith(x) || x.endsWith(g)))) {
27656
- report.alreadyCorrect++;
27657
- continue;
27658
- }
27659
- const target = named.map(resolvePath).find((f) => f !== null);
27660
- if (!target) {
27661
- report.pathUnknown++;
27662
- continue;
27517
+ function deriveContextFromAnchors(store2, problemId, ts, pkgIndex) {
27518
+ const report = { languages: [], packages: [] };
27519
+ const p = store2.getNode(problemId);
27520
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return report;
27521
+ const files = store2.outEdges(problemId, ["ANCHORED_AT"]).map((e) => store2.getNode(e.to)).filter((n) => !!n && n.label === "File");
27522
+ if (files.length === 0) return report;
27523
+ const idx = pkgIndex ?? buildPackageIndex(store2);
27524
+ const haveLang = new Set(store2.outEdges(problemId, ["OCCURS_IN"]).map((e) => e.to));
27525
+ const havePkg = new Set(store2.outEdges(problemId, ["DEPENDS_ON"]).map((e) => e.to));
27526
+ for (const file2 of files) {
27527
+ const lang = String(file2.attrs["language"] ?? "").trim();
27528
+ if (lang) {
27529
+ const langId = langNodeId(lang);
27530
+ if (!store2.getNode(langId)) {
27531
+ store2.mergeNode({
27532
+ id: langId,
27533
+ label: "Language",
27534
+ description: lang,
27535
+ extractionConfidence: 1,
27536
+ extractionSource: "daemon-extracted",
27537
+ embedding: [],
27538
+ cumulativeSurprise: 0,
27539
+ peakSurprise: 0,
27540
+ cumulativeHits: 0,
27541
+ lastUpdatedAt: ts,
27542
+ createdAt: ts,
27543
+ memoryTier: "short-term",
27544
+ pageRank: 0,
27545
+ isLandmark: false,
27546
+ community: null,
27547
+ stability: "stable",
27548
+ attrs: { name: lang, workspaceId: file2.attrs["workspaceId"] }
27549
+ });
27663
27550
  }
27664
- store2.mergeEdge(restatedAnchor(p.id, target.id, guessedPaths[0] ?? "", ts));
27665
- for (const e of legacy) {
27666
- if (e.to !== target.id) store2.closeEdge(e.id, ts);
27551
+ if (!haveLang.has(langId)) {
27552
+ mergeContextEdge(store2, problemId, langId, "OCCURS_IN", ts);
27553
+ haveLang.add(langId);
27554
+ report.languages.push(langId);
27667
27555
  }
27668
- report.retiredGuesses += legacy.filter((e) => e.to !== target.id).length;
27669
- report.repaired.push({
27670
- problemId: p.id,
27671
- problem: p.description,
27672
- guessed: guessedPaths[0] ?? "",
27673
- restated: target.path
27674
- });
27675
- } catch {
27676
27556
  }
27677
- }
27678
- return report;
27679
- }
27680
- function backfillLegacyAnchors(store2, ts) {
27681
- const report = {
27682
- demotedEdges: 0,
27683
- demotedProblems: 0,
27684
- restatedProblems: 0,
27685
- labeledEdges: 0,
27686
- suspects: []
27687
- };
27688
- let resolvePath;
27689
- for (const p of store2.findNodesByLabel("Problem")) {
27690
- const resolved = p.attrs["resolvedAt"] !== void 0;
27691
- const legacy = store2.outEdges(p.id, ["ANCHORED_AT"]).filter((e) => isLegacyAnchor(e.attrs) || !resolved && e.attrs?.["anchorProvenance"] === "legacy");
27692
- if (legacy.length === 0) continue;
27693
- try {
27694
- if (!resolved) {
27695
- resolvePath ??= filePathResolver(store2);
27696
- const target = statementTarget(String(p.description ?? ""), resolvePath);
27697
- if (target) {
27698
- const guessed = store2.getNode(legacy[0].to);
27699
- store2.mergeEdge(
27700
- restatedAnchor(p.id, target.id, String(guessed?.attrs["relPath"] ?? guessed?.description ?? ""), ts)
27701
- );
27702
- report.restatedProblems++;
27703
- }
27704
- const retired = legacy.filter((e) => !(target && e.to === target.id));
27705
- for (const e of retired) {
27706
- const file2 = store2.getNode(e.to);
27707
- const path = String(file2?.attrs["relPath"] ?? file2?.description ?? "");
27708
- if (path) recordAnchorHint(store2, p.id, path, "legacy", ts);
27709
- store2.closeEdge(e.id, ts);
27710
- report.demotedEdges++;
27711
- }
27712
- if (retired.length > 0) report.demotedProblems++;
27713
- continue;
27714
- }
27715
- for (const e of legacy) {
27716
- store2.mergeEdge({ ...e, attrs: { ...e.attrs, anchorProvenance: "legacy" }, lastSeenAt: ts });
27717
- report.labeledEdges++;
27718
- }
27719
- for (const se of store2.outEdges(p.id, ["SOLVED_BY"])) {
27720
- const sol = store2.getNode(se.to);
27721
- if (!sol || !sol.description.startsWith(AUTO_MINT_PREFIX)) continue;
27722
- const guessedAnchor = sol.description.slice(AUTO_MINT_PREFIX.length);
27723
- store2.updateNode(sol.id, {
27724
- attrs: { ...sol.attrs, provisional: true, resolutionSuspect: true },
27725
- lastUpdatedAt: ts
27726
- });
27727
- markRevisit(
27728
- store2,
27729
- sol,
27730
- `auto-closed off a pre-provenance anchor (guessed ${guessedAnchor}) \u2014 confirm the problem is really fixed, or reopen it`,
27731
- ts,
27732
- // "or reopen it" is half the ask, and the nightly reopen takes that
27733
- // branch — so record what would answer this, or the flag can never be
27734
- // lowered and the band fills with settled questions.
27735
- "parentProblemOpen"
27736
- );
27737
- store2.updateNode(p.id, {
27738
- attrs: { ...p.attrs, resolutionSuspect: true },
27739
- lastUpdatedAt: ts
27740
- });
27741
- report.suspects.push({
27742
- problemId: p.id,
27743
- problem: p.description,
27744
- solutionId: sol.id,
27745
- guessedAnchor
27746
- });
27747
- }
27748
- } catch {
27557
+ for (const e of store2.outEdges(file2.id, ["IMPORTS"])) {
27558
+ const mod = store2.getNode(e.to);
27559
+ if (!mod) continue;
27560
+ const name2 = specToPackageName(mod.description);
27561
+ if (!name2) continue;
27562
+ const pkg = idx.get(name2);
27563
+ if (!pkg || havePkg.has(pkg.id)) continue;
27564
+ addDependency(store2, {
27565
+ fromId: problemId,
27566
+ toId: pkg.id,
27567
+ relation: "depends-on",
27568
+ evidence: "imported by the anchored code",
27569
+ ts,
27570
+ extractionSource: "daemon-extracted"
27571
+ });
27572
+ havePkg.add(pkg.id);
27573
+ report.packages.push(pkg.id);
27574
+ }
27575
+ }
27576
+ return report;
27577
+ }
27578
+ function backfillProblemContext(store2, ts) {
27579
+ const report = { problems: 0, edges: 0 };
27580
+ const idx = buildPackageIndex(store2);
27581
+ for (const p of store2.findNodesByLabel("Problem")) {
27582
+ if (p.attrs["resolvedAs"] === "false_positive") continue;
27583
+ const r = deriveContextFromAnchors(store2, p.id, ts, idx);
27584
+ const n = r.languages.length + r.packages.length;
27585
+ if (n > 0) {
27586
+ report.problems++;
27587
+ report.edges += n;
27749
27588
  }
27750
27589
  }
27751
27590
  return report;
27752
27591
  }
27753
27592
 
27754
- // ../../packages/local-graph/src/percolate.ts
27593
+ // ../../packages/local-graph/src/design-problem.ts
27755
27594
  init_src2();
27756
- init_src();
27757
27595
 
27758
- // ../../packages/local-graph/src/abstraction.ts
27596
+ // ../../packages/local-graph/src/problem-dedup.ts
27759
27597
  init_src();
27760
- init_src2();
27761
-
27762
- // ../../packages/math/src/pagerank.ts
27763
- function pagerank(input) {
27764
- const damping = input.damping ?? 0.85;
27765
- const tol = input.tolerance ?? 1e-6;
27766
- const maxIter = input.maxIterations ?? 100;
27767
- const ids = input.nodeIds;
27768
- const n = ids.length;
27769
- if (n === 0) {
27770
- return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true, danglingRank: 0 };
27771
- }
27772
- const index = /* @__PURE__ */ new Map();
27773
- for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
27774
- const outSum = new Float64Array(n);
27775
- const dangling = new Uint8Array(n).fill(1);
27776
- const rowLen = new Int32Array(n);
27777
- let edgeCount = 0;
27778
- for (const [from, edges] of input.out) {
27779
- const fi = index.get(from);
27780
- if (fi === void 0) continue;
27781
- let sum = 0;
27782
- let inSet = 0;
27783
- for (const e of edges) {
27784
- sum += e.weight;
27785
- if (index.has(e.to)) inSet++;
27786
- }
27787
- if (sum > 0) {
27788
- outSum[fi] = sum;
27789
- dangling[fi] = 0;
27598
+ init_src();
27599
+ var REDIRECT_EDGES = [
27600
+ "CAUSED_BY",
27601
+ "SOLVED_BY",
27602
+ "FIXED_BY",
27603
+ "ANCHORED_AT",
27604
+ "MANIFESTED_IN",
27605
+ "EVIDENCED_BY"
27606
+ ];
27607
+ function corroborations(n) {
27608
+ return Number(n.attrs["corroborations"] ?? 0);
27609
+ }
27610
+ function overlap(a, b) {
27611
+ if (a.size === 0 || b.size === 0) return 0;
27612
+ let inter = 0;
27613
+ for (const x of a) if (b.has(x)) inter++;
27614
+ return inter / Math.min(a.size, b.size);
27615
+ }
27616
+ function mergeDuplicateProblems(store2, opts) {
27617
+ const minOverlap = opts.minOverlap ?? 0.85;
27618
+ const minTokens = opts.minTokens ?? 4;
27619
+ const report = { clusters: 0, merged: 0 };
27620
+ const open = store2.findNodesByLabel("Problem").filter((p) => p.attrs["resolvedAt"] == null && p.attrs["mergedInto"] === void 0);
27621
+ const tokens = /* @__PURE__ */ new Map();
27622
+ for (const p of open) tokens.set(p.id, new Set(retrievalTokens(p.description)));
27623
+ const consumed = /* @__PURE__ */ new Set();
27624
+ store2.transaction(() => {
27625
+ for (let i2 = 0; i2 < open.length; i2++) {
27626
+ const a = open[i2];
27627
+ if (consumed.has(a.id)) continue;
27628
+ const cluster = [a];
27629
+ const ta = tokens.get(a.id);
27630
+ for (let j = i2 + 1; j < open.length; j++) {
27631
+ const b = open[j];
27632
+ if (consumed.has(b.id)) continue;
27633
+ const tb = tokens.get(b.id);
27634
+ if (Math.min(ta.size, tb.size) < minTokens) continue;
27635
+ if (isConstraintProblem(a) !== isConstraintProblem(b)) continue;
27636
+ if (overlap(ta, tb) >= minOverlap) {
27637
+ cluster.push(b);
27638
+ consumed.add(b.id);
27639
+ }
27640
+ }
27641
+ if (cluster.length < 2) continue;
27642
+ report.clusters++;
27643
+ cluster.sort((x, y) => corroborations(y) - corroborations(x) || x.createdAt - y.createdAt);
27644
+ const survivor = cluster[0];
27645
+ for (const dup of cluster.slice(1)) {
27646
+ foldDuplicateProblem(store2, dup, survivor, opts.ts);
27647
+ report.merged++;
27648
+ }
27790
27649
  }
27791
- rowLen[fi] = inSet;
27792
- edgeCount += inSet;
27650
+ });
27651
+ return report;
27652
+ }
27653
+ function foldDuplicateProblem(store2, dup, survivor, ts) {
27654
+ const surv = store2.getNode(survivor.id);
27655
+ if (!surv) return;
27656
+ const sources = new Set(surv.attrs["sources"] ?? []);
27657
+ for (const s of dup.attrs["sources"] ?? []) sources.add(s);
27658
+ const dupSeq = dup.attrs["createdAtSeq"] ?? store2.currentIngestSeq();
27659
+ const exposure = priorExposure(store2, survivor.id, dupSeq);
27660
+ const expField = `reinforce${exposure[0].toUpperCase()}${exposure.slice(1)}Count`;
27661
+ store2.updateNode(survivor.id, {
27662
+ attrs: {
27663
+ ...surv.attrs,
27664
+ sources: [...sources],
27665
+ corroborations: corroborations(surv) + corroborations(dup) + 1,
27666
+ [expField]: (surv.attrs[expField] ?? 0) + 1,
27667
+ lastReinforceExposure: exposure
27668
+ },
27669
+ cumulativeHits: (surv.cumulativeHits ?? 0) + (dup.cumulativeHits ?? 0),
27670
+ lastUpdatedAt: ts
27671
+ });
27672
+ for (const e of store2.outEdges(dup.id, REDIRECT_EDGES)) {
27673
+ if (e.to === survivor.id) continue;
27674
+ const id = `edge_${digest({ from: survivor.id, type: e.type, to: e.to })}`.slice(0, 24);
27675
+ if (store2.getEdge(id)) continue;
27676
+ const redirected = {
27677
+ ...e,
27678
+ id,
27679
+ from: survivor.id,
27680
+ createdAt: ts,
27681
+ lastSeenAt: ts
27682
+ };
27683
+ store2.mergeEdge(redirected);
27793
27684
  }
27794
- const rowStart = new Int32Array(n + 1);
27795
- for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
27796
- const colIdx = new Int32Array(edgeCount);
27797
- const colW = new Float64Array(edgeCount);
27798
- const cursor = rowStart.slice(0, n);
27799
- for (const [from, edges] of input.out) {
27800
- const fi = index.get(from);
27801
- if (fi === void 0) continue;
27802
- for (const e of edges) {
27803
- const ti = index.get(e.to);
27804
- if (ti === void 0) continue;
27805
- const c = cursor[fi];
27806
- cursor[fi] = c + 1;
27807
- colIdx[c] = ti;
27808
- colW[c] = e.weight;
27809
- }
27685
+ store2.updateNode(dup.id, {
27686
+ attrs: { ...dup.attrs, mergedInto: survivor.id, resolvedAs: "duplicate", resolvedAt: ts },
27687
+ lastUpdatedAt: ts
27688
+ });
27689
+ store2.closeNode(dup.id, ts);
27690
+ }
27691
+
27692
+ // ../../packages/local-graph/src/design-problem.ts
27693
+ init_src();
27694
+ function isConstraintProblem(node) {
27695
+ return node.attrs["kind"] === "constraint";
27696
+ }
27697
+ var RECURRENCE_DEBOUNCE_MS = 60 * 60 * 1e3;
27698
+ function resolveFileNode(store2, path, workspaceId) {
27699
+ const want = path.trim().replace(/^\.?\//, "");
27700
+ for (const n of store2.findNodesByLabel("File")) {
27701
+ if (n.attrs["workspaceId"] && n.attrs["workspaceId"] !== workspaceId) continue;
27702
+ if (n.attrs["relPath"] === want || n.description === want) return n;
27810
27703
  }
27811
- let score2 = new Float64Array(n).fill(1 / n);
27812
- let next = new Float64Array(n);
27813
- const base = (1 - damping) / n;
27814
- let iter = 0;
27815
- let converged = false;
27816
- for (; iter < maxIter; iter++) {
27817
- next.fill(base);
27818
- let danglingMass = 0;
27819
- for (let i2 = 0; i2 < n; i2++) if (dangling[i2]) danglingMass += score2[i2];
27820
- for (let i2 = 0; i2 < n; i2++) {
27821
- if (dangling[i2]) continue;
27822
- const f = damping * score2[i2] / outSum[i2];
27823
- const end = rowStart[i2 + 1];
27824
- for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
27825
- }
27826
- if (danglingMass > 0) {
27827
- const share = damping * danglingMass / n;
27828
- for (let i2 = 0; i2 < n; i2++) next[i2] += share;
27829
- }
27830
- let diff = 0;
27831
- for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
27832
- const tmp = score2;
27833
- score2 = next;
27834
- next = tmp;
27835
- if (diff < tol) {
27836
- iter++;
27837
- converged = true;
27704
+ return null;
27705
+ }
27706
+ var CITABLE_PRIOR_LABELS = /* @__PURE__ */ new Set([
27707
+ "Solution",
27708
+ "RootCause",
27709
+ "Pattern",
27710
+ "Technique",
27711
+ "AntiPattern"
27712
+ ]);
27713
+ function isAutoMinted(n) {
27714
+ return isEditPlaceholderSolution(n);
27715
+ }
27716
+ function priorsForFile(store2, relPath) {
27717
+ const want = relPath.trim().replace(/^\.?\//, "");
27718
+ let file2 = null;
27719
+ for (const n of store2.findNodesByLabel("File")) {
27720
+ if (n.description === want || n.attrs["relPath"] === want) {
27721
+ file2 = n;
27838
27722
  break;
27839
27723
  }
27840
27724
  }
27841
- const scores = /* @__PURE__ */ new Map();
27842
- let danglingRank = 0;
27843
- for (let i2 = 0; i2 < n; i2++) {
27844
- scores.set(ids[i2], score2[i2]);
27845
- if (dangling[i2]) danglingRank += score2[i2];
27846
- }
27847
- return { scores, iterations: iter, converged, danglingRank };
27848
- }
27849
- function markLandmarks(scores, percentile = 0.1, filter) {
27850
- const entries = [...scores.entries()];
27851
- if (filter) {
27852
- for (let i2 = entries.length - 1; i2 >= 0; i2--) {
27853
- if (!filter(entries[i2][0])) entries.splice(i2, 1);
27725
+ if (!file2) return null;
27726
+ const openProblems = [];
27727
+ const constraints = [];
27728
+ const resolvedProblems = [];
27729
+ const seenProblem = /* @__PURE__ */ new Set();
27730
+ const related = /* @__PURE__ */ new Map();
27731
+ const anchorEdges = [
27732
+ ...store2.inEdges(file2.id, ["ANCHORED_AT"]),
27733
+ ...store2.outEdges(file2.id, ["DEFINES"]).flatMap((d) => store2.inEdges(d.to, ["ANCHORED_AT"]))
27734
+ ];
27735
+ for (const e of anchorEdges) {
27736
+ const n = store2.getNode(e.from);
27737
+ if (!n) continue;
27738
+ if (n.label === "Problem" || n.label === "Vulnerability") {
27739
+ if (seenProblem.has(n.id)) continue;
27740
+ seenProblem.add(n.id);
27741
+ if (!n.attrs["resolvedAt"]) {
27742
+ (isConstraintProblem(n) ? constraints : openProblems).push(n);
27743
+ } else if (n.attrs["resolvedAs"] == null && n.attrs["mergedInto"] === void 0 && e.attrs?.["anchorProvenance"] !== "legacy") {
27744
+ resolvedProblems.push(n);
27745
+ }
27746
+ } else if (CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) {
27747
+ related.set(n.id, n);
27854
27748
  }
27855
27749
  }
27856
- if (entries.length === 0) return /* @__PURE__ */ new Set();
27857
- entries.sort((a, b) => b[1] - a[1] || (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0));
27858
- const cutoff = Math.max(1, Math.floor(entries.length * percentile));
27859
- const out2 = /* @__PURE__ */ new Set();
27860
- for (let i2 = 0; i2 < cutoff; i2++) {
27861
- out2.add(entries[i2][0]);
27750
+ const solutionsByProblem = /* @__PURE__ */ new Map();
27751
+ for (const p of [...openProblems, ...constraints, ...resolvedProblems]) {
27752
+ for (const e of store2.outEdges(p.id, ["SOLVED_BY", "CAUSED_BY", "INSTANCE_OF"])) {
27753
+ const n = store2.getNode(e.to);
27754
+ if (n && CITABLE_PRIOR_LABELS.has(n.label) && !isAutoMinted(n)) related.set(n.id, n);
27755
+ if (n && n.label === "Solution" && e.type === "SOLVED_BY" && !isAutoMinted(n)) {
27756
+ const list = solutionsByProblem.get(p.id) ?? [];
27757
+ if (!list.some((s) => s.id === n.id)) list.push(n);
27758
+ solutionsByProblem.set(p.id, list);
27759
+ }
27760
+ }
27862
27761
  }
27863
- return out2;
27762
+ if (openProblems.length === 0 && constraints.length === 0 && resolvedProblems.length === 0 && related.size === 0)
27763
+ return null;
27764
+ const recentFirst = (a, b) => (b.lastUpdatedAt ?? 0) - (a.lastUpdatedAt ?? 0);
27765
+ openProblems.sort(recentFirst);
27766
+ constraints.sort(recentFirst);
27767
+ resolvedProblems.sort(
27768
+ (a, b) => Number(b.attrs["resolvedAt"] ?? 0) - Number(a.attrs["resolvedAt"] ?? 0)
27769
+ );
27770
+ return {
27771
+ file: file2,
27772
+ openProblems,
27773
+ constraints,
27774
+ resolvedProblems,
27775
+ related: [...related.values()],
27776
+ solutionsByProblem
27777
+ };
27864
27778
  }
27865
-
27866
- // ../../packages/math/src/leiden.ts
27867
- function buildGraph(nodeIds, adj) {
27868
- const nbr = /* @__PURE__ */ new Map();
27869
- const deg = /* @__PURE__ */ new Map();
27870
- const present = new Set(nodeIds);
27871
- for (const id of nodeIds) {
27872
- nbr.set(id, /* @__PURE__ */ new Map());
27873
- deg.set(id, 0);
27779
+ function anchorProblemToWorkingFile(store2, problemId, relPath, workspaceId, ts, provenance = "edited", opts = {}) {
27780
+ const p = store2.getNode(problemId);
27781
+ if (!p || p.label !== "Problem" && p.label !== "Vulnerability") return false;
27782
+ if (p.label === "Vulnerability" && provenance !== "witnessed") return false;
27783
+ if (!isAnchorableCodePath(relPath)) return false;
27784
+ const file2 = resolveFileNode(store2, relPath, workspaceId);
27785
+ if (!file2) return false;
27786
+ const already = new Set(store2.outEdges(problemId, ["ANCHORED_AT"]).map((e) => e.to));
27787
+ const confidence = provenance === "witnessed" ? 0.4 : 0.3;
27788
+ if (!already.has(file2.id)) {
27789
+ store2.mergeEdge({
27790
+ id: `edge_${digest({ from: problemId, type: "ANCHORED_AT", to: file2.id })}`.slice(0, 24),
27791
+ from: problemId,
27792
+ to: file2.id,
27793
+ type: "ANCHORED_AT",
27794
+ confidence,
27795
+ extractionSource: "agent-observed",
27796
+ createdAt: ts,
27797
+ lastSeenAt: ts,
27798
+ navSuccesses: 0,
27799
+ navFailures: 0,
27800
+ attrs: { provisional: true, captureTime: true, anchorProvenance: provenance }
27801
+ });
27874
27802
  }
27875
- let m2 = 0;
27876
- for (const id of nodeIds) {
27877
- for (const { to, weight } of adj.get(id) ?? []) {
27878
- if (!present.has(to)) continue;
27879
- const row = nbr.get(id);
27880
- row.set(to, (row.get(to) ?? 0) + weight);
27881
- deg.set(id, (deg.get(id) ?? 0) + weight);
27882
- m2 += weight;
27803
+ if (opts.line !== void 0 && opts.fileText !== void 0) {
27804
+ const sym = resolveEnclosingSymbol(store2, file2.id, opts.line, opts.fileText);
27805
+ if (sym && !already.has(sym.id)) {
27806
+ store2.mergeEdge({
27807
+ id: `edge_${digest({ from: problemId, type: "ANCHORED_AT", to: sym.id })}`.slice(0, 24),
27808
+ from: problemId,
27809
+ to: sym.id,
27810
+ type: "ANCHORED_AT",
27811
+ confidence,
27812
+ extractionSource: "agent-observed",
27813
+ createdAt: ts,
27814
+ lastSeenAt: ts,
27815
+ navSuccesses: 0,
27816
+ navFailures: 0,
27817
+ attrs: {
27818
+ provisional: true,
27819
+ captureTime: true,
27820
+ anchorProvenance: provenance,
27821
+ anchorLine: opts.line,
27822
+ enclosingSymbol: true,
27823
+ // PJ-anchor-version: WHICH version of the symbol this was witnessed
27824
+ // against. `bodyHash` is the indexer's 16-hex sha256 of the body;
27825
+ // `headSha` is the checkout's HEAD at capture. Both opaque, both
27826
+ // cross the membrane on the anchor edge, and a teammate's reanchor
27827
+ // compares the hash with their own symbol to tell "current" from
27828
+ // "pre-change" — the branch hazard where a fix witnessed on feat/x
27829
+ // would otherwise read as a fact about the unchanged symbol on dev.
27830
+ ...typeof sym.attrs["bodyHash"] === "string" ? { symbolContentHash: sym.attrs["bodyHash"] } : {},
27831
+ ...opts.headSha ? { observedAtCommit: opts.headSha } : {}
27832
+ }
27833
+ });
27883
27834
  }
27884
27835
  }
27885
- return { nbr, deg, m2 };
27836
+ return true;
27886
27837
  }
27887
- function localMoving(nodeIds, g) {
27888
- const community = new Map(nodeIds.map((n) => [n, n]));
27889
- const comTot = new Map(nodeIds.map((n) => [n, g.deg.get(n) ?? 0]));
27890
- const order = [...nodeIds].sort();
27891
- const m2 = g.m2 > 0 ? g.m2 : 1;
27892
- let improved = true;
27893
- let iter = 0;
27894
- while (improved && iter < 100) {
27895
- improved = false;
27896
- iter++;
27897
- for (const node of order) {
27898
- const ci = community.get(node);
27899
- const ki = g.deg.get(node) ?? 0;
27900
- comTot.set(ci, (comTot.get(ci) ?? 0) - ki);
27901
- const wToComm = /* @__PURE__ */ new Map();
27902
- for (const [nb, w] of g.nbr.get(node) ?? []) {
27903
- if (nb === node) continue;
27904
- const c = community.get(nb);
27905
- wToComm.set(c, (wToComm.get(c) ?? 0) + w);
27906
- }
27907
- let bestC = ci;
27908
- let bestGain = (wToComm.get(ci) ?? 0) - (comTot.get(ci) ?? 0) * ki / m2;
27909
- for (const [c, kiIn] of [...wToComm.entries()].sort(
27910
- (a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0
27911
- )) {
27912
- const gain = kiIn - (comTot.get(c) ?? 0) * ki / m2;
27913
- if (gain > bestGain) {
27914
- bestGain = gain;
27915
- bestC = c;
27838
+ var ENCLOSING_SYMBOL_KINDS = /* @__PURE__ */ new Set([
27839
+ "Function",
27840
+ "Class",
27841
+ "Method",
27842
+ "Module",
27843
+ "Interface",
27844
+ "Const",
27845
+ "Enum",
27846
+ "TypeAlias",
27847
+ "Namespace",
27848
+ "Property"
27849
+ ]);
27850
+ var ENCLOSING_WALK_DEPTH = 8;
27851
+ function resolveEnclosingSymbol(store2, fileId, line, fileText) {
27852
+ if (!Number.isFinite(line) || line < 1) return null;
27853
+ const row = Math.floor(line) - 1;
27854
+ const lineStarts = [0];
27855
+ for (let i2 = 0; i2 < fileText.length; i2++) if (fileText.charCodeAt(i2) === 10) lineStarts.push(i2 + 1);
27856
+ const rowOf = (index) => {
27857
+ let lo = 0;
27858
+ let hi = lineStarts.length - 1;
27859
+ while (lo < hi) {
27860
+ const mid = lo + hi + 1 >> 1;
27861
+ if (lineStarts[mid] <= index) lo = mid;
27862
+ else hi = mid - 1;
27863
+ }
27864
+ return lo;
27865
+ };
27866
+ let best = null;
27867
+ let bestStart = -1;
27868
+ let bestSpan = Number.POSITIVE_INFINITY;
27869
+ const seen = /* @__PURE__ */ new Set([fileId]);
27870
+ let frontier = [fileId];
27871
+ for (let depth = 0; depth < ENCLOSING_WALK_DEPTH && frontier.length > 0; depth++) {
27872
+ const next = [];
27873
+ for (const id of frontier) {
27874
+ for (const e of store2.outEdges(id, ["DEFINES"])) {
27875
+ if (seen.has(e.to)) continue;
27876
+ seen.add(e.to);
27877
+ const n = store2.getNode(e.to);
27878
+ if (!n) continue;
27879
+ next.push(n.id);
27880
+ if (!ENCLOSING_SYMBOL_KINDS.has(n.label)) continue;
27881
+ const startRow = n.attrs["startRow"];
27882
+ const startByte = n.attrs["startByte"];
27883
+ const endByte = n.attrs["endByte"];
27884
+ if (typeof startRow !== "number" || typeof startByte !== "number" || typeof endByte !== "number") continue;
27885
+ if (startRow < 0 || endByte < startByte) continue;
27886
+ const endRow = rowOf(Math.max(startByte, Math.min(endByte, fileText.length) - 1));
27887
+ if (row < startRow || row > endRow) continue;
27888
+ const span = endByte - startByte;
27889
+ if (startRow > bestStart || startRow === bestStart && span < bestSpan) {
27890
+ best = n;
27891
+ bestStart = startRow;
27892
+ bestSpan = span;
27916
27893
  }
27917
27894
  }
27918
- community.set(node, bestC);
27919
- comTot.set(bestC, (comTot.get(bestC) ?? 0) + ki);
27920
- if (bestC !== ci) improved = true;
27921
27895
  }
27896
+ frontier = next;
27922
27897
  }
27923
- return community;
27898
+ return best;
27924
27899
  }
27925
- function refineConnectivity(community, g) {
27926
- const members = /* @__PURE__ */ new Map();
27927
- for (const [id, c] of community) {
27928
- const l = members.get(c) ?? [];
27929
- l.push(id);
27930
- members.set(c, l);
27900
+ function recordAnchorHint(store2, problemId, relPath, provenance, ts) {
27901
+ const p = store2.getNode(problemId);
27902
+ if (!p || p.label !== "Problem") return false;
27903
+ if (p.attrs["anchorHint"]) return false;
27904
+ store2.updateNode(problemId, {
27905
+ attrs: { ...p.attrs, anchorHint: { path: relPath, provenance, ts } }
27906
+ });
27907
+ return true;
27908
+ }
27909
+ function openAnchorHint(p) {
27910
+ const h = p.attrs["anchorHint"];
27911
+ if (!h || typeof h.path !== "string" || h.upgradedAt) return null;
27912
+ return { path: h.path, ts: typeof h.ts === "number" ? h.ts : 0 };
27913
+ }
27914
+ function consumeAnchorHint(store2, p, ts) {
27915
+ const hint = p.attrs["anchorHint"];
27916
+ store2.updateNode(p.id, { attrs: { ...p.attrs, anchorHint: { ...hint, upgradedAt: ts } } });
27917
+ deriveContextFromAnchors(store2, p.id, ts);
27918
+ }
27919
+ function upgradeAnchorHints(store2, workspaceId, ts) {
27920
+ let upgraded = 0;
27921
+ let standing = 0;
27922
+ let unresolvable = 0;
27923
+ let blocked = 0;
27924
+ const filesByPath = /* @__PURE__ */ new Map();
27925
+ for (const f of store2.findNodesByLabel("File")) {
27926
+ if (f.attrs["workspaceId"] && f.attrs["workspaceId"] !== workspaceId) continue;
27927
+ const rel = typeof f.attrs["relPath"] === "string" ? f.attrs["relPath"] : f.description;
27928
+ if (rel && !filesByPath.has(rel)) filesByPath.set(rel, f);
27931
27929
  }
27932
- const refined = /* @__PURE__ */ new Map();
27933
- for (const [, group] of members) {
27934
- const inGroup = new Set(group);
27935
- const seen = /* @__PURE__ */ new Set();
27936
- for (const start2 of [...group].sort()) {
27937
- if (seen.has(start2)) continue;
27938
- const comp = [];
27939
- const stack = [start2];
27940
- seen.add(start2);
27941
- while (stack.length) {
27942
- const cur = stack.pop();
27943
- comp.push(cur);
27944
- for (const [nb] of g.nbr.get(cur) ?? []) {
27945
- if (inGroup.has(nb) && !seen.has(nb)) {
27946
- seen.add(nb);
27947
- stack.push(nb);
27948
- }
27949
- }
27950
- }
27951
- const anchor = [...comp].sort()[0];
27952
- for (const id of comp) refined.set(id, anchor);
27930
+ for (const p of store2.findNodesByLabel("Problem")) {
27931
+ if (p.validTo != null || p.attrs["resolvedAt"]) continue;
27932
+ const hint = openAnchorHint(p);
27933
+ if (!hint) continue;
27934
+ const file2 = filesByPath.get(hint.path.trim().replace(/^\.?\//, ""));
27935
+ if (!file2 || !isAnchorableCodePath(hint.path)) {
27936
+ unresolvable++;
27937
+ continue;
27938
+ }
27939
+ standing++;
27940
+ const changedAt = Number(file2.attrs["contentChangedAt"] ?? 0);
27941
+ if (changedAt <= hint.ts) continue;
27942
+ if (anchorProblemToWorkingFile(store2, p.id, hint.path, workspaceId, ts, "hint-confirmed")) {
27943
+ consumeAnchorHint(store2, p, ts);
27944
+ upgraded++;
27945
+ standing--;
27946
+ } else {
27947
+ blocked++;
27953
27948
  }
27954
27949
  }
27955
- return refined;
27950
+ return { upgraded, standing, unresolvable, blocked };
27956
27951
  }
27957
- function normalize(assignment) {
27958
- const groups = /* @__PURE__ */ new Map();
27959
- for (const [id, c] of assignment) {
27960
- const l = groups.get(c) ?? [];
27961
- l.push(id);
27962
- groups.set(c, l);
27963
- }
27964
- const community = /* @__PURE__ */ new Map();
27965
- for (const grp of groups.values()) {
27966
- const anchor = [...grp].sort()[0];
27967
- const cid = `community_${anchor}`;
27968
- for (const m of grp) community.set(m, cid);
27952
+ function reopenAutoClosedProblems(store2, t) {
27953
+ const report = { reopened: 0, alreadySuspect: 0, agentDescribed: 0 };
27954
+ for (const p of store2.findNodesByLabel("Problem")) {
27955
+ if (p.attrs["resolvedAt"] == null) continue;
27956
+ if (p.attrs["resolvedAs"] != null) continue;
27957
+ if (p.attrs["mergedInto"] !== void 0) continue;
27958
+ if (p.attrs["resolutionWitnessed"] === true) {
27959
+ report.agentDescribed++;
27960
+ continue;
27961
+ }
27962
+ const sols = store2.outEdges(p.id, ["SOLVED_BY"]).map((e) => store2.getNode(e.to)).filter((n) => n !== null);
27963
+ if (sols.length === 0 && p.attrs["retiredEditPlaceholder"] !== true) continue;
27964
+ if (!sols.every((s) => String(s.description ?? "").startsWith(AUTO_MINT_PREFIX))) {
27965
+ report.agentDescribed++;
27966
+ continue;
27967
+ }
27968
+ if (p.attrs["resolutionSuspect"] === true) report.alreadySuspect++;
27969
+ const attrs = { ...p.attrs };
27970
+ delete attrs["resolvedAt"];
27971
+ attrs["reopenedFrom"] = "auto-close";
27972
+ attrs["reopenedAt"] = t;
27973
+ store2.updateNode(p.id, { attrs, lastUpdatedAt: t });
27974
+ report.reopened++;
27969
27975
  }
27970
- return { community, count: groups.size, iterations: 1 };
27976
+ return report;
27971
27977
  }
27972
- function causalComponents(nodeIds, pairs) {
27973
- const parent = new Map(nodeIds.map((n) => [n, n]));
27974
- const find = (x) => {
27975
- let r = x;
27976
- while (parent.get(r) !== r) r = parent.get(r);
27977
- let c = x;
27978
- while (parent.get(c) !== r) {
27979
- const next = parent.get(c);
27980
- parent.set(c, r);
27981
- c = next;
27978
+ function markFixCandidates(store2, t) {
27979
+ let resolved = 0;
27980
+ for (const p of store2.findNodesByLabel("Problem")) {
27981
+ if (!p.id.startsWith("dprob_")) continue;
27982
+ if (p.attrs["resolvedAt"]) continue;
27983
+ if (isConstraintProblem(p)) continue;
27984
+ let symName = "";
27985
+ let symRelPath;
27986
+ let edited = false;
27987
+ const since = Math.max(p.createdAt, Number(p.attrs["fixCandidateClearedAt"] ?? 0));
27988
+ for (const e of store2.outEdges(p.id, ["ANCHORED_AT"])) {
27989
+ if (e.attrs?.["mention"] === true) continue;
27990
+ if (e.attrs?.["anchorProvenance"] === "legacy") continue;
27991
+ const sym = store2.getNode(e.to);
27992
+ const changedAt = sym?.label === "File" ? Number(sym.attrs["contentChangedAt"] ?? 0) : sym?.lastUpdatedAt ?? 0;
27993
+ if (sym && changedAt > since) {
27994
+ edited = true;
27995
+ symName = sym.description;
27996
+ symRelPath = sym.attrs["relPath"] ?? void 0;
27997
+ break;
27998
+ }
27982
27999
  }
27983
- return r;
27984
- };
27985
- for (const [a, b] of pairs) {
27986
- const ra = find(a);
27987
- const rb = find(b);
27988
- if (ra !== rb) parent.set(ra < rb ? rb : ra, ra < rb ? ra : rb);
28000
+ if (!edited) continue;
28001
+ if (p.attrs["fixCandidateAt"] !== void 0) continue;
28002
+ if (store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
28003
+ const s = store2.getNode(e.to);
28004
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
28005
+ }))
28006
+ continue;
28007
+ store2.updateNode(p.id, {
28008
+ attrs: {
28009
+ ...p.attrs,
28010
+ // The cue, kept as structured data so the render can name the file it is
28011
+ // asking about. Deliberately NOT `resolvedAt` — this is a question.
28012
+ fixCandidateAt: t,
28013
+ fixCandidateSymbol: symName,
28014
+ ...symRelPath ? { fixCandidateFile: symRelPath } : {},
28015
+ // Snapshot the render-ledger counter so "shown since THIS ask" is
28016
+ // measurable. `shownCount` is the node's LIFETIME count across every
28017
+ // band, so comparing it raw would retire a fresh ask on a
28018
+ // frequently-surfaced problem before anyone had seen the question once.
28019
+ fixCandidateShownBase: Number(p.attrs["shownCount"] ?? 0)
28020
+ }
28021
+ // No `lastUpdatedAt`: raising a question is bookkeeping, not a new
28022
+ // observation of the problem, and the passive block's recent-problems band
28023
+ // sorts on that field (see clearSettledFixCandidates).
28024
+ });
28025
+ resolved++;
27989
28026
  }
27990
- return find;
28027
+ return resolved;
27991
28028
  }
27992
- function detectCommunitiesLeiden(input, opts = {}) {
27993
- const idSet = new Set(input.nodeIds);
27994
- const protect = [...opts.protect ?? []].filter(
27995
- ([a, b]) => a !== b && idSet.has(a) && idSet.has(b)
27996
- );
27997
- if (protect.length === 0) {
27998
- const g = buildGraph(input.nodeIds, input.adj);
27999
- return normalize(refineConnectivity(localMoving(input.nodeIds, g), g));
28000
- }
28001
- const rep = causalComponents(input.nodeIds, protect);
28002
- const superIds = [...new Set(input.nodeIds.map(rep))];
28003
- const superAdj = /* @__PURE__ */ new Map();
28004
- const acc = /* @__PURE__ */ new Map();
28005
- for (const id of input.nodeIds) {
28006
- const ra = rep(id);
28007
- for (const { to, weight } of input.adj.get(id) ?? []) {
28008
- if (!idSet.has(to)) continue;
28009
- const rb = rep(to);
28010
- if (ra === rb) continue;
28011
- const row = acc.get(ra) ?? /* @__PURE__ */ new Map();
28012
- row.set(rb, (row.get(rb) ?? 0) + weight);
28013
- acc.set(ra, row);
28029
+ var FIX_CANDIDATE_ASK_LIMIT = 5;
28030
+ function clearSettledFixCandidates(store2, t) {
28031
+ const report = { answered: 0, ignored: 0, unsubstantiated: 0, standing: 0 };
28032
+ for (const p of store2.findNodesByLabel("Problem")) {
28033
+ if (p.attrs["fixCandidateAt"] === void 0) continue;
28034
+ 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");
28035
+ const unsubstantiated = fileAnchors.length > 0 && fileAnchors.every((f) => f.attrs["contentChangedAt"] === void 0);
28036
+ const answered = p.attrs["resolvedAt"] != null || store2.outEdges(p.id, ["SOLVED_BY"]).some((e) => {
28037
+ const s = store2.getNode(e.to);
28038
+ return s !== null && !String(s.description ?? "").startsWith(AUTO_MINT_PREFIX);
28039
+ });
28040
+ const base = Number(
28041
+ p.attrs["fixCandidateShownBase"] ?? p.attrs["shownCount"] ?? 0
28042
+ );
28043
+ const shownSinceAsk = Number(p.attrs["shownCount"] ?? 0) - base;
28044
+ const ignored = shownSinceAsk >= FIX_CANDIDATE_ASK_LIMIT;
28045
+ if (!answered && !ignored && !unsubstantiated) {
28046
+ report.standing++;
28047
+ continue;
28014
28048
  }
28049
+ const attrs = { ...p.attrs };
28050
+ delete attrs["fixCandidateAt"];
28051
+ delete attrs["fixCandidateSymbol"];
28052
+ delete attrs["fixCandidateFile"];
28053
+ attrs["fixCandidateClearedAt"] = t;
28054
+ store2.updateNode(p.id, { attrs });
28055
+ if (answered) report.answered++;
28056
+ else if (ignored) report.ignored++;
28057
+ else report.unsubstantiated++;
28015
28058
  }
28016
- for (const [a, row] of acc) superAdj.set(a, [...row].map(([to, weight]) => ({ to, weight })));
28017
- const sg = buildGraph(superIds, superAdj);
28018
- const superComm = refineConnectivity(localMoving(superIds, sg), sg);
28019
- const assignment = /* @__PURE__ */ new Map();
28020
- for (const id of input.nodeIds) assignment.set(id, superComm.get(rep(id)) ?? rep(id));
28021
- return normalize(assignment);
28059
+ return report;
28022
28060
  }
28023
-
28024
- // ../../packages/math/src/bko/a1-stable-motif.ts
28025
- var MIN_INSTANCES = 5;
28026
- var MIN_SURVIVED_RUNS = 2;
28027
- function a1StableMotif(input) {
28028
- if (input.instanceCount >= MIN_INSTANCES && input.survivedRuns >= MIN_SURVIVED_RUNS) {
28029
- return {
28030
- shouldMarkStable: true,
28031
- confidence: Math.min(1, input.instanceCount / 20),
28032
- rule: "A1"
28033
- };
28034
- }
28035
- return { shouldMarkStable: false, confidence: 0, rule: "A1" };
28061
+ function priorExposure(store2, nodeId, atSeq) {
28062
+ const n = store2.getNode(nodeId);
28063
+ if (!n || !n.attrs) return "unshown";
28064
+ const shownSeq = n.attrs["lastShownAtSeq"];
28065
+ if (shownSeq !== void 0 && shownSeq <= atSeq) return "shown";
28066
+ const evictedSeq = n.attrs["lastEvictedAtSeq"];
28067
+ if (evictedSeq !== void 0 && evictedSeq <= atSeq) return "evicted";
28068
+ if ((n.attrs["evictedCount"] ?? 0) > 0) return "evicted";
28069
+ return "unshown";
28036
28070
  }
28037
28071
 
28038
- // ../../packages/math/src/bko/a2-motif-quality.ts
28039
- var MIN_INSTANCES2 = 3;
28040
- var MIN_QUALITY = 0.65;
28041
- var MAX_FAILURE_RATE = 0.15;
28042
- var QUALITY_BOOST = 1.2;
28043
- function a2MotifQuality(input) {
28044
- if (input.instances.length === 0) {
28045
- return {
28046
- qualityScore: 0,
28047
- qualityFailureRate: 0,
28048
- qualified: false,
28049
- qualityBoost: 1,
28050
- rule: "A2"
28051
- };
28052
- }
28053
- const mean = input.instances.reduce((s, i2) => s + i2.confidence, 0) / input.instances.length;
28054
- const failures = input.instances.filter((i2) => i2.failed).length;
28055
- const failureRate = failures / input.instances.length;
28056
- const qualified = input.instances.length >= MIN_INSTANCES2 && mean >= MIN_QUALITY && failureRate <= MAX_FAILURE_RATE;
28057
- return {
28058
- qualityScore: mean,
28059
- qualityFailureRate: failureRate,
28060
- qualified,
28061
- qualityBoost: qualified ? QUALITY_BOOST : 1,
28062
- rule: "A2"
28063
- };
28064
- }
28072
+ // ../../packages/local-graph/src/intent.ts
28073
+ init_src();
28065
28074
 
28066
- // ../../packages/math/src/motif-gate.ts
28067
- var MOTIF_MIN_MEMBERS = 5;
28068
- var MOTIF_MIN_SURVIVED = 2;
28069
- function motifDecision(input) {
28070
- if (input.memberCount < MOTIF_MIN_MEMBERS) {
28071
- return {
28072
- isCandidate: false,
28073
- survivedRuns: input.priorSurvivedRuns,
28074
- stable: false,
28075
- qualified: false,
28076
- promoted: false,
28077
- motifConfidence: 0
28078
- };
28075
+ // ../../packages/local-graph/src/anchor-backfill.ts
28076
+ init_src();
28077
+ init_src2();
28078
+ function isLegacyAnchor(attrs) {
28079
+ return attrs?.["captureTime"] === true && attrs["anchorProvenance"] === void 0;
28080
+ }
28081
+ var STATEMENT_PATH_RE = new RegExp(
28082
+ String.raw`(?:[\w.+-]+\/)+[\w.+-]+\.(?:${anchorableExtensionAlternation()})`,
28083
+ "g"
28084
+ );
28085
+ function filePathResolver(store2) {
28086
+ const byPath = /* @__PURE__ */ new Map();
28087
+ for (const f of store2.findNodesByLabel("File")) {
28088
+ const path = String(f.attrs["relPath"] ?? f.description ?? "");
28089
+ if (path) byPath.set(path, { id: f.id, path });
28090
+ }
28091
+ return (named) => {
28092
+ const exact = byPath.get(named);
28093
+ if (exact) return exact;
28094
+ const hits = [...byPath.values()].filter(
28095
+ (f) => f.path.endsWith(`/${named}`) || named.endsWith(`/${f.path}`)
28096
+ );
28097
+ return hits.length === 1 ? hits[0] : null;
28098
+ };
28099
+ }
28100
+ function statementTarget(statement, resolvePath) {
28101
+ for (const m of statement.matchAll(STATEMENT_PATH_RE)) {
28102
+ const f = resolvePath(m[0]);
28103
+ if (f) return f;
28079
28104
  }
28080
- const survivedRuns = input.priorSurvivedRuns + 1;
28081
- const a2 = a2MotifQuality({ instances: input.instances });
28082
- const a1 = a1StableMotif({ instanceCount: input.instanceCount, survivedRuns });
28083
- const stable = a1.shouldMarkStable;
28084
- const promoted = survivedRuns >= MOTIF_MIN_SURVIVED && stable && a2.qualified;
28105
+ return null;
28106
+ }
28107
+ function restatedAnchor(problemId, fileId, restatedFrom, ts) {
28085
28108
  return {
28086
- isCandidate: true,
28087
- survivedRuns,
28088
- stable,
28089
- qualified: a2.qualified,
28090
- promoted,
28091
- motifConfidence: Math.round(a2.qualityScore * 100) / 100
28109
+ id: `edge_${digest({ from: problemId, type: "ANCHORED_AT", to: fileId })}`.slice(0, 24),
28110
+ from: problemId,
28111
+ to: fileId,
28112
+ type: "ANCHORED_AT",
28113
+ // Same confidence a capture-time anchor enters at: the source is the
28114
+ // agent's own statement either way, only the moment of reading differs.
28115
+ confidence: 0.4,
28116
+ extractionSource: "agent-observed",
28117
+ createdAt: ts,
28118
+ lastSeenAt: ts,
28119
+ navSuccesses: 0,
28120
+ navFailures: 0,
28121
+ attrs: { anchorProvenance: "restated", restatedFrom, restatedAt: ts }
28092
28122
  };
28093
28123
  }
28094
-
28095
- // ../../packages/math/src/pagerank-local.ts
28096
- function pagerankLocal(input) {
28097
- const damping = input.damping ?? 0.85;
28098
- const tol = input.tolerance ?? 1e-6;
28099
- const maxIter = input.maxIterations ?? 100;
28100
- const ids = input.regionIds;
28101
- const n = ids.length;
28102
- if (n === 0 || input.globalN === 0) {
28103
- return { scores: /* @__PURE__ */ new Map(), iterations: 0, converged: true };
28104
- }
28105
- const index = /* @__PURE__ */ new Map();
28106
- for (let i2 = 0; i2 < n; i2++) index.set(ids[i2], i2);
28107
- const base = (1 - damping) / input.globalN;
28108
- const outSum = new Float64Array(n);
28109
- const inflow = new Float64Array(n);
28110
- let score2 = new Float64Array(n);
28111
- for (let i2 = 0; i2 < n; i2++) {
28112
- const id = ids[i2];
28113
- outSum[i2] = input.outSum.get(id) ?? 0;
28114
- inflow[i2] = input.boundaryInflow.get(id) ?? 0;
28115
- score2[i2] = input.rank0.get(id) ?? base;
28116
- }
28117
- const rowLen = new Int32Array(n);
28118
- let edgeCount = 0;
28119
- for (const [from, edges] of input.out) {
28120
- const fi = index.get(from);
28121
- if (fi === void 0) continue;
28122
- let inSet = 0;
28123
- for (const e of edges) if (index.has(e.to)) inSet++;
28124
- rowLen[fi] = inSet;
28125
- edgeCount += inSet;
28126
- }
28127
- const rowStart = new Int32Array(n + 1);
28128
- for (let i2 = 0; i2 < n; i2++) rowStart[i2 + 1] = rowStart[i2] + rowLen[i2];
28129
- const colIdx = new Int32Array(edgeCount);
28130
- const colW = new Float64Array(edgeCount);
28131
- const cursor = rowStart.slice(0, n);
28132
- for (const [from, edges] of input.out) {
28133
- const fi = index.get(from);
28134
- if (fi === void 0) continue;
28135
- for (const e of edges) {
28136
- const ti = index.get(e.to);
28137
- if (ti === void 0) continue;
28138
- const c = cursor[fi];
28139
- cursor[fi] = c + 1;
28140
- colIdx[c] = ti;
28141
- colW[c] = e.weight;
28124
+ function repairLegacyAnchorsFromStatement(store2, ts) {
28125
+ const report = {
28126
+ repaired: [],
28127
+ retiredGuesses: 0,
28128
+ noPathNamed: 0,
28129
+ pathUnknown: 0,
28130
+ alreadyCorrect: 0
28131
+ };
28132
+ const resolvePath = filePathResolver(store2);
28133
+ for (const p of store2.findNodesByLabel("Problem")) {
28134
+ try {
28135
+ const anchors = store2.outEdges(p.id, ["ANCHORED_AT"]);
28136
+ const legacy = anchors.filter((e) => e.attrs?.["anchorProvenance"] === "legacy");
28137
+ if (legacy.length === 0) continue;
28138
+ const better = anchors.filter((e) => {
28139
+ const prov = e.attrs?.["anchorProvenance"];
28140
+ return prov === "restated" || prov === "witnessed" || prov === "edited";
28141
+ });
28142
+ if (better.length > 0) {
28143
+ const betterTargets = new Set(better.map((e) => e.to));
28144
+ let retired = 0;
28145
+ for (const e of legacy) {
28146
+ if (!betterTargets.has(e.to)) {
28147
+ store2.closeEdge(e.id, ts);
28148
+ retired++;
28149
+ }
28150
+ }
28151
+ report.retiredGuesses += retired;
28152
+ continue;
28153
+ }
28154
+ const named = [...String(p.description ?? "").matchAll(STATEMENT_PATH_RE)].map((m) => m[0]);
28155
+ if (named.length === 0) {
28156
+ report.noPathNamed++;
28157
+ continue;
28158
+ }
28159
+ const guessedPaths = legacy.map((e) => {
28160
+ const f = store2.getNode(e.to);
28161
+ return String(f?.attrs["relPath"] ?? f?.description ?? "");
28162
+ });
28163
+ if (named.some((x) => guessedPaths.some((g) => g.endsWith(x) || x.endsWith(g)))) {
28164
+ report.alreadyCorrect++;
28165
+ continue;
28166
+ }
28167
+ const target = named.map(resolvePath).find((f) => f !== null);
28168
+ if (!target) {
28169
+ report.pathUnknown++;
28170
+ continue;
28171
+ }
28172
+ store2.mergeEdge(restatedAnchor(p.id, target.id, guessedPaths[0] ?? "", ts));
28173
+ for (const e of legacy) {
28174
+ if (e.to !== target.id) store2.closeEdge(e.id, ts);
28175
+ }
28176
+ report.retiredGuesses += legacy.filter((e) => e.to !== target.id).length;
28177
+ report.repaired.push({
28178
+ problemId: p.id,
28179
+ problem: p.description,
28180
+ guessed: guessedPaths[0] ?? "",
28181
+ restated: target.path
28182
+ });
28183
+ } catch {
28142
28184
  }
28143
28185
  }
28144
- const externalDangling = input.externalDanglingRank ?? 0;
28145
- let next = new Float64Array(n);
28146
- let iter = 0;
28147
- let converged = false;
28148
- for (; iter < maxIter; iter++) {
28149
- let internalDangling = 0;
28150
- for (let i2 = 0; i2 < n; i2++) if (outSum[i2] <= 0) internalDangling += score2[i2];
28151
- const danglingShare = damping * (internalDangling + externalDangling) / input.globalN;
28152
- for (let i2 = 0; i2 < n; i2++) next[i2] = base + danglingShare + damping * inflow[i2];
28153
- for (let i2 = 0; i2 < n; i2++) {
28154
- const os2 = outSum[i2];
28155
- if (os2 <= 0) continue;
28156
- const f = damping * score2[i2] / os2;
28157
- const end = rowStart[i2 + 1];
28158
- for (let c = rowStart[i2]; c < end; c++) next[colIdx[c]] += f * colW[c];
28159
- }
28160
- let diff = 0;
28161
- for (let i2 = 0; i2 < n; i2++) diff += Math.abs(next[i2] - score2[i2]);
28162
- const tmp = score2;
28163
- score2 = next;
28164
- next = tmp;
28165
- if (diff < tol) {
28166
- iter++;
28167
- converged = true;
28168
- break;
28186
+ return report;
28187
+ }
28188
+ function backfillLegacyAnchors(store2, ts) {
28189
+ const report = {
28190
+ demotedEdges: 0,
28191
+ demotedProblems: 0,
28192
+ restatedProblems: 0,
28193
+ labeledEdges: 0,
28194
+ suspects: []
28195
+ };
28196
+ let resolvePath;
28197
+ for (const p of store2.findNodesByLabel("Problem")) {
28198
+ const resolved = p.attrs["resolvedAt"] !== void 0;
28199
+ const legacy = store2.outEdges(p.id, ["ANCHORED_AT"]).filter((e) => isLegacyAnchor(e.attrs) || !resolved && e.attrs?.["anchorProvenance"] === "legacy");
28200
+ if (legacy.length === 0) continue;
28201
+ try {
28202
+ if (!resolved) {
28203
+ resolvePath ??= filePathResolver(store2);
28204
+ const target = statementTarget(String(p.description ?? ""), resolvePath);
28205
+ if (target) {
28206
+ const guessed = store2.getNode(legacy[0].to);
28207
+ store2.mergeEdge(
28208
+ restatedAnchor(p.id, target.id, String(guessed?.attrs["relPath"] ?? guessed?.description ?? ""), ts)
28209
+ );
28210
+ report.restatedProblems++;
28211
+ }
28212
+ const retired = legacy.filter((e) => !(target && e.to === target.id));
28213
+ for (const e of retired) {
28214
+ const file2 = store2.getNode(e.to);
28215
+ const path = String(file2?.attrs["relPath"] ?? file2?.description ?? "");
28216
+ if (path) recordAnchorHint(store2, p.id, path, "legacy", ts);
28217
+ store2.closeEdge(e.id, ts);
28218
+ report.demotedEdges++;
28219
+ }
28220
+ if (retired.length > 0) report.demotedProblems++;
28221
+ continue;
28222
+ }
28223
+ for (const e of legacy) {
28224
+ store2.mergeEdge({ ...e, attrs: { ...e.attrs, anchorProvenance: "legacy" }, lastSeenAt: ts });
28225
+ report.labeledEdges++;
28226
+ }
28227
+ for (const se of store2.outEdges(p.id, ["SOLVED_BY"])) {
28228
+ const sol = store2.getNode(se.to);
28229
+ if (!sol || !sol.description.startsWith(AUTO_MINT_PREFIX)) continue;
28230
+ const guessedAnchor = sol.description.slice(AUTO_MINT_PREFIX.length);
28231
+ store2.updateNode(sol.id, {
28232
+ attrs: { ...sol.attrs, provisional: true, resolutionSuspect: true },
28233
+ lastUpdatedAt: ts
28234
+ });
28235
+ markRevisit(
28236
+ store2,
28237
+ sol,
28238
+ `auto-closed off a pre-provenance anchor (guessed ${guessedAnchor}) \u2014 confirm the problem is really fixed, or reopen it`,
28239
+ ts,
28240
+ // "or reopen it" is half the ask, and the nightly reopen takes that
28241
+ // branch — so record what would answer this, or the flag can never be
28242
+ // lowered and the band fills with settled questions.
28243
+ "parentProblemOpen"
28244
+ );
28245
+ store2.updateNode(p.id, {
28246
+ attrs: { ...p.attrs, resolutionSuspect: true },
28247
+ lastUpdatedAt: ts
28248
+ });
28249
+ report.suspects.push({
28250
+ problemId: p.id,
28251
+ problem: p.description,
28252
+ solutionId: sol.id,
28253
+ guessedAnchor
28254
+ });
28255
+ }
28256
+ } catch {
28169
28257
  }
28170
28258
  }
28171
- const scores = /* @__PURE__ */ new Map();
28172
- for (let i2 = 0; i2 < n; i2++) scores.set(ids[i2], score2[i2]);
28173
- return { scores, iterations: iter, converged };
28259
+ return report;
28174
28260
  }
28175
28261
 
28262
+ // ../../packages/local-graph/src/percolate.ts
28263
+ init_src2();
28264
+ init_src();
28265
+
28266
+ // ../../packages/local-graph/src/abstraction.ts
28267
+ init_src();
28268
+ init_src2();
28269
+
28176
28270
  // ../../packages/local-graph/src/triage.ts
28177
28271
  init_src();
28178
28272
  init_src();
@@ -28813,6 +28907,7 @@ function tryIncrementalRescore(store2, lastRescoreAt, started) {
28813
28907
  store2.setPageRank(id, score2);
28814
28908
  pageRankWritten++;
28815
28909
  }
28910
+ if (pageRankWritten > 0) store2.refreshPageRankScale?.();
28816
28911
  });
28817
28912
  const sweep = store2.landmarkSweepRows();
28818
28913
  const candidates = /* @__PURE__ */ new Map();
@@ -28883,6 +28978,7 @@ function runFullRescore(store2) {
28883
28978
  store2.setPageRank(id, score2);
28884
28979
  pageRankWritten++;
28885
28980
  }
28981
+ store2.refreshPageRankScale?.();
28886
28982
  });
28887
28983
  const landmarkCandidates = /* @__PURE__ */ new Map();
28888
28984
  for (const [id, score2] of result.scores) {