@merittdev/horus 0.10.0 → 0.12.0

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.
Files changed (2) hide show
  1. package/dist/index.cjs +990 -214
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -65880,8 +65880,8 @@ init_cjs_shims();
65880
65880
 
65881
65881
  // ../../packages/core/src/version.ts
65882
65882
  init_cjs_shims();
65883
- var HORUS_VERSION = true ? "0.10.0" : "dev";
65884
- var PINNED_SOURCE_VERSION = "1.6.1";
65883
+ var HORUS_VERSION = true ? "0.12.0" : "dev";
65884
+ var PINNED_SOURCE_VERSION = "2.0.0";
65885
65885
 
65886
65886
  // ../../packages/core/src/evidence.ts
65887
65887
  init_cjs_shims();
@@ -70251,6 +70251,22 @@ var connectorsSchema = external_exports.object({
70251
70251
  /** Env var holding the base URL. Defaults to "SENTRY_URL". */
70252
70252
  urlEnv: external_exports.string().optional()
70253
70253
  }).optional(),
70254
+ /**
70255
+ * Axiom logs connector (HOR-429). Structured log rows queried with APL become
70256
+ * `kind: 'log'` evidence (message + level + fields), alongside Elasticsearch.
70257
+ */
70258
+ axiom: external_exports.object({
70259
+ /** Dataset to query (the Axiom analogue of an ES index). */
70260
+ dataset: external_exports.string(),
70261
+ /** Direct API token (takes priority over tokenEnv). */
70262
+ token: external_exports.string().optional(),
70263
+ /** Env var holding the API token. Defaults to "AXIOM_TOKEN". */
70264
+ tokenEnv: external_exports.string().optional(),
70265
+ /** Direct base URL (US default https://api.axiom.co; EU https://api.eu.axiom.co). Takes priority over urlEnv. */
70266
+ url: external_exports.string().optional(),
70267
+ /** Env var holding the base URL. Defaults to "AXIOM_URL". */
70268
+ urlEnv: external_exports.string().optional()
70269
+ }).optional(),
70254
70270
  grafana: external_exports.object({
70255
70271
  dashboard: external_exports.string().optional(),
70256
70272
  /** Multiple dashboard UIDs to fetch. Takes precedence over `dashboard` when set. */
@@ -70469,6 +70485,16 @@ function resolveEnvironment(config2, opts) {
70469
70485
  ...url !== void 0 ? { url } : {}
70470
70486
  };
70471
70487
  }
70488
+ if (c4.axiom !== void 0) {
70489
+ const a2 = c4.axiom;
70490
+ const token = a2.token ?? process.env[a2.tokenEnv ?? "AXIOM_TOKEN"];
70491
+ const url = a2.url ?? process.env[a2.urlEnv ?? "AXIOM_URL"];
70492
+ resolved.axiom = {
70493
+ dataset: a2.dataset,
70494
+ ...token !== void 0 ? { token } : {},
70495
+ ...url !== void 0 ? { url } : {}
70496
+ };
70497
+ }
70472
70498
  if (c4.grafana !== void 0) {
70473
70499
  const g4 = c4.grafana;
70474
70500
  resolved.grafana = {
@@ -70963,8 +70989,72 @@ var SourceHttpClient = class {
70963
70989
  return this.request("POST", "/api/diff", { base: base2, compare });
70964
70990
  }
70965
70991
  async nodeCount() {
70966
- const result = await this.cypher("MATCH (n) RETURN count(n)");
70967
- return Number(result.rows[0]?.[0]) || 0;
70992
+ const o5 = await this.overview();
70993
+ return Number(o5.totalNodes) || 0;
70994
+ }
70995
+ // -------------------------------------------------------------------------
70996
+ // Typed read-path endpoints (HOR-392). These replace the CLI's raw Cypher:
70997
+ // the host owns the queries and returns shaped JSON, so the client never
70998
+ // emits or escapes Cypher on the read path.
70999
+ // -------------------------------------------------------------------------
71000
+ /** Nodes whose full content contains ANY of `tokens` — full (untruncated) content. */
71001
+ async contentSearch(tokens, limit = 500) {
71002
+ const res = await this.request("POST", "/api/content-search", {
71003
+ tokens,
71004
+ limit
71005
+ });
71006
+ return res.results ?? [];
71007
+ }
71008
+ /** Exact-name symbol lookup (file label excluded), with line ranges. */
71009
+ async exactSymbols(name2, limit = 10) {
71010
+ const res = await this.request(
71011
+ "GET",
71012
+ `/api/symbols/exact?name=${encodeURIComponent(name2)}&limit=${limit}`
71013
+ );
71014
+ return res.results ?? [];
71015
+ }
71016
+ /** Symbol nodes for the given comma-joinable lowercase labels (source-graph extraction). */
71017
+ async symbolsByLabel(labels, limit = 1e3) {
71018
+ const res = await this.request(
71019
+ "GET",
71020
+ `/api/symbols?labels=${encodeURIComponent(labels.join(","))}&limit=${limit}`
71021
+ );
71022
+ return res.symbols ?? [];
71023
+ }
71024
+ /** Batch-resolve node ids to their line ranges (CLI line hydration). */
71025
+ async nodesLines(ids2) {
71026
+ const res = await this.request(
71027
+ "POST",
71028
+ "/api/nodes/lines",
71029
+ { ids: ids2 }
71030
+ );
71031
+ return res.lines ?? {};
71032
+ }
71033
+ /** Process flows a symbol participates in, with each flow's named ordered steps. */
71034
+ flows(nodeId) {
71035
+ return this.request("GET", `/api/flows/${encodeURI(nodeId)}`);
71036
+ }
71037
+ /** Method symbols of `className` defined in `file`, ordered by start line. */
71038
+ async classMethods(file, className) {
71039
+ const res = await this.request(
71040
+ "GET",
71041
+ `/api/class-methods?file=${encodeURIComponent(file)}&class=${encodeURIComponent(className)}`
71042
+ );
71043
+ return res.methods ?? [];
71044
+ }
71045
+ /** Extended node detail — node + content + callers/callees/typeRefs + imports + coupling + communities. */
71046
+ node(nodeId) {
71047
+ return this.request("GET", `/api/node/${encodeURI(nodeId)}`);
71048
+ }
71049
+ /** Community clusters with their member nodes (source-graph extraction). */
71050
+ async communities() {
71051
+ const res = await this.request("GET", "/api/communities");
71052
+ return res.communities ?? [];
71053
+ }
71054
+ /** Discovered execution processes with their ordered steps (source-graph extraction). */
71055
+ async processes() {
71056
+ const res = await this.request("GET", "/api/processes");
71057
+ return res.processes ?? [];
70968
71058
  }
70969
71059
  // -------------------------------------------------------------------------
70970
71060
  // Memory vector bridge (M2). These reuse `request<T>` (retry/timeout/4xx) so
@@ -71245,13 +71335,6 @@ var SourceCodeProvider = class {
71245
71335
  id = "source";
71246
71336
  kind = "code";
71247
71337
  // --- helpers -------------------------------------------------------------
71248
- /**
71249
- * Escape a node id for inlining inside a double-quoted Cypher string literal:
71250
- * first double every backslash, then escape every double-quote.
71251
- */
71252
- escapeId(id) {
71253
- return id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
71254
- }
71255
71338
  nodeToSymbol(n4) {
71256
71339
  return {
71257
71340
  id: n4.id,
@@ -71264,29 +71347,34 @@ var SourceCodeProvider = class {
71264
71347
  className: n4.className
71265
71348
  };
71266
71349
  }
71267
- async rows(query) {
71350
+ /** Map a serialized graph node to a `Symbol`, coercing the host's `0` line sentinel to undefined. */
71351
+ serializedToSymbol(n4) {
71352
+ const sym = {
71353
+ id: n4.id ?? "",
71354
+ name: n4.name ?? "",
71355
+ filePath: n4.filePath ?? ""
71356
+ };
71357
+ if (typeof n4.startLine === "number" && n4.startLine > 0) sym.startLine = n4.startLine;
71358
+ if (typeof n4.endLine === "number" && n4.endLine > 0) sym.endLine = n4.endLine;
71359
+ if (n4.className) sym.className = n4.className;
71360
+ if (n4.signature) sym.signature = n4.signature;
71361
+ if (n4.language) sym.language = n4.language;
71362
+ return sym;
71363
+ }
71364
+ /**
71365
+ * Graceful degradation (HOR-353): a 4xx for THIS repo's graph shape must not abort the
71366
+ * whole investigation. The caller passes a default the slice degrades to.
71367
+ */
71368
+ async degrade(p5, fallback) {
71268
71369
  try {
71269
- return (await this.client.cypher(query)).rows;
71370
+ return await p5;
71270
71371
  } catch (err2) {
71271
71372
  if (err2 instanceof SourceHttpError && err2.status >= 400 && err2.status < 500) {
71272
- return [];
71373
+ return fallback;
71273
71374
  }
71274
71375
  throw err2;
71275
71376
  }
71276
71377
  }
71277
- cypherRowToSymbol(row, idIdx, nameIdx, fileIdx, lineIdx, classNameIdx) {
71278
- const startLine = lineIdx != null && row[lineIdx] != null ? Number(row[lineIdx]) : void 0;
71279
- const sym = {
71280
- id: String(row[idIdx] ?? ""),
71281
- name: String(row[nameIdx] ?? ""),
71282
- filePath: String(row[fileIdx] ?? ""),
71283
- startLine
71284
- };
71285
- if (classNameIdx != null && row[classNameIdx] != null) {
71286
- sym.className = String(row[classNameIdx]);
71287
- }
71288
- return sym;
71289
- }
71290
71378
  // --- contract ------------------------------------------------------------
71291
71379
  async health() {
71292
71380
  const h3 = await this.client.health();
@@ -71296,19 +71384,22 @@ var SourceCodeProvider = class {
71296
71384
  };
71297
71385
  }
71298
71386
  async searchSymbols(query, limit = 10) {
71299
- const E2 = this.escapeId(query);
71300
- const exactQuery = `MATCH (n) WHERE toLower(n.name) = toLower("${E2}") AND label(n) <> "File" RETURN n.id, n.name, n.file_path LIMIT ${limit}`;
71301
- const [exactRows, semanticRes] = await Promise.all([
71302
- this.rows(exactQuery).catch(() => []),
71387
+ const [exactHits, semanticRes] = await Promise.all([
71388
+ this.degrade(this.client.exactSymbols(query, limit), []),
71303
71389
  this.client.search(query, limit)
71304
71390
  ]);
71305
- const exactSymbols = exactRows.map((row) => ({
71306
- id: String(row[0] ?? ""),
71307
- name: String(row[1] ?? ""),
71308
- filePath: String(row[2] ?? ""),
71309
- score: 1
71310
- // an exact-name Cypher hit is authoritative
71311
- }));
71391
+ const exactSymbols = exactHits.map((h3) => {
71392
+ const sym = {
71393
+ id: h3.nodeId,
71394
+ name: h3.name,
71395
+ filePath: h3.filePath,
71396
+ score: 1
71397
+ // an exact-name hit is authoritative
71398
+ };
71399
+ if (h3.startLine > 0) sym.startLine = h3.startLine;
71400
+ if (h3.endLine > 0) sym.endLine = h3.endLine;
71401
+ return sym;
71402
+ });
71312
71403
  const exactIds = new Set(exactSymbols.map((s5) => s5.id));
71313
71404
  const ql = query.toLowerCase();
71314
71405
  const semantic = semanticRes.filter((r) => !exactIds.has(r.nodeId)).map((r) => {
@@ -71330,78 +71421,54 @@ var SourceCodeProvider = class {
71330
71421
  async hydrateLines(symbols) {
71331
71422
  const ids2 = symbols.map((s5) => s5.id).filter(Boolean);
71332
71423
  if (ids2.length === 0) return symbols;
71333
- const idList = ids2.map((id) => `"${this.escapeId(id)}"`).join(", ");
71334
- const rows = await this.rows(`MATCH (n) WHERE n.id IN [${idList}] RETURN n.id, n.start_line, n.end_line`).catch(() => []);
71335
- const lineById = /* @__PURE__ */ new Map();
71336
- for (const row of rows) {
71337
- const id = String(row[0] ?? "");
71338
- const start2 = row[1] != null ? Number(row[1]) : void 0;
71339
- const end = row[2] != null ? Number(row[2]) : void 0;
71340
- lineById.set(id, { start: start2, end });
71341
- }
71424
+ const lines = await this.degrade(this.client.nodesLines(ids2), {});
71342
71425
  return symbols.map((s5) => {
71343
- const l2 = lineById.get(s5.id);
71426
+ const l2 = lines[s5.id];
71344
71427
  if (l2 === void 0) return s5;
71345
71428
  const out2 = { ...s5 };
71346
- if (l2.start !== void 0 && Number.isFinite(l2.start)) out2.startLine = l2.start;
71347
- if (l2.end !== void 0 && Number.isFinite(l2.end)) out2.endLine = l2.end;
71429
+ if (typeof l2.startLine === "number" && l2.startLine > 0) out2.startLine = l2.startLine;
71430
+ if (typeof l2.endLine === "number" && l2.endLine > 0) out2.endLine = l2.endLine;
71348
71431
  return out2;
71349
71432
  });
71350
71433
  }
71351
71434
  async context(symbolId) {
71352
- const E2 = this.escapeId(symbolId);
71353
- const nodeQuery = `MATCH (n) WHERE n.id = "${E2}" RETURN n.id, n.name, n.file_path, n.start_line, n.end_line, n.signature, n.class_name, n.language, n.is_dead, n.content LIMIT 1`;
71354
- const calleesQuery = `MATCH (n)-[r:CodeRelation]->(m) WHERE n.id = "${E2}" AND r.rel_type = "calls" RETURN m.id, m.name, m.file_path, m.start_line, m.class_name`;
71355
- const callersQuery = `MATCH (m)-[r:CodeRelation]->(n) WHERE n.id = "${E2}" AND r.rel_type = "calls" RETURN m.id, m.name, m.file_path, m.start_line, m.class_name`;
71356
- const usesTypeQuery = `MATCH (n)-[r:CodeRelation]->(t) WHERE n.id = "${E2}" AND r.rel_type = "uses_type" RETURN t.id, t.name, t.file_path, t.start_line`;
71357
- const communityQuery = `MATCH (n)-[r:CodeRelation]->(c:Community) WHERE n.id = "${E2}" AND r.rel_type = "member_of" RETURN c.id, c.name LIMIT 1`;
71358
- const fileQuery = `MATCH (f:File)-[r:CodeRelation]->(n) WHERE n.id = "${E2}" AND r.rel_type = "defines" RETURN f.id, f.file_path LIMIT 1`;
71359
- const [nodeRows, calleeRows, callerRows, usesTypeRows, communityRows, fileRows] = await Promise.all([
71360
- this.rows(nodeQuery),
71361
- this.rows(calleesQuery),
71362
- this.rows(callersQuery),
71363
- this.rows(usesTypeQuery),
71364
- this.rows(communityQuery),
71365
- this.rows(fileQuery)
71366
- ]);
71367
- const node = nodeRows[0];
71435
+ const detail = await this.degrade(this.client.node(symbolId), null);
71436
+ if (detail === null) {
71437
+ return {
71438
+ symbol: { id: symbolId, name: "", filePath: "" },
71439
+ callers: [],
71440
+ callees: [],
71441
+ imports: [],
71442
+ usesType: [],
71443
+ community: null,
71444
+ coupledWith: [],
71445
+ isDead: false
71446
+ };
71447
+ }
71448
+ const n4 = detail.node;
71368
71449
  const symbol = {
71369
- id: String(node?.[0] ?? symbolId),
71370
- name: String(node?.[1] ?? ""),
71371
- filePath: String(node?.[2] ?? ""),
71372
- startLine: node?.[3] != null ? Number(node[3]) : void 0,
71373
- endLine: node?.[4] != null ? Number(node[4]) : void 0,
71374
- signature: node?.[5] != null ? String(node[5]) : void 0,
71375
- className: node?.[6] != null ? String(node[6]) : void 0,
71376
- language: node?.[7] != null ? String(node[7]) : void 0
71377
- };
71378
- const isDead = Boolean(node?.[8]);
71379
- const content = node?.[9];
71450
+ id: n4.id ?? symbolId,
71451
+ name: n4.name ?? "",
71452
+ filePath: n4.filePath ?? ""
71453
+ };
71454
+ if (typeof n4.startLine === "number" && n4.startLine > 0) symbol.startLine = n4.startLine;
71455
+ if (typeof n4.endLine === "number" && n4.endLine > 0) symbol.endLine = n4.endLine;
71456
+ if (n4.signature) symbol.signature = n4.signature;
71457
+ if (n4.className) symbol.className = n4.className;
71458
+ if (n4.language) symbol.language = n4.language;
71459
+ const content = n4.content;
71380
71460
  const snippet = typeof content === "string" ? content.slice(0, 600) : void 0;
71381
71461
  const sourceBody = typeof content === "string" ? content.slice(0, 2e4) : void 0;
71382
- const callees = calleeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
71383
- const callers = callerRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
71384
- const usesType = usesTypeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
71385
- const communityRow = communityRows[0];
71386
- const community = communityRow ? { id: String(communityRow[0] ?? ""), name: String(communityRow[1] ?? "") } : null;
71387
- let imports = [];
71388
- let coupledWith = [];
71389
- const fileRow = fileRows[0];
71390
- const fileId = fileRow?.[0];
71391
- if (fileId != null) {
71392
- const FE = this.escapeId(String(fileId));
71393
- const importsQuery = `MATCH (f)-[r:CodeRelation]->(g:File) WHERE f.id = "${FE}" AND r.rel_type = "imports" RETURN g.file_path`;
71394
- const coupledQuery = `MATCH (f)-[r:CodeRelation]->(g:File) WHERE f.id = "${FE}" AND r.rel_type = "coupled_with" RETURN g.file_path, r.co_changes`;
71395
- const [importRows, coupledRows] = await Promise.all([
71396
- this.rows(importsQuery),
71397
- this.rows(coupledQuery)
71398
- ]);
71399
- imports = importRows.map((row) => String(row[0] ?? ""));
71400
- coupledWith = coupledRows.map((row) => ({
71401
- file: String(row[0] ?? ""),
71402
- coChanges: Number(row[1] ?? 0)
71403
- }));
71404
- }
71462
+ const callees = (detail.callees ?? []).map((c4) => this.serializedToSymbol(c4.node));
71463
+ const callers = (detail.callers ?? []).map((c4) => this.serializedToSymbol(c4.node));
71464
+ const usesType = (detail.typeRefs ?? []).map((t2) => this.serializedToSymbol(t2));
71465
+ const communityRow = (detail.communities ?? [])[0];
71466
+ const community = communityRow ? { id: String(communityRow.id ?? ""), name: String(communityRow.name ?? "") } : null;
71467
+ const imports = (detail.imports ?? []).map((i3) => String(i3));
71468
+ const coupledWith = (detail.coupledWith ?? []).map((c4) => ({
71469
+ file: String(c4.file ?? ""),
71470
+ coChanges: Number(c4.coChanges ?? 0)
71471
+ }));
71405
71472
  return {
71406
71473
  symbol,
71407
71474
  snippet,
@@ -71412,7 +71479,7 @@ var SourceCodeProvider = class {
71412
71479
  usesType,
71413
71480
  community,
71414
71481
  coupledWith,
71415
- isDead
71482
+ isDead: Boolean(n4.isDead)
71416
71483
  };
71417
71484
  }
71418
71485
  async impact(symbolId, depth = 3) {
@@ -71427,21 +71494,22 @@ var SourceCodeProvider = class {
71427
71494
  };
71428
71495
  }
71429
71496
  async flowsFor(symbolId) {
71430
- const E2 = this.escapeId(symbolId);
71431
- const procQuery = `MATCH (s)-[r:CodeRelation]->(p:Process) WHERE s.id = "${E2}" AND r.rel_type = "step_in_process" RETURN p.id, p.name`;
71432
- const procRows = await this.rows(procQuery);
71433
- const flows = await Promise.all(
71434
- procRows.map(async (proc) => {
71435
- const pid = proc[0];
71436
- const pname = proc[1];
71437
- const PE = this.escapeId(String(pid ?? ""));
71438
- const stepQuery = `MATCH (s)-[r:CodeRelation]->(p:Process) WHERE p.id = "${PE}" AND r.rel_type = "step_in_process" RETURN s.id, s.name, s.file_path, s.start_line, r.step_number ORDER BY r.step_number`;
71439
- const stepRows = await this.rows(stepQuery);
71440
- const steps = stepRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
71441
- return { id: String(pid ?? ""), name: String(pname ?? ""), steps };
71442
- })
71443
- );
71444
- return flows;
71497
+ const res = await this.degrade(this.client.flows(symbolId), { processes: [], steps: [] });
71498
+ const steps = (res.steps ?? []).map((s5) => {
71499
+ const sym = { id: s5.nodeId, name: s5.name, filePath: s5.filePath };
71500
+ if (typeof s5.startLine === "number" && s5.startLine > 0) sym.startLine = s5.startLine;
71501
+ return sym;
71502
+ });
71503
+ return (res.processes ?? []).map((p5) => ({
71504
+ id: String(p5.id ?? ""),
71505
+ name: String(p5.name ?? ""),
71506
+ steps
71507
+ }));
71508
+ }
71509
+ /** Method symbols of a class (typed /api/class-methods). Used for class-seed walkthroughs. */
71510
+ async classMethods(file, className) {
71511
+ const methods = await this.degrade(this.client.classMethods(file, className), []);
71512
+ return methods.map((m5) => this.serializedToSymbol(m5)).filter((s5) => s5.id !== "" && s5.name !== "");
71445
71513
  }
71446
71514
  async detectChanges(diff) {
71447
71515
  const d3 = await this.client.diff(diff.base, diff.compare);
@@ -73724,6 +73792,296 @@ function shortTs2(iso) {
73724
73792
  return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
73725
73793
  }
73726
73794
 
73795
+ // ../../packages/connectors/src/axiom/client.ts
73796
+ init_cjs_shims();
73797
+ var REQUEST_TIMEOUT_MS2 = 8e3;
73798
+ var APL_PATH = "/v1/datasets/_apl?format=tabular";
73799
+ var AxiomClient = class {
73800
+ baseUrl;
73801
+ token;
73802
+ dataset;
73803
+ constructor(opts) {
73804
+ this.baseUrl = (opts.baseUrl ?? "https://api.axiom.co").replace(/\/$/, "");
73805
+ this.token = opts.token;
73806
+ this.dataset = opts.dataset;
73807
+ }
73808
+ async request(method, path, body2) {
73809
+ const url = `${this.baseUrl}${path}`;
73810
+ const init2 = {
73811
+ method,
73812
+ headers: {
73813
+ Authorization: `Bearer ${this.token}`,
73814
+ "Content-Type": "application/json"
73815
+ },
73816
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS2)
73817
+ };
73818
+ if (body2 !== void 0) init2.body = JSON.stringify(body2);
73819
+ const res = await fetch(url, init2);
73820
+ if (!res.ok) {
73821
+ const text2 = await res.text().catch(() => "");
73822
+ throw new Error(`Axiom ${method} ${path} -> ${res.status}: ${text2.slice(0, 200)}`);
73823
+ }
73824
+ return res.json();
73825
+ }
73826
+ /**
73827
+ * List the datasets reachable with the configured token. Returns [] on any error so
73828
+ * callers degrade gracefully (never throws past here).
73829
+ */
73830
+ async listDatasets() {
73831
+ try {
73832
+ const raw = await this.request("GET", "/v1/datasets");
73833
+ if (!Array.isArray(raw)) return [];
73834
+ return raw.map(parseDataset);
73835
+ } catch {
73836
+ return [];
73837
+ }
73838
+ }
73839
+ /**
73840
+ * Run an APL query over the configured time window. `startTime`/`endTime` are ISO
73841
+ * strings and bound the query server-side. Returns parsed log records, or [] on any
73842
+ * failure (never throws past here).
73843
+ */
73844
+ async query(apl, startTime, endTime) {
73845
+ try {
73846
+ const raw = await this.request("POST", APL_PATH, { apl, startTime, endTime });
73847
+ return parseTabular(raw);
73848
+ } catch {
73849
+ return [];
73850
+ }
73851
+ }
73852
+ /**
73853
+ * Cheap reachability probe: list datasets. Returns a structured status; never throws.
73854
+ */
73855
+ async health() {
73856
+ try {
73857
+ await this.request("GET", "/v1/datasets");
73858
+ return { ok: true, detail: `axiom reachable (${this.dataset})` };
73859
+ } catch (err2) {
73860
+ return { ok: false, detail: err2.message };
73861
+ }
73862
+ }
73863
+ };
73864
+ function parseDataset(raw) {
73865
+ const ds = { name: String(raw["name"] ?? "") };
73866
+ if (typeof raw["description"] === "string" && raw["description"] !== "") {
73867
+ ds.description = raw["description"];
73868
+ }
73869
+ return ds;
73870
+ }
73871
+ var AXIOM_ERROR_LEVELS = ["error", "warn", "fatal"];
73872
+ function datasetRef(dataset) {
73873
+ return `['${dataset.replace(/'/g, "")}']`;
73874
+ }
73875
+ function searchTerms(hintTerms) {
73876
+ return hintTerms.map((t2) => t2.trim()).filter((t2) => t2.length > 2);
73877
+ }
73878
+ function searchClause(hintTerms) {
73879
+ const terms = searchTerms(hintTerms);
73880
+ return terms.length > 0 ? ` | search ${terms.map((t2) => `"${t2.replace(/"/g, "")}"`).join(" or ")}` : "";
73881
+ }
73882
+ function errorLevelClause() {
73883
+ const levels = AXIOM_ERROR_LEVELS.map((l2) => `'${l2}'`).join(", ");
73884
+ return ` | where tolower(tostring(level)) in (${levels})`;
73885
+ }
73886
+ function buildApl(dataset, hintTerms = [], limit = 100) {
73887
+ const lim = Math.max(1, Math.min(limit, 1e3));
73888
+ return `${datasetRef(dataset)}${searchClause(hintTerms)} | sort by _time desc | limit ${lim}`;
73889
+ }
73890
+ function buildErrorSignatureApl(dataset, hintTerms = [], topN = 10) {
73891
+ const lim = Math.max(1, Math.min(topN, 1e3));
73892
+ return `${datasetRef(dataset)}${errorLevelClause()}${searchClause(hintTerms)} | summarize ['count'] = count(), ['_time'] = max(_time) by message, level | sort by ['count'] desc | limit ${lim}`;
73893
+ }
73894
+ function buildRecentErrorsApl(dataset, hintTerms = [], limit = 5) {
73895
+ const lim = Math.max(1, Math.min(limit, 1e3));
73896
+ return `${datasetRef(dataset)}${errorLevelClause()}${searchClause(hintTerms)} | sort by _time desc | limit ${lim}`;
73897
+ }
73898
+ function parseTabular(raw) {
73899
+ if (raw === null || raw === void 0 || typeof raw !== "object") return [];
73900
+ const tables = raw["tables"];
73901
+ if (!Array.isArray(tables)) return [];
73902
+ const records = [];
73903
+ for (const table of tables) {
73904
+ const fields = table["fields"];
73905
+ if (!Array.isArray(fields)) continue;
73906
+ const names = fields.map((f5) => String(f5["name"] ?? ""));
73907
+ const columns = table["columns"];
73908
+ const rows = table["rows"];
73909
+ if (Array.isArray(columns)) {
73910
+ const first = columns[0];
73911
+ const rowCount = Array.isArray(first) ? first.length : 0;
73912
+ for (let r = 0; r < rowCount; r++) {
73913
+ const row = {};
73914
+ for (let c4 = 0; c4 < names.length; c4++) {
73915
+ const col = columns[c4];
73916
+ if (Array.isArray(col)) row[names[c4]] = col[r];
73917
+ }
73918
+ records.push(toRecord(row));
73919
+ }
73920
+ } else if (Array.isArray(rows)) {
73921
+ for (const rowVals of rows) {
73922
+ if (!Array.isArray(rowVals)) continue;
73923
+ const row = {};
73924
+ for (let c4 = 0; c4 < names.length; c4++) {
73925
+ row[names[c4]] = rowVals[c4];
73926
+ }
73927
+ records.push(toRecord(row));
73928
+ }
73929
+ }
73930
+ }
73931
+ return records;
73932
+ }
73933
+ function toRecord(fields) {
73934
+ const rec = { fields };
73935
+ const t2 = fields["_time"];
73936
+ if (typeof t2 === "string") rec.timestamp = t2;
73937
+ return rec;
73938
+ }
73939
+
73940
+ // ../../packages/connectors/src/axiom/provider.ts
73941
+ init_cjs_shims();
73942
+ var DEFAULT_WINDOW_MS = 24 * 60 * 60 * 1e3;
73943
+ var AxiomProvider = class {
73944
+ constructor(client, opts) {
73945
+ this.client = client;
73946
+ this.opts = opts;
73947
+ }
73948
+ client;
73949
+ opts;
73950
+ id = "axiom";
73951
+ kind = "logs";
73952
+ /**
73953
+ * Collect log rows over the window, INCIDENT-AWARE. Rather than a broad newest-first
73954
+ * scan (which on a noisy dataset returns the 15 freshest `info` rows and misses a
73955
+ * high-volume error storm), this runs two error-biased queries in parallel:
73956
+ *
73957
+ * 1. TOP ERROR SIGNATURES — `summarize count() + max(_time) by message, level` over
73958
+ * error/warn/fatal rows (AND the hint terms), highest-volume first. This is what
73959
+ * surfaces a 3k-row `level=error` signature that a newest-first scan buries.
73960
+ * 2. RECENT RAW ERRORS — a few newest error rows with full fields, for grounding.
73961
+ *
73962
+ * Signatures lead so high-volume errors aren't crowded out. When NO error-level row
73963
+ * matches (neither query returns anything) it broadens to the all-levels fallback so
73964
+ * non-error datasets still produce evidence. Best-effort: a failing query yields [].
73965
+ * Never throws.
73966
+ */
73967
+ async collect(opts = {}) {
73968
+ const to = opts.to ?? (/* @__PURE__ */ new Date()).toISOString();
73969
+ const from = opts.from ?? new Date(Date.parse(to) - (this.opts.windowMs ?? DEFAULT_WINDOW_MS)).toISOString();
73970
+ const hintTerms = opts.hintTerms ?? [];
73971
+ const limit = this.opts.limit ?? 100;
73972
+ const topN = Math.max(1, Math.min(limit, 10));
73973
+ const recentN = Math.max(1, Math.min(limit, 5));
73974
+ const [signatures, recent] = await Promise.all([
73975
+ this.client.query(buildErrorSignatureApl(this.opts.dataset, hintTerms, topN), from, to),
73976
+ this.client.query(buildRecentErrorsApl(this.opts.dataset, hintTerms, recentN), from, to)
73977
+ ]);
73978
+ if (signatures.length === 0 && recent.length === 0) {
73979
+ return this.client.query(buildApl(this.opts.dataset, hintTerms, limit), from, to);
73980
+ }
73981
+ return [...signatures, ...recent];
73982
+ }
73983
+ /**
73984
+ * Synthesize Evidence from Axiom rows. One `kind: 'log'` Evidence per row, with the
73985
+ * row fields on the payload (under `source: 'axiom'`), a redacted message in the
73986
+ * title, and relevance weighted by hint-term match / recency / level.
73987
+ */
73988
+ toEvidence(records, hintTerms, collectedAt) {
73989
+ const query = `axiom ${this.opts.dataset}`;
73990
+ return records.map(
73991
+ (rec, i3) => this.recordToEvidence(rec, hintTerms, query, collectedAt, i3)
73992
+ );
73993
+ }
73994
+ /**
73995
+ * One-shot evidence query: collect rows + convert to Evidence. Preferred entry point
73996
+ * for the investigation engine. Degrades to [] on any failure so a flaky Axiom never
73997
+ * aborts an investigation.
73998
+ */
73999
+ async queryEvidence(opts = {}) {
74000
+ try {
74001
+ const hintTerms = opts.hintTerms ?? [];
74002
+ const records = await this.collect({
74003
+ ...opts.from !== void 0 ? { from: opts.from } : {},
74004
+ ...opts.to !== void 0 ? { to: opts.to } : {},
74005
+ hintTerms
74006
+ });
74007
+ return this.toEvidence(records, hintTerms, opts.collectedAt ?? (/* @__PURE__ */ new Date()).toISOString());
74008
+ } catch {
74009
+ return [];
74010
+ }
74011
+ }
74012
+ async health() {
74013
+ return this.client.health();
74014
+ }
74015
+ recordToEvidence(rec, hintTerms, query, collectedAt, index2) {
74016
+ const title = buildTitle2(rec, this.opts.dataset);
74017
+ const relevance = computeRelevance2(rec, hintTerms);
74018
+ const payload = {
74019
+ source: "axiom",
74020
+ dataset: this.opts.dataset,
74021
+ ...rec.fields
74022
+ };
74023
+ const ev = {
74024
+ id: `ev_axiom_${index2}`,
74025
+ source: "logs",
74026
+ kind: "log",
74027
+ title,
74028
+ relevance,
74029
+ payload,
74030
+ links: {},
74031
+ provenance: { query, collectedAt }
74032
+ };
74033
+ if (rec.timestamp !== void 0) ev.timestamp = rec.timestamp;
74034
+ return ev;
74035
+ }
74036
+ };
74037
+ var MESSAGE_FIELDS = ["message", "msg", "body", "event", "log", "_raw"];
74038
+ var LEVEL_FIELDS = ["level", "severity", "lvl", "loglevel", "status"];
74039
+ function pickField2(fields, candidates) {
74040
+ for (const key of candidates) {
74041
+ const v4 = fields[key];
74042
+ if (typeof v4 === "string" && v4 !== "") return v4;
74043
+ if (typeof v4 === "number") return String(v4);
74044
+ }
74045
+ return void 0;
74046
+ }
74047
+ function buildTitle2(rec, dataset) {
74048
+ const msg = pickField2(rec.fields, MESSAGE_FIELDS);
74049
+ const level = pickField2(rec.fields, LEVEL_FIELDS);
74050
+ const lvl = level ? `[${level}] ` : "";
74051
+ const body2 = msg ? redactSecrets(msg) : "(log event)";
74052
+ const rawCount = rec.fields["count"];
74053
+ const count = typeof rawCount === "number" && Number.isFinite(rawCount) && rawCount > 1 ? rawCount : void 0;
74054
+ if (count !== void 0) {
74055
+ const latest = rec.timestamp ? ` (latest ${shortTs3(rec.timestamp)})` : "";
74056
+ return `Axiom ${dataset}: ${lvl}${body2} \xD7${count.toLocaleString("en-US")}${latest}`.slice(0, 220);
74057
+ }
74058
+ const ts = rec.timestamp ? ` \xB7 ${shortTs3(rec.timestamp)}` : "";
74059
+ return `Axiom ${dataset}: ${lvl}${body2}${ts}`.slice(0, 220);
74060
+ }
74061
+ function computeRelevance2(rec, hintTerms, now = Date.now()) {
74062
+ let score = 0.6;
74063
+ const hay = JSON.stringify(rec.fields).toLowerCase();
74064
+ const domainTerms = hintTerms.filter((t2) => t2.length > 2);
74065
+ if (domainTerms.some((t2) => hay.includes(t2.toLowerCase()))) score += 0.2;
74066
+ if (rec.timestamp) {
74067
+ const ageMs = now - Date.parse(rec.timestamp);
74068
+ if (Number.isFinite(ageMs)) {
74069
+ if (ageMs <= 864e5) score += 0.1;
74070
+ else if (ageMs <= 7 * 864e5) score += 0.05;
74071
+ }
74072
+ }
74073
+ const level = (pickField2(rec.fields, LEVEL_FIELDS) ?? "").toLowerCase();
74074
+ if (level === "error" || level === "fatal" || level === "critical") score += 0.1;
74075
+ else if (level === "warn" || level === "warning") score += 0.05;
74076
+ const count = rec.fields["count"];
74077
+ if (typeof count === "number" && count >= 10) score += 0.1;
74078
+ return Math.min(0.95, Math.max(0.5, score));
74079
+ }
74080
+ function shortTs3(iso) {
74081
+ if (!iso || iso.length < 16) return iso || "\u2014";
74082
+ return `${iso.slice(5, 10)} ${iso.slice(11, 16)}`;
74083
+ }
74084
+
73727
74085
  // ../../packages/connectors/src/bullmq/client.ts
73728
74086
  init_cjs_shims();
73729
74087
  var import_ioredis = __toESM(require_built3(), 1);
@@ -74388,6 +74746,18 @@ function sentryForEnv(renv) {
74388
74746
  { org: s5.org, project: s5.project }
74389
74747
  );
74390
74748
  }
74749
+ function axiomForEnv(renv) {
74750
+ const a2 = renv.connectors.axiom;
74751
+ if (!a2 || !a2.token || !a2.dataset) return null;
74752
+ return new AxiomProvider(
74753
+ new AxiomClient({
74754
+ token: a2.token,
74755
+ dataset: a2.dataset,
74756
+ ...a2.url !== void 0 ? { baseUrl: a2.url } : {}
74757
+ }),
74758
+ { dataset: a2.dataset }
74759
+ );
74760
+ }
74391
74761
  function codeForRepo(config2, repoName) {
74392
74762
  const renv = resolveEnvironment(config2, { project: repoName });
74393
74763
  const c4 = codeForEnv(renv);
@@ -74563,6 +74933,9 @@ init_cjs_shims();
74563
74933
  // ../../packages/connectors/src/sentry/index.ts
74564
74934
  init_cjs_shims();
74565
74935
 
74936
+ // ../../packages/connectors/src/axiom/index.ts
74937
+ init_cjs_shims();
74938
+
74566
74939
  // ../../packages/connectors/src/bullmq/index.ts
74567
74940
  init_cjs_shims();
74568
74941
 
@@ -90486,6 +90859,32 @@ async function checkEnv(renv, deps) {
90486
90859
  ` ${mark("pending")} ${import_picocolors3.default.bold("Sentry")} ${import_picocolors3.default.dim("not configured")}`
90487
90860
  );
90488
90861
  }
90862
+ const axiomCfg = renv.connectors.axiom;
90863
+ if (axiomCfg) {
90864
+ const label = axiomCfg.dataset ?? "(no dataset)";
90865
+ const axiom = (deps?.axiomFactory ?? axiomForEnv)(renv);
90866
+ if (axiom) {
90867
+ const h3 = await axiom.health();
90868
+ if (!h3.ok) {
90869
+ console.log(
90870
+ ` ${mark(false)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`unreachable \xB7 ${label}`)}`
90871
+ );
90872
+ allOk = false;
90873
+ } else {
90874
+ console.log(
90875
+ ` ${mark(true)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`reachable \xB7 ${label}`)}`
90876
+ );
90877
+ }
90878
+ } else {
90879
+ console.log(
90880
+ ` ${mark(false)} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim(`configured (${label}) but API token not set`)}`
90881
+ );
90882
+ }
90883
+ } else {
90884
+ console.log(
90885
+ ` ${mark("pending")} ${import_picocolors3.default.bold("Axiom")} ${import_picocolors3.default.dim("not configured")}`
90886
+ );
90887
+ }
90489
90888
  const redisCfg = renv.connectors.redis;
90490
90889
  if (redisCfg?.url) {
90491
90890
  const server = redisServerLabel(redisCfg.url);
@@ -92915,12 +93314,15 @@ function detectMissingEvidence(r, connectors = {}, mode) {
92915
93314
  if (!hasLog && !sourceImpact) {
92916
93315
  let logWhy;
92917
93316
  let logNextSource;
92918
- if (!connectors.elasticsearch && !connectors.sentry) {
92919
- logWhy = "No Elasticsearch connector (nor Sentry) configured for this environment \u2014 no runtime error evidence.";
92920
- logNextSource = "Add an `elasticsearch` and/or `sentry` connector to the project/environment";
93317
+ if (!connectors.elasticsearch && !connectors.sentry && !connectors.axiom) {
93318
+ logWhy = "No Elasticsearch connector (nor Sentry / Axiom) configured for this environment \u2014 no runtime error evidence.";
93319
+ logNextSource = "Add an `elasticsearch`, `sentry`, and/or `axiom` connector to the project/environment";
92921
93320
  } else if (!connectors.elasticsearch && connectors.sentry) {
92922
93321
  logWhy = connectors.sentryCollected ? "No open Sentry issues matched in the window \u2014 cannot confirm the actual error signatures." : "Sentry collection failed or timed out \u2014 cannot confirm the actual error signatures.";
92923
93322
  logNextSource = connectors.sentryCollected ? "Widen the window (--since) or check the Sentry project for open issues" : "Check the Sentry auth token / project, then retry";
93323
+ } else if (!connectors.elasticsearch && connectors.axiom) {
93324
+ logWhy = connectors.axiomCollected ? "No Axiom log rows matched in the window \u2014 cannot confirm the actual error signatures." : "Axiom log collection failed or timed out \u2014 cannot confirm the actual error signatures.";
93325
+ logNextSource = connectors.axiomCollected ? "Widen the window (--since) or check the Axiom dataset for matching rows" : "Check the Axiom API token / dataset, then retry";
92924
93326
  } else if (connectors.logsCompatibilityError) {
92925
93327
  logWhy = `Elasticsearch field mapping is incompatible with the index \u2014 log collection was blocked. ` + connectors.logsCompatibilityError;
92926
93328
  logNextSource = "Fix fields.* overrides in your connector config or choose the correct preset (meritt / ecs)";
@@ -92931,7 +93333,7 @@ function detectMissingEvidence(r, connectors = {}, mode) {
92931
93333
  logWhy = "No error logs matched in the window \u2014 cannot confirm the actual error signatures.";
92932
93334
  logNextSource = "Widen the window (--since) or inspect `horus logs <service>`";
92933
93335
  }
92934
- const logRouteHint = !connectors.elasticsearch && !connectors.sentry ? { nextTool: "connect", args: "elasticsearch", reason: logNextSource } : { nextTool: "logs", args: service, reason: logNextSource };
93336
+ const logRouteHint = !connectors.elasticsearch && !connectors.sentry && !connectors.axiom ? { nextTool: "connect", args: "elasticsearch", reason: logNextSource } : { nextTool: "logs", args: service, reason: logNextSource };
92935
93337
  gaps.push({
92936
93338
  dimension: "logs",
92937
93339
  why: logWhy,
@@ -92958,8 +93360,10 @@ function detectMissingEvidence(r, connectors = {}, mode) {
92958
93360
  if (hasQueueTopology && !hasQueueState && !sourceImpact) {
92959
93361
  gaps.push({
92960
93362
  dimension: "queue runtime state",
92961
- 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.",
92962
- 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",
93363
+ 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 connector for live queue depth/failures.",
93364
+ // Stack-agnostic: the tip names Redis (the connector to add) but NOT a specific queue
93365
+ // library — "BullMQ" is Node-only and was leaking onto Python/Redis repos (HOR-428).
93366
+ nextSource: connectors.redis ? "Run `horus queues --live` to see real-time queue depths and failed-job counts" : "Add a `redis` connector to read live queue state",
92963
93367
  confidenceImpact: 0.1,
92964
93368
  // Redis wired → read live state with `queues --live`; otherwise `connect redis` first.
92965
93369
  routeHint: connectors.redis ? {
@@ -92969,7 +93373,7 @@ function detectMissingEvidence(r, connectors = {}, mode) {
92969
93373
  } : {
92970
93374
  nextTool: "connect",
92971
93375
  args: "redis",
92972
- reason: "Add a `redis` connector to read live BullMQ state"
93376
+ reason: "Add a `redis` connector to read live queue state"
92973
93377
  }
92974
93378
  });
92975
93379
  blindSpots.push("Cannot determine if the queue is actually backed up.");
@@ -93047,10 +93451,11 @@ function buildEntry(source, configured, evidenceCount, failed, detail) {
93047
93451
  }
93048
93452
  function buildRuntimeSourceStatus(evidence2, connectors) {
93049
93453
  const logsCount = evidence2.filter((e) => e.source === "logs").length;
93050
- const logsConfigured = !!(connectors.elasticsearch || connectors.sentry);
93454
+ const logsConfigured = !!(connectors.elasticsearch || connectors.sentry || connectors.axiom);
93051
93455
  const esFailed = !!connectors.elasticsearch && !connectors.logsCollected;
93052
93456
  const sentryFailed = !!connectors.sentry && !connectors.sentryCollected;
93053
- const logsFailed = logsConfigured && logsCount === 0 && (esFailed || sentryFailed);
93457
+ const axiomFailed = !!connectors.axiom && !connectors.axiomCollected;
93458
+ const logsFailed = logsConfigured && logsCount === 0 && (esFailed || sentryFailed || axiomFailed);
93054
93459
  const metricsCount = evidence2.filter((e) => e.source === "metrics").length;
93055
93460
  const metricsConfigured = !!connectors.grafana;
93056
93461
  const metricsFailed = metricsConfigured && !connectors.metricsCollected;
@@ -94016,6 +94421,17 @@ function classifyLogRelevance(sigKey, sigServices, seedTerms, inputService) {
94016
94421
  relevanceReason: "no structural link to seed \u2014 ambient runtime noise"
94017
94422
  };
94018
94423
  }
94424
+ var AXIOM_MESSAGE_FIELDS = ["message", "msg", "body", "event", "log", "_raw"];
94425
+ var AXIOM_LEVEL_FIELDS = ["level", "severity", "lvl", "loglevel", "status"];
94426
+ var AXIOM_SERVICE_FIELDS = ["service", "app", "logger", "source", "namespace"];
94427
+ function pickAxiomField(fields, candidates) {
94428
+ for (const key of candidates) {
94429
+ const v4 = fields[key];
94430
+ if (typeof v4 === "string" && v4 !== "") return v4;
94431
+ if (typeof v4 === "number") return String(v4);
94432
+ }
94433
+ return void 0;
94434
+ }
94019
94435
  function extractEventCodes(...texts) {
94020
94436
  const RE = /\b(?=[A-Z0-9_]*[0-9_])[A-Z][A-Z0-9_]{3,}\b/g;
94021
94437
  const out2 = [];
@@ -94203,20 +94619,10 @@ function isClassSymbol(sym) {
94203
94619
  return sym !== void 0 && sym.id.split(":", 1)[0]?.toLowerCase() === "class";
94204
94620
  }
94205
94621
  async function fetchClassMethods(code, cls) {
94206
- const safe = (s5) => s5.replace(/["\\]/g, "");
94207
- const name2 = safe(cls.name);
94208
- if (!name2) return [];
94209
- const file = safe(cls.filePath);
94622
+ const name2 = cls.name?.trim();
94623
+ if (!name2 || typeof code.classMethods !== "function") return [];
94210
94624
  try {
94211
- const res = await code.cypher(
94212
- `MATCH (m:Method) WHERE m.class_name = "${name2}"` + (file ? ` AND m.file_path = "${file}"` : "") + " RETURN m.id, m.name, m.file_path, m.start_line ORDER BY m.start_line LIMIT 40"
94213
- );
94214
- return res.rows.map((r) => ({
94215
- id: String(r[0] ?? ""),
94216
- name: String(r[1] ?? ""),
94217
- filePath: String(r[2] ?? ""),
94218
- ...typeof r[3] === "number" ? { startLine: r[3] } : {}
94219
- })).filter((s5) => s5.id !== "" && s5.name !== "");
94625
+ return await code.classMethods(cls.filePath ?? "", name2);
94220
94626
  } catch {
94221
94627
  return [];
94222
94628
  }
@@ -94965,6 +95371,61 @@ async function investigate(input, deps) {
94965
95371
  sentryCollected = false;
94966
95372
  }
94967
95373
  }
95374
+ let axiomCollected = false;
95375
+ if (deps.axiom) {
95376
+ try {
95377
+ const from = logWindowFrom(input.logsSince ?? input.since);
95378
+ const to = (/* @__PURE__ */ new Date()).toISOString();
95379
+ const axiomSeedBase = top ? top.filePath.split("/").pop() ?? "" : "";
95380
+ const axiomTerms = top ? [.../* @__PURE__ */ new Set([...tokenize(hint), ...tokenize(top.name), ...tokenize(axiomSeedBase)])] : [...new Set(tokenize(hint))];
95381
+ const records = await deps.axiom.collect({ from, to, hintTerms: axiomTerms });
95382
+ axiomCollected = true;
95383
+ for (const rec of records.slice(0, 15)) {
95384
+ const msg = pickAxiomField(rec.fields, AXIOM_MESSAGE_FIELDS);
95385
+ const level = pickAxiomField(rec.fields, AXIOM_LEVEL_FIELDS);
95386
+ const svc = pickAxiomField(rec.fields, AXIOM_SERVICE_FIELDS) ?? "";
95387
+ const sigKey = [msg ?? "", svc].filter(Boolean).join(" ");
95388
+ const { relevanceClass, relevanceReason } = classifyLogRelevance(
95389
+ sigKey,
95390
+ svc ? [svc] : [],
95391
+ axiomTerms,
95392
+ input.service
95393
+ );
95394
+ const lvl = level ? `[${level}] ` : "";
95395
+ const body2 = msg ?? "(log event)";
95396
+ const tag = relevanceClass === "ambient" ? " [ambient]" : "";
95397
+ const title = `Axiom log: ${lvl}${body2}${tag}`.slice(0, 220);
95398
+ const isError = ["error", "fatal", "critical"].includes((level ?? "").toLowerCase());
95399
+ const ev = mkEv(
95400
+ "log",
95401
+ title,
95402
+ {
95403
+ source: "axiom",
95404
+ signature: msg ?? null,
95405
+ level: level ?? null,
95406
+ service: svc || null,
95407
+ relevanceClass,
95408
+ relevanceReason,
95409
+ fields: rec.fields
95410
+ },
95411
+ {},
95412
+ rec.timestamp,
95413
+ // Direct: weighted up for error-level rows. Ambient: demoted so unrelated
95414
+ // high-volume log lines don't inflate confidence.
95415
+ relevanceClass === "direct" ? isError ? 0.9 : 0.75 : 0.4
95416
+ );
95417
+ logEvIds.push(ev.id);
95418
+ if (relevanceClass === "direct") {
95419
+ directLogEvIds.push(ev.id);
95420
+ directSignatures.push({ key: msg ?? "(log event)", count: 1, message: msg ?? "" });
95421
+ } else {
95422
+ ambientLogEvIds.push(ev.id);
95423
+ }
95424
+ }
95425
+ } catch {
95426
+ axiomCollected = false;
95427
+ }
95428
+ }
94968
95429
  let stateAnalysis = null;
94969
95430
  const stateEvIds = [];
94970
95431
  const stateCollections = /* @__PURE__ */ new Set();
@@ -95725,6 +96186,10 @@ async function investigate(input, deps) {
95725
96186
  // its collection ran (distinguishes "no open issues" from "collection failed").
95726
96187
  sentry: deps.sentry != null,
95727
96188
  sentryCollected,
96189
+ // Axiom is configured iff a provider was built; axiomCollected tracks whether its
96190
+ // collection ran (distinguishes "no matching rows" from "collection failed").
96191
+ axiom: deps.axiom != null,
96192
+ axiomCollected,
95728
96193
  metricsCollected,
95729
96194
  metricsFailureReason,
95730
96195
  logsCollected,
@@ -95736,6 +96201,8 @@ async function investigate(input, deps) {
95736
96201
  postgres: deps.postgres != null,
95737
96202
  sentry: deps.sentry != null,
95738
96203
  sentryCollected,
96204
+ axiom: deps.axiom != null,
96205
+ axiomCollected,
95739
96206
  grafana: deps.metrics != null,
95740
96207
  queue: deps.queue != null,
95741
96208
  metricsCollected,
@@ -101056,31 +101523,50 @@ function dedupeBySymbolFile(rows) {
101056
101523
  // ../../packages/stitcher/src/stitch.ts
101057
101524
  init_cjs_shims();
101058
101525
  async function stitch(client, db, opts = {}) {
101059
- const producerRows = (await client.cypher(
101526
+ const producerRows = await client.contentSearch([
101060
101527
  // "new Queue" (no paren) so generic-typed `new Queue<T>(...)` also matches.
101061
- 'MATCH (n) WHERE n.content CONTAINS "@InjectQueue(" OR n.content CONTAINS "new Queue" OR n.content CONTAINS "QUEUE_NAME" OR n.content CONTAINS "QueueName" OR n.content CONTAINS "enum " OR n.content CONTAINS "Object.values(" OR n.content CONTAINS "]: (" RETURN n.name, n.file_path, n.content'
101062
- )).rows;
101528
+ "@InjectQueue(",
101529
+ "new Queue",
101530
+ "QUEUE_NAME",
101531
+ "QueueName",
101532
+ "enum ",
101533
+ "Object.values(",
101534
+ // HOR-341: dispatch tables `[Enum.MEMBER]: () => this.ctrl.handler()` map runtime
101535
+ // queues to owning code, and often live in a file with NO new Queue/Worker of their
101536
+ // own (e.g. a scheduler controller) — so pull computed-key arrow entries too.
101537
+ "]: ("
101538
+ ]);
101063
101539
  const producerClasses = producerRows.map((r) => ({
101064
- name: String(r[0] ?? ""),
101065
- filePath: String(r[1] ?? ""),
101066
- content: String(r[2] ?? "")
101540
+ name: r.name,
101541
+ filePath: r.filePath,
101542
+ content: r.content
101067
101543
  }));
101068
- const workerRows = (await client.cypher(
101069
- // "new Worker" (no paren) so generic-typed `new Worker<T>(...)` also matches.
101070
- 'MATCH (n) WHERE n.content CONTAINS "@Processor(" OR n.content CONTAINS "new Worker" RETURN n.name, n.file_path, n.content'
101071
- )).rows;
101544
+ const workerRows = await client.contentSearch(["@Processor(", "new Worker"]);
101072
101545
  const workerFiles = workerRows.map((r) => ({
101073
- filePath: String(r[1] ?? ""),
101074
- content: String(r[2] ?? "")
101546
+ filePath: r.filePath,
101547
+ content: r.content
101075
101548
  }));
101076
101549
  const graph = extractQueueGraph({ producerClasses, workerFiles });
101077
- const celeryRows = (await client.cypher(
101078
- 'MATCH (n) WHERE n.content CONTAINS ".delay(" OR n.content CONTAINS ".apply_async(" OR n.content CONTAINS ".schedule(" OR n.content CONTAINS ".defer(" OR n.content CONTAINS ".defer_async(" OR n.content CONTAINS ".send(" OR n.content CONTAINS ".enqueue_job(" OR n.content CONTAINS ".enqueue(" OR n.content CONTAINS "@shared_task" OR n.content CONTAINS "@task" OR n.content CONTAINS ".task" OR n.content CONTAINS "@actor" OR n.content CONTAINS "periodic_task" OR n.content CONTAINS "db_task" RETURN n.name, n.file_path, n.content'
101079
- )).rows;
101550
+ const celeryRows = await client.contentSearch([
101551
+ ".delay(",
101552
+ ".apply_async(",
101553
+ ".schedule(",
101554
+ ".defer(",
101555
+ ".defer_async(",
101556
+ ".send(",
101557
+ ".enqueue_job(",
101558
+ ".enqueue(",
101559
+ "@shared_task",
101560
+ "@task",
101561
+ ".task",
101562
+ "@actor",
101563
+ "periodic_task",
101564
+ "db_task"
101565
+ ]);
101080
101566
  const celeryNodes = celeryRows.map((r) => ({
101081
- name: String(r[0] ?? ""),
101082
- filePath: String(r[1] ?? ""),
101083
- content: String(r[2] ?? "")
101567
+ name: r.name,
101568
+ filePath: r.filePath,
101569
+ content: r.content
101084
101570
  }));
101085
101571
  const celery = extractCeleryQueueGraph(celeryNodes);
101086
101572
  const dramatiq = extractDramatiqQueueGraph(celeryNodes);
@@ -101232,6 +101718,10 @@ async function stitchQueueMap(hostUrl, dbUrl, label) {
101232
101718
  console.log(
101233
101719
  import_picocolors6.default.dim(`[${label}] `) + `Stitched ${summary.edges} queue edge(s) across ${summary.queues} queue(s) \u2014 ${summary.producers} producer(s), ${summary.workers} worker(s).`
101234
101720
  );
101721
+ } catch (err2) {
101722
+ console.log(
101723
+ import_picocolors6.default.dim(`[${label}] `) + `Queue map skipped (degraded): ${err2.message}. Stitched 0 queue edge(s).`
101724
+ );
101235
101725
  } finally {
101236
101726
  await sql2.end();
101237
101727
  }
@@ -101257,60 +101747,49 @@ function findConfiguredRepo(config2, root) {
101257
101747
  return null;
101258
101748
  }
101259
101749
  var SOURCE_GRAPH_PULL_LIMIT = 5e3;
101750
+ var SOURCE_SYMBOL_LABELS = [
101751
+ "class",
101752
+ "interface",
101753
+ "type_alias",
101754
+ "function",
101755
+ "method",
101756
+ "enum"
101757
+ ];
101260
101758
  async function extractSourceGraph(hostUrl, repo) {
101261
101759
  const client = new SourceHttpClient({ baseUrl: hostUrl });
101262
101760
  const N2 = SOURCE_GRAPH_PULL_LIMIT;
101263
- const rows = async (query) => {
101264
- try {
101265
- return (await client.cypher(query)).rows;
101266
- } catch {
101267
- return [];
101268
- }
101269
- };
101270
- const symbolRows = await rows(
101271
- `MATCH (n) WHERE label(n) IN ["Class","Interface","TypeAlias","Function","Method","Enum"] RETURN label(n), n.name, n.file_path, n.start_line, n.end_line, n.class_name, n.is_entry_point, n.is_exported, n.signature LIMIT ${N2 * 2}`
101272
- );
101761
+ const symbolRows = await client.symbolsByLabel(SOURCE_SYMBOL_LABELS, N2 * 2).catch(() => []);
101273
101762
  const symbols = symbolRows.map((r) => ({
101274
- label: String(r[0] ?? ""),
101275
- name: String(r[1] ?? ""),
101276
- filePath: r[2] != null ? String(r[2]) : void 0,
101277
- startLine: r[3] != null ? Number(r[3]) : void 0,
101278
- endLine: r[4] != null ? Number(r[4]) : void 0,
101279
- className: r[5] != null ? String(r[5]) : void 0,
101280
- isEntryPoint: Boolean(r[6]),
101281
- isExported: Boolean(r[7]),
101282
- signature: r[8] != null ? String(r[8]) : void 0
101763
+ label: r.label,
101764
+ name: r.name,
101765
+ filePath: r.filePath || void 0,
101766
+ startLine: r.startLine > 0 ? r.startLine : void 0,
101767
+ endLine: r.endLine > 0 ? r.endLine : void 0,
101768
+ className: r.className || void 0,
101769
+ isEntryPoint: Boolean(r.isEntryPoint),
101770
+ isExported: Boolean(r.isExported),
101771
+ signature: r.signature || void 0
101283
101772
  }));
101284
- const communityRows = await rows(
101285
- `MATCH (n:Community) RETURN n.id, n.name LIMIT ${N2}`
101286
- );
101287
- const communities = communityRows.map((r) => ({
101288
- id: r[0] != null ? String(r[0]) : void 0,
101289
- name: String(r[1] ?? "")
101773
+ const nameById = /* @__PURE__ */ new Map();
101774
+ for (const r of symbolRows) {
101775
+ if (r.id) nameById.set(r.id, { name: r.name, filePath: r.filePath || void 0 });
101776
+ }
101777
+ const communityRows = await client.communities().catch(() => []);
101778
+ const communities = communityRows.map((c4) => ({
101779
+ id: c4.id ? String(c4.id) : void 0,
101780
+ name: String(c4.name ?? "")
101781
+ }));
101782
+ const processRows = await client.processes().catch(() => []);
101783
+ const processes = processRows.map((p5) => ({
101784
+ name: String(p5.name ?? ""),
101785
+ steps: (p5.steps ?? []).map((s5) => {
101786
+ const resolved = nameById.get(s5.nodeId);
101787
+ const component = resolved?.name ?? s5.nodeId;
101788
+ if (!component) return null;
101789
+ const detail = resolved?.filePath;
101790
+ return { component, ...detail ? { detail } : {} };
101791
+ }).filter((s5) => s5 !== null)
101290
101792
  }));
101291
- const processRows = await rows(`MATCH (n:Process) RETURN n.id, n.name LIMIT ${N2}`);
101292
- const stepRows = await rows(
101293
- `MATCH (s)-[r:CodeRelation]->(p:Process) WHERE r.rel_type = "step_in_process" RETURN p.id, s.name, s.file_path, r.step_number ORDER BY p.id, r.step_number LIMIT ${N2 * 4}`
101294
- );
101295
- const stepsByProcess = /* @__PURE__ */ new Map();
101296
- for (const r of stepRows) {
101297
- const pid = String(r[0] ?? "");
101298
- if (!pid) continue;
101299
- const component = String(r[1] ?? "");
101300
- if (!component) continue;
101301
- const file = r[2] != null ? String(r[2]) : void 0;
101302
- const list = stepsByProcess.get(pid) ?? [];
101303
- list.push({ component, ...file ? { detail: file } : {} });
101304
- stepsByProcess.set(pid, list);
101305
- }
101306
- const processes = processRows.map((r) => {
101307
- const id = r[0] != null ? String(r[0]) : void 0;
101308
- return {
101309
- id,
101310
- name: String(r[1] ?? ""),
101311
- steps: id ? stepsByProcess.get(id) ?? [] : []
101312
- };
101313
- });
101314
101793
  if (symbols.length === 0 && communities.length === 0 && processes.length === 0) {
101315
101794
  return null;
101316
101795
  }
@@ -101484,7 +101963,13 @@ async function runIndex(opts) {
101484
101963
  );
101485
101964
  return 1;
101486
101965
  }
101487
- await stitchQueueMap(hostUrl, dbUrl, label);
101966
+ try {
101967
+ await stitchQueueMap(hostUrl, dbUrl, label);
101968
+ } catch (err2) {
101969
+ console.log(
101970
+ import_picocolors6.default.dim(`[${label}] `) + `Queue map skipped (degraded): ${err2.message}. Stitched 0 queue edge(s).`
101971
+ );
101972
+ }
101488
101973
  await buildKnowledgeIndex(root, config2, label, configuredProject, opts, hostUrl);
101489
101974
  const hasLocalConfig = discoverLocalConfig(root) !== null;
101490
101975
  if (!isConfigured && !hasLocalConfig) {
@@ -114838,6 +115323,7 @@ async function buildInvestigationContext(renv, opts) {
114838
115323
  const mongo = mongoForEnv(renv);
114839
115324
  const postgres2 = postgresForEnv(renv);
114840
115325
  const sentry = sentryForEnv(renv);
115326
+ const axiom = axiomForEnv(renv);
114841
115327
  const queue = queueForEnv(renv);
114842
115328
  const redisState = redisStateForEnv(renv);
114843
115329
  const metrics = metricsForEnv(renv);
@@ -114854,6 +115340,7 @@ async function buildInvestigationContext(renv, opts) {
114854
115340
  mongo,
114855
115341
  postgres: postgres2,
114856
115342
  sentry,
115343
+ axiom,
114857
115344
  queue,
114858
115345
  redisState,
114859
115346
  metrics,
@@ -114882,6 +115369,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
114882
115369
  mongo: ctx.mongo,
114883
115370
  postgres: ctx.postgres,
114884
115371
  sentry: ctx.sentry,
115372
+ axiom: ctx.axiom,
114885
115373
  queue: ctx.queue,
114886
115374
  redisState: ctx.redisState,
114887
115375
  metrics: ctx.metrics,
@@ -114892,6 +115380,7 @@ async function runOneInvestigation(input, ctx, opts = {}) {
114892
115380
  mongodb: !!renv.connectors.mongodb?.url,
114893
115381
  postgres: !!renv.connectors.postgres?.url,
114894
115382
  sentry: !!renv.connectors.sentry,
115383
+ axiom: !!renv.connectors.axiom,
114895
115384
  redis: !!renv.connectors.redis?.url,
114896
115385
  queue: !!ctx.queue
114897
115386
  }
@@ -117828,6 +118317,8 @@ function outcomeLabelToJSON(l2) {
117828
118317
  at: l2.at instanceof Date ? l2.at.toISOString() : l2.at
117829
118318
  };
117830
118319
  }
118320
+ var DEFAULT_ACCURACY_WINDOW_DAYS = 30;
118321
+ var MS_PER_DAY2 = 24 * 60 * 60 * 1e3;
117831
118322
  async function runMemoryAccuracy(opts) {
117832
118323
  try {
117833
118324
  let source;
@@ -117839,12 +118330,29 @@ async function runMemoryAccuracy(opts) {
117839
118330
  source = opts.source;
117840
118331
  }
117841
118332
  let since;
117842
- if (opts.days !== void 0) {
118333
+ let window2;
118334
+ if (opts.all) {
118335
+ window2 = "all time";
118336
+ } else if (opts.since !== void 0) {
118337
+ const parsed = new Date(opts.since);
118338
+ if (Number.isNaN(parsed.getTime())) {
118339
+ console.error(
118340
+ import_picocolors31.default.red("--since must be a parseable date (e.g. 2026-06-01 or an ISO timestamp).")
118341
+ );
118342
+ return 1;
118343
+ }
118344
+ since = parsed;
118345
+ window2 = `since ${parsed.toISOString().slice(0, 10)}`;
118346
+ } else if (opts.days !== void 0) {
117843
118347
  if (!Number.isFinite(opts.days) || opts.days <= 0) {
117844
118348
  console.error(import_picocolors31.default.red("--days must be a positive number."));
117845
118349
  return 1;
117846
118350
  }
117847
- since = new Date(Date.now() - opts.days * 24 * 60 * 60 * 1e3);
118351
+ since = new Date(Date.now() - opts.days * MS_PER_DAY2);
118352
+ window2 = `last ${opts.days} day(s)`;
118353
+ } else {
118354
+ since = new Date(Date.now() - DEFAULT_ACCURACY_WINDOW_DAYS * MS_PER_DAY2);
118355
+ window2 = `last ${DEFAULT_ACCURACY_WINDOW_DAYS} day(s) (default \u2014 pass --all for full history)`;
117848
118356
  }
117849
118357
  const config2 = await loadConfig(opts.config);
117850
118358
  const project = resolveProject(config2, opts.repo);
@@ -117865,6 +118373,7 @@ async function runMemoryAccuracy(opts) {
117865
118373
  project,
117866
118374
  source: source ?? null,
117867
118375
  since: since ? since.toISOString() : null,
118376
+ window: window2,
117868
118377
  summary,
117869
118378
  labels: labels.map(outcomeLabelToJSON)
117870
118379
  },
@@ -117874,11 +118383,11 @@ async function runMemoryAccuracy(opts) {
117874
118383
  );
117875
118384
  } else {
117876
118385
  const pct = (n4) => `${(n4 * 100).toFixed(0)}%`;
117877
- console.log(import_picocolors31.default.bold(`Investigation accuracy \u2014 ${project}`));
118386
+ console.log(import_picocolors31.default.bold(`Investigation accuracy \u2014 ${project}`) + import_picocolors31.default.dim(` \xB7 window: ${window2}`));
117878
118387
  if (summary.evaluated === 0) {
117879
118388
  console.log(
117880
118389
  import_picocolors31.default.dim(
117881
- "No outcome labels yet. Confirm an investigation (`horus memory confirm <id>`) or leave feedback (`horus feedback <id> --resolved yes|partly|no`) to start the eval set."
118390
+ `No outcome labels in this window (${window2}). Confirm an investigation (\`horus memory confirm <id>\`) or leave feedback (\`horus feedback <id> --resolved yes|partly|no\`) to add to the eval set` + (opts.all ? "." : ", or pass --all to include older labels.")
117882
118391
  )
117883
118392
  );
117884
118393
  } else {
@@ -119060,6 +119569,148 @@ async function checkboxSearch(opts) {
119060
119569
  render();
119061
119570
  });
119062
119571
  }
119572
+ async function selectSearch(opts) {
119573
+ const { message, choices } = opts;
119574
+ const pageSize = opts.pageSize ?? 10;
119575
+ const input = opts.input ?? process.stdin;
119576
+ const output = opts.output ?? process.stdout;
119577
+ if (!isInteractive(input)) {
119578
+ throw new Error("selectSearch requires an interactive TTY");
119579
+ }
119580
+ if (choices.length === 0) {
119581
+ throw new ExitPromptError("No choices to select from");
119582
+ }
119583
+ return new Promise((resolve12, reject) => {
119584
+ let filter = "";
119585
+ let cursor = 0;
119586
+ let filtered = choices;
119587
+ let visibleOffset = 0;
119588
+ let lastRenderLines = 0;
119589
+ const wasRaw = input.isRaw;
119590
+ function updateFiltered() {
119591
+ const f5 = filter.toLowerCase();
119592
+ filtered = f5 ? choices.filter((c4) => c4.toLowerCase().includes(f5)) : choices;
119593
+ cursor = Math.min(cursor, Math.max(0, filtered.length - 1));
119594
+ updateVisibleOffset();
119595
+ }
119596
+ function updateVisibleOffset() {
119597
+ if (cursor < visibleOffset) {
119598
+ visibleOffset = cursor;
119599
+ } else if (cursor >= visibleOffset + pageSize) {
119600
+ visibleOffset = cursor - pageSize + 1;
119601
+ }
119602
+ const maxOffset = Math.max(0, filtered.length - pageSize);
119603
+ visibleOffset = Math.max(0, Math.min(visibleOffset, maxOffset));
119604
+ }
119605
+ function clear() {
119606
+ output.write("\x1B[?25l");
119607
+ if (lastRenderLines > 0) {
119608
+ output.write(`\x1B[${lastRenderLines}A`);
119609
+ output.write("\x1B[0J");
119610
+ }
119611
+ }
119612
+ function render() {
119613
+ clear();
119614
+ const lines = [];
119615
+ lines.push(
119616
+ `? ${import_picocolors39.default.bold(message)} ${import_picocolors39.default.dim("(\u2191\u2193 navigate \u2022 enter confirm \u2022 type filter)")}`
119617
+ );
119618
+ const visible = filtered.slice(visibleOffset, visibleOffset + pageSize);
119619
+ for (let i3 = 0; i3 < visible.length; i3++) {
119620
+ const idx = visibleOffset + i3;
119621
+ const choice = visible[i3];
119622
+ const isCursor = idx === cursor;
119623
+ const pointer = isCursor ? import_picocolors39.default.cyan("\u276F") : " ";
119624
+ const label = isCursor ? import_picocolors39.default.cyan(choice) : choice;
119625
+ lines.push(` ${pointer} ${label}`);
119626
+ }
119627
+ if (filtered.length === 0) {
119628
+ lines.push(import_picocolors39.default.dim(" No matches"));
119629
+ }
119630
+ const statusParts = [`${filtered.length}/${choices.length} matches`];
119631
+ if (filter) statusParts.push(`filter: ${filter}`);
119632
+ lines.push(import_picocolors39.default.dim(` ${statusParts.join(" \xB7 ")}`));
119633
+ output.write(lines.join("\n"));
119634
+ lastRenderLines = lines.length;
119635
+ }
119636
+ function finish(value) {
119637
+ cleanup();
119638
+ output.write(`
119639
+ ? ${import_picocolors39.default.bold(message)} ${value}
119640
+ `);
119641
+ resolve12(value);
119642
+ }
119643
+ function cancel() {
119644
+ cleanup();
119645
+ reject(new ExitPromptError());
119646
+ }
119647
+ function cleanup() {
119648
+ input.setRawMode(wasRaw);
119649
+ input.pause();
119650
+ input.removeListener("data", onData);
119651
+ output.write("\x1B[?25h");
119652
+ }
119653
+ function onData(chunk) {
119654
+ const s5 = chunk.toString();
119655
+ if (s5 === "") {
119656
+ cancel();
119657
+ return;
119658
+ }
119659
+ if (s5 === "\x1B") {
119660
+ if (filter) {
119661
+ filter = "";
119662
+ cursor = 0;
119663
+ updateFiltered();
119664
+ } else {
119665
+ cancel();
119666
+ }
119667
+ render();
119668
+ return;
119669
+ }
119670
+ if (s5 === "\r" || s5 === "\n") {
119671
+ const choice = filtered[cursor];
119672
+ if (choice !== void 0) finish(choice);
119673
+ return;
119674
+ }
119675
+ if (s5 === "\x7F" || s5 === "\b") {
119676
+ if (filter.length > 0) {
119677
+ filter = filter.slice(0, -1);
119678
+ cursor = 0;
119679
+ updateFiltered();
119680
+ }
119681
+ render();
119682
+ return;
119683
+ }
119684
+ if (s5 === "\x1B[A") {
119685
+ if (cursor > 0) {
119686
+ cursor -= 1;
119687
+ updateVisibleOffset();
119688
+ }
119689
+ render();
119690
+ return;
119691
+ }
119692
+ if (s5 === "\x1B[B") {
119693
+ if (cursor < filtered.length - 1) {
119694
+ cursor += 1;
119695
+ updateVisibleOffset();
119696
+ }
119697
+ render();
119698
+ return;
119699
+ }
119700
+ if (s5.length === 1 && s5.charCodeAt(0) >= 32 && s5.charCodeAt(0) <= 126) {
119701
+ filter += s5;
119702
+ cursor = 0;
119703
+ updateFiltered();
119704
+ render();
119705
+ }
119706
+ }
119707
+ input.setRawMode(true);
119708
+ input.resume();
119709
+ input.on("data", onData);
119710
+ updateFiltered();
119711
+ render();
119712
+ });
119713
+ }
119063
119714
 
119064
119715
  // ../../packages/cli/src/commands/connect-ai.ts
119065
119716
  init_cjs_shims();
@@ -119197,7 +119848,7 @@ function parseDbSpec(spec) {
119197
119848
  }
119198
119849
  return { db, roles };
119199
119850
  }
119200
- var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "grafana", "redis"];
119851
+ var SUPPORTED = ["elasticsearch", "mongodb", "postgres", "sentry", "axiom", "grafana", "redis"];
119201
119852
  async function runConnect(type, opts) {
119202
119853
  if (type === "ai") {
119203
119854
  return runConnectAi({
@@ -119234,6 +119885,9 @@ async function runConnect(type, opts) {
119234
119885
  case "sentry":
119235
119886
  patch = buildSentryPatch(filled);
119236
119887
  break;
119888
+ case "axiom":
119889
+ patch = buildAxiomPatch(filled);
119890
+ break;
119237
119891
  case "grafana":
119238
119892
  patch = buildGrafanaPatch(filled);
119239
119893
  break;
@@ -119255,7 +119909,7 @@ ${import_picocolors41.default.red(`\u2717 Could not reach ${connectorType}:`)} $
119255
119909
  ${import_picocolors41.default.green("\u2713")} ${connectorType} reachable ${import_picocolors41.default.dim(`(${probeResult.detail})`)}`
119256
119910
  );
119257
119911
  }
119258
- const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0 || filled.authToken !== void 0;
119912
+ const hasLiteralCredentials = filled.url !== void 0 || filled.password !== void 0 || filled.username !== void 0 || filled.authToken !== void 0 || filled.token !== void 0;
119259
119913
  if (hasLiteralCredentials) {
119260
119914
  if (isGitTracked(configPath, root)) {
119261
119915
  console.error(
@@ -119332,6 +119986,35 @@ ${import_picocolors41.default.bold(`Connect ${type}`)} ${import_picocolors41.def
119332
119986
  filled.authToken = filled.authToken ?? (await askPassword("Auth token") || void 0);
119333
119987
  filled.url = filled.url ?? (await ask("Base URL (self-hosted)", "https://sentry.io", false) || void 0);
119334
119988
  break;
119989
+ case "axiom": {
119990
+ filled.token = filled.token ?? (await askPassword("Axiom API token") || void 0);
119991
+ if (filled.url === void 0) {
119992
+ filled.url = await askAxiomRegion();
119993
+ }
119994
+ if (filled.dataset === void 0) {
119995
+ const baseUrl = filled.url ?? "https://api.axiom.co";
119996
+ let names = [];
119997
+ if (filled.token) {
119998
+ const client = new AxiomClient({ token: filled.token, dataset: "", baseUrl });
119999
+ names = (await client.listDatasets()).map((d3) => d3.name).filter(Boolean);
120000
+ }
120001
+ if (names.length > 0) {
120002
+ const picked = await askDatasetSelection(names);
120003
+ if (picked) filled.dataset = picked;
120004
+ }
120005
+ if (filled.dataset === void 0) {
120006
+ if (names.length === 0) {
120007
+ console.log(
120008
+ import_picocolors41.default.yellow(
120009
+ " Couldn't list datasets (check token / network / region) \u2014 enter the name manually."
120010
+ )
120011
+ );
120012
+ }
120013
+ filled.dataset = await ask("Dataset name", "logs");
120014
+ }
120015
+ }
120016
+ break;
120017
+ }
119335
120018
  case "grafana":
119336
120019
  filled.url = filled.url ?? await ask("URL", "https://grafana.example.com");
119337
120020
  filled.username = filled.username ?? (await ask("Username", "", false) || void 0);
@@ -119389,6 +120072,8 @@ function missingRequired(type, opts) {
119389
120072
  return !opts.url;
119390
120073
  case "sentry":
119391
120074
  return !opts.org || !opts.project || !opts.authToken;
120075
+ case "axiom":
120076
+ return !opts.dataset || !opts.token;
119392
120077
  case "grafana":
119393
120078
  return !opts.url;
119394
120079
  case "redis":
@@ -119487,6 +120172,13 @@ function buildSentryPatch(opts) {
119487
120172
  if (opts.url && opts.url.replace(/\/$/, "") !== "https://sentry.io") patch["url"] = opts.url;
119488
120173
  return patch;
119489
120174
  }
120175
+ function buildAxiomPatch(opts) {
120176
+ if (!opts.dataset) throw new Error("Dataset is required for axiom");
120177
+ const patch = { dataset: opts.dataset };
120178
+ if (opts.token) patch["token"] = opts.token;
120179
+ if (opts.url && opts.url.replace(/\/$/, "") !== "https://api.axiom.co") patch["url"] = opts.url;
120180
+ return patch;
120181
+ }
119490
120182
  function buildGrafanaPatch(opts) {
119491
120183
  const patch = {};
119492
120184
  if (opts.url) patch["url"] = opts.url;
@@ -119622,6 +120314,17 @@ async function probe(type, opts) {
119622
120314
  });
119623
120315
  return client.health();
119624
120316
  }
120317
+ case "axiom": {
120318
+ if (!opts.token || !opts.dataset) {
120319
+ return { ok: true, detail: "skipped (missing dataset/token)" };
120320
+ }
120321
+ const client = new AxiomClient({
120322
+ token: opts.token,
120323
+ dataset: opts.dataset,
120324
+ ...opts.url ? { baseUrl: opts.url } : {}
120325
+ });
120326
+ return client.health();
120327
+ }
119625
120328
  case "grafana": {
119626
120329
  if (!opts.url) return { ok: true, detail: "skipped (no URL)" };
119627
120330
  const headers = { "Content-Type": "application/json" };
@@ -119673,8 +120376,11 @@ function printSummary(type, opts) {
119673
120376
  if (opts.collections) lines.push(` collections: ${opts.collections}`);
119674
120377
  if (opts.org) lines.push(` org: ${opts.org}`);
119675
120378
  if (opts.project) lines.push(` project: ${opts.project}`);
120379
+ if (opts.dataset) lines.push(` dataset: ${opts.dataset}`);
119676
120380
  if (opts.authToken)
119677
120381
  lines.push(` auth-token: ${"\u2022".repeat(Math.min(opts.authToken.length, 8))}`);
120382
+ if (opts.token)
120383
+ lines.push(` token: ${"\u2022".repeat(Math.min(opts.token.length, 8))}`);
119678
120384
  if (opts.dashboardUids && opts.dashboardUids.length > 0) {
119679
120385
  lines.push(` dashboards: ${opts.dashboardUids.join(", ")}`);
119680
120386
  } else if (opts.dashboard) {
@@ -119785,6 +120491,63 @@ async function askDashboardSelection(dashboards) {
119785
120491
  }
119786
120492
  return [];
119787
120493
  }
120494
+ async function askAxiomRegion() {
120495
+ const US = "US \u2014 api.axiom.co";
120496
+ const EU = "EU \u2014 api.eu.axiom.co";
120497
+ const CUSTOM = "Custom URL\u2026";
120498
+ const US_URL = "https://api.axiom.co";
120499
+ const EU_URL = "https://api.eu.axiom.co";
120500
+ let choice = US;
120501
+ if (isInteractive()) {
120502
+ try {
120503
+ choice = await selectSearch({
120504
+ message: "Axiom server / region",
120505
+ choices: [US, EU, CUSTOM],
120506
+ pageSize: 3
120507
+ });
120508
+ } catch (err2) {
120509
+ if (err2 instanceof ExitPromptError) throw err2;
120510
+ return US_URL;
120511
+ }
120512
+ } else {
120513
+ return US_URL;
120514
+ }
120515
+ if (choice === EU) return EU_URL;
120516
+ if (choice === CUSTOM) {
120517
+ return await ask("Custom Axiom base URL", US_URL) || US_URL;
120518
+ }
120519
+ return US_URL;
120520
+ }
120521
+ async function askDatasetSelection(names) {
120522
+ if (names.length === 0) return void 0;
120523
+ if (isInteractive()) {
120524
+ try {
120525
+ return await selectSearch({
120526
+ message: "Select an Axiom dataset",
120527
+ choices: names,
120528
+ pageSize: 12
120529
+ });
120530
+ } catch (err2) {
120531
+ if (err2 instanceof ExitPromptError) throw err2;
120532
+ }
120533
+ }
120534
+ const MAX_DISPLAY = 25;
120535
+ const shown = names.slice(0, MAX_DISPLAY);
120536
+ console.log("\n Available Axiom datasets:");
120537
+ shown.forEach((name2, i3) => {
120538
+ console.log(` ${import_picocolors41.default.dim(`[${i3 + 1}]`)} ${name2}`);
120539
+ });
120540
+ if (names.length > MAX_DISPLAY) {
120541
+ console.log(import_picocolors41.default.dim(` \u2026 and ${names.length - MAX_DISPLAY} more`));
120542
+ }
120543
+ const input = (await ask(" Select a dataset (e.g. 1 or Enter to type a name manually)", "", false)).trim();
120544
+ if (!input) return void 0;
120545
+ if (/^\d+$/.test(input)) {
120546
+ const idx = parseInt(input, 10) - 1;
120547
+ if (idx >= 0 && idx < shown.length) return shown[idx];
120548
+ }
120549
+ return input;
120550
+ }
119788
120551
  function redisUrlHasPassword(raw) {
119789
120552
  if (!raw) return false;
119790
120553
  try {
@@ -120879,6 +121642,7 @@ async function runReadiness(opts) {
120879
121642
  let anyMongo = false;
120880
121643
  let anyPostgres = false;
120881
121644
  let anySentry = false;
121645
+ let anyAxiom = false;
120882
121646
  let anyRedis = false;
120883
121647
  for (const project of globalConfig2.projects) {
120884
121648
  for (const repo of project.repositories) {
@@ -120891,6 +121655,7 @@ async function runReadiness(opts) {
120891
121655
  if (c4.mongodb) anyMongo = true;
120892
121656
  if (c4.postgres) anyPostgres = true;
120893
121657
  if (c4.sentry) anySentry = true;
121658
+ if (c4.axiom) anyAxiom = true;
120894
121659
  if (c4.redis) anyRedis = true;
120895
121660
  }
120896
121661
  }
@@ -120948,6 +121713,15 @@ async function runReadiness(opts) {
120948
121713
  next: "run `horus connect sentry`"
120949
121714
  }
120950
121715
  );
121716
+ checks.push(
121717
+ anyAxiom ? { label: "Axiom", status: "pass", blocking: false, detail: "configured" } : {
121718
+ label: "Axiom",
121719
+ status: "warn",
121720
+ blocking: false,
121721
+ detail: "not configured \u2014 no runtime log evidence",
121722
+ next: "run `horus connect axiom`"
121723
+ }
121724
+ );
120951
121725
  checks.push(
120952
121726
  anyRedis ? { label: "Redis", status: "pass", blocking: false, detail: "configured" } : {
120953
121727
  label: "Redis",
@@ -122675,8 +123449,8 @@ Examples:
122675
123449
  process.exitCode = await runReadiness({ config: opts.config, ai: opts.ai, aiModel: opts.aiModel });
122676
123450
  });
122677
123451
  program2.command("connect <type>").description(
122678
- "Add or update a connector (elasticsearch / mongodb / postgres / sentry / grafana / redis / ai) in .horus/config.json"
122679
- ).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").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("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
123452
+ "Add or update a connector (elasticsearch / mongodb / postgres / sentry / axiom / grafana / redis / ai) in .horus/config.json"
123453
+ ).option("--env <name>", "target environment (default: first environment in config)").option("--provider <name>", "AI provider for `connect ai` (anthropic / claude / codex / gemini)").option("--api-key <key>", "Anthropic API key for `connect ai`").option("--model <id>", "default model for `connect ai`").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("--schema <name>", "schema to introspect (postgres; default public)").option("--tables <list>", "comma-separated table allowlist (postgres)").option("--auth-token <token>", "Sentry API auth token (required for sentry)").option("--org <slug>", "Sentry org slug (required for sentry)").option("--project <slug>", "Sentry project slug (required for sentry)").option("--token <token>", "Axiom API token (required for axiom)").option("--dataset <name>", "Axiom dataset to query (required for axiom)").option("--dashboard <uid>", "default dashboard uid (grafana)").option(
122680
123454
  "--db <spec>",
122681
123455
  "redis logical DB as db:role1,role2 (e.g. 0:cache,state or 1:bullmq,queues); repeatable",
122682
123456
  (val, acc) => {
@@ -122700,6 +123474,8 @@ Examples:
122700
123474
  authToken: opts.authToken,
122701
123475
  org: opts.org,
122702
123476
  project: opts.project,
123477
+ token: opts.token,
123478
+ dataset: opts.dataset,
122703
123479
  dashboard: opts.dashboard,
122704
123480
  db: opts.db,
122705
123481
  bullmqPrefix: opts.bullmqPrefix,
@@ -122814,7 +123590,7 @@ Examples:
122814
123590
  process.exitCode = await runMemoryDetect(opts);
122815
123591
  }
122816
123592
  );
122817
- memory.command("accuracy").description("Report Horus's measured hit-rate from the converged outcome-label eval set").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "project/repository to scope to (default: inferred from cwd)").option("--source <source>", "filter to one signal source: feedback | confirm").option("--days <n>", "only count labels from the last N days", (v4) => Number(v4)).option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--json", "output JSON").action(
123593
+ memory.command("accuracy").description("Report Horus's measured hit-rate from the converged outcome-label eval set").option("-c, --config <path>", "path to horus.config.ts").option("--repo <name>", "project/repository to scope to (default: inferred from cwd)").option("--source <source>", "filter to one signal source: feedback | confirm").option("--since <date>", "only count labels on/after this date (e.g. 2026-06-01)").option("--days <n>", "only count labels from the last N days", (v4) => Number(v4)).option("--all", "count the full retained history (disable the default recent window)").option("--limit <n>", "max labels to scan", (v4) => Number(v4)).option("--json", "output JSON").action(
122818
123594
  async (opts) => {
122819
123595
  process.exitCode = await runMemoryAccuracy(opts);
122820
123596
  }