@merittdev/horus 0.12.1 → 0.12.3

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.
Files changed (2) hide show
  1. package/dist/index.cjs +548 -235
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -65880,8 +65880,8 @@ init_cjs_shims();
65880
65880
 
65881
65881
  // ../../packages/core/src/version.ts
65882
65882
  init_cjs_shims();
65883
- var HORUS_VERSION = true ? "0.12.1" : "dev";
65884
- var PINNED_SOURCE_VERSION = "2.0.0";
65883
+ var HORUS_VERSION = true ? "0.12.3" : "dev";
65884
+ var PINNED_SOURCE_VERSION = "2.0.2";
65885
65885
 
65886
65886
  // ../../packages/core/src/evidence.ts
65887
65887
  init_cjs_shims();
@@ -71127,6 +71127,42 @@ async function assertSourceVersionPinned() {
71127
71127
  function isAnalyzed(root) {
71128
71128
  return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".horus", "source"));
71129
71129
  }
71130
+ var CURRENT_STORE_BACKEND = (process.env["HORUS_SOURCE_STORAGE_BACKEND"] ?? "sqlite").trim().toLowerCase();
71131
+ function readSourceMeta(root) {
71132
+ try {
71133
+ const raw = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(root, ".horus", "source", "meta.json"), "utf8");
71134
+ const j3 = JSON.parse(raw);
71135
+ const stats = j3["stats"] ?? void 0;
71136
+ return {
71137
+ storeBackend: typeof j3["store_backend"] === "string" ? j3["store_backend"].toLowerCase() : void 0,
71138
+ embeddingsComplete: typeof j3["embeddings_complete"] === "boolean" ? j3["embeddings_complete"] : void 0,
71139
+ stats: stats ? { symbols: stats["symbols"], embeddings: stats["embeddings"] } : void 0
71140
+ };
71141
+ } catch {
71142
+ return null;
71143
+ }
71144
+ }
71145
+ function metaHasEmbeddings(meta) {
71146
+ if (meta.embeddingsComplete === false) return false;
71147
+ const embeddings = meta.stats?.embeddings ?? 0;
71148
+ const symbols = meta.stats?.symbols ?? 0;
71149
+ return !(symbols > 0 && embeddings === 0);
71150
+ }
71151
+ function indexNeedsReanalyze(root) {
71152
+ if (!isAnalyzed(root)) return null;
71153
+ if (CURRENT_STORE_BACKEND !== "kuzu" && (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".horus", "source", "kuzu"))) {
71154
+ return "legacy K\xF9zuDB store present";
71155
+ }
71156
+ const meta = readSourceMeta(root);
71157
+ if (meta === null) return null;
71158
+ if (meta.storeBackend && meta.storeBackend !== CURRENT_STORE_BACKEND) {
71159
+ return `store backend changed (${meta.storeBackend} -> ${CURRENT_STORE_BACKEND})`;
71160
+ }
71161
+ if (!metaHasEmbeddings(meta)) {
71162
+ return "index has no semantic embeddings (symbols present but 0 vectors)";
71163
+ }
71164
+ return null;
71165
+ }
71130
71166
  async function analyzeRepo(root) {
71131
71167
  const bin = await resolveSourceBin();
71132
71168
  if (!bin) throw new Error("horus-source not found on PATH. Install it: curl -fsSL https://horus.sh/install.sh | bash");
@@ -71457,7 +71493,7 @@ var SourceCodeProvider = class {
71457
71493
  if (n4.className) symbol.className = n4.className;
71458
71494
  if (n4.language) symbol.language = n4.language;
71459
71495
  const content = n4.content;
71460
- const snippet = typeof content === "string" ? content.slice(0, 600) : void 0;
71496
+ const snippet2 = typeof content === "string" ? content.slice(0, 600) : void 0;
71461
71497
  const sourceBody = typeof content === "string" ? content.slice(0, 2e4) : void 0;
71462
71498
  const callees = (detail.callees ?? []).map((c4) => this.serializedToSymbol(c4.node));
71463
71499
  const callers = (detail.callers ?? []).map((c4) => this.serializedToSymbol(c4.node));
@@ -71471,7 +71507,7 @@ var SourceCodeProvider = class {
71471
71507
  }));
