@merittdev/horus 0.8.6 → 0.8.8

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 +133 -68
  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.6" : "dev";
65883
+ var HORUS_VERSION = true ? "0.8.8" : "dev";
65884
65884
  var PINNED_SOURCE_VERSION = "1.5.0";
65885
65885
 
65886
65886
  // ../../packages/core/src/evidence.ts
@@ -70614,6 +70614,11 @@ async function loadConfig(configPath, opts) {
70614
70614
  } else {
70615
70615
  const jsPath = (0, import_node_path2.resolve)(cwd, "config/horus.config.js");
70616
70616
  const tsPath = (0, import_node_path2.resolve)(cwd, "config/horus.config.ts");
70617
+ if (!(0, import_node_fs2.existsSync)(jsPath) && !(0, import_node_fs2.existsSync)(tsPath)) {
70618
+ throw new Error(
70619
+ `No Horus config found for ${cwd}. Run \`horus index\` in this repo to set it up, or pass --config <path> / --name <project>.`
70620
+ );
70621
+ }
70617
70622
  target = (0, import_node_fs2.existsSync)(jsPath) ? jsPath : tsPath;
70618
70623
  }
70619
70624
  }
@@ -91925,6 +91930,8 @@ function correlate(evidence2) {
91925
91930
  init_cjs_shims();
91926
91931
  var PREFER = /resolver|controller|\bservice\b|route|router|handler|gateway|processor|worker|consumer|repository|usecase|use-case|endpoint/i;
91927
91932
  var DEMOTE = /\butil|helper|\bscript|backfill|legacy|migration|seeder|fixture|\bmock\b|\btest\b|\bspec\b/i;
91933
+ var PRESENTATION_NAME = /^(build|format|render|compose|serialize)[A-Z].*(Message|Markup|Html|Text|Label|Summary|Notification|String)$/;
91934
+ var PREDICATE_NAME = /^(is|has|can|should|was|were|are|does|did|needs|allows?|must)[A-Z]/;
91928
91935
  var TYPE_SUFFIX = /(Result|Input|Response|Payload|Args|Dto|Props|Edge|Connection)$/;
91929
91936
  var INTERFACE_PREFIX = /^I(?=[A-Z][a-z])/;
91930
91937
  var TYPE_LIKE_NAME = new RegExp(`(?:${TYPE_SUFFIX.source})|(?:${INTERFACE_PREFIX.source})`);
@@ -91952,8 +91959,9 @@ function scoreSeed(s5, index2, hintTokens, changedFiles, hintHasCode) {
91952
91959
  let score = 0;
91953
91960
  if (PREFER.test(hay)) score += hintHasCode ? 0 : 3;
91954
91961
  if (DEMOTE.test(hay)) score -= 3;
91962
+ if (PRESENTATION_NAME.test(s5.name)) score -= 3;
91955
91963
  if (isTypeLikeName(s5.name)) score -= 4;
91956
- if (s5.startLine !== void 0 && s5.endLine !== void 0 && s5.endLine - s5.startLine <= 3) {
91964
+ if (s5.startLine !== void 0 && s5.endLine !== void 0 && s5.endLine - s5.startLine <= 3 && !PREDICATE_NAME.test(s5.name)) {
91957
91965
  score -= 3;
91958
91966
  }
91959
91967
  if (changedFiles !== void 0 && changedFiles.has(s5.filePath)) score += 5;
@@ -92279,6 +92287,15 @@ function clamp015(n4) {
92279
92287
  var SEV_ERROR = 2;
92280
92288
  var SEV_WARN = 1;
92281
92289
  var SEV_INFO = 0;
92290
+ var GENERIC_SYMBOL_NAMES = /* @__PURE__ */ new Set([
92291
+ "",
92292
+ "constructor",
92293
+ "unknown",
92294
+ "unknown-producer",
92295
+ "unknown-worker",
92296
+ "anonymous",
92297
+ "default"
92298
+ ]);
92282
92299
  function seedEmittedSeverityTier(level, code, message) {
92283
92300
  const lv = level?.toLowerCase();
92284
92301
  if (lv === "error" || lv === "fatal" || lv === "critical") return SEV_ERROR;
@@ -92740,16 +92757,33 @@ async function investigate(input, deps) {
92740
92757
  } catch {
92741
92758
  edges = [];
92742
92759
  }
92743
- const symbolNames = /* @__PURE__ */ new Set([
92744
- top.name,
92745
- ...ctx.callers.map((s5) => s5.name),
92746
- ...ctx.callees.map((s5) => s5.name)
92747
- ]);
92760
+ const symbolNames = new Set(
92761
+ [top.name, ...ctx.callers.map((s5) => s5.name), ...ctx.callees.map((s5) => s5.name)].filter(
92762
+ (n4) => n4 && !GENERIC_SYMBOL_NAMES.has(n4)
92763
+ )
92764
+ );
92748
92765
  queueHits = edges.filter((e) => {
92749
92766
  const bySymbol = e.producerSymbol !== null && symbolNames.has(e.producerSymbol) || e.workerSymbol !== null && symbolNames.has(e.workerSymbol);
92750
92767
  const byFile = e.producerFile === top.filePath || e.workerFile === top.filePath;
92751
92768
  return bySymbol || byFile;
92752
92769
  });
92770
+ if (queueHits.length > 0 && typeof code.searchSymbols === "function") {
92771
+ const resolvesInRepo = async (name2) => {
92772
+ if (!name2 || GENERIC_SYMBOL_NAMES.has(name2)) return false;
92773
+ try {
92774
+ return (await code.searchSymbols(name2, 3)).some((s5) => s5.name === name2);
92775
+ } catch {
92776
+ return true;
92777
+ }
92778
+ };
92779
+ const validated2 = [];
92780
+ for (const e of queueHits) {
92781
+ if (await resolvesInRepo(e.producerSymbol) || await resolvesInRepo(e.workerSymbol)) {
92782
+ validated2.push(e);
92783
+ }
92784
+ }
92785
+ queueHits = validated2;
92786
+ }
92753
92787
  if (input.since !== void 0 && looksDiffable(input.since)) {
92754
92788
  try {
92755
92789
  changes = await code.detectChanges({ base: input.since, compare: "HEAD" });
@@ -92933,7 +92967,10 @@ async function investigate(input, deps) {
92933
92967
  message: sig.sampleMessage ?? "",
92934
92968
  evId: existingEvId,
92935
92969
  isNew: sig.isNew ?? false,
92936
- level: sig.level ?? null
92970
+ level: sig.level ?? null,
92971
+ raisedBySeed: true,
92972
+ // literal is in the seed's own body
92973
+ raiserName: top?.name ?? ""
92937
92974
  });
92938
92975
  }
92939
92976
  }
@@ -92981,7 +93018,10 @@ async function investigate(input, deps) {
92981
93018
  message: sig.sampleMessage ?? "",
92982
93019
  evId: ev.id,
92983
93020
  isNew: sig.isNew ?? false,
92984
- level: sig.level ?? null
93021
+ level: sig.level ?? null,
93022
+ raisedBySeed: true,
93023
+ // literal is in the seed's own body
93024
+ raiserName: top?.name ?? ""
92985
93025
  });
92986
93026
  }
92987
93027
  } catch {
@@ -93005,13 +93045,14 @@ async function investigate(input, deps) {
93005
93045
  ])
93006
93046
  ].slice(0, GAP_D_MAX_RESOLVE * 2);
