@merittdev/horus 0.11.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 +246 -196
  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.11.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();
@@ -70989,8 +70989,72 @@ var SourceHttpClient = class {
70989
70989
  return this.request("POST", "/api/diff", { base: base2, compare });
70990
70990
  }
70991
70991
  async nodeCount() {
70992
- const result = await this.cypher("MATCH (n) RETURN count(n)");
70993
- 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 ?? [];
70994
71058
  }
70995
71059
  // -------------------------------------------------------------------------
70996
71060
  // Memory vector bridge (M2). These reuse `request<T>` (retry/timeout/4xx) so
@@ -71271,13 +71335,6 @@ var SourceCodeProvider = class {
71271
71335
  id = "source";
71272
71336
  kind = "code";
71273
71337
  // --- helpers -------------------------------------------------------------
71274
- /**
71275
- * Escape a node id for inlining inside a double-quoted Cypher string literal:
71276
- * first double every backslash, then escape every double-quote.
71277
- */
71278
- escapeId(id) {
71279
- return id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
71280
- }
71281
71338
  nodeToSymbol(n4) {
71282
71339
  return {
71283
71340
  id: n4.id,
@@ -71290,29 +71347,34 @@ var SourceCodeProvider = class {
71290
71347
  className: n4.className
71291
71348
  };
71292
71349
  }
71293
- 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) {
71294
71369
  try {
71295
- return (await this.client.cypher(query)).rows;
71370
+ return await p5;
71296
71371
  } catch (err2) {
71297
71372
  if (err2 instanceof SourceHttpError && err2.status >= 400 && err2.status < 500) {
71298
- return [];
71373
+ return fallback;
71299
71374
  }
71300
71375
  throw err2;
71301
71376
  }
71302
71377
  }
71303
- cypherRowToSymbol(row, idIdx, nameIdx, fileIdx, lineIdx, classNameIdx) {
71304
- const startLine = lineIdx != null && row[lineIdx] != null ? Number(row[lineIdx]) : void 0;
71305
- const sym = {
71306
- id: String(row[idIdx] ?? ""),
71307
- name: String(row[nameIdx] ?? ""),
71308
- filePath: String(row[fileIdx] ?? ""),
71309
- startLine
71310
- };
71311
- if (classNameIdx != null && row[classNameIdx] != null) {
71312
- sym.className = String(row[classNameIdx]);
71313
- }
71314
- return sym;
71315
- }
71316
71378
  // --- contract ------------------------------------------------------------
71317
71379
  async health() {
71318
71380
  const h3 = await this.client.health();
@@ -71322,19 +71384,22 @@ var SourceCodeProvider = class {
71322
71384
  };
71323
71385
  }
71324
71386
  async searchSymbols(query, limit = 10) {
71325
- const E2 = this.escapeId(query);
71326
- const exactQuery = `MATCH (n) WHERE toLower(n.name) = toLower("${E2}") AND label(n) <> "File" RETURN n.id, n.name, n.file_path LIMIT ${limit}`;
71327
- const [exactRows, semanticRes] = await Promise.all([
71328
- this.rows(exactQuery).catch(() => []),
71387
+ const [exactHits, semanticRes] = await Promise.all([
71388
+ this.degrade(this.client.exactSymbols(query, limit), []),
71329
71389
  this.client.search(query, limit)
71330
71390
  ]);
71331
- const exactSymbols = exactRows.map((row) => ({
71332
- id: String(row[0] ?? ""),
71333
- name: String(row[1] ?? ""),
71334
- filePath: String(row[2] ?? ""),
71335
- score: 1
71336
- // an exact-name Cypher hit is authoritative
71337
- }));
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
+ });
71338
71403
  const exactIds = new Set(exactSymbols.map((s5) => s5.id));
71339
71404
  const ql = query.toLowerCase();
71340
71405
  const semantic = semanticRes.filter((r) => !exactIds.has(r.nodeId)).map((r) => {
@@ -71356,78 +71421,54 @@ var SourceCodeProvider = class {
71356
71421
  async hydrateLines(symbols) {
71357
71422
  const ids2 = symbols.map((s5) => s5.id).filter(Boolean);
71358
71423
  if (ids2.length === 0) return symbols;
71359
- const idList = ids2.map((id) => `"${this.escapeId(id)}"`).join(", ");
71360
- const rows = await this.rows(`MATCH (n) WHERE n.id IN [${idList}] RETURN n.id, n.start_line, n.end_line`).catch(() => []);
71361
- const lineById = /* @__PURE__ */ new Map();
71362
- for (const row of rows) {
71363
- const id = String(row[0] ?? "");
71364
- const start2 = row[1] != null ? Number(row[1]) : void 0;
71365
- const end = row[2] != null ? Number(row[2]) : void 0;
71366
- lineById.set(id, { start: start2, end });
71367
- }
71424
+ const lines = await this.degrade(this.client.nodesLines(ids2), {});
71368
71425
  return symbols.map((s5) => {
71369
- const l2 = lineById.get(s5.id);
71426
+ const l2 = lines[s5.id];
71370
71427
  if (l2 === void 0) return s5;
71371
71428
  const out2 = { ...s5 };
71372
- if (l2.start !== void 0 && Number.isFinite(l2.start)) out2.startLine = l2.start;
71373
- 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;
71374
71431
  return out2;
71375
71432
  });
71376
71433
  }
71377
71434
  async context(symbolId) {
71378
- const E2 = this.escapeId(symbolId);
71379
- 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`;
71380
- 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`;
71381
- 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`;
71382
- 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`;
71383
- 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`;
71384
- 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`;
71385
- const [nodeRows, calleeRows, callerRows, usesTypeRows, communityRows, fileRows] = await Promise.all([
71386
- this.rows(nodeQuery),
71387
- this.rows(calleesQuery),
71388
- this.rows(callersQuery),
71389
- this.rows(usesTypeQuery),
71390
- this.rows(communityQuery),
71391
- this.rows(fileQuery)
71392
- ]);
71393
- 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;
71394
71449
  const symbol = {
71395
- id: String(node?.[0] ?? symbolId),
71396
- name: String(node?.[1] ?? ""),
71397
- filePath: String(node?.[2] ?? ""),
71398
- startLine: node?.[3] != null ? Number(node[3]) : void 0,
71399
- endLine: node?.[4] != null ? Number(node[4]) : void 0,
71400
- signature: node?.[5] != null ? String(node[5]) : void 0,
71401
- className: node?.[6] != null ? String(node[6]) : void 0,
71402
- language: node?.[7] != null ? String(node[7]) : void 0
71403
- };
71404
- const isDead = Boolean(node?.[8]);
71405
- 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;
71406
71460
  const snippet = typeof content === "string" ? content.slice(0, 600) : void 0;
71407
71461
  const sourceBody = typeof content === "string" ? content.slice(0, 2e4) : void 0;
71408
- const callees = calleeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
71409
- const callers = callerRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3, 4));
71410
- const usesType = usesTypeRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
71411
- const communityRow = communityRows[0];
71412
- const community = communityRow ? { id: String(communityRow[0] ?? ""), name: String(communityRow[1] ?? "") } : null;
71413
- let imports = [];
71414
- let coupledWith = [];
71415
- const fileRow = fileRows[0];
71416
- const fileId = fileRow?.[0];
71417
- if (fileId != null) {
71418
- const FE = this.escapeId(String(fileId));
71419
- const importsQuery = `MATCH (f)-[r:CodeRelation]->(g:File) WHERE f.id = "${FE}" AND r.rel_type = "imports" RETURN g.file_path`;
71420
- 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`;
71421
- const [importRows, coupledRows] = await Promise.all([
71422
- this.rows(importsQuery),
71423
- this.rows(coupledQuery)
71424
- ]);
71425
- imports = importRows.map((row) => String(row[0] ?? ""));
71426
- coupledWith = coupledRows.map((row) => ({
71427
- file: String(row[0] ?? ""),
71428
- coChanges: Number(row[1] ?? 0)
71429
- }));
71430
- }
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
+ }));
71431
71472
  return {
71432
71473
  symbol,
71433
71474
  snippet,
@@ -71438,7 +71479,7 @@ var SourceCodeProvider = class {
71438
71479
  usesType,
71439
71480
  community,
71440
71481
  coupledWith,
71441
- isDead
71482
+ isDead: Boolean(n4.isDead)
71442
71483
  };
71443
71484
  }
71444
71485
  async impact(symbolId, depth = 3) {
@@ -71453,21 +71494,22 @@ var SourceCodeProvider = class {
71453
71494
  };
71454
71495
  }
71455
71496
  async flowsFor(symbolId) {
71456
- const E2 = this.escapeId(symbolId);
71457
- const procQuery = `MATCH (s)-[r:CodeRelation]->(p:Process) WHERE s.id = "${E2}" AND r.rel_type = "step_in_process" RETURN p.id, p.name`;
71458
- const procRows = await this.rows(procQuery);
71459
- const flows = await Promise.all(
71460
- procRows.map(async (proc) => {
71461
- const pid = proc[0];
71462
- const pname = proc[1];
71463
- const PE = this.escapeId(String(pid ?? ""));
71464
- 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`;
71465
- const stepRows = await this.rows(stepQuery);
71466
- const steps = stepRows.map((row) => this.cypherRowToSymbol(row, 0, 1, 2, 3));
71467
- return { id: String(pid ?? ""), name: String(pname ?? ""), steps };
71468
- })
71469
- );
71470
- 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 !== "");
71471
71513
  }
