@merittdev/horus 0.2.1 → 0.2.3
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 +271 -154
- 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.2.
|
|
57242
|
+
var HORUS_VERSION = true ? "0.2.3" : "dev";
|
|
57243
57243
|
var PINNED_AXON_VERSION = "1.0.7";
|
|
57244
57244
|
var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
|
|
57245
57245
|
|
|
@@ -77014,6 +77014,8 @@ function detectEntityFields(context, limit = 2) {
|
|
|
77014
77014
|
}
|
|
77015
77015
|
async function reseedFromLogs(hint, input, deps) {
|
|
77016
77016
|
if (!deps.logs || typeof deps.logs.searchLogs !== "function") return null;
|
|
77017
|
+
if (!deps.code) return null;
|
|
77018
|
+
const code = deps.code;
|
|
77017
77019
|
try {
|
|
77018
77020
|
const from = logWindowFrom(input.since);
|
|
77019
77021
|
const records = await deps.logs.searchLogs({
|
|
@@ -77026,7 +77028,7 @@ async function reseedFromLogs(hint, input, deps) {
|
|
|
77026
77028
|
if (records.length === 0) return null;
|
|
77027
77029
|
const candidates = [...new Set(records.map((r) => r.component).filter((c) => !!c))];
|
|
77028
77030
|
for (const name of candidates) {
|
|
77029
|
-
const syms = await
|
|
77031
|
+
const syms = await code.searchSymbols(name, 5);
|
|
77030
77032
|
if (syms.length > 0) {
|
|
77031
77033
|
const rankedSeeds = rankSeeds(syms, [...new Set(tokenize(name))]);
|
|
77032
77034
|
const first = records[0];
|
|
@@ -77077,20 +77079,21 @@ async function investigate(input, deps) {
|
|
|
77077
77079
|
evidence2.push(ev);
|
|
77078
77080
|
return ev;
|
|
77079
77081
|
}
|
|
77080
|
-
const
|
|
77082
|
+
const degradedNoSource = !code;
|
|
77083
|
+
const rawSeeds = code ? await code.searchSymbols(hint, 5) : [];
|
|
77081
77084
|
const hintTokens = [...new Set(tokenize(hint))];
|
|
77082
77085
|
const ranked = rankSeeds(rawSeeds, hintTokens);
|
|
77083
77086
|
let seeds = ranked.map((r) => r.symbol);
|
|
77084
77087
|
let top = seeds[0];
|
|
77085
77088
|
let logReseed = null;
|
|
77086
|
-
if (!top) {
|
|
77089
|
+
if (!top && !degradedNoSource) {
|
|
77087
77090
|
logReseed = await reseedFromLogs(hint, input, deps);
|
|
77088
77091
|
if (logReseed !== null) {
|
|
77089
77092
|
seeds = logReseed.seeds;
|
|
77090
77093
|
top = seeds[0];
|
|
77091
77094
|
}
|
|
77092
77095
|
}
|
|
77093
|
-
if (!top) {
|
|
77096
|
+
if (!top && !degradedNoSource) {
|
|
77094
77097
|
const report2 = {
|
|
77095
77098
|
id: globalThis.crypto.randomUUID(),
|
|
77096
77099
|
input,
|
|
@@ -77114,31 +77117,6 @@ async function investigate(input, deps) {
|
|
|
77114
77117
|
if (persistedId2) report2.id = persistedId2;
|
|
77115
77118
|
return report2;
|
|
77116
77119
|
}
|
|
77117
|
-
const label = top.name === "constructor" ? `${(top.id.split(":").pop() ?? "").replace(/\.constructor$/, "") || top.name} (constructor)` : top.name;
|
|
77118
|
-
const [ctx, impact, flows] = await Promise.all([
|
|
77119
|
-
code.context(top.id),
|
|
77120
|
-
code.impact(top.id, 2),
|
|
77121
|
-
code.flowsFor(top.id)
|
|
77122
|
-
]);
|
|
77123
|
-
const edges = await listQueueEdges(db, { project: input.repo });
|
|
77124
|
-
const symbolNames = /* @__PURE__ */ new Set([
|
|
77125
|
-
top.name,
|
|
77126
|
-
...ctx.callers.map((s) => s.name),
|
|
77127
|
-
...ctx.callees.map((s) => s.name)
|
|
77128
|
-
]);
|
|
77129
|
-
const queueHits = edges.filter((e) => {
|
|
77130
|
-
const bySymbol = e.producerSymbol !== null && symbolNames.has(e.producerSymbol) || e.workerSymbol !== null && symbolNames.has(e.workerSymbol);
|
|
77131
|
-
const byFile = e.producerFile === top.filePath || e.workerFile === top.filePath;
|
|
77132
|
-
return bySymbol || byFile;
|
|
77133
|
-
});
|
|
77134
|
-
let changes = null;
|
|
77135
|
-
if (input.since !== void 0 && looksDiffable(input.since)) {
|
|
77136
|
-
try {
|
|
77137
|
-
changes = await code.detectChanges({ base: input.since, compare: "HEAD" });
|
|
77138
|
-
} catch {
|
|
77139
|
-
changes = null;
|
|
77140
|
-
}
|
|
77141
|
-
}
|
|
77142
77120
|
let recentChanges;
|
|
77143
77121
|
if (deps.repoPath && input.since) {
|
|
77144
77122
|
try {
|
|
@@ -77146,62 +77124,97 @@ async function investigate(input, deps) {
|
|
|
77146
77124
|
} catch {
|
|
77147
77125
|
}
|
|
77148
77126
|
}
|
|
77149
|
-
|
|
77150
|
-
|
|
77151
|
-
|
|
77152
|
-
|
|
77153
|
-
|
|
77154
|
-
|
|
77155
|
-
|
|
77127
|
+
let label = input.service ?? hint;
|
|
77128
|
+
let seedLoc = "";
|
|
77129
|
+
let ctx = null;
|
|
77130
|
+
let impact = null;
|
|
77131
|
+
let flows = [];
|
|
77132
|
+
let seedEv = null;
|
|
77133
|
+
let impactEv = null;
|
|
77156
77134
|
const flowEvIds = [];
|
|
77157
|
-
|
|
77158
|
-
const ev = mkEv(
|
|
77159
|
-
"flow",
|
|
77160
|
-
`Flow "${flow.name}" (${flow.steps.length} step(s))`,
|
|
77161
|
-
{ flowId: flow.id, name: flow.name, steps: flow.steps.map((s) => s.name) },
|
|
77162
|
-
{ symbolId: top.id, file: top.filePath }
|
|
77163
|
-
);
|
|
77164
|
-
flowEvIds.push(ev.id);
|
|
77165
|
-
}
|
|
77166
|
-
const impactEv = mkEv(
|
|
77167
|
-
"impact",
|
|
77168
|
-
`Impact of ${top.name}: ${impact.affected} affected symbol(s)`,
|
|
77169
|
-
{ affected: impact.affected },
|
|
77170
|
-
{ symbolId: top.id, file: top.filePath }
|
|
77171
|
-
);
|
|
77135
|
+
let queueHits = [];
|
|
77172
77136
|
const queueEvByName = /* @__PURE__ */ new Map();
|
|
77173
|
-
|
|
77174
|
-
const producer = edge.producerSymbol ?? "unknown-producer";
|
|
77175
|
-
const worker = edge.workerSymbol ?? "unknown-worker";
|
|
77176
|
-
const ev = mkEv(
|
|
77177
|
-
"queue-edge",
|
|
77178
|
-
`Queue "${edge.queueName}": ${producer} -> ${worker}`,
|
|
77179
|
-
{
|
|
77180
|
-
queueName: edge.queueName,
|
|
77181
|
-
producerSymbol: edge.producerSymbol,
|
|
77182
|
-
producerFile: edge.producerFile,
|
|
77183
|
-
workerSymbol: edge.workerSymbol,
|
|
77184
|
-
workerFile: edge.workerFile,
|
|
77185
|
-
source: edge.source
|
|
77186
|
-
},
|
|
77187
|
-
{ queueName: edge.queueName }
|
|
77188
|
-
);
|
|
77189
|
-
const list = queueEvByName.get(edge.queueName) ?? [];
|
|
77190
|
-
list.push(ev.id);
|
|
77191
|
-
queueEvByName.set(edge.queueName, list);
|
|
77192
|
-
}
|
|
77137
|
+
let changes = null;
|
|
77193
77138
|
let changeEvId = null;
|
|
77194
|
-
if (
|
|
77195
|
-
|
|
77196
|
-
|
|
77197
|
-
|
|
77198
|
-
|
|
77199
|
-
|
|
77200
|
-
|
|
77201
|
-
|
|
77202
|
-
|
|
77139
|
+
if (top && code) {
|
|
77140
|
+
label = top.name === "constructor" ? `${(top.id.split(":").pop() ?? "").replace(/\.constructor$/, "") || top.name} (constructor)` : top.name;
|
|
77141
|
+
[ctx, impact, flows] = await Promise.all([
|
|
77142
|
+
code.context(top.id),
|
|
77143
|
+
code.impact(top.id, 2),
|
|
77144
|
+
code.flowsFor(top.id)
|
|
77145
|
+
]);
|
|
77146
|
+
const edges = await listQueueEdges(db, { project: input.repo });
|
|
77147
|
+
const symbolNames = /* @__PURE__ */ new Set([
|
|
77148
|
+
top.name,
|
|
77149
|
+
...ctx.callers.map((s) => s.name),
|
|
77150
|
+
...ctx.callees.map((s) => s.name)
|
|
77151
|
+
]);
|
|
77152
|
+
queueHits = edges.filter((e) => {
|
|
77153
|
+
const bySymbol = e.producerSymbol !== null && symbolNames.has(e.producerSymbol) || e.workerSymbol !== null && symbolNames.has(e.workerSymbol);
|
|
77154
|
+
const byFile = e.producerFile === top.filePath || e.workerFile === top.filePath;
|
|
77155
|
+
return bySymbol || byFile;
|
|
77156
|
+
});
|
|
77157
|
+
if (input.since !== void 0 && looksDiffable(input.since)) {
|
|
77158
|
+
try {
|
|
77159
|
+
changes = await code.detectChanges({ base: input.since, compare: "HEAD" });
|
|
77160
|
+
} catch {
|
|
77161
|
+
changes = null;
|
|
77162
|
+
}
|
|
77163
|
+
}
|
|
77164
|
+
seedLoc = formatSymbolLocation(top.filePath, top.startLine, top.endLine);
|
|
77165
|
+
seedEv = mkEv(
|
|
77166
|
+
"symbol",
|
|
77167
|
+
`Seed symbol ${top.name} (${seedLoc})`,
|
|
77168
|
+
{ symbol: top, snippet: ctx.snippet ?? null },
|
|
77169
|
+
top.startLine !== void 0 && top.startLine > 0 ? { symbolId: top.id, file: top.filePath, line: top.startLine } : { symbolId: top.id, file: top.filePath }
|
|
77203
77170
|
);
|
|
77204
|
-
|
|
77171
|
+
for (const flow of flows) {
|
|
77172
|
+
const ev = mkEv(
|
|
77173
|
+
"flow",
|
|
77174
|
+
`Flow "${flow.name}" (${flow.steps.length} step(s))`,
|
|
77175
|
+
{ flowId: flow.id, name: flow.name, steps: flow.steps.map((s) => s.name) },
|
|
77176
|
+
{ symbolId: top.id, file: top.filePath }
|
|
77177
|
+
);
|
|
77178
|
+
flowEvIds.push(ev.id);
|
|
77179
|
+
}
|
|
77180
|
+
impactEv = mkEv(
|
|
77181
|
+
"impact",
|
|
77182
|
+
`Impact of ${top.name}: ${impact.affected} affected symbol(s)`,
|
|
77183
|
+
{ affected: impact.affected },
|
|
77184
|
+
{ symbolId: top.id, file: top.filePath }
|
|
77185
|
+
);
|
|
77186
|
+
for (const edge of queueHits) {
|
|
77187
|
+
const producer = edge.producerSymbol ?? "unknown-producer";
|
|
77188
|
+
const worker = edge.workerSymbol ?? "unknown-worker";
|
|
77189
|
+
const ev = mkEv(
|
|
77190
|
+
"queue-edge",
|
|
77191
|
+
`Queue "${edge.queueName}": ${producer} -> ${worker}`,
|
|
77192
|
+
{
|
|
77193
|
+
queueName: edge.queueName,
|
|
77194
|
+
producerSymbol: edge.producerSymbol,
|
|
77195
|
+
producerFile: edge.producerFile,
|
|
77196
|
+
workerSymbol: edge.workerSymbol,
|
|
77197
|
+
workerFile: edge.workerFile,
|
|
77198
|
+
source: edge.source
|
|
77199
|
+
},
|
|
77200
|
+
{ queueName: edge.queueName }
|
|
77201
|
+
);
|
|
77202
|
+
const list = queueEvByName.get(edge.queueName) ?? [];
|
|
77203
|
+
list.push(ev.id);
|
|
77204
|
+
queueEvByName.set(edge.queueName, list);
|
|
77205
|
+
}
|
|
77206
|
+
if (changes) {
|
|
77207
|
+
const addedN = changes.added.length;
|
|
77208
|
+
const removedN = changes.removed.length;
|
|
77209
|
+
const modifiedN = changes.modified.length;
|
|
77210
|
+
const ev = mkEv(
|
|
77211
|
+
"commit",
|
|
77212
|
+
`Change range ${input.since}..HEAD: +${addedN} -${removedN} ~${modifiedN} symbol(s)`,
|
|
77213
|
+
{ added: addedN, removed: removedN, modified: modifiedN },
|
|
77214
|
+
{}
|
|
77215
|
+
);
|
|
77216
|
+
changeEvId = ev.id;
|
|
77217
|
+
}
|
|
77205
77218
|
}
|
|
77206
77219
|
if (changes === null && recentChanges !== void 0 && recentChanges.commits.length > 0) {
|
|
77207
77220
|
const { commits, changedFiles, totalInsertions, totalDeletions } = recentChanges;
|
|
@@ -77235,10 +77248,8 @@ async function investigate(input, deps) {
|
|
|
77235
77248
|
const from = logWindowFrom(input.since);
|
|
77236
77249
|
analysis = await deps.logs.analyzeErrors({ service: input.service, from });
|
|
77237
77250
|
logsCollected = true;
|
|
77238
|
-
const seedBase = top.filePath.split("/").pop() ?? "";
|
|
77239
|
-
const logSeedTerms = [
|
|
77240
|
-
.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
|
|
77241
|
-
];
|
|
77251
|
+
const seedBase = top ? top.filePath.split("/").pop() ?? "" : "";
|
|
77252
|
+
const logSeedTerms = top ? [.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])] : [...new Set(tokenize(hint))];
|
|
77242
77253
|
for (const s of analysis.signatures.slice(0, 15)) {
|
|
77243
77254
|
const { relevanceClass, relevanceReason } = classifyLogRelevance(
|
|
77244
77255
|
s.key,
|
|
@@ -77486,10 +77497,10 @@ async function investigate(input, deps) {
|
|
|
77486
77497
|
}
|
|
77487
77498
|
}
|
|
77488
77499
|
let ownershipEstimate = null;
|
|
77489
|
-
if (deps.repoPath) {
|
|
77500
|
+
if (deps.repoPath && top && code) {
|
|
77490
77501
|
try {
|
|
77491
77502
|
ownershipEstimate = await estimateOwnership(top.name, {
|
|
77492
|
-
code
|
|
77503
|
+
code,
|
|
77493
77504
|
repoPath: deps.repoPath,
|
|
77494
77505
|
symbol: top
|
|
77495
77506
|
});
|
|
@@ -77515,14 +77526,16 @@ async function investigate(input, deps) {
|
|
|
77515
77526
|
const validated = validateHypotheses(hyps, evidence2);
|
|
77516
77527
|
const causeChains = buildCauseChains(validated, evidence2, graph, label);
|
|
77517
77528
|
const findings2 = [];
|
|
77518
|
-
|
|
77519
|
-
|
|
77520
|
-
|
|
77521
|
-
|
|
77522
|
-
|
|
77523
|
-
|
|
77524
|
-
|
|
77525
|
-
|
|
77529
|
+
if (top && seedEv) {
|
|
77530
|
+
findings2.push({
|
|
77531
|
+
kind: "observation",
|
|
77532
|
+
title: `Seed resolves to ${label} at ${seedLoc}`,
|
|
77533
|
+
detail: top.signature ?? void 0,
|
|
77534
|
+
confidence: 1,
|
|
77535
|
+
evidenceIds: [seedEv.id]
|
|
77536
|
+
});
|
|
77537
|
+
}
|
|
77538
|
+
if (logReseed !== null && seedEv) {
|
|
77526
77539
|
findings2.push({
|
|
77527
77540
|
kind: "observation",
|
|
77528
77541
|
title: logReseed.note,
|
|
@@ -77531,7 +77544,7 @@ async function investigate(input, deps) {
|
|
|
77531
77544
|
evidenceIds: [seedEv.id]
|
|
77532
77545
|
});
|
|
77533
77546
|
}
|
|
77534
|
-
if (ranked.length > 1) {
|
|
77547
|
+
if (ranked.length > 1 && seedEv) {
|
|
77535
77548
|
const candidates = ranked.slice(0, 4).map((r) => `${r.symbol.name} [${r.role}]`).join(", ");
|
|
77536
77549
|
findings2.push({
|
|
77537
77550
|
kind: "observation",
|
|
@@ -77550,7 +77563,7 @@ async function investigate(input, deps) {
|
|
|
77550
77563
|
evidenceIds: flowEvIds
|
|
77551
77564
|
});
|
|
77552
77565
|
}
|
|
77553
|
-
if (impact.affected > 0) {
|
|
77566
|
+
if (top && impactEv && impact && impact.affected > 0) {
|
|
77554
77567
|
findings2.push({
|
|
77555
77568
|
kind: "observation",
|
|
77556
77569
|
title: `Changing ${top.name} impacts ${impact.affected} symbol(s) (blast radius)`,
|
|
@@ -77701,7 +77714,7 @@ async function investigate(input, deps) {
|
|
|
77701
77714
|
});
|
|
77702
77715
|
}
|
|
77703
77716
|
const causeInputs = [];
|
|
77704
|
-
const blastRadius = impact
|
|
77717
|
+
const blastRadius = impact?.affected ?? 0;
|
|
77705
77718
|
if (queueRuntimeState !== null) {
|
|
77706
77719
|
for (const q of queueRuntimeState.queues) {
|
|
77707
77720
|
const isStarved = q.waiting >= 10 && q.active === 0;
|
|
@@ -77730,26 +77743,25 @@ async function investigate(input, deps) {
|
|
|
77730
77743
|
id: `cause:queue-path:${queueName}`,
|
|
77731
77744
|
title: `The ${queueName} processing path (${producer} -> ${worker}) is implicated`,
|
|
77732
77745
|
category: "queue-path",
|
|
77733
|
-
sourceEvidenceIds: [...evIds, impactEv.id],
|
|
77746
|
+
sourceEvidenceIds: [...evIds, ...impactEv ? [impactEv.id] : []],
|
|
77734
77747
|
baseScore: 0.35,
|
|
77735
77748
|
metadata: { blastRadius }
|
|
77736
77749
|
});
|
|
77737
77750
|
}
|
|
77738
77751
|
if (changeEvId) {
|
|
77739
77752
|
const gitChangedFiles = recentChanges?.changedFiles ?? [];
|
|
77740
|
-
const
|
|
77741
|
-
|
|
77742
|
-
);
|
|
77753
|
+
const seedFile = top?.filePath;
|
|
77754
|
+
const seedInChanges = seedFile !== void 0 && gitChangedFiles.length > 0 && gitChangedFiles.some((f) => f === seedFile || seedFile.endsWith(f) || f.endsWith(seedFile));
|
|
77743
77755
|
causeInputs.push({
|
|
77744
77756
|
id: "cause:deployment-regression",
|
|
77745
|
-
title: `Recent change to ${top.name} in ${input.since}..HEAD may have introduced the regression`,
|
|
77757
|
+
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`,
|
|
77746
77758
|
category: "deployment-regression",
|
|
77747
|
-
sourceEvidenceIds: [changeEvId, seedEv.id],
|
|
77759
|
+
sourceEvidenceIds: [changeEvId, ...seedEv ? [seedEv.id] : []],
|
|
77748
77760
|
baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
|
|
77749
77761
|
metadata: { blastRadius }
|
|
77750
77762
|
});
|
|
77751
77763
|
}
|
|
77752
|
-
if (impact.affected > 0) {
|
|
77764
|
+
if (top && impactEv && seedEv && impact && impact.affected > 0) {
|
|
77753
77765
|
causeInputs.push({
|
|
77754
77766
|
id: "cause:blast-radius",
|
|
77755
77767
|
title: `${top.name} sits on a high-fan-out path (${impact.affected} affected) and may propagate the fault`,
|
|
@@ -77812,13 +77824,20 @@ async function investigate(input, deps) {
|
|
|
77812
77824
|
);
|
|
77813
77825
|
const seedResolved = seeds.length > 0 ? 1 : 0;
|
|
77814
77826
|
const confidence = clamp015(0.5 * evidenceConfidence + 0.5 * seedResolved);
|
|
77815
|
-
const area = ctx
|
|
77827
|
+
const area = ctx?.community?.name ?? top?.filePath ?? (input.service ?? "runtime evidence");
|
|
77816
77828
|
const topCause = rankedCauses[0];
|
|
77817
|
-
const
|
|
77829
|
+
const banner = degradedNoSource ? "Runtime-only (source intelligence unavailable). " : "";
|
|
77830
|
+
const scope = top ? `resolved to ${label} (${area})` : `over runtime evidence (${area})`;
|
|
77831
|
+
const summary = topCause ? `${banner}Investigation of "${hint}" ${scope}. Top suspected cause: ${topCause.title}.` : `${banner}Investigation of "${hint}" ${scope}. No dominant suspected cause emerged from the available ${degradedNoSource ? "runtime" : "structural"} evidence.`;
|
|
77818
77832
|
const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
|
|
77833
|
+
if (degradedNoSource) {
|
|
77834
|
+
nextActions.unshift(
|
|
77835
|
+
"Source intelligence was unavailable \u2014 run `horus index` to enable code-aware analysis, then re-run for a full investigation."
|
|
77836
|
+
);
|
|
77837
|
+
}
|
|
77819
77838
|
if (ownershipEstimate?.likelyMaintainer) {
|
|
77820
77839
|
nextActions.unshift(
|
|
77821
|
-
`Route to likely maintainer: ${ownershipEstimate.likelyMaintainer} (${Math.round(ownershipEstimate.maintainerShare * 100)}% of commits to ${ownershipEstimate.file ?? top
|
|
77840
|
+
`Route to likely maintainer: ${ownershipEstimate.likelyMaintainer} (${Math.round(ownershipEstimate.maintainerShare * 100)}% of commits to ${ownershipEstimate.file ?? top?.filePath ?? "the implicated file"})`
|
|
77822
77841
|
);
|
|
77823
77842
|
}
|
|
77824
77843
|
const report = {
|
|
@@ -77839,7 +77858,8 @@ async function investigate(input, deps) {
|
|
|
77839
77858
|
nextActions,
|
|
77840
77859
|
ownership: ownershipEstimate,
|
|
77841
77860
|
causeChains: causeChains.length > 0 ? causeChains : void 0,
|
|
77842
|
-
...recentChanges !== void 0 ? { recentChanges } : {}
|
|
77861
|
+
...recentChanges !== void 0 ? { recentChanges } : {},
|
|
77862
|
+
...degradedNoSource ? { degraded: { sourceIntelligence: false, reason: "source-intelligence host unreachable" } } : {}
|
|
77843
77863
|
};
|
|
77844
77864
|
const connectorFlags = deps.connectors ? {
|
|
77845
77865
|
...deps.connectors,
|
|
@@ -77892,16 +77912,20 @@ function buildNextActions(top, _ctx, impact, queueHits, changes, input) {
|
|
|
77892
77912
|
actions.push(`Inspect logs for worker ${worker} on queue ${edge.queueName}`);
|
|
77893
77913
|
actions.push(`Check depth/failures of queue ${edge.queueName}`);
|
|
77894
77914
|
}
|
|
77895
|
-
if (impact.affected > 0) {
|
|
77915
|
+
if (top && impact && impact.affected > 0) {
|
|
77896
77916
|
actions.push(`Review impact set of ${top.name} (${impact.affected} affected symbol(s))`);
|
|
77897
77917
|
}
|
|
77898
|
-
if (
|
|
77899
|
-
|
|
77900
|
-
|
|
77901
|
-
|
|
77918
|
+
if (top) {
|
|
77919
|
+
if (changes && input.since !== void 0) {
|
|
77920
|
+
actions.push(`Diff recent commits touching ${top.filePath} in ${input.since}..HEAD`);
|
|
77921
|
+
} else {
|
|
77922
|
+
actions.push(`Diff recent commits touching ${top.filePath}`);
|
|
77923
|
+
}
|
|
77902
77924
|
}
|
|
77903
77925
|
if (actions.length === 0) {
|
|
77904
|
-
actions.push(
|
|
77926
|
+
actions.push(
|
|
77927
|
+
top ? `Inspect the source of ${top.name} at ${top.filePath}` : input.service ? `Inspect runtime logs and metrics for service "${input.service}"` : "Inspect runtime logs and metrics for the affected service"
|
|
77928
|
+
);
|
|
77905
77929
|
}
|
|
77906
77930
|
return actions;
|
|
77907
77931
|
}
|
|
@@ -93758,7 +93782,7 @@ async function uploadInvestigationToCloud(client, cfg, report) {
|
|
|
93758
93782
|
await client.createAgentRun(projectId, investigation.id, {
|
|
93759
93783
|
repositoryId: cfg.repository?.id,
|
|
93760
93784
|
status: "completed",
|
|
93761
|
-
cliVersion:
|
|
93785
|
+
cliVersion: HORUS_VERSION,
|
|
93762
93786
|
summary: report.summary,
|
|
93763
93787
|
idempotencyKey: idempotencyKey(report.id, "run")
|
|
93764
93788
|
});
|
|
@@ -93796,6 +93820,40 @@ async function listCloudInvestigations(client, cfg) {
|
|
|
93796
93820
|
return result.investigations;
|
|
93797
93821
|
}
|
|
93798
93822
|
|
|
93823
|
+
// ../../packages/cli/src/lib/ensure-host.ts
|
|
93824
|
+
init_cjs_shims();
|
|
93825
|
+
function parseHostPort(hostUrl) {
|
|
93826
|
+
try {
|
|
93827
|
+
const port = parseInt(new URL(hostUrl).port, 10);
|
|
93828
|
+
return Number.isNaN(port) ? null : port;
|
|
93829
|
+
} catch {
|
|
93830
|
+
return null;
|
|
93831
|
+
}
|
|
93832
|
+
}
|
|
93833
|
+
function ensureHostReasonHint(reason) {
|
|
93834
|
+
switch (reason) {
|
|
93835
|
+
case "source-unavailable":
|
|
93836
|
+
return "horus-source is not installed \u2014 install it: pip install horus-source";
|
|
93837
|
+
case "not-analyzed":
|
|
93838
|
+
return "this repo has not been indexed yet \u2014 run: horus index";
|
|
93839
|
+
case "bad-url":
|
|
93840
|
+
return "the configured source host URL is not a valid URL";
|
|
93841
|
+
default:
|
|
93842
|
+
return "the host did not become healthy \u2014 run: horus index";
|
|
93843
|
+
}
|
|
93844
|
+
}
|
|
93845
|
+
async function ensureSourceHost(root, hostUrl, opts = {}) {
|
|
93846
|
+
if (await isHostHealthy(hostUrl)) return { ok: true, hostUrl };
|
|
93847
|
+
if (!await sourceAvailable()) return { ok: false, reason: "source-unavailable" };
|
|
93848
|
+
if (!isAnalyzed(root)) return { ok: false, reason: "not-analyzed" };
|
|
93849
|
+
const port = parseHostPort(hostUrl);
|
|
93850
|
+
if (port === null) return { ok: false, reason: "bad-url" };
|
|
93851
|
+
startHost(root, port);
|
|
93852
|
+
if (await waitForHost(hostUrl, opts.timeoutMs ?? 2e4)) return { ok: true, hostUrl };
|
|
93853
|
+
removeSpawnedHostRecord(root);
|
|
93854
|
+
return { ok: false, reason: "unhealthy" };
|
|
93855
|
+
}
|
|
93856
|
+
|
|
93799
93857
|
// ../../packages/cli/src/commands/investigate.ts
|
|
93800
93858
|
function extractEvidenceExcerpt(e) {
|
|
93801
93859
|
if (!e.payload || typeof e.payload !== "object") return void 0;
|
|
@@ -93933,15 +93991,27 @@ async function runInvestigate(hint, opts) {
|
|
|
93933
93991
|
);
|
|
93934
93992
|
return 1;
|
|
93935
93993
|
}
|
|
93936
|
-
const
|
|
93937
|
-
|
|
93938
|
-
|
|
93994
|
+
const sourceUrl = renv.repositories[0]?.sourceHostUrl ?? renv.repositories[0]?.axonHostUrl;
|
|
93995
|
+
let health = await code.health();
|
|
93996
|
+
if (!health.ok && sourceUrl) {
|
|
93939
93997
|
console.error(
|
|
93940
|
-
import_picocolors8.default.
|
|
93941
|
-
|
|
93998
|
+
import_picocolors8.default.yellow(`Source-intelligence host unreachable (${sourceUrl}) \u2014 attempting to start it\u2026`)
|
|
93999
|
+
);
|
|
94000
|
+
const healed = await ensureSourceHost(renv.path, sourceUrl);
|
|
94001
|
+
if (healed.ok) {
|
|
94002
|
+
console.error(import_picocolors8.default.green(`Source-intelligence host is up at ${healed.hostUrl}.`));
|
|
94003
|
+
health = await code.health();
|
|
94004
|
+
} else {
|
|
94005
|
+
console.error(import_picocolors8.default.dim(` ${ensureHostReasonHint(healed.reason)}`));
|
|
94006
|
+
}
|
|
94007
|
+
}
|
|
94008
|
+
const runtimeOnly = !health.ok;
|
|
94009
|
+
if (runtimeOnly) {
|
|
94010
|
+
console.error(
|
|
94011
|
+
import_picocolors8.default.yellow(
|
|
94012
|
+
`Proceeding in runtime-only mode \u2014 no source intelligence. Run ${import_picocolors8.default.bold("horus index")} for a full (code-aware) investigation.`
|
|
93942
94013
|
)
|
|
93943
94014
|
);
|
|
93944
|
-
return 1;
|
|
93945
94015
|
}
|
|
93946
94016
|
const logs = logsForEnv(renv);
|
|
93947
94017
|
const mongo = mongoForEnv(renv);
|
|
@@ -93954,7 +94024,9 @@ async function runInvestigate(hint, opts) {
|
|
|
93954
94024
|
const report = await investigate(
|
|
93955
94025
|
{ hint, repo: renv.project, since: opts.since, service },
|
|
93956
94026
|
{
|
|
93957
|
-
|
|
94027
|
+
// Runtime-only degrade (HOR-319 layer-2): pass no source provider so the engine
|
|
94028
|
+
// skips seed resolution + structural evidence and builds from runtime evidence.
|
|
94029
|
+
code: runtimeOnly ? null : code,
|
|
93958
94030
|
db,
|
|
93959
94031
|
logs,
|
|
93960
94032
|
mongo,
|
|
@@ -94026,6 +94098,10 @@ async function runInvestigate(hint, opts) {
|
|
|
94026
94098
|
return reportCloudError(err);
|
|
94027
94099
|
}
|
|
94028
94100
|
}
|
|
94101
|
+
if (format !== "json") {
|
|
94102
|
+
console.log(import_picocolors8.default.dim(`
|
|
94103
|
+
Ask a follow-up: horus ask ${report.id} "<question>"`));
|
|
94104
|
+
}
|
|
94029
94105
|
} finally {
|
|
94030
94106
|
await sql2.end();
|
|
94031
94107
|
if (mongo) await mongo.close();
|
|
@@ -94874,10 +94950,27 @@ function renderAnswer(report, directive, opts) {
|
|
|
94874
94950
|
}
|
|
94875
94951
|
return 0;
|
|
94876
94952
|
}
|
|
94953
|
+
async function lookupLocalInvestigation(id, configPath) {
|
|
94954
|
+
const { db, sql: sql2 } = createDb(await resolveDbUrl(configPath));
|
|
94955
|
+
try {
|
|
94956
|
+
const row = await getInvestigation(db, id);
|
|
94957
|
+
if (!row) return { kind: "not-found" };
|
|
94958
|
+
if (!row.report) return { kind: "no-report" };
|
|
94959
|
+
return { kind: "found", report: migrateReport(row.report) };
|
|
94960
|
+
} catch (err) {
|
|
94961
|
+
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
94962
|
+
if (code === "22P02") return { kind: "not-found" };
|
|
94963
|
+
return { kind: "error", message: err instanceof Error ? err.message : String(err) };
|
|
94964
|
+
} finally {
|
|
94965
|
+
await sql2.end();
|
|
94966
|
+
}
|
|
94967
|
+
}
|
|
94877
94968
|
async function runAsk(id, directive, opts) {
|
|
94878
94969
|
const repoRoot = repoRootOrCwd();
|
|
94879
94970
|
const cloudCfg = readCloudConfig(repoRoot);
|
|
94880
94971
|
if (isCloudActive(cloudCfg)) {
|
|
94972
|
+
const local2 = await lookupLocalInvestigation(id, opts.config);
|
|
94973
|
+
if (local2.kind === "found") return renderAnswer(local2.report, directive, opts);
|
|
94881
94974
|
const session = authedClient();
|
|
94882
94975
|
if (!session) {
|
|
94883
94976
|
console.error(
|
|
@@ -94888,47 +94981,40 @@ async function runAsk(id, directive, opts) {
|
|
|
94888
94981
|
return 1;
|
|
94889
94982
|
}
|
|
94890
94983
|
try {
|
|
94891
|
-
const report = await fetchInvestigationReportFromCloud(
|
|
94892
|
-
|
|
94893
|
-
|
|
94894
|
-
|
|
94984
|
+
const report = await fetchInvestigationReportFromCloud(session.client, cloudCfg, id);
|
|
94985
|
+
if (report) return renderAnswer(report, directive, opts);
|
|
94986
|
+
console.error(
|
|
94987
|
+
import_picocolors21.default.red(
|
|
94988
|
+
`Investigation ${id} has no saved report locally or in Horus Cloud. Run ${import_picocolors21.default.bold("horus investigate")} first.`
|
|
94989
|
+
)
|
|
94895
94990
|
);
|
|
94896
|
-
|
|
94991
|
+
return 1;
|
|
94992
|
+
} catch (err) {
|
|
94993
|
+
if (err instanceof CloudError && err.status === 404) {
|
|
94897
94994
|
console.error(
|
|
94898
94995
|
import_picocolors21.default.red(
|
|
94899
|
-
`
|
|
94996
|
+
`No investigation found for "${id}" locally or in Horus Cloud. Use the id printed at the top of \`horus investigate\` output.`
|
|
94900
94997
|
)
|
|
94901
94998
|
);
|
|
94902
94999
|
return 1;
|
|
94903
95000
|
}
|
|
94904
|
-
return renderAnswer(report, directive, opts);
|
|
94905
|
-
} catch (err) {
|
|
94906
95001
|
return reportCloudError(err);
|
|
94907
95002
|
}
|
|
94908
95003
|
}
|
|
94909
|
-
const
|
|
94910
|
-
|
|
94911
|
-
|
|
94912
|
-
|
|
94913
|
-
|
|
94914
|
-
return 1;
|
|
94915
|
-
}
|
|
94916
|
-
if (!row.report) {
|
|
95004
|
+
const local = await lookupLocalInvestigation(id, opts.config);
|
|
95005
|
+
switch (local.kind) {
|
|
95006
|
+
case "found":
|
|
95007
|
+
return renderAnswer(local.report, directive, opts);
|
|
95008
|
+
case "no-report":
|
|
94917
95009
|
console.error(import_picocolors21.default.red("Investigation " + id + " has no stored report."));
|
|
94918
95010
|
return 1;
|
|
94919
|
-
|
|
94920
|
-
|
|
94921
|
-
|
|
94922
|
-
|
|
94923
|
-
|
|
94924
|
-
if (code === "22P02") {
|
|
95011
|
+
case "error":
|
|
95012
|
+
console.error(import_picocolors21.default.red(local.message));
|
|
95013
|
+
return 1;
|
|
95014
|
+
case "not-found":
|
|
95015
|
+
default:
|
|
94925
95016
|
console.error(import_picocolors21.default.red("No investigation found: " + id));
|
|
94926
95017
|
return 1;
|
|
94927
|
-
}
|
|
94928
|
-
console.error(import_picocolors21.default.red(err instanceof Error ? err.message : String(err)));
|
|
94929
|
-
return 1;
|
|
94930
|
-
} finally {
|
|
94931
|
-
await sql2.end();
|
|
94932
95018
|
}
|
|
94933
95019
|
}
|
|
94934
95020
|
|
|
@@ -95102,6 +95188,14 @@ function levelColor(level, text2) {
|
|
|
95102
95188
|
if (level === "warn") return import_picocolors24.default.yellow(text2);
|
|
95103
95189
|
return import_picocolors24.default.dim(text2);
|
|
95104
95190
|
}
|
|
95191
|
+
function matchServiceLabel(requested, candidates) {
|
|
95192
|
+
if (candidates.includes(requested)) return { kind: "unique", matched: requested };
|
|
95193
|
+
const needle = requested.toLowerCase();
|
|
95194
|
+
const subs = candidates.filter((c) => c.toLowerCase().includes(needle));
|
|
95195
|
+
if (subs.length === 1 && subs[0] !== void 0) return { kind: "unique", matched: subs[0] };
|
|
95196
|
+
if (subs.length > 1) return { kind: "ambiguous", candidates: subs };
|
|
95197
|
+
return { kind: "none" };
|
|
95198
|
+
}
|
|
95105
95199
|
async function runLogs(service, opts) {
|
|
95106
95200
|
try {
|
|
95107
95201
|
const config2 = await loadConfig(opts.config, { name: opts.name });
|
|
@@ -95279,9 +95373,32 @@ async function runLogs(service, opts) {
|
|
|
95279
95373
|
if (analysis.signatures.length === 0 && resolvedService !== void 0) {
|
|
95280
95374
|
const broader = await logs.analyzeErrors({ from, text: opts.grep, broadText: opts.grep !== void 0 });
|
|
95281
95375
|
if (broader.signatures.length > 0) {
|
|
95282
|
-
|
|
95283
|
-
|
|
95284
|
-
|
|
95376
|
+
const match = matchServiceLabel(resolvedService, broader.affectedServices);
|
|
95377
|
+
if (match.kind === "unique" && match.matched !== void 0 && match.matched !== resolvedService) {
|
|
95378
|
+
const scoped = await logs.analyzeErrors({
|
|
95379
|
+
service: match.matched,
|
|
95380
|
+
from,
|
|
95381
|
+
text: opts.grep,
|
|
95382
|
+
broadText: opts.grep !== void 0
|
|
95383
|
+
});
|
|
95384
|
+
if (scoped.signatures.length > 0) {
|
|
95385
|
+
analysis = scoped;
|
|
95386
|
+
scopeService = match.matched;
|
|
95387
|
+
broadeningNote = `No exact match for service "${resolvedService}" \u2014 matched "${match.matched}"`;
|
|
95388
|
+
} else {
|
|
95389
|
+
analysis = broader;
|
|
95390
|
+
scopeService = void 0;
|
|
95391
|
+
broadeningNote = `No errors found for service "${resolvedService}" \u2014 showing all services`;
|
|
95392
|
+
}
|
|
95393
|
+
} else if (match.kind === "ambiguous" && match.candidates !== void 0) {
|
|
95394
|
+
analysis = broader;
|
|
95395
|
+
scopeService = void 0;
|
|
95396
|
+
broadeningNote = `"${resolvedService}" matches multiple services: ${match.candidates.join(", ")} \u2014 showing all; re-run with a full label`;
|
|
95397
|
+
} else {
|
|
95398
|
+
analysis = broader;
|
|
95399
|
+
scopeService = void 0;
|
|
95400
|
+
broadeningNote = `No errors found for service "${resolvedService}" \u2014 showing all services`;
|
|
95401
|
+
}
|
|
95285
95402
|
}
|
|
95286
95403
|
}
|
|
95287
95404
|
if (opts.json) {
|