93007
93047
  for (const sig of candidates) {
93008
- let raiser;
93048
+ let raisers = [];
93009
93049
  try {
93010
- raiser = (await deps.code.searchSymbols(sig.key, 1))[0];
93050
+ raisers = await deps.code.searchSymbols(sig.key, 3);
93011
93051
  } catch {
93012
93052
  continue;
93013
93053
  }
93014
- if (raiser === void 0 || !seedScope.has(raiser.id)) continue;
93054
+ const raiser = raisers.find((r) => r.id === top.id) ?? raisers.find((r) => seedScope.has(r.id));
93055
+ if (raiser === void 0) continue;
93015
93056
  let evId = directEvIdByKey.get(sig.key);
93016
93057
  if (evId === void 0) {
93017
93058
  const where = raiser.id === top.id ? "the seed" : "a callee of the seed";
@@ -93054,7 +93095,9 @@ async function investigate(input, deps) {
93054
93095
  message: sig.sampleMessage ?? "",
93055
93096
  evId,
93056
93097
  isNew: sig.isNew ?? false,
93057
- level: sig.level ?? null
93098
+ level: sig.level ?? null,
93099
+ raisedBySeed: raiser.id === top.id,
93100
+ raiserName: raiser.name
93058
93101
  });
93059
93102
  }
