@neat.is/core 0.7.9 → 0.8.0-rc.1

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 path82 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
- if (exactUnauthPaths.has(path82) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path82)) {
64
+ const path84 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
65
+ if (exactUnauthPaths.has(path84) || PROJECT_SCOPED_UNAUTH_PATTERN.test(path84)) {
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 path82 = q === -1 ? v : v.slice(0, q);
419
- if (path82.length > 0) return path82;
418
+ const path84 = q === -1 ? v : v.slice(0, q);
419
+ if (path84.length > 0) return path84;
420
420
  }
421
421
  }
422
422
  return void 0;
@@ -771,9 +771,9 @@ __export(cli_exports, {
771
771
  });
772
772
  module.exports = __toCommonJS(cli_exports);
773
773
  init_cjs_shims();
774
- var import_node_path81 = __toESM(require("path"), 1);
774
+ var import_node_path83 = __toESM(require("path"), 1);
775
775
  var import_node_os8 = __toESM(require("os"), 1);
776
- var import_node_fs46 = require("fs");
776
+ var import_node_fs48 = require("fs");
777
777
 
778
778
  // src/banner.ts
779
779
  init_cjs_shims();
@@ -1341,19 +1341,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1341
1341
  function longestIncomingWalk(graph, start, maxDepth) {
1342
1342
  let best = { path: [start], edges: [] };
1343
1343
  const visited = /* @__PURE__ */ new Set([start]);
1344
- function step(node, path82, edges) {
1345
- if (path82.length > best.path.length) {
1346
- best = { path: [...path82], edges: [...edges] };
1344
+ function step(node, path84, edges) {
1345
+ if (path84.length > best.path.length) {
1346
+ best = { path: [...path84], edges: [...edges] };
1347
1347
  }
1348
- if (path82.length - 1 >= maxDepth) return;
1348
+ if (path84.length - 1 >= maxDepth) return;
1349
1349
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1350
1350
  for (const [srcId, edge] of incoming) {
1351
1351
  if (visited.has(srcId)) continue;
1352
1352
  visited.add(srcId);
1353
- path82.push(srcId);
1353
+ path84.push(srcId);
1354
1354
  edges.push(edge);
1355
- step(srcId, path82, edges);
1356
- path82.pop();
1355
+ step(srcId, path84, edges);
1356
+ path84.pop();
1357
1357
  edges.pop();
1358
1358
  visited.delete(srcId);
1359
1359
  }
@@ -1445,7 +1445,7 @@ var rootCauseShapes = {
1445
1445
  [import_types.NodeType.FileNode]: fileRootCauseShape,
1446
1446
  [import_types.NodeType.SymbolNode]: symbolRootCauseShape
1447
1447
  };
1448
- function getRootCause(graph, errorNodeId, errorEvent, incidents) {
1448
+ function legacyRootCause(graph, errorNodeId, errorEvent, incidents) {
1449
1449
  if (!graph.hasNode(errorNodeId)) return null;
1450
1450
  const origin = graph.getNodeAttributes(errorNodeId);
1451
1451
  const shape = rootCauseShapes[origin.type];
@@ -1560,26 +1560,26 @@ function dominantFailingCall(graph, serviceId9, visited) {
1560
1560
  return best;
1561
1561
  }
1562
1562
  function followFailingCallChain(graph, originServiceId, maxDepth) {
1563
- const path82 = [originServiceId];
1563
+ const path84 = [originServiceId];
1564
1564
  const edges = [];
1565
1565
  const visited = /* @__PURE__ */ new Set([originServiceId]);
1566
1566
  let current = originServiceId;
1567
1567
  for (let depth = 0; depth < maxDepth; depth++) {
1568
1568
  const hop = dominantFailingCall(graph, current, visited);
1569
1569
  if (!hop) break;
1570
- path82.push(hop.nextService);
1570
+ path84.push(hop.nextService);
1571
1571
  edges.push(hop.edge);
1572
1572
  visited.add(hop.nextService);
1573
1573
  current = hop.nextService;
1574
1574
  }
1575
1575
  if (edges.length === 0) return null;
1576
- return { path: path82, edges, culprit: current };
1576
+ return { path: path84, edges, culprit: current };
1577
1577
  }
1578
1578
  function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1579
1579
  const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
1580
1580
  if (!chain) return null;
1581
1581
  const culprit = chain.culprit;
1582
- const path82 = [...chain.path];
1582
+ const path84 = [...chain.path];
1583
1583
  const edgeProvenances = chain.edges.map((e) => e.provenance);
1584
1584
  const baseConfidence = confidenceFromMix(chain.edges);
1585
1585
  const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
@@ -1587,14 +1587,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1587
1587
  if (loc) {
1588
1588
  let rootCauseNode = culprit;
1589
1589
  if (loc.fileNode) {
1590
- path82.push(loc.fileNode);
1590
+ path84.push(loc.fileNode);
1591
1591
  edgeProvenances.push(import_types.Provenance.OBSERVED);
1592
1592
  rootCauseNode = loc.fileNode;
1593
1593
  }
1594
1594
  return import_types.RootCauseResultSchema.parse({
1595
1595
  rootCauseNode,
1596
1596
  rootCauseReason: loc.rootCauseReason,
1597
- traversalPath: path82,
1597
+ traversalPath: path84,
1598
1598
  edgeProvenances,
1599
1599
  confidence,
1600
1600
  ...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
@@ -1606,7 +1606,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
1606
1606
  return import_types.RootCauseResultSchema.parse({
1607
1607
  rootCauseNode: culprit,
1608
1608
  rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
1609
- traversalPath: path82,
1609
+ traversalPath: path84,
1610
1610
  edgeProvenances,
1611
1611
  confidence,
1612
1612
  fixRecommendation: `Inspect ${culpritName}'s failing handler`
@@ -1702,7 +1702,9 @@ function getObservedDependencies(graph, nodeId) {
1702
1702
  dependencies: [],
1703
1703
  observed: false,
1704
1704
  inboundObservedCount: 0,
1705
- hasExtractedOutbound: false
1705
+ hasExtractedOutbound: false,
1706
+ inboundVolume: 0,
1707
+ window: "lifetime"
1706
1708
  });
1707
1709
  }
1708
1710
  const attrs = graph.getNodeAttributes(nodeId);
@@ -1733,11 +1735,19 @@ function getObservedDependencies(graph, nodeId) {
1733
1735
  }
1734
1736
  }
1735
1737
  let inboundObservedCount = 0;
1738
+ let inboundVolume = 0;
1739
+ let inboundLastObserved;
1736
1740
  for (const tgt of scope) {
1737
1741
  for (const edgeId of graph.inboundEdges(tgt)) {
1738
1742
  const e = graph.getEdgeAttributes(edgeId);
1739
1743
  if (e.type === import_types.EdgeType.CONTAINS) continue;
1740
- if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
1744
+ if (e.provenance === import_types.Provenance.OBSERVED) {
1745
+ inboundObservedCount += 1;
1746
+ inboundVolume += e.callCount ?? e.signal?.spanCount ?? 1;
1747
+ if (e.lastObserved && (!inboundLastObserved || e.lastObserved > inboundLastObserved)) {
1748
+ inboundLastObserved = e.lastObserved;
1749
+ }
1750
+ }
1741
1751
  }
1742
1752
  }
1743
1753
  dependencies.sort(
@@ -1748,7 +1758,291 @@ function getObservedDependencies(graph, nodeId) {
1748
1758
  dependencies,
1749
1759
  observed: dependencies.length > 0 || inboundObservedCount > 0,
1750
1760
  inboundObservedCount,
1751
- hasExtractedOutbound
1761
+ hasExtractedOutbound,
1762
+ // The signal is cumulative, so the honest window label is "lifetime" (ADR-190).
1763
+ inboundVolume,
1764
+ window: "lifetime",
1765
+ ...inboundLastObserved ? { inboundLastObserved } : {}
1766
+ });
1767
+ }
1768
+ var SATURATION_P95_MS = 1e3;
1769
+ function nodeScope(graph, nodeId) {
1770
+ const scope = [nodeId];
1771
+ if (!graph.hasNode(nodeId)) return scope;
1772
+ const attrs = graph.getNodeAttributes(nodeId);
1773
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1774
+ for (const edgeId of graph.outboundEdges(nodeId)) {
1775
+ const e = graph.getEdgeAttributes(edgeId);
1776
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1777
+ const owned = graph.getNodeAttributes(e.target);
1778
+ if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
1779
+ }
1780
+ }
1781
+ return scope;
1782
+ }
1783
+ function incidentCountForNode(nodeId, incidents) {
1784
+ if (!incidents || incidents.length === 0) return 0;
1785
+ return incidents.filter((ev) => incidentMatchesNode(ev, nodeId)).length;
1786
+ }
1787
+ function nodeContext(graph, nodeId, incidents, now = Date.now()) {
1788
+ const scope = nodeScope(graph, nodeId);
1789
+ let errorsFromCallers = 0;
1790
+ let inboundVolume = 0;
1791
+ let outboundVolume = 0;
1792
+ let outboundErrors = 0;
1793
+ let latestInboundMs;
1794
+ let latencyP95Ms;
1795
+ let stale = false;
1796
+ for (const n of scope) {
1797
+ if (!graph.hasNode(n)) continue;
1798
+ for (const edgeId of graph.inboundEdges(n)) {
1799
+ const e = graph.getEdgeAttributes(edgeId);
1800
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1801
+ errorsFromCallers += e.signal?.errorCount ?? 0;
1802
+ inboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
1803
+ if (e.provenance === import_types.Provenance.STALE) stale = true;
1804
+ const p95 = e.signal?.latencyMs?.p95;
1805
+ if (p95 !== void 0) latencyP95Ms = Math.max(latencyP95Ms ?? 0, p95);
1806
+ if (e.lastObserved) {
1807
+ const t = Date.parse(e.lastObserved);
1808
+ if (Number.isFinite(t)) latestInboundMs = Math.max(latestInboundMs ?? 0, t);
1809
+ }
1810
+ }
1811
+ for (const edgeId of graph.outboundEdges(n)) {
1812
+ const e = graph.getEdgeAttributes(edgeId);
1813
+ if (e.type === import_types.EdgeType.CONTAINS) continue;
1814
+ outboundVolume += e.callCount ?? e.signal?.spanCount ?? 0;
1815
+ if (e.type === import_types.EdgeType.CALLS) outboundErrors += e.signal?.errorCount ?? 0;
1816
+ if (e.provenance === import_types.Provenance.STALE) stale = true;
1817
+ }
1818
+ }
1819
+ const errorsEmittedHere = incidentCountForNode(nodeId, incidents) + outboundErrors;
1820
+ const lastObservedAgeMs = latestInboundMs !== void 0 ? Math.max(0, now - latestInboundMs) : void 0;
1821
+ return {
1822
+ errorsEmittedHere,
1823
+ errorsFromCallers,
1824
+ callCount: inboundVolume,
1825
+ outboundVolume,
1826
+ ...lastObservedAgeMs !== void 0 ? { lastObservedAgeMs } : {},
1827
+ ...latencyP95Ms !== void 0 ? { latencyP95Ms } : {},
1828
+ stale
1829
+ };
1830
+ }
1831
+ function isSaturated(ctx) {
1832
+ return ctx.latencyP95Ms !== void 0 && ctx.latencyP95Ms >= SATURATION_P95_MS;
1833
+ }
1834
+ function classifyNode(ctx) {
1835
+ if (ctx.errorsEmittedHere > 0) {
1836
+ if ((ctx.stale || isSaturated(ctx)) && ctx.errorsFromCallers >= ctx.errorsEmittedHere) {
1837
+ return "symptom-only";
1838
+ }
1839
+ return "primary-failure";
1840
+ }
1841
+ if (ctx.errorsFromCallers > 0) return "symptom-only";
1842
+ return "unrelated";
1843
+ }
1844
+ function isVictimSeed(ctx) {
1845
+ return ctx.errorsFromCallers > 0 && (ctx.stale || isSaturated(ctx)) && ctx.errorsEmittedHere <= ctx.errorsFromCallers;
1846
+ }
1847
+ function grainOf(graph, nodeId) {
1848
+ if (!graph.hasNode(nodeId)) return "unknown";
1849
+ const t = graph.getNodeAttributes(nodeId).type;
1850
+ if (t === import_types.NodeType.ServiceNode) return "service";
1851
+ if (t === import_types.NodeType.FileNode) return "file";
1852
+ if (t === import_types.NodeType.SymbolNode) return "symbol";
1853
+ return t;
1854
+ }
1855
+ function findPath(graph, from, to, direction, maxDepth) {
1856
+ if (!graph.hasNode(from) || !graph.hasNode(to)) return null;
1857
+ if (from === to) return { nodes: [from], edges: [] };
1858
+ const queue = [{ nodeId: from, depth: 0, nodes: [from], edges: [] }];
1859
+ const enqueued = /* @__PURE__ */ new Set([from]);
1860
+ while (queue.length > 0) {
1861
+ const frame = queue.shift();
1862
+ if (frame.depth >= maxDepth) continue;
1863
+ const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
1864
+ const neighbours = [...best.entries()].sort((a, b) => a[0].localeCompare(b[0]));
1865
+ for (const [nid, edge] of neighbours) {
1866
+ if (nid === to) return { nodes: [...frame.nodes, nid], edges: [...frame.edges, edge] };
1867
+ if (enqueued.has(nid)) continue;
1868
+ enqueued.add(nid);
1869
+ queue.push({
1870
+ nodeId: nid,
1871
+ depth: frame.depth + 1,
1872
+ nodes: [...frame.nodes, nid],
1873
+ edges: [...frame.edges, edge]
1874
+ });
1875
+ }
1876
+ }
1877
+ return null;
1878
+ }
1879
+ var EMPTY_CONTEXT = {
1880
+ errorsEmittedHere: 0,
1881
+ errorsFromCallers: 0,
1882
+ callCount: 0,
1883
+ outboundVolume: 0,
1884
+ stale: false
1885
+ };
1886
+ function expandNode(graph, nodeId, direction, incidents, now = Date.now()) {
1887
+ if (!graph.hasNode(nodeId)) {
1888
+ return import_types.ExpandResultSchema.parse({
1889
+ origin: nodeId,
1890
+ direction,
1891
+ node: { id: nodeId, classification: "unrelated", context: EMPTY_CONTEXT },
1892
+ neighbours: []
1893
+ });
1894
+ }
1895
+ const ctx = nodeContext(graph, nodeId, incidents, now);
1896
+ const best = direction === "up" ? bestEdgeBySource(graph, graph.inboundEdges(nodeId)) : bestEdgeByTarget(graph, graph.outboundEdges(nodeId));
1897
+ const neighbours = [];
1898
+ for (const [nid, edge] of best) {
1899
+ if (edge.type === import_types.EdgeType.CONTAINS) continue;
1900
+ const nctx = nodeContext(graph, nid, incidents, now);
1901
+ neighbours.push({
1902
+ node: nid,
1903
+ edgeType: edge.type,
1904
+ provenance: edge.provenance,
1905
+ classification: classifyNode(nctx),
1906
+ context: nctx
1907
+ });
1908
+ }
1909
+ neighbours.sort((a, b) => a.node.localeCompare(b.node));
1910
+ return import_types.ExpandResultSchema.parse({
1911
+ origin: nodeId,
1912
+ direction,
1913
+ node: { id: nodeId, classification: classifyNode(ctx), context: ctx },
1914
+ neighbours
1915
+ });
1916
+ }
1917
+ function relate(graph, a, b, maxDepth = ROOT_CAUSE_MAX_DEPTH) {
1918
+ const buildPath = (fp) => ({
1919
+ nodes: fp.nodes,
1920
+ edgeTypes: fp.edges.map((e) => e.type),
1921
+ provenance: fp.edges.map((e) => e.provenance),
1922
+ grain: fp.nodes.map((n) => grainOf(graph, n)),
1923
+ // The failure runs end to end when every hop carries error / latency / alert
1924
+ // signal — that is what turns reachability into cause-confirmation.
1925
+ carriesSignal: fp.edges.length > 0 && fp.edges.every(
1926
+ (e) => (e.signal?.errorCount ?? 0) > 0 || e.signal?.latencyMs !== void 0 || e.signal?.anomalous !== void 0
1927
+ )
1928
+ });
1929
+ if (!graph.hasNode(a) || !graph.hasNode(b)) {
1930
+ return import_types.RelateResultSchema.parse({
1931
+ a,
1932
+ b,
1933
+ related: false,
1934
+ direction: null,
1935
+ paths: [],
1936
+ note: !graph.hasNode(a) ? `node not found: ${a}` : `node not found: ${b}`
1937
+ });
1938
+ }
1939
+ const down = findPath(graph, a, b, "down", maxDepth);
1940
+ const up = findPath(graph, a, b, "up", maxDepth);
1941
+ if (!down && !up) {
1942
+ return import_types.RelateResultSchema.parse({
1943
+ a,
1944
+ b,
1945
+ related: false,
1946
+ direction: null,
1947
+ paths: [],
1948
+ note: `no path within ${maxDepth} hops`
1949
+ });
1950
+ }
1951
+ const paths = [];
1952
+ const direction = down ? "a->b" : "b->a";
1953
+ if (down) paths.push(buildPath(down));
1954
+ if (up) paths.push(buildPath(up));
1955
+ const endpointsFine = grainOf(graph, a) !== "service" && grainOf(graph, b) !== "service";
1956
+ const grainGap = endpointsFine && paths[0].grain.some((g) => g === "service");
1957
+ return import_types.RelateResultSchema.parse({
1958
+ a,
1959
+ b,
1960
+ related: true,
1961
+ direction,
1962
+ paths,
1963
+ ...grainGap ? { grainGap: true } : {}
1964
+ });
1965
+ }
1966
+ function findLoadOrigin(graph, alertNodeId, incidents, now) {
1967
+ const upstream = getBlastRadius(graph, alertNodeId).affectedNodes.map((n) => n.nodeId);
1968
+ let best = null;
1969
+ for (const nid of upstream) {
1970
+ if (nid === alertNodeId) continue;
1971
+ const ctx = nodeContext(graph, nid, incidents, now);
1972
+ if (ctx.outboundVolume === 0) continue;
1973
+ const isSource = ctx.callCount === 0;
1974
+ const better = !best || (isSource !== best.isSource ? isSource : ctx.outboundVolume !== best.ctx.outboundVolume ? ctx.outboundVolume > best.ctx.outboundVolume : nid < best.node);
1975
+ if (better) best = { node: nid, ctx, isSource };
1976
+ }
1977
+ return best ? { node: best.node, ctx: best.ctx } : null;
1978
+ }
1979
+ function getRootCause(graph, errorNodeId, errorEvent, incidents, opts) {
1980
+ const legacy = legacyRootCause(graph, errorNodeId, errorEvent, incidents);
1981
+ if (!legacy) return null;
1982
+ const navigation = opts?.navigation ?? process.env.NEAT_RCA_NAVIGATION !== "0";
1983
+ if (!navigation) return legacy;
1984
+ return enrichWithNavigation(graph, errorNodeId, legacy, incidents, opts?.now ?? Date.now());
1985
+ }
1986
+ function enrichWithNavigation(graph, errorNodeId, legacy, incidents, now) {
1987
+ const seedNode = legacy.rootCauseNode;
1988
+ const seedCtx = graph.hasNode(seedNode) ? nodeContext(graph, seedNode, incidents, now) : null;
1989
+ const lastProv = legacy.edgeProvenances[legacy.edgeProvenances.length - 1];
1990
+ const candidates = [];
1991
+ if (seedCtx && isVictimSeed(seedCtx)) {
1992
+ const origin = findLoadOrigin(graph, errorNodeId, incidents, now);
1993
+ if (origin) {
1994
+ const path84 = findPath(graph, errorNodeId, origin.node, "up", ROOT_CAUSE_MAX_DEPTH);
1995
+ const originConfidence = confidenceFromMix(path84?.edges ?? [], now);
1996
+ candidates.push({
1997
+ node: origin.node,
1998
+ classification: "primary-failure",
1999
+ reason: `Highest-volume upstream source (${origin.ctx.outboundVolume} observed outbound calls) driving a saturated/stale subgraph; the alerting path decays downstream into a starved victim rather than a fault at the callee.`,
2000
+ context: origin.ctx,
2001
+ confidence: Math.max(0.3, Math.min(0.8, originConfidence || 0.5)),
2002
+ provenance: import_types.Provenance.OBSERVED
2003
+ });
2004
+ }
2005
+ const staleNote = seedCtx.stale ? "; the node has gone STALE" : "";
2006
+ const satNote = isSaturated(seedCtx) ? `; inbound p95 ${Math.round(seedCtx.latencyP95Ms)}ms is saturated` : "";
2007
+ candidates.push({
2008
+ node: seedNode,
2009
+ classification: "symptom-only",
2010
+ reason: `Errors arrive from callers (${seedCtx.errorsFromCallers}) but none originate here${staleNote}${satNote} \u2014 a downstream victim of load, not the fault.`,
2011
+ context: seedCtx,
2012
+ confidence: Math.min(legacy.confidence, 0.4),
2013
+ ...lastProv ? { provenance: lastProv } : {}
2014
+ });
2015
+ } else {
2016
+ candidates.push({
2017
+ node: seedNode,
2018
+ classification: "primary-failure",
2019
+ reason: legacy.rootCauseReason,
2020
+ context: seedCtx ?? EMPTY_CONTEXT,
2021
+ confidence: legacy.confidence,
2022
+ ...lastProv ? { provenance: lastProv } : {}
2023
+ });
2024
+ }
2025
+ const top = candidates[0];
2026
+ let traversalPath = legacy.traversalPath;
2027
+ let edgeProvenances = legacy.edgeProvenances;
2028
+ if (top.node !== seedNode) {
2029
+ const path84 = findPath(graph, errorNodeId, top.node, "up", ROOT_CAUSE_MAX_DEPTH);
2030
+ if (path84) {
2031
+ traversalPath = path84.nodes;
2032
+ edgeProvenances = path84.edges.map((e) => e.provenance);
2033
+ } else {
2034
+ traversalPath = [errorNodeId, top.node];
2035
+ edgeProvenances = [top.provenance ?? import_types.Provenance.OBSERVED];
2036
+ }
2037
+ }
2038
+ return import_types.RootCauseResultSchema.parse({
2039
+ rootCauseNode: top.node,
2040
+ rootCauseReason: top.reason,
2041
+ traversalPath,
2042
+ edgeProvenances,
2043
+ confidence: top.confidence,
2044
+ ...legacy.fixRecommendation ? { fixRecommendation: legacy.fixRecommendation } : {},
2045
+ candidates
1752
2046
  });
1753
2047
  }
1754
2048
 
@@ -2274,6 +2568,7 @@ var import_yaml = require("yaml");
2274
2568
  var import_types3 = require("@neat.is/types");
2275
2569
  var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py", ".go", ".rb", ".php"]);
2276
2570
  var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
2571
+ var JSON_CONFIG_FILENAMES = /* @__PURE__ */ new Set(["app.json", "app.config.json", "eas.json"]);
2277
2572
  var IGNORED_DIRS = /* @__PURE__ */ new Set([
2278
2573
  "node_modules",
2279
2574
  ".git",
@@ -2313,6 +2608,7 @@ async function isPythonVenvDir(dir) {
2313
2608
  function isConfigFile(name) {
2314
2609
  const ext = import_node_path4.default.extname(name);
2315
2610
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2611
+ if (JSON_CONFIG_FILENAMES.has(name)) return { match: true, fileType: "json" };
2316
2612
  if (name === ".env" || name.startsWith(".env.")) {
2317
2613
  if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2318
2614
  if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
@@ -3141,8 +3437,8 @@ function chiRoutesFromSource(source, parser) {
3141
3437
  chiWalk(tree.rootNode, "", out);
3142
3438
  return out;
3143
3439
  }
3144
- function stripChiRegex(path82) {
3145
- return path82.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3440
+ function stripChiRegex(path84) {
3441
+ return path84.replace(/\{([^{}:]+):[^{}]*\}/g, "{$1}");
3146
3442
  }
3147
3443
  function chiWalk(node, prefix, out) {
3148
3444
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -3814,9 +4110,9 @@ function rubyRocketRoute(args) {
3814
4110
  if (!pair || pair.type !== "pair") continue;
3815
4111
  const k = pair.childForFieldName("key");
3816
4112
  if (k?.type !== "string") continue;
3817
- const path82 = rubyLiteral(k);
3818
- if (path82 === null) continue;
3819
- return { path: path82, target: rubyLiteral(pair.childForFieldName("value")) };
4113
+ const path84 = rubyLiteral(k);
4114
+ if (path84 === null) continue;
4115
+ return { path: path84, target: rubyLiteral(pair.childForFieldName("value")) };
3820
4116
  }
3821
4117
  return null;
3822
4118
  }
@@ -4436,7 +4732,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
4436
4732
  };
4437
4733
  const filePrefix = /* @__PURE__ */ new Map();
4438
4734
  const conflicted = /* @__PURE__ */ new Set();
4439
- const apply4 = (file, prefix) => {
4735
+ const apply6 = (file, prefix) => {
4440
4736
  if (conflicted.has(file)) return;
4441
4737
  const existing = filePrefix.get(file);
4442
4738
  if (existing === void 0) filePrefix.set(file, prefix);
@@ -4453,7 +4749,7 @@ async function expressMountPrefixes(files, serviceDir, tsPaths) {
4453
4749
  const info = fileInfo.get(file);
4454
4750
  const rv = info?.routerVars.get(name);
4455
4751
  if (!info || !rv) return;
4456
- if (rv.declares && info.appVars.size === 0) apply4(file, accPrefix);
4752
+ if (rv.declares && info.appVars.size === 0) apply6(file, accPrefix);
4457
4753
  for (const m of rv.mounts) {
4458
4754
  if (!m.target) continue;
4459
4755
  const t = resolveTarget(m.target, file);
@@ -4641,6 +4937,63 @@ function columnIsObserved(col) {
4641
4937
  return col.provenances.includes(import_types7.Provenance.OBSERVED);
4642
4938
  }
4643
4939
 
4940
+ // src/latency-digest.ts
4941
+ init_cjs_shims();
4942
+ var SUB = 16;
4943
+ var MIN_EXP = -10;
4944
+ var MAX_EXP = 22;
4945
+ var OCTAVES = MAX_EXP - MIN_EXP + 1;
4946
+ var OVERFLOW_INDEX = OCTAVES * SUB + 1;
4947
+ function latencyBucketIndex(ms) {
4948
+ if (!Number.isFinite(ms) || ms <= 0) return 0;
4949
+ const e = Math.floor(Math.log2(ms));
4950
+ if (e < MIN_EXP) return 0;
4951
+ if (e > MAX_EXP) return OVERFLOW_INDEX;
4952
+ const base = 2 ** e;
4953
+ const raw = Math.floor((ms / base - 1) * SUB);
4954
+ const s = raw < 0 ? 0 : raw >= SUB ? SUB - 1 : raw;
4955
+ return (e - MIN_EXP) * SUB + s + 1;
4956
+ }
4957
+ function bucketRepresentativeMs(index) {
4958
+ if (index <= 0) return 0;
4959
+ if (index >= OVERFLOW_INDEX) return 2 ** (MAX_EXP + 1);
4960
+ const zeroBased = index - 1;
4961
+ const e = MIN_EXP + Math.floor(zeroBased / SUB);
4962
+ const s = zeroBased % SUB;
4963
+ const base = 2 ** e;
4964
+ const lower = base * (1 + s / SUB);
4965
+ const upper = base * (1 + (s + 1) / SUB);
4966
+ return (lower + upper) / 2;
4967
+ }
4968
+ function recordLatency(hist, ms) {
4969
+ const key = String(latencyBucketIndex(ms));
4970
+ hist[key] = (hist[key] ?? 0) + 1;
4971
+ return hist;
4972
+ }
4973
+ function quantile(hist, q) {
4974
+ const entries = Object.entries(hist).map(([k, c]) => [Number(k), c]).filter(([idx, c]) => Number.isFinite(idx) && c > 0).sort((a, b) => a[0] - b[0]);
4975
+ let total = 0;
4976
+ for (const [, c] of entries) total += c;
4977
+ if (total === 0) return 0;
4978
+ const rank = Math.max(1, Math.ceil(q * total));
4979
+ let cumulative = 0;
4980
+ for (const [idx, c] of entries) {
4981
+ cumulative += c;
4982
+ if (cumulative >= rank) return bucketRepresentativeMs(idx);
4983
+ }
4984
+ return bucketRepresentativeMs(entries[entries.length - 1][0]);
4985
+ }
4986
+ function round(ms) {
4987
+ return Math.round(ms * 100) / 100;
4988
+ }
4989
+ function latencyPercentiles(hist) {
4990
+ if (!hist) return void 0;
4991
+ let total = 0;
4992
+ for (const c of Object.values(hist)) total += c;
4993
+ if (total === 0) return void 0;
4994
+ return { p50: round(quantile(hist, 0.5)), p95: round(quantile(hist, 0.95)) };
4995
+ }
4996
+
4644
4997
  // src/ingest.ts
4645
4998
  var HOUR_MS = 60 * 60 * 1e3;
4646
4999
  var DAY_MS = 24 * HOUR_MS;
@@ -5336,7 +5689,7 @@ function ensureFrontierNode(graph, host, ts) {
5336
5689
  graph.addNode(id, node);
5337
5690
  return id;
5338
5691
  }
5339
- function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
5692
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence, durationMs) {
5340
5693
  if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
5341
5694
  const grain = source.startsWith("file:") || source.startsWith("symbol:") ? "file" : "service";
5342
5695
  const id = makeObservedEdgeId(type, source, target);
@@ -5344,10 +5697,15 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
5344
5697
  const existing = graph.getEdgeAttributes(id);
5345
5698
  const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
5346
5699
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
5700
+ const latencyHist2 = durationMs !== void 0 ? recordLatency({ ...existing.signal?.latencyHist ?? {} }, durationMs) : existing.signal?.latencyHist;
5701
+ const latencyMs2 = latencyPercentiles(latencyHist2) ?? existing.signal?.latencyMs;
5347
5702
  const newSignal = {
5348
5703
  spanCount: newSpanCount,
5349
5704
  errorCount: newErrorCount,
5350
- lastObservedAgeMs: 0
5705
+ lastObservedAgeMs: 0,
5706
+ ...latencyHist2 ? { latencyHist: latencyHist2 } : {},
5707
+ ...latencyMs2 ? { latencyMs: latencyMs2 } : {},
5708
+ ...existing.signal?.anomalous !== void 0 ? { anomalous: existing.signal.anomalous } : {}
5351
5709
  };
5352
5710
  const updated = {
5353
5711
  ...existing,
@@ -5362,10 +5720,14 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false, ev
5362
5720
  graph.replaceEdgeAttributes(id, updated);
5363
5721
  return { edge: updated, created: false };
5364
5722
  }
5723
+ const latencyHist = durationMs !== void 0 ? recordLatency({}, durationMs) : void 0;
5724
+ const latencyMs = latencyHist ? latencyPercentiles(latencyHist) : void 0;
5365
5725
  const signal = {
5366
5726
  spanCount: 1,
5367
5727
  errorCount: isError ? 1 : 0,
5368
- lastObservedAgeMs: 0
5728
+ lastObservedAgeMs: 0,
5729
+ ...latencyHist ? { latencyHist } : {},
5730
+ ...latencyMs ? { latencyMs } : {}
5369
5731
  };
5370
5732
  const edge = {
5371
5733
  id,
@@ -5429,6 +5791,21 @@ async function appendErrorEvent(ctx, ev) {
5429
5791
  await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(ctx.errorsPath), { recursive: true });
5430
5792
  await import_node_fs8.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
5431
5793
  }
5794
+ async function appendConnectorIncident(errorsPath, input) {
5795
+ const ev = {
5796
+ id: input.id,
5797
+ timestamp: input.timestamp,
5798
+ service: input.service,
5799
+ traceId: input.id,
5800
+ spanId: input.id,
5801
+ errorType: input.errorType,
5802
+ errorMessage: input.errorMessage,
5803
+ ...input.attributes && Object.keys(input.attributes).length > 0 ? { attributes: input.attributes } : {},
5804
+ affectedNode: input.affectedNode
5805
+ };
5806
+ await import_node_fs8.promises.mkdir(import_node_path9.default.dirname(errorsPath), { recursive: true });
5807
+ await import_node_fs8.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
5808
+ }
5432
5809
  function incidentAffectedNode(span, graph, scanPath) {
5433
5810
  const sid = graph ? resolveFusedServiceId(graph, span.service, span.env) : (0, import_types8.serviceId)(span.service, span.env);
5434
5811
  const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
@@ -5575,6 +5952,7 @@ async function handleSpan(ctx, span) {
5575
5952
  }
5576
5953
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
5577
5954
  const isError = span.statusCode === 2;
5955
+ const durationMs = span.durationNanos > 0n ? Number(span.durationNanos) / 1e6 : void 0;
5578
5956
  const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
5579
5957
  const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
5580
5958
  cacheSpanService(span, nowMs, callSite);
@@ -5613,7 +5991,8 @@ async function handleSpan(ctx, span) {
5613
5991
  targetId,
5614
5992
  ts,
5615
5993
  isError,
5616
- callSiteEvidence
5994
+ callSiteEvidence,
5995
+ durationMs
5617
5996
  );
5618
5997
  if (result) affectedNode = targetId;
5619
5998
  if (span.dbSystem === "mongodb" && span.dbCollection) {
@@ -5625,7 +6004,8 @@ async function handleSpan(ctx, span) {
5625
6004
  collectionId,
5626
6005
  ts,
5627
6006
  isError,
5628
- callSiteEvidence
6007
+ callSiteEvidence,
6008
+ durationMs
5629
6009
  );
5630
6010
  }
5631
6011
  if (span.dbTable) {
@@ -5637,7 +6017,8 @@ async function handleSpan(ctx, span) {
5637
6017
  tableId,
5638
6018
  ts,
5639
6019
  isError,
5640
- callSiteEvidence
6020
+ callSiteEvidence,
6021
+ durationMs
5641
6022
  );
5642
6023
  mergeObservedColumns(ctx.graph, tableId, span.dbColumns);
5643
6024
  }
@@ -5656,7 +6037,8 @@ async function handleSpan(ctx, span) {
5656
6037
  targetId,
5657
6038
  ts,
5658
6039
  isError,
5659
- callSiteEvidence
6040
+ callSiteEvidence,
6041
+ durationMs
5660
6042
  );
5661
6043
  if (result) affectedNode = targetId;
5662
6044
  } else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
@@ -5673,7 +6055,8 @@ async function handleSpan(ctx, span) {
5673
6055
  targetId,
5674
6056
  ts,
5675
6057
  isError,
5676
- callSiteEvidence
6058
+ callSiteEvidence,
6059
+ durationMs
5677
6060
  );
5678
6061
  if (result) affectedNode = targetId;
5679
6062
  } else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
@@ -5685,7 +6068,8 @@ async function handleSpan(ctx, span) {
5685
6068
  targetId,
5686
6069
  ts,
5687
6070
  isError,
5688
- callSiteEvidence
6071
+ callSiteEvidence,
6072
+ durationMs
5689
6073
  );
5690
6074
  if (result) affectedNode = targetId;
5691
6075
  } else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
@@ -5701,7 +6085,8 @@ async function handleSpan(ctx, span) {
5701
6085
  targetId,
5702
6086
  ts,
5703
6087
  isError,
5704
- callSiteEvidence
6088
+ callSiteEvidence,
6089
+ durationMs
5705
6090
  );
5706
6091
  if (result) affectedNode = targetId;
5707
6092
  } else {
@@ -5717,7 +6102,8 @@ async function handleSpan(ctx, span) {
5717
6102
  targetId,
5718
6103
  ts,
5719
6104
  isError,
5720
- callSiteEvidence
6105
+ callSiteEvidence,
6106
+ durationMs
5721
6107
  );
5722
6108
  affectedNode = targetId;
5723
6109
  resolvedViaAddress = true;
@@ -5730,7 +6116,8 @@ async function handleSpan(ctx, span) {
5730
6116
  frontierNodeId,
5731
6117
  ts,
5732
6118
  isError,
5733
- callSiteEvidence
6119
+ callSiteEvidence,
6120
+ durationMs
5734
6121
  );
5735
6122
  affectedNode = frontierNodeId;
5736
6123
  resolvedViaAddress = true;
@@ -5756,7 +6143,8 @@ async function handleSpan(ctx, span) {
5756
6143
  sourceId,
5757
6144
  ts,
5758
6145
  isError,
5759
- fallbackEvidence
6146
+ fallbackEvidence,
6147
+ durationMs
5760
6148
  );
5761
6149
  }
5762
6150
  }
@@ -5770,7 +6158,7 @@ async function handleSpan(ctx, span) {
5770
6158
  );
5771
6159
  if (routeNodeId) {
5772
6160
  const routeSvc = ctx.graph.getNodeAttributes(routeNodeId).service;
5773
- upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError);
6161
+ upsertObservedEdge(ctx.graph, import_types8.EdgeType.CONTAINS, (0, import_types8.serviceId)(routeSvc), routeNodeId, ts, isError, void 0, durationMs);
5774
6162
  }
5775
6163
  }
5776
6164
  if (span.statusCode === 2) {
@@ -11242,8 +11630,41 @@ var import_tree_sitter14 = __toESM(require("tree-sitter"), 1);
11242
11630
  var import_tree_sitter_go3 = __toESM(require("tree-sitter-go"), 1);
11243
11631
  var import_types35 = require("@neat.is/types");
11244
11632
  init_otel();
11245
- var SQL_METHODS = /* @__PURE__ */ new Set(["Exec", "ExecContext", "Query", "QueryContext", "QueryRow", "QueryRowContext"]);
11246
11633
  var PARSE_CHUNK10 = 16384;
11634
+ var DATABASE_SQL_METHODS = /* @__PURE__ */ new Set([
11635
+ "Query",
11636
+ "QueryContext",
11637
+ "QueryRow",
11638
+ "QueryRowContext",
11639
+ "Exec",
11640
+ "ExecContext",
11641
+ "Prepare",
11642
+ "PrepareContext"
11643
+ ]);
11644
+ var SQLX_METHODS = /* @__PURE__ */ new Set([
11645
+ "Get",
11646
+ "Select",
11647
+ "Queryx",
11648
+ "QueryRowx",
11649
+ "NamedExec",
11650
+ "NamedQuery",
11651
+ "MustExec",
11652
+ "Preparex",
11653
+ "GetContext",
11654
+ "SelectContext"
11655
+ ]);
11656
+ var DATABASE_SQL_IMPORT = "database/sql";
11657
+ var SQLX_IMPORT = "github.com/jmoiron/sqlx";
11658
+ function makeGoParser3() {
11659
+ const p = new import_tree_sitter14.default();
11660
+ p.setLanguage(import_tree_sitter_go3.default);
11661
+ return p;
11662
+ }
11663
+ function parseSource10(parser, source) {
11664
+ return parser.parse(
11665
+ (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK10)
11666
+ );
11667
+ }
11247
11668
  function walk7(node, visit) {
11248
11669
  visit(node);
11249
11670
  for (let i = 0; i < node.namedChildCount; i++) {
@@ -11251,25 +11672,54 @@ function walk7(node, visit) {
11251
11672
  if (child) walk7(child, visit);
11252
11673
  }
11253
11674
  }
11675
+ function goStringLiteralValue(node) {
11676
+ if (!node) return null;
11677
+ if (node.type === "interpreted_string_literal" || node.type === "raw_string_literal") {
11678
+ const t = node.text;
11679
+ return t.length >= 2 ? t.slice(1, -1) : "";
11680
+ }
11681
+ return null;
11682
+ }
11683
+ function goImportsAny(root, names) {
11684
+ let found = false;
11685
+ walk7(root, (node) => {
11686
+ if (found || node.type !== "import_spec") return;
11687
+ for (let i = 0; i < node.namedChildCount; i++) {
11688
+ const value = goStringLiteralValue(node.namedChild(i));
11689
+ if (value !== null && names.has(value)) found = true;
11690
+ }
11691
+ });
11692
+ return found;
11693
+ }
11694
+ function firstStringLiteralArg(argsNode) {
11695
+ if (!argsNode) return null;
11696
+ for (let i = 0; i < argsNode.namedChildCount; i++) {
11697
+ const value = goStringLiteralValue(argsNode.namedChild(i));
11698
+ if (value !== null) return value;
11699
+ }
11700
+ return null;
11701
+ }
11254
11702
  function goSqlEndpointsFromFile(file, serviceDir) {
11255
11703
  if (import_node_path48.default.extname(file.path) !== ".go") return [];
11256
- const parser = new import_tree_sitter14.default();
11257
- parser.setLanguage(import_tree_sitter_go3.default);
11258
- const tree = parser.parse(
11259
- (index) => index >= file.content.length ? "" : file.content.slice(index, index + PARSE_CHUNK10)
11260
- );
11704
+ if (!file.content.includes(DATABASE_SQL_IMPORT) && !file.content.includes(SQLX_IMPORT)) return [];
11705
+ const tree = parseSource10(makeGoParser3(), file.content);
11706
+ const importsDatabaseSql = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([DATABASE_SQL_IMPORT]));
11707
+ const importsSqlx = goImportsAny(tree.rootNode, /* @__PURE__ */ new Set([SQLX_IMPORT]));
11708
+ if (!importsDatabaseSql && !importsSqlx) return [];
11261
11709
  const out = [];
11262
11710
  walk7(tree.rootNode, (node) => {
11263
11711
  if (node.type !== "call_expression") return;
11264
11712
  const fn = node.childForFieldName("function");
11265
11713
  if (fn?.type !== "selector_expression") return;
11266
11714
  const method = fn.childForFieldName("field")?.text;
11267
- if (!method || !SQL_METHODS.has(method)) return;
11268
- const arg = node.childForFieldName("arguments")?.namedChild(0);
11269
- if (!arg || arg.type !== "interpreted_string_literal" && arg.type !== "raw_string_literal") return;
11270
- const sql = arg.text.slice(1, -1);
11715
+ if (!method) return;
11716
+ const recognized = DATABASE_SQL_METHODS.has(method) || importsSqlx && SQLX_METHODS.has(method);
11717
+ if (!recognized) return;
11718
+ const sql = firstStringLiteralArg(node.childForFieldName("arguments"));
11719
+ if (sql === null) return;
11271
11720
  const table = tableFromSqlStatement(sql);
11272
11721
  if (!table) return;
11722
+ const columns = columnsFromSqlStatement(sql);
11273
11723
  const line = node.startPosition.row + 1;
11274
11724
  out.push({
11275
11725
  infraId: (0, import_types35.infraId)("sql-table", table),
@@ -11277,7 +11727,12 @@ function goSqlEndpointsFromFile(file, serviceDir) {
11277
11727
  kind: "sql-table",
11278
11728
  edgeType: "CALLS",
11279
11729
  confidenceKind: "verified-call-site",
11280
- evidence: { file: toPosix(import_node_path48.default.relative(serviceDir, file.path)), line, snippet: snippet(file.content, line) }
11730
+ ...columns.length > 0 ? { columns } : {},
11731
+ evidence: {
11732
+ file: toPosix(import_node_path48.default.relative(serviceDir, file.path)),
11733
+ line,
11734
+ snippet: snippet(file.content, line)
11735
+ }
11281
11736
  });
11282
11737
  });
11283
11738
  return out;
@@ -11291,12 +11746,12 @@ var import_tree_sitter_go4 = __toESM(require("tree-sitter-go"), 1);
11291
11746
  var import_types36 = require("@neat.is/types");
11292
11747
  var GORM_IMPORT_RE = /gorm\.io\/gorm/;
11293
11748
  var PARSE_CHUNK11 = 16384;
11294
- function makeGoParser3() {
11749
+ function makeGoParser4() {
11295
11750
  const p = new import_tree_sitter15.default();
11296
11751
  p.setLanguage(import_tree_sitter_go4.default);
11297
11752
  return p;
11298
11753
  }
11299
- function parseSource10(parser, source) {
11754
+ function parseSource11(parser, source) {
11300
11755
  return parser.parse(
11301
11756
  (index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK11)
11302
11757
  );
@@ -11720,7 +12175,7 @@ function collectColumns(struct, structs, seen, prefix, out, emitted) {
11720
12175
  function gormEndpointsFromFile(file, serviceDir) {
11721
12176
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11722
12177
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11723
- const tree = parseSource10(makeGoParser3(), file.content);
12178
+ const tree = parseSource11(makeGoParser4(), file.content);
11724
12179
  const { structs, models, tableFor } = analyze(tree);
11725
12180
  const out = [];
11726
12181
  const seenTables = /* @__PURE__ */ new Set();
@@ -11751,7 +12206,7 @@ function gormEndpointsFromFile(file, serviceDir) {
11751
12206
  function gormForeignKeys(file, serviceDir) {
11752
12207
  if (import_node_path49.default.extname(file.path) !== ".go") return [];
11753
12208
  if (!GORM_IMPORT_RE.test(file.content)) return [];
11754
- const tree = parseSource10(makeGoParser3(), file.content);
12209
+ const tree = parseSource11(makeGoParser4(), file.content);
11755
12210
  const { structs, models, tableFor } = analyze(tree);
11756
12211
  const out = [];
11757
12212
  const seen = /* @__PURE__ */ new Set();
@@ -13978,7 +14433,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
13978
14433
  init_cjs_shims();
13979
14434
  var import_fastify2 = __toESM(require("fastify"), 1);
13980
14435
  var import_cors = __toESM(require("@fastify/cors"), 1);
13981
- var import_types81 = require("@neat.is/types");
14436
+ var import_types86 = require("@neat.is/types");
13982
14437
 
13983
14438
  // src/extend/index.ts
13984
14439
  init_cjs_shims();
@@ -15307,6 +15762,23 @@ async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
15307
15762
  unresolved++;
15308
15763
  continue;
15309
15764
  }
15765
+ if (signal.incident) {
15766
+ ensureServiceNode(graph, resolved.serviceName, NO_ENV);
15767
+ if (!ctx.errorsPath || !graph.hasNode(resolved.targetNodeId)) {
15768
+ unresolved++;
15769
+ continue;
15770
+ }
15771
+ await appendConnectorIncident(ctx.errorsPath, {
15772
+ id: signal.incident.id,
15773
+ timestamp: signal.incident.timestamp,
15774
+ service: signal.incident.service,
15775
+ errorType: signal.incident.errorType,
15776
+ errorMessage: signal.incident.errorMessage,
15777
+ ...signal.incident.attributes ? { attributes: signal.incident.attributes } : {},
15778
+ affectedNode: resolved.targetNodeId
15779
+ });
15780
+ continue;
15781
+ }
15310
15782
  if (resolved.ensureInfraNode) {
15311
15783
  const { kind, name, provider } = resolved.ensureInfraNode;
15312
15784
  ensureInfraNode(graph, kind, name, provider);
@@ -15772,10 +16244,10 @@ var SUPABASE_RPC_TARGET_KIND = "supabase-rpc";
15772
16244
  // src/connectors/supabase/map.ts
15773
16245
  var REST_RPC_PATH_RE = /^\/rest\/v1\/rpc\/([^/?]+)/;
15774
16246
  var REST_TABLE_PATH_RE = /^\/rest\/v1\/([^/?]+)/;
15775
- function targetFromRestPath(path82) {
15776
- const rpcMatch = REST_RPC_PATH_RE.exec(path82);
16247
+ function targetFromRestPath(path84) {
16248
+ const rpcMatch = REST_RPC_PATH_RE.exec(path84);
15777
16249
  if (rpcMatch) return { targetKind: SUPABASE_RPC_TARGET_KIND, name: rpcMatch[1] };
15778
- const tableMatch = REST_TABLE_PATH_RE.exec(path82);
16250
+ const tableMatch = REST_TABLE_PATH_RE.exec(path84);
15779
16251
  if (tableMatch) return { targetKind: SUPABASE_TABLE_TARGET_KIND, name: tableMatch[1] };
15780
16252
  return null;
15781
16253
  }
@@ -16379,9 +16851,9 @@ function parseFirebaseTargetName(targetName) {
16379
16851
  const secondSep = rest.indexOf(FIELD_SEP);
16380
16852
  if (secondSep === -1) return null;
16381
16853
  const method = rest.slice(0, secondSep);
16382
- const path82 = rest.slice(secondSep + 1);
16383
- if (!resourceName || !method || !path82) return null;
16384
- return { resourceName, method, path: path82 };
16854
+ const path84 = rest.slice(secondSep + 1);
16855
+ if (!resourceName || !method || !path84) return null;
16856
+ return { resourceName, method, path: path84 };
16385
16857
  }
16386
16858
  function resourceNameFor(type, labels) {
16387
16859
  if (!labels) return null;
@@ -16419,14 +16891,14 @@ function mapLogEntryToSignal(entry2) {
16419
16891
  if (!req) return null;
16420
16892
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
16421
16893
  const method = req.requestMethod.toUpperCase();
16422
- const path82 = pathFromRequestUrl(req.requestUrl);
16423
- if (path82 === null) return null;
16894
+ const path84 = pathFromRequestUrl(req.requestUrl);
16895
+ if (path84 === null) return null;
16424
16896
  const timestamp = entry2.timestamp;
16425
16897
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
16426
16898
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD2;
16427
16899
  return {
16428
16900
  targetKind: resourceType,
16429
- targetName: packFirebaseTargetName({ resourceName, method, path: path82 }),
16901
+ targetName: packFirebaseTargetName({ resourceName, method, path: path84 }),
16430
16902
  callCount: 1,
16431
16903
  errorCount: isError ? 1 : 0,
16432
16904
  lastObservedIso: timestamp
@@ -16633,7 +17105,7 @@ function mapEventToSignal(event) {
16633
17105
  if (Number.isNaN(observedAt.getTime())) return null;
16634
17106
  const statusCode = metadata?.statusCode;
16635
17107
  const isError = typeof statusCode === "number" && statusCode >= ERROR_STATUS_THRESHOLD3;
16636
- const path82 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
17108
+ const path84 = metadata?.trigger ? parsePathFromTrigger(metadata.trigger) : void 0;
16637
17109
  return {
16638
17110
  targetKind: CLOUDFLARE_TARGET_KIND,
16639
17111
  targetName: scriptName,
@@ -16641,7 +17113,7 @@ function mapEventToSignal(event) {
16641
17113
  errorCount: isError ? 1 : 0,
16642
17114
  lastObservedIso: observedAt.toISOString(),
16643
17115
  method,
16644
- ...path82 ? { path: path82 } : {},
17116
+ ...path84 ? { path: path84 } : {},
16645
17117
  ...typeof statusCode === "number" ? { statusCode } : {},
16646
17118
  ...typeof metadata?.duration === "number" ? { duration: metadata.duration } : {}
16647
17119
  };
@@ -16687,8 +17159,8 @@ function findTaggedWorkerFileNode(graph, workerName) {
16687
17159
  });
16688
17160
  return found;
16689
17161
  }
16690
- function findMatchingRouteNode(graph, serviceName, method, path82) {
16691
- const normalizedPath = normalizePathTemplate(path82);
17162
+ function findMatchingRouteNode(graph, serviceName, method, path84) {
17163
+ const normalizedPath = normalizePathTemplate(path84);
16692
17164
  let found = null;
16693
17165
  graph.forEachNode((id, attrs) => {
16694
17166
  if (found) return;
@@ -16705,10 +17177,10 @@ function createCloudflareResolveTarget(config, graph) {
16705
17177
  return (signal) => {
16706
17178
  if (signal.targetKind !== CLOUDFLARE_TARGET_KIND) return null;
16707
17179
  const scriptName = signal.targetName;
16708
- const { method, path: path82 } = signal;
17180
+ const { method, path: path84 } = signal;
16709
17181
  const resolveRouteGrain = (serviceName, wholeFileId) => {
16710
- if (!method || !path82) return wholeFileId;
16711
- return findMatchingRouteNode(graph, serviceName, method, path82) ?? wholeFileId;
17182
+ if (!method || !path84) return wholeFileId;
17183
+ return findMatchingRouteNode(graph, serviceName, method, path84) ?? wholeFileId;
16712
17184
  };
16713
17185
  const mapping = config.workers?.[scriptName];
16714
17186
  if (mapping) {
@@ -17060,9 +17532,9 @@ function parseCloudRunTargetName(targetName) {
17060
17532
  const secondSep = rest.indexOf(FIELD_SEP2);
17061
17533
  if (secondSep === -1) return null;
17062
17534
  const method = rest.slice(0, secondSep);
17063
- const path82 = rest.slice(secondSep + 1);
17064
- if (!serviceName || !method || !path82) return null;
17065
- return { serviceName, method, path: path82 };
17535
+ const path84 = rest.slice(secondSep + 1);
17536
+ if (!serviceName || !method || !path84) return null;
17537
+ return { serviceName, method, path: path84 };
17066
17538
  }
17067
17539
 
17068
17540
  // src/connectors/cloud-run/map.ts
@@ -17091,14 +17563,14 @@ function mapLogEntryToSignal2(entry2) {
17091
17563
  if (!req) return null;
17092
17564
  if (typeof req.requestMethod !== "string" || req.requestMethod.length === 0) return null;
17093
17565
  const method = req.requestMethod.toUpperCase();
17094
- const path82 = pathFromRequestUrl2(req.requestUrl);
17095
- if (path82 === null) return null;
17566
+ const path84 = pathFromRequestUrl2(req.requestUrl);
17567
+ if (path84 === null) return null;
17096
17568
  const timestamp = entry2.timestamp;
17097
17569
  if (typeof timestamp !== "string" || timestamp.length === 0) return null;
17098
17570
  const isError = typeof req.status === "number" && req.status >= ERROR_STATUS_THRESHOLD4;
17099
17571
  return {
17100
17572
  targetKind: CLOUD_RUN_TARGET_KIND,
17101
- targetName: packCloudRunTargetName({ serviceName, method, path: path82 }),
17573
+ targetName: packCloudRunTargetName({ serviceName, method, path: path84 }),
17102
17574
  callCount: 1,
17103
17575
  errorCount: isError ? 1 : 0,
17104
17576
  lastObservedIso: timestamp
@@ -17137,14 +17609,14 @@ function createCloudRunResolveTarget(graph, config) {
17137
17609
  if (signal.targetKind !== CLOUD_RUN_TARGET_KIND) return null;
17138
17610
  const identity = parseCloudRunTargetName(signal.targetName);
17139
17611
  if (!identity) return null;
17140
- const { serviceName: gcpServiceName, method, path: path82 } = identity;
17612
+ const { serviceName: gcpServiceName, method, path: path84 } = identity;
17141
17613
  const mappedService = config.serviceMap?.[gcpServiceName];
17142
17614
  if (mappedService) {
17143
17615
  const routeNodeId = findMatchingRouteNode2(
17144
17616
  graph,
17145
17617
  mappedService,
17146
17618
  method,
17147
- normalizePathTemplate(path82)
17619
+ normalizePathTemplate(path84)
17148
17620
  );
17149
17621
  if (routeNodeId) {
17150
17622
  return { targetNodeId: routeNodeId, serviceName: mappedService, edgeType: import_types71.EdgeType.CALLS };
@@ -17541,6 +18013,338 @@ function createPlanetscaleConnector(graph, config, deps = {}) {
17541
18013
  };
17542
18014
  }
17543
18015
 
18016
+ // src/connectors/eas/index.ts
18017
+ init_cjs_shims();
18018
+
18019
+ // src/connectors/eas/client.ts
18020
+ init_cjs_shims();
18021
+
18022
+ // src/connectors/eas/types.ts
18023
+ init_cjs_shims();
18024
+ function readEasCredentials(raw) {
18025
+ const token = raw["token"];
18026
+ if (typeof token !== "string" || token.length === 0) {
18027
+ throw new Error("eas connector: credentials.token (EXPO_TOKEN) must be a non-empty string");
18028
+ }
18029
+ return { token };
18030
+ }
18031
+ var EAS_STATUS_ERRORED = "ERRORED";
18032
+ var TRANSIENT_BUILD_PHASES = /* @__PURE__ */ new Set([
18033
+ "SPIN_UP_BUILDER",
18034
+ "PREPARE_CREDENTIALS",
18035
+ "RESTORE_CACHE",
18036
+ "UPLOAD_APPLICATION_ARCHIVE"
18037
+ ]);
18038
+ var INTERNAL_ERROR_CODE = /INTERNAL_SERVER_ERROR|EAS_BUILD_.*INTERNAL|_INTERNAL_ERROR|UNKNOWN_ERROR/i;
18039
+ function isTransientFailure(err) {
18040
+ if (!err) return false;
18041
+ const phase = typeof err.buildPhase === "string" ? err.buildPhase : void 0;
18042
+ if (phase) {
18043
+ if (TRANSIENT_BUILD_PHASES.has(phase)) return true;
18044
+ if (/^SPIN_UP|_CREDENTIALS$|^RESTORE_CACHE/i.test(phase)) return true;
18045
+ }
18046
+ const code = typeof err.errorCode === "string" ? err.errorCode : void 0;
18047
+ if (code && INTERNAL_ERROR_CODE.test(code)) return true;
18048
+ return false;
18049
+ }
18050
+ var FIELD_SEP3 = "\0";
18051
+ var EAS_TARGET_KIND = "eas-build";
18052
+ function packEasTargetName(identity) {
18053
+ return [identity.serviceName, identity.phase].join(FIELD_SEP3);
18054
+ }
18055
+ function parseEasTargetName(targetName) {
18056
+ const sep = targetName.indexOf(FIELD_SEP3);
18057
+ if (sep === -1) return null;
18058
+ const serviceName = targetName.slice(0, sep);
18059
+ const phase = targetName.slice(sep + 1);
18060
+ if (!serviceName) return null;
18061
+ return { serviceName, phase };
18062
+ }
18063
+
18064
+ // src/connectors/eas/client.ts
18065
+ var DEFAULT_EAS_API_URL = "https://api.expo.dev/graphql";
18066
+ var DEFAULT_PAGE_SIZE = 50;
18067
+ var DEFAULT_MAX_PAGES = 10;
18068
+ var DEFAULT_MAX_LOG_BYTES = 16 * 1024;
18069
+ var DEFAULT_MAX_LOOKBACK_MS6 = 7 * 24 * 60 * 60 * 1e3;
18070
+ var BUILDS_QUERY = `
18071
+ query NeatEasErroredBuilds($appId: String!, $offset: Int!, $limit: Int!) {
18072
+ app {
18073
+ byId(appId: $appId) {
18074
+ builds(offset: $offset, limit: $limit, filter: { status: ERRORED }) {
18075
+ id
18076
+ status
18077
+ platform
18078
+ buildProfile
18079
+ gitCommitHash
18080
+ gitCommitMessage
18081
+ gitRef
18082
+ isGitWorkingTreeDirty
18083
+ createdAt
18084
+ completedAt
18085
+ error {
18086
+ buildPhase
18087
+ errorCode
18088
+ message
18089
+ docsUrl
18090
+ }
18091
+ logFileUrls
18092
+ }
18093
+ }
18094
+ }
18095
+ }
18096
+ `;
18097
+ async function easGraphQL(apiUrl, token, query, variables, accountKey, fetchImpl) {
18098
+ const res = await junctionFetch(
18099
+ apiUrl,
18100
+ {
18101
+ method: "POST",
18102
+ headers: {
18103
+ "Content-Type": "application/json",
18104
+ ...bearerAuthHeader(token)
18105
+ },
18106
+ body: JSON.stringify({ query, variables })
18107
+ },
18108
+ // accountKey: the Expo app id — the per-(provider, accountKey) rate-limit
18109
+ // bucket (ADR-131), the closest thing this connector carries to "one account".
18110
+ { provider: "eas", accountKey, ...fetchImpl ? { fetchImpl } : {} }
18111
+ );
18112
+ if (!res.ok) {
18113
+ throw new Error(`Expo GraphQL request failed: ${res.status} ${res.statusText}`);
18114
+ }
18115
+ const body = await res.json();
18116
+ if (body.errors && body.errors.length > 0) {
18117
+ throw new Error(`Expo GraphQL errors: ${body.errors.map((e) => e.message).join("; ")}`);
18118
+ }
18119
+ if (!body.data) throw new Error("Expo GraphQL response carried no data");
18120
+ return body.data;
18121
+ }
18122
+ async function fetchErroredBuilds(token, config, fetchImpl) {
18123
+ const apiUrl = config.apiUrl ?? DEFAULT_EAS_API_URL;
18124
+ const pageSize = config.pageSize ?? DEFAULT_PAGE_SIZE;
18125
+ const maxPages = config.maxPages ?? DEFAULT_MAX_PAGES;
18126
+ const out = [];
18127
+ const seen = /* @__PURE__ */ new Set();
18128
+ for (let page = 0; page < maxPages; page++) {
18129
+ const data = await easGraphQL(
18130
+ apiUrl,
18131
+ token,
18132
+ BUILDS_QUERY,
18133
+ { appId: config.appId, offset: page * pageSize, limit: pageSize },
18134
+ config.appId,
18135
+ fetchImpl
18136
+ );
18137
+ const builds = data.app?.byId?.builds;
18138
+ if (!Array.isArray(builds)) break;
18139
+ let added = 0;
18140
+ for (const b of builds) {
18141
+ if (!b || typeof b.id !== "string" || b.id.length === 0) continue;
18142
+ if (b.status !== EAS_STATUS_ERRORED) continue;
18143
+ if (seen.has(b.id)) continue;
18144
+ seen.add(b.id);
18145
+ out.push(b);
18146
+ added++;
18147
+ }
18148
+ if (builds.length < pageSize) break;
18149
+ if (added === 0) break;
18150
+ }
18151
+ return out;
18152
+ }
18153
+ async function fetchBuildLogs(logFileUrls, maxBytes = DEFAULT_MAX_LOG_BYTES, fetchImpl) {
18154
+ if (!Array.isArray(logFileUrls) || logFileUrls.length === 0) return "";
18155
+ const doFetch = fetchImpl ?? fetch;
18156
+ const chunks = [];
18157
+ for (const url of logFileUrls) {
18158
+ if (typeof url !== "string" || url.length === 0) continue;
18159
+ try {
18160
+ const res = await doFetch(url);
18161
+ if (!res.ok) continue;
18162
+ chunks.push(await res.text());
18163
+ } catch {
18164
+ }
18165
+ }
18166
+ const joined = chunks.join("\n");
18167
+ return joined.length > maxBytes ? joined.slice(joined.length - maxBytes) : joined;
18168
+ }
18169
+
18170
+ // src/connectors/eas/map.ts
18171
+ init_cjs_shims();
18172
+ function buildEventTime(build) {
18173
+ if (typeof build.completedAt === "string" && build.completedAt.length > 0) return build.completedAt;
18174
+ if (typeof build.createdAt === "string" && build.createdAt.length > 0) return build.createdAt;
18175
+ return (/* @__PURE__ */ new Date()).toISOString();
18176
+ }
18177
+ function incidentMessage2(build) {
18178
+ const err = build.error ?? {};
18179
+ const phase = typeof err.buildPhase === "string" && err.buildPhase.length > 0 ? ` at ${err.buildPhase}` : "";
18180
+ const detail = typeof err.message === "string" && err.message.trim().length > 0 && err.message.trim() || typeof err.errorCode === "string" && err.errorCode.length > 0 && err.errorCode || "no error detail reported";
18181
+ let msg = `EAS build failed${phase}: ${detail}`;
18182
+ if (build.isGitWorkingTreeDirty === true) {
18183
+ msg += " (built from a dirty working tree \u2014 the commit may not represent what built)";
18184
+ }
18185
+ return msg;
18186
+ }
18187
+ function incidentAttributes(build) {
18188
+ const attrs = {};
18189
+ const err = build.error ?? {};
18190
+ const put = (k, v) => {
18191
+ if (typeof v === "string" && v.length === 0) return;
18192
+ if (v !== void 0 && v !== null) attrs[k] = v;
18193
+ };
18194
+ put("eas.buildId", build.id);
18195
+ put("eas.platform", build.platform ?? void 0);
18196
+ put("eas.buildProfile", build.buildProfile ?? void 0);
18197
+ put("eas.buildPhase", err.buildPhase ?? void 0);
18198
+ put("eas.errorCode", err.errorCode ?? void 0);
18199
+ put("eas.docsUrl", err.docsUrl ?? void 0);
18200
+ put("eas.gitCommitHash", build.gitCommitHash ?? void 0);
18201
+ put("eas.gitRef", build.gitRef ?? void 0);
18202
+ put("eas.gitCommitMessage", build.gitCommitMessage ?? void 0);
18203
+ if (typeof build.isGitWorkingTreeDirty === "boolean") {
18204
+ attrs["eas.gitWorkingTreeDirty"] = build.isGitWorkingTreeDirty;
18205
+ if (build.isGitWorkingTreeDirty) attrs["eas.confidence"] = "low";
18206
+ }
18207
+ put("eas.createdAt", build.createdAt ?? void 0);
18208
+ put("eas.completedAt", build.completedAt ?? void 0);
18209
+ if (typeof build.logsText === "string" && build.logsText.length > 0) {
18210
+ attrs["eas.logs"] = build.logsText;
18211
+ }
18212
+ return attrs;
18213
+ }
18214
+ function mapBuildToSignal(build, serviceName) {
18215
+ if (!build || typeof build !== "object") return null;
18216
+ if (build.status !== EAS_STATUS_ERRORED) return null;
18217
+ if (!build.error) return null;
18218
+ if (isTransientFailure(build.error)) return null;
18219
+ const timestamp = buildEventTime(build);
18220
+ const phase = typeof build.error.buildPhase === "string" ? build.error.buildPhase : "";
18221
+ return {
18222
+ targetKind: EAS_TARGET_KIND,
18223
+ targetName: packEasTargetName({ serviceName, phase }),
18224
+ // Incident-only — no edge, so no call/error count to replay.
18225
+ callCount: 0,
18226
+ errorCount: 0,
18227
+ lastObservedIso: timestamp,
18228
+ incident: {
18229
+ id: `eas:build:${build.id}`,
18230
+ timestamp,
18231
+ service: serviceName,
18232
+ errorType: "eas-build-failure",
18233
+ errorMessage: incidentMessage2(build),
18234
+ attributes: incidentAttributes(build)
18235
+ }
18236
+ };
18237
+ }
18238
+ function mapBuildsToSignals(builds, serviceName) {
18239
+ const out = [];
18240
+ for (const build of builds) {
18241
+ const signal = mapBuildToSignal(build, serviceName);
18242
+ if (signal) out.push(signal);
18243
+ }
18244
+ return out;
18245
+ }
18246
+
18247
+ // src/connectors/eas/resolve.ts
18248
+ init_cjs_shims();
18249
+ var import_types83 = require("@neat.is/types");
18250
+ var NO_ENV2 = "unknown";
18251
+ var EAS_JSON_PHASES = /* @__PURE__ */ new Set(["READ_EAS_JSON"]);
18252
+ var APP_CONFIG_PHASES = /* @__PURE__ */ new Set([
18253
+ "READ_APP_CONFIG",
18254
+ "CONFIGURE_EXPO_UPDATES",
18255
+ "CALCULATE_EXPO_UPDATES_RUNTIME_VERSION"
18256
+ ]);
18257
+ function configBasenamesForPhase(phase) {
18258
+ if (EAS_JSON_PHASES.has(phase)) return ["eas.json"];
18259
+ if (APP_CONFIG_PHASES.has(phase)) return ["app.json", "app.config.json"];
18260
+ return [];
18261
+ }
18262
+ function configNodeService(graph, configNodeId) {
18263
+ for (const edgeId of graph.inboundEdges(configNodeId)) {
18264
+ const edge = graph.getEdgeAttributes(edgeId);
18265
+ if (edge.type !== import_types83.EdgeType.CONFIGURED_BY) continue;
18266
+ const parsed = (0, import_types83.parseFileId)(edge.source);
18267
+ if (parsed) return parsed.service;
18268
+ }
18269
+ return null;
18270
+ }
18271
+ function findConfigNode(graph, basenames, serviceName) {
18272
+ let scoped = null;
18273
+ let anyMatch = null;
18274
+ graph.forEachNode((id, attrs) => {
18275
+ if (scoped) return;
18276
+ const node = attrs;
18277
+ if (node.type !== import_types83.NodeType.ConfigNode) return;
18278
+ if (typeof node.name !== "string" || !basenames.includes(node.name)) return;
18279
+ if (anyMatch === null) anyMatch = id;
18280
+ if (configNodeService(graph, id) === serviceName) scoped = id;
18281
+ });
18282
+ return scoped ?? anyMatch;
18283
+ }
18284
+ function createEasResolveTarget(graph) {
18285
+ return (signal) => {
18286
+ if (signal.targetKind !== EAS_TARGET_KIND) return null;
18287
+ const identity = parseEasTargetName(signal.targetName);
18288
+ if (!identity) return null;
18289
+ const { serviceName, phase } = identity;
18290
+ const basenames = configBasenamesForPhase(phase);
18291
+ if (basenames.length > 0) {
18292
+ const configNodeId = findConfigNode(graph, basenames, serviceName);
18293
+ if (configNodeId) {
18294
+ return { targetNodeId: configNodeId, serviceName, edgeType: import_types83.EdgeType.CALLS };
18295
+ }
18296
+ }
18297
+ return {
18298
+ targetNodeId: resolveFusedServiceId(graph, serviceName, NO_ENV2),
18299
+ serviceName,
18300
+ edgeType: import_types83.EdgeType.CALLS
18301
+ };
18302
+ };
18303
+ }
18304
+
18305
+ // src/connectors/eas/index.ts
18306
+ function isBuildSince(build, sinceIso) {
18307
+ const t = Date.parse(buildEventTime(build));
18308
+ const s = Date.parse(sinceIso);
18309
+ if (Number.isNaN(t) || Number.isNaN(s)) return true;
18310
+ return t > s;
18311
+ }
18312
+ function boundedSinceIso2(since, now, maxLookbackMs) {
18313
+ const floor = new Date(now.getTime() - maxLookbackMs);
18314
+ if (!since) return floor.toISOString();
18315
+ const sinceMs = new Date(since).getTime();
18316
+ if (Number.isNaN(sinceMs)) return floor.toISOString();
18317
+ return sinceMs < floor.getTime() ? floor.toISOString() : new Date(sinceMs).toISOString();
18318
+ }
18319
+ var EasConnector = class {
18320
+ constructor(config, fetchImpl) {
18321
+ this.config = config;
18322
+ this.fetchImpl = fetchImpl;
18323
+ }
18324
+ config;
18325
+ fetchImpl;
18326
+ provider = "eas";
18327
+ async poll(ctx) {
18328
+ const creds = readEasCredentials(ctx.credentials);
18329
+ const serviceName = this.config.serviceName ?? this.config.appId;
18330
+ const maxLookbackMs = this.config.maxLookbackMs ?? DEFAULT_MAX_LOOKBACK_MS6;
18331
+ const sinceIso = boundedSinceIso2(ctx.since, /* @__PURE__ */ new Date(), maxLookbackMs);
18332
+ const builds = await fetchErroredBuilds(creds.token, this.config, this.fetchImpl);
18333
+ const fresh = builds.filter((b) => isBuildSince(b, sinceIso));
18334
+ const maxLogBytes = this.config.maxLogBytes ?? DEFAULT_MAX_LOG_BYTES;
18335
+ for (const build of fresh) {
18336
+ build.logsText = await fetchBuildLogs(build.logFileUrls, maxLogBytes, this.fetchImpl);
18337
+ }
18338
+ return mapBuildsToSignals(fresh, serviceName);
18339
+ }
18340
+ };
18341
+ function createEasConnector(graph, config, fetchImpl) {
18342
+ return {
18343
+ connector: new EasConnector(config, fetchImpl),
18344
+ resolveTarget: createEasResolveTarget(graph)
18345
+ };
18346
+ }
18347
+
17544
18348
  // src/connectors/registry.ts
17545
18349
  var CLOUDFLARE_API_BASE_URL = "https://api.cloudflare.com/client/v4";
17546
18350
  async function authProbe(input) {
@@ -17828,6 +18632,41 @@ var PROVIDER_DISPATCH = {
17828
18632
  ...fetchImpl ? { fetchImpl } : {}
17829
18633
  });
17830
18634
  }
18635
+ },
18636
+ eas: {
18637
+ provider: "eas",
18638
+ // The secret is a single robot-user EXPO_TOKEN; a single-string credential
18639
+ // maps to `token`. `appId` is non-secret config (connector-config.md §7.1).
18640
+ primaryCredentialKey: "token",
18641
+ requiredCredentialFields: ["token"],
18642
+ requiredOptionFields: ["appId"],
18643
+ build(graph, options) {
18644
+ return createEasConnector(graph, options);
18645
+ },
18646
+ // Runs the connector's real `builds` query at limit 1 — the exact read poll()
18647
+ // performs, minus the pages — so the probe checks both that the EXPO_TOKEN
18648
+ // authenticates and that this app id is reachable, the same probe-the-real-
18649
+ // query discipline Railway and Cloud Run use over a trivial `{ __typename }`.
18650
+ // A bad or wrong-scoped token comes back as an Expo GraphQL error, which
18651
+ // `fetchErroredBuilds` throws on, so it fails honestly here rather than
18652
+ // silently at the first poll.
18653
+ async validate({ credentials, options, fetchImpl }) {
18654
+ const cfg = options;
18655
+ const appId = String(cfg.appId ?? "");
18656
+ if (!appId) return { ok: false, reason: "eas: appId is required to validate" };
18657
+ const probeConfig = {
18658
+ appId,
18659
+ pageSize: 1,
18660
+ maxPages: 1,
18661
+ ...cfg.apiUrl ? { apiUrl: cfg.apiUrl } : {}
18662
+ };
18663
+ try {
18664
+ await fetchErroredBuilds(String(credentials.token ?? ""), probeConfig, fetchImpl);
18665
+ return { ok: true };
18666
+ } catch (err) {
18667
+ return { ok: false, reason: `eas auth check failed: ${err.message}` };
18668
+ }
18669
+ }
17831
18670
  }
17832
18671
  };
17833
18672
  function vercelCredsFrom(credentials) {
@@ -18040,7 +18879,11 @@ async function startConnectorPolling(input) {
18040
18879
  const stopFns = all.map(
18041
18880
  (registration) => startConnectorPollLoop(
18042
18881
  registration.connector,
18043
- { projectDir: input.projectDir, credentials: registration.credentials },
18882
+ {
18883
+ projectDir: input.projectDir,
18884
+ credentials: registration.credentials,
18885
+ ...input.errorsPath ? { errorsPath: input.errorsPath } : {}
18886
+ },
18044
18887
  input.graph,
18045
18888
  registration.resolveTarget,
18046
18889
  { intervalMs: registration.intervalMs, connectorId: registration.id }
@@ -18258,11 +19101,11 @@ function registerRoutes(scope, ctx) {
18258
19101
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
18259
19102
  const parsed = [];
18260
19103
  for (const c of candidates) {
18261
- const r = import_types81.DivergenceTypeSchema.safeParse(c);
19104
+ const r = import_types86.DivergenceTypeSchema.safeParse(c);
18262
19105
  if (!r.success) {
18263
19106
  return reply.code(400).send({
18264
19107
  error: `unknown divergence type "${c}"`,
18265
- allowed: import_types81.DivergenceTypeSchema.options
19108
+ allowed: import_types86.DivergenceTypeSchema.options
18266
19109
  });
18267
19110
  }
18268
19111
  parsed.push(r.data);
@@ -18369,10 +19212,15 @@ function registerRoutes(scope, ctx) {
18369
19212
  }
18370
19213
  const reg = built.registration;
18371
19214
  const at = (/* @__PURE__ */ new Date()).toISOString();
19215
+ const incidentsPath = errorsPathFor(proj);
18372
19216
  try {
18373
19217
  const result = await ctx.runPoll(
18374
19218
  reg.connector,
18375
- { projectDir: proj.scanPath ?? "", credentials: reg.credentials },
19219
+ {
19220
+ projectDir: proj.scanPath ?? "",
19221
+ credentials: reg.credentials,
19222
+ ...incidentsPath ? { errorsPath: incidentsPath } : {}
19223
+ },
18376
19224
  proj.graph,
18377
19225
  reg.resolveTarget
18378
19226
  );
@@ -18442,6 +19290,34 @@ function registerRoutes(scope, ctx) {
18442
19290
  }
18443
19291
  return getBlastRadius(proj.graph, nodeId, depth);
18444
19292
  });
19293
+ scope.get("/graph/expand/:nodeId", async (req, reply) => {
19294
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
19295
+ if (!proj) return;
19296
+ const { nodeId } = req.params;
19297
+ if (!proj.graph.hasNode(nodeId)) {
19298
+ return reply.code(404).send({ error: "node not found", id: nodeId });
19299
+ }
19300
+ const direction = req.query.direction;
19301
+ if (direction !== "up" && direction !== "down") {
19302
+ return reply.code(400).send({ error: 'direction must be "up" or "down"' });
19303
+ }
19304
+ const epath = errorsPathFor(proj);
19305
+ const incidents = epath ? await readErrorEvents(epath) : [];
19306
+ return expandNode(proj.graph, nodeId, direction, incidents);
19307
+ });
19308
+ scope.get("/graph/relate", async (req, reply) => {
19309
+ const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
19310
+ if (!proj) return;
19311
+ const { a, b } = req.query;
19312
+ if (!a || !b) {
19313
+ return reply.code(400).send({ error: "both a and b query params are required" });
19314
+ }
19315
+ const maxDepth = req.query.maxDepth ? Number(req.query.maxDepth) : void 0;
19316
+ if (maxDepth !== void 0 && (!Number.isFinite(maxDepth) || maxDepth < 1)) {
19317
+ return reply.code(400).send({ error: "maxDepth must be a positive integer" });
19318
+ }
19319
+ return relate(proj.graph, a, b, maxDepth);
19320
+ });
18445
19321
  scope.get("/search", async (req, reply) => {
18446
19322
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18447
19323
  if (!proj) return;
@@ -18571,7 +19447,7 @@ function registerRoutes(scope, ctx) {
18571
19447
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
18572
19448
  let violations = await log.readAll();
18573
19449
  if (req.query.severity) {
18574
- const sev = import_types81.PolicySeveritySchema.safeParse(req.query.severity);
19450
+ const sev = import_types86.PolicySeveritySchema.safeParse(req.query.severity);
18575
19451
  if (!sev.success) {
18576
19452
  return reply.code(400).send({
18577
19453
  error: "invalid severity",
@@ -18610,7 +19486,7 @@ function registerRoutes(scope, ctx) {
18610
19486
  scope.post("/policies/check", async (req, reply) => {
18611
19487
  const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
18612
19488
  if (!proj) return;
18613
- const parsed = import_types81.PoliciesCheckBodySchema.safeParse(req.body ?? {});
19489
+ const parsed = import_types86.PoliciesCheckBodySchema.safeParse(req.body ?? {});
18614
19490
  if (!parsed.success) {
18615
19491
  return reply.code(400).send({
18616
19492
  error: "invalid /policies/check body",
@@ -18943,7 +19819,7 @@ var import_node_fs34 = require("fs");
18943
19819
  var import_node_path68 = __toESM(require("path"), 1);
18944
19820
 
18945
19821
  // src/daemon.ts
18946
- var import_types82 = require("@neat.is/types");
19822
+ var import_types87 = require("@neat.is/types");
18947
19823
  function daemonJsonPath(scanPath) {
18948
19824
  return import_node_path69.default.join(scanPath, "neat-out", "daemon.json");
18949
19825
  }
@@ -19601,6 +20477,9 @@ async function startWatch(graph, opts) {
19601
20477
  project: projectName,
19602
20478
  graph,
19603
20479
  projectDir: opts.scanPath,
20480
+ // Incident ledger for an incident-emitting connector (ADR-185), same path
20481
+ // `neat watch`'s own error-span writer already uses.
20482
+ errorsPath: opts.errorsPath,
19604
20483
  ...opts.neatHome ? { home: opts.neatHome } : {},
19605
20484
  onSkip: (skipped, reason) => console.warn(
19606
20485
  `neat watch: connector "${skipped.id}" (${skipped.provider}) skipped for project "${projectName}" \u2014 ${reason}`
@@ -22229,14 +23108,519 @@ async function apply3(installPlan) {
22229
23108
  await import_node_fs41.promises.writeFile(generated.file, generated.contents, "utf8");
22230
23109
  writtenFiles.push(generated.file);
22231
23110
  }
22232
- return { serviceDir: installPlan.serviceDir, outcome: writtenFiles.length ? "instrumented" : "already-instrumented", writtenFiles };
23111
+ const wroteManifest = writtenFiles.some((f) => f.split(/[\\/]/).pop() === "go.mod");
23112
+ return {
23113
+ serviceDir: installPlan.serviceDir,
23114
+ outcome: writtenFiles.length ? "instrumented" : "already-instrumented",
23115
+ writtenFiles,
23116
+ ...wroteManifest ? { followUpInstall: "go mod download" } : {}
23117
+ };
22233
23118
  }
22234
23119
  var goInstaller = { name: "go", detect: detect3, plan: plan3, apply: apply3 };
22235
23120
 
23121
+ // src/installers/ruby.ts
23122
+ init_cjs_shims();
23123
+ var import_node_fs42 = require("fs");
23124
+ var import_node_path76 = __toESM(require("path"), 1);
23125
+ var RUBY_MARKERS = [
23126
+ "Gemfile",
23127
+ "Gemfile.lock"
23128
+ ];
23129
+ var RUBY_GEMS = [
23130
+ { name: "opentelemetry-sdk", version: "~> 1.5" },
23131
+ { name: "opentelemetry-exporter-otlp", version: "~> 0.29" },
23132
+ { name: "opentelemetry-instrumentation-all", version: "~> 0.62" }
23133
+ ];
23134
+ var NEAT_OTEL_STAMP2 = "neat-otel-init v1";
23135
+ var INITIALIZER_REL = import_node_path76.default.join("config", "initializers", "neat_otel.rb");
23136
+ function neatOtelRb(opts = {}) {
23137
+ const service = opts.project ?? "ruby-service";
23138
+ const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
23139
+ return `# ${NEAT_OTEL_STAMP2} \u2014 generated by NEAT. Safe to re-generate; do not edit.
23140
+ # Rails auto-loads this at boot (config/initializers/*). It points the
23141
+ # OpenTelemetry SDK at your NEAT daemon, enables the Ruby auto-instrumentation
23142
+ # set, and installs a span processor that stamps code.file.path /
23143
+ # code.line.number / code.function.name on the CLIENT/PRODUCER spans your app
23144
+ # issues, so NEAT fuses each runtime span onto the source file that made the
23145
+ # call (docs/contracts/file-awareness.md). Absolute paths are emitted here;
23146
+ # ingest anchors them against the service root. If the OpenTelemetry gems are
23147
+ # not installed this file degrades to a no-op rather than breaking boot.
23148
+
23149
+ begin
23150
+ require 'opentelemetry/sdk'
23151
+ require 'opentelemetry/exporter/otlp'
23152
+ require 'opentelemetry/instrumentation/all'
23153
+ _neat_otel_loaded = true
23154
+ rescue LoadError
23155
+ _neat_otel_loaded = false
23156
+ end
23157
+
23158
+ if _neat_otel_loaded && ENV['NEAT_CALLSITE_DISABLED'] != '1'
23159
+ # Walk the Ruby call stack to the first application frame and stamp the stable
23160
+ # OTel source attributes on CLIENT/PRODUCER spans. SERVER spans are created
23161
+ # before the handler runs, so they stay route/service-grained, honestly.
23162
+ class NeatCallSiteSpanProcessor
23163
+ def initialize(root)
23164
+ @root = root.to_s.end_with?(File::SEPARATOR) ? root.to_s : root.to_s + File::SEPARATOR
23165
+ end
23166
+
23167
+ def on_start(span, _parent_context)
23168
+ kind = span.kind
23169
+ return unless kind == OpenTelemetry::Trace::SpanKind::CLIENT ||
23170
+ kind == OpenTelemetry::Trace::SpanKind::PRODUCER
23171
+ caller_locations(1).each do |loc|
23172
+ file = loc.absolute_path || loc.path
23173
+ next if file.nil?
23174
+ next unless file.start_with?(@root)
23175
+ next if file.include?('/vendor/') || file.include?('/.bundle/')
23176
+ next if file.end_with?('neat_otel.rb')
23177
+ span.set_attribute('code.file.path', file)
23178
+ span.set_attribute('code.line.number', loc.lineno)
23179
+ span.set_attribute('code.function.name', loc.label.to_s)
23180
+ break
23181
+ end
23182
+ rescue StandardError
23183
+ # never break the host application
23184
+ end
23185
+
23186
+ def on_finish(_span); end
23187
+
23188
+ def force_flush(timeout: nil)
23189
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
23190
+ end
23191
+
23192
+ def shutdown(timeout: nil)
23193
+ OpenTelemetry::SDK::Trace::Export::SUCCESS
23194
+ end
23195
+ end
23196
+
23197
+ _neat_root = defined?(Rails) ? Rails.root.to_s : Dir.pwd
23198
+ _neat_service = ENV.fetch('OTEL_SERVICE_NAME', '${service}')
23199
+ _neat_endpoint = ENV.fetch('OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', '${endpoint2}')
23200
+
23201
+ OpenTelemetry::SDK.configure do |c|
23202
+ c.service_name = _neat_service
23203
+ c.use_all
23204
+ c.add_span_processor(
23205
+ OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
23206
+ OpenTelemetry::Exporter::OTLP::Exporter.new(endpoint: _neat_endpoint)
23207
+ )
23208
+ )
23209
+ c.add_span_processor(NeatCallSiteSpanProcessor.new(_neat_root))
23210
+ end
23211
+ end
23212
+ `;
23213
+ }
23214
+ async function exists6(p) {
23215
+ try {
23216
+ await import_node_fs42.promises.stat(p);
23217
+ return true;
23218
+ } catch {
23219
+ return false;
23220
+ }
23221
+ }
23222
+ async function detect4(serviceDir) {
23223
+ for (const marker of RUBY_MARKERS) {
23224
+ if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
23225
+ }
23226
+ return false;
23227
+ }
23228
+ async function isRailsApp(serviceDir, gemfile) {
23229
+ if (gemfile && /^\s*gem\s+['"]rails['"]/m.test(gemfile)) return true;
23230
+ for (const marker of ["config/application.rb", "config/environment.rb", "bin/rails"]) {
23231
+ if (await exists6(import_node_path76.default.join(serviceDir, marker))) return true;
23232
+ }
23233
+ return false;
23234
+ }
23235
+ function gemPresent(gemfile, name) {
23236
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
23237
+ return new RegExp(`^\\s*gem\\s+['"]${escaped}['"]`, "m").test(gemfile);
23238
+ }
23239
+ async function readGemfile(serviceDir) {
23240
+ const file = import_node_path76.default.join(serviceDir, "Gemfile");
23241
+ if (!await exists6(file)) return null;
23242
+ return { file, body: await import_node_fs42.promises.readFile(file, "utf8") };
23243
+ }
23244
+ async function plan4(serviceDir, opts) {
23245
+ const empty = {
23246
+ language: "ruby",
23247
+ serviceDir,
23248
+ dependencyEdits: [],
23249
+ entrypointEdits: [],
23250
+ envEdits: []
23251
+ };
23252
+ const gemfile = await readGemfile(serviceDir);
23253
+ const dependencyEdits = [];
23254
+ if (gemfile) {
23255
+ for (const gem of RUBY_GEMS) {
23256
+ if (!gemPresent(gemfile.body, gem.name)) {
23257
+ dependencyEdits.push({ file: gemfile.file, kind: "add", name: gem.name, version: gem.version });
23258
+ }
23259
+ }
23260
+ }
23261
+ const rails = await isRailsApp(serviceDir, gemfile?.body ?? null);
23262
+ const initializer = import_node_path76.default.join(serviceDir, INITIALIZER_REL);
23263
+ const generatedFiles = [];
23264
+ if (rails && !await exists6(initializer)) {
23265
+ generatedFiles.push({
23266
+ file: initializer,
23267
+ contents: neatOtelRb({ project: opts?.project }),
23268
+ skipIfExists: true
23269
+ });
23270
+ }
23271
+ if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
23272
+ return empty;
23273
+ }
23274
+ const envEdits = [
23275
+ { file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
23276
+ ];
23277
+ return {
23278
+ language: "ruby",
23279
+ serviceDir,
23280
+ dependencyEdits,
23281
+ entrypointEdits: [],
23282
+ envEdits,
23283
+ ...generatedFiles.length > 0 ? { generatedFiles } : {}
23284
+ };
23285
+ }
23286
+ async function writeFileAtomic2(file, contents) {
23287
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
23288
+ await import_node_fs42.promises.writeFile(tmp, contents, "utf8");
23289
+ await import_node_fs42.promises.rename(tmp, file);
23290
+ }
23291
+ async function applyGemfile(file, edits, original) {
23292
+ const lines = edits.filter((e) => e.kind === "add").map((e) => `gem '${e.name}', '${e.version}'`);
23293
+ const banner = `
23294
+ # ${NEAT_OTEL_STAMP2} \u2014 OpenTelemetry gems added by NEAT
23295
+ `;
23296
+ const trailing = original.endsWith("\n") ? "" : "\n";
23297
+ await writeFileAtomic2(file, `${original}${trailing}${banner}${lines.join("\n")}
23298
+ `);
23299
+ }
23300
+ async function rollback3(serviceDir, language, originals, created) {
23301
+ const restored = [];
23302
+ for (const [file, raw] of originals.entries()) {
23303
+ try {
23304
+ await import_node_fs42.promises.writeFile(file, raw, "utf8");
23305
+ restored.push(file);
23306
+ } catch {
23307
+ }
23308
+ }
23309
+ const removed = [];
23310
+ for (const file of created) {
23311
+ try {
23312
+ await import_node_fs42.promises.rm(file, { force: true });
23313
+ removed.push(file);
23314
+ } catch {
23315
+ }
23316
+ }
23317
+ const body = [
23318
+ "# neat-rollback.patch",
23319
+ "",
23320
+ `# Generated after a partial apply failure in the ${language} installer.`,
23321
+ "# Files listed below were restored to their pre-apply contents.",
23322
+ "",
23323
+ ...restored.map((f) => `restored: ${f}`),
23324
+ ...removed.map((f) => `removed: ${f}`),
23325
+ ""
23326
+ ];
23327
+ await import_node_fs42.promises.writeFile(import_node_path76.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
23328
+ }
23329
+ async function apply4(installPlan) {
23330
+ const { serviceDir } = installPlan;
23331
+ const generatedFiles = installPlan.generatedFiles ?? [];
23332
+ const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
23333
+ if (manifests.size === 0 && generatedFiles.length === 0) {
23334
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [] };
23335
+ }
23336
+ const originals = /* @__PURE__ */ new Map();
23337
+ for (const file of manifests) {
23338
+ try {
23339
+ originals.set(file, await import_node_fs42.promises.readFile(file, "utf8"));
23340
+ } catch {
23341
+ }
23342
+ }
23343
+ const writtenFiles = [];
23344
+ const created = [];
23345
+ try {
23346
+ for (const gf of generatedFiles) {
23347
+ if (await exists6(gf.file)) continue;
23348
+ await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(gf.file), { recursive: true });
23349
+ await writeFileAtomic2(gf.file, gf.contents);
23350
+ writtenFiles.push(gf.file);
23351
+ created.push(gf.file);
23352
+ }
23353
+ for (const file of manifests) {
23354
+ const raw = originals.get(file);
23355
+ if (raw === void 0) throw new Error(`ruby installer: cannot read ${file} during apply`);
23356
+ const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
23357
+ if (edits.length > 0) {
23358
+ await applyGemfile(file, edits, raw);
23359
+ writtenFiles.push(file);
23360
+ }
23361
+ }
23362
+ } catch (err) {
23363
+ await rollback3(serviceDir, installPlan.language, originals, created);
23364
+ throw err;
23365
+ }
23366
+ const wroteManifest = writtenFiles.some((f) => import_node_path76.default.basename(f) === "Gemfile");
23367
+ return {
23368
+ serviceDir,
23369
+ outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
23370
+ writtenFiles,
23371
+ ...wroteManifest ? { followUpInstall: "bundle install" } : {}
23372
+ };
23373
+ }
23374
+ var rubyInstaller = { name: "ruby", detect: detect4, plan: plan4, apply: apply4 };
23375
+
23376
+ // src/installers/php.ts
23377
+ init_cjs_shims();
23378
+ var import_node_fs43 = require("fs");
23379
+ var import_node_path77 = __toESM(require("path"), 1);
23380
+ var PHP_MARKERS = [
23381
+ "composer.json",
23382
+ "composer.lock"
23383
+ ];
23384
+ var NEAT_OTEL_FILENAME2 = "neat_otel.php";
23385
+ var NEAT_OTEL_STAMP3 = "neat-otel-init v1";
23386
+ var LARAVEL_PACKAGE = "open-telemetry/opentelemetry-auto-laravel";
23387
+ var PHP_PACKAGES = [
23388
+ { name: "open-telemetry/sdk", version: "^1.0" },
23389
+ { name: "open-telemetry/exporter-otlp", version: "^1.0" },
23390
+ { name: "php-http/guzzle7-adapter", version: "^1.0" },
23391
+ { name: LARAVEL_PACKAGE, version: "^0.1" }
23392
+ ];
23393
+ var PHP_PECL_CAVEAT = "PHP auto-instrumentation requires the `opentelemetry` PECL extension (`pecl install opentelemetry`, then `extension=opentelemetry.so` in php.ini). NEAT cannot install a PECL extension via composer \u2014 until it is loaded no spans are produced. See neat_otel.php and ADR-186.";
23394
+ function neatOtelPhp(opts = {}) {
23395
+ const service = opts.project ?? "php-service";
23396
+ const endpoint2 = opts.project ? `http://localhost:4318/projects/${opts.project}/v1/traces` : "http://localhost:4318/v1/traces";
23397
+ return `<?php
23398
+ // ${NEAT_OTEL_STAMP3} \u2014 generated by NEAT. Safe to re-generate; do not edit.
23399
+ //
23400
+ // REQUIRED SYSTEM STEP \u2014 NEAT CANNOT DO THIS FOR YOU:
23401
+ // PHP OpenTelemetry auto-instrumentation needs the \`opentelemetry\` PECL
23402
+ // extension, a system-level install composer cannot provide:
23403
+ // pecl install opentelemetry
23404
+ // then enable it in your php.ini:
23405
+ // extension=opentelemetry.so
23406
+ // Verify with \`php -m | grep opentelemetry\`. Until the extension is loaded
23407
+ // the Laravel auto-instrumentation hooks never fire and no spans are emitted.
23408
+ //
23409
+ // Wire this file so it runs before the framework boots \u2014 either set
23410
+ // auto_prepend_file = /absolute/path/to/neat_otel.php
23411
+ // in php.ini / .user.ini, or require it at the very top of public/index.php and
23412
+ // artisan. It points the exporter at your NEAT daemon and turns on the SDK
23413
+ // autoloader; the auto-laravel instrumentation then produces route, DB, cache,
23414
+ // and queue spans that fuse onto your extracted routes and Eloquent tables.
23415
+ //
23416
+ // FILE-GRAIN (code.file.path call-site attribution) is a documented follow-up
23417
+ // for PHP \u2014 see ADR-186. Route, table, and service grain land now.
23418
+
23419
+ declare(strict_types=1);
23420
+
23421
+ // Degrade to a no-op when the extension isn't present, so a bare app still
23422
+ // boots (never break the host application).
23423
+ if (!extension_loaded('opentelemetry')) {
23424
+ return;
23425
+ }
23426
+
23427
+ // Point the exporter at NEAT unless the operator already set these. The
23428
+ // endpoint is NEAT's project-scoped traces path (ADR-183).
23429
+ $neat_defaults = [
23430
+ 'OTEL_PHP_AUTOLOAD_ENABLED' => 'true',
23431
+ 'OTEL_SERVICE_NAME' => '${service}',
23432
+ 'OTEL_TRACES_EXPORTER' => 'otlp',
23433
+ 'OTEL_EXPORTER_OTLP_PROTOCOL' => 'http/json',
23434
+ 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT' => '${endpoint2}',
23435
+ 'OTEL_PROPAGATORS' => 'baggage,tracecontext',
23436
+ ];
23437
+ foreach ($neat_defaults as $neat_key => $neat_value) {
23438
+ if (getenv($neat_key) === false && !isset($_SERVER[$neat_key]) && !isset($_ENV[$neat_key])) {
23439
+ putenv($neat_key . '=' . $neat_value);
23440
+ $_SERVER[$neat_key] = $neat_value;
23441
+ $_ENV[$neat_key] = $neat_value;
23442
+ }
23443
+ }
23444
+ `;
23445
+ }
23446
+ async function exists7(p) {
23447
+ try {
23448
+ await import_node_fs43.promises.stat(p);
23449
+ return true;
23450
+ } catch {
23451
+ return false;
23452
+ }
23453
+ }
23454
+ async function detect5(serviceDir) {
23455
+ for (const marker of PHP_MARKERS) {
23456
+ if (await exists7(import_node_path77.default.join(serviceDir, marker))) return true;
23457
+ }
23458
+ return false;
23459
+ }
23460
+ function readComposerObject(body) {
23461
+ let parsed = null;
23462
+ try {
23463
+ parsed = JSON.parse(body);
23464
+ } catch {
23465
+ parsed = null;
23466
+ }
23467
+ const obj = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
23468
+ const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
23469
+ const requireDev = obj["require-dev"] && typeof obj["require-dev"] === "object" && !Array.isArray(obj["require-dev"]) ? obj["require-dev"] : {};
23470
+ return { require: require2, requireDev };
23471
+ }
23472
+ async function plan5(serviceDir, opts) {
23473
+ const empty = {
23474
+ language: "php",
23475
+ serviceDir,
23476
+ dependencyEdits: [],
23477
+ entrypointEdits: [],
23478
+ envEdits: []
23479
+ };
23480
+ const composerPath = import_node_path77.default.join(serviceDir, "composer.json");
23481
+ const hasComposer = await exists7(composerPath);
23482
+ const dependencyEdits = [];
23483
+ if (hasComposer) {
23484
+ const body = await import_node_fs43.promises.readFile(composerPath, "utf8");
23485
+ const { require: require2, requireDev } = readComposerObject(body);
23486
+ const laravel = "laravel/framework" in require2 || "laravel/framework" in requireDev || await exists7(import_node_path77.default.join(serviceDir, "artisan"));
23487
+ const wanted = laravel ? PHP_PACKAGES : PHP_PACKAGES.filter((p) => p.name !== LARAVEL_PACKAGE);
23488
+ for (const pkg of wanted) {
23489
+ if (!(pkg.name in require2)) {
23490
+ dependencyEdits.push({ file: composerPath, kind: "add", name: pkg.name, version: pkg.version });
23491
+ }
23492
+ }
23493
+ }
23494
+ const bootstrap = import_node_path77.default.join(serviceDir, NEAT_OTEL_FILENAME2);
23495
+ const generatedFiles = [];
23496
+ if (hasComposer && !await exists7(bootstrap)) {
23497
+ generatedFiles.push({ file: bootstrap, contents: neatOtelPhp({ project: opts?.project }), skipIfExists: true });
23498
+ }
23499
+ if (dependencyEdits.length === 0 && generatedFiles.length === 0) {
23500
+ return empty;
23501
+ }
23502
+ const envEdits = [
23503
+ { file: null, key: "OTEL_PHP_AUTOLOAD_ENABLED", value: "true" },
23504
+ { file: null, key: "OTEL_EXPORTER_OTLP_ENDPOINT", value: "http://localhost:4318" }
23505
+ ];
23506
+ return {
23507
+ language: "php",
23508
+ serviceDir,
23509
+ dependencyEdits,
23510
+ entrypointEdits: [],
23511
+ envEdits,
23512
+ ...generatedFiles.length > 0 ? { generatedFiles } : {}
23513
+ };
23514
+ }
23515
+ async function writeFileAtomic3(file, contents) {
23516
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
23517
+ await import_node_fs43.promises.writeFile(tmp, contents, "utf8");
23518
+ await import_node_fs43.promises.rename(tmp, file);
23519
+ }
23520
+ async function applyComposerJson(file, edits, original) {
23521
+ let parsed;
23522
+ try {
23523
+ parsed = JSON.parse(original);
23524
+ } catch {
23525
+ throw new Error(`php installer: composer.json at ${file} is not valid JSON`);
23526
+ }
23527
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
23528
+ throw new Error(`php installer: composer.json at ${file} is not a JSON object`);
23529
+ }
23530
+ const obj = parsed;
23531
+ const require2 = obj.require && typeof obj.require === "object" && !Array.isArray(obj.require) ? obj.require : {};
23532
+ for (const e of edits) {
23533
+ if (e.kind !== "add") continue;
23534
+ if (!(e.name in require2)) require2[e.name] = e.version;
23535
+ }
23536
+ obj.require = require2;
23537
+ await writeFileAtomic3(file, JSON.stringify(obj, null, 2) + "\n");
23538
+ }
23539
+ async function rollback4(serviceDir, language, originals, created) {
23540
+ const restored = [];
23541
+ for (const [file, raw] of originals.entries()) {
23542
+ try {
23543
+ await import_node_fs43.promises.writeFile(file, raw, "utf8");
23544
+ restored.push(file);
23545
+ } catch {
23546
+ }
23547
+ }
23548
+ const removed = [];
23549
+ for (const file of created) {
23550
+ try {
23551
+ await import_node_fs43.promises.rm(file, { force: true });
23552
+ removed.push(file);
23553
+ } catch {
23554
+ }
23555
+ }
23556
+ const body = [
23557
+ "# neat-rollback.patch",
23558
+ "",
23559
+ `# Generated after a partial apply failure in the ${language} installer.`,
23560
+ "# Files listed below were restored to their pre-apply contents.",
23561
+ "",
23562
+ ...restored.map((f) => `restored: ${f}`),
23563
+ ...removed.map((f) => `removed: ${f}`),
23564
+ ""
23565
+ ];
23566
+ await import_node_fs43.promises.writeFile(import_node_path77.default.join(serviceDir, "neat-rollback.patch"), body.join("\n"), "utf8");
23567
+ }
23568
+ async function apply5(installPlan) {
23569
+ const { serviceDir } = installPlan;
23570
+ const generatedFiles = installPlan.generatedFiles ?? [];
23571
+ const manifests = new Set(installPlan.dependencyEdits.map((e) => e.file));
23572
+ if (manifests.size === 0 && generatedFiles.length === 0) {
23573
+ return { serviceDir, outcome: "already-instrumented", writtenFiles: [], reason: PHP_PECL_CAVEAT };
23574
+ }
23575
+ const originals = /* @__PURE__ */ new Map();
23576
+ for (const file of manifests) {
23577
+ try {
23578
+ originals.set(file, await import_node_fs43.promises.readFile(file, "utf8"));
23579
+ } catch {
23580
+ }
23581
+ }
23582
+ const writtenFiles = [];
23583
+ const created = [];
23584
+ try {
23585
+ for (const gf of generatedFiles) {
23586
+ if (await exists7(gf.file)) continue;
23587
+ await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(gf.file), { recursive: true });
23588
+ await writeFileAtomic3(gf.file, gf.contents);
23589
+ writtenFiles.push(gf.file);
23590
+ created.push(gf.file);
23591
+ }
23592
+ for (const file of manifests) {
23593
+ const raw = originals.get(file);
23594
+ if (raw === void 0) throw new Error(`php installer: cannot read ${file} during apply`);
23595
+ const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
23596
+ if (edits.length > 0) {
23597
+ await applyComposerJson(file, edits, raw);
23598
+ writtenFiles.push(file);
23599
+ }
23600
+ }
23601
+ } catch (err) {
23602
+ await rollback4(serviceDir, installPlan.language, originals, created);
23603
+ throw err;
23604
+ }
23605
+ if (writtenFiles.length > 0) {
23606
+ console.warn(`neat: PHP instrumentation staged in ${import_node_path77.default.basename(serviceDir)}, but a system step remains:
23607
+ ${PHP_PECL_CAVEAT}`);
23608
+ }
23609
+ const wroteManifest = writtenFiles.some((f) => import_node_path77.default.basename(f) === "composer.json");
23610
+ return {
23611
+ serviceDir,
23612
+ outcome: writtenFiles.length > 0 ? "instrumented" : "already-instrumented",
23613
+ writtenFiles,
23614
+ reason: PHP_PECL_CAVEAT,
23615
+ ...wroteManifest ? { followUpInstall: "composer install" } : {}
23616
+ };
23617
+ }
23618
+ var phpInstaller = { name: "php", detect: detect5, plan: plan5, apply: apply5 };
23619
+
22236
23620
  // src/installers/shared.ts
22237
23621
  init_cjs_shims();
22238
- function isEmptyPlan(plan4) {
22239
- return plan4.dependencyEdits.length === 0 && plan4.entrypointEdits.length === 0 && plan4.envEdits.length === 0 && (plan4.generatedFiles?.length ?? 0) === 0 && plan4.nextConfigEdit === void 0;
23622
+ function isEmptyPlan(plan6) {
23623
+ return plan6.dependencyEdits.length === 0 && plan6.entrypointEdits.length === 0 && plan6.envEdits.length === 0 && (plan6.generatedFiles?.length ?? 0) === 0 && plan6.nextConfigEdit === void 0;
22240
23624
  }
22241
23625
 
22242
23626
  // src/installers/index.ts
@@ -22248,9 +23632,16 @@ var FORBIDDEN_LOCKFILES = /* @__PURE__ */ new Set([
22248
23632
  "Pipfile.lock",
22249
23633
  "Gemfile.lock",
22250
23634
  "Cargo.lock",
22251
- "go.sum"
23635
+ "go.sum",
23636
+ "composer.lock"
22252
23637
  ]);
22253
- var INSTALLERS = [javascriptInstaller, pythonInstaller, goInstaller];
23638
+ var INSTALLERS = [
23639
+ javascriptInstaller,
23640
+ pythonInstaller,
23641
+ goInstaller,
23642
+ rubyInstaller,
23643
+ phpInstaller
23644
+ ];
22254
23645
  async function pickInstaller(serviceDir) {
22255
23646
  for (const inst of INSTALLERS) {
22256
23647
  if (await inst.detect(serviceDir)) return inst;
@@ -22265,7 +23656,7 @@ function renderPatch(sections) {
22265
23656
  "No SDK installers matched the discovered services. Two reasons this",
22266
23657
  "normally happens:",
22267
23658
  " - the project uses a language NEAT does not yet instrument",
22268
- " (Java / Ruby / .NET / Rust are out of MVP scope per ADR-047);",
23659
+ " (Java / .NET / Rust are out of scope per ADR-047);",
22269
23660
  " - the SDK is already installed, so the installer returned an empty",
22270
23661
  " plan.",
22271
23662
  "",
@@ -22275,22 +23666,22 @@ function renderPatch(sections) {
22275
23666
  }
22276
23667
  const lines = ["# neat install plan", ""];
22277
23668
  for (const section of sections) {
22278
- const { installer, plan: plan4 } = section;
22279
- lines.push(`## ${installer} (${plan4.language}) \u2014 ${plan4.serviceDir}`);
23669
+ const { installer, plan: plan6 } = section;
23670
+ lines.push(`## ${installer} (${plan6.language}) \u2014 ${plan6.serviceDir}`);
22280
23671
  lines.push("");
22281
- if (plan4.libOnly) {
23672
+ if (plan6.libOnly) {
22282
23673
  lines.push("### skipped \u2014 no resolvable entry point (lib-only)");
22283
23674
  lines.push("");
22284
23675
  continue;
22285
23676
  }
22286
- if (plan4.entryFile) {
22287
- lines.push(`entry: ${plan4.entryFile}`);
23677
+ if (plan6.entryFile) {
23678
+ lines.push(`entry: ${plan6.entryFile}`);
22288
23679
  lines.push("");
22289
23680
  }
22290
- if (plan4.dependencyEdits.length > 0) {
23681
+ if (plan6.dependencyEdits.length > 0) {
22291
23682
  lines.push("### dependencies");
22292
23683
  const byFile = /* @__PURE__ */ new Map();
22293
- for (const dep of plan4.dependencyEdits) {
23684
+ for (const dep of plan6.dependencyEdits) {
22294
23685
  const base = dep.file.split(/[\\/]/).pop() ?? dep.file;
22295
23686
  if (FORBIDDEN_LOCKFILES.has(base)) {
22296
23687
  throw new Error(
@@ -22309,9 +23700,9 @@ function renderPatch(sections) {
22309
23700
  }
22310
23701
  lines.push("");
22311
23702
  }
22312
- if (plan4.generatedFiles && plan4.generatedFiles.length > 0) {
23703
+ if (plan6.generatedFiles && plan6.generatedFiles.length > 0) {
22313
23704
  lines.push("### generated files");
22314
- for (const gen of plan4.generatedFiles) {
23705
+ for (const gen of plan6.generatedFiles) {
22315
23706
  lines.push(`--- (new file) ${gen.file}`);
22316
23707
  for (const ln of gen.contents.split(/\r?\n/)) {
22317
23708
  lines.push(`+ ${ln}`);
@@ -22319,26 +23710,26 @@ function renderPatch(sections) {
22319
23710
  }
22320
23711
  lines.push("");
22321
23712
  }
22322
- if (plan4.entrypointEdits.length > 0) {
23713
+ if (plan6.entrypointEdits.length > 0) {
22323
23714
  lines.push("### entry-point injection");
22324
- for (const e of plan4.entrypointEdits) {
23715
+ for (const e of plan6.entrypointEdits) {
22325
23716
  lines.push(`--- ${e.file}`);
22326
23717
  lines.push(`+ ${e.after}`);
22327
23718
  lines.push(` ${e.before}`);
22328
23719
  }
22329
23720
  lines.push("");
22330
23721
  }
22331
- if (plan4.envEdits.length > 0) {
23722
+ if (plan6.envEdits.length > 0) {
22332
23723
  lines.push("### env (written to <package-dir>/.env.neat)");
22333
- for (const env of plan4.envEdits) {
23724
+ for (const env of plan6.envEdits) {
22334
23725
  lines.push(`- ${env.key}=${env.value}`);
22335
23726
  }
22336
23727
  lines.push("");
22337
23728
  }
22338
- if (plan4.nextConfigEdit) {
23729
+ if (plan6.nextConfigEdit) {
22339
23730
  lines.push("### next.config (framework flag)");
22340
- lines.push(`--- ${plan4.nextConfigEdit.file}`);
22341
- lines.push(`+ experimental: { instrumentationHook: true }, // ${plan4.nextConfigEdit.reason}`);
23731
+ lines.push(`--- ${plan6.nextConfigEdit.file}`);
23732
+ lines.push(`+ experimental: { instrumentationHook: true }, // ${plan6.nextConfigEdit.reason}`);
22342
23733
  lines.push("");
22343
23734
  }
22344
23735
  }
@@ -22347,10 +23738,10 @@ function renderPatch(sections) {
22347
23738
 
22348
23739
  // src/orchestrator.ts
22349
23740
  init_cjs_shims();
22350
- var import_node_fs42 = require("fs");
23741
+ var import_node_fs44 = require("fs");
22351
23742
  var import_node_http = __toESM(require("http"), 1);
22352
23743
  var import_node_net = __toESM(require("net"), 1);
22353
- var import_node_path76 = __toESM(require("path"), 1);
23744
+ var import_node_path78 = __toESM(require("path"), 1);
22354
23745
  var import_node_url4 = require("url");
22355
23746
  var import_node_child_process3 = require("child_process");
22356
23747
  var import_node_readline = __toESM(require("readline"), 1);
@@ -22360,7 +23751,7 @@ async function extractAndPersist(opts) {
22360
23751
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
22361
23752
  resetGraph(graphKey);
22362
23753
  const graph = getGraph(graphKey);
22363
- const projectPaths = pathsForProject(graphKey, import_node_path76.default.join(opts.scanPath, "neat-out"));
23754
+ const projectPaths = pathsForProject(graphKey, import_node_path78.default.join(opts.scanPath, "neat-out"));
22364
23755
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
22365
23756
  errorsPath: projectPaths.errorsPath
22366
23757
  });
@@ -22391,28 +23782,34 @@ async function applyInstallersOver(services, project, options = {}) {
22391
23782
  let cloudflareWorkers = 0;
22392
23783
  let electron = 0;
22393
23784
  const installPlans = /* @__PURE__ */ new Map();
23785
+ const dependencyInstructions = /* @__PURE__ */ new Map();
22394
23786
  for (const svc of services) {
22395
23787
  const installer = await pickInstaller(svc.dir);
22396
23788
  if (!installer) continue;
22397
- const plan4 = await installer.plan(svc.dir, { project });
22398
- if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) {
23789
+ const plan6 = await installer.plan(svc.dir, { project });
23790
+ if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) {
22399
23791
  already++;
22400
23792
  continue;
22401
23793
  }
22402
- const outcome = await installer.apply(plan4);
23794
+ const outcome = await installer.apply(plan6);
22403
23795
  if (outcome.outcome === "instrumented") {
22404
23796
  instrumented++;
22405
- if (plan4.dependencyEdits.length > 0) {
22406
- const cmd = await resolveManager(svc.dir);
22407
- const key = `${cmd.pm}:${cmd.cwd}`;
22408
- if (!installPlans.has(key)) installPlans.set(key, cmd);
23797
+ if (plan6.dependencyEdits.length > 0) {
23798
+ const manifest = import_node_path78.default.basename(plan6.dependencyEdits[0].file);
23799
+ if (manifest === "package.json") {
23800
+ const cmd = await resolveManager(svc.dir);
23801
+ const key = `${cmd.pm}:${cmd.cwd}`;
23802
+ if (!installPlans.has(key)) installPlans.set(key, cmd);
23803
+ } else if (outcome.followUpInstall) {
23804
+ dependencyInstructions.set(svc.dir, outcome.followUpInstall);
23805
+ }
22409
23806
  }
22410
23807
  } else if (outcome.outcome === "already-instrumented") already++;
22411
23808
  else if (outcome.outcome === "lib-only") {
22412
23809
  libOnly++;
22413
23810
  const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
22414
23811
  if (appDeps.length > 0) {
22415
- const svcName = import_node_path76.default.basename(svc.dir);
23812
+ const svcName = import_node_path78.default.basename(svc.dir);
22416
23813
  const list = appDeps.join(", ");
22417
23814
  console.warn(
22418
23815
  `neat: runtime layer won't engage for ${svcName}: no entry point found.
@@ -22425,7 +23822,7 @@ async function applyInstallersOver(services, project, options = {}) {
22425
23822
  console.log(`skipping ${svc.dir}: browser bundle; browser-OTel support lands in a future release.`);
22426
23823
  } else if (outcome.outcome === "react-native") {
22427
23824
  reactNative++;
22428
- const svcName = import_node_path76.default.basename(svc.dir);
23825
+ const svcName = import_node_path78.default.basename(svc.dir);
22429
23826
  console.log(
22430
23827
  `neat: ${svc.dir} detected as React Native / Expo
22431
23828
  The installer doesn't cover this runtime deterministically.
@@ -22436,7 +23833,7 @@ async function applyInstallersOver(services, project, options = {}) {
22436
23833
  );
22437
23834
  } else if (outcome.outcome === "bun") {
22438
23835
  bun++;
22439
- const svcName = import_node_path76.default.basename(svc.dir);
23836
+ const svcName = import_node_path78.default.basename(svc.dir);
22440
23837
  console.log(
22441
23838
  `neat: ${svc.dir} detected as Bun
22442
23839
  The installer doesn't cover this runtime deterministically.
@@ -22447,7 +23844,7 @@ async function applyInstallersOver(services, project, options = {}) {
22447
23844
  );
22448
23845
  } else if (outcome.outcome === "deno") {
22449
23846
  deno++;
22450
- const svcName = import_node_path76.default.basename(svc.dir);
23847
+ const svcName = import_node_path78.default.basename(svc.dir);
22451
23848
  console.log(
22452
23849
  `neat: ${svc.dir} detected as Deno
22453
23850
  The installer doesn't cover this runtime deterministically.
@@ -22458,7 +23855,7 @@ async function applyInstallersOver(services, project, options = {}) {
22458
23855
  );
22459
23856
  } else if (outcome.outcome === "cloudflare-workers") {
22460
23857
  cloudflareWorkers++;
22461
- const svcName = import_node_path76.default.basename(svc.dir);
23858
+ const svcName = import_node_path78.default.basename(svc.dir);
22462
23859
  console.log(
22463
23860
  `neat: ${svc.dir} detected as Cloudflare Workers
22464
23861
  The installer doesn't cover this runtime deterministically.
@@ -22469,7 +23866,7 @@ async function applyInstallersOver(services, project, options = {}) {
22469
23866
  );
22470
23867
  } else if (outcome.outcome === "electron") {
22471
23868
  electron++;
22472
- const svcName = import_node_path76.default.basename(svc.dir);
23869
+ const svcName = import_node_path78.default.basename(svc.dir);
22473
23870
  console.log(
22474
23871
  `neat: ${svc.dir} detected as Electron
22475
23872
  The installer doesn't cover this runtime deterministically.
@@ -22482,7 +23879,7 @@ async function applyInstallersOver(services, project, options = {}) {
22482
23879
  if (svc.pkg && (outcome.outcome === "instrumented" || outcome.outcome === "already-instrumented")) {
22483
23880
  const gaps = uninstrumentedLibraries(svc.pkg);
22484
23881
  if (gaps.length > 0) {
22485
- const svcName = import_node_path76.default.basename(svc.dir);
23882
+ const svcName = import_node_path78.default.basename(svc.dir);
22486
23883
  const list = gaps.join(", ");
22487
23884
  const subject = gaps.length === 1 ? "this library" : "these libraries";
22488
23885
  const aux = gaps.length === 1 ? "isn't" : "aren't";
@@ -22510,6 +23907,11 @@ async function applyInstallersOver(services, project, options = {}) {
22510
23907
  }
22511
23908
  }
22512
23909
  }
23910
+ for (const [dir, command] of dependencyInstructions) {
23911
+ console.log(
23912
+ `neat: dependencies staged in ${dir}; run \`${command}\` to install them \u2014 NEAT does not run it for you.`
23913
+ );
23914
+ }
22513
23915
  return {
22514
23916
  instrumented,
22515
23917
  alreadyInstrumented: already,
@@ -22520,7 +23922,8 @@ async function applyInstallersOver(services, project, options = {}) {
22520
23922
  deno,
22521
23923
  cloudflareWorkers,
22522
23924
  electron,
22523
- packageManagerInstalls
23925
+ packageManagerInstalls,
23926
+ dependencyInstructions: [...dependencyInstructions].map(([dir, command]) => ({ dir, command }))
22524
23927
  };
22525
23928
  }
22526
23929
  async function promptYesNo(question) {
@@ -22688,24 +24091,24 @@ async function persistedPortsFor(scanPath) {
22688
24091
  return { rest: record.ports.rest, otlp: record.ports.otlp, web: record.ports.web };
22689
24092
  }
22690
24093
  async function acquireSpawnLock(scanPath) {
22691
- const lockPath = import_node_path76.default.join(scanPath, "neat-out", "daemon.spawn.lock");
22692
- await import_node_fs42.promises.mkdir(import_node_path76.default.dirname(lockPath), { recursive: true });
24094
+ const lockPath = import_node_path78.default.join(scanPath, "neat-out", "daemon.spawn.lock");
24095
+ await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(lockPath), { recursive: true });
22693
24096
  const STALE_LOCK_MS = 6e4;
22694
24097
  try {
22695
- const fd = await import_node_fs42.promises.open(lockPath, "wx");
24098
+ const fd = await import_node_fs44.promises.open(lockPath, "wx");
22696
24099
  await fd.writeFile(`${process.pid}
22697
24100
  `, "utf8");
22698
24101
  await fd.close();
22699
24102
  return async () => {
22700
- await import_node_fs42.promises.unlink(lockPath).catch(() => {
24103
+ await import_node_fs44.promises.unlink(lockPath).catch(() => {
22701
24104
  });
22702
24105
  };
22703
24106
  } catch (err) {
22704
24107
  if (err.code !== "EEXIST") return null;
22705
24108
  try {
22706
- const stat = await import_node_fs42.promises.stat(lockPath);
24109
+ const stat = await import_node_fs44.promises.stat(lockPath);
22707
24110
  if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
22708
- await import_node_fs42.promises.unlink(lockPath).catch(() => {
24111
+ await import_node_fs44.promises.unlink(lockPath).catch(() => {
22709
24112
  });
22710
24113
  return acquireSpawnLock(scanPath);
22711
24114
  }
@@ -22734,13 +24137,13 @@ async function healthIsForProject(restPort, project) {
22734
24137
  return false;
22735
24138
  }
22736
24139
  function daemonLogPath(projectPath3) {
22737
- return import_node_path76.default.join(projectPath3, "neat-out", "daemon.log");
24140
+ return import_node_path78.default.join(projectPath3, "neat-out", "daemon.log");
22738
24141
  }
22739
24142
  function spawnDaemonDetached(spec) {
22740
- const here = import_node_path76.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
24143
+ const here = import_node_path78.default.dirname((0, import_node_url4.fileURLToPath)(importMetaUrl));
22741
24144
  const candidates = [
22742
- import_node_path76.default.join(here, "neatd.cjs"),
22743
- import_node_path76.default.join(here, "neatd.js")
24145
+ import_node_path78.default.join(here, "neatd.cjs"),
24146
+ import_node_path78.default.join(here, "neatd.js")
22744
24147
  ];
22745
24148
  let entry2 = null;
22746
24149
  const fsSync = require("fs");
@@ -22770,7 +24173,7 @@ function spawnDaemonDetached(spec) {
22770
24173
  let logFd = null;
22771
24174
  if (spec) {
22772
24175
  const logPath = daemonLogPath(spec.projectPath);
22773
- fsSync.mkdirSync(import_node_path76.default.dirname(logPath), { recursive: true });
24176
+ fsSync.mkdirSync(import_node_path78.default.dirname(logPath), { recursive: true });
22774
24177
  logFd = fsSync.openSync(logPath, "a");
22775
24178
  }
22776
24179
  const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
@@ -22809,7 +24212,7 @@ async function runOrchestrator(opts) {
22809
24212
  browser: "skipped"
22810
24213
  }
22811
24214
  };
22812
- const stat = await import_node_fs42.promises.stat(opts.scanPath).catch(() => null);
24215
+ const stat = await import_node_fs44.promises.stat(opts.scanPath).catch(() => null);
22813
24216
  if (!stat || !stat.isDirectory()) {
22814
24217
  console.error(`neat: ${opts.scanPath} is not a directory`);
22815
24218
  result.exitCode = 2;
@@ -22969,7 +24372,7 @@ async function runOrchestrator(opts) {
22969
24372
  result.steps.browser = openBrowser(dashboardUrl);
22970
24373
  }
22971
24374
  const daemonRunning = result.steps.daemon === "spawned" || result.steps.daemon === "already-running";
22972
- const daemonLog = daemonRunning ? import_node_path76.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
24375
+ const daemonLog = daemonRunning ? import_node_path78.default.relative(opts.scanPath, daemonLogPath(opts.scanPath)) : null;
22973
24376
  printSummary(result, graph, dashboardUrl, daemonLog);
22974
24377
  return result;
22975
24378
  }
@@ -23428,27 +24831,27 @@ async function runConnectorCommand(rawArgs, deps = {}) {
23428
24831
 
23429
24832
  // src/hooks-cli.ts
23430
24833
  init_cjs_shims();
23431
- var import_node_path77 = __toESM(require("path"), 1);
24834
+ var import_node_path79 = __toESM(require("path"), 1);
23432
24835
  var import_node_os5 = __toESM(require("os"), 1);
23433
- var import_node_fs43 = require("fs");
24836
+ var import_node_fs45 = require("fs");
23434
24837
  var import_node_url5 = require("url");
23435
24838
  var HOOK_FILENAME = "neat-search-nudge.mjs";
23436
24839
  var GUIDE_FILENAME = "GRAPH_FIRST.md";
23437
24840
  var GUIDE_INSTALL_NAME = "neat-graph-first.md";
23438
24841
  var HOOK_MATCHER = "Grep|Glob|Bash";
23439
24842
  function moduleDir() {
23440
- return typeof __dirname !== "undefined" ? __dirname : import_node_path77.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
24843
+ return typeof __dirname !== "undefined" ? __dirname : import_node_path79.default.dirname((0, import_node_url5.fileURLToPath)(importMetaUrl));
23441
24844
  }
23442
24845
  async function readSkillAsset(rel) {
23443
24846
  const here = moduleDir();
23444
24847
  const candidates = [
23445
- import_node_path77.default.resolve(here, "../../claude-skill", rel),
23446
- import_node_path77.default.resolve(here, "../../../claude-skill", rel),
23447
- import_node_path77.default.resolve(here, "../claude-skill", rel)
24848
+ import_node_path79.default.resolve(here, "../../claude-skill", rel),
24849
+ import_node_path79.default.resolve(here, "../../../claude-skill", rel),
24850
+ import_node_path79.default.resolve(here, "../claude-skill", rel)
23448
24851
  ];
23449
24852
  for (const candidate of candidates) {
23450
24853
  try {
23451
- return await import_node_fs43.promises.readFile(candidate, "utf8");
24854
+ return await import_node_fs45.promises.readFile(candidate, "utf8");
23452
24855
  } catch {
23453
24856
  }
23454
24857
  }
@@ -23458,17 +24861,17 @@ async function readSkillAsset(rel) {
23458
24861
  }
23459
24862
  function neatHome3() {
23460
24863
  const override = process.env.NEAT_HOME;
23461
- if (override && override.length > 0) return import_node_path77.default.resolve(override);
23462
- return import_node_path77.default.join(import_node_os5.default.homedir(), ".neat");
24864
+ if (override && override.length > 0) return import_node_path79.default.resolve(override);
24865
+ return import_node_path79.default.join(import_node_os5.default.homedir(), ".neat");
23463
24866
  }
23464
24867
  function claudeSettingsPath() {
23465
24868
  const override = process.env.NEAT_CLAUDE_SETTINGS;
23466
- if (override && override.length > 0) return import_node_path77.default.resolve(override);
24869
+ if (override && override.length > 0) return import_node_path79.default.resolve(override);
23467
24870
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os5.default.homedir();
23468
- return import_node_path77.default.join(home, ".claude", "settings.json");
24871
+ return import_node_path79.default.join(home, ".claude", "settings.json");
23469
24872
  }
23470
24873
  function installedHookPath() {
23471
- return import_node_path77.default.join(neatHome3(), "hooks", HOOK_FILENAME);
24874
+ return import_node_path79.default.join(neatHome3(), "hooks", HOOK_FILENAME);
23472
24875
  }
23473
24876
  function isNeatSearchEntry(entry2) {
23474
24877
  return (entry2.hooks ?? []).some(
@@ -23501,14 +24904,14 @@ async function runHooks(opts) {
23501
24904
  const hookScript = await readSkillAsset(`hooks/${HOOK_FILENAME}`);
23502
24905
  const guide = await readSkillAsset(GUIDE_FILENAME);
23503
24906
  const scriptPath = installedHookPath();
23504
- await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(scriptPath), { recursive: true });
23505
- await import_node_fs43.promises.writeFile(scriptPath, hookScript, { mode: 493 });
23506
- const guidePath = import_node_path77.default.join(neatHome3(), GUIDE_INSTALL_NAME);
23507
- await import_node_fs43.promises.writeFile(guidePath, guide, "utf8");
24907
+ await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(scriptPath), { recursive: true });
24908
+ await import_node_fs45.promises.writeFile(scriptPath, hookScript, { mode: 493 });
24909
+ const guidePath = import_node_path79.default.join(neatHome3(), GUIDE_INSTALL_NAME);
24910
+ await import_node_fs45.promises.writeFile(guidePath, guide, "utf8");
23508
24911
  const settingsFile = claudeSettingsPath();
23509
24912
  let settings = {};
23510
24913
  try {
23511
- settings = JSON.parse(await import_node_fs43.promises.readFile(settingsFile, "utf8"));
24914
+ settings = JSON.parse(await import_node_fs45.promises.readFile(settingsFile, "utf8"));
23512
24915
  } catch (err) {
23513
24916
  if (err.code !== "ENOENT") {
23514
24917
  console.error(
@@ -23530,8 +24933,8 @@ async function runHooks(opts) {
23530
24933
  ...settings,
23531
24934
  hooks: { ...hooks, PreToolUse: preToolUse }
23532
24935
  };
23533
- await import_node_fs43.promises.mkdir(import_node_path77.default.dirname(settingsFile), { recursive: true });
23534
- await import_node_fs43.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
24936
+ await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(settingsFile), { recursive: true });
24937
+ await import_node_fs45.promises.writeFile(settingsFile, JSON.stringify(merged, null, 2) + "\n", "utf8");
23535
24938
  console.log(`neat hooks: installed the search-nudge hook`);
23536
24939
  console.log(` script: ${scriptPath}`);
23537
24940
  console.log(` settings: ${settingsFile} (PreToolUse \u2192 ${HOOK_MATCHER})`);
@@ -23602,9 +25005,9 @@ async function runHooksCommand(args) {
23602
25005
 
23603
25006
  // src/codex-cli.ts
23604
25007
  init_cjs_shims();
23605
- var import_node_path78 = __toESM(require("path"), 1);
25008
+ var import_node_path80 = __toESM(require("path"), 1);
23606
25009
  var import_node_os6 = __toESM(require("os"), 1);
23607
- var import_node_fs44 = require("fs");
25010
+ var import_node_fs46 = require("fs");
23608
25011
  var import_node_util = require("util");
23609
25012
  var import_smol_toml5 = require("smol-toml");
23610
25013
  var CODEX_MCP_SERVER = {
@@ -23622,14 +25025,14 @@ var NEAT_GRAPH_FIRST_START = "<!-- neat:graph-first -->";
23622
25025
  var NEAT_GRAPH_FIRST_END = "<!-- /neat:graph-first -->";
23623
25026
  function codexConfigPath() {
23624
25027
  const override = process.env.NEAT_CODEX_CONFIG;
23625
- if (override && override.length > 0) return import_node_path78.default.resolve(override);
25028
+ if (override && override.length > 0) return import_node_path80.default.resolve(override);
23626
25029
  const home = process.env.HOME ?? process.env.USERPROFILE ?? import_node_os6.default.homedir();
23627
- return import_node_path78.default.join(home, ".codex", "config.toml");
25030
+ return import_node_path80.default.join(home, ".codex", "config.toml");
23628
25031
  }
23629
25032
  function agentsFilePath() {
23630
25033
  const override = process.env.NEAT_CODEX_AGENTS;
23631
- if (override && override.length > 0) return import_node_path78.default.resolve(override);
23632
- return import_node_path78.default.join(process.cwd(), "AGENTS.md");
25034
+ if (override && override.length > 0) return import_node_path80.default.resolve(override);
25035
+ return import_node_path80.default.join(process.cwd(), "AGENTS.md");
23633
25036
  }
23634
25037
  function isTableHeader(line) {
23635
25038
  return /^\s*\[\[?[^\]]+\]\]?\s*$/.test(line);
@@ -23763,7 +25166,7 @@ async function runCodex(opts) {
23763
25166
  const agentsPath = agentsFilePath();
23764
25167
  let configRaw = "";
23765
25168
  try {
23766
- configRaw = await import_node_fs44.promises.readFile(configPath, "utf8");
25169
+ configRaw = await import_node_fs46.promises.readFile(configPath, "utf8");
23767
25170
  } catch (err) {
23768
25171
  if (err.code !== "ENOENT") {
23769
25172
  console.error(`neat codex: failed to read ${configPath} \u2014 ${err.message}`);
@@ -23772,7 +25175,7 @@ async function runCodex(opts) {
23772
25175
  }
23773
25176
  let agentsRaw = "";
23774
25177
  try {
23775
- agentsRaw = await import_node_fs44.promises.readFile(agentsPath, "utf8");
25178
+ agentsRaw = await import_node_fs46.promises.readFile(agentsPath, "utf8");
23776
25179
  } catch (err) {
23777
25180
  if (err.code !== "ENOENT") {
23778
25181
  console.error(`neat codex: failed to read ${agentsPath} \u2014 ${err.message}`);
@@ -23812,15 +25215,15 @@ async function runCodex(opts) {
23812
25215
  return { exitCode: 0 };
23813
25216
  }
23814
25217
  if (config.changed) {
23815
- await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(configPath), { recursive: true });
23816
- await import_node_fs44.promises.writeFile(configPath, config.text, "utf8");
25218
+ await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(configPath), { recursive: true });
25219
+ await import_node_fs46.promises.writeFile(configPath, config.text, "utf8");
23817
25220
  console.log(`neat codex: wrote [mcp_servers.neat] to ${configPath}`);
23818
25221
  } else {
23819
25222
  console.log(`neat codex: ${configPath} already has NEAT's MCP server`);
23820
25223
  }
23821
25224
  if (agents.changed) {
23822
- await import_node_fs44.promises.mkdir(import_node_path78.default.dirname(agentsPath), { recursive: true });
23823
- await import_node_fs44.promises.writeFile(agentsPath, agents.text, "utf8");
25225
+ await import_node_fs46.promises.mkdir(import_node_path80.default.dirname(agentsPath), { recursive: true });
25226
+ await import_node_fs46.promises.writeFile(agentsPath, agents.text, "utf8");
23824
25227
  console.log(`neat codex: wrote the graph-first block to ${agentsPath}`);
23825
25228
  } else {
23826
25229
  console.log(`neat codex: ${agentsPath} already has the graph-first block`);
@@ -23877,9 +25280,9 @@ async function runCodexCommand(args) {
23877
25280
 
23878
25281
  // src/editors-cli.ts
23879
25282
  init_cjs_shims();
23880
- var import_node_path79 = __toESM(require("path"), 1);
25283
+ var import_node_path81 = __toESM(require("path"), 1);
23881
25284
  var import_node_os7 = __toESM(require("os"), 1);
23882
- var import_node_fs45 = require("fs");
25285
+ var import_node_fs47 = require("fs");
23883
25286
  var import_node_util2 = require("util");
23884
25287
  var jsonc = __toESM(require("jsonc-parser"), 1);
23885
25288
  var NEAT_MCP_SERVER = {
@@ -23903,17 +25306,17 @@ function homeDir() {
23903
25306
  }
23904
25307
  function xdgConfigDir() {
23905
25308
  const xdg = process.env.XDG_CONFIG_HOME;
23906
- return xdg && xdg.length > 0 ? import_node_path79.default.resolve(xdg) : import_node_path79.default.join(homeDir(), ".config");
25309
+ return xdg && xdg.length > 0 ? import_node_path81.default.resolve(xdg) : import_node_path81.default.join(homeDir(), ".config");
23907
25310
  }
23908
25311
  function envOverride(name) {
23909
25312
  const v = process.env[name];
23910
- return v && v.length > 0 ? import_node_path79.default.resolve(v) : void 0;
25313
+ return v && v.length > 0 ? import_node_path81.default.resolve(v) : void 0;
23911
25314
  }
23912
25315
  var CURSOR_CLIENT = {
23913
25316
  id: "cursor",
23914
25317
  label: "Cursor",
23915
25318
  docsUrl: "https://docs.cursor.com/context/mcp",
23916
- mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path79.default.join(homeDir(), ".cursor", "mcp.json"),
25319
+ mcpConfigPath: () => envOverride("NEAT_CURSOR_CONFIG") ?? import_node_path81.default.join(homeDir(), ".cursor", "mcp.json"),
23917
25320
  mcpContainerKey: "mcpServers",
23918
25321
  format: "json",
23919
25322
  // Cursor still reads a single `.cursorrules` at the project root (the modern
@@ -23925,7 +25328,7 @@ var DEVIN_CLIENT = {
23925
25328
  id: "devin",
23926
25329
  label: "Devin Desktop (Cascade)",
23927
25330
  docsUrl: "https://docs.devin.ai/desktop/cascade/mcp",
23928
- mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path79.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
25331
+ mcpConfigPath: () => envOverride("NEAT_DEVIN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".codeium", "windsurf", "mcp_config.json"),
23929
25332
  mcpContainerKey: "mcpServers",
23930
25333
  format: "json",
23931
25334
  rulesFileName: ".windsurfrules"
@@ -23934,7 +25337,7 @@ var GEMINI_CLIENT = {
23934
25337
  id: "gemini",
23935
25338
  label: "Gemini CLI",
23936
25339
  docsUrl: "https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md",
23937
- mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path79.default.join(homeDir(), ".gemini", "settings.json"),
25340
+ mcpConfigPath: () => envOverride("NEAT_GEMINI_CONFIG") ?? import_node_path81.default.join(homeDir(), ".gemini", "settings.json"),
23938
25341
  mcpContainerKey: "mcpServers",
23939
25342
  format: "json",
23940
25343
  rulesFileName: "GEMINI.md"
@@ -23943,7 +25346,7 @@ var QWEN_CLIENT = {
23943
25346
  id: "qwen",
23944
25347
  label: "Qwen Code",
23945
25348
  docsUrl: "https://qwenlm.github.io/qwen-code-docs/en/users/features/mcp/",
23946
- mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path79.default.join(homeDir(), ".qwen", "settings.json"),
25349
+ mcpConfigPath: () => envOverride("NEAT_QWEN_CONFIG") ?? import_node_path81.default.join(homeDir(), ".qwen", "settings.json"),
23947
25350
  mcpContainerKey: "mcpServers",
23948
25351
  format: "json",
23949
25352
  rulesFileName: "QWEN.md"
@@ -23952,7 +25355,7 @@ var AMAZONQ_CLIENT = {
23952
25355
  id: "amazonq",
23953
25356
  label: "Amazon Q Developer CLI",
23954
25357
  docsUrl: "https://docs.aws.amazon.com/amazonq/latest/qdeveloper-ug/command-line-mcp-configuration.html",
23955
- mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path79.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
25358
+ mcpConfigPath: () => envOverride("NEAT_AMAZONQ_CONFIG") ?? import_node_path81.default.join(homeDir(), ".aws", "amazonq", "mcp.json"),
23956
25359
  mcpContainerKey: "mcpServers",
23957
25360
  format: "json"
23958
25361
  };
@@ -23960,7 +25363,7 @@ var ROOCODE_CLIENT = {
23960
25363
  id: "roocode",
23961
25364
  label: "Roo Code",
23962
25365
  docsUrl: "https://roocodeinc.github.io/Roo-Code/features/mcp/using-mcp-in-roo",
23963
- mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path79.default.join(process.cwd(), ".roo", "mcp.json"),
25366
+ mcpConfigPath: () => envOverride("NEAT_ROOCODE_CONFIG") ?? import_node_path81.default.join(process.cwd(), ".roo", "mcp.json"),
23964
25367
  mcpContainerKey: "mcpServers",
23965
25368
  format: "json"
23966
25369
  };
@@ -23973,9 +25376,9 @@ var ZED_CLIENT = {
23973
25376
  if (override) return override;
23974
25377
  if (process.platform === "win32") {
23975
25378
  const appData = process.env.APPDATA;
23976
- if (appData && appData.length > 0) return import_node_path79.default.join(appData, "Zed", "settings.json");
25379
+ if (appData && appData.length > 0) return import_node_path81.default.join(appData, "Zed", "settings.json");
23977
25380
  }
23978
- return import_node_path79.default.join(homeDir(), ".config", "zed", "settings.json");
25381
+ return import_node_path81.default.join(homeDir(), ".config", "zed", "settings.json");
23979
25382
  },
23980
25383
  mcpContainerKey: "context_servers",
23981
25384
  format: "jsonc",
@@ -23985,7 +25388,7 @@ var OPENCODE_CLIENT = {
23985
25388
  id: "opencode",
23986
25389
  label: "OpenCode",
23987
25390
  docsUrl: "https://opencode.ai/docs/mcp-servers/",
23988
- mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path79.default.join(xdgConfigDir(), "opencode", "opencode.json"),
25391
+ mcpConfigPath: () => envOverride("NEAT_OPENCODE_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "opencode", "opencode.json"),
23989
25392
  mcpContainerKey: "mcp",
23990
25393
  format: "json",
23991
25394
  serverEntry: NEAT_OPENCODE_SERVER,
@@ -23995,7 +25398,7 @@ var CRUSH_CLIENT = {
23995
25398
  id: "crush",
23996
25399
  label: "Crush",
23997
25400
  docsUrl: "https://charmbracelet-crush.mintlify.app/configuration/mcp",
23998
- mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path79.default.join(xdgConfigDir(), "crush", "crush.json"),
25401
+ mcpConfigPath: () => envOverride("NEAT_CRUSH_CONFIG") ?? import_node_path81.default.join(xdgConfigDir(), "crush", "crush.json"),
23999
25402
  mcpContainerKey: "mcp",
24000
25403
  format: "json",
24001
25404
  serverEntry: NEAT_CRUSH_SERVER,
@@ -24058,7 +25461,7 @@ async function planMcp(client, mcpPath) {
24058
25461
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
24059
25462
  let raw = "";
24060
25463
  try {
24061
- raw = await import_node_fs45.promises.readFile(mcpPath, "utf8");
25464
+ raw = await import_node_fs47.promises.readFile(mcpPath, "utf8");
24062
25465
  } catch (err) {
24063
25466
  const e = err;
24064
25467
  if (e.code === "ENOENT") {
@@ -24100,7 +25503,7 @@ async function runEditorInstall(client, opts) {
24100
25503
  const mcpPath = client.mcpConfigPath();
24101
25504
  const serverEntry = client.serverEntry ?? NEAT_MCP_SERVER;
24102
25505
  const hasRules = typeof client.rulesFileName === "string";
24103
- const rulesPath = hasRules ? import_node_path79.default.join(opts.projectDir, client.rulesFileName) : "";
25506
+ const rulesPath = hasRules ? import_node_path81.default.join(opts.projectDir, client.rulesFileName) : "";
24104
25507
  const mcp = await planMcp(client, mcpPath);
24105
25508
  if (mcp === null) return { exitCode: 1 };
24106
25509
  let existingRules = "";
@@ -24109,7 +25512,7 @@ async function runEditorInstall(client, opts) {
24109
25512
  let block = "";
24110
25513
  if (hasRules) {
24111
25514
  try {
24112
- existingRules = await import_node_fs45.promises.readFile(rulesPath, "utf8");
25515
+ existingRules = await import_node_fs47.promises.readFile(rulesPath, "utf8");
24113
25516
  } catch (err) {
24114
25517
  if (err.code !== "ENOENT") {
24115
25518
  console.error(`neat ${client.id}: failed to read ${rulesPath} \u2014 ${err.message}`);
@@ -24143,11 +25546,11 @@ async function runEditorInstall(client, opts) {
24143
25546
  );
24144
25547
  return { exitCode: 0 };
24145
25548
  }
24146
- await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(mcpPath), { recursive: true });
24147
- await import_node_fs45.promises.writeFile(mcpPath, mcp.text, "utf8");
25549
+ await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(mcpPath), { recursive: true });
25550
+ await import_node_fs47.promises.writeFile(mcpPath, mcp.text, "utf8");
24148
25551
  if (hasRules) {
24149
- await import_node_fs45.promises.mkdir(import_node_path79.default.dirname(rulesPath), { recursive: true });
24150
- await import_node_fs45.promises.writeFile(rulesPath, newRules, "utf8");
25552
+ await import_node_fs47.promises.mkdir(import_node_path81.default.dirname(rulesPath), { recursive: true });
25553
+ await import_node_fs47.promises.writeFile(rulesPath, newRules, "utf8");
24151
25554
  }
24152
25555
  console.log(`neat ${client.id}: wired NEAT into ${client.label}`);
24153
25556
  console.log(` MCP server: ${mcpPath} (${client.mcpContainerKey}.neat \u2192 npx -y @neat.is/mcp)`);
@@ -24183,11 +25586,11 @@ function usage3(client) {
24183
25586
  }
24184
25587
  async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24185
25588
  const client = CLIENTS[clientId];
24186
- let apply4 = false;
25589
+ let apply6 = false;
24187
25590
  for (const arg of args) {
24188
25591
  switch (arg) {
24189
25592
  case "--apply":
24190
- apply4 = true;
25593
+ apply6 = true;
24191
25594
  break;
24192
25595
  case "-h":
24193
25596
  case "--help":
@@ -24200,7 +25603,7 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24200
25603
  }
24201
25604
  }
24202
25605
  try {
24203
- const { exitCode } = await runEditorInstall(client, { apply: apply4, projectDir });
25606
+ const { exitCode } = await runEditorInstall(client, { apply: apply6, projectDir });
24204
25607
  return exitCode;
24205
25608
  } catch (err) {
24206
25609
  console.error(err.message);
@@ -24210,11 +25613,11 @@ async function runEditorCommand(clientId, args, projectDir = process.cwd()) {
24210
25613
 
24211
25614
  // src/monitor.ts
24212
25615
  init_cjs_shims();
24213
- var import_types84 = require("@neat.is/types");
25616
+ var import_types89 = require("@neat.is/types");
24214
25617
 
24215
25618
  // src/cli-client.ts
24216
25619
  init_cjs_shims();
24217
- var import_types83 = require("@neat.is/types");
25620
+ var import_types88 = require("@neat.is/types");
24218
25621
  var HttpError = class extends Error {
24219
25622
  constructor(status2, message, responseBody = "") {
24220
25623
  super(message);
@@ -24239,10 +25642,10 @@ function createHttpClient(baseUrl, bearerToken) {
24239
25642
  const root = baseUrl.replace(/\/$/, "");
24240
25643
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
24241
25644
  return {
24242
- async get(path82) {
25645
+ async get(path84) {
24243
25646
  let res;
24244
25647
  try {
24245
- res = await fetch(`${root}${path82}`, {
25648
+ res = await fetch(`${root}${path84}`, {
24246
25649
  headers: { ...authHeader }
24247
25650
  });
24248
25651
  } catch (err) {
@@ -24254,16 +25657,16 @@ function createHttpClient(baseUrl, bearerToken) {
24254
25657
  const body = await res.text().catch(() => "");
24255
25658
  throw new HttpError(
24256
25659
  res.status,
24257
- `${res.status} ${res.statusText} on GET ${path82}: ${body}`,
25660
+ `${res.status} ${res.statusText} on GET ${path84}: ${body}`,
24258
25661
  body
24259
25662
  );
24260
25663
  }
24261
25664
  return await res.json();
24262
25665
  },
24263
- async post(path82, body) {
25666
+ async post(path84, body) {
24264
25667
  let res;
24265
25668
  try {
24266
- res = await fetch(`${root}${path82}`, {
25669
+ res = await fetch(`${root}${path84}`, {
24267
25670
  method: "POST",
24268
25671
  headers: { "content-type": "application/json", ...authHeader },
24269
25672
  body: JSON.stringify(body)
@@ -24277,7 +25680,7 @@ function createHttpClient(baseUrl, bearerToken) {
24277
25680
  const text = await res.text().catch(() => "");
24278
25681
  throw new HttpError(
24279
25682
  res.status,
24280
- `${res.status} ${res.statusText} on POST ${path82}: ${text}`,
25683
+ `${res.status} ${res.statusText} on POST ${path84}: ${text}`,
24281
25684
  text
24282
25685
  );
24283
25686
  }
@@ -24291,12 +25694,12 @@ function projectPath(project, suffix) {
24291
25694
  }
24292
25695
  async function runRootCause(client, input) {
24293
25696
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
24294
- const path82 = projectPath(
25697
+ const path84 = projectPath(
24295
25698
  input.project,
24296
25699
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
24297
25700
  );
24298
25701
  try {
24299
- const result = await client.get(path82);
25702
+ const result = await client.get(path84);
24300
25703
  const arrowPath = result.traversalPath.join(" \u2190 ");
24301
25704
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
24302
25705
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -24322,12 +25725,12 @@ async function runRootCause(client, input) {
24322
25725
  }
24323
25726
  async function runBlastRadius(client, input) {
24324
25727
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
24325
- const path82 = projectPath(
25728
+ const path84 = projectPath(
24326
25729
  input.project,
24327
25730
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
24328
25731
  );
24329
25732
  try {
24330
- const result = await client.get(path82);
25733
+ const result = await client.get(path84);
24331
25734
  if (result.totalAffected === 0) {
24332
25735
  return {
24333
25736
  summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
@@ -24356,17 +25759,17 @@ async function runBlastRadius(client, input) {
24356
25759
  }
24357
25760
  }
24358
25761
  function formatBlastEntry(n) {
24359
- const tag = n.edgeProvenance === import_types83.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
25762
+ const tag = n.edgeProvenance === import_types88.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
24360
25763
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
24361
25764
  }
24362
25765
  async function runDependencies(client, input) {
24363
25766
  const depth = input.depth ?? 3;
24364
- const path82 = projectPath(
25767
+ const path84 = projectPath(
24365
25768
  input.project,
24366
25769
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
24367
25770
  );
24368
25771
  try {
24369
- const result = await client.get(path82);
25772
+ const result = await client.get(path84);
24370
25773
  if (result.total === 0) {
24371
25774
  return {
24372
25775
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -24413,7 +25816,7 @@ async function runObservedDependencies(client, input) {
24413
25816
  if (result.observed) {
24414
25817
  return {
24415
25818
  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.`,
24416
- provenance: import_types83.Provenance.OBSERVED
25819
+ provenance: import_types88.Provenance.OBSERVED
24417
25820
  };
24418
25821
  }
24419
25822
  const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
@@ -24423,7 +25826,7 @@ async function runObservedDependencies(client, input) {
24423
25826
  return {
24424
25827
  summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
24425
25828
  block: blockLines.join("\n"),
24426
- provenance: import_types83.Provenance.OBSERVED
25829
+ provenance: import_types88.Provenance.OBSERVED
24427
25830
  };
24428
25831
  } catch (err) {
24429
25832
  if (err instanceof HttpError && err.status === 404) {
@@ -24458,9 +25861,9 @@ function formatDuration(ms) {
24458
25861
  return `${Math.round(h / 24)}d`;
24459
25862
  }
24460
25863
  async function runIncidents(client, input) {
24461
- const path82 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
25864
+ const path84 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
24462
25865
  try {
24463
- const body = await client.get(path82);
25866
+ const body = await client.get(path84);
24464
25867
  const events = body.events;
24465
25868
  if (events.length === 0) {
24466
25869
  return {
@@ -24477,7 +25880,7 @@ async function runIncidents(client, input) {
24477
25880
  return {
24478
25881
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
24479
25882
  block: blockLines.join("\n"),
24480
- provenance: import_types83.Provenance.OBSERVED
25883
+ provenance: import_types88.Provenance.OBSERVED
24481
25884
  };
24482
25885
  } catch (err) {
24483
25886
  if (err instanceof HttpError && err.status === 404) {
@@ -24586,7 +25989,7 @@ async function runStaleEdges(client, input) {
24586
25989
  return {
24587
25990
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
24588
25991
  block: blockLines.join("\n"),
24589
- provenance: import_types83.Provenance.STALE
25992
+ provenance: import_types88.Provenance.STALE
24590
25993
  };
24591
25994
  }
24592
25995
  async function runPolicies(client, input) {
@@ -24745,10 +26148,10 @@ async function pushSnapshotToRemote(input) {
24745
26148
 
24746
26149
  // src/monitor.ts
24747
26150
  var OBSERVED_DEP_EDGE_TYPES = /* @__PURE__ */ new Set([
24748
- import_types84.EdgeType.CALLS,
24749
- import_types84.EdgeType.CONNECTS_TO,
24750
- import_types84.EdgeType.PUBLISHES_TO,
24751
- import_types84.EdgeType.CONSUMES_FROM
26151
+ import_types89.EdgeType.CALLS,
26152
+ import_types89.EdgeType.CONNECTS_TO,
26153
+ import_types89.EdgeType.PUBLISHES_TO,
26154
+ import_types89.EdgeType.CONSUMES_FROM
24752
26155
  ]);
24753
26156
  function divergenceKey(d) {
24754
26157
  const column = "column" in d && d.column ? d.column : "";
@@ -24793,7 +26196,7 @@ function formatDivergenceLine2(d) {
24793
26196
  }
24794
26197
  }
24795
26198
  function formatStaleLine(edgeId) {
24796
- const parsed = (0, import_types84.parseEdgeId)(edgeId);
26199
+ const parsed = (0, import_types89.parseEdgeId)(edgeId);
24797
26200
  if (parsed) {
24798
26201
  return `\u22EF stale ${parsed.source} \u2192 ${parsed.target} (observed edge went quiet)`;
24799
26202
  }
@@ -24806,7 +26209,7 @@ function divergenceJson(d) {
24806
26209
  return JSON.stringify({ kind: "divergence", ...d });
24807
26210
  }
24808
26211
  function staleJson(edgeId) {
24809
- const parsed = (0, import_types84.parseEdgeId)(edgeId);
26212
+ const parsed = (0, import_types89.parseEdgeId)(edgeId);
24810
26213
  return JSON.stringify({
24811
26214
  kind: "stale",
24812
26215
  edgeId,
@@ -24876,7 +26279,7 @@ var MonitorEmitter = class {
24876
26279
  // ignores non-OBSERVED edges and non-dependency edge types (structural
24877
26280
  // ownership), so only real runtime dependencies reach stdout.
24878
26281
  emitObservedEdge(edge) {
24879
- if (edge.provenance !== import_types84.Provenance.OBSERVED) return false;
26282
+ if (edge.provenance !== import_types89.Provenance.OBSERVED) return false;
24880
26283
  if (!OBSERVED_DEP_EDGE_TYPES.has(edge.type)) return false;
24881
26284
  const key = `edge|${edge.id}`;
24882
26285
  if (this.seen.has(key)) return false;
@@ -25034,7 +26437,7 @@ async function runMonitor(opts) {
25034
26437
  case "edge-added": {
25035
26438
  const payload = safeParse(frame.data);
25036
26439
  const edge = payload?.edge;
25037
- if (edge && edge.provenance === import_types84.Provenance.OBSERVED) {
26440
+ if (edge && edge.provenance === import_types89.Provenance.OBSERVED) {
25038
26441
  emitter.emitObservedEdge(edge);
25039
26442
  divergences.schedule();
25040
26443
  }
@@ -25114,7 +26517,7 @@ function sleep(ms, signal) {
25114
26517
 
25115
26518
  // src/cli-verbs.ts
25116
26519
  init_cjs_shims();
25117
- var import_node_path80 = __toESM(require("path"), 1);
26520
+ var import_node_path82 = __toESM(require("path"), 1);
25118
26521
  async function resolveProjectEntry(opts) {
25119
26522
  const entries = await listProjects();
25120
26523
  if (opts.project) {
@@ -25124,7 +26527,7 @@ async function resolveProjectEntry(opts) {
25124
26527
  const cwd = opts.cwd ?? process.cwd();
25125
26528
  const resolvedCwd = await normalizeProjectPath(cwd);
25126
26529
  for (const entry2 of entries) {
25127
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path80.default.sep}`)) {
26530
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path82.default.sep}`)) {
25128
26531
  return entry2;
25129
26532
  }
25130
26533
  }
@@ -25277,7 +26680,7 @@ async function runSync(opts) {
25277
26680
  }
25278
26681
 
25279
26682
  // src/cli.ts
25280
- var import_types85 = require("@neat.is/types");
26683
+ var import_types90 = require("@neat.is/types");
25281
26684
  function isNpxInvocation() {
25282
26685
  if (process.env.npm_command === "exec") return true;
25283
26686
  const execpath = process.env.npm_execpath ?? "";
@@ -25631,15 +27034,15 @@ async function buildPatchSections(services, project) {
25631
27034
  for (const svc of services) {
25632
27035
  const installer = await pickInstaller(svc.dir);
25633
27036
  if (!installer) continue;
25634
- const plan4 = await installer.plan(svc.dir, { project });
25635
- if (isEmptyPlan(plan4) && !plan4.libOnly && plan4.runtimeKind === void 0) continue;
25636
- sections.push({ installer: installer.name, plan: plan4 });
27037
+ const plan6 = await installer.plan(svc.dir, { project });
27038
+ if (isEmptyPlan(plan6) && !plan6.libOnly && plan6.runtimeKind === void 0) continue;
27039
+ sections.push({ installer: installer.name, plan: plan6 });
25637
27040
  }
25638
27041
  return sections;
25639
27042
  }
25640
27043
  async function runInit(opts) {
25641
27044
  const written = [];
25642
- const stat = await import_node_fs46.promises.stat(opts.scanPath).catch(() => null);
27045
+ const stat = await import_node_fs48.promises.stat(opts.scanPath).catch(() => null);
25643
27046
  if (!stat || !stat.isDirectory()) {
25644
27047
  console.error(`neat init: ${opts.scanPath} is not a directory`);
25645
27048
  return { exitCode: 2, writtenFiles: written };
@@ -25648,13 +27051,13 @@ async function runInit(opts) {
25648
27051
  printDiscoveryReport(opts, services);
25649
27052
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
25650
27053
  const patch = renderPatch(sections);
25651
- const patchPath = import_node_path81.default.join(opts.scanPath, "neat.patch");
27054
+ const patchPath = import_node_path83.default.join(opts.scanPath, "neat.patch");
25652
27055
  if (opts.dryRun) {
25653
- await import_node_fs46.promises.writeFile(patchPath, patch, "utf8");
27056
+ await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
25654
27057
  written.push(patchPath);
25655
27058
  console.log(`dry-run: patch written to ${patchPath}`);
25656
- const gitignorePath = import_node_path81.default.join(opts.scanPath, ".gitignore");
25657
- const gitignoreExists = await import_node_fs46.promises.stat(gitignorePath).then(() => true).catch(() => false);
27059
+ const gitignorePath = import_node_path83.default.join(opts.scanPath, ".gitignore");
27060
+ const gitignoreExists = await import_node_fs48.promises.stat(gitignorePath).then(() => true).catch(() => false);
25658
27061
  const verb = gitignoreExists ? "append" : "create";
25659
27062
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
25660
27063
  console.log("rerun without --dry-run to register and snapshot.");
@@ -25665,9 +27068,9 @@ async function runInit(opts) {
25665
27068
  const graph = getGraph(graphKey);
25666
27069
  const projectPaths = pathsForProject(
25667
27070
  graphKey,
25668
- import_node_path81.default.join(opts.scanPath, "neat-out")
27071
+ import_node_path83.default.join(opts.scanPath, "neat-out")
25669
27072
  );
25670
- const errorsPath = import_node_path81.default.join(import_node_path81.default.dirname(opts.outPath), import_node_path81.default.basename(projectPaths.errorsPath));
27073
+ const errorsPath = import_node_path83.default.join(import_node_path83.default.dirname(opts.outPath), import_node_path83.default.basename(projectPaths.errorsPath));
25671
27074
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
25672
27075
  await saveGraphToDisk(graph, opts.outPath);
25673
27076
  written.push(opts.outPath);
@@ -25746,7 +27149,7 @@ async function runInit(opts) {
25746
27149
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
25747
27150
  }
25748
27151
  } else {
25749
- await import_node_fs46.promises.writeFile(patchPath, patch, "utf8");
27152
+ await import_node_fs48.promises.writeFile(patchPath, patch, "utf8");
25750
27153
  written.push(patchPath);
25751
27154
  }
25752
27155
  }
@@ -25786,9 +27189,9 @@ var CLAUDE_SKILL_CONFIG = {
25786
27189
  };
25787
27190
  function claudeConfigPath() {
25788
27191
  const override = process.env.NEAT_CLAUDE_CONFIG;
25789
- if (override && override.length > 0) return import_node_path81.default.resolve(override);
27192
+ if (override && override.length > 0) return import_node_path83.default.resolve(override);
25790
27193
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
25791
- return import_node_path81.default.join(home, ".claude.json");
27194
+ return import_node_path83.default.join(home, ".claude.json");
25792
27195
  }
25793
27196
  async function runSkill(opts) {
25794
27197
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -25800,7 +27203,7 @@ async function runSkill(opts) {
25800
27203
  const target = claudeConfigPath();
25801
27204
  let existing = {};
25802
27205
  try {
25803
- existing = JSON.parse(await import_node_fs46.promises.readFile(target, "utf8"));
27206
+ existing = JSON.parse(await import_node_fs48.promises.readFile(target, "utf8"));
25804
27207
  } catch (err) {
25805
27208
  if (err.code !== "ENOENT") {
25806
27209
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -25812,8 +27215,8 @@ async function runSkill(opts) {
25812
27215
  ...existing,
25813
27216
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
25814
27217
  };
25815
- await import_node_fs46.promises.mkdir(import_node_path81.default.dirname(target), { recursive: true });
25816
- await import_node_fs46.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
27218
+ await import_node_fs48.promises.mkdir(import_node_path83.default.dirname(target), { recursive: true });
27219
+ await import_node_fs48.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
25817
27220
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
25818
27221
  console.log("restart Claude Code to pick up the new MCP server.");
25819
27222
  console.log("");
@@ -25886,7 +27289,7 @@ async function main() {
25886
27289
  }
25887
27290
  const cmd = argvParsed.positional[0];
25888
27291
  const parsed = { ...argvParsed, positional: argvParsed.positional.slice(1) };
25889
- const { positional, apply: apply4, dryRun, noInstall } = parsed;
27292
+ const { positional, apply: apply6, dryRun, noInstall } = parsed;
25890
27293
  const project = parsed.project ?? DEFAULT_PROJECT;
25891
27294
  if (cmd === "init") {
25892
27295
  const target = positional[0];
@@ -25895,22 +27298,22 @@ async function main() {
25895
27298
  usage4();
25896
27299
  process.exit(2);
25897
27300
  }
25898
- if (apply4 && dryRun) {
27301
+ if (apply6 && dryRun) {
25899
27302
  console.error("neat init: --apply and --dry-run are mutually exclusive");
25900
27303
  process.exit(2);
25901
27304
  }
25902
- const scanPath = import_node_path81.default.resolve(target);
27305
+ const scanPath = import_node_path83.default.resolve(target);
25903
27306
  const projectExplicit = parsed.project !== null;
25904
- const projectName = projectExplicit ? project : import_node_path81.default.basename(scanPath);
27307
+ const projectName = projectExplicit ? project : import_node_path83.default.basename(scanPath);
25905
27308
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
25906
- const fallback = pathsForProject(projectKey, import_node_path81.default.join(scanPath, "neat-out")).snapshotPath;
25907
- const outPath = import_node_path81.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
27309
+ const fallback = pathsForProject(projectKey, import_node_path83.default.join(scanPath, "neat-out")).snapshotPath;
27310
+ const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
25908
27311
  const result = await runInit({
25909
27312
  scanPath,
25910
27313
  outPath,
25911
27314
  project: projectName,
25912
27315
  projectExplicit,
25913
- apply: apply4,
27316
+ apply: apply6,
25914
27317
  dryRun,
25915
27318
  noInstall,
25916
27319
  verbose: parsed.verbose
@@ -25925,21 +27328,21 @@ async function main() {
25925
27328
  usage4();
25926
27329
  process.exit(2);
25927
27330
  }
25928
- const scanPath = import_node_path81.default.resolve(target);
25929
- const stat = await import_node_fs46.promises.stat(scanPath).catch(() => null);
27331
+ const scanPath = import_node_path83.default.resolve(target);
27332
+ const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
25930
27333
  if (!stat || !stat.isDirectory()) {
25931
27334
  console.error(`neat watch: ${scanPath} is not a directory`);
25932
27335
  process.exit(2);
25933
27336
  }
25934
- const projectPaths = pathsForProject(project, import_node_path81.default.join(scanPath, "neat-out"));
25935
- const outPath = import_node_path81.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
25936
- const errorsPath = import_node_path81.default.resolve(
25937
- process.env.NEAT_ERRORS_PATH ?? import_node_path81.default.join(import_node_path81.default.dirname(outPath), import_node_path81.default.basename(projectPaths.errorsPath))
27337
+ const projectPaths = pathsForProject(project, import_node_path83.default.join(scanPath, "neat-out"));
27338
+ const outPath = import_node_path83.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
27339
+ const errorsPath = import_node_path83.default.resolve(
27340
+ process.env.NEAT_ERRORS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.errorsPath))
25938
27341
  );
25939
- const staleEventsPath = import_node_path81.default.resolve(
25940
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path81.default.join(import_node_path81.default.dirname(outPath), import_node_path81.default.basename(projectPaths.staleEventsPath))
27342
+ const staleEventsPath = import_node_path83.default.resolve(
27343
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path83.default.join(import_node_path83.default.dirname(outPath), import_node_path83.default.basename(projectPaths.staleEventsPath))
25941
27344
  );
25942
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path81.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
27345
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path83.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
25943
27346
  const handle = await startWatch(getGraph(project), {
25944
27347
  scanPath,
25945
27348
  outPath,
@@ -25948,7 +27351,7 @@ async function main() {
25948
27351
  project,
25949
27352
  // Resolve NEAT_HOME so a `neat watch` picks up connectors added to
25950
27353
  // ~/.neat/connectors.json (#871). Same resolution the rest of the CLI uses.
25951
- neatHome: process.env.NEAT_HOME ? import_node_path81.default.resolve(process.env.NEAT_HOME) : import_node_path81.default.join(import_node_os8.default.homedir(), ".neat"),
27354
+ neatHome: process.env.NEAT_HOME ? import_node_path83.default.resolve(process.env.NEAT_HOME) : import_node_path83.default.join(import_node_os8.default.homedir(), ".neat"),
25952
27355
  ...embeddingsCachePath ? { embeddingsCachePath } : {},
25953
27356
  host: process.env.HOST ?? "0.0.0.0",
25954
27357
  port: Number(process.env.PORT ?? 8080),
@@ -26130,11 +27533,11 @@ async function main() {
26130
27533
  process.exit(1);
26131
27534
  }
26132
27535
  async function tryOrchestrator(cmd, parsed) {
26133
- const scanPath = import_node_path81.default.resolve(cmd);
26134
- const stat = await import_node_fs46.promises.stat(scanPath).catch(() => null);
27536
+ const scanPath = import_node_path83.default.resolve(cmd);
27537
+ const stat = await import_node_fs48.promises.stat(scanPath).catch(() => null);
26135
27538
  if (!stat || !stat.isDirectory()) return null;
26136
27539
  const projectExplicit = parsed.project !== null;
26137
- const projectName = projectExplicit ? parsed.project : import_node_path81.default.basename(scanPath);
27540
+ const projectName = projectExplicit ? parsed.project : import_node_path83.default.basename(scanPath);
26138
27541
  const result = await runOrchestrator({
26139
27542
  scanPath,
26140
27543
  project: projectName,
@@ -26323,10 +27726,10 @@ async function runQueryVerb(cmd, parsed) {
26323
27726
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
26324
27727
  const out = [];
26325
27728
  for (const p of parts) {
26326
- const r = import_types85.DivergenceTypeSchema.safeParse(p);
27729
+ const r = import_types90.DivergenceTypeSchema.safeParse(p);
26327
27730
  if (!r.success) {
26328
27731
  console.error(
26329
- `neat divergences: unknown --type "${p}". allowed: ${import_types85.DivergenceTypeSchema.options.join(", ")}`
27732
+ `neat divergences: unknown --type "${p}". allowed: ${import_types90.DivergenceTypeSchema.options.join(", ")}`
26330
27733
  );
26331
27734
  return 2;
26332
27735
  }