@merittdev/horus 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +2561 -475
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -16235,9 +16235,9 @@ var require_common2 = __commonJS({
|
|
|
16235
16235
|
};
|
|
16236
16236
|
exports2.Batch = Batch;
|
|
16237
16237
|
var BulkWriteResult = class _BulkWriteResult {
|
|
16238
|
-
static generateIdMap(
|
|
16238
|
+
static generateIdMap(ids2) {
|
|
16239
16239
|
const idMap = {};
|
|
16240
|
-
for (const doc of
|
|
16240
|
+
for (const doc of ids2) {
|
|
16241
16241
|
idMap[doc.index] = doc._id;
|
|
16242
16242
|
}
|
|
16243
16243
|
return idMap;
|
|
@@ -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 = "0.1.
|
|
50317
|
+
var HORUS_VERSION = true ? "0.1.3" : "dev";
|
|
50318
50318
|
var PINNED_AXON_VERSION = "1.0.1";
|
|
50319
50319
|
var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
|
|
50320
50320
|
|
|
@@ -54530,7 +54530,9 @@ var repositorySchema = external_exports.object({
|
|
|
54530
54530
|
});
|
|
54531
54531
|
var connectorsSchema = external_exports.object({
|
|
54532
54532
|
elasticsearch: external_exports.object({
|
|
54533
|
-
indexPattern: external_exports.string(),
|
|
54533
|
+
indexPattern: external_exports.string().optional(),
|
|
54534
|
+
/** Multiple index patterns — joined with ',' when resolving. Takes precedence over indexPattern. */
|
|
54535
|
+
indexPatterns: external_exports.array(external_exports.string()).optional(),
|
|
54534
54536
|
serviceName: external_exports.string().optional(),
|
|
54535
54537
|
/**
|
|
54536
54538
|
* Log schema preset. Controls which Elasticsearch field names Horus
|
|
@@ -54585,6 +54587,8 @@ var connectorsSchema = external_exports.object({
|
|
|
54585
54587
|
}).optional(),
|
|
54586
54588
|
grafana: external_exports.object({
|
|
54587
54589
|
dashboard: external_exports.string().optional(),
|
|
54590
|
+
/** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
|
|
54591
|
+
dashboards: external_exports.array(external_exports.string()).optional(),
|
|
54588
54592
|
/** Direct URL value (takes priority over urlEnv). */
|
|
54589
54593
|
url: external_exports.string().optional(),
|
|
54590
54594
|
/** Name of the env var holding the Grafana base URL. Defaults to "GRAFANA_URL". */
|
|
@@ -54713,11 +54717,13 @@ function resolveEnvironment(config, opts) {
|
|
|
54713
54717
|
if (c.elasticsearch !== void 0) {
|
|
54714
54718
|
const es = c.elasticsearch;
|
|
54715
54719
|
const url = es.url ?? process.env[es.urlEnv ?? "ES_URL"] ?? "";
|
|
54720
|
+
const effectivePattern = es.indexPatterns?.join(",") ?? es.indexPattern ?? "";
|
|
54716
54721
|
resolved.elasticsearch = {
|
|
54717
54722
|
url,
|
|
54718
54723
|
username: es.username ?? process.env[es.usernameEnv ?? "ES_USERNAME"],
|
|
54719
54724
|
password: es.password ?? process.env[es.passwordEnv ?? "ES_PASSWORD"],
|
|
54720
|
-
indexPattern:
|
|
54725
|
+
indexPattern: effectivePattern,
|
|
54726
|
+
...es.indexPatterns !== void 0 ? { indexPatterns: es.indexPatterns } : {},
|
|
54721
54727
|
serviceName: es.serviceName,
|
|
54722
54728
|
preset: es.preset,
|
|
54723
54729
|
...es.fields !== void 0 ? { fields: es.fields } : {}
|
|
@@ -54737,7 +54743,8 @@ function resolveEnvironment(config, opts) {
|
|
|
54737
54743
|
url: g.url ?? process.env[g.urlEnv ?? "GRAFANA_URL"],
|
|
54738
54744
|
username: g.username ?? process.env[g.usernameEnv ?? "GRAFANA_USER"],
|
|
54739
54745
|
password: g.password ?? process.env[g.passwordEnv ?? "GRAFANA_PASSWORD"],
|
|
54740
|
-
dashboard: g.dashboard
|
|
54746
|
+
dashboard: g.dashboard,
|
|
54747
|
+
...g.dashboards !== void 0 ? { dashboards: g.dashboards } : {}
|
|
54741
54748
|
};
|
|
54742
54749
|
}
|
|
54743
54750
|
if (c.redis !== void 0) {
|
|
@@ -54763,7 +54770,7 @@ var CONFIG_EXAMPLES = {
|
|
|
54763
54770
|
"projects.*.repositories": 'e.g. [{ name: "my-api", path: "/path/to/repo" }]',
|
|
54764
54771
|
"projects.*.repositories.*.name": 'e.g. name: "my-api"',
|
|
54765
54772
|
"projects.*.repositories.*.path": 'e.g. path: "/absolute/path/to/repo"',
|
|
54766
|
-
"projects.*.repositories.*.source.hostUrl": 'e.g. "http://127.0.0.1:8420" (start one with:
|
|
54773
|
+
"projects.*.repositories.*.source.hostUrl": 'e.g. "http://127.0.0.1:8420" (start one with: horus index)',
|
|
54767
54774
|
"projects.*.repositories.*.axon.hostUrl": 'e.g. "http://127.0.0.1:8420" (deprecated: use source.hostUrl instead)',
|
|
54768
54775
|
"projects.*.environments": 'e.g. [{ name: "production", connectors: {} }]',
|
|
54769
54776
|
"projects.*.environments.*.name": 'e.g. name: "production"',
|
|
@@ -55046,17 +55053,21 @@ var import_node_fs3 = require("fs");
|
|
|
55046
55053
|
var import_node_path3 = require("path");
|
|
55047
55054
|
var import_node_net = require("net");
|
|
55048
55055
|
var exec = (0, import_node_util.promisify)(import_node_child_process2.execFile);
|
|
55049
|
-
|
|
55056
|
+
var SOURCE_BINARY = "horus-source";
|
|
55057
|
+
async function resolveSourceBin() {
|
|
55050
55058
|
try {
|
|
55051
|
-
await exec(
|
|
55052
|
-
return
|
|
55059
|
+
await exec(SOURCE_BINARY, ["--version"], { timeout: 5e3 });
|
|
55060
|
+
return SOURCE_BINARY;
|
|
55053
55061
|
} catch {
|
|
55054
|
-
return
|
|
55062
|
+
return null;
|
|
55055
55063
|
}
|
|
55056
55064
|
}
|
|
55065
|
+
async function axonAvailable() {
|
|
55066
|
+
return await resolveSourceBin() !== null;
|
|
55067
|
+
}
|
|
55057
55068
|
async function getAxonVersion() {
|
|
55058
55069
|
try {
|
|
55059
|
-
const { stdout } = await exec(
|
|
55070
|
+
const { stdout } = await exec(SOURCE_BINARY, ["--version"], { timeout: 5e3 });
|
|
55060
55071
|
const match = stdout.trim().match(/(\d+\.\d+\.\d+)/);
|
|
55061
55072
|
return match?.[1] ?? null;
|
|
55062
55073
|
} catch {
|
|
@@ -55064,10 +55075,12 @@ async function getAxonVersion() {
|
|
|
55064
55075
|
}
|
|
55065
55076
|
}
|
|
55066
55077
|
function isAnalyzed(root) {
|
|
55067
|
-
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".axon"));
|
|
55078
|
+
return (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".horus", "source")) || (0, import_node_fs3.existsSync)((0, import_node_path3.join)(root, ".axon"));
|
|
55068
55079
|
}
|
|
55069
55080
|
async function analyzeRepo(root) {
|
|
55070
|
-
|
|
55081
|
+
const bin = await resolveSourceBin();
|
|
55082
|
+
if (!bin) throw new Error("horus-source not found on PATH. Install it: pip install horus-source");
|
|
55083
|
+
await exec(bin, ["analyze", "."], {
|
|
55071
55084
|
cwd: root,
|
|
55072
55085
|
timeout: 9e5,
|
|
55073
55086
|
maxBuffer: 64 * 1024 * 1024
|
|
@@ -55119,11 +55132,18 @@ function startHost(root, port) {
|
|
|
55119
55132
|
(0, import_node_fs3.mkdirSync)((0, import_node_path3.join)(root, ".horus"), { recursive: true });
|
|
55120
55133
|
const logPath = (0, import_node_path3.join)(root, ".horus", "source-host.log");
|
|
55121
55134
|
const fd = (0, import_node_fs3.openSync)(logPath, "a");
|
|
55122
|
-
const child = (0, import_node_child_process2.spawn)(
|
|
55135
|
+
const child = (0, import_node_child_process2.spawn)(SOURCE_BINARY, ["host", "--port", String(port)], {
|
|
55123
55136
|
cwd: root,
|
|
55124
55137
|
detached: true,
|
|
55125
55138
|
stdio: ["ignore", fd, fd]
|
|
55126
55139
|
});
|
|
55140
|
+
child.on("error", (err) => {
|
|
55141
|
+
if (err.code === "ENOENT") {
|
|
55142
|
+
process.stderr.write(
|
|
55143
|
+
"\nhorus-source not found on PATH. Install it: pip install horus-source\n"
|
|
55144
|
+
);
|
|
55145
|
+
}
|
|
55146
|
+
});
|
|
55127
55147
|
if (child.pid !== void 0) {
|
|
55128
55148
|
const record = {
|
|
55129
55149
|
pid: child.pid,
|
|
@@ -55202,12 +55222,37 @@ var AxonCodeProvider = class {
|
|
|
55202
55222
|
const h = await this.client.health();
|
|
55203
55223
|
return {
|
|
55204
55224
|
ok: h.ok,
|
|
55205
|
-
detail: h.ok ? "
|
|
55225
|
+
detail: h.ok ? "Source intelligence host responded " + h.status : "Source intelligence host unreachable"
|
|
55206
55226
|
};
|
|
55207
55227
|
}
|
|
55208
55228
|
async searchSymbols(query, limit = 10) {
|
|
55209
|
-
const
|
|
55210
|
-
|
|
55229
|
+
const E = this.escapeId(query);
|
|
55230
|
+
const exactQuery = `MATCH (n) WHERE toLower(n.name) = toLower("${E}") AND NOT n:File RETURN n.id, n.name, n.file_path LIMIT ${limit}`;
|
|
55231
|
+
const [exactRows, semanticRes] = await Promise.all([
|
|
55232
|
+
this.rows(exactQuery).catch(() => []),
|
|
55233
|
+
this.client.search(query, limit)
|
|
55234
|
+
]);
|
|
55235
|
+
const exactSymbols = exactRows.map((row) => ({
|
|
55236
|
+
id: String(row[0] ?? ""),
|
|
55237
|
+
name: String(row[1] ?? ""),
|
|
55238
|
+
filePath: String(row[2] ?? "")
|
|
55239
|
+
}));
|
|
55240
|
+
const exactIds = new Set(exactSymbols.map((s) => s.id));
|
|
55241
|
+
const ql = query.toLowerCase();
|
|
55242
|
+
const semantic = semanticRes.filter((r) => !exactIds.has(r.nodeId)).map((r) => {
|
|
55243
|
+
const nl = r.name.toLowerCase();
|
|
55244
|
+
const isFile = r.label === "File";
|
|
55245
|
+
let rank = 0;
|
|
55246
|
+
if (!isFile && (nl.includes(ql) || ql.includes(nl))) rank = 2;
|
|
55247
|
+
else if (isFile && (nl.includes(ql) || ql.includes(nl))) rank = 1;
|
|
55248
|
+
return { r, rank };
|
|
55249
|
+
});
|
|
55250
|
+
semantic.sort((a, b2) => b2.rank - a.rank || b2.r.score - a.r.score);
|
|
55251
|
+
const combined = [
|
|
55252
|
+
...exactSymbols,
|
|
55253
|
+
...semantic.map(({ r }) => ({ id: r.nodeId, name: r.name, filePath: r.filePath }))
|
|
55254
|
+
];
|
|
55255
|
+
return combined.slice(0, limit);
|
|
55211
55256
|
}
|
|
55212
55257
|
async context(symbolId) {
|
|
55213
55258
|
const E = this.escapeId(symbolId);
|
|
@@ -55406,6 +55451,61 @@ var ElasticsearchClient = class {
|
|
|
55406
55451
|
return { ok: false, detail: err.message };
|
|
55407
55452
|
}
|
|
55408
55453
|
}
|
|
55454
|
+
/**
|
|
55455
|
+
* Discover available index names. Resolution order:
|
|
55456
|
+
* 1. Data streams (modern ILM — clean names without date suffixes)
|
|
55457
|
+
* 2. Aliases (user-defined names mapping to one or more indices)
|
|
55458
|
+
* 3. Raw concrete indices (fallback for legacy clusters)
|
|
55459
|
+
* System entries (starting with '.') are always filtered out.
|
|
55460
|
+
* Returns [] on any error so callers can gracefully fall back.
|
|
55461
|
+
*/
|
|
55462
|
+
async listIndices(signal) {
|
|
55463
|
+
const results = /* @__PURE__ */ new Set();
|
|
55464
|
+
try {
|
|
55465
|
+
const dsRes = await this.request("GET", "/_data_stream", void 0, signal);
|
|
55466
|
+
const streams = dsRes["data_streams"];
|
|
55467
|
+
if (Array.isArray(streams)) {
|
|
55468
|
+
for (const s of streams) {
|
|
55469
|
+
const name = String(s["name"] ?? "");
|
|
55470
|
+
if (name && !name.startsWith(".")) results.add(name);
|
|
55471
|
+
}
|
|
55472
|
+
}
|
|
55473
|
+
} catch {
|
|
55474
|
+
}
|
|
55475
|
+
try {
|
|
55476
|
+
const aliasRes = await this.request(
|
|
55477
|
+
"GET",
|
|
55478
|
+
"/_cat/aliases?format=json&h=alias&s=alias",
|
|
55479
|
+
void 0,
|
|
55480
|
+
signal
|
|
55481
|
+
);
|
|
55482
|
+
if (Array.isArray(aliasRes)) {
|
|
55483
|
+
for (const a of aliasRes) {
|
|
55484
|
+
const alias = String(a["alias"] ?? "");
|
|
55485
|
+
if (alias && !alias.startsWith(".")) results.add(alias);
|
|
55486
|
+
}
|
|
55487
|
+
}
|
|
55488
|
+
} catch {
|
|
55489
|
+
}
|
|
55490
|
+
if (results.size === 0) {
|
|
55491
|
+
try {
|
|
55492
|
+
const idxRes = await this.request(
|
|
55493
|
+
"GET",
|
|
55494
|
+
"/_cat/indices?format=json&h=index&s=index&expand_wildcards=open",
|
|
55495
|
+
void 0,
|
|
55496
|
+
signal
|
|
55497
|
+
);
|
|
55498
|
+
if (Array.isArray(idxRes)) {
|
|
55499
|
+
for (const r of idxRes) {
|
|
55500
|
+
const name = String(r["index"] ?? "");
|
|
55501
|
+
if (name && !name.startsWith(".")) results.add(name);
|
|
55502
|
+
}
|
|
55503
|
+
}
|
|
55504
|
+
} catch {
|
|
55505
|
+
}
|
|
55506
|
+
}
|
|
55507
|
+
return [...results].sort();
|
|
55508
|
+
}
|
|
55409
55509
|
};
|
|
55410
55510
|
|
|
55411
55511
|
// ../../packages/connectors/src/elasticsearch/provider.ts
|
|
@@ -56380,13 +56480,34 @@ function sanitizeExpr(expr) {
|
|
|
56380
56480
|
if (replaced.includes("$")) return null;
|
|
56381
56481
|
return replaced;
|
|
56382
56482
|
}
|
|
56483
|
+
function extractHintTokens(hint) {
|
|
56484
|
+
const camelSplit = hint.replace(/([a-z])([A-Z])/g, "$1 $2");
|
|
56485
|
+
const acronymSplit = camelSplit.replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
56486
|
+
return acronymSplit.toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3);
|
|
56487
|
+
}
|
|
56383
56488
|
function panelMatchesHint(p, hint) {
|
|
56384
56489
|
if (hint === "") return true;
|
|
56385
|
-
const tokens = hint
|
|
56490
|
+
const tokens = extractHintTokens(hint);
|
|
56386
56491
|
if (tokens.length === 0) return true;
|
|
56387
56492
|
const haystack = p.title.toLowerCase() + " " + p.exprs.map((e) => e.toLowerCase()).join(" ");
|
|
56388
56493
|
return tokens.some((tok) => haystack.includes(tok));
|
|
56389
56494
|
}
|
|
56495
|
+
function findMatchSource(p, hint) {
|
|
56496
|
+
if (hint === "") return null;
|
|
56497
|
+
const tokens = extractHintTokens(hint);
|
|
56498
|
+
if (tokens.length === 0) return null;
|
|
56499
|
+
if (tokens.some((tok) => p.title.toLowerCase().includes(tok))) return "panel-title";
|
|
56500
|
+
if (tokens.some((tok) => p.exprs.some((e) => e.toLowerCase().includes(tok)))) {
|
|
56501
|
+
return "query-text";
|
|
56502
|
+
}
|
|
56503
|
+
return null;
|
|
56504
|
+
}
|
|
56505
|
+
function findingLabelsMatchHint(labels, hint) {
|
|
56506
|
+
const tokens = extractHintTokens(hint);
|
|
56507
|
+
if (tokens.length === 0) return true;
|
|
56508
|
+
const haystack = Object.values(labels).join(" ").toLowerCase();
|
|
56509
|
+
return tokens.some((tok) => haystack.includes(tok));
|
|
56510
|
+
}
|
|
56390
56511
|
|
|
56391
56512
|
// ../../packages/connectors/src/grafana/analyze.ts
|
|
56392
56513
|
init_cjs_shims();
|
|
@@ -56498,7 +56619,8 @@ var GrafanaMetricsProvider = class {
|
|
|
56498
56619
|
* Each panel is tagged with the dashboardUid it came from.
|
|
56499
56620
|
*/
|
|
56500
56621
|
async findPanels(hint, signal) {
|
|
56501
|
-
const
|
|
56622
|
+
const uids = this.opts.dashboardUids;
|
|
56623
|
+
const dashboards = uids !== void 0 && uids.length > 0 ? uids.map((uid2) => ({ uid: uid2, title: uid2 })) : await this.client.searchDashboards(void 0, signal);
|
|
56502
56624
|
const allPanels = [];
|
|
56503
56625
|
for (const dash of dashboards) {
|
|
56504
56626
|
signal?.throwIfAborted();
|
|
@@ -56521,48 +56643,63 @@ var GrafanaMetricsProvider = class {
|
|
|
56521
56643
|
/**
|
|
56522
56644
|
* Discover relevant panels, query current + baseline windows for each expr,
|
|
56523
56645
|
* and return MetricFindings (including "none" anomalies for completeness).
|
|
56646
|
+
*
|
|
56647
|
+
* Two-pass hint matching:
|
|
56648
|
+
* 1. Filter panels by title / PromQL expression (fast, cheap).
|
|
56649
|
+
* 2. If no panels match on pass 1, query ALL panels and filter the returned
|
|
56650
|
+
* series by their label values (source, topic, queue, etc.).
|
|
56524
56651
|
*/
|
|
56525
56652
|
async analyze(opts) {
|
|
56526
56653
|
const { signal } = opts;
|
|
56527
56654
|
const step = opts.step ?? this.opts.defaultStep;
|
|
56528
56655
|
const windowSecs = opts.to - opts.from;
|
|
56529
|
-
const
|
|
56530
|
-
const
|
|
56531
|
-
|
|
56532
|
-
|
|
56533
|
-
for (const rawExpr of panel.exprs) {
|
|
56656
|
+
const hint = opts.hint;
|
|
56657
|
+
const queryPanels = async (panels) => {
|
|
56658
|
+
const results = [];
|
|
56659
|
+
for (const panel of panels) {
|
|
56534
56660
|
signal?.throwIfAborted();
|
|
56535
|
-
const
|
|
56536
|
-
|
|
56537
|
-
|
|
56538
|
-
const
|
|
56539
|
-
|
|
56540
|
-
|
|
56541
|
-
|
|
56542
|
-
expr,
|
|
56543
|
-
|
|
56544
|
-
|
|
56545
|
-
|
|
56546
|
-
|
|
56547
|
-
|
|
56548
|
-
|
|
56549
|
-
|
|
56550
|
-
|
|
56551
|
-
|
|
56552
|
-
|
|
56553
|
-
|
|
56554
|
-
|
|
56555
|
-
|
|
56556
|
-
|
|
56557
|
-
|
|
56558
|
-
|
|
56559
|
-
|
|
56560
|
-
|
|
56561
|
-
|
|
56661
|
+
const panelMatchSrc = hint !== void 0 && hint !== "" ? findMatchSource(panel, hint) : null;
|
|
56662
|
+
for (const rawExpr of panel.exprs) {
|
|
56663
|
+
signal?.throwIfAborted();
|
|
56664
|
+
const expr = sanitizeExpr(rawExpr);
|
|
56665
|
+
if (expr === null) continue;
|
|
56666
|
+
try {
|
|
56667
|
+
const [currentResp, baselineResp] = await Promise.all([
|
|
56668
|
+
this.client.datasourceRange(panel.datasourceUid, expr, opts.from, opts.to, step, signal),
|
|
56669
|
+
this.client.datasourceRange(
|
|
56670
|
+
panel.datasourceUid,
|
|
56671
|
+
expr,
|
|
56672
|
+
opts.from - windowSecs,
|
|
56673
|
+
opts.from,
|
|
56674
|
+
step,
|
|
56675
|
+
signal
|
|
56676
|
+
)
|
|
56677
|
+
]);
|
|
56678
|
+
const current = parseRange(currentResp);
|
|
56679
|
+
const baseline = parseRange(baselineResp);
|
|
56680
|
+
const findings2 = buildFindings(
|
|
56681
|
+
panel.dashboardUid ?? "",
|
|
56682
|
+
panel.title,
|
|
56683
|
+
panel.kind,
|
|
56684
|
+
baseline,
|
|
56685
|
+
current
|
|
56686
|
+
).map((f) => ({ ...f, matchSource: panelMatchSrc }));
|
|
56687
|
+
results.push(...findings2);
|
|
56688
|
+
} catch (err) {
|
|
56689
|
+
if (signal?.aborted) throw signal.reason ?? err;
|
|
56690
|
+
continue;
|
|
56691
|
+
}
|
|
56562
56692
|
}
|
|
56563
56693
|
}
|
|
56694
|
+
return results;
|
|
56695
|
+
};
|
|
56696
|
+
const matchedPanels = await this.findPanels(hint, signal);
|
|
56697
|
+
if (matchedPanels.length > 0 || hint === void 0 || hint === "") {
|
|
56698
|
+
return queryPanels(matchedPanels);
|
|
56564
56699
|
}
|
|
56565
|
-
|
|
56700
|
+
const allPanels = await this.findPanels(void 0, signal);
|
|
56701
|
+
const allFindings = await queryPanels(allPanels);
|
|
56702
|
+
return allFindings.filter((f) => findingLabelsMatchHint(f.labels, hint)).map((f) => ({ ...f, matchSource: "series-labels" }));
|
|
56566
56703
|
}
|
|
56567
56704
|
/**
|
|
56568
56705
|
* Raw escape hatch: execute a single PromQL expression against the default
|
|
@@ -56603,12 +56740,19 @@ var MongoStateClient = class {
|
|
|
56603
56740
|
opts;
|
|
56604
56741
|
client = null;
|
|
56605
56742
|
assertAllowed(collection) {
|
|
56743
|
+
if (this.opts.allowlist.length === 0) return;
|
|
56606
56744
|
if (!this.opts.allowlist.includes(collection)) {
|
|
56607
56745
|
throw new Error(
|
|
56608
56746
|
`Collection "${collection}" is not allowlisted for ${this.opts.database}`
|
|
56609
56747
|
);
|
|
56610
56748
|
}
|
|
56611
56749
|
}
|
|
56750
|
+
/** List all collection names in the configured database (for auto-discovery). */
|
|
56751
|
+
async listCollections() {
|
|
56752
|
+
const db = await this.db();
|
|
56753
|
+
const cols = await db.listCollections({}, { nameOnly: true }).toArray();
|
|
56754
|
+
return cols.map((c) => c["name"]).filter(Boolean);
|
|
56755
|
+
}
|
|
56612
56756
|
async db() {
|
|
56613
56757
|
if (this.client === null) {
|
|
56614
56758
|
this.client = new import_mongodb.MongoClient(this.opts.url, {
|
|
@@ -56806,7 +56950,13 @@ var MongoStateProvider = class {
|
|
|
56806
56950
|
const legacyHours = opts.legacyHours ?? DEFAULT_LEGACY_HOURS;
|
|
56807
56951
|
const nowMs = Date.now();
|
|
56808
56952
|
const collections = [];
|
|
56809
|
-
|
|
56953
|
+
let targetCollections = this.opts.collections;
|
|
56954
|
+
let autoDiscovered = false;
|
|
56955
|
+
if (targetCollections.length === 0) {
|
|
56956
|
+
targetCollections = await this.client.listCollections();
|
|
56957
|
+
autoDiscovered = true;
|
|
56958
|
+
}
|
|
56959
|
+
for (const coll of targetCollections) {
|
|
56810
56960
|
try {
|
|
56811
56961
|
const count = await this.client.count(coll);
|
|
56812
56962
|
const fields = await this.client.sampleFields(coll);
|
|
@@ -56838,7 +56988,13 @@ var MongoStateProvider = class {
|
|
|
56838
56988
|
} catch {
|
|
56839
56989
|
}
|
|
56840
56990
|
}
|
|
56841
|
-
return {
|
|
56991
|
+
return {
|
|
56992
|
+
database: this.opts.database,
|
|
56993
|
+
staleHours,
|
|
56994
|
+
legacyHours,
|
|
56995
|
+
collections,
|
|
56996
|
+
autoDiscovered
|
|
56997
|
+
};
|
|
56842
56998
|
}
|
|
56843
56999
|
toEvidence(analysis) {
|
|
56844
57000
|
return stateToEvidence(analysis, "mongo.analyzeState", (/* @__PURE__ */ new Date()).toISOString());
|
|
@@ -56846,6 +57002,9 @@ var MongoStateProvider = class {
|
|
|
56846
57002
|
async health() {
|
|
56847
57003
|
return this.client.health();
|
|
56848
57004
|
}
|
|
57005
|
+
async listCollections() {
|
|
57006
|
+
return this.client.listCollections();
|
|
57007
|
+
}
|
|
56849
57008
|
async close() {
|
|
56850
57009
|
await this.client.close();
|
|
56851
57010
|
}
|
|
@@ -57206,7 +57365,7 @@ function metricsForEnv(renv) {
|
|
|
57206
57365
|
if (!g || !g.url) return null;
|
|
57207
57366
|
return new GrafanaMetricsProvider(
|
|
57208
57367
|
new GrafanaClient({ baseUrl: g.url, username: g.username, password: g.password }),
|
|
57209
|
-
{ defaultStep: 60 }
|
|
57368
|
+
{ defaultStep: 60, dashboardUids: g.dashboards }
|
|
57210
57369
|
);
|
|
57211
57370
|
}
|
|
57212
57371
|
function queueForEnv(renv) {
|
|
@@ -57328,23 +57487,28 @@ function parseFileContributors(stdout) {
|
|
|
57328
57487
|
for (const line2 of stdout.split("\n")) {
|
|
57329
57488
|
const trimmed = line2.trim();
|
|
57330
57489
|
if (!trimmed) continue;
|
|
57331
|
-
const
|
|
57332
|
-
if (
|
|
57333
|
-
const
|
|
57334
|
-
const
|
|
57335
|
-
|
|
57336
|
-
|
|
57490
|
+
const parts = trimmed.split("");
|
|
57491
|
+
if (parts.length < 3) continue;
|
|
57492
|
+
const name = parts[0].trim();
|
|
57493
|
+
const email = parts[1].trim();
|
|
57494
|
+
const date2 = parts[2].trim();
|
|
57495
|
+
if (!name || !date2) continue;
|
|
57496
|
+
const key = email || name;
|
|
57497
|
+
const existing = tally.get(key);
|
|
57337
57498
|
if (existing === void 0) {
|
|
57338
|
-
tally.set(
|
|
57499
|
+
tally.set(key, { name, commits: 1, firstDate: date2, lastDate: date2 });
|
|
57339
57500
|
} else {
|
|
57340
57501
|
existing.commits += 1;
|
|
57341
57502
|
if (date2 < existing.firstDate) existing.firstDate = date2;
|
|
57342
|
-
if (date2 > existing.lastDate)
|
|
57503
|
+
if (date2 > existing.lastDate) {
|
|
57504
|
+
existing.lastDate = date2;
|
|
57505
|
+
existing.name = name;
|
|
57506
|
+
}
|
|
57343
57507
|
}
|
|
57344
57508
|
}
|
|
57345
57509
|
const result = [];
|
|
57346
|
-
for (const [
|
|
57347
|
-
result.push({ author,
|
|
57510
|
+
for (const [, stats] of tally.entries()) {
|
|
57511
|
+
result.push({ author: stats.name, commits: stats.commits, firstDate: stats.firstDate, lastDate: stats.lastDate });
|
|
57348
57512
|
}
|
|
57349
57513
|
result.sort((a, b2) => {
|
|
57350
57514
|
if (b2.commits !== a.commits) return b2.commits - a.commits;
|
|
@@ -57356,7 +57520,7 @@ async function gitFileContributors(repoPath, file) {
|
|
|
57356
57520
|
try {
|
|
57357
57521
|
const { stdout } = await exec2(
|
|
57358
57522
|
"git",
|
|
57359
|
-
["-C", repoPath, "log", "--follow", "--format=%an%x1f%aI", "--", file],
|
|
57523
|
+
["-C", repoPath, "log", "--follow", "--format=%an%x1f%ae%x1f%aI", "--", file],
|
|
57360
57524
|
{ maxBuffer: 10 * 1024 * 1024 }
|
|
57361
57525
|
);
|
|
57362
57526
|
return parseFileContributors(stdout);
|
|
@@ -66366,6 +66530,9 @@ async function listQueueEdges(db, opts = {}) {
|
|
|
66366
66530
|
|
|
66367
66531
|
// ../../packages/db/src/investigations.ts
|
|
66368
66532
|
init_cjs_shims();
|
|
66533
|
+
async function updateInvestigationReport(db, id, report) {
|
|
66534
|
+
await db.update(investigations).set({ report }).where(eq(investigations.id, id));
|
|
66535
|
+
}
|
|
66369
66536
|
async function getInvestigation(db, id) {
|
|
66370
66537
|
const rows = await db.select().from(investigations).where(eq(investigations.id, id)).limit(1);
|
|
66371
66538
|
return rows[0] ?? null;
|
|
@@ -66511,12 +66678,14 @@ function mark(ok) {
|
|
|
66511
66678
|
if (ok === "pending") return import_picocolors.default.yellow("\u25CB");
|
|
66512
66679
|
return ok ? import_picocolors.default.green("\u25CF") : import_picocolors.default.red("\u25CF");
|
|
66513
66680
|
}
|
|
66514
|
-
async function checkEnv(renv) {
|
|
66681
|
+
async function checkEnv(renv, deps) {
|
|
66515
66682
|
const header = ` ${import_picocolors.default.bold(renv.project)} / ${import_picocolors.default.bold(renv.env)}` + (renv.readOnly ? import_picocolors.default.dim(" (read-only)") : "");
|
|
66516
66683
|
console.log(header);
|
|
66517
66684
|
let allOk = true;
|
|
66518
66685
|
if (renv.repositories.length === 0) {
|
|
66519
|
-
console.log(
|
|
66686
|
+
console.log(
|
|
66687
|
+
` ${mark("pending")} ${import_picocolors.default.bold("Source")} ${import_picocolors.default.dim("no repositories configured")}`
|
|
66688
|
+
);
|
|
66520
66689
|
}
|
|
66521
66690
|
for (const repo of renv.repositories) {
|
|
66522
66691
|
const axonHostUrl = repo.sourceHostUrl ?? repo.axonHostUrl;
|
|
@@ -66540,7 +66709,9 @@ async function checkEnv(renv) {
|
|
|
66540
66709
|
versionPart = `v${compat.version} (pinned ${compat.pinned} \u2014 MISMATCH)`;
|
|
66541
66710
|
}
|
|
66542
66711
|
const axonDetail = health.ok ? `${repo.name} \xB7 responded ${health.status} \xB7 ${versionPart} at ${axonHostUrl}` : `${repo.name} \xB7 unreachable at ${axonHostUrl}`;
|
|
66543
|
-
console.log(
|
|
66712
|
+
console.log(
|
|
66713
|
+
` ${mark(health.ok)} ${import_picocolors.default.bold("Source")} ${import_picocolors.default.dim(axonDetail)}`
|
|
66714
|
+
);
|
|
66544
66715
|
if (!health.ok) allOk = false;
|
|
66545
66716
|
}
|
|
66546
66717
|
const esCfg = renv.connectors.elasticsearch;
|
|
@@ -66548,12 +66719,14 @@ async function checkEnv(renv) {
|
|
|
66548
66719
|
const logsProvider = logsForEnv(renv);
|
|
66549
66720
|
if (logsProvider) {
|
|
66550
66721
|
const h = await logsProvider.health();
|
|
66551
|
-
const
|
|
66722
|
+
const idxDisplay = esCfg.indexPatterns ? esCfg.indexPatterns.join(", ") : esCfg.indexPattern;
|
|
66723
|
+
const detail = h.ok ? `reachable \xB7 index ${idxDisplay}` : `unreachable \xB7 index ${idxDisplay}`;
|
|
66552
66724
|
console.log(` ${mark(h.ok)} ${import_picocolors.default.bold("Elasticsearch")} ${import_picocolors.default.dim(detail)}`);
|
|
66553
66725
|
if (!h.ok) allOk = false;
|
|
66554
66726
|
} else {
|
|
66727
|
+
const idxDisplay = esCfg.indexPatterns ? esCfg.indexPatterns.join(", ") : esCfg.indexPattern;
|
|
66555
66728
|
console.log(
|
|
66556
|
-
` ${mark(false)} ${import_picocolors.default.bold("Elasticsearch")} ${import_picocolors.default.dim(`configured (index ${
|
|
66729
|
+
` ${mark(false)} ${import_picocolors.default.bold("Elasticsearch")} ${import_picocolors.default.dim(`configured (index ${idxDisplay}) but ES_URL not set`)}`
|
|
66557
66730
|
);
|
|
66558
66731
|
}
|
|
66559
66732
|
} else {
|
|
@@ -66566,7 +66739,8 @@ async function checkEnv(renv) {
|
|
|
66566
66739
|
const metricsProvider = metricsForEnv(renv);
|
|
66567
66740
|
if (metricsProvider) {
|
|
66568
66741
|
const h = await metricsProvider.health();
|
|
66569
|
-
const
|
|
66742
|
+
const dashDisplay = grafanaCfg.dashboards ? grafanaCfg.dashboards.join(", ") : grafanaCfg.dashboard;
|
|
66743
|
+
const dashSuffix = dashDisplay ? ` \xB7 dashboards: ${dashDisplay}` : "";
|
|
66570
66744
|
const detail = h.ok ? `reachable${dashSuffix}` : `unreachable${dashSuffix}`;
|
|
66571
66745
|
console.log(` ${mark(h.ok)} ${import_picocolors.default.bold("Grafana")} ${import_picocolors.default.dim(detail)}`);
|
|
66572
66746
|
if (!h.ok) allOk = false;
|
|
@@ -66582,20 +66756,73 @@ async function checkEnv(renv) {
|
|
|
66582
66756
|
}
|
|
66583
66757
|
const mongoCfg = renv.connectors.mongodb;
|
|
66584
66758
|
if (mongoCfg) {
|
|
66585
|
-
const
|
|
66586
|
-
|
|
66587
|
-
|
|
66759
|
+
const mongo = (deps?.mongoFactory ?? mongoForEnv)(renv);
|
|
66760
|
+
if (mongo) {
|
|
66761
|
+
try {
|
|
66762
|
+
const h = await mongo.health();
|
|
66763
|
+
if (!h.ok) {
|
|
66764
|
+
console.log(
|
|
66765
|
+
` ${mark(false)} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(`unreachable \xB7 db ${mongoCfg.database}`)}`
|
|
66766
|
+
);
|
|
66767
|
+
allOk = false;
|
|
66768
|
+
} else {
|
|
66769
|
+
const allowlist = mongoCfg.collections;
|
|
66770
|
+
const allowlistPart = allowlist.length === 0 ? "allowlist: all" : `allowlist: ${allowlist.length}`;
|
|
66771
|
+
const discovered = mongo.listCollections ? await mongo.listCollections() : void 0;
|
|
66772
|
+
const discoveredPart = discovered ? ` \xB7 discovered: ${discovered.length} collection(s)` : "";
|
|
66773
|
+
const detail = `reachable \xB7 db ${mongoCfg.database} \xB7 ${allowlistPart}${discoveredPart}`;
|
|
66774
|
+
console.log(
|
|
66775
|
+
` ${mark(true)} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(detail)}`
|
|
66776
|
+
);
|
|
66777
|
+
}
|
|
66778
|
+
} finally {
|
|
66779
|
+
await mongo.close();
|
|
66780
|
+
}
|
|
66781
|
+
} else {
|
|
66782
|
+
console.log(
|
|
66783
|
+
` ${mark(false)} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim(`configured (db ${mongoCfg.database}) but Mongo URL not set`)}`
|
|
66784
|
+
);
|
|
66785
|
+
}
|
|
66786
|
+
} else {
|
|
66787
|
+
console.log(
|
|
66788
|
+
` ${mark("pending")} ${import_picocolors.default.bold("MongoDB")} ${import_picocolors.default.dim("not configured")}`
|
|
66789
|
+
);
|
|
66790
|
+
}
|
|
66791
|
+
const redisCfg = renv.connectors.redis;
|
|
66792
|
+
if (redisCfg?.url) {
|
|
66793
|
+
const safeUrl = redactRedisUrl(redisCfg.url);
|
|
66794
|
+
console.log(
|
|
66795
|
+
` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`configured \xB7 ${safeUrl}`)}`
|
|
66796
|
+
);
|
|
66588
66797
|
} else {
|
|
66589
|
-
console.log(
|
|
66798
|
+
console.log(
|
|
66799
|
+
` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim("not configured")}`
|
|
66800
|
+
);
|
|
66590
66801
|
}
|
|
66591
66802
|
console.log("");
|
|
66592
66803
|
return allOk;
|
|
66593
66804
|
}
|
|
66805
|
+
function redactRedisUrl(raw) {
|
|
66806
|
+
try {
|
|
66807
|
+
const u = new URL(raw);
|
|
66808
|
+
if (u.password) {
|
|
66809
|
+
u.password = "***";
|
|
66810
|
+
}
|
|
66811
|
+
if (u.username) {
|
|
66812
|
+
u.username = u.username === "" ? "" : "***";
|
|
66813
|
+
}
|
|
66814
|
+
return u.toString();
|
|
66815
|
+
} catch {
|
|
66816
|
+
return raw.replace(/\/\/:?[^@]*@/, "//:***@");
|
|
66817
|
+
}
|
|
66818
|
+
}
|
|
66594
66819
|
async function runStatus(configPath, opts) {
|
|
66595
66820
|
console.log(import_picocolors.default.bold(`
|
|
66596
66821
|
Horus ${HORUS_VERSION}`));
|
|
66597
|
-
console.log(
|
|
66598
|
-
`
|
|
66822
|
+
console.log(
|
|
66823
|
+
import_picocolors.default.dim(`pinned backend: ${PINNED_SOURCE_VERSION} \xB7 transport: HTTP/MCP only
|
|
66824
|
+
`)
|
|
66825
|
+
);
|
|
66599
66826
|
let config;
|
|
66600
66827
|
const checks = [];
|
|
66601
66828
|
try {
|
|
@@ -66618,8 +66845,12 @@ Horus ${HORUS_VERSION}`));
|
|
|
66618
66845
|
}
|
|
66619
66846
|
const dbUrl = config.database.url;
|
|
66620
66847
|
const h = await checkDatabase(dbUrl);
|
|
66621
|
-
console.log(
|
|
66622
|
-
|
|
66848
|
+
console.log(
|
|
66849
|
+
` ${mark(h.reachable)} ${import_picocolors.default.bold("Postgres")} ${import_picocolors.default.dim(h.reachableDetail)}`
|
|
66850
|
+
);
|
|
66851
|
+
console.log(
|
|
66852
|
+
` ${mark(h.schemaReady)} ${import_picocolors.default.bold("Schema")} ${import_picocolors.default.dim(h.schemaDetail)}`
|
|
66853
|
+
);
|
|
66623
66854
|
console.log("");
|
|
66624
66855
|
const envList = listEnvironments(config);
|
|
66625
66856
|
if (envList.length === 0) {
|
|
@@ -66634,7 +66865,7 @@ Horus ${HORUS_VERSION}`));
|
|
|
66634
66865
|
console.error(import_picocolors.default.red(err.message));
|
|
66635
66866
|
return 1;
|
|
66636
66867
|
}
|
|
66637
|
-
const ok = await checkEnv(renv);
|
|
66868
|
+
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory });
|
|
66638
66869
|
return ok ? 0 : 1;
|
|
66639
66870
|
}
|
|
66640
66871
|
let allHealthy = true;
|
|
@@ -66647,7 +66878,7 @@ Horus ${HORUS_VERSION}`));
|
|
|
66647
66878
|
allHealthy = false;
|
|
66648
66879
|
continue;
|
|
66649
66880
|
}
|
|
66650
|
-
const ok = await checkEnv(renv);
|
|
66881
|
+
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory });
|
|
66651
66882
|
if (!ok) allHealthy = false;
|
|
66652
66883
|
}
|
|
66653
66884
|
return checks.some((c) => c.ok === false && c.fatal) ? 1 : 0;
|
|
@@ -66666,11 +66897,32 @@ async function runExplain(query, opts) {
|
|
|
66666
66897
|
}
|
|
66667
66898
|
const symbols = await code.searchSymbols(query, 5);
|
|
66668
66899
|
if (symbols.length === 0) {
|
|
66669
|
-
|
|
66900
|
+
if (await isQueueBoundary(config, query)) {
|
|
66901
|
+
console.log(`No symbol found for: ${import_picocolors2.default.bold(query)}`);
|
|
66902
|
+
console.log(
|
|
66903
|
+
import_picocolors2.default.dim(` "${query}" matches an async boundary \u2014 try: `) + import_picocolors2.default.bold(`horus queues ${query}`)
|
|
66904
|
+
);
|
|
66905
|
+
return 1;
|
|
66906
|
+
}
|
|
66907
|
+
console.log(`No symbol found for: ${query}`);
|
|
66908
|
+
console.log(import_picocolors2.default.dim(` Tip: use an exact class or function name, e.g. "MyService" or "processOrder"`));
|
|
66670
66909
|
return 1;
|
|
66671
66910
|
}
|
|
66672
66911
|
const top = symbols[0];
|
|
66673
66912
|
if (!top) return 1;
|
|
66913
|
+
const isExactMatch = top.name.toLowerCase() === query.toLowerCase();
|
|
66914
|
+
if (!isExactMatch) {
|
|
66915
|
+
if (await isQueueBoundary(config, query)) {
|
|
66916
|
+
console.log(`No exact symbol match for: ${import_picocolors2.default.bold(query)}`);
|
|
66917
|
+
console.log(
|
|
66918
|
+
import_picocolors2.default.dim(` "${query}" matches an async boundary \u2014 try: `) + import_picocolors2.default.bold(`horus queues ${query}`)
|
|
66919
|
+
);
|
|
66920
|
+
return 1;
|
|
66921
|
+
}
|
|
66922
|
+
console.log(
|
|
66923
|
+
import_picocolors2.default.yellow(` No exact match for "${query}"`) + import_picocolors2.default.dim(` \u2014 showing closest: "${top.name}" (fuzzy match)`)
|
|
66924
|
+
);
|
|
66925
|
+
}
|
|
66674
66926
|
const siblings = symbols.filter((s) => s.name === top.name && s.id !== top.id);
|
|
66675
66927
|
const [ctx, impact, flows] = await Promise.all([
|
|
66676
66928
|
code.context(top.id),
|
|
@@ -66698,6 +66950,20 @@ async function runExplain(query, opts) {
|
|
|
66698
66950
|
renderReport(top, ctx, impact, flows, siblings);
|
|
66699
66951
|
return 0;
|
|
66700
66952
|
}
|
|
66953
|
+
async function isQueueBoundary(config, query) {
|
|
66954
|
+
try {
|
|
66955
|
+
const { db, sql: sql2 } = createDb(config.database.url);
|
|
66956
|
+
try {
|
|
66957
|
+
const edges = await listQueueEdges(db, { queueName: query });
|
|
66958
|
+
return edges.length > 0;
|
|
66959
|
+
} finally {
|
|
66960
|
+
await sql2.end().catch(() => {
|
|
66961
|
+
});
|
|
66962
|
+
}
|
|
66963
|
+
} catch {
|
|
66964
|
+
return false;
|
|
66965
|
+
}
|
|
66966
|
+
}
|
|
66701
66967
|
function renderReport(top, ctx, impact, flows, siblings) {
|
|
66702
66968
|
const kind = top.id.includes(":") ? top.id.substring(0, top.id.indexOf(":")) : top.id;
|
|
66703
66969
|
const sym = ctx.symbol;
|
|
@@ -66798,22 +67064,22 @@ function extractQueueGraph(input) {
|
|
|
66798
67064
|
}
|
|
66799
67065
|
}
|
|
66800
67066
|
const producers = [];
|
|
66801
|
-
for (const
|
|
66802
|
-
for (const m of
|
|
67067
|
+
for (const pc34 of input.producerClasses) {
|
|
67068
|
+
for (const m of pc34.content.matchAll(INJECT_QUEUE_RE)) {
|
|
66803
67069
|
const queue = m[1] ?? "";
|
|
66804
|
-
if (queue) producers.push({ queue, symbol:
|
|
67070
|
+
if (queue) producers.push({ queue, symbol: pc34.name, file: pc34.filePath });
|
|
66805
67071
|
}
|
|
66806
|
-
for (const m of
|
|
67072
|
+
for (const m of pc34.content.matchAll(NEW_BULL_RE)) {
|
|
66807
67073
|
if (m[2] !== "Queue") continue;
|
|
66808
67074
|
const queue = resolveArg(m[3] ?? "", constMap);
|
|
66809
67075
|
if (queue) {
|
|
66810
|
-
producers.push({ queue, symbol: (m[1] ??
|
|
67076
|
+
producers.push({ queue, symbol: (m[1] ?? pc34.name) || baseName(pc34.filePath), file: pc34.filePath });
|
|
66811
67077
|
}
|
|
66812
67078
|
}
|
|
66813
|
-
for (const m of
|
|
67079
|
+
for (const m of pc34.content.matchAll(QUEUE_NAME_CONST_RE)) {
|
|
66814
67080
|
const ident = m[1] ?? "";
|
|
66815
67081
|
const queue = m[2] ?? "";
|
|
66816
|
-
if (queue) producers.push({ queue, symbol: ident || baseName(
|
|
67082
|
+
if (queue) producers.push({ queue, symbol: ident || baseName(pc34.filePath), file: pc34.filePath });
|
|
66817
67083
|
}
|
|
66818
67084
|
}
|
|
66819
67085
|
const workers = [];
|
|
@@ -66995,7 +67261,7 @@ async function runIndex(opts) {
|
|
|
66995
67261
|
spawned = true;
|
|
66996
67262
|
console.log(import_picocolors3.default.bold(`Indexing ${label}`) + import_picocolors3.default.dim(` (${root})`));
|
|
66997
67263
|
if (!await sourceAvailable()) {
|
|
66998
|
-
console.error(import_picocolors3.default.red("
|
|
67264
|
+
console.error(import_picocolors3.default.red("horus-source not found on PATH. Install it: pip install horus-source"));
|
|
66999
67265
|
return 1;
|
|
67000
67266
|
}
|
|
67001
67267
|
if (!isAnalyzed(root)) {
|
|
@@ -67073,64 +67339,29 @@ init_cjs_shims();
|
|
|
67073
67339
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
67074
67340
|
async function runQueues(name, opts) {
|
|
67075
67341
|
try {
|
|
67076
|
-
const config = await loadConfig(opts.config);
|
|
67342
|
+
const config = await loadConfig(opts.config, { name: opts.name });
|
|
67077
67343
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
67078
67344
|
try {
|
|
67079
67345
|
const rows = await listQueueEdges(db, { project: opts.project, queueName: name });
|
|
67346
|
+
console.log(
|
|
67347
|
+
import_picocolors4.default.bold("Queue topology") + import_picocolors4.default.dim(" \xB7 source: code / source intelligence \xB7 static (run horus index to refresh)")
|
|
67348
|
+
);
|
|
67349
|
+
console.log("");
|
|
67080
67350
|
if (rows.length === 0) {
|
|
67081
|
-
console.log("No queue edges. Run: horus index");
|
|
67082
|
-
|
|
67351
|
+
console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
|
|
67352
|
+
} else {
|
|
67353
|
+
const byQueue = buildQueueMap(rows);
|
|
67354
|
+
printTopology(byQueue);
|
|
67083
67355
|
}
|
|
67084
|
-
|
|
67085
|
-
|
|
67086
|
-
|
|
67087
|
-
|
|
67088
|
-
|
|
67089
|
-
|
|
67090
|
-
|
|
67091
|
-
|
|
67092
|
-
|
|
67093
|
-
for (const [queueName, edges] of byQueue) {
|
|
67094
|
-
console.log(import_picocolors4.default.bold(queueName));
|
|
67095
|
-
const producerSet = /* @__PURE__ */ new Set();
|
|
67096
|
-
const producerDetails = /* @__PURE__ */ new Map();
|
|
67097
|
-
for (const edge of edges) {
|
|
67098
|
-
if (edge.producerSymbol) {
|
|
67099
|
-
producerSet.add(edge.producerSymbol);
|
|
67100
|
-
if (edge.producerFile) {
|
|
67101
|
-
producerDetails.set(edge.producerSymbol, edge.producerFile);
|
|
67102
|
-
}
|
|
67103
|
-
}
|
|
67104
|
-
}
|
|
67105
|
-
if (producerSet.size === 0) {
|
|
67106
|
-
console.log(" producers: " + import_picocolors4.default.dim("none"));
|
|
67107
|
-
} else {
|
|
67108
|
-
const producerList = Array.from(producerSet).map((sym) => {
|
|
67109
|
-
const file = producerDetails.get(sym);
|
|
67110
|
-
return file ? `${sym} (${file})` : sym;
|
|
67111
|
-
}).join(", ");
|
|
67112
|
-
console.log(" producers: " + producerList);
|
|
67113
|
-
}
|
|
67114
|
-
const workerSet = /* @__PURE__ */ new Set();
|
|
67115
|
-
const workerDetails = /* @__PURE__ */ new Map();
|
|
67116
|
-
for (const edge of edges) {
|
|
67117
|
-
if (edge.workerSymbol) {
|
|
67118
|
-
workerSet.add(edge.workerSymbol);
|
|
67119
|
-
if (edge.workerFile) {
|
|
67120
|
-
workerDetails.set(edge.workerSymbol, edge.workerFile);
|
|
67121
|
-
}
|
|
67122
|
-
}
|
|
67123
|
-
}
|
|
67124
|
-
if (workerSet.size === 0) {
|
|
67125
|
-
console.log(" workers: " + import_picocolors4.default.dim("none"));
|
|
67126
|
-
} else {
|
|
67127
|
-
const workerList = Array.from(workerSet).map((sym) => {
|
|
67128
|
-
const file = workerDetails.get(sym);
|
|
67129
|
-
return file ? `${sym} (${file})` : sym;
|
|
67130
|
-
}).join(", ");
|
|
67131
|
-
console.log(" workers: " + workerList);
|
|
67132
|
-
}
|
|
67133
|
-
console.log("");
|
|
67356
|
+
console.log("");
|
|
67357
|
+
if (opts.live) {
|
|
67358
|
+
await runLiveMode(config, rows, name);
|
|
67359
|
+
} else {
|
|
67360
|
+
console.log(
|
|
67361
|
+
import_picocolors4.default.dim(
|
|
67362
|
+
" Tip: run horus queues --live to show real-time Redis/BullMQ depths and failed-job counts."
|
|
67363
|
+
)
|
|
67364
|
+
);
|
|
67134
67365
|
}
|
|
67135
67366
|
} finally {
|
|
67136
67367
|
await sql2.end();
|
|
@@ -67141,6 +67372,141 @@ async function runQueues(name, opts) {
|
|
|
67141
67372
|
return 1;
|
|
67142
67373
|
}
|
|
67143
67374
|
}
|
|
67375
|
+
function buildQueueMap(rows) {
|
|
67376
|
+
const byQueue = /* @__PURE__ */ new Map();
|
|
67377
|
+
for (const row of rows) {
|
|
67378
|
+
const existing = byQueue.get(row.queueName);
|
|
67379
|
+
if (existing) {
|
|
67380
|
+
existing.push(row);
|
|
67381
|
+
} else {
|
|
67382
|
+
byQueue.set(row.queueName, [row]);
|
|
67383
|
+
}
|
|
67384
|
+
}
|
|
67385
|
+
return byQueue;
|
|
67386
|
+
}
|
|
67387
|
+
function printTopology(byQueue) {
|
|
67388
|
+
for (const [queueName, edges] of byQueue) {
|
|
67389
|
+
console.log(import_picocolors4.default.bold(queueName));
|
|
67390
|
+
const producerSet = /* @__PURE__ */ new Set();
|
|
67391
|
+
const producerDetails = /* @__PURE__ */ new Map();
|
|
67392
|
+
for (const edge of edges) {
|
|
67393
|
+
if (edge.producerSymbol) {
|
|
67394
|
+
producerSet.add(edge.producerSymbol);
|
|
67395
|
+
if (edge.producerFile) producerDetails.set(edge.producerSymbol, edge.producerFile);
|
|
67396
|
+
}
|
|
67397
|
+
}
|
|
67398
|
+
if (producerSet.size === 0) {
|
|
67399
|
+
console.log(" producers: " + import_picocolors4.default.dim("none"));
|
|
67400
|
+
} else {
|
|
67401
|
+
const list = Array.from(producerSet).map((sym) => {
|
|
67402
|
+
const file = producerDetails.get(sym);
|
|
67403
|
+
return file ? `${sym} (${file})` : sym;
|
|
67404
|
+
}).join(", ");
|
|
67405
|
+
console.log(" producers: " + list);
|
|
67406
|
+
}
|
|
67407
|
+
const workerSet = /* @__PURE__ */ new Set();
|
|
67408
|
+
const workerDetails = /* @__PURE__ */ new Map();
|
|
67409
|
+
for (const edge of edges) {
|
|
67410
|
+
if (edge.workerSymbol) {
|
|
67411
|
+
workerSet.add(edge.workerSymbol);
|
|
67412
|
+
if (edge.workerFile) workerDetails.set(edge.workerSymbol, edge.workerFile);
|
|
67413
|
+
}
|
|
67414
|
+
}
|
|
67415
|
+
if (workerSet.size === 0) {
|
|
67416
|
+
console.log(" workers: " + import_picocolors4.default.dim("none"));
|
|
67417
|
+
} else {
|
|
67418
|
+
const list = Array.from(workerSet).map((sym) => {
|
|
67419
|
+
const file = workerDetails.get(sym);
|
|
67420
|
+
return file ? `${sym} (${file})` : sym;
|
|
67421
|
+
}).join(", ");
|
|
67422
|
+
console.log(" workers: " + list);
|
|
67423
|
+
}
|
|
67424
|
+
console.log("");
|
|
67425
|
+
}
|
|
67426
|
+
}
|
|
67427
|
+
async function runLiveMode(config, rows, nameFilter) {
|
|
67428
|
+
let renv;
|
|
67429
|
+
try {
|
|
67430
|
+
renv = resolveEnvironment(config);
|
|
67431
|
+
} catch (err) {
|
|
67432
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
67433
|
+
console.log(import_picocolors4.default.yellow(` \u26A0 Cannot resolve environment: ${err.message}`));
|
|
67434
|
+
return;
|
|
67435
|
+
}
|
|
67436
|
+
const queueProvider = queueForEnv(renv);
|
|
67437
|
+
if (!queueProvider) {
|
|
67438
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
67439
|
+
console.log(
|
|
67440
|
+
import_picocolors4.default.yellow(" \u25CB Redis not configured \u2014 run: ") + import_picocolors4.default.bold("horus connect redis")
|
|
67441
|
+
);
|
|
67442
|
+
return;
|
|
67443
|
+
}
|
|
67444
|
+
let headerPrinted = false;
|
|
67445
|
+
try {
|
|
67446
|
+
const health = await queueProvider.health();
|
|
67447
|
+
if (!health.ok) {
|
|
67448
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
67449
|
+
console.log(import_picocolors4.default.red(` \u2717 Redis unreachable: ${health.detail}`));
|
|
67450
|
+
return;
|
|
67451
|
+
}
|
|
67452
|
+
const topologyNames = [...buildQueueMap(rows).keys()];
|
|
67453
|
+
const queueNames = nameFilter !== void 0 ? [nameFilter] : topologyNames.length > 0 ? topologyNames : void 0;
|
|
67454
|
+
const state = await queueProvider.analyzeQueues({ queueNames });
|
|
67455
|
+
const collectedAt = new Date(state.collectedAt).toLocaleTimeString();
|
|
67456
|
+
console.log(
|
|
67457
|
+
import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(` \xB7 source: Redis/BullMQ (prefix: ${state.prefix}) \xB7 collected: ${collectedAt}`)
|
|
67458
|
+
);
|
|
67459
|
+
headerPrinted = true;
|
|
67460
|
+
console.log("");
|
|
67461
|
+
if (state.queues.length === 0) {
|
|
67462
|
+
console.log(import_picocolors4.default.dim(" No queues found in Redis."));
|
|
67463
|
+
console.log(
|
|
67464
|
+
import_picocolors4.default.dim(
|
|
67465
|
+
" If queues exist under a custom prefix, set the BullMQ prefix in your connector config."
|
|
67466
|
+
)
|
|
67467
|
+
);
|
|
67468
|
+
return;
|
|
67469
|
+
}
|
|
67470
|
+
printLiveTable(state.queues);
|
|
67471
|
+
} catch (err) {
|
|
67472
|
+
if (!headerPrinted) {
|
|
67473
|
+
console.log(import_picocolors4.default.bold("Live queue state") + import_picocolors4.default.dim(" \xB7 source: Redis/BullMQ"));
|
|
67474
|
+
}
|
|
67475
|
+
console.log(import_picocolors4.default.red(` \u2717 ${err.message}`));
|
|
67476
|
+
} finally {
|
|
67477
|
+
await queueProvider.close().catch(() => {
|
|
67478
|
+
});
|
|
67479
|
+
}
|
|
67480
|
+
}
|
|
67481
|
+
function printLiveTable(queues) {
|
|
67482
|
+
const nameWidth = Math.max(10, ...queues.map((q) => q.queueName.length));
|
|
67483
|
+
const numWidth = 7;
|
|
67484
|
+
const header = " " + "queue".padEnd(nameWidth) + " " + "waiting".padStart(numWidth) + " " + "active".padStart(numWidth) + " " + "failed".padStart(numWidth) + " " + "delayed".padStart(numWidth) + " " + "paused".padStart(numWidth);
|
|
67485
|
+
console.log(import_picocolors4.default.dim(header));
|
|
67486
|
+
console.log(import_picocolors4.default.dim(" " + "\u2500".repeat(header.length - 2)));
|
|
67487
|
+
for (const q of queues) {
|
|
67488
|
+
const hasIssue = q.failed > 0 || q.waiting > 100 || q.delayed > 50 || q.isPaused;
|
|
67489
|
+
const color = hasIssue ? import_picocolors4.default.yellow : (s) => s;
|
|
67490
|
+
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));
|
|
67491
|
+
console.log(color(row));
|
|
67492
|
+
if (q.oldestWaitingMs !== void 0) {
|
|
67493
|
+
const age = formatAge(q.oldestWaitingMs);
|
|
67494
|
+
console.log(import_picocolors4.default.dim(` oldest waiting: ${age}`));
|
|
67495
|
+
}
|
|
67496
|
+
if (q.failedBreakdown && q.failedBreakdown.length > 0) {
|
|
67497
|
+
for (const { reason, count } of q.failedBreakdown.slice(0, 3)) {
|
|
67498
|
+
console.log(import_picocolors4.default.red(` \u2717 [${count}x] ${reason}`));
|
|
67499
|
+
}
|
|
67500
|
+
}
|
|
67501
|
+
}
|
|
67502
|
+
console.log("");
|
|
67503
|
+
}
|
|
67504
|
+
function formatAge(ms) {
|
|
67505
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
67506
|
+
if (ms < 36e5) return `${Math.round(ms / 6e4)}m`;
|
|
67507
|
+
if (ms < 864e5) return `${Math.round(ms / 36e5)}h`;
|
|
67508
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
67509
|
+
}
|
|
67144
67510
|
|
|
67145
67511
|
// ../../packages/cli/src/commands/investigate.ts
|
|
67146
67512
|
init_cjs_shims();
|
|
@@ -67157,8 +67523,96 @@ init_cjs_shims();
|
|
|
67157
67523
|
|
|
67158
67524
|
// ../../packages/engine/src/ownership.ts
|
|
67159
67525
|
init_cjs_shims();
|
|
67526
|
+
var CODE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
67527
|
+
"ts",
|
|
67528
|
+
"tsx",
|
|
67529
|
+
"js",
|
|
67530
|
+
"jsx",
|
|
67531
|
+
"mjs",
|
|
67532
|
+
"cjs",
|
|
67533
|
+
"py",
|
|
67534
|
+
"rb",
|
|
67535
|
+
"go",
|
|
67536
|
+
"rs",
|
|
67537
|
+
"java",
|
|
67538
|
+
"kt",
|
|
67539
|
+
"swift",
|
|
67540
|
+
"cs",
|
|
67541
|
+
"cpp",
|
|
67542
|
+
"c",
|
|
67543
|
+
"h",
|
|
67544
|
+
"hpp",
|
|
67545
|
+
"cc",
|
|
67546
|
+
"vue",
|
|
67547
|
+
"svelte",
|
|
67548
|
+
"astro",
|
|
67549
|
+
"php",
|
|
67550
|
+
"scala",
|
|
67551
|
+
"clj",
|
|
67552
|
+
"ex",
|
|
67553
|
+
"exs",
|
|
67554
|
+
"hs",
|
|
67555
|
+
"sh",
|
|
67556
|
+
"bash",
|
|
67557
|
+
"zsh",
|
|
67558
|
+
"json",
|
|
67559
|
+
"yaml",
|
|
67560
|
+
"yml",
|
|
67561
|
+
"toml",
|
|
67562
|
+
"sql",
|
|
67563
|
+
"graphql",
|
|
67564
|
+
"proto",
|
|
67565
|
+
"html",
|
|
67566
|
+
"css",
|
|
67567
|
+
"scss",
|
|
67568
|
+
"sass",
|
|
67569
|
+
"less",
|
|
67570
|
+
"md",
|
|
67571
|
+
"mdx"
|
|
67572
|
+
]);
|
|
67573
|
+
function looksLikeFilePath(query) {
|
|
67574
|
+
const base2 = query.split("/").pop() ?? "";
|
|
67575
|
+
const dotIdx = base2.lastIndexOf(".");
|
|
67576
|
+
if (dotIdx <= 0) return false;
|
|
67577
|
+
return CODE_EXTENSIONS.has(base2.slice(dotIdx + 1).toLowerCase());
|
|
67578
|
+
}
|
|
67160
67579
|
async function estimateOwnership(query, deps) {
|
|
67161
|
-
|
|
67580
|
+
let top = deps.symbol ?? null;
|
|
67581
|
+
const needsSearch = deps.symbol === void 0 || deps.symbol === null;
|
|
67582
|
+
if (needsSearch) {
|
|
67583
|
+
if (looksLikeFilePath(query)) {
|
|
67584
|
+
const queryBase = query.split("/").pop() ?? query;
|
|
67585
|
+
const broad = await deps.code.searchSymbols(query, 20);
|
|
67586
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
67587
|
+
for (const sym of broad) {
|
|
67588
|
+
const fp = sym.filePath;
|
|
67589
|
+
if (fp === query || fp.endsWith("/" + query) || fp.split("/").pop() === queryBase) {
|
|
67590
|
+
if (!byFile.has(fp)) byFile.set(fp, sym);
|
|
67591
|
+
}
|
|
67592
|
+
}
|
|
67593
|
+
if (byFile.size === 1) {
|
|
67594
|
+
top = [...byFile.values()][0] ?? null;
|
|
67595
|
+
} else if (byFile.size > 1) {
|
|
67596
|
+
return {
|
|
67597
|
+
query,
|
|
67598
|
+
symbol: null,
|
|
67599
|
+
file: null,
|
|
67600
|
+
contributors: [],
|
|
67601
|
+
likelyMaintainer: null,
|
|
67602
|
+
maintainerShare: 0,
|
|
67603
|
+
mostActiveRecent: null,
|
|
67604
|
+
confidence: 0,
|
|
67605
|
+
evidence: [],
|
|
67606
|
+
candidates: [...byFile.keys()],
|
|
67607
|
+
note: "Ambiguous: " + byFile.size + ' files match "' + query + '". Use a more specific path to disambiguate.'
|
|
67608
|
+
};
|
|
67609
|
+
} else {
|
|
67610
|
+
top = (await deps.code.searchSymbols(query, 5))[0] ?? null;
|
|
67611
|
+
}
|
|
67612
|
+
} else {
|
|
67613
|
+
top = (await deps.code.searchSymbols(query, 5))[0] ?? null;
|
|
67614
|
+
}
|
|
67615
|
+
}
|
|
67162
67616
|
const file = top?.filePath ?? null;
|
|
67163
67617
|
if (file === null) {
|
|
67164
67618
|
return {
|
|
@@ -67282,7 +67736,48 @@ function processEvidence(ev, nodes, edges) {
|
|
|
67282
67736
|
addEdge(edges, "observed_in", evId, deployId, ev.id);
|
|
67283
67737
|
return;
|
|
67284
67738
|
}
|
|
67285
|
-
|
|
67739
|
+
case "symbol": {
|
|
67740
|
+
const p = ev.payload;
|
|
67741
|
+
const sym = p.symbol ?? {};
|
|
67742
|
+
const name = sym.name ?? "";
|
|
67743
|
+
if (!name) return;
|
|
67744
|
+
const symbolId = `symbol:${name}`;
|
|
67745
|
+
upsertNode(nodes, symbolId, "symbol", name, ev.id);
|
|
67746
|
+
addEdge(edges, "observed_in", evId, symbolId, ev.id);
|
|
67747
|
+
const filePath = sym.filePath ?? p.filePath ?? ev.links.file;
|
|
67748
|
+
if (filePath) {
|
|
67749
|
+
const fileId = `file:${filePath}`;
|
|
67750
|
+
upsertNode(nodes, fileId, "file", filePath, ev.id);
|
|
67751
|
+
addEdge(edges, "in-file", symbolId, fileId, ev.id);
|
|
67752
|
+
}
|
|
67753
|
+
return;
|
|
67754
|
+
}
|
|
67755
|
+
case "flow": {
|
|
67756
|
+
const p = ev.payload;
|
|
67757
|
+
const name = p.name ?? ev.title;
|
|
67758
|
+
const flowNodeId = `flow:${p.flowId ?? name}`;
|
|
67759
|
+
upsertNode(nodes, flowNodeId, "flow", name, ev.id);
|
|
67760
|
+
addEdge(edges, "observed_in", evId, flowNodeId, ev.id);
|
|
67761
|
+
for (const stepName of p.steps ?? []) {
|
|
67762
|
+
if (!stepName) continue;
|
|
67763
|
+
const stepSymbolId = `symbol:${stepName}`;
|
|
67764
|
+
upsertNode(nodes, stepSymbolId, "symbol", stepName);
|
|
67765
|
+
addEdge(edges, "symbol-in-flow", stepSymbolId, flowNodeId, ev.id);
|
|
67766
|
+
}
|
|
67767
|
+
return;
|
|
67768
|
+
}
|
|
67769
|
+
case "metric": {
|
|
67770
|
+
const p = ev.payload;
|
|
67771
|
+
const labels = p.labels ?? {};
|
|
67772
|
+
const svc = labels["service"] ?? labels["job"] ?? labels["instance"];
|
|
67773
|
+
if (svc) {
|
|
67774
|
+
const serviceId = `service:${svc}`;
|
|
67775
|
+
upsertNode(nodes, serviceId, "service", svc, ev.id);
|
|
67776
|
+
addEdge(edges, "metric-from", evId, serviceId, ev.id);
|
|
67777
|
+
}
|
|
67778
|
+
return;
|
|
67779
|
+
}
|
|
67780
|
+
// impact, redis-key: evidence node only, no infra derived
|
|
67286
67781
|
default:
|
|
67287
67782
|
return;
|
|
67288
67783
|
}
|
|
@@ -67303,18 +67798,28 @@ function scoreImplication(nodes, edges, evidence2) {
|
|
|
67303
67798
|
const evById = new Map(evidence2.map((e) => [e.id, e]));
|
|
67304
67799
|
for (const node of nodes.values()) {
|
|
67305
67800
|
if (node.type === "evidence") continue;
|
|
67801
|
+
if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
|
|
67306
67802
|
node.implicationScore = node.evidenceIds.reduce((max, eid) => {
|
|
67307
67803
|
const ev = evById.get(eid);
|
|
67308
67804
|
if (!ev || isExcludedFromImplication(ev)) return max;
|
|
67309
67805
|
return Math.max(max, ev.relevance);
|
|
67310
67806
|
}, 0);
|
|
67311
67807
|
}
|
|
67808
|
+
const CODE_TOPOLOGY_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
67809
|
+
"evidence",
|
|
67810
|
+
"symbol",
|
|
67811
|
+
"file",
|
|
67812
|
+
"flow"
|
|
67813
|
+
]);
|
|
67312
67814
|
const adj = /* @__PURE__ */ new Map();
|
|
67313
67815
|
for (const edge of edges.values()) {
|
|
67314
67816
|
if (edge.type === "observed_in") continue;
|
|
67817
|
+
if (edge.type === "in-file") continue;
|
|
67818
|
+
if (edge.type === "symbol-in-flow") continue;
|
|
67819
|
+
if (edge.type === "metric-from") continue;
|
|
67315
67820
|
const f = nodes.get(edge.from);
|
|
67316
67821
|
const t = nodes.get(edge.to);
|
|
67317
|
-
if (!f || !t || f.type
|
|
67822
|
+
if (!f || !t || CODE_TOPOLOGY_NODE_TYPES.has(f.type) || CODE_TOPOLOGY_NODE_TYPES.has(t.type)) continue;
|
|
67318
67823
|
const fs3 = adj.get(edge.from) ?? /* @__PURE__ */ new Set();
|
|
67319
67824
|
const ts = adj.get(edge.to) ?? /* @__PURE__ */ new Set();
|
|
67320
67825
|
fs3.add(edge.to);
|
|
@@ -67339,6 +67844,7 @@ function scoreImplication(nodes, edges, evidence2) {
|
|
|
67339
67844
|
}
|
|
67340
67845
|
for (const node of nodes.values()) {
|
|
67341
67846
|
if (node.type === "evidence") continue;
|
|
67847
|
+
if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
|
|
67342
67848
|
node.implicated = node.implicationScore >= IMPLICATION_THRESHOLD;
|
|
67343
67849
|
}
|
|
67344
67850
|
}
|
|
@@ -67370,6 +67876,302 @@ function implicatedNodeIds(graph, evidenceIds) {
|
|
|
67370
67876
|
(n) => n.type !== "evidence" && n.implicated && n.evidenceIds.some((eid) => idSet.has(eid))
|
|
67371
67877
|
).map((n) => n.id);
|
|
67372
67878
|
}
|
|
67879
|
+
function implicatedEvidenceIdsByNodeType(graph, nodeType) {
|
|
67880
|
+
const ids2 = [];
|
|
67881
|
+
for (const node of graph.nodes) {
|
|
67882
|
+
if (node.type !== nodeType || !node.implicated) continue;
|
|
67883
|
+
for (const eid of node.evidenceIds) {
|
|
67884
|
+
if (!ids2.includes(eid)) ids2.push(eid);
|
|
67885
|
+
}
|
|
67886
|
+
}
|
|
67887
|
+
return ids2;
|
|
67888
|
+
}
|
|
67889
|
+
|
|
67890
|
+
// ../../packages/engine/src/cause-chain.ts
|
|
67891
|
+
init_cjs_shims();
|
|
67892
|
+
function groupByKind(ids2, evById) {
|
|
67893
|
+
const map2 = /* @__PURE__ */ new Map();
|
|
67894
|
+
for (const id of ids2) {
|
|
67895
|
+
const ev = evById.get(id);
|
|
67896
|
+
if (!ev) continue;
|
|
67897
|
+
const list = map2.get(ev.kind) ?? [];
|
|
67898
|
+
list.push(ev);
|
|
67899
|
+
map2.set(ev.kind, list);
|
|
67900
|
+
}
|
|
67901
|
+
return map2;
|
|
67902
|
+
}
|
|
67903
|
+
function ids(evs) {
|
|
67904
|
+
return evs.map((e) => e.id);
|
|
67905
|
+
}
|
|
67906
|
+
function firstImplicatedNode(graph, type) {
|
|
67907
|
+
return graph.nodes.find((n) => n.type === type && n.implicated)?.id;
|
|
67908
|
+
}
|
|
67909
|
+
function chainForDeploymentRegression(hyp, byKind, graph, seedLabel) {
|
|
67910
|
+
const commitEvs = byKind.get("commit") ?? [];
|
|
67911
|
+
const symbolEvs = byKind.get("symbol") ?? [];
|
|
67912
|
+
const logEvs = byKind.get("log") ?? [];
|
|
67913
|
+
const metricEvs = byKind.get("metric") ?? [];
|
|
67914
|
+
const impactEvs = byKind.get("impact") ?? [];
|
|
67915
|
+
const runtimeEvIds = [...ids(logEvs), ...ids(metricEvs)];
|
|
67916
|
+
const steps = [];
|
|
67917
|
+
steps.push({
|
|
67918
|
+
role: "trigger",
|
|
67919
|
+
label: commitEvs.length > 0 ? `Recent change: ${commitEvs[0]?.title ?? "commit"}` : "Recent change (commit range not captured)",
|
|
67920
|
+
evidenceIds: ids(commitEvs),
|
|
67921
|
+
graphNodeId: graph.nodes.find((n) => n.type === "deployment")?.id
|
|
67922
|
+
});
|
|
67923
|
+
steps.push({
|
|
67924
|
+
role: "propagation",
|
|
67925
|
+
label: `Affected code path: ${seedLabel}` + (impactEvs.length > 0 ? ` (${impactEvs[0]?.payload?.affected ?? "?"} downstream symbol(s))` : ""),
|
|
67926
|
+
evidenceIds: [...ids(symbolEvs), ...ids(impactEvs)],
|
|
67927
|
+
graphNodeId: graph.nodes.find((n) => n.type === "symbol")?.id
|
|
67928
|
+
});
|
|
67929
|
+
if (runtimeEvIds.length > 0) {
|
|
67930
|
+
steps.push({
|
|
67931
|
+
role: "symptom",
|
|
67932
|
+
label: logEvs.length > 0 ? `Runtime error signatures observed (${logEvs.length} signature(s))` : `Metric anomalies observed (${metricEvs.length} signal(s))`,
|
|
67933
|
+
evidenceIds: runtimeEvIds,
|
|
67934
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
67935
|
+
});
|
|
67936
|
+
}
|
|
67937
|
+
const nonEmpty = steps.filter((s) => s.evidenceIds.length > 0 || s.role === "trigger");
|
|
67938
|
+
const summary = `Change to ${seedLabel} \u2192 affected code path` + (runtimeEvIds.length > 0 ? " \u2192 runtime symptoms observed" : "");
|
|
67939
|
+
return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps: nonEmpty, summary };
|
|
67940
|
+
}
|
|
67941
|
+
function chainForQueueBacklog(hyp, byKind, graph, queueName) {
|
|
67942
|
+
const queueEdgeEvs = byKind.get("queue-edge") ?? [];
|
|
67943
|
+
const queueStateEvs = byKind.get("queue-state") ?? [];
|
|
67944
|
+
const logEvs = byKind.get("log") ?? [];
|
|
67945
|
+
const metricEvs = byKind.get("metric") ?? [];
|
|
67946
|
+
const queueNodeId = queueName ? graph.nodes.find((n) => n.id === `queue:${queueName}`)?.id : firstImplicatedNode(graph, "queue");
|
|
67947
|
+
const steps = [];
|
|
67948
|
+
steps.push({
|
|
67949
|
+
role: "trigger",
|
|
67950
|
+
label: queueName ? `Producer enqueuing to "${queueName}" faster than consumer drains` : "Producer enqueuing faster than consumer drains",
|
|
67951
|
+
evidenceIds: ids(queueEdgeEvs),
|
|
67952
|
+
graphNodeId: queueNodeId
|
|
67953
|
+
});
|
|
67954
|
+
if (queueStateEvs.length > 0) {
|
|
67955
|
+
steps.push({
|
|
67956
|
+
role: "propagation",
|
|
67957
|
+
label: `Queue depth growing${queueName ? ` on "${queueName}"` : ""}`,
|
|
67958
|
+
evidenceIds: ids(queueStateEvs),
|
|
67959
|
+
graphNodeId: queueNodeId
|
|
67960
|
+
});
|
|
67961
|
+
}
|
|
67962
|
+
const symptomEvIds = [...ids(logEvs), ...ids(metricEvs)];
|
|
67963
|
+
if (symptomEvIds.length > 0) {
|
|
67964
|
+
steps.push({
|
|
67965
|
+
role: "symptom",
|
|
67966
|
+
label: "Worker throughput degraded \u2014 downstream delays observable",
|
|
67967
|
+
evidenceIds: symptomEvIds,
|
|
67968
|
+
graphNodeId: firstImplicatedNode(graph, "worker") ?? firstImplicatedNode(graph, "service")
|
|
67969
|
+
});
|
|
67970
|
+
}
|
|
67971
|
+
const summary = (queueName ? `"${queueName}" queue` : "Queue") + " backed up \u2014 producers outpacing consumers" + (symptomEvIds.length > 0 ? " \u2192 downstream delays" : "");
|
|
67972
|
+
return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
|
|
67973
|
+
}
|
|
67974
|
+
function chainForWorkerSlowdown(hyp, byKind, graph, queueName) {
|
|
67975
|
+
const queueStateEvs = byKind.get("queue-state") ?? [];
|
|
67976
|
+
const metricEvs = byKind.get("metric") ?? [];
|
|
67977
|
+
const logEvs = byKind.get("log") ?? [];
|
|
67978
|
+
const workerNodeId = firstImplicatedNode(graph, "worker");
|
|
67979
|
+
const queueNodeId = queueName ? graph.nodes.find((n) => n.id === `queue:${queueName}`)?.id : firstImplicatedNode(graph, "queue");
|
|
67980
|
+
const steps = [];
|
|
67981
|
+
const triggerEvIds = [...ids(queueStateEvs.filter((e) => {
|
|
67982
|
+
const p = e.payload;
|
|
67983
|
+
return p?.isPaused === true || p?.active === 0;
|
|
67984
|
+
})), ...ids(metricEvs)];
|
|
67985
|
+
steps.push({
|
|
67986
|
+
role: "trigger",
|
|
67987
|
+
label: queueName ? `Workers consuming "${queueName}" stalled or processing slowly` : "Workers stalled or processing slowly",
|
|
67988
|
+
evidenceIds: triggerEvIds.length > 0 ? triggerEvIds : ids(queueStateEvs),
|
|
67989
|
+
graphNodeId: workerNodeId
|
|
67990
|
+
});
|
|
67991
|
+
if (queueStateEvs.length > 0) {
|
|
67992
|
+
steps.push({
|
|
67993
|
+
role: "propagation",
|
|
67994
|
+
label: `Queue depth accumulating${queueName ? ` on "${queueName}"` : ""}`,
|
|
67995
|
+
evidenceIds: ids(queueStateEvs),
|
|
67996
|
+
graphNodeId: queueNodeId
|
|
67997
|
+
});
|
|
67998
|
+
}
|
|
67999
|
+
if (logEvs.length > 0) {
|
|
68000
|
+
steps.push({
|
|
68001
|
+
role: "symptom",
|
|
68002
|
+
label: "Downstream latency and error signatures observed",
|
|
68003
|
+
evidenceIds: ids(logEvs),
|
|
68004
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68005
|
+
});
|
|
68006
|
+
}
|
|
68007
|
+
const summary = "Worker stall on " + (queueName ? `"${queueName}"` : "queue") + " \u2192 queue depth growing" + (logEvs.length > 0 ? " \u2192 downstream errors" : "");
|
|
68008
|
+
return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
|
|
68009
|
+
}
|
|
68010
|
+
function chainForExternalApiLatency(hyp, byKind, graph) {
|
|
68011
|
+
const metricEvs = byKind.get("metric") ?? [];
|
|
68012
|
+
const logEvs = byKind.get("log") ?? [];
|
|
68013
|
+
const queueStateEvs = byKind.get("queue-state") ?? [];
|
|
68014
|
+
const steps = [];
|
|
68015
|
+
steps.push({
|
|
68016
|
+
role: "trigger",
|
|
68017
|
+
label: `External or upstream dependency latency spike (${metricEvs.length} metric signal(s))`,
|
|
68018
|
+
evidenceIds: ids(metricEvs),
|
|
68019
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68020
|
+
});
|
|
68021
|
+
if (logEvs.length > 0) {
|
|
68022
|
+
steps.push({
|
|
68023
|
+
role: "propagation",
|
|
68024
|
+
label: "Calls to upstream timing out or returning errors",
|
|
68025
|
+
evidenceIds: ids(logEvs),
|
|
68026
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68027
|
+
});
|
|
68028
|
+
}
|
|
68029
|
+
if (queueStateEvs.length > 0) {
|
|
68030
|
+
steps.push({
|
|
68031
|
+
role: "symptom",
|
|
68032
|
+
label: "Queue backlog accumulating from stalled downstream workers",
|
|
68033
|
+
evidenceIds: ids(queueStateEvs),
|
|
68034
|
+
graphNodeId: firstImplicatedNode(graph, "queue")
|
|
68035
|
+
});
|
|
68036
|
+
}
|
|
68037
|
+
const summary = "External API latency spike" + (logEvs.length > 0 ? " \u2192 timeout errors propagating" : "") + (queueStateEvs.length > 0 ? " \u2192 queue backlog" : "");
|
|
68038
|
+
return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
|
|
68039
|
+
}
|
|
68040
|
+
function chainForInfrastructure(hyp, byKind, graph) {
|
|
68041
|
+
const stateEvs = byKind.get("state") ?? [];
|
|
68042
|
+
const queueStateEvs = byKind.get("queue-state") ?? [];
|
|
68043
|
+
const logEvs = byKind.get("log") ?? [];
|
|
68044
|
+
const metricEvs = byKind.get("metric") ?? [];
|
|
68045
|
+
const triggerEvIds = [...ids(stateEvs), ...ids(metricEvs)];
|
|
68046
|
+
const propagationEvIds = [...ids(queueStateEvs)];
|
|
68047
|
+
const symptomEvIds = ids(logEvs);
|
|
68048
|
+
const steps = [];
|
|
68049
|
+
steps.push({
|
|
68050
|
+
role: "trigger",
|
|
68051
|
+
label: "Infrastructure component degraded (database, network, or cache)",
|
|
68052
|
+
evidenceIds: triggerEvIds.length > 0 ? triggerEvIds : ids(queueStateEvs),
|
|
68053
|
+
graphNodeId: firstImplicatedNode(graph, "collection") ?? firstImplicatedNode(graph, "database") ?? firstImplicatedNode(graph, "queue")
|
|
68054
|
+
});
|
|
68055
|
+
if (propagationEvIds.length > 0) {
|
|
68056
|
+
steps.push({
|
|
68057
|
+
role: "propagation",
|
|
68058
|
+
label: "Dependent services / workers lost access to the degraded component",
|
|
68059
|
+
evidenceIds: propagationEvIds,
|
|
68060
|
+
graphNodeId: firstImplicatedNode(graph, "worker") ?? firstImplicatedNode(graph, "service")
|
|
68061
|
+
});
|
|
68062
|
+
}
|
|
68063
|
+
if (symptomEvIds.length > 0) {
|
|
68064
|
+
steps.push({
|
|
68065
|
+
role: "symptom",
|
|
68066
|
+
label: "Processing failures and error signatures observed",
|
|
68067
|
+
evidenceIds: symptomEvIds,
|
|
68068
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68069
|
+
});
|
|
68070
|
+
}
|
|
68071
|
+
const summary = "Infrastructure degradation \u2192 services/workers affected" + (symptomEvIds.length > 0 ? " \u2192 processing failures" : "");
|
|
68072
|
+
return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
|
|
68073
|
+
}
|
|
68074
|
+
function chainForRetryStorm(hyp, byKind, graph) {
|
|
68075
|
+
const logEvs = byKind.get("log") ?? [];
|
|
68076
|
+
const queueStateEvs = byKind.get("queue-state") ?? [];
|
|
68077
|
+
const metricEvs = byKind.get("metric") ?? [];
|
|
68078
|
+
const steps = [];
|
|
68079
|
+
steps.push({
|
|
68080
|
+
role: "trigger",
|
|
68081
|
+
label: `Initial failure with automatic retry enabled (${logEvs.length} log signature(s) spiking)`,
|
|
68082
|
+
evidenceIds: ids(logEvs),
|
|
68083
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68084
|
+
});
|
|
68085
|
+
if (queueStateEvs.length > 0) {
|
|
68086
|
+
steps.push({
|
|
68087
|
+
role: "propagation",
|
|
68088
|
+
label: "Retry amplification growing queue depth and error volume",
|
|
68089
|
+
evidenceIds: ids(queueStateEvs),
|
|
68090
|
+
graphNodeId: firstImplicatedNode(graph, "queue")
|
|
68091
|
+
});
|
|
68092
|
+
}
|
|
68093
|
+
if (metricEvs.length > 0) {
|
|
68094
|
+
steps.push({
|
|
68095
|
+
role: "symptom",
|
|
68096
|
+
label: "Cascading load visible in metric anomalies",
|
|
68097
|
+
evidenceIds: ids(metricEvs),
|
|
68098
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68099
|
+
});
|
|
68100
|
+
}
|
|
68101
|
+
const summary = "Initial failure + retry \u2192 amplified load" + (metricEvs.length > 0 ? " \u2192 cascading metric anomalies" : "");
|
|
68102
|
+
return { hypothesisId: hyp.id, category: hyp.category, confidence: hyp.confidence, steps, summary };
|
|
68103
|
+
}
|
|
68104
|
+
function chainGeneric(hyp, byKind, graph) {
|
|
68105
|
+
const allEvIds = hyp.supportingEvidenceIds;
|
|
68106
|
+
const logOrMetric = [
|
|
68107
|
+
...byKind.get("log") ?? [],
|
|
68108
|
+
...byKind.get("metric") ?? []
|
|
68109
|
+
];
|
|
68110
|
+
const steps = [
|
|
68111
|
+
{
|
|
68112
|
+
role: "trigger",
|
|
68113
|
+
label: hyp.statement,
|
|
68114
|
+
evidenceIds: allEvIds,
|
|
68115
|
+
graphNodeId: firstImplicatedNode(graph, "service") ?? firstImplicatedNode(graph, "queue")
|
|
68116
|
+
}
|
|
68117
|
+
];
|
|
68118
|
+
if (logOrMetric.length > 0) {
|
|
68119
|
+
steps.push({
|
|
68120
|
+
role: "symptom",
|
|
68121
|
+
label: "Observable symptoms in logs / metrics",
|
|
68122
|
+
evidenceIds: ids(logOrMetric),
|
|
68123
|
+
graphNodeId: firstImplicatedNode(graph, "service")
|
|
68124
|
+
});
|
|
68125
|
+
}
|
|
68126
|
+
return {
|
|
68127
|
+
hypothesisId: hyp.id,
|
|
68128
|
+
category: hyp.category,
|
|
68129
|
+
confidence: hyp.confidence,
|
|
68130
|
+
steps,
|
|
68131
|
+
summary: hyp.statement
|
|
68132
|
+
};
|
|
68133
|
+
}
|
|
68134
|
+
function buildCauseChains(hypotheses2, evidence2, graph, seedLabel) {
|
|
68135
|
+
const evById = new Map(evidence2.map((e) => [e.id, e]));
|
|
68136
|
+
const chains = [];
|
|
68137
|
+
for (const hyp of hypotheses2) {
|
|
68138
|
+
if (hyp.verdict !== "supported" && hyp.verdict !== "weakened") continue;
|
|
68139
|
+
const byKind = groupByKind(hyp.supportingEvidenceIds, evById);
|
|
68140
|
+
let chain;
|
|
68141
|
+
switch (hyp.category) {
|
|
68142
|
+
case "deployment-regression":
|
|
68143
|
+
chain = chainForDeploymentRegression(hyp, byKind, graph, seedLabel);
|
|
68144
|
+
break;
|
|
68145
|
+
case "queue-backlog": {
|
|
68146
|
+
const queueName = extractQueueName(hyp.statement);
|
|
68147
|
+
chain = chainForQueueBacklog(hyp, byKind, graph, queueName);
|
|
68148
|
+
break;
|
|
68149
|
+
}
|
|
68150
|
+
case "worker-slowdown": {
|
|
68151
|
+
const queueName = extractQueueName(hyp.statement);
|
|
68152
|
+
chain = chainForWorkerSlowdown(hyp, byKind, graph, queueName);
|
|
68153
|
+
break;
|
|
68154
|
+
}
|
|
68155
|
+
case "external-api-latency":
|
|
68156
|
+
chain = chainForExternalApiLatency(hyp, byKind, graph);
|
|
68157
|
+
break;
|
|
68158
|
+
case "infrastructure":
|
|
68159
|
+
chain = chainForInfrastructure(hyp, byKind, graph);
|
|
68160
|
+
break;
|
|
68161
|
+
case "retry-storm":
|
|
68162
|
+
chain = chainForRetryStorm(hyp, byKind, graph);
|
|
68163
|
+
break;
|
|
68164
|
+
default:
|
|
68165
|
+
chain = chainGeneric(hyp, byKind, graph);
|
|
68166
|
+
}
|
|
68167
|
+
chains.push(chain);
|
|
68168
|
+
}
|
|
68169
|
+
return chains;
|
|
68170
|
+
}
|
|
68171
|
+
function extractQueueName(statement) {
|
|
68172
|
+
const m = /\bon ([a-zA-Z0-9_-]+)/.exec(statement) ?? /consuming ([a-zA-Z0-9_-]+)/.exec(statement);
|
|
68173
|
+
return m?.[1];
|
|
68174
|
+
}
|
|
67373
68175
|
|
|
67374
68176
|
// ../../packages/engine/src/score-cause.ts
|
|
67375
68177
|
init_cjs_shims();
|
|
@@ -67632,6 +68434,15 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
67632
68434
|
const queueEvs = evidence2.filter((e) => e.kind === "queue-edge");
|
|
67633
68435
|
const hasCommit = commitEvs.length > 0;
|
|
67634
68436
|
const { queues } = ctx;
|
|
68437
|
+
const logSpikeEvIds = evidence2.filter((e) => e.kind === "log" && e.ratio !== void 0 && e.ratio >= 2).map((e) => e.id);
|
|
68438
|
+
const stateEvIds = evidence2.filter((e) => e.kind === "state").map((e) => e.id);
|
|
68439
|
+
const allQueueBacklogEvIds = [
|
|
68440
|
+
...ctx.queueBacklogEvIdsByQueue?.values() ?? []
|
|
68441
|
+
].flat();
|
|
68442
|
+
const allQueueStarvationEvIds = [
|
|
68443
|
+
...ctx.queueStarvationEvIdsByQueue?.values() ?? []
|
|
68444
|
+
].flat();
|
|
68445
|
+
const graphImplicatedCollectionEvIds = ctx.graph ? implicatedEvidenceIdsByNodeType(ctx.graph, "collection") : [];
|
|
67635
68446
|
const hyps = [];
|
|
67636
68447
|
hyps.push({
|
|
67637
68448
|
id: globalThis.crypto.randomUUID(),
|
|
@@ -67640,9 +68451,7 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
67640
68451
|
confidence: hasCommit ? 0.5 : 0.15,
|
|
67641
68452
|
supportingEvidenceIds: commitEvs.map((e) => e.id),
|
|
67642
68453
|
contradictingEvidenceIds: [],
|
|
67643
|
-
missingEvidence: hasCommit ? [] : [
|
|
67644
|
-
"A change/deployment range \u2014 re-run with --since <ref> to diff what shipped"
|
|
67645
|
-
]
|
|
68454
|
+
missingEvidence: hasCommit ? [] : ctx.sinceProvided ? ["No git changes found in the specified range \u2014 verify the ref is accessible or use HEAD~N for exact commit ranges"] : ["A change/deployment range \u2014 re-run with --since <ref> to diff what shipped"]
|
|
67646
68455
|
});
|
|
67647
68456
|
for (const queueName of queues) {
|
|
67648
68457
|
const backlogEvIds = ctx.queueBacklogEvIdsByQueue?.get(queueName) ?? [];
|
|
@@ -67678,25 +68487,32 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
67678
68487
|
contradictingEvidenceIds: [],
|
|
67679
68488
|
missingEvidence: latencyMetricEvIds.length > 0 ? [] : ["Request latency metrics (Grafana) + error logs (Elasticsearch)"]
|
|
67680
68489
|
});
|
|
68490
|
+
const retryStormSupport = [.../* @__PURE__ */ new Set([...logSpikeEvIds, ...allQueueBacklogEvIds])];
|
|
67681
68491
|
hyps.push({
|
|
67682
68492
|
id: globalThis.crypto.randomUUID(),
|
|
67683
68493
|
category: "retry-storm",
|
|
67684
68494
|
statement: "A retry storm is amplifying load on the failing path.",
|
|
67685
|
-
confidence: 0.15,
|
|
67686
|
-
supportingEvidenceIds:
|
|
67687
|
-
contradictingEvidenceIds:
|
|
67688
|
-
missingEvidence: [
|
|
67689
|
-
"Retry/error logs + queue retry statistics (Elasticsearch + BullMQ)"
|
|
67690
|
-
]
|
|
68495
|
+
confidence: retryStormSupport.length > 0 ? 0.35 : 0.15,
|
|
68496
|
+
supportingEvidenceIds: retryStormSupport,
|
|
68497
|
+
contradictingEvidenceIds: allQueueStarvationEvIds,
|
|
68498
|
+
missingEvidence: retryStormSupport.length > 0 ? [] : ["Retry/error logs + queue retry statistics (Elasticsearch + BullMQ)"]
|
|
67691
68499
|
});
|
|
68500
|
+
const infraSupport = [
|
|
68501
|
+
.../* @__PURE__ */ new Set([
|
|
68502
|
+
...stateEvIds,
|
|
68503
|
+
...allQueueStarvationEvIds,
|
|
68504
|
+
...ctx.latencyMetricEvIds ?? [],
|
|
68505
|
+
...graphImplicatedCollectionEvIds
|
|
68506
|
+
])
|
|
68507
|
+
];
|
|
67692
68508
|
hyps.push({
|
|
67693
68509
|
id: globalThis.crypto.randomUUID(),
|
|
67694
68510
|
category: "infrastructure",
|
|
67695
68511
|
statement: "An infrastructure issue (database, Redis, or network) is degrading processing.",
|
|
67696
|
-
confidence: 0.15,
|
|
67697
|
-
supportingEvidenceIds:
|
|
68512
|
+
confidence: infraSupport.length > 0 ? 0.35 : 0.15,
|
|
68513
|
+
supportingEvidenceIds: infraSupport,
|
|
67698
68514
|
contradictingEvidenceIds: [],
|
|
67699
|
-
missingEvidence: ["Infra/Redis metrics (Grafana) + Redis state"]
|
|
68515
|
+
missingEvidence: infraSupport.length > 0 ? [] : ["Infra/Redis metrics (Grafana) + Redis state"]
|
|
67700
68516
|
});
|
|
67701
68517
|
hyps.sort((a, b2) => b2.confidence - a.confidence);
|
|
67702
68518
|
return hyps;
|
|
@@ -67917,7 +68733,8 @@ function detectMissingEvidence(r, connectors = {}) {
|
|
|
67917
68733
|
blindSpots.push("Cannot see the real error.");
|
|
67918
68734
|
}
|
|
67919
68735
|
if (!hasMetric && !(connectors.grafana && connectors.metricsCollected)) {
|
|
67920
|
-
const
|
|
68736
|
+
const failureDetail = connectors.metricsFailureReason ? ` (${connectors.metricsFailureReason})` : "";
|
|
68737
|
+
const metricsWhy = !connectors.grafana ? "No Grafana connector configured \u2014 cannot see latency/error-rate trends." : `Grafana metrics collection failed or timed out${failureDetail} \u2014 metric trends unavailable for this investigation.`;
|
|
67921
68738
|
const metricsNextSource = !connectors.grafana ? "Add a `grafana` connector to the environment" : 'Check Grafana connectivity, then run `horus metrics "<hint>"` manually';
|
|
67922
68739
|
gaps.push({
|
|
67923
68740
|
dimension: "metrics",
|
|
@@ -67931,7 +68748,7 @@ function detectMissingEvidence(r, connectors = {}) {
|
|
|
67931
68748
|
gaps.push({
|
|
67932
68749
|
dimension: "queue runtime state",
|
|
67933
68750
|
why: connectors.redis ? "Queue topology is known but live depth + failed/delayed counts were not collected." : "Queue topology is known but there is no Redis/BullMQ connector for live depth/failures.",
|
|
67934
|
-
nextSource: connectors.redis ? "
|
|
68751
|
+
nextSource: connectors.redis ? "Run `horus queues --live` to see real-time queue depths and failed-job counts" : "Add a `redis` connector to read live BullMQ state",
|
|
67935
68752
|
confidenceImpact: 0.1
|
|
67936
68753
|
});
|
|
67937
68754
|
blindSpots.push("Cannot determine if the queue is actually backed up.");
|
|
@@ -67939,8 +68756,8 @@ function detectMissingEvidence(r, connectors = {}) {
|
|
|
67939
68756
|
if (!hasCommit) {
|
|
67940
68757
|
gaps.push({
|
|
67941
68758
|
dimension: "deployment records",
|
|
67942
|
-
why: "No deployment/change data in scope \u2014 cannot tell what shipped before the incident.",
|
|
67943
|
-
nextSource: "Re-run with --since <ref>, or `horus what-changed <service>`",
|
|
68759
|
+
why: connectors.sinceProvided ? "No git changes found in the specified range \u2014 the ref may not be diffable or no commits fall in this window." : "No deployment/change data in scope \u2014 cannot tell what shipped before the incident.",
|
|
68760
|
+
nextSource: connectors.sinceProvided ? "Use HEAD~N or a specific SHA/branch for git diff ranges (e.g. --since HEAD~5)" : "Re-run with --since <ref>, or `horus what-changed <service>`",
|
|
67944
68761
|
confidenceImpact: 0.08
|
|
67945
68762
|
});
|
|
67946
68763
|
blindSpots.push("Cannot correlate with a recent change.");
|
|
@@ -68117,33 +68934,33 @@ function buildGroups(evidence2) {
|
|
|
68117
68934
|
}
|
|
68118
68935
|
}
|
|
68119
68936
|
const raw = [];
|
|
68120
|
-
for (const [key,
|
|
68121
|
-
if (
|
|
68937
|
+
for (const [key, ids2] of symbolMap) {
|
|
68938
|
+
if (ids2.length >= 2) {
|
|
68122
68939
|
raw.push({
|
|
68123
68940
|
key,
|
|
68124
68941
|
dimension: "symbol",
|
|
68125
68942
|
reason: `Share symbol ${key}`,
|
|
68126
|
-
evidenceIds: [...
|
|
68943
|
+
evidenceIds: [...ids2]
|
|
68127
68944
|
});
|
|
68128
68945
|
}
|
|
68129
68946
|
}
|
|
68130
|
-
for (const [key,
|
|
68131
|
-
if (
|
|
68947
|
+
for (const [key, ids2] of fileMap) {
|
|
68948
|
+
if (ids2.length >= 2) {
|
|
68132
68949
|
raw.push({
|
|
68133
68950
|
key,
|
|
68134
68951
|
dimension: "file",
|
|
68135
68952
|
reason: `Share file ${key}`,
|
|
68136
|
-
evidenceIds: [...
|
|
68953
|
+
evidenceIds: [...ids2]
|
|
68137
68954
|
});
|
|
68138
68955
|
}
|
|
68139
68956
|
}
|
|
68140
|
-
for (const [key,
|
|
68141
|
-
if (
|
|
68957
|
+
for (const [key, ids2] of queueMap) {
|
|
68958
|
+
if (ids2.length >= 2) {
|
|
68142
68959
|
raw.push({
|
|
68143
68960
|
key,
|
|
68144
68961
|
dimension: "queue",
|
|
68145
68962
|
reason: `Share queue ${key}`,
|
|
68146
|
-
evidenceIds: [...
|
|
68963
|
+
evidenceIds: [...ids2]
|
|
68147
68964
|
});
|
|
68148
68965
|
}
|
|
68149
68966
|
}
|
|
@@ -68167,13 +68984,13 @@ function buildChains(evidence2) {
|
|
|
68167
68984
|
const commitEvs = evidence2.filter((e) => e.kind === "commit");
|
|
68168
68985
|
const symbolEvs = evidence2.filter((e) => e.kind === "symbol");
|
|
68169
68986
|
const relevanceById = new Map(evidence2.map((e) => [e.id, e.relevance]));
|
|
68170
|
-
function avgRelevance(
|
|
68171
|
-
if (
|
|
68987
|
+
function avgRelevance(ids2) {
|
|
68988
|
+
if (ids2.length === 0) return 0;
|
|
68172
68989
|
let sum = 0;
|
|
68173
|
-
for (const id of
|
|
68990
|
+
for (const id of ids2) {
|
|
68174
68991
|
sum += relevanceById.get(id) ?? 0;
|
|
68175
68992
|
}
|
|
68176
|
-
return sum /
|
|
68993
|
+
return sum / ids2.length;
|
|
68177
68994
|
}
|
|
68178
68995
|
const chains = [];
|
|
68179
68996
|
if (queueEdges2.length > 0) {
|
|
@@ -68277,7 +69094,7 @@ function seedRole(s) {
|
|
|
68277
69094
|
if (/util|helper/i.test(hay)) return "util";
|
|
68278
69095
|
return "code";
|
|
68279
69096
|
}
|
|
68280
|
-
function scoreSeed(s, index2) {
|
|
69097
|
+
function scoreSeed(s, index2, hintTokens) {
|
|
68281
69098
|
const hay = `${s.name} ${s.filePath}`.toLowerCase();
|
|
68282
69099
|
let score = 0;
|
|
68283
69100
|
if (PREFER.test(hay)) score += 3;
|
|
@@ -68285,10 +69102,20 @@ function scoreSeed(s, index2) {
|
|
|
68285
69102
|
if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s.filePath)) score += 2;
|
|
68286
69103
|
if (/(^|\/)scripts?\//i.test(s.filePath)) score -= 2;
|
|
68287
69104
|
score += Math.max(0, 5 - index2) * 0.1;
|
|
69105
|
+
if (hintTokens !== void 0 && hintTokens.length > 0) {
|
|
69106
|
+
let hintBoost = 0;
|
|
69107
|
+
for (const tok of hintTokens) {
|
|
69108
|
+
if (hay.includes(tok)) {
|
|
69109
|
+
hintBoost += 2;
|
|
69110
|
+
if (hintBoost >= 6) break;
|
|
69111
|
+
}
|
|
69112
|
+
}
|
|
69113
|
+
score += hintBoost;
|
|
69114
|
+
}
|
|
68288
69115
|
return score;
|
|
68289
69116
|
}
|
|
68290
|
-
function rankSeeds(seeds) {
|
|
68291
|
-
return seeds.map((symbol, i) => ({ symbol, score: scoreSeed(symbol, i), role: seedRole(symbol), i })).sort((a, b2) => b2.score === a.score ? a.i - b2.i : b2.score - a.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
|
|
69117
|
+
function rankSeeds(seeds, hintTokens) {
|
|
69118
|
+
return seeds.map((symbol, i) => ({ symbol, score: scoreSeed(symbol, i, hintTokens), role: seedRole(symbol), i })).sort((a, b2) => b2.score === a.score ? a.i - b2.i : b2.score - a.score).map(({ symbol, score, role }) => ({ symbol, score, role }));
|
|
68292
69119
|
}
|
|
68293
69120
|
|
|
68294
69121
|
// ../../packages/engine/src/normalize.ts
|
|
@@ -68348,6 +69175,7 @@ var RUNTIME_KINDS2 = /* @__PURE__ */ new Set([
|
|
|
68348
69175
|
]);
|
|
68349
69176
|
var MAX_RUNTIME_CONTRIBUTION = 2;
|
|
68350
69177
|
var MAX_STRUCTURAL_CONTRIBUTION = 0.6;
|
|
69178
|
+
var MAX_AMBIENT_RUNTIME_CONTRIBUTION = 0.6;
|
|
68351
69179
|
var NORMALIZATION = 6;
|
|
68352
69180
|
function clamp014(n) {
|
|
68353
69181
|
if (Number.isNaN(n)) return 0;
|
|
@@ -68355,13 +69183,18 @@ function clamp014(n) {
|
|
|
68355
69183
|
if (n > 1) return 1;
|
|
68356
69184
|
return n;
|
|
68357
69185
|
}
|
|
68358
|
-
function computeWeightedEvidenceConfidence(evidence2) {
|
|
69186
|
+
function computeWeightedEvidenceConfidence(evidence2, ambientEvidenceIds) {
|
|
68359
69187
|
const runtimeBySource = /* @__PURE__ */ new Map();
|
|
69188
|
+
const ambientBySource = /* @__PURE__ */ new Map();
|
|
68360
69189
|
const structuralBySource = /* @__PURE__ */ new Map();
|
|
68361
69190
|
for (const e of evidence2) {
|
|
68362
69191
|
const r = clamp014(e.relevance);
|
|
68363
69192
|
if (RUNTIME_KINDS2.has(e.kind)) {
|
|
68364
|
-
|
|
69193
|
+
if (ambientEvidenceIds?.has(e.id)) {
|
|
69194
|
+
ambientBySource.set(e.source, (ambientBySource.get(e.source) ?? 0) + 0.5 * r);
|
|
69195
|
+
} else {
|
|
69196
|
+
runtimeBySource.set(e.source, (runtimeBySource.get(e.source) ?? 0) + 1.5 * r);
|
|
69197
|
+
}
|
|
68365
69198
|
} else {
|
|
68366
69199
|
structuralBySource.set(e.source, (structuralBySource.get(e.source) ?? 0) + 0.5 * r);
|
|
68367
69200
|
}
|
|
@@ -68370,11 +69203,15 @@ function computeWeightedEvidenceConfidence(evidence2) {
|
|
|
68370
69203
|
(acc, w) => acc + Math.min(w, MAX_RUNTIME_CONTRIBUTION),
|
|
68371
69204
|
0
|
|
68372
69205
|
);
|
|
69206
|
+
const ambientSum = [...ambientBySource.values()].reduce(
|
|
69207
|
+
(acc, w) => acc + Math.min(w, MAX_AMBIENT_RUNTIME_CONTRIBUTION),
|
|
69208
|
+
0
|
|
69209
|
+
);
|
|
68373
69210
|
const structuralSum = [...structuralBySource.values()].reduce(
|
|
68374
69211
|
(acc, w) => acc + Math.min(w, MAX_STRUCTURAL_CONTRIBUTION),
|
|
68375
69212
|
0
|
|
68376
69213
|
);
|
|
68377
|
-
return clamp014((runtimeSum + structuralSum) / NORMALIZATION);
|
|
69214
|
+
return clamp014((runtimeSum + ambientSum + structuralSum) / NORMALIZATION);
|
|
68378
69215
|
}
|
|
68379
69216
|
|
|
68380
69217
|
// ../../packages/engine/src/git-collector.ts
|
|
@@ -68390,8 +69227,9 @@ function isRefLike(s) {
|
|
|
68390
69227
|
if (t.startsWith("-")) return false;
|
|
68391
69228
|
if (t.includes("..")) return false;
|
|
68392
69229
|
if (/\s|:|ago|yesterday|week|month|year|=/i.test(t)) return false;
|
|
69230
|
+
if (/^\d+[smhd]$/i.test(t)) return false;
|
|
68393
69231
|
if (/^[0-9a-f]{7,40}$/i.test(t)) return true;
|
|
68394
|
-
if (/^[a-zA-Z0-9_
|
|
69232
|
+
if (/^[a-zA-Z0-9_.~^/-]{1,200}$/.test(t) && !/^\d{4}-\d{2}-\d{2}$/.test(t)) return true;
|
|
68395
69233
|
return false;
|
|
68396
69234
|
}
|
|
68397
69235
|
function parseDiffStat(stdout) {
|
|
@@ -68610,6 +69448,33 @@ function logWindowFrom(since) {
|
|
|
68610
69448
|
}
|
|
68611
69449
|
return new Date(now - 7 * 864e5).toISOString();
|
|
68612
69450
|
}
|
|
69451
|
+
function classifyLogRelevance(sigKey, sigServices, seedTerms, inputService) {
|
|
69452
|
+
if (seedTerms.length === 0) {
|
|
69453
|
+
return { relevanceClass: "direct", relevanceReason: "no seed context \u2014 included by default" };
|
|
69454
|
+
}
|
|
69455
|
+
const keyTokens = tokenize(sigKey);
|
|
69456
|
+
const serviceTokens = sigServices.flatMap((s) => tokenize(s));
|
|
69457
|
+
const combined = [...keyTokens, ...serviceTokens];
|
|
69458
|
+
const matchingTerms = seedTerms.filter(
|
|
69459
|
+
(t) => combined.some((c) => c.includes(t) || t.includes(c))
|
|
69460
|
+
);
|
|
69461
|
+
if (matchingTerms.length > 0) {
|
|
69462
|
+
return {
|
|
69463
|
+
relevanceClass: "direct",
|
|
69464
|
+
relevanceReason: `matches seed terms: ${matchingTerms.slice(0, 3).join(", ")}`
|
|
69465
|
+
};
|
|
69466
|
+
}
|
|
69467
|
+
if (inputService && sigServices.some((s) => s.toLowerCase().includes(inputService.toLowerCase()))) {
|
|
69468
|
+
return {
|
|
69469
|
+
relevanceClass: "direct",
|
|
69470
|
+
relevanceReason: `from configured service: ${inputService}`
|
|
69471
|
+
};
|
|
69472
|
+
}
|
|
69473
|
+
return {
|
|
69474
|
+
relevanceClass: "ambient",
|
|
69475
|
+
relevanceReason: "no structural link to seed \u2014 ambient runtime noise"
|
|
69476
|
+
};
|
|
69477
|
+
}
|
|
68613
69478
|
function queueFindingConfidence(opts) {
|
|
68614
69479
|
const { starvedCount, backloggedCount, failingCount } = opts;
|
|
68615
69480
|
return starvedCount > 0 && backloggedCount === 0 && failingCount === 0 ? 0.65 : 0.85;
|
|
@@ -68618,7 +69483,8 @@ function looksDiffable(since) {
|
|
|
68618
69483
|
const s = since.trim();
|
|
68619
69484
|
if (s.length === 0) return false;
|
|
68620
69485
|
if (s.includes("..")) return true;
|
|
68621
|
-
|
|
69486
|
+
if (/^\d+[smhd]$/i.test(s)) return false;
|
|
69487
|
+
return /^[A-Za-z0-9._/~^-]+$/.test(s);
|
|
68622
69488
|
}
|
|
68623
69489
|
async function investigate(input, deps) {
|
|
68624
69490
|
const { code, db } = deps;
|
|
@@ -68641,7 +69507,8 @@ async function investigate(input, deps) {
|
|
|
68641
69507
|
return ev;
|
|
68642
69508
|
}
|
|
68643
69509
|
const rawSeeds = await code.searchSymbols(hint, 5);
|
|
68644
|
-
const
|
|
69510
|
+
const hintTokens = [...new Set(tokenize(hint))];
|
|
69511
|
+
const ranked = rankSeeds(rawSeeds, hintTokens);
|
|
68645
69512
|
const seeds = ranked.map((r) => r.symbol);
|
|
68646
69513
|
const top = seeds[0];
|
|
68647
69514
|
if (!top) {
|
|
@@ -68693,6 +69560,13 @@ async function investigate(input, deps) {
|
|
|
68693
69560
|
changes = null;
|
|
68694
69561
|
}
|
|
68695
69562
|
}
|
|
69563
|
+
let recentChanges;
|
|
69564
|
+
if (deps.repoPath && input.since) {
|
|
69565
|
+
try {
|
|
69566
|
+
recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
|
|
69567
|
+
} catch {
|
|
69568
|
+
}
|
|
69569
|
+
}
|
|
68696
69570
|
const seedLine = top.startLine ?? 0;
|
|
68697
69571
|
const seedEv = mkEv(
|
|
68698
69572
|
"symbol",
|
|
@@ -68750,8 +69624,20 @@ async function investigate(input, deps) {
|
|
|
68750
69624
|
);
|
|
68751
69625
|
changeEvId = ev.id;
|
|
68752
69626
|
}
|
|
69627
|
+
if (changes === null && recentChanges !== void 0 && recentChanges.commits.length > 0) {
|
|
69628
|
+
const { commits, changedFiles, totalInsertions, totalDeletions } = recentChanges;
|
|
69629
|
+
const ev = mkEv(
|
|
69630
|
+
"commit",
|
|
69631
|
+
`Git history ${input.since}..HEAD: ${commits.length} commit(s), ${changedFiles.length} file(s) changed (+${totalInsertions} -${totalDeletions})`,
|
|
69632
|
+
{ commits: commits.slice(0, 10), changedFiles: changedFiles.slice(0, 30), totalInsertions, totalDeletions, source: "git" },
|
|
69633
|
+
{}
|
|
69634
|
+
);
|
|
69635
|
+
changeEvId = ev.id;
|
|
69636
|
+
}
|
|
68753
69637
|
let analysis = null;
|
|
68754
69638
|
const logEvIds = [];
|
|
69639
|
+
const directLogEvIds = [];
|
|
69640
|
+
const ambientLogEvIds = [];
|
|
68755
69641
|
let logsCollected = false;
|
|
68756
69642
|
let logsCompatibilityError;
|
|
68757
69643
|
if (deps.logs) {
|
|
@@ -68768,8 +69654,19 @@ async function investigate(input, deps) {
|
|
|
68768
69654
|
const from = logWindowFrom(input.since);
|
|
68769
69655
|
analysis = await deps.logs.analyzeErrors({ service: input.service, from });
|
|
68770
69656
|
logsCollected = true;
|
|
69657
|
+
const seedBase = top.filePath.split("/").pop() ?? "";
|
|
69658
|
+
const logSeedTerms = [
|
|
69659
|
+
.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
|
|
69660
|
+
];
|
|
68771
69661
|
for (const s of analysis.signatures.slice(0, 15)) {
|
|
69662
|
+
const { relevanceClass, relevanceReason } = classifyLogRelevance(
|
|
69663
|
+
s.key,
|
|
69664
|
+
s.services,
|
|
69665
|
+
logSeedTerms,
|
|
69666
|
+
input.service
|
|
69667
|
+
);
|
|
68772
69668
|
const tags = [];
|
|
69669
|
+
if (relevanceClass === "ambient") tags.push("ambient");
|
|
68773
69670
|
if (s.isNew) tags.push("NEW");
|
|
68774
69671
|
else if (s.ratio !== void 0 && Number.isFinite(s.ratio) && s.ratio >= 1.5) {
|
|
68775
69672
|
tags.push(`spike x${s.ratio.toFixed(1)}`);
|
|
@@ -68790,15 +69687,21 @@ async function investigate(input, deps) {
|
|
|
68790
69687
|
services: s.services,
|
|
68791
69688
|
isNew: s.isNew ?? false,
|
|
68792
69689
|
ratio: s.ratio ?? null,
|
|
68793
|
-
sampleMessage: s.sampleMessage ?? null
|
|
69690
|
+
sampleMessage: s.sampleMessage ?? null,
|
|
69691
|
+
relevanceClass,
|
|
69692
|
+
relevanceReason
|
|
68794
69693
|
},
|
|
68795
69694
|
{},
|
|
68796
69695
|
s.lastSeen || void 0,
|
|
68797
|
-
|
|
69696
|
+
// Direct evidence: full recurrence weight. Ambient: demoted baseline.
|
|
69697
|
+
// This prevents unrelated high-volume errors from inflating confidence.
|
|
69698
|
+
relevanceClass === "direct" ? s.isNew ? 0.95 : s.ratio !== void 0 && s.ratio >= 1.5 ? 0.9 : 0.85 : s.isNew ? 0.7 : s.ratio !== void 0 && s.ratio >= 1.5 ? 0.55 : 0.35
|
|
68798
69699
|
);
|
|
68799
69700
|
if (s.isNew) ev.isNew = s.isNew;
|
|
68800
69701
|
if (typeof s.ratio === "number" && Number.isFinite(s.ratio)) ev.ratio = s.ratio;
|
|
68801
69702
|
logEvIds.push(ev.id);
|
|
69703
|
+
if (relevanceClass === "direct") directLogEvIds.push(ev.id);
|
|
69704
|
+
else ambientLogEvIds.push(ev.id);
|
|
68802
69705
|
}
|
|
68803
69706
|
}
|
|
68804
69707
|
} catch {
|
|
@@ -68811,11 +69714,8 @@ async function investigate(input, deps) {
|
|
|
68811
69714
|
if (deps.mongo) {
|
|
68812
69715
|
try {
|
|
68813
69716
|
stateAnalysis = await deps.mongo.analyzeState();
|
|
68814
|
-
const
|
|
68815
|
-
const
|
|
68816
|
-
.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
|
|
68817
|
-
];
|
|
68818
|
-
for (const s of selectStateSignals(stateAnalysis, terms)) {
|
|
69717
|
+
const stateTerms = [...new Set(tokenize(hint))];
|
|
69718
|
+
for (const s of selectStateSignals(stateAnalysis, stateTerms)) {
|
|
68819
69719
|
const ev = mkEv("state", s.title, s.payload, {}, s.timestamp, s.relevance);
|
|
68820
69720
|
stateEvIds.push(ev.id);
|
|
68821
69721
|
stateCollections.add(s.collection);
|
|
@@ -68853,12 +69753,13 @@ async function investigate(input, deps) {
|
|
|
68853
69753
|
queueRuntimeState = null;
|
|
68854
69754
|
}
|
|
68855
69755
|
}
|
|
68856
|
-
const METRICS_TIMEOUT_MS =
|
|
69756
|
+
const METRICS_TIMEOUT_MS = 3e4;
|
|
68857
69757
|
const metricEvIds = [];
|
|
68858
69758
|
const latencyMetricEvIds = [];
|
|
68859
69759
|
const queueMetricEvIds = [];
|
|
68860
69760
|
const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
68861
69761
|
let metricsCollected = false;
|
|
69762
|
+
let metricsFailureReason;
|
|
68862
69763
|
if (deps.metrics) {
|
|
68863
69764
|
const ac = new AbortController();
|
|
68864
69765
|
let metricsTimerId;
|
|
@@ -68919,7 +69820,8 @@ async function investigate(input, deps) {
|
|
|
68919
69820
|
}
|
|
68920
69821
|
}
|
|
68921
69822
|
metricsCollected = true;
|
|
68922
|
-
} catch {
|
|
69823
|
+
} catch (metricsErr) {
|
|
69824
|
+
metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
|
|
68923
69825
|
} finally {
|
|
68924
69826
|
if (metricsTimerId !== void 0) clearTimeout(metricsTimerId);
|
|
68925
69827
|
}
|
|
@@ -68947,9 +69849,12 @@ async function investigate(input, deps) {
|
|
|
68947
69849
|
latencyMetricEvIds,
|
|
68948
69850
|
queueBacklogEvIdsByQueue,
|
|
68949
69851
|
queueStarvationEvIdsByQueue,
|
|
68950
|
-
queueMetricEvIdsByQueue
|
|
69852
|
+
queueMetricEvIdsByQueue,
|
|
69853
|
+
sinceProvided: input.since !== void 0,
|
|
69854
|
+
graph
|
|
68951
69855
|
});
|
|
68952
69856
|
const validated = validateHypotheses(hyps, evidence2);
|
|
69857
|
+
const causeChains = buildCauseChains(validated, evidence2, graph, label);
|
|
68953
69858
|
const findings2 = [];
|
|
68954
69859
|
findings2.push({
|
|
68955
69860
|
kind: "observation",
|
|
@@ -69004,24 +69909,43 @@ async function investigate(input, deps) {
|
|
|
69004
69909
|
confidence: clamp015(0.4 + Math.min(m, 20) / 40),
|
|
69005
69910
|
evidenceIds: [changeEvId]
|
|
69006
69911
|
});
|
|
69912
|
+
} else if (!changes && changeEvId && recentChanges) {
|
|
69913
|
+
findings2.push({
|
|
69914
|
+
kind: "observation",
|
|
69915
|
+
title: `${recentChanges.commits.length} commit(s) in ${input.since}..HEAD touching ${recentChanges.changedFiles.length} file(s)`,
|
|
69916
|
+
confidence: clamp015(0.3 + Math.min(recentChanges.commits.length, 10) / 20),
|
|
69917
|
+
evidenceIds: [changeEvId]
|
|
69918
|
+
});
|
|
69007
69919
|
}
|
|
69008
69920
|
if (analysis !== null && analysis.signatures.length > 0) {
|
|
69009
69921
|
const newN = analysis.newSignatures.length;
|
|
69010
69922
|
const affected = analysis.affectedServices.length > 0 ? analysis.affectedServices.join(", ") : input.service ?? "the service";
|
|
69923
|
+
const hasDirectEvidence = directLogEvIds.length > 0;
|
|
69924
|
+
const ambientCount = ambientLogEvIds.length;
|
|
69925
|
+
const ambientSuffix = !hasDirectEvidence && ambientCount > 0 ? ` (${ambientCount} ambient \u2014 no direct link to seed established)` : ambientCount > 0 ? ` (${directLogEvIds.length} direct, ${ambientCount} ambient)` : "";
|
|
69011
69926
|
findings2.push({
|
|
69012
69927
|
kind: "observation",
|
|
69013
|
-
title: `${analysis.signatures.length} error signature(s) (${newN} new, ${analysis.totalErrors} error(s)) \u2014 affected: ${affected}`,
|
|
69014
|
-
confidence: 0.65,
|
|
69928
|
+
title: `${analysis.signatures.length} error signature(s) (${newN} new, ${analysis.totalErrors} error(s)) \u2014 affected: ${affected}${ambientSuffix}`,
|
|
69929
|
+
confidence: hasDirectEvidence ? 0.65 : 0.4,
|
|
69015
69930
|
evidenceIds: logEvIds
|
|
69016
69931
|
});
|
|
69017
|
-
const
|
|
69018
|
-
|
|
69019
|
-
|
|
69932
|
+
const topDirectId = directLogEvIds[0];
|
|
69933
|
+
const topSig = analysis.signatures[0];
|
|
69934
|
+
if (topDirectId !== void 0 && topSig !== void 0) {
|
|
69935
|
+
const flag = topSig.isNew ? " (NEW)" : topSig.ratio !== void 0 && Number.isFinite(topSig.ratio) && topSig.ratio >= 1.5 ? ` (spike x${topSig.ratio.toFixed(1)})` : "";
|
|
69020
69936
|
findings2.push({
|
|
69021
69937
|
kind: "anomaly",
|
|
69022
|
-
title: `Top error signature: ${
|
|
69938
|
+
title: `Top error signature: ${topSig.key} \u2014 ${topSig.count}x${flag}, last ${shortTs(topSig.lastSeen)}`,
|
|
69023
69939
|
confidence: 0.7,
|
|
69024
|
-
evidenceIds:
|
|
69940
|
+
evidenceIds: [topDirectId]
|
|
69941
|
+
});
|
|
69942
|
+
} else if (topSig !== void 0 && ambientCount > 0) {
|
|
69943
|
+
const flag = topSig.isNew ? " (NEW)" : topSig.ratio !== void 0 && Number.isFinite(topSig.ratio) && topSig.ratio >= 1.5 ? ` (spike x${topSig.ratio.toFixed(1)})` : "";
|
|
69944
|
+
findings2.push({
|
|
69945
|
+
kind: "observation",
|
|
69946
|
+
title: `Top error signature (ambient): ${topSig.key} \u2014 ${topSig.count}x${flag} \u2014 no structural link to seed`,
|
|
69947
|
+
confidence: 0.35,
|
|
69948
|
+
evidenceIds: ambientLogEvIds.slice(0, 1)
|
|
69025
69949
|
});
|
|
69026
69950
|
}
|
|
69027
69951
|
}
|
|
@@ -69119,13 +70043,17 @@ async function investigate(input, deps) {
|
|
|
69119
70043
|
metadata: { blastRadius }
|
|
69120
70044
|
});
|
|
69121
70045
|
}
|
|
69122
|
-
if (
|
|
70046
|
+
if (changeEvId) {
|
|
70047
|
+
const gitChangedFiles = recentChanges?.changedFiles ?? [];
|
|
70048
|
+
const seedInChanges = gitChangedFiles.length > 0 && gitChangedFiles.some(
|
|
70049
|
+
(f) => f === top.filePath || top.filePath.endsWith(f) || f.endsWith(top.filePath)
|
|
70050
|
+
);
|
|
69123
70051
|
causeInputs.push({
|
|
69124
70052
|
id: "cause:deployment-regression",
|
|
69125
70053
|
title: `Recent change to ${top.name} in ${input.since}..HEAD may have introduced the regression`,
|
|
69126
70054
|
category: "deployment-regression",
|
|
69127
70055
|
sourceEvidenceIds: [changeEvId, seedEv.id],
|
|
69128
|
-
baseScore: clamp015(0.25 + (queueHits.length > 0 ? 0.05 : 0)),
|
|
70056
|
+
baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
|
|
69129
70057
|
metadata: { blastRadius }
|
|
69130
70058
|
});
|
|
69131
70059
|
}
|
|
@@ -69139,7 +70067,7 @@ async function investigate(input, deps) {
|
|
|
69139
70067
|
metadata: { blastRadius }
|
|
69140
70068
|
});
|
|
69141
70069
|
}
|
|
69142
|
-
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0) {
|
|
70070
|
+
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
|
|
69143
70071
|
const firstQueue = queueHits[0];
|
|
69144
70072
|
const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
|
|
69145
70073
|
const topSig = analysis.signatures[0];
|
|
@@ -69147,7 +70075,7 @@ async function investigate(input, deps) {
|
|
|
69147
70075
|
id: "cause:error-correlation",
|
|
69148
70076
|
title: `Runtime errors (${analysis.totalErrors}${topSig ? `, top ${topSig.key}` : ""}) correlate with the implicated queue path ${queueLabel}`,
|
|
69149
70077
|
category: "error-correlation",
|
|
69150
|
-
sourceEvidenceIds:
|
|
70078
|
+
sourceEvidenceIds: directLogEvIds.slice(0, 3),
|
|
69151
70079
|
baseScore: 0.3,
|
|
69152
70080
|
metadata: { blastRadius }
|
|
69153
70081
|
});
|
|
@@ -69186,19 +70114,15 @@ async function investigate(input, deps) {
|
|
|
69186
70114
|
providerReliability,
|
|
69187
70115
|
request: { hint: input.hint, service: input.service }
|
|
69188
70116
|
});
|
|
69189
|
-
const evidenceConfidence = computeWeightedEvidenceConfidence(
|
|
70117
|
+
const evidenceConfidence = computeWeightedEvidenceConfidence(
|
|
70118
|
+
evidence2,
|
|
70119
|
+
ambientLogEvIds.length > 0 ? new Set(ambientLogEvIds) : void 0
|
|
70120
|
+
);
|
|
69190
70121
|
const seedResolved = seeds.length > 0 ? 1 : 0;
|
|
69191
70122
|
const confidence = clamp015(0.5 * evidenceConfidence + 0.5 * seedResolved);
|
|
69192
70123
|
const area = ctx.community?.name ?? top.filePath;
|
|
69193
70124
|
const topCause = rankedCauses[0];
|
|
69194
70125
|
const summary = topCause ? `Investigation of "${hint}" resolved to ${label} (${area}). Top suspected cause: ${topCause.title}.` : `Investigation of "${hint}" resolved to ${label} (${area}). No dominant suspected cause emerged from the available structural evidence.`;
|
|
69195
|
-
let recentChanges;
|
|
69196
|
-
if (deps.repoPath && input.since) {
|
|
69197
|
-
try {
|
|
69198
|
-
recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
|
|
69199
|
-
} catch {
|
|
69200
|
-
}
|
|
69201
|
-
}
|
|
69202
70126
|
const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
|
|
69203
70127
|
if (ownershipEstimate?.likelyMaintainer) {
|
|
69204
70128
|
nextActions.unshift(
|
|
@@ -69222,15 +70146,18 @@ async function investigate(input, deps) {
|
|
|
69222
70146
|
confidence,
|
|
69223
70147
|
nextActions,
|
|
69224
70148
|
ownership: ownershipEstimate,
|
|
70149
|
+
causeChains: causeChains.length > 0 ? causeChains : void 0,
|
|
69225
70150
|
...recentChanges !== void 0 ? { recentChanges } : {}
|
|
69226
70151
|
};
|
|
69227
|
-
const connectorFlags = deps.connectors ? { ...deps.connectors, metricsCollected, logsCollected, logsCompatibilityError } : {
|
|
70152
|
+
const connectorFlags = deps.connectors ? { ...deps.connectors, metricsCollected, metricsFailureReason, logsCollected, logsCompatibilityError, sinceProvided: input.since !== void 0 } : {
|
|
69228
70153
|
elasticsearch: deps.logs != null,
|
|
69229
70154
|
mongodb: deps.mongo != null,
|
|
69230
70155
|
grafana: deps.metrics != null,
|
|
69231
70156
|
metricsCollected,
|
|
70157
|
+
metricsFailureReason,
|
|
69232
70158
|
logsCollected,
|
|
69233
|
-
logsCompatibilityError
|
|
70159
|
+
logsCompatibilityError,
|
|
70160
|
+
sinceProvided: input.since !== void 0
|
|
69234
70161
|
};
|
|
69235
70162
|
const gapAnalysis = detectMissingEvidence(report, connectorFlags);
|
|
69236
70163
|
report.gapAnalysis = gapAnalysis;
|
|
@@ -69374,11 +70301,11 @@ function groupQueueEvidence(evidence2) {
|
|
|
69374
70301
|
return map2;
|
|
69375
70302
|
}
|
|
69376
70303
|
function queueEvidenceIds(evidence2) {
|
|
69377
|
-
const
|
|
70304
|
+
const ids2 = /* @__PURE__ */ new Set();
|
|
69378
70305
|
for (const e of evidence2) {
|
|
69379
|
-
if (e.source === "queue" && e.kind === "queue-state")
|
|
70306
|
+
if (e.source === "queue" && e.kind === "queue-state") ids2.add(e.id);
|
|
69380
70307
|
}
|
|
69381
|
-
return
|
|
70308
|
+
return ids2;
|
|
69382
70309
|
}
|
|
69383
70310
|
var CONFIDENCE_EXPLAIN_THRESHOLD = 0.8;
|
|
69384
70311
|
function explainLowConfidence(r) {
|
|
@@ -69541,6 +70468,12 @@ function renderReport2(r) {
|
|
|
69541
70468
|
}
|
|
69542
70469
|
lines.push("");
|
|
69543
70470
|
lines.push("## Hypotheses");
|
|
70471
|
+
const allUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
|
|
70472
|
+
if (allUnconfirmed) {
|
|
70473
|
+
lines.push(
|
|
70474
|
+
"_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
|
|
70475
|
+
);
|
|
70476
|
+
}
|
|
69544
70477
|
if (r.hypotheses.length === 0) {
|
|
69545
70478
|
lines.push("(none)");
|
|
69546
70479
|
} else {
|
|
@@ -69555,6 +70488,19 @@ function renderReport2(r) {
|
|
|
69555
70488
|
}
|
|
69556
70489
|
}
|
|
69557
70490
|
lines.push("");
|
|
70491
|
+
if (r.causeChains !== void 0 && r.causeChains.length > 0) {
|
|
70492
|
+
lines.push("## Cause chains");
|
|
70493
|
+
for (const chain of r.causeChains) {
|
|
70494
|
+
lines.push(` [${chain.confidence.toFixed(2)}] ${chain.category}: ${chain.summary}`);
|
|
70495
|
+
for (let i = 0; i < chain.steps.length; i++) {
|
|
70496
|
+
const step = chain.steps[i];
|
|
70497
|
+
const prefix = i === chain.steps.length - 1 ? " \u2514\u2500" : " \u251C\u2500";
|
|
70498
|
+
const evCite = step.evidenceIds.length > 0 ? ` (evidence: ${step.evidenceIds.map(shortId).join(", ")})` : "";
|
|
70499
|
+
lines.push(`${prefix} [${step.role}] ${step.label}${evCite}`);
|
|
70500
|
+
}
|
|
70501
|
+
}
|
|
70502
|
+
lines.push("");
|
|
70503
|
+
}
|
|
69558
70504
|
lines.push("## Evidence gaps (what we don't know)");
|
|
69559
70505
|
if (r.gapAnalysis.gaps.length === 0) {
|
|
69560
70506
|
lines.push("(no major evidence gaps)");
|
|
@@ -69685,6 +70631,12 @@ function reportToMarkdown(r) {
|
|
|
69685
70631
|
}
|
|
69686
70632
|
out.push("");
|
|
69687
70633
|
out.push("## Hypotheses");
|
|
70634
|
+
const mdAllUnconfirmed = r.hypotheses.length > 0 && r.hypotheses.every((h) => h.verdict === "unconfirmed");
|
|
70635
|
+
if (mdAllUnconfirmed) {
|
|
70636
|
+
out.push(
|
|
70637
|
+
"_All hypotheses below are unconfirmed placeholders \u2014 runtime evidence is required to validate them._"
|
|
70638
|
+
);
|
|
70639
|
+
}
|
|
69688
70640
|
if (r.hypotheses.length === 0) {
|
|
69689
70641
|
out.push("_none_");
|
|
69690
70642
|
} else {
|
|
@@ -69900,7 +70852,8 @@ async function reconstructChangeTimeline(input, deps) {
|
|
|
69900
70852
|
}
|
|
69901
70853
|
}
|
|
69902
70854
|
}
|
|
69903
|
-
const
|
|
70855
|
+
const impactPart = impact !== null ? "; " + (impact.summary.endsWith(".") ? impact.summary.slice(0, -1) : impact.summary) : "";
|
|
70856
|
+
const summary = commits.length + " commit(s)" + (service !== void 0 ? " touching " + service : "") + " in window" + (input.since !== void 0 ? " since " + input.since : "") + impactPart + ".";
|
|
69904
70857
|
const note = "Changes are evidence, not conclusions \u2014 a change in this window is not automatically the cause.";
|
|
69905
70858
|
return {
|
|
69906
70859
|
window: {
|
|
@@ -69923,6 +70876,9 @@ function renderChangeTimeline(t) {
|
|
|
69923
70876
|
lines.push("# Change Timeline");
|
|
69924
70877
|
lines.push("");
|
|
69925
70878
|
lines.push("## Summary");
|
|
70879
|
+
const sinceLabel = t.window.since ?? "(all history)";
|
|
70880
|
+
const untilLabel = t.window.until ?? "HEAD";
|
|
70881
|
+
lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
|
|
69926
70882
|
lines.push(t.summary);
|
|
69927
70883
|
lines.push("");
|
|
69928
70884
|
lines.push("> " + t.note);
|
|
@@ -69945,9 +70901,13 @@ function renderChangeTimeline(t) {
|
|
|
69945
70901
|
if (t.changeImpact !== null) {
|
|
69946
70902
|
lines.push("");
|
|
69947
70903
|
lines.push("## Change impact");
|
|
69948
|
-
lines.push(
|
|
69949
|
-
|
|
69950
|
-
)
|
|
70904
|
+
lines.push("Git range: " + t.changeImpact.base + ".." + t.changeImpact.compare);
|
|
70905
|
+
lines.push(t.changeImpact.summary);
|
|
70906
|
+
if (t.changeImpact.affectedFlows.length > 0) {
|
|
70907
|
+
for (const f of t.changeImpact.affectedFlows) {
|
|
70908
|
+
lines.push(" - " + f.flowName);
|
|
70909
|
+
}
|
|
70910
|
+
}
|
|
69951
70911
|
}
|
|
69952
70912
|
return lines.join("\n");
|
|
69953
70913
|
}
|
|
@@ -70007,6 +70967,9 @@ function renderWhatChanged(r) {
|
|
|
70007
70967
|
const lines = [];
|
|
70008
70968
|
lines.push("# What changed");
|
|
70009
70969
|
lines.push("");
|
|
70970
|
+
const sinceLabel = r.window.since ?? "(all history)";
|
|
70971
|
+
const untilLabel = r.window.until ?? "HEAD";
|
|
70972
|
+
lines.push("Range: " + sinceLabel + " \u2192 " + untilLabel);
|
|
70010
70973
|
lines.push(r.summary);
|
|
70011
70974
|
lines.push("");
|
|
70012
70975
|
lines.push("> " + r.note);
|
|
@@ -70036,6 +70999,11 @@ function renderWhatChanged(r) {
|
|
|
70036
70999
|
lines.push(
|
|
70037
71000
|
"## Affected flows: " + r.changeImpact.affectedFlows.length + " execution flow(s) affected"
|
|
70038
71001
|
);
|
|
71002
|
+
if (r.changeImpact.affectedFlows.length > 0) {
|
|
71003
|
+
for (const f of r.changeImpact.affectedFlows) {
|
|
71004
|
+
lines.push(" - " + f.flowName);
|
|
71005
|
+
}
|
|
71006
|
+
}
|
|
70039
71007
|
}
|
|
70040
71008
|
return lines.join("\n");
|
|
70041
71009
|
}
|
|
@@ -70166,7 +71134,7 @@ async function discoverArchitecture(deps) {
|
|
|
70166
71134
|
})();
|
|
70167
71135
|
const largestSubsystem = subsystems[0];
|
|
70168
71136
|
const largestDesc = largestSubsystem != null ? `${largestSubsystem.name} with ${largestSubsystem.members} symbols` : "none";
|
|
70169
|
-
const summary = `${subsystems.length} subsystems (largest: ${largestDesc}), ${asyncBoundaries.length} async queue boundaries, ${externalSystems.length} external systems, ${deadCode}
|
|
71137
|
+
const summary = `${subsystems.length} subsystems (largest: ${largestDesc}), ${asyncBoundaries.length} async queue boundaries, ${externalSystems.length} external systems, ${deadCode} unreferenced symbols.`;
|
|
70170
71138
|
return {
|
|
70171
71139
|
nodeStats,
|
|
70172
71140
|
subsystems,
|
|
@@ -70234,7 +71202,7 @@ function renderArchitecture(m) {
|
|
|
70234
71202
|
}
|
|
70235
71203
|
lines.push("");
|
|
70236
71204
|
lines.push("## Fragility");
|
|
70237
|
-
lines.push(`-
|
|
71205
|
+
lines.push(`- Unreferenced symbols: ${m.fragile.deadCode}`);
|
|
70238
71206
|
lines.push(`- High-coupling pairs (co-changes \u2265 3): ${m.fragile.highCouplingPairs}`);
|
|
70239
71207
|
return lines.join("\n");
|
|
70240
71208
|
}
|
|
@@ -70425,6 +71393,13 @@ function renderOwnership(o) {
|
|
|
70425
71393
|
lines.push("");
|
|
70426
71394
|
if (o.file === null) {
|
|
70427
71395
|
lines.push(o.note);
|
|
71396
|
+
if (o.candidates !== void 0 && o.candidates.length > 0) {
|
|
71397
|
+
lines.push("");
|
|
71398
|
+
lines.push("Candidates:");
|
|
71399
|
+
for (const c of o.candidates) {
|
|
71400
|
+
lines.push(" " + c);
|
|
71401
|
+
}
|
|
71402
|
+
}
|
|
70428
71403
|
return lines.join("\n");
|
|
70429
71404
|
}
|
|
70430
71405
|
if (o.symbol !== null) {
|
|
@@ -70550,17 +71525,35 @@ function generatePostmortem(r) {
|
|
|
70550
71525
|
);
|
|
70551
71526
|
}
|
|
70552
71527
|
} else {
|
|
70553
|
-
if (
|
|
70554
|
-
lines.push("The following
|
|
71528
|
+
if (r.causeChains !== void 0 && r.causeChains.length > 0) {
|
|
71529
|
+
lines.push("The following causal sequences are supported by the collected evidence:");
|
|
70555
71530
|
lines.push("");
|
|
70556
|
-
for (const
|
|
70557
|
-
lines.push(
|
|
70558
|
-
|
|
70559
|
-
);
|
|
71531
|
+
for (const chain of r.causeChains) {
|
|
71532
|
+
lines.push(`### ${chain.category} _(confidence ${chain.confidence.toFixed(2)})_`);
|
|
71533
|
+
lines.push("");
|
|
71534
|
+
lines.push(chain.summary);
|
|
71535
|
+
lines.push("");
|
|
71536
|
+
for (let i = 0; i < chain.steps.length; i++) {
|
|
71537
|
+
const step = chain.steps[i];
|
|
71538
|
+
const num = i + 1;
|
|
71539
|
+
const evCite = step.evidenceIds.length > 0 ? ` \u2014 evidence: ${step.evidenceIds.map((id) => `\`${shortId2(id)}\``).join(", ")}` : "";
|
|
71540
|
+
lines.push(`${num}. **[${step.role}]** ${step.label}${evCite}`);
|
|
71541
|
+
}
|
|
71542
|
+
lines.push("");
|
|
71543
|
+
}
|
|
71544
|
+
} else {
|
|
71545
|
+
if (supportedHypotheses.length > 0) {
|
|
71546
|
+
lines.push("The following factors are supported by the collected evidence:");
|
|
71547
|
+
lines.push("");
|
|
71548
|
+
for (const h of supportedHypotheses) {
|
|
71549
|
+
lines.push(
|
|
71550
|
+
`- **${h.category}:** ${h.statement} _(confidence ${h.confidence.toFixed(2)})_`
|
|
71551
|
+
);
|
|
71552
|
+
}
|
|
70560
71553
|
}
|
|
70561
71554
|
}
|
|
70562
71555
|
if (commitEvidence.length > 0) {
|
|
70563
|
-
if (supportedHypotheses.length > 0) lines.push("");
|
|
71556
|
+
if (supportedHypotheses.length > 0 || (r.causeChains?.length ?? 0) > 0) lines.push("");
|
|
70564
71557
|
lines.push("Recent changes (commit evidence) present during this incident:");
|
|
70565
71558
|
lines.push("");
|
|
70566
71559
|
for (const e of commitEvidence) {
|
|
@@ -70621,19 +71614,29 @@ function generatePostmortem(r) {
|
|
|
70621
71614
|
lines.push("");
|
|
70622
71615
|
lines.push("## Follow-up actions");
|
|
70623
71616
|
lines.push("");
|
|
70624
|
-
const seen = /* @__PURE__ */ new Set();
|
|
70625
71617
|
const checkboxItems = [];
|
|
71618
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70626
71619
|
for (const action of r.nextActions) {
|
|
70627
71620
|
if (!seen.has(action)) {
|
|
70628
71621
|
seen.add(action);
|
|
70629
71622
|
checkboxItems.push(action);
|
|
70630
71623
|
}
|
|
70631
71624
|
}
|
|
71625
|
+
const seenGapSources = /* @__PURE__ */ new Set();
|
|
70632
71626
|
for (const gap of r.gapAnalysis.gaps) {
|
|
70633
|
-
|
|
70634
|
-
|
|
70635
|
-
|
|
70636
|
-
|
|
71627
|
+
if (seenGapSources.has(gap.nextSource)) continue;
|
|
71628
|
+
seenGapSources.add(gap.nextSource);
|
|
71629
|
+
const cleanGapAction = `${gap.nextSource} to close the '${gap.dimension}' evidence gap`;
|
|
71630
|
+
if (seen.has(gap.nextSource)) {
|
|
71631
|
+
const idx = checkboxItems.indexOf(gap.nextSource);
|
|
71632
|
+
if (idx !== -1 && !seen.has(cleanGapAction)) {
|
|
71633
|
+
checkboxItems[idx] = cleanGapAction;
|
|
71634
|
+
seen.delete(gap.nextSource);
|
|
71635
|
+
seen.add(cleanGapAction);
|
|
71636
|
+
}
|
|
71637
|
+
} else if (!seen.has(cleanGapAction)) {
|
|
71638
|
+
seen.add(cleanGapAction);
|
|
71639
|
+
checkboxItems.push(cleanGapAction);
|
|
70637
71640
|
}
|
|
70638
71641
|
}
|
|
70639
71642
|
if (checkboxItems.length === 0) {
|
|
@@ -70765,11 +71768,113 @@ var TOPIC_MAP = {
|
|
|
70765
71768
|
}
|
|
70766
71769
|
};
|
|
70767
71770
|
var ALL_TOPICS = Object.keys(TOPIC_MAP);
|
|
71771
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
71772
|
+
"on",
|
|
71773
|
+
"the",
|
|
71774
|
+
"a",
|
|
71775
|
+
"an",
|
|
71776
|
+
"at",
|
|
71777
|
+
"in",
|
|
71778
|
+
"of",
|
|
71779
|
+
"and",
|
|
71780
|
+
"or",
|
|
71781
|
+
"for",
|
|
71782
|
+
"with",
|
|
71783
|
+
"by",
|
|
71784
|
+
"to",
|
|
71785
|
+
"from",
|
|
71786
|
+
"that",
|
|
71787
|
+
"this",
|
|
71788
|
+
"is",
|
|
71789
|
+
"are",
|
|
71790
|
+
"was",
|
|
71791
|
+
"were",
|
|
71792
|
+
"it",
|
|
71793
|
+
"be",
|
|
71794
|
+
"about",
|
|
71795
|
+
"focus",
|
|
71796
|
+
"ignore",
|
|
71797
|
+
"exclude",
|
|
71798
|
+
"only",
|
|
71799
|
+
"just",
|
|
71800
|
+
"concentrate",
|
|
71801
|
+
"look",
|
|
71802
|
+
"without",
|
|
71803
|
+
"skip",
|
|
71804
|
+
"drop"
|
|
71805
|
+
]);
|
|
71806
|
+
function hasFocusVerb(d) {
|
|
71807
|
+
return /\b(focus|only|just|concentrate|look at)\b/.test(d);
|
|
71808
|
+
}
|
|
71809
|
+
function hasIgnoreVerb(d) {
|
|
71810
|
+
return /\b(ignore|exclude|without|skip|drop)\b/.test(d);
|
|
71811
|
+
}
|
|
70768
71812
|
function detectMode(d) {
|
|
70769
71813
|
if (/\b(ignore|exclude|without|skip|drop)\b/.test(d)) return "ignore";
|
|
70770
71814
|
if (/\b(focus|only|just|concentrate|look at)\b/.test(d)) return "focus";
|
|
70771
71815
|
return "none";
|
|
70772
71816
|
}
|
|
71817
|
+
function splitIntoClauses(d) {
|
|
71818
|
+
const parts = d.split(/\s+and\s+|\s*,\s*/);
|
|
71819
|
+
const clauses = [];
|
|
71820
|
+
for (const part of parts) {
|
|
71821
|
+
const trimmed = part.trim();
|
|
71822
|
+
if (hasFocusVerb(trimmed)) clauses.push({ clause: trimmed, mode: "focus" });
|
|
71823
|
+
else if (hasIgnoreVerb(trimmed)) clauses.push({ clause: trimmed, mode: "ignore" });
|
|
71824
|
+
}
|
|
71825
|
+
return clauses;
|
|
71826
|
+
}
|
|
71827
|
+
function applyMixedDirective(r, directive, d) {
|
|
71828
|
+
const clauses = splitIntoClauses(d);
|
|
71829
|
+
const focusClauses = clauses.filter((c) => c.mode === "focus");
|
|
71830
|
+
const ignoreClauses = clauses.filter((c) => c.mode === "ignore");
|
|
71831
|
+
const focusTopics = [...new Set(focusClauses.flatMap((c) => matchedTopics(c.clause)))];
|
|
71832
|
+
const ignoreTopics = [...new Set(ignoreClauses.flatMap((c) => matchedTopics(c.clause)))];
|
|
71833
|
+
const focusTerms = focusClauses.filter((c) => matchedTopics(c.clause).length === 0).flatMap((c) => extractSignificantTerms(c.clause));
|
|
71834
|
+
const ignoreTerms = ignoreClauses.filter((c) => matchedTopics(c.clause).length === 0).flatMap((c) => extractSignificantTerms(c.clause));
|
|
71835
|
+
let hypotheses2 = r.hypotheses;
|
|
71836
|
+
let evidence2 = r.evidence;
|
|
71837
|
+
let suspectedCauses = r.suspectedCauses;
|
|
71838
|
+
if (focusTopics.length > 0) {
|
|
71839
|
+
const focusCategories = unionCategories(focusTopics);
|
|
71840
|
+
const focusKinds = unionKinds(focusTopics);
|
|
71841
|
+
const focusKeywords = topicKeywords(focusTopics);
|
|
71842
|
+
hypotheses2 = hypotheses2.filter((h) => focusCategories.includes(h.category));
|
|
71843
|
+
evidence2 = evidence2.filter((e) => focusKinds.includes(e.kind) || e.kind === "symbol");
|
|
71844
|
+
const filtered = suspectedCauses.filter(
|
|
71845
|
+
(c) => focusKeywords.some((k) => c.title.toLowerCase().includes(k))
|
|
71846
|
+
);
|
|
71847
|
+
suspectedCauses = filtered.length > 0 ? filtered : suspectedCauses;
|
|
71848
|
+
} else if (focusTerms.length > 0) {
|
|
71849
|
+
const matchesFocus = (text2) => focusTerms.some((t) => text2.toLowerCase().includes(t));
|
|
71850
|
+
hypotheses2 = hypotheses2.filter((h) => matchesFocus(h.statement) || matchesFocus(h.category));
|
|
71851
|
+
evidence2 = evidence2.filter((e) => matchesFocus(e.title) || matchesFocus(e.kind));
|
|
71852
|
+
const filtered = suspectedCauses.filter((c) => matchesFocus(c.title));
|
|
71853
|
+
suspectedCauses = filtered.length > 0 ? filtered : suspectedCauses;
|
|
71854
|
+
}
|
|
71855
|
+
if (ignoreTopics.length > 0) {
|
|
71856
|
+
const ignoreCategories = unionCategories(ignoreTopics);
|
|
71857
|
+
const ignoreKinds = unionKinds(ignoreTopics);
|
|
71858
|
+
const ignoreKeywords = topicKeywords(ignoreTopics);
|
|
71859
|
+
hypotheses2 = hypotheses2.filter((h) => !ignoreCategories.includes(h.category));
|
|
71860
|
+
evidence2 = evidence2.filter((e) => !ignoreKinds.includes(e.kind));
|
|
71861
|
+
suspectedCauses = suspectedCauses.filter(
|
|
71862
|
+
(c) => !ignoreKeywords.some((k) => c.title.toLowerCase().includes(k))
|
|
71863
|
+
);
|
|
71864
|
+
}
|
|
71865
|
+
if (ignoreTerms.length > 0) {
|
|
71866
|
+
const matchesIgnore = (text2) => ignoreTerms.some((t) => text2.toLowerCase().includes(t));
|
|
71867
|
+
hypotheses2 = hypotheses2.filter((h) => !matchesIgnore(h.statement) && !matchesIgnore(h.category));
|
|
71868
|
+
evidence2 = evidence2.filter((e) => !matchesIgnore(e.title) && !matchesIgnore(e.kind));
|
|
71869
|
+
suspectedCauses = suspectedCauses.filter((c) => !matchesIgnore(c.title));
|
|
71870
|
+
}
|
|
71871
|
+
const allTopics = [.../* @__PURE__ */ new Set([...focusTopics, ...ignoreTopics])];
|
|
71872
|
+
const focusDesc = focusTopics.length > 0 ? `focus: ${focusTopics.join(", ")}` : focusTerms.length > 0 ? `focus on: ${focusTerms.join(", ")}` : null;
|
|
71873
|
+
const ignoreDesc = ignoreTopics.length > 0 ? `ignore: ${ignoreTopics.join(", ")}` : ignoreTerms.length > 0 ? `ignore: ${ignoreTerms.join(", ")}` : null;
|
|
71874
|
+
const modeDesc = [focusDesc, ignoreDesc].filter(Boolean).join("; ");
|
|
71875
|
+
const note = modeDesc + ". Reused the saved investigation's evidence \u2014 no re-query of production.";
|
|
71876
|
+
return { directive, mode: "mixed", topics: allTopics, hypotheses: hypotheses2, suspectedCauses, evidence: evidence2, note };
|
|
71877
|
+
}
|
|
70773
71878
|
function matchedTopics(d) {
|
|
70774
71879
|
return ALL_TOPICS.filter((t) => {
|
|
70775
71880
|
const entry2 = TOPIC_MAP[t];
|
|
@@ -70810,11 +71915,54 @@ function topicKeywords(topics) {
|
|
|
70810
71915
|
}
|
|
70811
71916
|
return kws;
|
|
70812
71917
|
}
|
|
71918
|
+
function extractSignificantTerms(directive) {
|
|
71919
|
+
return directive.toLowerCase().replace(/[^\w\s]/g, " ").split(/\s+/).filter((w) => w.length > 2 && !STOP_WORDS.has(w));
|
|
71920
|
+
}
|
|
71921
|
+
function applyTextFilter(r, directive, mode, terms) {
|
|
71922
|
+
const matches = (text2) => terms.some((t) => text2.toLowerCase().includes(t));
|
|
71923
|
+
const note = (mode === "focus" ? "Focused on" : "Excluding") + " terms: " + terms.join(", ") + ". No predefined topic matched; filtered by text search. Reused the saved investigation's evidence \u2014 no re-query of production.";
|
|
71924
|
+
if (mode === "focus") {
|
|
71925
|
+
const hypotheses2 = r.hypotheses.filter(
|
|
71926
|
+
(h) => matches(h.statement) || matches(h.category)
|
|
71927
|
+
);
|
|
71928
|
+
const evidence2 = r.evidence.filter((e) => matches(e.title) || matches(e.kind));
|
|
71929
|
+
const suspectedCauses = r.suspectedCauses.filter((c) => matches(c.title));
|
|
71930
|
+
return {
|
|
71931
|
+
directive,
|
|
71932
|
+
mode: "focus",
|
|
71933
|
+
topics: [],
|
|
71934
|
+
hypotheses: hypotheses2,
|
|
71935
|
+
evidence: evidence2,
|
|
71936
|
+
suspectedCauses: suspectedCauses.length > 0 ? suspectedCauses : r.suspectedCauses,
|
|
71937
|
+
note
|
|
71938
|
+
};
|
|
71939
|
+
}
|
|
71940
|
+
return {
|
|
71941
|
+
directive,
|
|
71942
|
+
mode: "ignore",
|
|
71943
|
+
topics: [],
|
|
71944
|
+
hypotheses: r.hypotheses.filter(
|
|
71945
|
+
(h) => !matches(h.statement) && !matches(h.category)
|
|
71946
|
+
),
|
|
71947
|
+
evidence: r.evidence.filter((e) => !matches(e.title) && !matches(e.kind)),
|
|
71948
|
+
suspectedCauses: r.suspectedCauses.filter((c) => !matches(c.title)),
|
|
71949
|
+
note
|
|
71950
|
+
};
|
|
71951
|
+
}
|
|
70813
71952
|
function refineInvestigation(r, directive) {
|
|
70814
71953
|
const d = directive.toLowerCase();
|
|
71954
|
+
if (hasFocusVerb(d) && hasIgnoreVerb(d)) {
|
|
71955
|
+
return applyMixedDirective(r, directive, d);
|
|
71956
|
+
}
|
|
70815
71957
|
const mode = detectMode(d);
|
|
70816
71958
|
const topics = matchedTopics(d);
|
|
70817
71959
|
if (mode === "none" || topics.length === 0) {
|
|
71960
|
+
if (mode !== "none" && topics.length === 0) {
|
|
71961
|
+
const terms = extractSignificantTerms(d);
|
|
71962
|
+
if (terms.length > 0) {
|
|
71963
|
+
return applyTextFilter(r, directive, mode, terms);
|
|
71964
|
+
}
|
|
71965
|
+
}
|
|
70818
71966
|
const recognizedList = ALL_TOPICS.join(", ");
|
|
70819
71967
|
const note2 = "No specific topic directive recognized. Recognized topics: " + recognizedList + '. Example usage: "focus on queue behavior", "ignore deployment changes".';
|
|
70820
71968
|
return {
|
|
@@ -70862,7 +72010,7 @@ function renderRefined(r, v) {
|
|
|
70862
72010
|
const lines = [];
|
|
70863
72011
|
lines.push("# Refined investigation \u2014 " + r.input.hint);
|
|
70864
72012
|
lines.push("");
|
|
70865
|
-
const modeLabel = v.mode === "focus" ? "focus: " + v.topics.join(", ") : v.mode === "ignore" ? "ignore: " + v.topics.join(", ") : "none (all evidence returned)";
|
|
72013
|
+
const modeLabel = v.mode === "focus" ? "focus: " + v.topics.join(", ") : v.mode === "ignore" ? "ignore: " + v.topics.join(", ") : v.mode === "mixed" ? "focus+ignore" + (v.topics.length > 0 ? ": " + v.topics.join(", ") : "") : "none (all evidence returned)";
|
|
70866
72014
|
lines.push(" [mode: " + modeLabel + "] reusing saved evidence, no re-query");
|
|
70867
72015
|
lines.push("");
|
|
70868
72016
|
lines.push("## Hypotheses");
|
|
@@ -70917,20 +72065,175 @@ function refinedToJSON(r, v) {
|
|
|
70917
72065
|
|
|
70918
72066
|
// ../../packages/engine/src/onboard.ts
|
|
70919
72067
|
init_cjs_shims();
|
|
72068
|
+
function tokenize2(text2) {
|
|
72069
|
+
const withSpaces = text2.replace(/[^a-zA-Z0-9/_.-]+/g, " ").replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2");
|
|
72070
|
+
const parts = withSpaces.toLowerCase().split(/[\s/_.-]+/).filter((t) => t.length > 0);
|
|
72071
|
+
return [...new Set(parts)];
|
|
72072
|
+
}
|
|
72073
|
+
var GENERIC_PATH_TOKENS = /* @__PURE__ */ new Set([
|
|
72074
|
+
"src",
|
|
72075
|
+
"lib",
|
|
72076
|
+
"app",
|
|
72077
|
+
"test",
|
|
72078
|
+
"tests",
|
|
72079
|
+
"spec",
|
|
72080
|
+
"dist",
|
|
72081
|
+
"node",
|
|
72082
|
+
"modules",
|
|
72083
|
+
"common",
|
|
72084
|
+
"shared",
|
|
72085
|
+
"public",
|
|
72086
|
+
"private",
|
|
72087
|
+
"types",
|
|
72088
|
+
"interfaces",
|
|
72089
|
+
"constants",
|
|
72090
|
+
"config",
|
|
72091
|
+
"index",
|
|
72092
|
+
"main",
|
|
72093
|
+
"server",
|
|
72094
|
+
"client",
|
|
72095
|
+
"api",
|
|
72096
|
+
"http",
|
|
72097
|
+
"db",
|
|
72098
|
+
"database",
|
|
72099
|
+
"model",
|
|
72100
|
+
"models",
|
|
72101
|
+
"schema",
|
|
72102
|
+
"migration",
|
|
72103
|
+
"seed",
|
|
72104
|
+
"fixture",
|
|
72105
|
+
"mock",
|
|
72106
|
+
"hook",
|
|
72107
|
+
"context",
|
|
72108
|
+
"provider",
|
|
72109
|
+
"factory",
|
|
72110
|
+
"middleware",
|
|
72111
|
+
"route",
|
|
72112
|
+
"router",
|
|
72113
|
+
"routes",
|
|
72114
|
+
"service",
|
|
72115
|
+
"services",
|
|
72116
|
+
"controller",
|
|
72117
|
+
"controllers",
|
|
72118
|
+
"resolver",
|
|
72119
|
+
"resolvers",
|
|
72120
|
+
"worker",
|
|
72121
|
+
"workers",
|
|
72122
|
+
"helper",
|
|
72123
|
+
"helpers",
|
|
72124
|
+
"util",
|
|
72125
|
+
"utils"
|
|
72126
|
+
]);
|
|
72127
|
+
function buildAreaTokens(area, symbols) {
|
|
72128
|
+
const tokens = new Set(tokenize2(area));
|
|
72129
|
+
for (const sym of symbols) {
|
|
72130
|
+
for (const t of tokenize2(sym.name)) {
|
|
72131
|
+
if (t.length >= 3 && !GENERIC_PATH_TOKENS.has(t)) tokens.add(t);
|
|
72132
|
+
}
|
|
72133
|
+
for (const t of tokenize2(sym.filePath)) {
|
|
72134
|
+
if (t.length >= 3 && !GENERIC_PATH_TOKENS.has(t)) tokens.add(t);
|
|
72135
|
+
}
|
|
72136
|
+
}
|
|
72137
|
+
return tokens;
|
|
72138
|
+
}
|
|
72139
|
+
function matchesArea(text2, tokens) {
|
|
72140
|
+
const textTokens = new Set(tokenize2(text2));
|
|
72141
|
+
for (const t of tokens) {
|
|
72142
|
+
if (textTokens.has(t)) return true;
|
|
72143
|
+
}
|
|
72144
|
+
return false;
|
|
72145
|
+
}
|
|
72146
|
+
function areaMatchScore(symbol, tokens) {
|
|
72147
|
+
let score = 0;
|
|
72148
|
+
const nameTokens = tokenize2(symbol.name);
|
|
72149
|
+
const pathTokens = tokenize2(symbol.filePath);
|
|
72150
|
+
for (const t of tokens) {
|
|
72151
|
+
if (nameTokens.includes(t)) score += 2;
|
|
72152
|
+
if (pathTokens.includes(t)) score += 1;
|
|
72153
|
+
}
|
|
72154
|
+
return score;
|
|
72155
|
+
}
|
|
72156
|
+
function bestAreaSymbol(area, symbols) {
|
|
72157
|
+
if (symbols.length === 0) return null;
|
|
72158
|
+
const areaOnlyTokens = new Set(tokenize2(area));
|
|
72159
|
+
let best = null;
|
|
72160
|
+
let bestScore = -1;
|
|
72161
|
+
for (const sym of symbols) {
|
|
72162
|
+
const score = areaMatchScore(sym, areaOnlyTokens);
|
|
72163
|
+
if (score > bestScore) {
|
|
72164
|
+
bestScore = score;
|
|
72165
|
+
best = sym;
|
|
72166
|
+
}
|
|
72167
|
+
}
|
|
72168
|
+
return best;
|
|
72169
|
+
}
|
|
72170
|
+
function filterArchitecture(architecture, tokens) {
|
|
72171
|
+
return {
|
|
72172
|
+
...architecture,
|
|
72173
|
+
subsystems: architecture.subsystems.filter((s) => matchesArea(s.name, tokens)),
|
|
72174
|
+
asyncBoundaries: architecture.asyncBoundaries.filter(
|
|
72175
|
+
(b2) => matchesArea(b2.queueName, tokens) || b2.producers.some((p) => matchesArea(p, tokens)) || b2.workers.some((w) => matchesArea(w, tokens))
|
|
72176
|
+
),
|
|
72177
|
+
keyFlows: architecture.keyFlows.filter((f) => matchesArea(f, tokens)),
|
|
72178
|
+
externalSystems: architecture.externalSystems.filter((e) => matchesArea(e.name, tokens))
|
|
72179
|
+
};
|
|
72180
|
+
}
|
|
70920
72181
|
async function buildOnboarding(input, deps) {
|
|
70921
72182
|
const architecture = await discoverArchitecture({ code: deps.code, db: deps.db });
|
|
70922
|
-
const
|
|
70923
|
-
|
|
70924
|
-
|
|
70925
|
-
|
|
70926
|
-
|
|
70927
|
-
|
|
70928
|
-
|
|
70929
|
-
|
|
70930
|
-
|
|
72183
|
+
const area = input.area?.trim();
|
|
72184
|
+
let filteredArchitecture = architecture;
|
|
72185
|
+
let pastIncidents = [];
|
|
72186
|
+
let areaSymbol = null;
|
|
72187
|
+
if (area != null && area !== "") {
|
|
72188
|
+
const symbols = await deps.code.searchSymbols(area, 20);
|
|
72189
|
+
const tokens = buildAreaTokens(area, symbols);
|
|
72190
|
+
areaSymbol = bestAreaSymbol(area, symbols);
|
|
72191
|
+
filteredArchitecture = filterArchitecture(architecture, tokens);
|
|
72192
|
+
const invs = await listInvestigationsWithReports(deps.db, 50);
|
|
72193
|
+
const areaTokenArray = [...tokens];
|
|
72194
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
72195
|
+
for (const inv of invs) {
|
|
72196
|
+
if (!inv.report || seenIds.has(inv.id)) continue;
|
|
72197
|
+
seenIds.add(inv.id);
|
|
72198
|
+
let relevant = false;
|
|
72199
|
+
try {
|
|
72200
|
+
const report = inv.report;
|
|
72201
|
+
const tags = deriveTags(report);
|
|
72202
|
+
const tagSet = new Set(tags.map((t) => t.toLowerCase()));
|
|
72203
|
+
relevant = areaTokenArray.some((t) => tagSet.has(t.toLowerCase()));
|
|
72204
|
+
} catch {
|
|
72205
|
+
relevant = false;
|
|
72206
|
+
}
|
|
72207
|
+
if (!relevant && inv.title != null) {
|
|
72208
|
+
relevant = matchesArea(inv.title, tokens);
|
|
72209
|
+
}
|
|
72210
|
+
if (relevant) {
|
|
72211
|
+
pastIncidents.push({
|
|
72212
|
+
id: inv.id,
|
|
72213
|
+
title: inv.title,
|
|
72214
|
+
createdAt: inv.createdAt != null ? new Date(inv.createdAt).toISOString() : null
|
|
72215
|
+
});
|
|
72216
|
+
}
|
|
72217
|
+
}
|
|
72218
|
+
pastIncidents = pastIncidents.slice(0, 8);
|
|
72219
|
+
} else {
|
|
72220
|
+
const invs = await listInvestigations(deps.db, 8);
|
|
72221
|
+
pastIncidents = invs.map((i) => ({
|
|
72222
|
+
id: i.id,
|
|
72223
|
+
title: i.title,
|
|
72224
|
+
createdAt: i.createdAt != null ? new Date(i.createdAt).toISOString() : null
|
|
72225
|
+
}));
|
|
72226
|
+
}
|
|
72227
|
+
const ownership = area != null && area !== "" ? await estimateOwnership(area, {
|
|
72228
|
+
code: deps.code,
|
|
72229
|
+
repoPath: deps.repoPath,
|
|
72230
|
+
symbol: areaSymbol
|
|
72231
|
+
}) : null;
|
|
72232
|
+
const largestName = filteredArchitecture.subsystems[0]?.name ?? "n/a";
|
|
72233
|
+
const summary = (area != null ? 'Onboarding for "' + area + '": ' : "System onboarding: ") + filteredArchitecture.subsystems.length + " subsystems (largest " + largestName + "), " + filteredArchitecture.asyncBoundaries.length + " async queue boundaries, " + filteredArchitecture.externalSystems.length + " external systems, " + pastIncidents.length + " past investigation(s) on record." + (area != null ? ' Filtered toward "' + area + '".' : "");
|
|
70931
72234
|
return {
|
|
70932
|
-
area:
|
|
70933
|
-
architecture,
|
|
72235
|
+
area: area ?? null,
|
|
72236
|
+
architecture: filteredArchitecture,
|
|
70934
72237
|
ownership,
|
|
70935
72238
|
pastIncidents,
|
|
70936
72239
|
summary
|
|
@@ -70989,7 +72292,7 @@ function renderOnboarding(g) {
|
|
|
70989
72292
|
lines.push("");
|
|
70990
72293
|
lines.push("## What usually breaks");
|
|
70991
72294
|
lines.push("");
|
|
70992
|
-
lines.push(`-
|
|
72295
|
+
lines.push(`- Unreferenced symbols: ${g.architecture.fragile.deadCode}`);
|
|
70993
72296
|
lines.push(
|
|
70994
72297
|
`- High-coupling pairs (co-changes \u2265 3): ${g.architecture.fragile.highCouplingPairs}`
|
|
70995
72298
|
);
|
|
@@ -71083,6 +72386,7 @@ var SCENARIOS = [
|
|
|
71083
72386
|
since: "HEAD~10",
|
|
71084
72387
|
expectedSignals: [
|
|
71085
72388
|
{ key: "seed", label: "Seed symbols resolved" },
|
|
72389
|
+
{ key: "commit", label: "Recent change evidence found" },
|
|
71086
72390
|
{ key: "hyp:deployment-regression", label: "deployment-regression hypothesis present" },
|
|
71087
72391
|
{ key: "actions", label: "Next actions generated" }
|
|
71088
72392
|
],
|
|
@@ -71138,6 +72442,8 @@ function evaluateScenario(scenario, report) {
|
|
|
71138
72442
|
ok = report.gapAnalysis.gaps.length > 0;
|
|
71139
72443
|
} else if (signal.key === "actions") {
|
|
71140
72444
|
ok = report.nextActions.length > 0;
|
|
72445
|
+
} else if (signal.key === "commit") {
|
|
72446
|
+
ok = report.evidence.some((e) => e.kind === "commit");
|
|
71141
72447
|
} else if (signal.key.startsWith("hyp:")) {
|
|
71142
72448
|
const category = signal.key.slice(4);
|
|
71143
72449
|
ok = report.hypotheses.some((h) => h.category === category);
|
|
@@ -71178,6 +72484,16 @@ function renderSimulation(scenario, report, evaluation) {
|
|
|
71178
72484
|
"Form your own hypothesis before reading on \u2014 then compare it with what Horus found."
|
|
71179
72485
|
);
|
|
71180
72486
|
lines.push("");
|
|
72487
|
+
const isWeak = evaluation.passed < evaluation.total;
|
|
72488
|
+
if (isWeak) {
|
|
72489
|
+
lines.push(
|
|
72490
|
+
"> **Weak investigation** \u2014 Horus did not surface all expected signals for this scenario."
|
|
72491
|
+
);
|
|
72492
|
+
lines.push(
|
|
72493
|
+
"> This can happen when the hint resolves to a symbol that is not directly connected to the expected runtime/change evidence."
|
|
72494
|
+
);
|
|
72495
|
+
lines.push("");
|
|
72496
|
+
}
|
|
71181
72497
|
lines.push("## Horus investigation");
|
|
71182
72498
|
lines.push("");
|
|
71183
72499
|
lines.push(report.summary);
|
|
@@ -71209,6 +72525,28 @@ function renderSimulation(scenario, report, evaluation) {
|
|
|
71209
72525
|
for (const tip of scenario.coachingTips) {
|
|
71210
72526
|
lines.push(`- ${tip}`);
|
|
71211
72527
|
}
|
|
72528
|
+
if (scenario.category === "queue") {
|
|
72529
|
+
const queueBoundaryCheck = evaluation.checks.find(
|
|
72530
|
+
(c) => c.label === "Queue boundary crossing detected"
|
|
72531
|
+
);
|
|
72532
|
+
if (queueBoundaryCheck && !queueBoundaryCheck.ok) {
|
|
72533
|
+
lines.push("");
|
|
72534
|
+
lines.push(
|
|
72535
|
+
"_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._"
|
|
72536
|
+
);
|
|
72537
|
+
}
|
|
72538
|
+
}
|
|
72539
|
+
if (scenario.category === "change") {
|
|
72540
|
+
const commitCheck = evaluation.checks.find(
|
|
72541
|
+
(c) => c.label === "Recent change evidence found"
|
|
72542
|
+
);
|
|
72543
|
+
if (commitCheck && !commitCheck.ok) {
|
|
72544
|
+
lines.push("");
|
|
72545
|
+
lines.push(
|
|
72546
|
+
"_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._"
|
|
72547
|
+
);
|
|
72548
|
+
}
|
|
72549
|
+
}
|
|
71212
72550
|
return lines.join("\n");
|
|
71213
72551
|
}
|
|
71214
72552
|
|
|
@@ -71380,6 +72718,54 @@ function validateNarrative(output, input) {
|
|
|
71380
72718
|
}
|
|
71381
72719
|
}
|
|
71382
72720
|
}
|
|
72721
|
+
if (output.hypothesisJudgments !== void 0) {
|
|
72722
|
+
const knownHypIds = new Set(input.hypotheses?.map((h) => h.id) ?? []);
|
|
72723
|
+
const validVerdicts = /* @__PURE__ */ new Set(["supported", "weakened", "eliminated", "unconfirmed"]);
|
|
72724
|
+
for (const j of output.hypothesisJudgments) {
|
|
72725
|
+
if (input.hypotheses && !knownHypIds.has(j.hypothesisId)) {
|
|
72726
|
+
errors2.push(
|
|
72727
|
+
`hypothesisJudgment references unknown hypothesis ID "${j.hypothesisId}"`
|
|
72728
|
+
);
|
|
72729
|
+
}
|
|
72730
|
+
if (!validVerdicts.has(j.verdict)) {
|
|
72731
|
+
errors2.push(
|
|
72732
|
+
`hypothesisJudgment "${j.hypothesisId}" has invalid verdict "${j.verdict}"`
|
|
72733
|
+
);
|
|
72734
|
+
}
|
|
72735
|
+
if (!j.rationale || j.rationale.trim().length === 0) {
|
|
72736
|
+
errors2.push(`hypothesisJudgment "${j.hypothesisId}" has empty rationale`);
|
|
72737
|
+
}
|
|
72738
|
+
for (const eid of j.citedEvidenceIds) {
|
|
72739
|
+
if (!knownIds.has(eid)) {
|
|
72740
|
+
errors2.push(
|
|
72741
|
+
`hypothesisJudgment "${j.hypothesisId}" cites unknown evidence ID "${eid}"`
|
|
72742
|
+
);
|
|
72743
|
+
}
|
|
72744
|
+
}
|
|
72745
|
+
if (j.confidence < 0 || j.confidence > 1) {
|
|
72746
|
+
errors2.push(
|
|
72747
|
+
`hypothesisJudgment "${j.hypothesisId}" confidence must be between 0 and 1`
|
|
72748
|
+
);
|
|
72749
|
+
}
|
|
72750
|
+
}
|
|
72751
|
+
}
|
|
72752
|
+
if (output.rootCauseAssessment !== void 0) {
|
|
72753
|
+
const rca = output.rootCauseAssessment;
|
|
72754
|
+
if (!rca.summary || rca.summary.trim().length === 0) {
|
|
72755
|
+
errors2.push("rootCauseAssessment.summary must be non-empty");
|
|
72756
|
+
}
|
|
72757
|
+
for (const eid of rca.citedEvidenceIds) {
|
|
72758
|
+
if (!knownIds.has(eid)) {
|
|
72759
|
+
errors2.push(`rootCauseAssessment cites unknown evidence ID "${eid}"`);
|
|
72760
|
+
}
|
|
72761
|
+
}
|
|
72762
|
+
const validUncertainty = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
72763
|
+
if (!validUncertainty.has(rca.uncertainty)) {
|
|
72764
|
+
errors2.push(
|
|
72765
|
+
`rootCauseAssessment.uncertainty must be 'low', 'medium', or 'high' (got "${rca.uncertainty}")`
|
|
72766
|
+
);
|
|
72767
|
+
}
|
|
72768
|
+
}
|
|
71383
72769
|
return { valid: errors2.length === 0, errors: errors2 };
|
|
71384
72770
|
}
|
|
71385
72771
|
async function renderNarrative(input, opts = {}) {
|
|
@@ -71397,11 +72783,11 @@ async function renderNarrative(input, opts = {}) {
|
|
|
71397
72783
|
fromProvider: false,
|
|
71398
72784
|
validationErrors: validation.errors
|
|
71399
72785
|
};
|
|
71400
|
-
} catch {
|
|
72786
|
+
} catch (err) {
|
|
71401
72787
|
return {
|
|
71402
72788
|
output: deterministicFallback(input),
|
|
71403
72789
|
fromProvider: false,
|
|
71404
|
-
validationErrors: ["Provider threw an error"]
|
|
72790
|
+
validationErrors: [err instanceof Error ? err.message : "Provider threw an error"]
|
|
71405
72791
|
};
|
|
71406
72792
|
}
|
|
71407
72793
|
}
|
|
@@ -71468,7 +72854,26 @@ function buildPrompt(input, ceiling) {
|
|
|
71468
72854
|
(e) => `- [${e.id}] (${e.kind}) ${e.title}${e.excerpt ? `: ${e.excerpt}` : ""}`
|
|
71469
72855
|
).join("\n");
|
|
71470
72856
|
const causeLines = input.suspectedCauses.map((c) => `- ${c.label} (score: ${c.score})`).join("\n");
|
|
71471
|
-
|
|
72857
|
+
const hypothesisLines = input.hypotheses && input.hypotheses.length > 0 ? input.hypotheses.map(
|
|
72858
|
+
(h) => `- [${h.id}] (${h.category}) [deterministic: ${h.deterministicVerdict} @ ${h.deterministicConfidence.toFixed(2)}] ${h.statement}`
|
|
72859
|
+
).join("\n") : null;
|
|
72860
|
+
const hypothesisJudgmentSchema = hypothesisLines ? ` "hypothesisJudgments": [
|
|
72861
|
+
{
|
|
72862
|
+
"hypothesisId": "<id from hypotheses list above>",
|
|
72863
|
+
"category": "<category from hypotheses list>",
|
|
72864
|
+
"verdict": "<supported|weakened|eliminated|unconfirmed>",
|
|
72865
|
+
"rationale": "<one paragraph grounded in evidence IDs>",
|
|
72866
|
+
"citedEvidenceIds": ["<evidence IDs from the list>"],
|
|
72867
|
+
"confidence": <0\u2013${ceiling}>
|
|
72868
|
+
}
|
|
72869
|
+
],
|
|
72870
|
+
"rootCauseAssessment": {
|
|
72871
|
+
"summary": "<one paragraph root cause grounded in evidence>",
|
|
72872
|
+
"primaryHypothesisId": "<id of the hypothesis you consider the primary driver, or omit>",
|
|
72873
|
+
"citedEvidenceIds": ["<evidence IDs from the list>"],
|
|
72874
|
+
"uncertainty": "<low|medium|high>"
|
|
72875
|
+
},` : "";
|
|
72876
|
+
return `You are an incident analysis assistant. Analyze this investigation and return a JSON judgment.
|
|
71472
72877
|
|
|
71473
72878
|
Investigation hint: ${input.hint}
|
|
71474
72879
|
Deterministic summary: ${input.deterministicSummary}
|
|
@@ -71478,23 +72883,30 @@ ${evidenceLines}
|
|
|
71478
72883
|
|
|
71479
72884
|
Suspected causes:
|
|
71480
72885
|
${causeLines}
|
|
72886
|
+
${hypothesisLines ? `
|
|
72887
|
+
Deterministic hypotheses (provide a second-pass judgment for each):
|
|
72888
|
+
${hypothesisLines}` : ""}
|
|
71481
72889
|
|
|
71482
72890
|
Known services: ${input.knownServices.join(", ")}
|
|
71483
72891
|
|
|
71484
72892
|
Return ONLY valid JSON with this exact shape:
|
|
71485
72893
|
{
|
|
71486
72894
|
"what": "<what happened \u2014 one concise paragraph>",
|
|
71487
|
-
"why": "<root cause
|
|
72895
|
+
"why": "<root cause narrative grounded in the evidence above>",
|
|
71488
72896
|
"whereNext": ["<action 1>", "<action 2>"],
|
|
71489
72897
|
"citations": [{"evidenceId": "<id from the evidence list>", "rationale": "<why this supports the claim>"}],
|
|
71490
72898
|
"confidence": <number between 0 and ${ceiling}>,
|
|
71491
|
-
"mentionedServices": ["<service name from known list only>"]
|
|
72899
|
+
"mentionedServices": ["<service name from known list only>"],
|
|
72900
|
+
${hypothesisJudgmentSchema}
|
|
71492
72901
|
}
|
|
71493
72902
|
|
|
71494
72903
|
Hard rules:
|
|
71495
72904
|
- confidence must not exceed ${ceiling}
|
|
71496
72905
|
- only cite evidence IDs from the list above \u2014 any other ID is a hallucination
|
|
71497
|
-
- only include services from: ${input.knownServices.join(", ")}
|
|
72906
|
+
- only include services from: ${input.knownServices.join(", ")}
|
|
72907
|
+
- hypothesisJudgments must only reference hypothesis IDs from the hypotheses list above
|
|
72908
|
+
- verdict must be exactly one of: supported, weakened, eliminated, unconfirmed
|
|
72909
|
+
- uncertainty must be exactly one of: low, medium, high`;
|
|
71498
72910
|
}
|
|
71499
72911
|
function parseOutput(raw, input, ceiling) {
|
|
71500
72912
|
let parsed;
|
|
@@ -71508,21 +72920,52 @@ function parseOutput(raw, input, ceiling) {
|
|
|
71508
72920
|
parsed = JSON.parse(jsonMatch[0]);
|
|
71509
72921
|
}
|
|
71510
72922
|
const confidence = Math.min(
|
|
71511
|
-
typeof parsed
|
|
72923
|
+
typeof parsed["confidence"] === "number" ? parsed["confidence"] : input.reportConfidence,
|
|
71512
72924
|
ceiling
|
|
71513
72925
|
);
|
|
72926
|
+
const citations = Array.isArray(parsed["citations"]) ? parsed["citations"] : [];
|
|
71514
72927
|
const output = {
|
|
71515
|
-
what: typeof parsed
|
|
71516
|
-
why: typeof parsed
|
|
71517
|
-
whereNext: Array.isArray(parsed
|
|
71518
|
-
citations
|
|
72928
|
+
what: typeof parsed["what"] === "string" ? parsed["what"] : "",
|
|
72929
|
+
why: typeof parsed["why"] === "string" ? parsed["why"] : "",
|
|
72930
|
+
whereNext: Array.isArray(parsed["whereNext"]) ? parsed["whereNext"].map(String) : [],
|
|
72931
|
+
citations,
|
|
71519
72932
|
confidence
|
|
71520
72933
|
};
|
|
71521
|
-
if (Array.isArray(parsed
|
|
71522
|
-
output.mentionedServices = parsed
|
|
72934
|
+
if (Array.isArray(parsed["mentionedServices"])) {
|
|
72935
|
+
output.mentionedServices = parsed["mentionedServices"].map(String);
|
|
72936
|
+
}
|
|
72937
|
+
if (Array.isArray(parsed["hypothesisJudgments"])) {
|
|
72938
|
+
output.hypothesisJudgments = parsed["hypothesisJudgments"].map(
|
|
72939
|
+
(j) => ({
|
|
72940
|
+
hypothesisId: typeof j["hypothesisId"] === "string" ? j["hypothesisId"] : "",
|
|
72941
|
+
category: typeof j["category"] === "string" ? j["category"] : "",
|
|
72942
|
+
verdict: isValidVerdict(j["verdict"]) ? j["verdict"] : "unconfirmed",
|
|
72943
|
+
rationale: typeof j["rationale"] === "string" ? j["rationale"] : "",
|
|
72944
|
+
citedEvidenceIds: Array.isArray(j["citedEvidenceIds"]) ? j["citedEvidenceIds"].map(String) : [],
|
|
72945
|
+
confidence: Math.min(
|
|
72946
|
+
typeof j["confidence"] === "number" ? j["confidence"] : 0,
|
|
72947
|
+
ceiling
|
|
72948
|
+
)
|
|
72949
|
+
})
|
|
72950
|
+
);
|
|
72951
|
+
}
|
|
72952
|
+
if (parsed["rootCauseAssessment"] && typeof parsed["rootCauseAssessment"] === "object") {
|
|
72953
|
+
const rca = parsed["rootCauseAssessment"];
|
|
72954
|
+
output.rootCauseAssessment = {
|
|
72955
|
+
summary: typeof rca["summary"] === "string" ? rca["summary"] : "",
|
|
72956
|
+
primaryHypothesisId: typeof rca["primaryHypothesisId"] === "string" ? rca["primaryHypothesisId"] : void 0,
|
|
72957
|
+
citedEvidenceIds: Array.isArray(rca["citedEvidenceIds"]) ? rca["citedEvidenceIds"].map(String) : [],
|
|
72958
|
+
uncertainty: isValidUncertainty(rca["uncertainty"]) ? rca["uncertainty"] : "high"
|
|
72959
|
+
};
|
|
71523
72960
|
}
|
|
71524
72961
|
return output;
|
|
71525
72962
|
}
|
|
72963
|
+
function isValidVerdict(v) {
|
|
72964
|
+
return v === "supported" || v === "weakened" || v === "eliminated" || v === "unconfirmed";
|
|
72965
|
+
}
|
|
72966
|
+
function isValidUncertainty(v) {
|
|
72967
|
+
return v === "low" || v === "medium" || v === "high";
|
|
72968
|
+
}
|
|
71526
72969
|
|
|
71527
72970
|
// ../../packages/ai/src/fake-provider.ts
|
|
71528
72971
|
init_cjs_shims();
|
|
@@ -71562,6 +73005,12 @@ init_cjs_shims();
|
|
|
71562
73005
|
init_cjs_shims();
|
|
71563
73006
|
|
|
71564
73007
|
// ../../packages/cli/src/commands/investigate.ts
|
|
73008
|
+
function extractEvidenceExcerpt(e) {
|
|
73009
|
+
if (!e.payload || typeof e.payload !== "object") return void 0;
|
|
73010
|
+
const p = e.payload;
|
|
73011
|
+
const candidate = (typeof p["pattern"] === "string" ? p["pattern"] : null) ?? (typeof p["message"] === "string" ? p["message"] : null) ?? (typeof p["label"] === "string" ? p["label"] : null) ?? (typeof p["summary"] === "string" ? p["summary"] : null) ?? (typeof p["valueLabel"] === "string" ? p["valueLabel"] : null);
|
|
73012
|
+
return candidate ? candidate.slice(0, 120) : void 0;
|
|
73013
|
+
}
|
|
71565
73014
|
function buildNarrativeInput(report) {
|
|
71566
73015
|
return {
|
|
71567
73016
|
investigationId: report.id,
|
|
@@ -71570,7 +73019,8 @@ function buildNarrativeInput(report) {
|
|
|
71570
73019
|
evidence: report.evidence.map((e) => ({
|
|
71571
73020
|
id: e.id,
|
|
71572
73021
|
kind: e.kind,
|
|
71573
|
-
title: e.title
|
|
73022
|
+
title: e.title,
|
|
73023
|
+
excerpt: extractEvidenceExcerpt(e)
|
|
71574
73024
|
})),
|
|
71575
73025
|
knownServices: report.input.service ? [report.input.service] : [],
|
|
71576
73026
|
suspectedCauses: report.suspectedCauses.map((c) => ({
|
|
@@ -71582,9 +73032,83 @@ function buildNarrativeInput(report) {
|
|
|
71582
73032
|
findings: report.findings.map((f) => ({
|
|
71583
73033
|
title: f.title,
|
|
71584
73034
|
evidenceIds: f.evidenceIds
|
|
73035
|
+
})),
|
|
73036
|
+
hypotheses: report.hypotheses.map((h) => ({
|
|
73037
|
+
id: h.id,
|
|
73038
|
+
category: h.category,
|
|
73039
|
+
statement: h.statement,
|
|
73040
|
+
deterministicVerdict: h.verdict,
|
|
73041
|
+
deterministicConfidence: h.confidence,
|
|
73042
|
+
supportingEvidenceIds: h.supportingEvidenceIds
|
|
71585
73043
|
}))
|
|
71586
73044
|
};
|
|
71587
73045
|
}
|
|
73046
|
+
function narrativeOutputToStoredJudgment(output, provider) {
|
|
73047
|
+
const judgment = {
|
|
73048
|
+
what: output.what,
|
|
73049
|
+
why: output.why,
|
|
73050
|
+
whereNext: output.whereNext,
|
|
73051
|
+
citations: output.citations,
|
|
73052
|
+
confidence: output.confidence,
|
|
73053
|
+
provider,
|
|
73054
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
73055
|
+
};
|
|
73056
|
+
if (output.mentionedServices) judgment.mentionedServices = output.mentionedServices;
|
|
73057
|
+
if (output.hypothesisJudgments) judgment.hypothesisJudgments = output.hypothesisJudgments;
|
|
73058
|
+
if (output.rootCauseAssessment) judgment.rootCauseAssessment = output.rootCauseAssessment;
|
|
73059
|
+
return judgment;
|
|
73060
|
+
}
|
|
73061
|
+
function renderStoredAIJudgment(judgment, write = (l) => console.log(l)) {
|
|
73062
|
+
const sep = "\u2500".repeat(60);
|
|
73063
|
+
write(`
|
|
73064
|
+
${sep}`);
|
|
73065
|
+
write(import_picocolors5.default.bold("AI Judgment") + import_picocolors5.default.dim(` (confidence: ${(judgment.confidence * 100).toFixed(0)}%, provider: ${judgment.provider})`));
|
|
73066
|
+
write(sep);
|
|
73067
|
+
if (judgment.hypothesisJudgments && judgment.hypothesisJudgments.length > 0) {
|
|
73068
|
+
write(import_picocolors5.default.bold("Hypothesis verdicts:"));
|
|
73069
|
+
for (const j of judgment.hypothesisJudgments) {
|
|
73070
|
+
const verdictColor = j.verdict === "supported" ? import_picocolors5.default.green : j.verdict === "weakened" ? import_picocolors5.default.yellow : j.verdict === "eliminated" ? import_picocolors5.default.red : import_picocolors5.default.dim;
|
|
73071
|
+
write(` ${verdictColor(`[${j.verdict}]`)} ${j.category}` + import_picocolors5.default.dim(` (${(j.confidence * 100).toFixed(0)}%)`));
|
|
73072
|
+
write(import_picocolors5.default.dim(` ${j.rationale}`));
|
|
73073
|
+
}
|
|
73074
|
+
write("");
|
|
73075
|
+
}
|
|
73076
|
+
if (judgment.rootCauseAssessment) {
|
|
73077
|
+
const rca = judgment.rootCauseAssessment;
|
|
73078
|
+
const uncertaintyColor = rca.uncertainty === "low" ? import_picocolors5.default.green : rca.uncertainty === "medium" ? import_picocolors5.default.yellow : import_picocolors5.default.red;
|
|
73079
|
+
write(import_picocolors5.default.bold("Root cause assessment:") + import_picocolors5.default.dim(` uncertainty: ${uncertaintyColor(rca.uncertainty)}`));
|
|
73080
|
+
write(rca.summary);
|
|
73081
|
+
if (rca.citedEvidenceIds.length > 0) {
|
|
73082
|
+
write(import_picocolors5.default.dim(` Cited: ${rca.citedEvidenceIds.join(", ")}`));
|
|
73083
|
+
}
|
|
73084
|
+
write("");
|
|
73085
|
+
}
|
|
73086
|
+
write(import_picocolors5.default.bold("What:") + " " + judgment.what);
|
|
73087
|
+
write(import_picocolors5.default.bold("Why:") + " " + judgment.why);
|
|
73088
|
+
if (judgment.whereNext.length > 0) {
|
|
73089
|
+
write(import_picocolors5.default.bold("Next steps:"));
|
|
73090
|
+
for (const step of judgment.whereNext) {
|
|
73091
|
+
write(` \u2022 ${step}`);
|
|
73092
|
+
}
|
|
73093
|
+
}
|
|
73094
|
+
if (judgment.citations.length > 0) {
|
|
73095
|
+
write(import_picocolors5.default.dim(`
|
|
73096
|
+
Cited evidence: ${judgment.citations.map((c) => c.evidenceId).join(", ")}`));
|
|
73097
|
+
}
|
|
73098
|
+
}
|
|
73099
|
+
function classifyAIFailure(firstError) {
|
|
73100
|
+
if (!firstError) return "provider unavailable";
|
|
73101
|
+
if (/401|unauthorized|api.key|api key/i.test(firstError)) {
|
|
73102
|
+
return "missing or invalid API key \u2014 set ANTHROPIC_API_KEY";
|
|
73103
|
+
}
|
|
73104
|
+
if (/invalid.request|model|not.found/i.test(firstError)) {
|
|
73105
|
+
return `invalid model or request \u2014 ${firstError}`;
|
|
73106
|
+
}
|
|
73107
|
+
if (/econnrefused|enotfound|network|fetch|abort/i.test(firstError)) {
|
|
73108
|
+
return "network error \u2014 check connectivity";
|
|
73109
|
+
}
|
|
73110
|
+
return firstError;
|
|
73111
|
+
}
|
|
71588
73112
|
async function runInvestigate(hint, opts) {
|
|
71589
73113
|
try {
|
|
71590
73114
|
const config = await loadConfig(opts.config, { name: opts.name });
|
|
@@ -71644,33 +73168,22 @@ async function runInvestigate(hint, opts) {
|
|
|
71644
73168
|
const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport2(report);
|
|
71645
73169
|
console.log(rendered);
|
|
71646
73170
|
if (opts.ai && format !== "json") {
|
|
73171
|
+
const model = opts.aiModel ?? "claude-opus-4-8";
|
|
73172
|
+
const provider = opts._aiProvider ?? new AnthropicNarrativeProvider({ model });
|
|
73173
|
+
console.log(import_picocolors5.default.dim(`[ai] model: ${model}`));
|
|
71647
73174
|
const narrativeInput = buildNarrativeInput(report);
|
|
71648
|
-
const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
|
|
71649
73175
|
const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
|
|
71650
73176
|
if (!fromProvider) {
|
|
71651
|
-
|
|
71652
|
-
|
|
71653
|
-
console.error(import_picocolors5.default.dim(` ${validationErrors[0]}`));
|
|
71654
|
-
}
|
|
73177
|
+
const reason = classifyAIFailure(validationErrors?.[0]);
|
|
73178
|
+
console.error(import_picocolors5.default.yellow(`[ai] fallback to deterministic \u2014 ${reason}`));
|
|
71655
73179
|
} else {
|
|
71656
|
-
const
|
|
71657
|
-
|
|
71658
|
-
|
|
71659
|
-
|
|
71660
|
-
|
|
71661
|
-
console.log(import_picocolors5.default.bold("What:"), output.what);
|
|
71662
|
-
console.log(import_picocolors5.default.bold("Why:"), output.why);
|
|
71663
|
-
if (output.whereNext.length > 0) {
|
|
71664
|
-
console.log(import_picocolors5.default.bold("Next steps:"));
|
|
71665
|
-
for (const step of output.whereNext) {
|
|
71666
|
-
console.log(` \u2022 ${step}`);
|
|
71667
|
-
}
|
|
71668
|
-
}
|
|
71669
|
-
if (output.citations.length > 0) {
|
|
71670
|
-
console.log(import_picocolors5.default.dim(`
|
|
71671
|
-
Cited evidence: ${output.citations.map((c) => c.evidenceId).join(", ")}`));
|
|
73180
|
+
const stored = narrativeOutputToStoredJudgment(output, "anthropic");
|
|
73181
|
+
report.aiJudgment = stored;
|
|
73182
|
+
try {
|
|
73183
|
+
await updateInvestigationReport(db, report.id, report);
|
|
73184
|
+
} catch {
|
|
71672
73185
|
}
|
|
71673
|
-
|
|
73186
|
+
renderStoredAIJudgment(stored);
|
|
71674
73187
|
}
|
|
71675
73188
|
}
|
|
71676
73189
|
} finally {
|
|
@@ -71806,9 +73319,15 @@ async function runBlastRadius(query, opts) {
|
|
|
71806
73319
|
try {
|
|
71807
73320
|
const r = await analyzeBlastRadius(query, { code, db }, opts.depth ?? 3);
|
|
71808
73321
|
if (!r) {
|
|
71809
|
-
console.log(
|
|
73322
|
+
console.log(`No symbol found for: ${query}`);
|
|
73323
|
+
console.log(import_picocolors10.default.dim(` Tip: use an exact class or function name, e.g. "MyService"`));
|
|
71810
73324
|
return 1;
|
|
71811
73325
|
}
|
|
73326
|
+
if (r.seed.name.toLowerCase() !== query.toLowerCase()) {
|
|
73327
|
+
console.log(
|
|
73328
|
+
import_picocolors10.default.yellow(` No exact match for "${query}"`) + import_picocolors10.default.dim(` \u2014 showing closest: "${r.seed.name}" (fuzzy match)`)
|
|
73329
|
+
);
|
|
73330
|
+
}
|
|
71812
73331
|
console.log(opts.json ? blastRadiusToJSON(r) : renderBlastRadius(r));
|
|
71813
73332
|
} finally {
|
|
71814
73333
|
await sql2.end();
|
|
@@ -71881,6 +73400,26 @@ async function runSearch(query, opts) {
|
|
|
71881
73400
|
|
|
71882
73401
|
// ../../packages/cli/src/commands/investigations.ts
|
|
71883
73402
|
init_cjs_shims();
|
|
73403
|
+
|
|
73404
|
+
// ../../packages/cli/src/lib/format.ts
|
|
73405
|
+
init_cjs_shims();
|
|
73406
|
+
function formatDateTime(date2) {
|
|
73407
|
+
if (date2 == null) return "";
|
|
73408
|
+
const d = date2 instanceof Date ? date2 : new Date(date2);
|
|
73409
|
+
if (Number.isNaN(d.getTime())) return String(date2);
|
|
73410
|
+
const tzOffset = -d.getTimezoneOffset();
|
|
73411
|
+
const sign = tzOffset >= 0 ? "+" : "-";
|
|
73412
|
+
const offsetHours = String(Math.floor(Math.abs(tzOffset) / 60)).padStart(2, "0");
|
|
73413
|
+
const offsetMins = String(Math.abs(tzOffset) % 60).padStart(2, "0");
|
|
73414
|
+
const y = String(d.getFullYear());
|
|
73415
|
+
const m = String(d.getMonth() + 1).padStart(2, "0");
|
|
73416
|
+
const day = String(d.getDate()).padStart(2, "0");
|
|
73417
|
+
const h = String(d.getHours()).padStart(2, "0");
|
|
73418
|
+
const min = String(d.getMinutes()).padStart(2, "0");
|
|
73419
|
+
return `${y}-${m}-${day} ${h}:${min} (UTC${sign}${offsetHours}:${offsetMins})`;
|
|
73420
|
+
}
|
|
73421
|
+
|
|
73422
|
+
// ../../packages/cli/src/commands/investigations.ts
|
|
71884
73423
|
async function runInvestigations(opts) {
|
|
71885
73424
|
const config = await loadConfig(opts.config);
|
|
71886
73425
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
@@ -71890,7 +73429,7 @@ async function runInvestigations(opts) {
|
|
|
71890
73429
|
console.log('No investigations yet. Run: horus investigate "<hint>"');
|
|
71891
73430
|
} else {
|
|
71892
73431
|
for (const row of rows) {
|
|
71893
|
-
const ts = row.createdAt
|
|
73432
|
+
const ts = formatDateTime(row.createdAt);
|
|
71894
73433
|
const title = (row.title ?? "").length > 60 ? (row.title ?? "").slice(0, 57) + "..." : row.title ?? "";
|
|
71895
73434
|
console.log(`${row.id} ${ts} ${title}`);
|
|
71896
73435
|
}
|
|
@@ -71926,33 +73465,27 @@ async function runReplay(id, opts) {
|
|
|
71926
73465
|
const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) : renderReport2(report);
|
|
71927
73466
|
console.log(out);
|
|
71928
73467
|
if (opts.ai && fmt !== "json") {
|
|
71929
|
-
|
|
71930
|
-
|
|
71931
|
-
|
|
71932
|
-
if (!fromProvider) {
|
|
71933
|
-
console.error(import_picocolors13.default.yellow("[ai] Provider unavailable \u2014 deterministic output shown above."));
|
|
71934
|
-
if (validationErrors?.length) {
|
|
71935
|
-
console.error(import_picocolors13.default.dim(` ${validationErrors[0]}`));
|
|
71936
|
-
}
|
|
73468
|
+
if (report.aiJudgment && !opts.refreshAi) {
|
|
73469
|
+
renderStoredAIJudgment(report.aiJudgment);
|
|
73470
|
+
console.log(import_picocolors13.default.dim("[ai] Stored judgment replayed. Use --refresh-ai to regenerate."));
|
|
71937
73471
|
} else {
|
|
71938
|
-
const
|
|
71939
|
-
|
|
71940
|
-
|
|
71941
|
-
|
|
71942
|
-
|
|
71943
|
-
|
|
71944
|
-
|
|
71945
|
-
if (output.whereNext.length > 0) {
|
|
71946
|
-
console.log(import_picocolors13.default.bold("Next steps:"));
|
|
71947
|
-
for (const step of output.whereNext) {
|
|
71948
|
-
console.log(` \u2022 ${step}`);
|
|
73472
|
+
const narrativeInput = buildNarrativeInput(report);
|
|
73473
|
+
const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
|
|
73474
|
+
const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
|
|
73475
|
+
if (!fromProvider) {
|
|
73476
|
+
console.error(import_picocolors13.default.yellow("[ai] Provider unavailable \u2014 deterministic output shown above."));
|
|
73477
|
+
if (validationErrors?.length) {
|
|
73478
|
+
console.error(import_picocolors13.default.dim(` ${validationErrors[0]}`));
|
|
71949
73479
|
}
|
|
73480
|
+
} else {
|
|
73481
|
+
const stored = narrativeOutputToStoredJudgment(output, "anthropic");
|
|
73482
|
+
report.aiJudgment = stored;
|
|
73483
|
+
try {
|
|
73484
|
+
await updateInvestigationReport(db, report.id, report);
|
|
73485
|
+
} catch {
|
|
73486
|
+
}
|
|
73487
|
+
renderStoredAIJudgment(stored);
|
|
71950
73488
|
}
|
|
71951
|
-
if (output.citations.length > 0) {
|
|
71952
|
-
console.log(import_picocolors13.default.dim(`
|
|
71953
|
-
Cited evidence: ${output.citations.map((c) => c.evidenceId).join(", ")}`));
|
|
71954
|
-
}
|
|
71955
|
-
console.log(import_picocolors13.default.dim(`AI confidence: ${(output.confidence * 100).toFixed(0)}%`));
|
|
71956
73489
|
}
|
|
71957
73490
|
}
|
|
71958
73491
|
} finally {
|
|
@@ -72014,34 +73547,74 @@ async function runPostmortem(id, opts) {
|
|
|
72014
73547
|
}
|
|
72015
73548
|
let content = generatePostmortem(report);
|
|
72016
73549
|
if (opts.aiSummary) {
|
|
72017
|
-
const
|
|
72018
|
-
|
|
72019
|
-
|
|
72020
|
-
|
|
72021
|
-
content += `
|
|
72022
|
-
|
|
72023
|
-
## AI Summary
|
|
73550
|
+
const storedJudgment = report.aiJudgment;
|
|
73551
|
+
if (storedJudgment && !opts.refreshAi) {
|
|
73552
|
+
content += "\n\n## AI Summary\n\n";
|
|
73553
|
+
content += `_Stored AI judgment (provider: ${storedJudgment.provider}, generated: ${storedJudgment.generatedAt})_
|
|
72024
73554
|
|
|
72025
|
-
_AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
|
|
72026
73555
|
`;
|
|
72027
|
-
|
|
72028
|
-
content += "\n\n## AI Summary\n\n";
|
|
72029
|
-
content += `**What happened:** ${output.what}
|
|
73556
|
+
content += `**What happened:** ${storedJudgment.what}
|
|
72030
73557
|
|
|
72031
73558
|
`;
|
|
72032
|
-
content += `**Why:** ${
|
|
73559
|
+
content += `**Why:** ${storedJudgment.why}
|
|
73560
|
+
`;
|
|
73561
|
+
if (storedJudgment.rootCauseAssessment) {
|
|
73562
|
+
content += `
|
|
73563
|
+
**Root cause (AI):** ${storedJudgment.rootCauseAssessment.summary}
|
|
73564
|
+
`;
|
|
73565
|
+
content += `_(uncertainty: ${storedJudgment.rootCauseAssessment.uncertainty})_
|
|
72033
73566
|
`;
|
|
72034
|
-
|
|
73567
|
+
}
|
|
73568
|
+
if (storedJudgment.whereNext.length > 0) {
|
|
72035
73569
|
content += "\n**Next steps:**\n";
|
|
72036
|
-
for (const step of
|
|
73570
|
+
for (const step of storedJudgment.whereNext) {
|
|
72037
73571
|
content += `- ${step}
|
|
72038
73572
|
`;
|
|
72039
73573
|
}
|
|
72040
73574
|
}
|
|
72041
|
-
if (
|
|
73575
|
+
if (storedJudgment.citations.length > 0) {
|
|
73576
|
+
content += `
|
|
73577
|
+
**Cited evidence:** ${storedJudgment.citations.map((c) => c.evidenceId).join(", ")}
|
|
73578
|
+
`;
|
|
73579
|
+
}
|
|
73580
|
+
} else {
|
|
73581
|
+
const narrativeInput = buildNarrativeInput(report);
|
|
73582
|
+
const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
|
|
73583
|
+
const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
|
|
73584
|
+
if (!fromProvider) {
|
|
72042
73585
|
content += `
|
|
73586
|
+
|
|
73587
|
+
## AI Summary
|
|
73588
|
+
|
|
73589
|
+
_AI summary unavailable: ${validationErrors?.[0] ?? "provider error"}_
|
|
73590
|
+
`;
|
|
73591
|
+
} else {
|
|
73592
|
+
content += "\n\n## AI Summary\n\n";
|
|
73593
|
+
content += `**What happened:** ${output.what}
|
|
73594
|
+
|
|
73595
|
+
`;
|
|
73596
|
+
content += `**Why:** ${output.why}
|
|
73597
|
+
`;
|
|
73598
|
+
if (output.rootCauseAssessment) {
|
|
73599
|
+
content += `
|
|
73600
|
+
**Root cause (AI):** ${output.rootCauseAssessment.summary}
|
|
73601
|
+
`;
|
|
73602
|
+
content += `_(uncertainty: ${output.rootCauseAssessment.uncertainty})_
|
|
73603
|
+
`;
|
|
73604
|
+
}
|
|
73605
|
+
if (output.whereNext.length > 0) {
|
|
73606
|
+
content += "\n**Next steps:**\n";
|
|
73607
|
+
for (const step of output.whereNext) {
|
|
73608
|
+
content += `- ${step}
|
|
73609
|
+
`;
|
|
73610
|
+
}
|
|
73611
|
+
}
|
|
73612
|
+
if (output.citations.length > 0) {
|
|
73613
|
+
content += `
|
|
72043
73614
|
**Cited evidence:** ${output.citations.map((c) => c.evidenceId).join(", ")}
|
|
72044
73615
|
`;
|
|
73616
|
+
}
|
|
73617
|
+
report.aiJudgment = narrativeOutputToStoredJudgment(output, "anthropic");
|
|
72045
73618
|
}
|
|
72046
73619
|
}
|
|
72047
73620
|
}
|
|
@@ -72114,7 +73687,7 @@ async function runScores(opts) {
|
|
|
72114
73687
|
}
|
|
72115
73688
|
for (const s of scored) {
|
|
72116
73689
|
console.log(
|
|
72117
|
-
" " + String(s.score).padStart(3) + "/100 " + (s.createdAt
|
|
73690
|
+
" " + String(s.score).padStart(3) + "/100 " + formatDateTime(s.createdAt) + " " + s.id + " " + (s.title ?? "")
|
|
72118
73691
|
);
|
|
72119
73692
|
}
|
|
72120
73693
|
const avg = Math.round(
|
|
@@ -72146,6 +73719,18 @@ async function runAsk(id, directive, opts) {
|
|
|
72146
73719
|
const report = migrateReport(row.report);
|
|
72147
73720
|
const v = refineInvestigation(report, directive);
|
|
72148
73721
|
console.log(opts.json ? refinedToJSON(report, v) : renderRefined(report, v));
|
|
73722
|
+
if (!opts.json && report.aiJudgment) {
|
|
73723
|
+
const j = report.aiJudgment;
|
|
73724
|
+
console.log("");
|
|
73725
|
+
console.log(import_picocolors17.default.dim("\u2500".repeat(60)));
|
|
73726
|
+
console.log(import_picocolors17.default.dim(`Stored AI judgment (${j.provider}, ${j.generatedAt}):`));
|
|
73727
|
+
if (j.rootCauseAssessment) {
|
|
73728
|
+
console.log(import_picocolors17.default.bold("Root cause (AI):"), j.rootCauseAssessment.summary);
|
|
73729
|
+
console.log(import_picocolors17.default.dim(`Uncertainty: ${j.rootCauseAssessment.uncertainty}`));
|
|
73730
|
+
} else {
|
|
73731
|
+
console.log(import_picocolors17.default.bold("AI Why:"), j.why);
|
|
73732
|
+
}
|
|
73733
|
+
}
|
|
72149
73734
|
} catch (err) {
|
|
72150
73735
|
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
72151
73736
|
if (code === "22P02") {
|
|
@@ -72290,6 +73875,7 @@ async function runLogs(service, opts) {
|
|
|
72290
73875
|
return 1;
|
|
72291
73876
|
}
|
|
72292
73877
|
const resolvedService = service ?? renv.connectors.elasticsearch?.serviceName;
|
|
73878
|
+
const indexPattern = renv.connectors.elasticsearch?.indexPattern ?? "*";
|
|
72293
73879
|
try {
|
|
72294
73880
|
const compat = await logs.checkCompatibility({
|
|
72295
73881
|
requiresService: resolvedService !== void 0,
|
|
@@ -72315,7 +73901,8 @@ async function runLogs(service, opts) {
|
|
|
72315
73901
|
} catch {
|
|
72316
73902
|
console.warn(import_picocolors20.default.dim("[warn] Elasticsearch compatibility check unavailable \u2014 proceeding."));
|
|
72317
73903
|
}
|
|
72318
|
-
const from = sinceToIso(opts.since);
|
|
73904
|
+
const from = sinceToIso(opts.since) ?? new Date(Date.now() - 7 * 864e5).toISOString();
|
|
73905
|
+
const fromDisplay = from.slice(0, 16).replace("T", " ");
|
|
72319
73906
|
if (opts.raw === true) {
|
|
72320
73907
|
const records = await logs.searchLogs({
|
|
72321
73908
|
service: resolvedService,
|
|
@@ -72337,22 +73924,42 @@ async function runLogs(service, opts) {
|
|
|
72337
73924
|
}
|
|
72338
73925
|
return 0;
|
|
72339
73926
|
}
|
|
72340
|
-
|
|
73927
|
+
let analysis = await logs.analyzeErrors({
|
|
72341
73928
|
service: resolvedService,
|
|
72342
73929
|
from,
|
|
72343
73930
|
text: opts.grep
|
|
72344
73931
|
});
|
|
73932
|
+
let scopeService = resolvedService;
|
|
73933
|
+
let broadeningNote;
|
|
73934
|
+
if (analysis.signatures.length === 0 && resolvedService !== void 0) {
|
|
73935
|
+
const broader = await logs.analyzeErrors({ from, text: opts.grep });
|
|
73936
|
+
if (broader.signatures.length > 0) {
|
|
73937
|
+
analysis = broader;
|
|
73938
|
+
broadeningNote = `No errors found for service "${resolvedService}" \u2014 showing all services`;
|
|
73939
|
+
scopeService = void 0;
|
|
73940
|
+
}
|
|
73941
|
+
}
|
|
72345
73942
|
console.log(
|
|
72346
73943
|
import_picocolors20.default.bold(`Error analysis`) + import_picocolors20.default.dim(
|
|
72347
|
-
` \u2014 ${renv.project}/${renv.env}` + (
|
|
73944
|
+
` \u2014 ${renv.project}/${renv.env}` + (scopeService ? ` \xB7 service ${scopeService}` : "") + (opts.grep ? ` \xB7 grep "${opts.grep}"` : "")
|
|
72348
73945
|
)
|
|
72349
73946
|
);
|
|
73947
|
+
console.log(import_picocolors20.default.dim(` index: ${indexPattern} \xB7 from: ${fromDisplay} UTC \xB7 severity: error+`));
|
|
73948
|
+
if (broadeningNote) {
|
|
73949
|
+
console.log(import_picocolors20.default.yellow(` ${broadeningNote}`));
|
|
73950
|
+
}
|
|
72350
73951
|
console.log(
|
|
72351
73952
|
` ${analysis.totalErrors} error(s) \xB7 ${analysis.signatures.length} signature(s) \xB7 ${import_picocolors20.default.yellow(String(analysis.newSignatures.length))} new`
|
|
72352
73953
|
);
|
|
72353
73954
|
console.log("");
|
|
72354
73955
|
if (analysis.signatures.length === 0) {
|
|
72355
73956
|
console.log(import_picocolors20.default.dim(" No error-level logs in the window."));
|
|
73957
|
+
console.log(
|
|
73958
|
+
import_picocolors20.default.dim(
|
|
73959
|
+
` Searched: ${scopeService ? `service "${scopeService}"` : "all services"} \xB7 index: ${indexPattern} \xB7 from: ${fromDisplay} UTC`
|
|
73960
|
+
)
|
|
73961
|
+
);
|
|
73962
|
+
console.log(import_picocolors20.default.dim(` Tip: use --since to widen the window (e.g. --since 30d).`));
|
|
72356
73963
|
return 0;
|
|
72357
73964
|
}
|
|
72358
73965
|
for (const s of analysis.signatures) {
|
|
@@ -72477,18 +74084,29 @@ async function runMetrics(hint, opts) {
|
|
|
72477
74084
|
const flagged = findings2.filter((f) => f.anomaly !== "none");
|
|
72478
74085
|
const ok = findings2.filter((f) => f.anomaly === "none");
|
|
72479
74086
|
if (findings2.length === 0) {
|
|
72480
|
-
|
|
72481
|
-
|
|
72482
|
-
|
|
72483
|
-
)
|
|
72484
|
-
|
|
74087
|
+
if (hint !== void 0) {
|
|
74088
|
+
const tokens = extractHintTokens(hint);
|
|
74089
|
+
console.log(import_picocolors21.default.dim(`No panels matched hint "${hint}".`));
|
|
74090
|
+
if (tokens.length > 0) {
|
|
74091
|
+
console.log(import_picocolors21.default.dim(` Searched for panels containing: ${tokens.join(", ")}`));
|
|
74092
|
+
}
|
|
74093
|
+
console.log(
|
|
74094
|
+
import_picocolors21.default.dim(
|
|
74095
|
+
` Tip: run ${import_picocolors21.default.bold("horus metrics")} (no hint) to see all available panels, then use a panel title or keyword directly.`
|
|
74096
|
+
)
|
|
74097
|
+
);
|
|
74098
|
+
} else {
|
|
74099
|
+
console.log(import_picocolors21.default.dim("No panels found in configured Grafana dashboards."));
|
|
74100
|
+
}
|
|
72485
74101
|
return 0;
|
|
72486
74102
|
}
|
|
72487
74103
|
const hintSuffix = hint !== void 0 ? ` (hint: "${hint}")` : "";
|
|
74104
|
+
const matchSources = hint !== void 0 ? [...new Set(findings2.map((f) => f.matchSource).filter(Boolean))] : [];
|
|
74105
|
+
const matchSuffix = matchSources.length > 0 ? import_picocolors21.default.dim(` [matched via ${matchSources.join(", ")}]`) : "";
|
|
72488
74106
|
console.log(
|
|
72489
74107
|
import_picocolors21.default.bold(
|
|
72490
74108
|
`Grafana metrics${hintSuffix} \u2014 ${findings2.length} series across panels`
|
|
72491
|
-
)
|
|
74109
|
+
) + matchSuffix
|
|
72492
74110
|
);
|
|
72493
74111
|
console.log(
|
|
72494
74112
|
import_picocolors21.default.dim(
|
|
@@ -72555,8 +74173,9 @@ async function runState(opts) {
|
|
|
72555
74173
|
const analysis = await mongo.analyzeState(
|
|
72556
74174
|
staleHours !== void 0 ? { staleHours } : {}
|
|
72557
74175
|
);
|
|
74176
|
+
const discoveryNote = analysis.autoDiscovered ? import_picocolors22.default.dim(` (${analysis.collections.length} collections, auto-discovered)`) : "";
|
|
72558
74177
|
console.log(
|
|
72559
|
-
import_picocolors22.default.bold("State analysis") + import_picocolors22.default.dim(` \u2014 ${renv.project}/${renv.env} \xB7 db ${analysis.database}`)
|
|
74178
|
+
import_picocolors22.default.bold("State analysis") + import_picocolors22.default.dim(` \u2014 ${renv.project}/${renv.env} \xB7 db ${analysis.database}`) + discoveryNote
|
|
72560
74179
|
);
|
|
72561
74180
|
console.log("");
|
|
72562
74181
|
let signals = 0;
|
|
@@ -72574,9 +74193,23 @@ async function runState(opts) {
|
|
|
72574
74193
|
console.log(flags.length > 0 ? `${head} ${flags.join(" ")}` : import_picocolors22.default.dim(head));
|
|
72575
74194
|
}
|
|
72576
74195
|
console.log("");
|
|
72577
|
-
|
|
72578
|
-
|
|
72579
|
-
|
|
74196
|
+
if (analysis.collections.length === 0) {
|
|
74197
|
+
console.log(
|
|
74198
|
+
import_picocolors22.default.yellow(" No collections discovered in this database.") + import_picocolors22.default.dim(" Verify the database name in your MongoDB connector config.")
|
|
74199
|
+
);
|
|
74200
|
+
} else {
|
|
74201
|
+
const scope = analysis.autoDiscovered ? "collection(s)" : "allowlisted collection(s)";
|
|
74202
|
+
console.log(
|
|
74203
|
+
signals > 0 ? ` ${import_picocolors22.default.bold(String(signals))} state signal(s) across ${analysis.collections.length} ${scope}` : import_picocolors22.default.dim(` No state anomalies across ${analysis.collections.length} ${scope}.`)
|
|
74204
|
+
);
|
|
74205
|
+
if (analysis.autoDiscovered) {
|
|
74206
|
+
console.log(
|
|
74207
|
+
import_picocolors22.default.dim(
|
|
74208
|
+
` Tip: add a "collections" list to your MongoDB connector config to restrict analysis.`
|
|
74209
|
+
)
|
|
74210
|
+
);
|
|
74211
|
+
}
|
|
74212
|
+
}
|
|
72580
74213
|
return 0;
|
|
72581
74214
|
} finally {
|
|
72582
74215
|
await mongo.close();
|
|
@@ -72669,8 +74302,7 @@ async function runSetup(opts) {
|
|
|
72669
74302
|
write(
|
|
72670
74303
|
import_picocolors25.default.dim(
|
|
72671
74304
|
` install it (Python 3.11+ required):
|
|
72672
|
-
|
|
72673
|
-
or: pip install axoniq==${PINNED_SOURCE_VERSION}
|
|
74305
|
+
pip install horus-source
|
|
72674
74306
|
ensure ~/.local/bin is on your PATH`
|
|
72675
74307
|
)
|
|
72676
74308
|
);
|
|
@@ -72682,8 +74314,7 @@ async function runSetup(opts) {
|
|
|
72682
74314
|
write(
|
|
72683
74315
|
import_picocolors25.default.dim(
|
|
72684
74316
|
` update it:
|
|
72685
|
-
|
|
72686
|
-
or: pip install axoniq==${PINNED_SOURCE_VERSION}`
|
|
74317
|
+
pip install --upgrade horus-source`
|
|
72687
74318
|
)
|
|
72688
74319
|
);
|
|
72689
74320
|
} else {
|
|
@@ -72787,12 +74418,192 @@ async function runSetup(opts) {
|
|
|
72787
74418
|
init_cjs_shims();
|
|
72788
74419
|
var import_node_readline = require("readline");
|
|
72789
74420
|
var import_node_child_process5 = require("child_process");
|
|
74421
|
+
var import_picocolors27 = __toESM(require_picocolors(), 1);
|
|
74422
|
+
|
|
74423
|
+
// ../../packages/cli/src/lib/tty-selector.ts
|
|
74424
|
+
init_cjs_shims();
|
|
74425
|
+
var import_node_tty = require("tty");
|
|
72790
74426
|
var import_picocolors26 = __toESM(require_picocolors(), 1);
|
|
74427
|
+
var ExitPromptError = class extends Error {
|
|
74428
|
+
constructor(message = "Prompt was cancelled") {
|
|
74429
|
+
super(message);
|
|
74430
|
+
this.name = "ExitPromptError";
|
|
74431
|
+
}
|
|
74432
|
+
};
|
|
74433
|
+
function isInteractive(input) {
|
|
74434
|
+
const stream = input ?? process.stdin;
|
|
74435
|
+
return stream.isTTY === true && typeof stream.setRawMode === "function";
|
|
74436
|
+
}
|
|
74437
|
+
async function checkboxSearch(opts) {
|
|
74438
|
+
const { message, choices } = opts;
|
|
74439
|
+
const pageSize = opts.pageSize ?? 10;
|
|
74440
|
+
const input = opts.input ?? process.stdin;
|
|
74441
|
+
const output = opts.output ?? process.stdout;
|
|
74442
|
+
if (!isInteractive(input)) {
|
|
74443
|
+
throw new Error("checkboxSearch requires an interactive TTY");
|
|
74444
|
+
}
|
|
74445
|
+
if (choices.length === 0) {
|
|
74446
|
+
return [];
|
|
74447
|
+
}
|
|
74448
|
+
return new Promise((resolve8, reject) => {
|
|
74449
|
+
let filter = "";
|
|
74450
|
+
const selected = /* @__PURE__ */ new Set();
|
|
74451
|
+
let cursor = 0;
|
|
74452
|
+
let filtered = choices;
|
|
74453
|
+
let visibleOffset = 0;
|
|
74454
|
+
let lastRenderLines = 0;
|
|
74455
|
+
const wasRaw = input.isRaw;
|
|
74456
|
+
function updateFiltered() {
|
|
74457
|
+
const f = filter.toLowerCase();
|
|
74458
|
+
filtered = f ? choices.filter((c) => c.toLowerCase().includes(f)) : choices;
|
|
74459
|
+
cursor = Math.min(cursor, Math.max(0, filtered.length - 1));
|
|
74460
|
+
updateVisibleOffset();
|
|
74461
|
+
}
|
|
74462
|
+
function updateVisibleOffset() {
|
|
74463
|
+
if (cursor < visibleOffset) {
|
|
74464
|
+
visibleOffset = cursor;
|
|
74465
|
+
} else if (cursor >= visibleOffset + pageSize) {
|
|
74466
|
+
visibleOffset = cursor - pageSize + 1;
|
|
74467
|
+
}
|
|
74468
|
+
const maxOffset = Math.max(0, filtered.length - pageSize);
|
|
74469
|
+
visibleOffset = Math.max(0, Math.min(visibleOffset, maxOffset));
|
|
74470
|
+
}
|
|
74471
|
+
function clear() {
|
|
74472
|
+
output.write("\x1B[?25l");
|
|
74473
|
+
if (lastRenderLines > 0) {
|
|
74474
|
+
output.write(`\x1B[${lastRenderLines}A`);
|
|
74475
|
+
output.write("\x1B[0J");
|
|
74476
|
+
}
|
|
74477
|
+
}
|
|
74478
|
+
function render() {
|
|
74479
|
+
clear();
|
|
74480
|
+
const lines = [];
|
|
74481
|
+
lines.push(
|
|
74482
|
+
`? ${import_picocolors26.default.bold(message)} ${import_picocolors26.default.dim("(\u2191\u2193 navigate \u2022 space toggle \u2022 enter confirm \u2022 type filter)")}`
|
|
74483
|
+
);
|
|
74484
|
+
const visible = filtered.slice(visibleOffset, visibleOffset + pageSize);
|
|
74485
|
+
for (let i = 0; i < visible.length; i++) {
|
|
74486
|
+
const idx = visibleOffset + i;
|
|
74487
|
+
const choice = visible[i];
|
|
74488
|
+
const isCursor = idx === cursor;
|
|
74489
|
+
const isSelected = selected.has(choice);
|
|
74490
|
+
const marker = isSelected ? import_picocolors26.default.green("\u25CF") : " ";
|
|
74491
|
+
const prefix = `[${marker}]`;
|
|
74492
|
+
const label = isCursor ? import_picocolors26.default.cyan(choice) : choice;
|
|
74493
|
+
const pointer = isCursor ? import_picocolors26.default.cyan("\u276F") : " ";
|
|
74494
|
+
lines.push(` ${pointer} ${prefix} ${label}`);
|
|
74495
|
+
}
|
|
74496
|
+
if (filtered.length === 0) {
|
|
74497
|
+
lines.push(import_picocolors26.default.dim(" No matches"));
|
|
74498
|
+
}
|
|
74499
|
+
const statusParts = [
|
|
74500
|
+
`${selected.size} selected`,
|
|
74501
|
+
`${filtered.length}/${choices.length} matches`
|
|
74502
|
+
];
|
|
74503
|
+
if (filter) statusParts.push(`filter: ${filter}`);
|
|
74504
|
+
lines.push(import_picocolors26.default.dim(` ${statusParts.join(" \xB7 ")}`));
|
|
74505
|
+
output.write(lines.join("\n"));
|
|
74506
|
+
lastRenderLines = lines.length;
|
|
74507
|
+
}
|
|
74508
|
+
function finish(values2) {
|
|
74509
|
+
cleanup();
|
|
74510
|
+
const summary = values2.length > 0 ? values2.join(", ") : import_picocolors26.default.dim("none");
|
|
74511
|
+
output.write(`
|
|
74512
|
+
? ${import_picocolors26.default.bold(message)} ${summary}
|
|
74513
|
+
`);
|
|
74514
|
+
resolve8(values2);
|
|
74515
|
+
}
|
|
74516
|
+
function cancel() {
|
|
74517
|
+
cleanup();
|
|
74518
|
+
reject(new ExitPromptError());
|
|
74519
|
+
}
|
|
74520
|
+
function cleanup() {
|
|
74521
|
+
input.setRawMode(wasRaw);
|
|
74522
|
+
input.pause();
|
|
74523
|
+
input.removeListener("data", onData);
|
|
74524
|
+
output.write("\x1B[?25h");
|
|
74525
|
+
}
|
|
74526
|
+
function toggleCurrent() {
|
|
74527
|
+
const choice = filtered[cursor];
|
|
74528
|
+
if (choice === void 0) return;
|
|
74529
|
+
if (selected.has(choice)) {
|
|
74530
|
+
selected.delete(choice);
|
|
74531
|
+
} else {
|
|
74532
|
+
selected.add(choice);
|
|
74533
|
+
}
|
|
74534
|
+
}
|
|
74535
|
+
function onData(chunk) {
|
|
74536
|
+
const s = chunk.toString();
|
|
74537
|
+
if (s === "") {
|
|
74538
|
+
cancel();
|
|
74539
|
+
return;
|
|
74540
|
+
}
|
|
74541
|
+
if (s === "\x1B") {
|
|
74542
|
+
if (filter) {
|
|
74543
|
+
filter = "";
|
|
74544
|
+
cursor = 0;
|
|
74545
|
+
updateFiltered();
|
|
74546
|
+
} else {
|
|
74547
|
+
cancel();
|
|
74548
|
+
}
|
|
74549
|
+
render();
|
|
74550
|
+
return;
|
|
74551
|
+
}
|
|
74552
|
+
if (s === "\r" || s === "\n") {
|
|
74553
|
+
finish(Array.from(selected));
|
|
74554
|
+
return;
|
|
74555
|
+
}
|
|
74556
|
+
if (s === " ") {
|
|
74557
|
+
toggleCurrent();
|
|
74558
|
+
render();
|
|
74559
|
+
return;
|
|
74560
|
+
}
|
|
74561
|
+
if (s === "\x7F" || s === "\b") {
|
|
74562
|
+
if (filter.length > 0) {
|
|
74563
|
+
filter = filter.slice(0, -1);
|
|
74564
|
+
cursor = 0;
|
|
74565
|
+
updateFiltered();
|
|
74566
|
+
}
|
|
74567
|
+
render();
|
|
74568
|
+
return;
|
|
74569
|
+
}
|
|
74570
|
+
if (s === "\x1B[A") {
|
|
74571
|
+
if (cursor > 0) {
|
|
74572
|
+
cursor -= 1;
|
|
74573
|
+
updateVisibleOffset();
|
|
74574
|
+
}
|
|
74575
|
+
render();
|
|
74576
|
+
return;
|
|
74577
|
+
}
|
|
74578
|
+
if (s === "\x1B[B") {
|
|
74579
|
+
if (cursor < filtered.length - 1) {
|
|
74580
|
+
cursor += 1;
|
|
74581
|
+
updateVisibleOffset();
|
|
74582
|
+
}
|
|
74583
|
+
render();
|
|
74584
|
+
return;
|
|
74585
|
+
}
|
|
74586
|
+
if (s.length === 1 && s.charCodeAt(0) >= 32 && s.charCodeAt(0) <= 126) {
|
|
74587
|
+
filter += s;
|
|
74588
|
+
cursor = 0;
|
|
74589
|
+
updateFiltered();
|
|
74590
|
+
render();
|
|
74591
|
+
}
|
|
74592
|
+
}
|
|
74593
|
+
input.setRawMode(true);
|
|
74594
|
+
input.resume();
|
|
74595
|
+
input.on("data", onData);
|
|
74596
|
+
updateFiltered();
|
|
74597
|
+
render();
|
|
74598
|
+
});
|
|
74599
|
+
}
|
|
74600
|
+
|
|
74601
|
+
// ../../packages/cli/src/commands/connect.ts
|
|
72791
74602
|
var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
|
|
72792
74603
|
async function runConnect(type, opts) {
|
|
72793
74604
|
if (!SUPPORTED.includes(type)) {
|
|
72794
74605
|
console.error(
|
|
72795
|
-
|
|
74606
|
+
import_picocolors27.default.red(`Unknown connector type: ${type}`) + import_picocolors27.default.dim(`
|
|
72796
74607
|
supported: ${SUPPORTED.join(", ")}`)
|
|
72797
74608
|
);
|
|
72798
74609
|
return 1;
|
|
@@ -72823,18 +74634,20 @@ async function runConnect(type, opts) {
|
|
|
72823
74634
|
if (!probeResult.ok) {
|
|
72824
74635
|
console.error(
|
|
72825
74636
|
`
|
|
72826
|
-
${
|
|
74637
|
+
${import_picocolors27.default.red(`\u2717 Could not reach ${connectorType}:`)} ${probeResult.detail}` + import_picocolors27.default.dim("\n Fix the connection and retry, or pass --no-test to skip.")
|
|
72827
74638
|
);
|
|
72828
74639
|
return 1;
|
|
72829
74640
|
}
|
|
72830
|
-
console.log(
|
|
72831
|
-
|
|
74641
|
+
console.log(
|
|
74642
|
+
`
|
|
74643
|
+
${import_picocolors27.default.green("\u2713")} ${connectorType} reachable ${import_picocolors27.default.dim(`(${probeResult.detail})`)}`
|
|
74644
|
+
);
|
|
72832
74645
|
}
|
|
72833
74646
|
const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0;
|
|
72834
74647
|
if (hasLiteralCredentials) {
|
|
72835
74648
|
if (isGitTracked(configPath, root)) {
|
|
72836
74649
|
console.error(
|
|
72837
|
-
|
|
74650
|
+
import_picocolors27.default.red(".horus/config.json is already tracked by Git.") + "\nStoring credentials here would expose them in the repository.\n" + import_picocolors27.default.dim(
|
|
72838
74651
|
" Option A \u2014 remove from Git index then re-run:\n git rm --cached .horus/config.json\n horus connect " + type + " ...\n\n Option B \u2014 keep credentials in the environment and reference them:\n export ES_URL=https://...\n export ES_USERNAME=...\n export ES_PASSWORD=...\n Then edit .horus/config.json manually to use urlEnv/usernameEnv/passwordEnv."
|
|
72839
74652
|
)
|
|
72840
74653
|
);
|
|
@@ -72843,29 +74656,52 @@ ${import_picocolors26.default.green("\u2713")} ${connectorType} reachable ${impo
|
|
|
72843
74656
|
ensureCredentialGitignore(root);
|
|
72844
74657
|
}
|
|
72845
74658
|
patchLocalConnector(configPath, connectorType, patch, filled.env);
|
|
72846
|
-
console.log(
|
|
74659
|
+
console.log(
|
|
74660
|
+
`${import_picocolors27.default.green("\u2713")} ${import_picocolors27.default.bold(connectorType)} connector saved \u2192 ${import_picocolors27.default.dim(configPath)}`
|
|
74661
|
+
);
|
|
72847
74662
|
printSummary(connectorType, filled);
|
|
72848
|
-
console.log(
|
|
74663
|
+
console.log(import_picocolors27.default.dim(`
|
|
72849
74664
|
run: horus investigate "<hint>"`));
|
|
72850
74665
|
return 0;
|
|
72851
74666
|
} catch (err) {
|
|
72852
|
-
|
|
74667
|
+
if (err instanceof ExitPromptError) {
|
|
74668
|
+
console.error(import_picocolors27.default.red("Cancelled."));
|
|
74669
|
+
return 1;
|
|
74670
|
+
}
|
|
74671
|
+
console.error(import_picocolors27.default.red(err.message));
|
|
72853
74672
|
return 1;
|
|
72854
74673
|
}
|
|
72855
74674
|
}
|
|
72856
74675
|
async function fillInteractive(type, opts) {
|
|
72857
74676
|
const needsInteraction = missingRequired(type, opts);
|
|
72858
74677
|
if (!needsInteraction) return opts;
|
|
72859
|
-
console.log(
|
|
72860
|
-
|
|
72861
|
-
`)
|
|
74678
|
+
console.log(
|
|
74679
|
+
`
|
|
74680
|
+
${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.default.dim("(press Enter to skip optional fields)")}
|
|
74681
|
+
`
|
|
74682
|
+
);
|
|
72862
74683
|
const filled = { ...opts };
|
|
72863
74684
|
switch (type) {
|
|
72864
74685
|
case "elasticsearch":
|
|
72865
74686
|
filled.url = filled.url ?? await ask("URL", "https://elastic.example.com");
|
|
72866
74687
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
72867
74688
|
filled.password = filled.password ?? (await askPassword("Password") || void 0);
|
|
72868
|
-
filled.indexPattern
|
|
74689
|
+
if (filled.indexPattern === void 0 && filled.indexPatterns === void 0) {
|
|
74690
|
+
const discovered = await discoverEsIndices(
|
|
74691
|
+
filled.url,
|
|
74692
|
+
filled.username,
|
|
74693
|
+
filled.password
|
|
74694
|
+
);
|
|
74695
|
+
if (discovered.length > 0) {
|
|
74696
|
+
const selected = await askIndexSelection(discovered);
|
|
74697
|
+
if (selected.length > 0) {
|
|
74698
|
+
filled.indexPatterns = selected;
|
|
74699
|
+
}
|
|
74700
|
+
}
|
|
74701
|
+
if (filled.indexPatterns === void 0) {
|
|
74702
|
+
filled.indexPattern = await ask("Index pattern", "logs-*");
|
|
74703
|
+
}
|
|
74704
|
+
}
|
|
72869
74705
|
filled.service = filled.service ?? (await ask("Service name", "") || void 0);
|
|
72870
74706
|
break;
|
|
72871
74707
|
case "mongodb":
|
|
@@ -72877,18 +74713,45 @@ ${import_picocolors26.default.bold(`Connect ${type}`)} ${import_picocolors26.def
|
|
|
72877
74713
|
filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
|
|
72878
74714
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
72879
74715
|
filled.password = filled.password ?? (await askPassword("Password") || void 0);
|
|
72880
|
-
|
|
74716
|
+
if (filled.dashboard === void 0 && filled.dashboardUids === void 0) {
|
|
74717
|
+
const discovered = await discoverGrafanaDashboards(
|
|
74718
|
+
filled.url,
|
|
74719
|
+
filled.username,
|
|
74720
|
+
filled.password
|
|
74721
|
+
);
|
|
74722
|
+
if (discovered.length > 0) {
|
|
74723
|
+
const selected = await askDashboardSelection(discovered);
|
|
74724
|
+
if (selected.length > 0) {
|
|
74725
|
+
filled.dashboardUids = selected.map((d) => d.uid);
|
|
74726
|
+
}
|
|
74727
|
+
}
|
|
74728
|
+
if (filled.dashboardUids === void 0) {
|
|
74729
|
+
filled.dashboard = await ask("Default dashboard uid", "", false) || void 0;
|
|
74730
|
+
}
|
|
74731
|
+
}
|
|
72881
74732
|
break;
|
|
72882
|
-
case "redis":
|
|
74733
|
+
case "redis": {
|
|
74734
|
+
console.log(
|
|
74735
|
+
import_picocolors27.default.dim(
|
|
74736
|
+
" Tip: embed credentials directly in the URL \u2014 redis://:password@host:6379\n or enter the URL and password separately below."
|
|
74737
|
+
)
|
|
74738
|
+
);
|
|
72883
74739
|
filled.url = filled.url ?? await ask("URL", "redis://localhost:6379");
|
|
74740
|
+
if (!redisUrlHasPassword(filled.url)) {
|
|
74741
|
+
const pw = await askPassword("Password") || void 0;
|
|
74742
|
+
if (pw !== void 0 && filled.url !== void 0) {
|
|
74743
|
+
filled.url = injectRedisPassword(filled.url, pw);
|
|
74744
|
+
}
|
|
74745
|
+
}
|
|
72884
74746
|
break;
|
|
74747
|
+
}
|
|
72885
74748
|
}
|
|
72886
74749
|
return filled;
|
|
72887
74750
|
}
|
|
72888
74751
|
function missingRequired(type, opts) {
|
|
72889
74752
|
switch (type) {
|
|
72890
74753
|
case "elasticsearch":
|
|
72891
|
-
return !opts.url || !opts.indexPattern;
|
|
74754
|
+
return !opts.url || !opts.indexPattern && !opts.indexPatterns?.length;
|
|
72892
74755
|
case "mongodb":
|
|
72893
74756
|
return !opts.url || !opts.database;
|
|
72894
74757
|
case "grafana":
|
|
@@ -72899,10 +74762,14 @@ function missingRequired(type, opts) {
|
|
|
72899
74762
|
}
|
|
72900
74763
|
function ask(label, placeholder = "", required = true) {
|
|
72901
74764
|
return new Promise((resolve8) => {
|
|
72902
|
-
const hint = placeholder ?
|
|
72903
|
-
const suffix = required ? "" :
|
|
74765
|
+
const hint = placeholder ? import_picocolors27.default.dim(` (${placeholder})`) : "";
|
|
74766
|
+
const suffix = required ? "" : import_picocolors27.default.dim(" [optional]");
|
|
72904
74767
|
process.stdout.write(` ${label}${suffix}${hint}: `);
|
|
72905
|
-
const rl = (0, import_node_readline.createInterface)({
|
|
74768
|
+
const rl = (0, import_node_readline.createInterface)({
|
|
74769
|
+
input: process.stdin,
|
|
74770
|
+
output: process.stdout,
|
|
74771
|
+
terminal: false
|
|
74772
|
+
});
|
|
72906
74773
|
rl.once("line", (line2) => {
|
|
72907
74774
|
rl.close();
|
|
72908
74775
|
resolve8(line2.trim() || (required ? placeholder : ""));
|
|
@@ -72913,7 +74780,7 @@ function askPassword(label) {
|
|
|
72913
74780
|
return new Promise((resolve8) => {
|
|
72914
74781
|
const stdin = process.stdin;
|
|
72915
74782
|
if (typeof stdin.setRawMode === "function") {
|
|
72916
|
-
process.stdout.write(` ${label}${
|
|
74783
|
+
process.stdout.write(` ${label}${import_picocolors27.default.dim(" [optional]")}: `);
|
|
72917
74784
|
stdin.setRawMode(true);
|
|
72918
74785
|
stdin.resume();
|
|
72919
74786
|
let value = "";
|
|
@@ -72944,8 +74811,15 @@ function askPassword(label) {
|
|
|
72944
74811
|
});
|
|
72945
74812
|
}
|
|
72946
74813
|
function buildEsPatch(opts) {
|
|
72947
|
-
if (!opts.indexPattern
|
|
72948
|
-
|
|
74814
|
+
if (!opts.indexPattern && !opts.indexPatterns?.length) {
|
|
74815
|
+
throw new Error("Index pattern is required for elasticsearch");
|
|
74816
|
+
}
|
|
74817
|
+
const patch = {};
|
|
74818
|
+
if (opts.indexPatterns && opts.indexPatterns.length > 0) {
|
|
74819
|
+
patch["indexPatterns"] = opts.indexPatterns;
|
|
74820
|
+
} else {
|
|
74821
|
+
patch["indexPattern"] = opts.indexPattern;
|
|
74822
|
+
}
|
|
72949
74823
|
if (opts.url) patch["url"] = opts.url;
|
|
72950
74824
|
if (opts.username) patch["username"] = opts.username;
|
|
72951
74825
|
if (opts.password) patch["password"] = opts.password;
|
|
@@ -72966,7 +74840,11 @@ function buildGrafanaPatch(opts) {
|
|
|
72966
74840
|
if (opts.url) patch["url"] = opts.url;
|
|
72967
74841
|
if (opts.username) patch["username"] = opts.username;
|
|
72968
74842
|
if (opts.password) patch["password"] = opts.password;
|
|
72969
|
-
if (opts.
|
|
74843
|
+
if (opts.dashboardUids && opts.dashboardUids.length > 0) {
|
|
74844
|
+
patch["dashboards"] = opts.dashboardUids;
|
|
74845
|
+
} else if (opts.dashboard) {
|
|
74846
|
+
patch["dashboard"] = opts.dashboard;
|
|
74847
|
+
}
|
|
72970
74848
|
return patch;
|
|
72971
74849
|
}
|
|
72972
74850
|
function buildRedisPatch(opts) {
|
|
@@ -72985,7 +74863,10 @@ async function probe(type, opts) {
|
|
|
72985
74863
|
password: opts.password
|
|
72986
74864
|
});
|
|
72987
74865
|
const controller = new AbortController();
|
|
72988
|
-
const timer2 = setTimeout(
|
|
74866
|
+
const timer2 = setTimeout(
|
|
74867
|
+
() => controller.abort(new Error("timed out after 8s")),
|
|
74868
|
+
8e3
|
|
74869
|
+
);
|
|
72989
74870
|
try {
|
|
72990
74871
|
return await client.health(controller.signal);
|
|
72991
74872
|
} catch (err) {
|
|
@@ -73012,7 +74893,9 @@ async function probe(type, opts) {
|
|
|
73012
74893
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
73013
74894
|
const headers = { "Content-Type": "application/json" };
|
|
73014
74895
|
if (opts.username && opts.password) {
|
|
73015
|
-
const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString(
|
|
74896
|
+
const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString(
|
|
74897
|
+
"base64"
|
|
74898
|
+
);
|
|
73016
74899
|
headers["Authorization"] = `Basic ${encoded}`;
|
|
73017
74900
|
}
|
|
73018
74901
|
const res = await fetch(`${opts.url.replace(/\/$/, "")}/api/health`, {
|
|
@@ -73056,15 +74939,24 @@ function tcpProbe(host, port, timeoutMs = 3e3) {
|
|
|
73056
74939
|
}
|
|
73057
74940
|
function printSummary(type, opts) {
|
|
73058
74941
|
const lines = [];
|
|
73059
|
-
if (opts.url) lines.push(` url:
|
|
73060
|
-
if (opts.username) lines.push(` username:
|
|
73061
|
-
if (opts.password)
|
|
73062
|
-
|
|
73063
|
-
if (opts.
|
|
73064
|
-
|
|
73065
|
-
if (opts.
|
|
73066
|
-
|
|
73067
|
-
|
|
74942
|
+
if (opts.url) lines.push(` url: ${redactUrl(opts.url)}`);
|
|
74943
|
+
if (opts.username) lines.push(` username: ${opts.username}`);
|
|
74944
|
+
if (opts.password)
|
|
74945
|
+
lines.push(` password: ${"\u2022".repeat(Math.min(opts.password.length, 8))}`);
|
|
74946
|
+
if (opts.indexPatterns && opts.indexPatterns.length > 0) {
|
|
74947
|
+
lines.push(` index-patterns: ${opts.indexPatterns.join(", ")}`);
|
|
74948
|
+
} else if (opts.indexPattern) {
|
|
74949
|
+
lines.push(` index-pattern: ${opts.indexPattern}`);
|
|
74950
|
+
}
|
|
74951
|
+
if (opts.service) lines.push(` service: ${opts.service}`);
|
|
74952
|
+
if (opts.database) lines.push(` database: ${opts.database}`);
|
|
74953
|
+
if (opts.collections) lines.push(` collections: ${opts.collections}`);
|
|
74954
|
+
if (opts.dashboardUids && opts.dashboardUids.length > 0) {
|
|
74955
|
+
lines.push(` dashboards: ${opts.dashboardUids.join(", ")}`);
|
|
74956
|
+
} else if (opts.dashboard) {
|
|
74957
|
+
lines.push(` dashboard: ${opts.dashboard}`);
|
|
74958
|
+
}
|
|
74959
|
+
if (lines.length > 0) console.log(import_picocolors27.default.dim(lines.join("\n")));
|
|
73068
74960
|
}
|
|
73069
74961
|
function isGitTracked(filePath, cwd) {
|
|
73070
74962
|
try {
|
|
@@ -73077,6 +74969,119 @@ function isGitTracked(filePath, cwd) {
|
|
|
73077
74969
|
return false;
|
|
73078
74970
|
}
|
|
73079
74971
|
}
|
|
74972
|
+
async function discoverEsIndices(url, username, password) {
|
|
74973
|
+
if (!url) return [];
|
|
74974
|
+
try {
|
|
74975
|
+
const client = new ElasticsearchClient({ baseUrl: url, username, password });
|
|
74976
|
+
const signal = AbortSignal.timeout(8e3);
|
|
74977
|
+
return await client.listIndices(signal);
|
|
74978
|
+
} catch {
|
|
74979
|
+
return [];
|
|
74980
|
+
}
|
|
74981
|
+
}
|
|
74982
|
+
async function askIndexSelection(indices) {
|
|
74983
|
+
if (isInteractive()) {
|
|
74984
|
+
try {
|
|
74985
|
+
return await checkboxSearch({
|
|
74986
|
+
message: "Select index patterns to use",
|
|
74987
|
+
choices: indices,
|
|
74988
|
+
pageSize: 12
|
|
74989
|
+
});
|
|
74990
|
+
} catch (err) {
|
|
74991
|
+
if (err instanceof ExitPromptError) throw err;
|
|
74992
|
+
}
|
|
74993
|
+
}
|
|
74994
|
+
const MAX_DISPLAY = 25;
|
|
74995
|
+
const shown = indices.slice(0, MAX_DISPLAY);
|
|
74996
|
+
console.log("\n Available Elasticsearch indexes/data streams:");
|
|
74997
|
+
shown.forEach((name, i) => {
|
|
74998
|
+
console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${name}`);
|
|
74999
|
+
});
|
|
75000
|
+
if (indices.length > MAX_DISPLAY) {
|
|
75001
|
+
console.log(
|
|
75002
|
+
import_picocolors27.default.dim(
|
|
75003
|
+
` \u2026 and ${indices.length - MAX_DISPLAY} more (type a pattern manually to match all)`
|
|
75004
|
+
)
|
|
75005
|
+
);
|
|
75006
|
+
}
|
|
75007
|
+
const input = (await ask(
|
|
75008
|
+
` Select index patterns to use (e.g. 1,2 or Enter to type pattern manually)`,
|
|
75009
|
+
"",
|
|
75010
|
+
false
|
|
75011
|
+
)).trim();
|
|
75012
|
+
if (!input) return [];
|
|
75013
|
+
if (/^[\d,\s]+$/.test(input)) {
|
|
75014
|
+
const picks = input.split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < shown.length).map((i) => shown[i]);
|
|
75015
|
+
if (picks.length > 0) return picks;
|
|
75016
|
+
}
|
|
75017
|
+
return [input];
|
|
75018
|
+
}
|
|
75019
|
+
async function discoverGrafanaDashboards(url, username, password) {
|
|
75020
|
+
if (!url) return [];
|
|
75021
|
+
try {
|
|
75022
|
+
const client = new GrafanaClient({ baseUrl: url, username, password });
|
|
75023
|
+
const signal = AbortSignal.timeout(8e3);
|
|
75024
|
+
return await client.searchDashboards(void 0, signal);
|
|
75025
|
+
} catch {
|
|
75026
|
+
return [];
|
|
75027
|
+
}
|
|
75028
|
+
}
|
|
75029
|
+
async function askDashboardSelection(dashboards) {
|
|
75030
|
+
if (isInteractive()) {
|
|
75031
|
+
try {
|
|
75032
|
+
const selected = await checkboxSearch({
|
|
75033
|
+
message: "Select dashboards to use",
|
|
75034
|
+
choices: dashboards.map((d) => d.title),
|
|
75035
|
+
pageSize: 12
|
|
75036
|
+
});
|
|
75037
|
+
return dashboards.filter((d) => selected.includes(d.title));
|
|
75038
|
+
} catch (err) {
|
|
75039
|
+
if (err instanceof ExitPromptError) throw err;
|
|
75040
|
+
}
|
|
75041
|
+
}
|
|
75042
|
+
const MAX_DISPLAY = 25;
|
|
75043
|
+
const shown = dashboards.slice(0, MAX_DISPLAY);
|
|
75044
|
+
console.log("\n Available Grafana dashboards:");
|
|
75045
|
+
shown.forEach((d, i) => {
|
|
75046
|
+
const folder = d.folderTitle ? import_picocolors27.default.dim(` (${d.folderTitle})`) : "";
|
|
75047
|
+
console.log(` ${import_picocolors27.default.dim(`[${i + 1}]`)} ${d.title}${folder}`);
|
|
75048
|
+
});
|
|
75049
|
+
if (dashboards.length > MAX_DISPLAY) {
|
|
75050
|
+
console.log(import_picocolors27.default.dim(` \u2026 and ${dashboards.length - MAX_DISPLAY} more`));
|
|
75051
|
+
}
|
|
75052
|
+
const input = (await ask(
|
|
75053
|
+
` Select dashboards to use (e.g. 1,2 or Enter to type uid manually)`,
|
|
75054
|
+
"",
|
|
75055
|
+
false
|
|
75056
|
+
)).trim();
|
|
75057
|
+
if (!input) return [];
|
|
75058
|
+
if (/^[\d,\s]+$/.test(input)) {
|
|
75059
|
+
const picks = input.split(",").map((s) => s.trim()).filter(Boolean).map((s) => parseInt(s, 10) - 1).filter((i) => i >= 0 && i < shown.length).map((i) => shown[i]);
|
|
75060
|
+
return picks;
|
|
75061
|
+
}
|
|
75062
|
+
return [];
|
|
75063
|
+
}
|
|
75064
|
+
function redisUrlHasPassword(raw) {
|
|
75065
|
+
if (!raw) return false;
|
|
75066
|
+
try {
|
|
75067
|
+
return new URL(raw).password !== "";
|
|
75068
|
+
} catch {
|
|
75069
|
+
return false;
|
|
75070
|
+
}
|
|
75071
|
+
}
|
|
75072
|
+
function injectRedisPassword(raw, password) {
|
|
75073
|
+
try {
|
|
75074
|
+
const u = new URL(raw);
|
|
75075
|
+
u.password = encodeURIComponent(password);
|
|
75076
|
+
return u.toString();
|
|
75077
|
+
} catch {
|
|
75078
|
+
const match = /^(rediss?:\/\/)(.*)$/.exec(raw);
|
|
75079
|
+
if (match?.[1] && match?.[2]) {
|
|
75080
|
+
return `${match[1]}:${encodeURIComponent(password)}@${match[2]}`;
|
|
75081
|
+
}
|
|
75082
|
+
return raw;
|
|
75083
|
+
}
|
|
75084
|
+
}
|
|
73080
75085
|
function redactUrl(raw) {
|
|
73081
75086
|
try {
|
|
73082
75087
|
const u = new URL(raw);
|
|
@@ -73096,11 +75101,14 @@ var import_node_child_process6 = require("child_process");
|
|
|
73096
75101
|
var import_node_fs7 = require("fs");
|
|
73097
75102
|
var import_node_util4 = require("util");
|
|
73098
75103
|
var import_node_path9 = require("path");
|
|
73099
|
-
var
|
|
75104
|
+
var import_picocolors28 = __toESM(require_picocolors(), 1);
|
|
73100
75105
|
var execFileAsync = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
|
|
73101
75106
|
var unlinkAsync = (0, import_node_util4.promisify)(import_node_fs7.unlink);
|
|
73102
75107
|
var SPAWNED_HOST_FILE2 = "spawned-host.json";
|
|
73103
75108
|
var START_TIME_TOLERANCE_S = 60;
|
|
75109
|
+
var STOP_WAIT_MS = 5e3;
|
|
75110
|
+
var STOP_POLL_MS = 200;
|
|
75111
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
73104
75112
|
async function runStop(opts) {
|
|
73105
75113
|
try {
|
|
73106
75114
|
if (opts.all) {
|
|
@@ -73110,30 +75118,30 @@ async function runStop(opts) {
|
|
|
73110
75118
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
73111
75119
|
const hostUrl = readSourceHostUrl(root);
|
|
73112
75120
|
if (!hostUrl) {
|
|
73113
|
-
console.log(
|
|
75121
|
+
console.log(import_picocolors28.default.dim("No source-intelligence host found for this repo (.horus/source/host.json absent)."));
|
|
73114
75122
|
return 0;
|
|
73115
75123
|
}
|
|
73116
75124
|
return await stopHost(root, hostUrl);
|
|
73117
75125
|
} catch (err) {
|
|
73118
|
-
console.error(
|
|
75126
|
+
console.error(import_picocolors28.default.red(err.message));
|
|
73119
75127
|
return 1;
|
|
73120
75128
|
}
|
|
73121
75129
|
}
|
|
73122
75130
|
async function stopHost(root, hostUrl) {
|
|
73123
75131
|
const alive = await isHostHealthy(hostUrl);
|
|
73124
75132
|
if (!alive) {
|
|
73125
|
-
console.log(
|
|
75133
|
+
console.log(import_picocolors28.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
73126
75134
|
return 0;
|
|
73127
75135
|
}
|
|
73128
75136
|
const port = extractPort(hostUrl);
|
|
73129
75137
|
if (port === null) {
|
|
73130
|
-
console.error(
|
|
75138
|
+
console.error(import_picocolors28.default.red(`Cannot determine port from host URL: ${hostUrl}`));
|
|
73131
75139
|
return 1;
|
|
73132
75140
|
}
|
|
73133
75141
|
const spawned = readSpawnedHost(root);
|
|
73134
75142
|
if (spawned === null) {
|
|
73135
75143
|
console.error(
|
|
73136
|
-
|
|
75144
|
+
import_picocolors28.default.red(
|
|
73137
75145
|
`No ownership record found (.horus/${SPAWNED_HOST_FILE2} absent). Horus will not stop a host it did not spawn.`
|
|
73138
75146
|
)
|
|
73139
75147
|
);
|
|
@@ -73141,12 +75149,12 @@ async function stopHost(root, hostUrl) {
|
|
|
73141
75149
|
}
|
|
73142
75150
|
const recordError = validateSpawnedRecord(spawned);
|
|
73143
75151
|
if (recordError !== null) {
|
|
73144
|
-
console.error(
|
|
75152
|
+
console.error(import_picocolors28.default.red(`Ownership record is malformed: ${recordError}. Aborting for safety.`));
|
|
73145
75153
|
return 1;
|
|
73146
75154
|
}
|
|
73147
75155
|
if (spawned.port !== port) {
|
|
73148
75156
|
console.error(
|
|
73149
|
-
|
|
75157
|
+
import_picocolors28.default.red(
|
|
73150
75158
|
`Ownership record port (${spawned.port}) does not match host URL port (${port}). Record may be stale.`
|
|
73151
75159
|
)
|
|
73152
75160
|
);
|
|
@@ -73154,7 +75162,7 @@ async function stopHost(root, hostUrl) {
|
|
|
73154
75162
|
}
|
|
73155
75163
|
if (spawned.root !== root) {
|
|
73156
75164
|
console.error(
|
|
73157
|
-
|
|
75165
|
+
import_picocolors28.default.red(
|
|
73158
75166
|
`Ownership record root (${spawned.root}) does not match resolved root (${root}). Record may be stale.`
|
|
73159
75167
|
)
|
|
73160
75168
|
);
|
|
@@ -73162,17 +75170,21 @@ async function stopHost(root, hostUrl) {
|
|
|
73162
75170
|
}
|
|
73163
75171
|
const info = await getProcessInfo(spawned.pid);
|
|
73164
75172
|
if (info === null) {
|
|
73165
|
-
console.
|
|
73166
|
-
|
|
75173
|
+
console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} is no longer running \u2014 already stopped.`));
|
|
75174
|
+
try {
|
|
75175
|
+
await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
75176
|
+
} catch {
|
|
75177
|
+
}
|
|
75178
|
+
return 0;
|
|
73167
75179
|
}
|
|
73168
75180
|
const portStr = String(port);
|
|
73169
|
-
const
|
|
73170
|
-
`(?:^|\\s)(?:\\S*/)?
|
|
75181
|
+
const hostPortRe = new RegExp(
|
|
75182
|
+
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${portStr}(?=\\s|$)`
|
|
73171
75183
|
);
|
|
73172
|
-
if (!
|
|
75184
|
+
if (!hostPortRe.test(info.args)) {
|
|
73173
75185
|
console.error(
|
|
73174
|
-
|
|
73175
|
-
`Pid ${spawned.pid} args do not match "
|
|
75186
|
+
import_picocolors28.default.red(
|
|
75187
|
+
`Pid ${spawned.pid} args do not match "horus-source host --port ${portStr}". Got: "${info.args.slice(0, 120)}". Aborting for safety.`
|
|
73176
75188
|
)
|
|
73177
75189
|
);
|
|
73178
75190
|
return 1;
|
|
@@ -73180,29 +75192,50 @@ async function stopHost(root, hostUrl) {
|
|
|
73180
75192
|
const startTs = new Date(spawned.startedAt).getTime();
|
|
73181
75193
|
const recordedAgeS = Math.round((Date.now() - startTs) / 1e3);
|
|
73182
75194
|
if (!Number.isFinite(info.etimeSeconds)) {
|
|
73183
|
-
console.error(
|
|
75195
|
+
console.error(import_picocolors28.default.red(`Could not read elapsed time for pid ${spawned.pid}. Aborting for safety.`));
|
|
73184
75196
|
return 1;
|
|
73185
75197
|
}
|
|
73186
75198
|
if (Math.abs(info.etimeSeconds - recordedAgeS) > START_TIME_TOLERANCE_S) {
|
|
73187
75199
|
console.error(
|
|
73188
|
-
|
|
75200
|
+
import_picocolors28.default.red(
|
|
73189
75201
|
`Pid ${spawned.pid} age mismatch: record says ~${recordedAgeS}s, process reports ${info.etimeSeconds}s elapsed. Possible PID reuse \u2014 aborting for safety.`
|
|
73190
75202
|
)
|
|
73191
75203
|
);
|
|
73192
75204
|
return 1;
|
|
73193
75205
|
}
|
|
75206
|
+
let signaled = false;
|
|
73194
75207
|
try {
|
|
73195
75208
|
process.kill(spawned.pid, "SIGTERM");
|
|
73196
|
-
|
|
73197
|
-
`${import_picocolors27.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors27.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
|
|
73198
|
-
);
|
|
75209
|
+
signaled = true;
|
|
73199
75210
|
} catch (err) {
|
|
73200
75211
|
if (err.code === "ESRCH") {
|
|
73201
|
-
console.log(
|
|
75212
|
+
console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} exited before signal \u2014 already stopped.`));
|
|
73202
75213
|
} else {
|
|
73203
|
-
console.error(
|
|
75214
|
+
console.error(import_picocolors28.default.red(`Failed to signal pid ${spawned.pid}: ${err.message}`));
|
|
75215
|
+
return 1;
|
|
75216
|
+
}
|
|
75217
|
+
}
|
|
75218
|
+
if (signaled) {
|
|
75219
|
+
const deadline = Date.now() + STOP_WAIT_MS;
|
|
75220
|
+
let exited = false;
|
|
75221
|
+
while (Date.now() < deadline) {
|
|
75222
|
+
await sleep(STOP_POLL_MS);
|
|
75223
|
+
if (await getProcessInfo(spawned.pid) === null) {
|
|
75224
|
+
exited = true;
|
|
75225
|
+
break;
|
|
75226
|
+
}
|
|
75227
|
+
}
|
|
75228
|
+
if (!exited) {
|
|
75229
|
+
console.error(
|
|
75230
|
+
import_picocolors28.default.red(
|
|
75231
|
+
`Host pid ${spawned.pid} did not exit within ${STOP_WAIT_MS / 1e3}s after SIGTERM.`
|
|
75232
|
+
)
|
|
75233
|
+
);
|
|
73204
75234
|
return 1;
|
|
73205
75235
|
}
|
|
75236
|
+
console.log(
|
|
75237
|
+
`${import_picocolors28.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors28.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
|
|
75238
|
+
);
|
|
73206
75239
|
}
|
|
73207
75240
|
try {
|
|
73208
75241
|
await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
@@ -73214,7 +75247,7 @@ async function stopAll() {
|
|
|
73214
75247
|
const registry = readRegistry();
|
|
73215
75248
|
const projects2 = Object.entries(registry.projects);
|
|
73216
75249
|
if (projects2.length === 0) {
|
|
73217
|
-
console.log(
|
|
75250
|
+
console.log(import_picocolors28.default.dim("No registered projects."));
|
|
73218
75251
|
return 0;
|
|
73219
75252
|
}
|
|
73220
75253
|
let stopped = 0;
|
|
@@ -73224,17 +75257,17 @@ async function stopAll() {
|
|
|
73224
75257
|
if (!hostUrl) continue;
|
|
73225
75258
|
const alive = await isHostHealthy(hostUrl);
|
|
73226
75259
|
if (!alive) continue;
|
|
73227
|
-
console.log(` Stopping ${
|
|
75260
|
+
console.log(` Stopping ${import_picocolors28.default.bold(name)} ${import_picocolors28.default.dim(`(${hostUrl})`)}`);
|
|
73228
75261
|
const code = await stopHost(entry2.root, hostUrl);
|
|
73229
75262
|
if (code === 0) stopped++;
|
|
73230
75263
|
else failed++;
|
|
73231
75264
|
}
|
|
73232
75265
|
if (stopped === 0 && failed === 0) {
|
|
73233
|
-
console.log(
|
|
75266
|
+
console.log(import_picocolors28.default.dim("No running source-intelligence hosts found."));
|
|
73234
75267
|
} else {
|
|
73235
75268
|
console.log(
|
|
73236
75269
|
`
|
|
73237
|
-
Stopped ${stopped} host(s)${failed > 0 ?
|
|
75270
|
+
Stopped ${stopped} host(s)${failed > 0 ? import_picocolors28.default.red(`, ${failed} failed`) : ""}.`
|
|
73238
75271
|
);
|
|
73239
75272
|
}
|
|
73240
75273
|
return failed > 0 ? 1 : 0;
|
|
@@ -73286,12 +75319,12 @@ function extractPort(hostUrl) {
|
|
|
73286
75319
|
|
|
73287
75320
|
// ../../packages/cli/src/commands/hosts.ts
|
|
73288
75321
|
init_cjs_shims();
|
|
73289
|
-
var
|
|
75322
|
+
var import_picocolors29 = __toESM(require_picocolors(), 1);
|
|
73290
75323
|
async function runHosts() {
|
|
73291
75324
|
const registry = readRegistry();
|
|
73292
75325
|
const projects2 = Object.entries(registry.projects);
|
|
73293
75326
|
if (projects2.length === 0) {
|
|
73294
|
-
console.log(
|
|
75327
|
+
console.log(import_picocolors29.default.dim("No registered projects. Run `horus index` in a repo first."));
|
|
73295
75328
|
return 0;
|
|
73296
75329
|
}
|
|
73297
75330
|
const rows = [];
|
|
@@ -73308,29 +75341,29 @@ async function runHosts() {
|
|
|
73308
75341
|
});
|
|
73309
75342
|
const anyHost = rows.some((r) => r.hostUrl !== null);
|
|
73310
75343
|
if (!anyHost) {
|
|
73311
|
-
console.log(
|
|
75344
|
+
console.log(import_picocolors29.default.dim("No source-intelligence hosts found. Run `horus index` to start one."));
|
|
73312
75345
|
return 0;
|
|
73313
75346
|
}
|
|
73314
75347
|
console.log("");
|
|
73315
75348
|
for (const row of rows) {
|
|
73316
75349
|
if (row.hostUrl === null) continue;
|
|
73317
|
-
const status = row.healthy ?
|
|
75350
|
+
const status = row.healthy ? import_picocolors29.default.green("\u25CF running") : import_picocolors29.default.red("\u25CF stopped");
|
|
73318
75351
|
const port = extractPort2(row.hostUrl) ?? "?";
|
|
73319
75352
|
console.log(
|
|
73320
|
-
` ${status} ${
|
|
75353
|
+
` ${status} ${import_picocolors29.default.bold(row.name.padEnd(24))} port ${String(port).padEnd(6)} ${import_picocolors29.default.dim(row.root)}`
|
|
73321
75354
|
);
|
|
73322
75355
|
}
|
|
73323
75356
|
console.log("");
|
|
73324
75357
|
const noHost = rows.filter((r) => r.hostUrl === null);
|
|
73325
75358
|
if (noHost.length > 0) {
|
|
73326
75359
|
for (const row of noHost) {
|
|
73327
|
-
console.log(` ${
|
|
75360
|
+
console.log(` ${import_picocolors29.default.dim("\u25CB no host")} ${import_picocolors29.default.dim(row.name)}`);
|
|
73328
75361
|
}
|
|
73329
75362
|
console.log("");
|
|
73330
75363
|
}
|
|
73331
75364
|
const running = rows.filter((r) => r.healthy).length;
|
|
73332
75365
|
console.log(
|
|
73333
|
-
|
|
75366
|
+
import_picocolors29.default.dim(
|
|
73334
75367
|
`${running} running \xB7 horus stop to reap \xB7 horus stop --all to stop everything`
|
|
73335
75368
|
)
|
|
73336
75369
|
);
|
|
@@ -73347,12 +75380,12 @@ function extractPort2(hostUrl) {
|
|
|
73347
75380
|
|
|
73348
75381
|
// ../../packages/cli/src/commands/doctor.ts
|
|
73349
75382
|
init_cjs_shims();
|
|
73350
|
-
var
|
|
75383
|
+
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
73351
75384
|
var DEFAULT_DB_URL3 = "postgresql://horus:horus@localhost:5433/horus";
|
|
73352
75385
|
function mark2(status) {
|
|
73353
|
-
if (status === "pass") return
|
|
73354
|
-
if (status === "warn") return
|
|
73355
|
-
return
|
|
75386
|
+
if (status === "pass") return import_picocolors30.default.green("\u2713");
|
|
75387
|
+
if (status === "warn") return import_picocolors30.default.yellow("~");
|
|
75388
|
+
return import_picocolors30.default.red("\u2717");
|
|
73356
75389
|
}
|
|
73357
75390
|
async function runDoctor(opts) {
|
|
73358
75391
|
const cwd = opts?.cwd ?? process.cwd();
|
|
@@ -73579,11 +75612,11 @@ async function runDoctor(opts) {
|
|
|
73579
75612
|
write(JSON.stringify(output, null, 2));
|
|
73580
75613
|
return hasFailure ? 1 : 0;
|
|
73581
75614
|
}
|
|
73582
|
-
write(
|
|
75615
|
+
write(import_picocolors30.default.bold("\nHorus readiness check\n"));
|
|
73583
75616
|
for (const check of checks) {
|
|
73584
|
-
write(` ${mark2(check.status)} ${
|
|
75617
|
+
write(` ${mark2(check.status)} ${import_picocolors30.default.bold(check.label.padEnd(26))} ${import_picocolors30.default.dim(check.detail)}`);
|
|
73585
75618
|
if (check.next) {
|
|
73586
|
-
write(` ${
|
|
75619
|
+
write(` ${import_picocolors30.default.dim("\u2192 " + check.next)}`);
|
|
73587
75620
|
}
|
|
73588
75621
|
}
|
|
73589
75622
|
write("");
|
|
@@ -73592,29 +75625,34 @@ async function runDoctor(opts) {
|
|
|
73592
75625
|
|
|
73593
75626
|
// ../../packages/cli/src/commands/providers-doctor.ts
|
|
73594
75627
|
init_cjs_shims();
|
|
73595
|
-
var
|
|
75628
|
+
var import_node_child_process7 = require("child_process");
|
|
75629
|
+
var import_picocolors31 = __toESM(require_picocolors(), 1);
|
|
73596
75630
|
function statusMark(status) {
|
|
73597
|
-
if (status === "ready") return
|
|
73598
|
-
if (status === "installed") return
|
|
73599
|
-
return
|
|
75631
|
+
if (status === "ready") return import_picocolors31.default.green("\u2713");
|
|
75632
|
+
if (status === "installed") return import_picocolors31.default.yellow("~");
|
|
75633
|
+
return import_picocolors31.default.red("\u2717");
|
|
73600
75634
|
}
|
|
73601
75635
|
function statusLabel(status) {
|
|
73602
|
-
if (status === "ready") return
|
|
73603
|
-
if (status === "installed") return
|
|
73604
|
-
return
|
|
73605
|
-
}
|
|
73606
|
-
function
|
|
73607
|
-
|
|
73608
|
-
|
|
73609
|
-
status: "unavailable",
|
|
73610
|
-
|
|
73611
|
-
}
|
|
75636
|
+
if (status === "ready") return import_picocolors31.default.green("ready");
|
|
75637
|
+
if (status === "installed") return import_picocolors31.default.yellow("installed (not configured)");
|
|
75638
|
+
return import_picocolors31.default.dim("not found on PATH");
|
|
75639
|
+
}
|
|
75640
|
+
function detectBinary(id) {
|
|
75641
|
+
const result = (0, import_node_child_process7.spawnSync)(id, ["--version"], { stdio: "pipe", timeout: 2e3 });
|
|
75642
|
+
if (result.error) {
|
|
75643
|
+
return { id, status: "unavailable", detail: `${id}: command not found` };
|
|
75644
|
+
}
|
|
75645
|
+
return { id, status: "installed", detail: `${id}: found on PATH` };
|
|
75646
|
+
}
|
|
75647
|
+
function buildProviderResults(registry, _detect) {
|
|
75648
|
+
const detect = _detect ?? detectBinary;
|
|
75649
|
+
return registry.providers.map((p) => detect(p.id));
|
|
73612
75650
|
}
|
|
73613
75651
|
async function runProvidersDoctorCommand(opts) {
|
|
73614
75652
|
const registry = opts?.registry ?? DEFAULT_LOCAL_PROVIDER_REGISTRY;
|
|
73615
75653
|
const write = opts?.write ?? ((line2) => console.log(line2));
|
|
73616
|
-
const results = buildProviderResults(registry);
|
|
73617
|
-
write(
|
|
75654
|
+
const results = buildProviderResults(registry, opts?._detect);
|
|
75655
|
+
write(import_picocolors31.default.bold("\nLocal AI providers\n"));
|
|
73618
75656
|
for (const result of results) {
|
|
73619
75657
|
const descriptor = registry.get(result.id);
|
|
73620
75658
|
const name = descriptor?.displayName ?? result.id;
|
|
@@ -73622,12 +75660,24 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
73622
75660
|
` ${statusMark(result.status)} ${result.id.padEnd(8)} ${name.padEnd(22)} ${statusLabel(result.status)}`
|
|
73623
75661
|
);
|
|
73624
75662
|
if (result.status !== "ready" && result.detail) {
|
|
73625
|
-
write(` ${
|
|
75663
|
+
write(` ${import_picocolors31.default.dim("\u2192 " + result.detail)}`);
|
|
73626
75664
|
}
|
|
73627
75665
|
}
|
|
73628
75666
|
write("");
|
|
73629
|
-
write(
|
|
73630
|
-
|
|
75667
|
+
write(import_picocolors31.default.bold("Cloud AI providers\n"));
|
|
75668
|
+
const anthropicKey = opts?._anthropicKey !== void 0 ? opts._anthropicKey : process.env["ANTHROPIC_API_KEY"] ?? null;
|
|
75669
|
+
if (anthropicKey) {
|
|
75670
|
+
write(
|
|
75671
|
+
` ${import_picocolors31.default.green("\u2713")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.green("ANTHROPIC_API_KEY configured")}`
|
|
75672
|
+
);
|
|
75673
|
+
} else {
|
|
75674
|
+
write(
|
|
75675
|
+
` ${import_picocolors31.default.red("\u2717")} ${"anthropic".padEnd(8)} ${"Anthropic Claude API".padEnd(22)} ${import_picocolors31.default.dim("ANTHROPIC_API_KEY not set")}`
|
|
75676
|
+
);
|
|
75677
|
+
write(
|
|
75678
|
+
` ${import_picocolors31.default.dim('\u2192 set ANTHROPIC_API_KEY=<key> to enable AI-powered investigation (horus investigate "hint" --ai)')}`
|
|
75679
|
+
);
|
|
75680
|
+
}
|
|
73631
75681
|
write("");
|
|
73632
75682
|
return 0;
|
|
73633
75683
|
}
|
|
@@ -73636,7 +75686,7 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
73636
75686
|
init_cjs_shims();
|
|
73637
75687
|
var import_node_fs8 = require("fs");
|
|
73638
75688
|
var import_node_path10 = require("path");
|
|
73639
|
-
var
|
|
75689
|
+
var import_picocolors32 = __toESM(require_picocolors(), 1);
|
|
73640
75690
|
function configTemplate(name, repoPath) {
|
|
73641
75691
|
return `export default {
|
|
73642
75692
|
database: {
|
|
@@ -73662,39 +75712,66 @@ function configTemplate(name, repoPath) {
|
|
|
73662
75712
|
};
|
|
73663
75713
|
`;
|
|
73664
75714
|
}
|
|
75715
|
+
function projectDefaults(localConfigPath2) {
|
|
75716
|
+
try {
|
|
75717
|
+
const file = readLocalConfig(localConfigPath2);
|
|
75718
|
+
const project = file.project;
|
|
75719
|
+
if (!project) return null;
|
|
75720
|
+
const name = typeof project.name === "string" ? project.name : null;
|
|
75721
|
+
const repositories2 = Array.isArray(project.repositories) ? project.repositories : [];
|
|
75722
|
+
const firstRepo = repositories2[0];
|
|
75723
|
+
const repoPath = firstRepo && typeof firstRepo.path === "string" ? firstRepo.path : null;
|
|
75724
|
+
if (name == null) return null;
|
|
75725
|
+
return { name, repoPath: repoPath ?? name };
|
|
75726
|
+
} catch {
|
|
75727
|
+
return null;
|
|
75728
|
+
}
|
|
75729
|
+
}
|
|
73665
75730
|
async function runGenerateConfig(opts) {
|
|
73666
75731
|
const log = opts.write ?? ((line2) => console.log(line2));
|
|
73667
75732
|
const cwd = opts.cwd ?? process.cwd();
|
|
73668
|
-
const
|
|
73669
|
-
const
|
|
73670
|
-
const
|
|
75733
|
+
const localConfigPath2 = discoverLocalConfig(cwd);
|
|
75734
|
+
const defaults = localConfigPath2 != null ? projectDefaults(localConfigPath2) : null;
|
|
75735
|
+
const hasLocalConfig = defaults != null;
|
|
75736
|
+
const defaultOut = hasLocalConfig ? "horus.config.example.js" : "horus.config.js";
|
|
75737
|
+
const outPath = (0, import_node_path10.resolve)(cwd, opts.out ?? defaultOut);
|
|
75738
|
+
const name = opts.name ?? defaults?.name ?? "my-project";
|
|
75739
|
+
const repoPath = opts.repo ?? defaults?.repoPath ?? `/path/to/${name}`;
|
|
73671
75740
|
if ((0, import_node_fs8.existsSync)(outPath) && !opts.force) {
|
|
73672
|
-
log(`${
|
|
73673
|
-
log(
|
|
75741
|
+
log(`${import_picocolors32.default.red("\u2717")} ${outPath} already exists`);
|
|
75742
|
+
log(import_picocolors32.default.dim(" pass --force to overwrite"));
|
|
73674
75743
|
return 1;
|
|
73675
75744
|
}
|
|
73676
75745
|
try {
|
|
73677
75746
|
(0, import_node_fs8.mkdirSync)((0, import_node_path10.dirname)(outPath), { recursive: true });
|
|
73678
75747
|
(0, import_node_fs8.writeFileSync)(outPath, configTemplate(name, repoPath), "utf8");
|
|
73679
75748
|
} catch (err) {
|
|
73680
|
-
log(`${
|
|
75749
|
+
log(`${import_picocolors32.default.red("\u2717")} Could not write ${outPath}: ${err.message}`);
|
|
73681
75750
|
return 1;
|
|
73682
75751
|
}
|
|
73683
|
-
log(`${
|
|
73684
|
-
log(
|
|
73685
|
-
log(
|
|
73686
|
-
|
|
75752
|
+
log(`${import_picocolors32.default.green("\u2713")} Created ${outPath}`);
|
|
75753
|
+
log(import_picocolors32.default.dim(` project: ${name}`));
|
|
75754
|
+
log(import_picocolors32.default.dim(` repo: ${repoPath}`));
|
|
75755
|
+
if (hasLocalConfig && localConfigPath2 != null) {
|
|
75756
|
+
log("");
|
|
75757
|
+
log(import_picocolors32.default.yellow("Note:") + ` an initialized Horus project config exists at ${localConfigPath2}`);
|
|
75758
|
+
log(" \u2022 .horus/config.json \u2014 project config used by `horus investigate` from this repo");
|
|
75759
|
+
log(" \u2022 horus.config.js \u2014 standalone/global config used with `horus doctor --config <path>`");
|
|
75760
|
+
log(import_picocolors32.default.dim(` next: review ${outPath} and copy/adapt it as needed`));
|
|
75761
|
+
} else {
|
|
75762
|
+
log(import_picocolors32.default.dim(` next: horus doctor --config ${outPath}`));
|
|
75763
|
+
}
|
|
73687
75764
|
return 0;
|
|
73688
75765
|
}
|
|
73689
75766
|
|
|
73690
75767
|
// ../../packages/cli/src/commands/readiness.ts
|
|
73691
75768
|
init_cjs_shims();
|
|
73692
|
-
var
|
|
75769
|
+
var import_picocolors33 = __toESM(require_picocolors(), 1);
|
|
73693
75770
|
var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
|
|
73694
75771
|
function mark3(status) {
|
|
73695
|
-
if (status === "pass") return
|
|
73696
|
-
if (status === "warn") return
|
|
73697
|
-
return
|
|
75772
|
+
if (status === "pass") return import_picocolors33.default.green("\u2713");
|
|
75773
|
+
if (status === "warn") return import_picocolors33.default.yellow("~");
|
|
75774
|
+
return import_picocolors33.default.red("\u2717");
|
|
73698
75775
|
}
|
|
73699
75776
|
async function runReadiness(opts) {
|
|
73700
75777
|
const cwd = opts?.cwd ?? process.cwd();
|
|
@@ -73757,7 +75834,7 @@ async function runReadiness(opts) {
|
|
|
73757
75834
|
status: "warn",
|
|
73758
75835
|
blocking: false,
|
|
73759
75836
|
detail: "not installed \u2014 source intelligence unavailable",
|
|
73760
|
-
next: `
|
|
75837
|
+
next: `pip install horus-source`
|
|
73761
75838
|
});
|
|
73762
75839
|
} else if (sourceVersion !== PINNED_SOURCE_VERSION) {
|
|
73763
75840
|
checks.push({
|
|
@@ -73765,7 +75842,7 @@ async function runReadiness(opts) {
|
|
|
73765
75842
|
status: "warn",
|
|
73766
75843
|
blocking: false,
|
|
73767
75844
|
detail: `version mismatch (installed: ${sourceVersion}, required: ${PINNED_SOURCE_VERSION})`,
|
|
73768
|
-
next: `
|
|
75845
|
+
next: `pip install horus-source`
|
|
73769
75846
|
});
|
|
73770
75847
|
} else {
|
|
73771
75848
|
checks.push({
|
|
@@ -73849,20 +75926,20 @@ async function runReadiness(opts) {
|
|
|
73849
75926
|
}
|
|
73850
75927
|
const blockingChecks = checks.filter((c) => c.blocking);
|
|
73851
75928
|
const optionalChecks = checks.filter((c) => !c.blocking);
|
|
73852
|
-
write(
|
|
73853
|
-
write(
|
|
75929
|
+
write(import_picocolors33.default.bold("\nHorus release readiness\n"));
|
|
75930
|
+
write(import_picocolors33.default.bold(" Blocking"));
|
|
73854
75931
|
for (const check of blockingChecks) {
|
|
73855
|
-
write(` ${mark3(check.status)} ${
|
|
75932
|
+
write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
|
|
73856
75933
|
if (check.next) {
|
|
73857
|
-
write(` ${
|
|
75934
|
+
write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
|
|
73858
75935
|
}
|
|
73859
75936
|
}
|
|
73860
75937
|
write("");
|
|
73861
|
-
write(
|
|
75938
|
+
write(import_picocolors33.default.bold(" Optional"));
|
|
73862
75939
|
for (const check of optionalChecks) {
|
|
73863
|
-
write(` ${mark3(check.status)} ${
|
|
75940
|
+
write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
|
|
73864
75941
|
if (check.next) {
|
|
73865
|
-
write(` ${
|
|
75942
|
+
write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
|
|
73866
75943
|
}
|
|
73867
75944
|
}
|
|
73868
75945
|
write("");
|
|
@@ -73870,21 +75947,21 @@ async function runReadiness(opts) {
|
|
|
73870
75947
|
const optionalWarns = optionalChecks.filter((c) => c.status === "warn").length;
|
|
73871
75948
|
if (blockingFails.length === 0) {
|
|
73872
75949
|
if (optionalWarns === 0) {
|
|
73873
|
-
write(
|
|
75950
|
+
write(import_picocolors33.default.green(" Ready for demo/release."));
|
|
73874
75951
|
} else {
|
|
73875
75952
|
write(
|
|
73876
|
-
|
|
75953
|
+
import_picocolors33.default.yellow(
|
|
73877
75954
|
` Ready for a basic demo. ${optionalWarns} optional item(s) not configured \u2014 investigation evidence will be limited.`
|
|
73878
75955
|
)
|
|
73879
75956
|
);
|
|
73880
75957
|
}
|
|
73881
75958
|
} else {
|
|
73882
75959
|
write(
|
|
73883
|
-
|
|
75960
|
+
import_picocolors33.default.red(
|
|
73884
75961
|
` Not ready. ${blockingFails.length} blocking item(s) must be resolved before demo/release.`
|
|
73885
75962
|
)
|
|
73886
75963
|
);
|
|
73887
|
-
write(
|
|
75964
|
+
write(import_picocolors33.default.dim(" Re-run `horus readiness` after resolving the items above."));
|
|
73888
75965
|
}
|
|
73889
75966
|
write("");
|
|
73890
75967
|
return blockingFails.length > 0 ? 1 : 0;
|
|
@@ -73935,7 +76012,7 @@ Examples:
|
|
|
73935
76012
|
});
|
|
73936
76013
|
program2.command("connect <type>").description(
|
|
73937
76014
|
"Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
|
|
73938
|
-
).option("--env <name>", "target environment (default: first environment in config)").option("--url <url>", "connector URL or connection string").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--dashboard <uid>", "default dashboard uid (grafana)").option("--no-test", "skip live connection probe").action(
|
|
76015
|
+
).option("--env <name>", "target environment (default: first environment in config)").option("--url <url>", "connector URL or connection string (Redis with auth: redis://:password@host:6379)").option("--username <user>", "username (elasticsearch / grafana)").option("--password <pass>", "password (elasticsearch / grafana; for Redis embed in --url)").option("--index-pattern <pattern>", "Elasticsearch index pattern (required for elasticsearch)").option("--service <name>", "service name scope for log queries").option("--database <name>", "database name (required for mongodb)").option("--collections <list>", "comma-separated collection allowlist (mongodb)").option("--dashboard <uid>", "default dashboard uid (grafana)").option("--no-test", "skip live connection probe").action(
|
|
73939
76016
|
async (type, opts) => {
|
|
73940
76017
|
process.exitCode = await runConnect(type, {
|
|
73941
76018
|
env: opts.env,
|
|
@@ -73984,9 +76061,16 @@ Examples:
|
|
|
73984
76061
|
process.exitCode = await runIndex(opts);
|
|
73985
76062
|
}
|
|
73986
76063
|
);
|
|
73987
|
-
program2.command("queues [name]").description("Show
|
|
73988
|
-
|
|
73989
|
-
|
|
76064
|
+
program2.command("queues [name]").description("Show queue topology from source intelligence; --live adds real-time Redis/BullMQ state").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via registry)").option("--project <name>", "filter edges by project").option("--live", "fetch real-time queue depths and failed-job counts from Redis/BullMQ").action(
|
|
76065
|
+
async (name, opts) => {
|
|
76066
|
+
process.exitCode = await runQueues(name, {
|
|
76067
|
+
config: opts.config,
|
|
76068
|
+
name: opts.name,
|
|
76069
|
+
project: opts.project,
|
|
76070
|
+
live: opts.live
|
|
76071
|
+
});
|
|
76072
|
+
}
|
|
76073
|
+
);
|
|
73990
76074
|
program2.command("investigate <hint>").description("Run a deterministic investigation for an incident hint").option("-c, --config <path>", "path to horus.config.ts").option("--name <name>", "registered project name (resolves via the registry)").option("--project <name>", "project name to scope to").option("--env <name>", "environment name (e.g. production)").option("--repo <name>", "repository/project to scope to (alias for --project)").option("--since <ref>", "git ref/range for change-impact (e.g. HEAD~5)").option(
|
|
73991
76075
|
"--service <name>",
|
|
73992
76076
|
"service name to scope runtime logs, e.g. leadcall-api-prod"
|
|
@@ -74081,8 +76165,8 @@ Examples:
|
|
|
74081
76165
|
horus investigations
|
|
74082
76166
|
horus investigations -n 20
|
|
74083
76167
|
`);
|
|
74084
|
-
program2.command("replay <id>").description("Re-render a saved investigation from the audit store (no re-query)").option("-c, --config <path>", "path to horus.config.ts").option("--format <fmt>", "text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").action(async (id, opts) => {
|
|
74085
|
-
process.exitCode = await runReplay(id, { config: opts.config, format: opts.format, ai: opts.ai, aiModel: opts.aiModel });
|
|
76168
|
+
program2.command("replay <id>").description("Re-render a saved investigation from the audit store (no re-query)").option("-c, --config <path>", "path to horus.config.ts").option("--format <fmt>", "text | markdown | json", "text").option("--ai", "enrich report with AI narrative (requires ANTHROPIC_API_KEY; falls back to deterministic on failure)").option("--ai-model <model>", "AI model for --ai (default: claude-opus-4-8)").option("--refresh-ai", "re-run AI even if a stored judgment already exists").action(async (id, opts) => {
|
|
76169
|
+
process.exitCode = await runReplay(id, { config: opts.config, format: opts.format, ai: opts.ai, aiModel: opts.aiModel, refreshAi: opts.refreshAi });
|
|
74086
76170
|
}).addHelpText("after", `
|
|
74087
76171
|
Examples:
|
|
74088
76172
|
horus replay <id>
|
|
@@ -74090,11 +76174,12 @@ Examples:
|
|
|
74090
76174
|
horus replay <id> --format json
|
|
74091
76175
|
horus replay <id> --ai
|
|
74092
76176
|
horus replay <id> --ai --ai-model claude-sonnet-4-6
|
|
76177
|
+
horus replay <id> --ai --refresh-ai
|
|
74093
76178
|
|
|
74094
76179
|
(Use 'horus investigations' to list saved investigation ids.)
|
|
74095
76180
|
`);
|
|
74096
|
-
program2.command("postmortem <id>").description("Draft an editable incident postmortem from a saved investigation").option("-c, --config <path>", "path to horus.config.ts").option("--output <path>", "write Markdown to a file instead of printing to stdout").option("--force", "overwrite the output file if it already exists").option("--ai-summary", "append an AI-generated summary section (requires ANTHROPIC_API_KEY; falls back gracefully)").option("--ai-model <model>", "AI model for --ai-summary (default: claude-opus-4-8)").action(async (id, opts) => {
|
|
74097
|
-
process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force, aiSummary: opts.aiSummary, aiModel: opts.aiModel });
|
|
76181
|
+
program2.command("postmortem <id>").description("Draft an editable incident postmortem from a saved investigation").option("-c, --config <path>", "path to horus.config.ts").option("--output <path>", "write Markdown to a file instead of printing to stdout").option("--force", "overwrite the output file if it already exists").option("--ai-summary", "append an AI-generated summary section (requires ANTHROPIC_API_KEY; falls back gracefully)").option("--ai-model <model>", "AI model for --ai-summary (default: claude-opus-4-8)").option("--refresh-ai", "re-run AI even if a stored judgment already exists").action(async (id, opts) => {
|
|
76182
|
+
process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force, aiSummary: opts.aiSummary, aiModel: opts.aiModel, refreshAi: opts.refreshAi });
|
|
74098
76183
|
}).addHelpText("after", `
|
|
74099
76184
|
Examples:
|
|
74100
76185
|
horus postmortem <id>
|
|
@@ -74102,6 +76187,7 @@ Examples:
|
|
|
74102
76187
|
horus postmortem <id> --output ./postmortem.md --force
|
|
74103
76188
|
horus postmortem <id> --ai-summary
|
|
74104
76189
|
horus postmortem <id> --output ./postmortem.md --ai-summary --ai-model claude-sonnet-4-6
|
|
76190
|
+
horus postmortem <id> --ai-summary --refresh-ai
|
|
74105
76191
|
|
|
74106
76192
|
(Use 'horus investigations' to list saved investigation ids.)
|
|
74107
76193
|
`);
|