@merittdev/horus 0.1.11 → 0.1.12

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 +215 -7
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -50314,7 +50314,7 @@ init_cjs_shims();
50314
50314
 
50315
50315
  // ../../packages/core/src/version.ts
50316
50316
  init_cjs_shims();
50317
- var HORUS_VERSION = true ? "0.1.11" : "dev";
50317
+ var HORUS_VERSION = true ? "0.1.12" : "dev";
50318
50318
  var PINNED_AXON_VERSION = "1.0.7";
50319
50319
  var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
50320
50320
 
@@ -70237,8 +70237,10 @@ async function investigate(input, deps) {
70237
70237
  const latencyMetricEvIds = [];
70238
70238
  const queueMetricEvIds = [];
70239
70239
  const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
70240
+ const nominalMetricEvIds = [];
70240
70241
  let metricsCollected = false;
70241
70242
  let metricsFailureReason;
70243
+ let metricSeriesChecked = 0;
70242
70244
  if (deps.metrics) {
70243
70245
  const ac = new AbortController();
70244
70246
  let metricsTimerId;
@@ -70298,6 +70300,19 @@ async function investigate(input, deps) {
70298
70300
  }
70299
70301
  }
70300
70302
  }
70303
+ metricSeriesChecked = mFindings.length;
70304
+ if (metricEvIds.length === 0 && mFindings.length > 0) {
70305
+ const panels = [...new Set(mFindings.map((f) => f.panelTitle))];
70306
+ const ev = mkEv(
70307
+ "metric",
70308
+ `Metrics checked \u2014 ${mFindings.length} series across ${panels.length} panel(s), no anomalies in window`,
70309
+ { seriesChecked: mFindings.length, panelCount: panels.length, panels: panels.slice(0, 10), anomalies: 0, stance: "neutral" },
70310
+ {},
70311
+ collectedAt2,
70312
+ 0.2
70313
+ );
70314
+ nominalMetricEvIds.push(ev.id);
70315
+ }
70301
70316
  metricsCollected = true;
70302
70317
  } catch (metricsErr) {
70303
70318
  metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
@@ -70495,6 +70510,13 @@ async function investigate(input, deps) {
70495
70510
  confidence: 0.7,
70496
70511
  evidenceIds: metricEvIds
70497
70512
  });
70513
+ } else if (nominalMetricEvIds.length > 0) {
70514
+ findings2.push({
70515
+ kind: "observation",
70516
+ title: `Metrics nominal \u2014 ${metricSeriesChecked} series checked, no anomalies in window`,
70517
+ confidence: 0.5,
70518
+ evidenceIds: nominalMetricEvIds
70519
+ });
70498
70520
  }
70499
70521
  const causeInputs = [];
70500
70522
  const blastRadius = impact.affected;
@@ -72551,6 +72573,167 @@ function refinedToJSON(r, v) {
72551
72573
  );
72552
72574
  }
72553
72575
 
72576
+ // ../../packages/engine/src/qa.ts
72577
+ init_cjs_shims();
72578
+ var CONTRADICTS_RE = /\b(contradict|contradicts|argues?\s+against|evidence\s+against|rule\s+out|disprove|refute|weaken)\b/i;
72579
+ var MISSING_RE = /\b(missing|absent|gaps?|don'?t\s+have|do\s+not\s+have|lack(?:ing)?|what\s+else)\b/i;
72580
+ var CONFIDENCE_RE = /\b(confidence|confident|certainty|sure|why\s+(?:is\s+)?(?:it\s+)?not\s+higher)\b/i;
72581
+ function detectQuestion(text2) {
72582
+ const t = text2.toLowerCase();
72583
+ if (CONFIDENCE_RE.test(t)) return "confidence";
72584
+ if (CONTRADICTS_RE.test(t)) return "contradicts";
72585
+ if (MISSING_RE.test(t)) return "missing-evidence";
72586
+ return null;
72587
+ }
72588
+ function categoriesForTopic(text2) {
72589
+ const t = text2.toLowerCase();
72590
+ for (const [topic, entry2] of Object.entries(TOPIC_MAP)) {
72591
+ if (topic === t.trim()) return { topic, categories: entry2.categories };
72592
+ if (entry2.keywords.some((kw) => new RegExp(`\\b${kw}\\b`).test(t))) {
72593
+ return { topic, categories: entry2.categories };
72594
+ }
72595
+ }
72596
+ return null;
72597
+ }
72598
+ function evidenceById(report) {
72599
+ return new Map(report.evidence.map((e) => [e.id, e]));
72600
+ }
72601
+ function answerContradicts(report, question) {
72602
+ const matched = categoriesForTopic(question);
72603
+ const byId = evidenceById(report);
72604
+ const hyps = matched ? report.hypotheses.filter((h) => matched.categories.includes(h.category)) : [];
72605
+ if (matched && hyps.length === 0) {
72606
+ const evaluated = [...new Set(report.hypotheses.map((h) => h.category))];
72607
+ return {
72608
+ question,
72609
+ kind: "contradicts",
72610
+ headline: `"${matched.topic}" was not among the evaluated hypotheses \u2014 no evidence supports it.`,
72611
+ details: evaluated.length > 0 ? [`Hypotheses evaluated: ${evaluated.join(", ")}.`] : ["No hypotheses were formed for this investigation."],
72612
+ evidence: []
72613
+ };
72614
+ }
72615
+ if (hyps.length === 0) {
72616
+ const ids3 = [...new Set(report.hypotheses.flatMap((h) => h.contradictingEvidenceIds))];
72617
+ const ev2 = ids3.map((id) => byId.get(id)).filter((e) => e !== void 0);
72618
+ return {
72619
+ question,
72620
+ kind: "contradicts",
72621
+ headline: ev2.length > 0 ? `${ev2.length} item(s) contradict the leading hypotheses.` : "No contradicting evidence was recorded for any hypothesis.",
72622
+ details: ev2.length > 0 ? [] : ["Evidence either supports or is neutral to the hypotheses."],
72623
+ evidence: ev2
72624
+ };
72625
+ }
72626
+ const ids2 = [...new Set(hyps.flatMap((h) => h.contradictingEvidenceIds))];
72627
+ const ev = ids2.map((id) => byId.get(id)).filter((e) => e !== void 0);
72628
+ const verdicts = [...new Set(hyps.map((h) => h.verdict))].join(", ");
72629
+ if (ev.length === 0) {
72630
+ return {
72631
+ question,
72632
+ kind: "contradicts",
72633
+ headline: `No evidence contradicts "${matched?.topic ?? "this"}" (verdict: ${verdicts}).`,
72634
+ details: hyps.map((h) => `${h.category}: ${h.rationale ?? h.statement}`),
72635
+ evidence: []
72636
+ };
72637
+ }
72638
+ return {
72639
+ question,
72640
+ kind: "contradicts",
72641
+ headline: `${ev.length} item(s) contradict "${matched?.topic ?? "this"}" (verdict: ${verdicts}).`,
72642
+ details: hyps.map((h) => `${h.category}: ${h.rationale ?? h.statement}`),
72643
+ evidence: ev
72644
+ };
72645
+ }
72646
+ function answerMissing(report, question) {
72647
+ const { gaps, blindSpots } = report.gapAnalysis;
72648
+ const hypMissing = [...new Set(report.hypotheses.flatMap((h) => h.missingEvidence))];
72649
+ if (gaps.length === 0 && hypMissing.length === 0) {
72650
+ return {
72651
+ question,
72652
+ kind: "missing-evidence",
72653
+ headline: "No evidence gaps \u2014 all expected dimensions were collected.",
72654
+ details: [],
72655
+ evidence: []
72656
+ };
72657
+ }
72658
+ const details = gaps.map(
72659
+ (g) => `${g.dimension}: ${g.why} \u2192 ${g.nextSource} (\u2212${(g.confidenceImpact * 100).toFixed(0)}% ceiling)`
72660
+ );
72661
+ if (hypMissing.length > 0) {
72662
+ details.push(`To confirm hypotheses: ${hypMissing.join("; ")}.`);
72663
+ }
72664
+ for (const bs of blindSpots) details.push(`Blind spot: ${bs}`);
72665
+ return {
72666
+ question,
72667
+ kind: "missing-evidence",
72668
+ headline: `${gaps.length} evidence gap(s) limit this investigation.`,
72669
+ details,
72670
+ evidence: []
72671
+ };
72672
+ }
72673
+ function answerConfidence(report, question) {
72674
+ const { gaps, confidenceCeiling } = report.gapAnalysis;
72675
+ const ceilingPct = Math.round(confidenceCeiling * 100);
72676
+ const actualPct = Math.round(report.confidence * 100);
72677
+ const limiting = [...gaps].sort((a, b2) => b2.confidenceImpact - a.confidenceImpact);
72678
+ const details = [];
72679
+ if (limiting.length > 0) {
72680
+ details.push(`Confidence is capped at ${ceilingPct}% by missing evidence:`);
72681
+ for (const g of limiting) {
72682
+ details.push(` \u2022 ${g.dimension} (\u2212${(g.confidenceImpact * 100).toFixed(0)}%): ${g.why}`);
72683
+ }
72684
+ } else {
72685
+ details.push("No evidence gaps cap the ceiling.");
72686
+ }
72687
+ const weak = report.hypotheses.filter((h) => h.verdict === "weakened" || h.verdict === "unconfirmed");
72688
+ if (weak.length > 0) {
72689
+ details.push(
72690
+ `Unconfirmed/weakened hypotheses: ${weak.map((h) => `${h.category} (${h.verdict})`).join(", ")}.`
72691
+ );
72692
+ }
72693
+ return {
72694
+ question,
72695
+ kind: "confidence",
72696
+ headline: `Confidence is ${actualPct}% (ceiling ${ceilingPct}%). ${limiting.length > 0 ? "Missing evidence is the main limiter." : "Limited by hypothesis support, not gaps."}`,
72697
+ details,
72698
+ evidence: []
72699
+ };
72700
+ }
72701
+ function answerQuestion(report, question) {
72702
+ const kind = detectQuestion(question);
72703
+ if (kind === null) return null;
72704
+ if (kind === "contradicts") return answerContradicts(report, question);
72705
+ if (kind === "missing-evidence") return answerMissing(report, question);
72706
+ return answerConfidence(report, question);
72707
+ }
72708
+ function renderQAAnswer(a) {
72709
+ const lines = [];
72710
+ lines.push(`Q: ${a.question}`);
72711
+ lines.push("");
72712
+ lines.push(a.headline);
72713
+ for (const d of a.details) lines.push(d.startsWith(" ") ? d : ` ${d}`);
72714
+ if (a.evidence.length > 0) {
72715
+ lines.push("");
72716
+ lines.push("Evidence:");
72717
+ for (const e of a.evidence) {
72718
+ lines.push(` [${e.id.slice(0, 8)}] (${e.kind}) ${e.title}`);
72719
+ }
72720
+ }
72721
+ return lines.join("\n");
72722
+ }
72723
+ function qaToJSON(a) {
72724
+ return JSON.stringify(
72725
+ {
72726
+ question: a.question,
72727
+ kind: a.kind,
72728
+ headline: a.headline,
72729
+ details: a.details,
72730
+ evidence: a.evidence.map((e) => ({ id: e.id, kind: e.kind, title: e.title }))
72731
+ },
72732
+ null,
72733
+ 2
72734
+ );
72735
+ }
72736
+
72554
72737
  // ../../packages/engine/src/onboard.ts
72555
72738
  init_cjs_shims();
72556
72739
  function tokenize2(text2) {
@@ -73714,6 +73897,12 @@ async function runChanges(base2, compare, opts) {
73714
73897
  // ../../packages/cli/src/commands/timeline.ts
73715
73898
  init_cjs_shims();
73716
73899
  var import_picocolors7 = __toESM(require_picocolors(), 1);
73900
+ var DEFAULT_SINCE = "7 days ago";
73901
+ function resolveTimelineWindow(opts) {
73902
+ const usingDefault = !opts.all && opts.since === void 0;
73903
+ const since = opts.all ? void 0 : opts.since ?? DEFAULT_SINCE;
73904
+ return { since, usingDefault };
73905
+ }
73717
73906
  async function runTimeline(service, opts) {
73718
73907
  try {
73719
73908
  const config = await loadConfig(opts.config);
@@ -73725,11 +73914,24 @@ async function runTimeline(service, opts) {
73725
73914
  return 1;
73726
73915
  }
73727
73916
  const { code } = createConnectors(config);
73917
+ const { since, usingDefault: usingDefaultWindow } = resolveTimelineWindow(opts);
73728
73918
  const t = await reconstructChangeTimeline(
73729
- { repoPath: renv.path, since: opts.since, until: opts.until, service },
73919
+ { repoPath: renv.path, since, until: opts.until, service },
73730
73920
  { code }
73731
73921
  );
73732
- console.log(opts.json ? changeTimelineToJSON(t) : renderChangeTimeline(t));
73922
+ if (opts.json) {
73923
+ console.log(changeTimelineToJSON(t));
73924
+ } else {
73925
+ console.log(renderChangeTimeline(t));
73926
+ if (usingDefaultWindow) {
73927
+ console.log(
73928
+ import_picocolors7.default.dim(
73929
+ `
73930
+ Showing the last 7 days (default). Widen with ${import_picocolors7.default.bold('--since "30 days ago"')}, pin a range with ${import_picocolors7.default.bold("--since <when> --until <when>")}, or see everything with ${import_picocolors7.default.bold("--all")}.`
73931
+ )
73932
+ );
73933
+ }
73934
+ }
73733
73935
  return 0;
73734
73936
  } catch (err) {
73735
73937
  console.error(import_picocolors7.default.red(err.message));
@@ -73740,7 +73942,7 @@ async function runTimeline(service, opts) {
73740
73942
  // ../../packages/cli/src/commands/what-changed.ts
73741
73943
  init_cjs_shims();
73742
73944
  var import_picocolors8 = __toESM(require_picocolors(), 1);
73743
- var DEFAULT_SINCE = "7 days ago";
73945
+ var DEFAULT_SINCE2 = "7 days ago";
73744
73946
  async function runWhatChanged(service, opts) {
73745
73947
  try {
73746
73948
  const config = await loadConfig(opts.config);
@@ -73752,7 +73954,7 @@ async function runWhatChanged(service, opts) {
73752
73954
  return 1;
73753
73955
  }
73754
73956
  const { code } = createConnectors(config);
73755
- const since = opts.since ?? DEFAULT_SINCE;
73957
+ const since = opts.since ?? DEFAULT_SINCE2;
73756
73958
  const r = await whatChanged(
73757
73959
  { repoPath: renv.path, since, until: opts.until, service },
73758
73960
  { code }
@@ -74213,6 +74415,11 @@ async function runAsk(id, directive, opts) {
74213
74415
  return 1;
74214
74416
  }
74215
74417
  const report = migrateReport(row.report);
74418
+ const answer = answerQuestion(report, directive);
74419
+ if (answer) {
74420
+ console.log(opts.json ? qaToJSON(answer) : renderQAAnswer(answer));
74421
+ return 0;
74422
+ }
74216
74423
  const v = refineInvestigation(report, directive);
74217
74424
  console.log(opts.json ? refinedToJSON(report, v) : renderRefined(report, v));
74218
74425
  if (!opts.json && report.aiJudgment) {
@@ -76677,13 +76884,14 @@ Examples:
76677
76884
  });
76678
76885
  program2.command("timeline [service]").description(
76679
76886
  "Reconstruct what changed in a time window (git + change-impact) \u2014 evidence, not conclusions"
76680
- ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (e.g. "7 days ago", a date)').option("--until <when>", "git --until").option("--json", "output JSON").action(
76887
+ ).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago"; e.g. "30 days ago", a date)').option("--until <when>", "git --until").option("--all", "include all history instead of the default recent window").option("--json", "output JSON").action(
76681
76888
  async (service, opts) => {
76682
76889
  process.exitCode = await runTimeline(service, {
76683
76890
  config: opts.config,
76684
76891
  repo: opts.repo,
76685
76892
  since: opts.since,
76686
76893
  until: opts.until,
76894
+ all: opts.all,
76687
76895
  json: opts.json
76688
76896
  });
76689
76897
  }
@@ -76781,7 +76989,7 @@ Examples:
76781
76989
  process.exitCode = await runScore(id, { config: opts.config, json: opts.json });
76782
76990
  });
76783
76991
  program2.command("ask <id> <directive>").description(
76784
- 'Refine a saved investigation with a follow-up directive (e.g. "focus on queue behavior", "ignore deployment changes") \u2014 reuses evidence, no re-query'
76992
+ 'Ask about or refine a saved investigation \u2014 reuses evidence, no re-query.\n Questions (direct answers):\n "what evidence contradicts <topic>?" \xB7 "what evidence is missing?"\n "why is confidence not higher?"\n Topic filters (deterministic scoping):\n "focus on queue behavior" \xB7 "ignore deployment changes" \xB7 "retry"'
76785
76993
  ).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (id, directive, opts) => {
76786
76994
  process.exitCode = await runAsk(id, directive, { config: opts.config, json: opts.json });
76787
76995
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {