@merittdev/horus 0.13.2 → 0.14.1

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 +716 -336
  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.13.2" : "dev";
65883
+ var HORUS_VERSION = true ? "0.14.1" : "dev";
65884
65884
  var PINNED_SOURCE_VERSION = "2.0.2";
65885
65885
 
65886
65886
  // ../../packages/core/src/evidence.ts
@@ -70856,6 +70856,9 @@ init_cjs_shims();
70856
70856
 
70857
70857
  // ../../packages/connectors/src/source/client.ts
70858
70858
  init_cjs_shims();
70859
+ function encodeNodePath(nodeId) {
70860
+ return encodeURI(nodeId).replace(/#/g, "%23").replace(/\?/g, "%3F");
70861
+ }
70859
70862
  var SourceHttpError = class extends Error {
70860
70863
  status;
70861
70864
  body;
@@ -70982,7 +70985,7 @@ var SourceHttpClient = class {
70982
70985
  impact(nodeId, depth = 3) {
70983
70986
  return this.request(
70984
70987
  "GET",
70985
- `/api/impact/${encodeURI(nodeId)}?depth=${depth}`
70988
+ `/api/impact/${encodeNodePath(nodeId)}?depth=${depth}`
70986
70989
  );
70987
70990
  }
70988
70991
  diff(base2, compare) {
@@ -71032,7 +71035,7 @@ var SourceHttpClient = class {
71032
71035
  }
71033
71036
  /** Process flows a symbol participates in, with each flow's named ordered steps. */
71034
71037
  flows(nodeId) {
71035
- return this.request("GET", `/api/flows/${encodeURI(nodeId)}`);
71038
+ return this.request("GET", `/api/flows/${encodeNodePath(nodeId)}`);
71036
71039
  }
71037
71040
  /** Method symbols of `className` defined in `file`, ordered by start line. */
71038
71041
  async classMethods(file, className) {
@@ -71044,7 +71047,7 @@ var SourceHttpClient = class {
71044
71047
  }
71045
71048
  /** Extended node detail — node + content + callers/callees/typeRefs + imports + coupling + communities. */
71046
71049
  node(nodeId) {
71047
- return this.request("GET", `/api/node/${encodeURI(nodeId)}`);
71050
+ return this.request("GET", `/api/node/${encodeNodePath(nodeId)}`);
71048
71051
  }
71049
71052
  /** Community clusters with their member nodes (source-graph extraction). */
71050
71053
  async communities() {
@@ -74634,21 +74637,21 @@ function analyzeQueueSignals(q2) {
74634
74637
  if (q2.failedBreakdown && q2.failedBreakdown.length > 0 && q2.failed > 0) {
74635
74638
  const top = q2.failedBreakdown[0];
74636
74639
  const sampleTotal = q2.failedBreakdown.reduce((sum, b5) => sum + b5.count, 0);
74637
- const pct = Math.round(top.count / Math.max(sampleTotal, 1) * 100);
74638
- if (pct >= BREAKDOWN_PCT_THRESHOLD) {
74640
+ const pct2 = Math.round(top.count / Math.max(sampleTotal, 1) * 100);
74641
+ if (pct2 >= BREAKDOWN_PCT_THRESHOLD) {
74639
74642
  const stale = isStaleFailure(top.lastFailedAgeMs);
74640
74643
  const ageStr = top.lastFailedAgeMs !== void 0 ? `, last failed ${fmtMs(top.lastFailedAgeMs)} ago${stale ? " [STALE]" : ""}` : "";
74641
74644
  signals.push({
74642
74645
  queueName: q2.queueName,
74643
74646
  kind: "failed-breakdown",
74644
- title: `${q2.queueName}: ${pct}% of recently sampled failures (${top.count}/${sampleTotal} sampled) are "${top.reason}"${ageStr}`,
74647
+ title: `${q2.queueName}: ${pct2}% of recently sampled failures (${top.count}/${sampleTotal} sampled) are "${top.reason}"${ageStr}`,
74645
74648
  // Demote a stale-only breakdown so a days-old error isn't read as live (HOR-217).
74646
74649
  relevance: stale ? 0.5 : 0.82,
74647
74650
  payload: {
74648
74651
  queueName: q2.queueName,
74649
74652
  topReason: top.reason,
74650
74653
  topCount: top.count,
74651
- topPct: pct,
74654
+ topPct: pct2,
74652
74655
  totalFailed: q2.failed,
74653
74656
  topLastFailedAgeMs: top.lastFailedAgeMs ?? null,
74654
74657
  topStale: stale,
@@ -76748,7 +76751,7 @@ function sql(strings, ...params) {
76748
76751
  return new SQL([new StringChunk(str3)]);
76749
76752
  }
76750
76753
  sql2.raw = raw;
76751
- function join18(chunks, separator) {
76754
+ function join20(chunks, separator) {
76752
76755
  const result = [];
76753
76756
  for (const [i3, chunk] of chunks.entries()) {
76754
76757
  if (i3 > 0 && separator !== void 0) {
@@ -76758,7 +76761,7 @@ function sql(strings, ...params) {
76758
76761
  }
76759
76762
  return new SQL(result);
76760
76763
  }
76761
- sql2.join = join18;
76764
+ sql2.join = join20;
76762
76765
  function identifier(value) {
76763
76766
  return new Name(value);
76764
76767
  }
@@ -80349,7 +80352,7 @@ var PgSelectQueryBuilderBase = class extends TypedQueryBuilder {
80349
80352
  return (table, on2) => {
80350
80353
  const baseTableName = this.tableName;
80351
80354
  const tableName = getTableLikeName(table);
80352
- if (typeof tableName === "string" && this.config.joins?.some((join18) => join18.alias === tableName)) {
80355
+ if (typeof tableName === "string" && this.config.joins?.some((join20) => join20.alias === tableName)) {
80353
80356
  throw new Error(`Alias "${tableName}" is already used in this query`);
80354
80357
  }
80355
80358
  if (!this.isPartialSelect) {
@@ -81361,7 +81364,7 @@ var PgUpdateBase = class extends QueryPromise {
81361
81364
  createJoin(joinType) {
81362
81365
  return (table, on2) => {
81363
81366
  const tableName = getTableLikeName(table);
81364
- if (typeof tableName === "string" && this.config.joins.some((join18) => join18.alias === tableName)) {
81367
+ if (typeof tableName === "string" && this.config.joins.some((join20) => join20.alias === tableName)) {
81365
81368
  throw new Error(`Alias "${tableName}" is already used in this query`);
81366
81369
  }
81367
81370
  if (typeof on2 === "function") {
@@ -81457,10 +81460,10 @@ var PgUpdateBase = class extends QueryPromise {
81457
81460
  const fromFields = this.getTableLikeFields(this.config.from);
81458
81461
  fields[tableName] = fromFields;
81459
81462
  }
81460
- for (const join18 of this.config.joins) {
81461
- const tableName2 = getTableLikeName(join18.table);
81462
- if (typeof tableName2 === "string" && !is(join18.table, SQL)) {
81463
- const fromFields = this.getTableLikeFields(join18.table);
81463
+ for (const join20 of this.config.joins) {
81464
+ const tableName2 = getTableLikeName(join20.table);
81465
+ if (typeof tableName2 === "string" && !is(join20.table, SQL)) {
81466
+ const fromFields = this.getTableLikeFields(join20.table);
81464
81467
  fields[tableName2] = fromFields;
81465
81468
  }
81466
81469
  }
@@ -93552,16 +93555,27 @@ function rankCauses(inputs, ctx, limit = 3) {
93552
93555
  });
93553
93556
  return scored.slice(0, limit);
93554
93557
  }
93555
- function selectHeadlineCause(rankedCauses, isLinkedToSeed) {
93558
+ function selectHeadlineCause(rankedCauses, isLinkedToSeed, rerank) {
93556
93559
  let topCause;
93557
- let topLinkedCause;
93558
93560
  for (const c4 of rankedCauses) {
93559
93561
  if (topCause === void 0 || c4.finalScore > topCause.finalScore) topCause = c4;
93560
- if (c4.finalScore >= 0.2 && isLinkedToSeed(c4.sourceEvidenceIds) && (topLinkedCause === void 0 || c4.finalScore > topLinkedCause.finalScore)) {
93561
- topLinkedCause = c4;
93562
- }
93563
93562
  }
93564
- const headlineCause = topLinkedCause ?? (topCause && topCause.finalScore >= 0.2 ? topCause : void 0);
93563
+ const pickEligible = (set) => {
93564
+ if (set.length === 0) return void 0;
93565
+ if (rerank) {
93566
+ const top = rerank(set).find((c4) => set.includes(c4));
93567
+ if (top) return top;
93568
+ }
93569
+ let best = set[0];
93570
+ for (const c4 of set) if (c4.finalScore > best.finalScore) best = c4;
93571
+ return best;
93572
+ };
93573
+ const linkedEligible = rankedCauses.filter(
93574
+ (c4) => c4.finalScore >= 0.2 && isLinkedToSeed(c4.sourceEvidenceIds)
93575
+ );
93576
+ const allEligible = rankedCauses.filter((c4) => c4.finalScore >= 0.2);
93577
+ const topLinkedCause = pickEligible(linkedEligible);
93578
+ const headlineCause = topLinkedCause ?? pickEligible(allEligible);
93565
93579
  const headlineLinked = headlineCause !== void 0 && isLinkedToSeed(headlineCause.sourceEvidenceIds);
93566
93580
  return { topCause, topLinkedCause, headlineCause, headlineLinked };
93567
93581
  }
@@ -96300,7 +96314,7 @@ function formatRegressionCitation(recentChanges, changes, seedFile) {
96300
96314
  return { commitClause, symbolClause };
96301
96315
  }
96302
96316
  async function investigate(input, deps) {
96303
- const { code, db } = deps;
96317
+ const { code, db, rerank } = deps;
96304
96318
  const hint = input.hint.trim();
96305
96319
  const intent = classifyIntent(hint, input.since);
96306
96320
  const namedSymbols = parseNamedSymbols(hint);
@@ -96490,8 +96504,8 @@ async function investigate(input, deps) {
96490
96504
  label = top.name === "constructor" ? `${(top.id.split(":").pop() ?? "").replace(/\.constructor$/, "") || top.name} (constructor)` : top.name;
96491
96505
  [ctx, impact, flows] = await Promise.all([
96492
96506
  code.context(top.id),
96493
- code.impact(top.id, 2),
96494
- code.flowsFor(top.id)
96507
+ code.impact(top.id, 2).catch(() => null),
96508
+ code.flowsFor(top.id).catch(() => [])
96495
96509
  ]);
96496
96510
  let edges = [];
96497
96511
  try {
@@ -96549,12 +96563,14 @@ async function investigate(input, deps) {
96549
96563
  );
96550
96564
  flowEvIds.push(ev.id);
96551
96565
  }
96552
- impactEv = mkEv(
96553
- "impact",
96554
- `Impact of ${top.name}: ${impact.affected} affected symbol(s)`,
96555
- { affected: impact.affected },
96556
- { symbolId: top.id, file: top.filePath }
96557
- );
96566
+ if (impact) {
96567
+ impactEv = mkEv(
96568
+ "impact",
96569
+ `Impact of ${top.name}: ${impact.affected} affected symbol(s)`,
96570
+ { affected: impact.affected },
96571
+ { symbolId: top.id, file: top.filePath }
96572
+ );
96573
+ }
96558
96574
  for (const edge of queueHits) {
96559
96575
  const producer = edge.producerSymbol ?? "unknown-producer";
96560
96576
  const worker = edge.workerSymbol ?? "unknown-worker";
@@ -97806,7 +97822,7 @@ async function investigate(input, deps) {
97806
97822
  ...looksPerformance(hint) ? latencyMetricEvIds : []
97807
97823
  ]);
97808
97824
  const isLinkedToSeed = (ids2) => ids2.some((id) => seedLinkedEvIds.has(id));
97809
- const { headlineCause, headlineLinked } = selectHeadlineCause(rankedCauses, isLinkedToSeed);
97825
+ const { headlineCause, headlineLinked } = selectHeadlineCause(rankedCauses, isLinkedToSeed, rerank);
97810
97826
  if (headlineCause && !headlineLinked && !sourceImpactHint) confidence = Math.min(confidence, 0.6);
97811
97827
  const banner2 = degradedNoSource ? "Runtime-only (source intelligence unavailable). " : "";
97812
97828
  const scope = top ? seedIsLowConfidence ? "could not be confidently localized from source alone" : `resolved to ${label} (${area})` : `over runtime evidence (${area})`;
@@ -99380,8 +99396,8 @@ function renderMemoryView(v4) {
99380
99396
  }
99381
99397
  const own = v4.ownedAreas.ownership;
99382
99398
  if (own != null && own.file != null) {
99383
- const pct = (own.confidence * 100).toFixed(0);
99384
- lines.push(`Likely maintainer: ${own.likelyMaintainer ?? "(unknown)"} (${pct}% confidence)`);
99399
+ const pct2 = (own.confidence * 100).toFixed(0);
99400
+ lines.push(`Likely maintainer: ${own.likelyMaintainer ?? "(unknown)"} (${pct2}% confidence)`);
99385
99401
  if (own.mostActiveRecent != null) {
99386
99402
  lines.push(`Most active recently: ${own.mostActiveRecent}`);
99387
99403
  }
@@ -99561,9 +99577,9 @@ function renderOwnership(o5) {
99561
99577
  }
99562
99578
  lines.push("File : " + o5.file);
99563
99579
  lines.push("");
99564
- const pct = (o5.confidence * 100).toFixed(0);
99580
+ const pct2 = (o5.confidence * 100).toFixed(0);
99565
99581
  lines.push(
99566
- "Likely maintainer : " + (o5.likelyMaintainer ?? "(unknown)") + " (" + pct + "% confidence)"
99582
+ "Likely maintainer : " + (o5.likelyMaintainer ?? "(unknown)") + " (" + pct2 + "% confidence)"
99567
99583
  );
99568
99584
  lines.push("Most active recently: " + (o5.mostActiveRecent ?? "(unknown)"));
99569
99585
  lines.push("");
@@ -100446,10 +100462,10 @@ function renderOnboarding(g4) {
100446
100462
  lines.push("## Who owns this area");
100447
100463
  lines.push("");
100448
100464
  if (g4.ownership != null && g4.ownership.file != null) {
100449
- const pct = (g4.ownership.confidence * 100).toFixed(0);
100465
+ const pct2 = (g4.ownership.confidence * 100).toFixed(0);
100450
100466
  lines.push(`File: ${g4.ownership.file}`);
100451
100467
  lines.push(
100452
- `Likely maintainer: ${g4.ownership.likelyMaintainer ?? "(unknown)"} (${pct}% confidence)`
100468
+ `Likely maintainer: ${g4.ownership.likelyMaintainer ?? "(unknown)"} (${pct2}% confidence)`
100453
100469
  );
100454
100470
  } else {
100455
100471
  lines.push(
@@ -102784,27 +102800,27 @@ function extractQueueGraph(input) {
102784
102800
  }
102785
102801
  }
102786
102802
  const producers = [];
102787
- for (const pc58 of input.producerClasses) {
102788
- const loopVarEnums = loopVarEnumsFor(pc58.content);
102789
- for (const m5 of pc58.content.matchAll(INJECT_QUEUE_RE)) {
102803
+ for (const pc59 of input.producerClasses) {
102804
+ const loopVarEnums = loopVarEnumsFor(pc59.content);
102805
+ for (const m5 of pc59.content.matchAll(INJECT_QUEUE_RE)) {
102790
102806
  const queue = m5[1] ?? "";
102791
- if (queue) producers.push({ queue, symbol: pc58.name, file: pc58.filePath });
102807
+ if (queue) producers.push({ queue, symbol: pc59.name, file: pc59.filePath });
102792
102808
  }
102793
- for (const m5 of pc58.content.matchAll(NEW_BULL_RE)) {
102809
+ for (const m5 of pc59.content.matchAll(NEW_BULL_RE)) {
102794
102810
  if (m5[2] !== "Queue") continue;
102795
102811
  const queues2 = resolveArg(m5[3] ?? "", constMap, enums, loopVarEnums);
102796
102812
  for (const queue of queues2) {
102797
102813
  producers.push({
102798
102814
  queue,
102799
- symbol: (m5[1] ?? pc58.name) || baseName(pc58.filePath),
102800
- file: pc58.filePath
102815
+ symbol: (m5[1] ?? pc59.name) || baseName(pc59.filePath),
102816
+ file: pc59.filePath
102801
102817
  });
102802
102818
  }
102803
102819
  }
102804
- for (const m5 of pc58.content.matchAll(QUEUE_NAME_CONST_RE)) {
102820
+ for (const m5 of pc59.content.matchAll(QUEUE_NAME_CONST_RE)) {
102805
102821
  const ident = m5[1] ?? "";
102806
102822
  const queue = m5[2] ?? "";
102807
- if (queue) producers.push({ queue, symbol: ident || baseName(pc58.filePath), file: pc58.filePath });
102823
+ if (queue) producers.push({ queue, symbol: ident || baseName(pc59.filePath), file: pc59.filePath });
102808
102824
  }
102809
102825
  }
102810
102826
  const workers = [];
@@ -116553,6 +116569,479 @@ function renderFreshness(f5) {
116553
116569
  // ../../packages/cli/src/lib/investigation-runner.ts
116554
116570
  init_cjs_shims();
116555
116571
  var import_picocolors16 = __toESM(require_picocolors(), 1);
116572
+ var import_node_fs19 = require("fs");
116573
+ var import_node_path21 = require("path");
116574
+ var import_node_os8 = require("os");
116575
+
116576
+ // ../../packages/eval/src/index.ts
116577
+ init_cjs_shims();
116578
+
116579
+ // ../../packages/eval/src/types.ts
116580
+ init_cjs_shims();
116581
+ var CORPUS_VERSION = "v1";
116582
+
116583
+ // ../../packages/eval/src/corpus.ts
116584
+ init_cjs_shims();
116585
+ function toUsableReport(blob) {
116586
+ if (blob === null || typeof blob !== "object") return null;
116587
+ const r = blob;
116588
+ if (!Array.isArray(r.suspectedCauses)) return null;
116589
+ const seeds = Array.isArray(r.seeds) ? r.seeds.map((s5) => s5 && typeof s5 === "object" && "name" in s5 ? String(s5.name) : "").filter(Boolean) : [];
116590
+ return {
116591
+ summary: typeof r.summary === "string" ? r.summary : "",
116592
+ confidence: typeof r.confidence === "number" && Number.isFinite(r.confidence) ? r.confidence : 0,
116593
+ seeds,
116594
+ causes: r.suspectedCauses
116595
+ };
116596
+ }
116597
+ function pickHeadlineCause(causes) {
116598
+ let best = null;
116599
+ for (const c4 of causes) {
116600
+ if (best === null || c4.finalScore > best.finalScore || c4.finalScore === best.finalScore && c4.id < best.id) {
116601
+ best = c4;
116602
+ }
116603
+ }
116604
+ return best;
116605
+ }
116606
+ function flattenFactors(cause) {
116607
+ const sums = /* @__PURE__ */ new Map();
116608
+ for (const e of cause.explanations ?? []) {
116609
+ sums.set(e.factor, (sums.get(e.factor) ?? 0) + e.delta);
116610
+ }
116611
+ return [...sums.entries()].map(([factor, delta]) => ({ factor, delta })).sort((a2, b5) => a2.factor < b5.factor ? -1 : a2.factor > b5.factor ? 1 : 0);
116612
+ }
116613
+ function projectHeadline(cause) {
116614
+ return {
116615
+ id: cause.id,
116616
+ category: cause.category,
116617
+ finalScore: cause.finalScore,
116618
+ confidence: cause.confidence,
116619
+ band: cause.band,
116620
+ sourceEvidenceIds: [...cause.sourceEvidenceIds ?? []].sort(),
116621
+ factors: flattenFactors(cause)
116622
+ };
116623
+ }
116624
+ function buildCorpus(labels, resolveReport) {
116625
+ const current = dedupeToCurrentVerdict(labels);
116626
+ const evaluated = current.length;
116627
+ const rows = [];
116628
+ const quarantined = [];
116629
+ const unjoinable = [];
116630
+ for (const label of current) {
116631
+ if (label.investigationId === null) {
116632
+ unjoinable.push({ investigationId: null, reason: "null-investigation" });
116633
+ continue;
116634
+ }
116635
+ if (!isOutcomeResolved(label.resolved) || !isOutcomeSource(label.source)) {
116636
+ quarantined.push({
116637
+ investigationId: label.investigationId,
116638
+ resolved: String(label.resolved),
116639
+ source: String(label.source),
116640
+ reason: !isOutcomeResolved(label.resolved) ? `invalid resolved "${String(label.resolved)}"` : `invalid source "${String(label.source)}"`
116641
+ });
116642
+ continue;
116643
+ }
116644
+ const blob = resolveReport(label.investigationId);
116645
+ if (blob === null || blob === void 0) {
116646
+ unjoinable.push({ investigationId: label.investigationId, reason: "missing-report" });
116647
+ continue;
116648
+ }
116649
+ const report = toUsableReport(blob);
116650
+ if (report === null) {
116651
+ unjoinable.push({ investigationId: label.investigationId, reason: "legacy-report" });
116652
+ continue;
116653
+ }
116654
+ const isConfirm = label.source === "confirm";
116655
+ const headline = pickHeadlineCause(report.causes);
116656
+ rows.push({
116657
+ investigationId: label.investigationId,
116658
+ project: label.project,
116659
+ at: label.at instanceof Date ? label.at.toISOString() : String(label.at),
116660
+ target: label.resolved,
116661
+ source: label.source,
116662
+ weak: isConfirm,
116663
+ confirmedCause: isConfirm ? null : label.confirmedCause ?? null,
116664
+ summary: report.summary,
116665
+ reportConfidence: report.confidence,
116666
+ seeds: report.seeds,
116667
+ causeCount: report.causes.length,
116668
+ headlineCause: headline ? projectHeadline(headline) : null
116669
+ });
116670
+ }
116671
+ const classBalance = { yes: 0, partly: 0, no: 0 };
116672
+ const bySource = { feedback: 0, confirm: 0 };
116673
+ for (const r of rows) {
116674
+ classBalance[r.target] += 1;
116675
+ bySource[r.source] += 1;
116676
+ }
116677
+ return { rows, evaluated, quarantined, unjoinable, classBalance, bySource };
116678
+ }
116679
+ function corpusFilename(version3 = CORPUS_VERSION) {
116680
+ return `corpus-${version3}.jsonl`;
116681
+ }
116682
+ function sortRows(rows) {
116683
+ return [...rows].sort((a2, b5) => {
116684
+ if (a2.investigationId !== b5.investigationId) return a2.investigationId < b5.investigationId ? -1 : 1;
116685
+ if (a2.at !== b5.at) return a2.at < b5.at ? -1 : 1;
116686
+ return 0;
116687
+ });
116688
+ }
116689
+ function fnv1a(s5) {
116690
+ let h3 = 2166136261;
116691
+ for (let i3 = 0; i3 < s5.length; i3++) {
116692
+ h3 ^= s5.charCodeAt(i3);
116693
+ h3 = Math.imul(h3, 16777619);
116694
+ }
116695
+ return h3 >>> 0;
116696
+ }
116697
+ function holdoutSplit(rows, holdoutPct = 20) {
116698
+ const train = [];
116699
+ const holdout = [];
116700
+ for (const r of rows) {
116701
+ if (fnv1a(r.investigationId) % 100 < holdoutPct) holdout.push(r.investigationId);
116702
+ else train.push(r.investigationId);
116703
+ }
116704
+ train.sort();
116705
+ holdout.sort();
116706
+ return { holdoutPct, train, holdout };
116707
+ }
116708
+ function serializeCorpus(build, version3 = CORPUS_VERSION) {
116709
+ const sorted = sortRows(build.rows);
116710
+ const jsonl = sorted.map((r) => JSON.stringify(r)).join("\n") + (sorted.length > 0 ? "\n" : "");
116711
+ const manifest = {
116712
+ version: version3,
116713
+ generatedAt: null,
116714
+ rows: sorted.length,
116715
+ evaluated: build.evaluated,
116716
+ quarantined: build.quarantined.length,
116717
+ unjoinable: build.unjoinable.length,
116718
+ classBalance: build.classBalance,
116719
+ bySource: build.bySource,
116720
+ holdout: holdoutSplit(sorted)
116721
+ };
116722
+ return { filename: corpusFilename(version3), jsonl, manifest };
116723
+ }
116724
+
116725
+ // ../../packages/eval/src/baseline.ts
116726
+ init_cjs_shims();
116727
+ function computeBaseline(labels) {
116728
+ const summary = summarizeOutcomeLabels(labels);
116729
+ const byProjectMap = /* @__PURE__ */ new Map();
116730
+ for (const l2 of labels) {
116731
+ const key = l2.project ?? null;
116732
+ const arr = byProjectMap.get(key);
116733
+ if (arr) arr.push(l2);
116734
+ else byProjectMap.set(key, [l2]);
116735
+ }
116736
+ const byProject = [...byProjectMap.entries()].map(([project, slice]) => {
116737
+ const s5 = summarizeOutcomeLabels(slice);
116738
+ return {
116739
+ project,
116740
+ n: s5.evaluated,
116741
+ strictHitRate: s5.accuracy,
116742
+ weightedHitRate: s5.weightedScore
116743
+ };
116744
+ }).sort((a2, b5) => {
116745
+ if (a2.project === b5.project) return 0;
116746
+ if (a2.project === null) return 1;
116747
+ if (b5.project === null) return -1;
116748
+ return a2.project < b5.project ? -1 : 1;
116749
+ });
116750
+ return {
116751
+ n: summary.evaluated,
116752
+ strictHitRate: summary.accuracy,
116753
+ weightedHitRate: summary.weightedScore,
116754
+ classBalance: summary.counts,
116755
+ bySource: summary.bySource,
116756
+ byProject
116757
+ };
116758
+ }
116759
+ function mean(xs) {
116760
+ if (xs.length === 0) return 0;
116761
+ return xs.reduce((a2, b5) => a2 + b5, 0) / xs.length;
116762
+ }
116763
+ function variance(xs, m5) {
116764
+ if (xs.length === 0) return 0;
116765
+ return xs.reduce((a2, b5) => a2 + (b5 - m5) * (b5 - m5), 0) / xs.length;
116766
+ }
116767
+ function featureSeparation(rows) {
116768
+ const usable = rows.filter(
116769
+ (r) => !r.weak && r.headlineCause !== null && (r.target === "yes" || r.target === "no")
116770
+ );
116771
+ const factorSet = /* @__PURE__ */ new Set();
116772
+ for (const r of usable) {
116773
+ for (const f5 of r.headlineCause.factors) factorSet.add(f5.factor);
116774
+ }
116775
+ const factors = [...factorSet].sort();
116776
+ const out2 = factors.map((factor) => {
116777
+ const yes = [];
116778
+ const no = [];
116779
+ for (const r of usable) {
116780
+ const f5 = r.headlineCause.factors.find((x4) => x4.factor === factor);
116781
+ const delta = f5 ? f5.delta : 0;
116782
+ if (r.target === "yes") yes.push(delta);
116783
+ else no.push(delta);
116784
+ }
116785
+ const meanYes = mean(yes);
116786
+ const meanNo = mean(no);
116787
+ const varYes = variance(yes, meanYes);
116788
+ const varNo = variance(no, meanNo);
116789
+ const separation = meanYes - meanNo;
116790
+ const all = yes.concat(no);
116791
+ const overallMean = mean(all);
116792
+ const pooledVar = variance(all, overallMean);
116793
+ const pooledStd = Math.sqrt(pooledVar);
116794
+ const effectSize = pooledStd === 0 ? 0 : separation / pooledStd;
116795
+ const cv = overallMean === 0 ? 0 : pooledStd / Math.abs(overallMean);
116796
+ return {
116797
+ factor,
116798
+ nYes: yes.length,
116799
+ nNo: no.length,
116800
+ meanYes,
116801
+ meanNo,
116802
+ varYes,
116803
+ varNo,
116804
+ separation,
116805
+ effectSize,
116806
+ cv
116807
+ };
116808
+ });
116809
+ out2.sort((a2, b5) => {
116810
+ const d3 = Math.abs(b5.effectSize) - Math.abs(a2.effectSize);
116811
+ if (d3 !== 0) return d3;
116812
+ return a2.factor < b5.factor ? -1 : a2.factor > b5.factor ? 1 : 0;
116813
+ });
116814
+ return { evaluated: usable.length, factors: out2 };
116815
+ }
116816
+
116817
+ // ../../packages/eval/src/reranker.ts
116818
+ init_cjs_shims();
116819
+ var RERANKER_VERSION = "v1";
116820
+ var MIN_TRAIN_INVESTIGATIONS = 25;
116821
+ var SCALAR_KEYS = ["finalScore", "confidence", "baseScore", "evidenceCount", "affectedCount"];
116822
+ function tokenize4(s5) {
116823
+ return new Set(
116824
+ s5.toLowerCase().split(/[^a-z0-9]+/).filter((t2) => t2.length >= 3)
116825
+ );
116826
+ }
116827
+ function matchesConfirmedCause(confirmedCause, cause) {
116828
+ const a2 = tokenize4(confirmedCause);
116829
+ const b5 = tokenize4(`${cause.title} ${cause.category}`);
116830
+ if (a2.size === 0 || b5.size === 0) return 0;
116831
+ let overlap = 0;
116832
+ for (const t2 of a2) if (b5.has(t2)) overlap += 1;
116833
+ return overlap / a2.size;
116834
+ }
116835
+ function collectFeatureKeys(investigations2) {
116836
+ const factors = /* @__PURE__ */ new Set();
116837
+ for (const inv of investigations2) {
116838
+ for (const c4 of inv.candidates) {
116839
+ for (const e of c4.explanations ?? []) factors.add(e.factor);
116840
+ }
116841
+ }
116842
+ return [...SCALAR_KEYS, ...[...factors].sort().map((f5) => `f:${f5}`)];
116843
+ }
116844
+ function extractRawFeatures(cause, featureKeys) {
116845
+ const factorDelta = /* @__PURE__ */ new Map();
116846
+ for (const e of cause.explanations ?? []) {
116847
+ factorDelta.set(e.factor, (factorDelta.get(e.factor) ?? 0) + e.delta);
116848
+ }
116849
+ return featureKeys.map((k3) => {
116850
+ switch (k3) {
116851
+ case "finalScore":
116852
+ return cause.finalScore;
116853
+ case "confidence":
116854
+ return cause.confidence;
116855
+ case "baseScore":
116856
+ return cause.baseScore;
116857
+ case "evidenceCount":
116858
+ return cause.sourceEvidenceIds?.length ?? 0;
116859
+ case "affectedCount":
116860
+ return cause.affectedNodeIds?.length ?? 0;
116861
+ default:
116862
+ return factorDelta.get(k3.slice(2)) ?? 0;
116863
+ }
116864
+ });
116865
+ }
116866
+ function baselineTop(candidates) {
116867
+ let best = null;
116868
+ for (const c4 of candidates) {
116869
+ if (best === null || c4.finalScore > best.finalScore || c4.finalScore === best.finalScore && c4.id < best.id) {
116870
+ best = c4;
116871
+ }
116872
+ }
116873
+ return best;
116874
+ }
116875
+ function deriveLabels(inv) {
116876
+ const cands = inv.candidates;
116877
+ if (cands.length === 0) return { relevantId: null, examples: [] };
116878
+ if (inv.confirmedCause && inv.confirmedCause.trim() !== "") {
116879
+ let bestId = null;
116880
+ let bestScore = 0;
116881
+ for (const c4 of cands) {
116882
+ const m5 = matchesConfirmedCause(inv.confirmedCause, c4);
116883
+ if (m5 > bestScore) {
116884
+ bestScore = m5;
116885
+ bestId = c4.id;
116886
+ }
116887
+ }
116888
+ if (bestId !== null && bestScore >= 0.34) {
116889
+ return {
116890
+ relevantId: bestId,
116891
+ examples: cands.map((c4) => ({ cause: c4, label: c4.id === bestId ? 1 : 0 }))
116892
+ };
116893
+ }
116894
+ }
116895
+ const top = baselineTop(cands);
116896
+ if (top === null) return { relevantId: null, examples: [] };
116897
+ if (inv.target === "yes") {
116898
+ return { relevantId: top.id, examples: cands.map((c4) => ({ cause: c4, label: c4.id === top.id ? 1 : 0 })) };
116899
+ }
116900
+ if (inv.target === "no") {
116901
+ return { relevantId: null, examples: [{ cause: top, label: 0 }] };
116902
+ }
116903
+ return { relevantId: null, examples: [] };
116904
+ }
116905
+ function sigmoid(z4) {
116906
+ if (z4 >= 0) return 1 / (1 + Math.exp(-z4));
116907
+ const e = Math.exp(z4);
116908
+ return e / (1 + e);
116909
+ }
116910
+ function fitLogistic(X2, y5, opts = {}) {
116911
+ const epochs = opts.epochs ?? 400;
116912
+ const lr = opts.lr ?? 0.1;
116913
+ const l2 = opts.l2 ?? 1e-3;
116914
+ const dim = X2[0]?.length ?? 0;
116915
+ const weights = new Array(dim).fill(0);
116916
+ let bias = 0;
116917
+ const n4 = X2.length;
116918
+ if (n4 === 0) return { weights, bias };
116919
+ for (let epoch = 0; epoch < epochs; epoch++) {
116920
+ const gradW = new Array(dim).fill(0);
116921
+ let gradB = 0;
116922
+ for (let i3 = 0; i3 < n4; i3++) {
116923
+ const xi = X2[i3];
116924
+ let z4 = bias;
116925
+ for (let j3 = 0; j3 < dim; j3++) z4 += weights[j3] * xi[j3];
116926
+ const err2 = sigmoid(z4) - y5[i3];
116927
+ for (let j3 = 0; j3 < dim; j3++) gradW[j3] = gradW[j3] + err2 * xi[j3];
116928
+ gradB += err2;
116929
+ }
116930
+ for (let j3 = 0; j3 < dim; j3++) weights[j3] = weights[j3] - lr * (gradW[j3] / n4 + l2 * weights[j3]);
116931
+ bias -= lr * (gradB / n4);
116932
+ }
116933
+ return { weights, bias };
116934
+ }
116935
+ function scoreVector(weights, bias, x4) {
116936
+ let z4 = bias;
116937
+ for (let j3 = 0; j3 < weights.length; j3++) z4 += weights[j3] * x4[j3];
116938
+ return z4;
116939
+ }
116940
+ function standardizeParams(X2) {
116941
+ const dim = X2[0]?.length ?? 0;
116942
+ const mean2 = new Array(dim).fill(0);
116943
+ const std = new Array(dim).fill(0);
116944
+ if (X2.length === 0) return { mean: mean2, std };
116945
+ for (const row of X2) for (let j3 = 0; j3 < dim; j3++) mean2[j3] = mean2[j3] + row[j3];
116946
+ for (let j3 = 0; j3 < dim; j3++) mean2[j3] = mean2[j3] / X2.length;
116947
+ for (const row of X2) for (let j3 = 0; j3 < dim; j3++) std[j3] = std[j3] + (row[j3] - mean2[j3]) ** 2;
116948
+ for (let j3 = 0; j3 < dim; j3++) std[j3] = Math.sqrt(std[j3] / X2.length) || 1;
116949
+ return { mean: mean2, std };
116950
+ }
116951
+ function applyStandardize(x4, mean2, std) {
116952
+ return x4.map((v4, j3) => (v4 - mean2[j3]) / std[j3]);
116953
+ }
116954
+ function top1HitRate(set, score) {
116955
+ if (set.length === 0) return 0;
116956
+ let hits = 0;
116957
+ for (const { inv, relevantId } of set) {
116958
+ let bestId = null;
116959
+ let bestScore = -Infinity;
116960
+ for (const c4 of inv.candidates) {
116961
+ const s5 = score(c4);
116962
+ if (s5 > bestScore || s5 === bestScore && bestId !== null && c4.id < bestId) {
116963
+ bestScore = s5;
116964
+ bestId = c4.id;
116965
+ }
116966
+ }
116967
+ if (bestId === relevantId) hits += 1;
116968
+ }
116969
+ return hits / set.length;
116970
+ }
116971
+ function trainReranker(investigations2, holdoutIds, opts = {}) {
116972
+ const labeled = investigations2.map((inv) => ({ inv, derived: deriveLabels(inv) })).filter((x4) => x4.derived.examples.length > 0);
116973
+ const scorable = labeled.filter((x4) => x4.derived.relevantId !== null && x4.inv.candidates.length >= 2);
116974
+ if (scorable.length < MIN_TRAIN_INVESTIGATIONS) {
116975
+ return {
116976
+ ok: false,
116977
+ reason: "insufficient-corpus",
116978
+ detail: `need \u2265${MIN_TRAIN_INVESTIGATIONS} labeled investigations with a known-correct candidate; have ${scorable.length}`,
116979
+ labeledInvestigations: scorable.length
116980
+ };
116981
+ }
116982
+ const featureKeys = collectFeatureKeys(investigations2);
116983
+ const trainSet = scorable.filter((x4) => !holdoutIds.has(x4.inv.investigationId));
116984
+ const holdoutSet = scorable.filter((x4) => holdoutIds.has(x4.inv.investigationId));
116985
+ if (holdoutSet.length === 0 || trainSet.length === 0) {
116986
+ return {
116987
+ ok: false,
116988
+ reason: "degenerate-holdout",
116989
+ detail: `train/holdout split left ${trainSet.length} train / ${holdoutSet.length} holdout`,
116990
+ labeledInvestigations: scorable.length
116991
+ };
116992
+ }
116993
+ const rawX = [];
116994
+ const y5 = [];
116995
+ for (const { inv } of trainSet) {
116996
+ for (const ex of deriveLabels(inv).examples) {
116997
+ rawX.push(extractRawFeatures(ex.cause, featureKeys));
116998
+ y5.push(ex.label);
116999
+ }
117000
+ }
117001
+ if (rawX.length === 0) {
117002
+ return { ok: false, reason: "no-derivable-labels", detail: "no training examples after split", labeledInvestigations: scorable.length };
117003
+ }
117004
+ const { mean: mean2, std } = standardizeParams(rawX);
117005
+ const X2 = rawX.map((row) => applyStandardize(row, mean2, std));
117006
+ const { weights, bias } = fitLogistic(X2, y5, opts);
117007
+ const modelScore = (c4) => scoreVector(weights, bias, applyStandardize(extractRawFeatures(c4, featureKeys), mean2, std));
117008
+ const holdoutScored = holdoutSet.map((x4) => ({ inv: x4.inv, relevantId: x4.derived.relevantId }));
117009
+ const baselineHitRate = top1HitRate(holdoutScored, (c4) => c4.finalScore);
117010
+ const rerankedHitRate = top1HitRate(holdoutScored, modelScore);
117011
+ return {
117012
+ ok: true,
117013
+ version: RERANKER_VERSION,
117014
+ featureKeys,
117015
+ mean: mean2,
117016
+ std,
117017
+ weights,
117018
+ bias,
117019
+ holdout: {
117020
+ n: holdoutScored.length,
117021
+ baselineHitRate,
117022
+ rerankedHitRate,
117023
+ delta: rerankedHitRate - baselineHitRate
117024
+ },
117025
+ trainExamples: rawX.length,
117026
+ trainInvestigations: trainSet.length
117027
+ };
117028
+ }
117029
+ function applyReranker(model, candidates) {
117030
+ const scored = candidates.map((c4, i3) => ({
117031
+ c: c4,
117032
+ i: i3,
117033
+ s: scoreVector(model.weights, model.bias, applyStandardize(extractRawFeatures(c4, model.featureKeys), model.mean, model.std))
117034
+ }));
117035
+ scored.sort((a2, b5) => b5.s - a2.s || a2.i - b5.i);
117036
+ return scored.map((x4) => x4.c);
117037
+ }
117038
+ function isRerankerModel(blob) {
117039
+ if (blob === null || typeof blob !== "object") return false;
117040
+ const m5 = blob;
117041
+ return Array.isArray(m5.featureKeys) && Array.isArray(m5.weights) && Array.isArray(m5.mean) && Array.isArray(m5.std) && typeof m5.bias === "number" && m5.weights.length === m5.featureKeys.length;
117042
+ }
117043
+
117044
+ // ../../packages/cli/src/lib/investigation-runner.ts
116556
117045
  async function buildInvestigationContext(renv, opts) {
116557
117046
  const log = opts.log ?? ((l2) => console.error(l2));
116558
117047
  let code = codeForEnv(renv);
@@ -116611,8 +117100,21 @@ function databaseUrlOrThrow(url) {
116611
117100
  if (!url) throw new Error("No database URL configured for the investigation store.");
116612
117101
  return url;
116613
117102
  }
117103
+ function loadReranker() {
117104
+ const flag = process.env.HORUS_RERANK;
117105
+ if (!flag || flag === "0" || flag.toLowerCase() === "false") return void 0;
117106
+ try {
117107
+ const blob = JSON.parse((0, import_node_fs19.readFileSync)((0, import_node_path21.join)((0, import_node_os8.homedir)(), ".horus", "reranker.json"), "utf8"));
117108
+ if (!isRerankerModel(blob)) return void 0;
117109
+ if ((blob.holdout?.delta ?? 0) <= 0) return void 0;
117110
+ return (causes) => applyReranker(blob, causes);
117111
+ } catch {
117112
+ return void 0;
117113
+ }
117114
+ }
116614
117115
  async function runOneInvestigation(input, ctx, opts = {}) {
116615
117116
  const { renv } = ctx;
117117
+ const rerank = loadReranker();
116616
117118
  const investigation = investigate(
116617
117119
  {
116618
117120
  hint: input.hint,
@@ -116646,7 +117148,10 @@ async function runOneInvestigation(input, ctx, opts = {}) {
116646
117148
  axiom: !!renv.connectors.axiom,
116647
117149
  redis: !!renv.connectors.redis?.url,
116648
117150
  queue: !!ctx.queue
116649
- }
117151
+ },
117152
+ // HOR-404: inject the learned reranker only when enabled + proven (loadReranker gates both);
117153
+ // CONTEXT-ONLY — it reorders eligible causes, never touches confidence/verdict/ceilings.
117154
+ ...rerank ? { rerank } : {}
116650
117155
  }
116651
117156
  );
116652
117157
  const timeoutMs = opts.timeoutMs ?? (Number(process.env.HORUS_INVESTIGATE_TIMEOUT_S) || 120) * 1e3;
@@ -117980,8 +118485,8 @@ async function runOwner(query, opts) {
117980
118485
 
117981
118486
  // ../../packages/cli/src/commands/postmortem.ts
117982
118487
  init_cjs_shims();
117983
- var import_node_fs19 = require("fs");
117984
- var import_node_path21 = require("path");
118488
+ var import_node_fs20 = require("fs");
118489
+ var import_node_path22 = require("path");
117985
118490
  var import_picocolors30 = __toESM(require_picocolors(), 1);
117986
118491
  async function runPostmortem(id, opts) {
117987
118492
  const log = opts.write ?? ((line2) => console.log(line2));
@@ -118092,14 +118597,14 @@ _AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
118092
118597
  }
118093
118598
  }
118094
118599
  if (opts.output) {
118095
- const outputPath = (0, import_node_path21.resolve)(opts.output);
118096
- if ((0, import_node_fs19.existsSync)(outputPath) && !opts.force) {
118600
+ const outputPath = (0, import_node_path22.resolve)(opts.output);
118601
+ if ((0, import_node_fs20.existsSync)(outputPath) && !opts.force) {
118097
118602
  log(import_picocolors30.default.red("\u2717") + " " + outputPath + " already exists");
118098
118603
  log(import_picocolors30.default.dim(" pass --force to overwrite"));
118099
118604
  return 1;
118100
118605
  }
118101
118606
  try {
118102
- (0, import_node_fs19.writeFileSync)(outputPath, content, "utf8");
118607
+ (0, import_node_fs20.writeFileSync)(outputPath, content, "utf8");
118103
118608
  } catch (err2) {
118104
118609
  log(import_picocolors30.default.red("\u2717 Could not write " + outputPath + ": ") + err2.message);
118105
118610
  return 1;
@@ -119683,7 +120188,7 @@ async function runMemoryAccuracy(opts) {
119683
120188
  )
119684
120189
  );
119685
120190
  } else {
119686
- const pct = (n4) => `${(n4 * 100).toFixed(0)}%`;
120191
+ const pct2 = (n4) => `${(n4 * 100).toFixed(0)}%`;
119687
120192
  console.log(import_picocolors34.default.bold(`Investigation accuracy \u2014 ${project}`) + import_picocolors34.default.dim(` \xB7 window: ${window2}`));
119688
120193
  if (summary.evaluated === 0) {
119689
120194
  console.log(
@@ -119693,7 +120198,7 @@ async function runMemoryAccuracy(opts) {
119693
120198
  );
119694
120199
  } else {
119695
120200
  console.log(
119696
- ` Pointed at the cause: ${import_picocolors34.default.green(pct(summary.accuracy))} ` + import_picocolors34.default.dim(`(weighted ${pct(summary.weightedScore)}) over ${summary.evaluated} investigation(s)`)
120201
+ ` Pointed at the cause: ${import_picocolors34.default.green(pct2(summary.accuracy))} ` + import_picocolors34.default.dim(`(weighted ${pct2(summary.weightedScore)}) over ${summary.evaluated} investigation(s)`)
119697
120202
  );
119698
120203
  console.log(
119699
120204
  ` Verdicts: ${import_picocolors34.default.green(`${summary.counts.yes} yes`)}, ${import_picocolors34.default.yellow(`${summary.counts.partly} partly`)}, ${import_picocolors34.default.red(`${summary.counts.no} no`)}`
@@ -120497,12 +121002,12 @@ async function runState(opts) {
120497
121002
  // ../../packages/cli/src/commands/init.ts
120498
121003
  init_cjs_shims();
120499
121004
  var import_picocolors39 = __toESM(require_picocolors(), 1);
120500
- var import_node_path22 = require("path");
121005
+ var import_node_path23 = require("path");
120501
121006
  async function runInit(opts) {
120502
121007
  try {
120503
121008
  const cwd = process.cwd();
120504
- const root = opts.path ? (0, import_node_path22.resolve)(opts.path) : findRepoRoot(cwd) ?? cwd;
120505
- const name2 = opts.name ?? (0, import_node_path22.basename)(root);
121009
+ const root = opts.path ? (0, import_node_path23.resolve)(opts.path) : findRepoRoot(cwd) ?? cwd;
121010
+ const name2 = opts.name ?? (0, import_node_path23.basename)(root);
120506
121011
  const envName = opts.env ?? "production";
120507
121012
  const repo = { name: name2, path: root };
120508
121013
  if (opts.source) repo["source"] = { hostUrl: opts.source };
@@ -121888,12 +122393,12 @@ function redactUrl(raw) {
121888
122393
  // ../../packages/cli/src/commands/stop.ts
121889
122394
  init_cjs_shims();
121890
122395
  var import_node_child_process10 = require("child_process");
121891
- var import_node_fs20 = require("fs");
122396
+ var import_node_fs21 = require("fs");
121892
122397
  var import_node_util6 = require("util");
121893
- var import_node_path23 = require("path");
122398
+ var import_node_path24 = require("path");
121894
122399
  var import_picocolors45 = __toESM(require_picocolors(), 1);
121895
122400
  var execFileAsync3 = (0, import_node_util6.promisify)(import_node_child_process10.execFile);
121896
- var unlinkAsync = (0, import_node_util6.promisify)(import_node_fs20.unlink);
122401
+ var unlinkAsync = (0, import_node_util6.promisify)(import_node_fs21.unlink);
121897
122402
  var SPAWNED_HOST_FILE2 = "spawned-host.json";
121898
122403
  var START_TIME_TOLERANCE_S = 60;
121899
122404
  var STOP_WAIT_MS = 5e3;
@@ -122035,11 +122540,11 @@ function isOwnHostProcess(pid, port, root, args2) {
122035
122540
  if (hostArgvRegex(port).test(args2)) return true;
122036
122541
  if (!hostArgvAnyPortRegex().test(args2)) return false;
122037
122542
  const rec = readSourceHostPid(root);
122038
- return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0, import_node_path23.resolve)(rec.repoPath) === (0, import_node_path23.resolve)(root);
122543
+ return rec !== null && rec.pid === pid && Number.isFinite(rec.port) && rec.port === port && !!rec.repoPath && (0, import_node_path24.resolve)(rec.repoPath) === (0, import_node_path24.resolve)(root);
122039
122544
  }
122040
122545
  async function cleanupSpawnedRecord(root) {
122041
122546
  try {
122042
- await unlinkAsync((0, import_node_path23.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
122547
+ await unlinkAsync((0, import_node_path24.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
122043
122548
  } catch {
122044
122549
  }
122045
122550
  }
@@ -122710,8 +123215,8 @@ async function runProvidersDoctorCommand(opts) {
122710
123215
 
122711
123216
  // ../../packages/cli/src/commands/generate-config.ts
122712
123217
  init_cjs_shims();
122713
- var import_node_fs21 = require("fs");
122714
- var import_node_path24 = require("path");
123218
+ var import_node_fs22 = require("fs");
123219
+ var import_node_path25 = require("path");
122715
123220
  var import_picocolors49 = __toESM(require_picocolors(), 1);
122716
123221
  function configTemplate(name2, repoPath) {
122717
123222
  return `export default {
@@ -122760,17 +123265,17 @@ async function runGenerateConfig(opts) {
122760
123265
  const defaults2 = localConfigPath2 != null ? projectDefaults(localConfigPath2) : null;
122761
123266
  const hasLocalConfig = defaults2 != null;
122762
123267
  const defaultOut = hasLocalConfig ? "horus.config.example.js" : "horus.config.js";
122763
- const outPath = (0, import_node_path24.resolve)(cwd, opts.out ?? defaultOut);
123268
+ const outPath = (0, import_node_path25.resolve)(cwd, opts.out ?? defaultOut);
122764
123269
  const name2 = opts.name ?? defaults2?.name ?? "my-project";
122765
123270
  const repoPath = opts.repo ?? defaults2?.repoPath ?? `/path/to/${name2}`;
122766
- if ((0, import_node_fs21.existsSync)(outPath) && !opts.force) {
123271
+ if ((0, import_node_fs22.existsSync)(outPath) && !opts.force) {
122767
123272
  log(`${import_picocolors49.default.red("\u2717")} ${outPath} already exists`);
122768
123273
  log(import_picocolors49.default.dim(" pass --force to overwrite"));
122769
123274
  return 1;
122770
123275
  }
122771
123276
  try {
122772
- (0, import_node_fs21.mkdirSync)((0, import_node_path24.dirname)(outPath), { recursive: true });
122773
- (0, import_node_fs21.writeFileSync)(outPath, configTemplate(name2, repoPath), "utf8");
123277
+ (0, import_node_fs22.mkdirSync)((0, import_node_path25.dirname)(outPath), { recursive: true });
123278
+ (0, import_node_fs22.writeFileSync)(outPath, configTemplate(name2, repoPath), "utf8");
122774
123279
  } catch (err2) {
122775
123280
  log(`${import_picocolors49.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
122776
123281
  return 1;
@@ -123080,9 +123585,9 @@ async function runReadiness(opts) {
123080
123585
 
123081
123586
  // ../../packages/cli/src/commands/skill.ts
123082
123587
  init_cjs_shims();
123083
- var import_node_fs22 = require("fs");
123084
- var import_node_path25 = require("path");
123085
- var import_node_os8 = require("os");
123588
+ var import_node_fs23 = require("fs");
123589
+ var import_node_path26 = require("path");
123590
+ var import_node_os9 = require("os");
123086
123591
  var import_promises3 = require("readline/promises");
123087
123592
  var import_picocolors51 = __toESM(require_picocolors(), 1);
123088
123593
  var SUPPORTED_TARGETS = ["claude", "codex", "gemini", "cursor", "generic"];
@@ -123090,23 +123595,23 @@ function isValidTarget(target) {
123090
123595
  return SUPPORTED_TARGETS.includes(target);
123091
123596
  }
123092
123597
  function getLegacyClaudeSkillPath(opts) {
123093
- const base2 = opts.global ? (0, import_node_os8.homedir)() : opts.cwd ?? process.cwd();
123094
- return (0, import_node_path25.join)(base2, ".claude", "skills", "horus.md");
123598
+ const base2 = opts.global ? (0, import_node_os9.homedir)() : opts.cwd ?? process.cwd();
123599
+ return (0, import_node_path26.join)(base2, ".claude", "skills", "horus.md");
123095
123600
  }
123096
123601
  function getSkillInstallPath(target, opts) {
123097
- const base2 = opts.global ? (0, import_node_os8.homedir)() : opts.cwd ?? process.cwd();
123602
+ const base2 = opts.global ? (0, import_node_os9.homedir)() : opts.cwd ?? process.cwd();
123098
123603
  switch (target) {
123099
123604
  case "claude":
123100
- return (0, import_node_path25.join)(base2, ".claude", "skills", "horus", "SKILL.md");
123605
+ return (0, import_node_path26.join)(base2, ".claude", "skills", "horus", "SKILL.md");
123101
123606
  case "gemini":
123102
- return opts.global ? (0, import_node_path25.join)(base2, ".gemini", "GEMINI.md") : (0, import_node_path25.join)(base2, "GEMINI.md");
123607
+ return opts.global ? (0, import_node_path26.join)(base2, ".gemini", "GEMINI.md") : (0, import_node_path26.join)(base2, "GEMINI.md");
123103
123608
  case "cursor":
123104
- return opts.global ? (0, import_node_path25.join)(base2, ".cursorrules") : (0, import_node_path25.join)(base2, ".cursor", "rules", "horus.mdc");
123609
+ return opts.global ? (0, import_node_path26.join)(base2, ".cursorrules") : (0, import_node_path26.join)(base2, ".cursor", "rules", "horus.mdc");
123105
123610
  case "codex":
123106
- return opts.global ? (0, import_node_path25.join)(base2, ".codex", "AGENTS.md") : (0, import_node_path25.join)(base2, "AGENTS.md");
123611
+ return opts.global ? (0, import_node_path26.join)(base2, ".codex", "AGENTS.md") : (0, import_node_path26.join)(base2, "AGENTS.md");
123107
123612
  case "generic":
123108
123613
  default:
123109
- return (0, import_node_path25.join)(base2, ".horus", "skills", `horus-${target}.md`);
123614
+ return (0, import_node_path26.join)(base2, ".horus", "skills", `horus-${target}.md`);
123110
123615
  }
123111
123616
  }
123112
123617
  var BASE_SKILL = `# Horus Grounded Investigation Skill
@@ -123431,7 +123936,7 @@ async function runSkillInstall(target, opts) {
123431
123936
  const content = generateSkillContent(target);
123432
123937
  if (target === "claude") {
123433
123938
  const legacyPath = getLegacyClaudeSkillPath({ global: opts.global, cwd: opts.cwd });
123434
- if ((0, import_node_fs22.existsSync)(legacyPath)) {
123939
+ if ((0, import_node_fs23.existsSync)(legacyPath)) {
123435
123940
  if (!opts.force) {
123436
123941
  const confirmed = await (opts.confirm ?? confirm)(
123437
123942
  `Found old flat skill file at ${legacyPath}. Replace it with the new directory layout?`
@@ -123443,7 +123948,7 @@ async function runSkillInstall(target, opts) {
123443
123948
  }
123444
123949
  }
123445
123950
  try {
123446
- (0, import_node_fs22.rmSync)(legacyPath);
123951
+ (0, import_node_fs23.rmSync)(legacyPath);
123447
123952
  log(`${import_picocolors51.default.yellow("!")} Replaced old flat skill file: ${legacyPath}`);
123448
123953
  } catch (err2) {
123449
123954
  log(`${import_picocolors51.default.red("\u2717")} Could not remove legacy skill file ${legacyPath}: ${err2.message}`);
@@ -123451,14 +123956,14 @@ async function runSkillInstall(target, opts) {
123451
123956
  }
123452
123957
  }
123453
123958
  }
123454
- if ((0, import_node_fs22.existsSync)(outPath) && !opts.force) {
123959
+ if ((0, import_node_fs23.existsSync)(outPath) && !opts.force) {
123455
123960
  log(`${import_picocolors51.default.red("\u2717")} ${outPath} already exists`);
123456
123961
  log(import_picocolors51.default.dim(" pass --force to overwrite"));
123457
123962
  return 1;
123458
123963
  }
123459
123964
  try {
123460
- (0, import_node_fs22.mkdirSync)((0, import_node_path25.dirname)(outPath), { recursive: true });
123461
- (0, import_node_fs22.writeFileSync)(outPath, content, "utf8");
123965
+ (0, import_node_fs23.mkdirSync)((0, import_node_path26.dirname)(outPath), { recursive: true });
123966
+ (0, import_node_fs23.writeFileSync)(outPath, content, "utf8");
123462
123967
  } catch (err2) {
123463
123968
  log(`${import_picocolors51.default.red("\u2717")} Could not write ${outPath}: ${err2.message}`);
123464
123969
  return 1;
@@ -124199,251 +124704,8 @@ function installCommandTelemetry(program2) {
124199
124704
  // ../../packages/cli/src/commands/eval.ts
124200
124705
  init_cjs_shims();
124201
124706
  var import_promises5 = require("fs/promises");
124202
- var import_node_path26 = require("path");
124707
+ var import_node_path27 = require("path");
124203
124708
  var import_picocolors56 = __toESM(require_picocolors(), 1);
124204
-
124205
- // ../../packages/eval/src/index.ts
124206
- init_cjs_shims();
124207
-
124208
- // ../../packages/eval/src/types.ts
124209
- init_cjs_shims();
124210
- var CORPUS_VERSION = "v1";
124211
-
124212
- // ../../packages/eval/src/corpus.ts
124213
- init_cjs_shims();
124214
- function toUsableReport(blob) {
124215
- if (blob === null || typeof blob !== "object") return null;
124216
- const r = blob;
124217
- if (!Array.isArray(r.suspectedCauses)) return null;
124218
- const seeds = Array.isArray(r.seeds) ? r.seeds.map((s5) => s5 && typeof s5 === "object" && "name" in s5 ? String(s5.name) : "").filter(Boolean) : [];
124219
- return {
124220
- summary: typeof r.summary === "string" ? r.summary : "",
124221
- confidence: typeof r.confidence === "number" && Number.isFinite(r.confidence) ? r.confidence : 0,
124222
- seeds,
124223
- causes: r.suspectedCauses
124224
- };
124225
- }
124226
- function pickHeadlineCause(causes) {
124227
- let best = null;
124228
- for (const c4 of causes) {
124229
- if (best === null || c4.finalScore > best.finalScore || c4.finalScore === best.finalScore && c4.id < best.id) {
124230
- best = c4;
124231
- }
124232
- }
124233
- return best;
124234
- }
124235
- function flattenFactors(cause) {
124236
- const sums = /* @__PURE__ */ new Map();
124237
- for (const e of cause.explanations ?? []) {
124238
- sums.set(e.factor, (sums.get(e.factor) ?? 0) + e.delta);
124239
- }
124240
- return [...sums.entries()].map(([factor, delta]) => ({ factor, delta })).sort((a2, b5) => a2.factor < b5.factor ? -1 : a2.factor > b5.factor ? 1 : 0);
124241
- }
124242
- function projectHeadline(cause) {
124243
- return {
124244
- id: cause.id,
124245
- category: cause.category,
124246
- finalScore: cause.finalScore,
124247
- confidence: cause.confidence,
124248
- band: cause.band,
124249
- sourceEvidenceIds: [...cause.sourceEvidenceIds ?? []].sort(),
124250
- factors: flattenFactors(cause)
124251
- };
124252
- }
124253
- function buildCorpus(labels, resolveReport) {
124254
- const current = dedupeToCurrentVerdict(labels);
124255
- const evaluated = current.length;
124256
- const rows = [];
124257
- const quarantined = [];
124258
- const unjoinable = [];
124259
- for (const label of current) {
124260
- if (label.investigationId === null) {
124261
- unjoinable.push({ investigationId: null, reason: "null-investigation" });
124262
- continue;
124263
- }
124264
- if (!isOutcomeResolved(label.resolved) || !isOutcomeSource(label.source)) {
124265
- quarantined.push({
124266
- investigationId: label.investigationId,
124267
- resolved: String(label.resolved),
124268
- source: String(label.source),
124269
- reason: !isOutcomeResolved(label.resolved) ? `invalid resolved "${String(label.resolved)}"` : `invalid source "${String(label.source)}"`
124270
- });
124271
- continue;
124272
- }
124273
- const blob = resolveReport(label.investigationId);
124274
- if (blob === null || blob === void 0) {
124275
- unjoinable.push({ investigationId: label.investigationId, reason: "missing-report" });
124276
- continue;
124277
- }
124278
- const report = toUsableReport(blob);
124279
- if (report === null) {
124280
- unjoinable.push({ investigationId: label.investigationId, reason: "legacy-report" });
124281
- continue;
124282
- }
124283
- const isConfirm = label.source === "confirm";
124284
- const headline = pickHeadlineCause(report.causes);
124285
- rows.push({
124286
- investigationId: label.investigationId,
124287
- project: label.project,
124288
- at: label.at instanceof Date ? label.at.toISOString() : String(label.at),
124289
- target: label.resolved,
124290
- source: label.source,
124291
- weak: isConfirm,
124292
- confirmedCause: isConfirm ? null : label.confirmedCause ?? null,
124293
- summary: report.summary,
124294
- reportConfidence: report.confidence,
124295
- seeds: report.seeds,
124296
- causeCount: report.causes.length,
124297
- headlineCause: headline ? projectHeadline(headline) : null
124298
- });
124299
- }
124300
- const classBalance = { yes: 0, partly: 0, no: 0 };
124301
- const bySource = { feedback: 0, confirm: 0 };
124302
- for (const r of rows) {
124303
- classBalance[r.target] += 1;
124304
- bySource[r.source] += 1;
124305
- }
124306
- return { rows, evaluated, quarantined, unjoinable, classBalance, bySource };
124307
- }
124308
- function corpusFilename(version3 = CORPUS_VERSION) {
124309
- return `corpus-${version3}.jsonl`;
124310
- }
124311
- function sortRows(rows) {
124312
- return [...rows].sort((a2, b5) => {
124313
- if (a2.investigationId !== b5.investigationId) return a2.investigationId < b5.investigationId ? -1 : 1;
124314
- if (a2.at !== b5.at) return a2.at < b5.at ? -1 : 1;
124315
- return 0;
124316
- });
124317
- }
124318
- function fnv1a(s5) {
124319
- let h3 = 2166136261;
124320
- for (let i3 = 0; i3 < s5.length; i3++) {
124321
- h3 ^= s5.charCodeAt(i3);
124322
- h3 = Math.imul(h3, 16777619);
124323
- }
124324
- return h3 >>> 0;
124325
- }
124326
- function holdoutSplit(rows, holdoutPct = 20) {
124327
- const train = [];
124328
- const holdout = [];
124329
- for (const r of rows) {
124330
- if (fnv1a(r.investigationId) % 100 < holdoutPct) holdout.push(r.investigationId);
124331
- else train.push(r.investigationId);
124332
- }
124333
- train.sort();
124334
- holdout.sort();
124335
- return { holdoutPct, train, holdout };
124336
- }
124337
- function serializeCorpus(build, version3 = CORPUS_VERSION) {
124338
- const sorted = sortRows(build.rows);
124339
- const jsonl = sorted.map((r) => JSON.stringify(r)).join("\n") + (sorted.length > 0 ? "\n" : "");
124340
- const manifest = {
124341
- version: version3,
124342
- generatedAt: null,
124343
- rows: sorted.length,
124344
- evaluated: build.evaluated,
124345
- quarantined: build.quarantined.length,
124346
- unjoinable: build.unjoinable.length,
124347
- classBalance: build.classBalance,
124348
- bySource: build.bySource,
124349
- holdout: holdoutSplit(sorted)
124350
- };
124351
- return { filename: corpusFilename(version3), jsonl, manifest };
124352
- }
124353
-
124354
- // ../../packages/eval/src/baseline.ts
124355
- init_cjs_shims();
124356
- function computeBaseline(labels) {
124357
- const summary = summarizeOutcomeLabels(labels);
124358
- const byProjectMap = /* @__PURE__ */ new Map();
124359
- for (const l2 of labels) {
124360
- const key = l2.project ?? null;
124361
- const arr = byProjectMap.get(key);
124362
- if (arr) arr.push(l2);
124363
- else byProjectMap.set(key, [l2]);
124364
- }
124365
- const byProject = [...byProjectMap.entries()].map(([project, slice]) => {
124366
- const s5 = summarizeOutcomeLabels(slice);
124367
- return {
124368
- project,
124369
- n: s5.evaluated,
124370
- strictHitRate: s5.accuracy,
124371
- weightedHitRate: s5.weightedScore
124372
- };
124373
- }).sort((a2, b5) => {
124374
- if (a2.project === b5.project) return 0;
124375
- if (a2.project === null) return 1;
124376
- if (b5.project === null) return -1;
124377
- return a2.project < b5.project ? -1 : 1;
124378
- });
124379
- return {
124380
- n: summary.evaluated,
124381
- strictHitRate: summary.accuracy,
124382
- weightedHitRate: summary.weightedScore,
124383
- classBalance: summary.counts,
124384
- bySource: summary.bySource,
124385
- byProject
124386
- };
124387
- }
124388
- function mean(xs) {
124389
- if (xs.length === 0) return 0;
124390
- return xs.reduce((a2, b5) => a2 + b5, 0) / xs.length;
124391
- }
124392
- function variance(xs, m5) {
124393
- if (xs.length === 0) return 0;
124394
- return xs.reduce((a2, b5) => a2 + (b5 - m5) * (b5 - m5), 0) / xs.length;
124395
- }
124396
- function featureSeparation(rows) {
124397
- const usable = rows.filter(
124398
- (r) => !r.weak && r.headlineCause !== null && (r.target === "yes" || r.target === "no")
124399
- );
124400
- const factorSet = /* @__PURE__ */ new Set();
124401
- for (const r of usable) {
124402
- for (const f5 of r.headlineCause.factors) factorSet.add(f5.factor);
124403
- }
124404
- const factors = [...factorSet].sort();
124405
- const out2 = factors.map((factor) => {
124406
- const yes = [];
124407
- const no = [];
124408
- for (const r of usable) {
124409
- const f5 = r.headlineCause.factors.find((x4) => x4.factor === factor);
124410
- const delta = f5 ? f5.delta : 0;
124411
- if (r.target === "yes") yes.push(delta);
124412
- else no.push(delta);
124413
- }
124414
- const meanYes = mean(yes);
124415
- const meanNo = mean(no);
124416
- const varYes = variance(yes, meanYes);
124417
- const varNo = variance(no, meanNo);
124418
- const separation = meanYes - meanNo;
124419
- const all = yes.concat(no);
124420
- const overallMean = mean(all);
124421
- const pooledVar = variance(all, overallMean);
124422
- const pooledStd = Math.sqrt(pooledVar);
124423
- const effectSize = pooledStd === 0 ? 0 : separation / pooledStd;
124424
- const cv = overallMean === 0 ? 0 : pooledStd / Math.abs(overallMean);
124425
- return {
124426
- factor,
124427
- nYes: yes.length,
124428
- nNo: no.length,
124429
- meanYes,
124430
- meanNo,
124431
- varYes,
124432
- varNo,
124433
- separation,
124434
- effectSize,
124435
- cv
124436
- };
124437
- });
124438
- out2.sort((a2, b5) => {
124439
- const d3 = Math.abs(b5.effectSize) - Math.abs(a2.effectSize);
124440
- if (d3 !== 0) return d3;
124441
- return a2.factor < b5.factor ? -1 : a2.factor > b5.factor ? 1 : 0;
124442
- });
124443
- return { evaluated: usable.length, factors: out2 };
124444
- }
124445
-
124446
- // ../../packages/cli/src/commands/eval.ts
124447
124709
  function resolveProject2(config2, repo) {
124448
124710
  let project;
124449
124711
  try {
@@ -124492,7 +124754,7 @@ async function runEvalBuild(opts) {
124492
124754
  const project = resolveProject2(config2, opts.repo);
124493
124755
  if (!project) return 1;
124494
124756
  const version3 = opts.version ?? CORPUS_VERSION;
124495
- const outDir = opts.out ? (0, import_node_path26.isAbsolute)(opts.out) ? opts.out : (0, import_node_path26.resolve)(process.cwd(), opts.out) : (0, import_node_path26.join)(process.cwd(), ".horus", "eval");
124757
+ const outDir = opts.out ? (0, import_node_path27.isAbsolute)(opts.out) ? opts.out : (0, import_node_path27.resolve)(process.cwd(), opts.out) : (0, import_node_path27.join)(process.cwd(), ".horus", "eval");
124496
124758
  const { db, sql: sql2 } = await openDb(config2.database.url);
124497
124759
  try {
124498
124760
  const labels = await listOutcomeLabels(db, {
@@ -124508,8 +124770,8 @@ async function runEvalBuild(opts) {
124508
124770
  const build = buildCorpus(labels, resolveReport);
124509
124771
  const artifact = serializeCorpus(build, version3);
124510
124772
  await (0, import_promises5.mkdir)(outDir, { recursive: true });
124511
- const corpusPath = (0, import_node_path26.join)(outDir, artifact.filename);
124512
- const manifestPath = (0, import_node_path26.join)(outDir, `corpus-${version3}.manifest.json`);
124773
+ const corpusPath = (0, import_node_path27.join)(outDir, artifact.filename);
124774
+ const manifestPath = (0, import_node_path27.join)(outDir, `corpus-${version3}.manifest.json`);
124513
124775
  const manifest = { ...artifact.manifest, generatedAt: (/* @__PURE__ */ new Date()).toISOString(), project };
124514
124776
  await (0, import_promises5.writeFile)(corpusPath, artifact.jsonl, "utf8");
124515
124777
  await (0, import_promises5.writeFile)(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
@@ -124564,7 +124826,7 @@ async function runEvalBaseline(opts) {
124564
124826
  if (opts.json) {
124565
124827
  console.log(JSON.stringify({ project, baseline, separation }, null, 2));
124566
124828
  } else {
124567
- const pct = (n4) => `${(n4 * 100).toFixed(0)}%`;
124829
+ const pct2 = (n4) => `${(n4 * 100).toFixed(0)}%`;
124568
124830
  console.log(import_picocolors56.default.bold(`Eval baseline \u2014 ${project}`));
124569
124831
  if (baseline.n === 0) {
124570
124832
  console.log(
@@ -124574,7 +124836,7 @@ async function runEvalBaseline(opts) {
124574
124836
  );
124575
124837
  } else {
124576
124838
  console.log(
124577
- ` Strict hit-rate: ${import_picocolors56.default.green(pct(baseline.strictHitRate))} ` + import_picocolors56.default.dim(`(weighted ${pct(baseline.weightedHitRate)}) over ${baseline.n} investigation(s)`)
124839
+ ` Strict hit-rate: ${import_picocolors56.default.green(pct2(baseline.strictHitRate))} ` + import_picocolors56.default.dim(`(weighted ${pct2(baseline.weightedHitRate)}) over ${baseline.n} investigation(s)`)
124578
124840
  );
124579
124841
  console.log(
124580
124842
  ` Class balance: ${import_picocolors56.default.green(`${baseline.classBalance.yes} yes`)}, ${import_picocolors56.default.yellow(`${baseline.classBalance.partly} partly`)}, ${import_picocolors56.default.red(`${baseline.classBalance.no} no`)} \xB7 ` + import_picocolors56.default.dim(`source: ${baseline.bySource.feedback} feedback, ${baseline.bySource.confirm} confirm`)
@@ -124607,12 +124869,120 @@ async function runEvalBaseline(opts) {
124607
124869
  }
124608
124870
  }
124609
124871
 
124610
- // ../../packages/cli/src/lib/report-crash.ts
124872
+ // ../../packages/cli/src/commands/train.ts
124611
124873
  init_cjs_shims();
124874
+ var import_promises6 = require("fs/promises");
124875
+ var import_node_path28 = require("path");
124876
+ var import_node_os10 = require("os");
124612
124877
  var import_picocolors57 = __toESM(require_picocolors(), 1);
124878
+ var RERANKER_MODEL_PATH = (0, import_node_path28.join)((0, import_node_os10.homedir)(), ".horus", "reranker.json");
124879
+ function candidatesFromReport(blob) {
124880
+ if (blob === null || typeof blob !== "object") return null;
124881
+ const causes = blob.suspectedCauses;
124882
+ if (!Array.isArray(causes)) return null;
124883
+ return causes;
124884
+ }
124885
+ function pct(n4) {
124886
+ return `${(n4 * 100).toFixed(1)}%`;
124887
+ }
124888
+ async function runTrain(opts) {
124889
+ let source;
124890
+ if (opts.source !== void 0) {
124891
+ if (!isOutcomeSource(opts.source)) {
124892
+ console.error(import_picocolors57.default.red(`Unknown --source "${opts.source}" (one of: feedback, confirm).`));
124893
+ return 1;
124894
+ }
124895
+ source = opts.source;
124896
+ }
124897
+ let since;
124898
+ if (opts.days !== void 0) {
124899
+ if (!Number.isFinite(opts.days) || opts.days <= 0) {
124900
+ console.error(import_picocolors57.default.red("--days must be a positive number."));
124901
+ return 1;
124902
+ }
124903
+ since = new Date(Date.now() - opts.days * 24 * 60 * 60 * 1e3);
124904
+ }
124905
+ let investigations2;
124906
+ try {
124907
+ const config2 = await loadConfig(opts.config);
124908
+ const { db, sql: sql2 } = await openDb(config2.database.url);
124909
+ try {
124910
+ const labels = await listOutcomeLabels(db, {
124911
+ ...source ? { source } : {},
124912
+ ...since ? { since } : {},
124913
+ ...opts.limit ? { limit: opts.limit } : {}
124914
+ });
124915
+ const current = dedupeToCurrentVerdict(labels);
124916
+ investigations2 = [];
124917
+ for (const label of current) {
124918
+ if (label.investigationId === null) continue;
124919
+ if (!isOutcomeResolved(label.resolved) || !isOutcomeSource(label.source)) continue;
124920
+ const inv = await getInvestigation(db, label.investigationId);
124921
+ const candidates = candidatesFromReport(inv?.report ?? null);
124922
+ if (candidates === null) continue;
124923
+ const isConfirm = label.source === "confirm";
124924
+ investigations2.push({
124925
+ investigationId: label.investigationId,
124926
+ target: label.resolved,
124927
+ // confirm's confirmedCause is Horus's own summary (circular) — drop it, like the corpus.
124928
+ confirmedCause: isConfirm ? null : label.confirmedCause ?? null,
124929
+ candidates
124930
+ });
124931
+ }
124932
+ } finally {
124933
+ await sql2.end();
124934
+ }
124935
+ } catch (err2) {
124936
+ console.error(import_picocolors57.default.red(`Could not read the local corpus: ${err2.message}`));
124937
+ return 1;
124938
+ }
124939
+ const split = holdoutSplit(investigations2);
124940
+ const result = trainReranker(investigations2, new Set(split.holdout));
124941
+ if (!result.ok) {
124942
+ if (result.reason === "insufficient-corpus") {
124943
+ console.log(
124944
+ import_picocolors57.default.yellow(
124945
+ `Not enough labeled investigations to train yet (have ${result.labeledInvestigations} with a known-correct cause; need \u2265${MIN_TRAIN_INVESTIGATIONS}).`
124946
+ )
124947
+ );
124948
+ console.log(import_picocolors57.default.dim(" Keep labeling outcomes with `horus feedback` \u2014 the reranker trains once the corpus is large enough."));
124949
+ } else {
124950
+ console.log(import_picocolors57.default.yellow(`Cannot train a meaningful model yet: ${result.detail}.`));
124951
+ }
124952
+ return 0;
124953
+ }
124954
+ const { baselineHitRate, rerankedHitRate, delta, n: n4 } = result.holdout;
124955
+ console.log(import_picocolors57.default.bold("\nReranker training (local, per-tenant)\n"));
124956
+ console.log(` Holdout investigations: ${n4}`);
124957
+ console.log(` Baseline top-1 hit-rate: ${pct(baselineHitRate)}`);
124958
+ console.log(` Reranked top-1 hit-rate: ${pct(rerankedHitRate)}`);
124959
+ console.log(` Delta: ${delta >= 0 ? "+" : ""}${pct(delta)}`);
124960
+ console.log("");
124961
+ if (delta <= 0) {
124962
+ console.log(import_picocolors57.default.yellow(" The reranker did NOT beat the deterministic baseline \u2014 not saving a model."));
124963
+ console.log(import_picocolors57.default.dim(" Horus stays on its hand-tuned ranking. Re-run as more labels accumulate."));
124964
+ return 0;
124965
+ }
124966
+ try {
124967
+ await (0, import_promises6.mkdir)((0, import_node_path28.join)((0, import_node_os10.homedir)(), ".horus"), { recursive: true });
124968
+ const { ok: _ok, ...model } = result;
124969
+ void _ok;
124970
+ await (0, import_promises6.writeFile)(RERANKER_MODEL_PATH, JSON.stringify(model, null, 2) + "\n", "utf8");
124971
+ } catch (err2) {
124972
+ console.error(import_picocolors57.default.red(`Trained, but could not write the model: ${err2.message}`));
124973
+ return 1;
124974
+ }
124975
+ console.log(import_picocolors57.default.green(` \u2713 Reranker beats baseline by +${pct(delta)} \u2014 saved to ${RERANKER_MODEL_PATH}`));
124976
+ console.log(import_picocolors57.default.dim(" It ships OFF by default. Enable it with `HORUS_RERANK=1 horus investigate \u2026`."));
124977
+ return 0;
124978
+ }
124979
+
124980
+ // ../../packages/cli/src/lib/report-crash.ts
124981
+ init_cjs_shims();
124982
+ var import_picocolors58 = __toESM(require_picocolors(), 1);
124613
124983
  function reportCrash(err2, log = console.error) {
124614
124984
  log(err2);
124615
- log(import_picocolors57.default.dim("\n Looks like a Horus bug or a missing capability? Report it: horus report"));
124985
+ log(import_picocolors58.default.dim("\n Looks like a Horus bug or a missing capability? Report it: horus report"));
124616
124986
  }
124617
124987
 
124618
124988
  // ../../packages/cli/src/index.ts
@@ -124824,6 +125194,16 @@ Examples:
124824
125194
  process.exitCode = await runEvalBaseline(opts);
124825
125195
  }
124826
125196
  );
125197
+ program2.command("train").description("Fit the local, per-tenant reranker over your outcome-label corpus (off until it beats baseline)").option("-c, --config <path>", "path to horus.config.ts").option("--source <source>", "filter to one signal source: feedback | confirm").option("--days <n>", "only train on labels from the last N days", (v4) => Number(v4)).option("--limit <n>", "max labels to scan", (v4) => Number(v4)).addHelpText(
125198
+ "after",
125199
+ `
125200
+ The reranker is LOCAL and per-tenant: your corpus never leaves this machine. It only REORDERS
125201
+ candidate causes among those that already clear Horus's confidence gates \u2014 it never changes a
125202
+ score, a confidence, or a verdict. It ships OFF; once trained, enable with:
125203
+ HORUS_RERANK=1 horus investigate "<hint>"`
125204
+ ).action(async (opts) => {
125205
+ process.exitCode = await runTrain(opts);
125206
+ });
124827
125207
  program2.command("mcp").description("Run the Horus MCP server (stdio) \u2014 exposes the local project-knowledge index to agents").action(async () => {
124828
125208
  process.exitCode = await runMcpServer();
124829
125209
  });