71472
71508
  return {
71473
71509
  symbol,
71474
- snippet,
71510
+ snippet: snippet2,
71475
71511
  ...sourceBody !== void 0 ? { sourceBody } : {},
71476
71512
  callers,
71477
71513
  callees,
@@ -93004,7 +93040,13 @@ var MEMORY_RELS = /* @__PURE__ */ new Set([
93004
93040
  function isSymmetricRel(rel) {
93005
93041
  return rel === "recurs-with";
93006
93042
  }
93007
- var INCIDENT_DERIVED_KINDS = /* @__PURE__ */ new Set(["confirmed-outcome", "incident-pattern"]);
93043
+ var INCIDENT_DERIVED_KINDS = /* @__PURE__ */ new Set([
93044
+ "confirmed-outcome",
93045
+ "incident-pattern",
93046
+ // HOR-432: auto-captured investigation memories carry the same incident-family signature/tags so
93047
+ // they participate in recurrence detection. CONTEXT-ONLY — never read by the scoring path.
93048
+ "investigation"
93049
+ ]);
93008
93050
  function normalizeStoredSignature(sig) {
93009
93051
  const s5 = (sig ?? "").trim();
93010
93052
  if (s5 === "" || /^\|*$/.test(s5)) return null;
@@ -93109,6 +93151,35 @@ function createLocalMemoryStore(db) {
93109
93151
  });
93110
93152
  return row;
93111
93153
  },
93154
+ async update(id, patch, opts) {
93155
+ const current = await getById(id);
93156
+ if (current === null) throw new Error(`memory item not found: ${id}`);
93157
+ const set = {};
93158
+ if (patch.claim !== void 0) {
93159
+ assertClaimClean(patch.claim);
93160
+ set.claim = patch.claim;
93161
+ }
93162
+ if (patch.confidence !== void 0) set.confidence = patch.confidence;
93163
+ if (patch.payload !== void 0) set.payload = patch.payload;
93164
+ if (Object.keys(set).length > 0) {
93165
+ await db.update(memoryItem).set(set).where(eq(memoryItem.id, id));
93166
+ }
93167
+ await appendAudit({
93168
+ memoryId: id,
93169
+ action: opts.action ?? "update",
93170
+ actor: opts.audit.actor,
93171
+ fromStatus: null,
93172
+ toStatus: null,
93173
+ note: opts.audit.note ?? null,
93174
+ detail: {
93175
+ ...opts.detection ? { detection: opts.detection } : {},
93176
+ ...opts.detail ?? {}
93177
+ }
93178
+ });
93179
+ const updated = await getById(id);
93180
+ if (updated === null) throw new Error(`memory item vanished during update: ${id}`);
93181
+ return updated;
93182
+ },
93112
93183
  get: getById,
93113
93184
  async query(q2) {
93114
93185
  const repo = q2.repo.trim();
@@ -93296,6 +93367,395 @@ function createLocalMemoryStore(db) {
93296
93367
  };
93297
93368
  }
93298
93369
 
93370
+ // ../../packages/engine/src/auto-investigation-memory.ts
93371
+ init_cjs_shims();
93372
+
93373
+ // ../../packages/engine/src/memory-detect.ts
93374
+ init_cjs_shims();
93375
+ var import_node_crypto3 = require("crypto");
93376
+
93377
+ // ../../packages/engine/src/memory-store.ts
93378
+ init_cjs_shims();
93379
+ var HIDDEN_STATUSES = [
93380
+ "forgotten",
93381
+ "deprecated",
93382
+ "contradicted"
93383
+ ];
93384
+ function tokenize2(text2) {
93385
+ const seen = /* @__PURE__ */ new Set();
93386
+ const out2 = [];
93387
+ for (const raw of text2.toLowerCase().split(/[^a-z0-9]+/)) {
93388
+ if (raw !== "" && !seen.has(raw)) {
93389
+ seen.add(raw);
93390
+ out2.push(raw);
93391
+ }
93392
+ }
93393
+ return out2;
93394
+ }
93395
+ function scopeSpecificity(scope) {
93396
+ if (scope.startsWith("symbol:")) return 3;
93397
+ if (scope.startsWith("module:")) return 2;
93398
+ if (scope === "repo") return 1;
93399
+ return 0;
93400
+ }
93401
+ var NoopVectorIndex = class {
93402
+ entries = /* @__PURE__ */ new Map();
93403
+ async upsert(i3) {
93404
+ this.entries.set(i3.memoryId, {
93405
+ memoryId: i3.memoryId,
93406
+ repo: i3.repo,
93407
+ scope: i3.scope,
93408
+ tokens: tokenize2(i3.claim)
93409
+ });
93410
+ }
93411
+ async search(i3) {
93412
+ const repo = i3.repo.trim();
93413
+ if (repo === "") return [];
93414
+ const queryTokens = tokenize2(i3.query);
93415
+ if (queryTokens.length === 0) return [];
93416
+ const scored = [...this.entries.values()].filter((e) => e.repo === repo).map((e) => ({
93417
+ memoryId: e.memoryId,
93418
+ score: tagOverlap(queryTokens, e.tokens),
93419
+ specificity: scopeSpecificity(e.scope)
93420
+ })).filter((h3) => h3.score > 0).sort(
93421
+ (a2, b5) => b5.score - a2.score || b5.specificity - a2.specificity || (a2.memoryId < b5.memoryId ? -1 : a2.memoryId > b5.memoryId ? 1 : 0)
93422
+ );
93423
+ const limit = Number.isFinite(i3.limit) && i3.limit > 0 ? Math.floor(i3.limit) : scored.length;
93424
+ return scored.slice(0, limit).map((h3) => ({ memoryId: h3.memoryId, score: h3.score }));
93425
+ }
93426
+ async remove(memoryId) {
93427
+ this.entries.delete(memoryId);
93428
+ }
93429
+ };
93430
+
93431
+ // ../../packages/engine/src/memory-detect.ts
93432
+ var TAG_OVERLAP_MIN = 0.6;
93433
+ var CLAIM_JACCARD_MIN = 0.6;
93434
+ var MIN_CLAIM_TOKENS = 4;
93435
+ var INCIDENT_DERIVED_KINDS2 = /* @__PURE__ */ new Set([
93436
+ "confirmed-outcome",
93437
+ "incident-pattern"
93438
+ ]);
93439
+ function usableSignature(sig) {
93440
+ const s5 = (sig ?? "").trim();
93441
+ if (s5 === "" || /^\|*$/.test(s5)) return null;
93442
+ const components = s5.split(/[|,]/).map((c4) => c4.trim()).filter((c4) => c4 !== "");
93443
+ if (components.length === 0) return null;
93444
+ if (components.every((c4) => isGenericTag(c4))) return null;
93445
+ return s5;
93446
+ }
93447
+ function sharedNonGenericTags(a2, b5) {
93448
+ const setB = new Set(b5);
93449
+ const seen = /* @__PURE__ */ new Set();
93450
+ const out2 = [];
93451
+ for (const t2 of a2) {
93452
+ if (setB.has(t2) && !isGenericTag(t2) && !seen.has(t2)) {
93453
+ seen.add(t2);
93454
+ out2.push(t2);
93455
+ }
93456
+ }
93457
+ return out2;
93458
+ }
93459
+ function recurrenceEdgeId(idA, idB) {
93460
+ const [lo, hi] = idA <= idB ? [idA, idB] : [idB, idA];
93461
+ const h3 = (0, import_node_crypto3.createHash)("sha256").update(`recurs-with\0${lo}\0${hi}`).digest("hex");
93462
+ return `lnk_recurs_${h3.slice(0, 32)}`;
93463
+ }
93464
+ function normalizeCause(s5) {
93465
+ return (s5 ?? "").trim().toLowerCase().replace(/\s+/g, " ");
93466
+ }
93467
+ function outcomeConflict(a2, b5) {
93468
+ const opposite = a2.resolved === "yes" && b5.resolved === "no" || a2.resolved === "no" && b5.resolved === "yes";
93469
+ if (opposite) {
93470
+ return `outcome verdicts disagree (${a2.resolved} vs ${b5.resolved})`;
93471
+ }
93472
+ const ca = normalizeCause(a2.confirmedCause);
93473
+ const cb = normalizeCause(b5.confirmedCause);
93474
+ if (ca !== "" && cb !== "" && ca !== cb) {
93475
+ return `confirmed causes differ ("${a2.confirmedCause}" vs "${b5.confirmedCause}")`;
93476
+ }
93477
+ return null;
93478
+ }
93479
+ function recurrenceReason(a2, b5) {
93480
+ const sigA = usableSignature(a2.signature);
93481
+ const sigB = usableSignature(b5.signature);
93482
+ if (sigA !== null && sigB !== null && sigA === sigB) {
93483
+ return `identical incident signature "${sigA}"`;
93484
+ }
93485
+ const tagsA = a2.tags ?? [];
93486
+ const tagsB = b5.tags ?? [];
93487
+ const shared = sharedNonGenericTags(tagsA, tagsB);
93488
+ const overlap = tagOverlap([...tagsA], [...tagsB]);
93489
+ if (overlap >= TAG_OVERLAP_MIN && shared.length >= 1) {
93490
+ return `tag overlap ${overlap.toFixed(2)} on [${shared.join(", ")}]`;
93491
+ }
93492
+ if (sigA === null && sigB === null && shared.length >= 1) {
93493
+ const ta = tokenize2(a2.claim);
93494
+ const tb = tokenize2(b5.claim);
93495
+ if (ta.length >= MIN_CLAIM_TOKENS && tb.length >= MIN_CLAIM_TOKENS) {
93496
+ const j3 = tagOverlap(ta, tb);
93497
+ if (j3 >= CLAIM_JACCARD_MIN) {
93498
+ return `claim similarity ${j3.toFixed(2)} corroborated by [${shared.join(", ")}]`;
93499
+ }
93500
+ }
93501
+ }
93502
+ return null;
93503
+ }
93504
+ function edgeKey(rel, from, to) {
93505
+ if (rel === "recurs-with" || rel === "contradicts") {
93506
+ const [lo, hi] = from <= to ? [from, to] : [to, from];
93507
+ return `${rel}|${lo}|${hi}`;
93508
+ }
93509
+ return `${rel}|${from}|${to}`;
93510
+ }
93511
+ async function detectMemoryEdges(store, opts, deps = {}) {
93512
+ const repo = opts.repo.trim();
93513
+ if (repo === "") return [];
93514
+ const items = await store.query({ repo, limit: opts.limit ?? 1e3 });
93515
+ const candidates = items.filter((i3) => INCIDENT_DERIVED_KINDS2.has(i3.kind));
93516
+ if (candidates.length < 2) return [];
93517
+ const existing = /* @__PURE__ */ new Set();
93518
+ const investigationOf = /* @__PURE__ */ new Map();
93519
+ for (const item of candidates) {
93520
+ const out2 = await store.links(item.id, { direction: "out" });
93521
+ for (const l2 of out2) {
93522
+ if (l2.toKind === "memory") {
93523
+ existing.add(edgeKey(l2.rel, l2.fromMemoryId, l2.toRef));
93524
+ } else if (l2.toKind === "incident" && l2.rel === "about-incident") {
93525
+ if (!investigationOf.has(item.id)) investigationOf.set(item.id, l2.toRef);
93526
+ }
93527
+ }
93528
+ }
93529
+ const proposals = [];
93530
+ const outcomeCache = /* @__PURE__ */ new Map();
93531
+ const resolveOutcome = async (investigationId) => {
93532
+ if (deps.outcomeFor === void 0) return null;
93533
+ if (outcomeCache.has(investigationId)) return outcomeCache.get(investigationId) ?? null;
93534
+ let v4 = null;
93535
+ try {
93536
+ v4 = await deps.outcomeFor(investigationId);
93537
+ } catch {
93538
+ v4 = null;
93539
+ }
93540
+ outcomeCache.set(investigationId, v4);
93541
+ return v4;
93542
+ };
93543
+ for (let i3 = 0; i3 < candidates.length; i3++) {
93544
+ const a2 = candidates[i3];
93545
+ for (let j3 = i3 + 1; j3 < candidates.length; j3++) {
93546
+ const b5 = candidates[j3];
93547
+ if (!existing.has(edgeKey("recurs-with", a2.id, b5.id))) {
93548
+ const reason = recurrenceReason(a2, b5);
93549
+ if (reason !== null) {
93550
+ const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
93551
+ proposals.push({
93552
+ id: recurrenceEdgeId(a2.id, b5.id),
93553
+ fromMemoryId: lo,
93554
+ toMemoryId: hi,
93555
+ rel: "recurs-with",
93556
+ detection: "auto:recurrence",
93557
+ reason
93558
+ });
93559
+ }
93560
+ }
93561
+ if (deps.outcomeFor !== void 0 && a2.kind === "confirmed-outcome" && b5.kind === "confirmed-outcome" && !existing.has(edgeKey("contradicts", a2.id, b5.id))) {
93562
+ const sigA = usableSignature(a2.signature);
93563
+ const sigB = usableSignature(b5.signature);
93564
+ const invA = investigationOf.get(a2.id);
93565
+ const invB = investigationOf.get(b5.id);
93566
+ if (sigA !== null && sigA === sigB && invA !== void 0 && invB !== void 0) {
93567
+ const oa = await resolveOutcome(invA);
93568
+ const ob = await resolveOutcome(invB);
93569
+ if (oa !== null && ob !== null) {
93570
+ const reason = outcomeConflict(oa, ob);
93571
+ if (reason !== null) {
93572
+ const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
93573
+ const invLo = lo === a2.id ? invA : invB;
93574
+ const invHi = lo === a2.id ? invB : invA;
93575
+ proposals.push({
93576
+ id: "",
93577
+ // directional — the store mints the id
93578
+ fromMemoryId: lo,
93579
+ toMemoryId: hi,
93580
+ rel: "contradicts",
93581
+ detection: "auto:contradiction",
93582
+ reason,
93583
+ detail: { investigationIds: [invLo, invHi] }
93584
+ });
93585
+ }
93586
+ }
93587
+ }
93588
+ }
93589
+ }
93590
+ }
93591
+ return proposals;
93592
+ }
93593
+
93594
+ // ../../packages/engine/src/auto-investigation-memory.ts
93595
+ var RECURRENCE_SCAN_LIMIT = 100;
93596
+ var MAX_INVESTIGATION_IDS = 20;
93597
+ function snippet(text2, max = 140) {
93598
+ const s5 = text2.trim().replace(/\s+/g, " ");
93599
+ return s5.length > max ? `${s5.slice(0, max - 1)}\u2026` : s5;
93600
+ }
93601
+ function autoInvestigationMemoryEnabled() {
93602
+ const v4 = (process.env.HORUS_AUTO_INVESTIGATION_MEMORY ?? "").trim().toLowerCase();
93603
+ return v4 !== "0" && v4 !== "false" && v4 !== "off" && v4 !== "no";
93604
+ }
93605
+ function deriveInvestigationFields(report) {
93606
+ const hint = report.input.hint;
93607
+ const topHyp = report.hypotheses[0];
93608
+ const headline = topHyp !== void 0 ? snippet(topHyp.statement || topHyp.category || report.summary) : snippet(report.summary || "(no leading hypothesis)");
93609
+ return {
93610
+ hint,
93611
+ claim: `Investigation: ${snippet(hint)} -> ${headline}`,
93612
+ confidence: report.confidence,
93613
+ signature: deriveSignature(report),
93614
+ tags: deriveTags(report),
93615
+ topHypothesisCategory: topHyp !== void 0 ? topHyp.category ?? null : null
93616
+ };
93617
+ }
93618
+ function recurrenceCountOf(payload) {
93619
+ if (payload !== null && typeof payload === "object") {
93620
+ const c4 = payload.recurrenceCount;
93621
+ if (typeof c4 === "number" && Number.isFinite(c4) && c4 >= 1) return Math.floor(c4);
93622
+ }
93623
+ return 1;
93624
+ }
93625
+ async function createInvestigationMemory(store, investigationId, report, audit) {
93626
+ const repo = report.input.repo?.trim() || null;
93627
+ if (repo === null) return null;
93628
+ const fields = deriveInvestigationFields(report);
93629
+ const item = {
93630
+ id: "",
93631
+ kind: "investigation",
93632
+ source: "investigation",
93633
+ scope: "repo",
93634
+ visibility: "private",
93635
+ // HONEST confidence — the report's own value, never inflated.
93636
+ confidence: fields.confidence,
93637
+ claim: fields.claim,
93638
+ repo,
93639
+ evidence: [],
93640
+ // Incident-family recall keys (same derivation as incident_memory) so this memory recurs.
93641
+ signature: fields.signature,
93642
+ tags: fields.tags,
93643
+ payload: {
93644
+ investigationId,
93645
+ hint: fields.hint,
93646
+ topHypothesis: fields.topHypothesisCategory,
93647
+ // First sighting: a single investigation is "recurred 1×". Bumped on each later recurrence.
93648
+ recurrenceCount: 1
93649
+ }
93650
+ };
93651
+ const created = await store.add(item, audit);
93652
+ if (investigationId !== null && investigationId.trim() !== "") {
93653
+ await store.addLink(
93654
+ {
93655
+ id: "",
93656
+ fromMemoryId: created.id,
93657
+ rel: "about-incident",
93658
+ toKind: "incident",
93659
+ toRef: investigationId
93660
+ },
93661
+ { detection: "structural", audit }
93662
+ );
93663
+ }
93664
+ return created;
93665
+ }
93666
+ async function detectRecurrence(store, probe2, repo) {
93667
+ const r = repo.trim();
93668
+ if (r === "") return null;
93669
+ const existing = await store.query({
93670
+ repo: r,
93671
+ kind: ["investigation"],
93672
+ limit: RECURRENCE_SCAN_LIMIT
93673
+ });
93674
+ for (const candidate of existing) {
93675
+ if (recurrenceReason(candidate, probe2) !== null) return candidate;
93676
+ }
93677
+ return null;
93678
+ }
93679
+ async function consolidateRecurrence(store, existing, investigationId, fields, reportTime, audit) {
93680
+ const prev = existing.payload ?? {};
93681
+ const recurrenceCount = recurrenceCountOf(prev) + 1;
93682
+ const prevIds = Array.isArray(prev.investigationIds) ? prev.investigationIds.filter((x4) => typeof x4 === "string") : [];
93683
+ const seedIds = prevIds.length > 0 ? prevIds : typeof prev.investigationId === "string" ? [prev.investigationId] : [];
93684
+ const thisId = investigationId !== null && investigationId.trim() !== "" ? investigationId : null;
93685
+ const investigationIds = (thisId !== null ? [...seedIds, thisId] : seedIds).slice(
93686
+ -MAX_INVESTIGATION_IDS
93687
+ );
93688
+ const payload = {
93689
+ ...prev,
93690
+ recurrenceCount,
93691
+ lastSeenAt: reportTime.toISOString(),
93692
+ investigationIds,
93693
+ // Refresh the "latest finding" provenance; keep the original first-seen id for reference.
93694
+ hint: fields.hint,
93695
+ topHypothesis: fields.topHypothesisCategory,
93696
+ investigationId: typeof prev.investigationId === "string" ? prev.investigationId : seedIds[0] ?? thisId
93697
+ };
93698
+ const updated = await store.update(
93699
+ existing.id,
93700
+ {
93701
+ claim: fields.claim,
93702
+ // HONEST: the LATEST report's confidence, verbatim — never inflated, never an average.
93703
+ confidence: fields.confidence,
93704
+ payload
93705
+ },
93706
+ {
93707
+ audit,
93708
+ action: "recurrence",
93709
+ detection: "auto:recurrence-consolidate",
93710
+ detail: { recurrenceCount, ...thisId !== null ? { investigationId: thisId } : {} }
93711
+ }
93712
+ );
93713
+ if (thisId !== null) {
93714
+ await store.addLink(
93715
+ {
93716
+ id: "",
93717
+ fromMemoryId: existing.id,
93718
+ rel: "about-incident",
93719
+ toKind: "incident",
93720
+ toRef: thisId
93721
+ },
93722
+ { detection: "structural", audit }
93723
+ );
93724
+ }
93725
+ return updated;
93726
+ }
93727
+ async function captureInvestigationMemory(store, investigationId, report, reportTime, audit) {
93728
+ const repo = report.input.repo?.trim() || null;
93729
+ if (repo === null) return { action: "skipped", memoryId: null, recurrenceCount: 0 };
93730
+ const fields = deriveInvestigationFields(report);
93731
+ const existing = await detectRecurrence(
93732
+ store,
93733
+ { signature: fields.signature, tags: fields.tags, claim: fields.claim },
93734
+ repo
93735
+ );
93736
+ if (existing !== null) {
93737
+ const updated = await consolidateRecurrence(
93738
+ store,
93739
+ existing,
93740
+ investigationId,
93741
+ fields,
93742
+ reportTime,
93743
+ audit
93744
+ );
93745
+ return {
93746
+ action: "consolidated",
93747
+ memoryId: updated.id,
93748
+ recurrenceCount: recurrenceCountOf(updated.payload)
93749
+ };
93750
+ }
93751
+ const created = await createInvestigationMemory(store, investigationId, report, audit);
93752
+ return {
93753
+ action: created !== null ? "created" : "skipped",
93754
+ memoryId: created?.id ?? null,
93755
+ recurrenceCount: 1
93756
+ };
93757
+ }
93758
+
93299
93759
  // ../../packages/engine/src/gaps.ts
93300
93760
  init_cjs_shims();
93301
93761
  function detectMissingEvidence(r, connectors = {}, mode) {
@@ -93865,7 +94325,7 @@ function scoreSeed(s5, index2, hintTokens, changedFiles, hintHasCode, qualifier,
93865
94325
  score -= 4;
93866
94326
  }
93867
94327
  score += Math.max(0, 5 - index2) * (hintHasCode ? 0.8 : 0.1);
93868
- if (s5.score !== void 0) score += s5.score * (hintHasCode ? 8 : 1.5);
94328
+ if (s5.score !== void 0) score += s5.score * (hintHasCode ? 8 : 2);
93869
94329
  if (/\.[jt]sx?$/i.test(s5.name)) score -= 5;
93870
94330
  if (hintTokens !== void 0 && hintTokens.length > 0) {
93871
94331
  let hintBoost = 0;
@@ -93886,17 +94346,27 @@ function isDeprioritizedSeedPath(filePath) {
93886
94346
  filePath
93887
94347
  );
93888
94348
  }
94349
+ function isAnchoredExactSeed(s5, qualifier, hintHasCode, preferNamed) {
94350
+ if (qualifier && qualifierBoost(s5, qualifier) >= 40) return true;
94351
+ if (preferNamed !== void 0 && s5.name.toLowerCase() === preferNamed.toLowerCase()) return true;
94352
+ if (hintHasCode === true && s5.score !== void 0 && s5.score >= 0.9) return true;
94353
+ return false;
94354
+ }
93889
94355
  function rankSeeds(seeds, hintTokens, changedFiles, hintHasCode, qualifier, preferNamed) {
93890
94356
  const scored = seeds.map((symbol, i3) => ({
93891
94357
  symbol,
93892
94358
  score: scoreSeed(symbol, i3, hintTokens, changedFiles, hintHasCode, qualifier, preferNamed),
93893
94359
  role: seedRole(symbol),
93894
94360
  i: i3,
93895
- deprio: isDeprioritizedSeedPath(symbol.filePath)
94361
+ deprio: isDeprioritizedSeedPath(symbol.filePath),
94362
+ // HOR-430: a deprioritized candidate the request anchors on exactly is exempt from the
94363
+ // hard demotion below — it keeps only the soft path penalty so a strong/exact match in an
94364
+ // example/test file can still surface.
94365
+ anchored: isAnchoredExactSeed(symbol, qualifier, hintHasCode, preferNamed)
93896
94366
  }));
93897
94367
  const hasReal = scored.some((s5) => !s5.deprio);
93898
94368
  if (hasReal) {
93899
- for (const s5 of scored) if (s5.deprio) s5.score -= 1e3;
94369
+ for (const s5 of scored) if (s5.deprio && !s5.anchored) s5.score -= 1e3;
93900
94370
  }
93901
94371
  return scored.sort((a2, b5) => b5.score === a2.score ? a2.i - b5.i : b5.score - a2.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
93902
94372
  }
@@ -94848,8 +95318,8 @@ async function investigate(input, deps) {
94848
95318
  // HOR-386 — empty-return: route to `search` for the right name (deterministic).
94849
95319
  nextSteps: route({ command: "investigate", empty: true, query: hint })
94850
95320
  };
94851
- const persistedId2 = await persist(db, input, report2);
94852
- if (persistedId2) report2.id = persistedId2;
95321
+ const persistedEmpty = await persist(db, input, report2);
95322
+ if (persistedEmpty) report2.id = persistedEmpty.id;
94853
95323
  return report2;
94854
95324
  }
94855
95325
  let label = input.service ?? hint;
@@ -96243,13 +96713,26 @@ async function investigate(input, deps) {
96243
96713
  query: hint
96244
96714
  });
96245
96715
  report.sourceStatus = buildRuntimeSourceStatus(evidence2, connectorFlags);
96246
- const persistedId = await persist(db, input, report);
96247
- if (persistedId) report.id = persistedId;
96716
+ const persisted = await persist(db, input, report);
96717
+ const persistedId = persisted?.id ?? null;
96718
+ if (persistedId !== null) report.id = persistedId;
96248
96719
  report.persisted = persistedId !== null;
96249
96720
  if (persistedId !== null) {
96250
96721
  const tags = deriveTags(report);
96251
96722
  report.similarIncidents = await recallSimilar(db, tags, persistedId, input.repo ?? null);
96252
96723
  await storeIncidentMemory(db, persistedId, report);
96724
+ const store = deps.store;
96725
+ if (store !== void 0 && autoInvestigationMemoryEnabled()) {
96726
+ try {
96727
+ const audit = {
96728
+ actor: { kind: "system" },
96729
+ note: "auto: investigation memory (HOR-432)"
96730
+ };
96731
+ const reportTime = persisted?.createdAt ?? /* @__PURE__ */ new Date();
96732
+ await captureInvestigationMemory(store, persistedId, report, reportTime, audit);
96733
+ } catch {
96734
+ }
96735
+ }
96253
96736
  }
96254
96737
  if (persistedId !== null) {
96255
96738
  try {
@@ -96293,7 +96776,7 @@ async function persist(db, input, report) {
96293
96776
  incidentInput: input,
96294
96777
  status: "open",
96295
96778
  summary: report.summary
96296
- }).returning({ id: investigations.id });
96779
+ }).returning({ id: investigations.id, createdAt: investigations.createdAt });
96297
96780
  const row = inserted[0];
96298
96781
  if (!row) return null;
96299
96782
  const investigationId = row.id;
@@ -96337,7 +96820,7 @@ async function persist(db, input, report) {
96337
96820
  }))
