@merittdev/horus 0.12.2 → 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 +535 -232
  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.2" : "dev";
65884
- var PINNED_SOURCE_VERSION = "2.0.1";
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) {
@@ -94858,8 +95318,8 @@ async function investigate(input, deps) {
94858
95318
  // HOR-386 — empty-return: route to `search` for the right name (deterministic).
94859
95319
  nextSteps: route({ command: "investigate", empty: true, query: hint })
94860
95320
  };
94861
- const persistedId2 = await persist(db, input, report2);
94862
- if (persistedId2) report2.id = persistedId2;
95321
+ const persistedEmpty = await persist(db, input, report2);
95322
+ if (persistedEmpty) report2.id = persistedEmpty.id;
94863
95323
  return report2;
94864
95324
  }
94865
95325
  let label = input.service ?? hint;
@@ -96253,13 +96713,26 @@ async function investigate(input, deps) {
96253
96713
  query: hint
96254
96714
  });
96255
96715
  report.sourceStatus = buildRuntimeSourceStatus(evidence2, connectorFlags);
96256
- const persistedId = await persist(db, input, report);
96257
- 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;
96258
96719
  report.persisted = persistedId !== null;
96259
96720
  if (persistedId !== null) {
96260
96721
  const tags = deriveTags(report);
96261
96722
  report.similarIncidents = await recallSimilar(db, tags, persistedId, input.repo ?? null);
96262
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
+ }
96263
96736
  }
96264
96737
  if (persistedId !== null) {
96265
96738
  try {
@@ -96303,7 +96776,7 @@ async function persist(db, input, report) {
96303
96776
  incidentInput: input,
96304
96777
  status: "open",
96305
96778
  summary: report.summary
96306
- }).returning({ id: investigations.id });
96779
+ }).returning({ id: investigations.id, createdAt: investigations.createdAt });
96307
96780
  const row = inserted[0];
96308
96781
  if (!row) return null;
96309
96782
  const investigationId = row.id;
@@ -96347,7 +96820,7 @@ async function persist(db, input, report) {
96347
96820
  }))
96348
96821
  );
96349
96822
  }
96350
- return investigationId;
96823
+ return { id: investigationId, createdAt: row.createdAt ?? null };
96351
96824
  } catch {
96352
96825
  return null;
96353
96826
  }
@@ -97056,225 +97529,6 @@ function blastRadiusToJSON(r) {
97056
97529
  return JSON.stringify(r, null, 2);
97057
97530
  }
97058
97531
 
