@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.
- package/dist/index.cjs +174 -20
- 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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
81238
|
-
|
|
81239
|
-
producers.push({
|
|
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
|
|
81258
|
-
|
|
81259
|
-
workers.push({
|
|
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 =
|
|
81267
|
-
const W =
|
|
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
|
|
81457
|
+
function dedupeBySymbolFile(rows) {
|
|
81305
81458
|
const seen = /* @__PURE__ */ new Set();
|
|
81306
81459
|
const out = [];
|
|
81307
81460
|
for (const r of rows) {
|
|
81308
|
-
|
|
81309
|
-
seen.
|
|
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] ?? ""),
|