71472
71514
  async detectChanges(diff) {
71473
71515
  const d3 = await this.client.diff(diff.base, diff.compare);
@@ -94577,20 +94619,10 @@ function isClassSymbol(sym) {
94577
94619
  return sym !== void 0 && sym.id.split(":", 1)[0]?.toLowerCase() === "class";
94578
94620
  }
94579
94621
  async function fetchClassMethods(code, cls) {
94580
- const safe = (s5) => s5.replace(/["\\]/g, "");
94581
- const name2 = safe(cls.name);
94582
- if (!name2) return [];
94583
- const file = safe(cls.filePath);
94622
+ const name2 = cls.name?.trim();
94623
+ if (!name2 || typeof code.classMethods !== "function") return [];
94584
94624
  try {
94585
- const res = await code.cypher(
94586
- `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"
94587
- );
94588
- return res.rows.map((r) => ({
94589
- id: String(r[0] ?? ""),
94590
- name: String(r[1] ?? ""),
94591
- filePath: String(r[2] ?? ""),
94592
- ...typeof r[3] === "number" ? { startLine: r[3] } : {}
94593
- })).filter((s5) => s5.id !== "" && s5.name !== "");
94625
+ return await code.classMethods(cls.filePath ?? "", name2);
94594
94626
  } catch {
94595
94627
  return [];
94596
94628
  }
@@ -101491,31 +101523,50 @@ function dedupeBySymbolFile(rows) {
101491
101523
  // ../../packages/stitcher/src/stitch.ts
101492
101524
  init_cjs_shims();
101493
101525
  async function stitch(client, db, opts = {}) {
101494
- const producerRows = (await client.cypher(
101526
+ const producerRows = await client.contentSearch([
101495
101527
  // "new Queue" (no paren) so generic-typed `new Queue<T>(...)` also matches.
101496
- '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'
101497
- )).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
+ ]);
101498
101539
  const producerClasses = producerRows.map((r) => ({
101499
- name: String(r[0] ?? ""),
101500
- filePath: String(r[1] ?? ""),
101501
- content: String(r[2] ?? "")
101540
+ name: r.name,
101541
+ filePath: r.filePath,
101542
+ content: r.content
101502
101543
  }));
101503
- const workerRows = (await client.cypher(
101504
- // "new Worker" (no paren) so generic-typed `new Worker<T>(...)` also matches.
101505
- 'MATCH (n) WHERE n.content CONTAINS "@Processor(" OR n.content CONTAINS "new Worker" RETURN n.name, n.file_path, n.content'
101506
- )).rows;
101544
+ const workerRows = await client.contentSearch(["@Processor(", "new Worker"]);
101507
101545
  const workerFiles = workerRows.map((r) => ({
101508
- filePath: String(r[1] ?? ""),
101509
- content: String(r[2] ?? "")
101546
+ filePath: r.filePath,
101547
+ content: r.content
101510
101548
  }));
101511
101549
  const graph = extractQueueGraph({ producerClasses, workerFiles });
101512
- const celeryRows = (await client.cypher(
101513
- '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'
101514
- )).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
+ ]);
101515
101566
  const celeryNodes = celeryRows.map((r) => ({
101516
- name: String(r[0] ?? ""),
101517
- filePath: String(r[1] ?? ""),
101518
- content: String(r[2] ?? "")
101567
+ name: r.name,
101568
+ filePath: r.filePath,
101569
+ content: r.content
101519
101570
  }));
101520
101571
  const celery = extractCeleryQueueGraph(celeryNodes);
101521
101572
  const dramatiq = extractDramatiqQueueGraph(celeryNodes);
@@ -101667,6 +101718,10 @@ async function stitchQueueMap(hostUrl, dbUrl, label) {
101667
101718
  console.log(
101668
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).`
101669
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
+ );
101670
101725
  } finally {
101671
101726
  await sql2.end();
101672
101727
  }
@@ -101692,60 +101747,49 @@ function findConfiguredRepo(config2, root) {
101692
101747
  return null;
101693
101748
  }
101694
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
+ ];
101695
101758
  async function extractSourceGraph(hostUrl, repo) {
101696
101759
  const client = new SourceHttpClient({ baseUrl: hostUrl });
101697
101760
  const N2 = SOURCE_GRAPH_PULL_LIMIT;
101698
- const rows = async (query) => {
101699
- try {
101700
- return (await client.cypher(query)).rows;
101701
- } catch {
101702
- return [];
101703
- }
101704
- };
101705
- const symbolRows = await rows(
101706
- `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}`
101707
- );
101761
+ const symbolRows = await client.symbolsByLabel(SOURCE_SYMBOL_LABELS, N2 * 2).catch(() => []);
101708
101762
  const symbols = symbolRows.map((r) => ({
101709
- label: String(r[0] ?? ""),
101710
- name: String(r[1] ?? ""),
101711
- filePath: r[2] != null ? String(r[2]) : void 0,
101712
- startLine: r[3] != null ? Number(r[3]) : void 0,
101713
- endLine: r[4] != null ? Number(r[4]) : void 0,
101714
- className: r[5] != null ? String(r[5]) : void 0,
101715
- isEntryPoint: Boolean(r[6]),
101716
- isExported: Boolean(r[7]),
101717
- 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
101718
101772
  }));
101719
- const communityRows = await rows(
101720
- `MATCH (n:Community) RETURN n.id, n.name LIMIT ${N2}`
101721
- );
101722
- const communities = communityRows.map((r) => ({
101723
- id: r[0] != null ? String(r[0]) : void 0,
101724
- 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)
101725
101792
  }));
101726
- const processRows = await rows(`MATCH (n:Process) RETURN n.id, n.name LIMIT ${N2}`);
101727
- const stepRows = await rows(
101728
- `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}`
101729
- );
101730
- const stepsByProcess = /* @__PURE__ */ new Map();
101731
- for (const r of stepRows) {
101732
- const pid = String(r[0] ?? "");
101733
- if (!pid) continue;
101734
- const component = String(r[1] ?? "");
101735
- if (!component) continue;
101736
- const file = r[2] != null ? String(r[2]) : void 0;
101737
- const list = stepsByProcess.get(pid) ?? [];
101738
- list.push({ component, ...file ? { detail: file } : {} });
101739
- stepsByProcess.set(pid, list);
101740
- }
101741
- const processes = processRows.map((r) => {
101742
- const id = r[0] != null ? String(r[0]) : void 0;
101743
- return {
101744
- id,
101745
- name: String(r[1] ?? ""),
101746
- steps: id ? stepsByProcess.get(id) ?? [] : []
101747
- };
101748
- });
101749
101793
  if (symbols.length === 0 && communities.length === 0 && processes.length === 0) {
101750
101794
  return null;
101751
101795
  }
@@ -101919,7 +101963,13 @@ async function runIndex(opts) {
101919
101963
  );
101920
101964
  return 1;
101921
101965
  }
101922
- 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
+ }
101923
101973
  await buildKnowledgeIndex(root, config2, label, configuredProject, opts, hostUrl);
101924
101974
  const hasLocalConfig = discoverLocalConfig(root) !== null;
101925
101975
  if (!isConfigured && !hasLocalConfig) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@merittdev/horus",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Local-first, source-aware production-incident investigation engine",
5
5
  "type": "module",
6
6
  "bin": {