@merittdev/horus 0.12.2 → 0.12.4

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 +1005 -234
  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.4" : "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,
@@ -71586,6 +71622,167 @@ var SourceMemoryVectorIndex = class {
71586
71622
  // ../../packages/connectors/src/contract.ts
71587
71623
  init_cjs_shims();
71588
71624
 
71625
+ // ../../packages/connectors/src/duration.ts
71626
+ init_cjs_shims();
71627
+ function readPath(obj, path) {
71628
+ if (Object.prototype.hasOwnProperty.call(obj, path)) return obj[path];
71629
+ const parts2 = path.split(".");
71630
+ let node = obj;
71631
+ for (const part of parts2) {
71632
+ if (node === null || typeof node !== "object") return void 0;
71633
+ node = node[part];
71634
+ }
71635
+ return node;
71636
+ }
71637
+ var UNIT_MS = {
71638
+ h: 36e5,
71639
+ m: 6e4,
71640
+ s: 1e3,
71641
+ ms: 1,
71642
+ us: 1e-3,
71643
+ "\xB5s": 1e-3,
71644
+ ns: 1e-6
71645
+ };
71646
+ var DURATION_TOKEN = /(\d+(?:\.\d+)?)\s*(ms|us|µs|ns|h|m|s)(?![a-zA-Z])/gi;
71647
+ function parseDurationMs(raw) {
71648
+ if (typeof raw !== "string") return null;
71649
+ const s5 = raw.trim().replace(/^~/, "");
71650
+ if (s5 === "") return null;
71651
+ let total = 0;
71652
+ let matched = false;
71653
+ DURATION_TOKEN.lastIndex = 0;
71654
+ let m5;
71655
+ while ((m5 = DURATION_TOKEN.exec(s5)) !== null) {
71656
+ const value = Number(m5[1]);
71657
+ const unit = m5[2].toLowerCase();
71658
+ const factor = UNIT_MS[unit];
71659
+ if (Number.isFinite(value) && factor !== void 0) {
71660
+ total += value * factor;
71661
+ matched = true;
71662
+ }
71663
+ }
71664
+ if (matched) return total;
71665
+ if (/^\d+(?:\.\d+)?$/.test(s5)) {
71666
+ const n4 = Number(s5);
71667
+ return Number.isFinite(n4) ? n4 : null;
71668
+ }
71669
+ return null;
71670
+ }
71671
+ function extractByRegex(text2, regex) {
71672
+ const m5 = regex.exec(text2);
71673
+ if (m5 === null) return null;
71674
+ const captured = m5[1] ?? m5[0];
71675
+ const v4 = captured.trim();
71676
+ return v4 === "" ? null : v4;
71677
+ }
71678
+ function percentile(sortedAsc, p5) {
71679
+ const n4 = sortedAsc.length;
71680
+ if (n4 === 0) return 0;
71681
+ if (n4 === 1) return sortedAsc[0];
71682
+ const rank = Math.ceil(p5 / 100 * n4);
71683
+ const idx = Math.min(n4 - 1, Math.max(0, rank - 1));
71684
+ return sortedAsc[idx];
71685
+ }
71686
+ function computeStat(durations) {
71687
+ const finite = durations.filter((d3) => Number.isFinite(d3));
71688
+ if (finite.length === 0) {
71689
+ return { avg: 0, p95: 0, count: 0, min: 0, max: 0 };
71690
+ }
71691
+ const sorted = [...finite].sort((a2, b5) => a2 - b5);
71692
+ const sum = sorted.reduce((acc, d3) => acc + d3, 0);
71693
+ return {
71694
+ avg: sum / sorted.length,
71695
+ p95: percentile(sorted, 95),
71696
+ count: sorted.length,
71697
+ min: sorted[0],
71698
+ max: sorted[sorted.length - 1]
71699
+ };
71700
+ }
71701
+ function readDimension(rec, spec) {
71702
+ if (spec.field !== void 0) {
71703
+ const raw = readPath(rec.fields, spec.field);
71704
+ if (raw === null || raw === void 0) {
71705
+ if (spec.pattern === void 0) return null;
71706
+ } else {
71707
+ const v4 = String(raw).trim();
71708
+ if (v4 !== "") return v4;
71709
+ if (spec.pattern === void 0) return null;
71710
+ }
71711
+ }
71712
+ if (spec.pattern !== void 0) {
71713
+ const sourceField = spec.patternField;
71714
+ const text2 = sourceField !== void 0 ? typeof readPath(rec.fields, sourceField) === "string" ? readPath(rec.fields, sourceField) : String(readPath(rec.fields, sourceField) ?? "") : rec.message ?? "";
71715
+ if (text2 === "") return null;
71716
+ try {
71717
+ return extractByRegex(text2, new RegExp(spec.pattern));
71718
+ } catch {
71719
+ return null;
71720
+ }
71721
+ }
71722
+ return null;
71723
+ }
71724
+ function readDuration(rec, opts) {
71725
+ if (opts.durationField !== void 0) {
71726
+ const raw = readPath(rec.fields, opts.durationField);
71727
+ if (typeof raw === "number" && Number.isFinite(raw)) {
71728
+ return opts.durationFieldUnit === "s" ? raw * 1e3 : raw;
71729
+ }
71730
+ if (typeof raw === "string") return parseDurationMs(raw);
71731
+ return null;
71732
+ }
71733
+ const msg = rec.message ?? "";
71734
+ if (msg === "") return null;
71735
+ if (opts.durationPattern !== void 0) {
71736
+ let isolated;
71737
+ try {
71738
+ isolated = extractByRegex(msg, new RegExp(opts.durationPattern));
71739
+ } catch {
71740
+ isolated = null;
71741
+ }
71742
+ return isolated !== null ? parseDurationMs(isolated) : null;
71743
+ }
71744
+ return parseDurationMs(msg);
71745
+ }
71746
+ function extractDurationSamples(records, opts) {
71747
+ const needle = opts.completionText?.toLowerCase();
71748
+ const out2 = [];
71749
+ for (const rec of records) {
71750
+ if (needle !== void 0) {
71751
+ const hay = (rec.message ?? "").toLowerCase();
71752
+ if (!hay.includes(needle)) continue;
71753
+ }
71754
+ const dimension = readDimension(rec, opts.dimension);
71755
+ if (dimension === null) continue;
71756
+ const durationMs = readDuration(rec, opts);
71757
+ if (durationMs === null || !Number.isFinite(durationMs)) continue;
71758
+ out2.push({ dimension, durationMs });
71759
+ }
71760
+ return out2;
71761
+ }
71762
+ function aggregateDurations(samples, dimensionName) {
71763
+ if (samples.length === 0) return null;
71764
+ const grouped = /* @__PURE__ */ new Map();
71765
+ for (const s5 of samples) {
71766
+ const list = grouped.get(s5.dimension);
71767
+ if (list === void 0) grouped.set(s5.dimension, [s5.durationMs]);
71768
+ else list.push(s5.durationMs);
71769
+ }
71770
+ const byValue = {};
71771
+ for (const [value, durations] of grouped) {
71772
+ byValue[value] = computeStat(durations);
71773
+ }
71774
+ return {
71775
+ dimension: dimensionName,
71776
+ unit: "ms",
71777
+ byValue,
71778
+ sampleCount: samples.length
71779
+ };
71780
+ }
71781
+ function durationsByDimension(records, opts) {
71782
+ const samples = extractDurationSamples(records, opts);
71783
+ return aggregateDurations(samples, opts.dimension.name);
71784
+ }
71785
+
71589
71786
  // ../../packages/connectors/src/factory.ts
71590
71787
  init_cjs_shims();
71591
71788
 
@@ -72486,6 +72683,33 @@ var ElasticsearchLogsProvider = class {
72486
72683
  }
72487
72684
  return analysis;
72488
72685
  }
72686
+ async analyzeDurations(opts) {
72687
+ try {
72688
+ const q2 = {
72689
+ level: opts.level ?? "info",
72690
+ limit: opts.limit ?? 500,
72691
+ ...opts.service !== void 0 ? { service: opts.service } : {},
72692
+ ...opts.from !== void 0 ? { from: opts.from } : {},
72693
+ ...opts.to !== void 0 ? { to: opts.to } : {},
72694
+ ...opts.completionText !== void 0 ? { text: opts.completionText, broadText: true } : {}
72695
+ };
72696
+ const records = await this.searchLogs(q2);
72697
+ const normalized = records.map((r) => ({
72698
+ message: r.message,
72699
+ fields: {
72700
+ ...r.raw,
72701
+ ...r.context !== void 0 ? { context: r.context } : {},
72702
+ ...r.eventCode !== void 0 ? { event_code: r.eventCode } : {},
72703
+ ...r.service !== void 0 ? { service_name: r.service } : {},
72704
+ ...r.detail !== void 0 ? { detail: r.detail } : {},
72705
+ message: r.message
72706
+ }
72707
+ }));
72708
+ return durationsByDimension(normalized, opts);
72709
+ } catch {
72710
+ return null;
72711
+ }
72712
+ }
72489
72713
  async checkCompatibility(opts) {
72490
72714
  const fields = [
72491
72715
  this.mapping.timestampField,
@@ -72706,6 +72930,85 @@ function compareWindows(baseline, current, spikeRatio = 1.5) {
72706
72930
  comparisons.sort((a2, b5) => b5.delta - a2.delta);
72707
72931
  return comparisons;
72708
72932
  }
72933
+ function quantileSorted(sortedAsc, p5) {
72934
+ const n4 = sortedAsc.length;
72935
+ if (n4 === 0) return NaN;
72936
+ if (n4 === 1) return sortedAsc[0];
72937
+ const pos = (n4 - 1) * p5;
72938
+ const lo = Math.floor(pos);
72939
+ const hi = Math.ceil(pos);
72940
+ if (lo === hi) return sortedAsc[lo];
72941
+ const frac = pos - lo;
72942
+ return sortedAsc[lo] * (1 - frac) + sortedAsc[hi] * frac;
72943
+ }
72944
+ function median(values2) {
72945
+ const finite = values2.filter((v4) => Number.isFinite(v4));
72946
+ if (finite.length === 0) return NaN;
72947
+ const sorted = [...finite].sort((a2, b5) => a2 - b5);
72948
+ return quantileSorted(sorted, 0.5);
72949
+ }
72950
+ function detectBimodalPopulation(comparisons, opts) {
72951
+ const spike = opts?.spike ?? 1.5;
72952
+ const drop = opts?.drop ?? 0.67;
72953
+ const minShare = opts?.minShare ?? 0.2;
72954
+ const minMinority = opts?.minMinority ?? 2;
72955
+ const minTotal = opts?.minTotal ?? 3;
72956
+ const gapFactor = opts?.gapFactor ?? 2;
72957
+ const total = comparisons.length;
72958
+ const empty = {
72959
+ bimodal: false,
72960
+ total,
72961
+ highCount: 0,
72962
+ lowCount: 0,
72963
+ degenerateCount: 0,
72964
+ highShare: 0,
72965
+ lowShare: 0,
72966
+ highRatio: NaN,
72967
+ lowRatio: NaN
72968
+ };
72969
+ const highRatios = [];
72970
+ const lowRatios = [];
72971
+ let degenerateCount = 0;
72972
+ for (const c4 of comparisons) {
72973
+ const r = c4.ratio;
72974
+ if (!Number.isFinite(r)) {
72975
+ degenerateCount += 1;
72976
+ } else if (r >= spike) {
72977
+ highRatios.push(r);
72978
+ } else if (r > 0 && r <= drop) {
72979
+ lowRatios.push(r);
72980
+ } else if (r === 0) {
72981
+ degenerateCount += 1;
72982
+ }
72983
+ }
72984
+ const highCount = highRatios.length;
72985
+ const lowCount = lowRatios.length;
72986
+ const counterCount = lowCount + degenerateCount;
72987
+ const result = {
72988
+ ...empty,
72989
+ highCount,
72990
+ lowCount,
72991
+ degenerateCount,
72992
+ highShare: total > 0 ? highCount / total : 0,
72993
+ lowShare: total > 0 ? counterCount / total : 0,
72994
+ highRatio: median(highRatios),
72995
+ lowRatio: median(lowRatios)
72996
+ };
72997
+ if (total < minTotal) return result;
72998
+ if (highCount < 1 || counterCount < 1) return result;
72999
+ if (counterCount < minMinority) return result;
73000
+ if (result.lowShare < minShare) return result;
73001
+ if (highCount < minMinority && result.highShare < minShare) return result;
73002
+ if (lowCount > 0) {
73003
+ const hi = result.highRatio;
73004
+ const lo = result.lowRatio;
73005
+ if (Number.isFinite(hi) && Number.isFinite(lo) && lo > 0 && hi / lo < gapFactor) {
73006
+ return result;
73007
+ }
73008
+ }
73009
+ result.bimodal = true;
73010
+ return result;
73011
+ }
72709
73012
 
72710
73013
  // ../../packages/connectors/src/grafana/panels.ts
72711
73014
  init_cjs_shims();
@@ -72833,8 +73136,19 @@ function classifyAnomaly(kind, cmp, opts) {
72833
73136
  }
72834
73137
  function buildFindings(dashboardUid, panelTitle, kind, baseline, current) {
72835
73138
  const comparisons = compareWindows(baseline, current);
73139
+ const bimodal = detectBimodalPopulation(comparisons);
73140
+ const spike = 1.5;
73141
+ const drop = 0.67;
73142
+ const isBimodalMember = (cmp) => {
73143
+ const r = cmp.ratio;
73144
+ if (!Number.isFinite(r)) return true;
73145
+ if (r >= spike) return true;
73146
+ if (r === 0) return true;
73147
+ return r > 0 && r <= drop;
73148
+ };
72836
73149
  return comparisons.map((cmp) => {
72837
- const anomaly = classifyAnomaly(kind, cmp);
73150
+ const baseAnomaly = classifyAnomaly(kind, cmp);
73151
+ const anomaly = bimodal.bimodal && baseAnomaly !== "none" && isBimodalMember(cmp) ? "bimodal-population" : baseAnomaly;
72838
73152
  const currentSeries = current.find((s5) => {
72839
73153
  const keys = Object.keys(cmp.labels);
72840
73154
  return keys.every((k3) => s5.labels[k3] === cmp.labels[k3]);
@@ -72865,6 +73179,8 @@ function anomalyLabel(anomaly) {
72865
73179
  return "Queue Growth";
72866
73180
  case "change":
72867
73181
  return "Change";
73182
+ case "bimodal-population":
73183
+ return "Bimodal Population (possible per-segment variance)";
72868
73184
  case "none":
72869
73185
  return "No Anomaly";
72870
73186
  }
@@ -72879,6 +73195,11 @@ function anomalyRelevance(anomaly) {
72879
73195
  return 0.8;
72880
73196
  case "change":
72881
73197
  return 0.55;
73198
+ // Lower than a confident single-cause spike: a two-population pattern is a
73199
+ // hypothesis ("per-segment variance"), not a uniform regression — keep the
73200
+ // engine honest by NOT presenting it with spike-level weight (HOR-435).
73201
+ case "bimodal-population":
73202
+ return 0.6;
72882
73203
  case "none":
72883
73204
  return 0;
72884
73205
  }
@@ -74009,6 +74330,37 @@ var AxiomProvider = class {
74009
74330
  return [];
74010
74331
  }
74011
74332
  }
74333
+ /**
74334
+ * INFO-level duration coverage (HOR-434). Query NON-error completion/duration rows
74335
+ * (e.g. `Completed MANAGE_SALES:KSA ~2m10s`) and aggregate duration by an extracted
74336
+ * dimension (region / market / tenant) — from a structured field or a regex over the
74337
+ * job id / message. Returns per-dimension stats `{ region: { KSA: {avg,p95,count,…} } }`
74338
+ * so the engine sees per-segment variance the error-biased `collect()` path misses.
74339
+ *
74340
+ * Graceful: no completion rows, no parseable duration, or no extractable dimension
74341
+ * → `null`. Never throws.
74342
+ */
74343
+ async analyzeDurations(opts) {
74344
+ try {
74345
+ const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
74346
+ const from = opts.from ?? new Date(
74347
+ Date.parse(to) - (this.opts.windowMs ?? DEFAULT_WINDOW_MS)
74348
+ ).toISOString();
74349
+ const apl = buildApl(
74350
+ this.opts.dataset,
74351
+ opts.completionText !== void 0 ? [opts.completionText] : [],
74352
+ opts.limit ?? 500
74353
+ );
74354
+ const rows = await this.client.query(apl, from, to);
74355
+ const normalized = rows.map((rec) => ({
74356
+ message: pickField2(rec.fields, MESSAGE_FIELDS) ?? "",
74357
+ fields: rec.fields
74358
+ }));
74359
+ return durationsByDimension(normalized, opts);
74360
+ } catch {
74361
+ return null;
74362
+ }
74363
+ }
74012
74364
  async health() {
74013
74365
  return this.client.health();
74014
74366
  }
@@ -92770,6 +93122,36 @@ function generateHypotheses(evidence2, _correlation, ctx) {
92770
93122
  contradictingEvidenceIds: [],
92771
93123
  missingEvidence: infraSupport.length > 0 ? [] : ["Infrastructure metrics + datastore/cache state (`horus metrics`)"]
92772
93124
  });
93125
+ const benignSupport = [
93126
+ .../* @__PURE__ */ new Set([
93127
+ ...ctx.perDimensionDurationEvIds ?? [],
93128
+ ...ctx.bimodalMetricEvIds ?? []
93129
+ ])
93130
+ ];
93131
+ if (benignSupport.length > 0 || ctx.benignVarianceApplicable === true) {
93132
+ hyps.push({
93133
+ id: globalThis.crypto.randomUUID(),
93134
+ category: "benign-variance",
93135
+ statement: "The anomaly may reflect expected per-segment/per-region variance (or a skewed average), not a failure.",
93136
+ confidence: 0.1,
93137
+ supportingEvidenceIds: benignSupport,
93138
+ contradictingEvidenceIds: [],
93139
+ missingEvidence: benignSupport.length > 0 ? [] : [
93140
+ "Per-segment duration breakdown (region/market/tenant) or a bimodal metric distribution (`horus logs` / `horus metrics`)"
93141
+ ]
93142
+ });
93143
+ }
93144
+ const suggested = new Set(ctx.alertSuggestedCategories ?? []);
93145
+ if (suggested.size > 0) {
93146
+ for (const h3 of hyps) {
93147
+ if (suggested.has(h3.category) && h3.supportingEvidenceIds.length === 0) {
93148
+ h3.missingEvidence = [
93149
+ ...h3.missingEvidence,
93150
+ "Suggested by the alert/hint text \u2014 confidence requires independent evidence, not the alert wording itself"
93151
+ ];
93152
+ }
93153
+ }
93154
+ }
92773
93155
  hyps.sort((a2, b5) => b5.confidence - a2.confidence);
92774
93156
  return hyps;
92775
93157
  }
@@ -92817,6 +93199,26 @@ function validateHypotheses(hyps, evidence2) {
92817
93199
  rationale
92818
93200
  };
92819
93201
  });
93202
+ const SHARED_EVIDENCE_CEILING = 0.75;
93203
+ const bySupport = /* @__PURE__ */ new Map();
93204
+ for (const v4 of validated) {
93205
+ if (v4.contradictingPresent > 0) continue;
93206
+ const presentSupport = v4.supportingEvidenceIds.filter((id) => present.has(id)).sort();
93207
+ if (presentSupport.length === 0) continue;
93208
+ const key = presentSupport.join("|");
93209
+ const list = bySupport.get(key);
93210
+ if (list === void 0) bySupport.set(key, [v4]);
93211
+ else list.push(v4);
93212
+ }
93213
+ for (const cohort of bySupport.values()) {
93214
+ if (cohort.length < 2) continue;
93215
+ for (const v4 of cohort) {
93216
+ if (v4.confidence > SHARED_EVIDENCE_CEILING) {
93217
+ v4.confidence = SHARED_EVIDENCE_CEILING;
93218
+ v4.rationale = v4.rationale.replace(/\.$/, "") + `; capped at ${SHARED_EVIDENCE_CEILING} \u2014 shares all supporting evidence with ${cohort.length - 1} other hypothesis(es) and nothing distinguishes them.`;
93219
+ }
93220
+ }
93221
+ }
92820
93222
  validated.sort((a2, b5) => {
92821
93223
  const aElim = a2.verdict === "eliminated" ? 1 : 0;
92822
93224
  const bElim = b5.verdict === "eliminated" ? 1 : 0;
@@ -93004,7 +93406,13 @@ var MEMORY_RELS = /* @__PURE__ */ new Set([
93004
93406
  function isSymmetricRel(rel) {
93005
93407
  return rel === "recurs-with";
93006
93408
  }
93007
- var INCIDENT_DERIVED_KINDS = /* @__PURE__ */ new Set(["confirmed-outcome", "incident-pattern"]);
93409
+ var INCIDENT_DERIVED_KINDS = /* @__PURE__ */ new Set([
93410
+ "confirmed-outcome",
93411
+ "incident-pattern",
93412
+ // HOR-432: auto-captured investigation memories carry the same incident-family signature/tags so
93413
+ // they participate in recurrence detection. CONTEXT-ONLY — never read by the scoring path.
93414
+ "investigation"
93415
+ ]);
93008
93416
  function normalizeStoredSignature(sig) {
93009
93417
  const s5 = (sig ?? "").trim();
93010
93418
  if (s5 === "" || /^\|*$/.test(s5)) return null;
@@ -93109,6 +93517,35 @@ function createLocalMemoryStore(db) {
93109
93517
  });
93110
93518
  return row;
93111
93519
  },
93520
+ async update(id, patch, opts) {
93521
+ const current = await getById(id);
93522
+ if (current === null) throw new Error(`memory item not found: ${id}`);
93523
+ const set = {};
93524
+ if (patch.claim !== void 0) {
93525
+ assertClaimClean(patch.claim);
93526
+ set.claim = patch.claim;
93527
+ }
93528
+ if (patch.confidence !== void 0) set.confidence = patch.confidence;
93529
+ if (patch.payload !== void 0) set.payload = patch.payload;
93530
+ if (Object.keys(set).length > 0) {
93531
+ await db.update(memoryItem).set(set).where(eq(memoryItem.id, id));
93532
+ }
93533
+ await appendAudit({
93534
+ memoryId: id,
93535
+ action: opts.action ?? "update",
93536
+ actor: opts.audit.actor,
93537
+ fromStatus: null,
93538
+ toStatus: null,
93539
+ note: opts.audit.note ?? null,
93540
+ detail: {
93541
+ ...opts.detection ? { detection: opts.detection } : {},
93542
+ ...opts.detail ?? {}
93543
+ }
93544
+ });
93545
+ const updated = await getById(id);
93546
+ if (updated === null) throw new Error(`memory item vanished during update: ${id}`);
93547
+ return updated;
93548
+ },
93112
93549
  get: getById,
93113
93550
  async query(q2) {
93114
93551
  const repo = q2.repo.trim();
@@ -93296,6 +93733,395 @@ function createLocalMemoryStore(db) {
93296
93733
  };
93297
93734
  }
93298
93735
 
93736
+ // ../../packages/engine/src/auto-investigation-memory.ts
93737
+ init_cjs_shims();
93738
+
93739
+ // ../../packages/engine/src/memory-detect.ts
93740
+ init_cjs_shims();
93741
+ var import_node_crypto3 = require("crypto");
93742
+
93743
+ // ../../packages/engine/src/memory-store.ts
93744
+ init_cjs_shims();
93745
+ var HIDDEN_STATUSES = [
93746
+ "forgotten",
93747
+ "deprecated",
93748
+ "contradicted"
93749
+ ];
93750
+ function tokenize2(text2) {
93751
+ const seen = /* @__PURE__ */ new Set();
93752
+ const out2 = [];
93753
+ for (const raw of text2.toLowerCase().split(/[^a-z0-9]+/)) {
93754
+ if (raw !== "" && !seen.has(raw)) {
93755
+ seen.add(raw);
93756
+ out2.push(raw);
93757
+ }
93758
+ }
93759
+ return out2;
93760
+ }
93761
+ function scopeSpecificity(scope) {
93762
+ if (scope.startsWith("symbol:")) return 3;
93763
+ if (scope.startsWith("module:")) return 2;
93764
+ if (scope === "repo") return 1;
93765
+ return 0;
93766
+ }
93767
+ var NoopVectorIndex = class {
93768
+ entries = /* @__PURE__ */ new Map();
93769
+ async upsert(i3) {
93770
+ this.entries.set(i3.memoryId, {
93771
+ memoryId: i3.memoryId,
93772
+ repo: i3.repo,
93773
+ scope: i3.scope,
93774
+ tokens: tokenize2(i3.claim)
93775
+ });
93776
+ }
93777
+ async search(i3) {
93778
+ const repo = i3.repo.trim();
93779
+ if (repo === "") return [];
93780
+ const queryTokens = tokenize2(i3.query);
93781
+ if (queryTokens.length === 0) return [];
93782
+ const scored = [...this.entries.values()].filter((e) => e.repo === repo).map((e) => ({
93783
+ memoryId: e.memoryId,
93784
+ score: tagOverlap(queryTokens, e.tokens),
93785
+ specificity: scopeSpecificity(e.scope)
93786
+ })).filter((h3) => h3.score > 0).sort(
93787
+ (a2, b5) => b5.score - a2.score || b5.specificity - a2.specificity || (a2.memoryId < b5.memoryId ? -1 : a2.memoryId > b5.memoryId ? 1 : 0)
93788
+ );
93789
+ const limit = Number.isFinite(i3.limit) && i3.limit > 0 ? Math.floor(i3.limit) : scored.length;
93790
+ return scored.slice(0, limit).map((h3) => ({ memoryId: h3.memoryId, score: h3.score }));
93791
+ }
93792
+ async remove(memoryId) {
93793
+ this.entries.delete(memoryId);
93794
+ }
93795
+ };
93796
+
93797
+ // ../../packages/engine/src/memory-detect.ts
93798
+ var TAG_OVERLAP_MIN = 0.6;
93799
+ var CLAIM_JACCARD_MIN = 0.6;
93800
+ var MIN_CLAIM_TOKENS = 4;
93801
+ var INCIDENT_DERIVED_KINDS2 = /* @__PURE__ */ new Set([
93802
+ "confirmed-outcome",
93803
+ "incident-pattern"
93804
+ ]);
93805
+ function usableSignature(sig) {
93806
+ const s5 = (sig ?? "").trim();
93807
+ if (s5 === "" || /^\|*$/.test(s5)) return null;
93808
+ const components = s5.split(/[|,]/).map((c4) => c4.trim()).filter((c4) => c4 !== "");
93809
+ if (components.length === 0) return null;
93810
+ if (components.every((c4) => isGenericTag(c4))) return null;
93811
+ return s5;
93812
+ }
93813
+ function sharedNonGenericTags(a2, b5) {
93814
+ const setB = new Set(b5);
93815
+ const seen = /* @__PURE__ */ new Set();
93816
+ const out2 = [];
93817
+ for (const t2 of a2) {
93818
+ if (setB.has(t2) && !isGenericTag(t2) && !seen.has(t2)) {
93819
+ seen.add(t2);
93820
+ out2.push(t2);
93821
+ }
93822
+ }
93823
+ return out2;
93824
+ }
93825
+ function recurrenceEdgeId(idA, idB) {
93826
+ const [lo, hi] = idA <= idB ? [idA, idB] : [idB, idA];
93827
+ const h3 = (0, import_node_crypto3.createHash)("sha256").update(`recurs-with\0${lo}\0${hi}`).digest("hex");
93828
+ return `lnk_recurs_${h3.slice(0, 32)}`;
93829
+ }
93830
+ function normalizeCause(s5) {
93831
+ return (s5 ?? "").trim().toLowerCase().replace(/\s+/g, " ");
93832
+ }
93833
+ function outcomeConflict(a2, b5) {
93834
+ const opposite = a2.resolved === "yes" && b5.resolved === "no" || a2.resolved === "no" && b5.resolved === "yes";
93835
+ if (opposite) {
93836
+ return `outcome verdicts disagree (${a2.resolved} vs ${b5.resolved})`;
93837
+ }
93838
+ const ca = normalizeCause(a2.confirmedCause);
93839
+ const cb = normalizeCause(b5.confirmedCause);
93840
+ if (ca !== "" && cb !== "" && ca !== cb) {
93841
+ return `confirmed causes differ ("${a2.confirmedCause}" vs "${b5.confirmedCause}")`;
93842
+ }
93843
+ return null;
93844
+ }
93845
+ function recurrenceReason(a2, b5) {
93846
+ const sigA = usableSignature(a2.signature);
93847
+ const sigB = usableSignature(b5.signature);
93848
+ if (sigA !== null && sigB !== null && sigA === sigB) {
93849
+ return `identical incident signature "${sigA}"`;
93850
+ }
93851
+ const tagsA = a2.tags ?? [];
93852
+ const tagsB = b5.tags ?? [];
93853
+ const shared = sharedNonGenericTags(tagsA, tagsB);
93854
+ const overlap = tagOverlap([...tagsA], [...tagsB]);
93855
+ if (overlap >= TAG_OVERLAP_MIN && shared.length >= 1) {
93856
+ return `tag overlap ${overlap.toFixed(2)} on [${shared.join(", ")}]`;
93857
+ }
93858
+ if (sigA === null && sigB === null && shared.length >= 1) {
93859
+ const ta = tokenize2(a2.claim);
93860
+ const tb = tokenize2(b5.claim);
93861
+ if (ta.length >= MIN_CLAIM_TOKENS && tb.length >= MIN_CLAIM_TOKENS) {
93862
+ const j3 = tagOverlap(ta, tb);
93863
+ if (j3 >= CLAIM_JACCARD_MIN) {
93864
+ return `claim similarity ${j3.toFixed(2)} corroborated by [${shared.join(", ")}]`;
93865
+ }
93866
+ }
93867
+ }
93868
+ return null;
93869
+ }
93870
+ function edgeKey(rel, from, to) {
93871
+ if (rel === "recurs-with" || rel === "contradicts") {
93872
+ const [lo, hi] = from <= to ? [from, to] : [to, from];
93873
+ return `${rel}|${lo}|${hi}`;
93874
+ }
93875
+ return `${rel}|${from}|${to}`;
93876
+ }
93877
+ async function detectMemoryEdges(store, opts, deps = {}) {
93878
+ const repo = opts.repo.trim();
93879
+ if (repo === "") return [];
93880
+ const items = await store.query({ repo, limit: opts.limit ?? 1e3 });
93881
+ const candidates = items.filter((i3) => INCIDENT_DERIVED_KINDS2.has(i3.kind));
93882
+ if (candidates.length < 2) return [];
93883
+ const existing = /* @__PURE__ */ new Set();
93884
+ const investigationOf = /* @__PURE__ */ new Map();
93885
+ for (const item of candidates) {
93886
+ const out2 = await store.links(item.id, { direction: "out" });
93887
+ for (const l2 of out2) {
93888
+ if (l2.toKind === "memory") {
93889
+ existing.add(edgeKey(l2.rel, l2.fromMemoryId, l2.toRef));
93890
+ } else if (l2.toKind === "incident" && l2.rel === "about-incident") {
93891
+ if (!investigationOf.has(item.id)) investigationOf.set(item.id, l2.toRef);
93892
+ }
93893
+ }
93894
+ }
93895
+ const proposals = [];
93896
+ const outcomeCache = /* @__PURE__ */ new Map();
93897
+ const resolveOutcome = async (investigationId) => {
93898
+ if (deps.outcomeFor === void 0) return null;
93899
+ if (outcomeCache.has(investigationId)) return outcomeCache.get(investigationId) ?? null;
93900
+ let v4 = null;
93901
+ try {
93902
+ v4 = await deps.outcomeFor(investigationId);
93903
+ } catch {
93904
+ v4 = null;
93905
+ }
93906
+ outcomeCache.set(investigationId, v4);
93907
+ return v4;
93908
+ };
93909
+ for (let i3 = 0; i3 < candidates.length; i3++) {
93910
+ const a2 = candidates[i3];
93911
+ for (let j3 = i3 + 1; j3 < candidates.length; j3++) {
93912
+ const b5 = candidates[j3];
93913
+ if (!existing.has(edgeKey("recurs-with", a2.id, b5.id))) {
93914
+ const reason = recurrenceReason(a2, b5);
93915
+ if (reason !== null) {
93916
+ const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
93917
+ proposals.push({
93918
+ id: recurrenceEdgeId(a2.id, b5.id),
93919
+ fromMemoryId: lo,
93920
+ toMemoryId: hi,
93921
+ rel: "recurs-with",
93922
+ detection: "auto:recurrence",
93923
+ reason
93924
+ });
93925
+ }
93926
+ }
93927
+ if (deps.outcomeFor !== void 0 && a2.kind === "confirmed-outcome" && b5.kind === "confirmed-outcome" && !existing.has(edgeKey("contradicts", a2.id, b5.id))) {
93928
+ const sigA = usableSignature(a2.signature);
93929
+ const sigB = usableSignature(b5.signature);
93930
+ const invA = investigationOf.get(a2.id);
93931
+ const invB = investigationOf.get(b5.id);
93932
+ if (sigA !== null && sigA === sigB && invA !== void 0 && invB !== void 0) {
93933
+ const oa = await resolveOutcome(invA);
93934
+ const ob = await resolveOutcome(invB);
93935
+ if (oa !== null && ob !== null) {
93936
+ const reason = outcomeConflict(oa, ob);
93937
+ if (reason !== null) {
93938
+ const [lo, hi] = a2.id <= b5.id ? [a2.id, b5.id] : [b5.id, a2.id];
93939
+ const invLo = lo === a2.id ? invA : invB;
93940
+ const invHi = lo === a2.id ? invB : invA;
93941
+ proposals.push({
93942
+ id: "",
93943
+ // directional — the store mints the id
93944
+ fromMemoryId: lo,
93945
+ toMemoryId: hi,
93946
+ rel: "contradicts",
93947
+ detection: "auto:contradiction",
93948
+ reason,
93949
+ detail: { investigationIds: [invLo, invHi] }
93950
+ });
93951
+ }
93952
+ }
93953
+ }
93954
+ }
93955
+ }
93956
+ }
93957
+ return proposals;
93958
+ }
93959
+
93960
+ // ../../packages/engine/src/auto-investigation-memory.ts
93961
+ var RECURRENCE_SCAN_LIMIT = 100;
93962
+ var MAX_INVESTIGATION_IDS = 20;
93963
+ function snippet(text2, max = 140) {
93964
+ const s5 = text2.trim().replace(/\s+/g, " ");
93965
+ return s5.length > max ? `${s5.slice(0, max - 1)}\u2026` : s5;
93966
+ }
93967
+ function autoInvestigationMemoryEnabled() {
93968
+ const v4 = (process.env.HORUS_AUTO_INVESTIGATION_MEMORY ?? "").trim().toLowerCase();
93969
+ return v4 !== "0" && v4 !== "false" && v4 !== "off" && v4 !== "no";
93970
+ }
93971
+ function deriveInvestigationFields(report) {
93972
+ const hint = report.input.hint;
93973
+ const topHyp = report.hypotheses[0];
93974
+ const headline = topHyp !== void 0 ? snippet(topHyp.statement || topHyp.category || report.summary) : snippet(report.summary || "(no leading hypothesis)");
93975
+ return {
93976
+ hint,
93977
+ claim: `Investigation: ${snippet(hint)} -> ${headline}`,
93978
+ confidence: report.confidence,
93979
+ signature: deriveSignature(report),
93980
+ tags: deriveTags(report),
93981
+ topHypothesisCategory: topHyp !== void 0 ? topHyp.category ?? null : null
93982
+ };
93983
+ }
93984
+ function recurrenceCountOf(payload) {
93985
+ if (payload !== null && typeof payload === "object") {
93986
+ const c4 = payload.recurrenceCount;
93987
+ if (typeof c4 === "number" && Number.isFinite(c4) && c4 >= 1) return Math.floor(c4);
93988
+ }
93989
+ return 1;
93990
+ }
93991
+ async function createInvestigationMemory(store, investigationId, report, audit) {
93992
+ const repo = report.input.repo?.trim() || null;
93993
+ if (repo === null) return null;
93994
+ const fields = deriveInvestigationFields(report);
93995
+ const item = {
93996
+ id: "",
93997
+ kind: "investigation",
93998
+ source: "investigation",
93999
+ scope: "repo",
94000
+ visibility: "private",
94001
+ // HONEST confidence — the report's own value, never inflated.
94002
+ confidence: fields.confidence,
94003
+ claim: fields.claim,
94004
+ repo,
94005
+ evidence: [],
94006
+ // Incident-family recall keys (same derivation as incident_memory) so this memory recurs.
94007
+ signature: fields.signature,
94008
+ tags: fields.tags,
94009
+ payload: {
94010
+ investigationId,
94011
+ hint: fields.hint,
94012
+ topHypothesis: fields.topHypothesisCategory,
94013
+ // First sighting: a single investigation is "recurred 1×". Bumped on each later recurrence.
94014
+ recurrenceCount: 1
94015
+ }
94016
+ };
94017
+ const created = await store.add(item, audit);
94018
+ if (investigationId !== null && investigationId.trim() !== "") {
94019
+ await store.addLink(
94020
+ {
94021
+ id: "",
94022
+ fromMemoryId: created.id,
94023
+ rel: "about-incident",
94024
+ toKind: "incident",
94025
+ toRef: investigationId
94026
+ },
94027
+ { detection: "structural", audit }
94028
+ );
94029
+ }
94030
+ return created;
94031
+ }
94032
+ async function detectRecurrence(store, probe2, repo) {
94033
+ const r = repo.trim();
94034
+ if (r === "") return null;
94035
+ const existing = await store.query({
94036
+ repo: r,
94037
+ kind: ["investigation"],
94038
+ limit: RECURRENCE_SCAN_LIMIT
94039
+ });
94040
+ for (const candidate of existing) {
94041
+ if (recurrenceReason(candidate, probe2) !== null) return candidate;
94042
+ }
94043
+ return null;
94044
+ }
94045
+ async function consolidateRecurrence(store, existing, investigationId, fields, reportTime, audit) {
94046
+ const prev = existing.payload ?? {};
94047
+ const recurrenceCount = recurrenceCountOf(prev) + 1;
94048
+ const prevIds = Array.isArray(prev.investigationIds) ? prev.investigationIds.filter((x4) => typeof x4 === "string") : [];
94049
+ const seedIds = prevIds.length > 0 ? prevIds : typeof prev.investigationId === "string" ? [prev.investigationId] : [];
94050
+ const thisId = investigationId !== null && investigationId.trim() !== "" ? investigationId : null;
94051
+ const investigationIds = (thisId !== null ? [...seedIds, thisId] : seedIds).slice(
94052
+ -MAX_INVESTIGATION_IDS
94053
+ );
94054
+ const payload = {
94055
+ ...prev,
94056
+ recurrenceCount,
94057
+ lastSeenAt: reportTime.toISOString(),
94058
+ investigationIds,
94059
+ // Refresh the "latest finding" provenance; keep the original first-seen id for reference.
94060
+ hint: fields.hint,
94061
+ topHypothesis: fields.topHypothesisCategory,
94062
+ investigationId: typeof prev.investigationId === "string" ? prev.investigationId : seedIds[0] ?? thisId
94063
+ };
94064
+ const updated = await store.update(
94065
+ existing.id,
94066
+ {
94067
+ claim: fields.claim,
94068
+ // HONEST: the LATEST report's confidence, verbatim — never inflated, never an average.
94069
+ confidence: fields.confidence,
94070
+ payload
94071
+ },
94072
+ {
94073
+ audit,
94074
+ action: "recurrence",
94075
+ detection: "auto:recurrence-consolidate",
94076
+ detail: { recurrenceCount, ...thisId !== null ? { investigationId: thisId } : {} }
94077
+ }
94078
+ );
94079
+ if (thisId !== null) {
94080
+ await store.addLink(
94081
+ {
94082
+ id: "",
94083
+ fromMemoryId: existing.id,
94084
+ rel: "about-incident",
94085
+ toKind: "incident",
94086
+ toRef: thisId
94087
+ },
94088
+ { detection: "structural", audit }
94089
+ );
94090
+ }
94091
+ return updated;
94092
+ }
94093
+ async function captureInvestigationMemory(store, investigationId, report, reportTime, audit) {
94094
+ const repo = report.input.repo?.trim() || null;
94095
+ if (repo === null) return { action: "skipped", memoryId: null, recurrenceCount: 0 };
94096
+ const fields = deriveInvestigationFields(report);
94097
+ const existing = await detectRecurrence(
94098
+ store,
94099
+ { signature: fields.signature, tags: fields.tags, claim: fields.claim },
94100
+ repo
94101
+ );
94102
+ if (existing !== null) {
94103
+ const updated = await consolidateRecurrence(
94104
+ store,
94105
+ existing,
94106
+ investigationId,
94107
+ fields,
94108
+ reportTime,
94109
+ audit
94110
+ );
94111
+ return {
94112
+ action: "consolidated",
94113
+ memoryId: updated.id,
94114
+ recurrenceCount: recurrenceCountOf(updated.payload)
94115
+ };
94116
+ }
94117
+ const created = await createInvestigationMemory(store, investigationId, report, audit);
94118
+ return {
94119
+ action: created !== null ? "created" : "skipped",
94120
+ memoryId: created?.id ?? null,
94121
+ recurrenceCount: 1
94122
+ };
94123
+ }
94124
+
93299
94125
  // ../../packages/engine/src/gaps.ts
93300
94126
  init_cjs_shims();
93301
94127
  function detectMissingEvidence(r, connectors = {}, mode) {
@@ -94544,6 +95370,46 @@ function looksPerformance(hint) {
94544
95370
  hint
94545
95371
  );
94546
95372
  }
95373
+ function looksDurationThemed(hint) {
95374
+ return looksPerformance(hint) || /\b(duration|took|elapsed|anomal(?:y|ies|ous)|spike|variance|regress(?:ion|ed)?|p\d{2})\b/i.test(
95375
+ hint
95376
+ );
95377
+ }
95378
+ function parseSuggestedCauses(hint) {
95379
+ const SUGGESTIVE = /\b(could indicate|may be|might be|maybe|possibly|suggest(?:s|ing|ed)?|likely (?:caused|due)|points? to|consistent with|indicat(?:es|ive)|appears? to be|seems? to be|probably)\b/i;
95380
+ if (!SUGGESTIVE.test(hint)) return [];
95381
+ const CATEGORY_CUES = [
95382
+ ["deployment-regression", /\b(deploy(?:ment|ed)?|release|rollout|regression|recent change|shipped)\b/i],
95383
+ ["infrastructure", /\b(database|db|infra(?:structure)?|network|cache|disk|memory|cpu|oom|out of memory|connection pool|datastore|redis|mongo|postgres)\b/i],
95384
+ ["external-api-latency", /\b(upstream|external api|third[- ]party|downstream|api latency|timeout|slow (?:api|service)|gateway)\b/i],
95385
+ ["retry-storm", /\b(retr(?:y|ies)|retry storm|thundering herd|amplif)/i],
95386
+ ["queue-backlog", /\b(queue|backlog|consumer lag|enqueue)\b/i],
95387
+ ["worker-slowdown", /\b(worker|consumer)\b/i],
95388
+ ["benign-variance", /\b(per[- ]segment|per[- ]region|skew(?:ed)?|variance|expected variation|noise)\b/i]
95389
+ ];
95390
+ const out2 = [];
95391
+ for (const [category, cue] of CATEGORY_CUES) {
95392
+ if (cue.test(hint)) out2.push(category);
95393
+ }
95394
+ return out2;
95395
+ }
95396
+ function formatDurationMs(ms) {
95397
+ if (!Number.isFinite(ms) || ms < 0) return "?";
95398
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
95399
+ const totalSec = ms / 1e3;
95400
+ if (totalSec < 60) {
95401
+ const s5 = Math.round(totalSec * 10) / 10;
95402
+ return `${s5}s`;
95403
+ }
95404
+ const totalMin = Math.floor(totalSec / 60);
95405
+ const remSec = Math.round(totalSec % 60);
95406
+ if (totalMin < 60) {
95407
+ return remSec > 0 ? `${totalMin}m${remSec}s` : `${totalMin}m`;
95408
+ }
95409
+ const h3 = Math.floor(totalMin / 60);
95410
+ const remMin = totalMin % 60;
95411
+ return remMin > 0 ? `${h3}h${remMin}m` : `${h3}h`;
95412
+ }
94547
95413
  var SOURCE_IMPACT_RE = [
94548
95414
  /\bwhat depends on\b/i,
94549
95415
  /\bblast[- ]?radius\b/i,
@@ -94694,6 +95560,7 @@ async function investigate(input, deps) {
94694
95560
  const hint = input.hint.trim();
94695
95561
  const intent = classifyIntent(hint, input.since);
94696
95562
  const namedSymbols = parseNamedSymbols(hint);
95563
+ const alertSuggestedCategories = parseSuggestedCauses(hint);
94697
95564
  const sourceImpactHint = intent === "source-impact";
94698
95565
  const preferNamed = sourceImpactHint && namedSymbols.length > 0 ? namedSymbols[0] : void 0;
94699
95566
  const evidence2 = [];
@@ -94858,8 +95725,8 @@ async function investigate(input, deps) {
94858
95725
  // HOR-386 — empty-return: route to `search` for the right name (deterministic).
94859
95726
  nextSteps: route({ command: "investigate", empty: true, query: hint })
94860
95727
  };
94861
- const persistedId2 = await persist(db, input, report2);
94862
- if (persistedId2) report2.id = persistedId2;
95728
+ const persistedEmpty = await persist(db, input, report2);
95729
+ if (persistedEmpty) report2.id = persistedEmpty.id;
94863
95730
  return report2;
94864
95731
  }
94865
95732
  let label = input.service ?? hint;
@@ -95018,6 +95885,7 @@ async function investigate(input, deps) {
95018
95885
  const logEvIds = [];
95019
95886
  const directLogEvIds = [];
95020
95887
  const ambientLogEvIds = [];
95888
+ const perDimensionDurationEvIds = [];
95021
95889
  const directSignatures = [];
95022
95890
  const directEvIdByKey = /* @__PURE__ */ new Map();
95023
95891
  const seedEmittedJoins = [];
@@ -95436,6 +96304,55 @@ async function investigate(input, deps) {
95436
96304
  axiomCollected = false;
95437
96305
  }
95438
96306
  }
96307
+ if (looksDurationThemed(hint)) {
96308
+ const from = logWindowFrom(input.logsSince ?? input.since);
96309
+ const baseDurationOpts = input.durationDimension ?? {
96310
+ // Conservative default for the common `Completed <JOB>:<REGION> ~2m10s` shape: extract an
96311
+ // alphanumeric token after a colon as the dimension, scoped to completion lines, parsing
96312
+ // the duration out of the message. A non-matching corpus yields null (graceful).
96313
+ dimension: { name: "region", pattern: ":\\s*([A-Za-z][\\w-]+)" },
96314
+ completionText: "Completed"
96315
+ };
96316
+ const durationOpts = {
96317
+ ...baseDurationOpts,
96318
+ from: baseDurationOpts.from ?? from,
96319
+ ...input.service !== void 0 && baseDurationOpts.service === void 0 ? { service: input.service } : {},
96320
+ level: baseDurationOpts.level ?? "info"
96321
+ };
96322
+ const durationProviders = [];
96323
+ if (deps.logs && typeof deps.logs.analyzeDurations === "function") {
96324
+ durationProviders.push({ source: "elasticsearch", analyze: (o5) => deps.logs.analyzeDurations(o5) });
96325
+ }
96326
+ if (deps.axiom && typeof deps.axiom.analyzeDurations === "function") {
96327
+ durationProviders.push({ source: "axiom", analyze: (o5) => deps.axiom.analyzeDurations(o5) });
96328
+ }
96329
+ for (const provider of durationProviders) {
96330
+ try {
96331
+ const result = await provider.analyze(durationOpts);
96332
+ if (result === null) continue;
96333
+ const values2 = Object.entries(result.byValue);
96334
+ if (values2.length === 0) continue;
96335
+ const summary2 = values2.sort((a2, b5) => b5[1].avg - a2[1].avg).map(([val, st3]) => `${val} ${formatDurationMs(st3.avg)}`).join(", ");
96336
+ const ev = mkEv(
96337
+ "log",
96338
+ `Duration by ${result.dimension}: ${summary2}`.slice(0, 220),
96339
+ {
96340
+ source: provider.source,
96341
+ kind: "duration-by-dimension",
96342
+ dimension: result.dimension,
96343
+ unit: result.unit,
96344
+ byValue: result.byValue,
96345
+ sampleCount: result.sampleCount
96346
+ },
96347
+ {},
96348
+ void 0,
96349
+ 0.6
96350
+ );
96351
+ perDimensionDurationEvIds.push(ev.id);
96352
+ } catch {
96353
+ }
96354
+ }
96355
+ }
95439
96356
  let stateAnalysis = null;
95440
96357
  const stateEvIds = [];
95441
96358
  const stateCollections = /* @__PURE__ */ new Set();
@@ -95526,6 +96443,7 @@ async function investigate(input, deps) {
95526
96443
  const METRICS_TIMEOUT_MS = 3e4;
95527
96444
  const metricEvIds = [];
95528
96445
  const latencyMetricEvIds = [];
96446
+ const bimodalMetricEvIds = [];
95529
96447
  const queueMetricEvIds = [];
95530
96448
  const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
95531
96449
  const nominalMetricEvIds = [];
@@ -95577,6 +96495,8 @@ async function investigate(input, deps) {
95577
96495
  );
95578
96496
  const genericPerf = serviceFilter.length === 0 && hintIsPerf && f5.anomaly === "latency-spike";
95579
96497
  if (matchesService || matchesHint || genericPerf) latencyMetricEvIds.push(ev.id);
96498
+ } else if (f5.anomaly === "bimodal-population") {
96499
+ bimodalMetricEvIds.push(ev.id);
95580
96500
  } else if (f5.anomaly === "queue-growth") {
95581
96501
  const panelNorm = f5.panelTitle.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
95582
96502
  const labelNorms = Object.values(f5.labels).map(
@@ -95647,7 +96567,13 @@ async function investigate(input, deps) {
95647
96567
  sinceProvided: input.since !== void 0,
95648
96568
  suppressRuntimeHypotheses: sourceImpactHint,
95649
96569
  recentChangeRelevant,
95650
- graph
96570
+ graph,
96571
+ // HOR-435: per-segment duration breakdown (#2) + bimodal-population metric (#3) support
96572
+ // the benign-variance hypothesis (#4); the suggested categories de-anchor alert text (#1).
96573
+ perDimensionDurationEvIds,
96574
+ bimodalMetricEvIds,
96575
+ benignVarianceApplicable: looksDurationThemed(hint) || perDimensionDurationEvIds.length > 0 || bimodalMetricEvIds.length > 0 || latencyMetricEvIds.length > 0,
96576
+ alertSuggestedCategories
95651
96577
  });
95652
96578
  const validated = validateHypotheses(hyps, evidence2);
95653
96579
  const causeChains = buildCauseChains(validated, evidence2, graph, label);
@@ -96253,13 +97179,26 @@ async function investigate(input, deps) {
96253
97179
  query: hint
96254
97180
  });
96255
97181
  report.sourceStatus = buildRuntimeSourceStatus(evidence2, connectorFlags);
96256
- const persistedId = await persist(db, input, report);
96257
- if (persistedId) report.id = persistedId;
97182
+ const persisted = await persist(db, input, report);
97183
+ const persistedId = persisted?.id ?? null;
97184
+ if (persistedId !== null) report.id = persistedId;
96258
97185
  report.persisted = persistedId !== null;
96259
97186
  if (persistedId !== null) {
96260
97187
  const tags = deriveTags(report);
96261
97188
  report.similarIncidents = await recallSimilar(db, tags, persistedId, input.repo ?? null);
96262
97189
  await storeIncidentMemory(db, persistedId, report);
97190
+ const store = deps.store;
97191
+ if (store !== void 0 && autoInvestigationMemoryEnabled()) {
97192
+ try {
97193
+ const audit = {
97194
+ actor: { kind: "system" },
97195
+ note: "auto: investigation memory (HOR-432)"
97196
+ };
97197
+ const reportTime = persisted?.createdAt ?? /* @__PURE__ */ new Date();
97198
+ await captureInvestigationMemory(store, persistedId, report, reportTime, audit);
97199
+ } catch {
97200
+ }
97201
+ }
96263
97202
  }
96264
97203
  if (persistedId !== null) {
96265
97204
  try {
@@ -96303,7 +97242,7 @@ async function persist(db, input, report) {
96303
97242
  incidentInput: input,
96304
97243
  status: "open",
96305
97244
  summary: report.summary
96306
- }).returning({ id: investigations.id });
97245
+ }).returning({ id: investigations.id, createdAt: investigations.createdAt });
96307
97246
  const row = inserted[0];
96308
97247
  if (!row) return null;
96309
97248
  const investigationId = row.id;
@@ -96347,7 +97286,7 @@ async function persist(db, input, report) {
96347
97286
  }))
96348
97287
  );
96349
97288
  }
96350
- return investigationId;
97289
+ return { id: investigationId, createdAt: row.createdAt ?? null };
96351
97290
  } catch {
96352
97291
  return null;
96353
97292
  }
@@ -97056,225 +97995,6 @@ function blastRadiusToJSON(r) {
97056
97995
  return JSON.stringify(r, null, 2);
97057
97996
  }
97058
97997
 
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
97998
  // ../../packages/engine/src/memory-recall.ts
97279
97999
  init_cjs_shims();
97280
98000
  var import_node_crypto4 = require("crypto");
@@ -101915,8 +102635,13 @@ async function runIndex(opts) {
101915
102635
  console.error(import_picocolors6.default.red(` ${err2.message}`));
101916
102636
  return 1;
101917
102637
  }
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"));
102638
+ const staleReason = isAnalyzed(root) ? indexNeedsReanalyze(root) : null;
102639
+ if (!isAnalyzed(root) || staleReason) {
102640
+ console.log(
102641
+ import_picocolors6.default.dim(
102642
+ 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"
102643
+ )
102644
+ );
101920
102645
  try {
101921
102646
  await analyzeRepo(root);
101922
102647
  } catch (err2) {
@@ -115375,6 +116100,9 @@ async function runOneInvestigation(input, ctx, opts = {}) {
115375
116100
  {
115376
116101
  code: ctx.code,
115377
116102
  db: ctx.dbHandle.db,
116103
+ // HOR-432 — the authored-memory store so every investigation auto-captures a recurrence-aware
116104
+ // memory. Best-effort + CONTEXT-ONLY in the engine (never feeds scoring, never blocks delivery).
116105
+ store: createLocalMemoryStore(ctx.dbHandle.db),
115378
116106
  logs: ctx.logs,
115379
116107
  mongo: ctx.mongo,
115380
116108
  postgres: ctx.postgres,
@@ -117432,6 +118160,39 @@ function createCloudMemoryStore(client, cfg) {
117432
118160
  tags: null
117433
118161
  };
117434
118162
  },
118163
+ // HOR-432 consolidation mirror: refresh the scalar fields the cloud accepts (claim/confidence).
118164
+ // PRIVACY: `payload` NEVER leaves the device (it can stash a vector) — it is intentionally dropped
118165
+ // here, exactly like `add`/`toSyncInput`. The return is the server echo (or a synthesized stub) and
118166
+ // is only consumed by the dual-write wrapper, which discards it; reads always come from local.
118167
+ async update(id, patch, _opts) {
118168
+ const input = { clientId: id };
118169
+ if (patch.claim !== void 0) input.claim = patch.claim;
118170
+ if (patch.confidence !== void 0) input.confidence = patch.confidence;
118171
+ const res = await client.syncMemoryItems(projectId, { items: [input] });
118172
+ const record2 = res.items?.find((r) => r.clientId === id);
118173
+ if (record2) return fromWire(record2, repoFallback);
118174
+ return {
118175
+ id,
118176
+ kind: "investigation",
118177
+ claim: patch.claim ?? "",
118178
+ scope: "repo",
118179
+ source: "investigation",
118180
+ evidence: [],
118181
+ confidence: patch.confidence ?? 0,
118182
+ status: "fresh",
118183
+ createdAt: /* @__PURE__ */ new Date(),
118184
+ lastVerifiedAt: null,
118185
+ lastVerifiedHash: null,
118186
+ orgId: null,
118187
+ workspaceId: null,
118188
+ repo: repoFallback,
118189
+ userId: null,
118190
+ visibility: "private",
118191
+ payload: null,
118192
+ signature: null,
118193
+ tags: null
118194
+ };
118195
+ },
117435
118196
  async get(id) {
117436
118197
  try {
117437
118198
  return await findInList(id);
@@ -117539,6 +118300,14 @@ function dualWriteMemoryStore(local, cloud, onCloudError = () => {
117539
118300
  });
117540
118301
  return row;
117541
118302
  },
118303
+ async update(id, patch, opts) {
118304
+ const row = await local.update(id, patch, opts);
118305
+ await mirror(async () => {
118306
+ await cloud.update(id, patch, opts);
118307
+ await mirrorLatestAudit(id);
118308
+ });
118309
+ return row;
118310
+ },
117542
118311
  async setStatus(id, status, audit) {
117543
118312
  await local.setStatus(id, status, audit);
117544
118313
  await mirror(async () => {
@@ -118932,6 +119701,8 @@ function anomalyLabel2(anomaly) {
118932
119701
  return "QUEUE-GROWTH";
118933
119702
  case "change":
118934
119703
  return "CHANGE";
119704
+ case "bimodal-population":
119705
+ return "BIMODAL-POPULATION";
118935
119706
  case "none":
118936
119707
  return "OK";
118937
119708
  }