@inerrata-corporation/errata 2.0.22-dev.4020 → 2.0.22-dev.4049

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.
@@ -16508,6 +16508,38 @@ function installHistoryAttrSlim(db) {
16508
16508
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
16509
16509
  }
16510
16510
 
16511
+ // ../../packages/local-graph/src/text-index.ts
16512
+ var TEXT_INDEX_KEY = "text-index-trigram-1";
16513
+ var TEXT_INDEX_MIN_TERM = 3;
16514
+ function installTextIndex(db) {
16515
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)) return;
16516
+ try {
16517
+ db.exec(`CREATE VIRTUAL TABLE temp.fts5_probe USING fts5(x, tokenize='trigram'); DROP TABLE temp.fts5_probe`);
16518
+ } catch {
16519
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "unavailable");
16520
+ return;
16521
+ }
16522
+ db.transaction(() => {
16523
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_text
16524
+ USING fts5(description, content='nodes', content_rowid='rowid', tokenize='trigram')`);
16525
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_insert AFTER INSERT ON nodes WHEN NEW.valid_to IS NULL
16526
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
16527
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_out AFTER UPDATE OF description, valid_to ON nodes
16528
+ WHEN OLD.valid_to IS NULL
16529
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
16530
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_in AFTER UPDATE OF description, valid_to ON nodes
16531
+ WHEN NEW.valid_to IS NULL
16532
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
16533
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_delete AFTER DELETE ON nodes WHEN OLD.valid_to IS NULL
16534
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
16535
+ db.exec(`INSERT INTO nodes_text(rowid, description) SELECT rowid, description FROM nodes WHERE valid_to IS NULL`);
16536
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "installed");
16537
+ });
16538
+ }
16539
+ function textIndexReady(db) {
16540
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
16541
+ }
16542
+
16511
16543
  // ../../packages/math/src/constants.ts
16512
16544
  var NEMORI_DEDUP_COSINE = 0.88;
16513
16545
 
@@ -16983,6 +17015,7 @@ var SqliteGraphStore = class {
16983
17015
  installEditPlaceholderRetirement(this.db);
16984
17016
  installCloudAttrCompaction(this.db);
16985
17017
  installHistoryAttrSlim(this.db);
17018
+ installTextIndex(this.db);
16986
17019
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
16987
17020
  this.db.transaction(() => {
16988
17021
  this.repairSessionAliases();
@@ -17637,6 +17670,20 @@ var SqliteGraphStore = class {
17637
17670
  }
17638
17671
  searchTextCandidates(terms) {
17639
17672
  if (terms.length === 0) return [];
17673
+ const indexed = terms.filter((t) => t.length >= TEXT_INDEX_MIN_TERM);
17674
+ if (indexed.length > 0 && textIndexReady(this.db)) {
17675
+ const short = terms.filter((t) => t.length < TEXT_INDEX_MIN_TERM).map((t) => t.toLowerCase());
17676
+ const rows2 = this.db.prepare(`WITH hits AS MATERIALIZED (
17677
+ SELECT t.rowid AS rid, COUNT(*) AS matched FROM json_each(:terms) q, nodes_text t
17678
+ WHERE t.description LIKE '%' || q.value || '%' GROUP BY t.rowid
17679
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, hits.matched FROM hits JOIN nodes ON nodes.rowid = hits.rid
17680
+ WHERE nodes.valid_to IS NULL ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(indexed) });
17681
+ return rows2.map((row) => {
17682
+ const text = row.description.toLowerCase();
17683
+ const extra = short.filter((t) => text.includes(t)).length;
17684
+ return { node: textSearchNode(row), matched: row.matched + extra };
17685
+ });
17686
+ }
17640
17687
  const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
17641
17688
  SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
17642
17689
  WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
@@ -17645,6 +17692,18 @@ var SqliteGraphStore = class {
17645
17692
  JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
17646
17693
  return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
17647
17694
  }
17695
+ commentAnchors(commentIds) {
17696
+ const out = /* @__PURE__ */ new Map();
17697
+ if (commentIds.length === 0) return out;
17698
+ const rows = this.db.prepare(`WITH a AS MATERIALIZED (
17699
+ SELECT q.value AS cid, COALESCE(
17700
+ (SELECT to_id FROM edges WHERE from_id = q.value AND type IN ('EXPLAINS', 'ANNOTATES') AND valid_to IS NULL LIMIT 1),
17701
+ (SELECT from_id FROM edges WHERE to_id = q.value AND type = 'DOCUMENTED_BY' AND valid_to IS NULL LIMIT 1)
17702
+ ) AS aid FROM json_each(:ids) q
17703
+ ) SELECT a.cid AS cid, ${TEXT_SEARCH_COLUMNS} FROM a JOIN nodes ON nodes.id = a.aid AND nodes.valid_to IS NULL`).all({ ids: JSON.stringify(commentIds) });
17704
+ for (const row of rows) out.set(row.cid, textSearchNode(row));
17705
+ return out;
17706
+ }
17648
17707
  getTextSearchNode(id) {
17649
17708
  const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
17650
17709
  return row ? textSearchNode(row) : null;
package/errata.mjs CHANGED
@@ -18483,6 +18483,44 @@ var init_history_attr_slim = __esm({
18483
18483
  }
18484
18484
  });
18485
18485
 
18486
+ // ../../packages/local-graph/src/text-index.ts
18487
+ function installTextIndex(db) {
18488
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)) return;
18489
+ try {
18490
+ db.exec(`CREATE VIRTUAL TABLE temp.fts5_probe USING fts5(x, tokenize='trigram'); DROP TABLE temp.fts5_probe`);
18491
+ } catch {
18492
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "unavailable");
18493
+ return;
18494
+ }
18495
+ db.transaction(() => {
18496
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_text
18497
+ USING fts5(description, content='nodes', content_rowid='rowid', tokenize='trigram')`);
18498
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_insert AFTER INSERT ON nodes WHEN NEW.valid_to IS NULL
18499
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
18500
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_out AFTER UPDATE OF description, valid_to ON nodes
18501
+ WHEN OLD.valid_to IS NULL
18502
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
18503
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_in AFTER UPDATE OF description, valid_to ON nodes
18504
+ WHEN NEW.valid_to IS NULL
18505
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
18506
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_delete AFTER DELETE ON nodes WHEN OLD.valid_to IS NULL
18507
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
18508
+ db.exec(`INSERT INTO nodes_text(rowid, description) SELECT rowid, description FROM nodes WHERE valid_to IS NULL`);
18509
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "installed");
18510
+ });
18511
+ }
18512
+ function textIndexReady(db) {
18513
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
18514
+ }
18515
+ var TEXT_INDEX_KEY, TEXT_INDEX_MIN_TERM;
18516
+ var init_text_index = __esm({
18517
+ "../../packages/local-graph/src/text-index.ts"() {
18518
+ "use strict";
18519
+ TEXT_INDEX_KEY = "text-index-trigram-1";
18520
+ TEXT_INDEX_MIN_TERM = 3;
18521
+ }
18522
+ });
18523
+
18486
18524
  // ../../packages/math/src/constants.ts
18487
18525
  var SURPRISE_MOMENTUM, SURPRISE_LEARNING_RATE, SURPRISE_LEARNING_RATE_SCALING, TIER_HALFLIFE_MULTIPLIER, HIT_RATE_SCALING_ALPHA, BASE_HALF_LIFE_SEQ, MOMENTUM_GATE_FLOOR, SURPRISE_RECENCY_LAMBDA_SEQ, NEMORI_SOFT_WRITE_MIN_COSINE, NEMORI_SOFT_WRITE_MAX_COSINE, NEMORI_DEDUP_COSINE, NEMORI_SOFT_ALPHA_MIN, NEMORI_SOFT_ALPHA_MAX, NEMORI_HIGH_SURPRISE_THRESHOLD, NEMORI_PREDICTION_FLOOR, REVIEW_SURPRISE_THRESHOLD, REVIEW_HITS_THRESHOLD;
18488
18526
  var init_constants = __esm({
@@ -19426,7 +19464,7 @@ function graphStoreNeedsPreparation(path2) {
19426
19464
  for (const match2 of (SCHEMA_SQL + TEMPORAL_INDEX_SQL).matchAll(/CREATE INDEX IF NOT EXISTS (\w+)/g)) {
19427
19465
  if (!indexes.has(match2[1])) return true;
19428
19466
  }
19429
- return [SESSION_REPAIR_KEY, HISTORY_CACHE_FLOOR_KEY, EDIT_PLACEHOLDER_FLOOR_KEY, CLOUD_ATTR_COMPACTION_KEY, HISTORY_ATTR_SLIM_KEY].some((key) => !db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
19467
+ return [SESSION_REPAIR_KEY, HISTORY_CACHE_FLOOR_KEY, EDIT_PLACEHOLDER_FLOOR_KEY, CLOUD_ATTR_COMPACTION_KEY, HISTORY_ATTR_SLIM_KEY, TEXT_INDEX_KEY].some((key) => !db.prepare("SELECT value FROM store_meta WHERE key = ?").get(key));
19430
19468
  } catch {
19431
19469
  return true;
19432
19470
  } finally {
@@ -19504,6 +19542,7 @@ var init_store = __esm({
19504
19542
  init_edit_placeholder_retirement();
19505
19543
  init_cloud_attr_compaction();
19506
19544
  init_history_attr_slim();
19545
+ init_text_index();
19507
19546
  init_src3();
19508
19547
  PAGE_RANK_SCALE_KEY = "pageRankScale.p95.v1";
19509
19548
  TEXT_SEARCH_COLUMNS = `id, label, description, page_rank,
@@ -19716,6 +19755,7 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
19716
19755
  installEditPlaceholderRetirement(this.db);
19717
19756
  installCloudAttrCompaction(this.db);
19718
19757
  installHistoryAttrSlim(this.db);
19758
+ installTextIndex(this.db);
19719
19759
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
19720
19760
  this.db.transaction(() => {
19721
19761
  this.repairSessionAliases();
@@ -20370,6 +20410,20 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
20370
20410
  }
20371
20411
  searchTextCandidates(terms) {
20372
20412
  if (terms.length === 0) return [];
20413
+ const indexed = terms.filter((t) => t.length >= TEXT_INDEX_MIN_TERM);
20414
+ if (indexed.length > 0 && textIndexReady(this.db)) {
20415
+ const short = terms.filter((t) => t.length < TEXT_INDEX_MIN_TERM).map((t) => t.toLowerCase());
20416
+ const rows2 = this.db.prepare(`WITH hits AS MATERIALIZED (
20417
+ SELECT t.rowid AS rid, COUNT(*) AS matched FROM json_each(:terms) q, nodes_text t
20418
+ WHERE t.description LIKE '%' || q.value || '%' GROUP BY t.rowid
20419
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, hits.matched FROM hits JOIN nodes ON nodes.rowid = hits.rid
20420
+ WHERE nodes.valid_to IS NULL ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(indexed) });
20421
+ return rows2.map((row) => {
20422
+ const text = row.description.toLowerCase();
20423
+ const extra = short.filter((t) => text.includes(t)).length;
20424
+ return { node: textSearchNode(row), matched: row.matched + extra };
20425
+ });
20426
+ }
20373
20427
  const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
20374
20428
  SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
20375
20429
  WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
@@ -20378,6 +20432,18 @@ CREATE INDEX IF NOT EXISTS nodes_enrichment_pending ON nodes(json_extract(attrs_
20378
20432
  JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
20379
20433
  return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
20380
20434
  }
20435
+ commentAnchors(commentIds) {
20436
+ const out2 = /* @__PURE__ */ new Map();
20437
+ if (commentIds.length === 0) return out2;
20438
+ const rows = this.db.prepare(`WITH a AS MATERIALIZED (
20439
+ SELECT q.value AS cid, COALESCE(
20440
+ (SELECT to_id FROM edges WHERE from_id = q.value AND type IN ('EXPLAINS', 'ANNOTATES') AND valid_to IS NULL LIMIT 1),
20441
+ (SELECT from_id FROM edges WHERE to_id = q.value AND type = 'DOCUMENTED_BY' AND valid_to IS NULL LIMIT 1)
20442
+ ) AS aid FROM json_each(:ids) q
20443
+ ) SELECT a.cid AS cid, ${TEXT_SEARCH_COLUMNS} FROM a JOIN nodes ON nodes.id = a.aid AND nodes.valid_to IS NULL`).all({ ids: JSON.stringify(commentIds) });
20444
+ for (const row of rows) out2.set(row.cid, textSearchNode(row));
20445
+ return out2;
20446
+ }
20381
20447
  getTextSearchNode(id) {
20382
20448
  const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
20383
20449
  return row ? textSearchNode(row) : null;
@@ -32983,42 +33049,77 @@ function burstStaticConductance(type, seedLabel) {
32983
33049
  function burstConductance(edge2, seedLabel) {
32984
33050
  return learnedConductance(burstStaticConductance(edge2.type, seedLabel), edge2);
32985
33051
  }
32986
- function localBurst(store, seedId, opts = {}) {
32987
- const maxHops = opts.maxHops ?? 3;
32988
- const limit = opts.limit ?? 20;
32989
- const nodeCap = opts.nodeCap ?? 200;
32990
- const seed = store.getNode(seedId);
32991
- if (!seed) return [];
32992
- const act = /* @__PURE__ */ new Map([[seedId, 1]]);
32993
- const hopOf = /* @__PURE__ */ new Map([[seedId, 0]]);
33052
+ function spreadActivation(store, seeds, opts) {
33053
+ const act = /* @__PURE__ */ new Map();
33054
+ const hopOf = /* @__PURE__ */ new Map();
32994
33055
  const viaOf = /* @__PURE__ */ new Map();
33056
+ const originLabel = /* @__PURE__ */ new Map();
33057
+ const bestArrival = /* @__PURE__ */ new Map();
33058
+ const seedIds = new Set(seeds.map((s) => s.id));
32995
33059
  const expanded = /* @__PURE__ */ new Set();
32996
33060
  const frontier = new ActivationHeap();
32997
- frontier.push(seedId, 1);
33061
+ for (const s of seeds) {
33062
+ act.set(s.id, (act.get(s.id) ?? 0) + s.weight);
33063
+ hopOf.set(s.id, 0);
33064
+ if (!originLabel.has(s.id)) originLabel.set(s.id, s.label);
33065
+ }
33066
+ for (const s of seeds) frontier.push(s.id, act.get(s.id));
32998
33067
  let strongest = 0;
32999
- while (frontier.size > 0 && expanded.size < nodeCap) {
33068
+ while (frontier.size > 0 && expanded.size < opts.nodeCap) {
33000
33069
  const top = frontier.pop();
33001
33070
  if (expanded.has(top.id) || top.a < (act.get(top.id) ?? 0)) continue;
33002
33071
  if (top.a < ACTIVATION_EPSILON || top.a < strongest * BURST_RELATIVE_FLOOR) break;
33003
33072
  expanded.add(top.id);
33004
33073
  const hops = hopOf.get(top.id) ?? 0;
33005
- if (hops >= maxHops) continue;
33074
+ if (hops >= opts.maxHops) continue;
33075
+ const label = originLabel.get(top.id) ?? "";
33006
33076
  const edges = [
33007
33077
  ...store.outEdges(top.id).map((e) => ({ e, nb: e.to, forward: true })),
33008
33078
  ...store.inEdges(top.id).map((e) => ({ e, nb: e.from, forward: false }))
33009
33079
  ].filter(({ e }) => walksRelatedness(e.type));
33010
33080
  const damp = BURST_STEP_DAMPING / Math.sqrt(Math.max(1, edges.length));
33011
33081
  for (const { e, nb, forward } of edges) {
33012
- if (nb === seedId || expanded.has(nb)) continue;
33013
- const a = top.a * burstConductance(e, seed.label) * damp;
33082
+ if (nb === top.id) continue;
33083
+ const a = top.a * burstConductance(e, label) * damp;
33084
+ if (opts.accumulate) {
33085
+ if (a < ACTIVATION_EPSILON) continue;
33086
+ const total = (act.get(nb) ?? 0) + a;
33087
+ act.set(nb, total);
33088
+ if (a > (bestArrival.get(nb) ?? 0)) {
33089
+ bestArrival.set(nb, a);
33090
+ if (!seedIds.has(nb)) {
33091
+ hopOf.set(nb, hops + 1);
33092
+ viaOf.set(nb, { edgeType: e.type, parentId: top.id, forward });
33093
+ originLabel.set(nb, label);
33094
+ }
33095
+ }
33096
+ if (!seedIds.has(nb) && total > strongest) strongest = total;
33097
+ if (!expanded.has(nb)) frontier.push(nb, total);
33098
+ continue;
33099
+ }
33100
+ if (seedIds.has(nb) || expanded.has(nb)) continue;
33014
33101
  if (a <= (act.get(nb) ?? 0)) continue;
33015
33102
  act.set(nb, a);
33016
33103
  if (a > strongest) strongest = a;
33017
33104
  hopOf.set(nb, hops + 1);
33018
33105
  viaOf.set(nb, { edgeType: e.type, parentId: top.id, forward });
33106
+ originLabel.set(nb, label);
33019
33107
  frontier.push(nb, a);
33020
33108
  }
33021
33109
  }
33110
+ return { act, hopOf, viaOf };
33111
+ }
33112
+ function localBurst(store, seedId, opts = {}) {
33113
+ const maxHops = opts.maxHops ?? 3;
33114
+ const limit = opts.limit ?? 20;
33115
+ const nodeCap = opts.nodeCap ?? 200;
33116
+ const seed = store.getNode(seedId);
33117
+ if (!seed) return [];
33118
+ const { act, hopOf, viaOf } = spreadActivation(store, [{ id: seedId, weight: 1, label: seed.label }], {
33119
+ maxHops,
33120
+ nodeCap,
33121
+ accumulate: false
33122
+ });
33022
33123
  act.delete(seedId);
33023
33124
  const ctxSeed = seed.embedding.length > 0 ? { seedEmbedding: seed.embedding, seedEmbeddingVersion: seed.attrs["embeddingVersion"] } : {};
33024
33125
  const gated = seed.embedding.length > 0;
@@ -33040,6 +33141,32 @@ function localBurst(store, seedId, opts = {}) {
33040
33141
  return { id: node2.id, score: score2, components: r.components, node: node2, hops: hopOf.get(node2.id) ?? 0, ...via ? { via } : {} };
33041
33142
  }).filter((r) => !gated || r.score > 0).sort((a, b) => b.score - a.score).slice(0, limit);
33042
33143
  }
33144
+ function burstFromSeeds(store, seeds, opts = {}) {
33145
+ const live3 = seeds.filter((s) => s.weight > 0);
33146
+ if (live3.length === 0) return [];
33147
+ const { act, hopOf, viaOf } = spreadActivation(store, live3, {
33148
+ maxHops: opts.maxHops ?? 3,
33149
+ nodeCap: opts.nodeCap ?? 300,
33150
+ accumulate: true
33151
+ });
33152
+ const seedIds = new Set(live3.map((s) => s.id));
33153
+ const labelOf = store.nodeLabels([...act.keys()]);
33154
+ const pool = (keep) => [...act.entries()].filter(([id]) => keep(labelOf.get(id) ?? "")).sort((x, y) => y[1] - x[1]).slice(0, BURST_SCORED_CANDIDATES);
33155
+ const isKnowledge = (label) => typeRelevance(label) >= KNOWLEDGE_TYPE_RELEVANCE;
33156
+ const reached = [...new Map([...pool(isKnowledge), ...pool((l) => !isKnowledge(l))]).entries()].map(([id, a]) => ({ node: store.getNode(id), a })).filter((x) => x.node != null);
33157
+ const maxAct = Math.max(1e-12, ...reached.map((x) => x.a));
33158
+ const weights = { centralityScale: store.pageRankScale?.() ?? 1, ...opts.weights };
33159
+ const now = store.currentIngestSeq();
33160
+ const typed = opts.typeRelevanceForSeeds ?? true;
33161
+ const hubDamp = (id) => seedIds.has(id) ? 1 : 1 / Math.sqrt(Math.max(1, store.outEdges(id).length + store.inEdges(id).length));
33162
+ return reached.map(({ node: node2, a }) => {
33163
+ const r = scoreCandidate(node2, { decay: nodeFreshness(node2, now) }, weights);
33164
+ const type = typed || !seedIds.has(node2.id) ? typeRelevance(node2.label) : 1;
33165
+ const score2 = a / maxAct * r.score * type * hubDamp(node2.id);
33166
+ const via = viaOf.get(node2.id);
33167
+ return { id: node2.id, score: score2, components: r.components, node: node2, hops: hopOf.get(node2.id) ?? 0, ...via ? { via } : {} };
33168
+ }).sort((a, b) => b.score - a.score).slice(0, opts.limit ?? 20);
33169
+ }
33043
33170
  var BURST_STEP_DAMPING, ACTIVATION_EPSILON, BURST_RELATIVE_FLOOR, BURST_SCORED_CANDIDATES, KNOWLEDGE_TYPE_RELEVANCE, BURST_ANCHORED_AT_CONDUCTANCE, BURST_DEPENDS_ON_FLOOR, ActivationHeap;
33044
33171
  var init_relevance_rank = __esm({
33045
33172
  "../../packages/generalizer/src/relevance-rank.ts"() {
@@ -33165,6 +33292,7 @@ __export(src_exports3, {
33165
33292
  bindCanonicalNeighbors: () => bindCanonicalNeighbors,
33166
33293
  bindResolvedRegressions: () => bindResolvedRegressions,
33167
33294
  burstConductance: () => burstConductance,
33295
+ burstFromSeeds: () => burstFromSeeds,
33168
33296
  burstStaticConductance: () => burstStaticConductance,
33169
33297
  closeProblem: () => closeProblem,
33170
33298
  commandSignature: () => commandSignature,
@@ -35652,8 +35780,28 @@ function mergeDualBurst(local, cloud, opts = {}) {
35652
35780
  if (parent === n.id) delete n.via;
35653
35781
  else if (parent !== n.via.parentId) n.via = { ...n.via, parentId: parent };
35654
35782
  }
35783
+ if (opts.fuse === "rank") fuseByRank();
35655
35784
  nodes.sort((a, b) => b.score - a.score);
35656
35785
  return { nodes, collectiveCount, corroboratedCount };
35786
+ function fuseByRank() {
35787
+ const localRank = /* @__PURE__ */ new Map();
35788
+ local.filter((l) => !isSeedTwin(l.id)).forEach((l, i2) => {
35789
+ const n = byId.get(l.id) ?? byId.get(recordedBy.get(l.id) ?? "");
35790
+ if (n && !localRank.has(n.id)) localRank.set(n.id, i2);
35791
+ });
35792
+ const cloudRank = /* @__PURE__ */ new Map();
35793
+ cloud.forEach((c, i2) => {
35794
+ if (isSeedTwin(c.id)) return;
35795
+ const mine = twins?.localOf(c.id);
35796
+ const n = owner.get(c.id) ?? byId.get(c.id) ?? (mine ? byId.get(mine.id) : void 0);
35797
+ if (n && !cloudRank.has(n.id)) cloudRank.set(n.id, i2);
35798
+ });
35799
+ for (const n of nodes) {
35800
+ const lr = localRank.get(n.id);
35801
+ const cr = cloudRank.get(n.id);
35802
+ n.score = (lr === void 0 ? 0 : 1 / (SEARCH_FUSE_K + lr + 1)) + (cr === void 0 ? 0 : SEARCH_FUSE_NETWORK_WEIGHT / (SEARCH_FUSE_K + cr + 1));
35803
+ }
35804
+ }
35657
35805
  function canonicalId(id) {
35658
35806
  if (opts.seedId && isSeedTwin(id)) return opts.seedId;
35659
35807
  return owner.get(id)?.id ?? recordedBy.get(id) ?? twins?.localOf(id)?.id ?? id;
@@ -35735,11 +35883,13 @@ function searchResultToCloudHits(result) {
35735
35883
  };
35736
35884
  });
35737
35885
  }
35738
- var DUAL_BURST_SEMANTIC_NEUTRAL;
35886
+ var DUAL_BURST_SEMANTIC_NEUTRAL, SEARCH_FUSE_K, SEARCH_FUSE_NETWORK_WEIGHT;
35739
35887
  var init_dual_burst = __esm({
35740
35888
  "src/dual-burst.ts"() {
35741
35889
  "use strict";
35742
35890
  DUAL_BURST_SEMANTIC_NEUTRAL = 0.3;
35891
+ SEARCH_FUSE_K = 10;
35892
+ SEARCH_FUSE_NETWORK_WEIGHT = 0.7;
35743
35893
  }
35744
35894
  });
35745
35895
 
@@ -35874,7 +36024,8 @@ async function dualAugment(opts) {
35874
36024
  const parent = c.via?.parentId;
35875
36025
  return parent ? [c.id, parent] : [c.id];
35876
36026
  })),
35877
- ...seedNode ? { seedId: seedNode.id } : {}
36027
+ ...seedNode ? { seedId: seedNode.id } : {},
36028
+ ...opts.fuse ? { fuse: opts.fuse } : {}
35878
36029
  }),
35879
36030
  seedProvenance,
35880
36031
  seedUsageCount
@@ -45027,10 +45178,17 @@ __export(mcp_exports, {
45027
45178
  INSTRUCTIONS: () => INSTRUCTIONS,
45028
45179
  RG_REFUTED_DAMP: () => RG_REFUTED_DAMP,
45029
45180
  RG_SUSPECT_BOOST: () => RG_SUSPECT_BOOST,
45181
+ SEARCH_CODE_SEEDS: () => SEARCH_CODE_SEEDS,
45182
+ SEARCH_KNOWLEDGE_SEEDS: () => SEARCH_KNOWLEDGE_SEEDS,
45030
45183
  createMcpHandler: () => createMcpHandler,
45031
45184
  runMcpServer: () => runMcpServer,
45032
45185
  runTool: () => runTool,
45033
- searchGraph: () => searchGraph
45186
+ searchEntries: () => searchEntries,
45187
+ searchGraph: () => searchGraph,
45188
+ searchGraphWalk: () => searchGraphWalk,
45189
+ searchWalkEnabled: () => searchWalkEnabled,
45190
+ textMatches: () => textMatches,
45191
+ withLinks: () => withLinks
45034
45192
  });
45035
45193
  import { existsSync as existsSync17, readFileSync as readFileSync16 } from "node:fs";
45036
45194
  import { resolve as resolve4 } from "node:path";
@@ -45152,49 +45310,166 @@ function localCommunityOf(node2) {
45152
45310
  return typeof cid === "string" ? cid : null;
45153
45311
  }
45154
45312
  function searchGraph(store, query, limit) {
45313
+ if (!Number.isFinite(limit) || limit < 1) return [];
45314
+ return textMatches(store, query).slice(0, limit).map((m) => toSearchHit(m.node, m.via, m.rg));
45315
+ }
45316
+ function toSearchHit(node2, via, rg) {
45317
+ return {
45318
+ id: node2.id,
45319
+ label: node2.label,
45320
+ description: node2.description,
45321
+ pageRank: node2.pageRank,
45322
+ ...via ? { via } : {},
45323
+ ...rg.state ? { state: rg.state } : {},
45324
+ ...rg.refutedWhy ? { refutedWhy: rg.refutedWhy } : {}
45325
+ };
45326
+ }
45327
+ function textMatches(store, query) {
45155
45328
  const q = query.trim();
45156
- if (!q || !Number.isFinite(limit) || limit < 1) return [];
45329
+ if (!q) return [];
45157
45330
  const terms = q.split(/\s+/).filter((t) => t.length >= 2);
45158
45331
  const raw2 = /* @__PURE__ */ new Map();
45159
45332
  const searchTerms = terms.length <= 1 ? [q] : terms;
45160
45333
  const scored = store.searchTextCandidates ? store.searchTextCandidates(searchTerms) : store.searchDescriptionTerms(searchTerms, Number.MAX_SAFE_INTEGER);
45161
45334
  for (const hit of scored) raw2.set(hit.node.id, hit);
45335
+ const lowered = searchTerms.map((t) => t.toLowerCase());
45336
+ const hasTerm = /* @__PURE__ */ new Map();
45337
+ const df = lowered.map(() => 0);
45338
+ for (const { node: node2 } of raw2.values()) {
45339
+ const text = node2.description.toLowerCase();
45340
+ const has = lowered.map((t) => text.includes(t));
45341
+ has.forEach((h, i2) => {
45342
+ if (h) df[i2] += 1;
45343
+ });
45344
+ hasTerm.set(node2.id, has);
45345
+ }
45346
+ const idf = df.map((d) => Math.log(1 + raw2.size / (1 + d)));
45347
+ const idfTotal = idf.reduce((a, b) => a + b, 0) || 1;
45348
+ const coverageOf = (id) => {
45349
+ const has = hasTerm.get(id);
45350
+ return has ? has.reduce((a, h, i2) => a + (h ? idf[i2] : 0), 0) / idfTotal : 0;
45351
+ };
45162
45352
  const merged = /* @__PURE__ */ new Map();
45163
- const add = (node2, matched, via) => {
45353
+ const add = (node2, matched, coverage, via) => {
45164
45354
  const e = merged.get(node2.id);
45165
45355
  if (e) {
45166
45356
  e.matched = Math.max(e.matched, matched);
45357
+ e.coverage = Math.max(e.coverage, coverage);
45167
45358
  if (!e.via && via) e.via = via;
45168
45359
  } else {
45169
- merged.set(node2.id, { node: node2, matched, via });
45360
+ merged.set(node2.id, { node: node2, matched, coverage, via });
45170
45361
  }
45171
45362
  };
45172
45363
  const anchors = /* @__PURE__ */ new Map();
45364
+ const batched = store.commentAnchors?.(
45365
+ [...raw2.values()].filter((h) => h.node.label === "Comment").map((h) => h.node.id)
45366
+ );
45173
45367
  for (const { node: node2, matched } of raw2.values()) {
45174
45368
  if (node2.label === "Comment") {
45175
- const anchorId = commentAnchorId(store, node2.id);
45176
- if (anchorId && !anchors.has(anchorId)) {
45177
- anchors.set(anchorId, store.getTextSearchNode ? store.getTextSearchNode(anchorId) : store.getNode(anchorId));
45369
+ let anchor;
45370
+ if (batched) {
45371
+ anchor = batched.get(node2.id) ?? null;
45372
+ } else {
45373
+ const anchorId = commentAnchorId(store, node2.id);
45374
+ if (anchorId && !anchors.has(anchorId)) {
45375
+ anchors.set(anchorId, store.getTextSearchNode ? store.getTextSearchNode(anchorId) : store.getNode(anchorId));
45376
+ }
45377
+ anchor = anchorId ? anchors.get(anchorId) : null;
45178
45378
  }
45179
- const anchor = anchorId ? anchors.get(anchorId) : null;
45180
45379
  if (anchor) {
45181
- add(anchor, matched, { kind: "comment", text: node2.description });
45380
+ add(anchor, matched, coverageOf(node2.id), { kind: "comment", text: node2.description });
45182
45381
  continue;
45183
45382
  }
45184
45383
  }
45185
- add(node2, matched);
45384
+ add(node2, matched, coverageOf(node2.id));
45186
45385
  }
45187
45386
  return [...merged.values()].map((e) => ({ ...e, rg: hitState(e.node) })).sort(
45188
45387
  (a, b) => rankTier(a.node) - rankTier(b.node) || b.matched - a.matched || b.node.pageRank * b.rg.factor - a.node.pageRank * a.rg.factor
45189
- ).slice(0, limit).map(({ node: node2, via, rg }) => ({
45190
- id: node2.id,
45191
- label: node2.label,
45192
- description: node2.description,
45193
- pageRank: node2.pageRank,
45194
- ...via ? { via } : {},
45195
- ...rg.state ? { state: rg.state } : {},
45196
- ...rg.refutedWhy ? { refutedWhy: rg.refutedWhy } : {}
45197
- }));
45388
+ ).map(({ node: node2, via, rg, coverage, matched }) => ({ node: node2, via, rg, coverage, matched }));
45389
+ }
45390
+ function isIdentifierToken(t) {
45391
+ return t.length >= 4 && IDENTIFIER_TOKEN.test(t) && (/[a-z][A-Z]/.test(t) || /_/.test(t) || t.includes("."));
45392
+ }
45393
+ function symbolName(description) {
45394
+ return description.split(/[\s—(]/, 1)[0] ?? "";
45395
+ }
45396
+ function withLinks(store, hits) {
45397
+ const idOf = (h) => String(h.id ?? "");
45398
+ const ids = new Set(hits.map(idOf));
45399
+ return hits.map((h) => {
45400
+ const id = idOf(h);
45401
+ const links = store.outEdges(id).filter((e) => e.to !== id && ids.has(e.to)).map((e) => ({ edgeType: e.type, to: e.to }));
45402
+ return links.length > 0 ? { ...h, links } : h;
45403
+ });
45404
+ }
45405
+ function searchEntries(query, matches) {
45406
+ const words = query.trim().split(/\s+/).filter(Boolean);
45407
+ const idents = new Set(words.filter(isIdentifierToken));
45408
+ const phrases = [];
45409
+ for (let i2 = 0; i2 + PHRASE_WORDS <= words.length; i2++) {
45410
+ phrases.push(words.slice(i2, i2 + PHRASE_WORDS).join(" ").toLowerCase());
45411
+ }
45412
+ const seeds = /* @__PURE__ */ new Map();
45413
+ const put = (m, weight) => {
45414
+ const w = weight * m.rg.factor;
45415
+ const prev = seeds.get(m.node.id);
45416
+ if (!prev || prev.weight < w) seeds.set(m.node.id, { id: m.node.id, label: m.node.label, weight: w });
45417
+ };
45418
+ for (const m of matches) {
45419
+ if (idents.size === 0) break;
45420
+ const name2 = symbolName(m.node.description);
45421
+ const last = name2.slice(name2.lastIndexOf(".") + 1);
45422
+ if (idents.has(name2) || idents.has(last)) put(m, m.coverage * m.coverage * (SEED_TIER_WEIGHT[rankTier(m.node)] ?? 1));
45423
+ }
45424
+ const knowledge = matches.filter((m) => KNOWLEDGE_LABELS.has(m.node.label)).map((m) => {
45425
+ const text = m.node.description.toLowerCase();
45426
+ return { m, w: phrases.some((ph) => text.includes(ph)) ? 1 : m.coverage * m.coverage };
45427
+ }).filter(({ m, w }) => w === 1 || (words.length < 2 || m.matched >= 2) && m.coverage >= DEFAULT_MIN_COVERAGE).sort((a, b) => b.w - a.w).slice(0, SEARCH_KNOWLEDGE_SEEDS);
45428
+ for (const { m, w } of knowledge) put(m, w);
45429
+ const code = matches.filter((m) => !KNOWLEDGE_LABELS.has(m.node.label)).map((m) => ({ m, w: m.coverage * m.coverage * (SEED_TIER_WEIGHT[rankTier(m.node)] ?? 1) })).sort((a, b) => b.w - a.w).slice(0, SEARCH_CODE_SEEDS);
45430
+ for (const { m, w } of code) put(m, w);
45431
+ return [...seeds.values()].filter((sd) => sd.weight > 0);
45432
+ }
45433
+ function withoutSymbolInternals(store, ranked) {
45434
+ const ids = new Set(ranked.map((r) => r.id));
45435
+ const internal = /* @__PURE__ */ new Set();
45436
+ for (const r of ranked) {
45437
+ for (const e of store.inEdges(r.id, ["DEFINES"])) {
45438
+ if (!ids.has(e.from)) continue;
45439
+ const parent = store.getNode(e.from);
45440
+ if (parent && SYMBOL_PARENT_LABELS.has(parent.label)) internal.add(r.id);
45441
+ }
45442
+ }
45443
+ return internal.size > 0 ? ranked.filter((r) => !internal.has(r.id)) : ranked;
45444
+ }
45445
+ function searchWalkEnabled(env2 = process.env) {
45446
+ return !/^(0|off|false|no)$/i.test((env2["ERRATA_SEARCH_WALK"] ?? "").trim());
45447
+ }
45448
+ function searchGraphWalk(store, query, limit) {
45449
+ if (!Number.isFinite(limit) || limit < 1) return [];
45450
+ const matches = textMatches(store, query);
45451
+ if (matches.length === 0) return [];
45452
+ const byId = new Map(matches.map((m) => [m.node.id, m]));
45453
+ const walked = withoutSymbolInternals(
45454
+ store,
45455
+ burstFromSeeds(store, searchEntries(query, matches), { limit: limit * 4, typeRelevanceForSeeds: false })
45456
+ );
45457
+ const lanes = [walked.filter((r) => KNOWLEDGE_LABELS.has(r.node.label)), walked.filter((r) => !KNOWLEDGE_LABELS.has(r.node.label))];
45458
+ const ranked = [];
45459
+ for (let i2 = 0; ranked.length < limit && i2 < walked.length; i2++) {
45460
+ for (const lane of lanes) if (lane[i2] && ranked.length < limit) ranked.push(lane[i2]);
45461
+ }
45462
+ return ranked.map((r) => {
45463
+ const m = byId.get(r.id);
45464
+ if (m) return { ...toSearchHit(m.node, m.via, m.rg), score: r.score, matched: true };
45465
+ const rg = hitState(r.node);
45466
+ return {
45467
+ ...toSearchHit(r.node, void 0, rg),
45468
+ score: r.score,
45469
+ matched: false,
45470
+ ...r.via ? { reachedVia: { edgeType: r.via.edgeType, from: r.via.parentId } } : {}
45471
+ };
45472
+ });
45198
45473
  }
45199
45474
  function runTool(name2, args2, store, ctx = {}) {
45200
45475
  const tool = TOOLS.find((t) => t.name === name2);
@@ -45530,7 +45805,7 @@ async function runMcpServer(workspaceRoot, options = {}) {
45530
45805
  if (resp) process.stdout.write(JSON.stringify(resp) + "\n");
45531
45806
  }
45532
45807
  }
45533
- var INSTRUCTIONS, AUDIT_RUN_THRESHOLD, AUDIT_NUDGE_COOLDOWN, DISCRIMINATOR_NUDGE_COOLDOWN, AUDIT_FLAG_NUDGE, RG_REFUTED_DAMP, RG_SUSPECT_BOOST, TOOLS;
45808
+ var INSTRUCTIONS, AUDIT_RUN_THRESHOLD, AUDIT_NUDGE_COOLDOWN, DISCRIMINATOR_NUDGE_COOLDOWN, AUDIT_FLAG_NUDGE, RG_REFUTED_DAMP, RG_SUSPECT_BOOST, SEARCH_KNOWLEDGE_SEEDS, SEARCH_CODE_SEEDS, PHRASE_WORDS, KNOWLEDGE_LABELS, IDENTIFIER_TOKEN, SYMBOL_PARENT_LABELS, SEED_TIER_WEIGHT, TOOLS;
45534
45809
  var init_mcp = __esm({
45535
45810
  "src/mcp.ts"() {
45536
45811
  "use strict";
@@ -45563,6 +45838,13 @@ var init_mcp = __esm({
45563
45838
  AUDIT_FLAG_NUDGE = "\u26A1 errata \u2014 reading/auditing? anything you notice that's off, flag it inline as `[!one line]` (`[?\u2026]` = TODO) \u2014 no tool call, we harvest it.";
45564
45839
  RG_REFUTED_DAMP = 0.75;
45565
45840
  RG_SUSPECT_BOOST = 1.25;
45841
+ SEARCH_KNOWLEDGE_SEEDS = 30;
45842
+ SEARCH_CODE_SEEDS = 15;
45843
+ PHRASE_WORDS = 3;
45844
+ KNOWLEDGE_LABELS = new Set(SEMANTIC_NODE_LABELS);
45845
+ IDENTIFIER_TOKEN = /^(?:[A-Za-z_$][\w$]*\.)*[A-Za-z_$][\w$]*$/;
45846
+ SYMBOL_PARENT_LABELS = /* @__PURE__ */ new Set(["Function", "Method", "Class", "Interface"]);
45847
+ SEED_TIER_WEIGHT = [1, 0.4, 0.4];
45566
45848
  TOOLS = [
45567
45849
  {
45568
45850
  name: "errata.switch_context",
@@ -45601,7 +45883,7 @@ var init_mcp = __esm({
45601
45883
  },
45602
45884
  {
45603
45885
  name: "errata.search",
45604
- description: "Find a symbol, string, or comment across the live code graph \u2014 reaches matches by structure, not just text. Multi-word queries are tokenized and OR'd (more terms matched ranks higher, then pageRank), so word order and punctuation don't matter. Returns up to `limit` hits with id, label, description, and pageRank (higher pageRank = more central \u2014 start there). A comment that documents a symbol resolves to that SYMBOL (the comment rides along as `via` \u2014 it's the why, not the destination); test scaffolding and floating comments sort below production code. When the collective is reachable, results BLEND cross-project knowledge, each tagged `provenance` (local | collective | corroborated) with `usageCount`.",
45886
+ description: "Find a symbol, string, comment, or the knowledge around them, as a GRAPH search. The query's words pick entry points \u2014 a symbol an identifier names, knowledge carrying the query's words (a literal phrase counts most), the strongest code matches \u2014 and one walk from all of them ranks what sits at their centre, so a Problem anchored on the function you named surfaces without repeating your words. Knowledge and code alternate down the list. Each hit carries `matched` (true = the text matched; false = the walk reached it, with `reachedVia` {edgeType, from}), `score`, and `links` \u2014 its edges to the OTHER hits in this answer, so a Problem \u2014CAUSED_BY\u2192 RootCause or a fix \u2014ANCHORED_AT\u2192 the function reads as a chain. Word order and punctuation don't matter. A comment that documents a symbol resolves to that SYMBOL (the comment rides along as `via` \u2014 it's the why, not the destination); test scaffolding and floating comments sort below production code. When the collective is reachable, results BLEND cross-project knowledge, each tagged `provenance` (local | collective | corroborated) with `usageCount`.",
45605
45887
  inputSchema: {
45606
45888
  type: "object",
45607
45889
  properties: {
@@ -45613,13 +45895,16 @@ var init_mcp = __esm({
45613
45895
  handler: async (args2, store, ctx) => {
45614
45896
  const query = String(args2["query"] ?? "");
45615
45897
  const limit = Math.max(1, Math.min(100, Number(args2["limit"] ?? 20)));
45616
- const hits = searchGraph(store, query, limit);
45898
+ const hits = withLinks(
45899
+ store,
45900
+ searchWalkEnabled() ? searchGraphWalk(store, query, limit) : searchGraph(store, query, limit).map((h) => ({ ...h, score: h.pageRank, matched: true }))
45901
+ );
45617
45902
  if (ctx.cloud && query.trim()) {
45618
45903
  const localHits = hits.map((h) => ({
45619
45904
  id: h.id,
45620
45905
  node: { label: h.label, description: h.description },
45621
- score: h.pageRank,
45622
- hops: 0
45906
+ score: h.score,
45907
+ hops: h.matched ? 0 : 1
45623
45908
  }));
45624
45909
  const dual = await dualAugment({
45625
45910
  store,
@@ -45627,7 +45912,8 @@ var init_mcp = __esm({
45627
45912
  ...ctx.generalizeSoft ? { generalizeSoft: ctx.generalizeSoft } : {},
45628
45913
  localHits,
45629
45914
  queryText: query,
45630
- limit
45915
+ limit,
45916
+ fuse: "rank"
45631
45917
  });
45632
45918
  if (dual.status === "merged") {
45633
45919
  return {
@@ -45637,7 +45923,7 @@ var init_mcp = __esm({
45637
45923
  count: dual.merged.nodes.length,
45638
45924
  corroborated: dual.merged.corroboratedCount,
45639
45925
  collective: dual.merged.collectiveCount,
45640
- results: formatDualNodes(dual.merged)
45926
+ results: withLinks(store, formatDualNodes(dual.merged))
45641
45927
  };
45642
45928
  }
45643
45929
  return {
@@ -77688,9 +77974,38 @@ function createSubagentState() {
77688
77974
  // src/subagent-context.ts
77689
77975
  init_src6();
77690
77976
  init_src8();
77691
- init_src5();
77692
77977
  init_src2();
77693
77978
  init_recall_signal();
77979
+
77980
+ // src/intent-walk.ts
77981
+ init_src8();
77982
+ init_src5();
77983
+ var INTENT_WALK_SEEDS = 30;
77984
+ var RECALLABLE2 = /* @__PURE__ */ new Set(["Problem", "Solution", "RootCause", "Pattern"]);
77985
+ function walkIntent(store, intent, opts) {
77986
+ const entries = searchByIntent(store, intent, { limit: INTENT_WALK_SEEDS });
77987
+ if (entries.length === 0) return [];
77988
+ const out2 = entries.slice(0, opts.limit);
77989
+ if (out2.length >= opts.limit) return out2;
77990
+ const shown = new Set(out2.map((h) => h.node.id));
77991
+ const seedIds = new Set(entries.map((h) => h.node.id));
77992
+ const ranked = burstFromSeeds(
77993
+ store,
77994
+ entries.map((h) => ({ id: h.node.id, label: h.node.label, weight: h.coverage * h.coverage })),
77995
+ { limit: opts.limit * 10, typeRelevanceForSeeds: false }
77996
+ );
77997
+ for (const r of ranked) {
77998
+ if (out2.length >= opts.limit) break;
77999
+ if (shown.has(r.id) || seedIds.has(r.id)) continue;
78000
+ const n = r.node;
78001
+ if (!RECALLABLE2.has(n.label) || n.attrs["mergedInto"] !== void 0) continue;
78002
+ if (n.label === "Problem" && n.attrs["resolvedAt"] != null && !isConstraintProblem(n)) continue;
78003
+ out2.push({ node: n, coverage: 0, matched: [] });
78004
+ }
78005
+ return out2;
78006
+ }
78007
+
78008
+ // src/subagent-context.ts
77694
78009
  var MAX_SHARED_PRIORS = 8;
77695
78010
  var INTENT_HITS = 4;
77696
78011
  function resolveSharedPriors(store, wanted, handleMap, remote) {
@@ -77713,7 +78028,7 @@ function renderSubagentBlock(args2) {
77713
78028
  const slice = sliceForFile(store, entry.workingFile, args2.atCommit);
77714
78029
  if (slice) snapshot.workingFile = slice;
77715
78030
  }
77716
- const hits = entry.intent ? searchByIntent(store, entry.intent, { limit: INTENT_HITS }).map((h) => ({
78031
+ const hits = entry.intent ? walkIntent(store, entry.intent, { limit: INTENT_HITS }).map((h) => ({
77717
78032
  id: h.node.id,
77718
78033
  label: h.node.label,
77719
78034
  description: h.node.description,
@@ -84076,7 +84391,7 @@ function createWatchBreaker(deps) {
84076
84391
  }
84077
84392
 
84078
84393
  // src/engine.ts
84079
- var DAEMON_VERSION = true ? "2.0.22-dev.4020" : "2.0.0-alpha.0";
84394
+ var DAEMON_VERSION = true ? "2.0.22-dev.4049" : "2.0.0-alpha.0";
84080
84395
  var IGNORED_PATH = /[\\/](?:\.git|node_modules|\.errata|\.claude|\.codex|\.cursor|\.turbo|\.next|dist|coverage|test-results|playwright-report|__pycache__)(?:[\\/]|$)/;
84081
84396
  var IGNORED_NOISE = /castalia\.db|eventlog\.sqlite|turn-cursors/;
84082
84397
  var GIT_OP_MUTE_MS = 4e3;
@@ -84546,7 +84861,7 @@ function createWorkspaceEngine(opts) {
84546
84861
  const collective = intentRemote?.forIntent === intent ? intentRemote : null;
84547
84862
  return {
84548
84863
  text: intent,
84549
- hits: searchByIntent(store, intent, { limit: 4 }).map((h) => ({
84864
+ hits: walkIntent(store, intent, { limit: 4 }).map((h) => ({
84550
84865
  id: h.node.id,
84551
84866
  label: h.node.label,
84552
84867
  description: h.node.description,
@@ -15093,6 +15093,38 @@ function installHistoryAttrSlim(db) {
15093
15093
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
15094
15094
  }
15095
15095
 
15096
+ // ../../packages/local-graph/src/text-index.ts
15097
+ var TEXT_INDEX_KEY = "text-index-trigram-1";
15098
+ var TEXT_INDEX_MIN_TERM = 3;
15099
+ function installTextIndex(db) {
15100
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)) return;
15101
+ try {
15102
+ db.exec(`CREATE VIRTUAL TABLE temp.fts5_probe USING fts5(x, tokenize='trigram'); DROP TABLE temp.fts5_probe`);
15103
+ } catch {
15104
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "unavailable");
15105
+ return;
15106
+ }
15107
+ db.transaction(() => {
15108
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_text
15109
+ USING fts5(description, content='nodes', content_rowid='rowid', tokenize='trigram')`);
15110
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_insert AFTER INSERT ON nodes WHEN NEW.valid_to IS NULL
15111
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
15112
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_out AFTER UPDATE OF description, valid_to ON nodes
15113
+ WHEN OLD.valid_to IS NULL
15114
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
15115
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_in AFTER UPDATE OF description, valid_to ON nodes
15116
+ WHEN NEW.valid_to IS NULL
15117
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
15118
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_delete AFTER DELETE ON nodes WHEN OLD.valid_to IS NULL
15119
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
15120
+ db.exec(`INSERT INTO nodes_text(rowid, description) SELECT rowid, description FROM nodes WHERE valid_to IS NULL`);
15121
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "installed");
15122
+ });
15123
+ }
15124
+ function textIndexReady(db) {
15125
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
15126
+ }
15127
+
15096
15128
  // ../../packages/math/src/relevance.ts
15097
15129
  function pageRankScale(ranks) {
15098
15130
  const positive = [...ranks].filter((r) => Number.isFinite(r) && r > 0).sort((a, b) => a - b);
@@ -15391,6 +15423,7 @@ var SqliteGraphStore = class {
15391
15423
  installEditPlaceholderRetirement(this.db);
15392
15424
  installCloudAttrCompaction(this.db);
15393
15425
  installHistoryAttrSlim(this.db);
15426
+ installTextIndex(this.db);
15394
15427
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
15395
15428
  this.db.transaction(() => {
15396
15429
  this.repairSessionAliases();
@@ -16045,6 +16078,20 @@ var SqliteGraphStore = class {
16045
16078
  }
16046
16079
  searchTextCandidates(terms) {
16047
16080
  if (terms.length === 0) return [];
16081
+ const indexed = terms.filter((t) => t.length >= TEXT_INDEX_MIN_TERM);
16082
+ if (indexed.length > 0 && textIndexReady(this.db)) {
16083
+ const short = terms.filter((t) => t.length < TEXT_INDEX_MIN_TERM).map((t) => t.toLowerCase());
16084
+ const rows2 = this.db.prepare(`WITH hits AS MATERIALIZED (
16085
+ SELECT t.rowid AS rid, COUNT(*) AS matched FROM json_each(:terms) q, nodes_text t
16086
+ WHERE t.description LIKE '%' || q.value || '%' GROUP BY t.rowid
16087
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, hits.matched FROM hits JOIN nodes ON nodes.rowid = hits.rid
16088
+ WHERE nodes.valid_to IS NULL ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(indexed) });
16089
+ return rows2.map((row) => {
16090
+ const text = row.description.toLowerCase();
16091
+ const extra = short.filter((t) => text.includes(t)).length;
16092
+ return { node: textSearchNode(row), matched: row.matched + extra };
16093
+ });
16094
+ }
16048
16095
  const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
16049
16096
  SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
16050
16097
  WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
@@ -16053,6 +16100,18 @@ var SqliteGraphStore = class {
16053
16100
  JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
16054
16101
  return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
16055
16102
  }
16103
+ commentAnchors(commentIds) {
16104
+ const out = /* @__PURE__ */ new Map();
16105
+ if (commentIds.length === 0) return out;
16106
+ const rows = this.db.prepare(`WITH a AS MATERIALIZED (
16107
+ SELECT q.value AS cid, COALESCE(
16108
+ (SELECT to_id FROM edges WHERE from_id = q.value AND type IN ('EXPLAINS', 'ANNOTATES') AND valid_to IS NULL LIMIT 1),
16109
+ (SELECT from_id FROM edges WHERE to_id = q.value AND type = 'DOCUMENTED_BY' AND valid_to IS NULL LIMIT 1)
16110
+ ) AS aid FROM json_each(:ids) q
16111
+ ) SELECT a.cid AS cid, ${TEXT_SEARCH_COLUMNS} FROM a JOIN nodes ON nodes.id = a.aid AND nodes.valid_to IS NULL`).all({ ids: JSON.stringify(commentIds) });
16112
+ for (const row of rows) out.set(row.cid, textSearchNode(row));
16113
+ return out;
16114
+ }
16056
16115
  getTextSearchNode(id) {
16057
16116
  const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
16058
16117
  return row ? textSearchNode(row) : null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inerrata-corporation/errata",
3
- "version": "2.0.22-dev.4020",
3
+ "version": "2.0.22-dev.4049",
4
4
  "description": "errata - local-first observation engine for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {
package/pass-worker.mjs CHANGED
@@ -25515,6 +25515,38 @@ function installHistoryAttrSlim(db) {
25515
25515
  db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(HISTORY_ATTR_SLIM_KEY, "done");
25516
25516
  }
25517
25517
 
25518
+ // ../../packages/local-graph/src/text-index.ts
25519
+ var TEXT_INDEX_KEY = "text-index-trigram-1";
25520
+ var TEXT_INDEX_MIN_TERM = 3;
25521
+ function installTextIndex(db) {
25522
+ if (db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)) return;
25523
+ try {
25524
+ db.exec(`CREATE VIRTUAL TABLE temp.fts5_probe USING fts5(x, tokenize='trigram'); DROP TABLE temp.fts5_probe`);
25525
+ } catch {
25526
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "unavailable");
25527
+ return;
25528
+ }
25529
+ db.transaction(() => {
25530
+ db.exec(`CREATE VIRTUAL TABLE IF NOT EXISTS nodes_text
25531
+ USING fts5(description, content='nodes', content_rowid='rowid', tokenize='trigram')`);
25532
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_insert AFTER INSERT ON nodes WHEN NEW.valid_to IS NULL
25533
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
25534
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_out AFTER UPDATE OF description, valid_to ON nodes
25535
+ WHEN OLD.valid_to IS NULL
25536
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
25537
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_update_in AFTER UPDATE OF description, valid_to ON nodes
25538
+ WHEN NEW.valid_to IS NULL
25539
+ BEGIN INSERT INTO nodes_text(rowid, description) VALUES (NEW.rowid, NEW.description); END;`);
25540
+ db.exec(`CREATE TRIGGER IF NOT EXISTS nodes_text_delete AFTER DELETE ON nodes WHEN OLD.valid_to IS NULL
25541
+ BEGIN INSERT INTO nodes_text(nodes_text, rowid, description) VALUES ('delete', OLD.rowid, OLD.description); END;`);
25542
+ db.exec(`INSERT INTO nodes_text(rowid, description) SELECT rowid, description FROM nodes WHERE valid_to IS NULL`);
25543
+ db.prepare("INSERT INTO store_meta(key,value) VALUES (?,?)").run(TEXT_INDEX_KEY, "installed");
25544
+ });
25545
+ }
25546
+ function textIndexReady(db) {
25547
+ return db.prepare("SELECT value FROM store_meta WHERE key = ?").get(TEXT_INDEX_KEY)?.["value"] === "installed";
25548
+ }
25549
+
25518
25550
  // ../../packages/math/src/pagerank.ts
25519
25551
  function pagerank(input) {
25520
25552
  const damping = input.damping ?? 0.85;
@@ -26249,6 +26281,7 @@ var SqliteGraphStore = class {
26249
26281
  installEditPlaceholderRetirement(this.db);
26250
26282
  installCloudAttrCompaction(this.db);
26251
26283
  installHistoryAttrSlim(this.db);
26284
+ installTextIndex(this.db);
26252
26285
  if (!this.db.prepare("SELECT value FROM store_meta WHERE key = ?").get(SESSION_REPAIR_KEY)) {
26253
26286
  this.db.transaction(() => {
26254
26287
  this.repairSessionAliases();
@@ -26903,6 +26936,20 @@ var SqliteGraphStore = class {
26903
26936
  }
26904
26937
  searchTextCandidates(terms) {
26905
26938
  if (terms.length === 0) return [];
26939
+ const indexed = terms.filter((t) => t.length >= TEXT_INDEX_MIN_TERM);
26940
+ if (indexed.length > 0 && textIndexReady(this.db)) {
26941
+ const short = terms.filter((t) => t.length < TEXT_INDEX_MIN_TERM).map((t) => t.toLowerCase());
26942
+ const rows2 = this.db.prepare(`WITH hits AS MATERIALIZED (
26943
+ SELECT t.rowid AS rid, COUNT(*) AS matched FROM json_each(:terms) q, nodes_text t
26944
+ WHERE t.description LIKE '%' || q.value || '%' GROUP BY t.rowid
26945
+ ) SELECT ${TEXT_SEARCH_COLUMNS}, hits.matched FROM hits JOIN nodes ON nodes.rowid = hits.rid
26946
+ WHERE nodes.valid_to IS NULL ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(indexed) });
26947
+ return rows2.map((row) => {
26948
+ const text = row.description.toLowerCase();
26949
+ const extra = short.filter((t) => text.includes(t)).length;
26950
+ return { node: textSearchNode(row), matched: row.matched + extra };
26951
+ });
26952
+ }
26906
26953
  const rows = this.db.prepare(`WITH matches AS MATERIALIZED (
26907
26954
  SELECT n.id, COUNT(*) AS matched FROM nodes n INDEXED BY nodes_live_description, json_each(:terms) t
26908
26955
  WHERE n.valid_to IS NULL AND n.description LIKE '%' || t.value || '%'
@@ -26911,6 +26958,18 @@ var SqliteGraphStore = class {
26911
26958
  JOIN matches USING (id) ORDER BY page_rank DESC, last_updated_at DESC`).all({ terms: JSON.stringify(terms) });
26912
26959
  return rows.map((row) => ({ node: textSearchNode(row), matched: row.matched }));
26913
26960
  }
26961
+ commentAnchors(commentIds) {
26962
+ const out2 = /* @__PURE__ */ new Map();
26963
+ if (commentIds.length === 0) return out2;
26964
+ const rows = this.db.prepare(`WITH a AS MATERIALIZED (
26965
+ SELECT q.value AS cid, COALESCE(
26966
+ (SELECT to_id FROM edges WHERE from_id = q.value AND type IN ('EXPLAINS', 'ANNOTATES') AND valid_to IS NULL LIMIT 1),
26967
+ (SELECT from_id FROM edges WHERE to_id = q.value AND type = 'DOCUMENTED_BY' AND valid_to IS NULL LIMIT 1)
26968
+ ) AS aid FROM json_each(:ids) q
26969
+ ) SELECT a.cid AS cid, ${TEXT_SEARCH_COLUMNS} FROM a JOIN nodes ON nodes.id = a.aid AND nodes.valid_to IS NULL`).all({ ids: JSON.stringify(commentIds) });
26970
+ for (const row of rows) out2.set(row.cid, textSearchNode(row));
26971
+ return out2;
26972
+ }
26914
26973
  getTextSearchNode(id) {
26915
26974
  const row = this.db.prepare(`SELECT ${TEXT_SEARCH_COLUMNS} FROM nodes WHERE id = ? AND valid_to IS NULL`).get(id);
26916
26975
  return row ? textSearchNode(row) : null;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "errata-context",
3
- "version": "2.0.22-dev.4020",
3
+ "version": "2.0.22-dev.4049",
4
4
  "description": "A minimal 3D view of loaded Errata priors and recent graph bursts.",
5
5
  "author": {
6
6
  "name": "inErrata"
@@ -1,4 +1,4 @@
1
- cefcb61c20b7e27f11cb65c4ab1bf3352957edd54c21cd575122b54dfd7f6581 .codex-plugin/plugin.json
1
+ fed0cc007727934c2ac9ad932429797dfe773ebb96c0219d09141979f2886c84 .codex-plugin/plugin.json
2
2
  a7bebae9fb01c838d6c75fc784129e28be571233fa66ff5ee8039b574f35ccf7 assets/icon.png
3
3
  fc38b64e4df1e0b72c35764bef6c4c05430d97c9009ad0ecc8a9e75b4deab232 configure-local.mjs
4
4
  73dc6028b3b070257c7c31f252d11e55a9e096f4aed0f187f10cb21716e1e6d7 dist/client.js