97059
- // ../../packages/engine/src/memory-store.ts
97060
- init_cjs_shims();
97061
- var HIDDEN_STATUSES = [
97062
- "forgotten",
97063
- "deprecated",
97064
- "contradicted"
97065
- ];
97066
- function tokenize2(text2) {
97067
- const seen = /* @__PURE__ */ new Set();
97068
- const out2 = [];
97069
- for (const raw of text2.toLowerCase().split(/[^a-z0-9]+/)) {
97070
- if (raw !== "" && !seen.has(raw)) {
97071
- seen.add(raw);
97072
- out2.push(raw);
97073
- }
97074
- }
97075
- return out2;
97076
- }
97077
- function scopeSpecificity(scope) {
97078
- if (scope.startsWith("symbol:")) return 3;
97079
- if (scope.startsWith("module:")) return 2;
97080
- if (scope === "repo") return 1;
97081
- return 0;
97082
- }
97083
- var NoopVectorIndex = class {
97084
- entries = /* @__PURE__ */ new Map();
97085
- async upsert(i3) {
97086
- this.entries.set(i3.memoryId, {
97087
- memoryId: i3.memoryId,
97088
- repo: i3.repo,
97089
- scope: i3.scope,
97090
- tokens: tokenize2(i3.claim)
97091
- });
97092
- }
97093
- async search(i3) {
97094
- const repo = i3.repo.trim();
97095
- if (repo === "") return [];
97096
- const queryTokens = tokenize2(i3.query);
97097
- if (queryTokens.length === 0) return [];
97098
- const scored = [...this.entries.values()].filter((e) => e.repo === repo).map((e) => ({
97099
- memoryId: e.memoryId,
97100
- score: tagOverlap(queryTokens, e.tokens),
97101
- specificity: scopeSpecificity(e.scope)
97102
- })).filter((h3) => h3.score > 0).sort(
97103
- (a2, b5) => b5.score - a2.score || b5.specificity - a2.specificity || (a2.memoryId < b5.memoryId ? -1 : a2.memoryId > b5.memoryId ? 1 : 0)
97104
- );
97105
- const limit = Number.isFinite(i3.limit) && i3.limit > 0 ? Math.floor(i3.limit) : scored.length;
97106
- return scored.slice(0, limit).map((h3) => ({ memoryId: h3.memoryId, score: h3.score }));
97107
- }
97108
- async remove(memoryId) {
97109
- this.entries.delete(memoryId);
97110
- }
97111
- };
97112
-
97113
- // ../../packages/engine/src/memory-detect.ts
97114
- init_cjs_shims();
97115
- var import_node_crypto3 = require("crypto");
97116
- var TAG_OVERLAP_MIN = 0.6;
97117
- var CLAIM_JACCARD_MIN = 0.6;
97118
- var MIN_CLAIM_TOKENS = 4;
97119
- var INCIDENT_DERIVED_KINDS2 = /* @__PURE__ */ new Set([
97120
- "confirmed-outcome",
97121
- "incident-pattern"
97122
- ]);
97123
- function usableSignature(sig) {
97124
- const s5 = (sig ?? "").trim();
97125
- if (s5 === "" || /^\|*$/.test(s5)) return null;
97126
- const components = s5.split(/[|,]/).map((c4) => c4.trim()).filter((c4) => c4 !== "");
97127
- if (components.length === 0) return null;
97128
- if (components.every((c4) => isGenericTag(c4))) return null;
97129
- return s5;
97130
- }
97131
- function sharedNonGenericTags(a2, b5) {
97132
- const setB = new Set(b5);
97133
- const seen = /* @__PURE__ */ new Set();
97134
- const out2 = [];
97135
- for (const t2 of a2) {
97136
- if (setB.has(t2) && !isGenericTag(t2) && !seen.has(t2)) {
97137
- seen.add(t2);
97138
- out2.push(t2);
97139
- }
97140
- }
97141
- return out2;
97142
- }
97143
- function recurrenceEdgeId(idA, idB) {
97144
- const [lo, hi] = idA <= idB ? [idA, idB] : [idB, idA];
97145
- const h3 = (0, import_node_crypto3.createHash)("sha256").update(`recurs-with\0${lo}\0${hi}`).digest("hex");
97146
- return `lnk_recurs_${h3.slice(0, 32)}`;
97147
- }
97148
- function normalizeCause(s5) {
97149
- return (s5 ?? "").trim().toLowerCase().replace(/\s+/g, " ");
97150
- }
97151
- function outcomeConflict(a2, b5) {
97152
- const opposite = a2.resolved === "yes" && b5.resolved === "no" || a2.resolved === "no" && b5.resolved === "yes";
97153
- if (opposite) {
97154
- return `outcome verdicts disagree (${a2.resolved} vs ${b5.resolved})`;
97155
- }
97156
- const ca = normalizeCause(a2.confirmedCause);
97157
- const cb = normalizeCause(b5.confirmedCause);
97158
- if (ca !== "" && cb !== "" && ca !== cb) {
97159
- return `confirmed causes differ ("${a2.confirmedCause}" vs "${b5.confirmedCause}")`;
97160
- }
97161
- return null;
97162
- }
97163
- function recurrenceReason(a2, b5) {
97164
- const sigA = usableSignature(a2.signature);
97165
- const sigB = usableSignature(b5.signature);
97166
- if (sigA !== null && sigB !== null && sigA === sigB) {
97167
- return `identical incident signature "${sigA}"`;
97168
- }
97169
- const tagsA = a2.tags ?? [];
97170
- const tagsB = b5.tags ?? [];
97171
- const shared = sharedNonGenericTags(tagsA, tagsB);
97172
- const overlap = tagOverlap([...tagsA], [...tagsB]);
97173
- if (overlap >= TAG_OVERLAP_MIN && shared.length >= 1) {
97174
- return `tag overlap ${overlap.toFixed(2)} on [${shared.join(", ")}]`;
97175
- }
97176
- if (sigA === null && sigB === null && shared.length >= 1) {
97177
- const ta = tokenize2(a2.claim);
97178
- const tb = tokenize2(b5.claim);
97179
- if (ta.length >= MIN_CLAIM_TOKENS && tb.length >= MIN_CLAIM_TOKENS) {
97180
- const j3 = tagOverlap(ta, tb);
97181
- if (j3 >= CLAIM_JACCARD_MIN) {
97182
- return `claim similarity ${j3.toFixed(2)} corroborated by [${shared.join(", ")}]`;
97183
- }
97184
- }
97185
- }
97186
- return null;
97187
- }
97188
- function edgeKey(rel, from, to) {
97189
- if (rel === "recurs-with" || rel === "contradicts") {
97190
- const [lo, hi] = from <= to ? [from, to] : [to, from];
97191
- return `${rel}|${lo}|${hi}`;
97192
- }
97193
- return `${rel}|${from}|${to}`;
97194
- }
97195
- async function detectMemoryEdges(store, opts, deps = {}) {
97196
- const repo = opts.repo.trim();
97197
- if (repo === "") return [];
97198
- const items = await store.query({ repo, limit: opts.limit ?? 1e3 });
97199
- const candidates = items.filter((i3) => INCIDENT_DERIVED_KINDS2.has(i3.kind));
97200
- if (candidates.length < 2) return [];
97201
- const existing = /* @__PURE__ */ new Set();
97202
- const investigationOf = /* @__PURE__ */ new Map();
97203
- for (const item of candidates) {
97204
- const out2 = await store.links(item.id, { direction: "out" });
97205
- for (const l2 of out2) {
97206
- if (l2.toKind === "memory") {
97207
- existing.add(edgeKey(l2.rel, l2.fromMemoryId, l2.toRef));
97208
- } else if (l2.toKind === "incident" && l2.rel === "about-incident") {
97209
- if (!investigationOf.has(item.id)) investigationOf.set(item.id, l2.toRef);
97210
- }
97211
- }
97212
- }
97213
- const proposals = [];
97214
- const outcomeCache = /* @__PURE__ */ new Map();
97215
- const resolveOutcome = async (investigationId) => {
97216
- if (deps.outcomeFor === void 0) return null;
97217
- if (outcomeCache.has(investigationId)) return outcomeCache.get(investigationId) ?? null;
97218
- let v4 = null;
97219
- try {
97220
- v4 = await deps.outcomeFor(investigationId);
97221
- } catch {
97222
- v4 = null;
97223
- }
97224
- outcomeCache.set(investigationId, v4);
97225
- return v4;
97226
- };
97227
- for (let i3 = 0; i3 < candidates.length; i3++) {
97228
- const a2 = candidates[i3];
97229
- for (let j3 = i3 + 1; j3 < candidates.length; j3++) {
97230
- const b5 = candidates[j3];
97231
- if (!existing.has(edgeKey("recurs-with", a2.id, b5.id))) {
97232
- const reason = recurrenceReason(a2, b5);
97233
- if (reason !== null) {
97234
- const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
97235
- proposals.push({
97236
- id: recurrenceEdgeId(a2.id, b5.id),
97237
- fromMemoryId: lo,
97238
- toMemoryId: hi,
97239
- rel: "recurs-with",
97240
- detection: "auto:recurrence",
97241
- reason
97242
- });
97243
- }
97244
- }
97245
- if (deps.outcomeFor !== void 0 && a2.kind === "confirmed-outcome" && b5.kind === "confirmed-outcome" && !existing.has(edgeKey("contradicts", a2.id, b5.id))) {
97246
- const sigA = usableSignature(a2.signature);
97247
- const sigB = usableSignature(b5.signature);
97248
- const invA = investigationOf.get(a2.id);
97249
- const invB = investigationOf.get(b5.id);
97250
- if (sigA !== null && sigA === sigB && invA !== void 0 && invB !== void 0) {
97251
- const oa = await resolveOutcome(invA);
97252
- const ob = await resolveOutcome(invB);
97253
- if (oa !== null && ob !== null) {
97254
- const reason = outcomeConflict(oa, ob);
97255
- if (reason !== null) {
97256
- const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
97257
- const invLo = lo === a2.id ? invA : invB;
97258
- const invHi = lo === a2.id ? invB : invA;
97259
- proposals.push({
97260
- id: "",
97261
- // directional — the store mints the id
97262
- fromMemoryId: lo,
97263
- toMemoryId: hi,
97264
- rel: "contradicts",
97265
- detection: "auto:contradiction",
97266
- reason,
97267
- detail: { investigationIds: [invLo, invHi] }
97268
- });
97269
- }
97270
- }
97271
- }
97272
- }
97273
- }
97274
- }
97275
- return proposals;
97276
- }
97277
-
97278
97532
  // ../../packages/engine/src/memory-recall.ts