96338
96821
  );
96339
96822
  }
96340
- return investigationId;
96823
+ return { id: investigationId, createdAt: row.createdAt ?? null };
96341
96824
  } catch {
96342
96825
  return null;
96343
96826
  }
@@ -97046,225 +97529,6 @@ function blastRadiusToJSON(r) {
97046
97529
  return JSON.stringify(r, null, 2);
97047
97530
  }
97048
97531
 
97049
- // ../../packages/engine/src/memory-store.ts
97050
- init_cjs_shims();
97051
- var HIDDEN_STATUSES = [
97052
- "forgotten",
97053
- "deprecated",
97054
- "contradicted"
97055
- ];
97056
- function tokenize2(text2) {
97057
- const seen = /* @__PURE__ */ new Set();
97058
- const out2 = [];
97059
- for (const raw of text2.toLowerCase().split(/[^a-z0-9]+/)) {
97060
- if (raw !== "" && !seen.has(raw)) {
97061
- seen.add(raw);
97062
- out2.push(raw);
97063
- }
97064
- }
97065
- return out2;
97066
- }
97067
- function scopeSpecificity(scope) {
97068
- if (scope.startsWith("symbol:")) return 3;
97069
- if (scope.startsWith("module:")) return 2;
97070
- if (scope === "repo") return 1;
97071
- return 0;
97072
- }
97073
- var NoopVectorIndex = class {
97074
- entries = /* @__PURE__ */ new Map();
97075
- async upsert(i3) {
97076
- this.entries.set(i3.memoryId, {
97077
- memoryId: i3.memoryId,
97078
- repo: i3.repo,
97079
- scope: i3.scope,
97080
- tokens: tokenize2(i3.claim)
97081
- });
97082
- }
97083
- async search(i3) {
97084
- const repo = i3.repo.trim();
97085
- if (repo === "") return [];
97086
- const queryTokens = tokenize2(i3.query);
97087
- if (queryTokens.length === 0) return [];
97088
- const scored = [...this.entries.values()].filter((e) => e.repo === repo).map((e) => ({
97089
- memoryId: e.memoryId,
97090
- score: tagOverlap(queryTokens, e.tokens),
97091
- specificity: scopeSpecificity(e.scope)
97092
- })).filter((h3) => h3.score > 0).sort(
97093
- (a2, b5) => b5.score - a2.score || b5.specificity - a2.specificity || (a2.memoryId < b5.memoryId ? -1 : a2.memoryId > b5.memoryId ? 1 : 0)
97094
- );
97095
- const limit = Number.isFinite(i3.limit) && i3.limit > 0 ? Math.floor(i3.limit) : scored.length;
97096
- return scored.slice(0, limit).map((h3) => ({ memoryId: h3.memoryId, score: h3.score }));
97097
- }
97098
- async remove(memoryId) {
97099
- this.entries.delete(memoryId);
97100
- }
97101
- };
97102
-
97103
- // ../../packages/engine/src/memory-detect.ts
97104
- init_cjs_shims();
97105
- var import_node_crypto3 = require("crypto");
97106
- var TAG_OVERLAP_MIN = 0.6;
97107
- var CLAIM_JACCARD_MIN = 0.6;
97108
- var MIN_CLAIM_TOKENS = 4;
97109
- var INCIDENT_DERIVED_KINDS2 = /* @__PURE__ */ new Set([
97110
- "confirmed-outcome",
97111
- "incident-pattern"
97112
- ]);
97113
- function usableSignature(sig) {
97114
- const s5 = (sig ?? "").trim();
97115
- if (s5 === "" || /^\|*$/.test(s5)) return null;
97116
- const components = s5.split(/[|,]/).map((c4) => c4.trim()).filter((c4) => c4 !== "");
97117
- if (components.length === 0) return null;
97118
- if (components.every((c4) => isGenericTag(c4))) return null;
97119
- return s5;
97120
- }
97121
- function sharedNonGenericTags(a2, b5) {
97122
- const setB = new Set(b5);
97123
- const seen = /* @__PURE__ */ new Set();
97124
- const out2 = [];
97125
- for (const t2 of a2) {
97126
- if (setB.has(t2) && !isGenericTag(t2) && !seen.has(t2)) {
97127
- seen.add(t2);
97128
- out2.push(t2);
97129
- }
97130
- }
97131
- return out2;
97132
- }
97133
- function recurrenceEdgeId(idA, idB) {
97134
- const [lo, hi] = idA <= idB ? [idA, idB] : [idB, idA];
97135
- const h3 = (0, import_node_crypto3.createHash)("sha256").update(`recurs-with\0${lo}\0${hi}`).digest("hex");
97136
- return `lnk_recurs_${h3.slice(0, 32)}`;
97137
- }
97138
- function normalizeCause(s5) {
97139
- return (s5 ?? "").trim().toLowerCase().replace(/\s+/g, " ");
97140
- }
97141
- function outcomeConflict(a2, b5) {
97142
- const opposite = a2.resolved === "yes" && b5.resolved === "no" || a2.resolved === "no" && b5.resolved === "yes";
97143
- if (opposite) {
97144
- return `outcome verdicts disagree (${a2.resolved} vs ${b5.resolved})`;
97145
- }
97146
- const ca = normalizeCause(a2.confirmedCause);
97147
- const cb = normalizeCause(b5.confirmedCause);
97148
- if (ca !== "" && cb !== "" && ca !== cb) {
97149
- return `confirmed causes differ ("${a2.confirmedCause}" vs "${b5.confirmedCause}")`;
97150
- }
97151
- return null;
97152
- }
97153
- function recurrenceReason(a2, b5) {
97154
- const sigA = usableSignature(a2.signature);
97155
- const sigB = usableSignature(b5.signature);
97156
- if (sigA !== null && sigB !== null && sigA === sigB) {
97157
- return `identical incident signature "${sigA}"`;
97158
- }
97159
- const tagsA = a2.tags ?? [];
97160
- const tagsB = b5.tags ?? [];
97161
- const shared = sharedNonGenericTags(tagsA, tagsB);
97162
- const overlap = tagOverlap([...tagsA], [...tagsB]);
97163
- if (overlap >= TAG_OVERLAP_MIN && shared.length >= 1) {
97164
- return `tag overlap ${overlap.toFixed(2)} on [${shared.join(", ")}]`;
97165
- }
97166
- if (sigA === null && sigB === null && shared.length >= 1) {
97167
- const ta = tokenize2(a2.claim);
97168
- const tb = tokenize2(b5.claim);
97169
- if (ta.length >= MIN_CLAIM_TOKENS && tb.length >= MIN_CLAIM_TOKENS) {
97170
- const j3 = tagOverlap(ta, tb);
97171
- if (j3 >= CLAIM_JACCARD_MIN) {
97172
- return `claim similarity ${j3.toFixed(2)} corroborated by [${shared.join(", ")}]`;
97173
- }
97174
- }
97175
- }
97176
- return null;
97177
- }
97178
- function edgeKey(rel, from, to) {
97179
- if (rel === "recurs-with" || rel === "contradicts") {
97180
- const [lo, hi] = from <= to ? [from, to] : [to, from];
97181
- return `${rel}|${lo}|${hi}`;
97182
- }
97183
- return `${rel}|${from}|${to}`;
97184
- }
97185
- async function detectMemoryEdges(store, opts, deps = {}) {
97186
- const repo = opts.repo.trim();
97187
- if (repo === "") return [];
97188
- const items = await store.query({ repo, limit: opts.limit ?? 1e3 });
97189
- const candidates = items.filter((i3) => INCIDENT_DERIVED_KINDS2.has(i3.kind));
97190
- if (candidates.length < 2) return [];
97191
- const existing = /* @__PURE__ */ new Set();
97192
- const investigationOf = /* @__PURE__ */ new Map();
97193
- for (const item of candidates) {
97194
- const out2 = await store.links(item.id, { direction: "out" });
97195
- for (const l2 of out2) {
97196
- if (l2.toKind === "memory") {
97197
- existing.add(edgeKey(l2.rel, l2.fromMemoryId, l2.toRef));
97198
- } else if (l2.toKind === "incident" && l2.rel === "about-incident") {
97199
- if (!investigationOf.has(item.id)) investigationOf.set(item.id, l2.toRef);
97200
- }
97201
- }
97202
- }
97203
- const proposals = [];
97204
- const outcomeCache = /* @__PURE__ */ new Map();
97205
- const resolveOutcome = async (investigationId) => {
97206
- if (deps.outcomeFor === void 0) return null;
97207
- if (outcomeCache.has(investigationId)) return outcomeCache.get(investigationId) ?? null;
97208
- let v4 = null;
97209
- try {
97210
- v4 = await deps.outcomeFor(investigationId);
97211
- } catch {
97212
- v4 = null;
97213
- }
97214
- outcomeCache.set(investigationId, v4);
97215
- return v4;
97216
- };
97217
- for (let i3 = 0; i3 < candidates.length; i3++) {
97218
- const a2 = candidates[i3];
97219
- for (let j3 = i3 + 1; j3 < candidates.length; j3++) {
97220
- const b5 = candidates[j3];
97221
- if (!existing.has(edgeKey("recurs-with", a2.id, b5.id))) {
97222
- const reason = recurrenceReason(a2, b5);
97223
- if (reason !== null) {
97224
- const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
97225
- proposals.push({
97226
- id: recurrenceEdgeId(a2.id, b5.id),
97227
- fromMemoryId: lo,
97228
- toMemoryId: hi,
97229
- rel: "recurs-with",
97230
- detection: "auto:recurrence",
97231
- reason
97232
- });
97233
- }
97234
- }
97235
- if (deps.outcomeFor !== void 0 && a2.kind === "confirmed-outcome" && b5.kind === "confirmed-outcome" && !existing.has(edgeKey("contradicts", a2.id, b5.id))) {
97236
- const sigA = usableSignature(a2.signature);
97237
- const sigB = usableSignature(b5.signature);
97238
- const invA = investigationOf.get(a2.id);
97239
- const invB = investigationOf.get(b5.id);
97240
- if (sigA !== null && sigA === sigB && invA !== void 0 && invB !== void 0) {
97241
- const oa = await resolveOutcome(invA);
97242
- const ob = await resolveOutcome(invB);
97243
- if (oa !== null && ob !== null) {
97244
- const reason = outcomeConflict(oa, ob);
97245
- if (reason !== null) {
97246
- const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
97247
- const invLo = lo === a2.id ? invA : invB;
97248
- const invHi = lo === a2.id ? invB : invA;
97249
- proposals.push({
97250
- id: "",
97251
- // directional — the store mints the id
97252
- fromMemoryId: lo,
97253
- toMemoryId: hi,
97254
- rel: "contradicts",
97255
- detection: "auto:contradiction",
97256
- reason,
97257
- detail: { investigationIds: [invLo, invHi] }
97258
- });
97259
- }
97260
- }
97261
- }
97262
- }
97263
- }
97264
- }
97265
- return proposals;
97266
- }
97267
-
97268
97532
  // ../../packages/engine/src/memory-recall.ts
97269
97533
  init_cjs_shims();
97270
97534
  var import_node_crypto4 = require("crypto");
@@ -101905,8 +102169,13 @@ async function runIndex(opts) {
101905
102169
  console.error(import_picocolors6.default.red(` ${err2.message}`));
101906
102170
  return 1;
101907
102171
  }
101908
- if (!isAnalyzed(root)) {
101909
- console.log(import_picocolors6.default.dim(" analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"));
102172
+ const staleReason = isAnalyzed(root) ? indexNeedsReanalyze(root) : null;
102173
+ if (!isAnalyzed(root) || staleReason) {
102174
+ console.log(
102175
+ import_picocolors6.default.dim(
102176
+ staleReason ? ` existing index is stale (${staleReason}) \u2014 re-analyzing with source-intelligence backend\u2026` : " analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"
102177
+ )
102178
+ );
101910
102179
  try {
101911
102180
  await analyzeRepo(root);
101912
102181
  } catch (err2) {
@@ -115365,6 +115634,9 @@ async function runOneInvestigation(input, ctx, opts = {}) {
115365
115634
  {
115366
115635
  code: ctx.code,
115367
115636
  db: ctx.dbHandle.db,
115637
+ // HOR-432 — the authored-memory store so every investigation auto-captures a recurrence-aware
115638
+ // memory. Best-effort + CONTEXT-ONLY in the engine (never feeds scoring, never blocks delivery).
115639
+ store: createLocalMemoryStore(ctx.dbHandle.db),
115368
115640
  logs: ctx.logs,
115369
115641
  mongo: ctx.mongo,
115370
115642
  postgres: ctx.postgres,
@@ -117422,6 +117694,39 @@ function createCloudMemoryStore(client, cfg) {
117422
117694
  tags: null
117423
117695
  };
117424
117696
  },
117697
+ // HOR-432 consolidation mirror: refresh the scalar fields the cloud accepts (claim/confidence).
117698
+ // PRIVACY: `payload` NEVER leaves the device (it can stash a vector) — it is intentionally dropped
117699
+ // here, exactly like `add`/`toSyncInput`. The return is the server echo (or a synthesized stub) and
117700
+ // is only consumed by the dual-write wrapper, which discards it; reads always come from local.
117701
+ async update(id, patch, _opts) {
117702
+ const input = { clientId: id };
117703
+ if (patch.claim !== void 0) input.claim = patch.claim;
117704
+ if (patch.confidence !== void 0) input.confidence = patch.confidence;
117705
+ const res = await client.syncMemoryItems(projectId, { items: [input] });
117706
+ const record2 = res.items?.find((r) => r.clientId === id);
117707
+ if (record2) return fromWire(record2, repoFallback);
117708
+ return {
117709
+ id,
117710
+ kind: "investigation",
117711
+ claim: patch.claim ?? "",
117712
+ scope: "repo",
117713
+ source: "investigation",
117714
+ evidence: [],
117715
+ confidence: patch.confidence ?? 0,
117716
+ status: "fresh",
117717
+ createdAt: /* @__PURE__ */ new Date(),
117718
+ lastVerifiedAt: null,
117719
+ lastVerifiedHash: null,
117720
+ orgId: null,
117721
+ workspaceId: null,
117722
+ repo: repoFallback,
117723
+ userId: null,
117724
+ visibility: "private",
117725
+ payload: null,
117726
+ signature: null,
117727
+ tags: null
117728
+ };
117729
+ },
117425
117730
  async get(id) {
117426
117731
  try {
117427
117732
  return await findInList(id);
@@ -117529,6 +117834,14 @@ function dualWriteMemoryStore(local, cloud, onCloudError = () => {
117529
117834
  });
117530
117835
  return row;
117531
117836
  },
117837
+ async update(id, patch, opts) {
117838
+ const row = await local.update(id, patch, opts);
117839
+ await mirror(async () => {
117840
+ await cloud.update(id, patch, opts);
117841
+ await mirrorLatestAudit(id);
117842
+ });
117843
+ return row;
117844
+ },
117532
117845
  async setStatus(id, status, audit) {
117533
117846
  await local.setStatus(id, status, audit);
117534
117847
  await mirror(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.12.1",
3
+ "version": "0.12.3",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {