@merittdev/horus 0.3.1 → 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 +402 -54
- 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.
|
|
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
|
|
@@ -63804,6 +63804,11 @@ init_cjs_shims();
|
|
|
63804
63804
|
function classifyAnomaly(kind, cmp, opts) {
|
|
63805
63805
|
const spike = opts?.spike ?? 1.5;
|
|
63806
63806
|
const drop = opts?.drop ?? 0.67;
|
|
63807
|
+
const NEAR_ZERO = 1e-9;
|
|
63808
|
+
if (cmp.baselineAvg <= NEAR_ZERO && cmp.currentAvg > 0) {
|
|
63809
|
+
const floor2 = kind === "queue" ? 1 : kind === "latency" ? 0.05 : kind === "error-rate" ? 5e-3 : 0;
|
|
63810
|
+
if (cmp.currentAvg < floor2) return "none";
|
|
63811
|
+
}
|
|
63807
63812
|
if (kind === "latency" && cmp.ratio >= spike) return "latency-spike";
|
|
63808
63813
|
if (kind === "queue" && (cmp.ratio >= spike || cmp.currentAvg > cmp.baselineAvg) && cmp.delta > 0) {
|
|
63809
63814
|
return "queue-growth";
|
|
@@ -63947,10 +63952,10 @@ var GrafanaMetricsProvider = class {
|
|
|
63947
63952
|
const queryPanels = async (panels) => {
|
|
63948
63953
|
const results = [];
|
|
63949
63954
|
for (const panel of panels) {
|
|
63950
|
-
signal?.
|
|
63955
|
+
if (signal?.aborted) return results;
|
|
63951
63956
|
const panelMatchSrc = hint !== void 0 && hint !== "" ? findMatchSource(panel, hint) : null;
|
|
63952
63957
|
for (const rawExpr of panel.exprs) {
|
|
63953
|
-
signal?.
|
|
63958
|
+
if (signal?.aborted) return results;
|
|
63954
63959
|
const expr = sanitizeExpr(rawExpr);
|
|
63955
63960
|
if (expr === null) continue;
|
|
63956
63961
|
try {
|
|
@@ -63976,7 +63981,7 @@ var GrafanaMetricsProvider = class {
|
|
|
63976
63981
|
).map((f) => ({ ...f, matchSource: panelMatchSrc }));
|
|
63977
63982
|
results.push(...findings2);
|
|
63978
63983
|
} catch (err) {
|
|
63979
|
-
if (signal?.aborted)
|
|
63984
|
+
if (signal?.aborted) return results;
|
|
63980
63985
|
continue;
|
|
63981
63986
|
}
|
|
63982
63987
|
}
|
|
@@ -76655,6 +76660,16 @@ function correlate(evidence2) {
|
|
|
76655
76660
|
init_cjs_shims();
|
|
76656
76661
|
var PREFER = /resolver|controller|\bservice\b|route|router|handler|gateway|processor|worker|consumer|repository|usecase|use-case|endpoint/i;
|
|
76657
76662
|
var DEMOTE = /\butil|helper|\bscript|backfill|legacy|migration|seeder|fixture|\bmock\b|\btest\b|\bspec\b/i;
|
|
76663
|
+
var TYPE_SUFFIX = /(Result|Input|Response|Payload|Args|Dto|Props|Edge|Connection)$/;
|
|
76664
|
+
var INTERFACE_PREFIX = /^I(?=[A-Z][a-z])/;
|
|
76665
|
+
var TYPE_LIKE_NAME = new RegExp(`(?:${TYPE_SUFFIX.source})|(?:${INTERFACE_PREFIX.source})`);
|
|
76666
|
+
function isTypeLikeName(name) {
|
|
76667
|
+
return TYPE_LIKE_NAME.test(name);
|
|
76668
|
+
}
|
|
76669
|
+
function executableBaseName(name) {
|
|
76670
|
+
const stripped = name.replace(TYPE_SUFFIX, "").replace(INTERFACE_PREFIX, "");
|
|
76671
|
+
return stripped !== name && stripped.length >= 4 ? stripped : null;
|
|
76672
|
+
}
|
|
76658
76673
|
function seedRole(s) {
|
|
76659
76674
|
const hay = `${s.name} ${s.filePath}`;
|
|
76660
76675
|
if (/\.resolver\.|resolver/i.test(hay)) return "resolver";
|
|
@@ -76667,11 +76682,16 @@ function seedRole(s) {
|
|
|
76667
76682
|
if (/util|helper/i.test(hay)) return "util";
|
|
76668
76683
|
return "code";
|
|
76669
76684
|
}
|
|
76670
|
-
function scoreSeed(s, index2, hintTokens) {
|
|
76685
|
+
function scoreSeed(s, index2, hintTokens, changedFiles) {
|
|
76671
76686
|
const hay = `${s.name} ${s.filePath}`.toLowerCase();
|
|
76672
76687
|
let score = 0;
|
|
76673
76688
|
if (PREFER.test(hay)) score += 3;
|
|
76674
76689
|
if (DEMOTE.test(hay)) score -= 3;
|
|
76690
|
+
if (isTypeLikeName(s.name)) score -= 4;
|
|
76691
|
+
if (s.startLine !== void 0 && s.endLine !== void 0 && s.endLine - s.startLine <= 3) {
|
|
76692
|
+
score -= 3;
|
|
76693
|
+
}
|
|
76694
|
+
if (changedFiles !== void 0 && changedFiles.has(s.filePath)) score += 5;
|
|
76675
76695
|
if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s.filePath)) score += 2;
|
|
76676
76696
|
if (/(^|\/)scripts?\//i.test(s.filePath)) score -= 2;
|
|
76677
76697
|
score += Math.max(0, 5 - index2) * 0.1;
|
|
@@ -76687,8 +76707,13 @@ function scoreSeed(s, index2, hintTokens) {
|
|
|
76687
76707
|
}
|
|
76688
76708
|
return score;
|
|
76689
76709
|
}
|
|
76690
|
-
function rankSeeds(seeds, hintTokens) {
|
|
76691
|
-
return seeds.map((symbol, i) => ({
|
|
76710
|
+
function rankSeeds(seeds, hintTokens, changedFiles) {
|
|
76711
|
+
return seeds.map((symbol, i) => ({
|
|
76712
|
+
symbol,
|
|
76713
|
+
score: scoreSeed(symbol, i, hintTokens, changedFiles),
|
|
76714
|
+
role: seedRole(symbol),
|
|
76715
|
+
i
|
|
76716
|
+
})).sort((a, b2) => b2.score === a.score ? a.i - b2.i : b2.score - a.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
|
|
76692
76717
|
}
|
|
76693
76718
|
|
|
76694
76719
|
// ../../packages/engine/src/normalize.ts
|
|
@@ -76984,6 +77009,17 @@ function clamp015(n) {
|
|
|
76984
77009
|
if (n > 1) return 1;
|
|
76985
77010
|
return n;
|
|
76986
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
|
+
}
|
|
76987
77023
|
function relevanceForKind(kind) {
|
|
76988
77024
|
switch (kind) {
|
|
76989
77025
|
case "symbol":
|
|
@@ -77022,13 +77058,36 @@ function logWindowFrom(since) {
|
|
|
77022
77058
|
return new Date(now - 7 * 864e5).toISOString();
|
|
77023
77059
|
}
|
|
77024
77060
|
function classifyLogRelevance(sigKey, sigServices, seedTerms, inputService) {
|
|
77025
|
-
|
|
77026
|
-
|
|
77061
|
+
const GENERIC_TERMS = /* @__PURE__ */ new Set([
|
|
77062
|
+
"error",
|
|
77063
|
+
"errors",
|
|
77064
|
+
"err",
|
|
77065
|
+
"fail",
|
|
77066
|
+
"failed",
|
|
77067
|
+
"failing",
|
|
77068
|
+
"failure",
|
|
77069
|
+
"exception",
|
|
77070
|
+
"exceptions",
|
|
77071
|
+
"crash",
|
|
77072
|
+
"crashing",
|
|
77073
|
+
"issue",
|
|
77074
|
+
"problem",
|
|
77075
|
+
"slow",
|
|
77076
|
+
"timeout",
|
|
77077
|
+
"broken",
|
|
77078
|
+
"down",
|
|
77079
|
+
"production",
|
|
77080
|
+
"prod",
|
|
77081
|
+
"staging"
|
|
77082
|
+
]);
|
|
77083
|
+
const domainTerms = seedTerms.filter((t) => !GENERIC_TERMS.has(t));
|
|
77084
|
+
if (domainTerms.length === 0) {
|
|
77085
|
+
return { relevanceClass: "direct", relevanceReason: "no specific seed context \u2014 included by default" };
|
|
77027
77086
|
}
|
|
77028
77087
|
const keyTokens = tokenize(sigKey);
|
|
77029
77088
|
const serviceTokens = sigServices.flatMap((s) => tokenize(s));
|
|
77030
77089
|
const combined = [...keyTokens, ...serviceTokens];
|
|
77031
|
-
const matchingTerms =
|
|
77090
|
+
const matchingTerms = domainTerms.filter(
|
|
77032
77091
|
(t) => combined.some((c) => c.includes(t) || t.includes(c))
|
|
77033
77092
|
);
|
|
77034
77093
|
if (matchingTerms.length > 0) {
|
|
@@ -77108,6 +77167,40 @@ function looksDiffable(since) {
|
|
|
77108
77167
|
if (/^\d+[smhd]$/i.test(s)) return false;
|
|
77109
77168
|
return /^[A-Za-z0-9._/~^-]+$/.test(s);
|
|
77110
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
|
+
}
|
|
77111
77204
|
async function investigate(input, deps) {
|
|
77112
77205
|
const { code, db } = deps;
|
|
77113
77206
|
const hint = input.hint.trim();
|
|
@@ -77129,11 +77222,31 @@ async function investigate(input, deps) {
|
|
|
77129
77222
|
return ev;
|
|
77130
77223
|
}
|
|
77131
77224
|
const degradedNoSource = !code;
|
|
77225
|
+
let recentChanges;
|
|
77226
|
+
if (deps.repoPath && input.since) {
|
|
77227
|
+
try {
|
|
77228
|
+
recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
|
|
77229
|
+
} catch {
|
|
77230
|
+
}
|
|
77231
|
+
}
|
|
77232
|
+
const changedFilePaths = recentChanges && recentChanges.changedFiles.length > 0 ? new Set(recentChanges.changedFiles) : void 0;
|
|
77132
77233
|
const rawSeeds = code ? await code.searchSymbols(hint, 5) : [];
|
|
77133
77234
|
const hintTokens = [...new Set(tokenize(hint))];
|
|
77134
|
-
const ranked = rankSeeds(rawSeeds, hintTokens);
|
|
77235
|
+
const ranked = rankSeeds(rawSeeds, hintTokens, changedFilePaths);
|
|
77135
77236
|
let seeds = ranked.map((r) => r.symbol);
|
|
77136
77237
|
let top = seeds[0];
|
|
77238
|
+
if (top && code && isTypeLikeName(top.name)) {
|
|
77239
|
+
const base2 = executableBaseName(top.name);
|
|
77240
|
+
if (base2) {
|
|
77241
|
+
const altTop = rankSeeds(await code.searchSymbols(base2, 5), hintTokens).find(
|
|
77242
|
+
(r) => !isTypeLikeName(r.symbol.name)
|
|
77243
|
+
)?.symbol;
|
|
77244
|
+
if (altTop && altTop.id !== top.id) {
|
|
77245
|
+
seeds = [altTop, ...seeds.filter((s) => s.id !== altTop.id)];
|
|
77246
|
+
top = altTop;
|
|
77247
|
+
}
|
|
77248
|
+
}
|
|
77249
|
+
}
|
|
77137
77250
|
let logReseed = null;
|
|
77138
77251
|
if (!top && !degradedNoSource) {
|
|
77139
77252
|
logReseed = await reseedFromLogs(hint, input, deps);
|
|
@@ -77142,6 +77255,50 @@ async function investigate(input, deps) {
|
|
|
77142
77255
|
top = seeds[0];
|
|
77143
77256
|
}
|
|
77144
77257
|
}
|
|
77258
|
+
const seedHay = top ? `${top.name} ${top.filePath}`.toLowerCase() : "";
|
|
77259
|
+
const GENERIC_HINT_TOKENS = /* @__PURE__ */ new Set([
|
|
77260
|
+
"service",
|
|
77261
|
+
"services",
|
|
77262
|
+
"controller",
|
|
77263
|
+
"controllers",
|
|
77264
|
+
"resolver",
|
|
77265
|
+
"resolvers",
|
|
77266
|
+
"worker",
|
|
77267
|
+
"workers",
|
|
77268
|
+
"repository",
|
|
77269
|
+
"provider",
|
|
77270
|
+
"module",
|
|
77271
|
+
"manager",
|
|
77272
|
+
"util",
|
|
77273
|
+
"utils",
|
|
77274
|
+
"helper",
|
|
77275
|
+
"handler",
|
|
77276
|
+
"error",
|
|
77277
|
+
"errors",
|
|
77278
|
+
"exception",
|
|
77279
|
+
"exceptions",
|
|
77280
|
+
"fail",
|
|
77281
|
+
"failed",
|
|
77282
|
+
"failing",
|
|
77283
|
+
"failure",
|
|
77284
|
+
"throw",
|
|
77285
|
+
"throwing",
|
|
77286
|
+
"thrown",
|
|
77287
|
+
"fatal",
|
|
77288
|
+
"crash",
|
|
77289
|
+
"crashing",
|
|
77290
|
+
"timeout",
|
|
77291
|
+
"production",
|
|
77292
|
+
"prod",
|
|
77293
|
+
"staging",
|
|
77294
|
+
"application",
|
|
77295
|
+
"issue",
|
|
77296
|
+
"problem",
|
|
77297
|
+
"broken",
|
|
77298
|
+
"slow"
|
|
77299
|
+
]);
|
|
77300
|
+
const meaningfulHintTokens = hintTokens.filter((t) => !GENERIC_HINT_TOKENS.has(t));
|
|
77301
|
+
const seedIsLowConfidence = top !== void 0 && logReseed === null && meaningfulHintTokens.length > 0 && !meaningfulHintTokens.some((t) => seedHay.includes(t));
|
|
77145
77302
|
if (!top && !degradedNoSource) {
|
|
77146
77303
|
const report2 = {
|
|
77147
77304
|
id: globalThis.crypto.randomUUID(),
|
|
@@ -77166,13 +77323,6 @@ async function investigate(input, deps) {
|
|
|
77166
77323
|
if (persistedId2) report2.id = persistedId2;
|
|
77167
77324
|
return report2;
|
|
77168
77325
|
}
|
|
77169
|
-
let recentChanges;
|
|
77170
|
-
if (deps.repoPath && input.since) {
|
|
77171
|
-
try {
|
|
77172
|
-
recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
|
|
77173
|
-
} catch {
|
|
77174
|
-
}
|
|
77175
|
-
}
|
|
77176
77326
|
let label = input.service ?? hint;
|
|
77177
77327
|
let seedLoc = "";
|
|
77178
77328
|
let ctx = null;
|
|
@@ -77284,6 +77434,7 @@ async function investigate(input, deps) {
|
|
|
77284
77434
|
const logEvIds = [];
|
|
77285
77435
|
const directLogEvIds = [];
|
|
77286
77436
|
const ambientLogEvIds = [];
|
|
77437
|
+
const directSignatures = [];
|
|
77287
77438
|
const entityEvIds = [];
|
|
77288
77439
|
const entitySummaries = [];
|
|
77289
77440
|
let logsCollected = false;
|
|
@@ -77319,11 +77470,12 @@ async function investigate(input, deps) {
|
|
|
77319
77470
|
}
|
|
77320
77471
|
const svc = s.services.length > 0 ? ` \xB7 ${s.services.slice(0, 3).join(", ")}` : "";
|
|
77321
77472
|
const tagStr = tags.length > 0 ? ` [${tags.join(", ")}]` : "";
|
|
77473
|
+
const msgStr = s.sampleMessage ? ` \u2014 "${s.sampleMessage.replace(/\s+/g, " ").trim().slice(0, 90)}"` : "";
|
|
77322
77474
|
const ev = mkEv(
|
|
77323
77475
|
"log",
|
|
77324
|
-
`Error ${s.key}: ${s.count}x (first ${shortTs(s.firstSeen)}, last ${shortTs(s.lastSeen)})${svc}${tagStr}`.slice(
|
|
77476
|
+
`Error ${s.key}: ${s.count}x (first ${shortTs(s.firstSeen)}, last ${shortTs(s.lastSeen)})${svc}${tagStr}${msgStr}`.slice(
|
|
77325
77477
|
0,
|
|
77326
|
-
|
|
77478
|
+
220
|
|
77327
77479
|
),
|
|
77328
77480
|
{
|
|
77329
77481
|
signature: s.key,
|
|
@@ -77346,8 +77498,10 @@ async function investigate(input, deps) {
|
|
|
77346
77498
|
if (s.isNew) ev.isNew = s.isNew;
|
|
77347
77499
|
if (typeof s.ratio === "number" && Number.isFinite(s.ratio)) ev.ratio = s.ratio;
|
|
77348
77500
|
logEvIds.push(ev.id);
|
|
77349
|
-
if (relevanceClass === "direct")
|
|
77350
|
-
|
|
77501
|
+
if (relevanceClass === "direct") {
|
|
77502
|
+
directLogEvIds.push(ev.id);
|
|
77503
|
+
directSignatures.push({ key: s.key, count: s.count, message: s.sampleMessage ?? "" });
|
|
77504
|
+
} else ambientLogEvIds.push(ev.id);
|
|
77351
77505
|
}
|
|
77352
77506
|
if (typeof deps.logs.aggregateErrors === "function") {
|
|
77353
77507
|
const sigForEntities = analysis.signatures.find(
|
|
@@ -77400,6 +77554,8 @@ async function investigate(input, deps) {
|
|
|
77400
77554
|
let stateAnalysis = null;
|
|
77401
77555
|
const stateEvIds = [];
|
|
77402
77556
|
const stateCollections = /* @__PURE__ */ new Set();
|
|
77557
|
+
const dataAnomalyEvIds = [];
|
|
77558
|
+
const dataAnomalyCollections = [];
|
|
77403
77559
|
if (deps.mongo) {
|
|
77404
77560
|
try {
|
|
77405
77561
|
stateAnalysis = await deps.mongo.analyzeState();
|
|
@@ -77408,6 +77564,10 @@ async function investigate(input, deps) {
|
|
|
77408
77564
|
const ev = mkEv("state", s.title, s.payload, {}, s.timestamp, s.relevance);
|
|
77409
77565
|
stateEvIds.push(ev.id);
|
|
77410
77566
|
stateCollections.add(s.collection);
|
|
77567
|
+
if (s.kind === "anomaly") {
|
|
77568
|
+
dataAnomalyEvIds.push(ev.id);
|
|
77569
|
+
dataAnomalyCollections.push(s.collection);
|
|
77570
|
+
}
|
|
77411
77571
|
}
|
|
77412
77572
|
} catch {
|
|
77413
77573
|
stateAnalysis = null;
|
|
@@ -77434,6 +77594,7 @@ async function investigate(input, deps) {
|
|
|
77434
77594
|
const queueRuntimeEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
77435
77595
|
const queueBacklogEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
77436
77596
|
const queueStarvationEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
77597
|
+
const queueFailureSignals = [];
|
|
77437
77598
|
if (deps.queue) {
|
|
77438
77599
|
try {
|
|
77439
77600
|
const staticNames = [...new Set(queueHits.map((e) => e.queueName))];
|
|
@@ -77457,6 +77618,18 @@ async function investigate(input, deps) {
|
|
|
77457
77618
|
st.push(ev.id);
|
|
77458
77619
|
queueStarvationEvIdsByQueue.set(s.queueName, st);
|
|
77459
77620
|
}
|
|
77621
|
+
if (s.kind === "failed-breakdown") {
|
|
77622
|
+
const p = s.payload;
|
|
77623
|
+
if (typeof p.topReason === "string") {
|
|
77624
|
+
queueFailureSignals.push({
|
|
77625
|
+
reason: p.topReason,
|
|
77626
|
+
queueName: s.queueName,
|
|
77627
|
+
evId: ev.id,
|
|
77628
|
+
count: typeof p.topCount === "number" ? p.topCount : 0,
|
|
77629
|
+
stale: p.topStale === true
|
|
77630
|
+
});
|
|
77631
|
+
}
|
|
77632
|
+
}
|
|
77460
77633
|
}
|
|
77461
77634
|
} catch {
|
|
77462
77635
|
queueRuntimeState = null;
|
|
@@ -77507,8 +77680,14 @@ async function investigate(input, deps) {
|
|
|
77507
77680
|
const panelLower = f.panelTitle.toLowerCase();
|
|
77508
77681
|
const labelVals = Object.values(f.labels).map((v) => v.toLowerCase());
|
|
77509
77682
|
if (f.anomaly === "latency-spike" || f.anomaly === "error-rate-change") {
|
|
77510
|
-
const
|
|
77511
|
-
|
|
77683
|
+
const matchesService = serviceFilter.length > 0 && (panelLower.includes(serviceFilter) || labelVals.some((v) => v.includes(serviceFilter)));
|
|
77684
|
+
const hintIsPerf = /\b(latenc|slow|spike|perf|throughput|duration|p9[5-9]|timeout|response\s*time|degrad)/i.test(
|
|
77685
|
+
hint
|
|
77686
|
+
);
|
|
77687
|
+
const matchesHint = serviceFilter.length === 0 && hintIsPerf && hintTokens.some(
|
|
77688
|
+
(t) => t.length >= 4 && (panelLower.includes(t) || labelVals.some((v) => v.includes(t)))
|
|
77689
|
+
);
|
|
77690
|
+
if (matchesService || matchesHint) latencyMetricEvIds.push(ev.id);
|
|
77512
77691
|
} else if (f.anomaly === "queue-growth") {
|
|
77513
77692
|
const panelNorm = f.panelTitle.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
77514
77693
|
const labelNorms = Object.values(f.labels).map(
|
|
@@ -77806,19 +77985,21 @@ async function investigate(input, deps) {
|
|
|
77806
77985
|
const gitChangedFiles = recentChanges?.changedFiles ?? [];
|
|
77807
77986
|
const seedFile = top?.filePath;
|
|
77808
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);
|
|
77809
77989
|
causeInputs.push({
|
|
77810
77990
|
id: "cause:deployment-regression",
|
|
77811
|
-
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`,
|
|
77812
77992
|
category: "deployment-regression",
|
|
77813
77993
|
sourceEvidenceIds: [changeEvId, ...seedEv ? [seedEv.id] : []],
|
|
77814
77994
|
baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
|
|
77815
77995
|
metadata: { blastRadius }
|
|
77816
77996
|
});
|
|
77817
77997
|
}
|
|
77818
|
-
|
|
77998
|
+
const MIN_FANOUT_FOR_CAUSE = 3;
|
|
77999
|
+
if (top && impactEv && seedEv && impact && impact.affected >= MIN_FANOUT_FOR_CAUSE) {
|
|
77819
78000
|
causeInputs.push({
|
|
77820
78001
|
id: "cause:blast-radius",
|
|
77821
|
-
title: `${top.name}
|
|
78002
|
+
title: `${top.name} has wide code reach (${impact.affected} dependent symbols) and may propagate the fault`,
|
|
77822
78003
|
category: "blast-radius",
|
|
77823
78004
|
sourceEvidenceIds: [impactEv.id, seedEv.id],
|
|
77824
78005
|
baseScore: clamp015(0.15 + (queueHits.length > 0 ? 0.05 : 0)),
|
|
@@ -77828,16 +78009,62 @@ async function investigate(input, deps) {
|
|
|
77828
78009
|
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
|
|
77829
78010
|
const firstQueue = queueHits[0];
|
|
77830
78011
|
const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
|
|
77831
|
-
const
|
|
78012
|
+
const directTotal = directSignatures.reduce((sum, d) => sum + d.count, 0);
|
|
78013
|
+
const directTop = directSignatures[0];
|
|
77832
78014
|
causeInputs.push({
|
|
77833
78015
|
id: "cause:error-correlation",
|
|
77834
|
-
title: `Runtime errors (${
|
|
78016
|
+
title: `Runtime errors (${directTotal}${directTop ? `, top ${directTop.key}` : ""}) directly linked to the implicated queue path ${queueLabel}`,
|
|
77835
78017
|
category: "error-correlation",
|
|
77836
78018
|
sourceEvidenceIds: directLogEvIds.slice(0, 3),
|
|
77837
78019
|
baseScore: 0.3,
|
|
77838
78020
|
metadata: { blastRadius }
|
|
77839
78021
|
});
|
|
77840
78022
|
}
|
|
78023
|
+
const INFRA_RE = /\b(ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|ECONNRESET|getaddrinfo|socket hang up|connection (?:refused|reset|timed out)|network (?:error|timeout)|DNS)\b/i;
|
|
78024
|
+
const infraSig = directSignatures.find((d) => INFRA_RE.test(d.message));
|
|
78025
|
+
const queueHitNames = new Set(queueHits.map((e) => e.queueName.toLowerCase()));
|
|
78026
|
+
const infraQueue = queueFailureSignals.find((q) => {
|
|
78027
|
+
if (!INFRA_RE.test(q.reason)) return false;
|
|
78028
|
+
if (queueHitNames.has(q.queueName.toLowerCase())) return true;
|
|
78029
|
+
const qTokens = new Set(tokenize(q.queueName));
|
|
78030
|
+
return hintTokens.some((t) => t.length >= 3 && qTokens.has(t));
|
|
78031
|
+
});
|
|
78032
|
+
if (infraSig) {
|
|
78033
|
+
causeInputs.push({
|
|
78034
|
+
id: "cause:dependency-failure",
|
|
78035
|
+
title: `Dependency/network failure: ${infraSig.count}x ${infraSig.key} \u2014 "${infraSig.message.replace(/\s+/g, " ").trim().slice(0, 100)}"`,
|
|
78036
|
+
category: "infrastructure",
|
|
78037
|
+
sourceEvidenceIds: directLogEvIds.slice(0, 3),
|
|
78038
|
+
baseScore: 0.5,
|
|
78039
|
+
metadata: { blastRadius }
|
|
78040
|
+
});
|
|
78041
|
+
} else if (infraQueue) {
|
|
78042
|
+
causeInputs.push({
|
|
78043
|
+
id: "cause:dependency-failure",
|
|
78044
|
+
title: `Dependency/network failure on queue ${infraQueue.queueName}: ${infraQueue.count} failed job(s) \u2014 "${infraQueue.reason.replace(/\s+/g, " ").trim().slice(0, 100)}"${infraQueue.stale ? " (stale \u2014 reflects a past run)" : ""}`,
|
|
78045
|
+
category: "infrastructure",
|
|
78046
|
+
sourceEvidenceIds: [infraQueue.evId],
|
|
78047
|
+
// A stale-only queue failure is weaker than a live error, but still the best
|
|
78048
|
+
// explanation — and far better than a co-occurring stale-collection guess.
|
|
78049
|
+
baseScore: infraQueue.stale ? 0.34 : 0.5,
|
|
78050
|
+
metadata: { blastRadius }
|
|
78051
|
+
});
|
|
78052
|
+
}
|
|
78053
|
+
if (dataAnomalyEvIds.length > 0) {
|
|
78054
|
+
const collections = [...new Set(dataAnomalyCollections)];
|
|
78055
|
+
const example = collections[0];
|
|
78056
|
+
causeInputs.push({
|
|
78057
|
+
id: "cause:data-state-anomaly",
|
|
78058
|
+
// Co-occurring, NOT asserted as the root cause: the stale collection is selected by
|
|
78059
|
+
// hint relevance, not proven reachable from the seed's call graph, so claiming it is
|
|
78060
|
+
// the cause mislead on e.g. a Shopify-fulfillment error citing unrelated gaia sync logs.
|
|
78061
|
+
title: `Data-state anomaly (co-occurring): ${collections.length} collection(s) hold records in a failed/stale state${example ? ` (e.g. ${example})` : ""} \u2014 verify whether it relates to this failure`,
|
|
78062
|
+
category: "data-state-anomaly",
|
|
78063
|
+
sourceEvidenceIds: dataAnomalyEvIds.slice(0, 3),
|
|
78064
|
+
baseScore: 0.22,
|
|
78065
|
+
metadata: { blastRadius }
|
|
78066
|
+
});
|
|
78067
|
+
}
|
|
77841
78068
|
if (latencyMetricEvIds.length > 0) {
|
|
77842
78069
|
causeInputs.push({
|
|
77843
78070
|
id: "cause:metric-latency",
|
|
@@ -77876,20 +78103,34 @@ async function investigate(input, deps) {
|
|
|
77876
78103
|
evidence2,
|
|
77877
78104
|
ambientLogEvIds.length > 0 ? new Set(ambientLogEvIds) : void 0
|
|
77878
78105
|
);
|
|
77879
|
-
const seedResolved = seeds.length
|
|
78106
|
+
const seedResolved = seeds.length === 0 ? 0 : seedIsLowConfidence ? 0.3 : 1;
|
|
77880
78107
|
const confidence = clamp015(0.5 * evidenceConfidence + 0.5 * seedResolved);
|
|
77881
78108
|
const area = ctx?.community?.name ?? top?.filePath ?? (input.service ?? "runtime evidence");
|
|
77882
78109
|
const topCause = rankedCauses[0];
|
|
78110
|
+
const headlineCause = topCause && topCause.finalScore >= 0.2 ? topCause : void 0;
|
|
77883
78111
|
const banner2 = degradedNoSource ? "Runtime-only (source intelligence unavailable). " : "";
|
|
77884
78112
|
const scope = top ? `resolved to ${label} (${area})` : `over runtime evidence (${area})`;
|
|
77885
|
-
const
|
|
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. ` : "";
|
|
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
|
+
}
|
|
77886
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
|
+
}
|
|
77887
78128
|
if (degradedNoSource) {
|
|
77888
78129
|
nextActions.unshift(
|
|
77889
78130
|
"Source intelligence was unavailable \u2014 run `horus index` to enable code-aware analysis, then re-run for a full investigation."
|
|
77890
78131
|
);
|
|
77891
78132
|
}
|
|
77892
|
-
if (ownershipEstimate?.likelyMaintainer) {
|
|
78133
|
+
if (ownershipEstimate?.likelyMaintainer && !seedIsLowConfidence) {
|
|
77893
78134
|
nextActions.unshift(
|
|
77894
78135
|
`Route to likely maintainer: ${ownershipEstimate.likelyMaintainer} (${Math.round(ownershipEstimate.maintainerShare * 100)}% of commits to ${ownershipEstimate.file ?? top?.filePath ?? "the implicated file"})`
|
|
77895
78136
|
);
|
|
@@ -77939,6 +78180,9 @@ async function investigate(input, deps) {
|
|
|
77939
78180
|
const gapAnalysis = detectMissingEvidence(report, connectorFlags);
|
|
77940
78181
|
report.gapAnalysis = gapAnalysis;
|
|
77941
78182
|
report.confidence = Math.min(report.confidence, gapAnalysis.confidenceCeiling);
|
|
78183
|
+
if (seedIsLowConfidence) report.confidence = Math.min(report.confidence, 0.45);
|
|
78184
|
+
const topCauseScore = report.suspectedCauses[0]?.finalScore ?? 0;
|
|
78185
|
+
report.confidence = Math.min(report.confidence, confidenceCeilingForCause(topCauseScore));
|
|
77942
78186
|
report.nextActions.push(...gapNextActions(gapAnalysis.gaps));
|
|
77943
78187
|
report.sourceStatus = buildRuntimeSourceStatus(evidence2, connectorFlags);
|
|
77944
78188
|
const persistedId = await persist(db, input, report);
|
|
@@ -79289,14 +79533,17 @@ function refineInvestigation(r, directive) {
|
|
|
79289
79533
|
}
|
|
79290
79534
|
}
|
|
79291
79535
|
const recognizedList = ALL_TOPICS.join(", ");
|
|
79292
|
-
const
|
|
79536
|
+
const looksLikeQuestion = /\?|\b(where|which|what|how|why|when|does|do|is|are|can|should)\b/i.test(
|
|
79537
|
+
directive
|
|
79538
|
+
);
|
|
79539
|
+
const note2 = looksLikeQuestion ? `This question can't be answered from the saved evidence \u2014 \`horus ask\` reuses the report and does not run a new source/runtime lookup. For a fresh answer run: horus investigate "${directive.slice(0, 80)}". (To filter the saved evidence instead, use a topic: ${recognizedList}.)` : "No specific topic directive recognized. Recognized topics: " + recognizedList + '. Example usage: "focus on queue behavior", "ignore deployment changes".';
|
|
79293
79540
|
return {
|
|
79294
79541
|
directive,
|
|
79295
79542
|
mode: "none",
|
|
79296
79543
|
topics,
|
|
79297
79544
|
hypotheses: r.hypotheses,
|
|
79298
79545
|
suspectedCauses: r.suspectedCauses,
|
|
79299
|
-
evidence: r.evidence,
|
|
79546
|
+
evidence: looksLikeQuestion ? [] : r.evidence,
|
|
79300
79547
|
note: note2
|
|
79301
79548
|
};
|
|
79302
79549
|
}
|
|
@@ -81012,24 +81259,98 @@ var PROCESSOR_RE = /@Processor\(\s*['"]([^'"]+)['"][^)]*\)\s*(?:@\w+[^\n]*\s*)*e
|
|
|
81012
81259
|
var CONST_DECL_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
|
|
81013
81260
|
var NEW_BULL_RE = /(?:(\w+)\s*[:=]\s*)?new\s+(Queue|Worker)\s*(?:<[^>]*>)?\s*\(\s*(['"][^'"]+['"]|[A-Za-z_$][\w$.]*)/g;
|
|
81014
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;
|
|
81015
81266
|
function baseName(filePath) {
|
|
81016
81267
|
const parts = filePath.split("/");
|
|
81017
81268
|
const last = parts[parts.length - 1] ?? filePath;
|
|
81018
81269
|
return last.replace(/\.[jt]sx?$/, "");
|
|
81019
81270
|
}
|
|
81020
|
-
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) {
|
|
81021
81300
|
const arg = raw.trim();
|
|
81022
81301
|
if (arg.startsWith('"') || arg.startsWith("'")) {
|
|
81023
|
-
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);
|
|
81024
81322
|
}
|
|
81025
|
-
return
|
|
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 });
|
|
81336
|
+
}
|
|
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;
|
|
81026
81347
|
}
|
|
81027
81348
|
function extractQueueGraph(input) {
|
|
81028
|
-
const constMap = /* @__PURE__ */ new Map();
|
|
81029
81349
|
const allContent = [
|
|
81030
81350
|
...input.producerClasses.map((p) => p.content),
|
|
81031
81351
|
...input.workerFiles.map((w) => w.content)
|
|
81032
81352
|
];
|
|
81353
|
+
const constMap = /* @__PURE__ */ new Map();
|
|
81033
81354
|
for (const content of allContent) {
|
|
81034
81355
|
for (const m of content.matchAll(CONST_DECL_RE)) {
|
|
81035
81356
|
const ident = m[1];
|
|
@@ -81037,17 +81358,30 @@ function extractQueueGraph(input) {
|
|
|
81037
81358
|
if (ident && value && !constMap.has(ident)) constMap.set(ident, value);
|
|
81038
81359
|
}
|
|
81039
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
|
+
}
|
|
81040
81369
|
const producers = [];
|
|
81041
81370
|
for (const pc47 of input.producerClasses) {
|
|
81371
|
+
const loopVarEnums = loopVarEnumsFor(pc47.content);
|
|
81042
81372
|
for (const m of pc47.content.matchAll(INJECT_QUEUE_RE)) {
|
|
81043
81373
|
const queue = m[1] ?? "";
|
|
81044
81374
|
if (queue) producers.push({ queue, symbol: pc47.name, file: pc47.filePath });
|
|
81045
81375
|
}
|
|
81046
81376
|
for (const m of pc47.content.matchAll(NEW_BULL_RE)) {
|
|
81047
81377
|
if (m[2] !== "Queue") continue;
|
|
81048
|
-
const
|
|
81049
|
-
|
|
81050
|
-
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
|
+
});
|
|
81051
81385
|
}
|
|
81052
81386
|
}
|
|
81053
81387
|
for (const m of pc47.content.matchAll(QUEUE_NAME_CONST_RE)) {
|
|
@@ -81058,6 +81392,7 @@ function extractQueueGraph(input) {
|
|
|
81058
81392
|
}
|
|
81059
81393
|
const workers = [];
|
|
81060
81394
|
for (const wf of input.workerFiles) {
|
|
81395
|
+
const loopVarEnums = loopVarEnumsFor(wf.content);
|
|
81061
81396
|
for (const m of wf.content.matchAll(PROCESSOR_RE)) {
|
|
81062
81397
|
const queue = m[1] ?? "";
|
|
81063
81398
|
const className = m[2] ?? "";
|
|
@@ -81065,17 +81400,24 @@ function extractQueueGraph(input) {
|
|
|
81065
81400
|
}
|
|
81066
81401
|
for (const m of wf.content.matchAll(NEW_BULL_RE)) {
|
|
81067
81402
|
if (m[2] !== "Worker") continue;
|
|
81068
|
-
const
|
|
81069
|
-
|
|
81070
|
-
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
|
+
});
|
|
81071
81410
|
}
|
|
81072
81411
|
}
|
|
81073
81412
|
}
|
|
81413
|
+
for (const h of dispatchHandlers) {
|
|
81414
|
+
workers.push({ queue: h.value, symbol: h.handler, file: h.file });
|
|
81415
|
+
}
|
|
81074
81416
|
const queues = [...new Set([...producers, ...workers].map((r) => r.queue))].sort();
|
|
81075
81417
|
const edges = [];
|
|
81076
81418
|
for (const q of queues) {
|
|
81077
|
-
const P =
|
|
81078
|
-
const W =
|
|
81419
|
+
const P = dedupeBySymbolFile(producers.filter((p) => p.queue === q));
|
|
81420
|
+
const W = dedupeBySymbolFile(workers.filter((w) => w.queue === q));
|
|
81079
81421
|
if (P.length && W.length) {
|
|
81080
81422
|
for (const p of P) {
|
|
81081
81423
|
for (const w of W) {
|
|
@@ -81112,12 +81454,13 @@ function extractQueueGraph(input) {
|
|
|
81112
81454
|
}
|
|
81113
81455
|
return { queues, producers, workers, edges };
|
|
81114
81456
|
}
|
|
81115
|
-
function
|
|
81457
|
+
function dedupeBySymbolFile(rows) {
|
|
81116
81458
|
const seen = /* @__PURE__ */ new Set();
|
|
81117
81459
|
const out = [];
|
|
81118
81460
|
for (const r of rows) {
|
|
81119
|
-
|
|
81120
|
-
seen.
|
|
81461
|
+
const key = `${r.symbol}\0${r.file}`;
|
|
81462
|
+
if (seen.has(key)) continue;
|
|
81463
|
+
seen.add(key);
|
|
81121
81464
|
out.push(r);
|
|
81122
81465
|
}
|
|
81123
81466
|
return out;
|
|
@@ -81128,7 +81471,7 @@ init_cjs_shims();
|
|
|
81128
81471
|
async function stitch(client, db, opts = {}) {
|
|
81129
81472
|
const producerRows = (await client.cypher(
|
|
81130
81473
|
// "new Queue" (no paren) so generic-typed `new Queue<T>(...)` also matches.
|
|
81131
|
-
'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'
|
|
81132
81475
|
)).rows;
|
|
81133
81476
|
const producerClasses = producerRows.map((r) => ({
|
|
81134
81477
|
name: String(r[0] ?? ""),
|
|
@@ -93528,10 +93871,11 @@ async function runQueues(name, opts) {
|
|
|
93528
93871
|
}
|
|
93529
93872
|
}
|
|
93530
93873
|
const rows = await listQueueEdges(db, { project, queueName: name });
|
|
93874
|
+
const showLive = opts.live || name !== void 0 && rows.length === 0;
|
|
93531
93875
|
if (opts.json) {
|
|
93532
93876
|
const topology = topologyToJson(buildQueueMap(rows));
|
|
93533
93877
|
const out = { project: project ?? null, topology };
|
|
93534
|
-
if (
|
|
93878
|
+
if (showLive) out["live"] = await gatherLiveState(config2, rows, name);
|
|
93535
93879
|
console.log(JSON.stringify(out, null, 2));
|
|
93536
93880
|
return 0;
|
|
93537
93881
|
}
|
|
@@ -93540,13 +93884,15 @@ async function runQueues(name, opts) {
|
|
|
93540
93884
|
);
|
|
93541
93885
|
console.log("");
|
|
93542
93886
|
if (rows.length === 0) {
|
|
93543
|
-
console.log(
|
|
93887
|
+
console.log(
|
|
93888
|
+
name !== void 0 ? import_picocolors7.default.dim(` No static topology for "${name}" \u2014 it may be dynamically registered; showing live state below.`) : import_picocolors7.default.dim(" No queue edges indexed. Run: horus index")
|
|
93889
|
+
);
|
|
93544
93890
|
} else {
|
|
93545
93891
|
const byQueue = buildQueueMap(rows);
|
|
93546
93892
|
printTopology(byQueue);
|
|
93547
93893
|
}
|
|
93548
93894
|
console.log("");
|
|
93549
|
-
if (
|
|
93895
|
+
if (showLive) {
|
|
93550
93896
|
await runLiveMode(config2, rows, name, opts.ai ? {
|
|
93551
93897
|
config: opts.config,
|
|
93552
93898
|
aiModel: opts.aiModel,
|
|
@@ -94513,7 +94859,9 @@ async function runInvestigate(hint, opts) {
|
|
|
94513
94859
|
if (cloudActive && cloudSession && cloudCfg) {
|
|
94514
94860
|
try {
|
|
94515
94861
|
const refs = await uploadInvestigationToCloud(cloudSession.client, cloudCfg, report);
|
|
94516
|
-
|
|
94862
|
+
const cloudNote = import_picocolors9.default.dim(`[cloud] investigation saved: ${refs.investigationId}`);
|
|
94863
|
+
if (format === "json") console.error(cloudNote);
|
|
94864
|
+
else console.log(cloudNote);
|
|
94517
94865
|
} catch (err) {
|
|
94518
94866
|
return reportCloudError(err);
|
|
94519
94867
|
}
|