@neat.is/core 0.4.5 → 0.4.7

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
@@ -49,9 +49,9 @@ function mountBearerAuth(app, opts) {
49
49
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
50
50
  const publicRead = opts.publicRead === true;
51
51
  app.addHook("preHandler", (req, reply, done) => {
52
- const path45 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
52
+ const path46 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
53
53
  for (const suffix of suffixes) {
54
- if (path45 === suffix || path45.endsWith(suffix)) {
54
+ if (path46 === suffix || path46.endsWith(suffix)) {
55
55
  done();
56
56
  return;
57
57
  }
@@ -562,8 +562,8 @@ __export(cli_exports, {
562
562
  });
563
563
  module.exports = __toCommonJS(cli_exports);
564
564
  init_cjs_shims();
565
- var import_node_path44 = __toESM(require("path"), 1);
566
- var import_node_fs28 = require("fs");
565
+ var import_node_path45 = __toESM(require("path"), 1);
566
+ var import_node_fs29 = require("fs");
567
567
  var import_node_url3 = require("url");
568
568
 
569
569
  // src/graph.ts
@@ -1074,19 +1074,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1074
1074
  function longestIncomingWalk(graph, start, maxDepth) {
1075
1075
  let best = { path: [start], edges: [] };
1076
1076
  const visited = /* @__PURE__ */ new Set([start]);
1077
- function step(node, path45, edges) {
1078
- if (path45.length > best.path.length) {
1079
- best = { path: [...path45], edges: [...edges] };
1077
+ function step(node, path46, edges) {
1078
+ if (path46.length > best.path.length) {
1079
+ best = { path: [...path46], edges: [...edges] };
1080
1080
  }
1081
- if (path45.length - 1 >= maxDepth) return;
1081
+ if (path46.length - 1 >= maxDepth) return;
1082
1082
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1083
1083
  for (const [srcId, edge] of incoming) {
1084
1084
  if (visited.has(srcId)) continue;
1085
1085
  visited.add(srcId);
1086
- path45.push(srcId);
1086
+ path46.push(srcId);
1087
1087
  edges.push(edge);
1088
- step(srcId, path45, edges);
1089
- path45.pop();
1088
+ step(srcId, path46, edges);
1089
+ path46.pop();
1090
1090
  edges.pop();
1091
1091
  visited.delete(srcId);
1092
1092
  }
@@ -1649,6 +1649,86 @@ function hostFromUrl(u) {
1649
1649
  function pickAddress(span) {
1650
1650
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1651
1651
  }
1652
+ var CODE_FILEPATH_ATTR = "code.filepath";
1653
+ var CODE_LINENO_ATTR = "code.lineno";
1654
+ var CODE_FUNCTION_ATTR = "code.function";
1655
+ function toPosix(p) {
1656
+ return p.split("\\").join("/");
1657
+ }
1658
+ function languageForExt(relPath) {
1659
+ const dot = relPath.lastIndexOf(".");
1660
+ if (dot === -1) return void 0;
1661
+ switch (relPath.slice(dot).toLowerCase()) {
1662
+ case ".py":
1663
+ return "python";
1664
+ case ".ts":
1665
+ case ".tsx":
1666
+ return "typescript";
1667
+ case ".js":
1668
+ case ".jsx":
1669
+ case ".mjs":
1670
+ case ".cjs":
1671
+ return "javascript";
1672
+ default:
1673
+ return void 0;
1674
+ }
1675
+ }
1676
+ function relPathForRuntimeFile(filepath, serviceNode) {
1677
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1678
+ const root = serviceNode?.repoPath;
1679
+ if (root && root !== "." && root.length > 0) {
1680
+ const rootPosix = toPosix(root);
1681
+ const anchor = `/${rootPosix}/`;
1682
+ const idx = p.lastIndexOf(anchor);
1683
+ if (idx !== -1) return p.slice(idx + anchor.length);
1684
+ const base = rootPosix.split("/").filter(Boolean).pop();
1685
+ if (base) {
1686
+ const baseAnchor = `/${base}/`;
1687
+ const bidx = p.lastIndexOf(baseAnchor);
1688
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1689
+ }
1690
+ }
1691
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1692
+ return p.length > 0 ? p : null;
1693
+ }
1694
+ function callSiteFromSpan(span, serviceNode) {
1695
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1696
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1697
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1698
+ if (!relPath) return null;
1699
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1700
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1701
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1702
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1703
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1704
+ }
1705
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1706
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1707
+ if (!graph.hasNode(fileNodeId)) {
1708
+ const language = languageForExt(callSite.relPath);
1709
+ const node = {
1710
+ id: fileNodeId,
1711
+ type: import_types3.NodeType.FileNode,
1712
+ service: serviceName,
1713
+ path: callSite.relPath,
1714
+ ...language ? { language } : {},
1715
+ discoveredVia: "otel"
1716
+ };
1717
+ graph.addNode(fileNodeId, node);
1718
+ }
1719
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1720
+ if (!graph.hasEdge(containsId)) {
1721
+ const edge = {
1722
+ id: containsId,
1723
+ source: serviceNodeId,
1724
+ target: fileNodeId,
1725
+ type: import_types3.EdgeType.CONTAINS,
1726
+ provenance: import_types3.Provenance.OBSERVED
1727
+ };
1728
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1729
+ }
1730
+ return fileNodeId;
1731
+ }
1652
1732
  function makeObservedEdgeId(type, source, target) {
1653
1733
  return (0, import_types3.observedEdgeId)(source, target, type);
1654
1734
  }
@@ -1891,6 +1971,9 @@ async function handleSpan(ctx, span) {
1891
1971
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1892
1972
  const isError = span.statusCode === 2;
1893
1973
  cacheSpanService(span, nowMs);
1974
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
1975
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
1976
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1894
1977
  let affectedNode = sourceId;
1895
1978
  if (span.dbSystem) {
1896
1979
  const host = pickAddress(span);
@@ -1900,7 +1983,7 @@ async function handleSpan(ctx, span) {
1900
1983
  const result = upsertObservedEdge(
1901
1984
  ctx.graph,
1902
1985
  import_types3.EdgeType.CONNECTS_TO,
1903
- sourceId,
1986
+ observedSource(),
1904
1987
  targetId,
1905
1988
  ts,
1906
1989
  isError
@@ -1916,7 +1999,7 @@ async function handleSpan(ctx, span) {
1916
1999
  upsertObservedEdge(
1917
2000
  ctx.graph,
1918
2001
  import_types3.EdgeType.CALLS,
1919
- sourceId,
2002
+ observedSource(),
1920
2003
  targetId,
1921
2004
  ts,
1922
2005
  isError
@@ -1928,7 +2011,7 @@ async function handleSpan(ctx, span) {
1928
2011
  upsertObservedEdge(
1929
2012
  ctx.graph,
1930
2013
  import_types3.EdgeType.CALLS,
1931
- sourceId,
2014
+ observedSource(),
1932
2015
  frontierNodeId,
1933
2016
  ts,
1934
2017
  isError
@@ -3571,7 +3654,7 @@ async function addConfigNodes(graph, services, scanPath) {
3571
3654
 
3572
3655
  // src/extract/calls/index.ts
3573
3656
  init_cjs_shims();
3574
- var import_types14 = require("@neat.is/types");
3657
+ var import_types15 = require("@neat.is/types");
3575
3658
 
3576
3659
  // src/extract/calls/http.ts
3577
3660
  init_cjs_shims();
@@ -3579,12 +3662,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3579
3662
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3580
3663
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3581
3664
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3582
- var import_types9 = require("@neat.is/types");
3665
+ var import_types10 = require("@neat.is/types");
3583
3666
 
3584
3667
  // src/extract/calls/shared.ts
3585
3668
  init_cjs_shims();
3586
3669
  var import_node_fs13 = require("fs");
3587
3670
  var import_node_path19 = __toESM(require("path"), 1);
3671
+ var import_types9 = require("@neat.is/types");
3588
3672
  async function walkSourceFiles(dir) {
3589
3673
  const out = [];
3590
3674
  async function walk(current) {
@@ -3624,6 +3708,58 @@ function snippet(text, line) {
3624
3708
  const lines = text.split("\n");
3625
3709
  return (lines[line - 1] ?? "").trim();
3626
3710
  }
3711
+ function toPosix2(p) {
3712
+ return p.split("\\").join("/");
3713
+ }
3714
+ function languageForPath(relPath) {
3715
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3716
+ case ".py":
3717
+ return "python";
3718
+ case ".ts":
3719
+ case ".tsx":
3720
+ return "typescript";
3721
+ case ".js":
3722
+ case ".jsx":
3723
+ case ".mjs":
3724
+ case ".cjs":
3725
+ return "javascript";
3726
+ default:
3727
+ return void 0;
3728
+ }
3729
+ }
3730
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3731
+ let nodesAdded = 0;
3732
+ let edgesAdded = 0;
3733
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3734
+ if (!graph.hasNode(fileNodeId)) {
3735
+ const language = languageForPath(relPath);
3736
+ const node = {
3737
+ id: fileNodeId,
3738
+ type: import_types9.NodeType.FileNode,
3739
+ service: serviceName,
3740
+ path: relPath,
3741
+ ...language ? { language } : {},
3742
+ discoveredVia: "static"
3743
+ };
3744
+ graph.addNode(fileNodeId, node);
3745
+ nodesAdded++;
3746
+ }
3747
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3748
+ if (!graph.hasEdge(containsId)) {
3749
+ const edge = {
3750
+ id: containsId,
3751
+ source: serviceNodeId,
3752
+ target: fileNodeId,
3753
+ type: import_types9.EdgeType.CONTAINS,
3754
+ provenance: import_types9.Provenance.EXTRACTED,
3755
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3756
+ evidence: { file: relPath }
3757
+ };
3758
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3759
+ edgesAdded++;
3760
+ }
3761
+ return { fileNodeId, nodesAdded, edgesAdded };
3762
+ }
3627
3763
 
3628
3764
  // src/extract/calls/http.ts
3629
3765
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3657,16 +3793,16 @@ function callsFromSource(source, parser, knownHosts) {
3657
3793
  const tree = parser.parse(source);
3658
3794
  const literals = [];
3659
3795
  collectStringLiterals(tree.rootNode, literals);
3660
- const targets = /* @__PURE__ */ new Set();
3796
+ const out = [];
3661
3797
  for (const lit of literals) {
3662
3798
  if (isInsideJsxExternalLink(lit.node)) continue;
3663
3799
  for (const host of knownHosts) {
3664
3800
  if (urlMatchesHost(lit.text, host)) {
3665
- targets.add(host);
3801
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3666
3802
  }
3667
3803
  }
3668
3804
  }
3669
- return targets;
3805
+ return out;
3670
3806
  }
3671
3807
  function makeJsParser() {
3672
3808
  const p = new import_tree_sitter.default();
@@ -3689,71 +3825,78 @@ async function addHttpCallEdges(graph, services) {
3689
3825
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3690
3826
  hostToNodeId.set(service.pkg.name, service.node.id);
3691
3827
  }
3828
+ let nodesAdded = 0;
3692
3829
  let edgesAdded = 0;
3693
3830
  for (const service of services) {
3694
3831
  const files = await loadSourceFiles(service.dir);
3695
- const seenTargets = /* @__PURE__ */ new Map();
3832
+ const seen = /* @__PURE__ */ new Set();
3696
3833
  for (const file of files) {
3697
3834
  if (isTestPath(file.path)) continue;
3698
3835
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3699
- let targets;
3836
+ let sites;
3700
3837
  try {
3701
- targets = callsFromSource(file.content, parser, knownHosts);
3838
+ sites = callsFromSource(file.content, parser, knownHosts);
3702
3839
  } catch (err) {
3703
3840
  recordExtractionError("http call extraction", file.path, err);
3704
3841
  continue;
3705
3842
  }
3706
- for (const t of targets) {
3707
- const targetId = hostToNodeId.get(t);
3843
+ if (sites.length === 0) continue;
3844
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3845
+ for (const site of sites) {
3846
+ const targetId = hostToNodeId.get(site.host);
3708
3847
  if (!targetId || targetId === service.node.id) continue;
3709
- if (!seenTargets.has(targetId)) {
3710
- seenTargets.set(targetId, { file: file.path, host: t });
3848
+ const dedupKey = `${relFile}|${targetId}`;
3849
+ if (seen.has(dedupKey)) continue;
3850
+ seen.add(dedupKey);
3851
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3852
+ const ev = {
3853
+ file: relFile,
3854
+ line: site.line,
3855
+ snippet: snippet(file.content, site.line)
3856
+ };
3857
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3858
+ noteExtractedDropped({
3859
+ source: service.node.id,
3860
+ target: targetId,
3861
+ type: import_types10.EdgeType.CALLS,
3862
+ confidence,
3863
+ confidenceKind: "hostname-shape-match",
3864
+ evidence: ev
3865
+ });
3866
+ continue;
3867
+ }
3868
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3869
+ graph,
3870
+ service.pkg.name,
3871
+ service.node.id,
3872
+ relFile
3873
+ );
3874
+ nodesAdded += n;
3875
+ edgesAdded += e;
3876
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3877
+ if (!graph.hasEdge(edgeId)) {
3878
+ const edge = {
3879
+ id: edgeId,
3880
+ source: fileNodeId,
3881
+ target: targetId,
3882
+ type: import_types10.EdgeType.CALLS,
3883
+ provenance: import_types10.Provenance.EXTRACTED,
3884
+ confidence,
3885
+ evidence: ev
3886
+ };
3887
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3888
+ edgesAdded++;
3711
3889
  }
3712
- }
3713
- }
3714
- for (const [targetId, evidenceFile] of seenTargets) {
3715
- const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3716
- const line = lineOf(fileContent, `//${evidenceFile.host}`);
3717
- const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3718
- const ev = {
3719
- file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3720
- line,
3721
- snippet: snippet(fileContent, line)
3722
- };
3723
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3724
- if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3725
- noteExtractedDropped({
3726
- source: service.node.id,
3727
- target: targetId,
3728
- type: import_types9.EdgeType.CALLS,
3729
- confidence,
3730
- confidenceKind: "hostname-shape-match",
3731
- evidence: ev
3732
- });
3733
- continue;
3734
- }
3735
- const edge = {
3736
- id: edgeId,
3737
- source: service.node.id,
3738
- target: targetId,
3739
- type: import_types9.EdgeType.CALLS,
3740
- provenance: import_types9.Provenance.EXTRACTED,
3741
- confidence,
3742
- evidence: ev
3743
- };
3744
- if (!graph.hasEdge(edge.id)) {
3745
- graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
3746
- edgesAdded++;
3747
3890
  }
3748
3891
  }
3749
3892
  }
3750
- return edgesAdded;
3893
+ return { nodesAdded, edgesAdded };
3751
3894
  }
3752
3895
 
3753
3896
  // src/extract/calls/kafka.ts
3754
3897
  init_cjs_shims();
3755
3898
  var import_node_path21 = __toESM(require("path"), 1);
3756
- var import_types10 = require("@neat.is/types");
3899
+ var import_types11 = require("@neat.is/types");
3757
3900
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3758
3901
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
3759
3902
  function findAll(re, text) {
@@ -3774,7 +3917,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3774
3917
  seen.add(key);
3775
3918
  const line = lineOf(file.content, topic);
3776
3919
  out.push({
3777
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3920
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3778
3921
  name: topic,
3779
3922
  kind: "kafka-topic",
3780
3923
  edgeType,
@@ -3797,7 +3940,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3797
3940
  // src/extract/calls/redis.ts
3798
3941
  init_cjs_shims();
3799
3942
  var import_node_path22 = __toESM(require("path"), 1);
3800
- var import_types11 = require("@neat.is/types");
3943
+ var import_types12 = require("@neat.is/types");
3801
3944
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3802
3945
  function redisEndpointsFromFile(file, serviceDir) {
3803
3946
  const out = [];
@@ -3810,7 +3953,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3810
3953
  seen.add(host);
3811
3954
  const line = lineOf(file.content, host);
3812
3955
  out.push({
3813
- infraId: (0, import_types11.infraId)("redis", host),
3956
+ infraId: (0, import_types12.infraId)("redis", host),
3814
3957
  name: host,
3815
3958
  kind: "redis",
3816
3959
  edgeType: "CALLS",
@@ -3831,7 +3974,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3831
3974
  // src/extract/calls/aws.ts
3832
3975
  init_cjs_shims();
3833
3976
  var import_node_path23 = __toESM(require("path"), 1);
3834
- var import_types12 = require("@neat.is/types");
3977
+ var import_types13 = require("@neat.is/types");
3835
3978
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3836
3979
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3837
3980
  function hasMarker(text, markers) {
@@ -3855,7 +3998,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3855
3998
  seen.add(key);
3856
3999
  const line = lineOf(file.content, name);
3857
4000
  out.push({
3858
- infraId: (0, import_types12.infraId)(kind, name),
4001
+ infraId: (0, import_types13.infraId)(kind, name),
3859
4002
  name,
3860
4003
  kind,
3861
4004
  edgeType: "CALLS",
@@ -3890,7 +4033,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3890
4033
  // src/extract/calls/grpc.ts
3891
4034
  init_cjs_shims();
3892
4035
  var import_node_path24 = __toESM(require("path"), 1);
3893
- var import_types13 = require("@neat.is/types");
4036
+ var import_types14 = require("@neat.is/types");
3894
4037
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3895
4038
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3896
4039
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3938,7 +4081,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3938
4081
  const { kind } = classified;
3939
4082
  const line = lineOf(file.content, m[0]);
3940
4083
  out.push({
3941
- infraId: (0, import_types13.infraId)(kind, name),
4084
+ infraId: (0, import_types14.infraId)(kind, name),
3942
4085
  name,
3943
4086
  kind,
3944
4087
  edgeType: "CALLS",
@@ -3960,11 +4103,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3960
4103
  function edgeTypeFromEndpoint(ep) {
3961
4104
  switch (ep.edgeType) {
3962
4105
  case "PUBLISHES_TO":
3963
- return import_types14.EdgeType.PUBLISHES_TO;
4106
+ return import_types15.EdgeType.PUBLISHES_TO;
3964
4107
  case "CONSUMES_FROM":
3965
- return import_types14.EdgeType.CONSUMES_FROM;
4108
+ return import_types15.EdgeType.CONSUMES_FROM;
3966
4109
  default:
3967
- return import_types14.EdgeType.CALLS;
4110
+ return import_types15.EdgeType.CALLS;
3968
4111
  }
3969
4112
  }
3970
4113
  function isAwsKind(kind) {
@@ -3991,7 +4134,7 @@ async function addExternalEndpointEdges(graph, services) {
3991
4134
  if (!graph.hasNode(ep.infraId)) {
3992
4135
  const node = {
3993
4136
  id: ep.infraId,
3994
- type: import_types14.NodeType.InfraNode,
4137
+ type: import_types15.NodeType.InfraNode,
3995
4138
  name: ep.name,
3996
4139
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3997
4140
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4003,11 +4146,8 @@ async function addExternalEndpointEdges(graph, services) {
4003
4146
  nodesAdded++;
4004
4147
  }
4005
4148
  const edgeType = edgeTypeFromEndpoint(ep);
4006
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
4007
- if (seenEdges.has(edgeId)) continue;
4008
- seenEdges.add(edgeId);
4009
- const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
4010
- if (!(0, import_types14.passesExtractedFloor)(confidence)) {
4149
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4150
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4011
4151
  noteExtractedDropped({
4012
4152
  source: service.node.id,
4013
4153
  target: ep.infraId,
@@ -4018,13 +4158,25 @@ async function addExternalEndpointEdges(graph, services) {
4018
4158
  });
4019
4159
  continue;
4020
4160
  }
4161
+ const relFile = toPosix2(ep.evidence.file);
4162
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4163
+ graph,
4164
+ service.pkg.name,
4165
+ service.node.id,
4166
+ relFile
4167
+ );
4168
+ nodesAdded += n;
4169
+ edgesAdded += e;
4170
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4171
+ if (seenEdges.has(edgeId)) continue;
4172
+ seenEdges.add(edgeId);
4021
4173
  if (!graph.hasEdge(edgeId)) {
4022
4174
  const edge = {
4023
4175
  id: edgeId,
4024
- source: service.node.id,
4176
+ source: fileNodeId,
4025
4177
  target: ep.infraId,
4026
4178
  type: edgeType,
4027
- provenance: import_types14.Provenance.EXTRACTED,
4179
+ provenance: import_types15.Provenance.EXTRACTED,
4028
4180
  confidence,
4029
4181
  evidence: ep.evidence
4030
4182
  };
@@ -4036,11 +4188,11 @@ async function addExternalEndpointEdges(graph, services) {
4036
4188
  return { nodesAdded, edgesAdded };
4037
4189
  }
4038
4190
  async function addCallEdges(graph, services) {
4039
- const httpEdges = await addHttpCallEdges(graph, services);
4191
+ const http2 = await addHttpCallEdges(graph, services);
4040
4192
  const ext = await addExternalEndpointEdges(graph, services);
4041
4193
  return {
4042
- nodesAdded: ext.nodesAdded,
4043
- edgesAdded: httpEdges + ext.edgesAdded
4194
+ nodesAdded: http2.nodesAdded + ext.nodesAdded,
4195
+ edgesAdded: http2.edgesAdded + ext.edgesAdded
4044
4196
  };
4045
4197
  }
4046
4198
 
@@ -4050,15 +4202,15 @@ init_cjs_shims();
4050
4202
  // src/extract/infra/docker-compose.ts
4051
4203
  init_cjs_shims();
4052
4204
  var import_node_path25 = __toESM(require("path"), 1);
4053
- var import_types16 = require("@neat.is/types");
4205
+ var import_types17 = require("@neat.is/types");
4054
4206
 
4055
4207
  // src/extract/infra/shared.ts
4056
4208
  init_cjs_shims();
4057
- var import_types15 = require("@neat.is/types");
4209
+ var import_types16 = require("@neat.is/types");
4058
4210
  function makeInfraNode(kind, name, provider = "self", extras) {
4059
4211
  return {
4060
- id: (0, import_types15.infraId)(kind, name),
4061
- type: import_types15.NodeType.InfraNode,
4212
+ id: (0, import_types16.infraId)(kind, name),
4213
+ type: import_types16.NodeType.InfraNode,
4062
4214
  name,
4063
4215
  provider,
4064
4216
  kind,
@@ -4137,15 +4289,15 @@ async function addComposeInfra(graph, scanPath, services) {
4137
4289
  for (const dep of dependsOnList(svc.depends_on)) {
4138
4290
  const targetId = composeNameToNodeId.get(dep);
4139
4291
  if (!targetId) continue;
4140
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4292
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4141
4293
  if (graph.hasEdge(edgeId)) continue;
4142
4294
  const edge = {
4143
4295
  id: edgeId,
4144
4296
  source: sourceId,
4145
4297
  target: targetId,
4146
- type: import_types16.EdgeType.DEPENDS_ON,
4147
- provenance: import_types16.Provenance.EXTRACTED,
4148
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4298
+ type: import_types17.EdgeType.DEPENDS_ON,
4299
+ provenance: import_types17.Provenance.EXTRACTED,
4300
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4149
4301
  evidence: { file: evidenceFile }
4150
4302
  };
4151
4303
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4159,7 +4311,7 @@ async function addComposeInfra(graph, scanPath, services) {
4159
4311
  init_cjs_shims();
4160
4312
  var import_node_path26 = __toESM(require("path"), 1);
4161
4313
  var import_node_fs14 = require("fs");
4162
- var import_types17 = require("@neat.is/types");
4314
+ var import_types18 = require("@neat.is/types");
4163
4315
  function runtimeImage(content) {
4164
4316
  const lines = content.split("\n");
4165
4317
  let last = null;
@@ -4198,15 +4350,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4198
4350
  graph.addNode(node.id, node);
4199
4351
  nodesAdded++;
4200
4352
  }
4201
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4353
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4202
4354
  if (!graph.hasEdge(edgeId)) {
4203
4355
  const edge = {
4204
4356
  id: edgeId,
4205
4357
  source: service.node.id,
4206
4358
  target: node.id,
4207
- type: import_types17.EdgeType.RUNS_ON,
4208
- provenance: import_types17.Provenance.EXTRACTED,
4209
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4359
+ type: import_types18.EdgeType.RUNS_ON,
4360
+ provenance: import_types18.Provenance.EXTRACTED,
4361
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4210
4362
  evidence: {
4211
4363
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4212
4364
  }
@@ -4334,17 +4486,29 @@ var import_node_path30 = __toESM(require("path"), 1);
4334
4486
  init_cjs_shims();
4335
4487
  var import_node_fs17 = require("fs");
4336
4488
  var import_node_path29 = __toESM(require("path"), 1);
4337
- var import_types18 = require("@neat.is/types");
4489
+ var import_types19 = require("@neat.is/types");
4490
+ function dropOrphanedFileNodes(graph) {
4491
+ const orphans = [];
4492
+ graph.forEachNode((id, attrs) => {
4493
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4494
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4495
+ orphans.push(id);
4496
+ }
4497
+ });
4498
+ for (const id of orphans) graph.dropNode(id);
4499
+ return orphans.length;
4500
+ }
4338
4501
  function retireEdgesByFile(graph, file) {
4339
4502
  const normalized = file.split("\\").join("/");
4340
4503
  const toDrop = [];
4341
4504
  graph.forEachEdge((id, attrs) => {
4342
4505
  const edge = attrs;
4343
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4506
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4344
4507
  if (!edge.evidence?.file) return;
4345
4508
  if (edge.evidence.file === normalized) toDrop.push(id);
4346
4509
  });
4347
4510
  for (const id of toDrop) graph.dropEdge(id);
4511
+ dropOrphanedFileNodes(graph);
4348
4512
  return toDrop.length;
4349
4513
  }
4350
4514
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
@@ -4352,7 +4516,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4352
4516
  const bases = [scanPath, ...serviceDirs];
4353
4517
  graph.forEachEdge((id, attrs) => {
4354
4518
  const edge = attrs;
4355
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4519
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4356
4520
  const evidenceFile = edge.evidence?.file;
4357
4521
  if (!evidenceFile) return;
4358
4522
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4363,6 +4527,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4363
4527
  if (!found) toDrop.push(id);
4364
4528
  });
4365
4529
  for (const id of toDrop) graph.dropEdge(id);
4530
+ dropOrphanedFileNodes(graph);
4366
4531
  return toDrop.length;
4367
4532
  }
4368
4533
 
@@ -4430,7 +4595,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4430
4595
 
4431
4596
  // src/divergences.ts
4432
4597
  init_cjs_shims();
4433
- var import_types19 = require("@neat.is/types");
4598
+ var import_types20 = require("@neat.is/types");
4434
4599
  function bucketKey(source, target, type) {
4435
4600
  return `${type}|${source}|${target}`;
4436
4601
  }
@@ -4438,22 +4603,22 @@ function bucketEdges(graph) {
4438
4603
  const buckets = /* @__PURE__ */ new Map();
4439
4604
  graph.forEachEdge((id, attrs) => {
4440
4605
  const e = attrs;
4441
- const parsed = (0, import_types19.parseEdgeId)(id);
4606
+ const parsed = (0, import_types20.parseEdgeId)(id);
4442
4607
  const provenance = parsed?.provenance ?? e.provenance;
4443
4608
  const key = bucketKey(e.source, e.target, e.type);
4444
4609
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4445
4610
  switch (provenance) {
4446
- case import_types19.Provenance.EXTRACTED:
4611
+ case import_types20.Provenance.EXTRACTED:
4447
4612
  cur.extracted = e;
4448
4613
  break;
4449
- case import_types19.Provenance.OBSERVED:
4614
+ case import_types20.Provenance.OBSERVED:
4450
4615
  cur.observed = e;
4451
4616
  break;
4452
- case import_types19.Provenance.INFERRED:
4617
+ case import_types20.Provenance.INFERRED:
4453
4618
  cur.inferred = e;
4454
4619
  break;
4455
4620
  default:
4456
- if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4621
+ if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4457
4622
  }
4458
4623
  buckets.set(key, cur);
4459
4624
  });
@@ -4462,7 +4627,7 @@ function bucketEdges(graph) {
4462
4627
  function nodeIsFrontier(graph, nodeId) {
4463
4628
  if (!graph.hasNode(nodeId)) return false;
4464
4629
  const attrs = graph.getNodeAttributes(nodeId);
4465
- return attrs.type === import_types19.NodeType.FrontierNode;
4630
+ return attrs.type === import_types20.NodeType.FrontierNode;
4466
4631
  }
4467
4632
  function clampConfidence(n) {
4468
4633
  if (!Number.isFinite(n)) return 0;
@@ -4483,6 +4648,7 @@ function gradedConfidence(edge) {
4483
4648
  }
4484
4649
  function detectMissingDivergences(graph, bucket) {
4485
4650
  const out = [];
4651
+ if (bucket.type === import_types20.EdgeType.CONTAINS) return out;
4486
4652
  if (bucket.extracted && !bucket.observed) {
4487
4653
  if (!nodeIsFrontier(graph, bucket.target)) {
4488
4654
  out.push({
@@ -4523,7 +4689,7 @@ function declaredHostFor(svc) {
4523
4689
  function hasExtractedConfiguredBy(graph, svcId) {
4524
4690
  for (const edgeId of graph.outboundEdges(svcId)) {
4525
4691
  const e = graph.getEdgeAttributes(edgeId);
4526
- if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4692
+ if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4527
4693
  return true;
4528
4694
  }
4529
4695
  }
@@ -4536,10 +4702,10 @@ function detectHostMismatch(graph, svcId, svc) {
4536
4702
  const out = [];
4537
4703
  for (const edgeId of graph.outboundEdges(svcId)) {
4538
4704
  const edge = graph.getEdgeAttributes(edgeId);
4539
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4540
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4705
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4706
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4541
4707
  const target = graph.getNodeAttributes(edge.target);
4542
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4708
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4543
4709
  const observedHost = target.host?.trim();
4544
4710
  if (!observedHost) continue;
4545
4711
  if (observedHost === declaredHost) continue;
@@ -4561,10 +4727,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4561
4727
  const deps = svc.dependencies ?? {};
4562
4728
  for (const edgeId of graph.outboundEdges(svcId)) {
4563
4729
  const edge = graph.getEdgeAttributes(edgeId);
4564
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4565
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4730
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4731
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4566
4732
  const target = graph.getNodeAttributes(edge.target);
4567
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4733
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4568
4734
  for (const pair of compatPairs()) {
4569
4735
  if (pair.engine !== target.engine) continue;
4570
4736
  const declared = deps[pair.driver];
@@ -4625,7 +4791,7 @@ function computeDivergences(graph, opts = {}) {
4625
4791
  }
4626
4792
  graph.forEachNode((nodeId, attrs) => {
4627
4793
  const n = attrs;
4628
- if (n.type !== import_types19.NodeType.ServiceNode) return;
4794
+ if (n.type !== import_types20.NodeType.ServiceNode) return;
4629
4795
  const svc = n;
4630
4796
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4631
4797
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4658,7 +4824,7 @@ function computeDivergences(graph, opts = {}) {
4658
4824
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4659
4825
  return a.target.localeCompare(b.target);
4660
4826
  });
4661
- return import_types19.DivergenceResultSchema.parse({
4827
+ return import_types20.DivergenceResultSchema.parse({
4662
4828
  divergences: filtered,
4663
4829
  totalAffected: filtered.length,
4664
4830
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4669,7 +4835,7 @@ function computeDivergences(graph, opts = {}) {
4669
4835
  init_cjs_shims();
4670
4836
  var import_node_fs18 = require("fs");
4671
4837
  var import_node_path31 = __toESM(require("path"), 1);
4672
- var import_types20 = require("@neat.is/types");
4838
+ var import_types21 = require("@neat.is/types");
4673
4839
  var SCHEMA_VERSION = 4;
4674
4840
  function migrateV1ToV2(payload) {
4675
4841
  const nodes = payload.graph.nodes;
@@ -4691,12 +4857,12 @@ function migrateV2ToV3(payload) {
4691
4857
  for (const edge of edges) {
4692
4858
  const attrs = edge.attributes;
4693
4859
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4694
- attrs.provenance = import_types20.Provenance.OBSERVED;
4860
+ attrs.provenance = import_types21.Provenance.OBSERVED;
4695
4861
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4696
4862
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4697
4863
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4698
4864
  if (type && source && target) {
4699
- const newId = (0, import_types20.observedEdgeId)(source, target, type);
4865
+ const newId = (0, import_types21.observedEdgeId)(source, target, type);
4700
4866
  attrs.id = newId;
4701
4867
  if (edge.key) edge.key = newId;
4702
4868
  }
@@ -4816,7 +4982,7 @@ ${NEAT_OUT_LINE}
4816
4982
 
4817
4983
  // src/summary.ts
4818
4984
  init_cjs_shims();
4819
- var import_types21 = require("@neat.is/types");
4985
+ var import_types22 = require("@neat.is/types");
4820
4986
  function renderOtelEnvBlock() {
4821
4987
  return [
4822
4988
  "for prod OTel routing, set these in your deploy platform's env:",
@@ -4826,19 +4992,19 @@ function renderOtelEnvBlock() {
4826
4992
  }
4827
4993
  function findIncompatServices(nodes) {
4828
4994
  return nodes.filter(
4829
- (n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4995
+ (n) => n.type === import_types22.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4830
4996
  );
4831
4997
  }
4832
4998
  function servicesWithoutObserved(nodes, edges) {
4833
4999
  const seen = /* @__PURE__ */ new Set();
4834
5000
  for (const e of edges) {
4835
- if (e.provenance === import_types21.Provenance.OBSERVED) {
5001
+ if (e.provenance === import_types22.Provenance.OBSERVED) {
4836
5002
  seen.add(e.source);
4837
5003
  seen.add(e.target);
4838
5004
  }
4839
5005
  }
4840
5006
  return nodes.filter(
4841
- (n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
5007
+ (n) => n.type === import_types22.NodeType.ServiceNode && !seen.has(n.id)
4842
5008
  );
4843
5009
  }
4844
5010
  function formatDivergence(d) {
@@ -4920,7 +5086,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
4920
5086
  init_cjs_shims();
4921
5087
  var import_fastify = __toESM(require("fastify"), 1);
4922
5088
  var import_cors = __toESM(require("@fastify/cors"), 1);
4923
- var import_types23 = require("@neat.is/types");
5089
+ var import_types24 = require("@neat.is/types");
4924
5090
 
4925
5091
  // src/diff.ts
4926
5092
  init_cjs_shims();
@@ -5056,7 +5222,7 @@ init_cjs_shims();
5056
5222
  var import_node_fs21 = require("fs");
5057
5223
  var import_node_os2 = __toESM(require("os"), 1);
5058
5224
  var import_node_path34 = __toESM(require("path"), 1);
5059
- var import_types22 = require("@neat.is/types");
5225
+ var import_types23 = require("@neat.is/types");
5060
5226
  var LOCK_TIMEOUT_MS = 5e3;
5061
5227
  var LOCK_RETRY_MS = 50;
5062
5228
  function neatHome() {
@@ -5135,10 +5301,10 @@ async function readRegistry() {
5135
5301
  throw err;
5136
5302
  }
5137
5303
  const parsed = JSON.parse(raw);
5138
- return import_types22.RegistryFileSchema.parse(parsed);
5304
+ return import_types23.RegistryFileSchema.parse(parsed);
5139
5305
  }
5140
5306
  async function writeRegistry(reg) {
5141
- const validated = import_types22.RegistryFileSchema.parse(reg);
5307
+ const validated = import_types23.RegistryFileSchema.parse(reg);
5142
5308
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5143
5309
  }
5144
5310
  var ProjectNameCollisionError = class extends Error {
@@ -5401,11 +5567,11 @@ function registerRoutes(scope, ctx) {
5401
5567
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5402
5568
  const parsed = [];
5403
5569
  for (const c of candidates) {
5404
- const r = import_types23.DivergenceTypeSchema.safeParse(c);
5570
+ const r = import_types24.DivergenceTypeSchema.safeParse(c);
5405
5571
  if (!r.success) {
5406
5572
  return reply.code(400).send({
5407
5573
  error: `unknown divergence type "${c}"`,
5408
- allowed: import_types23.DivergenceTypeSchema.options
5574
+ allowed: import_types24.DivergenceTypeSchema.options
5409
5575
  });
5410
5576
  }
5411
5577
  parsed.push(r.data);
@@ -5618,7 +5784,7 @@ function registerRoutes(scope, ctx) {
5618
5784
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5619
5785
  let violations = await log.readAll();
5620
5786
  if (req.query.severity) {
5621
- const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5787
+ const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
5622
5788
  if (!sev.success) {
5623
5789
  return reply.code(400).send({
5624
5790
  error: "invalid severity",
@@ -5635,7 +5801,7 @@ function registerRoutes(scope, ctx) {
5635
5801
  scope.post("/policies/check", async (req, reply) => {
5636
5802
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5637
5803
  if (!proj) return;
5638
- const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5804
+ const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5639
5805
  if (!parsed.success) {
5640
5806
  return reply.code(400).send({
5641
5807
  error: "invalid /policies/check body",
@@ -6569,38 +6735,119 @@ var import_node_path40 = __toESM(require("path"), 1);
6569
6735
  // src/installers/templates.ts
6570
6736
  init_cjs_shims();
6571
6737
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
6738
+ var OTEL_INIT_STAMP = "// neat-template-version: 2 \u2014 file-first call-site capture (ADR-089).";
6739
+ function neatCallsiteProcessorSource(ts) {
6740
+ const stackT = ts ? ": string" : "";
6741
+ const spanT = ts ? ": any" : "";
6742
+ const strOpt = ts ? ": string | undefined" : "";
6743
+ return `function __neatPickUserFrame(stack${stackT}) {
6744
+ const lines = String(stack || '').split('\\n')
6745
+ for (let i = 0; i < lines.length; i++) {
6746
+ const raw = lines[i].trim()
6747
+ if (raw.indexOf('at ') !== 0) continue
6748
+ if (raw.indexOf('node_modules') !== -1) continue
6749
+ if (raw.indexOf('@opentelemetry') !== -1) continue
6750
+ if (raw.indexOf('node:') !== -1) continue
6751
+ if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
6752
+ const bodyText = raw.slice(3)
6753
+ const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
6754
+ if (!loc) continue
6755
+ const paren = bodyText.lastIndexOf('(')
6756
+ let filepath${strOpt}
6757
+ let fn${strOpt}
6758
+ if (paren !== -1) {
6759
+ fn = bodyText.slice(0, paren).trim()
6760
+ if (fn.indexOf('async ') === 0) fn = fn.slice(6)
6761
+ if (fn.indexOf('new ') === 0) fn = fn.slice(4)
6762
+ filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
6763
+ } else {
6764
+ filepath = bodyText.slice(0, bodyText.length - loc[0].length)
6765
+ }
6766
+ if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
6767
+ if (!filepath) continue
6768
+ return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
6769
+ }
6770
+ return null
6771
+ }
6772
+
6773
+ class NeatCallSiteSpanProcessor {
6774
+ onStart(span${spanT}) {
6775
+ if (!span || (span.kind !== 2 && span.kind !== 3)) return
6776
+ const frame = __neatPickUserFrame(new Error().stack)
6777
+ if (!frame) return
6778
+ span.setAttribute('code.filepath', frame.filepath)
6779
+ if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
6780
+ if (frame.function) span.setAttribute('code.function', frame.function)
6781
+ }
6782
+ onEnd() {}
6783
+ forceFlush() { return Promise.resolve() }
6784
+ shutdown() { return Promise.resolve() }
6785
+ }`;
6786
+ }
6787
+ var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
6788
+ var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
6789
+ function neatRegisterCallsiteSource(ts) {
6790
+ const providerT = ts ? ": any" : "";
6791
+ return `try {
6792
+ const provider${providerT} = trace.getTracerProvider()
6793
+ const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
6794
+ if (delegate && typeof delegate.addSpanProcessor === 'function') {
6795
+ delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
6796
+ }
6797
+ } catch {
6798
+ // capture is best-effort; span export is unaffected
6799
+ }`;
6800
+ }
6572
6801
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
6802
+ ${OTEL_INIT_STAMP}
6573
6803
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6574
6804
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6575
6805
 
6576
6806
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6577
6807
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6808
+ const { trace } = require('@opentelemetry/api')
6809
+
6810
+ ${CALLSITE_PROCESSOR_JS}
6578
6811
 
6579
6812
  const instrumentations = [getNodeAutoInstrumentations()]
6580
6813
  __INSTRUMENTATION_BLOCK__
6581
- new NodeSDK({ instrumentations }).start()
6814
+ const sdk = new NodeSDK({ instrumentations })
6815
+ sdk.start()
6816
+ ${neatRegisterCallsiteSource(false)}
6582
6817
  `;
6583
6818
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
6819
+ ${OTEL_INIT_STAMP}
6584
6820
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6585
6821
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6586
6822
 
6587
6823
  import { NodeSDK } from '@opentelemetry/sdk-node'
6588
6824
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6825
+ import { trace } from '@opentelemetry/api'
6826
+
6827
+ ${CALLSITE_PROCESSOR_JS}
6589
6828
 
6590
6829
  const instrumentations = [getNodeAutoInstrumentations()]
6591
6830
  __INSTRUMENTATION_BLOCK__
6592
- new NodeSDK({ instrumentations }).start()
6831
+ const sdk = new NodeSDK({ instrumentations })
6832
+ sdk.start()
6833
+ ${neatRegisterCallsiteSource(false)}
6593
6834
  `;
6594
6835
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
6836
+ ${OTEL_INIT_STAMP}
6595
6837
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6596
6838
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6597
6839
 
6598
6840
  import { NodeSDK } from '@opentelemetry/sdk-node'
6599
6841
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6842
+ import { trace } from '@opentelemetry/api'
6843
+
6844
+ ${CALLSITE_PROCESSOR_TS}
6600
6845
 
6601
6846
  const instrumentations = [getNodeAutoInstrumentations()]
6602
6847
  __INSTRUMENTATION_BLOCK__
6603
- new NodeSDK({ instrumentations }).start()
6848
+ const sdk = new NodeSDK({ instrumentations })
6849
+ sdk.start()
6850
+ ${neatRegisterCallsiteSource(true)}
6604
6851
  `;
6605
6852
  function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
6606
6853
  const block = registrations.length === 0 ? "" : `
@@ -6753,13 +7000,20 @@ var SDK_PACKAGES = [
6753
7000
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
6754
7001
  { name: "@opentelemetry/auto-instrumentations-node", version: "^0.55.0" }
6755
7002
  ];
7003
+ function getMajor(versionRange) {
7004
+ if (!versionRange) return 0;
7005
+ const match = versionRange.match(/(\d+)/);
7006
+ return match ? parseInt(match[1], 10) : 0;
7007
+ }
6756
7008
  function detectNonBundledInstrumentations(pkg) {
6757
7009
  const deps = allDeps(pkg);
6758
7010
  const out = [];
6759
7011
  if ("@prisma/client" in deps) {
7012
+ const prismaMajor = getMajor(deps["@prisma/client"]);
7013
+ const prismaInstrVersion = prismaMajor >= 6 ? "^6.0.0" : "^5.0.0";
6760
7014
  out.push({
6761
7015
  pkg: "@prisma/instrumentation",
6762
- version: "^5.0.0",
7016
+ version: prismaInstrVersion,
6763
7017
  registration: "instrumentations.push(new (require('@prisma/instrumentation').PrismaInstrumentation)())"
6764
7018
  });
6765
7019
  }
@@ -6817,6 +7071,23 @@ async function exists2(p) {
6817
7071
  return false;
6818
7072
  }
6819
7073
  }
7074
+ async function readFileMaybe(p) {
7075
+ try {
7076
+ return await import_node_fs25.promises.readFile(p, "utf8");
7077
+ } catch {
7078
+ return null;
7079
+ }
7080
+ }
7081
+ async function planOtelInitGeneration(file, contents) {
7082
+ const existing = await readFileMaybe(file);
7083
+ if (existing === null) {
7084
+ return { file, contents, skipIfExists: true };
7085
+ }
7086
+ if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
7087
+ return { file, contents, skipIfExists: false };
7088
+ }
7089
+ return null;
7090
+ }
6820
7091
  async function detect(serviceDir) {
6821
7092
  const pkg = await readPackageJson(serviceDir);
6822
7093
  return pkg !== null && typeof pkg.name === "string";
@@ -7547,18 +7818,11 @@ async function plan(serviceDir, opts) {
7547
7818
  const projectName = projectToken(pkg, serviceDir, project);
7548
7819
  const registrations = nonBundled.map((i) => i.registration);
7549
7820
  const generatedFiles = [];
7550
- if (!await exists2(otelInitFile)) {
7551
- generatedFiles.push({
7552
- file: otelInitFile,
7553
- contents: renderNodeOtelInit(
7554
- otelInitContents(flavor),
7555
- svcName,
7556
- projectName,
7557
- registrations
7558
- ),
7559
- skipIfExists: true
7560
- });
7561
- }
7821
+ const otelInitGen = await planOtelInitGeneration(
7822
+ otelInitFile,
7823
+ renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
7824
+ );
7825
+ if (otelInitGen) generatedFiles.push(otelInitGen);
7562
7826
  if (!await exists2(envNeatFile)) {
7563
7827
  generatedFiles.push({
7564
7828
  file: envNeatFile,
@@ -8088,19 +8352,100 @@ function renderPatch(sections) {
8088
8352
 
8089
8353
  // src/orchestrator.ts
8090
8354
  init_cjs_shims();
8091
- var import_node_fs27 = require("fs");
8355
+ var import_node_fs28 = require("fs");
8092
8356
  var import_node_http = __toESM(require("http"), 1);
8093
8357
  var import_node_net = __toESM(require("net"), 1);
8358
+ var import_node_path43 = __toESM(require("path"), 1);
8359
+ var import_node_child_process3 = require("child_process");
8360
+ var import_node_readline = __toESM(require("readline"), 1);
8361
+
8362
+ // src/installers/package-manager.ts
8363
+ init_cjs_shims();
8364
+ var import_node_fs27 = require("fs");
8094
8365
  var import_node_path42 = __toESM(require("path"), 1);
8095
8366
  var import_node_child_process2 = require("child_process");
8096
- var import_node_readline = __toESM(require("readline"), 1);
8367
+ var LOCKFILE_PRIORITY = [
8368
+ { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
8369
+ { lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
8370
+ { lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
8371
+ {
8372
+ lockfile: "package-lock.json",
8373
+ pm: "npm",
8374
+ args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
8375
+ }
8376
+ ];
8377
+ var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
8378
+ async function exists4(p) {
8379
+ try {
8380
+ await import_node_fs27.promises.access(p);
8381
+ return true;
8382
+ } catch {
8383
+ return false;
8384
+ }
8385
+ }
8386
+ async function detectPackageManager(serviceDir) {
8387
+ let dir = import_node_path42.default.resolve(serviceDir);
8388
+ const stops = /* @__PURE__ */ new Set();
8389
+ for (let i = 0; i < 64; i++) {
8390
+ if (stops.has(dir)) break;
8391
+ stops.add(dir);
8392
+ for (const candidate of LOCKFILE_PRIORITY) {
8393
+ const lockPath = import_node_path42.default.join(dir, candidate.lockfile);
8394
+ if (await exists4(lockPath)) {
8395
+ return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
8396
+ }
8397
+ }
8398
+ const parent = import_node_path42.default.dirname(dir);
8399
+ if (parent === dir) break;
8400
+ dir = parent;
8401
+ }
8402
+ return { pm: "npm", cwd: import_node_path42.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
8403
+ }
8404
+ async function runPackageManagerInstall(cmd) {
8405
+ return new Promise((resolve) => {
8406
+ const child = (0, import_node_child_process2.spawn)(cmd.pm, cmd.args, {
8407
+ cwd: cmd.cwd,
8408
+ // Inherit PATH + HOME so the user's installed managers resolve.
8409
+ env: process.env,
8410
+ // `false` keeps the parent in control of cleanup if the orchestrator
8411
+ // exits before install finishes. Cross-platform-safe.
8412
+ shell: false,
8413
+ stdio: ["ignore", "ignore", "pipe"]
8414
+ });
8415
+ let stderr = "";
8416
+ child.stderr?.on("data", (chunk) => {
8417
+ stderr += chunk.toString("utf8");
8418
+ });
8419
+ child.on("error", (err) => {
8420
+ resolve({
8421
+ pm: cmd.pm,
8422
+ cwd: cmd.cwd,
8423
+ args: cmd.args,
8424
+ exitCode: 127,
8425
+ stderr: stderr + `
8426
+ ${err.message}`
8427
+ });
8428
+ });
8429
+ child.on("close", (code) => {
8430
+ resolve({
8431
+ pm: cmd.pm,
8432
+ cwd: cmd.cwd,
8433
+ args: cmd.args,
8434
+ exitCode: code ?? 1,
8435
+ stderr: stderr.trim()
8436
+ });
8437
+ });
8438
+ });
8439
+ }
8440
+
8441
+ // src/orchestrator.ts
8097
8442
  async function extractAndPersist(opts) {
8098
8443
  const services = await discoverServices(opts.scanPath);
8099
8444
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
8100
8445
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
8101
8446
  resetGraph(graphKey);
8102
8447
  const graph = getGraph(graphKey);
8103
- const projectPaths = pathsForProject(graphKey, import_node_path42.default.join(opts.scanPath, "neat-out"));
8448
+ const projectPaths = pathsForProject(graphKey, import_node_path43.default.join(opts.scanPath, "neat-out"));
8104
8449
  const extraction = await extractFromDirectory(graph, opts.scanPath, {
8105
8450
  errorsPath: projectPaths.errorsPath
8106
8451
  });
@@ -8118,12 +8463,15 @@ async function extractAndPersist(opts) {
8118
8463
  errorsPath: projectPaths.errorsPath
8119
8464
  };
8120
8465
  }
8121
- async function applyInstallersOver(services, project) {
8466
+ async function applyInstallersOver(services, project, options = {}) {
8467
+ const resolveManager = options.resolveManager ?? detectPackageManager;
8468
+ const runInstall = options.runInstall ?? runPackageManagerInstall;
8122
8469
  let instrumented = 0;
8123
8470
  let already = 0;
8124
8471
  let libOnly = 0;
8125
8472
  let browserBundle = 0;
8126
8473
  let reactNative = 0;
8474
+ const installPlans = /* @__PURE__ */ new Map();
8127
8475
  for (const svc of services) {
8128
8476
  const installer = await pickInstaller(svc.dir);
8129
8477
  if (!installer) continue;
@@ -8133,8 +8481,14 @@ async function applyInstallersOver(services, project) {
8133
8481
  continue;
8134
8482
  }
8135
8483
  const outcome = await installer.apply(plan3);
8136
- if (outcome.outcome === "instrumented") instrumented++;
8137
- else if (outcome.outcome === "already-instrumented") already++;
8484
+ if (outcome.outcome === "instrumented") {
8485
+ instrumented++;
8486
+ if (plan3.dependencyEdits.length > 0) {
8487
+ const cmd = await resolveManager(svc.dir);
8488
+ const key = `${cmd.pm}:${cmd.cwd}`;
8489
+ if (!installPlans.has(key)) installPlans.set(key, cmd);
8490
+ }
8491
+ } else if (outcome.outcome === "already-instrumented") already++;
8138
8492
  else if (outcome.outcome === "lib-only") libOnly++;
8139
8493
  else if (outcome.outcome === "browser-bundle") {
8140
8494
  browserBundle++;
@@ -8144,7 +8498,30 @@ async function applyInstallersOver(services, project) {
8144
8498
  console.log(`skipping ${svc.dir}: React Native target; browser-OTel support lands in a future release.`);
8145
8499
  }
8146
8500
  }
8147
- return { instrumented, alreadyInstrumented: already, libOnly, browserBundle, reactNative };
8501
+ const packageManagerInstalls = [];
8502
+ for (const cmd of installPlans.values()) {
8503
+ console.log(`running \`${cmd.pm} ${cmd.args.join(" ")}\` in ${cmd.cwd}`);
8504
+ const result = await runInstall(cmd);
8505
+ packageManagerInstalls.push(result);
8506
+ if (result.exitCode !== 0) {
8507
+ console.error(
8508
+ `neat: ${cmd.pm} install failed in ${cmd.cwd} (exit ${result.exitCode}); run it manually to surface the error.`
8509
+ );
8510
+ if (result.stderr.length > 0) {
8511
+ for (const line of result.stderr.split(/\r?\n/).slice(0, 20)) {
8512
+ console.error(` ${line}`);
8513
+ }
8514
+ }
8515
+ }
8516
+ }
8517
+ return {
8518
+ instrumented,
8519
+ alreadyInstrumented: already,
8520
+ libOnly,
8521
+ browserBundle,
8522
+ reactNative,
8523
+ packageManagerInstalls
8524
+ };
8148
8525
  }
8149
8526
  async function promptYesNo(question) {
8150
8527
  const rl = import_node_readline.default.createInterface({ input: process.stdin, output: process.stdout });
@@ -8280,10 +8657,10 @@ function formatPortCollisionMessage(port) {
8280
8657
  ];
8281
8658
  }
8282
8659
  function spawnDaemonDetached() {
8283
- const here = import_node_path42.default.dirname(new URL(importMetaUrl).pathname);
8660
+ const here = import_node_path43.default.dirname(new URL(importMetaUrl).pathname);
8284
8661
  const candidates = [
8285
- import_node_path42.default.join(here, "neatd.cjs"),
8286
- import_node_path42.default.join(here, "neatd.js")
8662
+ import_node_path43.default.join(here, "neatd.cjs"),
8663
+ import_node_path43.default.join(here, "neatd.js")
8287
8664
  ];
8288
8665
  let entry2 = null;
8289
8666
  const fsSync = require("fs");
@@ -8303,7 +8680,7 @@ function spawnDaemonDetached() {
8303
8680
  if (!hasToken && (!env.HOST || env.HOST.length === 0)) {
8304
8681
  env.HOST = "127.0.0.1";
8305
8682
  }
8306
- const child = (0, import_node_child_process2.spawn)(process.execPath, [entry2, "start"], {
8683
+ const child = (0, import_node_child_process3.spawn)(process.execPath, [entry2, "start"], {
8307
8684
  detached: true,
8308
8685
  // stderr inherits the orchestrator's fd so the daemon's
8309
8686
  // `BindAuthorityError` message lands in front of the operator instead
@@ -8320,7 +8697,7 @@ function openBrowser(url) {
8320
8697
  const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
8321
8698
  const args = platform === "win32" ? ["/c", "start", "", url] : [url];
8322
8699
  try {
8323
- const child = (0, import_node_child_process2.spawn)(cmd, args, { detached: true, stdio: "ignore" });
8700
+ const child = (0, import_node_child_process3.spawn)(cmd, args, { detached: true, stdio: "ignore" });
8324
8701
  child.on("error", () => {
8325
8702
  });
8326
8703
  child.unref();
@@ -8341,7 +8718,7 @@ async function runOrchestrator(opts) {
8341
8718
  browser: "skipped"
8342
8719
  }
8343
8720
  };
8344
- const stat = await import_node_fs27.promises.stat(opts.scanPath).catch(() => null);
8721
+ const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
8345
8722
  if (!stat || !stat.isDirectory()) {
8346
8723
  console.error(`neat: ${opts.scanPath} is not a directory`);
8347
8724
  result.exitCode = 2;
@@ -8409,6 +8786,10 @@ async function runOrchestrator(opts) {
8409
8786
  console.log(
8410
8787
  `instrumented ${tally.instrumented}, already ${tally.alreadyInstrumented}, lib-only ${tally.libOnly}`
8411
8788
  );
8789
+ const failedInstalls = tally.packageManagerInstalls.filter((i) => i.exitCode !== 0);
8790
+ if (failedInstalls.length > 0) {
8791
+ result.exitCode = 1;
8792
+ }
8412
8793
  }
8413
8794
  const restPort = Number(process.env.PORT ?? 8080);
8414
8795
  const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
@@ -8480,11 +8861,11 @@ function printSummary(result, graph, dashboardUrl) {
8480
8861
 
8481
8862
  // src/cli-verbs.ts
8482
8863
  init_cjs_shims();
8483
- var import_node_path43 = __toESM(require("path"), 1);
8864
+ var import_node_path44 = __toESM(require("path"), 1);
8484
8865
 
8485
8866
  // src/cli-client.ts
8486
8867
  init_cjs_shims();
8487
- var import_types24 = require("@neat.is/types");
8868
+ var import_types25 = require("@neat.is/types");
8488
8869
  var HttpError = class extends Error {
8489
8870
  constructor(status2, message, responseBody = "") {
8490
8871
  super(message);
@@ -8505,10 +8886,10 @@ function createHttpClient(baseUrl, bearerToken) {
8505
8886
  const root = baseUrl.replace(/\/$/, "");
8506
8887
  const authHeader = bearerToken && bearerToken.length > 0 ? { authorization: `Bearer ${bearerToken}` } : {};
8507
8888
  return {
8508
- async get(path45) {
8889
+ async get(path46) {
8509
8890
  let res;
8510
8891
  try {
8511
- res = await fetch(`${root}${path45}`, {
8892
+ res = await fetch(`${root}${path46}`, {
8512
8893
  headers: { ...authHeader }
8513
8894
  });
8514
8895
  } catch (err) {
@@ -8520,16 +8901,16 @@ function createHttpClient(baseUrl, bearerToken) {
8520
8901
  const body = await res.text().catch(() => "");
8521
8902
  throw new HttpError(
8522
8903
  res.status,
8523
- `${res.status} ${res.statusText} on GET ${path45}: ${body}`,
8904
+ `${res.status} ${res.statusText} on GET ${path46}: ${body}`,
8524
8905
  body
8525
8906
  );
8526
8907
  }
8527
8908
  return await res.json();
8528
8909
  },
8529
- async post(path45, body) {
8910
+ async post(path46, body) {
8530
8911
  let res;
8531
8912
  try {
8532
- res = await fetch(`${root}${path45}`, {
8913
+ res = await fetch(`${root}${path46}`, {
8533
8914
  method: "POST",
8534
8915
  headers: { "content-type": "application/json", ...authHeader },
8535
8916
  body: JSON.stringify(body)
@@ -8543,7 +8924,7 @@ function createHttpClient(baseUrl, bearerToken) {
8543
8924
  const text = await res.text().catch(() => "");
8544
8925
  throw new HttpError(
8545
8926
  res.status,
8546
- `${res.status} ${res.statusText} on POST ${path45}: ${text}`,
8927
+ `${res.status} ${res.statusText} on POST ${path46}: ${text}`,
8547
8928
  text
8548
8929
  );
8549
8930
  }
@@ -8557,12 +8938,12 @@ function projectPath(project, suffix) {
8557
8938
  }
8558
8939
  async function runRootCause(client, input) {
8559
8940
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
8560
- const path45 = projectPath(
8941
+ const path46 = projectPath(
8561
8942
  input.project,
8562
8943
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
8563
8944
  );
8564
8945
  try {
8565
- const result = await client.get(path45);
8946
+ const result = await client.get(path46);
8566
8947
  const arrowPath = result.traversalPath.join(" \u2190 ");
8567
8948
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
8568
8949
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -8588,12 +8969,12 @@ async function runRootCause(client, input) {
8588
8969
  }
8589
8970
  async function runBlastRadius(client, input) {
8590
8971
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
8591
- const path45 = projectPath(
8972
+ const path46 = projectPath(
8592
8973
  input.project,
8593
8974
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
8594
8975
  );
8595
8976
  try {
8596
- const result = await client.get(path45);
8977
+ const result = await client.get(path46);
8597
8978
  if (result.totalAffected === 0) {
8598
8979
  return {
8599
8980
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -8622,17 +9003,17 @@ async function runBlastRadius(client, input) {
8622
9003
  }
8623
9004
  }
8624
9005
  function formatBlastEntry(n) {
8625
- const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
9006
+ const tag = n.edgeProvenance === import_types25.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
8626
9007
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
8627
9008
  }
8628
9009
  async function runDependencies(client, input) {
8629
9010
  const depth = input.depth ?? 3;
8630
- const path45 = projectPath(
9011
+ const path46 = projectPath(
8631
9012
  input.project,
8632
9013
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
8633
9014
  );
8634
9015
  try {
8635
- const result = await client.get(path45);
9016
+ const result = await client.get(path46);
8636
9017
  if (result.total === 0) {
8637
9018
  return {
8638
9019
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -8668,9 +9049,9 @@ async function runObservedDependencies(client, input) {
8668
9049
  const edges = await client.get(
8669
9050
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
8670
9051
  );
8671
- const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
9052
+ const observed = edges.outbound.filter((e) => e.provenance === import_types25.Provenance.OBSERVED);
8672
9053
  if (observed.length === 0) {
8673
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
9054
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types25.Provenance.EXTRACTED);
8674
9055
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
8675
9056
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
8676
9057
  }
@@ -8678,7 +9059,7 @@ async function runObservedDependencies(client, input) {
8678
9059
  return {
8679
9060
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
8680
9061
  block: blockLines.join("\n"),
8681
- provenance: import_types24.Provenance.OBSERVED
9062
+ provenance: import_types25.Provenance.OBSERVED
8682
9063
  };
8683
9064
  } catch (err) {
8684
9065
  if (err instanceof HttpError && err.status === 404) {
@@ -8713,9 +9094,9 @@ function formatDuration(ms) {
8713
9094
  return `${Math.round(h / 24)}d`;
8714
9095
  }
8715
9096
  async function runIncidents(client, input) {
8716
- const path45 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
9097
+ const path46 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
8717
9098
  try {
8718
- const body = await client.get(path45);
9099
+ const body = await client.get(path46);
8719
9100
  const events = body.events;
8720
9101
  if (events.length === 0) {
8721
9102
  return {
@@ -8732,7 +9113,7 @@ async function runIncidents(client, input) {
8732
9113
  return {
8733
9114
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
8734
9115
  block: blockLines.join("\n"),
8735
- provenance: import_types24.Provenance.OBSERVED
9116
+ provenance: import_types25.Provenance.OBSERVED
8736
9117
  };
8737
9118
  } catch (err) {
8738
9119
  if (err instanceof HttpError && err.status === 404) {
@@ -8841,7 +9222,7 @@ async function runStaleEdges(client, input) {
8841
9222
  return {
8842
9223
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
8843
9224
  block: blockLines.join("\n"),
8844
- provenance: import_types24.Provenance.STALE
9225
+ provenance: import_types25.Provenance.STALE
8845
9226
  };
8846
9227
  }
8847
9228
  async function runPolicies(client, input) {
@@ -9005,7 +9386,7 @@ async function resolveProjectEntry(opts) {
9005
9386
  const cwd = opts.cwd ?? process.cwd();
9006
9387
  const resolvedCwd = await normalizeProjectPath(cwd);
9007
9388
  for (const entry2 of entries) {
9008
- if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path43.default.sep}`)) {
9389
+ if (resolvedCwd === entry2.path || resolvedCwd.startsWith(`${entry2.path}${import_node_path44.default.sep}`)) {
9009
9390
  return entry2;
9010
9391
  }
9011
9392
  }
@@ -9155,7 +9536,7 @@ async function runSync(opts) {
9155
9536
  }
9156
9537
 
9157
9538
  // src/cli.ts
9158
- var import_types25 = require("@neat.is/types");
9539
+ var import_types26 = require("@neat.is/types");
9159
9540
  function usage() {
9160
9541
  console.log("usage: neat <command> [args] [--project <name>]");
9161
9542
  console.log("");
@@ -9365,14 +9746,14 @@ function assignFlag(out, field, value) {
9365
9746
  out[field] = value;
9366
9747
  }
9367
9748
  function readPackageVersion() {
9368
- const here = typeof __dirname !== "undefined" ? __dirname : import_node_path44.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
9749
+ const here = typeof __dirname !== "undefined" ? __dirname : import_node_path45.default.dirname((0, import_node_url3.fileURLToPath)(importMetaUrl));
9369
9750
  const candidates = [
9370
- import_node_path44.default.resolve(here, "../package.json"),
9371
- import_node_path44.default.resolve(here, "../../package.json")
9751
+ import_node_path45.default.resolve(here, "../package.json"),
9752
+ import_node_path45.default.resolve(here, "../../package.json")
9372
9753
  ];
9373
9754
  for (const candidate of candidates) {
9374
9755
  try {
9375
- const raw = (0, import_node_fs28.readFileSync)(candidate, "utf8");
9756
+ const raw = (0, import_node_fs29.readFileSync)(candidate, "utf8");
9376
9757
  const parsed = JSON.parse(raw);
9377
9758
  if (parsed.name === "@neat.is/core" && typeof parsed.version === "string") {
9378
9759
  return parsed.version;
@@ -9436,7 +9817,7 @@ async function buildPatchSections(services, project) {
9436
9817
  }
9437
9818
  async function runInit(opts) {
9438
9819
  const written = [];
9439
- const stat = await import_node_fs28.promises.stat(opts.scanPath).catch(() => null);
9820
+ const stat = await import_node_fs29.promises.stat(opts.scanPath).catch(() => null);
9440
9821
  if (!stat || !stat.isDirectory()) {
9441
9822
  console.error(`neat init: ${opts.scanPath} is not a directory`);
9442
9823
  return { exitCode: 2, writtenFiles: written };
@@ -9445,13 +9826,13 @@ async function runInit(opts) {
9445
9826
  printDiscoveryReport(opts, services);
9446
9827
  const sections = opts.noInstall ? [] : await buildPatchSections(services, opts.project);
9447
9828
  const patch = renderPatch(sections);
9448
- const patchPath = import_node_path44.default.join(opts.scanPath, "neat.patch");
9829
+ const patchPath = import_node_path45.default.join(opts.scanPath, "neat.patch");
9449
9830
  if (opts.dryRun) {
9450
- await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
9831
+ await import_node_fs29.promises.writeFile(patchPath, patch, "utf8");
9451
9832
  written.push(patchPath);
9452
9833
  console.log(`dry-run: patch written to ${patchPath}`);
9453
- const gitignorePath = import_node_path44.default.join(opts.scanPath, ".gitignore");
9454
- const gitignoreExists = await import_node_fs28.promises.stat(gitignorePath).then(() => true).catch(() => false);
9834
+ const gitignorePath = import_node_path45.default.join(opts.scanPath, ".gitignore");
9835
+ const gitignoreExists = await import_node_fs29.promises.stat(gitignorePath).then(() => true).catch(() => false);
9455
9836
  const verb = gitignoreExists ? "append" : "create";
9456
9837
  console.log(`dry-run: would ${verb} ${gitignorePath} (add neat-out/)`);
9457
9838
  console.log("rerun without --dry-run to register and snapshot.");
@@ -9462,9 +9843,9 @@ async function runInit(opts) {
9462
9843
  const graph = getGraph(graphKey);
9463
9844
  const projectPaths = pathsForProject(
9464
9845
  graphKey,
9465
- import_node_path44.default.join(opts.scanPath, "neat-out")
9846
+ import_node_path45.default.join(opts.scanPath, "neat-out")
9466
9847
  );
9467
- const errorsPath = import_node_path44.default.join(import_node_path44.default.dirname(opts.outPath), import_node_path44.default.basename(projectPaths.errorsPath));
9848
+ const errorsPath = import_node_path45.default.join(import_node_path45.default.dirname(opts.outPath), import_node_path45.default.basename(projectPaths.errorsPath));
9468
9849
  const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
9469
9850
  await saveGraphToDisk(graph, opts.outPath);
9470
9851
  written.push(opts.outPath);
@@ -9543,7 +9924,7 @@ async function runInit(opts) {
9543
9924
  console.log("Run `npm install` (or your language equivalent) to refresh lockfiles.");
9544
9925
  }
9545
9926
  } else {
9546
- await import_node_fs28.promises.writeFile(patchPath, patch, "utf8");
9927
+ await import_node_fs29.promises.writeFile(patchPath, patch, "utf8");
9547
9928
  written.push(patchPath);
9548
9929
  }
9549
9930
  }
@@ -9583,9 +9964,9 @@ var CLAUDE_SKILL_CONFIG = {
9583
9964
  };
9584
9965
  function claudeConfigPath() {
9585
9966
  const override = process.env.NEAT_CLAUDE_CONFIG;
9586
- if (override && override.length > 0) return import_node_path44.default.resolve(override);
9967
+ if (override && override.length > 0) return import_node_path45.default.resolve(override);
9587
9968
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
9588
- return import_node_path44.default.join(home, ".claude.json");
9969
+ return import_node_path45.default.join(home, ".claude.json");
9589
9970
  }
9590
9971
  async function runSkill(opts) {
9591
9972
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -9597,7 +9978,7 @@ async function runSkill(opts) {
9597
9978
  const target = claudeConfigPath();
9598
9979
  let existing = {};
9599
9980
  try {
9600
- existing = JSON.parse(await import_node_fs28.promises.readFile(target, "utf8"));
9981
+ existing = JSON.parse(await import_node_fs29.promises.readFile(target, "utf8"));
9601
9982
  } catch (err) {
9602
9983
  if (err.code !== "ENOENT") {
9603
9984
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -9609,8 +9990,8 @@ async function runSkill(opts) {
9609
9990
  ...existing,
9610
9991
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
9611
9992
  };
9612
- await import_node_fs28.promises.mkdir(import_node_path44.default.dirname(target), { recursive: true });
9613
- await import_node_fs28.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
9993
+ await import_node_fs29.promises.mkdir(import_node_path45.default.dirname(target), { recursive: true });
9994
+ await import_node_fs29.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
9614
9995
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
9615
9996
  console.log("restart Claude Code to pick up the new MCP server.");
9616
9997
  return { exitCode: 0 };
@@ -9648,12 +10029,12 @@ async function main() {
9648
10029
  console.error("neat init: --apply and --dry-run are mutually exclusive");
9649
10030
  process.exit(2);
9650
10031
  }
9651
- const scanPath = import_node_path44.default.resolve(target);
10032
+ const scanPath = import_node_path45.default.resolve(target);
9652
10033
  const projectExplicit = parsed.project !== null;
9653
- const projectName = projectExplicit ? project : import_node_path44.default.basename(scanPath);
10034
+ const projectName = projectExplicit ? project : import_node_path45.default.basename(scanPath);
9654
10035
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
9655
- const fallback = pathsForProject(projectKey, import_node_path44.default.join(scanPath, "neat-out")).snapshotPath;
9656
- const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
10036
+ const fallback = pathsForProject(projectKey, import_node_path45.default.join(scanPath, "neat-out")).snapshotPath;
10037
+ const outPath = import_node_path45.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
9657
10038
  const result = await runInit({
9658
10039
  scanPath,
9659
10040
  outPath,
@@ -9674,21 +10055,21 @@ async function main() {
9674
10055
  usage();
9675
10056
  process.exit(2);
9676
10057
  }
9677
- const scanPath = import_node_path44.default.resolve(target);
9678
- const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
10058
+ const scanPath = import_node_path45.default.resolve(target);
10059
+ const stat = await import_node_fs29.promises.stat(scanPath).catch(() => null);
9679
10060
  if (!stat || !stat.isDirectory()) {
9680
10061
  console.error(`neat watch: ${scanPath} is not a directory`);
9681
10062
  process.exit(2);
9682
10063
  }
9683
- const projectPaths = pathsForProject(project, import_node_path44.default.join(scanPath, "neat-out"));
9684
- const outPath = import_node_path44.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
9685
- const errorsPath = import_node_path44.default.resolve(
9686
- process.env.NEAT_ERRORS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.errorsPath))
10064
+ const projectPaths = pathsForProject(project, import_node_path45.default.join(scanPath, "neat-out"));
10065
+ const outPath = import_node_path45.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
10066
+ const errorsPath = import_node_path45.default.resolve(
10067
+ process.env.NEAT_ERRORS_PATH ?? import_node_path45.default.join(import_node_path45.default.dirname(outPath), import_node_path45.default.basename(projectPaths.errorsPath))
9687
10068
  );
9688
- const staleEventsPath = import_node_path44.default.resolve(
9689
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path44.default.join(import_node_path44.default.dirname(outPath), import_node_path44.default.basename(projectPaths.staleEventsPath))
10069
+ const staleEventsPath = import_node_path45.default.resolve(
10070
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path45.default.join(import_node_path45.default.dirname(outPath), import_node_path45.default.basename(projectPaths.staleEventsPath))
9690
10071
  );
9691
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path44.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
10072
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path45.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
9692
10073
  const handle = await startWatch(getGraph(project), {
9693
10074
  scanPath,
9694
10075
  outPath,
@@ -9834,11 +10215,11 @@ async function main() {
9834
10215
  process.exit(1);
9835
10216
  }
9836
10217
  async function tryOrchestrator(cmd, parsed) {
9837
- const scanPath = import_node_path44.default.resolve(cmd);
9838
- const stat = await import_node_fs28.promises.stat(scanPath).catch(() => null);
10218
+ const scanPath = import_node_path45.default.resolve(cmd);
10219
+ const stat = await import_node_fs29.promises.stat(scanPath).catch(() => null);
9839
10220
  if (!stat || !stat.isDirectory()) return null;
9840
10221
  const projectExplicit = parsed.project !== null;
9841
- const projectName = projectExplicit ? parsed.project : import_node_path44.default.basename(scanPath);
10222
+ const projectName = projectExplicit ? parsed.project : import_node_path45.default.basename(scanPath);
9842
10223
  const result = await runOrchestrator({
9843
10224
  scanPath,
9844
10225
  project: projectName,
@@ -9988,10 +10369,10 @@ async function runQueryVerb(cmd, parsed) {
9988
10369
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
9989
10370
  const out = [];
9990
10371
  for (const p of parts) {
9991
- const r = import_types25.DivergenceTypeSchema.safeParse(p);
10372
+ const r = import_types26.DivergenceTypeSchema.safeParse(p);
9992
10373
  if (!r.success) {
9993
10374
  console.error(
9994
- `neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
10375
+ `neat divergences: unknown --type "${p}". allowed: ${import_types26.DivergenceTypeSchema.options.join(", ")}`
9995
10376
  );
9996
10377
  return 2;
9997
10378
  }