@merittdev/horus 0.1.13 → 0.1.14
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 +2197 -2057
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -50314,7 +50314,7 @@ init_cjs_shims();
|
|
|
50314
50314
|
|
|
50315
50315
|
// ../../packages/core/src/version.ts
|
|
50316
50316
|
init_cjs_shims();
|
|
50317
|
-
var HORUS_VERSION = true ? "0.1.
|
|
50317
|
+
var HORUS_VERSION = true ? "0.1.14" : "dev";
|
|
50318
50318
|
var PINNED_AXON_VERSION = "1.0.7";
|
|
50319
50319
|
var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
|
|
50320
50320
|
|
|
@@ -54494,6 +54494,37 @@ function ensureCredentialGitignore(root) {
|
|
|
54494
54494
|
function readLocalConfig(path) {
|
|
54495
54495
|
return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
|
|
54496
54496
|
}
|
|
54497
|
+
var LOCAL_SECRETS_FILE = "secrets.local.json";
|
|
54498
|
+
function localSecretsPath(root) {
|
|
54499
|
+
return (0, import_node_path.join)(root, HORUS_DIR, LOCAL_SECRETS_FILE);
|
|
54500
|
+
}
|
|
54501
|
+
function readLocalSecrets(root) {
|
|
54502
|
+
const p = localSecretsPath(root);
|
|
54503
|
+
if (!(0, import_node_fs.existsSync)(p)) return {};
|
|
54504
|
+
try {
|
|
54505
|
+
return JSON.parse((0, import_node_fs.readFileSync)(p, "utf8"));
|
|
54506
|
+
} catch {
|
|
54507
|
+
return {};
|
|
54508
|
+
}
|
|
54509
|
+
}
|
|
54510
|
+
function writeLocalSecrets(root, secrets) {
|
|
54511
|
+
const dir = (0, import_node_path.join)(root, HORUS_DIR);
|
|
54512
|
+
(0, import_node_fs.mkdirSync)(dir, { recursive: true });
|
|
54513
|
+
const p = localSecretsPath(root);
|
|
54514
|
+
(0, import_node_fs.writeFileSync)(p, JSON.stringify(secrets, null, 2) + "\n", { mode: 384 });
|
|
54515
|
+
(0, import_node_fs.chmodSync)(p, 384);
|
|
54516
|
+
const gitignorePath = (0, import_node_path.join)(dir, ".gitignore");
|
|
54517
|
+
const entry2 = LOCAL_SECRETS_FILE;
|
|
54518
|
+
if ((0, import_node_fs.existsSync)(gitignorePath)) {
|
|
54519
|
+
const existing = (0, import_node_fs.readFileSync)(gitignorePath, "utf8");
|
|
54520
|
+
if (!existing.split("\n").some((l) => l.trim() === entry2)) {
|
|
54521
|
+
(0, import_node_fs.writeFileSync)(gitignorePath, existing.trimEnd() + "\n" + entry2 + "\n");
|
|
54522
|
+
}
|
|
54523
|
+
} else {
|
|
54524
|
+
(0, import_node_fs.writeFileSync)(gitignorePath, entry2 + "\n");
|
|
54525
|
+
}
|
|
54526
|
+
return p;
|
|
54527
|
+
}
|
|
54497
54528
|
function ensureProjectGitignore(root) {
|
|
54498
54529
|
if (!(0, import_node_fs.existsSync)((0, import_node_path.join)(root, ".git"))) return;
|
|
54499
54530
|
const gitignorePath = (0, import_node_path.join)(root, ".gitignore");
|
|
@@ -54912,12 +54943,18 @@ ${details}`);
|
|
|
54912
54943
|
async function loadConfigFile(target) {
|
|
54913
54944
|
if (target.endsWith(".json")) {
|
|
54914
54945
|
const file = readLocalConfig(target);
|
|
54946
|
+
const root = (0, import_node_path2.dirname)((0, import_node_path2.dirname)(target));
|
|
54947
|
+
const secrets = readLocalSecrets(root);
|
|
54948
|
+
let ai = file.ai;
|
|
54949
|
+
if (secrets.anthropic?.apiKey) {
|
|
54950
|
+
ai = { ...ai ?? {}, anthropic: { ...ai?.anthropic ?? {}, apiKey: secrets.anthropic.apiKey } };
|
|
54951
|
+
}
|
|
54915
54952
|
const raw = {
|
|
54916
54953
|
projects: file.project ? [file.project] : [],
|
|
54917
54954
|
database: file.database ?? {
|
|
54918
54955
|
url: process.env["DATABASE_URL"] ?? DEFAULT_DB_URL
|
|
54919
54956
|
},
|
|
54920
|
-
...
|
|
54957
|
+
...ai !== void 0 ? { ai } : {}
|
|
54921
54958
|
};
|
|
54922
54959
|
return parseConfig(raw, target);
|
|
54923
54960
|
}
|
|
@@ -55313,14 +55350,18 @@ var AxonCodeProvider = class {
|
|
|
55313
55350
|
async rows(query) {
|
|
55314
55351
|
return (await this.client.cypher(query)).rows;
|
|
55315
55352
|
}
|
|
55316
|
-
cypherRowToSymbol(row, idIdx, nameIdx, fileIdx, lineIdx) {
|
|
55353
|
+
cypherRowToSymbol(row, idIdx, nameIdx, fileIdx, lineIdx, classNameIdx) {
|
|
55317
55354
|
const startLine = lineIdx != null && row[lineIdx] != null ? Number(row[lineIdx]) : void 0;
|
|
55318
|
-
|
|
55355
|
+
const sym = {
|
|
55319
55356
|
id: String(row[idIdx] ?? ""),
|
|
55320
55357
|
name: String(row[nameIdx] ?? ""),
|
|
55321
55358
|
filePath: String(row[fileIdx] ?? ""),
|
|
55322
55359
|
startLine
|
|
55323
55360
|
};
|
|
55361
|
+
if (classNameIdx != null && row[classNameIdx] != null) {
|
|
55362
|
+
sym.className = String(row[classNameIdx]);
|
|
55363
|
+
}
|
|
55364
|
+
return sym;
|
|
55324
55365
|
}
|
|
55325
55366
|
// --- contract ------------------------------------------------------------
|
|
55326
55367
|
async health() {
|
|
@@ -55357,13 +55398,35 @@ var AxonCodeProvider = class {
|
|
|
55357
55398
|
...exactSymbols,
|
|
55358
55399
|
...semantic.map(({ r }) => ({ id: r.nodeId, name: r.name, filePath: r.filePath }))
|
|
55359
55400
|
];
|
|
55360
|
-
return combined.slice(0, limit);
|
|
55401
|
+
return this.hydrateLines(combined.slice(0, limit));
|
|
55402
|
+
}
|
|
55403
|
+
/** Attach start/end line ranges to symbols via a single batched id lookup. */
|
|
55404
|
+
async hydrateLines(symbols) {
|
|
55405
|
+
const ids2 = symbols.map((s) => s.id).filter(Boolean);
|
|
55406
|
+
if (ids2.length === 0) return symbols;
|
|
55407
|
+
const idList = ids2.map((id) => `"${this.escapeId(id)}"`).join(", ");
|
|
55408
|
+
const rows = await this.rows(`MATCH (n) WHERE n.id IN [${idList}] RETURN n.id, n.start_line, n.end_line`).catch(() => []);
|
|
55409
|
+
const lineById = /* @__PURE__ */ new Map();
|
|
55410
|
+
for (const row of rows) {
|
|
55411
|
+
const id = String(row[0] ?? "");
|
|
55412
|
+
const start = row[1] != null ? Number(row[1]) : void 0;
|
|
55413
|
+
const end = row[2] != null ? Number(row[2]) : void 0;
|
|
55414
|
+
lineById.set(id, { start, end });
|
|
55415
|
+
}
|
|
55416
|
+
return symbols.map((s) => {
|
|
55417
|
+
const l = lineById.get(s.id);
|
|
55418
|
+
if (l === void 0) return s;
|
|
55419
|
+
const out = { ...s };
|
|
55420
|
+
if (l.start !== void 0 && Number.isFinite(l.start)) out.startLine = l.start;
|
|
55421
|
+
if (l.end !== void 0 && Number.isFinite(l.end)) out.endLine = l.end;
|
|
55422
|
+
return out;
|
|
55423
|
+
});
|
|
55361
55424
|
}
|
|
55362
55425
|
async context(symbolId) {
|
|
55363
55426
|
const E = this.escapeId(symbolId);
|
|
55364
55427
|
const nodeQuery = `MATCH (n) WHERE n.id = "${E}" RETURN n.id, n.name, n.file_path, n.start_line, n.end_line, n.signature, n.class_name, n.language, n.is_dead, n.content LIMIT 1`;
|
|
55365
|
-
const calleesQuery = `MATCH (n)-[r:CodeRelation]->(m) WHERE n.id = "${E}" AND r.rel_type = "calls" RETURN m.id, m.name, m.file_path, m.start_line`;
|
|
55366
|
-
const callersQuery = `MATCH (m)-[r:CodeRelation]->(n) WHERE n.id = "${E}" AND r.rel_type = "calls" RETURN m.id, m.name, m.file_path, m.start_line`;
|
|
55428
|
+
const calleesQuery = `MATCH (n)-[r:CodeRelation]->(m) WHERE n.id = "${E}" AND r.rel_type = "calls" RETURN m.id, m.name, m.file_path, m.start_line, m.class_name`;
|
|
55429
|
+
const callersQuery = `MATCH (m)-[r:CodeRelation]->(n) WHERE n.id = "${E}" AND r.rel_type = "calls" RETURN m.id, m.name, m.file_path, m.start_line, m.class_name`;
|
|
55367
55430
|
const usesTypeQuery = `MATCH (n)-[r:CodeRelation]->(t) WHERE n.id = "${E}" AND r.rel_type = "uses_type" RETURN t.id, t.name, t.file_path, t.start_line`;
|
|
55368
55431
|
const communityQuery = `MATCH (n)-[r:CodeRelation]->(c:Community) WHERE n.id = "${E}" AND r.rel_type = "member_of" RETURN c.id, c.name LIMIT 1`;
|
|
55369
55432
|
const fileQuery = `MATCH (f:File)-[r:CodeRelation]->(n) WHERE n.id = "${E}" AND r.rel_type = "defines" RETURN f.id, f.file_path LIMIT 1`;
|
|
@@ -55389,8 +55452,8 @@ var AxonCodeProvider = class {
|
|
|
55389
55452
|
const isDead = Boolean(node?.[8]);
|
|
55390
55453
|
const content = node?.[9];
|
|
55391
55454
|
const snippet = typeof content === "string" ? content.slice(0, 600) : void 0;
|
|
55392
|
-
const callees = calleeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
|
|
55393
|
-
const callers = callerRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
|
|
55455
|
+
const callees = calleeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
|
|
55456
|
+
const callers = callerRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
|
|
55394
55457
|
const usesType = usesTypeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
|
|
55395
55458
|
const communityRow = communityRows[0];
|
|
55396
55459
|
const community = communityRow ? { id: String(communityRow[0] ?? ""), name: String(communityRow[1] ?? "") } : null;
|
|
@@ -67356,720 +67419,483 @@ Horus ${HORUS_VERSION}`));
|
|
|
67356
67419
|
// ../../packages/cli/src/commands/explain.ts
|
|
67357
67420
|
init_cjs_shims();
|
|
67358
67421
|
var import_picocolors2 = __toESM(require_picocolors(), 1);
|
|
67359
|
-
|
|
67360
|
-
|
|
67361
|
-
|
|
67362
|
-
|
|
67363
|
-
|
|
67364
|
-
|
|
67365
|
-
|
|
67366
|
-
|
|
67367
|
-
|
|
67368
|
-
|
|
67369
|
-
|
|
67370
|
-
|
|
67371
|
-
|
|
67372
|
-
|
|
67373
|
-
|
|
67374
|
-
|
|
67375
|
-
|
|
67376
|
-
|
|
67377
|
-
console.log(import_picocolors2.default.dim(` Tip: use an exact class or function name, e.g. "MyService" or "processOrder"`));
|
|
67378
|
-
return 1;
|
|
67422
|
+
|
|
67423
|
+
// ../../packages/engine/src/index.ts
|
|
67424
|
+
init_cjs_shims();
|
|
67425
|
+
|
|
67426
|
+
// ../../packages/engine/src/types.ts
|
|
67427
|
+
init_cjs_shims();
|
|
67428
|
+
|
|
67429
|
+
// ../../packages/engine/src/engine.ts
|
|
67430
|
+
init_cjs_shims();
|
|
67431
|
+
|
|
67432
|
+
// ../../packages/engine/src/render.ts
|
|
67433
|
+
init_cjs_shims();
|
|
67434
|
+
function shortId(id) {
|
|
67435
|
+
return id.slice(0, 8);
|
|
67436
|
+
}
|
|
67437
|
+
function symbolDisplayName(sym) {
|
|
67438
|
+
if (sym.className && sym.className.length > 0 && !sym.name.includes(".")) {
|
|
67439
|
+
return `${sym.className}.${sym.name}`;
|
|
67379
67440
|
}
|
|
67380
|
-
|
|
67381
|
-
|
|
67382
|
-
|
|
67383
|
-
if (
|
|
67384
|
-
|
|
67385
|
-
|
|
67386
|
-
|
|
67387
|
-
|
|
67388
|
-
|
|
67389
|
-
|
|
67390
|
-
|
|
67391
|
-
|
|
67392
|
-
|
|
67393
|
-
|
|
67441
|
+
return sym.name;
|
|
67442
|
+
}
|
|
67443
|
+
function formatSymbolLocation(filePath, startLine, endLine) {
|
|
67444
|
+
if (startLine === void 0 || startLine <= 0) return filePath;
|
|
67445
|
+
if (endLine !== void 0 && endLine > startLine) return `${filePath}:${startLine}-${endLine}`;
|
|
67446
|
+
return `${filePath}:${startLine}`;
|
|
67447
|
+
}
|
|
67448
|
+
function isQueueSummary(e) {
|
|
67449
|
+
const p = e.payload ?? {};
|
|
67450
|
+
return "isPaused" in p;
|
|
67451
|
+
}
|
|
67452
|
+
function fmtQueueCounts(p) {
|
|
67453
|
+
return [
|
|
67454
|
+
`${p.waiting ?? 0} waiting`,
|
|
67455
|
+
`${p.active ?? 0} active`,
|
|
67456
|
+
`${p.failed ?? 0} failed`,
|
|
67457
|
+
`${p.delayed ?? 0} delayed`
|
|
67458
|
+
].join(" \xB7 ");
|
|
67459
|
+
}
|
|
67460
|
+
function stripQueueName(title, name) {
|
|
67461
|
+
const pfx = `${name}: `;
|
|
67462
|
+
return title.startsWith(pfx) ? title.slice(pfx.length) : title;
|
|
67463
|
+
}
|
|
67464
|
+
function groupQueueEvidence(evidence2) {
|
|
67465
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
67466
|
+
for (const e of evidence2) {
|
|
67467
|
+
if (e.source !== "queue" || e.kind !== "queue-state") continue;
|
|
67468
|
+
const name = e.links?.queueName ?? "unknown";
|
|
67469
|
+
const group = map2.get(name) ?? [];
|
|
67470
|
+
group.push(e);
|
|
67471
|
+
map2.set(name, group);
|
|
67394
67472
|
}
|
|
67395
|
-
|
|
67396
|
-
|
|
67397
|
-
|
|
67398
|
-
|
|
67399
|
-
|
|
67400
|
-
|
|
67401
|
-
|
|
67402
|
-
|
|
67403
|
-
JSON.stringify(
|
|
67404
|
-
{
|
|
67405
|
-
symbol: ctx.symbol,
|
|
67406
|
-
community: ctx.community,
|
|
67407
|
-
isDead: ctx.isDead,
|
|
67408
|
-
callers: ctx.callers,
|
|
67409
|
-
callees: ctx.callees,
|
|
67410
|
-
impact,
|
|
67411
|
-
flows
|
|
67412
|
-
},
|
|
67413
|
-
null,
|
|
67414
|
-
2
|
|
67415
|
-
)
|
|
67416
|
-
);
|
|
67417
|
-
return 0;
|
|
67473
|
+
for (const [, evs] of map2) {
|
|
67474
|
+
evs.sort((a, b2) => {
|
|
67475
|
+
const as_ = isQueueSummary(a);
|
|
67476
|
+
const bs_ = isQueueSummary(b2);
|
|
67477
|
+
if (as_ && !bs_) return 1;
|
|
67478
|
+
if (!as_ && bs_) return -1;
|
|
67479
|
+
return b2.relevance - a.relevance;
|
|
67480
|
+
});
|
|
67418
67481
|
}
|
|
67419
|
-
|
|
67420
|
-
return 0;
|
|
67482
|
+
return map2;
|
|
67421
67483
|
}
|
|
67422
|
-
|
|
67423
|
-
|
|
67424
|
-
|
|
67425
|
-
|
|
67426
|
-
project = resolveEnvironment(config).project;
|
|
67427
|
-
} catch {
|
|
67428
|
-
}
|
|
67429
|
-
const { db, sql: sql2 } = createDb(config.database.url);
|
|
67430
|
-
try {
|
|
67431
|
-
const edges = await listQueueEdges(db, { queueName: query, project });
|
|
67432
|
-
return edges.length > 0;
|
|
67433
|
-
} finally {
|
|
67434
|
-
await sql2.end().catch(() => {
|
|
67435
|
-
});
|
|
67436
|
-
}
|
|
67437
|
-
} catch {
|
|
67438
|
-
return false;
|
|
67484
|
+
function queueEvidenceIds(evidence2) {
|
|
67485
|
+
const ids2 = /* @__PURE__ */ new Set();
|
|
67486
|
+
for (const e of evidence2) {
|
|
67487
|
+
if (e.source === "queue" && e.kind === "queue-state") ids2.add(e.id);
|
|
67439
67488
|
}
|
|
67489
|
+
return ids2;
|
|
67440
67490
|
}
|
|
67441
|
-
|
|
67442
|
-
|
|
67443
|
-
|
|
67444
|
-
|
|
67445
|
-
if (
|
|
67446
|
-
|
|
67447
|
-
if (
|
|
67448
|
-
|
|
67491
|
+
var CONFIDENCE_EXPLAIN_THRESHOLD = 0.8;
|
|
67492
|
+
function explainLowConfidence(r) {
|
|
67493
|
+
if (r.confidence >= CONFIDENCE_EXPLAIN_THRESHOLD) return null;
|
|
67494
|
+
const reasons = [];
|
|
67495
|
+
if (r.sourceStatus) {
|
|
67496
|
+
const notConfigured = r.sourceStatus.sources.filter((s) => s.status === "not-configured").map((s) => s.source);
|
|
67497
|
+
if (notConfigured.length > 0) {
|
|
67498
|
+
reasons.push(`no runtime data \u2014 ${notConfigured.join(", ")} not configured`);
|
|
67449
67499
|
}
|
|
67450
67500
|
}
|
|
67451
|
-
|
|
67452
|
-
|
|
67453
|
-
|
|
67454
|
-
|
|
67455
|
-
const extra = names.length - 10;
|
|
67456
|
-
return names.slice(0, 10).join(", ") + ` +${extra} more`;
|
|
67457
|
-
};
|
|
67458
|
-
const callerStr = ctx.callers.length === 0 ? import_picocolors2.default.dim("none") : formatNames(ctx.callers);
|
|
67459
|
-
const calleeStr = ctx.callees.length === 0 ? import_picocolors2.default.dim("none") : formatNames(ctx.callees);
|
|
67460
|
-
const allImpacted = impact.byDepth.flatMap((d) => d.symbols);
|
|
67461
|
-
const impactNames = allImpacted.slice(0, 8).map((s) => s.name).join(", ");
|
|
67462
|
-
const impactSuffix = allImpacted.length > 8 ? ` +${allImpacted.length - 8} more` : "";
|
|
67463
|
-
console.log("");
|
|
67464
|
-
console.log(
|
|
67465
|
-
` ${import_picocolors2.default.bold("Symbol:")} ${top.name} ${import_picocolors2.default.dim("(kind = " + kind + ")")}`
|
|
67466
|
-
);
|
|
67467
|
-
if (siblings.length > 0) {
|
|
67468
|
-
const others = siblings.map((s) => s.filePath).join(", ");
|
|
67469
|
-
console.log(
|
|
67470
|
-
import_picocolors2.default.dim(
|
|
67471
|
-
` ${siblings.length + 1} symbols named '${top.name}'; showing this one \u2014 others: ${others}`
|
|
67472
|
-
)
|
|
67501
|
+
if (r.gapAnalysis.gaps.length > 0) {
|
|
67502
|
+
const top = [...r.gapAnalysis.gaps].sort((a, b2) => b2.confidenceImpact - a.confidenceImpact)[0];
|
|
67503
|
+
reasons.push(
|
|
67504
|
+
`top gap: ${top.dimension} (\u2212${top.confidenceImpact.toFixed(2)} conf) \u2014 ${top.why}`
|
|
67473
67505
|
);
|
|
67474
67506
|
}
|
|
67475
|
-
|
|
67476
|
-
|
|
67477
|
-
console.log(
|
|
67478
|
-
` ${import_picocolors2.default.bold("Callers (" + ctx.callers.length + "):")} ${callerStr}`
|
|
67479
|
-
);
|
|
67480
|
-
console.log(
|
|
67481
|
-
` ${import_picocolors2.default.bold("Callees (" + ctx.callees.length + "):")} ${calleeStr}`
|
|
67482
|
-
);
|
|
67483
|
-
console.log(
|
|
67484
|
-
` ${import_picocolors2.default.bold("Impact:")} ${impact.affected} symbols affected` + (impactNames ? ` ${import_picocolors2.default.dim(impactNames + impactSuffix)}` : "")
|
|
67485
|
-
);
|
|
67486
|
-
console.log(` ${import_picocolors2.default.bold("Related flows:")}`);
|
|
67487
|
-
if (flows.length === 0) {
|
|
67488
|
-
console.log(` ${import_picocolors2.default.dim("none")}`);
|
|
67489
|
-
} else {
|
|
67490
|
-
for (const flow of flows) {
|
|
67491
|
-
console.log(` ${flow.name} ${import_picocolors2.default.dim("(" + flow.steps.length + " steps)")}`);
|
|
67492
|
-
}
|
|
67507
|
+
if (r.correlation.missing.length > 0) {
|
|
67508
|
+
reasons.push(`missing evidence: ${r.correlation.missing[0].note}`);
|
|
67493
67509
|
}
|
|
67494
|
-
if (
|
|
67495
|
-
|
|
67510
|
+
if (r.gapAnalysis.confidenceCeiling < 1) {
|
|
67511
|
+
reasons.push(
|
|
67512
|
+
`confidence ceiling: ${r.gapAnalysis.confidenceCeiling} \u2014 filling gaps above would raise it`
|
|
67513
|
+
);
|
|
67496
67514
|
}
|
|
67497
|
-
|
|
67515
|
+
return reasons.length > 0 ? reasons : null;
|
|
67498
67516
|
}
|
|
67499
|
-
|
|
67500
|
-
|
|
67501
|
-
|
|
67502
|
-
|
|
67503
|
-
|
|
67504
|
-
|
|
67505
|
-
|
|
67506
|
-
|
|
67507
|
-
|
|
67508
|
-
// ../../packages/stitcher/src/extract.ts
|
|
67509
|
-
init_cjs_shims();
|
|
67510
|
-
var INJECT_QUEUE_RE = /@InjectQueue\(\s*['"]([^'"]+)['"]/g;
|
|
67511
|
-
var PROCESSOR_RE = /@Processor\(\s*['"]([^'"]+)['"][^)]*\)\s*(?:@\w+[^\n]*\s*)*export\s+class\s+(\w+)/g;
|
|
67512
|
-
var CONST_DECL_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
|
|
67513
|
-
var NEW_BULL_RE = /(?:(\w+)\s*[:=]\s*)?new\s+(Queue|Worker)\s*(?:<[^>]*>)?\s*\(\s*(['"][^'"]+['"]|[A-Za-z_$][\w$.]*)/g;
|
|
67514
|
-
var QUEUE_NAME_CONST_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*(?:QUEUE|Queue)[\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
|
|
67515
|
-
function baseName(filePath) {
|
|
67516
|
-
const parts = filePath.split("/");
|
|
67517
|
-
const last = parts[parts.length - 1] ?? filePath;
|
|
67518
|
-
return last.replace(/\.[jt]sx?$/, "");
|
|
67517
|
+
function runtimeSourceCaveat(r) {
|
|
67518
|
+
const status = r.sourceStatus;
|
|
67519
|
+
if (!status) return null;
|
|
67520
|
+
const contributed = status.sources.some((s) => s.status === "contributed");
|
|
67521
|
+
if (contributed) return null;
|
|
67522
|
+
const notConfigured = status.sources.filter((s) => s.status === "not-configured").map((s) => s.source);
|
|
67523
|
+
if (notConfigured.length === 0) return null;
|
|
67524
|
+
return `source-only \u2014 ${notConfigured.join(", ")} not configured`;
|
|
67519
67525
|
}
|
|
67520
|
-
function
|
|
67521
|
-
const
|
|
67522
|
-
|
|
67523
|
-
|
|
67524
|
-
}
|
|
67525
|
-
|
|
67526
|
-
}
|
|
67527
|
-
|
|
67528
|
-
const
|
|
67529
|
-
|
|
67530
|
-
|
|
67531
|
-
|
|
67532
|
-
|
|
67533
|
-
|
|
67534
|
-
|
|
67535
|
-
|
|
67536
|
-
|
|
67537
|
-
|
|
67526
|
+
function renderReport(r) {
|
|
67527
|
+
const lines = [];
|
|
67528
|
+
lines.push(`# Investigation ${r.id}`);
|
|
67529
|
+
lines.push(`Hint: ${r.input.hint}`);
|
|
67530
|
+
if (r.input.repo) lines.push(`Repo: ${r.input.repo}`);
|
|
67531
|
+
if (r.input.service) lines.push(`Service: ${r.input.service}`);
|
|
67532
|
+
if (r.input.since) lines.push(`Since: ${r.input.since}`);
|
|
67533
|
+
lines.push(`Confidence: ${r.confidence.toFixed(2)}`);
|
|
67534
|
+
const caveat = runtimeSourceCaveat(r);
|
|
67535
|
+
if (caveat) lines.push(` \u21B3 ${caveat}`);
|
|
67536
|
+
lines.push("");
|
|
67537
|
+
lines.push("## Summary");
|
|
67538
|
+
lines.push(r.summary);
|
|
67539
|
+
lines.push("");
|
|
67540
|
+
const explainLines = explainLowConfidence(r);
|
|
67541
|
+
if (explainLines) {
|
|
67542
|
+
lines.push("## Why confidence is not higher");
|
|
67543
|
+
for (const reason of explainLines) {
|
|
67544
|
+
lines.push(` - ${reason}`);
|
|
67538
67545
|
}
|
|
67546
|
+
lines.push("");
|
|
67539
67547
|
}
|
|
67540
|
-
|
|
67541
|
-
|
|
67542
|
-
|
|
67543
|
-
|
|
67544
|
-
|
|
67545
|
-
|
|
67546
|
-
|
|
67547
|
-
|
|
67548
|
-
|
|
67549
|
-
|
|
67550
|
-
|
|
67548
|
+
lines.push("## Similar past incidents");
|
|
67549
|
+
if (r.similarIncidents.length === 0) {
|
|
67550
|
+
lines.push("(none on record)");
|
|
67551
|
+
} else {
|
|
67552
|
+
for (const s of r.similarIncidents) {
|
|
67553
|
+
lines.push(
|
|
67554
|
+
`- ${s.title} (overlap ${(s.overlap * 100).toFixed(0)}%) \u2014 shared: ${s.sharedTags.join(", ")}`
|
|
67555
|
+
);
|
|
67556
|
+
if (s.summary) {
|
|
67557
|
+
const truncated = s.summary.length > 80 ? s.summary.slice(0, 77) + "..." : s.summary;
|
|
67558
|
+
lines.push(` ${truncated}`);
|
|
67551
67559
|
}
|
|
67552
67560
|
}
|
|
67553
|
-
|
|
67554
|
-
|
|
67555
|
-
|
|
67556
|
-
if (queue) producers.push({ queue, symbol: ident || baseName(pc35.filePath), file: pc35.filePath });
|
|
67557
|
-
}
|
|
67561
|
+
lines.push(
|
|
67562
|
+
"Context only \u2014 past incidents inform, but never override, the current evidence."
|
|
67563
|
+
);
|
|
67558
67564
|
}
|
|
67559
|
-
|
|
67560
|
-
|
|
67561
|
-
|
|
67562
|
-
|
|
67563
|
-
|
|
67564
|
-
|
|
67565
|
+
lines.push("");
|
|
67566
|
+
lines.push("## Seed(s)");
|
|
67567
|
+
if (r.seeds.length === 0) {
|
|
67568
|
+
lines.push("(none)");
|
|
67569
|
+
} else {
|
|
67570
|
+
for (const s of r.seeds) {
|
|
67571
|
+
const sig = s.signature ? ` \u2014 ${s.signature}` : "";
|
|
67572
|
+
lines.push(`- ${s.name} (${formatSymbolLocation(s.filePath, s.startLine, s.endLine)})${sig}`);
|
|
67565
67573
|
}
|
|
67566
|
-
|
|
67567
|
-
|
|
67568
|
-
|
|
67569
|
-
|
|
67570
|
-
|
|
67574
|
+
}
|
|
67575
|
+
lines.push("");
|
|
67576
|
+
lines.push("## Findings");
|
|
67577
|
+
if (r.findings.length === 0) {
|
|
67578
|
+
lines.push("(none)");
|
|
67579
|
+
} else {
|
|
67580
|
+
for (const f of r.findings) {
|
|
67581
|
+
lines.push(`- [${f.confidence.toFixed(2)}] ${f.title}`);
|
|
67582
|
+
if (f.detail) lines.push(` ${f.detail}`);
|
|
67583
|
+
if (f.evidenceIds.length > 0) {
|
|
67584
|
+
lines.push(` evidence: ${f.evidenceIds.map(shortId).join(", ")}`);
|
|
67571
67585
|
}
|
|
67572
67586
|
}
|
|
67573
67587
|
}
|
|
67574
|
-
|
|
67575
|
-
|
|
67576
|
-
|
|
67577
|
-
|
|
67578
|
-
|
|
67579
|
-
|
|
67580
|
-
|
|
67581
|
-
|
|
67582
|
-
edges.push({
|
|
67583
|
-
queueName: q,
|
|
67584
|
-
producerSymbol: p.symbol,
|
|
67585
|
-
producerFile: p.file,
|
|
67586
|
-
workerSymbol: w.symbol,
|
|
67587
|
-
workerFile: w.file
|
|
67588
|
-
});
|
|
67589
|
-
}
|
|
67590
|
-
}
|
|
67591
|
-
} else if (W.length) {
|
|
67592
|
-
for (const w of W) {
|
|
67593
|
-
edges.push({
|
|
67594
|
-
queueName: q,
|
|
67595
|
-
producerSymbol: null,
|
|
67596
|
-
producerFile: null,
|
|
67597
|
-
workerSymbol: w.symbol,
|
|
67598
|
-
workerFile: w.file
|
|
67599
|
-
});
|
|
67600
|
-
}
|
|
67601
|
-
} else if (P.length) {
|
|
67602
|
-
for (const p of P) {
|
|
67603
|
-
edges.push({
|
|
67604
|
-
queueName: q,
|
|
67605
|
-
producerSymbol: p.symbol,
|
|
67606
|
-
producerFile: p.file,
|
|
67607
|
-
workerSymbol: null,
|
|
67608
|
-
workerFile: null
|
|
67609
|
-
});
|
|
67610
|
-
}
|
|
67588
|
+
lines.push("");
|
|
67589
|
+
lines.push("## Timeline");
|
|
67590
|
+
if (r.timeline.events.length === 0) {
|
|
67591
|
+
lines.push("(none)");
|
|
67592
|
+
} else {
|
|
67593
|
+
for (const ev of r.timeline.events) {
|
|
67594
|
+
const at = ev.at ?? "structural";
|
|
67595
|
+
lines.push(` ${ev.order}. [${at}] ${ev.title}`);
|
|
67611
67596
|
}
|
|
67612
67597
|
}
|
|
67613
|
-
|
|
67614
|
-
|
|
67615
|
-
|
|
67616
|
-
|
|
67617
|
-
|
|
67618
|
-
|
|
67619
|
-
|
|
67620
|
-
|
|
67621
|
-
|
|
67598
|
+
lines.push("");
|
|
67599
|
+
lines.push("## Boundary crossings");
|
|
67600
|
+
if (r.timeline.boundaryCrossings.length === 0) {
|
|
67601
|
+
lines.push("(none)");
|
|
67602
|
+
} else {
|
|
67603
|
+
for (const bc of r.timeline.boundaryCrossings) {
|
|
67604
|
+
const producer = bc.producer ?? "?";
|
|
67605
|
+
const worker = bc.worker ?? "?";
|
|
67606
|
+
lines.push(` ${bc.queueName}: ${producer} -> ${worker}`);
|
|
67607
|
+
}
|
|
67622
67608
|
}
|
|
67623
|
-
|
|
67624
|
-
|
|
67625
|
-
|
|
67626
|
-
|
|
67627
|
-
|
|
67628
|
-
|
|
67629
|
-
|
|
67630
|
-
|
|
67631
|
-
|
|
67632
|
-
)).rows;
|
|
67633
|
-
const producerClasses = producerRows.map((r) => ({
|
|
67634
|
-
name: String(r[0] ?? ""),
|
|
67635
|
-
filePath: String(r[1] ?? ""),
|
|
67636
|
-
content: String(r[2] ?? "")
|
|
67637
|
-
}));
|
|
67638
|
-
const workerRows = (await client.cypher(
|
|
67639
|
-
// "new Worker" (no paren) so generic-typed `new Worker<T>(...)` also matches.
|
|
67640
|
-
'MATCH (n) WHERE n.content CONTAINS "@Processor(" OR n.content CONTAINS "new Worker" RETURN n.name, n.file_path, n.content'
|
|
67641
|
-
)).rows;
|
|
67642
|
-
const workerFiles = workerRows.map((r) => ({
|
|
67643
|
-
filePath: String(r[1] ?? ""),
|
|
67644
|
-
content: String(r[2] ?? "")
|
|
67645
|
-
}));
|
|
67646
|
-
const graph = extractQueueGraph({ producerClasses, workerFiles });
|
|
67647
|
-
const edges = graph.edges.map((e) => ({
|
|
67648
|
-
queueName: e.queueName,
|
|
67649
|
-
producerSymbol: e.producerSymbol,
|
|
67650
|
-
producerFile: e.producerFile,
|
|
67651
|
-
workerSymbol: e.workerSymbol,
|
|
67652
|
-
workerFile: e.workerFile,
|
|
67653
|
-
source: "stitcher",
|
|
67654
|
-
project: opts.project ?? null
|
|
67655
|
-
}));
|
|
67656
|
-
await replaceQueueEdges(db, edges, { project: opts.project });
|
|
67657
|
-
return {
|
|
67658
|
-
queues: graph.queues.length,
|
|
67659
|
-
producers: graph.producers.length,
|
|
67660
|
-
workers: graph.workers.length,
|
|
67661
|
-
edges: edges.length
|
|
67662
|
-
};
|
|
67663
|
-
}
|
|
67664
|
-
|
|
67665
|
-
// ../../packages/cli/src/commands/index-repo.ts
|
|
67666
|
-
async function stitchQueueMap(hostUrl, dbUrl, label) {
|
|
67667
|
-
const axon = new AxonHttpClient({ baseUrl: hostUrl });
|
|
67668
|
-
const { db, sql: sql2 } = createDb(dbUrl);
|
|
67669
|
-
try {
|
|
67670
|
-
const summary = await stitch(axon, db, { project: label });
|
|
67671
|
-
console.log(
|
|
67672
|
-
import_picocolors3.default.dim(`[${label}] `) + `Stitched ${summary.edges} queue edge(s) across ${summary.queues} queue(s) \u2014 ${summary.producers} producer(s), ${summary.workers} worker(s).`
|
|
67673
|
-
);
|
|
67674
|
-
} finally {
|
|
67675
|
-
await sql2.end();
|
|
67609
|
+
lines.push("");
|
|
67610
|
+
lines.push("## Correlation");
|
|
67611
|
+
lines.push("### Cause chains");
|
|
67612
|
+
if (r.correlation.chains.length === 0) {
|
|
67613
|
+
lines.push(" (none)");
|
|
67614
|
+
} else {
|
|
67615
|
+
for (const chain of r.correlation.chains) {
|
|
67616
|
+
lines.push(` [${chain.strength.toFixed(2)}] ${chain.title} \u2014 ${chain.rationale}`);
|
|
67617
|
+
}
|
|
67676
67618
|
}
|
|
67677
|
-
|
|
67678
|
-
|
|
67679
|
-
|
|
67680
|
-
|
|
67681
|
-
for (const
|
|
67682
|
-
|
|
67683
|
-
const hostUrl = r.source?.hostUrl ?? r.axon?.hostUrl;
|
|
67684
|
-
return { project: p.name, ...hostUrl ? { hostUrl } : {} };
|
|
67685
|
-
}
|
|
67619
|
+
lines.push("### Related evidence groups");
|
|
67620
|
+
if (r.correlation.groups.length === 0) {
|
|
67621
|
+
lines.push(" (none)");
|
|
67622
|
+
} else {
|
|
67623
|
+
for (const group of r.correlation.groups) {
|
|
67624
|
+
lines.push(` ${group.reason} (${group.evidenceIds.length} items)`);
|
|
67686
67625
|
}
|
|
67687
67626
|
}
|
|
67688
|
-
|
|
67689
|
-
|
|
67690
|
-
|
|
67691
|
-
|
|
67692
|
-
const
|
|
67693
|
-
|
|
67694
|
-
const dbUrlDefault = process.env["DATABASE_URL"] ?? "postgresql://horus:horus@localhost:5433/horus";
|
|
67695
|
-
let config = null;
|
|
67696
|
-
try {
|
|
67697
|
-
config = await loadConfig(opts.config, { name: opts.name });
|
|
67698
|
-
} catch {
|
|
67699
|
-
config = null;
|
|
67627
|
+
lines.push("### Missing evidence");
|
|
67628
|
+
if (r.correlation.missing.length === 0) {
|
|
67629
|
+
lines.push(" none");
|
|
67630
|
+
} else {
|
|
67631
|
+
for (const m of r.correlation.missing) {
|
|
67632
|
+
lines.push(` - ${m.note}`);
|
|
67700
67633
|
}
|
|
67701
|
-
|
|
67702
|
-
|
|
67703
|
-
|
|
67704
|
-
|
|
67705
|
-
|
|
67706
|
-
|
|
67707
|
-
|
|
67708
|
-
|
|
67709
|
-
|
|
67710
|
-
|
|
67711
|
-
|
|
67634
|
+
}
|
|
67635
|
+
lines.push("");
|
|
67636
|
+
const queueIds = queueEvidenceIds(r.evidence);
|
|
67637
|
+
lines.push("## Suspected causes (ranked)");
|
|
67638
|
+
if (r.suspectedCauses.length === 0) {
|
|
67639
|
+
lines.push("(none)");
|
|
67640
|
+
} else {
|
|
67641
|
+
r.suspectedCauses.forEach((c, i) => {
|
|
67642
|
+
const queueTag = c.sourceEvidenceIds.some((id) => queueIds.has(id)) ? " [\u2191 queue]" : "";
|
|
67643
|
+
lines.push(`${i + 1}. [${c.finalScore.toFixed(2)} / ${c.band}] ${c.title}${queueTag}`);
|
|
67644
|
+
if (c.sourceEvidenceIds.length > 0) {
|
|
67645
|
+
lines.push(` evidence: ${c.sourceEvidenceIds.map(shortId).join(", ")}`);
|
|
67712
67646
|
}
|
|
67713
|
-
|
|
67714
|
-
|
|
67715
|
-
|
|
67716
|
-
|
|
67717
|
-
|
|
67718
|
-
|
|
67647
|
+
});
|
|
67648
|
+
}
|
|
67649
|
+
lines.push("");
|
|
67650
|
+
lines.push("## Hypotheses");
|
|
67651
|
+
const allUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
|
|
67652
|
+
if (allUnconfirmed) {
|
|
67653
|
+
lines.push(
|
|
67654
|
+
"_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
|
|
67655
|
+
);
|
|
67656
|
+
}
|
|
67657
|
+
if (r.hypotheses.length === 0) {
|
|
67658
|
+
lines.push("(none)");
|
|
67659
|
+
} else {
|
|
67660
|
+
for (const h of r.hypotheses) {
|
|
67661
|
+
lines.push(
|
|
67662
|
+
` [${h.verdict}] [${h.confidence.toFixed(2)} (was ${h.priorConfidence.toFixed(2)})] ${h.category}: ${h.statement}`
|
|
67663
|
+
);
|
|
67664
|
+
lines.push(` ${h.rationale}`);
|
|
67665
|
+
if (h.missingEvidence.length > 0) {
|
|
67666
|
+
lines.push(` \xB7 missing: ${h.missingEvidence.join("; ")}`);
|
|
67719
67667
|
}
|
|
67720
67668
|
}
|
|
67721
|
-
|
|
67722
|
-
|
|
67723
|
-
|
|
67724
|
-
|
|
67725
|
-
|
|
67726
|
-
|
|
67727
|
-
|
|
67728
|
-
|
|
67729
|
-
|
|
67669
|
+
}
|
|
67670
|
+
lines.push("");
|
|
67671
|
+
if (r.causeChains !== void 0 && r.causeChains.length > 0) {
|
|
67672
|
+
lines.push("## Cause chains");
|
|
67673
|
+
for (const chain of r.causeChains) {
|
|
67674
|
+
lines.push(` [${chain.confidence.toFixed(2)}] ${chain.category}: ${chain.summary}`);
|
|
67675
|
+
for (let i = 0; i < chain.steps.length; i++) {
|
|
67676
|
+
const step = chain.steps[i];
|
|
67677
|
+
const prefix = i === chain.steps.length - 1 ? " \u2514\u2500" : " \u251C\u2500";
|
|
67678
|
+
const evCite = step.evidenceIds.length > 0 ? ` (evidence: ${step.evidenceIds.map(shortId).join(", ")})` : "";
|
|
67679
|
+
lines.push(`${prefix} [${step.role}] ${step.label}${evCite}`);
|
|
67730
67680
|
}
|
|
67731
67681
|
}
|
|
67732
|
-
|
|
67733
|
-
|
|
67682
|
+
lines.push("");
|
|
67683
|
+
}
|
|
67684
|
+
lines.push("## Evidence gaps (what we don't know)");
|
|
67685
|
+
if (r.gapAnalysis.gaps.length === 0) {
|
|
67686
|
+
lines.push("(no major evidence gaps)");
|
|
67687
|
+
} else {
|
|
67688
|
+
for (const gap of r.gapAnalysis.gaps) {
|
|
67689
|
+
lines.push(
|
|
67690
|
+
` - ${gap.dimension}: ${gap.why} \u2192 next: ${gap.nextSource} (\u2212${gap.confidenceImpact.toFixed(2)} conf)`
|
|
67691
|
+
);
|
|
67692
|
+
}
|
|
67693
|
+
lines.push("");
|
|
67694
|
+
lines.push("Blind spots:");
|
|
67695
|
+
for (const bs of r.gapAnalysis.blindSpots) {
|
|
67696
|
+
lines.push(` - ${bs}`);
|
|
67697
|
+
}
|
|
67698
|
+
lines.push("");
|
|
67699
|
+
lines.push(
|
|
67700
|
+
`Confidence ceiling: ${r.gapAnalysis.confidenceCeiling} (current confidence is capped at this until the gaps are filled).`
|
|
67701
|
+
);
|
|
67702
|
+
}
|
|
67703
|
+
lines.push("");
|
|
67704
|
+
lines.push("## Evidence");
|
|
67705
|
+
if (r.evidence.length === 0) {
|
|
67706
|
+
lines.push("(none)");
|
|
67707
|
+
} else {
|
|
67708
|
+
for (const e of r.evidence) {
|
|
67709
|
+
lines.push(`- ${shortId(e.id)} [${e.source}/${e.kind}] ${e.title}`);
|
|
67710
|
+
}
|
|
67711
|
+
}
|
|
67712
|
+
lines.push("");
|
|
67713
|
+
const queueGroups = groupQueueEvidence(r.evidence);
|
|
67714
|
+
const queueGap = r.gapAnalysis.gaps.find((g) => g.dimension === "queue runtime state");
|
|
67715
|
+
if (queueGroups.size > 0 || queueGap) {
|
|
67716
|
+
lines.push("## Queue runtime");
|
|
67717
|
+
if (queueGroups.size === 0) {
|
|
67718
|
+
lines.push(` (${queueGap.why})`);
|
|
67734
67719
|
} else {
|
|
67735
|
-
|
|
67736
|
-
|
|
67737
|
-
|
|
67738
|
-
|
|
67739
|
-
|
|
67740
|
-
}
|
|
67741
|
-
if (!isAnalyzed(root)) {
|
|
67742
|
-
console.log(import_picocolors3.default.dim(" analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"));
|
|
67743
|
-
try {
|
|
67744
|
-
await analyzeRepo(root);
|
|
67745
|
-
} catch (err) {
|
|
67746
|
-
console.error(import_picocolors3.default.red(` source analysis failed: ${err.message}`));
|
|
67747
|
-
return 1;
|
|
67720
|
+
for (const [name, evs] of queueGroups) {
|
|
67721
|
+
lines.push(` ${name}`);
|
|
67722
|
+
for (const e of evs) {
|
|
67723
|
+
const detail = isQueueSummary(e) ? fmtQueueCounts(e.payload) : stripQueueName(e.title, name);
|
|
67724
|
+
lines.push(` [${e.relevance.toFixed(2)}] ${detail}`);
|
|
67748
67725
|
}
|
|
67749
|
-
} else {
|
|
67750
|
-
console.log(import_picocolors3.default.dim(" already analyzed"));
|
|
67751
|
-
}
|
|
67752
|
-
const port = await findFreePort();
|
|
67753
|
-
hostUrl = `http://127.0.0.1:${port}`;
|
|
67754
|
-
console.log(import_picocolors3.default.dim(` starting source-intelligence host on port ${port}\u2026`));
|
|
67755
|
-
startHost(root, port);
|
|
67756
|
-
if (!await waitForHost(hostUrl)) {
|
|
67757
|
-
removeSpawnedHostRecord(root);
|
|
67758
|
-
console.error(
|
|
67759
|
-
import_picocolors3.default.red(` Source-intelligence host did not become healthy \u2014 see ${root}/.horus/source-host.log`)
|
|
67760
|
-
);
|
|
67761
|
-
return 1;
|
|
67762
67726
|
}
|
|
67763
67727
|
}
|
|
67764
|
-
|
|
67765
|
-
|
|
67766
|
-
|
|
67767
|
-
|
|
67768
|
-
|
|
67769
|
-
|
|
67770
|
-
|
|
67771
|
-
|
|
67772
|
-
|
|
67773
|
-
};
|
|
67774
|
-
|
|
67775
|
-
|
|
67776
|
-
|
|
67777
|
-
|
|
67778
|
-
|
|
67779
|
-
|
|
67780
|
-
import_picocolors3.default.dim(
|
|
67781
|
-
` investigate: horus investigate --name ${name} "<hint>" (or from this repo: horus investigate "<hint>")`
|
|
67782
|
-
)
|
|
67783
|
-
);
|
|
67784
|
-
} else if (hostUrl !== configuredHost) {
|
|
67785
|
-
const existingPath = discoverLocalConfig(root);
|
|
67786
|
-
if (existingPath) {
|
|
67787
|
-
const file = readLocalConfig(existingPath);
|
|
67788
|
-
const project = file.project;
|
|
67789
|
-
const repos = project["repositories"];
|
|
67790
|
-
if (repos && repos.length > 0) {
|
|
67791
|
-
repos[0]["source"] = { hostUrl };
|
|
67792
|
-
}
|
|
67793
|
-
writeLocalConfig(root, file);
|
|
67794
|
-
registerProject(label, root, existingPath);
|
|
67795
|
-
ensureProjectGitignore(root);
|
|
67796
|
-
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} \u2014 source host registered at ${hostUrl}`);
|
|
67797
|
-
console.log(import_picocolors3.default.dim(` ${existingPath}`));
|
|
67798
|
-
} else {
|
|
67799
|
-
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`);
|
|
67728
|
+
lines.push("");
|
|
67729
|
+
}
|
|
67730
|
+
const rc = r.recentChanges;
|
|
67731
|
+
if (rc && rc.commits.length > 0) {
|
|
67732
|
+
const until = rc.window.until ? `..${rc.window.until}` : "..HEAD";
|
|
67733
|
+
lines.push("## Recent changes");
|
|
67734
|
+
lines.push(` Window: ${rc.window.since}${until}`);
|
|
67735
|
+
lines.push(` Commits: ${rc.commits.length}${rc.truncated ? "+" : ""}`);
|
|
67736
|
+
for (const c of rc.commits.slice(0, 10)) {
|
|
67737
|
+
lines.push(` ${c.shortSha} ${c.subject}`);
|
|
67738
|
+
}
|
|
67739
|
+
if (rc.fileStats.length > 0) {
|
|
67740
|
+
const fileCount = rc.changedFiles.length;
|
|
67741
|
+
lines.push(` Files: ${fileCount}${rc.truncated ? "+" : ""}`);
|
|
67742
|
+
for (const f of rc.fileStats.slice(0, 8)) {
|
|
67743
|
+
lines.push(` +${f.insertions} -${f.deletions} ${f.path}`);
|
|
67800
67744
|
}
|
|
67801
|
-
} else {
|
|
67802
|
-
console.log(
|
|
67803
|
-
`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`
|
|
67804
|
-
);
|
|
67805
67745
|
}
|
|
67806
|
-
|
|
67807
|
-
|
|
67808
|
-
|
|
67809
|
-
|
|
67746
|
+
if (rc.truncated && rc.truncatedReason) {
|
|
67747
|
+
lines.push(` (truncated: ${rc.truncatedReason})`);
|
|
67748
|
+
}
|
|
67749
|
+
lines.push("");
|
|
67810
67750
|
}
|
|
67811
|
-
|
|
67812
|
-
|
|
67813
|
-
|
|
67814
|
-
|
|
67815
|
-
|
|
67816
|
-
|
|
67817
|
-
try {
|
|
67818
|
-
const config = await loadConfig(opts.config, { name: opts.name });
|
|
67819
|
-
const { db, sql: sql2 } = createDb(config.database.url);
|
|
67820
|
-
try {
|
|
67821
|
-
let project = opts.project;
|
|
67822
|
-
if (project === void 0) {
|
|
67823
|
-
try {
|
|
67824
|
-
project = resolveEnvironment(config, { project: opts.project }).project;
|
|
67825
|
-
} catch {
|
|
67826
|
-
}
|
|
67827
|
-
}
|
|
67828
|
-
const rows = await listQueueEdges(db, { project, queueName: name });
|
|
67829
|
-
if (opts.json) {
|
|
67830
|
-
const topology = topologyToJson(buildQueueMap(rows));
|
|
67831
|
-
const out = { project: project ?? null, topology };
|
|
67832
|
-
if (opts.live) out["live"] = await gatherLiveState(config, rows, name);
|
|
67833
|
-
console.log(JSON.stringify(out, null, 2));
|
|
67834
|
-
return 0;
|
|
67835
|
-
}
|
|
67836
|
-
console.log(
|
|
67837
|
-
import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
|
|
67838
|
-
);
|
|
67839
|
-
console.log("");
|
|
67840
|
-
if (rows.length === 0) {
|
|
67841
|
-
console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
|
|
67842
|
-
} else {
|
|
67843
|
-
const byQueue = buildQueueMap(rows);
|
|
67844
|
-
printTopology(byQueue);
|
|
67845
|
-
}
|
|
67846
|
-
console.log("");
|
|
67847
|
-
if (opts.live) {
|
|
67848
|
-
await runLiveMode(config, rows, name);
|
|
67849
|
-
} else {
|
|
67850
|
-
console.log(
|
|
67851
|
-
import_picocolors4.default.dim(
|
|
67852
|
-
" Tip: run horus queues --live to show real-time Redis/BullMQ depths and failed-job counts."
|
|
67853
|
-
)
|
|
67854
|
-
);
|
|
67855
|
-
}
|
|
67856
|
-
} finally {
|
|
67857
|
-
await sql2.end();
|
|
67751
|
+
lines.push("## Next actions");
|
|
67752
|
+
if (r.nextActions.length === 0) {
|
|
67753
|
+
lines.push("(none)");
|
|
67754
|
+
} else {
|
|
67755
|
+
for (const a of r.nextActions) {
|
|
67756
|
+
lines.push(`- ${a}`);
|
|
67858
67757
|
}
|
|
67859
|
-
return 0;
|
|
67860
|
-
} catch (err) {
|
|
67861
|
-
console.error(import_picocolors4.default.red(err.message));
|
|
67862
|
-
return 1;
|
|
67863
67758
|
}
|
|
67759
|
+
return lines.join("\n");
|
|
67864
67760
|
}
|
|
67865
|
-
function
|
|
67866
|
-
|
|
67867
|
-
for (const row of rows) {
|
|
67868
|
-
const existing = byQueue.get(row.queueName);
|
|
67869
|
-
if (existing) {
|
|
67870
|
-
existing.push(row);
|
|
67871
|
-
} else {
|
|
67872
|
-
byQueue.set(row.queueName, [row]);
|
|
67873
|
-
}
|
|
67874
|
-
}
|
|
67875
|
-
return byQueue;
|
|
67876
|
-
}
|
|
67877
|
-
function endpoints(edges, symKey, fileKey) {
|
|
67878
|
-
const seen = /* @__PURE__ */ new Map();
|
|
67879
|
-
for (const e of edges) {
|
|
67880
|
-
const symbol = e[symKey];
|
|
67881
|
-
if (!symbol) continue;
|
|
67882
|
-
if (!seen.has(symbol)) seen.set(symbol, { symbol, file: e[fileKey] ?? null });
|
|
67883
|
-
}
|
|
67884
|
-
return [...seen.values()];
|
|
67885
|
-
}
|
|
67886
|
-
function topologyToJson(byQueue) {
|
|
67887
|
-
return [...byQueue.entries()].map(([queueName, edges]) => ({
|
|
67888
|
-
queueName,
|
|
67889
|
-
producers: endpoints(edges, "producerSymbol", "producerFile"),
|
|
67890
|
-
workers: endpoints(edges, "workerSymbol", "workerFile")
|
|
67891
|
-
}));
|
|
67761
|
+
function reportToJSON(r) {
|
|
67762
|
+
return JSON.stringify(r, null, 2);
|
|
67892
67763
|
}
|
|
67893
|
-
|
|
67894
|
-
|
|
67895
|
-
|
|
67896
|
-
|
|
67897
|
-
|
|
67898
|
-
|
|
67899
|
-
}
|
|
67900
|
-
|
|
67901
|
-
|
|
67902
|
-
|
|
67903
|
-
|
|
67904
|
-
|
|
67905
|
-
|
|
67906
|
-
|
|
67907
|
-
|
|
67908
|
-
|
|
67909
|
-
|
|
67910
|
-
const discovered = await queueProvider.discoverQueues().catch(() => []);
|
|
67911
|
-
const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
|
|
67912
|
-
queueNames = union2.size > 0 ? [...union2] : void 0;
|
|
67764
|
+
function reportToMarkdown(r) {
|
|
67765
|
+
const out = [];
|
|
67766
|
+
out.push(`# Investigation Report \u2014 ${r.input.hint}`);
|
|
67767
|
+
out.push("");
|
|
67768
|
+
const mdCaveat = runtimeSourceCaveat(r);
|
|
67769
|
+
out.push(`**Confidence:** ${r.confidence.toFixed(2)}${mdCaveat ? ` _(${mdCaveat})_` : ""}`);
|
|
67770
|
+
if (r.input.service) out.push(`**Service:** ${r.input.service}`);
|
|
67771
|
+
if (r.input.since) out.push(`**Since:** ${r.input.since}`);
|
|
67772
|
+
out.push("");
|
|
67773
|
+
out.push("## Summary");
|
|
67774
|
+
out.push(r.summary);
|
|
67775
|
+
out.push("");
|
|
67776
|
+
const mdExplain = explainLowConfidence(r);
|
|
67777
|
+
if (mdExplain) {
|
|
67778
|
+
out.push("## Why confidence is not higher");
|
|
67779
|
+
for (const reason of mdExplain) {
|
|
67780
|
+
out.push(`- ${reason}`);
|
|
67913
67781
|
}
|
|
67914
|
-
|
|
67915
|
-
return {
|
|
67916
|
-
ok: true,
|
|
67917
|
-
prefix: state.prefix,
|
|
67918
|
-
collectedAt: state.collectedAt,
|
|
67919
|
-
queues: state.queues.map((q) => ({ ...q, runtimeOnly: !staticNames.has(q.queueName) }))
|
|
67920
|
-
};
|
|
67921
|
-
} finally {
|
|
67922
|
-
await queueProvider.close().catch(() => {
|
|
67923
|
-
});
|
|
67782
|
+
out.push("");
|
|
67924
67783
|
}
|
|
67925
|
-
|
|
67926
|
-
|
|
67927
|
-
|
|
67928
|
-
|
|
67929
|
-
const
|
|
67930
|
-
|
|
67931
|
-
|
|
67932
|
-
|
|
67933
|
-
|
|
67934
|
-
|
|
67935
|
-
|
|
67936
|
-
}
|
|
67937
|
-
if (producerSet.size === 0) {
|
|
67938
|
-
console.log(" producers: " + import_picocolors4.default.dim("none"));
|
|
67939
|
-
} else {
|
|
67940
|
-
const list = Array.from(producerSet).map((sym) => {
|
|
67941
|
-
const file = producerDetails.get(sym);
|
|
67942
|
-
return file ? `${sym} (${file})` : sym;
|
|
67943
|
-
}).join(", ");
|
|
67944
|
-
console.log(" producers: " + list);
|
|
67945
|
-
}
|
|
67946
|
-
const workerSet = /* @__PURE__ */ new Set();
|
|
67947
|
-
const workerDetails = /* @__PURE__ */ new Map();
|
|
67948
|
-
for (const edge of edges) {
|
|
67949
|
-
if (edge.workerSymbol) {
|
|
67950
|
-
workerSet.add(edge.workerSymbol);
|
|
67951
|
-
if (edge.workerFile) workerDetails.set(edge.workerSymbol, edge.workerFile);
|
|
67784
|
+
out.push("## Similar past incidents");
|
|
67785
|
+
if (r.similarIncidents.length === 0) {
|
|
67786
|
+
out.push("(none on record)");
|
|
67787
|
+
} else {
|
|
67788
|
+
for (const s of r.similarIncidents) {
|
|
67789
|
+
out.push(
|
|
67790
|
+
`- ${s.title} (overlap ${(s.overlap * 100).toFixed(0)}%) \u2014 shared: ${s.sharedTags.join(", ")}`
|
|
67791
|
+
);
|
|
67792
|
+
if (s.summary) {
|
|
67793
|
+
const truncated = s.summary.length > 80 ? s.summary.slice(0, 77) + "..." : s.summary;
|
|
67794
|
+
out.push(` _${truncated}_`);
|
|
67952
67795
|
}
|
|
67953
67796
|
}
|
|
67954
|
-
|
|
67955
|
-
|
|
67956
|
-
|
|
67957
|
-
const list = Array.from(workerSet).map((sym) => {
|
|
67958
|
-
const file = workerDetails.get(sym);
|
|
67959
|
-
return file ? `${sym} (${file})` : sym;
|
|
67960
|
-
}).join(", ");
|
|
67961
|
-
console.log(" workers: " + list);
|
|
67962
|
-
}
|
|
67963
|
-
console.log("");
|
|
67797
|
+
out.push(
|
|
67798
|
+
"_Context only \u2014 past incidents inform, but never override, the current evidence._"
|
|
67799
|
+
);
|
|
67964
67800
|
}
|
|
67965
|
-
|
|
67966
|
-
|
|
67967
|
-
|
|
67968
|
-
|
|
67969
|
-
|
|
67970
|
-
}
|
|
67971
|
-
|
|
67972
|
-
|
|
67973
|
-
|
|
67801
|
+
out.push("");
|
|
67802
|
+
const mdQueueIds = queueEvidenceIds(r.evidence);
|
|
67803
|
+
out.push("## Suspected causes");
|
|
67804
|
+
if (r.suspectedCauses.length === 0) {
|
|
67805
|
+
out.push("_none_");
|
|
67806
|
+
} else {
|
|
67807
|
+
r.suspectedCauses.forEach((c, i) => {
|
|
67808
|
+
const queueTag = c.sourceEvidenceIds.some((id) => mdQueueIds.has(id)) ? " `[\u2191 queue]`" : "";
|
|
67809
|
+
out.push(`${i + 1}. **(${c.finalScore.toFixed(2)}, ${c.band})**${queueTag} ${c.title}`);
|
|
67810
|
+
});
|
|
67974
67811
|
}
|
|
67975
|
-
|
|
67976
|
-
|
|
67977
|
-
|
|
67978
|
-
|
|
67979
|
-
|
|
67812
|
+
out.push("");
|
|
67813
|
+
out.push("## Hypotheses");
|
|
67814
|
+
const mdAllUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
|
|
67815
|
+
if (mdAllUnconfirmed) {
|
|
67816
|
+
out.push(
|
|
67817
|
+
"_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
|
|
67980
67818
|
);
|
|
67981
|
-
return;
|
|
67982
67819
|
}
|
|
67983
|
-
|
|
67984
|
-
|
|
67985
|
-
|
|
67986
|
-
|
|
67987
|
-
|
|
67988
|
-
|
|
67989
|
-
|
|
67990
|
-
}
|
|
67991
|
-
const staticNames = new Set(buildQueueMap(rows).keys());
|
|
67992
|
-
let queueNames;
|
|
67993
|
-
if (nameFilter !== void 0) {
|
|
67994
|
-
queueNames = [nameFilter];
|
|
67995
|
-
} else {
|
|
67996
|
-
const discovered = await queueProvider.discoverQueues().catch(() => []);
|
|
67997
|
-
const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
|
|
67998
|
-
queueNames = union2.size > 0 ? [...union2] : void 0;
|
|
67820
|
+
if (r.hypotheses.length === 0) {
|
|
67821
|
+
out.push("_none_");
|
|
67822
|
+
} else {
|
|
67823
|
+
for (const h of r.hypotheses) {
|
|
67824
|
+
out.push(
|
|
67825
|
+
`- \`${h.verdict}\` **${h.confidence.toFixed(2)}** (was ${h.priorConfidence.toFixed(2)}) \u2014 ${h.category}: ${h.statement}`
|
|
67826
|
+
);
|
|
67999
67827
|
}
|
|
68000
|
-
|
|
68001
|
-
|
|
68002
|
-
|
|
68003
|
-
|
|
68004
|
-
);
|
|
68005
|
-
|
|
68006
|
-
|
|
68007
|
-
|
|
68008
|
-
|
|
68009
|
-
console.log(
|
|
68010
|
-
import_picocolors4.default.dim(
|
|
68011
|
-
" If queues exist under a custom prefix, set the BullMQ prefix in your connector config."
|
|
68012
|
-
)
|
|
67828
|
+
}
|
|
67829
|
+
out.push("");
|
|
67830
|
+
out.push("## Evidence gaps (what we don't know)");
|
|
67831
|
+
if (r.gapAnalysis.gaps.length === 0) {
|
|
67832
|
+
out.push("_(no major evidence gaps)_");
|
|
67833
|
+
} else {
|
|
67834
|
+
for (const gap of r.gapAnalysis.gaps) {
|
|
67835
|
+
out.push(
|
|
67836
|
+
`- **${gap.dimension}**: ${gap.why} \u2192 _next: ${gap.nextSource}_ (\u2212${gap.confidenceImpact.toFixed(2)} conf)`
|
|
68013
67837
|
);
|
|
68014
|
-
return;
|
|
68015
67838
|
}
|
|
68016
|
-
|
|
68017
|
-
|
|
68018
|
-
|
|
68019
|
-
|
|
67839
|
+
out.push("");
|
|
67840
|
+
out.push("**Blind spots:**");
|
|
67841
|
+
for (const bs of r.gapAnalysis.blindSpots) {
|
|
67842
|
+
out.push(`- ${bs}`);
|
|
68020
67843
|
}
|
|
68021
|
-
|
|
68022
|
-
|
|
68023
|
-
|
|
68024
|
-
|
|
67844
|
+
out.push("");
|
|
67845
|
+
out.push(
|
|
67846
|
+
`_Confidence ceiling: **${r.gapAnalysis.confidenceCeiling}** \u2014 current confidence is capped at this until the gaps are filled._`
|
|
67847
|
+
);
|
|
68025
67848
|
}
|
|
68026
|
-
|
|
68027
|
-
|
|
68028
|
-
|
|
68029
|
-
|
|
68030
|
-
|
|
68031
|
-
|
|
68032
|
-
|
|
68033
|
-
for (const q of queues) {
|
|
68034
|
-
const hasIssue = q.failed > 0 || q.waiting > 100 || q.delayed > 50 || q.isPaused;
|
|
68035
|
-
const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
|
|
68036
|
-
const row = " " + q.queueName.padEnd(nameWidth) + " " + String(q.waiting).padStart(numWidth) + " " + String(q.active).padStart(numWidth) + " " + String(q.failed).padStart(numWidth) + " " + String(q.delayed).padStart(numWidth) + " " + (q.isPaused ? import_picocolors4.default.yellow("paused") : String(q.paused).padStart(numWidth));
|
|
68037
|
-
console.log(color(row));
|
|
68038
|
-
if (!staticNames.has(q.queueName)) {
|
|
68039
|
-
console.log(import_picocolors4.default.dim(" runtime-only \xB7 no static producer/worker mapping"));
|
|
68040
|
-
}
|
|
68041
|
-
if (q.oldestWaitingMs !== void 0) {
|
|
68042
|
-
const age = formatAge(q.oldestWaitingMs);
|
|
68043
|
-
console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
|
|
67849
|
+
out.push("");
|
|
67850
|
+
out.push("## Timeline");
|
|
67851
|
+
if (r.timeline.events.length === 0) {
|
|
67852
|
+
out.push("_none_");
|
|
67853
|
+
} else {
|
|
67854
|
+
for (const ev of r.timeline.events) {
|
|
67855
|
+
out.push(`${ev.order}. \`${ev.at ?? "structural"}\` ${ev.title}`);
|
|
68044
67856
|
}
|
|
68045
|
-
|
|
68046
|
-
|
|
68047
|
-
|
|
68048
|
-
|
|
67857
|
+
}
|
|
67858
|
+
out.push("");
|
|
67859
|
+
out.push(`## Evidence (${r.evidence.length})`);
|
|
67860
|
+
if (r.evidence.length === 0) {
|
|
67861
|
+
out.push("_none_");
|
|
67862
|
+
} else {
|
|
67863
|
+
for (const e of r.evidence) {
|
|
67864
|
+
out.push(`- \`${shortId(e.id)}\` [${e.source}/${e.kind}] ${e.title}`);
|
|
68049
67865
|
}
|
|
68050
67866
|
}
|
|
68051
|
-
|
|
68052
|
-
|
|
68053
|
-
|
|
68054
|
-
if (
|
|
68055
|
-
|
|
68056
|
-
|
|
68057
|
-
|
|
67867
|
+
out.push("");
|
|
67868
|
+
const mdQueueGroups = groupQueueEvidence(r.evidence);
|
|
67869
|
+
const mdQueueGap = r.gapAnalysis.gaps.find((g) => g.dimension === "queue runtime state");
|
|
67870
|
+
if (mdQueueGroups.size > 0 || mdQueueGap) {
|
|
67871
|
+
out.push("## Queue runtime");
|
|
67872
|
+
if (mdQueueGroups.size === 0) {
|
|
67873
|
+
out.push(`_${mdQueueGap.why}_`);
|
|
67874
|
+
} else {
|
|
67875
|
+
for (const [name, evs] of mdQueueGroups) {
|
|
67876
|
+
out.push(`**${name}**`);
|
|
67877
|
+
for (const e of evs) {
|
|
67878
|
+
const detail = isQueueSummary(e) ? fmtQueueCounts(e.payload) : stripQueueName(e.title, name);
|
|
67879
|
+
out.push(`- \`${e.relevance.toFixed(2)}\` ${detail}`);
|
|
67880
|
+
}
|
|
67881
|
+
}
|
|
67882
|
+
}
|
|
67883
|
+
out.push("");
|
|
67884
|
+
}
|
|
67885
|
+
out.push("## Next actions");
|
|
67886
|
+
if (r.nextActions.length === 0) {
|
|
67887
|
+
out.push("_none_");
|
|
67888
|
+
} else {
|
|
67889
|
+
for (const a of r.nextActions) {
|
|
67890
|
+
out.push(`- [ ] ${a}`);
|
|
67891
|
+
}
|
|
67892
|
+
}
|
|
67893
|
+
out.push("");
|
|
67894
|
+
out.push("---");
|
|
67895
|
+
out.push("_Generated by Horus \u2014 deterministic report, no AI._");
|
|
67896
|
+
return out.join("\n");
|
|
68058
67897
|
}
|
|
68059
67898
|
|
|
68060
|
-
// ../../packages/cli/src/commands/investigate.ts
|
|
68061
|
-
init_cjs_shims();
|
|
68062
|
-
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
68063
|
-
|
|
68064
|
-
// ../../packages/engine/src/index.ts
|
|
68065
|
-
init_cjs_shims();
|
|
68066
|
-
|
|
68067
|
-
// ../../packages/engine/src/types.ts
|
|
68068
|
-
init_cjs_shims();
|
|
68069
|
-
|
|
68070
|
-
// ../../packages/engine/src/engine.ts
|
|
68071
|
-
init_cjs_shims();
|
|
68072
|
-
|
|
68073
67899
|
// ../../packages/engine/src/ownership.ts
|
|
68074
67900
|
init_cjs_shims();
|
|
68075
67901
|
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
@@ -70116,12 +69942,12 @@ async function investigate(input, deps) {
|
|
|
70116
69942
|
} catch {
|
|
70117
69943
|
}
|
|
70118
69944
|
}
|
|
70119
|
-
const
|
|
69945
|
+
const seedLoc = formatSymbolLocation(top.filePath, top.startLine, top.endLine);
|
|
70120
69946
|
const seedEv = mkEv(
|
|
70121
69947
|
"symbol",
|
|
70122
|
-
`Seed symbol ${top.name} (${
|
|
69948
|
+
`Seed symbol ${top.name} (${seedLoc})`,
|
|
70123
69949
|
{ symbol: top, snippet: ctx.snippet ?? null },
|
|
70124
|
-
{ symbolId: top.id, file: top.filePath, line:
|
|
69950
|
+
top.startLine !== void 0 && top.startLine > 0 ? { symbolId: top.id, file: top.filePath, line: top.startLine } : { symbolId: top.id, file: top.filePath }
|
|
70125
69951
|
);
|
|
70126
69952
|
const flowEvIds = [];
|
|
70127
69953
|
for (const flow of flows) {
|
|
@@ -70438,7 +70264,7 @@ async function investigate(input, deps) {
|
|
|
70438
70264
|
const findings2 = [];
|
|
70439
70265
|
findings2.push({
|
|
70440
70266
|
kind: "observation",
|
|
70441
|
-
title: `Seed resolves to ${label} at ${
|
|
70267
|
+
title: `Seed resolves to ${label} at ${seedLoc}`,
|
|
70442
70268
|
detail: top.signature ?? void 0,
|
|
70443
70269
|
confidence: 1,
|
|
70444
70270
|
evidenceIds: [seedEv.id]
|
|
@@ -70855,1405 +70681,948 @@ async function persist(db, input, report) {
|
|
|
70855
70681
|
}
|
|
70856
70682
|
}
|
|
70857
70683
|
|
|
70858
|
-
// ../../packages/engine/src/
|
|
70684
|
+
// ../../packages/engine/src/changes.ts
|
|
70859
70685
|
init_cjs_shims();
|
|
70860
|
-
function
|
|
70861
|
-
|
|
70862
|
-
}
|
|
70863
|
-
|
|
70864
|
-
|
|
70865
|
-
|
|
70866
|
-
|
|
70867
|
-
|
|
70868
|
-
|
|
70869
|
-
|
|
70870
|
-
|
|
70871
|
-
|
|
70872
|
-
|
|
70873
|
-
|
|
70874
|
-
}
|
|
70875
|
-
|
|
70876
|
-
|
|
70877
|
-
|
|
70878
|
-
|
|
70879
|
-
|
|
70880
|
-
|
|
70881
|
-
|
|
70882
|
-
|
|
70883
|
-
|
|
70884
|
-
|
|
70885
|
-
|
|
70886
|
-
|
|
70887
|
-
|
|
70888
|
-
|
|
70889
|
-
|
|
70890
|
-
|
|
70891
|
-
|
|
70892
|
-
|
|
70893
|
-
if (!as_ && bs_) return -1;
|
|
70894
|
-
return b2.relevance - a.relevance;
|
|
70895
|
-
});
|
|
70686
|
+
async function changeImpact(input, deps) {
|
|
70687
|
+
const compare = input.compare ?? "HEAD";
|
|
70688
|
+
const changes = await deps.code.detectChanges({ base: input.base, compare });
|
|
70689
|
+
const presentSymbols = [
|
|
70690
|
+
...changes.added,
|
|
70691
|
+
...changes.modified.map((m) => m.after)
|
|
70692
|
+
];
|
|
70693
|
+
const capped = presentSymbols.filter((s) => !s.id.startsWith("file:")).slice(0, 25);
|
|
70694
|
+
const flowsPerSymbol = await Promise.all(
|
|
70695
|
+
capped.map(async (s) => {
|
|
70696
|
+
try {
|
|
70697
|
+
return await deps.code.flowsFor(s.id);
|
|
70698
|
+
} catch {
|
|
70699
|
+
return [];
|
|
70700
|
+
}
|
|
70701
|
+
})
|
|
70702
|
+
);
|
|
70703
|
+
const flowMap = /* @__PURE__ */ new Map();
|
|
70704
|
+
for (let i = 0; i < capped.length; i++) {
|
|
70705
|
+
const sym = capped[i];
|
|
70706
|
+
if (sym === void 0) continue;
|
|
70707
|
+
const flows = flowsPerSymbol[i] ?? [];
|
|
70708
|
+
for (const flow of flows) {
|
|
70709
|
+
const existing = flowMap.get(flow.id);
|
|
70710
|
+
if (existing !== void 0) {
|
|
70711
|
+
existing.changedSymbols.add(sym.name);
|
|
70712
|
+
} else {
|
|
70713
|
+
flowMap.set(flow.id, {
|
|
70714
|
+
flowName: flow.name,
|
|
70715
|
+
changedSymbols: /* @__PURE__ */ new Set([sym.name])
|
|
70716
|
+
});
|
|
70717
|
+
}
|
|
70718
|
+
}
|
|
70896
70719
|
}
|
|
70897
|
-
|
|
70720
|
+
const affectedFlows = [...flowMap].map(([flowId, v]) => ({
|
|
70721
|
+
flowId,
|
|
70722
|
+
flowName: v.flowName,
|
|
70723
|
+
changedSymbols: [...v.changedSymbols]
|
|
70724
|
+
}));
|
|
70725
|
+
const summary = changes.added.length + " added, " + changes.modified.length + " modified, " + changes.removed.length + " removed between " + input.base + ".." + compare + "; " + affectedFlows.length + " execution flow(s) affected.";
|
|
70726
|
+
return {
|
|
70727
|
+
base: input.base,
|
|
70728
|
+
compare,
|
|
70729
|
+
added: changes.added,
|
|
70730
|
+
removed: changes.removed,
|
|
70731
|
+
modified: changes.modified,
|
|
70732
|
+
affectedFlows,
|
|
70733
|
+
summary
|
|
70734
|
+
};
|
|
70898
70735
|
}
|
|
70899
|
-
|
|
70900
|
-
|
|
70901
|
-
|
|
70902
|
-
|
|
70903
|
-
|
|
70904
|
-
return
|
|
70736
|
+
|
|
70737
|
+
// ../../packages/engine/src/render-changes.ts
|
|
70738
|
+
init_cjs_shims();
|
|
70739
|
+
var LIST_CAP = 15;
|
|
70740
|
+
function symbolLine(s) {
|
|
70741
|
+
return `${s.name} (${s.filePath})`;
|
|
70905
70742
|
}
|
|
70906
|
-
|
|
70907
|
-
|
|
70908
|
-
|
|
70909
|
-
const
|
|
70910
|
-
|
|
70911
|
-
|
|
70912
|
-
if (notConfigured.length > 0) {
|
|
70913
|
-
reasons.push(`no runtime data \u2014 ${notConfigured.join(", ")} not configured`);
|
|
70914
|
-
}
|
|
70915
|
-
}
|
|
70916
|
-
if (r.gapAnalysis.gaps.length > 0) {
|
|
70917
|
-
const top = [...r.gapAnalysis.gaps].sort((a, b2) => b2.confidenceImpact - a.confidenceImpact)[0];
|
|
70918
|
-
reasons.push(
|
|
70919
|
-
`top gap: ${top.dimension} (\u2212${top.confidenceImpact.toFixed(2)} conf) \u2014 ${top.why}`
|
|
70920
|
-
);
|
|
70921
|
-
}
|
|
70922
|
-
if (r.correlation.missing.length > 0) {
|
|
70923
|
-
reasons.push(`missing evidence: ${r.correlation.missing[0].note}`);
|
|
70743
|
+
function renderSymbolList(label, symbols) {
|
|
70744
|
+
if (symbols.length === 0) return [];
|
|
70745
|
+
const lines = [`### ${label}`];
|
|
70746
|
+
const shown = symbols.slice(0, LIST_CAP);
|
|
70747
|
+
for (const s of shown) {
|
|
70748
|
+
lines.push(`- ${symbolLine(s)}`);
|
|
70924
70749
|
}
|
|
70925
|
-
|
|
70926
|
-
|
|
70927
|
-
|
|
70928
|
-
);
|
|
70750
|
+
const remaining = symbols.length - shown.length;
|
|
70751
|
+
if (remaining > 0) {
|
|
70752
|
+
lines.push(` +${remaining} more`);
|
|
70929
70753
|
}
|
|
70930
|
-
return
|
|
70931
|
-
}
|
|
70932
|
-
function runtimeSourceCaveat(r) {
|
|
70933
|
-
const status = r.sourceStatus;
|
|
70934
|
-
if (!status) return null;
|
|
70935
|
-
const contributed = status.sources.some((s) => s.status === "contributed");
|
|
70936
|
-
if (contributed) return null;
|
|
70937
|
-
const notConfigured = status.sources.filter((s) => s.status === "not-configured").map((s) => s.source);
|
|
70938
|
-
if (notConfigured.length === 0) return null;
|
|
70939
|
-
return `source-only \u2014 ${notConfigured.join(", ")} not configured`;
|
|
70754
|
+
return lines;
|
|
70940
70755
|
}
|
|
70941
|
-
function
|
|
70756
|
+
function renderChangeImpact(r) {
|
|
70942
70757
|
const lines = [];
|
|
70943
|
-
lines.push(`#
|
|
70944
|
-
lines.push(`Hint: ${r.input.hint}`);
|
|
70945
|
-
if (r.input.repo) lines.push(`Repo: ${r.input.repo}`);
|
|
70946
|
-
if (r.input.service) lines.push(`Service: ${r.input.service}`);
|
|
70947
|
-
if (r.input.since) lines.push(`Since: ${r.input.since}`);
|
|
70948
|
-
lines.push(`Confidence: ${r.confidence.toFixed(2)}`);
|
|
70949
|
-
const caveat = runtimeSourceCaveat(r);
|
|
70950
|
-
if (caveat) lines.push(` \u21B3 ${caveat}`);
|
|
70758
|
+
lines.push(`# Change Impact: ${r.base}..${r.compare}`);
|
|
70951
70759
|
lines.push("");
|
|
70952
70760
|
lines.push("## Summary");
|
|
70953
70761
|
lines.push(r.summary);
|
|
70954
70762
|
lines.push("");
|
|
70955
|
-
const
|
|
70956
|
-
if (
|
|
70957
|
-
lines.push(
|
|
70958
|
-
for (const reason of explainLines) {
|
|
70959
|
-
lines.push(` - ${reason}`);
|
|
70960
|
-
}
|
|
70763
|
+
const addedLines = renderSymbolList("Added", r.added);
|
|
70764
|
+
if (addedLines.length > 0) {
|
|
70765
|
+
lines.push(...addedLines);
|
|
70961
70766
|
lines.push("");
|
|
70962
70767
|
}
|
|
70963
|
-
|
|
70964
|
-
|
|
70965
|
-
|
|
70966
|
-
|
|
70967
|
-
|
|
70968
|
-
lines.push(
|
|
70969
|
-
`- ${s.title} (overlap ${(s.overlap * 100).toFixed(0)}%) \u2014 shared: ${s.sharedTags.join(", ")}`
|
|
70970
|
-
);
|
|
70971
|
-
if (s.summary) {
|
|
70972
|
-
const truncated = s.summary.length > 80 ? s.summary.slice(0, 77) + "..." : s.summary;
|
|
70973
|
-
lines.push(` ${truncated}`);
|
|
70974
|
-
}
|
|
70975
|
-
}
|
|
70976
|
-
lines.push(
|
|
70977
|
-
"Context only \u2014 past incidents inform, but never override, the current evidence."
|
|
70978
|
-
);
|
|
70768
|
+
const modifiedAfters = r.modified.map((m) => m.after);
|
|
70769
|
+
const modifiedLines = renderSymbolList("Modified", modifiedAfters);
|
|
70770
|
+
if (modifiedLines.length > 0) {
|
|
70771
|
+
lines.push(...modifiedLines);
|
|
70772
|
+
lines.push("");
|
|
70979
70773
|
}
|
|
70980
|
-
|
|
70981
|
-
|
|
70982
|
-
|
|
70983
|
-
lines.push("
|
|
70774
|
+
const removedLines = renderSymbolList("Removed", r.removed);
|
|
70775
|
+
if (removedLines.length > 0) {
|
|
70776
|
+
lines.push(...removedLines);
|
|
70777
|
+
lines.push("");
|
|
70778
|
+
}
|
|
70779
|
+
lines.push("## Affected flows");
|
|
70780
|
+
if (r.affectedFlows.length === 0) {
|
|
70781
|
+
lines.push("none");
|
|
70984
70782
|
} else {
|
|
70985
|
-
for (const
|
|
70986
|
-
|
|
70987
|
-
const sig = s.signature ? ` \u2014 ${s.signature}` : "";
|
|
70988
|
-
lines.push(`- ${s.name} (${s.filePath}:${line2})${sig}`);
|
|
70783
|
+
for (const f of r.affectedFlows) {
|
|
70784
|
+
lines.push(`- ${f.flowName} \u2014 changed: ${f.changedSymbols.join(", ")}`);
|
|
70989
70785
|
}
|
|
70990
70786
|
}
|
|
70991
|
-
lines.
|
|
70992
|
-
|
|
70993
|
-
|
|
70994
|
-
|
|
70995
|
-
|
|
70996
|
-
|
|
70997
|
-
|
|
70998
|
-
|
|
70999
|
-
|
|
71000
|
-
|
|
70787
|
+
return lines.join("\n");
|
|
70788
|
+
}
|
|
70789
|
+
function changeImpactToJSON(r) {
|
|
70790
|
+
return JSON.stringify(r, null, 2);
|
|
70791
|
+
}
|
|
70792
|
+
|
|
70793
|
+
// ../../packages/engine/src/deploy-timeline.ts
|
|
70794
|
+
init_cjs_shims();
|
|
70795
|
+
async function reconstructChangeTimeline(input, deps) {
|
|
70796
|
+
let commits = await gitLog(input.repoPath, {
|
|
70797
|
+
since: input.since,
|
|
70798
|
+
until: input.until
|
|
70799
|
+
});
|
|
70800
|
+
const service = input.service;
|
|
70801
|
+
if (service !== void 0) {
|
|
70802
|
+
commits = commits.filter(
|
|
70803
|
+
(c) => c.files.some((f) => f.toLowerCase().includes(service.toLowerCase()))
|
|
70804
|
+
);
|
|
70805
|
+
}
|
|
70806
|
+
let impact = null;
|
|
70807
|
+
if (commits.length >= 1) {
|
|
70808
|
+
const oldest = commits[commits.length - 1];
|
|
70809
|
+
const newest = commits[0];
|
|
70810
|
+
if (oldest !== void 0 && newest !== void 0) {
|
|
70811
|
+
const base2 = oldest.sha + "^";
|
|
70812
|
+
const compare = newest.sha;
|
|
70813
|
+
try {
|
|
70814
|
+
impact = await changeImpact({ base: base2, compare }, deps);
|
|
70815
|
+
} catch {
|
|
70816
|
+
impact = null;
|
|
71001
70817
|
}
|
|
71002
70818
|
}
|
|
71003
70819
|
}
|
|
70820
|
+
const impactPart = impact !== null ? "; " + (impact.summary.endsWith(".") ? impact.summary.slice(0, -1) : impact.summary) : "";
|
|
70821
|
+
const summary = commits.length + " commit(s)" + (service !== void 0 ? " touching " + service : "") + " in window" + (input.since !== void 0 ? " since " + input.since : "") + impactPart + ".";
|
|
70822
|
+
const note = "Changes are evidence, not conclusions \u2014 a change in this window is not automatically the cause.";
|
|
70823
|
+
return {
|
|
70824
|
+
window: {
|
|
70825
|
+
since: input.since ?? null,
|
|
70826
|
+
until: input.until ?? null,
|
|
70827
|
+
service: service ?? null
|
|
70828
|
+
},
|
|
70829
|
+
commits,
|
|
70830
|
+
changeImpact: impact,
|
|
70831
|
+
summary,
|
|
70832
|
+
note
|
|
70833
|
+
};
|
|
70834
|
+
}
|
|
70835
|
+
|
|
70836
|
+
// ../../packages/engine/src/render-timeline.ts
|
|
70837
|
+
init_cjs_shims();
|
|
70838
|
+
var COMMIT_CAP = 25;
|
|
70839
|
+
function renderChangeTimeline(t) {
|
|
70840
|
+
const lines = [];
|
|
70841
|
+
lines.push("# Change Timeline");
|
|
71004
70842
|
lines.push("");
|
|
71005
|
-
lines.push("##
|
|
71006
|
-
|
|
71007
|
-
|
|
71008
|
-
|
|
71009
|
-
|
|
71010
|
-
const at = ev.at ?? "structural";
|
|
71011
|
-
lines.push(` ${ev.order}. [${at}] ${ev.title}`);
|
|
71012
|
-
}
|
|
71013
|
-
}
|
|
70843
|
+
lines.push("## Summary");
|
|
70844
|
+
const sinceLabel = t.window.since ?? "(all history)";
|
|
70845
|
+
const untilLabel = t.window.until ?? "HEAD";
|
|
70846
|
+
lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
|
|
70847
|
+
lines.push(t.summary);
|
|
71014
70848
|
lines.push("");
|
|
71015
|
-
lines.push("
|
|
71016
|
-
if (r.timeline.boundaryCrossings.length === 0) {
|
|
71017
|
-
lines.push("(none)");
|
|
71018
|
-
} else {
|
|
71019
|
-
for (const bc of r.timeline.boundaryCrossings) {
|
|
71020
|
-
const producer = bc.producer ?? "?";
|
|
71021
|
-
const worker = bc.worker ?? "?";
|
|
71022
|
-
lines.push(` ${bc.queueName}: ${producer} -> ${worker}`);
|
|
71023
|
-
}
|
|
71024
|
-
}
|
|
70849
|
+
lines.push("> " + t.note);
|
|
71025
70850
|
lines.push("");
|
|
71026
|
-
lines.push("##
|
|
71027
|
-
|
|
71028
|
-
|
|
71029
|
-
lines.push(" (none)");
|
|
71030
|
-
} else {
|
|
71031
|
-
for (const chain of r.correlation.chains) {
|
|
71032
|
-
lines.push(` [${chain.strength.toFixed(2)}] ${chain.title} \u2014 ${chain.rationale}`);
|
|
71033
|
-
}
|
|
71034
|
-
}
|
|
71035
|
-
lines.push("### Related evidence groups");
|
|
71036
|
-
if (r.correlation.groups.length === 0) {
|
|
71037
|
-
lines.push(" (none)");
|
|
70851
|
+
lines.push("## Commits");
|
|
70852
|
+
if (t.commits.length === 0) {
|
|
70853
|
+
lines.push(" (none in window)");
|
|
71038
70854
|
} else {
|
|
71039
|
-
|
|
71040
|
-
|
|
70855
|
+
const shown = t.commits.slice(0, COMMIT_CAP);
|
|
70856
|
+
for (const c of shown) {
|
|
70857
|
+
lines.push(
|
|
70858
|
+
" " + c.shortSha + " " + c.dateIso + " " + c.subject + " (" + c.files.length + " file(s))"
|
|
70859
|
+
);
|
|
71041
70860
|
}
|
|
71042
|
-
|
|
71043
|
-
|
|
71044
|
-
|
|
71045
|
-
lines.push(" none");
|
|
71046
|
-
} else {
|
|
71047
|
-
for (const m of r.correlation.missing) {
|
|
71048
|
-
lines.push(` - ${m.note}`);
|
|
70861
|
+
const remaining = t.commits.length - shown.length;
|
|
70862
|
+
if (remaining > 0) {
|
|
70863
|
+
lines.push(" +" + remaining + " more");
|
|
71049
70864
|
}
|
|
71050
70865
|
}
|
|
71051
|
-
|
|
71052
|
-
|
|
71053
|
-
|
|
71054
|
-
|
|
71055
|
-
lines.push(
|
|
71056
|
-
|
|
71057
|
-
|
|
71058
|
-
|
|
71059
|
-
lines.push(`${i + 1}. [${c.finalScore.toFixed(2)} / ${c.band}] ${c.title}${queueTag}`);
|
|
71060
|
-
if (c.sourceEvidenceIds.length > 0) {
|
|
71061
|
-
lines.push(` evidence: ${c.sourceEvidenceIds.map(shortId).join(", ")}`);
|
|
70866
|
+
if (t.changeImpact !== null) {
|
|
70867
|
+
lines.push("");
|
|
70868
|
+
lines.push("## Change impact");
|
|
70869
|
+
lines.push("Git range: " + t.changeImpact.base + ".." + t.changeImpact.compare);
|
|
70870
|
+
lines.push(t.changeImpact.summary);
|
|
70871
|
+
if (t.changeImpact.affectedFlows.length > 0) {
|
|
70872
|
+
for (const f of t.changeImpact.affectedFlows) {
|
|
70873
|
+
lines.push(" - " + f.flowName);
|
|
71062
70874
|
}
|
|
71063
|
-
}
|
|
70875
|
+
}
|
|
71064
70876
|
}
|
|
71065
|
-
lines.
|
|
71066
|
-
|
|
71067
|
-
|
|
71068
|
-
|
|
71069
|
-
|
|
71070
|
-
|
|
71071
|
-
|
|
70877
|
+
return lines.join("\n");
|
|
70878
|
+
}
|
|
70879
|
+
function changeTimelineToJSON(t) {
|
|
70880
|
+
return JSON.stringify(t, null, 2);
|
|
70881
|
+
}
|
|
70882
|
+
|
|
70883
|
+
// ../../packages/engine/src/what-changed.ts
|
|
70884
|
+
init_cjs_shims();
|
|
70885
|
+
async function whatChanged(input, deps) {
|
|
70886
|
+
const t = await reconstructChangeTimeline(input, deps);
|
|
70887
|
+
const authorMap = /* @__PURE__ */ new Map();
|
|
70888
|
+
for (const commit of t.commits) {
|
|
70889
|
+
const prev = authorMap.get(commit.author) ?? 0;
|
|
70890
|
+
authorMap.set(commit.author, prev + 1);
|
|
71072
70891
|
}
|
|
71073
|
-
|
|
71074
|
-
|
|
71075
|
-
|
|
71076
|
-
for (const
|
|
71077
|
-
|
|
71078
|
-
|
|
71079
|
-
|
|
71080
|
-
|
|
71081
|
-
if (h.missingEvidence.length > 0) {
|
|
71082
|
-
lines.push(` \xB7 missing: ${h.missingEvidence.join("; ")}`);
|
|
70892
|
+
const contributors = [...authorMap].map(([author, commits]) => ({ author, commits })).sort((a, b2) => b2.commits - a.commits).slice(0, 5);
|
|
70893
|
+
const queueFileSet = /* @__PURE__ */ new Set();
|
|
70894
|
+
for (const commit of t.commits) {
|
|
70895
|
+
for (const file of commit.files) {
|
|
70896
|
+
const lower = file.toLowerCase();
|
|
70897
|
+
if (/\.(processor|module)\.ts$/i.test(file) || lower.includes("queue") || lower.includes("processor")) {
|
|
70898
|
+
queueFileSet.add(file);
|
|
70899
|
+
if (queueFileSet.size >= 20) break;
|
|
71083
70900
|
}
|
|
71084
70901
|
}
|
|
70902
|
+
if (queueFileSet.size >= 20) break;
|
|
71085
70903
|
}
|
|
70904
|
+
const queueFiles = [...queueFileSet].slice(0, 20);
|
|
70905
|
+
const queueTopology = {
|
|
70906
|
+
touched: queueFiles.length > 0,
|
|
70907
|
+
files: queueFiles
|
|
70908
|
+
};
|
|
70909
|
+
const topCommits = t.commits.slice(0, 3);
|
|
70910
|
+
const topContributor = contributors[0];
|
|
70911
|
+
const summary = t.commits.length + " commit(s)" + (input.service !== void 0 ? " touching " + input.service : "") + (input.since !== void 0 ? " since " + input.since : "") + (t.changeImpact !== null ? "; " + t.changeImpact.added.length + " symbols added/" + t.changeImpact.modified.length + " modified/" + t.changeImpact.removed.length + " removed" : "") + (topContributor !== void 0 ? "; top contributor " + topContributor.author + " (" + topContributor.commits + ")" : "") + (queueTopology.touched ? "; queue/worker files changed (topology may have shifted)" : "") + ".";
|
|
70912
|
+
const note = "A change is evidence, not a conclusion \u2014 confirm with logs/metrics before blaming a change.";
|
|
70913
|
+
return {
|
|
70914
|
+
window: {
|
|
70915
|
+
since: input.since ?? null,
|
|
70916
|
+
until: input.until ?? null,
|
|
70917
|
+
service: input.service ?? null
|
|
70918
|
+
},
|
|
70919
|
+
commitCount: t.commits.length,
|
|
70920
|
+
topCommits,
|
|
70921
|
+
changeImpact: t.changeImpact,
|
|
70922
|
+
contributors,
|
|
70923
|
+
queueTopology,
|
|
70924
|
+
summary,
|
|
70925
|
+
note
|
|
70926
|
+
};
|
|
70927
|
+
}
|
|
70928
|
+
|
|
70929
|
+
// ../../packages/engine/src/render-what-changed.ts
|
|
70930
|
+
init_cjs_shims();
|
|
70931
|
+
function renderWhatChanged(r) {
|
|
70932
|
+
const lines = [];
|
|
70933
|
+
lines.push("# What changed");
|
|
71086
70934
|
lines.push("");
|
|
71087
|
-
|
|
71088
|
-
|
|
71089
|
-
|
|
71090
|
-
|
|
71091
|
-
|
|
71092
|
-
|
|
71093
|
-
|
|
71094
|
-
const evCite = step.evidenceIds.length > 0 ? ` (evidence: ${step.evidenceIds.map(shortId).join(", ")})` : "";
|
|
71095
|
-
lines.push(`${prefix} [${step.role}] ${step.label}${evCite}`);
|
|
71096
|
-
}
|
|
71097
|
-
}
|
|
71098
|
-
lines.push("");
|
|
71099
|
-
}
|
|
71100
|
-
lines.push("## Evidence gaps (what we don't know)");
|
|
71101
|
-
if (r.gapAnalysis.gaps.length === 0) {
|
|
71102
|
-
lines.push("(no major evidence gaps)");
|
|
71103
|
-
} else {
|
|
71104
|
-
for (const gap of r.gapAnalysis.gaps) {
|
|
71105
|
-
lines.push(
|
|
71106
|
-
` - ${gap.dimension}: ${gap.why} \u2192 next: ${gap.nextSource} (\u2212${gap.confidenceImpact.toFixed(2)} conf)`
|
|
71107
|
-
);
|
|
71108
|
-
}
|
|
70935
|
+
const sinceLabel = r.window.since ?? "(all history)";
|
|
70936
|
+
const untilLabel = r.window.until ?? "HEAD";
|
|
70937
|
+
lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
|
|
70938
|
+
lines.push(r.summary);
|
|
70939
|
+
lines.push("");
|
|
70940
|
+
lines.push("> " + r.note);
|
|
70941
|
+
if (r.topCommits.length > 0) {
|
|
71109
70942
|
lines.push("");
|
|
71110
|
-
lines.push("
|
|
71111
|
-
for (const
|
|
71112
|
-
lines.push(
|
|
70943
|
+
lines.push("## Top commits");
|
|
70944
|
+
for (const c of r.topCommits) {
|
|
70945
|
+
lines.push(" " + c.shortSha + " " + c.dateIso + " " + c.subject);
|
|
71113
70946
|
}
|
|
70947
|
+
}
|
|
70948
|
+
if (r.contributors.length > 0) {
|
|
71114
70949
|
lines.push("");
|
|
71115
70950
|
lines.push(
|
|
71116
|
-
|
|
70951
|
+
"## Top contributors: " + r.contributors.map((c) => c.author + " \xD7" + c.commits).join(", ")
|
|
71117
70952
|
);
|
|
71118
70953
|
}
|
|
71119
70954
|
lines.push("");
|
|
71120
|
-
|
|
71121
|
-
|
|
71122
|
-
|
|
70955
|
+
if (r.queueTopology.touched) {
|
|
70956
|
+
lines.push(
|
|
70957
|
+
"## Queue topology: touched \u2014 " + r.queueTopology.files.slice(0, 6).join(", ")
|
|
70958
|
+
);
|
|
71123
70959
|
} else {
|
|
71124
|
-
|
|
71125
|
-
lines.push(`- ${shortId(e.id)} [${e.source}/${e.kind}] ${e.title}`);
|
|
71126
|
-
}
|
|
70960
|
+
lines.push("## Queue topology: no queue/worker files changed");
|
|
71127
70961
|
}
|
|
71128
|
-
|
|
71129
|
-
const queueGroups = groupQueueEvidence(r.evidence);
|
|
71130
|
-
const queueGap = r.gapAnalysis.gaps.find((g) => g.dimension === "queue runtime state");
|
|
71131
|
-
if (queueGroups.size > 0 || queueGap) {
|
|
71132
|
-
lines.push("## Queue runtime");
|
|
71133
|
-
if (queueGroups.size === 0) {
|
|
71134
|
-
lines.push(` (${queueGap.why})`);
|
|
71135
|
-
} else {
|
|
71136
|
-
for (const [name, evs] of queueGroups) {
|
|
71137
|
-
lines.push(` ${name}`);
|
|
71138
|
-
for (const e of evs) {
|
|
71139
|
-
const detail = isQueueSummary(e) ? fmtQueueCounts(e.payload) : stripQueueName(e.title, name);
|
|
71140
|
-
lines.push(` [${e.relevance.toFixed(2)}] ${detail}`);
|
|
71141
|
-
}
|
|
71142
|
-
}
|
|
71143
|
-
}
|
|
70962
|
+
if (r.changeImpact !== null) {
|
|
71144
70963
|
lines.push("");
|
|
71145
|
-
|
|
71146
|
-
|
|
71147
|
-
|
|
71148
|
-
|
|
71149
|
-
|
|
71150
|
-
|
|
71151
|
-
lines.push(` Commits: ${rc.commits.length}${rc.truncated ? "+" : ""}`);
|
|
71152
|
-
for (const c of rc.commits.slice(0, 10)) {
|
|
71153
|
-
lines.push(` ${c.shortSha} ${c.subject}`);
|
|
71154
|
-
}
|
|
71155
|
-
if (rc.fileStats.length > 0) {
|
|
71156
|
-
const fileCount = rc.changedFiles.length;
|
|
71157
|
-
lines.push(` Files: ${fileCount}${rc.truncated ? "+" : ""}`);
|
|
71158
|
-
for (const f of rc.fileStats.slice(0, 8)) {
|
|
71159
|
-
lines.push(` +${f.insertions} -${f.deletions} ${f.path}`);
|
|
70964
|
+
lines.push(
|
|
70965
|
+
"## Affected flows: " + r.changeImpact.affectedFlows.length + " execution flow(s) affected"
|
|
70966
|
+
);
|
|
70967
|
+
if (r.changeImpact.affectedFlows.length > 0) {
|
|
70968
|
+
for (const f of r.changeImpact.affectedFlows) {
|
|
70969
|
+
lines.push(" - " + f.flowName);
|
|
71160
70970
|
}
|
|
71161
70971
|
}
|
|
71162
|
-
if (rc.truncated && rc.truncatedReason) {
|
|
71163
|
-
lines.push(` (truncated: ${rc.truncatedReason})`);
|
|
71164
|
-
}
|
|
71165
|
-
lines.push("");
|
|
71166
|
-
}
|
|
71167
|
-
lines.push("## Next actions");
|
|
71168
|
-
if (r.nextActions.length === 0) {
|
|
71169
|
-
lines.push("(none)");
|
|
71170
|
-
} else {
|
|
71171
|
-
for (const a of r.nextActions) {
|
|
71172
|
-
lines.push(`- ${a}`);
|
|
71173
|
-
}
|
|
71174
70972
|
}
|
|
71175
70973
|
return lines.join("\n");
|
|
71176
70974
|
}
|
|
71177
|
-
function
|
|
70975
|
+
function whatChangedToJSON(r) {
|
|
71178
70976
|
return JSON.stringify(r, null, 2);
|
|
71179
70977
|
}
|
|
71180
|
-
|
|
71181
|
-
|
|
71182
|
-
|
|
71183
|
-
|
|
71184
|
-
|
|
71185
|
-
|
|
71186
|
-
|
|
71187
|
-
|
|
71188
|
-
|
|
71189
|
-
|
|
71190
|
-
|
|
71191
|
-
|
|
71192
|
-
|
|
71193
|
-
|
|
71194
|
-
|
|
71195
|
-
|
|
71196
|
-
|
|
70978
|
+
|
|
70979
|
+
// ../../packages/engine/src/architecture.ts
|
|
70980
|
+
init_cjs_shims();
|
|
70981
|
+
var EXTERNAL_MARKERS = [
|
|
70982
|
+
"zoho",
|
|
70983
|
+
"stripe",
|
|
70984
|
+
"prisma",
|
|
70985
|
+
"redis",
|
|
70986
|
+
"bullmq",
|
|
70987
|
+
"twilio",
|
|
70988
|
+
"sendgrid",
|
|
70989
|
+
"clerk",
|
|
70990
|
+
"pusher",
|
|
70991
|
+
"axios",
|
|
70992
|
+
"elasticsearch",
|
|
70993
|
+
"prometheus",
|
|
70994
|
+
"mongo",
|
|
70995
|
+
"graphql"
|
|
70996
|
+
];
|
|
70997
|
+
async function discoverArchitecture(deps) {
|
|
70998
|
+
const nodeStats = await (async () => {
|
|
70999
|
+
try {
|
|
71000
|
+
const result = await deps.code.cypher(
|
|
71001
|
+
"MATCH (n) RETURN label(n), count(n) ORDER BY count(n) DESC"
|
|
71002
|
+
);
|
|
71003
|
+
return result.rows.map((row) => ({
|
|
71004
|
+
label: String(row[0] ?? ""),
|
|
71005
|
+
count: Number(row[1] ?? 0)
|
|
71006
|
+
})).filter((s) => s.label !== "Embedding");
|
|
71007
|
+
} catch {
|
|
71008
|
+
return [];
|
|
71197
71009
|
}
|
|
71198
|
-
|
|
71199
|
-
|
|
71200
|
-
|
|
71201
|
-
|
|
71202
|
-
|
|
71203
|
-
} else {
|
|
71204
|
-
for (const s of r.similarIncidents) {
|
|
71205
|
-
out.push(
|
|
71206
|
-
`- ${s.title} (overlap ${(s.overlap * 100).toFixed(0)}%) \u2014 shared: ${s.sharedTags.join(", ")}`
|
|
71010
|
+
})();
|
|
71011
|
+
const subsystems = await (async () => {
|
|
71012
|
+
try {
|
|
71013
|
+
const result = await deps.code.cypher(
|
|
71014
|
+
'MATCH (m)-[r:CodeRelation]->(c:Community) WHERE r.rel_type = "member_of" RETURN c.name, count(m) ORDER BY count(m) DESC'
|
|
71207
71015
|
);
|
|
71208
|
-
|
|
71209
|
-
|
|
71210
|
-
|
|
71016
|
+
return result.rows.slice(0, 12).map((row) => ({
|
|
71017
|
+
name: String(row[0] ?? ""),
|
|
71018
|
+
members: Number(row[1] ?? 0)
|
|
71019
|
+
}));
|
|
71020
|
+
} catch {
|
|
71021
|
+
return [];
|
|
71022
|
+
}
|
|
71023
|
+
})();
|
|
71024
|
+
const asyncBoundaries = await (async () => {
|
|
71025
|
+
try {
|
|
71026
|
+
const edges = await listQueueEdges(deps.db, { project: deps.project });
|
|
71027
|
+
const byQueue = /* @__PURE__ */ new Map();
|
|
71028
|
+
for (const edge of edges) {
|
|
71029
|
+
const key = edge.queueName;
|
|
71030
|
+
if (!byQueue.has(key)) {
|
|
71031
|
+
byQueue.set(key, { producers: /* @__PURE__ */ new Set(), workers: /* @__PURE__ */ new Set() });
|
|
71032
|
+
}
|
|
71033
|
+
const entry2 = byQueue.get(key);
|
|
71034
|
+
if (edge.producerSymbol != null && edge.producerSymbol !== "") {
|
|
71035
|
+
entry2.producers.add(edge.producerSymbol);
|
|
71036
|
+
}
|
|
71037
|
+
if (edge.workerSymbol != null && edge.workerSymbol !== "") {
|
|
71038
|
+
entry2.workers.add(edge.workerSymbol);
|
|
71039
|
+
}
|
|
71211
71040
|
}
|
|
71041
|
+
return Array.from(byQueue.entries()).map(([queueName, { producers, workers }]) => ({
|
|
71042
|
+
queueName,
|
|
71043
|
+
producers: Array.from(producers),
|
|
71044
|
+
workers: Array.from(workers)
|
|
71045
|
+
}));
|
|
71046
|
+
} catch {
|
|
71047
|
+
return [];
|
|
71212
71048
|
}
|
|
71213
|
-
|
|
71214
|
-
|
|
71215
|
-
|
|
71216
|
-
|
|
71217
|
-
|
|
71218
|
-
const mdQueueIds = queueEvidenceIds(r.evidence);
|
|
71219
|
-
out.push("## Suspected causes");
|
|
71220
|
-
if (r.suspectedCauses.length === 0) {
|
|
71221
|
-
out.push("_none_");
|
|
71222
|
-
} else {
|
|
71223
|
-
r.suspectedCauses.forEach((c, i) => {
|
|
71224
|
-
const queueTag = c.sourceEvidenceIds.some((id) => mdQueueIds.has(id)) ? " `[\u2191 queue]`" : "";
|
|
71225
|
-
out.push(`${i + 1}. **(${c.finalScore.toFixed(2)}, ${c.band})**${queueTag} ${c.title}`);
|
|
71226
|
-
});
|
|
71227
|
-
}
|
|
71228
|
-
out.push("");
|
|
71229
|
-
out.push("## Hypotheses");
|
|
71230
|
-
const mdAllUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
|
|
71231
|
-
if (mdAllUnconfirmed) {
|
|
71232
|
-
out.push(
|
|
71233
|
-
"_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
|
|
71234
|
-
);
|
|
71235
|
-
}
|
|
71236
|
-
if (r.hypotheses.length === 0) {
|
|
71237
|
-
out.push("_none_");
|
|
71238
|
-
} else {
|
|
71239
|
-
for (const h of r.hypotheses) {
|
|
71240
|
-
out.push(
|
|
71241
|
-
`- \`${h.verdict}\` **${h.confidence.toFixed(2)}** (was ${h.priorConfidence.toFixed(2)}) \u2014 ${h.category}: ${h.statement}`
|
|
71049
|
+
})();
|
|
71050
|
+
const keyFlows = await (async () => {
|
|
71051
|
+
try {
|
|
71052
|
+
const result = await deps.code.cypher(
|
|
71053
|
+
"MATCH (p:Process) RETURN p.name ORDER BY p.name"
|
|
71242
71054
|
);
|
|
71055
|
+
return result.rows.slice(0, 20).map((row) => String(row[0] ?? ""));
|
|
71056
|
+
} catch {
|
|
71057
|
+
return [];
|
|
71243
71058
|
}
|
|
71244
|
-
}
|
|
71245
|
-
|
|
71246
|
-
|
|
71247
|
-
|
|
71248
|
-
|
|
71249
|
-
} else {
|
|
71250
|
-
for (const gap of r.gapAnalysis.gaps) {
|
|
71251
|
-
out.push(
|
|
71252
|
-
`- **${gap.dimension}**: ${gap.why} \u2192 _next: ${gap.nextSource}_ (\u2212${gap.confidenceImpact.toFixed(2)} conf)`
|
|
71059
|
+
})();
|
|
71060
|
+
const deadCode = await (async () => {
|
|
71061
|
+
try {
|
|
71062
|
+
const result = await deps.code.cypher(
|
|
71063
|
+
"MATCH (n) WHERE n.is_dead = true RETURN count(n)"
|
|
71253
71064
|
);
|
|
71065
|
+
return Number(result.rows[0]?.[0] ?? 0);
|
|
71066
|
+
} catch {
|
|
71067
|
+
return 0;
|
|
71254
71068
|
}
|
|
71255
|
-
|
|
71256
|
-
|
|
71257
|
-
|
|
71258
|
-
|
|
71259
|
-
|
|
71260
|
-
|
|
71261
|
-
|
|
71262
|
-
|
|
71263
|
-
|
|
71264
|
-
}
|
|
71265
|
-
out.push("");
|
|
71266
|
-
out.push("## Timeline");
|
|
71267
|
-
if (r.timeline.events.length === 0) {
|
|
71268
|
-
out.push("_none_");
|
|
71269
|
-
} else {
|
|
71270
|
-
for (const ev of r.timeline.events) {
|
|
71271
|
-
out.push(`${ev.order}. \`${ev.at ?? "structural"}\` ${ev.title}`);
|
|
71069
|
+
})();
|
|
71070
|
+
const highCouplingPairs = await (async () => {
|
|
71071
|
+
try {
|
|
71072
|
+
const result = await deps.code.cypher(
|
|
71073
|
+
'MATCH ()-[r:CodeRelation]->() WHERE r.rel_type = "coupled_with" AND r.co_changes >= 3 RETURN count(r)'
|
|
71074
|
+
);
|
|
71075
|
+
return Number(result.rows[0]?.[0] ?? 0);
|
|
71076
|
+
} catch {
|
|
71077
|
+
return 0;
|
|
71272
71078
|
}
|
|
71273
|
-
}
|
|
71274
|
-
|
|
71275
|
-
|
|
71276
|
-
|
|
71277
|
-
|
|
71278
|
-
|
|
71279
|
-
|
|
71280
|
-
|
|
71079
|
+
})();
|
|
71080
|
+
const externalSystems = await (async () => {
|
|
71081
|
+
try {
|
|
71082
|
+
const results = await Promise.all(
|
|
71083
|
+
EXTERNAL_MARKERS.map(async (marker) => {
|
|
71084
|
+
try {
|
|
71085
|
+
const result = await deps.code.cypher(
|
|
71086
|
+
`MATCH (n:File) WHERE n.content CONTAINS "${marker}" RETURN count(n)`
|
|
71087
|
+
);
|
|
71088
|
+
const count = Number(result.rows[0]?.[0] ?? 0);
|
|
71089
|
+
return { name: marker, files: count };
|
|
71090
|
+
} catch {
|
|
71091
|
+
return { name: marker, files: 0 };
|
|
71092
|
+
}
|
|
71093
|
+
})
|
|
71094
|
+
);
|
|
71095
|
+
return results.filter((r) => r.files > 0).sort((a, b2) => b2.files - a.files);
|
|
71096
|
+
} catch {
|
|
71097
|
+
return [];
|
|
71281
71098
|
}
|
|
71282
|
-
}
|
|
71283
|
-
|
|
71284
|
-
const
|
|
71285
|
-
const
|
|
71286
|
-
if (mdQueueGroups.size > 0 || mdQueueGap) {
|
|
71287
|
-
out.push("## Queue runtime");
|
|
71288
|
-
if (mdQueueGroups.size === 0) {
|
|
71289
|
-
out.push(`_${mdQueueGap.why}_`);
|
|
71290
|
-
} else {
|
|
71291
|
-
for (const [name, evs] of mdQueueGroups) {
|
|
71292
|
-
out.push(`**${name}**`);
|
|
71293
|
-
for (const e of evs) {
|
|
71294
|
-
const detail = isQueueSummary(e) ? fmtQueueCounts(e.payload) : stripQueueName(e.title, name);
|
|
71295
|
-
out.push(`- \`${e.relevance.toFixed(2)}\` ${detail}`);
|
|
71296
|
-
}
|
|
71297
|
-
}
|
|
71298
|
-
}
|
|
71299
|
-
out.push("");
|
|
71300
|
-
}
|
|
71301
|
-
out.push("## Next actions");
|
|
71302
|
-
if (r.nextActions.length === 0) {
|
|
71303
|
-
out.push("_none_");
|
|
71304
|
-
} else {
|
|
71305
|
-
for (const a of r.nextActions) {
|
|
71306
|
-
out.push(`- [ ] ${a}`);
|
|
71307
|
-
}
|
|
71308
|
-
}
|
|
71309
|
-
out.push("");
|
|
71310
|
-
out.push("---");
|
|
71311
|
-
out.push("_Generated by Horus \u2014 deterministic report, no AI._");
|
|
71312
|
-
return out.join("\n");
|
|
71313
|
-
}
|
|
71314
|
-
|
|
71315
|
-
// ../../packages/engine/src/changes.ts
|
|
71316
|
-
init_cjs_shims();
|
|
71317
|
-
async function changeImpact(input, deps) {
|
|
71318
|
-
const compare = input.compare ?? "HEAD";
|
|
71319
|
-
const changes = await deps.code.detectChanges({ base: input.base, compare });
|
|
71320
|
-
const presentSymbols = [
|
|
71321
|
-
...changes.added,
|
|
71322
|
-
...changes.modified.map((m) => m.after)
|
|
71323
|
-
];
|
|
71324
|
-
const capped = presentSymbols.filter((s) => !s.id.startsWith("file:")).slice(0, 25);
|
|
71325
|
-
const flowsPerSymbol = await Promise.all(
|
|
71326
|
-
capped.map(async (s) => {
|
|
71327
|
-
try {
|
|
71328
|
-
return await deps.code.flowsFor(s.id);
|
|
71329
|
-
} catch {
|
|
71330
|
-
return [];
|
|
71331
|
-
}
|
|
71332
|
-
})
|
|
71333
|
-
);
|
|
71334
|
-
const flowMap = /* @__PURE__ */ new Map();
|
|
71335
|
-
for (let i = 0; i < capped.length; i++) {
|
|
71336
|
-
const sym = capped[i];
|
|
71337
|
-
if (sym === void 0) continue;
|
|
71338
|
-
const flows = flowsPerSymbol[i] ?? [];
|
|
71339
|
-
for (const flow of flows) {
|
|
71340
|
-
const existing = flowMap.get(flow.id);
|
|
71341
|
-
if (existing !== void 0) {
|
|
71342
|
-
existing.changedSymbols.add(sym.name);
|
|
71343
|
-
} else {
|
|
71344
|
-
flowMap.set(flow.id, {
|
|
71345
|
-
flowName: flow.name,
|
|
71346
|
-
changedSymbols: /* @__PURE__ */ new Set([sym.name])
|
|
71347
|
-
});
|
|
71348
|
-
}
|
|
71349
|
-
}
|
|
71350
|
-
}
|
|
71351
|
-
const affectedFlows = [...flowMap].map(([flowId, v]) => ({
|
|
71352
|
-
flowId,
|
|
71353
|
-
flowName: v.flowName,
|
|
71354
|
-
changedSymbols: [...v.changedSymbols]
|
|
71355
|
-
}));
|
|
71356
|
-
const summary = changes.added.length + " added, " + changes.modified.length + " modified, " + changes.removed.length + " removed between " + input.base + ".." + compare + "; " + affectedFlows.length + " execution flow(s) affected.";
|
|
71099
|
+
})();
|
|
71100
|
+
const largestSubsystem = subsystems[0];
|
|
71101
|
+
const largestDesc = largestSubsystem != null ? `${largestSubsystem.name} with ${largestSubsystem.members} symbols` : "none";
|
|
71102
|
+
const summary = `${subsystems.length} subsystems (largest: ${largestDesc}), ${asyncBoundaries.length} async queue boundaries, ${externalSystems.length} external systems, ${deadCode} unreferenced symbols.`;
|
|
71357
71103
|
return {
|
|
71358
|
-
|
|
71359
|
-
|
|
71360
|
-
|
|
71361
|
-
|
|
71362
|
-
|
|
71363
|
-
|
|
71104
|
+
nodeStats,
|
|
71105
|
+
subsystems,
|
|
71106
|
+
asyncBoundaries,
|
|
71107
|
+
keyFlows,
|
|
71108
|
+
externalSystems,
|
|
71109
|
+
fragile: { deadCode, highCouplingPairs },
|
|
71364
71110
|
summary
|
|
71365
71111
|
};
|
|
71366
71112
|
}
|
|
71367
71113
|
|
|
71368
|
-
// ../../packages/engine/src/render-
|
|
71114
|
+
// ../../packages/engine/src/render-architecture.ts
|
|
71369
71115
|
init_cjs_shims();
|
|
71370
|
-
|
|
71371
|
-
function symbolLine(s) {
|
|
71372
|
-
return `${s.name} (${s.filePath})`;
|
|
71373
|
-
}
|
|
71374
|
-
function renderSymbolList(label, symbols) {
|
|
71375
|
-
if (symbols.length === 0) return [];
|
|
71376
|
-
const lines = [`### ${label}`];
|
|
71377
|
-
const shown = symbols.slice(0, LIST_CAP);
|
|
71378
|
-
for (const s of shown) {
|
|
71379
|
-
lines.push(`- ${symbolLine(s)}`);
|
|
71380
|
-
}
|
|
71381
|
-
const remaining = symbols.length - shown.length;
|
|
71382
|
-
if (remaining > 0) {
|
|
71383
|
-
lines.push(` +${remaining} more`);
|
|
71384
|
-
}
|
|
71385
|
-
return lines;
|
|
71386
|
-
}
|
|
71387
|
-
function renderChangeImpact(r) {
|
|
71116
|
+
function renderArchitecture(m) {
|
|
71388
71117
|
const lines = [];
|
|
71389
|
-
lines.push(
|
|
71118
|
+
lines.push("# Architecture (discovered)");
|
|
71390
71119
|
lines.push("");
|
|
71391
|
-
lines.push(
|
|
71392
|
-
lines.push(r.summary);
|
|
71120
|
+
lines.push(m.summary);
|
|
71393
71121
|
lines.push("");
|
|
71394
|
-
|
|
71395
|
-
if (
|
|
71396
|
-
lines.push(
|
|
71397
|
-
|
|
71122
|
+
lines.push("## Subsystems (most central first)");
|
|
71123
|
+
if (m.subsystems.length === 0) {
|
|
71124
|
+
lines.push("(none)");
|
|
71125
|
+
} else {
|
|
71126
|
+
for (const s of m.subsystems) {
|
|
71127
|
+
lines.push(`- ${s.name} \u2014 ${s.members} members`);
|
|
71128
|
+
}
|
|
71398
71129
|
}
|
|
71399
|
-
|
|
71400
|
-
|
|
71401
|
-
if (
|
|
71402
|
-
lines.push(
|
|
71403
|
-
|
|
71130
|
+
lines.push("");
|
|
71131
|
+
lines.push("## Async boundaries");
|
|
71132
|
+
if (m.asyncBoundaries.length === 0) {
|
|
71133
|
+
lines.push("(none)");
|
|
71134
|
+
} else {
|
|
71135
|
+
for (const b2 of m.asyncBoundaries) {
|
|
71136
|
+
const producers = b2.producers.length > 0 ? b2.producers.join(", ") : "(unknown)";
|
|
71137
|
+
const workers = b2.workers.length > 0 ? b2.workers.join(", ") : "(unknown)";
|
|
71138
|
+
lines.push(`- ${b2.queueName}: ${producers} -> ${workers}`);
|
|
71139
|
+
}
|
|
71404
71140
|
}
|
|
71405
|
-
|
|
71406
|
-
|
|
71407
|
-
|
|
71408
|
-
lines.push("");
|
|
71141
|
+
lines.push("");
|
|
71142
|
+
lines.push("## External systems");
|
|
71143
|
+
if (m.externalSystems.length === 0) {
|
|
71144
|
+
lines.push("(none)");
|
|
71145
|
+
} else {
|
|
71146
|
+
for (const e of m.externalSystems) {
|
|
71147
|
+
lines.push(`- ${e.name} (${e.files} files)`);
|
|
71148
|
+
}
|
|
71409
71149
|
}
|
|
71410
|
-
lines.push("
|
|
71411
|
-
|
|
71412
|
-
|
|
71150
|
+
lines.push("");
|
|
71151
|
+
lines.push("## Key flows");
|
|
71152
|
+
if (m.keyFlows.length === 0) {
|
|
71153
|
+
lines.push("(none)");
|
|
71413
71154
|
} else {
|
|
71414
|
-
for (const f of
|
|
71415
|
-
lines.push(`- ${f
|
|
71155
|
+
for (const f of m.keyFlows) {
|
|
71156
|
+
lines.push(`- ${f}`);
|
|
71416
71157
|
}
|
|
71417
71158
|
}
|
|
71159
|
+
lines.push("");
|
|
71160
|
+
lines.push("## Graph");
|
|
71161
|
+
if (m.nodeStats.length === 0) {
|
|
71162
|
+
lines.push("(no data)");
|
|
71163
|
+
} else {
|
|
71164
|
+
for (const s of m.nodeStats) {
|
|
71165
|
+
lines.push(`${s.label} \xD7 ${s.count}`);
|
|
71166
|
+
}
|
|
71167
|
+
}
|
|
71168
|
+
lines.push("");
|
|
71169
|
+
lines.push("## Fragility");
|
|
71170
|
+
lines.push(`- Unreferenced symbols: ${m.fragile.deadCode}`);
|
|
71171
|
+
lines.push(`- High-coupling pairs (co-changes \u2265 3): ${m.fragile.highCouplingPairs}`);
|
|
71418
71172
|
return lines.join("\n");
|
|
71419
71173
|
}
|
|
71420
|
-
function
|
|
71421
|
-
return JSON.stringify(
|
|
71174
|
+
function architectureToJSON(m) {
|
|
71175
|
+
return JSON.stringify(m, null, 2);
|
|
71422
71176
|
}
|
|
71423
71177
|
|
|
71424
|
-
// ../../packages/engine/src/
|
|
71178
|
+
// ../../packages/engine/src/blast-radius.ts
|
|
71425
71179
|
init_cjs_shims();
|
|
71426
|
-
async function
|
|
71427
|
-
|
|
71428
|
-
|
|
71429
|
-
|
|
71430
|
-
|
|
71431
|
-
|
|
71432
|
-
|
|
71433
|
-
|
|
71434
|
-
|
|
71435
|
-
|
|
71180
|
+
async function analyzeBlastRadius(query, deps, depth = 3) {
|
|
71181
|
+
const seeds = await deps.code.searchSymbols(query, 5);
|
|
71182
|
+
const top = seeds[0];
|
|
71183
|
+
if (!top) return null;
|
|
71184
|
+
const [ctx, impact] = await Promise.all([
|
|
71185
|
+
deps.code.context(top.id),
|
|
71186
|
+
deps.code.impact(top.id, depth)
|
|
71187
|
+
]);
|
|
71188
|
+
const upstream = ctx.callees;
|
|
71189
|
+
const downstream = impact.byDepth;
|
|
71190
|
+
const edges = await listQueueEdges(deps.db, { project: deps.project });
|
|
71191
|
+
const asyncDownstreamMap = /* @__PURE__ */ new Map();
|
|
71192
|
+
for (const edge of edges) {
|
|
71193
|
+
if (edge.producerFile === top.filePath || edge.producerSymbol === top.name) {
|
|
71194
|
+
const key = edge.queueName + "|" + (edge.workerSymbol ?? "unknown-worker");
|
|
71195
|
+
if (!asyncDownstreamMap.has(key)) {
|
|
71196
|
+
asyncDownstreamMap.set(key, {
|
|
71197
|
+
queueName: edge.queueName,
|
|
71198
|
+
counterpart: edge.workerSymbol ?? "unknown-worker",
|
|
71199
|
+
counterpartFile: edge.workerFile
|
|
71200
|
+
});
|
|
71201
|
+
}
|
|
71202
|
+
}
|
|
71436
71203
|
}
|
|
71437
|
-
|
|
71438
|
-
|
|
71439
|
-
|
|
71440
|
-
|
|
71441
|
-
|
|
71442
|
-
|
|
71443
|
-
|
|
71444
|
-
|
|
71445
|
-
|
|
71446
|
-
|
|
71447
|
-
|
|
71204
|
+
const asyncDownstream = Array.from(asyncDownstreamMap.values());
|
|
71205
|
+
const asyncUpstreamMap = /* @__PURE__ */ new Map();
|
|
71206
|
+
for (const edge of edges) {
|
|
71207
|
+
if (edge.workerFile === top.filePath || edge.workerSymbol === top.name) {
|
|
71208
|
+
const key = edge.queueName + "|" + (edge.producerSymbol ?? "unknown-producer");
|
|
71209
|
+
if (!asyncUpstreamMap.has(key)) {
|
|
71210
|
+
asyncUpstreamMap.set(key, {
|
|
71211
|
+
queueName: edge.queueName,
|
|
71212
|
+
counterpart: edge.producerSymbol ?? "unknown-producer",
|
|
71213
|
+
counterpartFile: edge.producerFile
|
|
71214
|
+
});
|
|
71448
71215
|
}
|
|
71449
71216
|
}
|
|
71450
71217
|
}
|
|
71451
|
-
const
|
|
71452
|
-
const
|
|
71453
|
-
const
|
|
71218
|
+
const asyncUpstream = Array.from(asyncUpstreamMap.values());
|
|
71219
|
+
const blastRadius = impact.affected + asyncDownstream.length;
|
|
71220
|
+
const criticality = blastRadius >= 10 ? "high" : blastRadius >= 3 ? "medium" : "low";
|
|
71221
|
+
const summary = "If " + top.name + " (" + top.filePath + ") fails, ~" + blastRadius + " symbol(s) are affected downstream" + (asyncDownstream.length ? " (incl. " + asyncDownstream.length + " across async queue boundaries)" : "") + "; it depends on " + upstream.length + " symbol(s) upstream" + (asyncUpstream.length ? " + " + asyncUpstream.length + " async producer(s)" : "") + ". Criticality: " + criticality + ".";
|
|
71222
|
+
const note = "The component reporting an error is often not the cause \u2014 inspect the upstream dependencies first.";
|
|
71454
71223
|
return {
|
|
71455
|
-
|
|
71456
|
-
|
|
71457
|
-
|
|
71458
|
-
|
|
71459
|
-
|
|
71460
|
-
|
|
71461
|
-
|
|
71224
|
+
seed: top,
|
|
71225
|
+
upstream,
|
|
71226
|
+
downstream,
|
|
71227
|
+
asyncUpstream,
|
|
71228
|
+
asyncDownstream,
|
|
71229
|
+
blastRadius,
|
|
71230
|
+
criticality,
|
|
71462
71231
|
summary,
|
|
71463
71232
|
note
|
|
71464
71233
|
};
|
|
71465
71234
|
}
|
|
71466
71235
|
|
|
71467
|
-
// ../../packages/engine/src/render-
|
|
71236
|
+
// ../../packages/engine/src/render-blast-radius.ts
|
|
71468
71237
|
init_cjs_shims();
|
|
71469
|
-
|
|
71470
|
-
function renderChangeTimeline(t) {
|
|
71238
|
+
function renderBlastRadius(r) {
|
|
71471
71239
|
const lines = [];
|
|
71472
|
-
lines.push("#
|
|
71240
|
+
lines.push("# Blast radius: " + r.seed.name);
|
|
71473
71241
|
lines.push("");
|
|
71474
|
-
lines.push(
|
|
71475
|
-
const sinceLabel = t.window.since ?? "(all history)";
|
|
71476
|
-
const untilLabel = t.window.until ?? "HEAD";
|
|
71477
|
-
lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
|
|
71478
|
-
lines.push(t.summary);
|
|
71242
|
+
lines.push(r.summary);
|
|
71479
71243
|
lines.push("");
|
|
71480
|
-
lines.push("> " +
|
|
71244
|
+
lines.push("> " + r.note);
|
|
71481
71245
|
lines.push("");
|
|
71482
|
-
lines.push("##
|
|
71483
|
-
if (
|
|
71484
|
-
lines.push(" (none
|
|
71246
|
+
lines.push("## Upstream (where failure could originate \u2014 what the seed depends on)");
|
|
71247
|
+
if (r.upstream.length === 0 && r.asyncUpstream.length === 0) {
|
|
71248
|
+
lines.push(" (none)");
|
|
71485
71249
|
} else {
|
|
71486
|
-
const
|
|
71487
|
-
for (const
|
|
71250
|
+
const upstreamSlice = r.upstream.slice(0, 15);
|
|
71251
|
+
for (const sym of upstreamSlice) {
|
|
71252
|
+
lines.push(" " + symbolDisplayName(sym) + " (" + (sym.filePath ?? "unknown") + ")");
|
|
71253
|
+
}
|
|
71254
|
+
for (const dep of r.asyncUpstream) {
|
|
71488
71255
|
lines.push(
|
|
71489
|
-
" " +
|
|
71256
|
+
" async: " + dep.counterpart + " (" + (dep.counterpartFile ?? "unknown") + ") [via queue: " + dep.queueName + "]"
|
|
71490
71257
|
);
|
|
71491
71258
|
}
|
|
71492
|
-
|
|
71493
|
-
|
|
71494
|
-
|
|
71259
|
+
}
|
|
71260
|
+
lines.push("");
|
|
71261
|
+
lines.push("## Downstream (affected if the seed fails)");
|
|
71262
|
+
if (r.downstream.length === 0 && r.asyncDownstream.length === 0) {
|
|
71263
|
+
lines.push(" (none)");
|
|
71264
|
+
} else {
|
|
71265
|
+
for (const layer of r.downstream) {
|
|
71266
|
+
const syms = layer.symbols.slice(0, 12);
|
|
71267
|
+
const names = syms.map((s) => symbolDisplayName(s));
|
|
71268
|
+
const extra = layer.symbols.length > 12 ? " +" + (layer.symbols.length - 12) + " more" : "";
|
|
71269
|
+
lines.push(" depth " + layer.depth + ": " + names.join(", ") + extra);
|
|
71270
|
+
}
|
|
71271
|
+
for (const dep of r.asyncDownstream) {
|
|
71272
|
+
lines.push(
|
|
71273
|
+
" async worker: " + dep.counterpart + " (" + (dep.counterpartFile ?? "unknown") + ") [via queue: " + dep.queueName + "]"
|
|
71274
|
+
);
|
|
71495
71275
|
}
|
|
71496
71276
|
}
|
|
71497
|
-
|
|
71498
|
-
|
|
71499
|
-
|
|
71500
|
-
lines.push("
|
|
71501
|
-
|
|
71502
|
-
|
|
71503
|
-
|
|
71504
|
-
|
|
71505
|
-
|
|
71277
|
+
lines.push("");
|
|
71278
|
+
lines.push("## Async boundaries");
|
|
71279
|
+
if (r.asyncUpstream.length === 0 && r.asyncDownstream.length === 0) {
|
|
71280
|
+
lines.push(" (none)");
|
|
71281
|
+
} else {
|
|
71282
|
+
for (const dep of r.asyncUpstream) {
|
|
71283
|
+
lines.push(
|
|
71284
|
+
" " + dep.queueName + ": upstream producer -> " + dep.counterpart + (dep.counterpartFile ? " (" + dep.counterpartFile + ")" : "")
|
|
71285
|
+
);
|
|
71286
|
+
}
|
|
71287
|
+
for (const dep of r.asyncDownstream) {
|
|
71288
|
+
lines.push(
|
|
71289
|
+
" " + dep.queueName + ": downstream worker -> " + dep.counterpart + (dep.counterpartFile ? " (" + dep.counterpartFile + ")" : "")
|
|
71290
|
+
);
|
|
71506
71291
|
}
|
|
71507
71292
|
}
|
|
71293
|
+
lines.push("");
|
|
71294
|
+
lines.push("## Blast radius");
|
|
71295
|
+
lines.push(" " + r.blastRadius + " symbol(s) affected \u2014 criticality: " + r.criticality);
|
|
71508
71296
|
return lines.join("\n");
|
|
71509
71297
|
}
|
|
71510
|
-
function
|
|
71511
|
-
return JSON.stringify(
|
|
71298
|
+
function blastRadiusToJSON(r) {
|
|
71299
|
+
return JSON.stringify(r, null, 2);
|
|
71512
71300
|
}
|
|
71513
71301
|
|
|
71514
|
-
// ../../packages/engine/src/
|
|
71302
|
+
// ../../packages/engine/src/multi-repo.ts
|
|
71515
71303
|
init_cjs_shims();
|
|
71516
|
-
async function
|
|
71517
|
-
|
|
71518
|
-
|
|
71519
|
-
|
|
71520
|
-
|
|
71521
|
-
|
|
71522
|
-
|
|
71523
|
-
|
|
71524
|
-
|
|
71525
|
-
|
|
71526
|
-
|
|
71527
|
-
|
|
71528
|
-
|
|
71529
|
-
|
|
71530
|
-
|
|
71304
|
+
async function reposHealth(providers) {
|
|
71305
|
+
return Promise.all(
|
|
71306
|
+
providers.map(async (provider) => {
|
|
71307
|
+
try {
|
|
71308
|
+
const h = await provider.code.health();
|
|
71309
|
+
return {
|
|
71310
|
+
repo: provider.name,
|
|
71311
|
+
path: provider.path,
|
|
71312
|
+
hostUrl: provider.hostUrl,
|
|
71313
|
+
reachable: h.ok,
|
|
71314
|
+
detail: h.detail
|
|
71315
|
+
};
|
|
71316
|
+
} catch (err) {
|
|
71317
|
+
return {
|
|
71318
|
+
repo: provider.name,
|
|
71319
|
+
path: provider.path,
|
|
71320
|
+
hostUrl: provider.hostUrl,
|
|
71321
|
+
reachable: false,
|
|
71322
|
+
detail: err.message
|
|
71323
|
+
};
|
|
71531
71324
|
}
|
|
71532
|
-
}
|
|
71533
|
-
|
|
71534
|
-
|
|
71535
|
-
|
|
71536
|
-
|
|
71537
|
-
|
|
71538
|
-
|
|
71539
|
-
|
|
71540
|
-
|
|
71541
|
-
|
|
71542
|
-
|
|
71543
|
-
|
|
71544
|
-
|
|
71545
|
-
|
|
71546
|
-
|
|
71547
|
-
|
|
71548
|
-
|
|
71549
|
-
|
|
71550
|
-
|
|
71551
|
-
|
|
71552
|
-
|
|
71553
|
-
|
|
71554
|
-
|
|
71555
|
-
|
|
71556
|
-
|
|
71557
|
-
|
|
71325
|
+
})
|
|
71326
|
+
);
|
|
71327
|
+
}
|
|
71328
|
+
async function searchAcrossRepos(query, providers, limit = 8) {
|
|
71329
|
+
return Promise.all(
|
|
71330
|
+
providers.map(async (provider) => {
|
|
71331
|
+
try {
|
|
71332
|
+
const h = await provider.code.health();
|
|
71333
|
+
const symbols = h.ok ? await provider.code.searchSymbols(query, limit) : [];
|
|
71334
|
+
return {
|
|
71335
|
+
repo: provider.name,
|
|
71336
|
+
hostUrl: provider.hostUrl,
|
|
71337
|
+
reachable: h.ok,
|
|
71338
|
+
symbols
|
|
71339
|
+
};
|
|
71340
|
+
} catch (err) {
|
|
71341
|
+
void err;
|
|
71342
|
+
return {
|
|
71343
|
+
repo: provider.name,
|
|
71344
|
+
hostUrl: provider.hostUrl,
|
|
71345
|
+
reachable: false,
|
|
71346
|
+
symbols: []
|
|
71347
|
+
};
|
|
71348
|
+
}
|
|
71349
|
+
})
|
|
71350
|
+
);
|
|
71558
71351
|
}
|
|
71559
71352
|
|
|
71560
|
-
// ../../packages/engine/src/render-
|
|
71353
|
+
// ../../packages/engine/src/render-ownership.ts
|
|
71561
71354
|
init_cjs_shims();
|
|
71562
|
-
function
|
|
71355
|
+
function renderOwnership(o) {
|
|
71563
71356
|
const lines = [];
|
|
71564
|
-
lines.push("#
|
|
71565
|
-
lines.push("");
|
|
71566
|
-
const sinceLabel = r.window.since ?? "(all history)";
|
|
71567
|
-
const untilLabel = r.window.until ?? "HEAD";
|
|
71568
|
-
lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
|
|
71569
|
-
lines.push(r.summary);
|
|
71357
|
+
lines.push("# Ownership: " + o.query);
|
|
71570
71358
|
lines.push("");
|
|
71571
|
-
|
|
71572
|
-
|
|
71573
|
-
|
|
71574
|
-
|
|
71575
|
-
|
|
71576
|
-
|
|
71359
|
+
if (o.file === null) {
|
|
71360
|
+
lines.push(o.note);
|
|
71361
|
+
if (o.candidates !== void 0 && o.candidates.length > 0) {
|
|
71362
|
+
lines.push("");
|
|
71363
|
+
lines.push("Candidates:");
|
|
71364
|
+
for (const c of o.candidates) {
|
|
71365
|
+
lines.push(" " + c);
|
|
71366
|
+
}
|
|
71577
71367
|
}
|
|
71368
|
+
return lines.join("\n");
|
|
71578
71369
|
}
|
|
71579
|
-
if (
|
|
71580
|
-
lines.push("");
|
|
71581
|
-
lines.push(
|
|
71582
|
-
"## Top contributors: " + r.contributors.map((c) => c.author + " \xD7" + c.commits).join(", ")
|
|
71583
|
-
);
|
|
71370
|
+
if (o.symbol !== null) {
|
|
71371
|
+
lines.push("Symbol : " + o.symbol.name);
|
|
71584
71372
|
}
|
|
71373
|
+
lines.push("File : " + o.file);
|
|
71585
71374
|
lines.push("");
|
|
71586
|
-
|
|
71375
|
+
const pct = (o.confidence * 100).toFixed(0);
|
|
71376
|
+
lines.push(
|
|
71377
|
+
"Likely maintainer : " + (o.likelyMaintainer ?? "(unknown)") + " (" + pct + "% confidence)"
|
|
71378
|
+
);
|
|
71379
|
+
lines.push("Most active recently: " + (o.mostActiveRecent ?? "(unknown)"));
|
|
71380
|
+
lines.push("");
|
|
71381
|
+
lines.push("Contributors:");
|
|
71382
|
+
const capped = o.contributors.slice(0, 8);
|
|
71383
|
+
for (const c of capped) {
|
|
71587
71384
|
lines.push(
|
|
71588
|
-
"
|
|
71385
|
+
" " + c.author + " \xD7 " + c.commits + " commits, last " + c.lastDate
|
|
71589
71386
|
);
|
|
71590
|
-
} else {
|
|
71591
|
-
lines.push("## Queue topology: no queue/worker files changed");
|
|
71592
71387
|
}
|
|
71593
|
-
if (
|
|
71594
|
-
lines.push("");
|
|
71595
|
-
|
|
71596
|
-
|
|
71597
|
-
|
|
71598
|
-
|
|
71599
|
-
|
|
71600
|
-
|
|
71601
|
-
}
|
|
71388
|
+
if (o.contributors.length > 8) {
|
|
71389
|
+
lines.push(" \u2026 and " + (o.contributors.length - 8) + " more");
|
|
71390
|
+
}
|
|
71391
|
+
lines.push("");
|
|
71392
|
+
if (o.evidence.length > 0) {
|
|
71393
|
+
lines.push("Evidence:");
|
|
71394
|
+
for (const e of o.evidence) {
|
|
71395
|
+
lines.push(" \u2022 " + e);
|
|
71602
71396
|
}
|
|
71397
|
+
lines.push("");
|
|
71603
71398
|
}
|
|
71399
|
+
lines.push("Note: " + o.note);
|
|
71604
71400
|
return lines.join("\n");
|
|
71605
71401
|
}
|
|
71606
|
-
function
|
|
71607
|
-
return JSON.stringify(
|
|
71402
|
+
function ownershipToJSON(o) {
|
|
71403
|
+
return JSON.stringify(o, null, 2);
|
|
71608
71404
|
}
|
|
71609
71405
|
|
|
71610
|
-
// ../../packages/engine/src/
|
|
71406
|
+
// ../../packages/engine/src/postmortem.ts
|
|
71611
71407
|
init_cjs_shims();
|
|
71612
|
-
|
|
71613
|
-
|
|
71614
|
-
"stripe",
|
|
71615
|
-
"prisma",
|
|
71616
|
-
"redis",
|
|
71617
|
-
"bullmq",
|
|
71618
|
-
"twilio",
|
|
71619
|
-
"sendgrid",
|
|
71620
|
-
"clerk",
|
|
71621
|
-
"pusher",
|
|
71622
|
-
"axios",
|
|
71623
|
-
"elasticsearch",
|
|
71624
|
-
"prometheus",
|
|
71625
|
-
"mongo",
|
|
71626
|
-
"graphql"
|
|
71627
|
-
];
|
|
71628
|
-
async function discoverArchitecture(deps) {
|
|
71629
|
-
const nodeStats = await (async () => {
|
|
71630
|
-
try {
|
|
71631
|
-
const result = await deps.code.cypher(
|
|
71632
|
-
"MATCH (n) RETURN label(n), count(n) ORDER BY count(n) DESC"
|
|
71633
|
-
);
|
|
71634
|
-
return result.rows.map((row) => ({
|
|
71635
|
-
label: String(row[0] ?? ""),
|
|
71636
|
-
count: Number(row[1] ?? 0)
|
|
71637
|
-
})).filter((s) => s.label !== "Embedding");
|
|
71638
|
-
} catch {
|
|
71639
|
-
return [];
|
|
71640
|
-
}
|
|
71641
|
-
})();
|
|
71642
|
-
const subsystems = await (async () => {
|
|
71643
|
-
try {
|
|
71644
|
-
const result = await deps.code.cypher(
|
|
71645
|
-
'MATCH (m)-[r:CodeRelation]->(c:Community) WHERE r.rel_type = "member_of" RETURN c.name, count(m) ORDER BY count(m) DESC'
|
|
71646
|
-
);
|
|
71647
|
-
return result.rows.slice(0, 12).map((row) => ({
|
|
71648
|
-
name: String(row[0] ?? ""),
|
|
71649
|
-
members: Number(row[1] ?? 0)
|
|
71650
|
-
}));
|
|
71651
|
-
} catch {
|
|
71652
|
-
return [];
|
|
71653
|
-
}
|
|
71654
|
-
})();
|
|
71655
|
-
const asyncBoundaries = await (async () => {
|
|
71656
|
-
try {
|
|
71657
|
-
const edges = await listQueueEdges(deps.db, { project: deps.project });
|
|
71658
|
-
const byQueue = /* @__PURE__ */ new Map();
|
|
71659
|
-
for (const edge of edges) {
|
|
71660
|
-
const key = edge.queueName;
|
|
71661
|
-
if (!byQueue.has(key)) {
|
|
71662
|
-
byQueue.set(key, { producers: /* @__PURE__ */ new Set(), workers: /* @__PURE__ */ new Set() });
|
|
71663
|
-
}
|
|
71664
|
-
const entry2 = byQueue.get(key);
|
|
71665
|
-
if (edge.producerSymbol != null && edge.producerSymbol !== "") {
|
|
71666
|
-
entry2.producers.add(edge.producerSymbol);
|
|
71667
|
-
}
|
|
71668
|
-
if (edge.workerSymbol != null && edge.workerSymbol !== "") {
|
|
71669
|
-
entry2.workers.add(edge.workerSymbol);
|
|
71670
|
-
}
|
|
71671
|
-
}
|
|
71672
|
-
return Array.from(byQueue.entries()).map(([queueName, { producers, workers }]) => ({
|
|
71673
|
-
queueName,
|
|
71674
|
-
producers: Array.from(producers),
|
|
71675
|
-
workers: Array.from(workers)
|
|
71676
|
-
}));
|
|
71677
|
-
} catch {
|
|
71678
|
-
return [];
|
|
71679
|
-
}
|
|
71680
|
-
})();
|
|
71681
|
-
const keyFlows = await (async () => {
|
|
71682
|
-
try {
|
|
71683
|
-
const result = await deps.code.cypher(
|
|
71684
|
-
"MATCH (p:Process) RETURN p.name ORDER BY p.name"
|
|
71685
|
-
);
|
|
71686
|
-
return result.rows.slice(0, 20).map((row) => String(row[0] ?? ""));
|
|
71687
|
-
} catch {
|
|
71688
|
-
return [];
|
|
71689
|
-
}
|
|
71690
|
-
})();
|
|
71691
|
-
const deadCode = await (async () => {
|
|
71692
|
-
try {
|
|
71693
|
-
const result = await deps.code.cypher(
|
|
71694
|
-
"MATCH (n) WHERE n.is_dead = true RETURN count(n)"
|
|
71695
|
-
);
|
|
71696
|
-
return Number(result.rows[0]?.[0] ?? 0);
|
|
71697
|
-
} catch {
|
|
71698
|
-
return 0;
|
|
71699
|
-
}
|
|
71700
|
-
})();
|
|
71701
|
-
const highCouplingPairs = await (async () => {
|
|
71702
|
-
try {
|
|
71703
|
-
const result = await deps.code.cypher(
|
|
71704
|
-
'MATCH ()-[r:CodeRelation]->() WHERE r.rel_type = "coupled_with" AND r.co_changes >= 3 RETURN count(r)'
|
|
71705
|
-
);
|
|
71706
|
-
return Number(result.rows[0]?.[0] ?? 0);
|
|
71707
|
-
} catch {
|
|
71708
|
-
return 0;
|
|
71709
|
-
}
|
|
71710
|
-
})();
|
|
71711
|
-
const externalSystems = await (async () => {
|
|
71712
|
-
try {
|
|
71713
|
-
const results = await Promise.all(
|
|
71714
|
-
EXTERNAL_MARKERS.map(async (marker) => {
|
|
71715
|
-
try {
|
|
71716
|
-
const result = await deps.code.cypher(
|
|
71717
|
-
`MATCH (n:File) WHERE n.content CONTAINS "${marker}" RETURN count(n)`
|
|
71718
|
-
);
|
|
71719
|
-
const count = Number(result.rows[0]?.[0] ?? 0);
|
|
71720
|
-
return { name: marker, files: count };
|
|
71721
|
-
} catch {
|
|
71722
|
-
return { name: marker, files: 0 };
|
|
71723
|
-
}
|
|
71724
|
-
})
|
|
71725
|
-
);
|
|
71726
|
-
return results.filter((r) => r.files > 0).sort((a, b2) => b2.files - a.files);
|
|
71727
|
-
} catch {
|
|
71728
|
-
return [];
|
|
71729
|
-
}
|
|
71730
|
-
})();
|
|
71731
|
-
const largestSubsystem = subsystems[0];
|
|
71732
|
-
const largestDesc = largestSubsystem != null ? `${largestSubsystem.name} with ${largestSubsystem.members} symbols` : "none";
|
|
71733
|
-
const summary = `${subsystems.length} subsystems (largest: ${largestDesc}), ${asyncBoundaries.length} async queue boundaries, ${externalSystems.length} external systems, ${deadCode} unreferenced symbols.`;
|
|
71734
|
-
return {
|
|
71735
|
-
nodeStats,
|
|
71736
|
-
subsystems,
|
|
71737
|
-
asyncBoundaries,
|
|
71738
|
-
keyFlows,
|
|
71739
|
-
externalSystems,
|
|
71740
|
-
fragile: { deadCode, highCouplingPairs },
|
|
71741
|
-
summary
|
|
71742
|
-
};
|
|
71408
|
+
function shortId2(id) {
|
|
71409
|
+
return id.slice(0, 8);
|
|
71743
71410
|
}
|
|
71744
|
-
|
|
71745
|
-
// ../../packages/engine/src/render-architecture.ts
|
|
71746
|
-
init_cjs_shims();
|
|
71747
|
-
function renderArchitecture(m) {
|
|
71411
|
+
function generatePostmortem(r) {
|
|
71748
71412
|
const lines = [];
|
|
71749
|
-
lines.push(
|
|
71413
|
+
lines.push(`# Incident Postmortem \u2014 ${r.input.hint}`);
|
|
71750
71414
|
lines.push("");
|
|
71751
|
-
lines.push(
|
|
71415
|
+
lines.push(
|
|
71416
|
+
"_Draft generated by Horus \u2014 deterministic, no AI. Edit freely; Horus drafts, it does not dictate._"
|
|
71417
|
+
);
|
|
71752
71418
|
lines.push("");
|
|
71753
|
-
lines.push(
|
|
71754
|
-
if (
|
|
71755
|
-
|
|
71756
|
-
|
|
71757
|
-
|
|
71758
|
-
|
|
71419
|
+
lines.push(`**Confidence:** ${r.confidence.toFixed(2)}`);
|
|
71420
|
+
if (r.input.service) lines.push(`**Service:** ${r.input.service}`);
|
|
71421
|
+
if (r.input.since) lines.push(`**Since:** ${r.input.since}`);
|
|
71422
|
+
lines.push("");
|
|
71423
|
+
lines.push("## Summary");
|
|
71424
|
+
lines.push("");
|
|
71425
|
+
lines.push(r.summary);
|
|
71426
|
+
lines.push("");
|
|
71427
|
+
lines.push("## Impact");
|
|
71428
|
+
lines.push("");
|
|
71429
|
+
const impactEvidence = r.evidence.filter((e) => e.kind === "impact");
|
|
71430
|
+
const hasBoundaryCrossings = r.timeline.boundaryCrossings.length > 0;
|
|
71431
|
+
if (impactEvidence.length > 0) {
|
|
71432
|
+
lines.push("The following impact signals were observed:");
|
|
71433
|
+
lines.push("");
|
|
71434
|
+
for (const e of impactEvidence) {
|
|
71435
|
+
lines.push(`- \`${shortId2(e.id)}\` ${e.title}`);
|
|
71759
71436
|
}
|
|
71437
|
+
lines.push("");
|
|
71760
71438
|
}
|
|
71761
|
-
|
|
71762
|
-
|
|
71763
|
-
|
|
71764
|
-
|
|
71765
|
-
|
|
71766
|
-
|
|
71767
|
-
|
|
71768
|
-
const workers = b2.workers.length > 0 ? b2.workers.join(", ") : "(unknown)";
|
|
71769
|
-
lines.push(`- ${b2.queueName}: ${producers} -> ${workers}`);
|
|
71439
|
+
if (hasBoundaryCrossings) {
|
|
71440
|
+
lines.push("Async boundaries crossed during the incident:");
|
|
71441
|
+
lines.push("");
|
|
71442
|
+
for (const bc of r.timeline.boundaryCrossings) {
|
|
71443
|
+
const producer = bc.producer ?? "?";
|
|
71444
|
+
const worker = bc.worker ?? "?";
|
|
71445
|
+
lines.push(`- **${bc.queueName}**: ${producer} \u2192 ${worker}`);
|
|
71770
71446
|
}
|
|
71447
|
+
lines.push("");
|
|
71771
71448
|
}
|
|
71772
|
-
|
|
71773
|
-
|
|
71774
|
-
|
|
71775
|
-
|
|
71449
|
+
if (impactEvidence.length === 0 && !hasBoundaryCrossings) {
|
|
71450
|
+
lines.push("_No impact evidence was captured during this investigation._");
|
|
71451
|
+
lines.push("");
|
|
71452
|
+
}
|
|
71453
|
+
const metricsGap = r.gapAnalysis.gaps.find((g) => g.dimension === "metrics");
|
|
71454
|
+
if (metricsGap) {
|
|
71455
|
+
lines.push(
|
|
71456
|
+
`> **Note:** Precise user/runtime impact (error rates, latency percentiles, affected user count) requires metrics that were unavailable during this investigation. ${metricsGap.why} Next: ${metricsGap.nextSource}.`
|
|
71457
|
+
);
|
|
71776
71458
|
} else {
|
|
71777
|
-
|
|
71778
|
-
|
|
71779
|
-
|
|
71459
|
+
lines.push(
|
|
71460
|
+
"> **Note:** Confirm precise user/runtime impact (error rates, affected user count) with your metrics provider."
|
|
71461
|
+
);
|
|
71780
71462
|
}
|
|
71781
71463
|
lines.push("");
|
|
71782
|
-
lines.push("##
|
|
71783
|
-
|
|
71784
|
-
|
|
71464
|
+
lines.push("## Timeline");
|
|
71465
|
+
lines.push("");
|
|
71466
|
+
if (r.timeline.events.length === 0) {
|
|
71467
|
+
lines.push("_no timeline_");
|
|
71785
71468
|
} else {
|
|
71786
|
-
for (const
|
|
71787
|
-
|
|
71469
|
+
for (const ev of r.timeline.events) {
|
|
71470
|
+
const at = ev.at ?? "structural";
|
|
71471
|
+
lines.push(`${ev.order}. [${at}] ${ev.title}`);
|
|
71788
71472
|
}
|
|
71789
71473
|
}
|
|
71790
71474
|
lines.push("");
|
|
71791
|
-
lines.push("##
|
|
71792
|
-
|
|
71793
|
-
|
|
71475
|
+
lines.push("## Contributing factors");
|
|
71476
|
+
lines.push("");
|
|
71477
|
+
const supportedHypotheses = r.hypotheses.filter((h) => h.verdict === "supported");
|
|
71478
|
+
const commitEvidence = r.evidence.filter((e) => e.kind === "commit");
|
|
71479
|
+
if (supportedHypotheses.length === 0 && commitEvidence.length === 0) {
|
|
71480
|
+
const topCause = r.suspectedCauses[0];
|
|
71481
|
+
if (topCause) {
|
|
71482
|
+
lines.push(
|
|
71483
|
+
`_No hypotheses reached 'supported' status. The leading suspected cause is unconfirmed:_`
|
|
71484
|
+
);
|
|
71485
|
+
lines.push("");
|
|
71486
|
+
lines.push(`1. **(score ${topCause.finalScore.toFixed(2)}, ${topCause.band})** ${topCause.title}`);
|
|
71487
|
+
} else {
|
|
71488
|
+
lines.push(
|
|
71489
|
+
"_No confirmed contributing factors were identified. Further evidence is needed._"
|
|
71490
|
+
);
|
|
71491
|
+
}
|
|
71794
71492
|
} else {
|
|
71795
|
-
|
|
71796
|
-
lines.push(
|
|
71493
|
+
if (r.causeChains !== void 0 && r.causeChains.length > 0) {
|
|
71494
|
+
lines.push("The following causal sequences are supported by the collected evidence:");
|
|
71495
|
+
lines.push("");
|
|
71496
|
+
for (const chain of r.causeChains) {
|
|
71497
|
+
lines.push(`### ${chain.category} _(confidence ${chain.confidence.toFixed(2)})_`);
|
|
71498
|
+
lines.push("");
|
|
71499
|
+
lines.push(chain.summary);
|
|
71500
|
+
lines.push("");
|
|
71501
|
+
for (let i = 0; i < chain.steps.length; i++) {
|
|
71502
|
+
const step = chain.steps[i];
|
|
71503
|
+
const num = i + 1;
|
|
71504
|
+
const evCite = step.evidenceIds.length > 0 ? ` \u2014 evidence: ${step.evidenceIds.map((id) => `\`${shortId2(id)}\``).join(", ")}` : "";
|
|
71505
|
+
lines.push(`${num}. **[${step.role}]** ${step.label}${evCite}`);
|
|
71506
|
+
}
|
|
71507
|
+
lines.push("");
|
|
71508
|
+
}
|
|
71509
|
+
} else {
|
|
71510
|
+
if (supportedHypotheses.length > 0) {
|
|
71511
|
+
lines.push("The following factors are supported by the collected evidence:");
|
|
71512
|
+
lines.push("");
|
|
71513
|
+
for (const h of supportedHypotheses) {
|
|
71514
|
+
lines.push(
|
|
71515
|
+
`- **${h.category}:** ${h.statement} _(confidence ${h.confidence.toFixed(2)})_`
|
|
71516
|
+
);
|
|
71517
|
+
}
|
|
71518
|
+
}
|
|
71519
|
+
}
|
|
71520
|
+
if (commitEvidence.length > 0) {
|
|
71521
|
+
if (supportedHypotheses.length > 0 || (r.causeChains?.length ?? 0) > 0) lines.push("");
|
|
71522
|
+
lines.push("Recent changes (commit evidence) present during this incident:");
|
|
71523
|
+
lines.push("");
|
|
71524
|
+
for (const e of commitEvidence) {
|
|
71525
|
+
lines.push(`- \`${shortId2(e.id)}\` ${e.title}`);
|
|
71526
|
+
}
|
|
71797
71527
|
}
|
|
71798
71528
|
}
|
|
71799
71529
|
lines.push("");
|
|
71800
|
-
lines.push("##
|
|
71801
|
-
lines.push(
|
|
71802
|
-
|
|
71803
|
-
|
|
71804
|
-
|
|
71805
|
-
|
|
71806
|
-
|
|
71807
|
-
|
|
71808
|
-
|
|
71809
|
-
|
|
71810
|
-
|
|
71811
|
-
|
|
71812
|
-
|
|
71813
|
-
|
|
71814
|
-
|
|
71815
|
-
|
|
71816
|
-
|
|
71817
|
-
|
|
71818
|
-
|
|
71819
|
-
|
|
71820
|
-
|
|
71821
|
-
|
|
71822
|
-
|
|
71823
|
-
|
|
71824
|
-
if (edge.producerFile === top.filePath || edge.producerSymbol === top.name) {
|
|
71825
|
-
const key = edge.queueName + "|" + (edge.workerSymbol ?? "unknown-worker");
|
|
71826
|
-
if (!asyncDownstreamMap.has(key)) {
|
|
71827
|
-
asyncDownstreamMap.set(key, {
|
|
71828
|
-
queueName: edge.queueName,
|
|
71829
|
-
counterpart: edge.workerSymbol ?? "unknown-worker",
|
|
71830
|
-
counterpartFile: edge.workerFile
|
|
71831
|
-
});
|
|
71832
|
-
}
|
|
71833
|
-
}
|
|
71530
|
+
lines.push("## Root cause (candidate)");
|
|
71531
|
+
lines.push("");
|
|
71532
|
+
const topHypothesis = supportedHypotheses[0];
|
|
71533
|
+
const topSuspectedCause = r.suspectedCauses[0];
|
|
71534
|
+
const gapDimensions = r.gapAnalysis.gaps.slice(0, 2).map((g) => g.dimension).join(", ");
|
|
71535
|
+
const gapCaveat = gapDimensions.length > 0 ? `This candidate cannot be confirmed while evidence gaps remain (missing: ${gapDimensions}).` : "Review the gap analysis and gather additional evidence before confirming.";
|
|
71536
|
+
if (topHypothesis) {
|
|
71537
|
+
lines.push(
|
|
71538
|
+
`**CANDIDATE** _(confidence ${topHypothesis.confidence.toFixed(2)})_: ${topHypothesis.statement}`
|
|
71539
|
+
);
|
|
71540
|
+
lines.push("");
|
|
71541
|
+
lines.push(`_${gapCaveat}_`);
|
|
71542
|
+
} else if (topSuspectedCause) {
|
|
71543
|
+
lines.push(
|
|
71544
|
+
`**CANDIDATE** _(score ${topSuspectedCause.finalScore.toFixed(2)}, ${topSuspectedCause.band}, unconfirmed)_: ${topSuspectedCause.title}`
|
|
71545
|
+
);
|
|
71546
|
+
lines.push("");
|
|
71547
|
+
lines.push(`_${gapCaveat}_`);
|
|
71548
|
+
} else {
|
|
71549
|
+
lines.push(
|
|
71550
|
+
"_No candidate root cause could be determined from the available evidence._"
|
|
71551
|
+
);
|
|
71552
|
+
lines.push("");
|
|
71553
|
+
lines.push(`_${gapCaveat}_`);
|
|
71834
71554
|
}
|
|
71835
|
-
|
|
71836
|
-
|
|
71837
|
-
|
|
71838
|
-
|
|
71839
|
-
|
|
71840
|
-
|
|
71841
|
-
|
|
71842
|
-
|
|
71843
|
-
counterpart: edge.producerSymbol ?? "unknown-producer",
|
|
71844
|
-
counterpartFile: edge.producerFile
|
|
71845
|
-
});
|
|
71846
|
-
}
|
|
71555
|
+
lines.push("");
|
|
71556
|
+
lines.push("## Evidence");
|
|
71557
|
+
lines.push("");
|
|
71558
|
+
if (r.evidence.length === 0) {
|
|
71559
|
+
lines.push("_No evidence was collected._");
|
|
71560
|
+
} else {
|
|
71561
|
+
for (const e of r.evidence) {
|
|
71562
|
+
lines.push(`- ${shortId2(e.id)} [${e.source}/${e.kind}] ${e.title}`);
|
|
71847
71563
|
}
|
|
71848
71564
|
}
|
|
71849
|
-
const asyncUpstream = Array.from(asyncUpstreamMap.values());
|
|
71850
|
-
const blastRadius = impact.affected + asyncDownstream.length;
|
|
71851
|
-
const criticality = blastRadius >= 10 ? "high" : blastRadius >= 3 ? "medium" : "low";
|
|
71852
|
-
const summary = "If " + top.name + " (" + top.filePath + ") fails, ~" + blastRadius + " symbol(s) are affected downstream" + (asyncDownstream.length ? " (incl. " + asyncDownstream.length + " across async queue boundaries)" : "") + "; it depends on " + upstream.length + " symbol(s) upstream" + (asyncUpstream.length ? " + " + asyncUpstream.length + " async producer(s)" : "") + ". Criticality: " + criticality + ".";
|
|
71853
|
-
const note = "The component reporting an error is often not the cause \u2014 inspect the upstream dependencies first.";
|
|
71854
|
-
return {
|
|
71855
|
-
seed: top,
|
|
71856
|
-
upstream,
|
|
71857
|
-
downstream,
|
|
71858
|
-
asyncUpstream,
|
|
71859
|
-
asyncDownstream,
|
|
71860
|
-
blastRadius,
|
|
71861
|
-
criticality,
|
|
71862
|
-
summary,
|
|
71863
|
-
note
|
|
71864
|
-
};
|
|
71865
|
-
}
|
|
71866
|
-
|
|
71867
|
-
// ../../packages/engine/src/render-blast-radius.ts
|
|
71868
|
-
init_cjs_shims();
|
|
71869
|
-
function renderBlastRadius(r) {
|
|
71870
|
-
const lines = [];
|
|
71871
|
-
lines.push("# Blast radius: " + r.seed.name);
|
|
71872
71565
|
lines.push("");
|
|
71873
|
-
lines.push(
|
|
71874
|
-
lines.push("");
|
|
71875
|
-
lines.push("> " + r.note);
|
|
71566
|
+
lines.push("## Lessons learned");
|
|
71876
71567
|
lines.push("");
|
|
71877
|
-
|
|
71878
|
-
|
|
71879
|
-
|
|
71568
|
+
if (r.gapAnalysis.gaps.length === 0) {
|
|
71569
|
+
lines.push(
|
|
71570
|
+
"Good observability coverage for this investigation. Consider reviewing your alerting and on-call runbooks to ensure similar incidents are detected and scoped faster in future."
|
|
71571
|
+
);
|
|
71880
71572
|
} else {
|
|
71881
|
-
const
|
|
71882
|
-
for (const sym of upstreamSlice) {
|
|
71883
|
-
lines.push(" " + sym.name + " (" + (sym.filePath ?? "unknown") + ")");
|
|
71884
|
-
}
|
|
71885
|
-
for (const dep of r.asyncUpstream) {
|
|
71573
|
+
for (const gap of r.gapAnalysis.gaps) {
|
|
71886
71574
|
lines.push(
|
|
71887
|
-
|
|
71575
|
+
`- During this incident, **${gap.dimension}** was unavailable \u2014 ${gap.why} Wiring **${gap.nextSource}** would let the next investigation confirm or rule this out faster.`
|
|
71888
71576
|
);
|
|
71889
71577
|
}
|
|
71890
71578
|
}
|
|
71891
71579
|
lines.push("");
|
|
71892
|
-
lines.push("##
|
|
71893
|
-
|
|
71894
|
-
|
|
71895
|
-
|
|
71896
|
-
|
|
71897
|
-
|
|
71898
|
-
|
|
71899
|
-
|
|
71900
|
-
lines.push(" depth " + layer.depth + ": " + names.join(", ") + extra);
|
|
71580
|
+
lines.push("## Follow-up actions");
|
|
71581
|
+
lines.push("");
|
|
71582
|
+
const checkboxItems = [];
|
|
71583
|
+
const seen = /* @__PURE__ */ new Set();
|
|
71584
|
+
for (const action of r.nextActions) {
|
|
71585
|
+
if (!seen.has(action)) {
|
|
71586
|
+
seen.add(action);
|
|
71587
|
+
checkboxItems.push(action);
|
|
71901
71588
|
}
|
|
71902
|
-
|
|
71903
|
-
|
|
71904
|
-
|
|
71905
|
-
|
|
71589
|
+
}
|
|
71590
|
+
const seenGapSources = /* @__PURE__ */ new Set();
|
|
71591
|
+
for (const gap of r.gapAnalysis.gaps) {
|
|
71592
|
+
if (seenGapSources.has(gap.nextSource)) continue;
|
|
71593
|
+
seenGapSources.add(gap.nextSource);
|
|
71594
|
+
const cleanGapAction = `${gap.nextSource} to close the '${gap.dimension}' evidence gap`;
|
|
71595
|
+
if (seen.has(gap.nextSource)) {
|
|
71596
|
+
const idx = checkboxItems.indexOf(gap.nextSource);
|
|
71597
|
+
if (idx !== -1 && !seen.has(cleanGapAction)) {
|
|
71598
|
+
checkboxItems[idx] = cleanGapAction;
|
|
71599
|
+
seen.delete(gap.nextSource);
|
|
71600
|
+
seen.add(cleanGapAction);
|
|
71601
|
+
}
|
|
71602
|
+
} else if (!seen.has(cleanGapAction)) {
|
|
71603
|
+
seen.add(cleanGapAction);
|
|
71604
|
+
checkboxItems.push(cleanGapAction);
|
|
71906
71605
|
}
|
|
71907
71606
|
}
|
|
71908
|
-
|
|
71909
|
-
|
|
71910
|
-
if (r.asyncUpstream.length === 0 && r.asyncDownstream.length === 0) {
|
|
71911
|
-
lines.push(" (none)");
|
|
71607
|
+
if (checkboxItems.length === 0) {
|
|
71608
|
+
lines.push("- [ ] Review this postmortem with the team and decide on follow-ups.");
|
|
71912
71609
|
} else {
|
|
71913
|
-
for (const
|
|
71914
|
-
lines.push(
|
|
71915
|
-
" " + dep.queueName + ": upstream producer -> " + dep.counterpart + (dep.counterpartFile ? " (" + dep.counterpartFile + ")" : "")
|
|
71916
|
-
);
|
|
71917
|
-
}
|
|
71918
|
-
for (const dep of r.asyncDownstream) {
|
|
71919
|
-
lines.push(
|
|
71920
|
-
" " + dep.queueName + ": downstream worker -> " + dep.counterpart + (dep.counterpartFile ? " (" + dep.counterpartFile + ")" : "")
|
|
71921
|
-
);
|
|
71610
|
+
for (const item of checkboxItems) {
|
|
71611
|
+
lines.push(`- [ ] ${item}`);
|
|
71922
71612
|
}
|
|
71923
71613
|
}
|
|
71924
71614
|
lines.push("");
|
|
71925
|
-
lines.push("## Blast radius");
|
|
71926
|
-
lines.push(" " + r.blastRadius + " symbol(s) affected \u2014 criticality: " + r.criticality);
|
|
71927
71615
|
return lines.join("\n");
|
|
71928
71616
|
}
|
|
71929
|
-
function blastRadiusToJSON(r) {
|
|
71930
|
-
return JSON.stringify(r, null, 2);
|
|
71931
|
-
}
|
|
71932
71617
|
|
|
71933
|
-
// ../../packages/engine/src/
|
|
71618
|
+
// ../../packages/engine/src/score.ts
|
|
71934
71619
|
init_cjs_shims();
|
|
71935
|
-
|
|
71936
|
-
return
|
|
71937
|
-
|
|
71938
|
-
|
|
71939
|
-
|
|
71940
|
-
|
|
71941
|
-
repo: provider.name,
|
|
71942
|
-
path: provider.path,
|
|
71943
|
-
hostUrl: provider.hostUrl,
|
|
71944
|
-
reachable: h.ok,
|
|
71945
|
-
detail: h.detail
|
|
71946
|
-
};
|
|
71947
|
-
} catch (err) {
|
|
71948
|
-
return {
|
|
71949
|
-
repo: provider.name,
|
|
71950
|
-
path: provider.path,
|
|
71951
|
-
hostUrl: provider.hostUrl,
|
|
71952
|
-
reachable: false,
|
|
71953
|
-
detail: err.message
|
|
71954
|
-
};
|
|
71955
|
-
}
|
|
71956
|
-
})
|
|
71957
|
-
);
|
|
71958
|
-
}
|
|
71959
|
-
async function searchAcrossRepos(query, providers, limit = 8) {
|
|
71960
|
-
return Promise.all(
|
|
71961
|
-
providers.map(async (provider) => {
|
|
71962
|
-
try {
|
|
71963
|
-
const h = await provider.code.health();
|
|
71964
|
-
const symbols = h.ok ? await provider.code.searchSymbols(query, limit) : [];
|
|
71965
|
-
return {
|
|
71966
|
-
repo: provider.name,
|
|
71967
|
-
hostUrl: provider.hostUrl,
|
|
71968
|
-
reachable: h.ok,
|
|
71969
|
-
symbols
|
|
71970
|
-
};
|
|
71971
|
-
} catch (err) {
|
|
71972
|
-
void err;
|
|
71973
|
-
return {
|
|
71974
|
-
repo: provider.name,
|
|
71975
|
-
hostUrl: provider.hostUrl,
|
|
71976
|
-
reachable: false,
|
|
71977
|
-
symbols: []
|
|
71978
|
-
};
|
|
71979
|
-
}
|
|
71980
|
-
})
|
|
71981
|
-
);
|
|
71982
|
-
}
|
|
71983
|
-
|
|
71984
|
-
// ../../packages/engine/src/render-ownership.ts
|
|
71985
|
-
init_cjs_shims();
|
|
71986
|
-
function renderOwnership(o) {
|
|
71987
|
-
const lines = [];
|
|
71988
|
-
lines.push("# Ownership: " + o.query);
|
|
71989
|
-
lines.push("");
|
|
71990
|
-
if (o.file === null) {
|
|
71991
|
-
lines.push(o.note);
|
|
71992
|
-
if (o.candidates !== void 0 && o.candidates.length > 0) {
|
|
71993
|
-
lines.push("");
|
|
71994
|
-
lines.push("Candidates:");
|
|
71995
|
-
for (const c of o.candidates) {
|
|
71996
|
-
lines.push(" " + c);
|
|
71997
|
-
}
|
|
71998
|
-
}
|
|
71999
|
-
return lines.join("\n");
|
|
72000
|
-
}
|
|
72001
|
-
if (o.symbol !== null) {
|
|
72002
|
-
lines.push("Symbol : " + o.symbol.name);
|
|
72003
|
-
}
|
|
72004
|
-
lines.push("File : " + o.file);
|
|
72005
|
-
lines.push("");
|
|
72006
|
-
const pct = (o.confidence * 100).toFixed(0);
|
|
72007
|
-
lines.push(
|
|
72008
|
-
"Likely maintainer : " + (o.likelyMaintainer ?? "(unknown)") + " (" + pct + "% confidence)"
|
|
72009
|
-
);
|
|
72010
|
-
lines.push("Most active recently: " + (o.mostActiveRecent ?? "(unknown)"));
|
|
72011
|
-
lines.push("");
|
|
72012
|
-
lines.push("Contributors:");
|
|
72013
|
-
const capped = o.contributors.slice(0, 8);
|
|
72014
|
-
for (const c of capped) {
|
|
72015
|
-
lines.push(
|
|
72016
|
-
" " + c.author + " \xD7 " + c.commits + " commits, last " + c.lastDate
|
|
72017
|
-
);
|
|
72018
|
-
}
|
|
72019
|
-
if (o.contributors.length > 8) {
|
|
72020
|
-
lines.push(" \u2026 and " + (o.contributors.length - 8) + " more");
|
|
72021
|
-
}
|
|
72022
|
-
lines.push("");
|
|
72023
|
-
if (o.evidence.length > 0) {
|
|
72024
|
-
lines.push("Evidence:");
|
|
72025
|
-
for (const e of o.evidence) {
|
|
72026
|
-
lines.push(" \u2022 " + e);
|
|
72027
|
-
}
|
|
72028
|
-
lines.push("");
|
|
72029
|
-
}
|
|
72030
|
-
lines.push("Note: " + o.note);
|
|
72031
|
-
return lines.join("\n");
|
|
72032
|
-
}
|
|
72033
|
-
function ownershipToJSON(o) {
|
|
72034
|
-
return JSON.stringify(o, null, 2);
|
|
72035
|
-
}
|
|
72036
|
-
|
|
72037
|
-
// ../../packages/engine/src/postmortem.ts
|
|
72038
|
-
init_cjs_shims();
|
|
72039
|
-
function shortId2(id) {
|
|
72040
|
-
return id.slice(0, 8);
|
|
72041
|
-
}
|
|
72042
|
-
function generatePostmortem(r) {
|
|
72043
|
-
const lines = [];
|
|
72044
|
-
lines.push(`# Incident Postmortem \u2014 ${r.input.hint}`);
|
|
72045
|
-
lines.push("");
|
|
72046
|
-
lines.push(
|
|
72047
|
-
"_Draft generated by Horus \u2014 deterministic, no AI. Edit freely; Horus drafts, it does not dictate._"
|
|
72048
|
-
);
|
|
72049
|
-
lines.push("");
|
|
72050
|
-
lines.push(`**Confidence:** ${r.confidence.toFixed(2)}`);
|
|
72051
|
-
if (r.input.service) lines.push(`**Service:** ${r.input.service}`);
|
|
72052
|
-
if (r.input.since) lines.push(`**Since:** ${r.input.since}`);
|
|
72053
|
-
lines.push("");
|
|
72054
|
-
lines.push("## Summary");
|
|
72055
|
-
lines.push("");
|
|
72056
|
-
lines.push(r.summary);
|
|
72057
|
-
lines.push("");
|
|
72058
|
-
lines.push("## Impact");
|
|
72059
|
-
lines.push("");
|
|
72060
|
-
const impactEvidence = r.evidence.filter((e) => e.kind === "impact");
|
|
72061
|
-
const hasBoundaryCrossings = r.timeline.boundaryCrossings.length > 0;
|
|
72062
|
-
if (impactEvidence.length > 0) {
|
|
72063
|
-
lines.push("The following impact signals were observed:");
|
|
72064
|
-
lines.push("");
|
|
72065
|
-
for (const e of impactEvidence) {
|
|
72066
|
-
lines.push(`- \`${shortId2(e.id)}\` ${e.title}`);
|
|
72067
|
-
}
|
|
72068
|
-
lines.push("");
|
|
72069
|
-
}
|
|
72070
|
-
if (hasBoundaryCrossings) {
|
|
72071
|
-
lines.push("Async boundaries crossed during the incident:");
|
|
72072
|
-
lines.push("");
|
|
72073
|
-
for (const bc of r.timeline.boundaryCrossings) {
|
|
72074
|
-
const producer = bc.producer ?? "?";
|
|
72075
|
-
const worker = bc.worker ?? "?";
|
|
72076
|
-
lines.push(`- **${bc.queueName}**: ${producer} \u2192 ${worker}`);
|
|
72077
|
-
}
|
|
72078
|
-
lines.push("");
|
|
72079
|
-
}
|
|
72080
|
-
if (impactEvidence.length === 0 && !hasBoundaryCrossings) {
|
|
72081
|
-
lines.push("_No impact evidence was captured during this investigation._");
|
|
72082
|
-
lines.push("");
|
|
72083
|
-
}
|
|
72084
|
-
const metricsGap = r.gapAnalysis.gaps.find((g) => g.dimension === "metrics");
|
|
72085
|
-
if (metricsGap) {
|
|
72086
|
-
lines.push(
|
|
72087
|
-
`> **Note:** Precise user/runtime impact (error rates, latency percentiles, affected user count) requires metrics that were unavailable during this investigation. ${metricsGap.why} Next: ${metricsGap.nextSource}.`
|
|
72088
|
-
);
|
|
72089
|
-
} else {
|
|
72090
|
-
lines.push(
|
|
72091
|
-
"> **Note:** Confirm precise user/runtime impact (error rates, affected user count) with your metrics provider."
|
|
72092
|
-
);
|
|
72093
|
-
}
|
|
72094
|
-
lines.push("");
|
|
72095
|
-
lines.push("## Timeline");
|
|
72096
|
-
lines.push("");
|
|
72097
|
-
if (r.timeline.events.length === 0) {
|
|
72098
|
-
lines.push("_no timeline_");
|
|
72099
|
-
} else {
|
|
72100
|
-
for (const ev of r.timeline.events) {
|
|
72101
|
-
const at = ev.at ?? "structural";
|
|
72102
|
-
lines.push(`${ev.order}. [${at}] ${ev.title}`);
|
|
72103
|
-
}
|
|
72104
|
-
}
|
|
72105
|
-
lines.push("");
|
|
72106
|
-
lines.push("## Contributing factors");
|
|
72107
|
-
lines.push("");
|
|
72108
|
-
const supportedHypotheses = r.hypotheses.filter((h) => h.verdict === "supported");
|
|
72109
|
-
const commitEvidence = r.evidence.filter((e) => e.kind === "commit");
|
|
72110
|
-
if (supportedHypotheses.length === 0 && commitEvidence.length === 0) {
|
|
72111
|
-
const topCause = r.suspectedCauses[0];
|
|
72112
|
-
if (topCause) {
|
|
72113
|
-
lines.push(
|
|
72114
|
-
`_No hypotheses reached 'supported' status. The leading suspected cause is unconfirmed:_`
|
|
72115
|
-
);
|
|
72116
|
-
lines.push("");
|
|
72117
|
-
lines.push(`1. **(score ${topCause.finalScore.toFixed(2)}, ${topCause.band})** ${topCause.title}`);
|
|
72118
|
-
} else {
|
|
72119
|
-
lines.push(
|
|
72120
|
-
"_No confirmed contributing factors were identified. Further evidence is needed._"
|
|
72121
|
-
);
|
|
72122
|
-
}
|
|
72123
|
-
} else {
|
|
72124
|
-
if (r.causeChains !== void 0 && r.causeChains.length > 0) {
|
|
72125
|
-
lines.push("The following causal sequences are supported by the collected evidence:");
|
|
72126
|
-
lines.push("");
|
|
72127
|
-
for (const chain of r.causeChains) {
|
|
72128
|
-
lines.push(`### ${chain.category} _(confidence ${chain.confidence.toFixed(2)})_`);
|
|
72129
|
-
lines.push("");
|
|
72130
|
-
lines.push(chain.summary);
|
|
72131
|
-
lines.push("");
|
|
72132
|
-
for (let i = 0; i < chain.steps.length; i++) {
|
|
72133
|
-
const step = chain.steps[i];
|
|
72134
|
-
const num = i + 1;
|
|
72135
|
-
const evCite = step.evidenceIds.length > 0 ? ` \u2014 evidence: ${step.evidenceIds.map((id) => `\`${shortId2(id)}\``).join(", ")}` : "";
|
|
72136
|
-
lines.push(`${num}. **[${step.role}]** ${step.label}${evCite}`);
|
|
72137
|
-
}
|
|
72138
|
-
lines.push("");
|
|
72139
|
-
}
|
|
72140
|
-
} else {
|
|
72141
|
-
if (supportedHypotheses.length > 0) {
|
|
72142
|
-
lines.push("The following factors are supported by the collected evidence:");
|
|
72143
|
-
lines.push("");
|
|
72144
|
-
for (const h of supportedHypotheses) {
|
|
72145
|
-
lines.push(
|
|
72146
|
-
`- **${h.category}:** ${h.statement} _(confidence ${h.confidence.toFixed(2)})_`
|
|
72147
|
-
);
|
|
72148
|
-
}
|
|
72149
|
-
}
|
|
72150
|
-
}
|
|
72151
|
-
if (commitEvidence.length > 0) {
|
|
72152
|
-
if (supportedHypotheses.length > 0 || (r.causeChains?.length ?? 0) > 0) lines.push("");
|
|
72153
|
-
lines.push("Recent changes (commit evidence) present during this incident:");
|
|
72154
|
-
lines.push("");
|
|
72155
|
-
for (const e of commitEvidence) {
|
|
72156
|
-
lines.push(`- \`${shortId2(e.id)}\` ${e.title}`);
|
|
72157
|
-
}
|
|
72158
|
-
}
|
|
72159
|
-
}
|
|
72160
|
-
lines.push("");
|
|
72161
|
-
lines.push("## Root cause (candidate)");
|
|
72162
|
-
lines.push("");
|
|
72163
|
-
const topHypothesis = supportedHypotheses[0];
|
|
72164
|
-
const topSuspectedCause = r.suspectedCauses[0];
|
|
72165
|
-
const gapDimensions = r.gapAnalysis.gaps.slice(0, 2).map((g) => g.dimension).join(", ");
|
|
72166
|
-
const gapCaveat = gapDimensions.length > 0 ? `This candidate cannot be confirmed while evidence gaps remain (missing: ${gapDimensions}).` : "Review the gap analysis and gather additional evidence before confirming.";
|
|
72167
|
-
if (topHypothesis) {
|
|
72168
|
-
lines.push(
|
|
72169
|
-
`**CANDIDATE** _(confidence ${topHypothesis.confidence.toFixed(2)})_: ${topHypothesis.statement}`
|
|
72170
|
-
);
|
|
72171
|
-
lines.push("");
|
|
72172
|
-
lines.push(`_${gapCaveat}_`);
|
|
72173
|
-
} else if (topSuspectedCause) {
|
|
72174
|
-
lines.push(
|
|
72175
|
-
`**CANDIDATE** _(score ${topSuspectedCause.finalScore.toFixed(2)}, ${topSuspectedCause.band}, unconfirmed)_: ${topSuspectedCause.title}`
|
|
72176
|
-
);
|
|
72177
|
-
lines.push("");
|
|
72178
|
-
lines.push(`_${gapCaveat}_`);
|
|
72179
|
-
} else {
|
|
72180
|
-
lines.push(
|
|
72181
|
-
"_No candidate root cause could be determined from the available evidence._"
|
|
72182
|
-
);
|
|
72183
|
-
lines.push("");
|
|
72184
|
-
lines.push(`_${gapCaveat}_`);
|
|
72185
|
-
}
|
|
72186
|
-
lines.push("");
|
|
72187
|
-
lines.push("## Evidence");
|
|
72188
|
-
lines.push("");
|
|
72189
|
-
if (r.evidence.length === 0) {
|
|
72190
|
-
lines.push("_No evidence was collected._");
|
|
72191
|
-
} else {
|
|
72192
|
-
for (const e of r.evidence) {
|
|
72193
|
-
lines.push(`- ${shortId2(e.id)} [${e.source}/${e.kind}] ${e.title}`);
|
|
72194
|
-
}
|
|
72195
|
-
}
|
|
72196
|
-
lines.push("");
|
|
72197
|
-
lines.push("## Lessons learned");
|
|
72198
|
-
lines.push("");
|
|
72199
|
-
if (r.gapAnalysis.gaps.length === 0) {
|
|
72200
|
-
lines.push(
|
|
72201
|
-
"Good observability coverage for this investigation. Consider reviewing your alerting and on-call runbooks to ensure similar incidents are detected and scoped faster in future."
|
|
72202
|
-
);
|
|
72203
|
-
} else {
|
|
72204
|
-
for (const gap of r.gapAnalysis.gaps) {
|
|
72205
|
-
lines.push(
|
|
72206
|
-
`- During this incident, **${gap.dimension}** was unavailable \u2014 ${gap.why} Wiring **${gap.nextSource}** would let the next investigation confirm or rule this out faster.`
|
|
72207
|
-
);
|
|
72208
|
-
}
|
|
72209
|
-
}
|
|
72210
|
-
lines.push("");
|
|
72211
|
-
lines.push("## Follow-up actions");
|
|
72212
|
-
lines.push("");
|
|
72213
|
-
const checkboxItems = [];
|
|
72214
|
-
const seen = /* @__PURE__ */ new Set();
|
|
72215
|
-
for (const action of r.nextActions) {
|
|
72216
|
-
if (!seen.has(action)) {
|
|
72217
|
-
seen.add(action);
|
|
72218
|
-
checkboxItems.push(action);
|
|
72219
|
-
}
|
|
72220
|
-
}
|
|
72221
|
-
const seenGapSources = /* @__PURE__ */ new Set();
|
|
72222
|
-
for (const gap of r.gapAnalysis.gaps) {
|
|
72223
|
-
if (seenGapSources.has(gap.nextSource)) continue;
|
|
72224
|
-
seenGapSources.add(gap.nextSource);
|
|
72225
|
-
const cleanGapAction = `${gap.nextSource} to close the '${gap.dimension}' evidence gap`;
|
|
72226
|
-
if (seen.has(gap.nextSource)) {
|
|
72227
|
-
const idx = checkboxItems.indexOf(gap.nextSource);
|
|
72228
|
-
if (idx !== -1 && !seen.has(cleanGapAction)) {
|
|
72229
|
-
checkboxItems[idx] = cleanGapAction;
|
|
72230
|
-
seen.delete(gap.nextSource);
|
|
72231
|
-
seen.add(cleanGapAction);
|
|
72232
|
-
}
|
|
72233
|
-
} else if (!seen.has(cleanGapAction)) {
|
|
72234
|
-
seen.add(cleanGapAction);
|
|
72235
|
-
checkboxItems.push(cleanGapAction);
|
|
72236
|
-
}
|
|
72237
|
-
}
|
|
72238
|
-
if (checkboxItems.length === 0) {
|
|
72239
|
-
lines.push("- [ ] Review this postmortem with the team and decide on follow-ups.");
|
|
72240
|
-
} else {
|
|
72241
|
-
for (const item of checkboxItems) {
|
|
72242
|
-
lines.push(`- [ ] ${item}`);
|
|
72243
|
-
}
|
|
72244
|
-
}
|
|
72245
|
-
lines.push("");
|
|
72246
|
-
return lines.join("\n");
|
|
72247
|
-
}
|
|
72248
|
-
|
|
72249
|
-
// ../../packages/engine/src/score.ts
|
|
72250
|
-
init_cjs_shims();
|
|
72251
|
-
function toGrade(score) {
|
|
72252
|
-
if (score >= 85) return "A";
|
|
72253
|
-
if (score >= 70) return "B";
|
|
72254
|
-
if (score >= 55) return "C";
|
|
72255
|
-
if (score >= 40) return "D";
|
|
72256
|
-
return "F";
|
|
71620
|
+
function toGrade(score) {
|
|
71621
|
+
if (score >= 85) return "A";
|
|
71622
|
+
if (score >= 70) return "B";
|
|
71623
|
+
if (score >= 55) return "C";
|
|
71624
|
+
if (score >= 40) return "D";
|
|
71625
|
+
return "F";
|
|
72257
71626
|
}
|
|
72258
71627
|
function scoreInvestigation(r) {
|
|
72259
71628
|
const evidenceValue = Math.min(1, r.evidence.length / 8);
|
|
@@ -73211,166 +72580,872 @@ function evaluateScenario(scenario, report) {
|
|
|
73211
72580
|
} else {
|
|
73212
72581
|
ok = false;
|
|
73213
72582
|
}
|
|
73214
|
-
return { label: signal.label, ok };
|
|
73215
|
-
});
|
|
73216
|
-
let passed = 0;
|
|
73217
|
-
for (const check of checks) {
|
|
73218
|
-
if (check.ok) passed++;
|
|
72583
|
+
return { label: signal.label, ok };
|
|
72584
|
+
});
|
|
72585
|
+
let passed = 0;
|
|
72586
|
+
for (const check of checks) {
|
|
72587
|
+
if (check.ok) passed++;
|
|
72588
|
+
}
|
|
72589
|
+
return { checks, passed, total: checks.length };
|
|
72590
|
+
}
|
|
72591
|
+
|
|
72592
|
+
// ../../packages/engine/src/render-simulate.ts
|
|
72593
|
+
init_cjs_shims();
|
|
72594
|
+
function renderScenarioList(scenarios) {
|
|
72595
|
+
const lines = [];
|
|
72596
|
+
lines.push("Available training scenarios");
|
|
72597
|
+
lines.push("");
|
|
72598
|
+
for (const s of scenarios) {
|
|
72599
|
+
lines.push(` ${s.id} [${s.category}] ${s.title}`);
|
|
72600
|
+
lines.push(` ${s.symptom}`);
|
|
72601
|
+
}
|
|
72602
|
+
lines.push("");
|
|
72603
|
+
lines.push("Run: horus simulate <id>");
|
|
72604
|
+
return lines.join("\n");
|
|
72605
|
+
}
|
|
72606
|
+
function renderSimulation(scenario, report, evaluation) {
|
|
72607
|
+
const lines = [];
|
|
72608
|
+
lines.push(`# Training scenario: ${scenario.title}`);
|
|
72609
|
+
lines.push("");
|
|
72610
|
+
lines.push("## Symptom");
|
|
72611
|
+
lines.push(scenario.symptom);
|
|
72612
|
+
lines.push("");
|
|
72613
|
+
lines.push(
|
|
72614
|
+
"Form your own hypothesis before reading on \u2014 then compare it with what Horus found."
|
|
72615
|
+
);
|
|
72616
|
+
lines.push("");
|
|
72617
|
+
const isWeak = evaluation.passed < evaluation.total;
|
|
72618
|
+
if (isWeak) {
|
|
72619
|
+
lines.push(
|
|
72620
|
+
"> **Weak investigation** \u2014 Horus did not surface all expected signals for this scenario."
|
|
72621
|
+
);
|
|
72622
|
+
lines.push(
|
|
72623
|
+
"> This can happen when the hint resolves to a symbol that is not directly connected to the expected runtime/change evidence."
|
|
72624
|
+
);
|
|
72625
|
+
lines.push("");
|
|
72626
|
+
}
|
|
72627
|
+
lines.push("## Horus investigation");
|
|
72628
|
+
lines.push("");
|
|
72629
|
+
lines.push(report.summary);
|
|
72630
|
+
lines.push("");
|
|
72631
|
+
lines.push("### Top hypotheses");
|
|
72632
|
+
const topHypotheses = report.hypotheses.slice(0, 2);
|
|
72633
|
+
if (topHypotheses.length === 0) {
|
|
72634
|
+
lines.push("(none)");
|
|
72635
|
+
} else {
|
|
72636
|
+
for (const h of topHypotheses) {
|
|
72637
|
+
lines.push(`- Verdict: ${h.verdict}`);
|
|
72638
|
+
lines.push(` Confidence: ${h.confidence.toFixed(2)}`);
|
|
72639
|
+
lines.push(` Category: ${h.category}`);
|
|
72640
|
+
lines.push(` ${h.statement}`);
|
|
72641
|
+
}
|
|
72642
|
+
}
|
|
72643
|
+
lines.push("");
|
|
72644
|
+
lines.push("## Did Horus surface the expected signals?");
|
|
72645
|
+
lines.push("");
|
|
72646
|
+
for (const check of evaluation.checks) {
|
|
72647
|
+
const mark4 = check.ok ? "[x]" : "[ ]";
|
|
72648
|
+
lines.push(`${mark4} ${check.label}`);
|
|
72649
|
+
}
|
|
72650
|
+
lines.push("");
|
|
72651
|
+
lines.push(`Score: ${evaluation.passed}/${evaluation.total}`);
|
|
72652
|
+
lines.push("");
|
|
72653
|
+
lines.push("## Coaching");
|
|
72654
|
+
lines.push("");
|
|
72655
|
+
for (const tip of scenario.coachingTips) {
|
|
72656
|
+
lines.push(`- ${tip}`);
|
|
72657
|
+
}
|
|
72658
|
+
if (scenario.category === "queue") {
|
|
72659
|
+
const queueBoundaryCheck = evaluation.checks.find(
|
|
72660
|
+
(c) => c.label === "Queue boundary crossing detected"
|
|
72661
|
+
);
|
|
72662
|
+
if (queueBoundaryCheck && !queueBoundaryCheck.ok) {
|
|
72663
|
+
lines.push("");
|
|
72664
|
+
lines.push(
|
|
72665
|
+
"_No queue boundary was detected. The hint likely resolved to a symbol that is not directly connected to a known queue producer or worker. Try re-running with a hint that names a queue worker, producer, or the queue itself._"
|
|
72666
|
+
);
|
|
72667
|
+
}
|
|
72668
|
+
}
|
|
72669
|
+
if (scenario.category === "change") {
|
|
72670
|
+
const commitCheck = evaluation.checks.find(
|
|
72671
|
+
(c) => c.label === "Recent change evidence found"
|
|
72672
|
+
);
|
|
72673
|
+
if (commitCheck && !commitCheck.ok) {
|
|
72674
|
+
lines.push("");
|
|
72675
|
+
lines.push(
|
|
72676
|
+
"_No recent change evidence was found. The deployment-regression scenario needs a diffable git range (`--since`) with commits touching the resolved symbol. Try a more specific hint or a different `--since` range._"
|
|
72677
|
+
);
|
|
72678
|
+
}
|
|
72679
|
+
}
|
|
72680
|
+
return lines.join("\n");
|
|
72681
|
+
}
|
|
72682
|
+
|
|
72683
|
+
// ../../packages/engine/src/migrate-report.ts
|
|
72684
|
+
init_cjs_shims();
|
|
72685
|
+
function getBand2(score) {
|
|
72686
|
+
if (score >= 0.85) return "highly-likely";
|
|
72687
|
+
if (score >= 0.65) return "likely";
|
|
72688
|
+
if (score >= 0.4) return "possible";
|
|
72689
|
+
return "observation";
|
|
72690
|
+
}
|
|
72691
|
+
function migrateReport(raw) {
|
|
72692
|
+
if (raw === null || typeof raw !== "object") {
|
|
72693
|
+
throw new Error("migrateReport: expected an object, got " + typeof raw);
|
|
72694
|
+
}
|
|
72695
|
+
const r = raw;
|
|
72696
|
+
if (!Array.isArray(r["nextActions"])) {
|
|
72697
|
+
r["nextActions"] = [];
|
|
72698
|
+
}
|
|
72699
|
+
if (Array.isArray(r["suspectedCauses"])) {
|
|
72700
|
+
r["suspectedCauses"] = r["suspectedCauses"].flatMap(
|
|
72701
|
+
(c, i) => {
|
|
72702
|
+
if (c === null || typeof c !== "object") return [];
|
|
72703
|
+
const cause = c;
|
|
72704
|
+
if (typeof cause["title"] === "string" && typeof cause["finalScore"] === "number") {
|
|
72705
|
+
const fs3 = cause["finalScore"];
|
|
72706
|
+
return [{
|
|
72707
|
+
id: typeof cause["id"] === "string" ? cause["id"] : `cause:partial:${i}`,
|
|
72708
|
+
title: cause["title"],
|
|
72709
|
+
category: typeof cause["category"] === "string" ? cause["category"] : "unknown",
|
|
72710
|
+
sourceEvidenceIds: Array.isArray(cause["sourceEvidenceIds"]) ? cause["sourceEvidenceIds"] : [],
|
|
72711
|
+
affectedNodeIds: Array.isArray(cause["affectedNodeIds"]) ? cause["affectedNodeIds"] : [],
|
|
72712
|
+
baseScore: typeof cause["baseScore"] === "number" ? cause["baseScore"] : fs3,
|
|
72713
|
+
finalScore: fs3,
|
|
72714
|
+
confidence: typeof cause["confidence"] === "number" ? cause["confidence"] : fs3,
|
|
72715
|
+
band: typeof cause["band"] === "string" ? cause["band"] : getBand2(fs3),
|
|
72716
|
+
explanations: Array.isArray(cause["explanations"]) ? cause["explanations"] : [],
|
|
72717
|
+
...cause["metadata"] !== void 0 ? { metadata: cause["metadata"] } : {}
|
|
72718
|
+
}];
|
|
72719
|
+
}
|
|
72720
|
+
const statement = typeof cause["statement"] === "string" ? cause["statement"] : "";
|
|
72721
|
+
if (!statement) return [];
|
|
72722
|
+
const score = typeof cause["score"] === "number" ? cause["score"] : 0;
|
|
72723
|
+
const evidenceIds = Array.isArray(cause["evidenceIds"]) ? cause["evidenceIds"] : [];
|
|
72724
|
+
return [{
|
|
72725
|
+
id: `cause:legacy:${i}`,
|
|
72726
|
+
title: statement,
|
|
72727
|
+
category: "unknown",
|
|
72728
|
+
sourceEvidenceIds: evidenceIds,
|
|
72729
|
+
affectedNodeIds: [],
|
|
72730
|
+
baseScore: score,
|
|
72731
|
+
finalScore: score,
|
|
72732
|
+
confidence: score,
|
|
72733
|
+
band: getBand2(score),
|
|
72734
|
+
explanations: [],
|
|
72735
|
+
...cause["metadata"] !== void 0 ? { metadata: cause["metadata"] } : {}
|
|
72736
|
+
}];
|
|
72737
|
+
}
|
|
72738
|
+
);
|
|
72739
|
+
}
|
|
72740
|
+
return r;
|
|
72741
|
+
}
|
|
72742
|
+
|
|
72743
|
+
// ../../packages/cli/src/commands/explain.ts
|
|
72744
|
+
async function runExplain(query, opts) {
|
|
72745
|
+
const config = await loadConfig(opts.config);
|
|
72746
|
+
const code = codeForRepo(config, opts.repo);
|
|
72747
|
+
const health = await code.health();
|
|
72748
|
+
if (!health.ok) {
|
|
72749
|
+
console.error(import_picocolors2.default.red("Source-intelligence host unreachable \u2014 run: horus index"));
|
|
72750
|
+
return 1;
|
|
72751
|
+
}
|
|
72752
|
+
const symbols = await code.searchSymbols(query, 5);
|
|
72753
|
+
if (symbols.length === 0) {
|
|
72754
|
+
if (await isQueueBoundary(config, query)) {
|
|
72755
|
+
console.log(`No symbol found for: ${import_picocolors2.default.bold(query)}`);
|
|
72756
|
+
console.log(
|
|
72757
|
+
import_picocolors2.default.dim(` "${query}" matches an async boundary \u2014 try: `) + import_picocolors2.default.bold(`horus queues ${query}`)
|
|
72758
|
+
);
|
|
72759
|
+
return 1;
|
|
72760
|
+
}
|
|
72761
|
+
console.log(`No symbol found for: ${query}`);
|
|
72762
|
+
console.log(import_picocolors2.default.dim(` Tip: use an exact class or function name, e.g. "MyService" or "processOrder"`));
|
|
72763
|
+
return 1;
|
|
72764
|
+
}
|
|
72765
|
+
const top = symbols[0];
|
|
72766
|
+
if (!top) return 1;
|
|
72767
|
+
const isExactMatch = top.name.toLowerCase() === query.toLowerCase();
|
|
72768
|
+
if (!isExactMatch) {
|
|
72769
|
+
if (await isQueueBoundary(config, query)) {
|
|
72770
|
+
console.log(`No exact symbol match for: ${import_picocolors2.default.bold(query)}`);
|
|
72771
|
+
console.log(
|
|
72772
|
+
import_picocolors2.default.dim(` "${query}" matches an async boundary \u2014 try: `) + import_picocolors2.default.bold(`horus queues ${query}`)
|
|
72773
|
+
);
|
|
72774
|
+
return 1;
|
|
72775
|
+
}
|
|
72776
|
+
console.log(
|
|
72777
|
+
import_picocolors2.default.yellow(` No exact match for "${query}"`) + import_picocolors2.default.dim(` \u2014 showing closest: "${top.name}" (fuzzy match)`)
|
|
72778
|
+
);
|
|
72779
|
+
}
|
|
72780
|
+
const siblings = symbols.filter((s) => s.name === top.name && s.id !== top.id);
|
|
72781
|
+
const [ctx, impact, flows] = await Promise.all([
|
|
72782
|
+
code.context(top.id),
|
|
72783
|
+
code.impact(top.id, opts.depth ?? 3),
|
|
72784
|
+
code.flowsFor(top.id)
|
|
72785
|
+
]);
|
|
72786
|
+
if (opts.json) {
|
|
72787
|
+
console.log(
|
|
72788
|
+
JSON.stringify(
|
|
72789
|
+
{
|
|
72790
|
+
symbol: ctx.symbol,
|
|
72791
|
+
community: ctx.community,
|
|
72792
|
+
isDead: ctx.isDead,
|
|
72793
|
+
callers: ctx.callers,
|
|
72794
|
+
callees: ctx.callees,
|
|
72795
|
+
impact,
|
|
72796
|
+
flows
|
|
72797
|
+
},
|
|
72798
|
+
null,
|
|
72799
|
+
2
|
|
72800
|
+
)
|
|
72801
|
+
);
|
|
72802
|
+
return 0;
|
|
72803
|
+
}
|
|
72804
|
+
renderReport2(top, ctx, impact, flows, siblings);
|
|
72805
|
+
return 0;
|
|
72806
|
+
}
|
|
72807
|
+
async function isQueueBoundary(config, query) {
|
|
72808
|
+
try {
|
|
72809
|
+
let project;
|
|
72810
|
+
try {
|
|
72811
|
+
project = resolveEnvironment(config).project;
|
|
72812
|
+
} catch {
|
|
72813
|
+
}
|
|
72814
|
+
const { db, sql: sql2 } = createDb(config.database.url);
|
|
72815
|
+
try {
|
|
72816
|
+
const edges = await listQueueEdges(db, { queueName: query, project });
|
|
72817
|
+
return edges.length > 0;
|
|
72818
|
+
} finally {
|
|
72819
|
+
await sql2.end().catch(() => {
|
|
72820
|
+
});
|
|
72821
|
+
}
|
|
72822
|
+
} catch {
|
|
72823
|
+
return false;
|
|
72824
|
+
}
|
|
72825
|
+
}
|
|
72826
|
+
function renderReport2(top, ctx, impact, flows, siblings) {
|
|
72827
|
+
const kind = top.id.includes(":") ? top.id.substring(0, top.id.indexOf(":")) : top.id;
|
|
72828
|
+
const sym = ctx.symbol;
|
|
72829
|
+
let location = sym.filePath;
|
|
72830
|
+
if (sym.startLine) {
|
|
72831
|
+
location += ":" + sym.startLine;
|
|
72832
|
+
if (sym.endLine) {
|
|
72833
|
+
location += "-" + sym.endLine;
|
|
72834
|
+
}
|
|
72835
|
+
}
|
|
72836
|
+
const community = ctx.community ? ctx.community.name : import_picocolors2.default.dim("\u2014");
|
|
72837
|
+
const formatNames = (list) => {
|
|
72838
|
+
const names = list.map((s) => symbolDisplayName(s));
|
|
72839
|
+
if (names.length <= 10) return names.join(", ");
|
|
72840
|
+
const extra = names.length - 10;
|
|
72841
|
+
return names.slice(0, 10).join(", ") + ` +${extra} more`;
|
|
72842
|
+
};
|
|
72843
|
+
const callerStr = ctx.callers.length === 0 ? import_picocolors2.default.dim("none") : formatNames(ctx.callers);
|
|
72844
|
+
const calleeStr = ctx.callees.length === 0 ? import_picocolors2.default.dim("none") : formatNames(ctx.callees);
|
|
72845
|
+
const allImpacted = impact.byDepth.flatMap((d) => d.symbols);
|
|
72846
|
+
const impactNames = allImpacted.slice(0, 8).map((s) => s.name).join(", ");
|
|
72847
|
+
const impactSuffix = allImpacted.length > 8 ? ` +${allImpacted.length - 8} more` : "";
|
|
72848
|
+
console.log("");
|
|
72849
|
+
console.log(
|
|
72850
|
+
` ${import_picocolors2.default.bold("Symbol:")} ${top.name} ${import_picocolors2.default.dim("(kind = " + kind + ")")}`
|
|
72851
|
+
);
|
|
72852
|
+
if (siblings.length > 0) {
|
|
72853
|
+
const others = siblings.map((s) => s.filePath).join(", ");
|
|
72854
|
+
console.log(
|
|
72855
|
+
import_picocolors2.default.dim(
|
|
72856
|
+
` ${siblings.length + 1} symbols named '${top.name}'; showing this one \u2014 others: ${others}`
|
|
72857
|
+
)
|
|
72858
|
+
);
|
|
72859
|
+
}
|
|
72860
|
+
console.log(` ${import_picocolors2.default.bold("Location:")} ${location}`);
|
|
72861
|
+
console.log(` ${import_picocolors2.default.bold("Community:")} ${community}`);
|
|
72862
|
+
console.log(
|
|
72863
|
+
` ${import_picocolors2.default.bold("Callers (" + ctx.callers.length + "):")} ${callerStr}`
|
|
72864
|
+
);
|
|
72865
|
+
console.log(
|
|
72866
|
+
` ${import_picocolors2.default.bold("Callees (" + ctx.callees.length + "):")} ${calleeStr}`
|
|
72867
|
+
);
|
|
72868
|
+
console.log(
|
|
72869
|
+
` ${import_picocolors2.default.bold("Impact:")} ${impact.affected} symbols affected` + (impactNames ? ` ${import_picocolors2.default.dim(impactNames + impactSuffix)}` : "")
|
|
72870
|
+
);
|
|
72871
|
+
console.log(` ${import_picocolors2.default.bold("Related flows:")}`);
|
|
72872
|
+
if (flows.length === 0) {
|
|
72873
|
+
console.log(` ${import_picocolors2.default.dim("none")}`);
|
|
72874
|
+
} else {
|
|
72875
|
+
for (const flow of flows) {
|
|
72876
|
+
console.log(` ${flow.name} ${import_picocolors2.default.dim("(" + flow.steps.length + " steps)")}`);
|
|
72877
|
+
}
|
|
72878
|
+
}
|
|
72879
|
+
if (ctx.isDead) {
|
|
72880
|
+
console.log(` ${import_picocolors2.default.bold("Dead code:")} ${import_picocolors2.default.red("yes")}`);
|
|
72881
|
+
}
|
|
72882
|
+
console.log("");
|
|
72883
|
+
}
|
|
72884
|
+
|
|
72885
|
+
// ../../packages/cli/src/commands/index-repo.ts
|
|
72886
|
+
init_cjs_shims();
|
|
72887
|
+
var import_node_path6 = require("path");
|
|
72888
|
+
var import_picocolors3 = __toESM(require_picocolors(), 1);
|
|
72889
|
+
|
|
72890
|
+
// ../../packages/stitcher/src/index.ts
|
|
72891
|
+
init_cjs_shims();
|
|
72892
|
+
|
|
72893
|
+
// ../../packages/stitcher/src/extract.ts
|
|
72894
|
+
init_cjs_shims();
|
|
72895
|
+
var INJECT_QUEUE_RE = /@InjectQueue\(\s*['"]([^'"]+)['"]/g;
|
|
72896
|
+
var PROCESSOR_RE = /@Processor\(\s*['"]([^'"]+)['"][^)]*\)\s*(?:@\w+[^\n]*\s*)*export\s+class\s+(\w+)/g;
|
|
72897
|
+
var CONST_DECL_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
|
|
72898
|
+
var NEW_BULL_RE = /(?:(\w+)\s*[:=]\s*)?new\s+(Queue|Worker)\s*(?:<[^>]*>)?\s*\(\s*(['"][^'"]+['"]|[A-Za-z_$][\w$.]*)/g;
|
|
72899
|
+
var QUEUE_NAME_CONST_RE = /(?:export\s+)?const\s+([A-Za-z_$][\w$]*(?:QUEUE|Queue)[\w$]*)\s*=\s*['"]([^'"]+)['"]/g;
|
|
72900
|
+
function baseName(filePath) {
|
|
72901
|
+
const parts = filePath.split("/");
|
|
72902
|
+
const last = parts[parts.length - 1] ?? filePath;
|
|
72903
|
+
return last.replace(/\.[jt]sx?$/, "");
|
|
72904
|
+
}
|
|
72905
|
+
function resolveArg(raw, constMap) {
|
|
72906
|
+
const arg = raw.trim();
|
|
72907
|
+
if (arg.startsWith('"') || arg.startsWith("'")) {
|
|
72908
|
+
return arg.slice(1, -1);
|
|
72909
|
+
}
|
|
72910
|
+
return constMap.get(arg) ?? null;
|
|
72911
|
+
}
|
|
72912
|
+
function extractQueueGraph(input) {
|
|
72913
|
+
const constMap = /* @__PURE__ */ new Map();
|
|
72914
|
+
const allContent = [
|
|
72915
|
+
...input.producerClasses.map((p) => p.content),
|
|
72916
|
+
...input.workerFiles.map((w) => w.content)
|
|
72917
|
+
];
|
|
72918
|
+
for (const content of allContent) {
|
|
72919
|
+
for (const m of content.matchAll(CONST_DECL_RE)) {
|
|
72920
|
+
const ident = m[1];
|
|
72921
|
+
const value = m[2];
|
|
72922
|
+
if (ident && value && !constMap.has(ident)) constMap.set(ident, value);
|
|
72923
|
+
}
|
|
72924
|
+
}
|
|
72925
|
+
const producers = [];
|
|
72926
|
+
for (const pc35 of input.producerClasses) {
|
|
72927
|
+
for (const m of pc35.content.matchAll(INJECT_QUEUE_RE)) {
|
|
72928
|
+
const queue = m[1] ?? "";
|
|
72929
|
+
if (queue) producers.push({ queue, symbol: pc35.name, file: pc35.filePath });
|
|
72930
|
+
}
|
|
72931
|
+
for (const m of pc35.content.matchAll(NEW_BULL_RE)) {
|
|
72932
|
+
if (m[2] !== "Queue") continue;
|
|
72933
|
+
const queue = resolveArg(m[3] ?? "", constMap);
|
|
72934
|
+
if (queue) {
|
|
72935
|
+
producers.push({ queue, symbol: (m[1] ?? pc35.name) || baseName(pc35.filePath), file: pc35.filePath });
|
|
72936
|
+
}
|
|
72937
|
+
}
|
|
72938
|
+
for (const m of pc35.content.matchAll(QUEUE_NAME_CONST_RE)) {
|
|
72939
|
+
const ident = m[1] ?? "";
|
|
72940
|
+
const queue = m[2] ?? "";
|
|
72941
|
+
if (queue) producers.push({ queue, symbol: ident || baseName(pc35.filePath), file: pc35.filePath });
|
|
72942
|
+
}
|
|
72943
|
+
}
|
|
72944
|
+
const workers = [];
|
|
72945
|
+
for (const wf of input.workerFiles) {
|
|
72946
|
+
for (const m of wf.content.matchAll(PROCESSOR_RE)) {
|
|
72947
|
+
const queue = m[1] ?? "";
|
|
72948
|
+
const className = m[2] ?? "";
|
|
72949
|
+
if (queue) workers.push({ queue, symbol: className, file: wf.filePath });
|
|
72950
|
+
}
|
|
72951
|
+
for (const m of wf.content.matchAll(NEW_BULL_RE)) {
|
|
72952
|
+
if (m[2] !== "Worker") continue;
|
|
72953
|
+
const queue = resolveArg(m[3] ?? "", constMap);
|
|
72954
|
+
if (queue) {
|
|
72955
|
+
workers.push({ queue, symbol: m[1] ?? baseName(wf.filePath), file: wf.filePath });
|
|
72956
|
+
}
|
|
72957
|
+
}
|
|
72958
|
+
}
|
|
72959
|
+
const queues = [...new Set([...producers, ...workers].map((r) => r.queue))].sort();
|
|
72960
|
+
const edges = [];
|
|
72961
|
+
for (const q of queues) {
|
|
72962
|
+
const P = dedupeByFile(producers.filter((p) => p.queue === q));
|
|
72963
|
+
const W = dedupeByFile(workers.filter((w) => w.queue === q));
|
|
72964
|
+
if (P.length && W.length) {
|
|
72965
|
+
for (const p of P) {
|
|
72966
|
+
for (const w of W) {
|
|
72967
|
+
edges.push({
|
|
72968
|
+
queueName: q,
|
|
72969
|
+
producerSymbol: p.symbol,
|
|
72970
|
+
producerFile: p.file,
|
|
72971
|
+
workerSymbol: w.symbol,
|
|
72972
|
+
workerFile: w.file
|
|
72973
|
+
});
|
|
72974
|
+
}
|
|
72975
|
+
}
|
|
72976
|
+
} else if (W.length) {
|
|
72977
|
+
for (const w of W) {
|
|
72978
|
+
edges.push({
|
|
72979
|
+
queueName: q,
|
|
72980
|
+
producerSymbol: null,
|
|
72981
|
+
producerFile: null,
|
|
72982
|
+
workerSymbol: w.symbol,
|
|
72983
|
+
workerFile: w.file
|
|
72984
|
+
});
|
|
72985
|
+
}
|
|
72986
|
+
} else if (P.length) {
|
|
72987
|
+
for (const p of P) {
|
|
72988
|
+
edges.push({
|
|
72989
|
+
queueName: q,
|
|
72990
|
+
producerSymbol: p.symbol,
|
|
72991
|
+
producerFile: p.file,
|
|
72992
|
+
workerSymbol: null,
|
|
72993
|
+
workerFile: null
|
|
72994
|
+
});
|
|
72995
|
+
}
|
|
72996
|
+
}
|
|
72997
|
+
}
|
|
72998
|
+
return { queues, producers, workers, edges };
|
|
72999
|
+
}
|
|
73000
|
+
function dedupeByFile(rows) {
|
|
73001
|
+
const seen = /* @__PURE__ */ new Set();
|
|
73002
|
+
const out = [];
|
|
73003
|
+
for (const r of rows) {
|
|
73004
|
+
if (seen.has(r.file)) continue;
|
|
73005
|
+
seen.add(r.file);
|
|
73006
|
+
out.push(r);
|
|
73007
|
+
}
|
|
73008
|
+
return out;
|
|
73009
|
+
}
|
|
73010
|
+
|
|
73011
|
+
// ../../packages/stitcher/src/stitch.ts
|
|
73012
|
+
init_cjs_shims();
|
|
73013
|
+
async function stitch(client, db, opts = {}) {
|
|
73014
|
+
const producerRows = (await client.cypher(
|
|
73015
|
+
// "new Queue" (no paren) so generic-typed `new Queue<T>(...)` also matches.
|
|
73016
|
+
'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'
|
|
73017
|
+
)).rows;
|
|
73018
|
+
const producerClasses = producerRows.map((r) => ({
|
|
73019
|
+
name: String(r[0] ?? ""),
|
|
73020
|
+
filePath: String(r[1] ?? ""),
|
|
73021
|
+
content: String(r[2] ?? "")
|
|
73022
|
+
}));
|
|
73023
|
+
const workerRows = (await client.cypher(
|
|
73024
|
+
// "new Worker" (no paren) so generic-typed `new Worker<T>(...)` also matches.
|
|
73025
|
+
'MATCH (n) WHERE n.content CONTAINS "@Processor(" OR n.content CONTAINS "new Worker" RETURN n.name, n.file_path, n.content'
|
|
73026
|
+
)).rows;
|
|
73027
|
+
const workerFiles = workerRows.map((r) => ({
|
|
73028
|
+
filePath: String(r[1] ?? ""),
|
|
73029
|
+
content: String(r[2] ?? "")
|
|
73030
|
+
}));
|
|
73031
|
+
const graph = extractQueueGraph({ producerClasses, workerFiles });
|
|
73032
|
+
const edges = graph.edges.map((e) => ({
|
|
73033
|
+
queueName: e.queueName,
|
|
73034
|
+
producerSymbol: e.producerSymbol,
|
|
73035
|
+
producerFile: e.producerFile,
|
|
73036
|
+
workerSymbol: e.workerSymbol,
|
|
73037
|
+
workerFile: e.workerFile,
|
|
73038
|
+
source: "stitcher",
|
|
73039
|
+
project: opts.project ?? null
|
|
73040
|
+
}));
|
|
73041
|
+
await replaceQueueEdges(db, edges, { project: opts.project });
|
|
73042
|
+
return {
|
|
73043
|
+
queues: graph.queues.length,
|
|
73044
|
+
producers: graph.producers.length,
|
|
73045
|
+
workers: graph.workers.length,
|
|
73046
|
+
edges: edges.length
|
|
73047
|
+
};
|
|
73048
|
+
}
|
|
73049
|
+
|
|
73050
|
+
// ../../packages/cli/src/commands/index-repo.ts
|
|
73051
|
+
async function stitchQueueMap(hostUrl, dbUrl, label) {
|
|
73052
|
+
const axon = new AxonHttpClient({ baseUrl: hostUrl });
|
|
73053
|
+
const { db, sql: sql2 } = createDb(dbUrl);
|
|
73054
|
+
try {
|
|
73055
|
+
const summary = await stitch(axon, db, { project: label });
|
|
73056
|
+
console.log(
|
|
73057
|
+
import_picocolors3.default.dim(`[${label}] `) + `Stitched ${summary.edges} queue edge(s) across ${summary.queues} queue(s) \u2014 ${summary.producers} producer(s), ${summary.workers} worker(s).`
|
|
73058
|
+
);
|
|
73059
|
+
} finally {
|
|
73060
|
+
await sql2.end();
|
|
73061
|
+
}
|
|
73062
|
+
}
|
|
73063
|
+
function findConfiguredRepo(config, root) {
|
|
73064
|
+
const target = (0, import_node_path6.resolve)(root);
|
|
73065
|
+
for (const p of config.projects) {
|
|
73066
|
+
for (const r of p.repositories) {
|
|
73067
|
+
if ((0, import_node_path6.resolve)(r.path) === target) {
|
|
73068
|
+
const hostUrl = r.source?.hostUrl ?? r.axon?.hostUrl;
|
|
73069
|
+
return { project: p.name, ...hostUrl ? { hostUrl } : {} };
|
|
73070
|
+
}
|
|
73071
|
+
}
|
|
73072
|
+
}
|
|
73073
|
+
return null;
|
|
73074
|
+
}
|
|
73075
|
+
async function runIndex(opts) {
|
|
73076
|
+
try {
|
|
73077
|
+
const cwd = process.cwd();
|
|
73078
|
+
const root = findRepoRoot(cwd) ?? cwd;
|
|
73079
|
+
const dbUrlDefault = process.env["DATABASE_URL"] ?? "postgresql://horus:horus@localhost:5433/horus";
|
|
73080
|
+
let config = null;
|
|
73081
|
+
try {
|
|
73082
|
+
config = await loadConfig(opts.config, { name: opts.name });
|
|
73083
|
+
} catch {
|
|
73084
|
+
config = null;
|
|
73085
|
+
}
|
|
73086
|
+
const dbUrl = config?.database.url ?? dbUrlDefault;
|
|
73087
|
+
let configuredProject = null;
|
|
73088
|
+
let configuredHost;
|
|
73089
|
+
if (config) {
|
|
73090
|
+
if (opts.project !== void 0 || opts.name !== void 0) {
|
|
73091
|
+
try {
|
|
73092
|
+
const renv = resolveEnvironment(config, { project: opts.project, env: opts.env });
|
|
73093
|
+
configuredProject = renv.project;
|
|
73094
|
+
configuredHost = renv.repositories[0]?.sourceHostUrl ?? renv.repositories[0]?.axonHostUrl;
|
|
73095
|
+
} catch {
|
|
73096
|
+
}
|
|
73097
|
+
}
|
|
73098
|
+
if (configuredProject === null) {
|
|
73099
|
+
const match = findConfiguredRepo(config, root);
|
|
73100
|
+
if (match) {
|
|
73101
|
+
configuredProject = match.project;
|
|
73102
|
+
configuredHost = match.hostUrl;
|
|
73103
|
+
}
|
|
73104
|
+
}
|
|
73105
|
+
}
|
|
73106
|
+
const isConfigured = configuredProject !== null;
|
|
73107
|
+
const name = opts.name ?? configuredProject ?? (0, import_node_path6.basename)(root);
|
|
73108
|
+
const label = configuredProject ?? name;
|
|
73109
|
+
let hostUrl;
|
|
73110
|
+
let spawned = false;
|
|
73111
|
+
for (const candidate of [configuredHost, readSourceHostUrl(root) ?? void 0]) {
|
|
73112
|
+
if (candidate && await isHostHealthy(candidate)) {
|
|
73113
|
+
hostUrl = candidate;
|
|
73114
|
+
break;
|
|
73115
|
+
}
|
|
73116
|
+
}
|
|
73117
|
+
if (hostUrl) {
|
|
73118
|
+
console.log(import_picocolors3.default.dim(`Reusing source-intelligence host for ${label} at ${hostUrl}`));
|
|
73119
|
+
} else {
|
|
73120
|
+
spawned = true;
|
|
73121
|
+
console.log(import_picocolors3.default.bold(`Indexing ${label}`) + import_picocolors3.default.dim(` (${root})`));
|
|
73122
|
+
if (!await sourceAvailable()) {
|
|
73123
|
+
console.error(import_picocolors3.default.red("horus-source not found on PATH. Install it: pip install horus-source"));
|
|
73124
|
+
return 1;
|
|
73125
|
+
}
|
|
73126
|
+
if (!isAnalyzed(root)) {
|
|
73127
|
+
console.log(import_picocolors3.default.dim(" analyzing with source-intelligence backend (first time \u2014 this can take a while)\u2026"));
|
|
73128
|
+
try {
|
|
73129
|
+
await analyzeRepo(root);
|
|
73130
|
+
} catch (err) {
|
|
73131
|
+
console.error(import_picocolors3.default.red(` source analysis failed: ${err.message}`));
|
|
73132
|
+
return 1;
|
|
73133
|
+
}
|
|
73134
|
+
} else {
|
|
73135
|
+
console.log(import_picocolors3.default.dim(" already analyzed"));
|
|
73136
|
+
}
|
|
73137
|
+
const port = await findFreePort();
|
|
73138
|
+
hostUrl = `http://127.0.0.1:${port}`;
|
|
73139
|
+
console.log(import_picocolors3.default.dim(` starting source-intelligence host on port ${port}\u2026`));
|
|
73140
|
+
startHost(root, port);
|
|
73141
|
+
if (!await waitForHost(hostUrl)) {
|
|
73142
|
+
removeSpawnedHostRecord(root);
|
|
73143
|
+
console.error(
|
|
73144
|
+
import_picocolors3.default.red(` Source-intelligence host did not become healthy \u2014 see ${root}/.horus/source-host.log`)
|
|
73145
|
+
);
|
|
73146
|
+
return 1;
|
|
73147
|
+
}
|
|
73148
|
+
}
|
|
73149
|
+
await stitchQueueMap(hostUrl, dbUrl, label);
|
|
73150
|
+
if (spawned && !isConfigured) {
|
|
73151
|
+
const file = {
|
|
73152
|
+
version: 1,
|
|
73153
|
+
project: {
|
|
73154
|
+
name,
|
|
73155
|
+
repositories: [{ name, path: root, source: { hostUrl } }],
|
|
73156
|
+
environments: [{ name: opts.env ?? "production", readOnly: true, connectors: {} }]
|
|
73157
|
+
}
|
|
73158
|
+
};
|
|
73159
|
+
const configPath = writeLocalConfig(root, file);
|
|
73160
|
+
registerProject(name, root, configPath);
|
|
73161
|
+
ensureProjectGitignore(root);
|
|
73162
|
+
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(name)} \u2014 host ${hostUrl}`);
|
|
73163
|
+
console.log(import_picocolors3.default.dim(` ${configPath}`));
|
|
73164
|
+
console.log(
|
|
73165
|
+
import_picocolors3.default.dim(
|
|
73166
|
+
` investigate: horus investigate --name ${name} "<hint>" (or from this repo: horus investigate "<hint>")`
|
|
73167
|
+
)
|
|
73168
|
+
);
|
|
73169
|
+
} else if (hostUrl !== configuredHost) {
|
|
73170
|
+
const existingPath = discoverLocalConfig(root);
|
|
73171
|
+
if (existingPath) {
|
|
73172
|
+
const file = readLocalConfig(existingPath);
|
|
73173
|
+
const project = file.project;
|
|
73174
|
+
const repos = project["repositories"];
|
|
73175
|
+
if (repos && repos.length > 0) {
|
|
73176
|
+
repos[0]["source"] = { hostUrl };
|
|
73177
|
+
}
|
|
73178
|
+
writeLocalConfig(root, file);
|
|
73179
|
+
registerProject(label, root, existingPath);
|
|
73180
|
+
ensureProjectGitignore(root);
|
|
73181
|
+
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} \u2014 source host registered at ${hostUrl}`);
|
|
73182
|
+
console.log(import_picocolors3.default.dim(` ${existingPath}`));
|
|
73183
|
+
} else {
|
|
73184
|
+
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`);
|
|
73185
|
+
}
|
|
73186
|
+
} else {
|
|
73187
|
+
console.log(
|
|
73188
|
+
`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`
|
|
73189
|
+
);
|
|
73190
|
+
}
|
|
73191
|
+
return 0;
|
|
73192
|
+
} catch (err) {
|
|
73193
|
+
console.error(import_picocolors3.default.red(err.message));
|
|
73194
|
+
return 1;
|
|
73195
|
+
}
|
|
73196
|
+
}
|
|
73197
|
+
|
|
73198
|
+
// ../../packages/cli/src/commands/queues.ts
|
|
73199
|
+
init_cjs_shims();
|
|
73200
|
+
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
73201
|
+
async function runQueues(name, opts) {
|
|
73202
|
+
try {
|
|
73203
|
+
const config = await loadConfig(opts.config, { name: opts.name });
|
|
73204
|
+
const { db, sql: sql2 } = createDb(config.database.url);
|
|
73205
|
+
try {
|
|
73206
|
+
let project = opts.project;
|
|
73207
|
+
if (project === void 0) {
|
|
73208
|
+
try {
|
|
73209
|
+
project = resolveEnvironment(config, { project: opts.project }).project;
|
|
73210
|
+
} catch {
|
|
73211
|
+
}
|
|
73212
|
+
}
|
|
73213
|
+
const rows = await listQueueEdges(db, { project, queueName: name });
|
|
73214
|
+
if (opts.json) {
|
|
73215
|
+
const topology = topologyToJson(buildQueueMap(rows));
|
|
73216
|
+
const out = { project: project ?? null, topology };
|
|
73217
|
+
if (opts.live) out["live"] = await gatherLiveState(config, rows, name);
|
|
73218
|
+
console.log(JSON.stringify(out, null, 2));
|
|
73219
|
+
return 0;
|
|
73220
|
+
}
|
|
73221
|
+
console.log(
|
|
73222
|
+
import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
|
|
73223
|
+
);
|
|
73224
|
+
console.log("");
|
|
73225
|
+
if (rows.length === 0) {
|
|
73226
|
+
console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
|
|
73227
|
+
} else {
|
|
73228
|
+
const byQueue = buildQueueMap(rows);
|
|
73229
|
+
printTopology(byQueue);
|
|
73230
|
+
}
|
|
73231
|
+
console.log("");
|
|
73232
|
+
if (opts.live) {
|
|
73233
|
+
await runLiveMode(config, rows, name);
|
|
73234
|
+
} else {
|
|
73235
|
+
console.log(
|
|
73236
|
+
import_picocolors4.default.dim(
|
|
73237
|
+
" Tip: run horus queues --live to show real-time Redis/BullMQ depths and failed-job counts."
|
|
73238
|
+
)
|
|
73239
|
+
);
|
|
73240
|
+
}
|
|
73241
|
+
} finally {
|
|
73242
|
+
await sql2.end();
|
|
73243
|
+
}
|
|
73244
|
+
return 0;
|
|
73245
|
+
} catch (err) {
|
|
73246
|
+
console.error(import_picocolors4.default.red(err.message));
|
|
73247
|
+
return 1;
|
|
73248
|
+
}
|
|
73249
|
+
}
|
|
73250
|
+
function buildQueueMap(rows) {
|
|
73251
|
+
const byQueue = /* @__PURE__ */ new Map();
|
|
73252
|
+
for (const row of rows) {
|
|
73253
|
+
const existing = byQueue.get(row.queueName);
|
|
73254
|
+
if (existing) {
|
|
73255
|
+
existing.push(row);
|
|
73256
|
+
} else {
|
|
73257
|
+
byQueue.set(row.queueName, [row]);
|
|
73258
|
+
}
|
|
73259
|
+
}
|
|
73260
|
+
return byQueue;
|
|
73261
|
+
}
|
|
73262
|
+
function endpoints(edges, symKey, fileKey) {
|
|
73263
|
+
const seen = /* @__PURE__ */ new Map();
|
|
73264
|
+
for (const e of edges) {
|
|
73265
|
+
const symbol = e[symKey];
|
|
73266
|
+
if (!symbol) continue;
|
|
73267
|
+
if (!seen.has(symbol)) seen.set(symbol, { symbol, file: e[fileKey] ?? null });
|
|
73268
|
+
}
|
|
73269
|
+
return [...seen.values()];
|
|
73270
|
+
}
|
|
73271
|
+
function topologyToJson(byQueue) {
|
|
73272
|
+
return [...byQueue.entries()].map(([queueName, edges]) => ({
|
|
73273
|
+
queueName,
|
|
73274
|
+
producers: endpoints(edges, "producerSymbol", "producerFile"),
|
|
73275
|
+
workers: endpoints(edges, "workerSymbol", "workerFile")
|
|
73276
|
+
}));
|
|
73277
|
+
}
|
|
73278
|
+
async function gatherLiveState(config, rows, nameFilter) {
|
|
73279
|
+
let renv;
|
|
73280
|
+
try {
|
|
73281
|
+
renv = resolveEnvironment(config);
|
|
73282
|
+
} catch (err) {
|
|
73283
|
+
return { ok: false, error: err.message };
|
|
73284
|
+
}
|
|
73285
|
+
const queueProvider = queueForEnv(renv);
|
|
73286
|
+
if (!queueProvider) return { ok: false, error: "Redis not configured" };
|
|
73287
|
+
try {
|
|
73288
|
+
const health = await queueProvider.health();
|
|
73289
|
+
if (!health.ok) return { ok: false, error: health.detail };
|
|
73290
|
+
const staticNames = new Set(buildQueueMap(rows).keys());
|
|
73291
|
+
let queueNames;
|
|
73292
|
+
if (nameFilter !== void 0) {
|
|
73293
|
+
queueNames = [nameFilter];
|
|
73294
|
+
} else {
|
|
73295
|
+
const discovered = await queueProvider.discoverQueues().catch(() => []);
|
|
73296
|
+
const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
|
|
73297
|
+
queueNames = union2.size > 0 ? [...union2] : void 0;
|
|
73298
|
+
}
|
|
73299
|
+
const state = await queueProvider.analyzeQueues({ queueNames });
|
|
73300
|
+
return {
|
|
73301
|
+
ok: true,
|
|
73302
|
+
prefix: state.prefix,
|
|
73303
|
+
collectedAt: state.collectedAt,
|
|
73304
|
+
queues: state.queues.map((q) => ({ ...q, runtimeOnly: !staticNames.has(q.queueName) }))
|
|
73305
|
+
};
|
|
73306
|
+
} finally {
|
|
73307
|
+
await queueProvider.close().catch(() => {
|
|
73308
|
+
});
|
|
73309
|
+
}
|
|
73310
|
+
}
|
|
73311
|
+
function printTopology(byQueue) {
|
|
73312
|
+
for (const [queueName, edges] of byQueue) {
|
|
73313
|
+
console.log(import_picocolors4.default.bold(queueName));
|
|
73314
|
+
const producerSet = /* @__PURE__ */ new Set();
|
|
73315
|
+
const producerDetails = /* @__PURE__ */ new Map();
|
|
73316
|
+
for (const edge of edges) {
|
|
73317
|
+
if (edge.producerSymbol) {
|
|
73318
|
+
producerSet.add(edge.producerSymbol);
|
|
73319
|
+
if (edge.producerFile) producerDetails.set(edge.producerSymbol, edge.producerFile);
|
|
73320
|
+
}
|
|
73321
|
+
}
|
|
73322
|
+
if (producerSet.size === 0) {
|
|
73323
|
+
console.log(" producers: " + import_picocolors4.default.dim("none"));
|
|
73324
|
+
} else {
|
|
73325
|
+
const list = Array.from(producerSet).map((sym) => {
|
|
73326
|
+
const file = producerDetails.get(sym);
|
|
73327
|
+
return file ? `${sym} (${file})` : sym;
|
|
73328
|
+
}).join(", ");
|
|
73329
|
+
console.log(" producers: " + list);
|
|
73330
|
+
}
|
|
73331
|
+
const workerSet = /* @__PURE__ */ new Set();
|
|
73332
|
+
const workerDetails = /* @__PURE__ */ new Map();
|
|
73333
|
+
for (const edge of edges) {
|
|
73334
|
+
if (edge.workerSymbol) {
|
|
73335
|
+
workerSet.add(edge.workerSymbol);
|
|
73336
|
+
if (edge.workerFile) workerDetails.set(edge.workerSymbol, edge.workerFile);
|
|
73337
|
+
}
|
|
73338
|
+
}
|
|
73339
|
+
if (workerSet.size === 0) {
|
|
73340
|
+
console.log(" workers: " + import_picocolors4.default.dim("none"));
|
|
73341
|
+
} else {
|
|
73342
|
+
const list = Array.from(workerSet).map((sym) => {
|
|
73343
|
+
const file = workerDetails.get(sym);
|
|
73344
|
+
return file ? `${sym} (${file})` : sym;
|
|
73345
|
+
}).join(", ");
|
|
73346
|
+
console.log(" workers: " + list);
|
|
73347
|
+
}
|
|
73348
|
+
console.log("");
|
|
73219
73349
|
}
|
|
73220
|
-
return { checks, passed, total: checks.length };
|
|
73221
73350
|
}
|
|
73222
|
-
|
|
73223
|
-
|
|
73224
|
-
|
|
73225
|
-
|
|
73226
|
-
|
|
73227
|
-
|
|
73228
|
-
|
|
73229
|
-
|
|
73230
|
-
lines.push(` ${s.id} [${s.category}] ${s.title}`);
|
|
73231
|
-
lines.push(` ${s.symptom}`);
|
|
73351
|
+
async function runLiveMode(config, rows, nameFilter) {
|
|
73352
|
+
let renv;
|
|
73353
|
+
try {
|
|
73354
|
+
renv = resolveEnvironment(config);
|
|
73355
|
+
} catch (err) {
|
|
73356
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
73357
|
+
console.log(import_picocolors4.default.yellow(` \u26A0 Cannot resolve environment: ${err.message}`));
|
|
73358
|
+
return;
|
|
73232
73359
|
}
|
|
73233
|
-
|
|
73234
|
-
|
|
73235
|
-
|
|
73236
|
-
|
|
73237
|
-
|
|
73238
|
-
const lines = [];
|
|
73239
|
-
lines.push(`# Training scenario: ${scenario.title}`);
|
|
73240
|
-
lines.push("");
|
|
73241
|
-
lines.push("## Symptom");
|
|
73242
|
-
lines.push(scenario.symptom);
|
|
73243
|
-
lines.push("");
|
|
73244
|
-
lines.push(
|
|
73245
|
-
"Form your own hypothesis before reading on \u2014 then compare it with what Horus found."
|
|
73246
|
-
);
|
|
73247
|
-
lines.push("");
|
|
73248
|
-
const isWeak = evaluation.passed < evaluation.total;
|
|
73249
|
-
if (isWeak) {
|
|
73250
|
-
lines.push(
|
|
73251
|
-
"> **Weak investigation** \u2014 Horus did not surface all expected signals for this scenario."
|
|
73252
|
-
);
|
|
73253
|
-
lines.push(
|
|
73254
|
-
"> This can happen when the hint resolves to a symbol that is not directly connected to the expected runtime/change evidence."
|
|
73360
|
+
const queueProvider = queueForEnv(renv);
|
|
73361
|
+
if (!queueProvider) {
|
|
73362
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
73363
|
+
console.log(
|
|
73364
|
+
import_picocolors4.default.yellow(" \u25CB Redis not configured \u2014 run: ") + import_picocolors4.default.bold("horus connect redis")
|
|
73255
73365
|
);
|
|
73256
|
-
|
|
73366
|
+
return;
|
|
73257
73367
|
}
|
|
73258
|
-
|
|
73259
|
-
|
|
73260
|
-
|
|
73261
|
-
|
|
73262
|
-
|
|
73263
|
-
|
|
73264
|
-
|
|
73265
|
-
lines.push("(none)");
|
|
73266
|
-
} else {
|
|
73267
|
-
for (const h of topHypotheses) {
|
|
73268
|
-
lines.push(`- Verdict: ${h.verdict}`);
|
|
73269
|
-
lines.push(` Confidence: ${h.confidence.toFixed(2)}`);
|
|
73270
|
-
lines.push(` Category: ${h.category}`);
|
|
73271
|
-
lines.push(` ${h.statement}`);
|
|
73368
|
+
let headerPrinted = false;
|
|
73369
|
+
try {
|
|
73370
|
+
const health = await queueProvider.health();
|
|
73371
|
+
if (!health.ok) {
|
|
73372
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
73373
|
+
console.log(import_picocolors4.default.red(` \u2717 Redis unreachable: ${health.detail}`));
|
|
73374
|
+
return;
|
|
73272
73375
|
}
|
|
73273
|
-
|
|
73274
|
-
|
|
73275
|
-
|
|
73276
|
-
|
|
73277
|
-
|
|
73278
|
-
|
|
73279
|
-
|
|
73280
|
-
|
|
73281
|
-
lines.push("");
|
|
73282
|
-
lines.push(`Score: ${evaluation.passed}/${evaluation.total}`);
|
|
73283
|
-
lines.push("");
|
|
73284
|
-
lines.push("## Coaching");
|
|
73285
|
-
lines.push("");
|
|
73286
|
-
for (const tip of scenario.coachingTips) {
|
|
73287
|
-
lines.push(`- ${tip}`);
|
|
73288
|
-
}
|
|
73289
|
-
if (scenario.category === "queue") {
|
|
73290
|
-
const queueBoundaryCheck = evaluation.checks.find(
|
|
73291
|
-
(c) => c.label === "Queue boundary crossing detected"
|
|
73292
|
-
);
|
|
73293
|
-
if (queueBoundaryCheck && !queueBoundaryCheck.ok) {
|
|
73294
|
-
lines.push("");
|
|
73295
|
-
lines.push(
|
|
73296
|
-
"_No queue boundary was detected. The hint likely resolved to a symbol that is not directly connected to a known queue producer or worker. Try re-running with a hint that names a queue worker, producer, or the queue itself._"
|
|
73297
|
-
);
|
|
73376
|
+
const staticNames = new Set(buildQueueMap(rows).keys());
|
|
73377
|
+
let queueNames;
|
|
73378
|
+
if (nameFilter !== void 0) {
|
|
73379
|
+
queueNames = [nameFilter];
|
|
73380
|
+
} else {
|
|
73381
|
+
const discovered = await queueProvider.discoverQueues().catch(() => []);
|
|
73382
|
+
const union2 = /* @__PURE__ */ new Set([...staticNames, ...discovered]);
|
|
73383
|
+
queueNames = union2.size > 0 ? [...union2] : void 0;
|
|
73298
73384
|
}
|
|
73299
|
-
|
|
73300
|
-
|
|
73301
|
-
|
|
73302
|
-
(
|
|
73385
|
+
const state = await queueProvider.analyzeQueues({ queueNames });
|
|
73386
|
+
const collectedAt = new Date(state.collectedAt).toLocaleTimeString();
|
|
73387
|
+
console.log(
|
|
73388
|
+
import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(` \xB7 source: Redis/BullMQ (prefix: ${state.prefix}) \xB7 collected: ${collectedAt}`)
|
|
73303
73389
|
);
|
|
73304
|
-
|
|
73305
|
-
|
|
73306
|
-
|
|
73307
|
-
|
|
73390
|
+
headerPrinted = true;
|
|
73391
|
+
console.log("");
|
|
73392
|
+
if (state.queues.length === 0) {
|
|
73393
|
+
console.log(import_picocolors4.default.dim(" No queues found in Redis."));
|
|
73394
|
+
console.log(
|
|
73395
|
+
import_picocolors4.default.dim(
|
|
73396
|
+
" If queues exist under a custom prefix, set the BullMQ prefix in your connector config."
|
|
73397
|
+
)
|
|
73308
73398
|
);
|
|
73399
|
+
return;
|
|
73400
|
+
}
|
|
73401
|
+
printLiveTable(state.queues, staticNames);
|
|
73402
|
+
} catch (err) {
|
|
73403
|
+
if (!headerPrinted) {
|
|
73404
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
73309
73405
|
}
|
|
73406
|
+
console.log(import_picocolors4.default.red(` \u2717 ${err.message}`));
|
|
73407
|
+
} finally {
|
|
73408
|
+
await queueProvider.close().catch(() => {
|
|
73409
|
+
});
|
|
73310
73410
|
}
|
|
73311
|
-
return lines.join("\n");
|
|
73312
|
-
}
|
|
73313
|
-
|
|
73314
|
-
// ../../packages/engine/src/migrate-report.ts
|
|
73315
|
-
init_cjs_shims();
|
|
73316
|
-
function getBand2(score) {
|
|
73317
|
-
if (score >= 0.85) return "highly-likely";
|
|
73318
|
-
if (score >= 0.65) return "likely";
|
|
73319
|
-
if (score >= 0.4) return "possible";
|
|
73320
|
-
return "observation";
|
|
73321
73411
|
}
|
|
73322
|
-
function
|
|
73323
|
-
|
|
73324
|
-
|
|
73325
|
-
|
|
73326
|
-
|
|
73327
|
-
|
|
73328
|
-
|
|
73329
|
-
|
|
73330
|
-
|
|
73331
|
-
|
|
73332
|
-
|
|
73333
|
-
|
|
73334
|
-
|
|
73335
|
-
|
|
73336
|
-
|
|
73337
|
-
|
|
73338
|
-
|
|
73339
|
-
|
|
73340
|
-
|
|
73341
|
-
|
|
73342
|
-
|
|
73343
|
-
baseScore: typeof cause["baseScore"] === "number" ? cause["baseScore"] : fs3,
|
|
73344
|
-
finalScore: fs3,
|
|
73345
|
-
confidence: typeof cause["confidence"] === "number" ? cause["confidence"] : fs3,
|
|
73346
|
-
band: typeof cause["band"] === "string" ? cause["band"] : getBand2(fs3),
|
|
73347
|
-
explanations: Array.isArray(cause["explanations"]) ? cause["explanations"] : [],
|
|
73348
|
-
...cause["metadata"] !== void 0 ? { metadata: cause["metadata"] } : {}
|
|
73349
|
-
}];
|
|
73350
|
-
}
|
|
73351
|
-
const statement = typeof cause["statement"] === "string" ? cause["statement"] : "";
|
|
73352
|
-
if (!statement) return [];
|
|
73353
|
-
const score = typeof cause["score"] === "number" ? cause["score"] : 0;
|
|
73354
|
-
const evidenceIds = Array.isArray(cause["evidenceIds"]) ? cause["evidenceIds"] : [];
|
|
73355
|
-
return [{
|
|
73356
|
-
id: `cause:legacy:${i}`,
|
|
73357
|
-
title: statement,
|
|
73358
|
-
category: "unknown",
|
|
73359
|
-
sourceEvidenceIds: evidenceIds,
|
|
73360
|
-
affectedNodeIds: [],
|
|
73361
|
-
baseScore: score,
|
|
73362
|
-
finalScore: score,
|
|
73363
|
-
confidence: score,
|
|
73364
|
-
band: getBand2(score),
|
|
73365
|
-
explanations: [],
|
|
73366
|
-
...cause["metadata"] !== void 0 ? { metadata: cause["metadata"] } : {}
|
|
73367
|
-
}];
|
|
73412
|
+
function printLiveTable(queues, staticNames = /* @__PURE__ */ new Set()) {
|
|
73413
|
+
const nameWidth = Math.max(10, ...queues.map((q) => q.queueName.length));
|
|
73414
|
+
const numWidth = 7;
|
|
73415
|
+
const header = " " + "queue".padEnd(nameWidth) + " " + "waiting".padStart(numWidth) + " " + "active".padStart(numWidth) + " " + "failed".padStart(numWidth) + " " + "delayed".padStart(numWidth) + " " + "paused".padStart(numWidth);
|
|
73416
|
+
console.log(import_picocolors4.default.dim(header));
|
|
73417
|
+
console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(header.length - 2)));
|
|
73418
|
+
for (const q of queues) {
|
|
73419
|
+
const hasIssue = q.failed > 0 || q.waiting > 100 || q.delayed > 50 || q.isPaused;
|
|
73420
|
+
const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
|
|
73421
|
+
const row = " " + q.queueName.padEnd(nameWidth) + " " + String(q.waiting).padStart(numWidth) + " " + String(q.active).padStart(numWidth) + " " + String(q.failed).padStart(numWidth) + " " + String(q.delayed).padStart(numWidth) + " " + (q.isPaused ? import_picocolors4.default.yellow("paused") : String(q.paused).padStart(numWidth));
|
|
73422
|
+
console.log(color(row));
|
|
73423
|
+
if (!staticNames.has(q.queueName)) {
|
|
73424
|
+
console.log(import_picocolors4.default.dim(" runtime-only \xB7 no static producer/worker mapping"));
|
|
73425
|
+
}
|
|
73426
|
+
if (q.oldestWaitingMs !== void 0) {
|
|
73427
|
+
const age = formatAge(q.oldestWaitingMs);
|
|
73428
|
+
console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
|
|
73429
|
+
}
|
|
73430
|
+
if (q.failedBreakdown && q.failedBreakdown.length > 0) {
|
|
73431
|
+
for (const { reason, count } of q.failedBreakdown.slice(0, 3)) {
|
|
73432
|
+
console.log(import_picocolors4.default.red(` \u2717 [${count}x] ${reason}`));
|
|
73368
73433
|
}
|
|
73369
|
-
|
|
73434
|
+
}
|
|
73370
73435
|
}
|
|
73371
|
-
|
|
73436
|
+
console.log("");
|
|
73437
|
+
}
|
|
73438
|
+
function formatAge(ms) {
|
|
73439
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
73440
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
73441
|
+
if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
|
|
73442
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
73372
73443
|
}
|
|
73373
73444
|
|
|
73445
|
+
// ../../packages/cli/src/commands/investigate.ts
|
|
73446
|
+
init_cjs_shims();
|
|
73447
|
+
var import_picocolors5 = __toESM(require_picocolors(), 1);
|
|
73448
|
+
|
|
73374
73449
|
// ../../packages/ai/src/index.ts
|
|
73375
73450
|
init_cjs_shims();
|
|
73376
73451
|
|
|
@@ -73452,8 +73527,8 @@ function validateNarrative(output, input) {
|
|
|
73452
73527
|
if (!output.why || output.why.trim().length === 0) {
|
|
73453
73528
|
errors2.push("NarrativeOutput.why is required and must be non-empty");
|
|
73454
73529
|
}
|
|
73455
|
-
if (!Array.isArray(output.whereNext)
|
|
73456
|
-
|
|
73530
|
+
if (!Array.isArray(output.whereNext)) {
|
|
73531
|
+
output.whereNext = [];
|
|
73457
73532
|
}
|
|
73458
73533
|
for (const citation of output.citations) {
|
|
73459
73534
|
if (!knownIds.has(citation.evidenceId)) {
|
|
@@ -73536,9 +73611,26 @@ async function renderNarrative(input, opts = {}) {
|
|
|
73536
73611
|
try {
|
|
73537
73612
|
const output = await opts.provider.render(safeInput, { confidenceCeiling: ceiling });
|
|
73538
73613
|
const validation = validateNarrative(output, safeInput);
|
|
73539
|
-
if (validation.valid) {
|
|
73614
|
+
if (validation.valid && output.degraded !== true) {
|
|
73540
73615
|
return { output, fromProvider: true };
|
|
73541
73616
|
}
|
|
73617
|
+
const hasText = (output.what?.trim()?.length ?? 0) > 0 || (output.why?.trim()?.length ?? 0) > 0;
|
|
73618
|
+
if (hasText) {
|
|
73619
|
+
const degradedOutput = {
|
|
73620
|
+
what: output.what ?? "",
|
|
73621
|
+
why: output.why ?? "",
|
|
73622
|
+
whereNext: Array.isArray(output.whereNext) ? output.whereNext : [],
|
|
73623
|
+
citations: [],
|
|
73624
|
+
confidence: Math.min(output.confidence ?? input.reportConfidence, ceiling),
|
|
73625
|
+
degraded: true
|
|
73626
|
+
};
|
|
73627
|
+
return {
|
|
73628
|
+
output: degradedOutput,
|
|
73629
|
+
fromProvider: false,
|
|
73630
|
+
degraded: true,
|
|
73631
|
+
validationErrors: validation.errors
|
|
73632
|
+
};
|
|
73633
|
+
}
|
|
73542
73634
|
return {
|
|
73543
73635
|
output: deterministicFallback(input),
|
|
73544
73636
|
fromProvider: false,
|
|
@@ -73727,7 +73819,8 @@ function rawNarrativeFallback(raw, input, ceiling) {
|
|
|
73727
73819
|
why: cleaned.slice(0, 4e3),
|
|
73728
73820
|
whereNext: [],
|
|
73729
73821
|
citations: [],
|
|
73730
|
-
confidence: Math.min(input.reportConfidence, ceiling)
|
|
73822
|
+
confidence: Math.min(input.reportConfidence, ceiling),
|
|
73823
|
+
degraded: true
|
|
73731
73824
|
};
|
|
73732
73825
|
}
|
|
73733
73826
|
function parseOutput(raw, input, ceiling) {
|
|
@@ -73993,7 +74086,7 @@ async function runInvestigate(hint, opts) {
|
|
|
73993
74086
|
}
|
|
73994
74087
|
);
|
|
73995
74088
|
const format = opts.json ? "json" : opts.format ?? "text";
|
|
73996
|
-
const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) :
|
|
74089
|
+
const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport(report);
|
|
73997
74090
|
console.log(rendered);
|
|
73998
74091
|
if (opts.ai && format !== "json") {
|
|
73999
74092
|
const ai = resolveAiSettings(config);
|
|
@@ -74004,11 +74097,11 @@ async function runInvestigate(hint, opts) {
|
|
|
74004
74097
|
});
|
|
74005
74098
|
console.log(import_picocolors5.default.dim(`[ai] model: ${model}`));
|
|
74006
74099
|
const narrativeInput = buildNarrativeInput(report);
|
|
74007
|
-
const { output, fromProvider, validationErrors } = await renderNarrative(
|
|
74008
|
-
|
|
74009
|
-
|
|
74010
|
-
|
|
74011
|
-
|
|
74100
|
+
const { output, fromProvider, degraded, validationErrors } = await renderNarrative(
|
|
74101
|
+
narrativeInput,
|
|
74102
|
+
{ provider }
|
|
74103
|
+
);
|
|
74104
|
+
if (fromProvider) {
|
|
74012
74105
|
const stored = narrativeOutputToStoredJudgment(output, "anthropic");
|
|
74013
74106
|
report.aiJudgment = stored;
|
|
74014
74107
|
try {
|
|
@@ -74016,6 +74109,26 @@ async function runInvestigate(hint, opts) {
|
|
|
74016
74109
|
} catch {
|
|
74017
74110
|
}
|
|
74018
74111
|
renderStoredAIJudgment(stored);
|
|
74112
|
+
} else if (degraded) {
|
|
74113
|
+
if (validationErrors && validationErrors.length > 0) {
|
|
74114
|
+
console.error(import_picocolors5.default.yellow(`[ai] structured validation incomplete \u2014 showing raw AI narrative`));
|
|
74115
|
+
}
|
|
74116
|
+
console.log("");
|
|
74117
|
+
console.log(import_picocolors5.default.dim("\u2500".repeat(60)));
|
|
74118
|
+
console.log(import_picocolors5.default.bold("AI narrative ") + import_picocolors5.default.yellow("(unstructured \u2014 not validated)"));
|
|
74119
|
+
if (output.what?.trim()) {
|
|
74120
|
+
console.log(import_picocolors5.default.bold("\nWhat:"), output.what.trim());
|
|
74121
|
+
}
|
|
74122
|
+
if (output.why?.trim()) {
|
|
74123
|
+
console.log(import_picocolors5.default.bold("\nWhy:"), output.why.trim());
|
|
74124
|
+
}
|
|
74125
|
+
if (output.whereNext.length > 0) {
|
|
74126
|
+
console.log(import_picocolors5.default.bold("\nNext:"));
|
|
74127
|
+
for (const a of output.whereNext) console.log(` - ${a}`);
|
|
74128
|
+
}
|
|
74129
|
+
} else {
|
|
74130
|
+
const reason = classifyAIFailure(validationErrors?.[0]);
|
|
74131
|
+
console.error(import_picocolors5.default.yellow(`[ai] fallback to deterministic \u2014 ${reason}`));
|
|
74019
74132
|
}
|
|
74020
74133
|
}
|
|
74021
74134
|
} finally {
|
|
@@ -74333,7 +74446,7 @@ async function runReplay(id, opts) {
|
|
|
74333
74446
|
}
|
|
74334
74447
|
const report = migrateReport(row.report);
|
|
74335
74448
|
const fmt = opts.format ?? "text";
|
|
74336
|
-
const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) :
|
|
74449
|
+
const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) : renderReport(report);
|
|
74337
74450
|
console.log(out);
|
|
74338
74451
|
if (opts.ai && fmt !== "json") {
|
|
74339
74452
|
if (report.aiJudgment && !opts.refreshAi) {
|
|
@@ -74342,8 +74455,20 @@ async function runReplay(id, opts) {
|
|
|
74342
74455
|
} else {
|
|
74343
74456
|
const narrativeInput = buildNarrativeInput(report);
|
|
74344
74457
|
const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
|
|
74345
|
-
const { output, fromProvider, validationErrors } = await renderNarrative(
|
|
74346
|
-
|
|
74458
|
+
const { output, fromProvider, degraded, validationErrors } = await renderNarrative(
|
|
74459
|
+
narrativeInput,
|
|
74460
|
+
{ provider }
|
|
74461
|
+
);
|
|
74462
|
+
if (!fromProvider && degraded) {
|
|
74463
|
+
if (validationErrors?.length) {
|
|
74464
|
+
console.error(import_picocolors13.default.yellow("[ai] structured validation incomplete \u2014 showing raw AI narrative"));
|
|
74465
|
+
}
|
|
74466
|
+
console.log("");
|
|
74467
|
+
console.log(import_picocolors13.default.dim("\u2500".repeat(60)));
|
|
74468
|
+
console.log(import_picocolors13.default.bold("AI narrative ") + import_picocolors13.default.yellow("(unstructured \u2014 not validated)"));
|
|
74469
|
+
if (output.what?.trim()) console.log(import_picocolors13.default.bold("\nWhat:"), output.what.trim());
|
|
74470
|
+
if (output.why?.trim()) console.log(import_picocolors13.default.bold("\nWhy:"), output.why.trim());
|
|
74471
|
+
} else if (!fromProvider) {
|
|
74347
74472
|
console.error(import_picocolors13.default.yellow("[ai] Provider unavailable \u2014 deterministic output shown above."));
|
|
74348
74473
|
if (validationErrors?.length) {
|
|
74349
74474
|
console.error(import_picocolors13.default.dim(` ${validationErrors[0]}`));
|
|
@@ -74450,8 +74575,18 @@ async function runPostmortem(id, opts) {
|
|
|
74450
74575
|
} else {
|
|
74451
74576
|
const narrativeInput = buildNarrativeInput(report);
|
|
74452
74577
|
const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
|
|
74453
|
-
const { output, fromProvider, validationErrors } = await renderNarrative(
|
|
74454
|
-
|
|
74578
|
+
const { output, fromProvider, degraded, validationErrors } = await renderNarrative(
|
|
74579
|
+
narrativeInput,
|
|
74580
|
+
{ provider }
|
|
74581
|
+
);
|
|
74582
|
+
if (!fromProvider && degraded) {
|
|
74583
|
+
content += "\n\n## AI Summary (unstructured \u2014 not validated)\n\n";
|
|
74584
|
+
if (output.what?.trim()) content += `**What happened:** ${output.what.trim()}
|
|
74585
|
+
|
|
74586
|
+
`;
|
|
74587
|
+
if (output.why?.trim()) content += `**Why:** ${output.why.trim()}
|
|
74588
|
+
`;
|
|
74589
|
+
} else if (!fromProvider) {
|
|
74455
74590
|
content += `
|
|
74456
74591
|
|
|
74457
74592
|
## AI Summary
|
|
@@ -75646,9 +75781,7 @@ ${import_picocolors27.default.red("\u2717 Anthropic key check failed:")} ${probe
|
|
|
75646
75781
|
console.log(`
|
|
75647
75782
|
${import_picocolors27.default.green("\u2713")} anthropic ${import_picocolors27.default.dim(`(${probe2.detail})`)}`);
|
|
75648
75783
|
}
|
|
75649
|
-
|
|
75650
|
-
if (opts.model) anthropic["model"] = opts.model;
|
|
75651
|
-
aiBlock["anthropic"] = anthropic;
|
|
75784
|
+
if (opts.model) aiBlock["anthropic"] = { model: opts.model };
|
|
75652
75785
|
storedKey = apiKey;
|
|
75653
75786
|
} else {
|
|
75654
75787
|
if (!installed.includes(provider)) {
|
|
@@ -75670,14 +75803,21 @@ ${import_picocolors27.default.green("\u2713")} ${desc2?.displayName ?? provider}
|
|
|
75670
75803
|
const file = discoverLocalConfig(cwd) ? readLocalConfig(configPath) : { version: 1, project: { name: root.split("/").pop() ?? "project", repositories: [{ name: root.split("/").pop() ?? "project", path: root }], environments: [{ name: "production", readOnly: true, connectors: {} }] } };
|
|
75671
75804
|
file.ai = aiBlock;
|
|
75672
75805
|
writeLocalConfig(root, file);
|
|
75806
|
+
ensureProjectGitignore(root);
|
|
75807
|
+
let secretsPathStr;
|
|
75673
75808
|
if (storedKey !== void 0) {
|
|
75674
|
-
|
|
75675
|
-
|
|
75809
|
+
const existing = readLocalSecrets(root);
|
|
75810
|
+
secretsPathStr = writeLocalSecrets(root, {
|
|
75811
|
+
...existing,
|
|
75812
|
+
anthropic: { ...existing.anthropic ?? {}, apiKey: storedKey }
|
|
75813
|
+
});
|
|
75676
75814
|
}
|
|
75677
75815
|
console.log(`
|
|
75678
75816
|
${import_picocolors27.default.green("\u2713")} ${import_picocolors27.default.bold("ai")} provider saved \u2192 ${import_picocolors27.default.dim(configPath)}`);
|
|
75679
75817
|
console.log(import_picocolors27.default.dim(` provider: ${provider}`));
|
|
75680
|
-
if (storedKey
|
|
75818
|
+
if (storedKey && secretsPathStr) {
|
|
75819
|
+
console.log(import_picocolors27.default.dim(` anthropic key: ${redactKey(storedKey)} \u2192 ${secretsPathStr} (gitignored)`));
|
|
75820
|
+
}
|
|
75681
75821
|
console.log(import_picocolors27.default.dim(' run: horus investigate "<hint>" --ai'));
|
|
75682
75822
|
return 0;
|
|
75683
75823
|
} catch (err) {
|
|
@@ -76840,7 +76980,7 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
76840
76980
|
}
|
|
76841
76981
|
if (source === "config") {
|
|
76842
76982
|
write(
|
|
76843
|
-
` ${import_picocolors32.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors32.default.green("configured (.horus
|
|
76983
|
+
` ${import_picocolors32.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors32.default.green("configured (local .horus config)")}`
|
|
76844
76984
|
);
|
|
76845
76985
|
} else if (source === "env") {
|
|
76846
76986
|
write(
|