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

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