@merittdev/horus 0.4.0 → 0.4.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 +174 -20
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -57239,7 +57239,7 @@ init_cjs_shims();
57239
57239
 
57240
57240
  // ../../packages/core/src/version.ts
57241
57241
  init_cjs_shims();
57242
- var HORUS_VERSION = true ? "0.4.0" : "dev";
57242
+ var HORUS_VERSION = true ? "0.4.1" : "dev";
57243
57243
  var PINNED_SOURCE_VERSION = "1.1.1";
57244
57244
 
57245
57245
  // ../../packages/core/src/evidence.ts
@@ -77009,6 +77009,17 @@ function clamp015(n) {
77009
77009
  if (n > 1) return 1;
77010
77010
  return n;
77011
77011
  }
77012
+ function confidenceCeilingForCause(score) {
77013
+ const s = clamp015(score);
77014
+ if (s < 0.2) return 0.6;
77015
+ if (s < 0.5) {
77016
+ return 0.6 + (s - 0.2) / (0.5 - 0.2) * (0.78 - 0.6);
77017
+ }
77018
+ if (s < 0.85) {
77019
+ return 0.78 + (s - 0.5) / (0.85 - 0.5) * (1 - 0.78);
77020
+ }
77021
+ return 1;
77022
+ }
77012
77023
  function relevanceForKind(kind) {
77013
77024
  switch (kind) {
77014
77025
  case "symbol":
@@ -77156,6 +77167,40 @@ function looksDiffable(since) {
77156
77167
  if (/^\d+[smhd]$/i.test(s)) return false;
77157
77168
  return /^[A-Za-z0-9._/~^-]+$/.test(s);
77158
77169
  }
77170
+ function looksExplanatory(hint) {
77171
+ const h = hint.trim();
77172
+ if (h.length === 0) return false;
77173
+ if (/\b(how|what|why|when|where)\b.*\b(work|works|working|do|does|done|happen|happens|flow|flows|behave|called|invoked|triggered|used)\b/i.test(h)) {
77174
+ return true;
77175
+ }
77176
+ if (/^\s*(how|what|why|when|where|explain|describe|walk\s+me\s+through|tell\s+me\s+(?:about|how))\b/i.test(h)) {
77177
+ return true;
77178
+ }
77179
+ return false;
77180
+ }
77181
+ function formatRegressionCitation(recentChanges, changes) {
77182
+ const commits = (recentChanges?.commits ?? []).slice(0, 2);
77183
+ const commitParts = commits.map((c) => {
77184
+ const sha = c.shortSha || c.sha.slice(0, 7);
77185
+ const subject = (c.subject ?? "").replace(/\s+/g, " ").trim();
77186
+ return subject ? `${sha} "${subject.slice(0, 50)}"` : sha;
77187
+ });
77188
+ const commitClause = commitParts.length > 0 ? ` (${commitParts.join(", ")})` : "";
77189
+ const modified = changes?.modified ?? [];
77190
+ const symbolNames = [];
77191
+ for (const m of modified) {
77192
+ const name = m.after?.name ?? m.before?.name;
77193
+ if (name && !symbolNames.includes(name)) symbolNames.push(name);
77194
+ if (symbolNames.length >= 3) break;
77195
+ }
77196
+ let symbolClause = "";
77197
+ if (symbolNames.length > 0) {
77198
+ const shown = symbolNames.join(", ");
77199
+ const more = modified.length > symbolNames.length ? ", \u2026" : "";
77200
+ symbolClause = ` touched ${shown}${more}`;
77201
+ }
77202
+ return { commitClause, symbolClause };
77203
+ }
77159
77204
  async function investigate(input, deps) {
77160
77205
  const { code, db } = deps;
77161
77206
  const hint = input.hint.trim();
@@ -77940,9 +77985,10 @@ async function investigate(input, deps) {
77940
77985
  const gitChangedFiles = recentChanges?.changedFiles ?? [];
77941
77986
  const seedFile = top?.filePath;
77942
77987
  const seedInChanges = seedFile !== void 0 && gitChangedFiles.length > 0 && gitChangedFiles.some((f) => f === seedFile || seedFile.endsWith(f) || f.endsWith(seedFile));
77988
+ const citation = formatRegressionCitation(recentChanges, changes);
77943
77989
  causeInputs.push({
77944
77990
  id: "cause:deployment-regression",
77945
- title: top ? `Recent change to ${top.name} in ${input.since}..HEAD may have introduced the regression` : `Recent changes in ${input.since}..HEAD may have introduced the regression`,
77991
+ title: top ? `Recent change${citation.commitClause} to ${top.name}${citation.symbolClause} in ${input.since}..HEAD may have introduced the regression` : `Recent change${citation.commitClause}${citation.symbolClause} in ${input.since}..HEAD may have introduced the regression`,
77946
77992
  category: "deployment-regression",
77947
77993
  sourceEvidenceIds: [changeEvId, ...seedEv ? [seedEv.id] : []],
77948
77994
  baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
@@ -78065,8 +78111,20 @@ async function investigate(input, deps) {
78065
78111
  const banner2 = degradedNoSource ? "Runtime-only (source intelligence unavailable). " : "";
78066
78112
  const scope = top ? `resolved to ${label} (${area})` : `over runtime evidence (${area})`;
78067
78113
  const seedDisclaimer = seedIsLowConfidence && top ? `\u26A0 No symbol closely matched "${hint}" \u2014 "${top.name}" is a low-confidence closest match (semantic). Refine with an exact symbol or error code to target precisely. ` : "";
78068
- const summary = headlineCause ? `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. Top suspected cause: ${headlineCause.title}.` : `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. No dominant suspected cause emerged from the available ${degradedNoSource ? "runtime" : "structural"} evidence.`;
78114
+ let summary = headlineCause ? `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. Top suspected cause: ${headlineCause.title}.` : `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. No dominant suspected cause emerged from the available ${degradedNoSource ? "runtime" : "structural"} evidence.`;
78115
+ const hasIncidentSignal = directLogEvIds.length > 0 || latencyMetricEvIds.length > 0 || queueMetricEvIds.length > 0 || dataAnomalyEvIds.length > 0 || queueFailureSignals.length > 0 || findings2.some((f) => f.kind === "anomaly") || headlineCause !== void 0;
78116
+ const explanatoryHint = looksExplanatory(hint) && !hasIncidentSignal;
78117
+ if (explanatoryHint) {
78118
+ const explainTarget = top?.name ?? input.service ?? hint;
78119
+ summary = `${seedDisclaimer}${banner2}"${hint}" reads as a "how does it work" question and no incident signal was found (no error signatures, anomalies, or failing queues). For a focused answer, run \`horus explain ${explainTarget}\`. ${top ? `Investigated ${label} (${area}); the` : "The"} incident sections below are empty by design \u2014 this is not a fault hunt.`;
78120
+ }
78069
78121
  const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
78122
+ if (explanatoryHint) {
78123
+ const explainTarget = top?.name ?? input.service ?? hint;
78124
+ nextActions.unshift(
78125
+ `This looks like a behavioral question \u2014 run \`horus explain ${explainTarget}\` for a focused walkthrough of how it works.`
78126
+ );
78127
+ }
78070
78128
  if (degradedNoSource) {
78071
78129
  nextActions.unshift(
78072
78130
  "Source intelligence was unavailable \u2014 run `horus index` to enable code-aware analysis, then re-run for a full investigation."
@@ -78124,7 +78182,7 @@ async function investigate(input, deps) {
78124
78182
  report.confidence = Math.min(report.confidence, gapAnalysis.confidenceCeiling);
78125
78183
  if (seedIsLowConfidence) report.confidence = Math.min(report.confidence, 0.45);
78126
78184
  const topCauseScore = report.suspectedCauses[0]?.finalScore ?? 0;
78127
- if (topCauseScore < 0.2) report.confidence = Math.min(report.confidence, 0.6);
78185
+ report.confidence = Math.min(report.confidence, confidenceCeilingForCause(topCauseScore));
78128
78186
  report.nextActions.push(...gapNextActions(gapAnalysis.gaps));
78129
78187
  report.sourceStatus = buildRuntimeSourceStatus(evidence2, connectorFlags);
78130
78188
  const persistedId = await persist(db, input, report);
@@ -81201,24 +81259,98 @@ var PROCESSOR_RE = /@Processor\(\s*['"]([^'"]+)['"][^)]*\)\s*(?:@\w+[^\n]*\s*)*e
81201
81259
  var CONST_DECL_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
81202
81260
  var NEW_BULL_RE = /(?:(\w+)\s*[:=]\s*)?new\s+(Queue|Worker)\s*(?:<[^>]*>)?\s*\(\s*(['"][^'"]+['"]|[A-Za-z_$][\w$.]*)/g;
81203
81261
  var QUEUE_NAME_CONST_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*(?:QUEUE|Queue)[\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
81262
+ var ENUM_DECL_RE = /(?:export\s+)?(?:const\s+)?enum\s+([A-Za-z_$][\w$]*)\s*\{([^}]*)\}/g;
81263
+ var ENUM_MEMBER_RE = /([A-Za-z_$][\w$]*)\s*(?:=\s*(['"])([^'"]*)\2)?/g;
81264
+ var OBJECT_VALUES_LOOP_RE = /for\s*\(\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s+of\s+Object\.values\(\s*([A-Za-z_$][\w$]*)\s*\)/g;
81265
+ var DISPATCH_ENTRY_RE = /\[\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s*\]\s*:\s*([^,}]+)/g;
81204
81266
  function baseName(filePath) {
81205
81267
  const parts = filePath.split("/");
81206
81268
  const last = parts[parts.length - 1] ?? filePath;
81207
81269
  return last.replace(/\.[jt]sx?$/, "");
81208
81270
  }
81209
- function resolveArg(raw, constMap) {
81271
+ function buildEnumMap(contents) {
81272
+ const enums = /* @__PURE__ */ new Map();
81273
+ for (const content of contents) {
81274
+ for (const decl of content.matchAll(ENUM_DECL_RE)) {
81275
+ const enumName = decl[1];
81276
+ const body = decl[2] ?? "";
81277
+ if (!enumName || enums.has(enumName)) continue;
81278
+ const members = [];
81279
+ for (const m of body.matchAll(ENUM_MEMBER_RE)) {
81280
+ const member = m[1];
81281
+ if (!member) continue;
81282
+ const value = m[3] !== void 0 ? m[3] : member;
81283
+ members.push({ member, value });
81284
+ }
81285
+ if (members.length) enums.set(enumName, members);
81286
+ }
81287
+ }
81288
+ return enums;
81289
+ }
81290
+ function resolveEnumMember(ref, enums) {
81291
+ const dot = ref.indexOf(".");
81292
+ if (dot < 0) return null;
81293
+ const enumName = ref.slice(0, dot);
81294
+ const member = ref.slice(dot + 1);
81295
+ const members = enums.get(enumName);
81296
+ if (!members) return null;
81297
+ return members.find((e) => e.member === member)?.value ?? null;
81298
+ }
81299
+ function resolveArg(raw, constMap, enums, loopVarEnums) {
81210
81300
  const arg = raw.trim();
81211
81301
  if (arg.startsWith('"') || arg.startsWith("'")) {
81212
- return arg.slice(1, -1);
81302
+ return [arg.slice(1, -1)];
81303
+ }
81304
+ if (arg.includes(".")) {
81305
+ const value = resolveEnumMember(arg, enums);
81306
+ return value !== null ? [value] : [];
81307
+ }
81308
+ const loopEnum = loopVarEnums.get(arg);
81309
+ if (loopEnum) {
81310
+ const members = enums.get(loopEnum);
81311
+ if (members) return members.map((e) => e.value);
81312
+ }
81313
+ const constValue = constMap.get(arg);
81314
+ return constValue !== void 0 ? [constValue] : [];
81315
+ }
81316
+ function loopVarEnumsFor(content) {
81317
+ const map2 = /* @__PURE__ */ new Map();
81318
+ for (const m of content.matchAll(OBJECT_VALUES_LOOP_RE)) {
81319
+ const loopVar = m[1];
81320
+ const enumName = m[2];
81321
+ if (loopVar && enumName) map2.set(loopVar, enumName);
81322
+ }
81323
+ return map2;
81324
+ }
81325
+ function extractDispatchHandlers(content, enums) {
81326
+ const out = [];
81327
+ for (const m of content.matchAll(DISPATCH_ENTRY_RE)) {
81328
+ const enumName = m[1];
81329
+ const member = m[2];
81330
+ const rhs = (m[3] ?? "").trim();
81331
+ if (!enumName || !member) continue;
81332
+ const value = resolveEnumMember(`${enumName}.${member}`, enums);
81333
+ if (value === null) continue;
81334
+ const handler = handlerSymbolFromExpr(rhs);
81335
+ if (handler) out.push({ value, handler });
81213
81336
  }
81214
- return constMap.get(arg) ?? null;
81337
+ return out;
81338
+ }
81339
+ function handlerSymbolFromExpr(expr) {
81340
+ const call = expr.match(/([A-Za-z_$][\w$]*)\s*\(/);
81341
+ if (call?.[1]) return call[1];
81342
+ const member = expr.match(/(?:[A-Za-z_$][\w$]*\.)+([A-Za-z_$][\w$]*)/);
81343
+ if (member?.[1]) return member[1];
81344
+ const ident = expr.match(/^([A-Za-z_$][\w$]*)$/);
81345
+ if (ident?.[1]) return ident[1];
81346
+ return null;
81215
81347
  }
81216
81348
  function extractQueueGraph(input) {
81217
- const constMap = /* @__PURE__ */ new Map();
81218
81349
  const allContent = [
81219
81350
  ...input.producerClasses.map((p) => p.content),
81220
81351
  ...input.workerFiles.map((w) => w.content)
81221
81352
  ];
81353
+ const constMap = /* @__PURE__ */ new Map();
81222
81354
  for (const content of allContent) {
81223
81355
  for (const m of content.matchAll(CONST_DECL_RE)) {
81224
81356
  const ident = m[1];
@@ -81226,17 +81358,30 @@ function extractQueueGraph(input) {
81226
81358
  if (ident && value && !constMap.has(ident)) constMap.set(ident, value);
81227
81359
  }
81228
81360
  }
81361
+ const enums = buildEnumMap(allContent);
81362
+ const dispatchHandlers = [];
81363
+ for (const unit of [...input.producerClasses, ...input.workerFiles]) {
81364
+ const file = "filePath" in unit ? unit.filePath : "";
81365
+ for (const h of extractDispatchHandlers(unit.content, enums)) {
81366
+ dispatchHandlers.push({ value: h.value, handler: h.handler, file });
81367
+ }
81368
+ }
81229
81369
  const producers = [];
81230
81370
  for (const pc47 of input.producerClasses) {
81371
+ const loopVarEnums = loopVarEnumsFor(pc47.content);
81231
81372
  for (const m of pc47.content.matchAll(INJECT_QUEUE_RE)) {
81232
81373
  const queue = m[1] ?? "";
81233
81374
  if (queue) producers.push({ queue, symbol: pc47.name, file: pc47.filePath });
81234
81375
  }
81235
81376
  for (const m of pc47.content.matchAll(NEW_BULL_RE)) {
81236
81377
  if (m[2] !== "Queue") continue;
81237
- const queue = resolveArg(m[3] ?? "", constMap);
81238
- if (queue) {
81239
- producers.push({ queue, symbol: (m[1] ?? pc47.name) || baseName(pc47.filePath), file: pc47.filePath });
81378
+ const queues2 = resolveArg(m[3] ?? "", constMap, enums, loopVarEnums);
81379
+ for (const queue of queues2) {
81380
+ producers.push({
81381
+ queue,
81382
+ symbol: (m[1] ?? pc47.name) || baseName(pc47.filePath),
81383
+ file: pc47.filePath
81384
+ });
81240
81385
  }
81241
81386
  }
81242
81387
  for (const m of pc47.content.matchAll(QUEUE_NAME_CONST_RE)) {
@@ -81247,6 +81392,7 @@ function extractQueueGraph(input) {
81247
81392
  }
81248
81393
  const workers = [];
81249
81394
  for (const wf of input.workerFiles) {
81395
+ const loopVarEnums = loopVarEnumsFor(wf.content);
81250
81396
  for (const m of wf.content.matchAll(PROCESSOR_RE)) {
81251
81397
  const queue = m[1] ?? "";
81252
81398
  const className = m[2] ?? "";
@@ -81254,17 +81400,24 @@ function extractQueueGraph(input) {
81254
81400
  }
81255
81401
  for (const m of wf.content.matchAll(NEW_BULL_RE)) {
81256
81402
  if (m[2] !== "Worker") continue;
81257
- const queue = resolveArg(m[3] ?? "", constMap);
81258
- if (queue) {
81259
- workers.push({ queue, symbol: m[1] ?? baseName(wf.filePath), file: wf.filePath });
81403
+ const queues2 = resolveArg(m[3] ?? "", constMap, enums, loopVarEnums);
81404
+ for (const queue of queues2) {
81405
+ workers.push({
81406
+ queue,
81407
+ symbol: m[1] ?? baseName(wf.filePath),
81408
+ file: wf.filePath
81409
+ });
81260
81410
  }
81261
81411
  }
81262
81412
  }
81413
+ for (const h of dispatchHandlers) {
81414
+ workers.push({ queue: h.value, symbol: h.handler, file: h.file });
81415
+ }
81263
81416
  const queues = [...new Set([...producers, ...workers].map((r) => r.queue))].sort();
81264
81417
  const edges = [];
81265
81418
  for (const q of queues) {
81266
- const P = dedupeByFile(producers.filter((p) => p.queue === q));
81267
- const W = dedupeByFile(workers.filter((w) => w.queue === q));
81419
+ const P = dedupeBySymbolFile(producers.filter((p) => p.queue === q));
81420
+ const W = dedupeBySymbolFile(workers.filter((w) => w.queue === q));
81268
81421
  if (P.length && W.length) {
81269
81422
  for (const p of P) {
81270
81423
  for (const w of W) {
@@ -81301,12 +81454,13 @@ function extractQueueGraph(input) {
81301
81454
  }
81302
81455
  return { queues, producers, workers, edges };
81303
81456
  }
81304
- function dedupeByFile(rows) {
81457
+ function dedupeBySymbolFile(rows) {
81305
81458
  const seen = /* @__PURE__ */ new Set();
81306
81459
  const out = [];
81307
81460
  for (const r of rows) {
81308
- if (seen.has(r.file)) continue;
81309
- seen.add(r.file);
81461
+ const key = `${r.symbol}\0${r.file}`;
81462
+ if (seen.has(key)) continue;
81463
+ seen.add(key);
81310
81464
  out.push(r);
81311
81465
  }
81312
81466
  return out;
@@ -81317,7 +81471,7 @@ init_cjs_shims();
81317
81471
  async function stitch(client, db, opts = {}) {
81318
81472
  const producerRows = (await client.cypher(
81319
81473
  // "new Queue" (no paren) so generic-typed `new Queue<T>(...)` also matches.
81320
- 'MATCH (n) WHERE n.content CONTAINS "@InjectQueue(" OR n.content CONTAINS "new Queue" OR n.content CONTAINS "QUEUE_NAME" OR n.content CONTAINS "QueueName" RETURN n.name, n.file_path, n.content'
81474
+ 'MATCH (n) WHERE n.content CONTAINS "@InjectQueue(" OR n.content CONTAINS "new Queue" OR n.content CONTAINS "QUEUE_NAME" OR n.content CONTAINS "QueueName" OR n.content CONTAINS "enum " OR n.content CONTAINS "Object.values(" OR n.content CONTAINS "]: (" RETURN n.name, n.file_path, n.content'
81321
81475
  )).rows;
81322
81476
  const producerClasses = producerRows.map((r) => ({
81323
81477
  name: String(r[0] ?? ""),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {