@merittdev/horus 0.7.0 → 0.8.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 +354 -52
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -65880,8 +65880,8 @@ init_cjs_shims();
|
|
|
65880
65880
|
|
|
65881
65881
|
// ../../packages/core/src/version.ts
|
|
65882
65882
|
init_cjs_shims();
|
|
65883
|
-
var HORUS_VERSION = true ? "0.
|
|
65884
|
-
var PINNED_SOURCE_VERSION = "1.0
|
|
65883
|
+
var HORUS_VERSION = true ? "0.8.1" : "dev";
|
|
65884
|
+
var PINNED_SOURCE_VERSION = "1.4.0";
|
|
65885
65885
|
|
|
65886
65886
|
// ../../packages/core/src/evidence.ts
|
|
65887
65887
|
init_cjs_shims();
|
|
@@ -70979,6 +70979,24 @@ async function getSourceVersion() {
|
|
|
70979
70979
|
return null;
|
|
70980
70980
|
}
|
|
70981
70981
|
}
|
|
70982
|
+
var SourceVersionMismatchError = class extends Error {
|
|
70983
|
+
constructor(installed, pinned) {
|
|
70984
|
+
super(
|
|
70985
|
+
`horus-source ${installed} is installed but Horus is pinned to ${pinned}. A drifted backend builds a graph this CLI cannot map and can corrupt the index (e.g. duplicate-primary-key failures during "Running initial index"). Install the pinned version: pip install 'horus-source==${pinned}'`
|
|
70986
|
+
);
|
|
70987
|
+
this.installed = installed;
|
|
70988
|
+
this.pinned = pinned;
|
|
70989
|
+
this.name = "SourceVersionMismatchError";
|
|
70990
|
+
}
|
|
70991
|
+
installed;
|
|
70992
|
+
pinned;
|
|
70993
|
+
};
|
|
70994
|
+
async function assertSourceVersionPinned() {
|
|
70995
|
+
const installed = await getSourceVersion();
|
|
70996
|
+
if (installed !== null && installed !== PINNED_SOURCE_VERSION) {
|
|
70997
|
+
throw new SourceVersionMismatchError(installed, PINNED_SOURCE_VERSION);
|
|
70998
|
+
}
|
|
70999
|
+
}
|
|
70982
71000
|
function isAnalyzed(root) {
|
|
70983
71001
|
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".horus", "source"));
|
|
70984
71002
|
}
|
|
@@ -71001,6 +71019,21 @@ function readSourceHostUrl(root) {
|
|
|
71001
71019
|
return null;
|
|
71002
71020
|
}
|
|
71003
71021
|
}
|
|
71022
|
+
function readSourceHostPid(root) {
|
|
71023
|
+
const p5 = (0, import_node_path3.join)(root, ".horus", "source", "host.json");
|
|
71024
|
+
if (!(0, import_node_fs3.existsSync)(p5)) return null;
|
|
71025
|
+
try {
|
|
71026
|
+
const j3 = JSON.parse((0, import_node_fs3.readFileSync)(p5, "utf8"));
|
|
71027
|
+
if (typeof j3.pid !== "number" || !Number.isInteger(j3.pid) || j3.pid <= 0) return null;
|
|
71028
|
+
return {
|
|
71029
|
+
pid: j3.pid,
|
|
71030
|
+
port: typeof j3.port === "number" ? j3.port : NaN,
|
|
71031
|
+
repoPath: typeof j3.repo_path === "string" ? j3.repo_path : ""
|
|
71032
|
+
};
|
|
71033
|
+
} catch {
|
|
71034
|
+
return null;
|
|
71035
|
+
}
|
|
71036
|
+
}
|
|
71004
71037
|
async function isHostHealthy(hostUrl) {
|
|
71005
71038
|
try {
|
|
71006
71039
|
const res = await fetch(`${hostUrl}/api/health`, {
|
|
@@ -71139,7 +71172,9 @@ var SourceCodeProvider = class {
|
|
|
71139
71172
|
const exactSymbols = exactRows.map((row) => ({
|
|
71140
71173
|
id: String(row[0] ?? ""),
|
|
71141
71174
|
name: String(row[1] ?? ""),
|
|
71142
|
-
filePath: String(row[2] ?? "")
|
|
71175
|
+
filePath: String(row[2] ?? ""),
|
|
71176
|
+
score: 1
|
|
71177
|
+
// an exact-name Cypher hit is authoritative
|
|
71143
71178
|
}));
|
|
71144
71179
|
const exactIds = new Set(exactSymbols.map((s5) => s5.id));
|
|
71145
71180
|
const ql = query.toLowerCase();
|
|
@@ -71154,7 +71189,7 @@ var SourceCodeProvider = class {
|
|
|
71154
71189
|
semantic.sort((a2, b5) => b5.rank - a2.rank || b5.r.score - a2.r.score);
|
|
71155
71190
|
const combined = [
|
|
71156
71191
|
...exactSymbols,
|
|
71157
|
-
...semantic.map(({ r }) => ({ id: r.nodeId, name: r.name, filePath: r.filePath }))
|
|
71192
|
+
...semantic.map(({ r }) => ({ id: r.nodeId, name: r.name, filePath: r.filePath, score: r.score }))
|
|
71158
71193
|
];
|
|
71159
71194
|
return this.hydrateLines(combined.slice(0, limit));
|
|
71160
71195
|
}
|
|
@@ -71210,6 +71245,7 @@ var SourceCodeProvider = class {
|
|
|
71210
71245
|
const isDead = Boolean(node?.[8]);
|
|
71211
71246
|
const content = node?.[9];
|
|
71212
71247
|
const snippet = typeof content === "string" ? content.slice(0, 600) : void 0;
|
|
71248
|
+
const sourceBody = typeof content === "string" ? content.slice(0, 2e4) : void 0;
|
|
71213
71249
|
const callees = calleeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
|
|
71214
71250
|
const callers = callerRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
|
|
71215
71251
|
const usesType = usesTypeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
|
|
@@ -71236,6 +71272,7 @@ var SourceCodeProvider = class {
|
|
|
71236
71272
|
return {
|
|
71237
71273
|
symbol,
|
|
71238
71274
|
snippet,
|
|
71275
|
+
...sourceBody !== void 0 ? { sourceBody } : {},
|
|
71239
71276
|
callers,
|
|
71240
71277
|
callees,
|
|
71241
71278
|
imports,
|
|
@@ -71780,8 +71817,12 @@ function shortTs(iso) {
|
|
|
71780
71817
|
}
|
|
71781
71818
|
function buildErrorAnalysisBody(q2, field = "event_code", mapping = MERITT_FIELD_MAPPING) {
|
|
71782
71819
|
const filters = [];
|
|
71783
|
-
|
|
71784
|
-
|
|
71820
|
+
if (q2.eventCode === void 0) {
|
|
71821
|
+
const minLevel = q2.level !== void 0 && levelToValue(q2.level) > 50 ? q2.level : "error";
|
|
71822
|
+
filters.push(buildLevelFilter(mapping, minLevel));
|
|
71823
|
+
} else if (q2.level !== void 0 && levelToValue(q2.level) > 50) {
|
|
71824
|
+
filters.push(buildLevelFilter(mapping, q2.level));
|
|
71825
|
+
}
|
|
71785
71826
|
if (q2.from !== void 0 || q2.to !== void 0) {
|
|
71786
71827
|
const rangeClause = {};
|
|
71787
71828
|
if (q2.from !== void 0) rangeClause["gte"] = q2.from;
|
|
@@ -89233,18 +89274,58 @@ async function createLocalDb(opts) {
|
|
|
89233
89274
|
} catch {
|
|
89234
89275
|
}
|
|
89235
89276
|
assertEmbeddedAssetsPresent();
|
|
89236
|
-
const
|
|
89237
|
-
|
|
89238
|
-
|
|
89239
|
-
|
|
89240
|
-
|
|
89241
|
-
db,
|
|
89242
|
-
|
|
89243
|
-
|
|
89244
|
-
|
|
89277
|
+
const releaseLock = await acquireDbLock(dataDir);
|
|
89278
|
+
try {
|
|
89279
|
+
const client = new Ve3(dataDir);
|
|
89280
|
+
await client.waitReady;
|
|
89281
|
+
await applyEmbeddedMigrations(client);
|
|
89282
|
+
const db = drizzle2(client, { schema: schema_exports });
|
|
89283
|
+
return {
|
|
89284
|
+
db,
|
|
89285
|
+
sql: {
|
|
89286
|
+
end: async () => {
|
|
89287
|
+
await client.close();
|
|
89288
|
+
releaseLock();
|
|
89289
|
+
}
|
|
89245
89290
|
}
|
|
89246
|
-
}
|
|
89291
|
+
};
|
|
89292
|
+
} catch (e) {
|
|
89293
|
+
releaseLock();
|
|
89294
|
+
throw e;
|
|
89295
|
+
}
|
|
89296
|
+
}
|
|
89297
|
+
async function acquireDbLock(dataDir) {
|
|
89298
|
+
const lockPath = `${dataDir}.lock`;
|
|
89299
|
+
const STALE_MS = 6e4;
|
|
89300
|
+
const TIMEOUT_MS = 3e4;
|
|
89301
|
+
const start2 = Date.now();
|
|
89302
|
+
const noop3 = () => {
|
|
89247
89303
|
};
|
|
89304
|
+
for (; ; ) {
|
|
89305
|
+
try {
|
|
89306
|
+
const fd = (0, import_node_fs4.openSync)(lockPath, "wx");
|
|
89307
|
+
(0, import_node_fs4.writeSync)(fd, `${process.pid} ${Date.now()}`);
|
|
89308
|
+
(0, import_node_fs4.closeSync)(fd);
|
|
89309
|
+
return () => {
|
|
89310
|
+
try {
|
|
89311
|
+
(0, import_node_fs4.unlinkSync)(lockPath);
|
|
89312
|
+
} catch {
|
|
89313
|
+
}
|
|
89314
|
+
};
|
|
89315
|
+
} catch (e) {
|
|
89316
|
+
if (e.code !== "EEXIST") return noop3;
|
|
89317
|
+
try {
|
|
89318
|
+
if (Date.now() - (0, import_node_fs4.statSync)(lockPath).mtimeMs > STALE_MS) {
|
|
89319
|
+
(0, import_node_fs4.unlinkSync)(lockPath);
|
|
89320
|
+
continue;
|
|
89321
|
+
}
|
|
89322
|
+
} catch {
|
|
89323
|
+
continue;
|
|
89324
|
+
}
|
|
89325
|
+
if (Date.now() - start2 > TIMEOUT_MS) return noop3;
|
|
89326
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
89327
|
+
}
|
|
89328
|
+
}
|
|
89248
89329
|
}
|
|
89249
89330
|
var EMBEDDED_PGLITE_ASSETS = ["pglite.wasm", "pglite.data", "initdb.wasm"];
|
|
89250
89331
|
function assertEmbeddedAssetsPresent() {
|
|
@@ -89841,6 +89922,34 @@ function renderReport(r) {
|
|
|
89841
89922
|
if (r.input.repo) lines.push(`Repo: ${r.input.repo}`);
|
|
89842
89923
|
if (r.input.service) lines.push(`Service: ${r.input.service}`);
|
|
89843
89924
|
if (r.input.since) lines.push(`Since: ${r.input.since}`);
|
|
89925
|
+
if (r.behavioral) {
|
|
89926
|
+
lines.push("");
|
|
89927
|
+
lines.push("## Summary");
|
|
89928
|
+
lines.push(r.summary);
|
|
89929
|
+
lines.push("");
|
|
89930
|
+
lines.push(`## Flow${r.behavioral.entry ? ` \u2014 ${r.behavioral.entry.name}` : ""}`);
|
|
89931
|
+
if (r.behavioral.steps.length === 0) {
|
|
89932
|
+
lines.push("(no execution flow could be reconstructed from the source graph)");
|
|
89933
|
+
} else {
|
|
89934
|
+
r.behavioral.steps.slice(0, 20).forEach((s5, i3) => {
|
|
89935
|
+
lines.push(` ${i3 + 1}. ${s5.name} (${formatSymbolLocation(s5.filePath, s5.startLine, s5.endLine)})`);
|
|
89936
|
+
});
|
|
89937
|
+
if (r.behavioral.steps.length > 20) lines.push(` \u2026 and ${r.behavioral.steps.length - 20} more`);
|
|
89938
|
+
}
|
|
89939
|
+
if (r.behavioral.externalCalls.length > 0 || r.behavioral.persistence.length > 0) lines.push("");
|
|
89940
|
+
if (r.behavioral.externalCalls.length > 0) lines.push(`External calls: ${r.behavioral.externalCalls.join(", ")}`);
|
|
89941
|
+
if (r.behavioral.persistence.length > 0) lines.push(`Persistence: ${r.behavioral.persistence.join(", ")}`);
|
|
89942
|
+
lines.push("");
|
|
89943
|
+
if (r.nextActions.length > 0) {
|
|
89944
|
+
lines.push("## Next actions");
|
|
89945
|
+
for (const a2 of r.nextActions) lines.push(`- ${a2}`);
|
|
89946
|
+
lines.push("");
|
|
89947
|
+
}
|
|
89948
|
+
lines.push(
|
|
89949
|
+
'\u2014 Behavioral walkthrough, not a fault hunt. Phrase it as a symptom (e.g. "X failing") for an incident investigation.'
|
|
89950
|
+
);
|
|
89951
|
+
return lines.join("\n");
|
|
89952
|
+
}
|
|
89844
89953
|
lines.push(`Confidence: ${r.confidence.toFixed(2)}`);
|
|
89845
89954
|
const caveat = runtimeSourceCaveat(r);
|
|
89846
89955
|
if (caveat) lines.push(` \u21B3 ${caveat}`);
|
|
@@ -91795,19 +91904,21 @@ function seedRole(s5) {
|
|
|
91795
91904
|
if (/util|helper/i.test(hay)) return "util";
|
|
91796
91905
|
return "code";
|
|
91797
91906
|
}
|
|
91798
|
-
function scoreSeed(s5, index2, hintTokens, changedFiles) {
|
|
91907
|
+
function scoreSeed(s5, index2, hintTokens, changedFiles, hintHasCode) {
|
|
91799
91908
|
const hay = `${s5.name} ${s5.filePath}`.toLowerCase();
|
|
91800
91909
|
let score = 0;
|
|
91801
|
-
if (PREFER.test(hay)) score += 3;
|
|
91910
|
+
if (PREFER.test(hay)) score += hintHasCode ? 0 : 3;
|
|
91802
91911
|
if (DEMOTE.test(hay)) score -= 3;
|
|
91803
91912
|
if (isTypeLikeName(s5.name)) score -= 4;
|
|
91804
91913
|
if (s5.startLine !== void 0 && s5.endLine !== void 0 && s5.endLine - s5.startLine <= 3) {
|
|
91805
91914
|
score -= 3;
|
|
91806
91915
|
}
|
|
91807
91916
|
if (changedFiles !== void 0 && changedFiles.has(s5.filePath)) score += 5;
|
|
91808
|
-
if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s5.filePath)) score += 2;
|
|
91917
|
+
if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s5.filePath)) score += hintHasCode ? 0 : 2;
|
|
91809
91918
|
if (/(^|\/)scripts?\//i.test(s5.filePath)) score -= 2;
|
|
91810
|
-
score += Math.max(0, 5 - index2) * 0.1;
|
|
91919
|
+
score += Math.max(0, 5 - index2) * (hintHasCode ? 0.8 : 0.1);
|
|
91920
|
+
if (s5.score !== void 0) score += s5.score * (hintHasCode ? 8 : 1.5);
|
|
91921
|
+
if (/\.[jt]sx?$/i.test(s5.name)) score -= 5;
|
|
91811
91922
|
if (hintTokens !== void 0 && hintTokens.length > 0) {
|
|
91812
91923
|
let hintBoost = 0;
|
|
91813
91924
|
for (const tok of hintTokens) {
|
|
@@ -91820,10 +91931,10 @@ function scoreSeed(s5, index2, hintTokens, changedFiles) {
|
|
|
91820
91931
|
}
|
|
91821
91932
|
return score;
|
|
91822
91933
|
}
|
|
91823
|
-
function rankSeeds(seeds, hintTokens, changedFiles) {
|
|
91934
|
+
function rankSeeds(seeds, hintTokens, changedFiles, hintHasCode) {
|
|
91824
91935
|
return seeds.map((symbol, i3) => ({
|
|
91825
91936
|
symbol,
|
|
91826
|
-
score: scoreSeed(symbol, i3, hintTokens, changedFiles),
|
|
91937
|
+
score: scoreSeed(symbol, i3, hintTokens, changedFiles, hintHasCode),
|
|
91827
91938
|
role: seedRole(symbol),
|
|
91828
91939
|
i: i3
|
|
91829
91940
|
})).sort((a2, b5) => b5.score === a2.score ? a2.i - b5.i : b5.score - a2.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
|
|
@@ -92307,6 +92418,51 @@ function looksExplanatory(hint) {
|
|
|
92307
92418
|
}
|
|
92308
92419
|
return false;
|
|
92309
92420
|
}
|
|
92421
|
+
function looksPerformance(hint) {
|
|
92422
|
+
return /\b(slow|slowness|sluggish|laggy|\blag\b|latency|perf|performance|throughput|degraded|degradation|response\s*time|p9[59])\b/i.test(
|
|
92423
|
+
hint
|
|
92424
|
+
);
|
|
92425
|
+
}
|
|
92426
|
+
function buildBehavioralWalkthrough(hint, top, ctx, flows) {
|
|
92427
|
+
const shortFile = (f5) => f5.replace(/^.*?((?:src|app|lib|packages)\/.*)$/, "$1");
|
|
92428
|
+
const isNoise = (s5) => /logger|logging|\.log\b|telemetry/i.test(s5.filePath) || /^(forcontext|getlogger|child|debug|info|warn|error|log|trace|tracer)$/i.test(s5.name);
|
|
92429
|
+
const flow = flows.slice().sort((a2, b5) => b5.steps.length - a2.steps.length)[0];
|
|
92430
|
+
const rawSteps = flow && flow.steps.length > 1 ? flow.steps : top ? [top, ...ctx?.callees ?? []] : ctx?.callees ?? [];
|
|
92431
|
+
const steps = rawSteps.filter((s5) => !isNoise(s5));
|
|
92432
|
+
const entry2 = steps[0] ?? top ?? null;
|
|
92433
|
+
const dedup = (xs) => {
|
|
92434
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92435
|
+
return xs.filter((s5) => {
|
|
92436
|
+
const k3 = `${s5.name}:${s5.filePath}`;
|
|
92437
|
+
if (seen.has(k3)) return false;
|
|
92438
|
+
seen.add(k3);
|
|
92439
|
+
return true;
|
|
92440
|
+
});
|
|
92441
|
+
};
|
|
92442
|
+
const PERSIST_RE = /(^|[._])(save|insert|upsert|persist|bulkwrite|find|findone|findmany|aggregate|delete|remove)\b|repositor|\brepo\b|\.model|entity|prisma|mongoose|typeorm|knex|\bdao\b|datasource/i;
|
|
92443
|
+
const EXTERNAL_RE = /(^|[._])(request|publish|enqueue|dispatch|emit|notify)\b|http|axios|\bapi\b|webhook|\bqueue\b|processor|producer|gateway|\.client\b/i;
|
|
92444
|
+
const pool = dedup([...steps, ...ctx?.callees ?? []]).filter((s5) => !isNoise(s5));
|
|
92445
|
+
const persistence = dedup(pool.filter((s5) => PERSIST_RE.test(s5.name) || PERSIST_RE.test(s5.filePath))).slice(0, 6).map((s5) => `${s5.name} (${shortFile(s5.filePath)})`);
|
|
92446
|
+
const persistNames = new Set(persistence.map((p5) => p5.split(" (")[0]));
|
|
92447
|
+
const externalCalls = dedup(
|
|
92448
|
+
pool.filter((s5) => (EXTERNAL_RE.test(s5.name) || EXTERNAL_RE.test(s5.filePath)) && !persistNames.has(s5.name))
|
|
92449
|
+
).slice(0, 6).map((s5) => `${s5.name} (${shortFile(s5.filePath)})`);
|
|
92450
|
+
const lines = [];
|
|
92451
|
+
if (entry2) {
|
|
92452
|
+
const loc = entry2.startLine ? `${shortFile(entry2.filePath)}:${entry2.startLine}` : shortFile(entry2.filePath);
|
|
92453
|
+
lines.push(`Entry point: \`${entry2.name}\` (${loc}).`);
|
|
92454
|
+
}
|
|
92455
|
+
if (steps.length > 1) {
|
|
92456
|
+
const chain = steps.slice(0, 12).map((s5) => s5.name).join(" \u2192 ");
|
|
92457
|
+
lines.push(`Flow: ${chain}${steps.length > 12 ? " \u2192 \u2026" : ""}${flow ? ` (${flow.steps.length} steps)` : ""}.`);
|
|
92458
|
+
} else if (entry2) {
|
|
92459
|
+
lines.push(`No multi-step execution flow was indexed for \`${entry2.name}\` \u2014 showing its direct calls.`);
|
|
92460
|
+
}
|
|
92461
|
+
if (externalCalls.length > 0) lines.push(`Calls out to: ${externalCalls.join(", ")}.`);
|
|
92462
|
+
if (persistence.length > 0) lines.push(`Persists via: ${persistence.join(", ")}.`);
|
|
92463
|
+
if (lines.length === 0) lines.push("No execution flow could be reconstructed for this query from the source graph.");
|
|
92464
|
+
return { question: hint, entry: entry2, steps, externalCalls, persistence, narrative: lines.join("\n") };
|
|
92465
|
+
}
|
|
92310
92466
|
function formatRegressionCitation(recentChanges, changes, seedFile) {
|
|
92311
92467
|
const matchesSeed = (f5) => seedFile === void 0 || f5 === seedFile || seedFile.endsWith(f5) || f5.endsWith(seedFile);
|
|
92312
92468
|
const all = recentChanges?.commits ?? [];
|
|
@@ -92366,13 +92522,14 @@ async function investigate(input, deps) {
|
|
|
92366
92522
|
const changedFilePaths = recentChanges && recentChanges.changedFiles.length > 0 ? new Set(recentChanges.changedFiles) : void 0;
|
|
92367
92523
|
const rawSeeds = code ? await code.searchSymbols(hint, 5) : [];
|
|
92368
92524
|
const hintTokens = [...new Set(tokenize(hint))];
|
|
92369
|
-
const
|
|
92525
|
+
const hintHasCode = /\b(?=[A-Z0-9_]*[0-9_])[A-Z][A-Z0-9_]{3,}\b/.test(hint);
|
|
92526
|
+
const ranked = rankSeeds(rawSeeds, hintTokens, changedFilePaths, hintHasCode);
|
|
92370
92527
|
let seeds = ranked.map((r) => r.symbol);
|
|
92371
92528
|
let top = seeds[0];
|
|
92372
92529
|
if (top && code && isTypeLikeName(top.name)) {
|
|
92373
92530
|
const base2 = executableBaseName(top.name);
|
|
92374
92531
|
if (base2) {
|
|
92375
|
-
const altTop = rankSeeds(await code.searchSymbols(base2, 5), hintTokens).find(
|
|
92532
|
+
const altTop = rankSeeds(await code.searchSymbols(base2, 5), hintTokens, void 0, hintHasCode).find(
|
|
92376
92533
|
(r) => !isTypeLikeName(r.symbol.name)
|
|
92377
92534
|
)?.symbol;
|
|
92378
92535
|
if (altTop && altTop.id !== top.id) {
|
|
@@ -92432,7 +92589,8 @@ async function investigate(input, deps) {
|
|
|
92432
92589
|
"slow"
|
|
92433
92590
|
]);
|
|
92434
92591
|
const meaningfulHintTokens = hintTokens.filter((t2) => !GENERIC_HINT_TOKENS.has(t2));
|
|
92435
|
-
const seedIsLowConfidence = top !== void 0 && logReseed === null &&
|
|
92592
|
+
const seedIsLowConfidence = top !== void 0 && logReseed === null && (top.score ?? 0) < 0.5 && // a strong exact-content/colocated source match is authoritative (gap 6)
|
|
92593
|
+
meaningfulHintTokens.length > 0 && !meaningfulHintTokens.some((t2) => seedHay.includes(t2));
|
|
92436
92594
|
if (!top && !degradedNoSource) {
|
|
92437
92595
|
const report2 = {
|
|
92438
92596
|
id: globalThis.crypto.randomUUID(),
|
|
@@ -92569,6 +92727,8 @@ async function investigate(input, deps) {
|
|
|
92569
92727
|
const directLogEvIds = [];
|
|
92570
92728
|
const ambientLogEvIds = [];
|
|
92571
92729
|
const directSignatures = [];
|
|
92730
|
+
const directEvIdByKey = /* @__PURE__ */ new Map();
|
|
92731
|
+
const seedEmittedJoins = [];
|
|
92572
92732
|
const entityEvIds = [];
|
|
92573
92733
|
const entitySummaries = [];
|
|
92574
92734
|
let logsCollected = false;
|
|
@@ -92643,13 +92803,17 @@ async function investigate(input, deps) {
|
|
|
92643
92803
|
if (relevanceClass === "direct") {
|
|
92644
92804
|
directLogEvIds.push(ev.id);
|
|
92645
92805
|
directSignatures.push({ key: s5.key, count: s5.count, message: s5.sampleMessage ?? "" });
|
|
92806
|
+
if (!directEvIdByKey.has(s5.key)) directEvIdByKey.set(s5.key, ev.id);
|
|
92646
92807
|
} else ambientLogEvIds.push(ev.id);
|
|
92647
92808
|
}
|
|
92648
|
-
const seedSnippet = ctx?.snippet !== void 0 && ctx.snippet !== null ? ctx.snippet : void 0;
|
|
92809
|
+
const seedSnippet = (ctx?.sourceBody !== void 0 && ctx.sourceBody !== null ? ctx.sourceBody : void 0) ?? (ctx?.snippet !== void 0 && ctx.snippet !== null ? ctx.snippet : void 0);
|
|
92649
92810
|
const sourceCodes = extractEventCodes(input.hint, top?.name, seedSnippet);
|
|
92811
|
+
const seedEmittedCodes = new Set(
|
|
92812
|
+
seedSnippet !== void 0 ? extractEventCodes(seedSnippet) : []
|
|
92813
|
+
);
|
|
92650
92814
|
const joinedCodes = new Set(directSignatures.map((d3) => d3.key));
|
|
92651
92815
|
for (const code2 of sourceCodes) {
|
|
92652
|
-
|
|
92816
|
+
const alreadyJoined = joinedCodes.has(code2);
|
|
92653
92817
|
joinedCodes.add(code2);
|
|
92654
92818
|
try {
|
|
92655
92819
|
const codeAnalysis = await deps.logs.analyzeErrors({
|
|
@@ -92659,6 +92823,21 @@ async function investigate(input, deps) {
|
|
|
92659
92823
|
});
|
|
92660
92824
|
const sig = codeAnalysis.signatures.find((s5) => s5.key === code2);
|
|
92661
92825
|
if (sig === void 0 || sig.count <= 0) continue;
|
|
92826
|
+
if (alreadyJoined) {
|
|
92827
|
+
if (seedEmittedCodes.has(code2)) {
|
|
92828
|
+
const existingEvId = directEvIdByKey.get(code2);
|
|
92829
|
+
if (existingEvId !== void 0) {
|
|
92830
|
+
seedEmittedJoins.push({
|
|
92831
|
+
code: code2,
|
|
92832
|
+
count: sig.count,
|
|
92833
|
+
message: sig.sampleMessage ?? "",
|
|
92834
|
+
evId: existingEvId,
|
|
92835
|
+
isNew: sig.isNew ?? false
|
|
92836
|
+
});
|
|
92837
|
+
}
|
|
92838
|
+
}
|
|
92839
|
+
continue;
|
|
92840
|
+
}
|
|
92662
92841
|
const links = top !== void 0 ? top.startLine !== void 0 && top.startLine > 0 ? { symbolId: top.id, file: top.filePath, line: top.startLine } : { symbolId: top.id, file: top.filePath } : {};
|
|
92663
92842
|
const ev = mkEv(
|
|
92664
92843
|
"log",
|
|
@@ -92677,7 +92856,8 @@ async function investigate(input, deps) {
|
|
|
92677
92856
|
sampleMessage: sig.sampleMessage ?? null,
|
|
92678
92857
|
relevanceClass: "direct",
|
|
92679
92858
|
relevanceReason: `exact event_code match to the source signal (${code2})`,
|
|
92680
|
-
crossSignalJoin: true
|
|
92859
|
+
crossSignalJoin: true,
|
|
92860
|
+
seedEmitted: seedEmittedCodes.has(code2)
|
|
92681
92861
|
},
|
|
92682
92862
|
links,
|
|
92683
92863
|
sig.lastSeen || void 0,
|
|
@@ -92692,6 +92872,16 @@ async function investigate(input, deps) {
|
|
|
92692
92872
|
count: sig.count,
|
|
92693
92873
|
message: sig.sampleMessage ?? ""
|
|
92694
92874
|
});
|
|
92875
|
+
if (!directEvIdByKey.has(code2)) directEvIdByKey.set(code2, ev.id);
|
|
92876
|
+
if (seedEmittedCodes.has(code2)) {
|
|
92877
|
+
seedEmittedJoins.push({
|
|
92878
|
+
code: code2,
|
|
92879
|
+
count: sig.count,
|
|
92880
|
+
message: sig.sampleMessage ?? "",
|
|
92881
|
+
evId: ev.id,
|
|
92882
|
+
isNew: sig.isNew ?? false
|
|
92883
|
+
});
|
|
92884
|
+
}
|
|
92695
92885
|
} catch {
|
|
92696
92886
|
}
|
|
92697
92887
|
}
|
|
@@ -93277,6 +93467,29 @@ async function investigate(input, deps) {
|
|
|
93277
93467
|
metadata: { blastRadius }
|
|
93278
93468
|
});
|
|
93279
93469
|
}
|
|
93470
|
+
if (seedEmittedJoins.length > 0 && top && seedEv) {
|
|
93471
|
+
const ordered = [...seedEmittedJoins].sort((a2, b5) => b5.count - a2.count);
|
|
93472
|
+
const lead = ordered[0];
|
|
93473
|
+
if (lead !== void 0) {
|
|
93474
|
+
const sourceEvidenceIds = [
|
|
93475
|
+
.../* @__PURE__ */ new Set([...ordered.map((j3) => j3.evId), seedEv.id])
|
|
93476
|
+
];
|
|
93477
|
+
const volumeBoost = Math.min(0.1, Math.log10(Math.max(1, lead.count)) * 0.025);
|
|
93478
|
+
const baseScore = clamp015(0.6 + volumeBoost);
|
|
93479
|
+
const msgClause = lead.message ? ` \u2014 "${lead.message.replace(/\s+/g, " ").trim().slice(0, 90)}"` : "";
|
|
93480
|
+
causeInputs.push({
|
|
93481
|
+
id: "cause:seed-emitted-error",
|
|
93482
|
+
title: `Runtime error ${lead.code} (${lead.count}x in Elasticsearch) is raised by ${top.name} \u2014 the likely failure${msgClause}`.slice(
|
|
93483
|
+
0,
|
|
93484
|
+
220
|
|
93485
|
+
),
|
|
93486
|
+
category: "error-correlation",
|
|
93487
|
+
sourceEvidenceIds,
|
|
93488
|
+
baseScore,
|
|
93489
|
+
metadata: { blastRadius, seedEmitted: true, code: lead.code, count: lead.count }
|
|
93490
|
+
});
|
|
93491
|
+
}
|
|
93492
|
+
}
|
|
93280
93493
|
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
|
|
93281
93494
|
const firstQueue = queueHits[0];
|
|
93282
93495
|
const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
|
|
@@ -93395,16 +93608,34 @@ async function investigate(input, deps) {
|
|
|
93395
93608
|
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. ` : "";
|
|
93396
93609
|
let summary = headlineCause ? headlineLinked ? `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. Top suspected cause: ${headlineCause.title}.` : `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. No cause is structurally linked to the seed; the strongest co-occurring signal (a lead to verify, not a diagnosis) is: ${headlineCause.title}.` : `${seedDisclaimer}${banner2}Investigation of "${hint}" ${scope}. No dominant suspected cause emerged from the available ${degradedNoSource ? "runtime" : "structural"} evidence.`;
|
|
93397
93610
|
const hasIncidentSignal = directLogEvIds.length > 0 || latencyMetricEvIds.length > 0 || queueMetricEvIds.length > 0 || dataAnomalyEvIds.length > 0 || queueFailureSignals.length > 0 || findings2.some((f5) => f5.kind === "anomaly") || headlineCause !== void 0;
|
|
93398
|
-
const explanatoryHint = looksExplanatory(hint)
|
|
93611
|
+
const explanatoryHint = looksExplanatory(hint);
|
|
93612
|
+
let behavioral;
|
|
93399
93613
|
if (explanatoryHint) {
|
|
93400
|
-
|
|
93401
|
-
|
|
93614
|
+
let flowPool = flows;
|
|
93615
|
+
const richestSeedFlow = flows.slice().sort((a2, b5) => b5.steps.length - a2.steps.length)[0];
|
|
93616
|
+
if (code && (richestSeedFlow === void 0 || richestSeedFlow.steps.length <= 1) && ctx?.callers?.length) {
|
|
93617
|
+
const ENTRY_RE = /controller|resolver|route|handler|command|consumer|listener|gateway|cron|scheduler|webhook|middleware/i;
|
|
93618
|
+
const entryCallers = ctx.callers.filter((c4) => ENTRY_RE.test(c4.name) || ENTRY_RE.test(c4.filePath)).slice(0, 3);
|
|
93619
|
+
for (const c4 of entryCallers) {
|
|
93620
|
+
try {
|
|
93621
|
+
flowPool = flowPool.concat(await code.flowsFor(c4.id));
|
|
93622
|
+
} catch {
|
|
93623
|
+
}
|
|
93624
|
+
}
|
|
93625
|
+
}
|
|
93626
|
+
behavioral = buildBehavioralWalkthrough(hint, top, ctx ?? void 0, flowPool);
|
|
93627
|
+
const noisy = hasIncidentSignal ? ' (note: live error signals also exist in this codebase \u2014 rephrase as a symptom like "X failing" for a fault hunt)' : "";
|
|
93628
|
+
summary = `${seedDisclaimer}${banner2}"${hint}" reads as a "how does it work" question \u2014 here is the code path${noisy}:
|
|
93629
|
+
|
|
93630
|
+
` + behavioral.narrative;
|
|
93631
|
+
}
|
|
93632
|
+
if (!explanatoryHint && looksPerformance(hint) && deps.metrics == null && seedIsLowConfidence) {
|
|
93633
|
+
summary = `${banner2}"${hint}" reads as a latency/performance question, but no metrics connector (Grafana) is configured \u2014 response-time and error-rate trends can't be assessed from source structure alone${top ? ` (the closest structural match, ${top.name}, is a low-confidence guess, not a diagnosis)` : ""}. Connect a metrics source (\`horus connect grafana\`) and re-run, or name the specific slow operation.`;
|
|
93402
93634
|
}
|
|
93403
93635
|
const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
|
|
93404
|
-
if (explanatoryHint) {
|
|
93405
|
-
const explainTarget = top?.name ?? input.service ?? hint;
|
|
93636
|
+
if (explanatoryHint && behavioral?.entry) {
|
|
93406
93637
|
nextActions.unshift(
|
|
93407
|
-
`
|
|
93638
|
+
`Trace the full call graph of the entry point: \`horus explain ${behavioral.entry.name}\`.`
|
|
93408
93639
|
);
|
|
93409
93640
|
}
|
|
93410
93641
|
if (degradedNoSource) {
|
|
@@ -93436,6 +93667,7 @@ async function investigate(input, deps) {
|
|
|
93436
93667
|
ownership: ownershipEstimate,
|
|
93437
93668
|
causeChains: causeChains.length > 0 ? causeChains : void 0,
|
|
93438
93669
|
...recentChanges !== void 0 ? { recentChanges } : {},
|
|
93670
|
+
...behavioral !== void 0 ? { behavioral } : {},
|
|
93439
93671
|
...degradedNoSource ? { degraded: { sourceIntelligence: false, reason: "source-intelligence host unreachable" } } : {}
|
|
93440
93672
|
};
|
|
93441
93673
|
const connectorFlags = deps.connectors ? {
|
|
@@ -96906,6 +97138,12 @@ async function runIndex(opts) {
|
|
|
96906
97138
|
console.error(import_picocolors3.default.red("horus-source not found on PATH. Install it: pip install horus-source"));
|
|
96907
97139
|
return 1;
|
|
96908
97140
|
}
|
|
97141
|
+
try {
|
|
97142
|
+
await assertSourceVersionPinned();
|
|
97143
|
+
} catch (err2) {
|
|
97144
|
+
console.error(import_picocolors3.default.red(` ${err2.message}`));
|
|
97145
|
+
return 1;
|
|
97146
|
+
}
|
|
96909
97147
|
if (!isAnalyzed(root)) {
|
|
96910
97148
|
console.log(import_picocolors3.default.dim(" analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"));
|
|
96911
97149
|
try {
|
|
@@ -109867,6 +110105,8 @@ function ensureHostReasonHint(reason) {
|
|
|
109867
110105
|
switch (reason) {
|
|
109868
110106
|
case "source-unavailable":
|
|
109869
110107
|
return "horus-source is not installed \u2014 install it: pip install horus-source";
|
|
110108
|
+
case "version-mismatch":
|
|
110109
|
+
return "the installed horus-source backend does not match the version Horus is pinned to \u2014 reinstall the pinned version (run `horus status` to see it)";
|
|
109870
110110
|
case "not-analyzed":
|
|
109871
110111
|
return "this repo has not been indexed yet \u2014 run: horus index";
|
|
109872
110112
|
case "bad-url":
|
|
@@ -109878,6 +110118,11 @@ function ensureHostReasonHint(reason) {
|
|
|
109878
110118
|
async function ensureSourceHost(root, hostUrl, opts = {}) {
|
|
109879
110119
|
if (await isHostHealthy(hostUrl)) return { ok: true, hostUrl };
|
|
109880
110120
|
if (!await sourceAvailable()) return { ok: false, reason: "source-unavailable" };
|
|
110121
|
+
try {
|
|
110122
|
+
await assertSourceVersionPinned();
|
|
110123
|
+
} catch {
|
|
110124
|
+
return { ok: false, reason: "version-mismatch" };
|
|
110125
|
+
}
|
|
109881
110126
|
if (!isAnalyzed(root)) return { ok: false, reason: "not-analyzed" };
|
|
109882
110127
|
const port = parseHostPort(hostUrl);
|
|
109883
110128
|
if (port === null) return { ok: false, reason: "bad-url" };
|
|
@@ -113331,16 +113576,28 @@ async function runStop(opts) {
|
|
|
113331
113576
|
}
|
|
113332
113577
|
}
|
|
113333
113578
|
async function stopHost(root, hostUrl) {
|
|
113334
|
-
const alive = await isHostHealthy(hostUrl);
|
|
113335
|
-
if (!alive) {
|
|
113336
|
-
console.log(import_picocolors36.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
113337
|
-
return 0;
|
|
113338
|
-
}
|
|
113339
113579
|
const port = extractPort(hostUrl);
|
|
113340
113580
|
if (port === null) {
|
|
113341
113581
|
console.error(import_picocolors36.default.red(`Cannot determine port from host URL: ${hostUrl}`));
|
|
113342
113582
|
return 1;
|
|
113343
113583
|
}
|
|
113584
|
+
const healthy = await isHostHealthy(hostUrl);
|
|
113585
|
+
if (!healthy) {
|
|
113586
|
+
const owned = readSpawnedHost(root);
|
|
113587
|
+
const wrapperAlive = owned !== null && await getProcessInfo(owned.pid) !== null;
|
|
113588
|
+
if (!wrapperAlive) {
|
|
113589
|
+
const res = await stopBackendServerPid(root, port);
|
|
113590
|
+
if (res === "failed") return 1;
|
|
113591
|
+
if (res !== "stopped") console.log(import_picocolors36.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
113592
|
+
await cleanupSpawnedRecord(root);
|
|
113593
|
+
return 0;
|
|
113594
|
+
}
|
|
113595
|
+
console.log(
|
|
113596
|
+
import_picocolors36.default.dim(
|
|
113597
|
+
`Host ${hostUrl} is unreachable but pid ${owned.pid} is still running \u2014 terminating a stuck host.`
|
|
113598
|
+
)
|
|
113599
|
+
);
|
|
113600
|
+
}
|
|
113344
113601
|
const spawned = readSpawnedHost(root);
|
|
113345
113602
|
if (spawned === null) {
|
|
113346
113603
|
console.error(
|
|
@@ -113373,21 +113630,18 @@ async function stopHost(root, hostUrl) {
|
|
|
113373
113630
|
}
|
|
113374
113631
|
const info2 = await getProcessInfo(spawned.pid);
|
|
113375
113632
|
if (info2 === null) {
|
|
113376
|
-
|
|
113377
|
-
|
|
113378
|
-
|
|
113379
|
-
|
|
113633
|
+
const res = await stopBackendServerPid(root, port);
|
|
113634
|
+
if (res === "failed") return 1;
|
|
113635
|
+
if (res !== "stopped") {
|
|
113636
|
+
console.log(import_picocolors36.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
|
|
113380
113637
|
}
|
|
113638
|
+
await cleanupSpawnedRecord(root);
|
|
113381
113639
|
return 0;
|
|
113382
113640
|
}
|
|
113383
|
-
|
|
113384
|
-
const hostPortRe = new RegExp(
|
|
113385
|
-
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${portStr}(?=\\s|$)`
|
|
113386
|
-
);
|
|
113387
|
-
if (!hostPortRe.test(info2.args)) {
|
|
113641
|
+
if (!hostArgvRegex(port).test(info2.args)) {
|
|
113388
113642
|
console.error(
|
|
113389
113643
|
import_picocolors36.default.red(
|
|
113390
|
-
`Pid ${spawned.pid} args do not match "horus-source host --port ${
|
|
113644
|
+
`Pid ${spawned.pid} args do not match "horus-source host --port ${port}". Got: "${info2.args.slice(0, 120)}". Aborting for safety.`
|
|
113391
113645
|
)
|
|
113392
113646
|
);
|
|
113393
113647
|
return 1;
|
|
@@ -113440,11 +113694,57 @@ async function stopHost(root, hostUrl) {
|
|
|
113440
113694
|
`${import_picocolors36.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors36.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
|
|
113441
113695
|
);
|
|
113442
113696
|
}
|
|
113697
|
+
if (await stopBackendServerPid(root, port) === "failed") return 1;
|
|
113698
|
+
await cleanupSpawnedRecord(root);
|
|
113699
|
+
return 0;
|
|
113700
|
+
}
|
|
113701
|
+
function hostArgvRegex(port) {
|
|
113702
|
+
return new RegExp(
|
|
113703
|
+
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${String(port)}(?=\\s|$)`
|
|
113704
|
+
);
|
|
113705
|
+
}
|
|
113706
|
+
async function cleanupSpawnedRecord(root) {
|
|
113443
113707
|
try {
|
|
113444
113708
|
await unlinkAsync((0, import_node_path18.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
113445
113709
|
} catch {
|
|
113446
113710
|
}
|
|
113447
|
-
|
|
113711
|
+
}
|
|
113712
|
+
async function stopBackendServerPid(root, port) {
|
|
113713
|
+
const rec = readSourceHostPid(root);
|
|
113714
|
+
if (!rec) return "none";
|
|
113715
|
+
if (Number.isFinite(rec.port) && rec.port !== port) return "none";
|
|
113716
|
+
if (rec.repoPath && rec.repoPath !== root) return "none";
|
|
113717
|
+
const info2 = await getProcessInfo(rec.pid);
|
|
113718
|
+
if (info2 === null) return "none";
|
|
113719
|
+
if (!hostArgvRegex(port).test(info2.args)) {
|
|
113720
|
+
console.error(
|
|
113721
|
+
import_picocolors36.default.red(
|
|
113722
|
+
`Backend host pid ${rec.pid} args do not match "horus-source host --port ${port}". Not signalling \u2014 possible stale source/host.json or PID reuse.`
|
|
113723
|
+
)
|
|
113724
|
+
);
|
|
113725
|
+
return "failed";
|
|
113726
|
+
}
|
|
113727
|
+
try {
|
|
113728
|
+
process.kill(rec.pid, "SIGTERM");
|
|
113729
|
+
} catch (err2) {
|
|
113730
|
+
if (err2.code === "ESRCH") return "stopped";
|
|
113731
|
+
console.error(import_picocolors36.default.red(`Failed to signal pid ${rec.pid}: ${err2.message}`));
|
|
113732
|
+
return "failed";
|
|
113733
|
+
}
|
|
113734
|
+
const deadline = Date.now() + STOP_WAIT_MS;
|
|
113735
|
+
while (Date.now() < deadline) {
|
|
113736
|
+
await sleep(STOP_POLL_MS);
|
|
113737
|
+
if (await getProcessInfo(rec.pid) === null) {
|
|
113738
|
+
console.log(
|
|
113739
|
+
`${import_picocolors36.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors36.default.dim(`(pid ${rec.pid}, port ${port})`) + ` for ${root}`
|
|
113740
|
+
);
|
|
113741
|
+
return "stopped";
|
|
113742
|
+
}
|
|
113743
|
+
}
|
|
113744
|
+
console.error(
|
|
113745
|
+
import_picocolors36.default.red(`Host pid ${rec.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`)
|
|
113746
|
+
);
|
|
113747
|
+
return "failed";
|
|
113448
113748
|
}
|
|
113449
113749
|
async function stopAll() {
|
|
113450
113750
|
const registry2 = readRegistry();
|
|
@@ -113458,8 +113758,10 @@ async function stopAll() {
|
|
|
113458
113758
|
for (const [name2, entry2] of projects2) {
|
|
113459
113759
|
const hostUrl = readSourceHostUrl(entry2.root);
|
|
113460
113760
|
if (!hostUrl) continue;
|
|
113461
|
-
const
|
|
113462
|
-
if (!
|
|
113761
|
+
const healthy = await isHostHealthy(hostUrl);
|
|
113762
|
+
if (!healthy && readSpawnedHost(entry2.root) === null && readSourceHostPid(entry2.root) === null) {
|
|
113763
|
+
continue;
|
|
113764
|
+
}
|
|
113463
113765
|
console.log(` Stopping ${import_picocolors36.default.bold(name2)} ${import_picocolors36.default.dim(`(${hostUrl})`)}`);
|
|
113464
113766
|
const code = await stopHost(entry2.root, hostUrl);
|
|
113465
113767
|
if (code === 0) stopped++;
|