97279
97533
  init_cjs_shims();
97280
97534
  var import_node_crypto4 = require("crypto");
@@ -101915,8 +102169,13 @@ async function runIndex(opts) {
101915
102169
  console.error(import_picocolors6.default.red(` ${err2.message}`));
101916
102170
  return 1;
101917
102171
  }
101918
- if (!isAnalyzed(root)) {
101919
- 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
+ );
101920
102179
  try {
101921
102180
  await analyzeRepo(root);
101922
102181
  } catch (err2) {
@@ -115375,6 +115634,9 @@ async function runOneInvestigation(input, ctx, opts = {}) {
115375
115634
  {
115376
115635
  code: ctx.code,
115377
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),
115378
115640
  logs: ctx.logs,
115379
115641
  mongo: ctx.mongo,
115380
115642
  postgres: ctx.postgres,
@@ -117432,6 +117694,39 @@ function createCloudMemoryStore(client, cfg) {
117432
117694
  tags: null
117433
117695
  };
117434
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
+ },
117435
117730
  async get(id) {
117436
117731
  try {
117437
117732
  return await findInList(id);
@@ -117539,6 +117834,14 @@ function dualWriteMemoryStore(local, cloud, onCloudError = () => {
117539
117834
  });
117540
117835
  return row;
117541
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
+ },
117542
117845
  async setStatus(id, status, audit) {
117543
117846
  await local.setStatus(id, status, audit);
117544
117847
  await mirror(async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {