@merittdev/horus 0.1.1 → 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/README.md +100 -116
- package/dist/index.cjs +2635 -459
- 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)) {
|
|
@@ -67040,6 +67306,22 @@ async function runIndex(opts) {
|
|
|
67040
67306
|
` investigate: horus investigate --name ${name} "<hint>" (or from this repo: horus investigate "<hint>")`
|
|
67041
67307
|
)
|
|
67042
67308
|
);
|
|
67309
|
+
} else if (spawned && !configuredHost) {
|
|
67310
|
+
const existingPath = discoverLocalConfig(root);
|
|
67311
|
+
if (existingPath) {
|
|
67312
|
+
const file = readLocalConfig(existingPath);
|
|
67313
|
+
const project = file.project;
|
|
67314
|
+
const repos = project["repositories"];
|
|
67315
|
+
if (repos && repos.length > 0) {
|
|
67316
|
+
repos[0]["source"] = { hostUrl };
|
|
67317
|
+
}
|
|
67318
|
+
writeLocalConfig(root, file);
|
|
67319
|
+
registerProject(label, root, existingPath);
|
|
67320
|
+
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} \u2014 source host registered at ${hostUrl}`);
|
|
67321
|
+
console.log(import_picocolors3.default.dim(` ${existingPath}`));
|
|
67322
|
+
} else {
|
|
67323
|
+
console.log(`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`);
|
|
67324
|
+
}
|
|
67043
67325
|
} else {
|
|
67044
67326
|
console.log(
|
|
67045
67327
|
`${import_picocolors3.default.green("\u2713")} Indexed ${import_picocolors3.default.bold(label)} ${import_picocolors3.default.dim("(queue map refreshed)")}`
|
|
@@ -67057,64 +67339,29 @@ init_cjs_shims();
|
|
|
67057
67339
|
var import_picocolors4 = __toESM(require_picocolors(), 1);
|
|
67058
67340
|
async function runQueues(name, opts) {
|
|
67059
67341
|
try {
|
|
67060
|
-
const config = await loadConfig(opts.config);
|
|
67342
|
+
const config = await loadConfig(opts.config, { name: opts.name });
|
|
67061
67343
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
67062
67344
|
try {
|
|
67063
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("");
|
|
67064
67350
|
if (rows.length === 0) {
|
|
67065
|
-
console.log("No queue edges. Run: horus index");
|
|
67066
|
-
|
|
67351
|
+
console.log(import_picocolors4.default.dim(" No queue edges indexed. Run: horus index"));
|
|
67352
|
+
} else {
|
|
67353
|
+
const byQueue = buildQueueMap(rows);
|
|
67354
|
+
printTopology(byQueue);
|
|
67067
67355
|
}
|
|
67068
|
-
|
|
67069
|
-
|
|
67070
|
-
|
|
67071
|
-
|
|
67072
|
-
|
|
67073
|
-
|
|
67074
|
-
|
|
67075
|
-
|
|
67076
|
-
|
|
67077
|
-
for (const [queueName, edges] of byQueue) {
|
|
67078
|
-
console.log(import_picocolors4.default.bold(queueName));
|
|
67079
|
-
const producerSet = /* @__PURE__ */ new Set();
|
|
67080
|
-
const producerDetails = /* @__PURE__ */ new Map();
|
|
67081
|
-
for (const edge of edges) {
|
|
67082
|
-
if (edge.producerSymbol) {
|
|
67083
|
-
producerSet.add(edge.producerSymbol);
|
|
67084
|
-
if (edge.producerFile) {
|
|
67085
|
-
producerDetails.set(edge.producerSymbol, edge.producerFile);
|
|
67086
|
-
}
|
|
67087
|
-
}
|
|
67088
|
-
}
|
|
67089
|
-
if (producerSet.size === 0) {
|
|
67090
|
-
console.log(" producers: " + import_picocolors4.default.dim("none"));
|
|
67091
|
-
} else {
|
|
67092
|
-
const producerList = Array.from(producerSet).map((sym) => {
|
|
67093
|
-
const file = producerDetails.get(sym);
|
|
67094
|
-
return file ? `${sym} (${file})` : sym;
|
|
67095
|
-
}).join(", ");
|
|
67096
|
-
console.log(" producers: " + producerList);
|
|
67097
|
-
}
|
|
67098
|
-
const workerSet = /* @__PURE__ */ new Set();
|
|
67099
|
-
const workerDetails = /* @__PURE__ */ new Map();
|
|
67100
|
-
for (const edge of edges) {
|
|
67101
|
-
if (edge.workerSymbol) {
|
|
67102
|
-
workerSet.add(edge.workerSymbol);
|
|
67103
|
-
if (edge.workerFile) {
|
|
67104
|
-
workerDetails.set(edge.workerSymbol, edge.workerFile);
|
|
67105
|
-
}
|
|
67106
|
-
}
|
|
67107
|
-
}
|
|
67108
|
-
if (workerSet.size === 0) {
|
|
67109
|
-
console.log(" workers: " + import_picocolors4.default.dim("none"));
|
|
67110
|
-
} else {
|
|
67111
|
-
const workerList = Array.from(workerSet).map((sym) => {
|
|
67112
|
-
const file = workerDetails.get(sym);
|
|
67113
|
-
return file ? `${sym} (${file})` : sym;
|
|
67114
|
-
}).join(", ");
|
|
67115
|
-
console.log(" workers: " + workerList);
|
|
67116
|
-
}
|
|
67117
|
-
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
|
+
);
|
|
67118
67365
|
}
|
|
67119
67366
|
} finally {
|
|
67120
67367
|
await sql2.end();
|
|
@@ -67125,6 +67372,141 @@ async function runQueues(name, opts) {
|
|
|
67125
67372
|
return 1;
|
|
67126
67373
|
}
|
|
67127
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
|
+
}
|
|
67128
67510
|
|
|
67129
67511
|
// ../../packages/cli/src/commands/investigate.ts
|
|
67130
67512
|
init_cjs_shims();
|
|
@@ -67141,8 +67523,96 @@ init_cjs_shims();
|
|
|
67141
67523
|
|
|
67142
67524
|
// ../../packages/engine/src/ownership.ts
|
|
67143
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
|
+
}
|
|
67144
67579
|
async function estimateOwnership(query, deps) {
|
|
67145
|
-
|
|
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
|
+
}
|
|
67146
67616
|
const file = top?.filePath ?? null;
|
|
67147
67617
|
if (file === null) {
|
|
67148
67618
|
return {
|
|
@@ -67266,7 +67736,48 @@ function processEvidence(ev, nodes, edges) {
|
|
|
67266
67736
|
addEdge(edges, "observed_in", evId, deployId, ev.id);
|
|
67267
67737
|
return;
|
|
67268
67738
|
}
|
|
67269
|
-
|
|
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
|
|
67270
67781
|
default:
|
|
67271
67782
|
return;
|
|
67272
67783
|
}
|
|
@@ -67287,18 +67798,28 @@ function scoreImplication(nodes, edges, evidence2) {
|
|
|
67287
67798
|
const evById = new Map(evidence2.map((e) => [e.id, e]));
|
|
67288
67799
|
for (const node of nodes.values()) {
|
|
67289
67800
|
if (node.type === "evidence") continue;
|
|
67801
|
+
if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
|
|
67290
67802
|
node.implicationScore = node.evidenceIds.reduce((max, eid) => {
|
|
67291
67803
|
const ev = evById.get(eid);
|
|
67292
67804
|
if (!ev || isExcludedFromImplication(ev)) return max;
|
|
67293
67805
|
return Math.max(max, ev.relevance);
|
|
67294
67806
|
}, 0);
|
|
67295
67807
|
}
|
|
67808
|
+
const CODE_TOPOLOGY_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
67809
|
+
"evidence",
|
|
67810
|
+
"symbol",
|
|
67811
|
+
"file",
|
|
67812
|
+
"flow"
|
|
67813
|
+
]);
|
|
67296
67814
|
const adj = /* @__PURE__ */ new Map();
|
|
67297
67815
|
for (const edge of edges.values()) {
|
|
67298
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;
|
|
67299
67820
|
const f = nodes.get(edge.from);
|
|
67300
67821
|
const t = nodes.get(edge.to);
|
|
67301
|
-
if (!f || !t || f.type
|
|
67822
|
+
if (!f || !t || CODE_TOPOLOGY_NODE_TYPES.has(f.type) || CODE_TOPOLOGY_NODE_TYPES.has(t.type)) continue;
|
|
67302
67823
|
const fs3 = adj.get(edge.from) ?? /* @__PURE__ */ new Set();
|
|
67303
67824
|
const ts = adj.get(edge.to) ?? /* @__PURE__ */ new Set();
|
|
67304
67825
|
fs3.add(edge.to);
|
|
@@ -67323,6 +67844,7 @@ function scoreImplication(nodes, edges, evidence2) {
|
|
|
67323
67844
|
}
|
|
67324
67845
|
for (const node of nodes.values()) {
|
|
67325
67846
|
if (node.type === "evidence") continue;
|
|
67847
|
+
if (node.type === "symbol" || node.type === "file" || node.type === "flow") continue;
|
|
67326
67848
|
node.implicated = node.implicationScore >= IMPLICATION_THRESHOLD;
|
|
67327
67849
|
}
|
|
67328
67850
|
}
|
|
@@ -67354,6 +67876,302 @@ function implicatedNodeIds(graph, evidenceIds) {
|
|
|
67354
67876
|
(n) => n.type !== "evidence" && n.implicated && n.evidenceIds.some((eid) => idSet.has(eid))
|
|
67355
67877
|
).map((n) => n.id);
|
|
67356
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
|
+
}
|
|
67357
68175
|
|
|
67358
68176
|
// ../../packages/engine/src/score-cause.ts
|
|
67359
68177
|
init_cjs_shims();
|
|
@@ -67435,20 +68253,23 @@ function factorRuntimeSignals(items, now) {
|
|
|
67435
68253
|
const timestamps = items.filter((e) => e.timestamp !== void 0).map((e) => new Date(e.timestamp).getTime()).sort((a, b2) => b2 - a);
|
|
67436
68254
|
const newestTs = timestamps[0];
|
|
67437
68255
|
if (newestTs !== void 0) {
|
|
67438
|
-
const
|
|
67439
|
-
|
|
67440
|
-
|
|
67441
|
-
|
|
67442
|
-
|
|
67443
|
-
|
|
67444
|
-
|
|
67445
|
-
|
|
67446
|
-
|
|
67447
|
-
|
|
67448
|
-
|
|
67449
|
-
|
|
67450
|
-
|
|
67451
|
-
|
|
68256
|
+
const rawAgeMs = nowMs - newestTs;
|
|
68257
|
+
const ageMs = rawAgeMs < 0 ? rawAgeMs >= -3e5 ? 0 : null : rawAgeMs;
|
|
68258
|
+
if (ageMs !== null) {
|
|
68259
|
+
if (ageMs <= 36e5) {
|
|
68260
|
+
recencyDelta = 0.05;
|
|
68261
|
+
recencyReason = "Evidence from within the last hour";
|
|
68262
|
+
} else if (ageMs <= 864e5) {
|
|
68263
|
+
recencyDelta = 0.02;
|
|
68264
|
+
recencyReason = "Evidence from within the last 24 hours";
|
|
68265
|
+
} else if (ageMs <= 2592e5) {
|
|
68266
|
+
} else if (ageMs <= 6048e5) {
|
|
68267
|
+
recencyDelta = -0.02;
|
|
68268
|
+
recencyReason = "Most recent evidence is 3\u20137 days old \u2014 may predate this incident";
|
|
68269
|
+
} else {
|
|
68270
|
+
recencyDelta = -0.05;
|
|
68271
|
+
recencyReason = "Most recent evidence is over 7 days old \u2014 likely predates this incident";
|
|
68272
|
+
}
|
|
67452
68273
|
}
|
|
67453
68274
|
}
|
|
67454
68275
|
let recurrenceDelta = 0;
|
|
@@ -67613,6 +68434,15 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
67613
68434
|
const queueEvs = evidence2.filter((e) => e.kind === "queue-edge");
|
|
67614
68435
|
const hasCommit = commitEvs.length > 0;
|
|
67615
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") : [];
|
|
67616
68446
|
const hyps = [];
|
|
67617
68447
|
hyps.push({
|
|
67618
68448
|
id: globalThis.crypto.randomUUID(),
|
|
@@ -67621,9 +68451,7 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
67621
68451
|
confidence: hasCommit ? 0.5 : 0.15,
|
|
67622
68452
|
supportingEvidenceIds: commitEvs.map((e) => e.id),
|
|
67623
68453
|
contradictingEvidenceIds: [],
|
|
67624
|
-
missingEvidence: hasCommit ? [] : [
|
|
67625
|
-
"A change/deployment range \u2014 re-run with --since <ref> to diff what shipped"
|
|
67626
|
-
]
|
|
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"]
|
|
67627
68455
|
});
|
|
67628
68456
|
for (const queueName of queues) {
|
|
67629
68457
|
const backlogEvIds = ctx.queueBacklogEvIdsByQueue?.get(queueName) ?? [];
|
|
@@ -67659,25 +68487,32 @@ function generateHypotheses(evidence2, _correlation, ctx) {
|
|
|
67659
68487
|
contradictingEvidenceIds: [],
|
|
67660
68488
|
missingEvidence: latencyMetricEvIds.length > 0 ? [] : ["Request latency metrics (Grafana) + error logs (Elasticsearch)"]
|
|
67661
68489
|
});
|
|
68490
|
+
const retryStormSupport = [.../* @__PURE__ */ new Set([...logSpikeEvIds, ...allQueueBacklogEvIds])];
|
|
67662
68491
|
hyps.push({
|
|
67663
68492
|
id: globalThis.crypto.randomUUID(),
|
|
67664
68493
|
category: "retry-storm",
|
|
67665
68494
|
statement: "A retry storm is amplifying load on the failing path.",
|
|
67666
|
-
confidence: 0.15,
|
|
67667
|
-
supportingEvidenceIds:
|
|
67668
|
-
contradictingEvidenceIds:
|
|
67669
|
-
missingEvidence: [
|
|
67670
|
-
"Retry/error logs + queue retry statistics (Elasticsearch + BullMQ)"
|
|
67671
|
-
]
|
|
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)"]
|
|
67672
68499
|
});
|
|
68500
|
+
const infraSupport = [
|
|
68501
|
+
.../* @__PURE__ */ new Set([
|
|
68502
|
+
...stateEvIds,
|
|
68503
|
+
...allQueueStarvationEvIds,
|
|
68504
|
+
...ctx.latencyMetricEvIds ?? [],
|
|
68505
|
+
...graphImplicatedCollectionEvIds
|
|
68506
|
+
])
|
|
68507
|
+
];
|
|
67673
68508
|
hyps.push({
|
|
67674
68509
|
id: globalThis.crypto.randomUUID(),
|
|
67675
68510
|
category: "infrastructure",
|
|
67676
68511
|
statement: "An infrastructure issue (database, Redis, or network) is degrading processing.",
|
|
67677
|
-
confidence: 0.15,
|
|
67678
|
-
supportingEvidenceIds:
|
|
68512
|
+
confidence: infraSupport.length > 0 ? 0.35 : 0.15,
|
|
68513
|
+
supportingEvidenceIds: infraSupport,
|
|
67679
68514
|
contradictingEvidenceIds: [],
|
|
67680
|
-
missingEvidence: ["Infra/Redis metrics (Grafana) + Redis state"]
|
|
68515
|
+
missingEvidence: infraSupport.length > 0 ? [] : ["Infra/Redis metrics (Grafana) + Redis state"]
|
|
67681
68516
|
});
|
|
67682
68517
|
hyps.sort((a, b2) => b2.confidence - a.confidence);
|
|
67683
68518
|
return hyps;
|
|
@@ -67898,7 +68733,8 @@ function detectMissingEvidence(r, connectors = {}) {
|
|
|
67898
68733
|
blindSpots.push("Cannot see the real error.");
|
|
67899
68734
|
}
|
|
67900
68735
|
if (!hasMetric && !(connectors.grafana && connectors.metricsCollected)) {
|
|
67901
|
-
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.`;
|
|
67902
68738
|
const metricsNextSource = !connectors.grafana ? "Add a `grafana` connector to the environment" : 'Check Grafana connectivity, then run `horus metrics "<hint>"` manually';
|
|
67903
68739
|
gaps.push({
|
|
67904
68740
|
dimension: "metrics",
|
|
@@ -67912,7 +68748,7 @@ function detectMissingEvidence(r, connectors = {}) {
|
|
|
67912
68748
|
gaps.push({
|
|
67913
68749
|
dimension: "queue runtime state",
|
|
67914
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.",
|
|
67915
|
-
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",
|
|
67916
68752
|
confidenceImpact: 0.1
|
|
67917
68753
|
});
|
|
67918
68754
|
blindSpots.push("Cannot determine if the queue is actually backed up.");
|
|
@@ -67920,8 +68756,8 @@ function detectMissingEvidence(r, connectors = {}) {
|
|
|
67920
68756
|
if (!hasCommit) {
|
|
67921
68757
|
gaps.push({
|
|
67922
68758
|
dimension: "deployment records",
|
|
67923
|
-
why: "No deployment/change data in scope \u2014 cannot tell what shipped before the incident.",
|
|
67924
|
-
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>`",
|
|
67925
68761
|
confidenceImpact: 0.08
|
|
67926
68762
|
});
|
|
67927
68763
|
blindSpots.push("Cannot correlate with a recent change.");
|
|
@@ -68098,33 +68934,33 @@ function buildGroups(evidence2) {
|
|
|
68098
68934
|
}
|
|
68099
68935
|
}
|
|
68100
68936
|
const raw = [];
|
|
68101
|
-
for (const [key,
|
|
68102
|
-
if (
|
|
68937
|
+
for (const [key, ids2] of symbolMap) {
|
|
68938
|
+
if (ids2.length >= 2) {
|
|
68103
68939
|
raw.push({
|
|
68104
68940
|
key,
|
|
68105
68941
|
dimension: "symbol",
|
|
68106
68942
|
reason: `Share symbol ${key}`,
|
|
68107
|
-
evidenceIds: [...
|
|
68943
|
+
evidenceIds: [...ids2]
|
|
68108
68944
|
});
|
|
68109
68945
|
}
|
|
68110
68946
|
}
|
|
68111
|
-
for (const [key,
|
|
68112
|
-
if (
|
|
68947
|
+
for (const [key, ids2] of fileMap) {
|
|
68948
|
+
if (ids2.length >= 2) {
|
|
68113
68949
|
raw.push({
|
|
68114
68950
|
key,
|
|
68115
68951
|
dimension: "file",
|
|
68116
68952
|
reason: `Share file ${key}`,
|
|
68117
|
-
evidenceIds: [...
|
|
68953
|
+
evidenceIds: [...ids2]
|
|
68118
68954
|
});
|
|
68119
68955
|
}
|
|
68120
68956
|
}
|
|
68121
|
-
for (const [key,
|
|
68122
|
-
if (
|
|
68957
|
+
for (const [key, ids2] of queueMap) {
|
|
68958
|
+
if (ids2.length >= 2) {
|
|
68123
68959
|
raw.push({
|
|
68124
68960
|
key,
|
|
68125
68961
|
dimension: "queue",
|
|
68126
68962
|
reason: `Share queue ${key}`,
|
|
68127
|
-
evidenceIds: [...
|
|
68963
|
+
evidenceIds: [...ids2]
|
|
68128
68964
|
});
|
|
68129
68965
|
}
|
|
68130
68966
|
}
|
|
@@ -68148,13 +68984,13 @@ function buildChains(evidence2) {
|
|
|
68148
68984
|
const commitEvs = evidence2.filter((e) => e.kind === "commit");
|
|
68149
68985
|
const symbolEvs = evidence2.filter((e) => e.kind === "symbol");
|
|
68150
68986
|
const relevanceById = new Map(evidence2.map((e) => [e.id, e.relevance]));
|
|
68151
|
-
function avgRelevance(
|
|
68152
|
-
if (
|
|
68987
|
+
function avgRelevance(ids2) {
|
|
68988
|
+
if (ids2.length === 0) return 0;
|
|
68153
68989
|
let sum = 0;
|
|
68154
|
-
for (const id of
|
|
68990
|
+
for (const id of ids2) {
|
|
68155
68991
|
sum += relevanceById.get(id) ?? 0;
|
|
68156
68992
|
}
|
|
68157
|
-
return sum /
|
|
68993
|
+
return sum / ids2.length;
|
|
68158
68994
|
}
|
|
68159
68995
|
const chains = [];
|
|
68160
68996
|
if (queueEdges2.length > 0) {
|
|
@@ -68258,7 +69094,7 @@ function seedRole(s) {
|
|
|
68258
69094
|
if (/util|helper/i.test(hay)) return "util";
|
|
68259
69095
|
return "code";
|
|
68260
69096
|
}
|
|
68261
|
-
function scoreSeed(s, index2) {
|
|
69097
|
+
function scoreSeed(s, index2, hintTokens) {
|
|
68262
69098
|
const hay = `${s.name} ${s.filePath}`.toLowerCase();
|
|
68263
69099
|
let score = 0;
|
|
68264
69100
|
if (PREFER.test(hay)) score += 3;
|
|
@@ -68266,10 +69102,20 @@ function scoreSeed(s, index2) {
|
|
|
68266
69102
|
if (/\.(resolver|controller|service)\.[jt]sx?$/i.test(s.filePath)) score += 2;
|
|
68267
69103
|
if (/(^|\/)scripts?\//i.test(s.filePath)) score -= 2;
|
|
68268
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
|
+
}
|
|
68269
69115
|
return score;
|
|
68270
69116
|
}
|
|
68271
|
-
function rankSeeds(seeds) {
|
|
68272
|
-
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 }));
|
|
68273
69119
|
}
|
|
68274
69120
|
|
|
68275
69121
|
// ../../packages/engine/src/normalize.ts
|
|
@@ -68329,6 +69175,7 @@ var RUNTIME_KINDS2 = /* @__PURE__ */ new Set([
|
|
|
68329
69175
|
]);
|
|
68330
69176
|
var MAX_RUNTIME_CONTRIBUTION = 2;
|
|
68331
69177
|
var MAX_STRUCTURAL_CONTRIBUTION = 0.6;
|
|
69178
|
+
var MAX_AMBIENT_RUNTIME_CONTRIBUTION = 0.6;
|
|
68332
69179
|
var NORMALIZATION = 6;
|
|
68333
69180
|
function clamp014(n) {
|
|
68334
69181
|
if (Number.isNaN(n)) return 0;
|
|
@@ -68336,13 +69183,18 @@ function clamp014(n) {
|
|
|
68336
69183
|
if (n > 1) return 1;
|
|
68337
69184
|
return n;
|
|
68338
69185
|
}
|
|
68339
|
-
function computeWeightedEvidenceConfidence(evidence2) {
|
|
69186
|
+
function computeWeightedEvidenceConfidence(evidence2, ambientEvidenceIds) {
|
|
68340
69187
|
const runtimeBySource = /* @__PURE__ */ new Map();
|
|
69188
|
+
const ambientBySource = /* @__PURE__ */ new Map();
|
|
68341
69189
|
const structuralBySource = /* @__PURE__ */ new Map();
|
|
68342
69190
|
for (const e of evidence2) {
|
|
68343
69191
|
const r = clamp014(e.relevance);
|
|
68344
69192
|
if (RUNTIME_KINDS2.has(e.kind)) {
|
|
68345
|
-
|
|
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
|
+
}
|
|
68346
69198
|
} else {
|
|
68347
69199
|
structuralBySource.set(e.source, (structuralBySource.get(e.source) ?? 0) + 0.5 * r);
|
|
68348
69200
|
}
|
|
@@ -68351,11 +69203,15 @@ function computeWeightedEvidenceConfidence(evidence2) {
|
|
|
68351
69203
|
(acc, w) => acc + Math.min(w, MAX_RUNTIME_CONTRIBUTION),
|
|
68352
69204
|
0
|
|
68353
69205
|
);
|
|
69206
|
+
const ambientSum = [...ambientBySource.values()].reduce(
|
|
69207
|
+
(acc, w) => acc + Math.min(w, MAX_AMBIENT_RUNTIME_CONTRIBUTION),
|
|
69208
|
+
0
|
|
69209
|
+
);
|
|
68354
69210
|
const structuralSum = [...structuralBySource.values()].reduce(
|
|
68355
69211
|
(acc, w) => acc + Math.min(w, MAX_STRUCTURAL_CONTRIBUTION),
|
|
68356
69212
|
0
|
|
68357
69213
|
);
|
|
68358
|
-
return clamp014((runtimeSum + structuralSum) / NORMALIZATION);
|
|
69214
|
+
return clamp014((runtimeSum + ambientSum + structuralSum) / NORMALIZATION);
|
|
68359
69215
|
}
|
|
68360
69216
|
|
|
68361
69217
|
// ../../packages/engine/src/git-collector.ts
|
|
@@ -68371,8 +69227,9 @@ function isRefLike(s) {
|
|
|
68371
69227
|
if (t.startsWith("-")) return false;
|
|
68372
69228
|
if (t.includes("..")) return false;
|
|
68373
69229
|
if (/\s|:|ago|yesterday|week|month|year|=/i.test(t)) return false;
|
|
69230
|
+
if (/^\d+[smhd]$/i.test(t)) return false;
|
|
68374
69231
|
if (/^[0-9a-f]{7,40}$/i.test(t)) return true;
|
|
68375
|
-
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;
|
|
68376
69233
|
return false;
|
|
68377
69234
|
}
|
|
68378
69235
|
function parseDiffStat(stdout) {
|
|
@@ -68591,6 +69448,33 @@ function logWindowFrom(since) {
|
|
|
68591
69448
|
}
|
|
68592
69449
|
return new Date(now - 7 * 864e5).toISOString();
|
|
68593
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
|
+
}
|
|
68594
69478
|
function queueFindingConfidence(opts) {
|
|
68595
69479
|
const { starvedCount, backloggedCount, failingCount } = opts;
|
|
68596
69480
|
return starvedCount > 0 && backloggedCount === 0 && failingCount === 0 ? 0.65 : 0.85;
|
|
@@ -68599,7 +69483,8 @@ function looksDiffable(since) {
|
|
|
68599
69483
|
const s = since.trim();
|
|
68600
69484
|
if (s.length === 0) return false;
|
|
68601
69485
|
if (s.includes("..")) return true;
|
|
68602
|
-
|
|
69486
|
+
if (/^\d+[smhd]$/i.test(s)) return false;
|
|
69487
|
+
return /^[A-Za-z0-9._/~^-]+$/.test(s);
|
|
68603
69488
|
}
|
|
68604
69489
|
async function investigate(input, deps) {
|
|
68605
69490
|
const { code, db } = deps;
|
|
@@ -68622,7 +69507,8 @@ async function investigate(input, deps) {
|
|
|
68622
69507
|
return ev;
|
|
68623
69508
|
}
|
|
68624
69509
|
const rawSeeds = await code.searchSymbols(hint, 5);
|
|
68625
|
-
const
|
|
69510
|
+
const hintTokens = [...new Set(tokenize(hint))];
|
|
69511
|
+
const ranked = rankSeeds(rawSeeds, hintTokens);
|
|
68626
69512
|
const seeds = ranked.map((r) => r.symbol);
|
|
68627
69513
|
const top = seeds[0];
|
|
68628
69514
|
if (!top) {
|
|
@@ -68674,6 +69560,13 @@ async function investigate(input, deps) {
|
|
|
68674
69560
|
changes = null;
|
|
68675
69561
|
}
|
|
68676
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
|
+
}
|
|
68677
69570
|
const seedLine = top.startLine ?? 0;
|
|
68678
69571
|
const seedEv = mkEv(
|
|
68679
69572
|
"symbol",
|
|
@@ -68731,8 +69624,20 @@ async function investigate(input, deps) {
|
|
|
68731
69624
|
);
|
|
68732
69625
|
changeEvId = ev.id;
|
|
68733
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
|
+
}
|
|
68734
69637
|
let analysis = null;
|
|
68735
69638
|
const logEvIds = [];
|
|
69639
|
+
const directLogEvIds = [];
|
|
69640
|
+
const ambientLogEvIds = [];
|
|
68736
69641
|
let logsCollected = false;
|
|
68737
69642
|
let logsCompatibilityError;
|
|
68738
69643
|
if (deps.logs) {
|
|
@@ -68749,8 +69654,19 @@ async function investigate(input, deps) {
|
|
|
68749
69654
|
const from = logWindowFrom(input.since);
|
|
68750
69655
|
analysis = await deps.logs.analyzeErrors({ service: input.service, from });
|
|
68751
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
|
+
];
|
|
68752
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
|
+
);
|
|
68753
69668
|
const tags = [];
|
|
69669
|
+
if (relevanceClass === "ambient") tags.push("ambient");
|
|
68754
69670
|
if (s.isNew) tags.push("NEW");
|
|
68755
69671
|
else if (s.ratio !== void 0 && Number.isFinite(s.ratio) && s.ratio >= 1.5) {
|
|
68756
69672
|
tags.push(`spike x${s.ratio.toFixed(1)}`);
|
|
@@ -68771,15 +69687,21 @@ async function investigate(input, deps) {
|
|
|
68771
69687
|
services: s.services,
|
|
68772
69688
|
isNew: s.isNew ?? false,
|
|
68773
69689
|
ratio: s.ratio ?? null,
|
|
68774
|
-
sampleMessage: s.sampleMessage ?? null
|
|
69690
|
+
sampleMessage: s.sampleMessage ?? null,
|
|
69691
|
+
relevanceClass,
|
|
69692
|
+
relevanceReason
|
|
68775
69693
|
},
|
|
68776
69694
|
{},
|
|
68777
69695
|
s.lastSeen || void 0,
|
|
68778
|
-
|
|
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
|
|
68779
69699
|
);
|
|
68780
69700
|
if (s.isNew) ev.isNew = s.isNew;
|
|
68781
69701
|
if (typeof s.ratio === "number" && Number.isFinite(s.ratio)) ev.ratio = s.ratio;
|
|
68782
69702
|
logEvIds.push(ev.id);
|
|
69703
|
+
if (relevanceClass === "direct") directLogEvIds.push(ev.id);
|
|
69704
|
+
else ambientLogEvIds.push(ev.id);
|
|
68783
69705
|
}
|
|
68784
69706
|
}
|
|
68785
69707
|
} catch {
|
|
@@ -68792,11 +69714,8 @@ async function investigate(input, deps) {
|
|
|
68792
69714
|
if (deps.mongo) {
|
|
68793
69715
|
try {
|
|
68794
69716
|
stateAnalysis = await deps.mongo.analyzeState();
|
|
68795
|
-
const
|
|
68796
|
-
const
|
|
68797
|
-
.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(seedBase)])
|
|
68798
|
-
];
|
|
68799
|
-
for (const s of selectStateSignals(stateAnalysis, terms)) {
|
|
69717
|
+
const stateTerms = [...new Set(tokenize(hint))];
|
|
69718
|
+
for (const s of selectStateSignals(stateAnalysis, stateTerms)) {
|
|
68800
69719
|
const ev = mkEv("state", s.title, s.payload, {}, s.timestamp, s.relevance);
|
|
68801
69720
|
stateEvIds.push(ev.id);
|
|
68802
69721
|
stateCollections.add(s.collection);
|
|
@@ -68834,12 +69753,13 @@ async function investigate(input, deps) {
|
|
|
68834
69753
|
queueRuntimeState = null;
|
|
68835
69754
|
}
|
|
68836
69755
|
}
|
|
68837
|
-
const METRICS_TIMEOUT_MS =
|
|
69756
|
+
const METRICS_TIMEOUT_MS = 3e4;
|
|
68838
69757
|
const metricEvIds = [];
|
|
68839
69758
|
const latencyMetricEvIds = [];
|
|
68840
69759
|
const queueMetricEvIds = [];
|
|
68841
69760
|
const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
68842
69761
|
let metricsCollected = false;
|
|
69762
|
+
let metricsFailureReason;
|
|
68843
69763
|
if (deps.metrics) {
|
|
68844
69764
|
const ac = new AbortController();
|
|
68845
69765
|
let metricsTimerId;
|
|
@@ -68900,7 +69820,8 @@ async function investigate(input, deps) {
|
|
|
68900
69820
|
}
|
|
68901
69821
|
}
|
|
68902
69822
|
metricsCollected = true;
|
|
68903
|
-
} catch {
|
|
69823
|
+
} catch (metricsErr) {
|
|
69824
|
+
metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
|
|
68904
69825
|
} finally {
|
|
68905
69826
|
if (metricsTimerId !== void 0) clearTimeout(metricsTimerId);
|
|
68906
69827
|
}
|
|
@@ -68928,9 +69849,12 @@ async function investigate(input, deps) {
|
|
|
68928
69849
|
latencyMetricEvIds,
|
|
68929
69850
|
queueBacklogEvIdsByQueue,
|
|
68930
69851
|
queueStarvationEvIdsByQueue,
|
|
68931
|
-
queueMetricEvIdsByQueue
|
|
69852
|
+
queueMetricEvIdsByQueue,
|
|
69853
|
+
sinceProvided: input.since !== void 0,
|
|
69854
|
+
graph
|
|
68932
69855
|
});
|
|
68933
69856
|
const validated = validateHypotheses(hyps, evidence2);
|
|
69857
|
+
const causeChains = buildCauseChains(validated, evidence2, graph, label);
|
|
68934
69858
|
const findings2 = [];
|
|
68935
69859
|
findings2.push({
|
|
68936
69860
|
kind: "observation",
|
|
@@ -68985,24 +69909,43 @@ async function investigate(input, deps) {
|
|
|
68985
69909
|
confidence: clamp015(0.4 + Math.min(m, 20) / 40),
|
|
68986
69910
|
evidenceIds: [changeEvId]
|
|
68987
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
|
+
});
|
|
68988
69919
|
}
|
|
68989
69920
|
if (analysis !== null && analysis.signatures.length > 0) {
|
|
68990
69921
|
const newN = analysis.newSignatures.length;
|
|
68991
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)` : "";
|
|
68992
69926
|
findings2.push({
|
|
68993
69927
|
kind: "observation",
|
|
68994
|
-
title: `${analysis.signatures.length} error signature(s) (${newN} new, ${analysis.totalErrors} error(s)) \u2014 affected: ${affected}`,
|
|
68995
|
-
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,
|
|
68996
69930
|
evidenceIds: logEvIds
|
|
68997
69931
|
});
|
|
68998
|
-
const
|
|
68999
|
-
|
|
69000
|
-
|
|
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)})` : "";
|
|
69001
69936
|
findings2.push({
|
|
69002
69937
|
kind: "anomaly",
|
|
69003
|
-
title: `Top error signature: ${
|
|
69938
|
+
title: `Top error signature: ${topSig.key} \u2014 ${topSig.count}x${flag}, last ${shortTs(topSig.lastSeen)}`,
|
|
69004
69939
|
confidence: 0.7,
|
|
69005
|
-
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)
|
|
69006
69949
|
});
|
|
69007
69950
|
}
|
|
69008
69951
|
}
|
|
@@ -69100,13 +70043,17 @@ async function investigate(input, deps) {
|
|
|
69100
70043
|
metadata: { blastRadius }
|
|
69101
70044
|
});
|
|
69102
70045
|
}
|
|
69103
|
-
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
|
+
);
|
|
69104
70051
|
causeInputs.push({
|
|
69105
70052
|
id: "cause:deployment-regression",
|
|
69106
70053
|
title: `Recent change to ${top.name} in ${input.since}..HEAD may have introduced the regression`,
|
|
69107
70054
|
category: "deployment-regression",
|
|
69108
70055
|
sourceEvidenceIds: [changeEvId, seedEv.id],
|
|
69109
|
-
baseScore: clamp015(0.25 + (queueHits.length > 0 ? 0.05 : 0)),
|
|
70056
|
+
baseScore: clamp015((seedInChanges ? 0.45 : 0.25) + (queueHits.length > 0 ? 0.05 : 0)),
|
|
69110
70057
|
metadata: { blastRadius }
|
|
69111
70058
|
});
|
|
69112
70059
|
}
|
|
@@ -69120,7 +70067,7 @@ async function investigate(input, deps) {
|
|
|
69120
70067
|
metadata: { blastRadius }
|
|
69121
70068
|
});
|
|
69122
70069
|
}
|
|
69123
|
-
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0) {
|
|
70070
|
+
if (analysis !== null && analysis.signatures.length > 0 && queueHits.length > 0 && directLogEvIds.length > 0) {
|
|
69124
70071
|
const firstQueue = queueHits[0];
|
|
69125
70072
|
const queueLabel = firstQueue !== void 0 ? `"${firstQueue.queueName}" (${firstQueue.producerSymbol ?? "unknown"} -> ${firstQueue.workerSymbol ?? "unknown"})` : "the queue path";
|
|
69126
70073
|
const topSig = analysis.signatures[0];
|
|
@@ -69128,7 +70075,7 @@ async function investigate(input, deps) {
|
|
|
69128
70075
|
id: "cause:error-correlation",
|
|
69129
70076
|
title: `Runtime errors (${analysis.totalErrors}${topSig ? `, top ${topSig.key}` : ""}) correlate with the implicated queue path ${queueLabel}`,
|
|
69130
70077
|
category: "error-correlation",
|
|
69131
|
-
sourceEvidenceIds:
|
|
70078
|
+
sourceEvidenceIds: directLogEvIds.slice(0, 3),
|
|
69132
70079
|
baseScore: 0.3,
|
|
69133
70080
|
metadata: { blastRadius }
|
|
69134
70081
|
});
|
|
@@ -69167,19 +70114,15 @@ async function investigate(input, deps) {
|
|
|
69167
70114
|
providerReliability,
|
|
69168
70115
|
request: { hint: input.hint, service: input.service }
|
|
69169
70116
|
});
|
|
69170
|
-
const evidenceConfidence = computeWeightedEvidenceConfidence(
|
|
70117
|
+
const evidenceConfidence = computeWeightedEvidenceConfidence(
|
|
70118
|
+
evidence2,
|
|
70119
|
+
ambientLogEvIds.length > 0 ? new Set(ambientLogEvIds) : void 0
|
|
70120
|
+
);
|
|
69171
70121
|
const seedResolved = seeds.length > 0 ? 1 : 0;
|
|
69172
70122
|
const confidence = clamp015(0.5 * evidenceConfidence + 0.5 * seedResolved);
|
|
69173
70123
|
const area = ctx.community?.name ?? top.filePath;
|
|
69174
70124
|
const topCause = rankedCauses[0];
|
|
69175
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.`;
|
|
69176
|
-
let recentChanges;
|
|
69177
|
-
if (deps.repoPath && input.since) {
|
|
69178
|
-
try {
|
|
69179
|
-
recentChanges = await collectGitChanges({ repoPath: deps.repoPath, since: input.since });
|
|
69180
|
-
} catch {
|
|
69181
|
-
}
|
|
69182
|
-
}
|
|
69183
70126
|
const nextActions = buildNextActions(top, ctx, impact, queueHits, changes, input);
|
|
69184
70127
|
if (ownershipEstimate?.likelyMaintainer) {
|
|
69185
70128
|
nextActions.unshift(
|
|
@@ -69203,15 +70146,18 @@ async function investigate(input, deps) {
|
|
|
69203
70146
|
confidence,
|
|
69204
70147
|
nextActions,
|
|
69205
70148
|
ownership: ownershipEstimate,
|
|
70149
|
+
causeChains: causeChains.length > 0 ? causeChains : void 0,
|
|
69206
70150
|
...recentChanges !== void 0 ? { recentChanges } : {}
|
|
69207
70151
|
};
|
|
69208
|
-
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 } : {
|
|
69209
70153
|
elasticsearch: deps.logs != null,
|
|
69210
70154
|
mongodb: deps.mongo != null,
|
|
69211
70155
|
grafana: deps.metrics != null,
|
|
69212
70156
|
metricsCollected,
|
|
70157
|
+
metricsFailureReason,
|
|
69213
70158
|
logsCollected,
|
|
69214
|
-
logsCompatibilityError
|
|
70159
|
+
logsCompatibilityError,
|
|
70160
|
+
sinceProvided: input.since !== void 0
|
|
69215
70161
|
};
|
|
69216
70162
|
const gapAnalysis = detectMissingEvidence(report, connectorFlags);
|
|
69217
70163
|
report.gapAnalysis = gapAnalysis;
|
|
@@ -69355,11 +70301,11 @@ function groupQueueEvidence(evidence2) {
|
|
|
69355
70301
|
return map2;
|
|
69356
70302
|
}
|
|
69357
70303
|
function queueEvidenceIds(evidence2) {
|
|
69358
|
-
const
|
|
70304
|
+
const ids2 = /* @__PURE__ */ new Set();
|
|
69359
70305
|
for (const e of evidence2) {
|
|
69360
|
-
if (e.source === "queue" && e.kind === "queue-state")
|
|
70306
|
+
if (e.source === "queue" && e.kind === "queue-state") ids2.add(e.id);
|
|
69361
70307
|
}
|
|
69362
|
-
return
|
|
70308
|
+
return ids2;
|
|
69363
70309
|
}
|
|
69364
70310
|
var CONFIDENCE_EXPLAIN_THRESHOLD = 0.8;
|
|
69365
70311
|
function explainLowConfidence(r) {
|
|
@@ -69522,6 +70468,12 @@ function renderReport2(r) {
|
|
|
69522
70468
|
}
|
|
69523
70469
|
lines.push("");
|
|
69524
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
|
+
}
|
|
69525
70477
|
if (r.hypotheses.length === 0) {
|
|
69526
70478
|
lines.push("(none)");
|
|
69527
70479
|
} else {
|
|
@@ -69536,6 +70488,19 @@ function renderReport2(r) {
|
|
|
69536
70488
|
}
|
|
69537
70489
|
}
|
|
69538
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
|
+
}
|
|
69539
70504
|
lines.push("## Evidence gaps (what we don't know)");
|
|
69540
70505
|
if (r.gapAnalysis.gaps.length === 0) {
|
|
69541
70506
|
lines.push("(no major evidence gaps)");
|
|
@@ -69666,6 +70631,12 @@ function reportToMarkdown(r) {
|
|
|
69666
70631
|
}
|
|
69667
70632
|
out.push("");
|
|
69668
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
|
+
}
|
|
69669
70640
|
if (r.hypotheses.length === 0) {
|
|
69670
70641
|
out.push("_none_");
|
|
69671
70642
|
} else {
|
|
@@ -69881,7 +70852,8 @@ async function reconstructChangeTimeline(input, deps) {
|
|
|
69881
70852
|
}
|
|
69882
70853
|
}
|
|
69883
70854
|
}
|
|
69884
|
-
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 + ".";
|
|
69885
70857
|
const note = "Changes are evidence, not conclusions \u2014 a change in this window is not automatically the cause.";
|
|
69886
70858
|
return {
|
|
69887
70859
|
window: {
|
|
@@ -69904,6 +70876,9 @@ function renderChangeTimeline(t) {
|
|
|
69904
70876
|
lines.push("# Change Timeline");
|
|
69905
70877
|
lines.push("");
|
|
69906
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);
|
|
69907
70882
|
lines.push(t.summary);
|
|
69908
70883
|
lines.push("");
|
|
69909
70884
|
lines.push("> " + t.note);
|
|
@@ -69926,9 +70901,13 @@ function renderChangeTimeline(t) {
|
|
|
69926
70901
|
if (t.changeImpact !== null) {
|
|
69927
70902
|
lines.push("");
|
|
69928
70903
|
lines.push("## Change impact");
|
|
69929
|
-
lines.push(
|
|
69930
|
-
|
|
69931
|
-
)
|
|
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
|
+
}
|
|
69932
70911
|
}
|
|
69933
70912
|
return lines.join("\n");
|
|
69934
70913
|
}
|
|
@@ -69988,6 +70967,9 @@ function renderWhatChanged(r) {
|
|
|
69988
70967
|
const lines = [];
|
|
69989
70968
|
lines.push("# What changed");
|
|
69990
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);
|
|
69991
70973
|
lines.push(r.summary);
|
|
69992
70974
|
lines.push("");
|
|
69993
70975
|
lines.push("> " + r.note);
|
|
@@ -70017,6 +70999,11 @@ function renderWhatChanged(r) {
|
|
|
70017
70999
|
lines.push(
|
|
70018
71000
|
"## Affected flows: " + r.changeImpact.affectedFlows.length + " execution flow(s) affected"
|
|
70019
71001
|
);
|
|
71002
|
+
if (r.changeImpact.affectedFlows.length > 0) {
|
|
71003
|
+
for (const f of r.changeImpact.affectedFlows) {
|
|
71004
|
+
lines.push(" - " + f.flowName);
|
|
71005
|
+
}
|
|
71006
|
+
}
|
|
70020
71007
|
}
|
|
70021
71008
|
return lines.join("\n");
|
|
70022
71009
|
}
|
|
@@ -70147,7 +71134,7 @@ async function discoverArchitecture(deps) {
|
|
|
70147
71134
|
})();
|
|
70148
71135
|
const largestSubsystem = subsystems[0];
|
|
70149
71136
|
const largestDesc = largestSubsystem != null ? `${largestSubsystem.name} with ${largestSubsystem.members} symbols` : "none";
|
|
70150
|
-
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.`;
|
|
70151
71138
|
return {
|
|
70152
71139
|
nodeStats,
|
|
70153
71140
|
subsystems,
|
|
@@ -70215,7 +71202,7 @@ function renderArchitecture(m) {
|
|
|
70215
71202
|
}
|
|
70216
71203
|
lines.push("");
|
|
70217
71204
|
lines.push("## Fragility");
|
|
70218
|
-
lines.push(`-
|
|
71205
|
+
lines.push(`- Unreferenced symbols: ${m.fragile.deadCode}`);
|
|
70219
71206
|
lines.push(`- High-coupling pairs (co-changes \u2265 3): ${m.fragile.highCouplingPairs}`);
|
|
70220
71207
|
return lines.join("\n");
|
|
70221
71208
|
}
|
|
@@ -70406,6 +71393,13 @@ function renderOwnership(o) {
|
|
|
70406
71393
|
lines.push("");
|
|
70407
71394
|
if (o.file === null) {
|
|
70408
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
|
+
}
|
|
70409
71403
|
return lines.join("\n");
|
|
70410
71404
|
}
|
|
70411
71405
|
if (o.symbol !== null) {
|
|
@@ -70531,17 +71525,35 @@ function generatePostmortem(r) {
|
|
|
70531
71525
|
);
|
|
70532
71526
|
}
|
|
70533
71527
|
} else {
|
|
70534
|
-
if (
|
|
70535
|
-
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:");
|
|
70536
71530
|
lines.push("");
|
|
70537
|
-
for (const
|
|
70538
|
-
lines.push(
|
|
70539
|
-
|
|
70540
|
-
);
|
|
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
|
+
}
|
|
70541
71553
|
}
|
|
70542
71554
|
}
|
|
70543
71555
|
if (commitEvidence.length > 0) {
|
|
70544
|
-
if (supportedHypotheses.length > 0) lines.push("");
|
|
71556
|
+
if (supportedHypotheses.length > 0 || (r.causeChains?.length ?? 0) > 0) lines.push("");
|
|
70545
71557
|
lines.push("Recent changes (commit evidence) present during this incident:");
|
|
70546
71558
|
lines.push("");
|
|
70547
71559
|
for (const e of commitEvidence) {
|
|
@@ -70602,19 +71614,29 @@ function generatePostmortem(r) {
|
|
|
70602
71614
|
lines.push("");
|
|
70603
71615
|
lines.push("## Follow-up actions");
|
|
70604
71616
|
lines.push("");
|
|
70605
|
-
const seen = /* @__PURE__ */ new Set();
|
|
70606
71617
|
const checkboxItems = [];
|
|
71618
|
+
const seen = /* @__PURE__ */ new Set();
|
|
70607
71619
|
for (const action of r.nextActions) {
|
|
70608
71620
|
if (!seen.has(action)) {
|
|
70609
71621
|
seen.add(action);
|
|
70610
71622
|
checkboxItems.push(action);
|
|
70611
71623
|
}
|
|
70612
71624
|
}
|
|
71625
|
+
const seenGapSources = /* @__PURE__ */ new Set();
|
|
70613
71626
|
for (const gap of r.gapAnalysis.gaps) {
|
|
70614
|
-
|
|
70615
|
-
|
|
70616
|
-
|
|
70617
|
-
|
|
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);
|
|
70618
71640
|
}
|
|
70619
71641
|
}
|
|
70620
71642
|
if (checkboxItems.length === 0) {
|
|
@@ -70746,11 +71768,113 @@ var TOPIC_MAP = {
|
|
|
70746
71768
|
}
|
|
70747
71769
|
};
|
|
70748
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
|
+
}
|
|
70749
71812
|
function detectMode(d) {
|
|
70750
71813
|
if (/\b(ignore|exclude|without|skip|drop)\b/.test(d)) return "ignore";
|
|
70751
71814
|
if (/\b(focus|only|just|concentrate|look at)\b/.test(d)) return "focus";
|
|
70752
71815
|
return "none";
|
|
70753
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
|
+
}
|
|
70754
71878
|
function matchedTopics(d) {
|
|
70755
71879
|
return ALL_TOPICS.filter((t) => {
|
|
70756
71880
|
const entry2 = TOPIC_MAP[t];
|
|
@@ -70791,11 +71915,54 @@ function topicKeywords(topics) {
|
|
|
70791
71915
|
}
|
|
70792
71916
|
return kws;
|
|
70793
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
|
+
}
|
|
70794
71952
|
function refineInvestigation(r, directive) {
|
|
70795
71953
|
const d = directive.toLowerCase();
|
|
71954
|
+
if (hasFocusVerb(d) && hasIgnoreVerb(d)) {
|
|
71955
|
+
return applyMixedDirective(r, directive, d);
|
|
71956
|
+
}
|
|
70796
71957
|
const mode = detectMode(d);
|
|
70797
71958
|
const topics = matchedTopics(d);
|
|
70798
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
|
+
}
|
|
70799
71966
|
const recognizedList = ALL_TOPICS.join(", ");
|
|
70800
71967
|
const note2 = "No specific topic directive recognized. Recognized topics: " + recognizedList + '. Example usage: "focus on queue behavior", "ignore deployment changes".';
|
|
70801
71968
|
return {
|
|
@@ -70843,7 +72010,7 @@ function renderRefined(r, v) {
|
|
|
70843
72010
|
const lines = [];
|
|
70844
72011
|
lines.push("# Refined investigation \u2014 " + r.input.hint);
|
|
70845
72012
|
lines.push("");
|
|
70846
|
-
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)";
|
|
70847
72014
|
lines.push(" [mode: " + modeLabel + "] reusing saved evidence, no re-query");
|
|
70848
72015
|
lines.push("");
|
|
70849
72016
|
lines.push("## Hypotheses");
|
|
@@ -70898,20 +72065,175 @@ function refinedToJSON(r, v) {
|
|
|
70898
72065
|
|
|
70899
72066
|
// ../../packages/engine/src/onboard.ts
|
|
70900
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
|
+
}
|
|
70901
72181
|
async function buildOnboarding(input, deps) {
|
|
70902
72182
|
const architecture = await discoverArchitecture({ code: deps.code, db: deps.db });
|
|
70903
|
-
const
|
|
70904
|
-
|
|
70905
|
-
|
|
70906
|
-
|
|
70907
|
-
|
|
70908
|
-
|
|
70909
|
-
|
|
70910
|
-
|
|
70911
|
-
|
|
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 + '".' : "");
|
|
70912
72234
|
return {
|
|
70913
|
-
area:
|
|
70914
|
-
architecture,
|
|
72235
|
+
area: area ?? null,
|
|
72236
|
+
architecture: filteredArchitecture,
|
|
70915
72237
|
ownership,
|
|
70916
72238
|
pastIncidents,
|
|
70917
72239
|
summary
|
|
@@ -70970,7 +72292,7 @@ function renderOnboarding(g) {
|
|
|
70970
72292
|
lines.push("");
|
|
70971
72293
|
lines.push("## What usually breaks");
|
|
70972
72294
|
lines.push("");
|
|
70973
|
-
lines.push(`-
|
|
72295
|
+
lines.push(`- Unreferenced symbols: ${g.architecture.fragile.deadCode}`);
|
|
70974
72296
|
lines.push(
|
|
70975
72297
|
`- High-coupling pairs (co-changes \u2265 3): ${g.architecture.fragile.highCouplingPairs}`
|
|
70976
72298
|
);
|
|
@@ -71064,6 +72386,7 @@ var SCENARIOS = [
|
|
|
71064
72386
|
since: "HEAD~10",
|
|
71065
72387
|
expectedSignals: [
|
|
71066
72388
|
{ key: "seed", label: "Seed symbols resolved" },
|
|
72389
|
+
{ key: "commit", label: "Recent change evidence found" },
|
|
71067
72390
|
{ key: "hyp:deployment-regression", label: "deployment-regression hypothesis present" },
|
|
71068
72391
|
{ key: "actions", label: "Next actions generated" }
|
|
71069
72392
|
],
|
|
@@ -71119,6 +72442,8 @@ function evaluateScenario(scenario, report) {
|
|
|
71119
72442
|
ok = report.gapAnalysis.gaps.length > 0;
|
|
71120
72443
|
} else if (signal.key === "actions") {
|
|
71121
72444
|
ok = report.nextActions.length > 0;
|
|
72445
|
+
} else if (signal.key === "commit") {
|
|
72446
|
+
ok = report.evidence.some((e) => e.kind === "commit");
|
|
71122
72447
|
} else if (signal.key.startsWith("hyp:")) {
|
|
71123
72448
|
const category = signal.key.slice(4);
|
|
71124
72449
|
ok = report.hypotheses.some((h) => h.category === category);
|
|
@@ -71159,6 +72484,16 @@ function renderSimulation(scenario, report, evaluation) {
|
|
|
71159
72484
|
"Form your own hypothesis before reading on \u2014 then compare it with what Horus found."
|
|
71160
72485
|
);
|
|
71161
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
|
+
}
|
|
71162
72497
|
lines.push("## Horus investigation");
|
|
71163
72498
|
lines.push("");
|
|
71164
72499
|
lines.push(report.summary);
|
|
@@ -71190,6 +72525,28 @@ function renderSimulation(scenario, report, evaluation) {
|
|
|
71190
72525
|
for (const tip of scenario.coachingTips) {
|
|
71191
72526
|
lines.push(`- ${tip}`);
|
|
71192
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
|
+
}
|
|
71193
72550
|
return lines.join("\n");
|
|
71194
72551
|
}
|
|
71195
72552
|
|
|
@@ -71361,6 +72718,54 @@ function validateNarrative(output, input) {
|
|
|
71361
72718
|
}
|
|
71362
72719
|
}
|
|
71363
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
|
+
}
|
|
71364
72769
|
return { valid: errors2.length === 0, errors: errors2 };
|
|
71365
72770
|
}
|
|
71366
72771
|
async function renderNarrative(input, opts = {}) {
|
|
@@ -71378,11 +72783,11 @@ async function renderNarrative(input, opts = {}) {
|
|
|
71378
72783
|
fromProvider: false,
|
|
71379
72784
|
validationErrors: validation.errors
|
|
71380
72785
|
};
|
|
71381
|
-
} catch {
|
|
72786
|
+
} catch (err) {
|
|
71382
72787
|
return {
|
|
71383
72788
|
output: deterministicFallback(input),
|
|
71384
72789
|
fromProvider: false,
|
|
71385
|
-
validationErrors: ["Provider threw an error"]
|
|
72790
|
+
validationErrors: [err instanceof Error ? err.message : "Provider threw an error"]
|
|
71386
72791
|
};
|
|
71387
72792
|
}
|
|
71388
72793
|
}
|
|
@@ -71449,7 +72854,26 @@ function buildPrompt(input, ceiling) {
|
|
|
71449
72854
|
(e) => `- [${e.id}] (${e.kind}) ${e.title}${e.excerpt ? `: ${e.excerpt}` : ""}`
|
|
71450
72855
|
).join("\n");
|
|
71451
72856
|
const causeLines = input.suspectedCauses.map((c) => `- ${c.label} (score: ${c.score})`).join("\n");
|
|
71452
|
-
|
|
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.
|
|
71453
72877
|
|
|
71454
72878
|
Investigation hint: ${input.hint}
|
|
71455
72879
|
Deterministic summary: ${input.deterministicSummary}
|
|
@@ -71459,23 +72883,30 @@ ${evidenceLines}
|
|
|
71459
72883
|
|
|
71460
72884
|
Suspected causes:
|
|
71461
72885
|
${causeLines}
|
|
72886
|
+
${hypothesisLines ? `
|
|
72887
|
+
Deterministic hypotheses (provide a second-pass judgment for each):
|
|
72888
|
+
${hypothesisLines}` : ""}
|
|
71462
72889
|
|
|
71463
72890
|
Known services: ${input.knownServices.join(", ")}
|
|
71464
72891
|
|
|
71465
72892
|
Return ONLY valid JSON with this exact shape:
|
|
71466
72893
|
{
|
|
71467
72894
|
"what": "<what happened \u2014 one concise paragraph>",
|
|
71468
|
-
"why": "<root cause
|
|
72895
|
+
"why": "<root cause narrative grounded in the evidence above>",
|
|
71469
72896
|
"whereNext": ["<action 1>", "<action 2>"],
|
|
71470
72897
|
"citations": [{"evidenceId": "<id from the evidence list>", "rationale": "<why this supports the claim>"}],
|
|
71471
72898
|
"confidence": <number between 0 and ${ceiling}>,
|
|
71472
|
-
"mentionedServices": ["<service name from known list only>"]
|
|
72899
|
+
"mentionedServices": ["<service name from known list only>"],
|
|
72900
|
+
${hypothesisJudgmentSchema}
|
|
71473
72901
|
}
|
|
71474
72902
|
|
|
71475
72903
|
Hard rules:
|
|
71476
72904
|
- confidence must not exceed ${ceiling}
|
|
71477
72905
|
- only cite evidence IDs from the list above \u2014 any other ID is a hallucination
|
|
71478
|
-
- 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`;
|
|
71479
72910
|
}
|
|
71480
72911
|
function parseOutput(raw, input, ceiling) {
|
|
71481
72912
|
let parsed;
|
|
@@ -71489,21 +72920,52 @@ function parseOutput(raw, input, ceiling) {
|
|
|
71489
72920
|
parsed = JSON.parse(jsonMatch[0]);
|
|
71490
72921
|
}
|
|
71491
72922
|
const confidence = Math.min(
|
|
71492
|
-
typeof parsed
|
|
72923
|
+
typeof parsed["confidence"] === "number" ? parsed["confidence"] : input.reportConfidence,
|
|
71493
72924
|
ceiling
|
|
71494
72925
|
);
|
|
72926
|
+
const citations = Array.isArray(parsed["citations"]) ? parsed["citations"] : [];
|
|
71495
72927
|
const output = {
|
|
71496
|
-
what: typeof parsed
|
|
71497
|
-
why: typeof parsed
|
|
71498
|
-
whereNext: Array.isArray(parsed
|
|
71499
|
-
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,
|
|
71500
72932
|
confidence
|
|
71501
72933
|
};
|
|
71502
|
-
if (Array.isArray(parsed
|
|
71503
|
-
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
|
+
};
|
|
71504
72960
|
}
|
|
71505
72961
|
return output;
|
|
71506
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
|
+
}
|
|
71507
72969
|
|
|
71508
72970
|
// ../../packages/ai/src/fake-provider.ts
|
|
71509
72971
|
init_cjs_shims();
|
|
@@ -71543,6 +73005,12 @@ init_cjs_shims();
|
|
|
71543
73005
|
init_cjs_shims();
|
|
71544
73006
|
|
|
71545
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
|
+
}
|
|
71546
73014
|
function buildNarrativeInput(report) {
|
|
71547
73015
|
return {
|
|
71548
73016
|
investigationId: report.id,
|
|
@@ -71551,7 +73019,8 @@ function buildNarrativeInput(report) {
|
|
|
71551
73019
|
evidence: report.evidence.map((e) => ({
|
|
71552
73020
|
id: e.id,
|
|
71553
73021
|
kind: e.kind,
|
|
71554
|
-
title: e.title
|
|
73022
|
+
title: e.title,
|
|
73023
|
+
excerpt: extractEvidenceExcerpt(e)
|
|
71555
73024
|
})),
|
|
71556
73025
|
knownServices: report.input.service ? [report.input.service] : [],
|
|
71557
73026
|
suspectedCauses: report.suspectedCauses.map((c) => ({
|
|
@@ -71563,9 +73032,83 @@ function buildNarrativeInput(report) {
|
|
|
71563
73032
|
findings: report.findings.map((f) => ({
|
|
71564
73033
|
title: f.title,
|
|
71565
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
|
|
71566
73043
|
}))
|
|
71567
73044
|
};
|
|
71568
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
|
+
}
|
|
71569
73112
|
async function runInvestigate(hint, opts) {
|
|
71570
73113
|
try {
|
|
71571
73114
|
const config = await loadConfig(opts.config, { name: opts.name });
|
|
@@ -71625,33 +73168,22 @@ async function runInvestigate(hint, opts) {
|
|
|
71625
73168
|
const rendered = format === "json" ? reportToJSON(report) : format === "markdown" || format === "md" ? reportToMarkdown(report) : renderReport2(report);
|
|
71626
73169
|
console.log(rendered);
|
|
71627
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}`));
|
|
71628
73174
|
const narrativeInput = buildNarrativeInput(report);
|
|
71629
|
-
const provider = new AnthropicNarrativeProvider({ model: opts.aiModel });
|
|
71630
73175
|
const { output, fromProvider, validationErrors } = await renderNarrative(narrativeInput, { provider });
|
|
71631
73176
|
if (!fromProvider) {
|
|
71632
|
-
|
|
71633
|
-
|
|
71634
|
-
console.error(import_picocolors5.default.dim(` ${validationErrors[0]}`));
|
|
71635
|
-
}
|
|
73177
|
+
const reason = classifyAIFailure(validationErrors?.[0]);
|
|
73178
|
+
console.error(import_picocolors5.default.yellow(`[ai] fallback to deterministic \u2014 ${reason}`));
|
|
71636
73179
|
} else {
|
|
71637
|
-
const
|
|
71638
|
-
|
|
71639
|
-
|
|
71640
|
-
|
|
71641
|
-
|
|
71642
|
-
console.log(import_picocolors5.default.bold("What:"), output.what);
|
|
71643
|
-
console.log(import_picocolors5.default.bold("Why:"), output.why);
|
|
71644
|
-
if (output.whereNext.length > 0) {
|
|
71645
|
-
console.log(import_picocolors5.default.bold("Next steps:"));
|
|
71646
|
-
for (const step of output.whereNext) {
|
|
71647
|
-
console.log(` \u2022 ${step}`);
|
|
71648
|
-
}
|
|
71649
|
-
}
|
|
71650
|
-
if (output.citations.length > 0) {
|
|
71651
|
-
console.log(import_picocolors5.default.dim(`
|
|
71652
|
-
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 {
|
|
71653
73185
|
}
|
|
71654
|
-
|
|
73186
|
+
renderStoredAIJudgment(stored);
|
|
71655
73187
|
}
|
|
71656
73188
|
}
|
|
71657
73189
|
} finally {
|
|
@@ -71787,9 +73319,15 @@ async function runBlastRadius(query, opts) {
|
|
|
71787
73319
|
try {
|
|
71788
73320
|
const r = await analyzeBlastRadius(query, { code, db }, opts.depth ?? 3);
|
|
71789
73321
|
if (!r) {
|
|
71790
|
-
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"`));
|
|
71791
73324
|
return 1;
|
|
71792
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
|
+
}
|
|
71793
73331
|
console.log(opts.json ? blastRadiusToJSON(r) : renderBlastRadius(r));
|
|
71794
73332
|
} finally {
|
|
71795
73333
|
await sql2.end();
|
|
@@ -71862,6 +73400,26 @@ async function runSearch(query, opts) {
|
|
|
71862
73400
|
|
|
71863
73401
|
// ../../packages/cli/src/commands/investigations.ts
|
|
71864
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
|
|
71865
73423
|
async function runInvestigations(opts) {
|
|
71866
73424
|
const config = await loadConfig(opts.config);
|
|
71867
73425
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
@@ -71871,7 +73429,7 @@ async function runInvestigations(opts) {
|
|
|
71871
73429
|
console.log('No investigations yet. Run: horus investigate "<hint>"');
|
|
71872
73430
|
} else {
|
|
71873
73431
|
for (const row of rows) {
|
|
71874
|
-
const ts = row.createdAt
|
|
73432
|
+
const ts = formatDateTime(row.createdAt);
|
|
71875
73433
|
const title = (row.title ?? "").length > 60 ? (row.title ?? "").slice(0, 57) + "..." : row.title ?? "";
|
|
71876
73434
|
console.log(`${row.id} ${ts} ${title}`);
|
|
71877
73435
|
}
|
|
@@ -71906,6 +73464,30 @@ async function runReplay(id, opts) {
|
|
|
71906
73464
|
const fmt = opts.format ?? "text";
|
|
71907
73465
|
const out = fmt === "json" ? reportToJSON(report) : fmt === "markdown" || fmt === "md" ? reportToMarkdown(report) : renderReport2(report);
|
|
71908
73466
|
console.log(out);
|
|
73467
|
+
if (opts.ai && fmt !== "json") {
|
|
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."));
|
|
73471
|
+
} else {
|
|
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]}`));
|
|
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);
|
|
73488
|
+
}
|
|
73489
|
+
}
|
|
73490
|
+
}
|
|
71909
73491
|
} finally {
|
|
71910
73492
|
await sql2.end();
|
|
71911
73493
|
}
|
|
@@ -71963,7 +73545,79 @@ async function runPostmortem(id, opts) {
|
|
|
71963
73545
|
}
|
|
71964
73546
|
report = migrateReport(row.report);
|
|
71965
73547
|
}
|
|
71966
|
-
|
|
73548
|
+
let content = generatePostmortem(report);
|
|
73549
|
+
if (opts.aiSummary) {
|
|
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})_
|
|
73554
|
+
|
|
73555
|
+
`;
|
|
73556
|
+
content += `**What happened:** ${storedJudgment.what}
|
|
73557
|
+
|
|
73558
|
+
`;
|
|
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})_
|
|
73566
|
+
`;
|
|
73567
|
+
}
|
|
73568
|
+
if (storedJudgment.whereNext.length > 0) {
|
|
73569
|
+
content += "\n**Next steps:**\n";
|
|
73570
|
+
for (const step of storedJudgment.whereNext) {
|
|
73571
|
+
content += `- ${step}
|
|
73572
|
+
`;
|
|
73573
|
+
}
|
|
73574
|
+
}
|
|
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) {
|
|
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 += `
|
|
73614
|
+
**Cited evidence:** ${output.citations.map((c) => c.evidenceId).join(", ")}
|
|
73615
|
+
`;
|
|
73616
|
+
}
|
|
73617
|
+
report.aiJudgment = narrativeOutputToStoredJudgment(output, "anthropic");
|
|
73618
|
+
}
|
|
73619
|
+
}
|
|
73620
|
+
}
|
|
71967
73621
|
if (opts.output) {
|
|
71968
73622
|
const outputPath = (0, import_node_path7.resolve)(opts.output);
|
|
71969
73623
|
if ((0, import_node_fs6.existsSync)(outputPath) && !opts.force) {
|
|
@@ -72033,7 +73687,7 @@ async function runScores(opts) {
|
|
|
72033
73687
|
}
|
|
72034
73688
|
for (const s of scored) {
|
|
72035
73689
|
console.log(
|
|
72036
|
-
" " + String(s.score).padStart(3) + "/100 " + (s.createdAt
|
|
73690
|
+
" " + String(s.score).padStart(3) + "/100 " + formatDateTime(s.createdAt) + " " + s.id + " " + (s.title ?? "")
|
|
72037
73691
|
);
|
|
72038
73692
|
}
|
|
72039
73693
|
const avg = Math.round(
|
|
@@ -72065,6 +73719,18 @@ async function runAsk(id, directive, opts) {
|
|
|
72065
73719
|
const report = migrateReport(row.report);
|
|
72066
73720
|
const v = refineInvestigation(report, directive);
|
|
72067
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
|
+
}
|
|
72068
73734
|
} catch (err) {
|
|
72069
73735
|
const code = typeof err === "object" && err !== null && "code" in err ? String(err.code) : void 0;
|
|
72070
73736
|
if (code === "22P02") {
|
|
@@ -72209,6 +73875,7 @@ async function runLogs(service, opts) {
|
|
|
72209
73875
|
return 1;
|
|
72210
73876
|
}
|
|
72211
73877
|
const resolvedService = service ?? renv.connectors.elasticsearch?.serviceName;
|
|
73878
|
+
const indexPattern = renv.connectors.elasticsearch?.indexPattern ?? "*";
|
|
72212
73879
|
try {
|
|
72213
73880
|
const compat = await logs.checkCompatibility({
|
|
72214
73881
|
requiresService: resolvedService !== void 0,
|
|
@@ -72234,7 +73901,8 @@ async function runLogs(service, opts) {
|
|
|
72234
73901
|
} catch {
|
|
72235
73902
|
console.warn(import_picocolors20.default.dim("[warn] Elasticsearch compatibility check unavailable \u2014 proceeding."));
|
|
72236
73903
|
}
|
|
72237
|
-
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", " ");
|
|
72238
73906
|
if (opts.raw === true) {
|
|
72239
73907
|
const records = await logs.searchLogs({
|
|
72240
73908
|
service: resolvedService,
|
|
@@ -72256,22 +73924,42 @@ async function runLogs(service, opts) {
|
|
|
72256
73924
|
}
|
|
72257
73925
|
return 0;
|
|
72258
73926
|
}
|
|
72259
|
-
|
|
73927
|
+
let analysis = await logs.analyzeErrors({
|
|
72260
73928
|
service: resolvedService,
|
|
72261
73929
|
from,
|
|
72262
73930
|
text: opts.grep
|
|
72263
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
|
+
}
|
|
72264
73942
|
console.log(
|
|
72265
73943
|
import_picocolors20.default.bold(`Error analysis`) + import_picocolors20.default.dim(
|
|
72266
|
-
` \u2014 ${renv.project}/${renv.env}` + (
|
|
73944
|
+
` \u2014 ${renv.project}/${renv.env}` + (scopeService ? ` \xB7 service ${scopeService}` : "") + (opts.grep ? ` \xB7 grep "${opts.grep}"` : "")
|
|
72267
73945
|
)
|
|
72268
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
|
+
}
|
|
72269
73951
|
console.log(
|
|
72270
73952
|
` ${analysis.totalErrors} error(s) \xB7 ${analysis.signatures.length} signature(s) \xB7 ${import_picocolors20.default.yellow(String(analysis.newSignatures.length))} new`
|
|
72271
73953
|
);
|
|
72272
73954
|
console.log("");
|
|
72273
73955
|
if (analysis.signatures.length === 0) {
|
|
72274
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).`));
|
|
72275
73963
|
return 0;
|
|
72276
73964
|
}
|
|
72277
73965
|
for (const s of analysis.signatures) {
|
|
@@ -72396,18 +74084,29 @@ async function runMetrics(hint, opts) {
|
|
|
72396
74084
|
const flagged = findings2.filter((f) => f.anomaly !== "none");
|
|
72397
74085
|
const ok = findings2.filter((f) => f.anomaly === "none");
|
|
72398
74086
|
if (findings2.length === 0) {
|
|
72399
|
-
|
|
72400
|
-
|
|
72401
|
-
|
|
72402
|
-
)
|
|
72403
|
-
|
|
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
|
+
}
|
|
72404
74101
|
return 0;
|
|
72405
74102
|
}
|
|
72406
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(", ")}]`) : "";
|
|
72407
74106
|
console.log(
|
|
72408
74107
|
import_picocolors21.default.bold(
|
|
72409
74108
|
`Grafana metrics${hintSuffix} \u2014 ${findings2.length} series across panels`
|
|
72410
|
-
)
|
|
74109
|
+
) + matchSuffix
|
|
72411
74110
|
);
|
|
72412
74111
|
console.log(
|
|
72413
74112
|
import_picocolors21.default.dim(
|
|
@@ -72474,8 +74173,9 @@ async function runState(opts) {
|
|
|
72474
74173
|
const analysis = await mongo.analyzeState(
|
|
72475
74174
|
staleHours !== void 0 ? { staleHours } : {}
|
|
72476
74175
|
);
|
|
74176
|
+
const discoveryNote = analysis.autoDiscovered ? import_picocolors22.default.dim(` (${analysis.collections.length} collections, auto-discovered)`) : "";
|
|
72477
74177
|
console.log(
|
|
72478
|
-
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
|
|
72479
74179
|
);
|
|
72480
74180
|
console.log("");
|
|
72481
74181
|
let signals = 0;
|
|
@@ -72493,9 +74193,23 @@ async function runState(opts) {
|
|
|
72493
74193
|
console.log(flags.length > 0 ? `${head} ${flags.join(" ")}` : import_picocolors22.default.dim(head));
|
|
72494
74194
|
}
|
|
72495
74195
|
console.log("");
|
|
72496
|
-
|
|
72497
|
-
|
|
72498
|
-
|
|
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
|
+
}
|
|
72499
74213
|
return 0;
|
|
72500
74214
|
} finally {
|
|
72501
74215
|
await mongo.close();
|
|
@@ -72517,7 +74231,7 @@ async function runInit(opts) {
|
|
|
72517
74231
|
const name = opts.name ?? (0, import_node_path8.basename)(root);
|
|
72518
74232
|
const envName = opts.env ?? "production";
|
|
72519
74233
|
const repo = { name, path: root };
|
|
72520
|
-
if (opts.
|
|
74234
|
+
if (opts.source) repo["source"] = { hostUrl: opts.source };
|
|
72521
74235
|
const file = {
|
|
72522
74236
|
version: 1,
|
|
72523
74237
|
project: {
|
|
@@ -72531,9 +74245,9 @@ async function runInit(opts) {
|
|
|
72531
74245
|
console.log(`${import_picocolors23.default.green("\u2713")} Initialized Horus project ${import_picocolors23.default.bold(name)}`);
|
|
72532
74246
|
console.log(import_picocolors23.default.dim(` config: ${configPath}`));
|
|
72533
74247
|
console.log(import_picocolors23.default.dim(` registered: horus investigate --name ${name} "<hint>"`));
|
|
72534
|
-
if (!opts.
|
|
74248
|
+
if (!opts.source) {
|
|
72535
74249
|
console.log(
|
|
72536
|
-
import_picocolors23.default.dim(" no source-intelligence host set \u2014 run `horus index` to analyze + host, or pass --
|
|
74250
|
+
import_picocolors23.default.dim(" no source-intelligence host set \u2014 run `horus index` to analyze + host, or pass --source <url>")
|
|
72537
74251
|
);
|
|
72538
74252
|
}
|
|
72539
74253
|
console.log(
|
|
@@ -72588,8 +74302,7 @@ async function runSetup(opts) {
|
|
|
72588
74302
|
write(
|
|
72589
74303
|
import_picocolors25.default.dim(
|
|
72590
74304
|
` install it (Python 3.11+ required):
|
|
72591
|
-
|
|
72592
|
-
or: pip install axoniq==${PINNED_SOURCE_VERSION}
|
|
74305
|
+
pip install horus-source
|
|
72593
74306
|
ensure ~/.local/bin is on your PATH`
|
|
72594
74307
|
)
|
|
72595
74308
|
);
|
|
@@ -72601,8 +74314,7 @@ async function runSetup(opts) {
|
|
|
72601
74314
|
write(
|
|
72602
74315
|
import_picocolors25.default.dim(
|
|
72603
74316
|
` update it:
|
|
72604
|
-
|
|
72605
|
-
or: pip install axoniq==${PINNED_SOURCE_VERSION}`
|
|
74317
|
+
pip install --upgrade horus-source`
|
|
72606
74318
|
)
|
|
72607
74319
|
);
|
|
72608
74320
|
} else {
|
|
@@ -72706,12 +74418,192 @@ async function runSetup(opts) {
|
|
|
72706
74418
|
init_cjs_shims();
|
|
72707
74419
|
var import_node_readline = require("readline");
|
|
72708
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");
|
|
72709
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
|
|
72710
74602
|
var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
|
|
72711
74603
|
async function runConnect(type, opts) {
|
|
72712
74604
|
if (!SUPPORTED.includes(type)) {
|
|
72713
74605
|
console.error(
|
|
72714
|
-
|
|
74606
|
+
import_picocolors27.default.red(`Unknown connector type: ${type}`) + import_picocolors27.default.dim(`
|
|
72715
74607
|
supported: ${SUPPORTED.join(", ")}`)
|
|
72716
74608
|
);
|
|
72717
74609
|
return 1;
|
|
@@ -72742,18 +74634,20 @@ async function runConnect(type, opts) {
|
|
|
72742
74634
|
if (!probeResult.ok) {
|
|
72743
74635
|
console.error(
|
|
72744
74636
|
`
|
|
72745
|
-
${
|
|
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.")
|
|
72746
74638
|
);
|
|
72747
74639
|
return 1;
|
|
72748
74640
|
}
|
|
72749
|
-
console.log(
|
|
72750
|
-
|
|
74641
|
+
console.log(
|
|
74642
|
+
`
|
|
74643
|
+
${import_picocolors27.default.green("\u2713")} ${connectorType} reachable ${import_picocolors27.default.dim(`(${probeResult.detail})`)}`
|
|
74644
|
+
);
|
|
72751
74645
|
}
|
|
72752
74646
|
const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0;
|
|
72753
74647
|
if (hasLiteralCredentials) {
|
|
72754
74648
|
if (isGitTracked(configPath, root)) {
|
|
72755
74649
|
console.error(
|
|
72756
|
-
|
|
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(
|
|
72757
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."
|
|
72758
74652
|
)
|
|
72759
74653
|
);
|
|
@@ -72762,29 +74656,52 @@ ${import_picocolors26.default.green("\u2713")} ${connectorType} reachable ${impo
|
|
|
72762
74656
|
ensureCredentialGitignore(root);
|
|
72763
74657
|
}
|
|
72764
74658
|
patchLocalConnector(configPath, connectorType, patch, filled.env);
|
|
72765
|
-
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
|
+
);
|
|
72766
74662
|
printSummary(connectorType, filled);
|
|
72767
|
-
console.log(
|
|
74663
|
+
console.log(import_picocolors27.default.dim(`
|
|
72768
74664
|
run: horus investigate "<hint>"`));
|
|
72769
74665
|
return 0;
|
|
72770
74666
|
} catch (err) {
|
|
72771
|
-
|
|
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));
|
|
72772
74672
|
return 1;
|
|
72773
74673
|
}
|
|
72774
74674
|
}
|
|
72775
74675
|
async function fillInteractive(type, opts) {
|
|
72776
74676
|
const needsInteraction = missingRequired(type, opts);
|
|
72777
74677
|
if (!needsInteraction) return opts;
|
|
72778
|
-
console.log(
|
|
72779
|
-
|
|
72780
|
-
`)
|
|
74678
|
+
console.log(
|
|
74679
|
+
`
|
|
74680
|
+
${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.default.dim("(press Enter to skip optional fields)")}
|
|
74681
|
+
`
|
|
74682
|
+
);
|
|
72781
74683
|
const filled = { ...opts };
|
|
72782
74684
|
switch (type) {
|
|
72783
74685
|
case "elasticsearch":
|
|
72784
74686
|
filled.url = filled.url ?? await ask("URL", "https://elastic.example.com");
|
|
72785
74687
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
72786
74688
|
filled.password = filled.password ?? (await askPassword("Password") || void 0);
|
|
72787
|
-
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
|
+
}
|
|
72788
74705
|
filled.service = filled.service ?? (await ask("Service name", "") || void 0);
|
|
72789
74706
|
break;
|
|
72790
74707
|
case "mongodb":
|
|
@@ -72796,18 +74713,45 @@ ${import_picocolors26.default.bold(`Connect ${type}`)} ${import_picocolors26.def
|
|
|
72796
74713
|
filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
|
|
72797
74714
|
filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
|
|
72798
74715
|
filled.password = filled.password ?? (await askPassword("Password") || void 0);
|
|
72799
|
-
|
|
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
|
+
}
|
|
72800
74732
|
break;
|
|
72801
|
-
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
|
+
);
|
|
72802
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
|
+
}
|
|
72803
74746
|
break;
|
|
74747
|
+
}
|
|
72804
74748
|
}
|
|
72805
74749
|
return filled;
|
|
72806
74750
|
}
|
|
72807
74751
|
function missingRequired(type, opts) {
|
|
72808
74752
|
switch (type) {
|
|
72809
74753
|
case "elasticsearch":
|
|
72810
|
-
return !opts.url || !opts.indexPattern;
|
|
74754
|
+
return !opts.url || !opts.indexPattern && !opts.indexPatterns?.length;
|
|
72811
74755
|
case "mongodb":
|
|
72812
74756
|
return !opts.url || !opts.database;
|
|
72813
74757
|
case "grafana":
|
|
@@ -72818,10 +74762,14 @@ function missingRequired(type, opts) {
|
|
|
72818
74762
|
}
|
|
72819
74763
|
function ask(label, placeholder = "", required = true) {
|
|
72820
74764
|
return new Promise((resolve8) => {
|
|
72821
|
-
const hint = placeholder ?
|
|
72822
|
-
const suffix = required ? "" :
|
|
74765
|
+
const hint = placeholder ? import_picocolors27.default.dim(` (${placeholder})`) : "";
|
|
74766
|
+
const suffix = required ? "" : import_picocolors27.default.dim(" [optional]");
|
|
72823
74767
|
process.stdout.write(` ${label}${suffix}${hint}: `);
|
|
72824
|
-
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
|
+
});
|
|
72825
74773
|
rl.once("line", (line2) => {
|
|
72826
74774
|
rl.close();
|
|
72827
74775
|
resolve8(line2.trim() || (required ? placeholder : ""));
|
|
@@ -72832,7 +74780,7 @@ function askPassword(label) {
|
|
|
72832
74780
|
return new Promise((resolve8) => {
|
|
72833
74781
|
const stdin = process.stdin;
|
|
72834
74782
|
if (typeof stdin.setRawMode === "function") {
|
|
72835
|
-
process.stdout.write(` ${label}${
|
|
74783
|
+
process.stdout.write(` ${label}${import_picocolors27.default.dim(" [optional]")}: `);
|
|
72836
74784
|
stdin.setRawMode(true);
|
|
72837
74785
|
stdin.resume();
|
|
72838
74786
|
let value = "";
|
|
@@ -72863,8 +74811,15 @@ function askPassword(label) {
|
|
|
72863
74811
|
});
|
|
72864
74812
|
}
|
|
72865
74813
|
function buildEsPatch(opts) {
|
|
72866
|
-
if (!opts.indexPattern
|
|
72867
|
-
|
|
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
|
+
}
|
|
72868
74823
|
if (opts.url) patch["url"] = opts.url;
|
|
72869
74824
|
if (opts.username) patch["username"] = opts.username;
|
|
72870
74825
|
if (opts.password) patch["password"] = opts.password;
|
|
@@ -72885,7 +74840,11 @@ function buildGrafanaPatch(opts) {
|
|
|
72885
74840
|
if (opts.url) patch["url"] = opts.url;
|
|
72886
74841
|
if (opts.username) patch["username"] = opts.username;
|
|
72887
74842
|
if (opts.password) patch["password"] = opts.password;
|
|
72888
|
-
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
|
+
}
|
|
72889
74848
|
return patch;
|
|
72890
74849
|
}
|
|
72891
74850
|
function buildRedisPatch(opts) {
|
|
@@ -72904,7 +74863,10 @@ async function probe(type, opts) {
|
|
|
72904
74863
|
password: opts.password
|
|
72905
74864
|
});
|
|
72906
74865
|
const controller = new AbortController();
|
|
72907
|
-
const timer2 = setTimeout(
|
|
74866
|
+
const timer2 = setTimeout(
|
|
74867
|
+
() => controller.abort(new Error("timed out after 8s")),
|
|
74868
|
+
8e3
|
|
74869
|
+
);
|
|
72908
74870
|
try {
|
|
72909
74871
|
return await client.health(controller.signal);
|
|
72910
74872
|
} catch (err) {
|
|
@@ -72931,7 +74893,9 @@ async function probe(type, opts) {
|
|
|
72931
74893
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
72932
74894
|
const headers = { "Content-Type": "application/json" };
|
|
72933
74895
|
if (opts.username && opts.password) {
|
|
72934
|
-
const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString(
|
|
74896
|
+
const encoded = Buffer.from(`${opts.username}:${opts.password}`).toString(
|
|
74897
|
+
"base64"
|
|
74898
|
+
);
|
|
72935
74899
|
headers["Authorization"] = `Basic ${encoded}`;
|
|
72936
74900
|
}
|
|
72937
74901
|
const res = await fetch(`${opts.url.replace(/\/$/, "")}/api/health`, {
|
|
@@ -72975,15 +74939,24 @@ function tcpProbe(host, port, timeoutMs = 3e3) {
|
|
|
72975
74939
|
}
|
|
72976
74940
|
function printSummary(type, opts) {
|
|
72977
74941
|
const lines = [];
|
|
72978
|
-
if (opts.url) lines.push(` url:
|
|
72979
|
-
if (opts.username) lines.push(` username:
|
|
72980
|
-
if (opts.password)
|
|
72981
|
-
|
|
72982
|
-
if (opts.
|
|
72983
|
-
|
|
72984
|
-
if (opts.
|
|
72985
|
-
|
|
72986
|
-
|
|
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")));
|
|
72987
74960
|
}
|
|
72988
74961
|
function isGitTracked(filePath, cwd) {
|
|
72989
74962
|
try {
|
|
@@ -72996,6 +74969,119 @@ function isGitTracked(filePath, cwd) {
|
|
|
72996
74969
|
return false;
|
|
72997
74970
|
}
|
|
72998
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
|
+
}
|
|
72999
75085
|
function redactUrl(raw) {
|
|
73000
75086
|
try {
|
|
73001
75087
|
const u = new URL(raw);
|
|
@@ -73015,11 +75101,14 @@ var import_node_child_process6 = require("child_process");
|
|
|
73015
75101
|
var import_node_fs7 = require("fs");
|
|
73016
75102
|
var import_node_util4 = require("util");
|
|
73017
75103
|
var import_node_path9 = require("path");
|
|
73018
|
-
var
|
|
75104
|
+
var import_picocolors28 = __toESM(require_picocolors(), 1);
|
|
73019
75105
|
var execFileAsync = (0, import_node_util4.promisify)(import_node_child_process6.execFile);
|
|
73020
75106
|
var unlinkAsync = (0, import_node_util4.promisify)(import_node_fs7.unlink);
|
|
73021
75107
|
var SPAWNED_HOST_FILE2 = "spawned-host.json";
|
|
73022
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));
|
|
73023
75112
|
async function runStop(opts) {
|
|
73024
75113
|
try {
|
|
73025
75114
|
if (opts.all) {
|
|
@@ -73029,30 +75118,30 @@ async function runStop(opts) {
|
|
|
73029
75118
|
const root = findRepoRoot(cwd) ?? cwd;
|
|
73030
75119
|
const hostUrl = readSourceHostUrl(root);
|
|
73031
75120
|
if (!hostUrl) {
|
|
73032
|
-
console.log(
|
|
75121
|
+
console.log(import_picocolors28.default.dim("No source-intelligence host found for this repo (.horus/source/host.json absent)."));
|
|
73033
75122
|
return 0;
|
|
73034
75123
|
}
|
|
73035
75124
|
return await stopHost(root, hostUrl);
|
|
73036
75125
|
} catch (err) {
|
|
73037
|
-
console.error(
|
|
75126
|
+
console.error(import_picocolors28.default.red(err.message));
|
|
73038
75127
|
return 1;
|
|
73039
75128
|
}
|
|
73040
75129
|
}
|
|
73041
75130
|
async function stopHost(root, hostUrl) {
|
|
73042
75131
|
const alive = await isHostHealthy(hostUrl);
|
|
73043
75132
|
if (!alive) {
|
|
73044
|
-
console.log(
|
|
75133
|
+
console.log(import_picocolors28.default.dim(`Host ${hostUrl} is already stopped.`));
|
|
73045
75134
|
return 0;
|
|
73046
75135
|
}
|
|
73047
75136
|
const port = extractPort(hostUrl);
|
|
73048
75137
|
if (port === null) {
|
|
73049
|
-
console.error(
|
|
75138
|
+
console.error(import_picocolors28.default.red(`Cannot determine port from host URL: ${hostUrl}`));
|
|
73050
75139
|
return 1;
|
|
73051
75140
|
}
|
|
73052
75141
|
const spawned = readSpawnedHost(root);
|
|
73053
75142
|
if (spawned === null) {
|
|
73054
75143
|
console.error(
|
|
73055
|
-
|
|
75144
|
+
import_picocolors28.default.red(
|
|
73056
75145
|
`No ownership record found (.horus/${SPAWNED_HOST_FILE2} absent). Horus will not stop a host it did not spawn.`
|
|
73057
75146
|
)
|
|
73058
75147
|
);
|
|
@@ -73060,12 +75149,12 @@ async function stopHost(root, hostUrl) {
|
|
|
73060
75149
|
}
|
|
73061
75150
|
const recordError = validateSpawnedRecord(spawned);
|
|
73062
75151
|
if (recordError !== null) {
|
|
73063
|
-
console.error(
|
|
75152
|
+
console.error(import_picocolors28.default.red(`Ownership record is malformed: ${recordError}. Aborting for safety.`));
|
|
73064
75153
|
return 1;
|
|
73065
75154
|
}
|
|
73066
75155
|
if (spawned.port !== port) {
|
|
73067
75156
|
console.error(
|
|
73068
|
-
|
|
75157
|
+
import_picocolors28.default.red(
|
|
73069
75158
|
`Ownership record port (${spawned.port}) does not match host URL port (${port}). Record may be stale.`
|
|
73070
75159
|
)
|
|
73071
75160
|
);
|
|
@@ -73073,7 +75162,7 @@ async function stopHost(root, hostUrl) {
|
|
|
73073
75162
|
}
|
|
73074
75163
|
if (spawned.root !== root) {
|
|
73075
75164
|
console.error(
|
|
73076
|
-
|
|
75165
|
+
import_picocolors28.default.red(
|
|
73077
75166
|
`Ownership record root (${spawned.root}) does not match resolved root (${root}). Record may be stale.`
|
|
73078
75167
|
)
|
|
73079
75168
|
);
|
|
@@ -73081,17 +75170,21 @@ async function stopHost(root, hostUrl) {
|
|
|
73081
75170
|
}
|
|
73082
75171
|
const info = await getProcessInfo(spawned.pid);
|
|
73083
75172
|
if (info === null) {
|
|
73084
|
-
console.
|
|
73085
|
-
|
|
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;
|
|
73086
75179
|
}
|
|
73087
75180
|
const portStr = String(port);
|
|
73088
|
-
const
|
|
73089
|
-
`(?:^|\\s)(?:\\S*/)?
|
|
75181
|
+
const hostPortRe = new RegExp(
|
|
75182
|
+
`(?:^|\\s)(?:\\S*/)?horus-source\\s+host\\s+--port(?:=|\\s+)${portStr}(?=\\s|$)`
|
|
73090
75183
|
);
|
|
73091
|
-
if (!
|
|
75184
|
+
if (!hostPortRe.test(info.args)) {
|
|
73092
75185
|
console.error(
|
|
73093
|
-
|
|
73094
|
-
`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.`
|
|
73095
75188
|
)
|
|
73096
75189
|
);
|
|
73097
75190
|
return 1;
|
|
@@ -73099,30 +75192,51 @@ async function stopHost(root, hostUrl) {
|
|
|
73099
75192
|
const startTs = new Date(spawned.startedAt).getTime();
|
|
73100
75193
|
const recordedAgeS = Math.round((Date.now() - startTs) / 1e3);
|
|
73101
75194
|
if (!Number.isFinite(info.etimeSeconds)) {
|
|
73102
|
-
console.error(
|
|
75195
|
+
console.error(import_picocolors28.default.red(`Could not read elapsed time for pid ${spawned.pid}. Aborting for safety.`));
|
|
73103
75196
|
return 1;
|
|
73104
75197
|
}
|
|
73105
75198
|
if (Math.abs(info.etimeSeconds - recordedAgeS) > START_TIME_TOLERANCE_S) {
|
|
73106
75199
|
console.error(
|
|
73107
|
-
|
|
75200
|
+
import_picocolors28.default.red(
|
|
73108
75201
|
`Pid ${spawned.pid} age mismatch: record says ~${recordedAgeS}s, process reports ${info.etimeSeconds}s elapsed. Possible PID reuse \u2014 aborting for safety.`
|
|
73109
75202
|
)
|
|
73110
75203
|
);
|
|
73111
75204
|
return 1;
|
|
73112
75205
|
}
|
|
75206
|
+
let signaled = false;
|
|
73113
75207
|
try {
|
|
73114
75208
|
process.kill(spawned.pid, "SIGTERM");
|
|
73115
|
-
|
|
73116
|
-
`${import_picocolors27.default.green("\u2713")} Stopped source-intelligence host ` + import_picocolors27.default.dim(`(pid ${spawned.pid}, port ${port})`) + ` for ${root}`
|
|
73117
|
-
);
|
|
75209
|
+
signaled = true;
|
|
73118
75210
|
} catch (err) {
|
|
73119
75211
|
if (err.code === "ESRCH") {
|
|
73120
|
-
console.log(
|
|
75212
|
+
console.log(import_picocolors28.default.dim(`Process pid ${spawned.pid} exited before signal \u2014 already stopped.`));
|
|
73121
75213
|
} else {
|
|
73122
|
-
console.error(
|
|
75214
|
+
console.error(import_picocolors28.default.red(`Failed to signal pid ${spawned.pid}: ${err.message}`));
|
|
73123
75215
|
return 1;
|
|
73124
75216
|
}
|
|
73125
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
|
+
);
|
|
75234
|
+
return 1;
|
|
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
|
+
);
|
|
75239
|
+
}
|
|
73126
75240
|
try {
|
|
73127
75241
|
await unlinkAsync((0, import_node_path9.join)(root, HORUS_DIR, SPAWNED_HOST_FILE2));
|
|
73128
75242
|
} catch {
|
|
@@ -73133,7 +75247,7 @@ async function stopAll() {
|
|
|
73133
75247
|
const registry = readRegistry();
|
|
73134
75248
|
const projects2 = Object.entries(registry.projects);
|
|
73135
75249
|
if (projects2.length === 0) {
|
|
73136
|
-
console.log(
|
|
75250
|
+
console.log(import_picocolors28.default.dim("No registered projects."));
|
|
73137
75251
|
return 0;
|
|
73138
75252
|
}
|
|
73139
75253
|
let stopped = 0;
|
|
@@ -73143,17 +75257,17 @@ async function stopAll() {
|
|
|
73143
75257
|
if (!hostUrl) continue;
|
|
73144
75258
|
const alive = await isHostHealthy(hostUrl);
|
|
73145
75259
|
if (!alive) continue;
|
|
73146
|
-
console.log(` Stopping ${
|
|
75260
|
+
console.log(` Stopping ${import_picocolors28.default.bold(name)} ${import_picocolors28.default.dim(`(${hostUrl})`)}`);
|
|
73147
75261
|
const code = await stopHost(entry2.root, hostUrl);
|
|
73148
75262
|
if (code === 0) stopped++;
|
|
73149
75263
|
else failed++;
|
|
73150
75264
|
}
|
|
73151
75265
|
if (stopped === 0 && failed === 0) {
|
|
73152
|
-
console.log(
|
|
75266
|
+
console.log(import_picocolors28.default.dim("No running source-intelligence hosts found."));
|
|
73153
75267
|
} else {
|
|
73154
75268
|
console.log(
|
|
73155
75269
|
`
|
|
73156
|
-
Stopped ${stopped} host(s)${failed > 0 ?
|
|
75270
|
+
Stopped ${stopped} host(s)${failed > 0 ? import_picocolors28.default.red(`, ${failed} failed`) : ""}.`
|
|
73157
75271
|
);
|
|
73158
75272
|
}
|
|
73159
75273
|
return failed > 0 ? 1 : 0;
|
|
@@ -73205,12 +75319,12 @@ function extractPort(hostUrl) {
|
|
|
73205
75319
|
|
|
73206
75320
|
// ../../packages/cli/src/commands/hosts.ts
|
|
73207
75321
|
init_cjs_shims();
|
|
73208
|
-
var
|
|
75322
|
+
var import_picocolors29 = __toESM(require_picocolors(), 1);
|
|
73209
75323
|
async function runHosts() {
|
|
73210
75324
|
const registry = readRegistry();
|
|
73211
75325
|
const projects2 = Object.entries(registry.projects);
|
|
73212
75326
|
if (projects2.length === 0) {
|
|
73213
|
-
console.log(
|
|
75327
|
+
console.log(import_picocolors29.default.dim("No registered projects. Run `horus index` in a repo first."));
|
|
73214
75328
|
return 0;
|
|
73215
75329
|
}
|
|
73216
75330
|
const rows = [];
|
|
@@ -73227,29 +75341,29 @@ async function runHosts() {
|
|
|
73227
75341
|
});
|
|
73228
75342
|
const anyHost = rows.some((r) => r.hostUrl !== null);
|
|
73229
75343
|
if (!anyHost) {
|
|
73230
|
-
console.log(
|
|
75344
|
+
console.log(import_picocolors29.default.dim("No source-intelligence hosts found. Run `horus index` to start one."));
|
|
73231
75345
|
return 0;
|
|
73232
75346
|
}
|
|
73233
75347
|
console.log("");
|
|
73234
75348
|
for (const row of rows) {
|
|
73235
75349
|
if (row.hostUrl === null) continue;
|
|
73236
|
-
const status = row.healthy ?
|
|
75350
|
+
const status = row.healthy ? import_picocolors29.default.green("\u25CF running") : import_picocolors29.default.red("\u25CF stopped");
|
|
73237
75351
|
const port = extractPort2(row.hostUrl) ?? "?";
|
|
73238
75352
|
console.log(
|
|
73239
|
-
` ${status} ${
|
|
75353
|
+
` ${status} ${import_picocolors29.default.bold(row.name.padEnd(24))} port ${String(port).padEnd(6)} ${import_picocolors29.default.dim(row.root)}`
|
|
73240
75354
|
);
|
|
73241
75355
|
}
|
|
73242
75356
|
console.log("");
|
|
73243
75357
|
const noHost = rows.filter((r) => r.hostUrl === null);
|
|
73244
75358
|
if (noHost.length > 0) {
|
|
73245
75359
|
for (const row of noHost) {
|
|
73246
|
-
console.log(` ${
|
|
75360
|
+
console.log(` ${import_picocolors29.default.dim("\u25CB no host")} ${import_picocolors29.default.dim(row.name)}`);
|
|
73247
75361
|
}
|
|
73248
75362
|
console.log("");
|
|
73249
75363
|
}
|
|
73250
75364
|
const running = rows.filter((r) => r.healthy).length;
|
|
73251
75365
|
console.log(
|
|
73252
|
-
|
|
75366
|
+
import_picocolors29.default.dim(
|
|
73253
75367
|
`${running} running \xB7 horus stop to reap \xB7 horus stop --all to stop everything`
|
|
73254
75368
|
)
|
|
73255
75369
|
);
|
|
@@ -73266,12 +75380,12 @@ function extractPort2(hostUrl) {
|
|
|
73266
75380
|
|
|
73267
75381
|
// ../../packages/cli/src/commands/doctor.ts
|
|
73268
75382
|
init_cjs_shims();
|
|
73269
|
-
var
|
|
75383
|
+
var import_picocolors30 = __toESM(require_picocolors(), 1);
|
|
73270
75384
|
var DEFAULT_DB_URL3 = "postgresql://horus:horus@localhost:5433/horus";
|
|
73271
75385
|
function mark2(status) {
|
|
73272
|
-
if (status === "pass") return
|
|
73273
|
-
if (status === "warn") return
|
|
73274
|
-
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");
|
|
73275
75389
|
}
|
|
73276
75390
|
async function runDoctor(opts) {
|
|
73277
75391
|
const cwd = opts?.cwd ?? process.cwd();
|
|
@@ -73301,7 +75415,7 @@ async function runDoctor(opts) {
|
|
|
73301
75415
|
const project = file.project;
|
|
73302
75416
|
const repos = project["repositories"];
|
|
73303
75417
|
const hasHost = repos?.some(
|
|
73304
|
-
(r) => r["axon"]?.["hostUrl"]
|
|
75418
|
+
(r) => r["source"]?.["hostUrl"] ?? r["axon"]?.["hostUrl"]
|
|
73305
75419
|
);
|
|
73306
75420
|
if (hasHost) {
|
|
73307
75421
|
checks.push({ label: "Source-intelligence host", status: "pass", detail: "configured" });
|
|
@@ -73310,7 +75424,7 @@ async function runDoctor(opts) {
|
|
|
73310
75424
|
label: "Source-intelligence host",
|
|
73311
75425
|
status: "warn",
|
|
73312
75426
|
detail: "not configured",
|
|
73313
|
-
next: "run `horus index` to analyze this repo and start a host, or pass --
|
|
75427
|
+
next: "run `horus index` to analyze this repo and start a host, or pass --source <url> to `horus init`"
|
|
73314
75428
|
});
|
|
73315
75429
|
}
|
|
73316
75430
|
} catch {
|
|
@@ -73498,11 +75612,11 @@ async function runDoctor(opts) {
|
|
|
73498
75612
|
write(JSON.stringify(output, null, 2));
|
|
73499
75613
|
return hasFailure ? 1 : 0;
|
|
73500
75614
|
}
|
|
73501
|
-
write(
|
|
75615
|
+
write(import_picocolors30.default.bold("\nHorus readiness check\n"));
|
|
73502
75616
|
for (const check of checks) {
|
|
73503
|
-
write(` ${mark2(check.status)} ${
|
|
75617
|
+
write(` ${mark2(check.status)} ${import_picocolors30.default.bold(check.label.padEnd(26))} ${import_picocolors30.default.dim(check.detail)}`);
|
|
73504
75618
|
if (check.next) {
|
|
73505
|
-
write(` ${
|
|
75619
|
+
write(` ${import_picocolors30.default.dim("\u2192 " + check.next)}`);
|
|
73506
75620
|
}
|
|
73507
75621
|
}
|
|
73508
75622
|
write("");
|
|
@@ -73511,29 +75625,34 @@ async function runDoctor(opts) {
|
|
|
73511
75625
|
|
|
73512
75626
|
// ../../packages/cli/src/commands/providers-doctor.ts
|
|
73513
75627
|
init_cjs_shims();
|
|
73514
|
-
var
|
|
75628
|
+
var import_node_child_process7 = require("child_process");
|
|
75629
|
+
var import_picocolors31 = __toESM(require_picocolors(), 1);
|
|
73515
75630
|
function statusMark(status) {
|
|
73516
|
-
if (status === "ready") return
|
|
73517
|
-
if (status === "installed") return
|
|
73518
|
-
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");
|
|
73519
75634
|
}
|
|
73520
75635
|
function statusLabel(status) {
|
|
73521
|
-
if (status === "ready") return
|
|
73522
|
-
if (status === "installed") return
|
|
73523
|
-
return
|
|
73524
|
-
}
|
|
73525
|
-
function
|
|
73526
|
-
|
|
73527
|
-
|
|
73528
|
-
status: "unavailable",
|
|
73529
|
-
|
|
73530
|
-
}
|
|
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));
|
|
73531
75650
|
}
|
|
73532
75651
|
async function runProvidersDoctorCommand(opts) {
|
|
73533
75652
|
const registry = opts?.registry ?? DEFAULT_LOCAL_PROVIDER_REGISTRY;
|
|
73534
75653
|
const write = opts?.write ?? ((line2) => console.log(line2));
|
|
73535
|
-
const results = buildProviderResults(registry);
|
|
73536
|
-
write(
|
|
75654
|
+
const results = buildProviderResults(registry, opts?._detect);
|
|
75655
|
+
write(import_picocolors31.default.bold("\nLocal AI providers\n"));
|
|
73537
75656
|
for (const result of results) {
|
|
73538
75657
|
const descriptor = registry.get(result.id);
|
|
73539
75658
|
const name = descriptor?.displayName ?? result.id;
|
|
@@ -73541,12 +75660,24 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
73541
75660
|
` ${statusMark(result.status)} ${result.id.padEnd(8)} ${name.padEnd(22)} ${statusLabel(result.status)}`
|
|
73542
75661
|
);
|
|
73543
75662
|
if (result.status !== "ready" && result.detail) {
|
|
73544
|
-
write(` ${
|
|
75663
|
+
write(` ${import_picocolors31.default.dim("\u2192 " + result.detail)}`);
|
|
73545
75664
|
}
|
|
73546
75665
|
}
|
|
73547
75666
|
write("");
|
|
73548
|
-
write(
|
|
73549
|
-
|
|
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
|
+
}
|
|
73550
75681
|
write("");
|
|
73551
75682
|
return 0;
|
|
73552
75683
|
}
|
|
@@ -73555,7 +75686,7 @@ async function runProvidersDoctorCommand(opts) {
|
|
|
73555
75686
|
init_cjs_shims();
|
|
73556
75687
|
var import_node_fs8 = require("fs");
|
|
73557
75688
|
var import_node_path10 = require("path");
|
|
73558
|
-
var
|
|
75689
|
+
var import_picocolors32 = __toESM(require_picocolors(), 1);
|
|
73559
75690
|
function configTemplate(name, repoPath) {
|
|
73560
75691
|
return `export default {
|
|
73561
75692
|
database: {
|
|
@@ -73566,7 +75697,7 @@ function configTemplate(name, repoPath) {
|
|
|
73566
75697
|
repositories: [{
|
|
73567
75698
|
name: '${name}',
|
|
73568
75699
|
path: '${repoPath}',
|
|
73569
|
-
|
|
75700
|
+
source: { hostUrl: 'http://127.0.0.1:8420' },
|
|
73570
75701
|
}],
|
|
73571
75702
|
environments: [{
|
|
73572
75703
|
name: 'production',
|
|
@@ -73581,39 +75712,66 @@ function configTemplate(name, repoPath) {
|
|
|
73581
75712
|
};
|
|
73582
75713
|
`;
|
|
73583
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
|
+
}
|
|
73584
75730
|
async function runGenerateConfig(opts) {
|
|
73585
75731
|
const log = opts.write ?? ((line2) => console.log(line2));
|
|
73586
75732
|
const cwd = opts.cwd ?? process.cwd();
|
|
73587
|
-
const
|
|
73588
|
-
const
|
|
73589
|
-
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}`;
|
|
73590
75740
|
if ((0, import_node_fs8.existsSync)(outPath) && !opts.force) {
|
|
73591
|
-
log(`${
|
|
73592
|
-
log(
|
|
75741
|
+
log(`${import_picocolors32.default.red("\u2717")} ${outPath} already exists`);
|
|
75742
|
+
log(import_picocolors32.default.dim(" pass --force to overwrite"));
|
|
73593
75743
|
return 1;
|
|
73594
75744
|
}
|
|
73595
75745
|
try {
|
|
73596
75746
|
(0, import_node_fs8.mkdirSync)((0, import_node_path10.dirname)(outPath), { recursive: true });
|
|
73597
75747
|
(0, import_node_fs8.writeFileSync)(outPath, configTemplate(name, repoPath), "utf8");
|
|
73598
75748
|
} catch (err) {
|
|
73599
|
-
log(`${
|
|
75749
|
+
log(`${import_picocolors32.default.red("\u2717")} Could not write ${outPath}: ${err.message}`);
|
|
73600
75750
|
return 1;
|
|
73601
75751
|
}
|
|
73602
|
-
log(`${
|
|
73603
|
-
log(
|
|
73604
|
-
log(
|
|
73605
|
-
|
|
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
|
+
}
|
|
73606
75764
|
return 0;
|
|
73607
75765
|
}
|
|
73608
75766
|
|
|
73609
75767
|
// ../../packages/cli/src/commands/readiness.ts
|
|
73610
75768
|
init_cjs_shims();
|
|
73611
|
-
var
|
|
75769
|
+
var import_picocolors33 = __toESM(require_picocolors(), 1);
|
|
73612
75770
|
var DEFAULT_DB_URL4 = "postgresql://horus:horus@localhost:5433/horus";
|
|
73613
75771
|
function mark3(status) {
|
|
73614
|
-
if (status === "pass") return
|
|
73615
|
-
if (status === "warn") return
|
|
73616
|
-
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");
|
|
73617
75775
|
}
|
|
73618
75776
|
async function runReadiness(opts) {
|
|
73619
75777
|
const cwd = opts?.cwd ?? process.cwd();
|
|
@@ -73676,7 +75834,7 @@ async function runReadiness(opts) {
|
|
|
73676
75834
|
status: "warn",
|
|
73677
75835
|
blocking: false,
|
|
73678
75836
|
detail: "not installed \u2014 source intelligence unavailable",
|
|
73679
|
-
next: `
|
|
75837
|
+
next: `pip install horus-source`
|
|
73680
75838
|
});
|
|
73681
75839
|
} else if (sourceVersion !== PINNED_SOURCE_VERSION) {
|
|
73682
75840
|
checks.push({
|
|
@@ -73684,7 +75842,7 @@ async function runReadiness(opts) {
|
|
|
73684
75842
|
status: "warn",
|
|
73685
75843
|
blocking: false,
|
|
73686
75844
|
detail: `version mismatch (installed: ${sourceVersion}, required: ${PINNED_SOURCE_VERSION})`,
|
|
73687
|
-
next: `
|
|
75845
|
+
next: `pip install horus-source`
|
|
73688
75846
|
});
|
|
73689
75847
|
} else {
|
|
73690
75848
|
checks.push({
|
|
@@ -73768,20 +75926,20 @@ async function runReadiness(opts) {
|
|
|
73768
75926
|
}
|
|
73769
75927
|
const blockingChecks = checks.filter((c) => c.blocking);
|
|
73770
75928
|
const optionalChecks = checks.filter((c) => !c.blocking);
|
|
73771
|
-
write(
|
|
73772
|
-
write(
|
|
75929
|
+
write(import_picocolors33.default.bold("\nHorus release readiness\n"));
|
|
75930
|
+
write(import_picocolors33.default.bold(" Blocking"));
|
|
73773
75931
|
for (const check of blockingChecks) {
|
|
73774
|
-
write(` ${mark3(check.status)} ${
|
|
75932
|
+
write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
|
|
73775
75933
|
if (check.next) {
|
|
73776
|
-
write(` ${
|
|
75934
|
+
write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
|
|
73777
75935
|
}
|
|
73778
75936
|
}
|
|
73779
75937
|
write("");
|
|
73780
|
-
write(
|
|
75938
|
+
write(import_picocolors33.default.bold(" Optional"));
|
|
73781
75939
|
for (const check of optionalChecks) {
|
|
73782
|
-
write(` ${mark3(check.status)} ${
|
|
75940
|
+
write(` ${mark3(check.status)} ${import_picocolors33.default.bold(check.label.padEnd(22))} ${import_picocolors33.default.dim(check.detail)}`);
|
|
73783
75941
|
if (check.next) {
|
|
73784
|
-
write(` ${
|
|
75942
|
+
write(` ${import_picocolors33.default.dim("\u2192 " + check.next)}`);
|
|
73785
75943
|
}
|
|
73786
75944
|
}
|
|
73787
75945
|
write("");
|
|
@@ -73789,21 +75947,21 @@ async function runReadiness(opts) {
|
|
|
73789
75947
|
const optionalWarns = optionalChecks.filter((c) => c.status === "warn").length;
|
|
73790
75948
|
if (blockingFails.length === 0) {
|
|
73791
75949
|
if (optionalWarns === 0) {
|
|
73792
|
-
write(
|
|
75950
|
+
write(import_picocolors33.default.green(" Ready for demo/release."));
|
|
73793
75951
|
} else {
|
|
73794
75952
|
write(
|
|
73795
|
-
|
|
75953
|
+
import_picocolors33.default.yellow(
|
|
73796
75954
|
` Ready for a basic demo. ${optionalWarns} optional item(s) not configured \u2014 investigation evidence will be limited.`
|
|
73797
75955
|
)
|
|
73798
75956
|
);
|
|
73799
75957
|
}
|
|
73800
75958
|
} else {
|
|
73801
75959
|
write(
|
|
73802
|
-
|
|
75960
|
+
import_picocolors33.default.red(
|
|
73803
75961
|
` Not ready. ${blockingFails.length} blocking item(s) must be resolved before demo/release.`
|
|
73804
75962
|
)
|
|
73805
75963
|
);
|
|
73806
|
-
write(
|
|
75964
|
+
write(import_picocolors33.default.dim(" Re-run `horus readiness` after resolving the items above."));
|
|
73807
75965
|
}
|
|
73808
75966
|
write("");
|
|
73809
75967
|
return blockingFails.length > 0 ? 1 : 0;
|
|
@@ -73828,9 +75986,14 @@ Examples:
|
|
|
73828
75986
|
program2.command("setup").description("Verify prerequisites (source-intelligence backend + Postgres) and guide any fixes").option("-c, --config <path>", "path to horus.config.ts").action(async (opts) => {
|
|
73829
75987
|
process.exitCode = await runSetup(opts);
|
|
73830
75988
|
});
|
|
73831
|
-
program2.command("init").description("Create a local .horus/config.json for this repo and register it").option("--name <name>", "project name (default: repo directory name)").option("--env <name>", "environment name (default: production)").option("--
|
|
75989
|
+
program2.command("init").description("Create a local .horus/config.json for this repo and register it").option("--name <name>", "project name (default: repo directory name)").option("--env <name>", "environment name (default: production)").option("--source <url>", "source-intelligence host URL for this repo (e.g. http://127.0.0.1:8420)").addOption(new Option("--axon <url>", "deprecated alias for --source").hideHelp()).option("--path <dir>", "repository root (default: nearest git root, else cwd)").action(
|
|
73832
75990
|
async (opts) => {
|
|
73833
|
-
process.exitCode = await runInit(
|
|
75991
|
+
process.exitCode = await runInit({
|
|
75992
|
+
name: opts.name,
|
|
75993
|
+
env: opts.env,
|
|
75994
|
+
source: opts.source ?? opts.axon,
|
|
75995
|
+
path: opts.path
|
|
75996
|
+
});
|
|
73834
75997
|
}
|
|
73835
75998
|
).addHelpText("after", `
|
|
73836
75999
|
Examples:
|
|
@@ -73849,7 +76012,7 @@ Examples:
|
|
|
73849
76012
|
});
|
|
73850
76013
|
program2.command("connect <type>").description(
|
|
73851
76014
|
"Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
|
|
73852
|
-
).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(
|
|
73853
76016
|
async (type, opts) => {
|
|
73854
76017
|
process.exitCode = await runConnect(type, {
|
|
73855
76018
|
env: opts.env,
|
|
@@ -73898,9 +76061,16 @@ Examples:
|
|
|
73898
76061
|
process.exitCode = await runIndex(opts);
|
|
73899
76062
|
}
|
|
73900
76063
|
);
|
|
73901
|
-
program2.command("queues [name]").description("Show
|
|
73902
|
-
|
|
73903
|
-
|
|
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
|
+
);
|
|
73904
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(
|
|
73905
76075
|
"--service <name>",
|
|
73906
76076
|
"service name to scope runtime logs, e.g. leadcall-api-prod"
|
|
@@ -73995,23 +76165,29 @@ Examples:
|
|
|
73995
76165
|
horus investigations
|
|
73996
76166
|
horus investigations -n 20
|
|
73997
76167
|
`);
|
|
73998
|
-
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").action(async (id, opts) => {
|
|
73999
|
-
process.exitCode = await runReplay(id, { config: opts.config, format: opts.format });
|
|
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 });
|
|
74000
76170
|
}).addHelpText("after", `
|
|
74001
76171
|
Examples:
|
|
74002
76172
|
horus replay <id>
|
|
74003
76173
|
horus replay <id> --format markdown
|
|
74004
76174
|
horus replay <id> --format json
|
|
76175
|
+
horus replay <id> --ai
|
|
76176
|
+
horus replay <id> --ai --ai-model claude-sonnet-4-6
|
|
76177
|
+
horus replay <id> --ai --refresh-ai
|
|
74005
76178
|
|
|
74006
76179
|
(Use 'horus investigations' to list saved investigation ids.)
|
|
74007
76180
|
`);
|
|
74008
|
-
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").action(async (id, opts) => {
|
|
74009
|
-
process.exitCode = await runPostmortem(id, { config: opts.config, output: opts.output, force: opts.force });
|
|
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 });
|
|
74010
76183
|
}).addHelpText("after", `
|
|
74011
76184
|
Examples:
|
|
74012
76185
|
horus postmortem <id>
|
|
74013
76186
|
horus postmortem <id> --output ./postmortem.md
|
|
74014
76187
|
horus postmortem <id> --output ./postmortem.md --force
|
|
76188
|
+
horus postmortem <id> --ai-summary
|
|
76189
|
+
horus postmortem <id> --output ./postmortem.md --ai-summary --ai-model claude-sonnet-4-6
|
|
76190
|
+
horus postmortem <id> --ai-summary --refresh-ai
|
|
74015
76191
|
|
|
74016
76192
|
(Use 'horus investigations' to list saved investigation ids.)
|
|
74017
76193
|
`);
|