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

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/cli.cjs CHANGED
@@ -61,8 +61,8 @@ function mountBearerAuth(app, opts) {
61
61
  ]);
62
62
  const publicRead = opts.publicRead === true;
63
63
  app.addHook("preHandler", (req, reply, done) => {
64
- const path92 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
- if (exactUnauthPaths.has(path92) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path92)) {
64
+ const path93 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path93) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path93)) {
66
66
  done();
67
67
  return;
68
68
  }
@@ -415,8 +415,8 @@ function websocketChannelPathOf(attrs) {
415
415
  const v = attrs[key];
416
416
  if (typeof v === "string" && v.length > 0) {
417
417
  const q = v.indexOf("?");
418
- const path92 = q === -1 ? v : v.slice(0, q);
419
- if (path92.length > 0) return path92;
418
+ const path93 = q === -1 ? v : v.slice(0, q);
419
+ if (path93.length > 0) return path93;
420
420
  }
421
421
  }
422
422
  return void 0;
@@ -799,13 +799,13 @@ __export(cli_exports, {
799
799
  runMonitorVerb: () => runMonitorVerb,
800
800
  runQueryVerb: () => runQueryVerb,
801
801
  runSkill: () => runSkill,
802
- usage: () => usage4
802
+ usage: () => usage5
803
803
  });
804
804
  module.exports = __toCommonJS(cli_exports);
805
805
  init_cjs_shims();
806
- var import_node_path91 = __toESM(require("path"), 1);
806
+ var import_node_path92 = __toESM(require("path"), 1);
807
807
  var import_node_os8 = __toESM(require("os"), 1);
808
- var import_node_fs55 = require("fs");
808
+ var import_node_fs56 = require("fs");
809
809
 
810
810
  // src/banner.ts
811
811
  init_cjs_shims();