93060
93103
  }
@@ -93643,6 +93686,7 @@ async function investigate(input, deps) {
93643
93686
  }
93644
93687
  if (seedEmittedJoins.length > 0 && top && seedEv) {
93645
93688
  const ordered = [...seedEmittedJoins].sort((a2, b5) => {
93689
+ if (a2.raisedBySeed !== b5.raisedBySeed) return a2.raisedBySeed ? -1 : 1;
93646
93690
  const ta = seedEmittedSeverityTier(a2.level, a2.code, a2.message);
93647
93691
  const tb = seedEmittedSeverityTier(b5.level, b5.code, b5.message);
93648
93692
  if (ta !== tb) return tb - ta;
@@ -93656,17 +93700,21 @@ async function investigate(input, deps) {
93656
93700
  const tier = seedEmittedSeverityTier(lead.level, lead.code, lead.message);
93657
93701
  const volumeBoost = Math.min(0.1, Math.log10(Math.max(1, lead.count)) * 0.025);
93658
93702
  const msgClause = lead.message ? ` \u2014 "${lead.message.replace(/\s+/g, " ").trim().slice(0, 90)}"` : "";
93703
+ const bySeed = lead.raisedBySeed;
93704
+ const raiser = bySeed ? top.name : lead.raiserName || top.name;
93705
+ const depClause = bySeed ? "" : `, a dependency of ${top.name}`;
93706
+ const calleePenalty = bySeed ? 0 : 0.18;
93659
93707
  let title;
93660
93708
  let baseScore;
93661
93709
  if (tier === SEV_ERROR) {
93662
- title = `Runtime error ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${top.name} \u2014 the likely failure${msgClause}`;
93663
- baseScore = clamp015(0.6 + volumeBoost);
93710
+ title = bySeed ? `Runtime error ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${raiser} \u2014 the likely failure${msgClause}` : `Runtime error ${lead.code} (${lead.count}x in Elasticsearch) surfaces on ${raiser}${depClause}${msgClause}`;
93711
+ baseScore = clamp015(0.6 + volumeBoost - calleePenalty);
93664
93712
  } else if (tier === SEV_WARN) {
93665
- title = `Warning ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${top.name}${msgClause}`;
93666
- baseScore = clamp015(0.42 + volumeBoost);
93713
+ title = `Warning ${lead.code} (${lead.count}x in Elasticsearch) ${bySeed ? "is raised by" : "surfaces on"} ${raiser}${depClause}${msgClause}`;
93714
+ baseScore = clamp015(0.42 + volumeBoost - calleePenalty);
93667
93715
  } else {
93668
- title = `Diagnostic signal ${lead.code} (${lead.count}x in Elasticsearch) is logged by ${top.name} \u2014 informational, not an error${msgClause}`;
93669
- baseScore = clamp015(0.3 + volumeBoost);
93716
+ title = `Diagnostic signal ${lead.code} (${lead.count}x in Elasticsearch) is logged by ${raiser}${depClause} \u2014 informational, not an error${msgClause}`;
93717
+ baseScore = clamp015(0.3 + volumeBoost - calleePenalty);
93670
93718
  }
93671
93719
  causeInputs.push({
93672
93720
  id: "cause:seed-emitted-error",
@@ -97230,6 +97278,50 @@ async function stitch(client, db, opts = {}) {
97230
97278
  };
97231
97279
  }
97232
97280
 
97281
+ // ../../packages/cli/src/lib/ensure-host.ts
97282
+ init_cjs_shims();
97283
+ function parseHostPort(hostUrl) {
97284
+ try {
97285
+ const port = parseInt(new URL(hostUrl).port, 10);
97286
+ return Number.isNaN(port) ? null : port;
97287
+ } catch {
97288
+ return null;
97289
+ }
97290
+ }
97291
+ function ensureHostReasonHint(reason) {
97292
+ switch (reason) {
97293
+ case "source-unavailable":
97294
+ return "horus-source is not installed \u2014 install it: pip install horus-source";
97295
+ case "version-mismatch":
97296
+ return "the installed horus-source backend does not match the version Horus is pinned to \u2014 reinstall the pinned version (run `horus status` to see it)";
97297
+ case "not-analyzed":
97298
+ return "this repo has not been indexed yet \u2014 run: horus index";
97299
+ case "bad-url":
97300
+ return "the configured source host URL is not a valid URL";
97301
+ default:
97302
+ return "the host did not become healthy \u2014 run: horus index";
97303
+ }
97304
+ }
97305
+ async function ensureSourceHost(root, hostUrl, opts = {}) {
97306
+ if (await isHostHealthy(hostUrl)) return { ok: true, hostUrl };
97307
+ if (!await sourceAvailable()) return { ok: false, reason: "source-unavailable" };
97308
+ try {
97309
+ await assertSourceVersionPinned();
97310
+ } catch {
97311
+ return { ok: false, reason: "version-mismatch" };
97312
+ }
97313
+ if (!isAnalyzed(root)) return { ok: false, reason: "not-analyzed" };
97314
+ const port = parseHostPort(hostUrl);
97315
+ if (port === null) return { ok: false, reason: "bad-url" };
97316
+ startHost(root, port);
97317
+ if (await waitForHost(hostUrl, opts.timeoutMs ?? 2e4)) {
97318
+ reconcileSpawnedHostPid(root, port);
97319
+ return { ok: true, hostUrl };
97320
+ }
97321
+ removeSpawnedHostRecord(root);
97322
+ return { ok: false, reason: "unhealthy" };
97323
+ }
97324
+
97233
97325
  // ../../packages/cli/src/commands/index-repo.ts
97234
97326
  async function stitchQueueMap(hostUrl, dbUrl, label) {
97235
97327
  const source = new SourceHttpClient({ baseUrl: hostUrl });
@@ -97243,6 +97335,14 @@ async function stitchQueueMap(hostUrl, dbUrl, label) {
97243
97335
  await sql2.end();
97244
97336
  }
97245
97337
  }
97338
+ async function hostServesRepo(hostUrl, root) {
97339
+ try {
97340
+ const info2 = await new SourceHttpClient({ baseUrl: hostUrl }).hostInfo();
97341
+ return (0, import_node_path10.resolve)(info2.repoPath) === (0, import_node_path10.resolve)(root);
97342
+ } catch {
97343
+ return true;
97344
+ }
97345
+ }
97246
97346
  function findConfiguredRepo(config2, root) {
97247
97347
  const target = (0, import_node_path10.resolve)(root);
97248
97348
  for (const p5 of config2.projects) {
@@ -97329,7 +97429,7 @@ async function runIndex(opts) {
97329
97429
  let hostUrl;
97330
97430
  let spawned = false;
97331
97431
  for (const candidate of [configuredHost, readSourceHostUrl(root) ?? void 0]) {
97332
- if (candidate && await isHostHealthy(candidate)) {
97432
+ if (candidate && await isHostHealthy(candidate) && await hostServesRepo(candidate, root)) {
97333
97433
  hostUrl = candidate;
97334
97434
  break;
97335
97435
  }
@@ -97360,7 +97460,17 @@ async function runIndex(opts) {
97360
97460
  } else {
97361
97461
  console.log(import_picocolors3.default.dim(" already analyzed"));
97362
97462
  }
97363
- const port = await findFreePort();
97463
+ const priorPort = parseHostPort(configuredHost ?? "") ?? parseHostPort(readSourceHostUrl(root) ?? "");
97464
+ let port;
97465
+ if (priorPort !== null) {
97466
+ try {
97467
+ port = await findFreePort(priorPort, priorPort);
97468
+ } catch {
97469
+ port = await findFreePort();
97470
+ }
97471
+ } else {
97472
+ port = await findFreePort();
97473
+ }
97364
97474
  hostUrl = `http://127.0.0.1:${port}`;
97365
97475
  console.log(import_picocolors3.default.dim(` starting source-intelligence host on port ${port}\u2026`));
97366
97476
  startHost(root, port);
@@ -97375,7 +97485,8 @@ async function runIndex(opts) {
97375
97485
  }
97376
97486
  await stitchQueueMap(hostUrl, dbUrl, label);
97377
97487
  buildKnowledgeIndex(root, config2, label, configuredProject, opts);
97378
- if (spawned && !isConfigured) {
97488
+ const hasLocalConfig = discoverLocalConfig(root) !== null;
97489
+ if (!isConfigured && !hasLocalConfig) {
97379
97490
  const file = {
97380
97491
  version: 1,
97381
97492
  project: {
@@ -110296,52 +110407,6 @@ async function maybePromptFeedback(opts) {
110296
110407
  // ../../packages/cli/src/lib/investigation-runner.ts
110297
110408
  init_cjs_shims();
110298
110409
  var import_picocolors9 = __toESM(require_picocolors(), 1);
110299
-
110300
- // ../../packages/cli/src/lib/ensure-host.ts
110301
- init_cjs_shims();
110302
- function parseHostPort(hostUrl) {
110303
- try {
110304
- const port = parseInt(new URL(hostUrl).port, 10);
110305
- return Number.isNaN(port) ? null : port;
110306
- } catch {
110307
- return null;
110308
- }
110309
- }
110310
- function ensureHostReasonHint(reason) {
110311
- switch (reason) {
110312
- case "source-unavailable":
110313
- return "horus-source is not installed \u2014 install it: pip install horus-source";
110314
- case "version-mismatch":
110315
- return "the installed horus-source backend does not match the version Horus is pinned to \u2014 reinstall the pinned version (run `horus status` to see it)";
110316
- case "not-analyzed":
110317
- return "this repo has not been indexed yet \u2014 run: horus index";
110318
- case "bad-url":
110319
- return "the configured source host URL is not a valid URL";
110320
- default:
110321
- return "the host did not become healthy \u2014 run: horus index";
110322
- }
110323
- }
110324
- async function ensureSourceHost(root, hostUrl, opts = {}) {
110325
- if (await isHostHealthy(hostUrl)) return { ok: true, hostUrl };
110326
- if (!await sourceAvailable()) return { ok: false, reason: "source-unavailable" };
110327
- try {
110328
- await assertSourceVersionPinned();
110329
- } catch {
110330
- return { ok: false, reason: "version-mismatch" };
110331
- }
110332
- if (!isAnalyzed(root)) return { ok: false, reason: "not-analyzed" };
110333
- const port = parseHostPort(hostUrl);
110334
- if (port === null) return { ok: false, reason: "bad-url" };
110335
- startHost(root, port);
110336
- if (await waitForHost(hostUrl, opts.timeoutMs ?? 2e4)) {
110337
- reconcileSpawnedHostPid(root, port);
110338
- return { ok: true, hostUrl };
110339
- }
110340
- removeSpawnedHostRecord(root);
110341
- return { ok: false, reason: "unhealthy" };
110342
- }
110343
-
110344
- // ../../packages/cli/src/lib/investigation-runner.ts
110345
110410
  async function buildInvestigationContext(renv, opts) {
110346
110411
  const log = opts.log ?? ((l2) => console.error(l2));
110347
110412
  const code = codeForEnv(renv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {