@neat.is/core 0.9.2-dev.20260822 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.cjs CHANGED
@@ -787,7 +787,7 @@ function getGraph(project = DEFAULT_PROJECT) {
787
787
  init_cjs_shims();
788
788
  var import_fastify2 = __toESM(require("fastify"), 1);
789
789
  var import_cors = __toESM(require("@fastify/cors"), 1);
790
- var import_types92 = require("@neat.is/types");
790
+ var import_types93 = require("@neat.is/types");
791
791
 
792
792
  // src/extend/index.ts
793
793
  init_cjs_shims();
@@ -4160,11 +4160,11 @@ function actixAttributeRoute(attrItem, out) {
4160
4160
  if (!nameNode) return;
4161
4161
  const macro = nameNode.type === "identifier" ? nameNode.text : nameNode.type === "scoped_identifier" ? nameNode.childForFieldName("name")?.text ?? null : null;
4162
4162
  if (!macro) return;
4163
- const tokens = attr.childForFieldName("arguments");
4164
- if (!tokens || tokens.type !== "token_tree") return;
4163
+ const tokens2 = attr.childForFieldName("arguments");
4164
+ if (!tokens2 || tokens2.type !== "token_tree") return;
4165
4165
  const strings = [];
4166
- for (let i = 0; i < tokens.namedChildCount; i++) {
4167
- const s = rustStringContent(tokens.namedChild(i));
4166
+ for (let i = 0; i < tokens2.namedChildCount; i++) {
4167
+ const s = rustStringContent(tokens2.namedChild(i));
4168
4168
  if (s !== null) strings.push(s);
4169
4169
  }
4170
4170
  const pathStr = strings[0];
@@ -5982,29 +5982,29 @@ function promoteFrontierNodes(graph, opts = {}) {
5982
5982
  toPromote.push({ frontierId: id, serviceId: target });
5983
5983
  });
5984
5984
  let promoted = 0;
5985
- for (const { frontierId: frontierId2, serviceId: serviceId15 } of toPromote) {
5985
+ for (const { frontierId: frontierId2, serviceId: serviceId16 } of toPromote) {
5986
5986
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
5987
5987
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
5988
5988
  if (!gate.allowed) {
5989
5989
  continue;
5990
5990
  }
5991
5991
  }
5992
- rewireFrontierEdges(graph, frontierId2, serviceId15);
5992
+ rewireFrontierEdges(graph, frontierId2, serviceId16);
5993
5993
  graph.dropNode(frontierId2);
5994
5994
  promoted++;
5995
5995
  }
5996
5996
  return promoted;
5997
5997
  }
5998
- function rewireFrontierEdges(graph, frontierId2, serviceId15) {
5998
+ function rewireFrontierEdges(graph, frontierId2, serviceId16) {
5999
5999
  const inbound = [...graph.inboundEdges(frontierId2)];
6000
6000
  const outbound = [...graph.outboundEdges(frontierId2)];
6001
6001
  for (const edgeId of inbound) {
6002
6002
  const edge = graph.getEdgeAttributes(edgeId);
6003
- rebuildEdge(graph, edge, edge.source, serviceId15, edgeId);
6003
+ rebuildEdge(graph, edge, edge.source, serviceId16, edgeId);
6004
6004
  }
6005
6005
  for (const edgeId of outbound) {
6006
6006
  const edge = graph.getEdgeAttributes(edgeId);
6007
- rebuildEdge(graph, edge, serviceId15, edge.target, edgeId);
6007
+ rebuildEdge(graph, edge, serviceId16, edge.target, edgeId);
6008
6008
  }
6009
6009
  }
6010
6010
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -6350,7 +6350,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
6350
6350
  for (const id of walk10.path) {
6351
6351
  const owner = resolveOwningService(graph, id);
6352
6352
  if (!owner) continue;
6353
- const { id: serviceId15, svc } = owner;
6353
+ const { id: serviceId16, svc } = owner;
6354
6354
  const deps = svc.dependencies ?? {};
6355
6355
  for (const pair of candidatePairs) {
6356
6356
  const declared = deps[pair.driver];
@@ -6363,7 +6363,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
6363
6363
  );
6364
6364
  if (!result.compatible) {
6365
6365
  return {
6366
- rootCauseNode: serviceId15,
6366
+ rootCauseNode: serviceId16,
6367
6367
  rootCauseReason: result.reason ?? "incompatible driver",
6368
6368
  ...result.minDriverVersion ? {
6369
6369
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -6378,7 +6378,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
6378
6378
  for (const id of walk10.path) {
6379
6379
  const owner = resolveOwningService(graph, id);
6380
6380
  if (!owner) continue;
6381
- const { id: serviceId15, svc } = owner;
6381
+ const { id: serviceId16, svc } = owner;
6382
6382
  const deps = svc.dependencies ?? {};
6383
6383
  const serviceNodeEngine = svc.nodeEngine;
6384
6384
  for (const constraint of nodeEngineConstraints()) {
@@ -6387,7 +6387,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
6387
6387
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
6388
6388
  if (!result.compatible && result.reason) {
6389
6389
  return {
6390
- rootCauseNode: serviceId15,
6390
+ rootCauseNode: serviceId16,
6391
6391
  rootCauseReason: result.reason,
6392
6392
  ...result.requiredNodeVersion ? {
6393
6393
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -6402,7 +6402,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
6402
6402
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
6403
6403
  if (!result.compatible && result.reason) {
6404
6404
  return {
6405
- rootCauseNode: serviceId15,
6405
+ rootCauseNode: serviceId16,
6406
6406
  rootCauseReason: result.reason,
6407
6407
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
6408
6408
  };
@@ -6507,9 +6507,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
6507
6507
  function isFailingCallEdge(e) {
6508
6508
  return e.type === import_types8.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
6509
6509
  }
6510
- function callSourcesForService(graph, serviceId15) {
6511
- const ids = [serviceId15];
6512
- for (const edgeId of graph.outboundEdges(serviceId15)) {
6510
+ function callSourcesForService(graph, serviceId16) {
6511
+ const ids = [serviceId16];
6512
+ for (const edgeId of graph.outboundEdges(serviceId16)) {
6513
6513
  const e = graph.getEdgeAttributes(edgeId);
6514
6514
  if (e.type !== import_types8.EdgeType.CONTAINS) continue;
6515
6515
  const tgt = graph.getNodeAttributes(e.target);
@@ -6533,9 +6533,9 @@ function failingCallDominates(e, id, curEdge, curId) {
6533
6533
  }
6534
6534
  return id < curId;
6535
6535
  }
6536
- function dominantFailingCall(graph, serviceId15, visited) {
6536
+ function dominantFailingCall(graph, serviceId16, visited) {
6537
6537
  let best = null;
6538
- for (const src of callSourcesForService(graph, serviceId15)) {
6538
+ for (const src of callSourcesForService(graph, serviceId16)) {
6539
6539
  for (const edgeId of graph.outboundEdges(src)) {
6540
6540
  const e = graph.getEdgeAttributes(edgeId);
6541
6541
  if (!isFailingCallEdge(e)) continue;
@@ -6574,9 +6574,9 @@ function staleCallDominates(e, id, curEdge, curId) {
6574
6574
  if (ev !== cv) return ev > cv;
6575
6575
  return id < curId;
6576
6576
  }
6577
- function dominantStaleCall(graph, serviceId15, visited) {
6577
+ function dominantStaleCall(graph, serviceId16, visited) {
6578
6578
  const bestByCallee = /* @__PURE__ */ new Map();
6579
- for (const src of callSourcesForService(graph, serviceId15)) {
6579
+ for (const src of callSourcesForService(graph, serviceId16)) {
6580
6580
  for (const edgeId of graph.outboundEdges(src)) {
6581
6581
  const e = graph.getEdgeAttributes(edgeId);
6582
6582
  if (e.type !== import_types8.EdgeType.CALLS) continue;
@@ -8431,9 +8431,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
8431
8431
  "StatefulSet",
8432
8432
  "DaemonSet"
8433
8433
  ]);
8434
- function addAliases(graph, serviceId15, candidates) {
8435
- if (!graph.hasNode(serviceId15)) return;
8436
- const node = graph.getNodeAttributes(serviceId15);
8434
+ function addAliases(graph, serviceId16, candidates) {
8435
+ if (!graph.hasNode(serviceId16)) return;
8436
+ const node = graph.getNodeAttributes(serviceId16);
8437
8437
  if (node.type !== import_types20.NodeType.ServiceNode) return;
8438
8438
  const set = new Set(node.aliases ?? []);
8439
8439
  for (const c of candidates) {
@@ -8443,7 +8443,7 @@ function addAliases(graph, serviceId15, candidates) {
8443
8443
  }
8444
8444
  if (set.size === 0) return;
8445
8445
  const updated = { ...node, aliases: [...set].sort() };
8446
- graph.replaceNodeAttributes(serviceId15, updated);
8446
+ graph.replaceNodeAttributes(serviceId16, updated);
8447
8447
  }
8448
8448
  function indexServicesByName(services) {
8449
8449
  const map = /* @__PURE__ */ new Map();
@@ -8476,12 +8476,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
8476
8476
  }
8477
8477
  if (!compose?.services) return;
8478
8478
  for (const [composeName, svc] of Object.entries(compose.services)) {
8479
- const serviceId15 = serviceIndex.get(composeName);
8480
- if (!serviceId15) continue;
8479
+ const serviceId16 = serviceIndex.get(composeName);
8480
+ if (!serviceId16) continue;
8481
8481
  const aliases = /* @__PURE__ */ new Set([composeName]);
8482
8482
  if (svc.container_name) aliases.add(svc.container_name);
8483
8483
  if (svc.hostname) aliases.add(svc.hostname);
8484
- addAliases(graph, serviceId15, aliases);
8484
+ addAliases(graph, serviceId16, aliases);
8485
8485
  }
8486
8486
  }
8487
8487
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -16268,6 +16268,639 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
16268
16268
  return result;
16269
16269
  }
16270
16270
 
16271
+ // src/ask.ts
16272
+ init_cjs_shims();
16273
+ var import_types58 = require("@neat.is/types");
16274
+ var DEFAULT_MAX_NODES = 3;
16275
+ var MAX_FACTS_PER_SECTION = 6;
16276
+ var INTENT_RULES = [
16277
+ {
16278
+ intent: "root-cause",
16279
+ test: /\b(why|root[\s-]?cause|failing|fail(?:s|ed)?|breaking|broke(?:n)?|crash(?:ing|ed)?|throw(?:ing|s)?|culprit|5\d\d)\b/
16280
+ },
16281
+ {
16282
+ intent: "blast-radius",
16283
+ test: /\b(blast|break[\s-]?if|breaks[\s-]?if|impact|downstream|dependents?|redeploy|who\s+(?:uses|calls|depends)|what\s+depends\s+on|affect(?:s|ed)?)\b/
16284
+ },
16285
+ {
16286
+ intent: "divergence",
16287
+ test: /\b(diverg\w*|weird|mismatch|drift|out\s+of\s+sync|inconsistent|disagree\w*|declared\s+vs|anything\s+wrong)\b/
16288
+ },
16289
+ {
16290
+ intent: "incidents",
16291
+ test: /\b(incidents?|recent\s+(?:errors?|failures?)|error\s+history|failure\s+history)\b/
16292
+ },
16293
+ {
16294
+ intent: "observed",
16295
+ test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
16296
+ },
16297
+ {
16298
+ intent: "dependencies",
16299
+ test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
16300
+ }
16301
+ ];
16302
+ function classifyIntent(question) {
16303
+ const q = question.toLowerCase();
16304
+ for (const rule of INTENT_RULES) {
16305
+ if (rule.test.test(q)) return rule.intent;
16306
+ }
16307
+ return "overview";
16308
+ }
16309
+ var STOPWORDS = /* @__PURE__ */ new Set([
16310
+ "the",
16311
+ "a",
16312
+ "an",
16313
+ "is",
16314
+ "are",
16315
+ "was",
16316
+ "were",
16317
+ "be",
16318
+ "been",
16319
+ "being",
16320
+ "of",
16321
+ "to",
16322
+ "in",
16323
+ "on",
16324
+ "at",
16325
+ "by",
16326
+ "for",
16327
+ "with",
16328
+ "and",
16329
+ "or",
16330
+ "but",
16331
+ "if",
16332
+ "as",
16333
+ "this",
16334
+ "that",
16335
+ "these",
16336
+ "those",
16337
+ "it",
16338
+ "its",
16339
+ "do",
16340
+ "does",
16341
+ "did",
16342
+ "has",
16343
+ "have",
16344
+ "had",
16345
+ "what",
16346
+ "which",
16347
+ "who",
16348
+ "whom",
16349
+ "how",
16350
+ "when",
16351
+ "where",
16352
+ "my",
16353
+ "me",
16354
+ "i",
16355
+ "we",
16356
+ "you",
16357
+ "they",
16358
+ "them",
16359
+ "from",
16360
+ "into",
16361
+ "about",
16362
+ "would",
16363
+ "will",
16364
+ "can",
16365
+ "could",
16366
+ "should",
16367
+ "anything",
16368
+ "everything",
16369
+ "something",
16370
+ "all",
16371
+ "any",
16372
+ "some",
16373
+ "no",
16374
+ "not",
16375
+ "so",
16376
+ "up",
16377
+ "out",
16378
+ "over",
16379
+ "get",
16380
+ "got",
16381
+ "show",
16382
+ "tell",
16383
+ "explain",
16384
+ "find",
16385
+ "give",
16386
+ "happen",
16387
+ "happens",
16388
+ "happening",
16389
+ "here",
16390
+ "there",
16391
+ "system",
16392
+ "code",
16393
+ "service",
16394
+ "node",
16395
+ "graph"
16396
+ ]);
16397
+ var INTENT_WORDS = /* @__PURE__ */ new Set([
16398
+ "why",
16399
+ "root",
16400
+ "cause",
16401
+ "failing",
16402
+ "fail",
16403
+ "fails",
16404
+ "failed",
16405
+ "breaking",
16406
+ "broke",
16407
+ "broken",
16408
+ "crash",
16409
+ "crashing",
16410
+ "crashed",
16411
+ "throw",
16412
+ "throwing",
16413
+ "throws",
16414
+ "culprit",
16415
+ "blast",
16416
+ "radius",
16417
+ "break",
16418
+ "breaks",
16419
+ "impact",
16420
+ "downstream",
16421
+ "dependent",
16422
+ "dependents",
16423
+ "redeploy",
16424
+ "affect",
16425
+ "affects",
16426
+ "affected",
16427
+ "diverge",
16428
+ "divergence",
16429
+ "divergences",
16430
+ "weird",
16431
+ "mismatch",
16432
+ "drift",
16433
+ "inconsistent",
16434
+ "disagree",
16435
+ "declared",
16436
+ "observed",
16437
+ "incident",
16438
+ "incidents",
16439
+ "recent",
16440
+ "errors",
16441
+ "error",
16442
+ "failures",
16443
+ "failure",
16444
+ "history",
16445
+ "runtime",
16446
+ "production",
16447
+ "prod",
16448
+ "traffic",
16449
+ "actually",
16450
+ "really",
16451
+ "depend",
16452
+ "depends",
16453
+ "dependency",
16454
+ "dependencies",
16455
+ "call",
16456
+ "calls",
16457
+ "use",
16458
+ "uses",
16459
+ "using",
16460
+ "import",
16461
+ "imports",
16462
+ "relies",
16463
+ "rely",
16464
+ "needs",
16465
+ "need",
16466
+ "sync"
16467
+ ]);
16468
+ function tokens(input) {
16469
+ const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
16470
+ return (spaced.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length >= 2);
16471
+ }
16472
+ function queryTokens(question) {
16473
+ const seen = /* @__PURE__ */ new Set();
16474
+ const out = [];
16475
+ for (const t of tokens(question)) {
16476
+ if (STOPWORDS.has(t) || INTENT_WORDS.has(t)) continue;
16477
+ if (seen.has(t)) continue;
16478
+ seen.add(t);
16479
+ out.push(t);
16480
+ }
16481
+ return out;
16482
+ }
16483
+ function nodeName(node) {
16484
+ return node.name ?? node.id;
16485
+ }
16486
+ function idBody(id) {
16487
+ const colon = id.indexOf(":");
16488
+ return colon >= 0 ? id.slice(colon + 1) : id;
16489
+ }
16490
+ var EMBED_MIN_SCORE = 0.35;
16491
+ async function resolveEntities(graph, question, searchIndex, maxNodes) {
16492
+ const qTokens = queryTokens(question);
16493
+ const normalized = question.toLowerCase();
16494
+ const best = /* @__PURE__ */ new Map();
16495
+ const consider = (cand) => {
16496
+ const cur = best.get(cand.nodeId);
16497
+ if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
16498
+ };
16499
+ graph.forEachNode((id, attrs) => {
16500
+ const node = attrs;
16501
+ if (node.type === import_types58.NodeType.FrontierNode) return;
16502
+ const name = nodeName(node);
16503
+ const body = idBody(id);
16504
+ const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
16505
+ if (labelTokens.size === 0) return;
16506
+ let matched = 0;
16507
+ for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
16508
+ const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
16509
+ if (matched === 0 && !named) return;
16510
+ const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
16511
+ const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
16512
+ const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
16513
+ consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
16514
+ });
16515
+ if (searchIndex) {
16516
+ try {
16517
+ const res = await searchIndex.search(question, 10);
16518
+ if (res.provider !== "substring") {
16519
+ for (const m of res.matches) {
16520
+ if (m.node.type === import_types58.NodeType.FrontierNode) continue;
16521
+ const already = best.get(m.node.id);
16522
+ if (!already && m.score < EMBED_MIN_SCORE) continue;
16523
+ consider({
16524
+ nodeId: m.node.id,
16525
+ label: nodeName(m.node),
16526
+ via: "embedding",
16527
+ score: Math.max(0, Math.min(1, m.score))
16528
+ });
16529
+ }
16530
+ }
16531
+ } catch {
16532
+ }
16533
+ }
16534
+ const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
16535
+ return [...best.values()].sort(
16536
+ (a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
16537
+ ).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
16538
+ }
16539
+ function incidentsForNode(nodeId, incidents) {
16540
+ if (!incidents || incidents.length === 0) return [];
16541
+ const svc = nodeId.replace(/^service:/, "");
16542
+ return incidents.filter((e) => e.affectedNode === nodeId || e.service === svc);
16543
+ }
16544
+ function edgeSignalNote(e) {
16545
+ const bits = [];
16546
+ if (e.signal) {
16547
+ bits.push(`${e.signal.spanCount} spans`);
16548
+ if (e.signal.errorCount > 0) bits.push(`${e.signal.errorCount} errors`);
16549
+ if (e.signal.latencyMs?.p95 !== void 0) bits.push(`p95 ${Math.round(e.signal.latencyMs.p95)}ms`);
16550
+ } else if (e.callCount !== void 0) {
16551
+ bits.push(`${e.callCount} calls`);
16552
+ }
16553
+ return bits.length ? ` (${bits.join(", ")})` : "";
16554
+ }
16555
+ function buildRootCauseSection(graph, node, incidents, now) {
16556
+ const result = getRootCause(graph, node, void 0, incidents, { now });
16557
+ if (!result) return null;
16558
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types58.Provenance.OBSERVED;
16559
+ const facts = [
16560
+ {
16561
+ text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
16562
+ provenance: lastProv,
16563
+ confidence: result.confidence
16564
+ }
16565
+ ];
16566
+ const candidates = result.candidates ?? [];
16567
+ for (const c of candidates.slice(0, 3)) {
16568
+ facts.push({
16569
+ text: `Candidate ${c.node} \u2014 ${c.classification}: ${c.reason}`,
16570
+ ...c.provenance ? { provenance: c.provenance } : {},
16571
+ confidence: c.confidence
16572
+ });
16573
+ }
16574
+ if (result.traversalPath.length > 1) {
16575
+ facts.push({ text: `Traversal: ${result.traversalPath.join(" \u2190 ")}` });
16576
+ }
16577
+ if (result.fixRecommendation) {
16578
+ facts.push({ text: `Recommended fix: ${result.fixRecommendation}` });
16579
+ }
16580
+ return { heading: "Root cause (navigation)", facts: facts.slice(0, MAX_FACTS_PER_SECTION) };
16581
+ }
16582
+ function buildDependenciesSection(graph, node) {
16583
+ const result = getTransitiveDependencies(graph, node, 2);
16584
+ if (result.total === 0) return null;
16585
+ const facts = result.dependencies.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16586
+ text: `${d.nodeId} \u2014 ${d.edgeType} (distance ${d.distance})`,
16587
+ // Per-fact provenance: the transitive walk mixes EXTRACTED and OBSERVED
16588
+ // edges, so the tag is on the fact, not asserted for the whole section.
16589
+ provenance: d.provenance
16590
+ }));
16591
+ return { heading: "Dependencies", facts };
16592
+ }
16593
+ function buildObservedSection(graph, node) {
16594
+ const result = getObservedDependencies(graph, node);
16595
+ const facts = [];
16596
+ for (const e of result.dependencies.slice(0, MAX_FACTS_PER_SECTION)) {
16597
+ const via = e.source !== node ? ` via ${e.source}` : "";
16598
+ facts.push({
16599
+ text: `calls ${e.target}${via}${edgeSignalNote(e)}`,
16600
+ provenance: e.provenance,
16601
+ confidence: confidenceForEdge(e)
16602
+ });
16603
+ }
16604
+ if (facts.length === 0) {
16605
+ if (result.observed && result.inboundObservedCount > 0) {
16606
+ facts.push({
16607
+ text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
16608
+ provenance: import_types58.Provenance.OBSERVED
16609
+ });
16610
+ } else {
16611
+ return null;
16612
+ }
16613
+ }
16614
+ return { heading: "Runtime dependencies (OBSERVED)", facts };
16615
+ }
16616
+ function buildBlastSection(graph, node) {
16617
+ const result = getBlastRadius(graph, node);
16618
+ if (result.totalAffected === 0) return null;
16619
+ const facts = result.affectedNodes.slice(0, MAX_FACTS_PER_SECTION).map((n) => ({
16620
+ text: `${n.nodeId} (distance ${n.distance})`,
16621
+ provenance: n.edgeProvenance,
16622
+ confidence: n.confidence
16623
+ }));
16624
+ return {
16625
+ heading: `Blast radius \u2014 ${result.totalAffected} dependent${result.totalAffected === 1 ? "" : "s"}`,
16626
+ facts
16627
+ };
16628
+ }
16629
+ function buildIncidentsSection(node, incidents) {
16630
+ const relevant = incidentsForNode(node, incidents);
16631
+ if (relevant.length === 0) return null;
16632
+ const ordered = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 4);
16633
+ const facts = ordered.map((ev) => ({
16634
+ text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
16635
+ // ErrorEvents are observation records — OBSERVED by definition.
16636
+ provenance: import_types58.Provenance.OBSERVED
16637
+ }));
16638
+ return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
16639
+ }
16640
+ function divergenceLine(d) {
16641
+ if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
16642
+ return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
16643
+ }
16644
+ return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
16645
+ }
16646
+ function buildDivergenceSection(graph, node) {
16647
+ const result = computeDivergences(graph, { node });
16648
+ if (result.totalAffected === 0) return null;
16649
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16650
+ text: divergenceLine(d),
16651
+ confidence: d.confidence
16652
+ // A divergence is composite by construction (EXTRACTED vs OBSERVED), so it
16653
+ // carries no single provenance — the fact leaves it unset.
16654
+ }));
16655
+ return {
16656
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16657
+ facts
16658
+ };
16659
+ }
16660
+ function buildGlobalDivergenceSection(graph) {
16661
+ const result = computeDivergences(graph);
16662
+ if (result.totalAffected === 0) {
16663
+ return {
16664
+ heading: "Divergences (EXTRACTED vs OBSERVED)",
16665
+ facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
16666
+ };
16667
+ }
16668
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
16669
+ text: divergenceLine(d),
16670
+ confidence: d.confidence
16671
+ // Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
16672
+ }));
16673
+ return {
16674
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
16675
+ facts
16676
+ };
16677
+ }
16678
+ function buildGlobalIncidentsSection(incidents) {
16679
+ if (!incidents || incidents.length === 0) {
16680
+ return {
16681
+ heading: "Incidents across the system",
16682
+ facts: [{ text: "None recorded \u2014 the OBSERVED incident store is empty." }]
16683
+ };
16684
+ }
16685
+ const byKey = /* @__PURE__ */ new Map();
16686
+ for (const ev of incidents) {
16687
+ const key = ev.affectedNode || (0, import_types58.serviceId)(ev.service);
16688
+ const cur = byKey.get(key);
16689
+ if (!cur) {
16690
+ byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
16691
+ } else {
16692
+ cur.count += 1;
16693
+ if (ev.timestamp > cur.latest) {
16694
+ cur.latest = ev.timestamp;
16695
+ cur.sampleMsg = ev.errorMessage;
16696
+ }
16697
+ }
16698
+ }
16699
+ const rows = [...byKey.values()].sort((a, b) => b.count - a.count || b.latest.localeCompare(a.latest) || a.key.localeCompare(b.key)).slice(0, MAX_FACTS_PER_SECTION);
16700
+ const facts = rows.map((r) => ({
16701
+ text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
16702
+ provenance: import_types58.Provenance.OBSERVED
16703
+ }));
16704
+ return {
16705
+ heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
16706
+ facts
16707
+ };
16708
+ }
16709
+ function buildOverviewSections(graph, incidents) {
16710
+ const nodeByType = /* @__PURE__ */ new Map();
16711
+ const services = [];
16712
+ graph.forEachNode((_id, attrs) => {
16713
+ const node = attrs;
16714
+ nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
16715
+ if (node.type === import_types58.NodeType.ServiceNode) services.push(node.id);
16716
+ });
16717
+ const edgeByProv = /* @__PURE__ */ new Map();
16718
+ graph.forEachEdge((_id, attrs) => {
16719
+ const p = attrs.provenance;
16720
+ edgeByProv.set(p, (edgeByProv.get(p) ?? 0) + 1);
16721
+ });
16722
+ const sections = [];
16723
+ const count = (t) => nodeByType.get(t) ?? 0;
16724
+ const shapeFacts = [
16725
+ { text: `${graph.order} nodes, ${graph.size} edges` },
16726
+ {
16727
+ text: `${count(import_types58.NodeType.ServiceNode)} services, ${count(import_types58.NodeType.FileNode)} files, ${count(import_types58.NodeType.SymbolNode)} symbols, ${count(import_types58.NodeType.DatabaseNode)} databases`
16728
+ }
16729
+ ];
16730
+ for (const p of [import_types58.Provenance.EXTRACTED, import_types58.Provenance.OBSERVED, import_types58.Provenance.INFERRED, import_types58.Provenance.STALE]) {
16731
+ const n = edgeByProv.get(p) ?? 0;
16732
+ if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
16733
+ }
16734
+ sections.push({ heading: "System shape", facts: shapeFacts });
16735
+ const depCounts = services.map((s) => ({ s, n: getTransitiveDependencies(graph, s, 1).total })).filter((r) => r.n > 0).sort((a, b) => b.n - a.n || a.s.localeCompare(b.s)).slice(0, MAX_FACTS_PER_SECTION);
16736
+ if (depCounts.length > 0) {
16737
+ sections.push({
16738
+ heading: "Busiest services (by direct dependencies)",
16739
+ facts: depCounts.map((r) => ({
16740
+ text: `${r.s} \u2014 ${r.n} direct dependenc${r.n === 1 ? "y" : "ies"}`
16741
+ }))
16742
+ });
16743
+ }
16744
+ if (incidents && incidents.length > 0) {
16745
+ const incCount = /* @__PURE__ */ new Map();
16746
+ for (const ev of incidents) {
16747
+ const key = ev.affectedNode || (0, import_types58.serviceId)(ev.service);
16748
+ incCount.set(key, (incCount.get(key) ?? 0) + 1);
16749
+ }
16750
+ const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
16751
+ sections.push({
16752
+ heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
16753
+ facts: top.map(([k, n]) => ({
16754
+ text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
16755
+ provenance: import_types58.Provenance.OBSERVED
16756
+ }))
16757
+ });
16758
+ }
16759
+ const div = computeDivergences(graph);
16760
+ sections.push({
16761
+ heading: "Divergences",
16762
+ facts: [
16763
+ {
16764
+ text: div.totalAffected === 0 ? "None \u2014 declared code and observed runtime agree across the graph." : `${div.totalAffected} divergence${div.totalAffected === 1 ? "" : "s"} between declared code and observed runtime \u2014 ask "are there any divergences?" for the list.`
16765
+ }
16766
+ ]
16767
+ });
16768
+ return sections;
16769
+ }
16770
+ function buildGlobalSections(intent, graph, incidents) {
16771
+ switch (intent) {
16772
+ case "divergence":
16773
+ return [buildGlobalDivergenceSection(graph)];
16774
+ case "incidents":
16775
+ return [buildGlobalIncidentsSection(incidents)];
16776
+ case "overview":
16777
+ return buildOverviewSections(graph, incidents);
16778
+ default:
16779
+ return null;
16780
+ }
16781
+ }
16782
+ var SECTION_ORDER = {
16783
+ "root-cause": ["root-cause", "incidents", "blast", "observed", "divergence"],
16784
+ "blast-radius": ["blast", "dependencies", "observed", "incidents"],
16785
+ dependencies: ["dependencies", "observed", "blast"],
16786
+ observed: ["observed", "dependencies", "incidents"],
16787
+ incidents: ["incidents", "root-cause", "observed"],
16788
+ divergence: ["divergence", "observed", "dependencies", "incidents"],
16789
+ overview: ["dependencies", "observed", "blast", "incidents", "divergence"]
16790
+ };
16791
+ function buildSection(kind, graph, node, incidents, now) {
16792
+ switch (kind) {
16793
+ case "root-cause":
16794
+ return buildRootCauseSection(graph, node, incidents, now);
16795
+ case "dependencies":
16796
+ return buildDependenciesSection(graph, node);
16797
+ case "observed":
16798
+ return buildObservedSection(graph, node);
16799
+ case "blast":
16800
+ return buildBlastSection(graph, node);
16801
+ case "incidents":
16802
+ return buildIncidentsSection(node, incidents);
16803
+ case "divergence":
16804
+ return buildDivergenceSection(graph, node);
16805
+ }
16806
+ }
16807
+ function summarizeGlobal(intent, sections) {
16808
+ const lead = sections[0];
16809
+ switch (intent) {
16810
+ case "divergence": {
16811
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
16812
+ return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
16813
+ }
16814
+ case "incidents": {
16815
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
16816
+ return none ? "No incidents recorded across the system \u2014 the OBSERVED incident store is empty." : `${lead.heading}, aggregated by node below.`;
16817
+ }
16818
+ case "overview":
16819
+ default:
16820
+ return `System overview across the whole graph: ${sections.length} view${sections.length === 1 ? "" : "s"} below \u2014 shape (nodes/edges by provenance), busiest services, incidents, and divergences.`;
16821
+ }
16822
+ }
16823
+ function summarize(question, intent, matched, primary, sections, scope) {
16824
+ if (scope === "global") {
16825
+ return summarizeGlobal(intent, sections);
16826
+ }
16827
+ if (!primary) {
16828
+ return `Nothing in "${question}" resolved to a node in the graph. Name a service, file, route, or table \u2014 e.g. \`ask "what does <service> depend on?"\`. For a graph-wide look, ask for an overview, divergences, or incidents.`;
16829
+ }
16830
+ const lead = sections[0];
16831
+ const others = matched.slice(1);
16832
+ const alsoNote = others.length ? ` Also matched: ${others.map((m) => m.nodeId).join(", ")}.` : "";
16833
+ let core;
16834
+ switch (intent) {
16835
+ case "root-cause": {
16836
+ const rc = sections.find((s) => s.heading === "Root cause (navigation)");
16837
+ if (rc) {
16838
+ core = rc.facts[0]?.text ?? "";
16839
+ } else if (lead) {
16840
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy, or the failure isn't recorded. Nearest context: ${lead.heading.toLowerCase()}.`;
16841
+ } else {
16842
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy.`;
16843
+ }
16844
+ break;
16845
+ }
16846
+ case "blast-radius":
16847
+ core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
16848
+ break;
16849
+ case "dependencies":
16850
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
16851
+ break;
16852
+ case "observed":
16853
+ core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
16854
+ break;
16855
+ case "incidents":
16856
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
16857
+ break;
16858
+ case "divergence":
16859
+ core = lead ? `${lead.heading} involving ${primary}.` : `No divergences involve ${primary} \u2014 declared and observed agree here.`;
16860
+ break;
16861
+ default:
16862
+ core = `${primary}: ${sections.length} view${sections.length === 1 ? "" : "s"} of its fused local context below.`;
16863
+ }
16864
+ return `${core}${alsoNote}`;
16865
+ }
16866
+ async function askGraph(graph, question, opts = {}) {
16867
+ const now = opts.now ?? Date.now();
16868
+ const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
16869
+ const intent = classifyIntent(question);
16870
+ const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
16871
+ const primary = matched[0]?.nodeId;
16872
+ const sections = [];
16873
+ let scope;
16874
+ if (primary) {
16875
+ scope = "node";
16876
+ for (const kind of SECTION_ORDER[intent]) {
16877
+ const s = buildSection(kind, graph, primary, opts.incidents, now);
16878
+ if (s && s.facts.length > 0) sections.push(s);
16879
+ }
16880
+ } else {
16881
+ const global = buildGlobalSections(intent, graph, opts.incidents);
16882
+ if (global) {
16883
+ scope = "global";
16884
+ for (const s of global) if (s.facts.length > 0) sections.push(s);
16885
+ }
16886
+ }
16887
+ const answer = summarize(question, intent, matched, primary, sections, scope);
16888
+ const provSet = /* @__PURE__ */ new Set();
16889
+ for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
16890
+ const confidence = sections[0]?.facts[0]?.confidence;
16891
+ return import_types58.AskResultSchema.parse({
16892
+ question,
16893
+ intent,
16894
+ matched,
16895
+ ...primary ? { primaryNode: primary } : {},
16896
+ ...scope ? { scope } : {},
16897
+ sections,
16898
+ answer,
16899
+ ...confidence !== void 0 ? { confidence } : {},
16900
+ provenance: [...provSet]
16901
+ });
16902
+ }
16903
+
16271
16904
  // src/diff.ts
16272
16905
  init_cjs_shims();
16273
16906
  var import_node_fs35 = require("fs");
@@ -16349,7 +16982,7 @@ function canonicalJson(value) {
16349
16982
  init_cjs_shims();
16350
16983
  var import_node_fs36 = require("fs");
16351
16984
  var import_node_path70 = __toESM(require("path"), 1);
16352
- var import_types58 = require("@neat.is/types");
16985
+ var import_types59 = require("@neat.is/types");
16353
16986
  var SCHEMA_VERSION = 7;
16354
16987
  function migrateV1ToV2(payload) {
16355
16988
  const nodes = payload.graph.nodes;
@@ -16373,7 +17006,7 @@ function migrateV5ToV6(payload) {
16373
17006
  if (Array.isArray(nodes)) {
16374
17007
  for (const node of nodes) {
16375
17008
  const attrs = node.attributes;
16376
- if (!attrs || attrs.type !== import_types58.NodeType.InfraNode) continue;
17009
+ if (!attrs || attrs.type !== import_types59.NodeType.InfraNode) continue;
16377
17010
  if (attrs.kind !== "sql-table" && attrs.kind !== "supabase-table") continue;
16378
17011
  if (!Array.isArray(attrs.columns)) attrs.columns = [];
16379
17012
  }
@@ -16389,12 +17022,12 @@ function migrateV2ToV3(payload) {
16389
17022
  for (const edge of edges) {
16390
17023
  const attrs = edge.attributes;
16391
17024
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
16392
- attrs.provenance = import_types58.Provenance.OBSERVED;
17025
+ attrs.provenance = import_types59.Provenance.OBSERVED;
16393
17026
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
16394
17027
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
16395
17028
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
16396
17029
  if (type && source && target) {
16397
- const newId = (0, import_types58.observedEdgeId)(source, target, type);
17030
+ const newId = (0, import_types59.observedEdgeId)(source, target, type);
16398
17031
  attrs.id = newId;
16399
17032
  if (edge.key) edge.key = newId;
16400
17033
  }
@@ -16552,7 +17185,7 @@ init_cjs_shims();
16552
17185
  var import_node_fs37 = require("fs");
16553
17186
  var import_node_os3 = __toESM(require("os"), 1);
16554
17187
  var import_node_path72 = __toESM(require("path"), 1);
16555
- var import_types59 = require("@neat.is/types");
17188
+ var import_types60 = require("@neat.is/types");
16556
17189
  function neatHome() {
16557
17190
  const override = process.env.NEAT_HOME;
16558
17191
  if (override && override.length > 0) return import_node_path72.default.resolve(override);
@@ -16642,7 +17275,7 @@ async function readRegistry() {
16642
17275
  throw err;
16643
17276
  }
16644
17277
  const parsed = JSON.parse(raw);
16645
- return import_types59.RegistryFileSchema.parse(parsed);
17278
+ return import_types60.RegistryFileSchema.parse(parsed);
16646
17279
  }
16647
17280
  async function getProject(name) {
16648
17281
  const reg = await readRegistry();
@@ -16923,15 +17556,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
16923
17556
 
16924
17557
  // src/connectors/index.ts
16925
17558
  init_cjs_shims();
16926
- var import_types60 = require("@neat.is/types");
17559
+ var import_types61 = require("@neat.is/types");
16927
17560
  var NO_ENV = "unknown";
16928
17561
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
16929
17562
  if (!graph.hasNode(targetNodeId)) return void 0;
16930
17563
  const sites = [];
16931
17564
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
16932
17565
  const edge = graph.getEdgeAttributes(edgeId);
16933
- if (edge.provenance !== import_types60.Provenance.EXTRACTED) continue;
16934
- const parsed = (0, import_types60.parseFileId)(edge.source);
17566
+ if (edge.provenance !== import_types61.Provenance.EXTRACTED) continue;
17567
+ const parsed = (0, import_types61.parseFileId)(edge.source);
16935
17568
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
16936
17569
  const site = { relPath: edge.evidence.file };
16937
17570
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -16942,7 +17575,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
16942
17575
  function routeCallSiteFor(graph, targetNodeId) {
16943
17576
  if (!graph.hasNode(targetNodeId)) return void 0;
16944
17577
  const attrs = graph.getNodeAttributes(targetNodeId);
16945
- if (attrs.type !== import_types60.NodeType.RouteNode || !attrs.path) return void 0;
17578
+ if (attrs.type !== import_types61.NodeType.RouteNode || !attrs.path) return void 0;
16946
17579
  const site = { relPath: attrs.path };
16947
17580
  if (attrs.line !== void 0) site.line = attrs.line;
16948
17581
  return site;
@@ -17513,23 +18146,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
17513
18146
 
17514
18147
  // src/connectors/supabase/resolve.ts
17515
18148
  init_cjs_shims();
17516
- var import_types62 = require("@neat.is/types");
18149
+ var import_types63 = require("@neat.is/types");
17517
18150
  function createSupabaseResolveTarget(graph, config) {
17518
18151
  return (signal, _ctx) => {
17519
18152
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
17520
18153
  return null;
17521
18154
  }
17522
- const subResourceId = (0, import_types62.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
18155
+ const subResourceId = (0, import_types63.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
17523
18156
  if (graph.hasNode(subResourceId)) {
17524
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types62.EdgeType.CALLS };
18157
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
17525
18158
  }
17526
- const bareResourceId = (0, import_types62.infraId)(signal.targetKind, signal.targetName);
18159
+ const bareResourceId = (0, import_types63.infraId)(signal.targetKind, signal.targetName);
17527
18160
  if (graph.hasNode(bareResourceId)) {
17528
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types62.EdgeType.CALLS };
18161
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
17529
18162
  }
17530
- const projectLevelId = (0, import_types62.infraId)("supabase", config.nodeRef);
18163
+ const projectLevelId = (0, import_types63.infraId)("supabase", config.nodeRef);
17531
18164
  if (graph.hasNode(projectLevelId)) {
17532
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types62.EdgeType.CALLS };
18165
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
17533
18166
  }
17534
18167
  return null;
17535
18168
  };
@@ -17622,7 +18255,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
17622
18255
 
17623
18256
  // src/connectors/railway/index.ts
17624
18257
  init_cjs_shims();
17625
- var import_types66 = require("@neat.is/types");
18258
+ var import_types67 = require("@neat.is/types");
17626
18259
 
17627
18260
  // src/connectors/railway/client.ts
17628
18261
  init_cjs_shims();
@@ -17773,7 +18406,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
17773
18406
  const out = [];
17774
18407
  graph.forEachNode((_id, attrs) => {
17775
18408
  const node = attrs;
17776
- if (node.type !== import_types66.NodeType.RouteNode) return;
18409
+ if (node.type !== import_types67.NodeType.RouteNode) return;
17777
18410
  const route = attrs;
17778
18411
  if (route.service !== serviceName) return;
17779
18412
  out.push({
@@ -17877,12 +18510,12 @@ function createRailwayResolveTarget(config) {
17877
18510
  const serviceName = config.serviceNameById[config.serviceId];
17878
18511
  if (!serviceName) return null;
17879
18512
  if (signal.targetKind === ROUTE_TARGET_KIND) {
17880
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types66.EdgeType.CALLS };
18513
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types67.EdgeType.CALLS };
17881
18514
  }
17882
18515
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
17883
18516
  const peerName = config.serviceNameById[signal.targetName];
17884
18517
  if (!peerName) return null;
17885
- return { targetNodeId: (0, import_types66.serviceId)(peerName), serviceName, edgeType: import_types66.EdgeType.CONNECTS_TO };
18518
+ return { targetNodeId: (0, import_types67.serviceId)(peerName), serviceName, edgeType: import_types67.EdgeType.CONNECTS_TO };
17886
18519
  }
17887
18520
  return null;
17888
18521
  };
@@ -18070,7 +18703,7 @@ function mapLogEntriesToSignals(entries) {
18070
18703
 
18071
18704
  // src/connectors/firebase/resolve.ts
18072
18705
  init_cjs_shims();
18073
- var import_types67 = require("@neat.is/types");
18706
+ var import_types68 = require("@neat.is/types");
18074
18707
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
18075
18708
  switch (resourceType) {
18076
18709
  case "cloud_function":
@@ -18085,7 +18718,7 @@ function routeEntriesFor(graph, serviceName) {
18085
18718
  const entries = [];
18086
18719
  graph.forEachNode((_id, attrs) => {
18087
18720
  const node = attrs;
18088
- if (node.type !== import_types67.NodeType.RouteNode) return;
18721
+ if (node.type !== import_types68.NodeType.RouteNode) return;
18089
18722
  const route = attrs;
18090
18723
  if (route.service !== serviceName) return;
18091
18724
  entries.push({
@@ -18117,7 +18750,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
18117
18750
  return {
18118
18751
  targetNodeId: match.routeNodeId,
18119
18752
  serviceName,
18120
- edgeType: import_types67.EdgeType.CALLS
18753
+ edgeType: import_types68.EdgeType.CALLS
18121
18754
  };
18122
18755
  };
18123
18756
  }
@@ -18144,7 +18777,7 @@ init_cjs_shims();
18144
18777
 
18145
18778
  // src/connectors/cloudflare/connector.ts
18146
18779
  init_cjs_shims();
18147
- var import_types69 = require("@neat.is/types");
18780
+ var import_types70 = require("@neat.is/types");
18148
18781
 
18149
18782
  // src/connectors/cloudflare/client.ts
18150
18783
  init_cjs_shims();
@@ -18308,7 +18941,7 @@ function findTaggedWorkerFileNode(graph, workerName) {
18308
18941
  graph.forEachNode((id, attrs) => {
18309
18942
  if (found) return;
18310
18943
  const a = attrs;
18311
- if (a.type === import_types69.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18944
+ if (a.type === import_types70.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
18312
18945
  found = id;
18313
18946
  }
18314
18947
  });
@@ -18320,7 +18953,7 @@ function findMatchingRouteNode(graph, serviceName, method, path76) {
18320
18953
  graph.forEachNode((id, attrs) => {
18321
18954
  if (found) return;
18322
18955
  const a = attrs;
18323
- if (a.type !== import_types69.NodeType.RouteNode || a.service !== serviceName) return;
18956
+ if (a.type !== import_types70.NodeType.RouteNode || a.service !== serviceName) return;
18324
18957
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
18325
18958
  const routeMethod = (a.method ?? "").toUpperCase();
18326
18959
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -18339,11 +18972,11 @@ function createCloudflareResolveTarget(config, graph) {
18339
18972
  };
18340
18973
  const mapping = config.workers?.[scriptName];
18341
18974
  if (mapping) {
18342
- const wholeFileId = (0, import_types69.fileId)(mapping.service, mapping.entryFile);
18975
+ const wholeFileId = (0, import_types70.fileId)(mapping.service, mapping.entryFile);
18343
18976
  return {
18344
18977
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
18345
18978
  serviceName: mapping.service,
18346
- edgeType: import_types69.EdgeType.CALLS
18979
+ edgeType: import_types70.EdgeType.CALLS
18347
18980
  };
18348
18981
  }
18349
18982
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -18352,13 +18985,13 @@ function createCloudflareResolveTarget(config, graph) {
18352
18985
  return {
18353
18986
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
18354
18987
  serviceName: fileNode.service,
18355
- edgeType: import_types69.EdgeType.CALLS
18988
+ edgeType: import_types70.EdgeType.CALLS
18356
18989
  };
18357
18990
  }
18358
18991
  return {
18359
- targetNodeId: (0, import_types69.infraId)("cloudflare-worker", scriptName),
18992
+ targetNodeId: (0, import_types70.infraId)("cloudflare-worker", scriptName),
18360
18993
  serviceName: scriptName,
18361
- edgeType: import_types69.EdgeType.CALLS,
18994
+ edgeType: import_types70.EdgeType.CALLS,
18362
18995
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
18363
18996
  };
18364
18997
  };
@@ -18554,14 +19187,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
18554
19187
 
18555
19188
  // src/connectors/neon/resolve.ts
18556
19189
  init_cjs_shims();
18557
- var import_types73 = require("@neat.is/types");
19190
+ var import_types74 = require("@neat.is/types");
18558
19191
  function createNeonResolveTarget(config) {
18559
19192
  return (signal) => {
18560
19193
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
18561
19194
  return {
18562
- targetNodeId: (0, import_types73.infraId)("sql-table", signal.targetName),
19195
+ targetNodeId: (0, import_types74.infraId)("sql-table", signal.targetName),
18563
19196
  serviceName: config.serviceName,
18564
- edgeType: import_types73.EdgeType.CALLS,
19197
+ edgeType: import_types74.EdgeType.CALLS,
18565
19198
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
18566
19199
  };
18567
19200
  };
@@ -18742,14 +19375,14 @@ function mapLogEntriesToSignals2(entries) {
18742
19375
 
18743
19376
  // src/connectors/cloud-run/resolve.ts
18744
19377
  init_cjs_shims();
18745
- var import_types77 = require("@neat.is/types");
19378
+ var import_types78 = require("@neat.is/types");
18746
19379
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
18747
19380
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
18748
19381
  let found = null;
18749
19382
  graph.forEachNode((_id, attrs) => {
18750
19383
  if (found) return;
18751
19384
  const node = attrs;
18752
- if (node.type !== import_types77.NodeType.RouteNode) return;
19385
+ if (node.type !== import_types78.NodeType.RouteNode) return;
18753
19386
  const route = attrs;
18754
19387
  if (route.service !== serviceName || !route.pathTemplate) return;
18755
19388
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -18774,13 +19407,13 @@ function createCloudRunResolveTarget(graph, config) {
18774
19407
  normalizePathTemplate(path76)
18775
19408
  );
18776
19409
  if (routeNodeId) {
18777
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types77.EdgeType.CALLS };
19410
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types78.EdgeType.CALLS };
18778
19411
  }
18779
19412
  }
18780
19413
  return {
18781
- targetNodeId: (0, import_types77.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
19414
+ targetNodeId: (0, import_types78.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
18782
19415
  serviceName: mappedService ?? gcpServiceName,
18783
- edgeType: import_types77.EdgeType.CALLS,
19416
+ edgeType: import_types78.EdgeType.CALLS,
18784
19417
  ensureInfraNode: {
18785
19418
  kind: CLOUD_RUN_SERVICE_INFRA_KIND,
18786
19419
  name: gcpServiceName,
@@ -18821,7 +19454,7 @@ function createCloudRunConnector(graph, config = {}) {
18821
19454
 
18822
19455
  // src/connectors/render/index.ts
18823
19456
  init_cjs_shims();
18824
- var import_types80 = require("@neat.is/types");
19457
+ var import_types81 = require("@neat.is/types");
18825
19458
 
18826
19459
  // src/connectors/render/types.ts
18827
19460
  init_cjs_shims();
@@ -18899,7 +19532,7 @@ function buildRenderRouteIndex(graph, serviceName) {
18899
19532
  const out = [];
18900
19533
  graph.forEachNode((_id, attrs) => {
18901
19534
  const node = attrs;
18902
- if (node.type !== import_types80.NodeType.RouteNode) return;
19535
+ if (node.type !== import_types81.NodeType.RouteNode) return;
18903
19536
  const route = attrs;
18904
19537
  if (route.service !== serviceName) return;
18905
19538
  out.push({
@@ -18984,7 +19617,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
18984
19617
  function createRenderResolveTarget(config) {
18985
19618
  return (signal) => {
18986
19619
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
18987
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types80.EdgeType.CALLS };
19620
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types81.EdgeType.CALLS };
18988
19621
  }
18989
19622
  return null;
18990
19623
  };
@@ -19122,21 +19755,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
19122
19755
 
19123
19756
  // src/connectors/planetscale/resolve.ts
19124
19757
  init_cjs_shims();
19125
- var import_types84 = require("@neat.is/types");
19758
+ var import_types85 = require("@neat.is/types");
19126
19759
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
19127
19760
  function createPlanetscaleResolveTarget(graph, config) {
19128
19761
  const databaseName = `${config.organization}/${config.database}`;
19129
19762
  return (signal, _ctx) => {
19130
19763
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
19131
- const tableId = (0, import_types84.infraId)("sql-table", signal.targetName);
19764
+ const tableId = (0, import_types85.infraId)("sql-table", signal.targetName);
19132
19765
  if (graph.hasNode(tableId)) {
19133
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types84.EdgeType.CALLS };
19766
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types85.EdgeType.CALLS };
19134
19767
  }
19135
- const providerId = (0, import_types84.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
19768
+ const providerId = (0, import_types85.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
19136
19769
  return {
19137
19770
  targetNodeId: providerId,
19138
19771
  serviceName: config.serviceName,
19139
- edgeType: import_types84.EdgeType.CALLS,
19772
+ edgeType: import_types85.EdgeType.CALLS,
19140
19773
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
19141
19774
  };
19142
19775
  };
@@ -19401,7 +20034,7 @@ function mapBuildsToSignals(builds, serviceName) {
19401
20034
 
19402
20035
  // src/connectors/eas/resolve.ts
19403
20036
  init_cjs_shims();
19404
- var import_types89 = require("@neat.is/types");
20037
+ var import_types90 = require("@neat.is/types");
19405
20038
  var NO_ENV2 = "unknown";
19406
20039
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
19407
20040
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -19417,8 +20050,8 @@ function configBasenamesForPhase(phase) {
19417
20050
  function configNodeService(graph, configNodeId) {
19418
20051
  for (const edgeId of graph.inboundEdges(configNodeId)) {
19419
20052
  const edge = graph.getEdgeAttributes(edgeId);
19420
- if (edge.type !== import_types89.EdgeType.CONFIGURED_BY) continue;
19421
- const parsed = (0, import_types89.parseFileId)(edge.source);
20053
+ if (edge.type !== import_types90.EdgeType.CONFIGURED_BY) continue;
20054
+ const parsed = (0, import_types90.parseFileId)(edge.source);
19422
20055
  if (parsed) return parsed.service;
19423
20056
  }
19424
20057
  return null;
@@ -19429,7 +20062,7 @@ function findConfigNode(graph, basenames, serviceName) {
19429
20062
  graph.forEachNode((id, attrs) => {
19430
20063
  if (scoped) return;
19431
20064
  const node = attrs;
19432
- if (node.type !== import_types89.NodeType.ConfigNode) return;
20065
+ if (node.type !== import_types90.NodeType.ConfigNode) return;
19433
20066
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
19434
20067
  if (anyMatch === null) anyMatch = id;
19435
20068
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -19446,13 +20079,13 @@ function createEasResolveTarget(graph) {
19446
20079
  if (basenames.length > 0) {
19447
20080
  const configNodeId = findConfigNode(graph, basenames, serviceName);
19448
20081
  if (configNodeId) {
19449
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types89.EdgeType.CALLS };
20082
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types90.EdgeType.CALLS };
19450
20083
  }
19451
20084
  }
19452
20085
  return {
19453
20086
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
19454
20087
  serviceName,
19455
- edgeType: import_types89.EdgeType.CALLS
20088
+ edgeType: import_types90.EdgeType.CALLS
19456
20089
  };
19457
20090
  };
19458
20091
  }
@@ -20131,11 +20764,11 @@ function registerRoutes(scope, ctx) {
20131
20764
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
20132
20765
  const parsed = [];
20133
20766
  for (const c of candidates) {
20134
- const r = import_types92.DivergenceTypeSchema.safeParse(c);
20767
+ const r = import_types93.DivergenceTypeSchema.safeParse(c);
20135
20768
  if (!r.success) {
20136
20769
  return reply.code(400).send({
20137
20770
  error: `unknown divergence type "${c}"`,
20138
- allowed: import_types92.DivergenceTypeSchema.options
20771
+ allowed: import_types93.DivergenceTypeSchema.options
20139
20772
  });
20140
20773
  }
20141
20774
  parsed.push(r.data);
@@ -20377,6 +21010,18 @@ function registerRoutes(scope, ctx) {
20377
21010
  matches: matches.slice(0, safeLimit)
20378
21011
  };
20379
21012
  });
21013
+ scope.get("/graph/ask", async (req, reply) => {
21014
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21015
+ if (!proj) return;
21016
+ const question = (req.query.q ?? "").trim();
21017
+ if (!question) return reply.code(400).send({ error: "query parameter `q` is required" });
21018
+ const epath = errorsPathFor(proj);
21019
+ const incidents = epath ? await readErrorEvents(epath) : [];
21020
+ return askGraph(proj.graph, question, {
21021
+ ...proj.searchIndex ? { searchIndex: proj.searchIndex } : {},
21022
+ incidents
21023
+ });
21024
+ });
20380
21025
  scope.get(
20381
21026
  "/graph/diff",
20382
21027
  async (req, reply) => {
@@ -20477,7 +21122,7 @@ function registerRoutes(scope, ctx) {
20477
21122
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
20478
21123
  let violations = await log.readAll();
20479
21124
  if (req.query.severity) {
20480
- const sev = import_types92.PolicySeveritySchema.safeParse(req.query.severity);
21125
+ const sev = import_types93.PolicySeveritySchema.safeParse(req.query.severity);
20481
21126
  if (!sev.success) {
20482
21127
  return reply.code(400).send({
20483
21128
  error: "invalid severity",
@@ -20516,7 +21161,7 @@ function registerRoutes(scope, ctx) {
20516
21161
  scope.post("/policies/check", async (req, reply) => {
20517
21162
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
20518
21163
  if (!proj) return;
20519
- const parsed = import_types92.PoliciesCheckBodySchema.safeParse(req.body ?? {});
21164
+ const parsed = import_types93.PoliciesCheckBodySchema.safeParse(req.body ?? {});
20520
21165
  if (!parsed.success) {
20521
21166
  return reply.code(400).send({
20522
21167
  error: "invalid /policies/check body",