@@ -1373,19 +1373,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1373
1373
  function longestIncomingWalk(graph, start, maxDepth) {
1374
1374
  let best = { path: [start], edges: [] };
1375
1375
  const visited = /* @__PURE__ */ new Set([start]);
1376
- function step(node, path92, edges) {
1377
- if (path92.length > best.path.length) {
1378
- best = { path: [...path92], edges: [...edges] };
1376
+ function step(node, path93, edges) {
1377
+ if (path93.length > best.path.length) {
1378
+ best = { path: [...path93], edges: [...edges] };
1379
1379
  }
1380
- if (path92.length - 1 >= maxDepth) return;
1380
+ if (path93.length - 1 >= maxDepth) return;
1381
1381
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1382
1382
  for (const [srcId, edge] of incoming) {
1383
1383
  if (visited.has(srcId)) continue;
1384
1384
  visited.add(srcId);
1385
- path92.push(srcId);
1385
+ path93.push(srcId);
1386
1386
  edges.push(edge);
1387
- step(srcId, path92, edges);
1388
- path92.pop();
1387
+ step(srcId, path93, edges);
1388
+ path93.pop();
1389
1389
  edges.pop();
1390
1390
  visited.delete(srcId);
1391
1391
  }
@@ -1400,7 +1400,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
1400
1400
  for (const id of walk10.path) {
1401
1401
  const owner = resolveOwningService(graph, id);
1402
1402
  if (!owner) continue;
1403
- const { id: serviceId15, svc } = owner;
1403
+ const { id: serviceId16, svc } = owner;
1404
1404
  const deps = svc.dependencies ?? {};
1405
1405
  for (const pair of candidatePairs) {
1406
1406
  const declared = deps[pair.driver];
@@ -1413,7 +1413,7 @@ function databaseRootCauseShape(graph, origin, walk10) {
1413
1413
  );
1414
1414
  if (!result.compatible) {
1415
1415
  return {
1416
- rootCauseNode: serviceId15,
1416
+ rootCauseNode: serviceId16,
1417
1417
  rootCauseReason: result.reason ?? "incompatible driver",
1418
1418
  ...result.minDriverVersion ? {
1419
1419
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1428,7 +1428,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
1428
1428
  for (const id of walk10.path) {
1429
1429
  const owner = resolveOwningService(graph, id);
1430
1430
  if (!owner) continue;
1431
- const { id: serviceId15, svc } = owner;
1431
+ const { id: serviceId16, svc } = owner;
1432
1432
  const deps = svc.dependencies ?? {};
1433
1433
  const serviceNodeEngine = svc.nodeEngine;
1434
1434
  for (const constraint of nodeEngineConstraints()) {
@@ -1437,7 +1437,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
1437
1437
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1438
1438
  if (!result.compatible && result.reason) {
1439
1439
  return {
1440
- rootCauseNode: serviceId15,
1440
+ rootCauseNode: serviceId16,
1441
1441
  rootCauseReason: result.reason,
1442
1442
  ...result.requiredNodeVersion ? {
1443
1443
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1452,7 +1452,7 @@ function serviceRootCauseShape(graph, _origin, walk10) {
1452
1452
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1453
1453
  if (!result.compatible && result.reason) {
1454
1454
  return {
1455
- rootCauseNode: serviceId15,
1455
+ rootCauseNode: serviceId16,
1456
1456
  rootCauseReason: result.reason,
1457
1457
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1458
1458
  };
@@ -1557,9 +1557,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
1557
1557
  function isFailingCallEdge(e) {
1558
1558
  return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
1559
1559
  }
1560
- function callSourcesForService(graph, serviceId15) {
1561
- const ids = [serviceId15];
1562
- for (const edgeId of graph.outboundEdges(serviceId15)) {
1560
+ function callSourcesForService(graph, serviceId16) {
1561
+ const ids = [serviceId16];
1562
+ for (const edgeId of graph.outboundEdges(serviceId16)) {
1563
1563
  const e = graph.getEdgeAttributes(edgeId);
1564
1564
  if (e.type !== import_types.EdgeType.CONTAINS) continue;
1565
1565
  const tgt = graph.getNodeAttributes(e.target);
@@ -1583,9 +1583,9 @@ function failingCallDominates(e, id, curEdge, curId) {
1583
1583
  }
1584
1584
  return id < curId;
1585
1585
  }
1586
- function dominantFailingCall(graph, serviceId15, visited) {
1586
+ function dominantFailingCall(graph, serviceId16, visited) {
1587
1587
  let best = null;
1588
- for (const src of callSourcesForService(graph, serviceId15)) {
1588
+ for (const src of callSourcesForService(graph, serviceId16)) {
1589
1589
  for (const edgeId of graph.outboundEdges(src)) {
1590
1590
  const e = graph.getEdgeAttributes(edgeId);
1591
1591
  if (!isFailingCallEdge(e)) continue;
@@ -1600,20 +1600,20 @@ function dominantFailingCall(graph, serviceId15, visited) {
1600
1600
  return best;
1601
1601
  }
1602
1602
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1603
- const path92 = [originServiceId];
1603
+ const path93 = [originServiceId];
1604
1604
  const edges = [];
1605
1605
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1606
1606
  let current = originServiceId;
1607
1607
  for (let depth = 0; depth < maxDepth; depth++) {
1608
1608
  const hop = dominantFailingCall(graph, current, visited);
1609
1609
  if (!hop) break;
1610
- path92.push(hop.nextService);
1610
+ path93.push(hop.nextService);
1611
1611
  edges.push(hop.edge);
1612
1612
  visited.add(hop.nextService);
1613
1613
  current = hop.nextService;
1614
1614
  }
1615
1615
  if (edges.length === 0) return null;
1616
- return { path: path92, edges, culprit: current };
1616
+ return { path: path93, edges, culprit: current };
1617
1617
  }
1618
1618
  function isStaleCallEdge(e) {
1619
1619
  return e.type === import_types.EdgeType.CALLS && e.provenance === import_types.Provenance.STALE;
@@ -1624,9 +1624,9 @@ function staleCallDominates(e, id, curEdge, curId) {
1624
1624
  if (ev !== cv) return ev > cv;
1625
1625
  return id < curId;
1626
1626
  }
1627
- function dominantStaleCall(graph, serviceId15, visited) {
1627
+ function dominantStaleCall(graph, serviceId16, visited) {
1628
1628
  const bestByCallee = /* @__PURE__ */ new Map();
1629
- for (const src of callSourcesForService(graph, serviceId15)) {
1629
+ for (const src of callSourcesForService(graph, serviceId16)) {
1630
1630
  for (const edgeId of graph.outboundEdges(src)) {
1631
1631
  const e = graph.getEdgeAttributes(edgeId);
1632
1632
  if (e.type !== import_types.EdgeType.CALLS) continue;
@@ -1649,26 +1649,26 @@ function dominantStaleCall(graph, serviceId15, visited) {
1649
1649
  return best;
1650
1650
  }
1651
1651
  function followStaleCallChain(graph, originServiceId, maxDepth) {
1652
- const path92 = [originServiceId];
1652
+ const path93 = [originServiceId];
1653
1653
  const edges = [];
1654
1654
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1655
1655
  let current = originServiceId;
1656
1656
  for (let depth = 0; depth < maxDepth; depth++) {
1657
1657
  const hop = dominantStaleCall(graph, current, visited);
1658
1658
  if (!hop) break;
1659
- path92.push(hop.nextService);
1659
+ path93.push(hop.nextService);
1660
1660
  edges.push(hop.edge);
1661
1661
  visited.add(hop.nextService);
1662
1662
  current = hop.nextService;
1663
1663
  }
1664
1664
  if (edges.length === 0) return null;
1665
- return { path: path92, edges, culprit: current };
1665
+ return { path: path93, edges, culprit: current };
1666
1666
  }
1667
1667
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1668
1668
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1669
1669
  if (!chain) return null;
1670
1670
  const culprit = chain.culprit;
1671
- const path92 = [...chain.path];
1671
+ const path93 = [...chain.path];
1672
1672
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1673
1673
  const baseConfidence = confidenceFromMix(chain.edges);
1674
1674
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1676,14 +1676,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1676
1676
  if (loc) {
1677
1677
  let rootCauseNode = culprit;
1678
1678
  if (loc.fileNode) {
1679
- path92.push(loc.fileNode);
1679
+ path93.push(loc.fileNode);
1680
1680
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1681
1681
  rootCauseNode = loc.fileNode;
1682
1682
  }
1683
1683
  return import_types.RootCauseResultSchema.parse({
1684
1684
  rootCauseNode,
1685
1685
  rootCauseReason: loc.rootCauseReason,
1686
- traversalPath: path92,
1686
+ traversalPath: path93,
1687
1687
  edgeProvenances,
1688
1688
  confidence,
1689
1689
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1695,7 +1695,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1695
1695
  return import_types.RootCauseResultSchema.parse({
1696
1696
  rootCauseNode: culprit,
1697
1697
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1698
- traversalPath: path92,
1698
+ traversalPath: path93,
1699
1699
  edgeProvenances,
1700
1700
  confidence,
1701
1701
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -2158,10 +2158,10 @@ function enrichWithNavigation(graph, errorNodeId, tagged, incidents, now) {
2158
2158
  traversalPath = staleChain.path;
2159
2159
  edgeProvenances = staleChain.edges.map((e) => e.provenance);
2160
2160
  } else if (top.node !== seedNode) {
2161
- const path92 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
2162
- if (path92) {
2163
- traversalPath = path92.nodes;
2164
- edgeProvenances = path92.edges.map((e) => e.provenance);
2161
+ const path93 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
2162
+ if (path93) {
2163
+ traversalPath = path93.nodes;
2164
+ edgeProvenances = path93.edges.map((e) => e.provenance);
2165
2165
  } else {
2166
2166
  traversalPath = [errorNodeId, top.node];
2167
2167
  edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
@@ -3609,8 +3609,8 @@ function chiRoutesFromSource(source, parser) {
3609
3609
  chiWalk(tree.rootNode, "", out);
3610
3610
  return out;
3611
3611
  }
3612
- function stripChiRegex(path92) {
3613
- return path92.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3612
+ function stripChiRegex(path93) {
3613
+ return path93.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3614
3614
  }
3615
3615
  function chiWalk(node, prefix, out) {
3616
3616
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -4282,9 +4282,9 @@ function rubyRocketRoute(args) {
4282
4282
  if (!pair || pair.type !== "pair") continue;
4283
4283
  const k = pair.childForFieldName("key");
4284
4284
  if (k?.type !== "string") continue;
4285
- const path92 = rubyLiteral(k);
4286
- if (path92 === null) continue;
4287
- return { path: path92, target: rubyLiteral(pair.childForFieldName("value")) };
4285
+ const path93 = rubyLiteral(k);
4286
+ if (path93 === null) continue;
4287
+ return { path: path93, target: rubyLiteral(pair.childForFieldName("value")) };
4288
4288
  }
4289
4289
  return null;
4290
4290
  }
@@ -4837,11 +4837,11 @@ function actixAttributeRoute(attrItem, out) {
4837
4837
  if (!nameNode) return;
4838
4838
  const macro = nameNode.type === "identifier" ? nameNode.text : nameNode.type === "scoped_identifier" ? nameNode.childForFieldName("name")?.text ?? null : null;
4839
4839
  if (!macro) return;
4840
- const tokens = attr.childForFieldName("arguments");
4841
- if (!tokens || tokens.type !== "token_tree") return;
4840
+ const tokens2 = attr.childForFieldName("arguments");
4841
+ if (!tokens2 || tokens2.type !== "token_tree") return;
4842
4842
  const strings = [];
4843
- for (let i = 0; i < tokens.namedChildCount; i++) {
4844
- const s = rustStringContent(tokens.namedChild(i));
4843
+ for (let i = 0; i < tokens2.namedChildCount; i++) {
4844
+ const s = rustStringContent(tokens2.namedChild(i));
4845
4845
  if (s !== null) strings.push(s);
4846
4846
  }
4847
4847
  const pathStr = strings[0];
@@ -6735,29 +6735,29 @@ function promoteFrontierNodes(graph, opts = {}) {
6735
6735
  toPromote.push({ frontierId: id, serviceId: target });
6736
6736
  });
6737
6737
  let promoted = 0;
6738
- for (const { frontierId: frontierId2, serviceId: serviceId15 } of toPromote) {
6738
+ for (const { frontierId: frontierId2, serviceId: serviceId16 } of toPromote) {
6739
6739
  if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
6740
6740
  const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
6741
6741
  if (!gate.allowed) {
6742
6742
  continue;
6743
6743
  }
6744
6744
  }
6745
- rewireFrontierEdges(graph, frontierId2, serviceId15);
6745
+ rewireFrontierEdges(graph, frontierId2, serviceId16);
6746
6746
  graph.dropNode(frontierId2);
6747
6747
  promoted++;
6748
6748
  }
6749
6749
  return promoted;
6750
6750
  }
6751
- function rewireFrontierEdges(graph, frontierId2, serviceId15) {
6751
+ function rewireFrontierEdges(graph, frontierId2, serviceId16) {
6752
6752
  const inbound = [...graph.inboundEdges(frontierId2)];
6753
6753
  const outbound = [...graph.outboundEdges(frontierId2)];
6754
6754
  for (const edgeId of inbound) {
6755
6755
  const edge = graph.getEdgeAttributes(edgeId);
6756
- rebuildEdge(graph, edge, edge.source, serviceId15, edgeId);
6756
+ rebuildEdge(graph, edge, edge.source, serviceId16, edgeId);
6757
6757
  }
6758
6758
  for (const edgeId of outbound) {
6759
6759
  const edge = graph.getEdgeAttributes(edgeId);
6760
- rebuildEdge(graph, edge, serviceId15, edge.target, edgeId);
6760
+ rebuildEdge(graph, edge, serviceId16, edge.target, edgeId);
6761
6761
  }
6762
6762
  }
6763
6763
  function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
@@ -7872,9 +7872,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
7872
7872
  "StatefulSet",
7873
7873
  "DaemonSet"
7874
7874
  ]);
7875
- function addAliases(graph, serviceId15, candidates) {
7876
- if (!graph.hasNode(serviceId15)) return;
7877
- const node = graph.getNodeAttributes(serviceId15);
7875
+ function addAliases(graph, serviceId16, candidates) {
7876
+ if (!graph.hasNode(serviceId16)) return;
7877
+ const node = graph.getNodeAttributes(serviceId16);
7878
7878
  if (node.type !== import_types19.NodeType.ServiceNode) return;
7879
7879
  const set = new Set(node.aliases ?? []);
7880
7880
  for (const c of candidates) {
@@ -7884,7 +7884,7 @@ function addAliases(graph, serviceId15, candidates) {
7884
7884
  }
7885
7885
  if (set.size === 0) return;
7886
7886
  const updated = { ...node, aliases: [...set].sort() };
7887
- graph.replaceNodeAttributes(serviceId15, updated);
7887
+ graph.replaceNodeAttributes(serviceId16, updated);
7888
7888
  }
7889
7889
  function indexServicesByName(services) {
7890
7890
  const map = /* @__PURE__ */ new Map();
@@ -7904,9 +7904,9 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
7904
7904
  }
7905
7905
  }
7906
7906
  if (!composePath) return;
7907
- let compose;
7907
+ let compose2;
7908
7908
  try {
7909
- compose = await readYaml(composePath);
7909
+ compose2 = await readYaml(composePath);
7910
7910
  } catch (err) {
7911
7911
  recordExtractionError(
7912
7912
  "aliases compose",
@@ -7915,14 +7915,14 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
7915
7915
  );
7916
7916
  return;
7917
7917
  }
7918
- if (!compose?.services) return;
7919
- for (const [composeName, svc] of Object.entries(compose.services)) {
7920
- const serviceId15 = serviceIndex.get(composeName);
7921
- if (!serviceId15) continue;
7918
+ if (!compose2?.services) return;
7919
+ for (const [composeName, svc] of Object.entries(compose2.services)) {
7920
+ const serviceId16 = serviceIndex.get(composeName);
7921
+ if (!serviceId16) continue;
7922
7922
  const aliases = /* @__PURE__ */ new Set([composeName]);
7923
7923
  if (svc.container_name) aliases.add(svc.container_name);
7924
7924
  if (svc.hostname) aliases.add(svc.hostname);
7925
- addAliases(graph, serviceId15, aliases);
7925
+ addAliases(graph, serviceId16, aliases);
7926
7926
  }
7927
7927
  }
7928
7928
  var LABEL_KEYS = /* @__PURE__ */ new Set([
@@ -9912,12 +9912,12 @@ async function resolveConfigs(literals, keys, serviceDir, looksLike, parse12) {
9912
9912
  const value = await interpolateEnvRefs(raw, serviceDir);
9913
9913
  if (!looksLike(value)) continue;
9914
9914
  const parsed = parse12(value);
9915
- if (parsed) out.push(parsed);
9915
+ if (parsed) out.push({ ...parsed, hostSource: "config" });
9916
9916
  }
9917
9917
  for (const lit of literals) {
9918
9918
  if (!looksLike(lit)) continue;
9919
9919
  const parsed = parse12(lit);
9920
- if (parsed) out.push(parsed);
9920
+ if (parsed) out.push({ ...parsed, hostSource: "literal" });
9921
9921
  }
9922
9922
  return out;
9923
9923
  }
@@ -10186,7 +10186,10 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
10186
10186
  type: import_types23.EdgeType.CONNECTS_TO,
10187
10187
  provenance: import_types23.Provenance.EXTRACTED,
10188
10188
  confidence: (0, import_types23.confidenceForExtracted)("structural"),
10189
- evidence: { file: evidenceFile }
10189
+ // Carry how the host was recovered (ADR-213) so the divergence ranker
10190
+ // can tell a real declared store from a hardcoded fault-injection /
10191
+ // flag-gated probe. Only set when the parser distinguished the two.
10192
+ evidence: config.hostSource ? { file: evidenceFile, hostSource: config.hostSource } : { file: evidenceFile }
10190
10193
  };
10191
10194
  if (!graph.hasEdge(edge.id)) {
10192
10195
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -14475,9 +14478,9 @@ async function addComposeInfra(graph, scanPath, services) {
14475
14478
  }
14476
14479
  }
14477
14480
  if (!composePath) return { nodesAdded, edgesAdded };
14478
- let compose;
14481
+ let compose2;
14479
14482
  try {
14480
- compose = await readYaml(composePath);
14483
+ compose2 = await readYaml(composePath);
14481
14484
  } catch (err) {
14482
14485
  recordExtractionError(
14483
14486
  "infra docker-compose",
@@ -14486,10 +14489,10 @@ async function addComposeInfra(graph, scanPath, services) {
14486
14489
  );
14487
14490
  return { nodesAdded, edgesAdded };
14488
14491
  }
14489
- if (!compose?.services) return { nodesAdded, edgesAdded };
14492
+ if (!compose2?.services) return { nodesAdded, edgesAdded };
14490
14493
  const evidenceFile = import_node_path58.default.relative(scanPath, composePath).split(import_node_path58.default.sep).join("/");
14491
14494
  const composeNameToNodeId = /* @__PURE__ */ new Map();
14492
- for (const [composeName, svc] of Object.entries(compose.services)) {
14495
+ for (const [composeName, svc] of Object.entries(compose2.services)) {
14493
14496
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
14494
14497
  if (matchedServiceId) {
14495
14498
  composeNameToNodeId.set(composeName, matchedServiceId);
@@ -14503,7 +14506,7 @@ async function addComposeInfra(graph, scanPath, services) {
14503
14506
  }
14504
14507
  composeNameToNodeId.set(composeName, node.id);
14505
14508
  }
14506
- for (const [composeName, svc] of Object.entries(compose.services)) {
14509
+ for (const [composeName, svc] of Object.entries(compose2.services)) {
14507
14510
  const sourceId = composeNameToNodeId.get(composeName);
14508
14511
  if (!sourceId) continue;
14509
14512
  for (const dep of dependsOnList(svc.depends_on)) {
@@ -15224,7 +15227,7 @@ async function addSupabaseProjects(graph, services, scanPath) {
15224
15227
 
15225
15228
  // src/extract/infra/index.ts
15226
15229
  async function addInfra(graph, scanPath, services) {
15227
- const compose = await addComposeInfra(graph, scanPath, services);
15230
+ const compose2 = await addComposeInfra(graph, scanPath, services);
15228
15231
  const dockerfile = await addDockerfileRuntimes(graph, services, scanPath);
15229
15232
  const terraform = await addTerraformResources(graph, scanPath);
15230
15233
  const k8s = await addK8sResources(graph, scanPath);
@@ -15233,8 +15236,8 @@ async function addInfra(graph, scanPath, services) {
15233
15236
  const railway = await addRailwayServices(graph, services, scanPath);
15234
15237
  const supabase = await addSupabaseProjects(graph, services, scanPath);
15235
15238
  return {
15236
- nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded + vercel.nodesAdded + railway.nodesAdded + supabase.nodesAdded,
15237
- edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded + vercel.edgesAdded + railway.edgesAdded + supabase.edgesAdded
15239
+ nodesAdded: compose2.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded + cloudflare.nodesAdded + vercel.nodesAdded + railway.nodesAdded + supabase.nodesAdded,
15240
+ edgesAdded: compose2.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded + cloudflare.edgesAdded + vercel.edgesAdded + railway.edgesAdded + supabase.edgesAdded
15238
15241
  };
15239
15242
  }
15240
15243
 
@@ -15981,6 +15984,53 @@ function detectColumnDrift(node) {
15981
15984
  function involvesNode(d, nodeId) {
15982
15985
  return d.source === nodeId || d.target === nodeId;
15983
15986
  }
15987
+ function datastoreEverObserved(graph, nodeId) {
15988
+ if (!graph.hasNode(nodeId)) return false;
15989
+ const n = graph.getNodeAttributes(nodeId);
15990
+ if (n.type === import_types57.NodeType.DatabaseNode) {
15991
+ const via = n.discoveredVia;
15992
+ if (via === "otel" || via === "merged") return true;
15993
+ }
15994
+ for (const edgeId of graph.inboundEdges(nodeId)) {
15995
+ const e = graph.getEdgeAttributes(edgeId);
15996
+ if (e.provenance === import_types57.Provenance.OBSERVED || e.provenance === import_types57.Provenance.STALE) return true;
15997
+ }
15998
+ return false;
15999
+ }
16000
+ function serviceHasObservedSameEngineStore(graph, buckets2, serviceId16, engine, excludeTarget) {
16001
+ for (const bucket of buckets2.values()) {
16002
+ if (bucket.type !== import_types57.EdgeType.CONNECTS_TO) continue;
16003
+ if (bucket.source !== serviceId16) continue;
16004
+ if (bucket.target === excludeTarget) continue;
16005
+ if (!graph.hasNode(bucket.target)) continue;
16006
+ const target = graph.getNodeAttributes(bucket.target);
16007
+ if (target.type !== import_types57.NodeType.DatabaseNode) continue;
16008
+ if (target.engine !== engine) continue;
16009
+ if (bucket.observed || datastoreEverObserved(graph, bucket.target)) return true;
16010
+ }
16011
+ return false;
16012
+ }
16013
+ var DEAD_CODE_PROBE_CONFIDENCE = 0.1;
16014
+ function dampenDeadCodeProbes(graph, buckets2, all) {
16015
+ return all.map((d) => {
16016
+ if (d.type !== "missing-observed") return d;
16017
+ if (!d.extracted || d.edgeType !== import_types57.EdgeType.CONNECTS_TO) return d;
16018
+ if (!graph.hasNode(d.target)) return d;
16019
+ const target = graph.getNodeAttributes(d.target);
16020
+ if (target.type !== import_types57.NodeType.DatabaseNode) return d;
16021
+ if (d.extracted.evidence?.hostSource !== "literal") return d;
16022
+ if (datastoreEverObserved(graph, d.target)) return d;
16023
+ const engine = target.engine;
16024
+ if (!serviceHasObservedSameEngineStore(graph, buckets2, d.source, engine, d.target)) return d;
16025
+ const host = target.host ?? target.name;
16026
+ return {
16027
+ ...d,
16028
+ confidence: Math.min(d.confidence, DEAD_CODE_PROBE_CONFIDENCE),
16029
+ reason: `${d.source} declares a ${engine} connection to a hardcoded-literal host (${host}) that production has never observed, while it does observe another ${engine} store \u2014 this reads as a flag-gated or dead-code declaration (e.g. a fault-injection probe), not a real declared-vs-observed gap.`,
16030
+ recommendation: "Confirm this is an intentional dead alternate \u2014 a fault-injection probe or a flag-gated branch. If it is meant to run in production, check the feature flag or conditional that gates it; if not, it can be ignored or removed."
16031
+ };
16032
+ });
16033
+ }
15984
16034
  function suppressHostMismatchHalves(all) {
15985
16035
  const observedHalf = /* @__PURE__ */ new Set();
15986
16036
  const declaredHalf = /* @__PURE__ */ new Set();
@@ -16017,7 +16067,8 @@ function computeDivergences(graph, opts = {}) {
16017
16067
  }
16018
16068
  });
16019
16069
  const reconciled = suppressHostMismatchHalves(all);
16020
- let filtered = reconciled;
16070
+ const dampened = dampenDeadCodeProbes(graph, buckets2, reconciled);
16071
+ let filtered = dampened;
16021
16072
  if (opts.type) {
16022
16073
  const allowed = opts.type;
16023
16074
  filtered = filtered.filter((d) => allowed.has(d.type));
@@ -16343,7 +16394,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
16343
16394
  init_cjs_shims();
16344
16395
  var import_fastify2 = __toESM(require("fastify"), 1);
16345
16396
  var import_cors = __toESM(require("@fastify/cors"), 1);
16346
- var import_types93 = require("@neat.is/types");
16397
+ var import_types94 = require("@neat.is/types");
16347
16398
 
16348
16399
  // src/extend/index.ts
16349
16400
  init_cjs_shims();
@@ -16733,6 +16784,639 @@ function queryLogEntries(opts) {
16733
16784
  return merged;
16734
16785
  }
16735
16786
 
16787
+ // src/ask.ts
16788
+ init_cjs_shims();
16789
+ var import_types60 = require("@neat.is/types");
16790
+ var DEFAULT_MAX_NODES = 3;
16791
+ var MAX_FACTS_PER_SECTION = 6;
16792
+ var INTENT_RULES = [
16793
+ {
16794
+ intent: "root-cause",
16795
+ test: /\b(why|root[\s-]?cause|failing|fail(?:s|ed)?|breaking|broke(?:n)?|crash(?:ing|ed)?|throw(?:ing|s)?|culprit|5\d\d)\b/
16796
+ },
16797
+ {
16798
+ intent: "blast-radius",
16799
+ 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/
16800
+ },
16801
+ {
16802
+ intent: "divergence",
16803
+ test: /\b(diverg\w*|weird|mismatch|drift|out\s+of\s+sync|inconsistent|disagree\w*|declared\s+vs|anything\s+wrong)\b/
16804
+ },
16805
+ {
16806
+ intent: "incidents",
16807
+ test: /\b(incidents?|recent\s+(?:errors?|failures?)|error\s+history|failure\s+history)\b/
16808
+ },
16809
+ {
16810
+ intent: "observed",
16811
+ test: /\b(at\s+runtime|in\s+prod(?:uction)?|actually\s+call\w*|really\s+call\w*|observed|runtime\s+traffic)\b/
16812
+ },
16813
+ {
16814
+ intent: "dependencies",
16815
+ test: /\b(depend\w*|calls?|uses?|imports?|relies\s+on|needs?)\b/
16816
+ }
16817
+ ];
16818
+ function classifyIntent(question) {
16819
+ const q = question.toLowerCase();
16820
+ for (const rule of INTENT_RULES) {
16821
+ if (rule.test.test(q)) return rule.intent;
16822
+ }
16823
+ return "overview";
16824
+ }
16825
+ var STOPWORDS = /* @__PURE__ */ new Set([
16826
+ "the",
16827
+ "a",
16828
+ "an",
16829
+ "is",
16830
+ "are",
16831
+ "was",
16832
+ "were",
16833
+ "be",
16834
+ "been",
16835
+ "being",
16836
+ "of",
16837
+ "to",
16838
+ "in",
16839
+ "on",
16840
+ "at",
16841
+ "by",
16842
+ "for",
16843
+ "with",
16844
+ "and",
16845
+ "or",
16846
+ "but",
16847
+ "if",
16848
+ "as",
16849
+ "this",
16850
+ "that",
16851
+ "these",
16852
+ "those",
16853
+ "it",
16854
+ "its",
16855
+ "do",
16856
+ "does",
16857
+ "did",
16858
+ "has",
16859
+ "have",
16860
+ "had",
16861
+ "what",
16862
+ "which",
16863
+ "who",
16864
+ "whom",
16865
+ "how",
16866
+ "when",
16867
+ "where",
16868
+ "my",
16869
+ "me",
16870
+ "i",
16871
+ "we",
16872
+ "you",
16873
+ "they",
16874
+ "them",
16875
+ "from",
16876
+ "into",
16877
+ "about",
16878
+ "would",
16879
+ "will",
16880
+ "can",
16881
+ "could",
16882
+ "should",
16883
+ "anything",
16884
+ "everything",
16885
+ "something",
16886
+ "all",
16887
+ "any",
16888
+ "some",
16889
+ "no",
16890
+ "not",
16891
+ "so",
16892
+ "up",
16893
+ "out",
16894
+ "over",
16895
+ "get",
16896
+ "got",
16897
+ "show",
16898
+ "tell",
16899
+ "explain",
16900
+ "find",
16901
+ "give",
16902
+ "happen",
16903
+ "happens",
16904
+ "happening",
16905
+ "here",
16906
+ "there",
16907
+ "system",
16908
+ "code",
16909
+ "service",
16910
+ "node",
16911
+ "graph"
16912
+ ]);
16913
+ var INTENT_WORDS = /* @__PURE__ */ new Set([
16914
+ "why",
16915
+ "root",
16916
+ "cause",
16917
+ "failing",
16918
+ "fail",
16919
+ "fails",
16920
+ "failed",
16921
+ "breaking",
16922
+ "broke",
16923
+ "broken",
16924
+ "crash",
16925
+ "crashing",
16926
+ "crashed",
16927
+ "throw",
16928
+ "throwing",
16929
+ "throws",
16930
+ "culprit",
16931
+ "blast",
16932
+ "radius",
16933
+ "break",
16934
+ "breaks",
16935
+ "impact",
16936
+ "downstream",
16937
+ "dependent",
16938
+ "dependents",
16939
+ "redeploy",
16940
+ "affect",
16941
+ "affects",
16942
+ "affected",
16943
+ "diverge",
16944
+ "divergence",
16945
+ "divergences",
16946
+ "weird",
16947
+ "mismatch",
16948
+ "drift",
16949
+ "inconsistent",
16950
+ "disagree",
16951
+ "declared",
16952
+ "observed",
16953
+ "incident",
16954
+ "incidents",
16955
+ "recent",
16956
+ "errors",
16957
+ "error",
16958
+ "failures",
16959
+ "failure",
16960
+ "history",
16961
+ "runtime",
16962
+ "production",
16963
+ "prod",
16964
+ "traffic",
16965
+ "actually",
16966
+ "really",
16967
+ "depend",
16968
+ "depends",
16969
+ "dependency",
16970
+ "dependencies",
16971
+ "call",
16972
+ "calls",
16973
+ "use",
16974
+ "uses",
16975
+ "using",
16976
+ "import",
16977
+ "imports",
16978
+ "relies",
16979
+ "rely",
16980
+ "needs",
16981
+ "need",
16982
+ "sync"
16983
+ ]);
16984
+ function tokens(input) {
16985
+ const spaced = input.replace(/([a-z0-9])([A-Z])/g, "$1 $2");
16986
+ return (spaced.toLowerCase().match(/[a-z0-9]+/g) ?? []).filter((t) => t.length >= 2);
16987
+ }
16988
+ function queryTokens(question) {
16989
+ const seen = /* @__PURE__ */ new Set();
16990
+ const out = [];
16991
+ for (const t of tokens(question)) {
16992
+ if (STOPWORDS.has(t) || INTENT_WORDS.has(t)) continue;
16993
+ if (seen.has(t)) continue;
16994
+ seen.add(t);
16995
+ out.push(t);
16996
+ }
16997
+ return out;
16998
+ }
16999
+ function nodeName(node) {
17000
+ return node.name ?? node.id;
17001
+ }
17002
+ function idBody(id) {
17003
+ const colon = id.indexOf(":");
17004
+ return colon >= 0 ? id.slice(colon + 1) : id;
17005
+ }
17006
+ var EMBED_MIN_SCORE = 0.35;
17007
+ async function resolveEntities(graph, question, searchIndex, maxNodes) {
17008
+ const qTokens = queryTokens(question);
17009
+ const normalized = question.toLowerCase();
17010
+ const best = /* @__PURE__ */ new Map();
17011
+ const consider = (cand) => {
17012
+ const cur = best.get(cand.nodeId);
17013
+ if (!cur || cand.score > cur.score) best.set(cand.nodeId, cand);
17014
+ };
17015
+ graph.forEachNode((id, attrs) => {
17016
+ const node = attrs;
17017
+ if (node.type === import_types60.NodeType.FrontierNode) return;
17018
+ const name = nodeName(node);
17019
+ const body = idBody(id);
17020
+ const labelTokens = /* @__PURE__ */ new Set([...tokens(body), ...tokens(name)]);
17021
+ if (labelTokens.size === 0) return;
17022
+ let matched = 0;
17023
+ for (const t of qTokens) if (labelTokens.has(t)) matched += 1;
17024
+ const named = body.length >= 2 && normalized.includes(body.toLowerCase()) || name.length >= 2 && normalized.includes(name.toLowerCase());
17025
+ if (matched === 0 && !named) return;
17026
+ const coverage = qTokens.length > 0 ? matched / qTokens.length : 0;
17027
+ const via = named ? "id" : matched > 0 && tokens(name).some((t) => qTokens.includes(t)) ? "label" : "token";
17028
+ const score = named ? Math.max(0.9, coverage) : Math.min(0.85, 0.3 + 0.7 * coverage);
17029
+ consider({ nodeId: id, label: name, via, score: Math.min(1, score) });
17030
+ });
17031
+ if (searchIndex) {
17032
+ try {
17033
+ const res = await searchIndex.search(question, 10);
17034
+ if (res.provider !== "substring") {
17035
+ for (const m of res.matches) {
17036
+ if (m.node.type === import_types60.NodeType.FrontierNode) continue;
17037
+ const already = best.get(m.node.id);
17038
+ if (!already && m.score < EMBED_MIN_SCORE) continue;
17039
+ consider({
17040
+ nodeId: m.node.id,
17041
+ label: nodeName(m.node),
17042
+ via: "embedding",
17043
+ score: Math.max(0, Math.min(1, m.score))
17044
+ });
17045
+ }
17046
+ }
17047
+ } catch {
17048
+ }
17049
+ }
17050
+ const viaRank = { id: 3, label: 2, token: 1, embedding: 0 };
17051
+ return [...best.values()].sort(
17052
+ (a, b) => b.score - a.score || viaRank[b.via] - viaRank[a.via] || a.nodeId.localeCompare(b.nodeId)
17053
+ ).slice(0, maxNodes).map((s) => ({ nodeId: s.nodeId, label: s.label, via: s.via, score: Number(s.score.toFixed(3)) }));
17054
+ }
17055
+ function incidentsForNode(nodeId, incidents) {
17056
+ if (!incidents || incidents.length === 0) return [];
17057
+ const svc = nodeId.replace(/^service:/, "");
17058
+ return incidents.filter((e) => e.affectedNode === nodeId || e.service === svc);
17059
+ }
17060
+ function edgeSignalNote(e) {
17061
+ const bits = [];
17062
+ if (e.signal) {
17063
+ bits.push(`${e.signal.spanCount} spans`);
17064
+ if (e.signal.errorCount > 0) bits.push(`${e.signal.errorCount} errors`);
17065
+ if (e.signal.latencyMs?.p95 !== void 0) bits.push(`p95 ${Math.round(e.signal.latencyMs.p95)}ms`);
17066
+ } else if (e.callCount !== void 0) {
17067
+ bits.push(`${e.callCount} calls`);
17068
+ }
17069
+ return bits.length ? ` (${bits.join(", ")})` : "";
17070
+ }
17071
+ function buildRootCauseSection(graph, node, incidents, now) {
17072
+ const result = getRootCause(graph, node, void 0, incidents, { now });
17073
+ if (!result) return null;
17074
+ const lastProv = result.edgeProvenances[result.edgeProvenances.length - 1] ?? import_types60.Provenance.OBSERVED;
17075
+ const facts = [
17076
+ {
17077
+ text: `Root cause: ${result.rootCauseNode} \u2014 ${result.rootCauseReason}`,
17078
+ provenance: lastProv,
17079
+ confidence: result.confidence
17080
+ }
17081
+ ];
17082
+ const candidates = result.candidates ?? [];
17083
+ for (const c of candidates.slice(0, 3)) {
17084
+ facts.push({
17085
+ text: `Candidate ${c.node} \u2014 ${c.classification}: ${c.reason}`,
17086
+ ...c.provenance ? { provenance: c.provenance } : {},
17087
+ confidence: c.confidence
17088
+ });
17089
+ }
17090
+ if (result.traversalPath.length > 1) {
17091
+ facts.push({ text: `Traversal: ${result.traversalPath.join(" \u2190 ")}` });
17092
+ }
17093
+ if (result.fixRecommendation) {
17094
+ facts.push({ text: `Recommended fix: ${result.fixRecommendation}` });
17095
+ }
17096
+ return { heading: "Root cause (navigation)", facts: facts.slice(0, MAX_FACTS_PER_SECTION) };
17097
+ }
17098
+ function buildDependenciesSection(graph, node) {
17099
+ const result = getTransitiveDependencies(graph, node, 2);
17100
+ if (result.total === 0) return null;
17101
+ const facts = result.dependencies.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17102
+ text: `${d.nodeId} \u2014 ${d.edgeType} (distance ${d.distance})`,
17103
+ // Per-fact provenance: the transitive walk mixes EXTRACTED and OBSERVED
17104
+ // edges, so the tag is on the fact, not asserted for the whole section.
17105
+ provenance: d.provenance
17106
+ }));
17107
+ return { heading: "Dependencies", facts };
17108
+ }
17109
+ function buildObservedSection(graph, node) {
17110
+ const result = getObservedDependencies(graph, node);
17111
+ const facts = [];
17112
+ for (const e of result.dependencies.slice(0, MAX_FACTS_PER_SECTION)) {
17113
+ const via = e.source !== node ? ` via ${e.source}` : "";
17114
+ facts.push({
17115
+ text: `calls ${e.target}${via}${edgeSignalNote(e)}`,
17116
+ provenance: e.provenance,
17117
+ confidence: confidenceForEdge(e)
17118
+ });
17119
+ }
17120
+ if (facts.length === 0) {
17121
+ if (result.observed && result.inboundObservedCount > 0) {
17122
+ facts.push({
17123
+ text: `no outbound runtime calls, but OTel observed ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 a pure receiver`,
17124
+ provenance: import_types60.Provenance.OBSERVED
17125
+ });
17126
+ } else {
17127
+ return null;
17128
+ }
17129
+ }
17130
+ return { heading: "Runtime dependencies (OBSERVED)", facts };
17131
+ }
17132
+ function buildBlastSection(graph, node) {
17133
+ const result = getBlastRadius(graph, node);
17134
+ if (result.totalAffected === 0) return null;
17135
+ const facts = result.affectedNodes.slice(0, MAX_FACTS_PER_SECTION).map((n) => ({
17136
+ text: `${n.nodeId} (distance ${n.distance})`,
17137
+ provenance: n.edgeProvenance,
17138
+ confidence: n.confidence
17139
+ }));
17140
+ return {
17141
+ heading: `Blast radius \u2014 ${result.totalAffected} dependent${result.totalAffected === 1 ? "" : "s"}`,
17142
+ facts
17143
+ };
17144
+ }
17145
+ function buildIncidentsSection(node, incidents) {
17146
+ const relevant = incidentsForNode(node, incidents);
17147
+ if (relevant.length === 0) return null;
17148
+ const ordered = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp)).slice(0, 4);
17149
+ const facts = ordered.map((ev) => ({
17150
+ text: `${ev.timestamp} \u2014 ${ev.service}: ${ev.errorMessage}`,
17151
+ // ErrorEvents are observation records — OBSERVED by definition.
17152
+ provenance: import_types60.Provenance.OBSERVED
17153
+ }));
17154
+ return { heading: `Recent incidents (OBSERVED) \u2014 ${relevant.length} recorded`, facts };
17155
+ }
17156
+ function divergenceLine(d) {
17157
+ if ((d.type === "missing-observed" || d.type === "missing-extracted") && d.column) {
17158
+ return `[${d.type}] ${d.table ?? d.source} column ${d.column} \u2014 ${d.reason}`;
17159
+ }
17160
+ return `[${d.type}] ${d.source} \u2192 ${d.target} \u2014 ${d.reason}`;
17161
+ }
17162
+ function buildDivergenceSection(graph, node) {
17163
+ const result = computeDivergences(graph, { node });
17164
+ if (result.totalAffected === 0) return null;
17165
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17166
+ text: divergenceLine(d),
17167
+ confidence: d.confidence
17168
+ // A divergence is composite by construction (EXTRACTED vs OBSERVED), so it
17169
+ // carries no single provenance — the fact leaves it unset.
17170
+ }));
17171
+ return {
17172
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
17173
+ facts
17174
+ };
17175
+ }
17176
+ function buildGlobalDivergenceSection(graph) {
17177
+ const result = computeDivergences(graph);
17178
+ if (result.totalAffected === 0) {
17179
+ return {
17180
+ heading: "Divergences (EXTRACTED vs OBSERVED)",
17181
+ facts: [{ text: "None \u2014 declared code and observed runtime agree across the graph." }]
17182
+ };
17183
+ }
17184
+ const facts = result.divergences.slice(0, MAX_FACTS_PER_SECTION).map((d) => ({
17185
+ text: divergenceLine(d),
17186
+ confidence: d.confidence
17187
+ // Composite by construction (EXTRACTED vs OBSERVED), so no single provenance.
17188
+ }));
17189
+ return {
17190
+ heading: `Divergences (EXTRACTED vs OBSERVED) \u2014 ${result.totalAffected}`,
17191
+ facts
17192
+ };
17193
+ }
17194
+ function buildGlobalIncidentsSection(incidents) {
17195
+ if (!incidents || incidents.length === 0) {
17196
+ return {
17197
+ heading: "Incidents across the system",
17198
+ facts: [{ text: "None recorded \u2014 the OBSERVED incident store is empty." }]
17199
+ };
17200
+ }
17201
+ const byKey = /* @__PURE__ */ new Map();
17202
+ for (const ev of incidents) {
17203
+ const key = ev.affectedNode || (0, import_types60.serviceId)(ev.service);
17204
+ const cur = byKey.get(key);
17205
+ if (!cur) {
17206
+ byKey.set(key, { key, count: 1, latest: ev.timestamp, sampleMsg: ev.errorMessage });
17207
+ } else {
17208
+ cur.count += 1;
17209
+ if (ev.timestamp > cur.latest) {
17210
+ cur.latest = ev.timestamp;
17211
+ cur.sampleMsg = ev.errorMessage;
17212
+ }
17213
+ }
17214
+ }
17215
+ 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);
17216
+ const facts = rows.map((r) => ({
17217
+ text: `${r.key} \u2014 ${r.count} incident${r.count === 1 ? "" : "s"}, latest ${r.latest}: ${r.sampleMsg}`,
17218
+ provenance: import_types60.Provenance.OBSERVED
17219
+ }));
17220
+ return {
17221
+ heading: `Incidents across the system \u2014 ${incidents.length} recorded`,
17222
+ facts
17223
+ };
17224
+ }
17225
+ function buildOverviewSections(graph, incidents) {
17226
+ const nodeByType = /* @__PURE__ */ new Map();
17227
+ const services = [];
17228
+ graph.forEachNode((_id, attrs) => {
17229
+ const node = attrs;
17230
+ nodeByType.set(node.type, (nodeByType.get(node.type) ?? 0) + 1);
17231
+ if (node.type === import_types60.NodeType.ServiceNode) services.push(node.id);
17232
+ });
17233
+ const edgeByProv = /* @__PURE__ */ new Map();
17234
+ graph.forEachEdge((_id, attrs) => {
17235
+ const p = attrs.provenance;
17236
+ edgeByProv.set(p, (edgeByProv.get(p) ?? 0) + 1);
17237
+ });
17238
+ const sections = [];
17239
+ const count = (t) => nodeByType.get(t) ?? 0;
17240
+ const shapeFacts = [
17241
+ { text: `${graph.order} nodes, ${graph.size} edges` },
17242
+ {
17243
+ text: `${count(import_types60.NodeType.ServiceNode)} services, ${count(import_types60.NodeType.FileNode)} files, ${count(import_types60.NodeType.SymbolNode)} symbols, ${count(import_types60.NodeType.DatabaseNode)} databases`
17244
+ }
17245
+ ];
17246
+ for (const p of [import_types60.Provenance.EXTRACTED, import_types60.Provenance.OBSERVED, import_types60.Provenance.INFERRED, import_types60.Provenance.STALE]) {
17247
+ const n = edgeByProv.get(p) ?? 0;
17248
+ if (n > 0) shapeFacts.push({ text: `${n} ${p} edge${n === 1 ? "" : "s"}`, provenance: p });
17249
+ }
17250
+ sections.push({ heading: "System shape", facts: shapeFacts });
17251
+ 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);
17252
+ if (depCounts.length > 0) {
17253
+ sections.push({
17254
+ heading: "Busiest services (by direct dependencies)",
17255
+ facts: depCounts.map((r) => ({
17256
+ text: `${r.s} \u2014 ${r.n} direct dependenc${r.n === 1 ? "y" : "ies"}`
17257
+ }))
17258
+ });
17259
+ }
17260
+ if (incidents && incidents.length > 0) {
17261
+ const incCount = /* @__PURE__ */ new Map();
17262
+ for (const ev of incidents) {
17263
+ const key = ev.affectedNode || (0, import_types60.serviceId)(ev.service);
17264
+ incCount.set(key, (incCount.get(key) ?? 0) + 1);
17265
+ }
17266
+ const top = [...incCount.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, MAX_FACTS_PER_SECTION);
17267
+ sections.push({
17268
+ heading: `Nodes with incidents \u2014 ${incidents.length} recorded`,
17269
+ facts: top.map(([k, n]) => ({
17270
+ text: `${k} \u2014 ${n} incident${n === 1 ? "" : "s"}`,
17271
+ provenance: import_types60.Provenance.OBSERVED
17272
+ }))
17273
+ });
17274
+ }
17275
+ const div = computeDivergences(graph);
17276
+ sections.push({
17277
+ heading: "Divergences",
17278
+ facts: [
17279
+ {
17280
+ 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.`
17281
+ }
17282
+ ]
17283
+ });
17284
+ return sections;
17285
+ }
17286
+ function buildGlobalSections(intent, graph, incidents) {
17287
+ switch (intent) {
17288
+ case "divergence":
17289
+ return [buildGlobalDivergenceSection(graph)];
17290
+ case "incidents":
17291
+ return [buildGlobalIncidentsSection(incidents)];
17292
+ case "overview":
17293
+ return buildOverviewSections(graph, incidents);
17294
+ default:
17295
+ return null;
17296
+ }
17297
+ }
17298
+ var SECTION_ORDER = {
17299
+ "root-cause": ["root-cause", "incidents", "blast", "observed", "divergence"],
17300
+ "blast-radius": ["blast", "dependencies", "observed", "incidents"],
17301
+ dependencies: ["dependencies", "observed", "blast"],
17302
+ observed: ["observed", "dependencies", "incidents"],
17303
+ incidents: ["incidents", "root-cause", "observed"],
17304
+ divergence: ["divergence", "observed", "dependencies", "incidents"],
17305
+ overview: ["dependencies", "observed", "blast", "incidents", "divergence"]
17306
+ };
17307
+ function buildSection(kind, graph, node, incidents, now) {
17308
+ switch (kind) {
17309
+ case "root-cause":
17310
+ return buildRootCauseSection(graph, node, incidents, now);
17311
+ case "dependencies":
17312
+ return buildDependenciesSection(graph, node);
17313
+ case "observed":
17314
+ return buildObservedSection(graph, node);
17315
+ case "blast":
17316
+ return buildBlastSection(graph, node);
17317
+ case "incidents":
17318
+ return buildIncidentsSection(node, incidents);
17319
+ case "divergence":
17320
+ return buildDivergenceSection(graph, node);
17321
+ }
17322
+ }
17323
+ function summarizeGlobal(intent, sections) {
17324
+ const lead = sections[0];
17325
+ switch (intent) {
17326
+ case "divergence": {
17327
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
17328
+ return none ? "No divergences across the graph \u2014 declared code and observed runtime agree." : `${lead.heading} across the graph, highest-confidence first below.`;
17329
+ }
17330
+ case "incidents": {
17331
+ const none = lead?.facts[0]?.text.startsWith("None") ?? true;
17332
+ return none ? "No incidents recorded across the system \u2014 the OBSERVED incident store is empty." : `${lead.heading}, aggregated by node below.`;
17333
+ }
17334
+ case "overview":
17335
+ default:
17336
+ 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.`;
17337
+ }
17338
+ }
17339
+ function summarize(question, intent, matched, primary, sections, scope) {
17340
+ if (scope === "global") {
17341
+ return summarizeGlobal(intent, sections);
17342
+ }
17343
+ if (!primary) {
17344
+ 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.`;
17345
+ }
17346
+ const lead = sections[0];
17347
+ const others = matched.slice(1);
17348
+ const alsoNote = others.length ? ` Also matched: ${others.map((m) => m.nodeId).join(", ")}.` : "";
17349
+ let core;
17350
+ switch (intent) {
17351
+ case "root-cause": {
17352
+ const rc = sections.find((s) => s.heading === "Root cause (navigation)");
17353
+ if (rc) {
17354
+ core = rc.facts[0]?.text ?? "";
17355
+ } else if (lead) {
17356
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy, or the failure isn't recorded. Nearest context: ${lead.heading.toLowerCase()}.`;
17357
+ } else {
17358
+ core = `No root cause surfaced for ${primary} \u2014 it may be healthy.`;
17359
+ }
17360
+ break;
17361
+ }
17362
+ case "blast-radius":
17363
+ core = lead ? `${lead.heading} of ${primary}.` : `${primary} has no dependents \u2014 nothing else would break if it failed.`;
17364
+ break;
17365
+ case "dependencies":
17366
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()} listed below.` : `${primary} has no declared dependencies in the graph.`;
17367
+ break;
17368
+ case "observed":
17369
+ core = lead ? `${primary} at runtime: ${lead.facts.length} OBSERVED fact${lead.facts.length === 1 ? "" : "s"}.` : `No runtime traffic OBSERVED for ${primary}.`;
17370
+ break;
17371
+ case "incidents":
17372
+ core = lead ? `${primary}: ${lead.heading.toLowerCase()}.` : `No incidents recorded against ${primary}.`;
17373
+ break;
17374
+ case "divergence":
17375
+ core = lead ? `${lead.heading} involving ${primary}.` : `No divergences involve ${primary} \u2014 declared and observed agree here.`;
17376
+ break;
17377
+ default:
17378
+ core = `${primary}: ${sections.length} view${sections.length === 1 ? "" : "s"} of its fused local context below.`;
17379
+ }
17380
+ return `${core}${alsoNote}`;
17381
+ }
17382
+ async function askGraph(graph, question, opts = {}) {
17383
+ const now = opts.now ?? Date.now();
17384
+ const maxNodes = opts.maxNodes ?? DEFAULT_MAX_NODES;
17385
+ const intent = classifyIntent(question);
17386
+ const matched = await resolveEntities(graph, question, opts.searchIndex, maxNodes);
17387
+ const primary = matched[0]?.nodeId;
17388
+ const sections = [];
17389
+ let scope;
17390
+ if (primary) {
17391
+ scope = "node";
17392
+ for (const kind of SECTION_ORDER[intent]) {
17393
+ const s = buildSection(kind, graph, primary, opts.incidents, now);
17394
+ if (s && s.facts.length > 0) sections.push(s);
17395
+ }
17396
+ } else {
17397
+ const global = buildGlobalSections(intent, graph, opts.incidents);
17398
+ if (global) {
17399
+ scope = "global";
17400
+ for (const s of global) if (s.facts.length > 0) sections.push(s);
17401
+ }
17402
+ }
17403
+ const answer = summarize(question, intent, matched, primary, sections, scope);
17404
+ const provSet = /* @__PURE__ */ new Set();
17405
+ for (const s of sections) for (const f of s.facts) if (f.provenance) provSet.add(f.provenance);
17406
+ const confidence = sections[0]?.facts[0]?.confidence;
17407
+ return import_types60.AskResultSchema.parse({
17408
+ question,
17409
+ intent,
17410
+ matched,
17411
+ ...primary ? { primaryNode: primary } : {},
17412
+ ...scope ? { scope } : {},
17413
+ sections,
17414
+ answer,
17415
+ ...confidence !== void 0 ? { confidence } : {},
17416
+ provenance: [...provSet]
17417
+ });
17418
+ }
17419
+
16736
17420
  // src/diff.ts
16737
17421
  init_cjs_shims();
16738
17422
  var import_node_fs38 = require("fs");
@@ -16867,7 +17551,7 @@ init_cjs_shims();
16867
17551
  var import_node_fs39 = require("fs");
16868
17552
  var import_node_os3 = __toESM(require("os"), 1);
16869
17553
  var import_node_path74 = __toESM(require("path"), 1);
16870
- var import_types60 = require("@neat.is/types");
17554
+ var import_types61 = require("@neat.is/types");
16871
17555
  var LOCK_TIMEOUT_MS = 5e3;
16872
17556
  var LOCK_RETRY_MS = 50;
16873
17557
  function neatHome() {
@@ -17121,10 +17805,10 @@ async function readRegistry() {
17121
17805
  throw err;
17122
17806
  }
17123
17807
  const parsed = JSON.parse(raw);
17124
- return import_types60.RegistryFileSchema.parse(parsed);
17808
+ return import_types61.RegistryFileSchema.parse(parsed);
17125
17809
  }
17126
17810
  async function writeRegistry(reg) {
17127
- const validated = import_types60.RegistryFileSchema.parse(reg);
17811
+ const validated = import_types61.RegistryFileSchema.parse(reg);
17128
17812
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
17129
17813
  }
17130
17814
  var ProjectNameCollisionError = class extends Error {
@@ -17637,15 +18321,15 @@ function getConnectorStatus(id, now = Date.now(), thresholdMs = CONNECTOR_STALE_
17637
18321
 
17638
18322
  // src/connectors/index.ts
17639
18323
  init_cjs_shims();
17640
- var import_types61 = require("@neat.is/types");
18324
+ var import_types62 = require("@neat.is/types");
17641
18325
  var NO_ENV = "unknown";
17642
18326
  function staticCallSiteFor(graph, serviceName, targetNodeId) {
17643
18327
  if (!graph.hasNode(targetNodeId)) return void 0;
17644
18328
  const sites = [];
17645
18329
  for (const edgeId of graph.inboundEdges(targetNodeId)) {
17646
18330
  const edge = graph.getEdgeAttributes(edgeId);
17647
- if (edge.provenance !== import_types61.Provenance.EXTRACTED) continue;
17648
- const parsed = (0, import_types61.parseFileId)(edge.source);
18331
+ if (edge.provenance !== import_types62.Provenance.EXTRACTED) continue;
18332
+ const parsed = (0, import_types62.parseFileId)(edge.source);
17649
18333
  if (!parsed || parsed.service !== serviceName || !edge.evidence) continue;
17650
18334
  const site = { relPath: edge.evidence.file };
17651
18335
  if (edge.evidence.line !== void 0) site.line = edge.evidence.line;
@@ -17656,7 +18340,7 @@ function staticCallSiteFor(graph, serviceName, targetNodeId) {
17656
18340
  function routeCallSiteFor(graph, targetNodeId) {
17657
18341
  if (!graph.hasNode(targetNodeId)) return void 0;
17658
18342
  const attrs = graph.getNodeAttributes(targetNodeId);
17659
- if (attrs.type !== import_types61.NodeType.RouteNode || !attrs.path) return void 0;
18343
+ if (attrs.type !== import_types62.NodeType.RouteNode || !attrs.path) return void 0;
17660
18344
  const site = { relPath: attrs.path };
17661
18345
  if (attrs.line !== void 0) site.line = attrs.line;
17662
18346
  return site;
@@ -18154,10 +18838,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
18154
18838
  // src/connectors/supabase/map.ts
18155
18839
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
18156
18840
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
18157
- function targetFromRestPath(path92) {
18158
- const rpcMatch = REST_RPC_PATH_RE.exec(path92);
18841
+ function targetFromRestPath(path93) {
18842
+ const rpcMatch = REST_RPC_PATH_RE.exec(path93);
18159
18843
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
18160
- const tableMatch = REST_TABLE_PATH_RE.exec(path92);
18844
+ const tableMatch = REST_TABLE_PATH_RE.exec(path93);
18161
18845
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
18162
18846
  return null;
18163
18847
  }
@@ -18268,23 +18952,23 @@ async function fetchPgStatStatements(connectionString, limit = DEFAULT_STATEMENT
18268
18952
 
18269
18953
  // src/connectors/supabase/resolve.ts
18270
18954
  init_cjs_shims();
18271
- var import_types63 = require("@neat.is/types");
18955
+ var import_types64 = require("@neat.is/types");
18272
18956
  function createSupabaseResolveTarget(graph, config) {
18273
18957
  return (signal, _ctx) => {
18274
18958
  if (signal.targetKind !== SUPABASE_TABLE_TARGET_KIND && signal.targetKind !== SUPABASE_RPC_TARGET_KIND) {
18275
18959
  return null;
18276
18960
  }
18277
- const subResourceId = (0, import_types63.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
18961
+ const subResourceId = (0, import_types64.infraId)(signal.targetKind, `${config.nodeRef}/${signal.targetName}`);
18278
18962
  if (graph.hasNode(subResourceId)) {
18279
- return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
18963
+ return { targetNodeId: subResourceId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
18280
18964
  }
18281
- const bareResourceId = (0, import_types63.infraId)(signal.targetKind, signal.targetName);
18965
+ const bareResourceId = (0, import_types64.infraId)(signal.targetKind, signal.targetName);
18282
18966
  if (graph.hasNode(bareResourceId)) {
18283
- return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
18967
+ return { targetNodeId: bareResourceId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
18284
18968
  }
18285
- const projectLevelId = (0, import_types63.infraId)("supabase", config.nodeRef);
18969
+ const projectLevelId = (0, import_types64.infraId)("supabase", config.nodeRef);
18286
18970
  if (graph.hasNode(projectLevelId)) {
18287
- return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types63.EdgeType.CALLS };
18971
+ return { targetNodeId: projectLevelId, serviceName: config.serviceName, edgeType: import_types64.EdgeType.CALLS };
18288
18972
  }
18289
18973
  return null;
18290
18974
  };
@@ -18377,7 +19061,7 @@ function createSupabaseConnector(graph, config, deps = {}) {
18377
19061
 
18378
19062
  // src/connectors/railway/index.ts
18379
19063
  init_cjs_shims();
18380
- var import_types67 = require("@neat.is/types");
19064
+ var import_types68 = require("@neat.is/types");
18381
19065
 
18382
19066
  // src/connectors/railway/client.ts
18383
19067
  init_cjs_shims();
@@ -18528,7 +19212,7 @@ function buildRailwayRouteIndex(graph, serviceName) {
18528
19212
  const out = [];
18529
19213
  graph.forEachNode((_id, attrs) => {
18530
19214
  const node = attrs;
18531
- if (node.type !== import_types67.NodeType.RouteNode) return;
19215
+ if (node.type !== import_types68.NodeType.RouteNode) return;
18532
19216
  const route = attrs;
18533
19217
  if (route.service !== serviceName) return;
18534
19218
  out.push({
@@ -18632,12 +19316,12 @@ function createRailwayResolveTarget(config) {
18632
19316
  const serviceName = config.serviceNameById[config.serviceId];
18633
19317
  if (!serviceName) return null;
18634
19318
  if (signal.targetKind === ROUTE_TARGET_KIND) {
18635
- return { targetNodeId: signal.targetName, serviceName, edgeType: import_types67.EdgeType.CALLS };
19319
+ return { targetNodeId: signal.targetName, serviceName, edgeType: import_types68.EdgeType.CALLS };
18636
19320
  }
18637
19321
  if (signal.targetKind === PEER_SERVICE_TARGET_KIND) {
18638
19322
  const peerName = config.serviceNameById[signal.targetName];
18639
19323
  if (!peerName) return null;
18640
- return { targetNodeId: (0, import_types67.serviceId)(peerName), serviceName, edgeType: import_types67.EdgeType.CONNECTS_TO };
19324
+ return { targetNodeId: (0, import_types68.serviceId)(peerName), serviceName, edgeType: import_types68.EdgeType.CONNECTS_TO };
18641
19325
  }
18642
19326
  return null;
18643
19327
  };
@@ -18761,9 +19445,9 @@ function parseFirebaseTargetName(targetName) {
18761
19445
  const secondSep = rest.indexOf(FIELD_SEP);
18762
19446
  if (secondSep === -1) return null;
18763
19447
  const method = rest.slice(0, secondSep);
18764
- const path92 = rest.slice(secondSep + 1);
18765
- if (!resourceName || !method || !path92) return null;
18766
- return { resourceName, method, path: path92 };
19448
+ const path93 = rest.slice(secondSep + 1);
19449
+ if (!resourceName || !method || !path93) return null;
19450
+ return { resourceName, method, path: path93 };
18767
19451
  }
18768
19452
  function resourceNameFor(type, labels) {
18769
19453
  if (!labels) return null;
@@ -18801,14 +19485,14 @@ function mapLogEntryToSignal(entry2) {
18801
19485
  if (!req) return null;
18802
19486
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
18803
19487
  const method = req.requestMethod.toUpperCase();
18804
- const path92 = pathFromRequestUrl(req.requestUrl);
18805
- if (path92 === null) return null;
19488
+ const path93 = pathFromRequestUrl(req.requestUrl);
19489
+ if (path93 === null) return null;
18806
19490
  const timestamp = entry2.timestamp;
18807
19491
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
18808
19492
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
18809
19493
  return {
18810
19494
  targetKind: resourceType,
18811
- targetName: packFirebaseTargetName({ resourceName, method, path: path92 }),
19495
+ targetName: packFirebaseTargetName({ resourceName, method, path: path93 }),
18812
19496
  callCount: 1,
18813
19497
  errorCount: isError ? 1 : 0,
18814
19498
  lastObservedIso: timestamp
@@ -18825,7 +19509,7 @@ function mapLogEntriesToSignals(entries) {
18825
19509
 
18826
19510
  // src/connectors/firebase/resolve.ts
18827
19511
  init_cjs_shims();
18828
- var import_types68 = require("@neat.is/types");
19512
+ var import_types69 = require("@neat.is/types");
18829
19513
  function neatServiceNameFor(resourceType, resourceName, serviceMap) {
18830
19514
  switch (resourceType) {
18831
19515
  case "cloud_function":
@@ -18840,7 +19524,7 @@ function routeEntriesFor(graph, serviceName) {
18840
19524
  const entries = [];
18841
19525
  graph.forEachNode((_id, attrs) => {
18842
19526
  const node = attrs;
18843
- if (node.type !== import_types68.NodeType.RouteNode) return;
19527
+ if (node.type !== import_types69.NodeType.RouteNode) return;
18844
19528
  const route = attrs;
18845
19529
  if (route.service !== serviceName) return;
18846
19530
  entries.push({
@@ -18872,7 +19556,7 @@ function createFirebaseResolveTarget(graph, serviceMap) {
18872
19556
  return {
18873
19557
  targetNodeId: match.routeNodeId,
18874
19558
  serviceName,
18875
- edgeType: import_types68.EdgeType.CALLS
19559
+ edgeType: import_types69.EdgeType.CALLS
18876
19560
  };
18877
19561
  };
18878
19562
  }
@@ -18899,7 +19583,7 @@ init_cjs_shims();
18899
19583
 
18900
19584
  // src/connectors/cloudflare/connector.ts
18901
19585
  init_cjs_shims();
18902
- var import_types70 = require("@neat.is/types");
19586
+ var import_types71 = require("@neat.is/types");
18903
19587
 
18904
19588
  // src/connectors/cloudflare/client.ts
18905
19589
  init_cjs_shims();
@@ -19015,7 +19699,7 @@ function mapEventToSignal(event) {
19015
19699
  if (Number.isNaN(observedAt.getTime())) return null;
19016
19700
  const statusCode = metadata?.statusCode;
19017
19701
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
19018
- const path92 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
19702
+ const path93 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
19019
19703
  return {
19020
19704
  targetKind: CLOUDFLARE_TARGET_KIND,
19021
19705
  targetName: scriptName,
@@ -19023,7 +19707,7 @@ function mapEventToSignal(event) {
19023
19707
  errorCount: isError ? 1 : 0,
19024
19708
  lastObservedIso: observedAt.toISOString(),
19025
19709
  method,
19026
- ...path92 ? { path: path92 } : {},
19710
+ ...path93 ? { path: path93 } : {},
19027
19711
  ...typeof statusCode === "number" ? { statusCode } : {},
19028
19712
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
19029
19713
  };
@@ -19063,19 +19747,19 @@ function findTaggedWorkerFileNode(graph, workerName) {
19063
19747
  graph.forEachNode((id, attrs) => {
19064
19748
  if (found) return;
19065
19749
  const a = attrs;
19066
- if (a.type === import_types70.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
19750
+ if (a.type === import_types71.NodeType.FileNode && a.platform === "cloudflare" && a.platformName === workerName) {
19067
19751
  found = id;
19068
19752
  }
19069
19753
  });
19070
19754
  return found;
19071
19755
  }
19072
- function findMatchingRouteNode(graph, serviceName, method, path92) {
19073
- const normalizedPath = normalizePathTemplate(path92);
19756
+ function findMatchingRouteNode(graph, serviceName, method, path93) {
19757
+ const normalizedPath = normalizePathTemplate(path93);
19074
19758
  let found = null;
19075
19759
  graph.forEachNode((id, attrs) => {
19076
19760
  if (found) return;
19077
19761
  const a = attrs;
19078
- if (a.type !== import_types70.NodeType.RouteNode || a.service !== serviceName) return;
19762
+ if (a.type !== import_types71.NodeType.RouteNode || a.service !== serviceName) return;
19079
19763
  if (!a.pathTemplate || normalizePathTemplate(a.pathTemplate) !== normalizedPath) return;
19080
19764
  const routeMethod = (a.method ?? "").toUpperCase();
19081
19765
  if (routeMethod !== "ALL" && routeMethod !== method) return;
@@ -19087,18 +19771,18 @@ function createCloudflareResolveTarget(config, graph) {
19087
19771
  return (signal) => {
19088
19772
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
19089
19773
  const scriptName = signal.targetName;
19090
- const { method, path: path92 } = signal;
19774
+ const { method, path: path93 } = signal;
19091
19775
  const resolveRouteGrain = (serviceName, wholeFileId) => {
19092
- if (!method || !path92) return wholeFileId;
19093
- return findMatchingRouteNode(graph, serviceName, method, path92) ?? wholeFileId;
19776
+ if (!method || !path93) return wholeFileId;
19777
+ return findMatchingRouteNode(graph, serviceName, method, path93) ?? wholeFileId;
19094
19778
  };
19095
19779
  const mapping = config.workers?.[scriptName];
19096
19780
  if (mapping) {
19097
- const wholeFileId = (0, import_types70.fileId)(mapping.service, mapping.entryFile);
19781
+ const wholeFileId = (0, import_types71.fileId)(mapping.service, mapping.entryFile);
19098
19782
  return {
19099
19783
  targetNodeId: resolveRouteGrain(mapping.service, wholeFileId),
19100
19784
  serviceName: mapping.service,
19101
- edgeType: import_types70.EdgeType.CALLS
19785
+ edgeType: import_types71.EdgeType.CALLS
19102
19786
  };
19103
19787
  }
19104
19788
  const taggedFileId = findTaggedWorkerFileNode(graph, scriptName);
@@ -19107,13 +19791,13 @@ function createCloudflareResolveTarget(config, graph) {
19107
19791
  return {
19108
19792
  targetNodeId: resolveRouteGrain(fileNode.service, taggedFileId),
19109
19793
  serviceName: fileNode.service,
19110
- edgeType: import_types70.EdgeType.CALLS
19794
+ edgeType: import_types71.EdgeType.CALLS
19111
19795
  };
19112
19796
  }
19113
19797
  return {
19114
- targetNodeId: (0, import_types70.infraId)("cloudflare-worker", scriptName),
19798
+ targetNodeId: (0, import_types71.infraId)("cloudflare-worker", scriptName),
19115
19799
  serviceName: scriptName,
19116
- edgeType: import_types70.EdgeType.CALLS,
19800
+ edgeType: import_types71.EdgeType.CALLS,
19117
19801
  ensureInfraNode: { kind: "cloudflare-worker", name: scriptName, provider: "cloudflare" }
19118
19802
  };
19119
19803
  };
@@ -19309,14 +19993,14 @@ function diffNeonStatementsToSignals(rows, previous, observedAtIso) {
19309
19993
 
19310
19994
  // src/connectors/neon/resolve.ts
19311
19995
  init_cjs_shims();
19312
- var import_types74 = require("@neat.is/types");
19996
+ var import_types75 = require("@neat.is/types");
19313
19997
  function createNeonResolveTarget(config) {
19314
19998
  return (signal) => {
19315
19999
  if (signal.targetKind !== NEON_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
19316
20000
  return {
19317
- targetNodeId: (0, import_types74.infraId)("sql-table", signal.targetName),
20001
+ targetNodeId: (0, import_types75.infraId)("sql-table", signal.targetName),
19318
20002
  serviceName: config.serviceName,
19319
- edgeType: import_types74.EdgeType.CALLS,
20003
+ edgeType: import_types75.EdgeType.CALLS,
19320
20004
  ensureInfraNode: { kind: "sql-table", name: signal.targetName, provider: "neon" }
19321
20005
  };
19322
20006
  };
@@ -19442,9 +20126,9 @@ function parseCloudRunTargetName(targetName) {
19442
20126
  const secondSep = rest.indexOf(FIELD_SEP2);
19443
20127
  if (secondSep === -1) return null;
19444
20128
  const method = rest.slice(0, secondSep);
19445
- const path92 = rest.slice(secondSep + 1);
19446
- if (!serviceName || !method || !path92) return null;
19447
- return { serviceName, method, path: path92 };
20129
+ const path93 = rest.slice(secondSep + 1);
20130
+ if (!serviceName || !method || !path93) return null;
20131
+ return { serviceName, method, path: path93 };
19448
20132
  }
19449
20133
 
19450
20134
  // src/connectors/cloud-run/map.ts
@@ -19473,14 +20157,14 @@ function mapLogEntryToSignal2(entry2) {
19473
20157
  if (!req) return null;
19474
20158
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
19475
20159
  const method = req.requestMethod.toUpperCase();
19476
- const path92 = pathFromRequestUrl2(req.requestUrl);
19477
- if (path92 === null) return null;
20160
+ const path93 = pathFromRequestUrl2(req.requestUrl);
20161
+ if (path93 === null) return null;
19478
20162
  const timestamp = entry2.timestamp;
19479
20163
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
19480
20164
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
19481
20165
  return {
19482
20166
  targetKind: CLOUD_RUN_TARGET_KIND,
19483
- targetName: packCloudRunTargetName({ serviceName, method, path: path92 }),
20167
+ targetName: packCloudRunTargetName({ serviceName, method, path: path93 }),
19484
20168
  callCount: 1,
19485
20169
  errorCount: isError ? 1 : 0,
19486
20170
  lastObservedIso: timestamp
@@ -19497,14 +20181,14 @@ function mapLogEntriesToSignals2(entries) {
19497
20181
 
19498
20182
  // src/connectors/cloud-run/resolve.ts
19499
20183
  init_cjs_shims();
19500
- var import_types78 = require("@neat.is/types");
20184
+ var import_types79 = require("@neat.is/types");
19501
20185
  var CLOUD_RUN_SERVICE_INFRA_KIND = "cloud-run-service";
19502
20186
  function findMatchingRouteNode2(graph, serviceName, method, normalizedPath) {
19503
20187
  let found = null;
19504
20188
  graph.forEachNode((_id, attrs) => {
19505
20189
  if (found) return;
19506
20190
  const node = attrs;
19507
- if (node.type !== import_types78.NodeType.RouteNode) return;
20191
+ if (node.type !== import_types79.NodeType.RouteNode) return;
19508
20192
  const route = attrs;
19509
20193
  if (route.service !== serviceName || !route.pathTemplate) return;
19510
20194
  if (normalizePathTemplate(route.pathTemplate) !== normalizedPath) return;
@@ -19519,23 +20203,23 @@ function createCloudRunResolveTarget(graph, config) {
19519
20203
  if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
19520
20204
  const identity = parseCloudRunTargetName(signal.targetName);
19521
20205
  if (!identity) return null;
19522
- const { serviceName: gcpServiceName, method, path: path92 } = identity;
20206
+ const { serviceName: gcpServiceName, method, path: path93 } = identity;
19523
20207
  const mappedService = config.serviceMap?.[gcpServiceName];
19524
20208
  if (mappedService) {
19525
20209
  const routeNodeId = findMatchingRouteNode2(
19526
20210
  graph,
19527
20211
  mappedService,
19528
20212
  method,
19529
- normalizePathTemplate(path92)
20213
+ normalizePathTemplate(path93)
19530
20214
  );
19531
20215
  if (routeNodeId) {
19532
- return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types78.EdgeType.CALLS };
20216
+ return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types79.EdgeType.CALLS };
19533
20217
  }
19534
20218
  }
19535
20219
  return {
19536
- targetNodeId: (0, import_types78.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
20220
+ targetNodeId: (0, import_types79.infraId)(CLOUD_RUN_SERVICE_INFRA_KIND, gcpServiceName),
19537
20221
  serviceName: mappedService ?? gcpServiceName,
19538
- edgeType: import_types78.EdgeType.CALLS,
20222
+ edgeType: import_types79.EdgeType.CALLS,
19539
20223
  ensureInfraNode: {
19540
20224
  kind: CLOUD_RUN_SERVICE_INFRA_KIND,
19541
20225
  name: gcpServiceName,
@@ -19576,7 +20260,7 @@ function createCloudRunConnector(graph, config = {}) {
19576
20260
 
19577
20261
  // src/connectors/render/index.ts
19578
20262
  init_cjs_shims();
19579
- var import_types81 = require("@neat.is/types");
20263
+ var import_types82 = require("@neat.is/types");
19580
20264
 
19581
20265
  // src/connectors/render/types.ts
19582
20266
  init_cjs_shims();
@@ -19654,7 +20338,7 @@ function buildRenderRouteIndex(graph, serviceName) {
19654
20338
  const out = [];
19655
20339
  graph.forEachNode((_id, attrs) => {
19656
20340
  const node = attrs;
19657
- if (node.type !== import_types81.NodeType.RouteNode) return;
20341
+ if (node.type !== import_types82.NodeType.RouteNode) return;
19658
20342
  const route = attrs;
19659
20343
  if (route.service !== serviceName) return;
19660
20344
  out.push({
@@ -19739,7 +20423,7 @@ function mapRenderRequestLogsToSignals(entries, routeIndex) {
19739
20423
  function createRenderResolveTarget(config) {
19740
20424
  return (signal) => {
19741
20425
  if (signal.targetKind === ROUTE_TARGET_KIND2) {
19742
- return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types81.EdgeType.CALLS };
20426
+ return { targetNodeId: signal.targetName, serviceName: config.serviceName, edgeType: import_types82.EdgeType.CALLS };
19743
20427
  }
19744
20428
  return null;
19745
20429
  };
@@ -19877,21 +20561,21 @@ function mapInsightsToSignals(rows, observedAtIso) {
19877
20561
 
19878
20562
  // src/connectors/planetscale/resolve.ts
19879
20563
  init_cjs_shims();
19880
- var import_types85 = require("@neat.is/types");
20564
+ var import_types86 = require("@neat.is/types");
19881
20565
  var PLANETSCALE_DATABASE_KIND = "planetscale-database";
19882
20566
  function createPlanetscaleResolveTarget(graph, config) {
19883
20567
  const databaseName = `${config.organization}/${config.database}`;
19884
20568
  return (signal, _ctx) => {
19885
20569
  if (signal.targetKind !== PLANETSCALE_SQL_TABLE_TARGET_KIND || !signal.targetName) return null;
19886
- const tableId = (0, import_types85.infraId)("sql-table", signal.targetName);
20570
+ const tableId = (0, import_types86.infraId)("sql-table", signal.targetName);
19887
20571
  if (graph.hasNode(tableId)) {
19888
- return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types85.EdgeType.CALLS };
20572
+ return { targetNodeId: tableId, serviceName: config.serviceName, edgeType: import_types86.EdgeType.CALLS };
19889
20573
  }
19890
- const providerId = (0, import_types85.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
20574
+ const providerId = (0, import_types86.infraId)(PLANETSCALE_DATABASE_KIND, databaseName);
19891
20575
  return {
19892
20576
  targetNodeId: providerId,
19893
20577
  serviceName: config.serviceName,
19894
- edgeType: import_types85.EdgeType.CALLS,
20578
+ edgeType: import_types86.EdgeType.CALLS,
19895
20579
  ensureInfraNode: { kind: PLANETSCALE_DATABASE_KIND, name: databaseName, provider: "planetscale" }
19896
20580
  };
19897
20581
  };
@@ -20156,7 +20840,7 @@ function mapBuildsToSignals(builds, serviceName) {
20156
20840
 
20157
20841
  // src/connectors/eas/resolve.ts
20158
20842
  init_cjs_shims();
20159
- var import_types90 = require("@neat.is/types");
20843
+ var import_types91 = require("@neat.is/types");
20160
20844
  var NO_ENV2 = "unknown";
20161
20845
  var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
20162
20846
  var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
@@ -20172,8 +20856,8 @@ function configBasenamesForPhase(phase) {
20172
20856
  function configNodeService(graph, configNodeId) {
20173
20857
  for (const edgeId of graph.inboundEdges(configNodeId)) {
20174
20858
  const edge = graph.getEdgeAttributes(edgeId);
20175
- if (edge.type !== import_types90.EdgeType.CONFIGURED_BY) continue;
20176
- const parsed = (0, import_types90.parseFileId)(edge.source);
20859
+ if (edge.type !== import_types91.EdgeType.CONFIGURED_BY) continue;
20860
+ const parsed = (0, import_types91.parseFileId)(edge.source);
20177
20861
  if (parsed) return parsed.service;
20178
20862
  }
20179
20863
  return null;
@@ -20184,7 +20868,7 @@ function findConfigNode(graph, basenames, serviceName) {
20184
20868
  graph.forEachNode((id, attrs) => {
20185
20869
  if (scoped) return;
20186
20870
  const node = attrs;
20187
- if (node.type !== import_types90.NodeType.ConfigNode) return;
20871
+ if (node.type !== import_types91.NodeType.ConfigNode) return;
20188
20872
  if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
20189
20873
  if (anyMatch === null) anyMatch = id;
20190
20874
  if (configNodeService(graph, id) === serviceName) scoped = id;
@@ -20201,13 +20885,13 @@ function createEasResolveTarget(graph) {
20201
20885
  if (basenames.length > 0) {
20202
20886
  const configNodeId = findConfigNode(graph, basenames, serviceName);
20203
20887
  if (configNodeId) {
20204
- return { targetNodeId: configNodeId, serviceName, edgeType: import_types90.EdgeType.CALLS };
20888
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types91.EdgeType.CALLS };
20205
20889
  }
20206
20890
  }
20207
20891
  return {
20208
20892
  targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
20209
20893
  serviceName,
20210
- edgeType: import_types90.EdgeType.CALLS
20894
+ edgeType: import_types91.EdgeType.CALLS
20211
20895
  };
20212
20896
  };
20213
20897
  }
@@ -21011,11 +21695,11 @@ function registerRoutes(scope, ctx) {
21011
21695
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
21012
21696
  const parsed = [];
21013
21697
  for (const c of candidates) {
21014
- const r = import_types93.DivergenceTypeSchema.safeParse(c);
21698
+ const r = import_types94.DivergenceTypeSchema.safeParse(c);
21015
21699
  if (!r.success) {
21016
21700
  return reply.code(400).send({
21017
21701
  error: `unknown divergence type "${c}"`,
21018
- allowed: import_types93.DivergenceTypeSchema.options
21702
+ allowed: import_types94.DivergenceTypeSchema.options
21019
21703
  });
21020
21704
  }
21021
21705
  parsed.push(r.data);
@@ -21257,6 +21941,18 @@ function registerRoutes(scope, ctx) {
21257
21941
  matches: matches.slice(0, safeLimit)
21258
21942
  };
21259
21943
  });
21944
+ scope.get("/graph/ask", async (req, reply) => {
21945
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21946
+ if (!proj) return;
21947
+ const question = (req.query.q ?? "").trim();
21948
+ if (!question) return reply.code(400).send({ error: "query parameter `q` is required" });
21949
+ const epath = errorsPathFor(proj);
21950
+ const incidents = epath ? await readErrorEvents(epath) : [];
21951
+ return askGraph(proj.graph, question, {
21952
+ ...proj.searchIndex ? { searchIndex: proj.searchIndex } : {},
21953
+ incidents
21954
+ });
21955
+ });
21260
21956
  scope.get(
21261
21957
  "/graph/diff",
21262
21958
  async (req, reply) => {
@@ -21357,7 +22053,7 @@ function registerRoutes(scope, ctx) {
21357
22053
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
21358
22054
  let violations = await log.readAll();
21359
22055
  if (req.query.severity) {
21360
- const sev = import_types93.PolicySeveritySchema.safeParse(req.query.severity);
22056
+ const sev = import_types94.PolicySeveritySchema.safeParse(req.query.severity);
21361
22057
  if (!sev.success) {
21362
22058
  return reply.code(400).send({
21363
22059
  error: "invalid severity",
@@ -21396,7 +22092,7 @@ function registerRoutes(scope, ctx) {
21396
22092
  scope.post("/policies/check", async (req, reply) => {
21397
22093
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
21398
22094
  if (!proj) return;
21399
- const parsed = import_types93.PoliciesCheckBodySchema.safeParse(req.body ?? {});
22095
+ const parsed = import_types94.PoliciesCheckBodySchema.safeParse(req.body ?? {});
21400
22096
  if (!parsed.success) {
21401
22097
  return reply.code(400).send({
21402
22098
  error: "invalid /policies/check body",
@@ -21729,7 +22425,7 @@ var import_node_fs41 = require("fs");
21729
22425
  var import_node_path76 = __toESM(require("path"), 1);
21730
22426
 
21731
22427
  // src/daemon.ts
21732
- var import_types94 = require("@neat.is/types");
22428
+ var import_types95 = require("@neat.is/types");
21733
22429
  function daemonJsonPath(scanPath) {
21734
22430
  return import_node_path77.default.join(scanPath, "neat-out", "daemon.json");
21735
22431
  }
@@ -23655,8 +24351,8 @@ function scriptHasShellChain(script) {
23655
24351
  function entryFromScript(script) {
23656
24352
  if (!script) return void 0;
23657
24353
  if (scriptHasShellChain(script)) return void 0;
23658
- const tokens = script.split(/\s+/).filter((t) => t.length > 0);
23659
- for (const token of tokens) {
24354
+ const tokens2 = script.split(/\s+/).filter((t) => t.length > 0);
24355
+ for (const token of tokens2) {
23660
24356
  const lower = token.toLowerCase();
23661
24357
  if (SCRIPT_LAUNCHERS.has(lower)) continue;
23662
24358
  const cleaned = token.startsWith("./") ? token.slice(2) : token;
@@ -25681,7 +26377,7 @@ async function extractAndPersist(opts) {
25681
26377
  }
25682
26378
  async function applyInstallersOver(services, project, options = {}) {
25683
26379
  const resolveManager = options.resolveManager ?? detectPackageManager;
25684
- const runInstall = options.runInstall ?? runPackageManagerInstall;
26380
+ const runInstall2 = options.runInstall ?? runPackageManagerInstall;
25685
26381
  let instrumented = 0;
25686
26382
  let already = 0;
25687
26383
  let libOnly = 0;
@@ -25804,7 +26500,7 @@ async function applyInstallersOver(services, project, options = {}) {
25804
26500
  const packageManagerInstalls = [];
25805
26501
  for (const cmd of installPlans.values()) {
25806
26502
  console.log(`running \`${cmd.pm} ${cmd.args.join(" ")}\` in ${cmd.cwd}`);
25807
- const result = await runInstall(cmd);
26503
+ const result = await runInstall2(cmd);
25808
26504
  packageManagerInstalls.push(result);
25809
26505
  if (result.exitCode !== 0) {
25810
26506
  console.error(
@@ -26783,6 +27479,9 @@ function claudeSettingsPath() {
26783
27479
  function installedHookPath() {
26784
27480
  return import_node_path87.default.join(neatHome3(), "hooks", HOOK_FILENAME);
26785
27481
  }
27482
+ function gateFlagPath() {
27483
+ return import_node_path87.default.join(neatHome3(), "hooks", "gate-enabled");
27484
+ }
26786
27485
  function isNeatSearchEntry(entry2) {
26787
27486
  return (entry2.hooks ?? []).some(
26788
27487
  (h) => typeof h.command === "string" && h.command.includes(HOOK_FILENAME)
@@ -26845,13 +27544,30 @@ async function runHooks(opts) {
26845
27544
  };
26846
27545
  await import_node_fs52.promises.mkdir(import_node_path87.default.dirname(settingsFile), { recursive: true });
26847
27546
  await import_node_fs52.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
26848
- console.log(`neat hooks: installed the search-nudge hook`);
27547
+ const flag = gateFlagPath();
27548
+ if (opts.gate) {
27549
+ await import_node_fs52.promises.mkdir(import_node_path87.default.dirname(flag), { recursive: true });
27550
+ await import_node_fs52.promises.writeFile(flag, "1\n", "utf8");
27551
+ } else {
27552
+ await import_node_fs52.promises.rm(flag, { force: true });
27553
+ }
27554
+ const mode = opts.gate ? "GATE (deny search until you ask the graph)" : "nudge (search still runs)";
27555
+ console.log(`neat hooks: installed the search hook in ${opts.gate ? "gate" : "nudge"} mode`);
26849
27556
  console.log(` script: ${scriptPath}`);
26850
27557
  console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
26851
27558
  console.log(` guidance: ${guidePath}`);
27559
+ console.log(` mode: ${mode}`);
26852
27560
  console.log("");
26853
- console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
26854
- console.log("your agent will now be nudged to query NEAT first.");
27561
+ if (opts.gate) {
27562
+ console.log("restart Claude Code to load the hook. A Grep/Glob or Bash grep is now DENIED");
27563
+ console.log('until you run `neat ask "<question>"` (or the ask MCP tool) once this session;');
27564
+ console.log("after that, search is allowed as a fallback. Set NEAT_SEARCH_GATE=0 to fall");
27565
+ console.log("back to nudge-only without re-running.");
27566
+ } else {
27567
+ console.log("restart Claude Code to load the hook. On a Grep/Glob or a Bash grep,");
27568
+ console.log("your agent will now be nudged to query NEAT first (the search still runs).");
27569
+ console.log("Re-run with --gate to hard-force the graph-first orientation.");
27570
+ }
26855
27571
  console.log("");
26856
27572
  console.log("The hook is Claude-Code-specific. For agents on other harnesses, paste");
26857
27573
  console.log(`the guidance above into your project instructions (CLAUDE.md / AGENTS.md).`);
@@ -26863,28 +27579,36 @@ async function runHooks(opts) {
26863
27579
  function usage() {
26864
27580
  console.log("neat hooks \u2014 wire NEAT into your agent so it queries the graph before grepping");
26865
27581
  console.log("");
26866
- console.log(" --apply install the Claude Code search-nudge hook and write the");
27582
+ console.log(" --apply install the Claude Code search hook and write the");
26867
27583
  console.log(" graph-first guidance to ~/.neat/, merging into");
26868
27584
  console.log(" ~/.claude/settings.json without touching your other hooks");
27585
+ console.log(" --gate with --apply, enable hard-gate mode: DENY Grep/Glob/grep-Bash");
27586
+ console.log(" until `neat ask` has run this session (default is nudge-only).");
27587
+ console.log(" Toggle off at run time with NEAT_SEARCH_GATE=0.");
26869
27588
  console.log(" --print-hook print the hook script to stdout");
26870
27589
  console.log(" --print-guide print the agent-agnostic graph-first guidance to stdout");
26871
27590
  console.log(" --print-settings print the settings.json PreToolUse block --apply would add");
26872
27591
  console.log("");
26873
- console.log("The hook is a gentle, non-blocking nudge \u2014 searches still run. It is");
26874
- console.log("Claude-Code-specific; other harnesses get the same steer from the guidance.");
27592
+ console.log("By default the hook is a gentle, non-blocking nudge \u2014 searches still run.");
27593
+ console.log("--gate turns it into a hard forcing mechanism. It is Claude-Code-specific;");
27594
+ console.log("other harnesses get the same steer from the graph-first guidance.");
26875
27595
  }
26876
27596
  async function runHooksCommand(args) {
26877
27597
  const opts = {
26878
27598
  apply: false,
26879
27599
  printHook: false,
26880
27600
  printGuide: false,
26881
- printSettings: false
27601
+ printSettings: false,
27602
+ gate: false
26882
27603
  };
26883
27604
  for (const arg of args) {
26884
27605
  switch (arg) {
26885
27606
  case "--apply":
26886
27607
  opts.apply = true;
26887
27608
  break;
27609
+ case "--gate":
27610
+ opts.gate = true;
27611
+ break;
26888
27612
  case "--print-hook":
26889
27613
  opts.printHook = true;
26890
27614
  break;
@@ -26913,11 +27637,151 @@ async function runHooksCommand(args) {
26913
27637
  }
26914
27638
  }
26915
27639
 
26916
- // src/codex-cli.ts
27640
+ // src/claude-cli.ts
26917
27641
  init_cjs_shims();
26918
27642
  var import_node_path88 = __toESM(require("path"), 1);
26919
- var import_node_os6 = __toESM(require("os"), 1);
26920
27643
  var import_node_fs53 = require("fs");
27644
+ var NEAT_SECTION_HEADING = "## neat";
27645
+ var NEAT_DIRECTIVE_BODY = `This project has NEAT wired in: a live, fused semantic graph of the system \u2014
27646
+ code and runtime behaviour (OpenTelemetry) in one model, every fact tagged with
27647
+ its provenance (EXTRACTED / OBSERVED / INFERRED / STALE) and a confidence.
27648
+
27649
+ For ANY question about this system's behaviour, dependencies, failures, root
27650
+ cause, or blast radius, call \`neat ask\` FIRST \u2014 before Read, Grep, Glob, or Bash:
27651
+
27652
+ neat ask "why is checkout failing?"
27653
+ neat ask "what breaks if I change the orders table?"
27654
+ neat ask "what does the payments service depend on at runtime?"
27655
+
27656
+ \`ask\` resolves your question to graph nodes and routes it to the right traversal
27657
+ \u2014 root cause, dependencies, observed runtime calls, incidents, divergences,
27658
+ blast radius \u2014 and returns one compact, provenance-tagged answer. The same door
27659
+ is the \`ask\` MCP tool (\`mcp__neat__ask\`) when NEAT's MCP server is wired in.
27660
+
27661
+ The graph is live and fused: it is faster and more accurate than scanning files,
27662
+ and it can tell you what the system actually does at runtime, not only what the
27663
+ source declares. Fall back to Read/Grep only when the graph does not have what
27664
+ you need \u2014 comments, string literals, config minutiae. Ask the graph first,
27665
+ then scan.
27666
+
27667
+ If \`neat ask\` errors, the daemon may not be running (\`neat list\`) \u2014 start it
27668
+ with \`neat <path>\`, then re-ask.`;
27669
+ function neatSection() {
27670
+ return `${NEAT_SECTION_HEADING}
27671
+
27672
+ ${NEAT_DIRECTIVE_BODY}
27673
+ `;
27674
+ }
27675
+ function claudeMdPath() {
27676
+ const override = process.env.NEAT_CLAUDE_MD;
27677
+ if (override && override.length > 0) return import_node_path88.default.resolve(override);
27678
+ return import_node_path88.default.join(process.cwd(), "CLAUDE.md");
27679
+ }
27680
+ function splitAroundSection(raw) {
27681
+ const lines = raw.split("\n");
27682
+ const startIdx = lines.findIndex((l) => l.replace(/\s+$/, "") === NEAT_SECTION_HEADING);
27683
+ if (startIdx === -1) {
27684
+ return { before: raw.replace(/\n*$/, ""), after: "", found: false };
27685
+ }
27686
+ let endIdx = lines.length;
27687
+ for (let i = startIdx + 1; i < lines.length; i++) {
27688
+ if (/^#{1,2}\s+/.test(lines[i] ?? "")) {
27689
+ endIdx = i;
27690
+ break;
27691
+ }
27692
+ }
27693
+ const before = lines.slice(0, startIdx).join("\n").replace(/\n*$/, "");
27694
+ const after = lines.slice(endIdx).join("\n").replace(/^\n*/, "");
27695
+ return { before, after, found: true };
27696
+ }
27697
+ function compose(before, after) {
27698
+ const parts = [];
27699
+ if (before.length > 0) parts.push(before);
27700
+ parts.push(neatSection().replace(/\n+$/, ""));
27701
+ if (after.length > 0) parts.push(after);
27702
+ return parts.join("\n\n").replace(/\n*$/, "") + "\n";
27703
+ }
27704
+ async function readIfExists2(file) {
27705
+ try {
27706
+ return await import_node_fs53.promises.readFile(file, "utf8");
27707
+ } catch (err) {
27708
+ if (err.code === "ENOENT") return null;
27709
+ throw err;
27710
+ }
27711
+ }
27712
+ async function runInstall() {
27713
+ const file = claudeMdPath();
27714
+ const raw = await readIfExists2(file) ?? "";
27715
+ const { before, after, found } = splitAroundSection(raw);
27716
+ const next = compose(before, after);
27717
+ await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(file), { recursive: true });
27718
+ await import_node_fs53.promises.writeFile(file, next, "utf8");
27719
+ const verb = raw.length === 0 ? "created" : found ? "refreshed" : "added";
27720
+ console.log(`neat claude: ${verb} the \`${NEAT_SECTION_HEADING}\` section in ${file}`);
27721
+ console.log("Your agent will now reach for `neat ask` before Read/Grep/Bash. Restart the");
27722
+ console.log("session (or reload CLAUDE.md) to pick it up.");
27723
+ return { exitCode: 0 };
27724
+ }
27725
+ async function runUninstall() {
27726
+ const file = claudeMdPath();
27727
+ const raw = await readIfExists2(file);
27728
+ if (raw === null) {
27729
+ console.log(`neat claude: no CLAUDE.md at ${file} \u2014 nothing to remove.`);
27730
+ return { exitCode: 0 };
27731
+ }
27732
+ const { before, after, found } = splitAroundSection(raw);
27733
+ if (!found) {
27734
+ console.log(`neat claude: no \`${NEAT_SECTION_HEADING}\` section in ${file} \u2014 nothing to remove.`);
27735
+ return { exitCode: 0 };
27736
+ }
27737
+ const remaining = [before, after].filter((s) => s.length > 0).join("\n\n");
27738
+ const next = remaining.length > 0 ? remaining.replace(/\n*$/, "") + "\n" : "";
27739
+ await import_node_fs53.promises.writeFile(file, next, "utf8");
27740
+ console.log(`neat claude: removed the \`${NEAT_SECTION_HEADING}\` section from ${file}.`);
27741
+ return { exitCode: 0 };
27742
+ }
27743
+ function usage2() {
27744
+ console.log("neat claude \u2014 make the query-first directive always-on in Claude Code");
27745
+ console.log("");
27746
+ console.log(" install write (or refresh) a `## neat` section in ./CLAUDE.md so your");
27747
+ console.log(" agent reaches for `neat ask` before Read/Grep/Bash");
27748
+ console.log(" uninstall remove the `## neat` section from ./CLAUDE.md");
27749
+ console.log(" print print the directive block to stdout (for a manual paste)");
27750
+ console.log("");
27751
+ console.log("Idempotent: re-running install replaces its own section, never duplicates it.");
27752
+ console.log("Target file overridable via NEAT_CLAUDE_MD.");
27753
+ }
27754
+ async function runClaudeCommand(args) {
27755
+ const sub = args[0];
27756
+ if (sub === "-h" || sub === "--help" || sub === void 0) {
27757
+ usage2();
27758
+ return sub === void 0 ? 2 : 0;
27759
+ }
27760
+ try {
27761
+ switch (sub) {
27762
+ case "install":
27763
+ return (await runInstall()).exitCode;
27764
+ case "uninstall":
27765
+ return (await runUninstall()).exitCode;
27766
+ case "print":
27767
+ process.stdout.write(neatSection());
27768
+ return 0;
27769
+ default:
27770
+ console.error(`neat claude: unknown subcommand "${sub}"`);
27771
+ usage2();
27772
+ return 2;
27773
+ }
27774
+ } catch (err) {
27775
+ console.error(`neat claude: ${err.message}`);
27776
+ return 1;
27777
+ }
27778
+ }
27779
+
27780
+ // src/codex-cli.ts
27781
+ init_cjs_shims();
27782
+ var import_node_path89 = __toESM(require("path"), 1);
27783
+ var import_node_os6 = __toESM(require("os"), 1);
27784
+ var import_node_fs54 = require("fs");
26921
27785
  var import_node_util = require("util");
26922
27786
  var import_smol_toml6 = require("smol-toml");
26923
27787
  var CODEX_MCP_SERVER = {
@@ -26935,14 +27799,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
26935
27799
  var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
26936
27800
  function codexConfigPath() {
26937
27801
  const override = process.env.NEAT_CODEX_CONFIG;
26938
- if (override && override.length > 0) return import_node_path88.default.resolve(override);
27802
+ if (override && override.length > 0) return import_node_path89.default.resolve(override);
26939
27803
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
26940
- return import_node_path88.default.join(home, ".codex", "config.toml");
27804
+ return import_node_path89.default.join(home, ".codex", "config.toml");
26941
27805
  }
26942
27806
  function agentsFilePath() {
26943
27807
  const override = process.env.NEAT_CODEX_AGENTS;
26944
- if (override && override.length > 0) return import_node_path88.default.resolve(override);
26945
- return import_node_path88.default.join(process.cwd(), "AGENTS.md");
27808
+ if (override && override.length > 0) return import_node_path89.default.resolve(override);
27809
+ return import_node_path89.default.join(process.cwd(), "AGENTS.md");
26946
27810
  }
26947
27811
  function isTableHeader(line) {
26948
27812
  return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
@@ -27076,7 +27940,7 @@ async function runCodex(opts) {
27076
27940
  const agentsPath = agentsFilePath();
27077
27941
  let configRaw = "";
27078
27942
  try {
27079
- configRaw = await import_node_fs53.promises.readFile(configPath, "utf8");
27943
+ configRaw = await import_node_fs54.promises.readFile(configPath, "utf8");
27080
27944
  } catch (err) {
27081
27945
  if (err.code !== "ENOENT") {
27082
27946
  console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
@@ -27085,7 +27949,7 @@ async function runCodex(opts) {
27085
27949
  }
27086
27950
  let agentsRaw = "";
27087
27951
  try {
27088
- agentsRaw = await import_node_fs53.promises.readFile(agentsPath, "utf8");
27952
+ agentsRaw = await import_node_fs54.promises.readFile(agentsPath, "utf8");
27089
27953
  } catch (err) {
27090
27954
  if (err.code !== "ENOENT") {
27091
27955
  console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
@@ -27125,15 +27989,15 @@ async function runCodex(opts) {
27125
27989
  return { exitCode: 0 };
27126
27990
  }
27127
27991
  if (config.changed) {
27128
- await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(configPath), { recursive: true });
27129
- await import_node_fs53.promises.writeFile(configPath, config.text, "utf8");
27992
+ await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(configPath), { recursive: true });
27993
+ await import_node_fs54.promises.writeFile(configPath, config.text, "utf8");
27130
27994
  console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
27131
27995
  } else {
27132
27996
  console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
27133
27997
  }
27134
27998
  if (agents.changed) {
27135
- await import_node_fs53.promises.mkdir(import_node_path88.default.dirname(agentsPath), { recursive: true });
27136
- await import_node_fs53.promises.writeFile(agentsPath, agents.text, "utf8");
27999
+ await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(agentsPath), { recursive: true });
28000
+ await import_node_fs54.promises.writeFile(agentsPath, agents.text, "utf8");
27137
28001
  console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
27138
28002
  } else {
27139
28003
  console.log(`neat codex: ${agentsPath} already has the graph-first block`);
@@ -27143,7 +28007,7 @@ async function runCodex(opts) {
27143
28007
  console.log("points the server at the local daemon \u2014 edit it for a non-default one.");
27144
28008
  return { exitCode: 0 };
27145
28009
  }
27146
- function usage2() {
28010
+ function usage3() {
27147
28011
  console.log("neat codex \u2014 install NEAT into the OpenAI Codex CLI (MCP server + AGENTS.md)");
27148
28012
  console.log("");
27149
28013
  console.log(" (no flag) plan: print what would change, write nothing");
@@ -27171,11 +28035,11 @@ async function runCodexCommand(args) {
27171
28035
  break;
27172
28036
  case "-h":
27173
28037
  case "--help":
27174
- usage2();
28038
+ usage3();
27175
28039
  return 0;
27176
28040
  default:
27177
28041
  console.error(`neat codex: unknown flag "${arg}"`);
27178
- usage2();
28042
+ usage3();
27179
28043
  return 2;
27180
28044
  }
27181
28045
  }
@@ -27190,9 +28054,9 @@ async function runCodexCommand(args) {
27190
28054
 
27191
28055
  // src/editors-cli.ts
27192
28056
  init_cjs_shims();
27193
- var import_node_path89 = __toESM(require("path"), 1);
28057
+ var import_node_path90 = __toESM(require("path"), 1);
27194
28058
  var import_node_os7 = __toESM(require("os"), 1);
27195
- var import_node_fs54 = require("fs");
28059
+ var import_node_fs55 = require("fs");
27196
28060
  var import_node_util2 = require("util");
27197
28061
  var jsonc = __toESM(require("jsonc-parser"), 1);
27198
28062
  var NEAT_MCP_SERVER = {
@@ -27216,17 +28080,17 @@ function homeDir() {
27216
28080
  }
27217
28081
  function xdgConfigDir() {
27218
28082
  const xdg = process.env.XDG_CONFIG_HOME;
27219
- return xdg && xdg.length > 0 ? import_node_path89.default.resolve(xdg) : import_node_path89.default.join(homeDir(), ".config");
28083
+ return xdg && xdg.length > 0 ? import_node_path90.default.resolve(xdg) : import_node_path90.default.join(homeDir(), ".config");
27220
28084
  }
27221
28085
  function envOverride(name) {
27222
28086
  const v = process.env[name];
27223
- return v && v.length > 0 ? import_node_path89.default.resolve(v) : void 0;
28087
+ return v && v.length > 0 ? import_node_path90.default.resolve(v) : void 0;
27224
28088
  }
27225
28089
  var CURSOR_CLIENT = {
27226
28090
  id: "cursor",
27227
28091
  label: "Cursor",
27228
28092
  docsUrl: "https://docs.cursor.com/context/mcp",
27229
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path89.default.join(homeDir(), ".cursor", "mcp.json"),
28093
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path90.default.join(homeDir(), ".cursor", "mcp.json"),
27230
28094
  mcpContainerKey: "mcpServers",
27231
28095
  format: "json",
27232
28096
  // Cursor still reads a single `.cursorrules` at the project root (the modern
@@ -27238,7 +28102,7 @@ var DEVIN_CLIENT = {
27238
28102
  id: "devin",
27239
28103
  label: "Devin Desktop (Cascade)",
27240
28104
  docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
27241
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path89.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
28105
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path90.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
27242
28106
  mcpContainerKey: "mcpServers",
27243
28107
  format: "json",
27244
28108
  rulesFileName: ".windsurfrules"
@@ -27247,7 +28111,7 @@ var GEMINI_CLIENT = {
27247
28111
  id: "gemini",
27248
28112
  label: "Gemini CLI",
27249
28113
  docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
27250
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path89.default.join(homeDir(), ".gemini", "settings.json"),
28114
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path90.default.join(homeDir(), ".gemini", "settings.json"),
27251
28115
  mcpContainerKey: "mcpServers",
27252
28116
  format: "json",
27253
28117
  rulesFileName: "GEMINI.md"
@@ -27256,7 +28120,7 @@ var QWEN_CLIENT = {
27256
28120
  id: "qwen",
27257
28121
  label: "Qwen Code",
27258
28122
  docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
27259
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path89.default.join(homeDir(), ".qwen", "settings.json"),
28123
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path90.default.join(homeDir(), ".qwen", "settings.json"),
27260
28124
  mcpContainerKey: "mcpServers",
27261
28125
  format: "json",
27262
28126
  rulesFileName: "QWEN.md"
@@ -27265,7 +28129,7 @@ var AMAZONQ_CLIENT = {
27265
28129
  id: "amazonq",
27266
28130
  label: "Amazon Q Developer CLI",
27267
28131
  docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
27268
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path89.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
28132
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path90.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
27269
28133
  mcpContainerKey: "mcpServers",
27270
28134
  format: "json"
27271
28135
  };
@@ -27273,7 +28137,7 @@ var ROOCODE_CLIENT = {
27273
28137
  id: "roocode",
27274
28138
  label: "Roo Code",
27275
28139
  docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
27276
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path89.default.join(process.cwd(), ".roo", "mcp.json"),
28140
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path90.default.join(process.cwd(), ".roo", "mcp.json"),
27277
28141
  mcpContainerKey: "mcpServers",
27278
28142
  format: "json"
27279
28143
  };
@@ -27286,9 +28150,9 @@ var ZED_CLIENT = {
27286
28150
  if (override) return override;
27287
28151
  if (process.platform === "win32") {
27288
28152
  const appData = process.env.APPDATA;
27289
- if (appData && appData.length > 0) return import_node_path89.default.join(appData, "Zed", "settings.json");
28153
+ if (appData && appData.length > 0) return import_node_path90.default.join(appData, "Zed", "settings.json");
27290
28154
  }
27291
- return import_node_path89.default.join(homeDir(), ".config", "zed", "settings.json");
28155
+ return import_node_path90.default.join(homeDir(), ".config", "zed", "settings.json");
27292
28156
  },
27293
28157
  mcpContainerKey: "context_servers",
27294
28158
  format: "jsonc",
@@ -27298,7 +28162,7 @@ var OPENCODE_CLIENT = {
27298
28162
  id: "opencode",
27299
28163
  label: "OpenCode",
27300
28164
  docsUrl: "https://opencode.ai/docs/mcp-servers/",
27301
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path89.default.join(xdgConfigDir(), "opencode", "opencode.json"),
28165
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path90.default.join(xdgConfigDir(), "opencode", "opencode.json"),
27302
28166
  mcpContainerKey: "mcp",
27303
28167
  format: "json",
27304
28168
  serverEntry: NEAT_OPENCODE_SERVER,
@@ -27308,7 +28172,7 @@ var CRUSH_CLIENT = {
27308
28172
  id: "crush",
27309
28173
  label: "Crush",
27310
28174
  docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
27311
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path89.default.join(xdgConfigDir(), "crush", "crush.json"),
28175
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path90.default.join(xdgConfigDir(), "crush", "crush.json"),
27312
28176
  mcpContainerKey: "mcp",
27313
28177
  format: "json",
27314
28178
  serverEntry: NEAT_CRUSH_SERVER,
@@ -27371,7 +28235,7 @@ async function planMcp(client, mcpPath) {
27371
28235
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
27372
28236
  let raw = "";
27373
28237
  try {
27374
- raw = await import_node_fs54.promises.readFile(mcpPath, "utf8");
28238
+ raw = await import_node_fs55.promises.readFile(mcpPath, "utf8");
27375
28239
  } catch (err) {
27376
28240
  const e = err;
27377
28241
  if (e.code === "ENOENT") {
@@ -27413,7 +28277,7 @@ async function runEditorInstall(client, opts) {
27413
28277
  const mcpPath = client.mcpConfigPath();
27414
28278
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
27415
28279
  const hasRules = typeof client.rulesFileName === "string";
27416
- const rulesPath = hasRules ? import_node_path89.default.join(opts.projectDir, client.rulesFileName) : "";
28280
+ const rulesPath = hasRules ? import_node_path90.default.join(opts.projectDir, client.rulesFileName) : "";
27417
28281
  const mcp = await planMcp(client, mcpPath);
27418
28282
  if (mcp === null) return { exitCode: 1 };
27419
28283
  let existingRules = "";
@@ -27422,7 +28286,7 @@ async function runEditorInstall(client, opts) {
27422
28286
  let block = "";
27423
28287
  if (hasRules) {
27424
28288
  try {
27425
- existingRules = await import_node_fs54.promises.readFile(rulesPath, "utf8");
28289
+ existingRules = await import_node_fs55.promises.readFile(rulesPath, "utf8");
27426
28290
  } catch (err) {
27427
28291
  if (err.code !== "ENOENT") {
27428
28292
  console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
@@ -27456,11 +28320,11 @@ async function runEditorInstall(client, opts) {
27456
28320
  );
27457
28321
  return { exitCode: 0 };
27458
28322
  }
27459
- await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(mcpPath), { recursive: true });
27460
- await import_node_fs54.promises.writeFile(mcpPath, mcp.text, "utf8");
28323
+ await import_node_fs55.promises.mkdir(import_node_path90.default.dirname(mcpPath), { recursive: true });
28324
+ await import_node_fs55.promises.writeFile(mcpPath, mcp.text, "utf8");
27461
28325
  if (hasRules) {
27462
- await import_node_fs54.promises.mkdir(import_node_path89.default.dirname(rulesPath), { recursive: true });
27463
- await import_node_fs54.promises.writeFile(rulesPath, newRules, "utf8");
28326
+ await import_node_fs55.promises.mkdir(import_node_path90.default.dirname(rulesPath), { recursive: true });
28327
+ await import_node_fs55.promises.writeFile(rulesPath, newRules, "utf8");
27464
28328
  }
27465
28329
  console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
27466
28330
  console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
@@ -27473,7 +28337,7 @@ async function runEditorInstall(client, opts) {
27473
28337
  function indent(text) {
27474
28338
  return text.split("\n").map((line) => line.length > 0 ? ` ${line}` : line).join("\n");
27475
28339
  }
27476
- function usage3(client) {
28340
+ function usage4(client) {
27477
28341
  const hasRules = typeof client.rulesFileName === "string";
27478
28342
  console.log(
27479
28343
  hasRules ? `neat ${client.id} \u2014 install NEAT's MCP server + graph-first guidance into ${client.label}` : `neat ${client.id} \u2014 install NEAT's MCP server into ${client.label}`
@@ -27504,11 +28368,11 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
27504
28368
  break;
27505
28369
  case "-h":
27506
28370
  case "--help":
27507
- usage3(client);
28371
+ usage4(client);
27508
28372
  return 0;
27509
28373
  default:
27510
28374
  console.error(`neat ${client.id}: unknown flag "${arg}"`);
27511
- usage3(client);
28375
+ usage4(client);
27512
28376
  return 2;
27513
28377
  }
27514
28378
  }
@@ -27523,11 +28387,11 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
27523
28387
 
27524
28388
  // src/monitor.ts
27525
28389
  init_cjs_shims();
27526
- var import_types96 = require("@neat.is/types");
28390
+ var import_types97 = require("@neat.is/types");
27527
28391
 
27528
28392
  // src/cli-client.ts
27529
28393
  init_cjs_shims();
27530
- var import_types95 = require("@neat.is/types");
28394
+ var import_types96 = require("@neat.is/types");
27531
28395
  var HttpError = class extends Error {
27532
28396
  constructor(status2, message, responseBody = "") {
27533
28397
  super(message);
@@ -27552,10 +28416,10 @@ function createHttpClient(baseUrl, bearerToken) {
27552
28416
  const root = baseUrl.replace(/\/$/, "");
27553
28417
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
27554
28418
  return {
27555
- async get(path92) {
28419
+ async get(path93) {
27556
28420
  let res;
27557
28421
  try {
27558
- res = await fetch(`${root}${path92}`, {
28422
+ res = await fetch(`${root}${path93}`, {
27559
28423
  headers: { ...authHeader }
27560
28424
  });
27561
28425
  } catch (err) {
@@ -27567,16 +28431,16 @@ function createHttpClient(baseUrl, bearerToken) {
27567
28431
  const body = await res.text().catch(() => "");
27568
28432
  throw new HttpError(
27569
28433
  res.status,
27570
- `${res.status} ${res.statusText} on GET ${path92}: ${body}`,
28434
+ `${res.status} ${res.statusText} on GET ${path93}: ${body}`,
27571
28435
  body
27572
28436
  );
27573
28437
  }
27574
28438
  return await res.json();
27575
28439
  },
27576
- async post(path92, body) {
28440
+ async post(path93, body) {
27577
28441
  let res;
27578
28442
  try {
27579
- res = await fetch(`${root}${path92}`, {
28443
+ res = await fetch(`${root}${path93}`, {
27580
28444
  method: "POST",
27581
28445
  headers: { "content-type": "application/json", ...authHeader },
27582
28446
  body: JSON.stringify(body)
@@ -27590,7 +28454,7 @@ function createHttpClient(baseUrl, bearerToken) {
27590
28454
  const text = await res.text().catch(() => "");
27591
28455
  throw new HttpError(
27592
28456
  res.status,
27593
- `${res.status} ${res.statusText} on POST ${path92}: ${text}`,
28457
+ `${res.status} ${res.statusText} on POST ${path93}: ${text}`,
27594
28458
  text
27595
28459
  );
27596
28460
  }
@@ -27604,12 +28468,12 @@ function projectPath(project, suffix) {
27604
28468
  }
27605
28469
  async function runRootCause(client, input) {
27606
28470
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
27607
- const path92 = projectPath(
28471
+ const path93 = projectPath(
27608
28472
  input.project,
27609
28473
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
27610
28474
  );
27611
28475
  try {
27612
- const result = await client.get(path92);
28476
+ const result = await client.get(path93);
27613
28477
  const arrowPath = result.traversalPath.join(" \u2190 ");
27614
28478
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
27615
28479
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -27635,12 +28499,12 @@ async function runRootCause(client, input) {
27635
28499
  }
27636
28500
  async function runBlastRadius(client, input) {
27637
28501
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
27638
- const path92 = projectPath(
28502
+ const path93 = projectPath(
27639
28503
  input.project,
27640
28504
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
27641
28505
  );
27642
28506
  try {
27643
- const result = await client.get(path92);
28507
+ const result = await client.get(path93);
27644
28508
  if (result.totalAffected === 0) {
27645
28509
  return {
27646
28510
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -27669,17 +28533,17 @@ async function runBlastRadius(client, input) {
27669
28533
  }
27670
28534
  }
27671
28535
  function formatBlastEntry(n) {
27672
- const tag = n.edgeProvenance === import_types95.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
28536
+ const tag = n.edgeProvenance === import_types96.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
27673
28537
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
27674
28538
  }
27675
28539
  async function runDependencies(client, input) {
27676
28540
  const depth = input.depth ?? 3;
27677
- const path92 = projectPath(
28541
+ const path93 = projectPath(
27678
28542
  input.project,
27679
28543
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
27680
28544
  );
27681
28545
  try {
27682
- const result = await client.get(path92);
28546
+ const result = await client.get(path93);
27683
28547
  if (result.total === 0) {
27684
28548
  return {
27685
28549
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -27726,7 +28590,7 @@ async function runObservedDependencies(client, input) {
27726
28590
  if (result.observed) {
27727
28591
  return {
27728
28592
  summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
27729
- provenance: import_types95.Provenance.OBSERVED
28593
+ provenance: import_types96.Provenance.OBSERVED
27730
28594
  };
27731
28595
  }
27732
28596
  const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
@@ -27736,7 +28600,7 @@ async function runObservedDependencies(client, input) {
27736
28600
  return {
27737
28601
  summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
27738
28602
  block: blockLines.join("\n"),
27739
- provenance: import_types95.Provenance.OBSERVED
28603
+ provenance: import_types96.Provenance.OBSERVED
27740
28604
  };
27741
28605
  } catch (err) {
27742
28606
  if (err instanceof HttpError && err.status === 404) {
@@ -27771,9 +28635,9 @@ function formatDuration(ms) {
27771
28635
  return `${Math.round(h / 24)}d`;
27772
28636
  }
27773
28637
  async function runIncidents(client, input) {
27774
- const path92 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
28638
+ const path93 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
27775
28639
  try {
27776
- const body = await client.get(path92);
28640
+ const body = await client.get(path93);
27777
28641
  const events = body.events;
27778
28642
  if (events.length === 0) {
27779
28643
  return {
@@ -27790,7 +28654,7 @@ async function runIncidents(client, input) {
27790
28654
  return {
27791
28655
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
27792
28656
  block: blockLines.join("\n"),
27793
- provenance: import_types95.Provenance.OBSERVED
28657
+ provenance: import_types96.Provenance.OBSERVED
27794
28658
  };
27795
28659
  } catch (err) {
27796
28660
  if (err instanceof HttpError && err.status === 404) {
@@ -27899,7 +28763,7 @@ async function runStaleEdges(client, input) {
27899
28763
  return {
27900
28764
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
27901
28765
  block: blockLines.join("\n"),
27902
- provenance: import_types95.Provenance.STALE
28766
+ provenance: import_types96.Provenance.STALE
27903
28767
  };
27904
28768
  }
27905
28769
  async function runPolicies(client, input) {
@@ -28014,6 +28878,33 @@ async function runDivergences(client, input) {
28014
28878
  provenance: "composite (EXTRACTED + OBSERVED)"
28015
28879
  };
28016
28880
  }
28881
+ async function runAsk(client, input) {
28882
+ const result = await client.get(
28883
+ projectPath(input.project, `/graph/ask?q=${encodeURIComponent(input.question)}`)
28884
+ );
28885
+ const blockLines = [];
28886
+ if (result.matched.length > 0) {
28887
+ blockLines.push(
28888
+ `Matched: ${result.matched.map((m) => `${m.nodeId} [${m.via} ${m.score.toFixed(2)}]`).join(", ")}`
28889
+ );
28890
+ blockLines.push(`Intent: ${result.intent}`);
28891
+ } else if (result.scope === "global") {
28892
+ blockLines.push(`Graph-wide answer (${result.intent}) \u2014 no entity named.`);
28893
+ }
28894
+ for (const section of result.sections) {
28895
+ blockLines.push("", section.heading + ":");
28896
+ for (const fact of section.facts) {
28897
+ const tag = fact.provenance ? ` [${fact.provenance}${fact.confidence !== void 0 ? ` ${fact.confidence.toFixed(2)}` : ""}]` : fact.confidence !== void 0 ? ` [confidence ${fact.confidence.toFixed(2)}]` : "";
28898
+ blockLines.push(` \u2022 ${fact.text}${tag}`);
28899
+ }
28900
+ }
28901
+ return {
28902
+ summary: result.answer,
28903
+ block: blockLines.join("\n").trim(),
28904
+ ...result.confidence !== void 0 ? { confidence: result.confidence } : {},
28905
+ ...result.provenance.length > 0 ? { provenance: result.provenance } : {}
28906
+ };
28907
+ }
28017
28908
  function formatFooter(confidence, provenance) {
28018
28909
  const c = confidence === void 0 ? "n/a" : confidence.toFixed(2);
28019
28910
  const p = provenance === void 0 ? "n/a" : Array.isArray(provenance) ? [...new Set(provenance)].join(", ") : provenance;
@@ -28058,10 +28949,10 @@ async function pushSnapshotToRemote(input) {
28058
28949
 
28059
28950
  // src/monitor.ts
28060
28951
  var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
28061
- import_types96.EdgeType.CALLS,
28062
- import_types96.EdgeType.CONNECTS_TO,
28063
- import_types96.EdgeType.PUBLISHES_TO,
28064
- import_types96.EdgeType.CONSUMES_FROM
28952
+ import_types97.EdgeType.CALLS,
28953
+ import_types97.EdgeType.CONNECTS_TO,
28954
+ import_types97.EdgeType.PUBLISHES_TO,
28955
+ import_types97.EdgeType.CONSUMES_FROM
28065
28956
  ]);
28066
28957
  function divergenceKey(d) {
28067
28958
  const column = "column" in d && d.column ? d.column : "";
@@ -28106,7 +28997,7 @@ function formatDivergenceLine2(d) {
28106
28997
  }
28107
28998
  }
28108
28999
  function formatStaleLine(edgeId) {
28109
- const parsed = (0, import_types96.parseEdgeId)(edgeId);
29000
+ const parsed = (0, import_types97.parseEdgeId)(edgeId);
28110
29001
  if (parsed) {
28111
29002
  return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
28112
29003
  }
@@ -28119,7 +29010,7 @@ function divergenceJson(d) {
28119
29010
  return JSON.stringify({ kind: "divergence", ...d });
28120
29011
  }
28121
29012
  function staleJson(edgeId) {
28122
- const parsed = (0, import_types96.parseEdgeId)(edgeId);
29013
+ const parsed = (0, import_types97.parseEdgeId)(edgeId);
28123
29014
  return JSON.stringify({
28124
29015
  kind: "stale",
28125
29016
  edgeId,
@@ -28189,7 +29080,7 @@ var MonitorEmitter = class {
28189
29080
  // ignores non-OBSERVED edges and non-dependency edge types (structural
28190
29081
  // ownership), so only real runtime dependencies reach stdout.
28191
29082
  emitObservedEdge(edge) {
28192
- if (edge.provenance !== import_types96.Provenance.OBSERVED) return false;
29083
+ if (edge.provenance !== import_types97.Provenance.OBSERVED) return false;
28193
29084
  if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
28194
29085
  const key = `edge|${edge.id}`;
28195
29086
  if (this.seen.has(key)) return false;
@@ -28347,7 +29238,7 @@ async function runMonitor(opts) {
28347
29238
  case "edge-added": {
28348
29239
  const payload = safeParse(frame.data);
28349
29240
  const edge = payload?.edge;
28350
- if (edge && edge.provenance === import_types96.Provenance.OBSERVED) {
29241
+ if (edge && edge.provenance === import_types97.Provenance.OBSERVED) {
28351
29242
  emitter.emitObservedEdge(edge);
28352
29243
  divergences.schedule();
28353
29244
  }
@@ -28427,7 +29318,7 @@ function sleep(ms, signal) {
28427
29318
 
28428
29319
  // src/cli-verbs.ts
28429
29320
  init_cjs_shims();
28430
- var import_node_path90 = __toESM(require("path"), 1);
29321
+ var import_node_path91 = __toESM(require("path"), 1);
28431
29322
  async function resolveProjectEntry(opts) {
28432
29323
  const entries = await listProjects();
28433
29324
  if (opts.project) {
@@ -28437,7 +29328,7 @@ async function resolveProjectEntry(opts) {
28437
29328
  const cwd = opts.cwd ?? process.cwd();
28438
29329
  const resolvedCwd = await normalizeProjectPath(cwd);
28439
29330
  for (const entry2 of entries) {
28440
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path90.default.sep}`)) {
29331
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path91.default.sep}`)) {
28441
29332
  return entry2;
28442
29333
  }
28443
29334
  }
@@ -28590,7 +29481,7 @@ async function runSync(opts) {
28590
29481
  }
28591
29482
 
28592
29483
  // src/cli.ts
28593
- var import_types97 = require("@neat.is/types");
29484
+ var import_types98 = require("@neat.is/types");
28594
29485
  function isNpxInvocation() {
28595
29486
  if (process.env.npm_command === "exec") return true;
28596
29487
  const execpath = process.env.npm_execpath ?? "";
@@ -28602,7 +29493,7 @@ function isNpxInvocation() {
28602
29493
  function commandPrefix() {
28603
29494
  return isNpxInvocation() ? "npx neat.is" : "neat";
28604
29495
  }
28605
- function usage4() {
29496
+ function usage5() {
28606
29497
  const neat = commandPrefix();
28607
29498
  console.log("Installed via npx? Prefix commands with `npx neat.is`, or install once: `npm i -g neat.is`.");
28608
29499
  console.log("");
@@ -28650,13 +29541,21 @@ function usage4() {
28650
29541
  console.log(" --print-config print the JSON snippet to stdout");
28651
29542
  console.log(" --apply merge mcpServers.neat into ~/.claude.json");
28652
29543
  console.log(" hooks Wire NEAT into your agent so it queries the graph before");
28653
- console.log(" grepping. Installs a gentle Claude Code search-nudge hook and");
28654
- console.log(" writes agent-agnostic graph-first guidance for other harnesses.");
29544
+ console.log(" grepping. Installs a Claude Code search hook (nudge by default,");
29545
+ console.log(" or a hard gate) and agent-agnostic graph-first guidance.");
28655
29546
  console.log(" Flags:");
28656
29547
  console.log(" --apply install the hook + guidance");
29548
+ console.log(" --gate with --apply: DENY search until `neat ask`");
29549
+ console.log(" has run this session (default: nudge-only)");
28657
29550
  console.log(" --print-hook print the hook script");
28658
29551
  console.log(" --print-guide print the graph-first guidance");
28659
29552
  console.log(" --print-settings print the settings.json block --apply adds");
29553
+ console.log(" claude Write the query-first directive into ./CLAUDE.md as a `## neat`");
29554
+ console.log(" section so an agent reaches for `neat ask` before Read/Grep/Bash.");
29555
+ console.log(" Subcommands:");
29556
+ console.log(" install write (or refresh) the `## neat` section");
29557
+ console.log(" uninstall remove the `## neat` section");
29558
+ console.log(" print print the directive block to stdout");
28660
29559
  console.log(" codex Install NEAT into the OpenAI Codex CLI: add [mcp_servers.neat]");
28661
29560
  console.log(" to ~/.codex/config.toml and the graph-first block to ./AGENTS.md.");
28662
29561
  console.log(" Plan by default; --apply to write.");
@@ -28733,6 +29632,10 @@ function usage4() {
28733
29632
  console.log(" run time; the config file is written owner-only (0600).");
28734
29633
  console.log("");
28735
29634
  console.log("query commands (mirror the MCP tools, ADR-050):");
29635
+ console.log(" ask <question> Plain-language door: resolves the question to");
29636
+ console.log(" nodes and routes it to the right traversal, with");
29637
+ console.log(" a compact provenance-tagged answer. Reach here first.");
29638
+ console.log(` example: ${neat} ask "why is checkout failing?"`);
28736
29639
  console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
28737
29640
  console.log(` example: ${neat} root-cause service:<name>`);
28738
29641
  console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
@@ -28952,7 +29855,7 @@ async function buildPatchSections(services, project) {
28952
29855
  }
28953
29856
  async function runInit(opts) {
28954
29857
  const written = [];
28955
- const stat = await import_node_fs55.promises.stat(opts.scanPath).catch(() => null);
29858
+ const stat = await import_node_fs56.promises.stat(opts.scanPath).catch(() => null);
28956
29859
  if (!stat || !stat.isDirectory()) {
28957
29860
  console.error(`neat init: ${opts.scanPath} is not a directory`);
28958
29861
  return { exitCode: 2, writtenFiles: written };
@@ -28961,13 +29864,13 @@ async function runInit(opts) {
28961
29864
  printDiscoveryReport(opts, services);
28962
29865
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
28963
29866
  const patch = renderPatch(sections);
28964
- const patchPath = import_node_path91.default.join(opts.scanPath, "neat.patch");
29867
+ const patchPath = import_node_path92.default.join(opts.scanPath, "neat.patch");
28965
29868
  if (opts.dryRun) {
28966
- await import_node_fs55.promises.writeFile(patchPath, patch, "utf8");
29869
+ await import_node_fs56.promises.writeFile(patchPath, patch, "utf8");
28967
29870
  written.push(patchPath);
28968
29871
  console.log(`dry-run: patch written to ${patchPath}`);
28969
- const gitignorePath = import_node_path91.default.join(opts.scanPath, ".gitignore");
28970
- const gitignoreExists = await import_node_fs55.promises.stat(gitignorePath).then(() => true).catch(() => false);
29872
+ const gitignorePath = import_node_path92.default.join(opts.scanPath, ".gitignore");
29873
+ const gitignoreExists = await import_node_fs56.promises.stat(gitignorePath).then(() => true).catch(() => false);
28971
29874
  const verb = gitignoreExists ? "append" : "create";
28972
29875
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
28973
29876
  console.log("rerun without --dry-run to register and snapshot.");
@@ -28978,9 +29881,9 @@ async function runInit(opts) {
28978
29881
  const graph = getGraph(graphKey);
28979
29882
  const projectPaths = pathsForProject(
28980
29883
  graphKey,
28981
- import_node_path91.default.join(opts.scanPath, "neat-out")
29884
+ import_node_path92.default.join(opts.scanPath, "neat-out")
28982
29885
  );
28983
- const errorsPath = import_node_path91.default.join(import_node_path91.default.dirname(opts.outPath), import_node_path91.default.basename(projectPaths.errorsPath));
29886
+ const errorsPath = import_node_path92.default.join(import_node_path92.default.dirname(opts.outPath), import_node_path92.default.basename(projectPaths.errorsPath));
28984
29887
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
28985
29888
  await saveGraphToDisk(graph, opts.outPath);
28986
29889
  written.push(opts.outPath);
@@ -29059,7 +29962,7 @@ async function runInit(opts) {
29059
29962
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
29060
29963
  }
29061
29964
  } else {
29062
- await import_node_fs55.promises.writeFile(patchPath, patch, "utf8");
29965
+ await import_node_fs56.promises.writeFile(patchPath, patch, "utf8");
29063
29966
  written.push(patchPath);
29064
29967
  }
29065
29968
  }
@@ -29099,9 +30002,9 @@ var CLAUDE_SKILL_CONFIG = {
29099
30002
  };
29100
30003
  function claudeConfigPath() {
29101
30004
  const override = process.env.NEAT_CLAUDE_CONFIG;
29102
- if (override && override.length > 0) return import_node_path91.default.resolve(override);
30005
+ if (override && override.length > 0) return import_node_path92.default.resolve(override);
29103
30006
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
29104
- return import_node_path91.default.join(home, ".claude.json");
30007
+ return import_node_path92.default.join(home, ".claude.json");
29105
30008
  }
29106
30009
  async function runSkill(opts) {
29107
30010
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -29113,7 +30016,7 @@ async function runSkill(opts) {
29113
30016
  const target = claudeConfigPath();
29114
30017
  let existing = {};
29115
30018
  try {
29116
- existing = JSON.parse(await import_node_fs55.promises.readFile(target, "utf8"));
30019
+ existing = JSON.parse(await import_node_fs56.promises.readFile(target, "utf8"));
29117
30020
  } catch (err) {
29118
30021
  if (err.code !== "ENOENT") {
29119
30022
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -29125,8 +30028,8 @@ async function runSkill(opts) {
29125
30028
  ...existing,
29126
30029
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
29127
30030
  };
29128
- await import_node_fs55.promises.mkdir(import_node_path91.default.dirname(target), { recursive: true });
29129
- await import_node_fs55.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
30031
+ await import_node_fs56.promises.mkdir(import_node_path92.default.dirname(target), { recursive: true });
30032
+ await import_node_fs56.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
29130
30033
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
29131
30034
  console.log("restart Claude Code to pick up the new MCP server.");
29132
30035
  console.log("");
@@ -29153,7 +30056,7 @@ async function main() {
29153
30056
  const argv = process.argv.slice(2);
29154
30057
  const cmd0 = argv[0];
29155
30058
  if (cmd0 === "-h" || cmd0 === "--help") {
29156
- usage4();
30059
+ usage5();
29157
30060
  process.exit(0);
29158
30061
  }
29159
30062
  if (cmd0 === "--version" || cmd0 === "-v" || cmd0 === "version") {
@@ -29175,6 +30078,11 @@ async function main() {
29175
30078
  if (code !== 0) process.exit(code);
29176
30079
  return;
29177
30080
  }
30081
+ if (cmd0 === "claude") {
30082
+ const code = await runClaudeCommand(argv.slice(1));
30083
+ if (code !== 0) process.exit(code);
30084
+ return;
30085
+ }
29178
30086
  const EDITOR_VERBS = [
29179
30087
  "cursor",
29180
30088
  "devin",
@@ -29205,19 +30113,19 @@ async function main() {
29205
30113
  const target = positional[0];
29206
30114
  if (!target) {
29207
30115
  console.error("neat init: missing <path>");
29208
- usage4();
30116
+ usage5();
29209
30117
  process.exit(2);
29210
30118
  }
29211
30119
  if (apply6 && dryRun) {
29212
30120
  console.error("neat init: --apply and --dry-run are mutually exclusive");
29213
30121
  process.exit(2);
29214
30122
  }
29215
- const scanPath = import_node_path91.default.resolve(target);
30123
+ const scanPath = import_node_path92.default.resolve(target);
29216
30124
  const projectExplicit = parsed.project !== null;
29217
- const projectName = projectExplicit ? project : import_node_path91.default.basename(scanPath);
30125
+ const projectName = projectExplicit ? project : import_node_path92.default.basename(scanPath);
29218
30126
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
29219
- const fallback = pathsForProject(projectKey, import_node_path91.default.join(scanPath, "neat-out")).snapshotPath;
29220
- const outPath = import_node_path91.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
30127
+ const fallback = pathsForProject(projectKey, import_node_path92.default.join(scanPath, "neat-out")).snapshotPath;
30128
+ const outPath = import_node_path92.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
29221
30129
  const result = await runInit({
29222
30130
  scanPath,
29223
30131
  outPath,
@@ -29235,24 +30143,24 @@ async function main() {
29235
30143
  const target = positional[0];
29236
30144
  if (!target) {
29237
30145
  console.error("neat watch: missing <path>");
29238
- usage4();
30146
+ usage5();
29239
30147
  process.exit(2);
29240
30148
  }
29241
- const scanPath = import_node_path91.default.resolve(target);
29242
- const stat = await import_node_fs55.promises.stat(scanPath).catch(() => null);
30149
+ const scanPath = import_node_path92.default.resolve(target);
30150
+ const stat = await import_node_fs56.promises.stat(scanPath).catch(() => null);
29243
30151
  if (!stat || !stat.isDirectory()) {
29244
30152
  console.error(`neat watch: ${scanPath} is not a directory`);
29245
30153
  process.exit(2);
29246
30154
  }
29247
- const projectPaths = pathsForProject(project, import_node_path91.default.join(scanPath, "neat-out"));
29248
- const outPath = import_node_path91.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
29249
- const errorsPath = import_node_path91.default.resolve(
29250
- process.env.NEAT_ERRORS_PATH ?? import_node_path91.default.join(import_node_path91.default.dirname(outPath), import_node_path91.default.basename(projectPaths.errorsPath))
30155
+ const projectPaths = pathsForProject(project, import_node_path92.default.join(scanPath, "neat-out"));
30156
+ const outPath = import_node_path92.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
30157
+ const errorsPath = import_node_path92.default.resolve(
30158
+ process.env.NEAT_ERRORS_PATH ?? import_node_path92.default.join(import_node_path92.default.dirname(outPath), import_node_path92.default.basename(projectPaths.errorsPath))
29251
30159
  );
29252
- const staleEventsPath = import_node_path91.default.resolve(
29253
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path91.default.join(import_node_path91.default.dirname(outPath), import_node_path91.default.basename(projectPaths.staleEventsPath))
30160
+ const staleEventsPath = import_node_path92.default.resolve(
30161
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path92.default.join(import_node_path92.default.dirname(outPath), import_node_path92.default.basename(projectPaths.staleEventsPath))
29254
30162
  );
29255
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path91.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
30163
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path92.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
29256
30164
  const handle = await startWatch(getGraph(project), {
29257
30165
  scanPath,
29258
30166
  outPath,
@@ -29261,7 +30169,7 @@ async function main() {
29261
30169
  project,
29262
30170
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
29263
30171
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
29264
- neatHome: process.env.NEAT_HOME ? import_node_path91.default.resolve(process.env.NEAT_HOME) : import_node_path91.default.join(import_node_os8.default.homedir(), ".neat"),
30172
+ neatHome: process.env.NEAT_HOME ? import_node_path92.default.resolve(process.env.NEAT_HOME) : import_node_path92.default.join(import_node_os8.default.homedir(), ".neat"),
29265
30173
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
29266
30174
  host: process.env.HOST ?? "0.0.0.0",
29267
30175
  port: Number(process.env.PORT ?? 8080),
@@ -29297,7 +30205,7 @@ async function main() {
29297
30205
  const name = positional[0];
29298
30206
  if (!name) {
29299
30207
  console.error("neat pause: missing <name>");
29300
- usage4();
30208
+ usage5();
29301
30209
  process.exit(2);
29302
30210
  }
29303
30211
  const daemon = await findDaemonByProject(name);
@@ -29322,7 +30230,7 @@ async function main() {
29322
30230
  const name = positional[0];
29323
30231
  if (!name) {
29324
30232
  console.error("neat resume: missing <name>");
29325
- usage4();
30233
+ usage5();
29326
30234
  process.exit(2);
29327
30235
  }
29328
30236
  const daemon = await findDaemonByProject(name);
@@ -29352,7 +30260,7 @@ async function main() {
29352
30260
  const name = positional[0];
29353
30261
  if (!name) {
29354
30262
  console.error("neat uninstall: missing <name>");
29355
- usage4();
30263
+ usage5();
29356
30264
  process.exit(2);
29357
30265
  }
29358
30266
  const daemon = await findDaemonByProject(name);
@@ -29439,15 +30347,15 @@ async function main() {
29439
30347
  return;
29440
30348
  }
29441
30349
  console.error(`neat: unknown command "${cmd}"`);
29442
- usage4();
30350
+ usage5();
29443
30351
  process.exit(1);
29444
30352
  }
29445
30353
  async function tryOrchestrator(cmd, parsed) {
29446
- const scanPath = import_node_path91.default.resolve(cmd);
29447
- const stat = await import_node_fs55.promises.stat(scanPath).catch(() => null);
30354
+ const scanPath = import_node_path92.default.resolve(cmd);
30355
+ const stat = await import_node_fs56.promises.stat(scanPath).catch(() => null);
29448
30356
  if (!stat || !stat.isDirectory()) return null;
29449
30357
  const projectExplicit = parsed.project !== null;
29450
- const projectName = projectExplicit ? parsed.project : import_node_path91.default.basename(scanPath);
30358
+ const projectName = projectExplicit ? parsed.project : import_node_path92.default.basename(scanPath);
29451
30359
  const result = await runOrchestrator({
29452
30360
  scanPath,
29453
30361
  project: projectName,
@@ -29469,7 +30377,10 @@ var QUERY_VERBS = /* @__PURE__ */ new Set([
29469
30377
  "stale-edges",
29470
30378
  "policies",
29471
30379
  // Tenth verb (ADR-060) — amends ADR-050's locked allowlist of nine.
29472
- "divergences"
30380
+ "divergences",
30381
+ // Twelfth verb (ADR-198) — the plain-language door over the whole tool
30382
+ // surface. Amends ADR-050's locked allowlist the same way `divergences` did.
30383
+ "ask"
29473
30384
  ]);
29474
30385
  function resolveProjectFlag(parsed) {
29475
30386
  if (parsed.project) return parsed.project;
@@ -29591,6 +30502,15 @@ async function runQueryVerb(cmd, parsed) {
29591
30502
  makeWork = (project) => runSearch(client, { query: q, ...project ? { project } : {} });
29592
30503
  break;
29593
30504
  }
30505
+ case "ask": {
30506
+ const question = positional.join(" ").trim();
30507
+ if (!question) {
30508
+ console.error("neat ask: missing <question>");
30509
+ return 2;
30510
+ }
30511
+ makeWork = (project) => runAsk(client, { question, ...project ? { project } : {} });
30512
+ break;
30513
+ }
29594
30514
  case "diff": {
29595
30515
  const against = parsed.against ?? parsed.since;
29596
30516
  if (!against) {
@@ -29636,10 +30556,10 @@ async function runQueryVerb(cmd, parsed) {
29636
30556
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
29637
30557
  const out = [];
29638
30558
  for (const p of parts) {
29639
- const r = import_types97.DivergenceTypeSchema.safeParse(p);
30559
+ const r = import_types98.DivergenceTypeSchema.safeParse(p);
29640
30560
  if (!r.success) {
29641
30561
  console.error(
29642
- `neat divergences: unknown --type "${p}". allowed: ${import_types97.DivergenceTypeSchema.options.join(", ")}`
30562
+ `neat divergences: unknown --type "${p}". allowed: ${import_types98.DivergenceTypeSchema.options.join(", ")}`
29643
30563
  );
29644
30564
  return 2;
29645
30565
  }