@merittdev/horus 0.8.2 → 0.8.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 +150 -26
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -65880,7 +65880,7 @@ init_cjs_shims();
65880
65880
 
65881
65881
  // ../../packages/core/src/version.ts
65882
65882
  init_cjs_shims();
65883
- var HORUS_VERSION = true ? "0.8.2" : "dev";
65883
+ var HORUS_VERSION = true ? "0.8.4" : "dev";
65884
65884
  var PINNED_SOURCE_VERSION = "1.5.0";
65885
65885
 
65886
65886
  // ../../packages/core/src/evidence.ts
@@ -71867,7 +71867,14 @@ function buildErrorAnalysisBody(q2, field = "event_code", mapping = MERITT_FIELD
71867
71867
  sample: {
71868
71868
  top_hits: {
71869
71869
  size: 1,
71870
- _source: [mapping.messageField, "component", "log_logger", "context", "detail"],
71870
+ _source: [
71871
+ mapping.messageField,
71872
+ mapping.levelField,
71873
+ "component",
71874
+ "log_logger",
71875
+ "context",
71876
+ "detail"
71877
+ ],
71871
71878
  sort: [{ [mapping.timestampField]: { order: "desc" } }]
71872
71879
  }
71873
71880
  }
@@ -71881,7 +71888,21 @@ function bucketsOf(node) {
71881
71888
  const n4 = node;
71882
71889
  return n4?.["buckets"] ?? [];
71883
71890
  }
71884
- function parseErrorAnalysis(resp, window2, messageField = "message") {
71891
+ function readLevel(src, levelField) {
71892
+ const raw = getField(src, levelField) ?? src[levelField];
71893
+ if (typeof raw === "number" && Number.isFinite(raw)) return valueToLevel(raw);
71894
+ if (typeof raw === "string") {
71895
+ const v4 = raw.trim().toLowerCase();
71896
+ if (v4 === "fatal" || v4 === "error" || v4 === "warn" || v4 === "info" || v4 === "debug" || v4 === "trace") {
71897
+ return v4;
71898
+ }
71899
+ if (v4 === "warning") return "warn";
71900
+ const n4 = Number(v4);
71901
+ if (Number.isFinite(n4)) return valueToLevel(n4);
71902
+ }
71903
+ return void 0;
71904
+ }
71905
+ function parseErrorAnalysis(resp, window2, messageField = "message", levelField) {
71885
71906
  const res = resp;
71886
71907
  const aggs = res["aggregations"] ?? {};
71887
71908
  const hits = res["hits"];
@@ -71898,12 +71919,14 @@ function parseErrorAnalysis(resp, window2, messageField = "message") {
71898
71919
  const sampleMessage = typeof rawMsg === "string" ? rawMsg : typeof sampleSrc["message"] === "string" ? sampleSrc["message"] : void 0;
71899
71920
  const ctx = sampleSrc["context"];
71900
71921
  const sampleContext = ctx !== null && typeof ctx === "object" && !Array.isArray(ctx) ? ctx : void 0;
71922
+ const level = levelField !== void 0 ? readLevel(sampleSrc, levelField) : void 0;
71901
71923
  return {
71902
71924
  key: String(b5["key"] ?? "") || "(none)",
71903
71925
  count: typeof b5["doc_count"] === "number" ? b5["doc_count"] : 0,
71904
71926
  firstSeen: typeof first === "string" ? first : "",
71905
71927
  lastSeen: typeof last === "string" ? last : "",
71906
71928
  services,
71929
+ ...level !== void 0 ? { level } : {},
71907
71930
  sampleMessage,
71908
71931
  sampleComponent: typeof sampleSrc["component"] === "string" ? sampleSrc["component"] : typeof sampleSrc["log_logger"] === "string" ? sampleSrc["log_logger"] : void 0,
71909
71932
  ...sampleContext !== void 0 ? { sampleContext } : {}
@@ -72182,7 +72205,12 @@ var ElasticsearchLogsProvider = class {
72182
72205
  this.resolveIndex(q2),
72183
72206
  buildErrorAnalysisBody(current, sigField, this.mapping)
72184
72207
  );
72185
- const analysis = parseErrorAnalysis(curRes, { from, to }, this.mapping.messageField);
72208
+ const analysis = parseErrorAnalysis(
72209
+ curRes,
72210
+ { from, to },
72211
+ this.mapping.messageField,
72212
+ this.mapping.levelField
72213
+ );
72186
72214
  const fromMs = Date.parse(from);
72187
72215
  const toMs = Date.parse(to);
72188
72216
  if (!Number.isNaN(fromMs) && !Number.isNaN(toMs) && toMs > fromMs) {
@@ -92248,6 +92276,24 @@ function clamp015(n4) {
92248
92276
  if (n4 > 1) return 1;
92249
92277
  return n4;
92250
92278
  }
92279
+ var SEV_ERROR = 2;
92280
+ var SEV_WARN = 1;
92281
+ var SEV_INFO = 0;
92282
+ function seedEmittedSeverityTier(level, code, message) {
92283
+ const lv = level?.toLowerCase();
92284
+ if (lv === "error" || lv === "fatal" || lv === "critical") return SEV_ERROR;
92285
+ if (lv === "warn" || lv === "warning") return SEV_WARN;
92286
+ if (lv === "info" || lv === "debug" || lv === "trace") return SEV_INFO;
92287
+ const m5 = /^([A-Z])_/.exec(code);
92288
+ const prefix = m5?.[1];
92289
+ if (prefix === "W") return SEV_WARN;
92290
+ if (prefix === "D" || prefix === "I" || prefix === "T") return SEV_INFO;
92291
+ if (prefix === "E" || prefix === "F" || prefix === "C") return SEV_ERROR;
92292
+ const text2 = `${code} ${message ?? ""}`.toLowerCase();
92293
+ if (/\b(skip|skipping|skipped|deprecat|info|debug|trace|notice)\b/.test(text2)) return SEV_INFO;
92294
+ if (/\bwarn(ing)?\b/.test(text2)) return SEV_WARN;
92295
+ return SEV_ERROR;
92296
+ }
92251
92297
  function confidenceCeilingForCause(score) {
92252
92298
  const s5 = clamp015(score);
92253
92299
  if (s5 < 0.2) return 0.6;
@@ -92438,7 +92484,7 @@ function looksPerformance(hint) {
92438
92484
  hint
92439
92485
  );
92440
92486
  }
92441
- function buildBehavioralWalkthrough(hint, top, ctx, flows) {
92487
+ function buildBehavioralWalkthrough(hint, top, ctx, flows, classMethods = []) {
92442
92488
  const shortFile = (f5) => f5.replace(/^.*?((?:src|app|lib|packages)\/.*)$/, "$1");
92443
92489
  const isNoise = (s5) => /logger|logging|\.log\b|telemetry/i.test(s5.filePath) || /^(forcontext|getlogger|child|debug|info|warn|error|log|trace|tracer)$/i.test(s5.name);
92444
92490
  const flow = flows.slice().sort((a2, b5) => b5.steps.length - a2.steps.length)[0];
@@ -92456,7 +92502,7 @@ function buildBehavioralWalkthrough(hint, top, ctx, flows) {
92456
92502
  };
92457
92503
  const PERSIST_RE = /(^|[._])(save|insert|upsert|persist|bulkwrite|find|findone|findmany|aggregate|delete|remove)\b|repositor|\brepo\b|\.model|entity|prisma|mongoose|typeorm|knex|\bdao\b|datasource/i;
92458
92504
  const EXTERNAL_RE = /(^|[._])(request|publish|enqueue|dispatch|emit|notify)\b|http|axios|\bapi\b|webhook|\bqueue\b|processor|producer|gateway|\.client\b/i;
92459
- const pool = dedup([...steps, ...ctx?.callees ?? []]).filter((s5) => !isNoise(s5));
92505
+ const pool = dedup([...steps, ...ctx?.callees ?? [], ...classMethods]).filter((s5) => !isNoise(s5));
92460
92506
  const persistence = dedup(pool.filter((s5) => PERSIST_RE.test(s5.name) || PERSIST_RE.test(s5.filePath))).slice(0, 6).map((s5) => `${s5.name} (${shortFile(s5.filePath)})`);
92461
92507
  const persistNames = new Set(persistence.map((p5) => p5.split(" (")[0]));
92462
92508
  const externalCalls = dedup(
@@ -92467,9 +92513,15 @@ function buildBehavioralWalkthrough(hint, top, ctx, flows) {
92467
92513
  const loc = entry2.startLine ? `${shortFile(entry2.filePath)}:${entry2.startLine}` : shortFile(entry2.filePath);
92468
92514
  lines.push(`Entry point: \`${entry2.name}\` (${loc}).`);
92469
92515
  }
92516
+ const methodList = dedup(classMethods.filter((s5) => !isNoise(s5)));
92470
92517
  if (steps.length > 1) {
92471
92518
  const chain = steps.slice(0, 12).map((s5) => s5.name).join(" \u2192 ");
92472
92519
  lines.push(`Flow: ${chain}${steps.length > 12 ? " \u2192 \u2026" : ""}${flow ? ` (${flow.steps.length} steps)` : ""}.`);
92520
+ } else if (entry2 && methodList.length > 0) {
92521
+ const names = methodList.slice(0, 12).map((s5) => s5.name).join(", ");
92522
+ lines.push(
92523
+ `\`${entry2.name}\` is a class \u2014 its methods are the entry points: ${names}${methodList.length > 12 ? ", \u2026" : ""}. Trace one with \`horus explain <method>\`.`
92524
+ );
92473
92525
  } else if (entry2) {
92474
92526
  lines.push(`No multi-step execution flow was indexed for \`${entry2.name}\` \u2014 showing its direct calls.`);
92475
92527
  }
@@ -92478,6 +92530,28 @@ function buildBehavioralWalkthrough(hint, top, ctx, flows) {
92478
92530
  if (lines.length === 0) lines.push("No execution flow could be reconstructed for this query from the source graph.");
92479
92531
  return { question: hint, entry: entry2, steps, externalCalls, persistence, narrative: lines.join("\n") };
92480
92532
  }
92533
+ function isClassSymbol(sym) {
92534
+ return sym !== void 0 && sym.id.split(":", 1)[0]?.toLowerCase() === "class";
92535
+ }
92536
+ async function fetchClassMethods(code, cls) {
92537
+ const safe = (s5) => s5.replace(/["\\]/g, "");
92538
+ const name2 = safe(cls.name);
92539
+ if (!name2) return [];
92540
+ const file = safe(cls.filePath);
92541
+ try {
92542
+ const res = await code.cypher(
92543
+ `MATCH (m:Method) WHERE m.class_name = "${name2}"` + (file ? ` AND m.file_path = "${file}"` : "") + " RETURN m.id, m.name, m.file_path, m.start_line ORDER BY m.start_line LIMIT 40"
92544
+ );
92545
+ return res.rows.map((r) => ({
92546
+ id: String(r[0] ?? ""),
92547
+ name: String(r[1] ?? ""),
92548
+ filePath: String(r[2] ?? ""),
92549
+ ...typeof r[3] === "number" ? { startLine: r[3] } : {}
92550
+ })).filter((s5) => s5.id !== "" && s5.name !== "");
92551
+ } catch {
92552
+ return [];
92553
+ }
92554
+ }
92481
92555
  function formatRegressionCitation(recentChanges, changes, seedFile) {
92482
92556
  const matchesSeed = (f5) => seedFile === void 0 || f5 === seedFile || seedFile.endsWith(f5) || f5.endsWith(seedFile);
92483
92557
  const all = recentChanges?.commits ?? [];
@@ -92847,7 +92921,8 @@ async function investigate(input, deps) {
92847
92921
  count: sig.count,
92848
92922
  message: sig.sampleMessage ?? "",
92849
92923
  evId: existingEvId,
92850
- isNew: sig.isNew ?? false
92924
+ isNew: sig.isNew ?? false,
92925
+ level: sig.level ?? null
92851
92926
  });
92852
92927
  }
92853
92928
  }
@@ -92894,7 +92969,8 @@ async function investigate(input, deps) {
92894
92969
  count: sig.count,
92895
92970
  message: sig.sampleMessage ?? "",
92896
92971
  evId: ev.id,
92897
- isNew: sig.isNew ?? false
92972
+ isNew: sig.isNew ?? false,
92973
+ level: sig.level ?? null
92898
92974
  });
92899
92975
  }
92900
92976
  } catch {
@@ -93161,7 +93237,8 @@ async function investigate(input, deps) {
93161
93237
  const matchesHint = serviceFilter.length === 0 && hintIsPerf && hintTokens.some(
93162
93238
  (t2) => t2.length >= 4 && (panelLower.includes(t2) || labelVals.some((v4) => v4.includes(t2)))
93163
93239
  );
93164
- if (matchesService || matchesHint) latencyMetricEvIds.push(ev.id);
93240
+ const genericPerf = serviceFilter.length === 0 && hintIsPerf && f5.anomaly === "latency-spike";
93241
+ if (matchesService || matchesHint || genericPerf) latencyMetricEvIds.push(ev.id);
93165
93242
  } else if (f5.anomaly === "queue-growth") {
93166
93243
  const panelNorm = f5.panelTitle.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
93167
93244
  const labelNorms = Object.values(f5.labels).map(
@@ -93483,25 +93560,45 @@ async function investigate(input, deps) {
93483
93560
  });
93484
93561
  }
93485
93562
  if (seedEmittedJoins.length > 0 && top && seedEv) {
93486
- const ordered = [...seedEmittedJoins].sort((a2, b5) => b5.count - a2.count);
93563
+ const ordered = [...seedEmittedJoins].sort((a2, b5) => {
93564
+ const ta = seedEmittedSeverityTier(a2.level, a2.code, a2.message);
93565
+ const tb = seedEmittedSeverityTier(b5.level, b5.code, b5.message);
93566
+ if (ta !== tb) return tb - ta;
93567
+ return b5.count - a2.count;
93568
+ });
93487
93569
  const lead = ordered[0];
93488
93570
  if (lead !== void 0) {
93489
93571
  const sourceEvidenceIds = [
93490
93572
  .../* @__PURE__ */ new Set([...ordered.map((j3) => j3.evId), seedEv.id])
93491
93573
  ];
93574
+ const tier = seedEmittedSeverityTier(lead.level, lead.code, lead.message);
93492
93575
  const volumeBoost = Math.min(0.1, Math.log10(Math.max(1, lead.count)) * 0.025);
93493
- const baseScore = clamp015(0.6 + volumeBoost);
93494
93576
  const msgClause = lead.message ? ` \u2014 "${lead.message.replace(/\s+/g, " ").trim().slice(0, 90)}"` : "";
93577
+ let title;
93578
+ let baseScore;
93579
+ if (tier === SEV_ERROR) {
93580
+ title = `Runtime error ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${top.name} \u2014 the likely failure${msgClause}`;
93581
+ baseScore = clamp015(0.6 + volumeBoost);
93582
+ } else if (tier === SEV_WARN) {
93583
+ title = `Warning ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${top.name}${msgClause}`;
93584
+ baseScore = clamp015(0.42 + volumeBoost);
93585
+ } else {
93586
+ title = `Diagnostic signal ${lead.code} (${lead.count}x in Elasticsearch) is logged by ${top.name} \u2014 informational, not an error${msgClause}`;
93587
+ baseScore = clamp015(0.3 + volumeBoost);
93588
+ }
93495
93589
  causeInputs.push({
93496
93590
  id: "cause:seed-emitted-error",
93497
- title: `Runtime error ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${top.name} \u2014 the likely failure${msgClause}`.slice(
93498
- 0,
93499
- 220
93500
- ),
93591
+ title: title.slice(0, 220),
93501
93592
  category: "error-correlation",
93502
93593
  sourceEvidenceIds,
93503
93594
  baseScore,
93504
- metadata: { blastRadius, seedEmitted: true, code: lead.code, count: lead.count }
93595
+ metadata: {
93596
+ blastRadius,
93597
+ seedEmitted: true,
93598
+ code: lead.code,
93599
+ count: lead.count,
93600
+ severity: tier === SEV_ERROR ? "error" : tier === SEV_WARN ? "warn" : "info"
93601
+ }
93505
93602
  });
93506
93603
  }
93507
93604
  }
@@ -93609,7 +93706,11 @@ async function investigate(input, deps) {
93609
93706
  const seedLinkedEvIds = /* @__PURE__ */ new Set([
93610
93707
  ...directLogEvIds,
93611
93708
  ...seedEv ? [seedEv.id] : [],
93612
- ...impactEv ? [impactEv.id] : []
93709
+ ...impactEv ? [impactEv.id] : [],
93710
+ // GAP B: for a performance hint, a latency/error-rate anomaly is the structural answer,
93711
+ // not co-occurring noise — let the metric-latency cause headline rather than yielding to
93712
+ // a higher-volume but lower-severity log code.
93713
+ ...looksPerformance(hint) ? latencyMetricEvIds : []
93613
93714
  ]);
93614
93715
  const isLinkedToSeed = (ids2) => ids2.some((id) => seedLinkedEvIds.has(id));
93615
93716
  const topLinkedCause = rankedCauses.find(
@@ -93638,7 +93739,14 @@ async function investigate(input, deps) {
93638
93739
  }
93639
93740
  }
93640
93741
  }
93641
- behavioral = buildBehavioralWalkthrough(hint, top, ctx ?? void 0, flowPool);
93742
+ let classMethods = [];
93743
+ if (code && isClassSymbol(top)) {
93744
+ const richest = flowPool.slice().sort((a2, b5) => b5.steps.length - a2.steps.length)[0];
93745
+ if (richest === void 0 || richest.steps.length <= 1) {
93746
+ classMethods = await fetchClassMethods(code, top);
93747
+ }
93748
+ }
93749
+ behavioral = buildBehavioralWalkthrough(hint, top, ctx ?? void 0, flowPool, classMethods);
93642
93750
  const noisy = hasIncidentSignal ? ' (note: live error signals also exist in this codebase \u2014 rephrase as a symptom like "X failing" for a fault hunt)' : "";
93643
93751
  summary = `${seedDisclaimer}${banner2}"${hint}" reads as a "how does it work" question \u2014 here is the code path${noisy}:
93644
93752
 
@@ -113819,18 +113927,34 @@ function validateSpawnedRecord(r) {
113819
113927
  return null;
113820
113928
  }
113821
113929
  async function getProcessInfo(pid) {
113930
+ let args2;
113822
113931
  try {
113823
- const [argsResult, etimeResult] = await Promise.all([
113824
- execFileAsync("ps", ["-p", String(pid), "-o", "args="], { timeout: 3e3 }),
113825
- execFileAsync("ps", ["-p", String(pid), "-o", "etimes="], { timeout: 3e3 })
113826
- ]);
113827
- const args2 = argsResult.stdout.trim();
113828
- const etimeSeconds = parseInt(etimeResult.stdout.trim(), 10);
113829
- if (!args2 || isNaN(etimeSeconds)) return null;
113830
- return { args: args2, etimeSeconds };
113932
+ const argsResult = await execFileAsync("ps", ["-p", String(pid), "-o", "args="], {
113933
+ timeout: 3e3
113934
+ });
113935
+ args2 = argsResult.stdout.trim();
113831
113936
  } catch {
113832
113937
  return null;
113833
113938
  }
113939
+ if (!args2) return null;
113940
+ let etimeSeconds = Number.NaN;
113941
+ try {
113942
+ const etimeResult = await execFileAsync("ps", ["-p", String(pid), "-o", "etime="], {
113943
+ timeout: 3e3
113944
+ });
113945
+ etimeSeconds = parseEtimeSeconds(etimeResult.stdout.trim());
113946
+ } catch {
113947
+ }
113948
+ return { args: args2, etimeSeconds };
113949
+ }
113950
+ function parseEtimeSeconds(etime) {
113951
+ const m5 = etime.match(/^(?:(\d+)-)?(?:(\d+):)?(\d+):(\d+)$/);
113952
+ if (!m5) return Number.NaN;
113953
+ const days = m5[1] ? Number(m5[1]) : 0;
113954
+ const hours = m5[2] ? Number(m5[2]) : 0;
113955
+ const minutes = Number(m5[3]);
113956
+ const seconds = Number(m5[4]);
113957
+ return days * 86400 + hours * 3600 + minutes * 60 + seconds;
113834
113958
  }
113835
113959
  function extractPort(hostUrl) {
113836
113960
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.8.2",
3
+ "version": "0.8.4",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {