@merittdev/horus 0.1.10 → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +788 -73
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -49547,7 +49547,7 @@ var require_Redis = __commonJS({
|
|
|
49547
49547
|
var lodash_1 = require_lodash();
|
|
49548
49548
|
var Deque = require_denque();
|
|
49549
49549
|
var debug = (0, utils_1.Debug)("redis");
|
|
49550
|
-
var
|
|
49550
|
+
var Redis3 = class _Redis extends Commander_1.default {
|
|
49551
49551
|
constructor(arg1, arg2, arg3) {
|
|
49552
49552
|
super();
|
|
49553
49553
|
this.status = "wait";
|
|
@@ -50212,12 +50212,12 @@ var require_Redis = __commonJS({
|
|
|
50212
50212
|
}).catch(lodash_1.noop);
|
|
50213
50213
|
}
|
|
50214
50214
|
};
|
|
50215
|
-
|
|
50216
|
-
|
|
50217
|
-
|
|
50218
|
-
(0, applyMixin_1.default)(
|
|
50219
|
-
(0, transaction_1.addTransactionSupport)(
|
|
50220
|
-
exports2.default =
|
|
50215
|
+
Redis3.Cluster = cluster_1.default;
|
|
50216
|
+
Redis3.Command = Command_1.default;
|
|
50217
|
+
Redis3.defaultOptions = RedisOptions_1.DEFAULT_REDIS_OPTIONS;
|
|
50218
|
+
(0, applyMixin_1.default)(Redis3, events_1.EventEmitter);
|
|
50219
|
+
(0, transaction_1.addTransactionSupport)(Redis3.prototype);
|
|
50220
|
+
exports2.default = Redis3;
|
|
50221
50221
|
}
|
|
50222
50222
|
});
|
|
50223
50223
|
|
|
@@ -50314,7 +50314,7 @@ init_cjs_shims();
|
|
|
50314
50314
|
|
|
50315
50315
|
// ../../packages/core/src/version.ts
|
|
50316
50316
|
init_cjs_shims();
|
|
50317
|
-
var HORUS_VERSION = true ? "0.1.
|
|
50317
|
+
var HORUS_VERSION = true ? "0.1.12" : "dev";
|
|
50318
50318
|
var PINNED_AXON_VERSION = "1.0.7";
|
|
50319
50319
|
var PINNED_SOURCE_VERSION = PINNED_AXON_VERSION;
|
|
50320
50320
|
|
|
@@ -54541,6 +54541,37 @@ var repositorySchema = external_exports.object({
|
|
|
54541
54541
|
*/
|
|
54542
54542
|
axon: external_exports.object({ hostUrl: external_exports.string().url() }).optional()
|
|
54543
54543
|
});
|
|
54544
|
+
var REDIS_ROLES = [
|
|
54545
|
+
"cache",
|
|
54546
|
+
"state",
|
|
54547
|
+
"locks",
|
|
54548
|
+
"rate-limit",
|
|
54549
|
+
"session",
|
|
54550
|
+
"dedupe",
|
|
54551
|
+
"bullmq",
|
|
54552
|
+
"queues"
|
|
54553
|
+
];
|
|
54554
|
+
var redisRoleSchema = external_exports.enum(REDIS_ROLES);
|
|
54555
|
+
var REDIS_QUEUE_ROLES = ["bullmq", "queues"];
|
|
54556
|
+
function redisDbFromUrl(url) {
|
|
54557
|
+
if (!url) return 0;
|
|
54558
|
+
try {
|
|
54559
|
+
const seg = new URL(url).pathname.replace(/^\//, "");
|
|
54560
|
+
const n = Number.parseInt(seg, 10);
|
|
54561
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
54562
|
+
} catch {
|
|
54563
|
+
return 0;
|
|
54564
|
+
}
|
|
54565
|
+
}
|
|
54566
|
+
function redisUrlForDb(url, db) {
|
|
54567
|
+
try {
|
|
54568
|
+
const u = new URL(url);
|
|
54569
|
+
u.pathname = `/${db}`;
|
|
54570
|
+
return u.toString();
|
|
54571
|
+
} catch {
|
|
54572
|
+
return url;
|
|
54573
|
+
}
|
|
54574
|
+
}
|
|
54544
54575
|
var connectorsSchema = external_exports.object({
|
|
54545
54576
|
elasticsearch: external_exports.object({
|
|
54546
54577
|
indexPattern: external_exports.string().optional(),
|
|
@@ -54619,7 +54650,33 @@ var connectorsSchema = external_exports.object({
|
|
|
54619
54650
|
/** Direct URL value (takes priority over urlEnv). */
|
|
54620
54651
|
url: external_exports.string().optional(),
|
|
54621
54652
|
/** Name of the env var holding the Redis URL. Defaults to "REDIS_URL". */
|
|
54622
|
-
urlEnv: external_exports.string().optional()
|
|
54653
|
+
urlEnv: external_exports.string().optional(),
|
|
54654
|
+
/**
|
|
54655
|
+
* Logical databases on the same Redis server (HOR-201). Redis is a general
|
|
54656
|
+
* runtime-evidence connector — a single server commonly holds BullMQ queues in
|
|
54657
|
+
* one DB and cache/locks/rate-limit/session keys in others. Declaring them here
|
|
54658
|
+
* (with roles) lets `queues --live` target the queue DB and lets investigation
|
|
54659
|
+
* collectors target cache/state/locks DBs. When omitted, the DB index in `url`
|
|
54660
|
+
* (default 0) is treated as a single configured database (backward compatible).
|
|
54661
|
+
*/
|
|
54662
|
+
databases: external_exports.array(
|
|
54663
|
+
external_exports.object({
|
|
54664
|
+
/** Logical DB index on the server (0–15 by default Redis config). */
|
|
54665
|
+
db: external_exports.number().int().min(0).max(15),
|
|
54666
|
+
/** Friendly name for display (e.g. "cache", "queues"). */
|
|
54667
|
+
name: external_exports.string().optional(),
|
|
54668
|
+
/** What this DB holds — drives which collectors/commands use it. */
|
|
54669
|
+
roles: external_exports.array(redisRoleSchema).default([]),
|
|
54670
|
+
/** BullMQ settings when this DB has the bullmq/queues role. */
|
|
54671
|
+
bullmq: external_exports.object({ prefix: external_exports.string().default("bull") }).optional(),
|
|
54672
|
+
/** Sampled-scan settings for cache/state/locks DBs. */
|
|
54673
|
+
scan: external_exports.object({
|
|
54674
|
+
enabled: external_exports.boolean().default(true),
|
|
54675
|
+
sampleLimit: external_exports.number().int().positive().default(500),
|
|
54676
|
+
patterns: external_exports.array(external_exports.string()).default([])
|
|
54677
|
+
}).optional()
|
|
54678
|
+
})
|
|
54679
|
+
).optional()
|
|
54623
54680
|
}).optional()
|
|
54624
54681
|
}).default({});
|
|
54625
54682
|
var environmentSchema = external_exports.object({
|
|
@@ -54762,8 +54819,28 @@ function resolveEnvironment(config, opts) {
|
|
|
54762
54819
|
}
|
|
54763
54820
|
if (c.redis !== void 0) {
|
|
54764
54821
|
const r = c.redis;
|
|
54822
|
+
const url = r.url ?? process.env[r.urlEnv ?? "REDIS_URL"];
|
|
54823
|
+
let databases;
|
|
54824
|
+
if (r.databases !== void 0 && r.databases.length > 0) {
|
|
54825
|
+
databases = r.databases.map((d) => ({
|
|
54826
|
+
db: d.db,
|
|
54827
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
54828
|
+
roles: d.roles,
|
|
54829
|
+
bullmqPrefix: d.bullmq?.prefix ?? "bull",
|
|
54830
|
+
...d.scan !== void 0 ? {
|
|
54831
|
+
scan: {
|
|
54832
|
+
enabled: d.scan.enabled,
|
|
54833
|
+
sampleLimit: d.scan.sampleLimit,
|
|
54834
|
+
patterns: d.scan.patterns
|
|
54835
|
+
}
|
|
54836
|
+
} : {}
|
|
54837
|
+
}));
|
|
54838
|
+
} else {
|
|
54839
|
+
databases = [{ db: redisDbFromUrl(url), roles: [], bullmqPrefix: "bull" }];
|
|
54840
|
+
}
|
|
54765
54841
|
resolved.redis = {
|
|
54766
|
-
url
|
|
54842
|
+
...url !== void 0 ? { url } : {},
|
|
54843
|
+
databases
|
|
54767
54844
|
};
|
|
54768
54845
|
}
|
|
54769
54846
|
return {
|
|
@@ -57339,6 +57416,174 @@ var BullMQRuntimeProvider = class {
|
|
|
57339
57416
|
}
|
|
57340
57417
|
};
|
|
57341
57418
|
|
|
57419
|
+
// ../../packages/connectors/src/redis/state-provider.ts
|
|
57420
|
+
init_cjs_shims();
|
|
57421
|
+
|
|
57422
|
+
// ../../packages/connectors/src/redis/scan-client.ts
|
|
57423
|
+
init_cjs_shims();
|
|
57424
|
+
var import_ioredis2 = __toESM(require_built3(), 1);
|
|
57425
|
+
var SAMPLE_DEFAULT = 500;
|
|
57426
|
+
var SCAN_BATCH = 200;
|
|
57427
|
+
var RedisScanClient = class {
|
|
57428
|
+
redis;
|
|
57429
|
+
db;
|
|
57430
|
+
constructor(opts) {
|
|
57431
|
+
this.db = opts.db ?? 0;
|
|
57432
|
+
const redisOpts = {
|
|
57433
|
+
db: this.db,
|
|
57434
|
+
lazyConnect: true,
|
|
57435
|
+
enableReadyCheck: false,
|
|
57436
|
+
maxRetriesPerRequest: 1,
|
|
57437
|
+
connectTimeout: 5e3
|
|
57438
|
+
};
|
|
57439
|
+
this.redis = new import_ioredis2.Redis(opts.url, redisOpts);
|
|
57440
|
+
this.redis.on("error", () => {
|
|
57441
|
+
});
|
|
57442
|
+
}
|
|
57443
|
+
/** Ping the server (also exercises AUTH). */
|
|
57444
|
+
async health() {
|
|
57445
|
+
try {
|
|
57446
|
+
await this.redis.ping();
|
|
57447
|
+
return { ok: true, detail: `Redis reachable (db ${this.db})` };
|
|
57448
|
+
} catch (err) {
|
|
57449
|
+
return { ok: false, detail: err.message };
|
|
57450
|
+
}
|
|
57451
|
+
}
|
|
57452
|
+
/** Total number of keys in this DB (DBSIZE). */
|
|
57453
|
+
async dbSize() {
|
|
57454
|
+
return this.redis.dbsize();
|
|
57455
|
+
}
|
|
57456
|
+
/**
|
|
57457
|
+
* Sample up to `limit` keys and group them by their leading segment (before the
|
|
57458
|
+
* first ':'), returning the most common prefixes. Used to characterise a cache/
|
|
57459
|
+
* state DB without reading values. Optional `patterns` restrict the scan.
|
|
57460
|
+
*/
|
|
57461
|
+
async samplePrefixes(limit = SAMPLE_DEFAULT, patterns) {
|
|
57462
|
+
const counts = /* @__PURE__ */ new Map();
|
|
57463
|
+
let sampled = 0;
|
|
57464
|
+
const scanPatterns = patterns && patterns.length > 0 ? patterns : ["*"];
|
|
57465
|
+
for (const pattern of scanPatterns) {
|
|
57466
|
+
let cursor = "0";
|
|
57467
|
+
do {
|
|
57468
|
+
const [next, batch] = await this.redis.scan(
|
|
57469
|
+
cursor,
|
|
57470
|
+
"MATCH",
|
|
57471
|
+
pattern,
|
|
57472
|
+
"COUNT",
|
|
57473
|
+
SCAN_BATCH
|
|
57474
|
+
);
|
|
57475
|
+
cursor = next;
|
|
57476
|
+
for (const key of batch) {
|
|
57477
|
+
const prefix = key.includes(":") ? key.slice(0, key.indexOf(":")) : key;
|
|
57478
|
+
counts.set(prefix, (counts.get(prefix) ?? 0) + 1);
|
|
57479
|
+
if (++sampled >= limit) break;
|
|
57480
|
+
}
|
|
57481
|
+
} while (cursor !== "0" && sampled < limit);
|
|
57482
|
+
if (sampled >= limit) break;
|
|
57483
|
+
}
|
|
57484
|
+
return [...counts.entries()].map(([prefix, count]) => ({ prefix, count })).sort((a, b2) => b2.count - a.count);
|
|
57485
|
+
}
|
|
57486
|
+
/**
|
|
57487
|
+
* Detect BullMQ queue names in this DB by scanning `{prefix}:*:meta`. `:meta` exists
|
|
57488
|
+
* for every instantiated queue (even idle ones), so this finds all queues, not just
|
|
57489
|
+
* those with pending jobs. Returns at most `limit` names.
|
|
57490
|
+
*/
|
|
57491
|
+
async detectBullmqQueues(prefix = "bull", limit = 200) {
|
|
57492
|
+
const pattern = `${prefix}:*:meta`;
|
|
57493
|
+
const suffixLen = ":meta".length;
|
|
57494
|
+
const prefixLen = prefix.length + 1;
|
|
57495
|
+
const names = [];
|
|
57496
|
+
let cursor = "0";
|
|
57497
|
+
do {
|
|
57498
|
+
const [next, batch] = await this.redis.scan(cursor, "MATCH", pattern, "COUNT", SCAN_BATCH);
|
|
57499
|
+
cursor = next;
|
|
57500
|
+
for (const key of batch) {
|
|
57501
|
+
names.push(key.slice(prefixLen, key.length - suffixLen));
|
|
57502
|
+
if (names.length >= limit) return names;
|
|
57503
|
+
}
|
|
57504
|
+
} while (cursor !== "0" && names.length < limit);
|
|
57505
|
+
return names;
|
|
57506
|
+
}
|
|
57507
|
+
async close() {
|
|
57508
|
+
this.redis.disconnect();
|
|
57509
|
+
}
|
|
57510
|
+
};
|
|
57511
|
+
|
|
57512
|
+
// ../../packages/connectors/src/redis/state-provider.ts
|
|
57513
|
+
var LOCK_RE = /lock/i;
|
|
57514
|
+
var RATELIMIT_RE = /rate.?limit|throttle/i;
|
|
57515
|
+
var RedisStateRuntimeProvider = class {
|
|
57516
|
+
constructor(dbs, mkClient = (d) => new RedisScanClient({ url: d.url, db: d.db })) {
|
|
57517
|
+
this.dbs = dbs;
|
|
57518
|
+
this.mkClient = mkClient;
|
|
57519
|
+
}
|
|
57520
|
+
dbs;
|
|
57521
|
+
mkClient;
|
|
57522
|
+
async analyzeRedisState() {
|
|
57523
|
+
const collectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
57524
|
+
const databases = [];
|
|
57525
|
+
const signals = [];
|
|
57526
|
+
for (const d of this.dbs) {
|
|
57527
|
+
const client = this.mkClient(d);
|
|
57528
|
+
try {
|
|
57529
|
+
const health = await client.health();
|
|
57530
|
+
if (!health.ok) continue;
|
|
57531
|
+
const keyCount = await client.dbSize();
|
|
57532
|
+
const prefixes = await client.samplePrefixes(d.scan?.sampleLimit ?? 500, d.scan?.patterns);
|
|
57533
|
+
databases.push({
|
|
57534
|
+
db: d.db,
|
|
57535
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57536
|
+
roles: d.roles,
|
|
57537
|
+
keyCount,
|
|
57538
|
+
prefixes
|
|
57539
|
+
});
|
|
57540
|
+
if (keyCount === 0) continue;
|
|
57541
|
+
const roleLabel = d.roles.length > 0 ? d.roles.join("/") : "state";
|
|
57542
|
+
const topPrefixes = prefixes.slice(0, 10);
|
|
57543
|
+
const topText = topPrefixes.map((p) => p.prefix).join(" ");
|
|
57544
|
+
signals.push({
|
|
57545
|
+
db: d.db,
|
|
57546
|
+
title: `Redis DB ${d.db} (${roleLabel}): ${keyCount} key(s), top prefixes ${topPrefixes.slice(0, 4).map((p) => `${p.prefix}:*`).join(", ")}`,
|
|
57547
|
+
payload: { db: d.db, roles: d.roles, keyCount, topPrefixes },
|
|
57548
|
+
relevance: 0.4,
|
|
57549
|
+
matchText: `${roleLabel} ${topText}`.toLowerCase()
|
|
57550
|
+
});
|
|
57551
|
+
const lockPrefixes = prefixes.filter((p) => LOCK_RE.test(p.prefix));
|
|
57552
|
+
if (d.roles.includes("locks") || lockPrefixes.length > 0) {
|
|
57553
|
+
const lockKeys = lockPrefixes.reduce((n, p) => n + p.count, 0);
|
|
57554
|
+
if (lockKeys > 0) {
|
|
57555
|
+
signals.push({
|
|
57556
|
+
db: d.db,
|
|
57557
|
+
title: `Redis DB ${d.db}: ${lockKeys} lock key(s) present (${lockPrefixes.map((p) => `${p.prefix}:*`).join(", ")})`,
|
|
57558
|
+
payload: { db: d.db, lockKeys, lockPrefixes },
|
|
57559
|
+
relevance: 0.6,
|
|
57560
|
+
matchText: `lock locks ${lockPrefixes.map((p) => p.prefix).join(" ")}`.toLowerCase()
|
|
57561
|
+
});
|
|
57562
|
+
}
|
|
57563
|
+
}
|
|
57564
|
+
const rlPrefixes = prefixes.filter((p) => RATELIMIT_RE.test(p.prefix));
|
|
57565
|
+
if (d.roles.includes("rate-limit") || rlPrefixes.length > 0) {
|
|
57566
|
+
const rlKeys = rlPrefixes.reduce((n, p) => n + p.count, 0);
|
|
57567
|
+
if (rlKeys > 0) {
|
|
57568
|
+
signals.push({
|
|
57569
|
+
db: d.db,
|
|
57570
|
+
title: `Redis DB ${d.db}: ${rlKeys} rate-limit key(s) present`,
|
|
57571
|
+
payload: { db: d.db, rateLimitKeys: rlKeys, prefixes: rlPrefixes },
|
|
57572
|
+
relevance: 0.6,
|
|
57573
|
+
matchText: `rate limit throttle ${rlPrefixes.map((p) => p.prefix).join(" ")}`.toLowerCase()
|
|
57574
|
+
});
|
|
57575
|
+
}
|
|
57576
|
+
}
|
|
57577
|
+
} finally {
|
|
57578
|
+
await client.close();
|
|
57579
|
+
}
|
|
57580
|
+
}
|
|
57581
|
+
return { collectedAt, databases, signals };
|
|
57582
|
+
}
|
|
57583
|
+
async close() {
|
|
57584
|
+
}
|
|
57585
|
+
};
|
|
57586
|
+
|
|
57342
57587
|
// ../../packages/connectors/src/factory.ts
|
|
57343
57588
|
function codeForEnv(renv) {
|
|
57344
57589
|
const repo = renv.repositories[0];
|
|
@@ -57380,10 +57625,43 @@ function metricsForEnv(renv) {
|
|
|
57380
57625
|
{ defaultStep: 60, dashboardUids: g.dashboards }
|
|
57381
57626
|
);
|
|
57382
57627
|
}
|
|
57628
|
+
function dbHasRole(db, roles) {
|
|
57629
|
+
return db.roles.some((r) => roles.includes(r));
|
|
57630
|
+
}
|
|
57631
|
+
function queueDatabaseForEnv(renv) {
|
|
57632
|
+
const dbs = renv.connectors.redis?.databases;
|
|
57633
|
+
if (!dbs || dbs.length === 0) return null;
|
|
57634
|
+
return dbs.find((d) => dbHasRole(d, REDIS_QUEUE_ROLES)) ?? (dbs.length === 1 ? dbs[0] : null);
|
|
57635
|
+
}
|
|
57636
|
+
var STATE_ROLES = ["cache", "state", "locks", "rate-limit", "session", "dedupe"];
|
|
57637
|
+
function stateDatabasesForEnv(renv) {
|
|
57638
|
+
const dbs = renv.connectors.redis?.databases ?? [];
|
|
57639
|
+
return dbs.filter((d) => dbHasRole(d, STATE_ROLES));
|
|
57640
|
+
}
|
|
57641
|
+
function redisStateForEnv(renv) {
|
|
57642
|
+
const r = renv.connectors.redis;
|
|
57643
|
+
if (!r?.url) return null;
|
|
57644
|
+
const stateDbs = stateDatabasesForEnv(renv);
|
|
57645
|
+
if (stateDbs.length === 0) return null;
|
|
57646
|
+
const baseUrl = r.url;
|
|
57647
|
+
return new RedisStateRuntimeProvider(
|
|
57648
|
+
stateDbs.map((d) => ({
|
|
57649
|
+
db: d.db,
|
|
57650
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57651
|
+
roles: d.roles,
|
|
57652
|
+
url: redisUrlForDb(baseUrl, d.db),
|
|
57653
|
+
...d.scan !== void 0 ? { scan: { sampleLimit: d.scan.sampleLimit, patterns: d.scan.patterns } } : {}
|
|
57654
|
+
}))
|
|
57655
|
+
);
|
|
57656
|
+
}
|
|
57383
57657
|
function queueForEnv(renv) {
|
|
57384
57658
|
const r = renv.connectors.redis;
|
|
57385
57659
|
if (!r?.url) return null;
|
|
57386
|
-
|
|
57660
|
+
const queueDb = queueDatabaseForEnv(renv);
|
|
57661
|
+
if (!queueDb) return null;
|
|
57662
|
+
return new BullMQRuntimeProvider(
|
|
57663
|
+
new BullMQRedisClient({ url: redisUrlForDb(r.url, queueDb.db), prefix: queueDb.bullmqPrefix })
|
|
57664
|
+
);
|
|
57387
57665
|
}
|
|
57388
57666
|
function mongoForEnv(renv) {
|
|
57389
57667
|
const m = renv.connectors.mongodb;
|
|
@@ -57553,6 +57831,130 @@ init_cjs_shims();
|
|
|
57553
57831
|
// ../../packages/connectors/src/bullmq/index.ts
|
|
57554
57832
|
init_cjs_shims();
|
|
57555
57833
|
|
|
57834
|
+
// ../../packages/connectors/src/redis/index.ts
|
|
57835
|
+
init_cjs_shims();
|
|
57836
|
+
|
|
57837
|
+
// ../../packages/connectors/src/redis/discovery.ts
|
|
57838
|
+
init_cjs_shims();
|
|
57839
|
+
function inferStateRoles(prefixes) {
|
|
57840
|
+
const roles = /* @__PURE__ */ new Set(["cache", "state"]);
|
|
57841
|
+
for (const { prefix } of prefixes) {
|
|
57842
|
+
const p = prefix.toLowerCase();
|
|
57843
|
+
if (/lock/.test(p)) roles.add("locks");
|
|
57844
|
+
if (/rate.?limit|throttle/.test(p)) roles.add("rate-limit");
|
|
57845
|
+
if (/session|sess/.test(p)) roles.add("session");
|
|
57846
|
+
if (/dedup/.test(p)) roles.add("dedupe");
|
|
57847
|
+
}
|
|
57848
|
+
return [...roles];
|
|
57849
|
+
}
|
|
57850
|
+
async function probeRedisDatabases(baseUrl, opts = {}) {
|
|
57851
|
+
const dbRange = opts.dbRange ?? Array.from({ length: 16 }, (_, i) => i);
|
|
57852
|
+
const sampleLimit = opts.sampleLimit ?? 500;
|
|
57853
|
+
const bullmqPrefix = opts.bullmqPrefix ?? "bull";
|
|
57854
|
+
const probes = [];
|
|
57855
|
+
for (const db of dbRange) {
|
|
57856
|
+
const client = new RedisScanClient({ url: baseUrl, db });
|
|
57857
|
+
try {
|
|
57858
|
+
const health = await client.health();
|
|
57859
|
+
if (!health.ok) {
|
|
57860
|
+
probes.push({
|
|
57861
|
+
db,
|
|
57862
|
+
reachable: false,
|
|
57863
|
+
detail: health.detail,
|
|
57864
|
+
keyCount: 0,
|
|
57865
|
+
prefixes: [],
|
|
57866
|
+
bullmqQueues: [],
|
|
57867
|
+
suggestedRoles: []
|
|
57868
|
+
});
|
|
57869
|
+
break;
|
|
57870
|
+
}
|
|
57871
|
+
const keyCount = await client.dbSize();
|
|
57872
|
+
if (keyCount === 0) {
|
|
57873
|
+
probes.push({ db, reachable: true, keyCount: 0, prefixes: [], bullmqQueues: [], suggestedRoles: [] });
|
|
57874
|
+
continue;
|
|
57875
|
+
}
|
|
57876
|
+
const bullmqQueues = await client.detectBullmqQueues(bullmqPrefix);
|
|
57877
|
+
if (bullmqQueues.length > 0) {
|
|
57878
|
+
probes.push({
|
|
57879
|
+
db,
|
|
57880
|
+
reachable: true,
|
|
57881
|
+
keyCount,
|
|
57882
|
+
prefixes: [],
|
|
57883
|
+
bullmqQueues,
|
|
57884
|
+
bullmqPrefix,
|
|
57885
|
+
suggestedRoles: ["bullmq", "queues"]
|
|
57886
|
+
});
|
|
57887
|
+
} else {
|
|
57888
|
+
const prefixes = await client.samplePrefixes(sampleLimit);
|
|
57889
|
+
probes.push({
|
|
57890
|
+
db,
|
|
57891
|
+
reachable: true,
|
|
57892
|
+
keyCount,
|
|
57893
|
+
prefixes,
|
|
57894
|
+
bullmqQueues: [],
|
|
57895
|
+
suggestedRoles: inferStateRoles(prefixes)
|
|
57896
|
+
});
|
|
57897
|
+
}
|
|
57898
|
+
} finally {
|
|
57899
|
+
await client.close();
|
|
57900
|
+
}
|
|
57901
|
+
}
|
|
57902
|
+
return probes;
|
|
57903
|
+
}
|
|
57904
|
+
|
|
57905
|
+
// ../../packages/connectors/src/redis/status.ts
|
|
57906
|
+
init_cjs_shims();
|
|
57907
|
+
async function redisServerStatus(renv) {
|
|
57908
|
+
const r = renv.connectors.redis;
|
|
57909
|
+
if (!r?.url) return null;
|
|
57910
|
+
const url = r.url;
|
|
57911
|
+
let reachable = false;
|
|
57912
|
+
let authFailed = false;
|
|
57913
|
+
const databases = [];
|
|
57914
|
+
for (const d of r.databases) {
|
|
57915
|
+
const client = new RedisScanClient({ url: redisUrlForDb(url, d.db), db: d.db });
|
|
57916
|
+
try {
|
|
57917
|
+
const health = await client.health();
|
|
57918
|
+
const isQueue = d.roles.some((role) => REDIS_QUEUE_ROLES.includes(role));
|
|
57919
|
+
if (!health.ok) {
|
|
57920
|
+
if (/WRONGPASS|NOAUTH|invalid password/i.test(health.detail)) authFailed = true;
|
|
57921
|
+
databases.push({
|
|
57922
|
+
db: d.db,
|
|
57923
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57924
|
+
roles: d.roles,
|
|
57925
|
+
reachable: false,
|
|
57926
|
+
detail: health.detail
|
|
57927
|
+
});
|
|
57928
|
+
continue;
|
|
57929
|
+
}
|
|
57930
|
+
reachable = true;
|
|
57931
|
+
if (isQueue) {
|
|
57932
|
+
const queues = await client.detectBullmqQueues(d.bullmqPrefix);
|
|
57933
|
+
databases.push({
|
|
57934
|
+
db: d.db,
|
|
57935
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57936
|
+
roles: d.roles,
|
|
57937
|
+
reachable: true,
|
|
57938
|
+
queueCount: queues.length,
|
|
57939
|
+
bullmqPrefix: d.bullmqPrefix
|
|
57940
|
+
});
|
|
57941
|
+
} else {
|
|
57942
|
+
const keyCount = await client.dbSize();
|
|
57943
|
+
databases.push({
|
|
57944
|
+
db: d.db,
|
|
57945
|
+
...d.name !== void 0 ? { name: d.name } : {},
|
|
57946
|
+
roles: d.roles,
|
|
57947
|
+
reachable: true,
|
|
57948
|
+
keyCount
|
|
57949
|
+
});
|
|
57950
|
+
}
|
|
57951
|
+
} finally {
|
|
57952
|
+
await client.close();
|
|
57953
|
+
}
|
|
57954
|
+
}
|
|
57955
|
+
return { reachable, authFailed, databases };
|
|
57956
|
+
}
|
|
57957
|
+
|
|
57556
57958
|
// ../../packages/db/src/index.ts
|
|
57557
57959
|
init_cjs_shims();
|
|
57558
57960
|
|
|
@@ -66802,37 +67204,31 @@ async function checkEnv(renv, deps) {
|
|
|
66802
67204
|
}
|
|
66803
67205
|
const redisCfg = renv.connectors.redis;
|
|
66804
67206
|
if (redisCfg?.url) {
|
|
66805
|
-
const
|
|
66806
|
-
const
|
|
66807
|
-
if (
|
|
66808
|
-
|
|
66809
|
-
|
|
66810
|
-
|
|
66811
|
-
|
|
66812
|
-
|
|
66813
|
-
|
|
66814
|
-
|
|
66815
|
-
|
|
66816
|
-
|
|
66817
|
-
|
|
66818
|
-
|
|
66819
|
-
|
|
67207
|
+
const server = redisServerLabel(redisCfg.url);
|
|
67208
|
+
const status = await (deps?.redisStatus ?? redisServerStatus)(renv);
|
|
67209
|
+
if (!status) {
|
|
67210
|
+
console.log(` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`configured \xB7 ${server}`)}`);
|
|
67211
|
+
} else if (!status.reachable) {
|
|
67212
|
+
const state = status.authFailed ? "auth failed" : "unreachable";
|
|
67213
|
+
console.log(` ${mark(false)} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`${state} \xB7 ${server}`)}`);
|
|
67214
|
+
allOk = false;
|
|
67215
|
+
} else {
|
|
67216
|
+
console.log(` ${mark(true)} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`reachable \xB7 ${server}`)}`);
|
|
67217
|
+
for (const d of status.databases) {
|
|
67218
|
+
const roleLabel = d.roles.length > 0 ? d.roles.join("/") : "unrolled";
|
|
67219
|
+
const name = d.name ? ` ${d.name}` : "";
|
|
67220
|
+
let detail;
|
|
67221
|
+
if (!d.reachable) {
|
|
67222
|
+
detail = `${/WRONGPASS|NOAUTH/i.test(d.detail ?? "") ? "auth failed" : "unreachable"}`;
|
|
67223
|
+
} else if (d.queueCount !== void 0) {
|
|
67224
|
+
detail = `${d.queueCount} queue(s), prefix ${d.bullmqPrefix}`;
|
|
66820
67225
|
} else {
|
|
66821
|
-
|
|
66822
|
-
const state = authFailed ? "auth failed" : "unreachable";
|
|
66823
|
-
console.log(
|
|
66824
|
-
` ${mark(false)} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`${state} \xB7 ${safeUrl} \xB7 ${h.detail}`)}`
|
|
66825
|
-
);
|
|
66826
|
-
allOk = false;
|
|
67226
|
+
detail = `${d.keyCount ?? 0} key(s)`;
|
|
66827
67227
|
}
|
|
66828
|
-
|
|
66829
|
-
|
|
66830
|
-
|
|
67228
|
+
console.log(
|
|
67229
|
+
` ${mark(d.reachable)} ${import_picocolors.default.bold(`DB ${d.db}`)}${name} ${import_picocolors.default.dim(`${roleLabel} \xB7 ${detail}`)}`
|
|
67230
|
+
);
|
|
66831
67231
|
}
|
|
66832
|
-
} else {
|
|
66833
|
-
console.log(
|
|
66834
|
-
` ${mark("pending")} ${import_picocolors.default.bold("Redis")} ${import_picocolors.default.dim(`configured \xB7 ${safeUrl}`)}`
|
|
66835
|
-
);
|
|
66836
67232
|
}
|
|
66837
67233
|
} else {
|
|
66838
67234
|
console.log(
|
|
@@ -66856,6 +67252,14 @@ function redactRedisUrl(raw) {
|
|
|
66856
67252
|
return raw.replace(/\/\/:?[^@]*@/, "//:***@");
|
|
66857
67253
|
}
|
|
66858
67254
|
}
|
|
67255
|
+
function redisServerLabel(raw) {
|
|
67256
|
+
try {
|
|
67257
|
+
const u = new URL(raw);
|
|
67258
|
+
return `${u.hostname}:${u.port || "6379"}`;
|
|
67259
|
+
} catch {
|
|
67260
|
+
return redactRedisUrl(raw);
|
|
67261
|
+
}
|
|
67262
|
+
}
|
|
66859
67263
|
async function runStatus(configPath, opts) {
|
|
66860
67264
|
console.log(import_picocolors.default.bold(`
|
|
66861
67265
|
Horus ${HORUS_VERSION}`));
|
|
@@ -66905,7 +67309,7 @@ Horus ${HORUS_VERSION}`));
|
|
|
66905
67309
|
console.error(import_picocolors.default.red(err.message));
|
|
66906
67310
|
return 1;
|
|
66907
67311
|
}
|
|
66908
|
-
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory,
|
|
67312
|
+
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory, redisStatus: opts?._redisStatus });
|
|
66909
67313
|
return ok ? 0 : 1;
|
|
66910
67314
|
}
|
|
66911
67315
|
let allHealthy = true;
|
|
@@ -66918,7 +67322,7 @@ Horus ${HORUS_VERSION}`));
|
|
|
66918
67322
|
allHealthy = false;
|
|
66919
67323
|
continue;
|
|
66920
67324
|
}
|
|
66921
|
-
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory,
|
|
67325
|
+
const ok = await checkEnv(renv, { mongoFactory: opts?._mongoFactory, redisStatus: opts?._redisStatus });
|
|
66922
67326
|
if (!ok) allHealthy = false;
|
|
66923
67327
|
}
|
|
66924
67328
|
return checks.some((c) => c.ok === false && c.fatal) ? 1 : 0;
|
|
@@ -69783,6 +70187,22 @@ async function investigate(input, deps) {
|
|
|
69783
70187
|
stateAnalysis = null;
|
|
69784
70188
|
}
|
|
69785
70189
|
}
|
|
70190
|
+
let redisStateAnalysis = null;
|
|
70191
|
+
const redisStateEvIds = [];
|
|
70192
|
+
if (deps.redisState) {
|
|
70193
|
+
try {
|
|
70194
|
+
redisStateAnalysis = await deps.redisState.analyzeRedisState();
|
|
70195
|
+
const redisTerms = [...new Set(tokenize(hint))];
|
|
70196
|
+
for (const s of redisStateAnalysis.signals) {
|
|
70197
|
+
const matched = redisTerms.some((t) => t.length > 2 && s.matchText.includes(t));
|
|
70198
|
+
const relevance = matched ? Math.min(1, s.relevance + 0.3) : s.relevance;
|
|
70199
|
+
const ev = mkEv("redis-key", s.title, s.payload, {}, redisStateAnalysis.collectedAt, relevance);
|
|
70200
|
+
redisStateEvIds.push(ev.id);
|
|
70201
|
+
}
|
|
70202
|
+
} catch {
|
|
70203
|
+
redisStateAnalysis = null;
|
|
70204
|
+
}
|
|
70205
|
+
}
|
|
69786
70206
|
let queueRuntimeState = null;
|
|
69787
70207
|
const queueRuntimeEvIds = [];
|
|
69788
70208
|
const queueRuntimeEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
@@ -69817,8 +70237,10 @@ async function investigate(input, deps) {
|
|
|
69817
70237
|
const latencyMetricEvIds = [];
|
|
69818
70238
|
const queueMetricEvIds = [];
|
|
69819
70239
|
const queueMetricEvIdsByQueue = /* @__PURE__ */ new Map();
|
|
70240
|
+
const nominalMetricEvIds = [];
|
|
69820
70241
|
let metricsCollected = false;
|
|
69821
70242
|
let metricsFailureReason;
|
|
70243
|
+
let metricSeriesChecked = 0;
|
|
69822
70244
|
if (deps.metrics) {
|
|
69823
70245
|
const ac = new AbortController();
|
|
69824
70246
|
let metricsTimerId;
|
|
@@ -69878,6 +70300,19 @@ async function investigate(input, deps) {
|
|
|
69878
70300
|
}
|
|
69879
70301
|
}
|
|
69880
70302
|
}
|
|
70303
|
+
metricSeriesChecked = mFindings.length;
|
|
70304
|
+
if (metricEvIds.length === 0 && mFindings.length > 0) {
|
|
70305
|
+
const panels = [...new Set(mFindings.map((f) => f.panelTitle))];
|
|
70306
|
+
const ev = mkEv(
|
|
70307
|
+
"metric",
|
|
70308
|
+
`Metrics checked \u2014 ${mFindings.length} series across ${panels.length} panel(s), no anomalies in window`,
|
|
70309
|
+
{ seriesChecked: mFindings.length, panelCount: panels.length, panels: panels.slice(0, 10), anomalies: 0, stance: "neutral" },
|
|
70310
|
+
{},
|
|
70311
|
+
collectedAt2,
|
|
70312
|
+
0.2
|
|
70313
|
+
);
|
|
70314
|
+
nominalMetricEvIds.push(ev.id);
|
|
70315
|
+
}
|
|
69881
70316
|
metricsCollected = true;
|
|
69882
70317
|
} catch (metricsErr) {
|
|
69883
70318
|
metricsFailureReason = metricsErr?.message?.slice(0, 120) ?? "unknown error";
|
|
@@ -70016,6 +70451,15 @@ async function investigate(input, deps) {
|
|
|
70016
70451
|
evidenceIds: stateEvIds
|
|
70017
70452
|
});
|
|
70018
70453
|
}
|
|
70454
|
+
if (redisStateAnalysis !== null && redisStateEvIds.length > 0) {
|
|
70455
|
+
const dbCount = redisStateAnalysis.databases.filter((d) => d.keyCount > 0).length;
|
|
70456
|
+
findings2.push({
|
|
70457
|
+
kind: "observation",
|
|
70458
|
+
title: `Redis runtime state: ${redisStateEvIds.length} signal(s) across ${dbCount} DB(s)`,
|
|
70459
|
+
confidence: 0.5,
|
|
70460
|
+
evidenceIds: redisStateEvIds
|
|
70461
|
+
});
|
|
70462
|
+
}
|
|
70019
70463
|
if (queueRuntimeState !== null && queueRuntimeEvIds.length > 0) {
|
|
70020
70464
|
const starved = queueRuntimeState.queues.filter((q) => q.waiting >= 10 && q.active === 0);
|
|
70021
70465
|
const starvedNames = new Set(starved.map((q) => q.queueName));
|
|
@@ -70066,6 +70510,13 @@ async function investigate(input, deps) {
|
|
|
70066
70510
|
confidence: 0.7,
|
|
70067
70511
|
evidenceIds: metricEvIds
|
|
70068
70512
|
});
|
|
70513
|
+
} else if (nominalMetricEvIds.length > 0) {
|
|
70514
|
+
findings2.push({
|
|
70515
|
+
kind: "observation",
|
|
70516
|
+
title: `Metrics nominal \u2014 ${metricSeriesChecked} series checked, no anomalies in window`,
|
|
70517
|
+
confidence: 0.5,
|
|
70518
|
+
evidenceIds: nominalMetricEvIds
|
|
70519
|
+
});
|
|
70069
70520
|
}
|
|
70070
70521
|
const causeInputs = [];
|
|
70071
70522
|
const blastRadius = impact.affected;
|
|
@@ -70162,7 +70613,7 @@ async function investigate(input, deps) {
|
|
|
70162
70613
|
const providerReliability = {
|
|
70163
70614
|
code: 0.8,
|
|
70164
70615
|
...deps.logs != null ? { logs: 0.7 } : {},
|
|
70165
|
-
...deps.mongo != null ? { state: 0.85 } : {},
|
|
70616
|
+
...deps.mongo != null || deps.redisState != null ? { state: 0.85 } : {},
|
|
70166
70617
|
...deps.queue != null ? { queue: 0.9 } : {},
|
|
70167
70618
|
...deps.metrics != null ? { metrics: 0.75 } : {}
|
|
70168
70619
|
};
|
|
@@ -72122,6 +72573,167 @@ function refinedToJSON(r, v) {
|
|
|
72122
72573
|
);
|
|
72123
72574
|
}
|
|
72124
72575
|
|
|
72576
|
+
// ../../packages/engine/src/qa.ts
|
|
72577
|
+
init_cjs_shims();
|
|
72578
|
+
var CONTRADICTS_RE = /\b(contradict|contradicts|argues?\s+against|evidence\s+against|rule\s+out|disprove|refute|weaken)\b/i;
|
|
72579
|
+
var MISSING_RE = /\b(missing|absent|gaps?|don'?t\s+have|do\s+not\s+have|lack(?:ing)?|what\s+else)\b/i;
|
|
72580
|
+
var CONFIDENCE_RE = /\b(confidence|confident|certainty|sure|why\s+(?:is\s+)?(?:it\s+)?not\s+higher)\b/i;
|
|
72581
|
+
function detectQuestion(text2) {
|
|
72582
|
+
const t = text2.toLowerCase();
|
|
72583
|
+
if (CONFIDENCE_RE.test(t)) return "confidence";
|
|
72584
|
+
if (CONTRADICTS_RE.test(t)) return "contradicts";
|
|
72585
|
+
if (MISSING_RE.test(t)) return "missing-evidence";
|
|
72586
|
+
return null;
|
|
72587
|
+
}
|
|
72588
|
+
function categoriesForTopic(text2) {
|
|
72589
|
+
const t = text2.toLowerCase();
|
|
72590
|
+
for (const [topic, entry2] of Object.entries(TOPIC_MAP)) {
|
|
72591
|
+
if (topic === t.trim()) return { topic, categories: entry2.categories };
|
|
72592
|
+
if (entry2.keywords.some((kw) => new RegExp(`\\b${kw}\\b`).test(t))) {
|
|
72593
|
+
return { topic, categories: entry2.categories };
|
|
72594
|
+
}
|
|
72595
|
+
}
|
|
72596
|
+
return null;
|
|
72597
|
+
}
|
|
72598
|
+
function evidenceById(report) {
|
|
72599
|
+
return new Map(report.evidence.map((e) => [e.id, e]));
|
|
72600
|
+
}
|
|
72601
|
+
function answerContradicts(report, question) {
|
|
72602
|
+
const matched = categoriesForTopic(question);
|
|
72603
|
+
const byId = evidenceById(report);
|
|
72604
|
+
const hyps = matched ? report.hypotheses.filter((h) => matched.categories.includes(h.category)) : [];
|
|
72605
|
+
if (matched && hyps.length === 0) {
|
|
72606
|
+
const evaluated = [...new Set(report.hypotheses.map((h) => h.category))];
|
|
72607
|
+
return {
|
|
72608
|
+
question,
|
|
72609
|
+
kind: "contradicts",
|
|
72610
|
+
headline: `"${matched.topic}" was not among the evaluated hypotheses \u2014 no evidence supports it.`,
|
|
72611
|
+
details: evaluated.length > 0 ? [`Hypotheses evaluated: ${evaluated.join(", ")}.`] : ["No hypotheses were formed for this investigation."],
|
|
72612
|
+
evidence: []
|
|
72613
|
+
};
|
|
72614
|
+
}
|
|
72615
|
+
if (hyps.length === 0) {
|
|
72616
|
+
const ids3 = [...new Set(report.hypotheses.flatMap((h) => h.contradictingEvidenceIds))];
|
|
72617
|
+
const ev2 = ids3.map((id) => byId.get(id)).filter((e) => e !== void 0);
|
|
72618
|
+
return {
|
|
72619
|
+
question,
|
|
72620
|
+
kind: "contradicts",
|
|
72621
|
+
headline: ev2.length > 0 ? `${ev2.length} item(s) contradict the leading hypotheses.` : "No contradicting evidence was recorded for any hypothesis.",
|
|
72622
|
+
details: ev2.length > 0 ? [] : ["Evidence either supports or is neutral to the hypotheses."],
|
|
72623
|
+
evidence: ev2
|
|
72624
|
+
};
|
|
72625
|
+
}
|
|
72626
|
+
const ids2 = [...new Set(hyps.flatMap((h) => h.contradictingEvidenceIds))];
|
|
72627
|
+
const ev = ids2.map((id) => byId.get(id)).filter((e) => e !== void 0);
|
|
72628
|
+
const verdicts = [...new Set(hyps.map((h) => h.verdict))].join(", ");
|
|
72629
|
+
if (ev.length === 0) {
|
|
72630
|
+
return {
|
|
72631
|
+
question,
|
|
72632
|
+
kind: "contradicts",
|
|
72633
|
+
headline: `No evidence contradicts "${matched?.topic ?? "this"}" (verdict: ${verdicts}).`,
|
|
72634
|
+
details: hyps.map((h) => `${h.category}: ${h.rationale ?? h.statement}`),
|
|
72635
|
+
evidence: []
|
|
72636
|
+
};
|
|
72637
|
+
}
|
|
72638
|
+
return {
|
|
72639
|
+
question,
|
|
72640
|
+
kind: "contradicts",
|
|
72641
|
+
headline: `${ev.length} item(s) contradict "${matched?.topic ?? "this"}" (verdict: ${verdicts}).`,
|
|
72642
|
+
details: hyps.map((h) => `${h.category}: ${h.rationale ?? h.statement}`),
|
|
72643
|
+
evidence: ev
|
|
72644
|
+
};
|
|
72645
|
+
}
|
|
72646
|
+
function answerMissing(report, question) {
|
|
72647
|
+
const { gaps, blindSpots } = report.gapAnalysis;
|
|
72648
|
+
const hypMissing = [...new Set(report.hypotheses.flatMap((h) => h.missingEvidence))];
|
|
72649
|
+
if (gaps.length === 0 && hypMissing.length === 0) {
|
|
72650
|
+
return {
|
|
72651
|
+
question,
|
|
72652
|
+
kind: "missing-evidence",
|
|
72653
|
+
headline: "No evidence gaps \u2014 all expected dimensions were collected.",
|
|
72654
|
+
details: [],
|
|
72655
|
+
evidence: []
|
|
72656
|
+
};
|
|
72657
|
+
}
|
|
72658
|
+
const details = gaps.map(
|
|
72659
|
+
(g) => `${g.dimension}: ${g.why} \u2192 ${g.nextSource} (\u2212${(g.confidenceImpact * 100).toFixed(0)}% ceiling)`
|
|
72660
|
+
);
|
|
72661
|
+
if (hypMissing.length > 0) {
|
|
72662
|
+
details.push(`To confirm hypotheses: ${hypMissing.join("; ")}.`);
|
|
72663
|
+
}
|
|
72664
|
+
for (const bs of blindSpots) details.push(`Blind spot: ${bs}`);
|
|
72665
|
+
return {
|
|
72666
|
+
question,
|
|
72667
|
+
kind: "missing-evidence",
|
|
72668
|
+
headline: `${gaps.length} evidence gap(s) limit this investigation.`,
|
|
72669
|
+
details,
|
|
72670
|
+
evidence: []
|
|
72671
|
+
};
|
|
72672
|
+
}
|
|
72673
|
+
function answerConfidence(report, question) {
|
|
72674
|
+
const { gaps, confidenceCeiling } = report.gapAnalysis;
|
|
72675
|
+
const ceilingPct = Math.round(confidenceCeiling * 100);
|
|
72676
|
+
const actualPct = Math.round(report.confidence * 100);
|
|
72677
|
+
const limiting = [...gaps].sort((a, b2) => b2.confidenceImpact - a.confidenceImpact);
|
|
72678
|
+
const details = [];
|
|
72679
|
+
if (limiting.length > 0) {
|
|
72680
|
+
details.push(`Confidence is capped at ${ceilingPct}% by missing evidence:`);
|
|
72681
|
+
for (const g of limiting) {
|
|
72682
|
+
details.push(` \u2022 ${g.dimension} (\u2212${(g.confidenceImpact * 100).toFixed(0)}%): ${g.why}`);
|
|
72683
|
+
}
|
|
72684
|
+
} else {
|
|
72685
|
+
details.push("No evidence gaps cap the ceiling.");
|
|
72686
|
+
}
|
|
72687
|
+
const weak = report.hypotheses.filter((h) => h.verdict === "weakened" || h.verdict === "unconfirmed");
|
|
72688
|
+
if (weak.length > 0) {
|
|
72689
|
+
details.push(
|
|
72690
|
+
`Unconfirmed/weakened hypotheses: ${weak.map((h) => `${h.category} (${h.verdict})`).join(", ")}.`
|
|
72691
|
+
);
|
|
72692
|
+
}
|
|
72693
|
+
return {
|
|
72694
|
+
question,
|
|
72695
|
+
kind: "confidence",
|
|
72696
|
+
headline: `Confidence is ${actualPct}% (ceiling ${ceilingPct}%). ${limiting.length > 0 ? "Missing evidence is the main limiter." : "Limited by hypothesis support, not gaps."}`,
|
|
72697
|
+
details,
|
|
72698
|
+
evidence: []
|
|
72699
|
+
};
|
|
72700
|
+
}
|
|
72701
|
+
function answerQuestion(report, question) {
|
|
72702
|
+
const kind = detectQuestion(question);
|
|
72703
|
+
if (kind === null) return null;
|
|
72704
|
+
if (kind === "contradicts") return answerContradicts(report, question);
|
|
72705
|
+
if (kind === "missing-evidence") return answerMissing(report, question);
|
|
72706
|
+
return answerConfidence(report, question);
|
|
72707
|
+
}
|
|
72708
|
+
function renderQAAnswer(a) {
|
|
72709
|
+
const lines = [];
|
|
72710
|
+
lines.push(`Q: ${a.question}`);
|
|
72711
|
+
lines.push("");
|
|
72712
|
+
lines.push(a.headline);
|
|
72713
|
+
for (const d of a.details) lines.push(d.startsWith(" ") ? d : ` ${d}`);
|
|
72714
|
+
if (a.evidence.length > 0) {
|
|
72715
|
+
lines.push("");
|
|
72716
|
+
lines.push("Evidence:");
|
|
72717
|
+
for (const e of a.evidence) {
|
|
72718
|
+
lines.push(` [${e.id.slice(0, 8)}] (${e.kind}) ${e.title}`);
|
|
72719
|
+
}
|
|
72720
|
+
}
|
|
72721
|
+
return lines.join("\n");
|
|
72722
|
+
}
|
|
72723
|
+
function qaToJSON(a) {
|
|
72724
|
+
return JSON.stringify(
|
|
72725
|
+
{
|
|
72726
|
+
question: a.question,
|
|
72727
|
+
kind: a.kind,
|
|
72728
|
+
headline: a.headline,
|
|
72729
|
+
details: a.details,
|
|
72730
|
+
evidence: a.evidence.map((e) => ({ id: e.id, kind: e.kind, title: e.title }))
|
|
72731
|
+
},
|
|
72732
|
+
null,
|
|
72733
|
+
2
|
|
72734
|
+
);
|
|
72735
|
+
}
|
|
72736
|
+
|
|
72125
72737
|
// ../../packages/engine/src/onboard.ts
|
|
72126
72738
|
init_cjs_shims();
|
|
72127
72739
|
function tokenize2(text2) {
|
|
@@ -73201,6 +73813,7 @@ async function runInvestigate(hint, opts) {
|
|
|
73201
73813
|
const logs = logsForEnv(renv);
|
|
73202
73814
|
const mongo = mongoForEnv(renv);
|
|
73203
73815
|
const queue = queueForEnv(renv);
|
|
73816
|
+
const redisState = redisStateForEnv(renv);
|
|
73204
73817
|
const metrics = metricsForEnv(renv);
|
|
73205
73818
|
const service = opts.service ?? renv.connectors.elasticsearch?.serviceName;
|
|
73206
73819
|
const { db, sql: sql2 } = createDb(config.database.url);
|
|
@@ -73213,6 +73826,7 @@ async function runInvestigate(hint, opts) {
|
|
|
73213
73826
|
logs,
|
|
73214
73827
|
mongo,
|
|
73215
73828
|
queue,
|
|
73829
|
+
redisState,
|
|
73216
73830
|
metrics,
|
|
73217
73831
|
repoPath: renv.path,
|
|
73218
73832
|
connectors: {
|
|
@@ -73283,6 +73897,12 @@ async function runChanges(base2, compare, opts) {
|
|
|
73283
73897
|
// ../../packages/cli/src/commands/timeline.ts
|
|
73284
73898
|
init_cjs_shims();
|
|
73285
73899
|
var import_picocolors7 = __toESM(require_picocolors(), 1);
|
|
73900
|
+
var DEFAULT_SINCE = "7 days ago";
|
|
73901
|
+
function resolveTimelineWindow(opts) {
|
|
73902
|
+
const usingDefault = !opts.all && opts.since === void 0;
|
|
73903
|
+
const since = opts.all ? void 0 : opts.since ?? DEFAULT_SINCE;
|
|
73904
|
+
return { since, usingDefault };
|
|
73905
|
+
}
|
|
73286
73906
|
async function runTimeline(service, opts) {
|
|
73287
73907
|
try {
|
|
73288
73908
|
const config = await loadConfig(opts.config);
|
|
@@ -73294,11 +73914,24 @@ async function runTimeline(service, opts) {
|
|
|
73294
73914
|
return 1;
|
|
73295
73915
|
}
|
|
73296
73916
|
const { code } = createConnectors(config);
|
|
73917
|
+
const { since, usingDefault: usingDefaultWindow } = resolveTimelineWindow(opts);
|
|
73297
73918
|
const t = await reconstructChangeTimeline(
|
|
73298
|
-
{ repoPath: renv.path, since
|
|
73919
|
+
{ repoPath: renv.path, since, until: opts.until, service },
|
|
73299
73920
|
{ code }
|
|
73300
73921
|
);
|
|
73301
|
-
|
|
73922
|
+
if (opts.json) {
|
|
73923
|
+
console.log(changeTimelineToJSON(t));
|
|
73924
|
+
} else {
|
|
73925
|
+
console.log(renderChangeTimeline(t));
|
|
73926
|
+
if (usingDefaultWindow) {
|
|
73927
|
+
console.log(
|
|
73928
|
+
import_picocolors7.default.dim(
|
|
73929
|
+
`
|
|
73930
|
+
Showing the last 7 days (default). Widen with ${import_picocolors7.default.bold('--since "30 days ago"')}, pin a range with ${import_picocolors7.default.bold("--since <when> --until <when>")}, or see everything with ${import_picocolors7.default.bold("--all")}.`
|
|
73931
|
+
)
|
|
73932
|
+
);
|
|
73933
|
+
}
|
|
73934
|
+
}
|
|
73302
73935
|
return 0;
|
|
73303
73936
|
} catch (err) {
|
|
73304
73937
|
console.error(import_picocolors7.default.red(err.message));
|
|
@@ -73309,7 +73942,7 @@ async function runTimeline(service, opts) {
|
|
|
73309
73942
|
// ../../packages/cli/src/commands/what-changed.ts
|
|
73310
73943
|
init_cjs_shims();
|
|
73311
73944
|
var import_picocolors8 = __toESM(require_picocolors(), 1);
|
|
73312
|
-
var
|
|
73945
|
+
var DEFAULT_SINCE2 = "7 days ago";
|
|
73313
73946
|
async function runWhatChanged(service, opts) {
|
|
73314
73947
|
try {
|
|
73315
73948
|
const config = await loadConfig(opts.config);
|
|
@@ -73321,7 +73954,7 @@ async function runWhatChanged(service, opts) {
|
|
|
73321
73954
|
return 1;
|
|
73322
73955
|
}
|
|
73323
73956
|
const { code } = createConnectors(config);
|
|
73324
|
-
const since = opts.since ??
|
|
73957
|
+
const since = opts.since ?? DEFAULT_SINCE2;
|
|
73325
73958
|
const r = await whatChanged(
|
|
73326
73959
|
{ repoPath: renv.path, since, until: opts.until, service },
|
|
73327
73960
|
{ code }
|
|
@@ -73782,6 +74415,11 @@ async function runAsk(id, directive, opts) {
|
|
|
73782
74415
|
return 1;
|
|
73783
74416
|
}
|
|
73784
74417
|
const report = migrateReport(row.report);
|
|
74418
|
+
const answer = answerQuestion(report, directive);
|
|
74419
|
+
if (answer) {
|
|
74420
|
+
console.log(opts.json ? qaToJSON(answer) : renderQAAnswer(answer));
|
|
74421
|
+
return 0;
|
|
74422
|
+
}
|
|
73785
74423
|
const v = refineInvestigation(report, directive);
|
|
73786
74424
|
console.log(opts.json ? refinedToJSON(report, v) : renderRefined(report, v));
|
|
73787
74425
|
if (!opts.json && report.aiJudgment) {
|
|
@@ -74665,6 +75303,20 @@ async function checkboxSearch(opts) {
|
|
|
74665
75303
|
}
|
|
74666
75304
|
|
|
74667
75305
|
// ../../packages/cli/src/commands/connect.ts
|
|
75306
|
+
function parseDbSpec(spec) {
|
|
75307
|
+
const [dbPart, rolesPart] = spec.split(":");
|
|
75308
|
+
const db = Number.parseInt((dbPart ?? "").trim(), 10);
|
|
75309
|
+
if (!Number.isInteger(db) || db < 0 || db > 15) {
|
|
75310
|
+
throw new Error(`Invalid --db spec "${spec}": DB index must be 0\u201315 (e.g. 1:bullmq,queues)`);
|
|
75311
|
+
}
|
|
75312
|
+
const roles = (rolesPart ?? "").split(",").map((r) => r.trim()).filter(Boolean);
|
|
75313
|
+
for (const role of roles) {
|
|
75314
|
+
if (!REDIS_ROLES.includes(role)) {
|
|
75315
|
+
throw new Error(`Invalid role "${role}" in --db spec "${spec}". Valid roles: ${REDIS_ROLES.join(", ")}`);
|
|
75316
|
+
}
|
|
75317
|
+
}
|
|
75318
|
+
return { db, roles };
|
|
75319
|
+
}
|
|
74668
75320
|
var SUPPORTED = ["elasticsearch", "mongodb", "grafana", "redis"];
|
|
74669
75321
|
async function runConnect(type, opts) {
|
|
74670
75322
|
if (!SUPPORTED.includes(type)) {
|
|
@@ -74809,6 +75461,14 @@ ${import_picocolors27.default.bold(`Connect ${type}`)} ${import_picocolors27.def
|
|
|
74809
75461
|
filled.url = injectRedisPassword(filled.url, pw);
|
|
74810
75462
|
}
|
|
74811
75463
|
}
|
|
75464
|
+
if (filled.db && filled.db.length > 0) {
|
|
75465
|
+
filled.redisDatabases = filled.db.map(parseDbSpec);
|
|
75466
|
+
} else if (filled.url && filled.scanDbs !== false && isInteractive()) {
|
|
75467
|
+
const scan = await askScanDbs();
|
|
75468
|
+
if (scan) {
|
|
75469
|
+
filled.redisDatabases = await discoverAndSelectDbs(filled.url, filled.bullmqPrefix ?? "bull");
|
|
75470
|
+
}
|
|
75471
|
+
}
|
|
74812
75472
|
break;
|
|
74813
75473
|
}
|
|
74814
75474
|
}
|
|
@@ -74916,8 +75576,64 @@ function buildGrafanaPatch(opts) {
|
|
|
74916
75576
|
function buildRedisPatch(opts) {
|
|
74917
75577
|
const patch = {};
|
|
74918
75578
|
if (opts.url) patch["url"] = opts.url;
|
|
75579
|
+
const dbs = opts.redisDatabases ?? (opts.db ? opts.db.map(parseDbSpec) : void 0);
|
|
75580
|
+
if (dbs && dbs.length > 0) {
|
|
75581
|
+
const prefix = opts.bullmqPrefix ?? "bull";
|
|
75582
|
+
patch["databases"] = dbs.map((d) => {
|
|
75583
|
+
const isQueue = d.roles.some((r) => r === "bullmq" || r === "queues");
|
|
75584
|
+
return {
|
|
75585
|
+
db: d.db,
|
|
75586
|
+
...d.name ? { name: d.name } : {},
|
|
75587
|
+
roles: d.roles,
|
|
75588
|
+
...isQueue ? { bullmq: { prefix } } : {}
|
|
75589
|
+
};
|
|
75590
|
+
});
|
|
75591
|
+
}
|
|
74919
75592
|
return patch;
|
|
74920
75593
|
}
|
|
75594
|
+
async function askScanDbs() {
|
|
75595
|
+
const answer = (await ask("Scan Redis DBs 0-15 to detect queues vs cache?", "yes", false)).trim().toLowerCase();
|
|
75596
|
+
return answer === "" || answer === "y" || answer === "yes";
|
|
75597
|
+
}
|
|
75598
|
+
function describeProbe(p) {
|
|
75599
|
+
if (!p.reachable) return `unreachable (${p.detail ?? "error"})`;
|
|
75600
|
+
if (p.keyCount === 0) return "empty";
|
|
75601
|
+
if (p.bullmqQueues.length > 0) {
|
|
75602
|
+
const sample = p.bullmqQueues.slice(0, 3).join(", ");
|
|
75603
|
+
return `${p.bullmqQueues.length} BullMQ queue(s) \xB7 prefix ${p.bullmqPrefix} \xB7 ${sample}`;
|
|
75604
|
+
}
|
|
75605
|
+
const examples = p.prefixes.slice(0, 3).map((x) => `${x.prefix}:*`).join(", ");
|
|
75606
|
+
return `${p.keyCount} keys \xB7 ${p.suggestedRoles.join("/")}${examples ? ` \xB7 ${examples}` : ""}`;
|
|
75607
|
+
}
|
|
75608
|
+
async function discoverAndSelectDbs(url, bullmqPrefix) {
|
|
75609
|
+
console.log(import_picocolors27.default.dim("\n Scanning DBs 0-15 (read-only, sampled)\u2026"));
|
|
75610
|
+
const probes = await probeRedisDatabases(url, { bullmqPrefix });
|
|
75611
|
+
const nonEmpty = probes.filter((p) => p.reachable && p.keyCount > 0);
|
|
75612
|
+
if (nonEmpty.length === 0) {
|
|
75613
|
+
console.log(import_picocolors27.default.dim(" No populated DBs found."));
|
|
75614
|
+
return [];
|
|
75615
|
+
}
|
|
75616
|
+
for (const p of nonEmpty) {
|
|
75617
|
+
console.log(` ${import_picocolors27.default.bold(`DB ${p.db}`)} ${import_picocolors27.default.dim("\xB7 " + describeProbe(p))}`);
|
|
75618
|
+
}
|
|
75619
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
75620
|
+
const choices = nonEmpty.map((p) => {
|
|
75621
|
+
const label = `DB ${p.db} \u2014 ${p.suggestedRoles.join("/") || "unrolled"} (${describeProbe(p)})`;
|
|
75622
|
+
byLabel.set(label, p);
|
|
75623
|
+
return label;
|
|
75624
|
+
});
|
|
75625
|
+
let selected;
|
|
75626
|
+
try {
|
|
75627
|
+
selected = await checkboxSearch({ message: "Select DBs to save", choices, pageSize: 12 });
|
|
75628
|
+
} catch (err) {
|
|
75629
|
+
if (err instanceof ExitPromptError) throw err;
|
|
75630
|
+
selected = choices;
|
|
75631
|
+
}
|
|
75632
|
+
return selected.map((label) => {
|
|
75633
|
+
const p = byLabel.get(label);
|
|
75634
|
+
return { db: p.db, name: p.suggestedRoles.includes("bullmq") ? "queues" : "cache", roles: p.suggestedRoles };
|
|
75635
|
+
});
|
|
75636
|
+
}
|
|
74921
75637
|
async function probe(type, opts) {
|
|
74922
75638
|
try {
|
|
74923
75639
|
switch (type) {
|
|
@@ -74973,36 +75689,23 @@ async function probe(type, opts) {
|
|
|
74973
75689
|
}
|
|
74974
75690
|
case "redis": {
|
|
74975
75691
|
if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
|
|
74976
|
-
const
|
|
74977
|
-
|
|
74978
|
-
|
|
74979
|
-
|
|
74980
|
-
|
|
75692
|
+
const client = new RedisScanClient({ url: opts.url });
|
|
75693
|
+
try {
|
|
75694
|
+
const h = await client.health();
|
|
75695
|
+
const host = new URL(opts.url).hostname;
|
|
75696
|
+
const port = new URL(opts.url).port || "6379";
|
|
75697
|
+
if (h.ok) return { ok: true, detail: `PING ok at ${host}:${port}` };
|
|
75698
|
+
const authFailed = /WRONGPASS|NOAUTH|invalid password/i.test(h.detail);
|
|
75699
|
+
return { ok: false, detail: authFailed ? `auth failed (${h.detail})` : h.detail };
|
|
75700
|
+
} finally {
|
|
75701
|
+
await client.close();
|
|
75702
|
+
}
|
|
74981
75703
|
}
|
|
74982
75704
|
}
|
|
74983
75705
|
} catch (err) {
|
|
74984
75706
|
return { ok: false, detail: err.message };
|
|
74985
75707
|
}
|
|
74986
75708
|
}
|
|
74987
|
-
function tcpProbe(host, port, timeoutMs = 3e3) {
|
|
74988
|
-
return new Promise((resolve8) => {
|
|
74989
|
-
const { createConnection } = require("net");
|
|
74990
|
-
const sock = createConnection({ host, port });
|
|
74991
|
-
const timer2 = setTimeout(() => {
|
|
74992
|
-
sock.destroy();
|
|
74993
|
-
resolve8(false);
|
|
74994
|
-
}, timeoutMs);
|
|
74995
|
-
sock.on("connect", () => {
|
|
74996
|
-
clearTimeout(timer2);
|
|
74997
|
-
sock.destroy();
|
|
74998
|
-
resolve8(true);
|
|
74999
|
-
});
|
|
75000
|
-
sock.on("error", () => {
|
|
75001
|
-
clearTimeout(timer2);
|
|
75002
|
-
resolve8(false);
|
|
75003
|
-
});
|
|
75004
|
-
});
|
|
75005
|
-
}
|
|
75006
75709
|
function printSummary(type, opts) {
|
|
75007
75710
|
const lines = [];
|
|
75008
75711
|
if (opts.url) lines.push(` url: ${redactUrl(opts.url)}`);
|
|
@@ -76078,7 +76781,15 @@ Examples:
|
|
|
76078
76781
|
});
|
|
76079
76782
|
program2.command("connect <type>").description(
|
|
76080
76783
|
"Add or update a runtime connector (elasticsearch / mongodb / grafana / redis) in .horus/config.json"
|
|
76081
|
-
).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(
|
|
76784
|
+
).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(
|
|
76785
|
+
"--db <spec>",
|
|
76786
|
+
"redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
|
|
76787
|
+
(val, acc) => {
|
|
76788
|
+
acc.push(val);
|
|
76789
|
+
return acc;
|
|
76790
|
+
},
|
|
76791
|
+
[]
|
|
76792
|
+
).option("--bullmq-prefix <prefix>", "BullMQ key prefix for redis queue DBs (default: bull)").option("--no-scan-dbs", "skip interactive Redis DB scan").option("--no-test", "skip live connection probe").action(
|
|
76082
76793
|
async (type, opts) => {
|
|
76083
76794
|
process.exitCode = await runConnect(type, {
|
|
76084
76795
|
env: opts.env,
|
|
@@ -76090,6 +76801,9 @@ Examples:
|
|
|
76090
76801
|
database: opts.database,
|
|
76091
76802
|
collections: opts.collections,
|
|
76092
76803
|
dashboard: opts.dashboard,
|
|
76804
|
+
db: opts.db,
|
|
76805
|
+
bullmqPrefix: opts.bullmqPrefix,
|
|
76806
|
+
scanDbs: opts.scanDbs,
|
|
76093
76807
|
noTest: opts.test === false
|
|
76094
76808
|
});
|
|
76095
76809
|
}
|
|
@@ -76170,13 +76884,14 @@ Examples:
|
|
|
76170
76884
|
});
|
|
76171
76885
|
program2.command("timeline [service]").description(
|
|
76172
76886
|
"Reconstruct what changed in a time window (git + change-impact) \u2014 evidence, not conclusions"
|
|
76173
|
-
).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (e.g. "
|
|
76887
|
+
).option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "repository name from config").option("--since <when>", 'git --since (default "7 days ago"; e.g. "30 days ago", a date)').option("--until <when>", "git --until").option("--all", "include all history instead of the default recent window").option("--json", "output JSON").action(
|
|
76174
76888
|
async (service, opts) => {
|
|
76175
76889
|
process.exitCode = await runTimeline(service, {
|
|
76176
76890
|
config: opts.config,
|
|
76177
76891
|
repo: opts.repo,
|
|
76178
76892
|
since: opts.since,
|
|
76179
76893
|
until: opts.until,
|
|
76894
|
+
all: opts.all,
|
|
76180
76895
|
json: opts.json
|
|
76181
76896
|
});
|
|
76182
76897
|
}
|
|
@@ -76274,7 +76989,7 @@ Examples:
|
|
|
76274
76989
|
process.exitCode = await runScore(id, { config: opts.config, json: opts.json });
|
|
76275
76990
|
});
|
|
76276
76991
|
program2.command("ask <id> <directive>").description(
|
|
76277
|
-
'
|
|
76992
|
+
'Ask about or refine a saved investigation \u2014 reuses evidence, no re-query.\n Questions (direct answers):\n "what evidence contradicts <topic>?" \xB7 "what evidence is missing?"\n "why is confidence not higher?"\n Topic filters (deterministic scoping):\n "focus on queue behavior" \xB7 "ignore deployment changes" \xB7 "retry"'
|
|
76278
76993
|
).option("-c, --config <path>", "path to horus.config.ts").option("--json", "output JSON").action(async (id, directive, opts) => {
|
|
76279
76994
|
process.exitCode = await runAsk(id, directive, { config: opts.config, json: opts.json });
|
|
76280
76995
|
});
|