@neat.is/core 0.4.6 → 0.4.8

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
@@ -42,6 +42,15 @@ var init_cjs_shims = __esm({
42
42
  });
43
43
 
44
44
  // src/auth.ts
45
+ function isLoopbackHost(host) {
46
+ if (!host) return false;
47
+ return LOOPBACK_HOSTS.has(host);
48
+ }
49
+ function assertBindAuthority(host, token) {
50
+ if (token && token.length > 0) return;
51
+ if (isLoopbackHost(host)) return;
52
+ throw new BindAuthorityError(host);
53
+ }
45
54
  function mountBearerAuth(app, opts) {
46
55
  if (!opts.token || opts.token.length === 0) return;
47
56
  if (opts.trustProxy) return;
@@ -73,12 +82,40 @@ function mountBearerAuth(app, opts) {
73
82
  done();
74
83
  });
75
84
  }
76
- var import_node_crypto, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
85
+ function parseBoolEnv(v) {
86
+ if (!v) return false;
87
+ return v === "true" || v === "1";
88
+ }
89
+ function readAuthEnv(env = process.env) {
90
+ const t = env.NEAT_AUTH_TOKEN;
91
+ const ot = env.NEAT_OTEL_TOKEN;
92
+ return {
93
+ authToken: t && t.length > 0 ? t : void 0,
94
+ otelToken: ot && ot.length > 0 ? ot : t && t.length > 0 ? t : void 0,
95
+ trustProxy: env.NEAT_AUTH_PROXY === "true",
96
+ publicRead: parseBoolEnv(env.NEAT_PUBLIC_READ)
97
+ };
98
+ }
99
+ var import_node_crypto, LOOPBACK_HOSTS, BindAuthorityError, PUBLIC_READ_METHODS, DEFAULT_UNAUTH_SUFFIXES;
77
100
  var init_auth = __esm({
78
101
  "src/auth.ts"() {
79
102
  "use strict";
80
103
  init_cjs_shims();
81
104
  import_node_crypto = require("crypto");
105
+ LOOPBACK_HOSTS = /* @__PURE__ */ new Set([
106
+ "127.0.0.1",
107
+ "localhost",
108
+ "::1",
109
+ "::ffff:127.0.0.1"
110
+ ]);
111
+ BindAuthorityError = class extends Error {
112
+ constructor(host) {
113
+ super(
114
+ `NEAT refuses to bind on a public interface without \`NEAT_AUTH_TOKEN\` set (host="${host}"). Set the token or bind to loopback only.`
115
+ );
116
+ this.name = "BindAuthorityError";
117
+ }
118
+ };
82
119
  PUBLIC_READ_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
83
120
  DEFAULT_UNAUTH_SUFFIXES = [
84
121
  "/health",
@@ -555,6 +592,7 @@ __export(cli_exports, {
555
592
  CLAUDE_SKILL_CONFIG: () => CLAUDE_SKILL_CONFIG,
556
593
  QUERY_VERBS: () => QUERY_VERBS,
557
594
  parseArgs: () => parseArgs,
595
+ printBanner: () => printBanner,
558
596
  readPackageVersion: () => readPackageVersion,
559
597
  runInit: () => runInit,
560
598
  runQueryVerb: () => runQueryVerb,
@@ -993,6 +1031,24 @@ function isFrontierNode(graph, nodeId) {
993
1031
  const attrs = graph.getNodeAttributes(nodeId);
994
1032
  return attrs.type === import_types.NodeType.FrontierNode;
995
1033
  }
1034
+ function resolveOwningService(graph, nodeId) {
1035
+ if (!graph.hasNode(nodeId)) return null;
1036
+ const attrs = graph.getNodeAttributes(nodeId);
1037
+ if (attrs.type === import_types.NodeType.ServiceNode) {
1038
+ return { id: nodeId, svc: attrs };
1039
+ }
1040
+ if (attrs.type === import_types.NodeType.FileNode) {
1041
+ for (const edgeId of graph.inboundEdges(nodeId)) {
1042
+ const e = graph.getEdgeAttributes(edgeId);
1043
+ if (e.type !== import_types.EdgeType.CONTAINS) continue;
1044
+ const owner = graph.getNodeAttributes(e.source);
1045
+ if (owner.type === import_types.NodeType.ServiceNode) {
1046
+ return { id: e.source, svc: owner };
1047
+ }
1048
+ }
1049
+ }
1050
+ return null;
1051
+ }
996
1052
  function bestEdgeBySource(graph, edgeIds) {
997
1053
  const best = /* @__PURE__ */ new Map();
998
1054
  for (const id of edgeIds) {
@@ -1099,9 +1155,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1099
1155
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
1100
1156
  if (candidatePairs.length === 0) return null;
1101
1157
  for (const id of walk.path) {
1102
- const attrs = graph.getNodeAttributes(id);
1103
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1104
- const svc = attrs;
1158
+ const owner = resolveOwningService(graph, id);
1159
+ if (!owner) continue;
1160
+ const { id: serviceId3, svc } = owner;
1105
1161
  const deps = svc.dependencies ?? {};
1106
1162
  for (const pair of candidatePairs) {
1107
1163
  const declared = deps[pair.driver];
@@ -1114,7 +1170,7 @@ function databaseRootCauseShape(graph, origin, walk) {
1114
1170
  );
1115
1171
  if (!result.compatible) {
1116
1172
  return {
1117
- rootCauseNode: id,
1173
+ rootCauseNode: serviceId3,
1118
1174
  rootCauseReason: result.reason ?? "incompatible driver",
1119
1175
  ...result.minDriverVersion ? {
1120
1176
  fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
@@ -1127,9 +1183,9 @@ function databaseRootCauseShape(graph, origin, walk) {
1127
1183
  }
1128
1184
  function serviceRootCauseShape(graph, _origin, walk) {
1129
1185
  for (const id of walk.path) {
1130
- const attrs = graph.getNodeAttributes(id);
1131
- if (attrs.type !== import_types.NodeType.ServiceNode) continue;
1132
- const svc = attrs;
1186
+ const owner = resolveOwningService(graph, id);
1187
+ if (!owner) continue;
1188
+ const { id: serviceId3, svc } = owner;
1133
1189
  const deps = svc.dependencies ?? {};
1134
1190
  const serviceNodeEngine = svc.nodeEngine;
1135
1191
  for (const constraint of nodeEngineConstraints()) {
@@ -1138,7 +1194,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1138
1194
  const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
1139
1195
  if (!result.compatible && result.reason) {
1140
1196
  return {
1141
- rootCauseNode: id,
1197
+ rootCauseNode: serviceId3,
1142
1198
  rootCauseReason: result.reason,
1143
1199
  ...result.requiredNodeVersion ? {
1144
1200
  fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
@@ -1153,7 +1209,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
1153
1209
  const result = checkPackageConflict(conflict, declared, requiredDeclared);
1154
1210
  if (!result.compatible && result.reason) {
1155
1211
  return {
1156
- rootCauseNode: id,
1212
+ rootCauseNode: serviceId3,
1157
1213
  rootCauseReason: result.reason,
1158
1214
  fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
1159
1215
  };
@@ -1162,9 +1218,15 @@ function serviceRootCauseShape(graph, _origin, walk) {
1162
1218
  }
1163
1219
  return null;
1164
1220
  }
1221
+ function fileRootCauseShape(graph, origin, walk) {
1222
+ const owner = resolveOwningService(graph, origin.id);
1223
+ if (!owner) return null;
1224
+ return serviceRootCauseShape(graph, owner.svc, walk);
1225
+ }
1165
1226
  var rootCauseShapes = {
1166
1227
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
1167
- [import_types.NodeType.ServiceNode]: serviceRootCauseShape
1228
+ [import_types.NodeType.ServiceNode]: serviceRootCauseShape,
1229
+ [import_types.NodeType.FileNode]: fileRootCauseShape
1168
1230
  };
1169
1231
  function getRootCause(graph, errorNodeId, errorEvent) {
1170
1232
  if (!graph.hasNode(errorNodeId)) return null;
@@ -1649,6 +1711,86 @@ function hostFromUrl(u) {
1649
1711
  function pickAddress(span) {
1650
1712
  return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1651
1713
  }
1714
+ var CODE_FILEPATH_ATTR = "code.filepath";
1715
+ var CODE_LINENO_ATTR = "code.lineno";
1716
+ var CODE_FUNCTION_ATTR = "code.function";
1717
+ function toPosix(p) {
1718
+ return p.split("\\").join("/");
1719
+ }
1720
+ function languageForExt(relPath) {
1721
+ const dot = relPath.lastIndexOf(".");
1722
+ if (dot === -1) return void 0;
1723
+ switch (relPath.slice(dot).toLowerCase()) {
1724
+ case ".py":
1725
+ return "python";
1726
+ case ".ts":
1727
+ case ".tsx":
1728
+ return "typescript";
1729
+ case ".js":
1730
+ case ".jsx":
1731
+ case ".mjs":
1732
+ case ".cjs":
1733
+ return "javascript";
1734
+ default:
1735
+ return void 0;
1736
+ }
1737
+ }
1738
+ function relPathForRuntimeFile(filepath, serviceNode) {
1739
+ let p = toPosix(filepath).replace(/^file:\/\//, "");
1740
+ const root = serviceNode?.repoPath;
1741
+ if (root && root !== "." && root.length > 0) {
1742
+ const rootPosix = toPosix(root);
1743
+ const anchor = `/${rootPosix}/`;
1744
+ const idx = p.lastIndexOf(anchor);
1745
+ if (idx !== -1) return p.slice(idx + anchor.length);
1746
+ const base = rootPosix.split("/").filter(Boolean).pop();
1747
+ if (base) {
1748
+ const baseAnchor = `/${base}/`;
1749
+ const bidx = p.lastIndexOf(baseAnchor);
1750
+ if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
1751
+ }
1752
+ }
1753
+ p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
1754
+ return p.length > 0 ? p : null;
1755
+ }
1756
+ function callSiteFromSpan(span, serviceNode) {
1757
+ const filepath = span.attributes[CODE_FILEPATH_ATTR];
1758
+ if (typeof filepath !== "string" || filepath.length === 0) return null;
1759
+ const relPath = relPathForRuntimeFile(filepath, serviceNode);
1760
+ if (!relPath) return null;
1761
+ const linenoRaw = span.attributes[CODE_LINENO_ATTR];
1762
+ const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
1763
+ const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
1764
+ const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
1765
+ return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
1766
+ }
1767
+ function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
1768
+ const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
1769
+ if (!graph.hasNode(fileNodeId)) {
1770
+ const language = languageForExt(callSite.relPath);
1771
+ const node = {
1772
+ id: fileNodeId,
1773
+ type: import_types3.NodeType.FileNode,
1774
+ service: serviceName,
1775
+ path: callSite.relPath,
1776
+ ...language ? { language } : {},
1777
+ discoveredVia: "otel"
1778
+ };
1779
+ graph.addNode(fileNodeId, node);
1780
+ }
1781
+ const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
1782
+ if (!graph.hasEdge(containsId)) {
1783
+ const edge = {
1784
+ id: containsId,
1785
+ source: serviceNodeId,
1786
+ target: fileNodeId,
1787
+ type: import_types3.EdgeType.CONTAINS,
1788
+ provenance: import_types3.Provenance.OBSERVED
1789
+ };
1790
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
1791
+ }
1792
+ return fileNodeId;
1793
+ }
1652
1794
  function makeObservedEdgeId(type, source, target) {
1653
1795
  return (0, import_types3.observedEdgeId)(source, target, type);
1654
1796
  }
@@ -1891,6 +2033,9 @@ async function handleSpan(ctx, span) {
1891
2033
  const sourceId = ensureServiceNode(ctx.graph, span.service, env);
1892
2034
  const isError = span.statusCode === 2;
1893
2035
  cacheSpanService(span, nowMs);
2036
+ const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
2037
+ const callSite = callSiteFromSpan(span, sourceServiceNode);
2038
+ const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
1894
2039
  let affectedNode = sourceId;
1895
2040
  if (span.dbSystem) {
1896
2041
  const host = pickAddress(span);
@@ -1900,7 +2045,7 @@ async function handleSpan(ctx, span) {
1900
2045
  const result = upsertObservedEdge(
1901
2046
  ctx.graph,
1902
2047
  import_types3.EdgeType.CONNECTS_TO,
1903
- sourceId,
2048
+ observedSource(),
1904
2049
  targetId,
1905
2050
  ts,
1906
2051
  isError
@@ -1916,7 +2061,7 @@ async function handleSpan(ctx, span) {
1916
2061
  upsertObservedEdge(
1917
2062
  ctx.graph,
1918
2063
  import_types3.EdgeType.CALLS,
1919
- sourceId,
2064
+ observedSource(),
1920
2065
  targetId,
1921
2066
  ts,
1922
2067
  isError
@@ -1928,7 +2073,7 @@ async function handleSpan(ctx, span) {
1928
2073
  upsertObservedEdge(
1929
2074
  ctx.graph,
1930
2075
  import_types3.EdgeType.CALLS,
1931
- sourceId,
2076
+ observedSource(),
1932
2077
  frontierNodeId,
1933
2078
  ts,
1934
2079
  isError
@@ -3571,7 +3716,7 @@ async function addConfigNodes(graph, services, scanPath) {
3571
3716
 
3572
3717
  // src/extract/calls/index.ts
3573
3718
  init_cjs_shims();
3574
- var import_types14 = require("@neat.is/types");
3719
+ var import_types15 = require("@neat.is/types");
3575
3720
 
3576
3721
  // src/extract/calls/http.ts
3577
3722
  init_cjs_shims();
@@ -3579,12 +3724,13 @@ var import_node_path20 = __toESM(require("path"), 1);
3579
3724
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3580
3725
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3581
3726
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
3582
- var import_types9 = require("@neat.is/types");
3727
+ var import_types10 = require("@neat.is/types");
3583
3728
 
3584
3729
  // src/extract/calls/shared.ts
3585
3730
  init_cjs_shims();
3586
3731
  var import_node_fs13 = require("fs");
3587
3732
  var import_node_path19 = __toESM(require("path"), 1);
3733
+ var import_types9 = require("@neat.is/types");
3588
3734
  async function walkSourceFiles(dir) {
3589
3735
  const out = [];
3590
3736
  async function walk(current) {
@@ -3624,6 +3770,58 @@ function snippet(text, line) {
3624
3770
  const lines = text.split("\n");
3625
3771
  return (lines[line - 1] ?? "").trim();
3626
3772
  }
3773
+ function toPosix2(p) {
3774
+ return p.split("\\").join("/");
3775
+ }
3776
+ function languageForPath(relPath) {
3777
+ switch (import_node_path19.default.extname(relPath).toLowerCase()) {
3778
+ case ".py":
3779
+ return "python";
3780
+ case ".ts":
3781
+ case ".tsx":
3782
+ return "typescript";
3783
+ case ".js":
3784
+ case ".jsx":
3785
+ case ".mjs":
3786
+ case ".cjs":
3787
+ return "javascript";
3788
+ default:
3789
+ return void 0;
3790
+ }
3791
+ }
3792
+ function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
3793
+ let nodesAdded = 0;
3794
+ let edgesAdded = 0;
3795
+ const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
3796
+ if (!graph.hasNode(fileNodeId)) {
3797
+ const language = languageForPath(relPath);
3798
+ const node = {
3799
+ id: fileNodeId,
3800
+ type: import_types9.NodeType.FileNode,
3801
+ service: serviceName,
3802
+ path: relPath,
3803
+ ...language ? { language } : {},
3804
+ discoveredVia: "static"
3805
+ };
3806
+ graph.addNode(fileNodeId, node);
3807
+ nodesAdded++;
3808
+ }
3809
+ const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
3810
+ if (!graph.hasEdge(containsId)) {
3811
+ const edge = {
3812
+ id: containsId,
3813
+ source: serviceNodeId,
3814
+ target: fileNodeId,
3815
+ type: import_types9.EdgeType.CONTAINS,
3816
+ provenance: import_types9.Provenance.EXTRACTED,
3817
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3818
+ evidence: { file: relPath }
3819
+ };
3820
+ graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
3821
+ edgesAdded++;
3822
+ }
3823
+ return { fileNodeId, nodesAdded, edgesAdded };
3824
+ }
3627
3825
 
3628
3826
  // src/extract/calls/http.ts
3629
3827
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
@@ -3657,16 +3855,16 @@ function callsFromSource(source, parser, knownHosts) {
3657
3855
  const tree = parser.parse(source);
3658
3856
  const literals = [];
3659
3857
  collectStringLiterals(tree.rootNode, literals);
3660
- const targets = /* @__PURE__ */ new Set();
3858
+ const out = [];
3661
3859
  for (const lit of literals) {
3662
3860
  if (isInsideJsxExternalLink(lit.node)) continue;
3663
3861
  for (const host of knownHosts) {
3664
3862
  if (urlMatchesHost(lit.text, host)) {
3665
- targets.add(host);
3863
+ out.push({ host, line: lit.node.startPosition.row + 1 });
3666
3864
  }
3667
3865
  }
3668
3866
  }
3669
- return targets;
3867
+ return out;
3670
3868
  }
3671
3869
  function makeJsParser() {
3672
3870
  const p = new import_tree_sitter.default();
@@ -3689,71 +3887,78 @@ async function addHttpCallEdges(graph, services) {
3689
3887
  hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3690
3888
  hostToNodeId.set(service.pkg.name, service.node.id);
3691
3889
  }
3890
+ let nodesAdded = 0;
3692
3891
  let edgesAdded = 0;
3693
3892
  for (const service of services) {
3694
3893
  const files = await loadSourceFiles(service.dir);
3695
- const seenTargets = /* @__PURE__ */ new Map();
3894
+ const seen = /* @__PURE__ */ new Set();
3696
3895
  for (const file of files) {
3697
3896
  if (isTestPath(file.path)) continue;
3698
3897
  const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3699
- let targets;
3898
+ let sites;
3700
3899
  try {
3701
- targets = callsFromSource(file.content, parser, knownHosts);
3900
+ sites = callsFromSource(file.content, parser, knownHosts);
3702
3901
  } catch (err) {
3703
3902
  recordExtractionError("http call extraction", file.path, err);
3704
3903
  continue;
3705
3904
  }
3706
- for (const t of targets) {
3707
- const targetId = hostToNodeId.get(t);
3905
+ if (sites.length === 0) continue;
3906
+ const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
3907
+ for (const site of sites) {
3908
+ const targetId = hostToNodeId.get(site.host);
3708
3909
  if (!targetId || targetId === service.node.id) continue;
3709
- if (!seenTargets.has(targetId)) {
3710
- seenTargets.set(targetId, { file: file.path, host: t });
3910
+ const dedupKey = `${relFile}|${targetId}`;
3911
+ if (seen.has(dedupKey)) continue;
3912
+ seen.add(dedupKey);
3913
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3914
+ const ev = {
3915
+ file: relFile,
3916
+ line: site.line,
3917
+ snippet: snippet(file.content, site.line)
3918
+ };
3919
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
3920
+ graph,
3921
+ service.pkg.name,
3922
+ service.node.id,
3923
+ relFile
3924
+ );
3925
+ nodesAdded += n;
3926
+ edgesAdded += e;
3927
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3928
+ noteExtractedDropped({
3929
+ source: fileNodeId,
3930
+ target: targetId,
3931
+ type: import_types10.EdgeType.CALLS,
3932
+ confidence,
3933
+ confidenceKind: "hostname-shape-match",
3934
+ evidence: ev
3935
+ });
3936
+ continue;
3937
+ }
3938
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
3939
+ if (!graph.hasEdge(edgeId)) {
3940
+ const edge = {
3941
+ id: edgeId,
3942
+ source: fileNodeId,
3943
+ target: targetId,
3944
+ type: import_types10.EdgeType.CALLS,
3945
+ provenance: import_types10.Provenance.EXTRACTED,
3946
+ confidence,
3947
+ evidence: ev
3948
+ };
3949
+ graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
3950
+ edgesAdded++;
3711
3951
  }
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
3952
  }
3748
3953
  }
3749
3954
  }
3750
- return edgesAdded;
3955
+ return { nodesAdded, edgesAdded };
3751
3956
  }
3752
3957
 
3753
3958
  // src/extract/calls/kafka.ts
3754
3959
  init_cjs_shims();
3755
3960
  var import_node_path21 = __toESM(require("path"), 1);
3756
- var import_types10 = require("@neat.is/types");
3961
+ var import_types11 = require("@neat.is/types");
3757
3962
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3758
3963
  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
3964
  function findAll(re, text) {
@@ -3774,7 +3979,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3774
3979
  seen.add(key);
3775
3980
  const line = lineOf(file.content, topic);
3776
3981
  out.push({
3777
- infraId: (0, import_types10.infraId)("kafka-topic", topic),
3982
+ infraId: (0, import_types11.infraId)("kafka-topic", topic),
3778
3983
  name: topic,
3779
3984
  kind: "kafka-topic",
3780
3985
  edgeType,
@@ -3797,7 +4002,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3797
4002
  // src/extract/calls/redis.ts
3798
4003
  init_cjs_shims();
3799
4004
  var import_node_path22 = __toESM(require("path"), 1);
3800
- var import_types11 = require("@neat.is/types");
4005
+ var import_types12 = require("@neat.is/types");
3801
4006
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3802
4007
  function redisEndpointsFromFile(file, serviceDir) {
3803
4008
  const out = [];
@@ -3810,7 +4015,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3810
4015
  seen.add(host);
3811
4016
  const line = lineOf(file.content, host);
3812
4017
  out.push({
3813
- infraId: (0, import_types11.infraId)("redis", host),
4018
+ infraId: (0, import_types12.infraId)("redis", host),
3814
4019
  name: host,
3815
4020
  kind: "redis",
3816
4021
  edgeType: "CALLS",
@@ -3831,7 +4036,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3831
4036
  // src/extract/calls/aws.ts
3832
4037
  init_cjs_shims();
3833
4038
  var import_node_path23 = __toESM(require("path"), 1);
3834
- var import_types12 = require("@neat.is/types");
4039
+ var import_types13 = require("@neat.is/types");
3835
4040
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3836
4041
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
3837
4042
  function hasMarker(text, markers) {
@@ -3855,7 +4060,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3855
4060
  seen.add(key);
3856
4061
  const line = lineOf(file.content, name);
3857
4062
  out.push({
3858
- infraId: (0, import_types12.infraId)(kind, name),
4063
+ infraId: (0, import_types13.infraId)(kind, name),
3859
4064
  name,
3860
4065
  kind,
3861
4066
  edgeType: "CALLS",
@@ -3890,7 +4095,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3890
4095
  // src/extract/calls/grpc.ts
3891
4096
  init_cjs_shims();
3892
4097
  var import_node_path24 = __toESM(require("path"), 1);
3893
- var import_types13 = require("@neat.is/types");
4098
+ var import_types14 = require("@neat.is/types");
3894
4099
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3895
4100
  var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3896
4101
  var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
@@ -3938,7 +4143,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3938
4143
  const { kind } = classified;
3939
4144
  const line = lineOf(file.content, m[0]);
3940
4145
  out.push({
3941
- infraId: (0, import_types13.infraId)(kind, name),
4146
+ infraId: (0, import_types14.infraId)(kind, name),
3942
4147
  name,
3943
4148
  kind,
3944
4149
  edgeType: "CALLS",
@@ -3960,11 +4165,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
3960
4165
  function edgeTypeFromEndpoint(ep) {
3961
4166
  switch (ep.edgeType) {
3962
4167
  case "PUBLISHES_TO":
3963
- return import_types14.EdgeType.PUBLISHES_TO;
4168
+ return import_types15.EdgeType.PUBLISHES_TO;
3964
4169
  case "CONSUMES_FROM":
3965
- return import_types14.EdgeType.CONSUMES_FROM;
4170
+ return import_types15.EdgeType.CONSUMES_FROM;
3966
4171
  default:
3967
- return import_types14.EdgeType.CALLS;
4172
+ return import_types15.EdgeType.CALLS;
3968
4173
  }
3969
4174
  }
3970
4175
  function isAwsKind(kind) {
@@ -3991,7 +4196,7 @@ async function addExternalEndpointEdges(graph, services) {
3991
4196
  if (!graph.hasNode(ep.infraId)) {
3992
4197
  const node = {
3993
4198
  id: ep.infraId,
3994
- type: import_types14.NodeType.InfraNode,
4199
+ type: import_types15.NodeType.InfraNode,
3995
4200
  name: ep.name,
3996
4201
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3997
4202
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4003,13 +4208,19 @@ async function addExternalEndpointEdges(graph, services) {
4003
4208
  nodesAdded++;
4004
4209
  }
4005
4210
  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)) {
4211
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
4212
+ const relFile = toPosix2(ep.evidence.file);
4213
+ const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4214
+ graph,
4215
+ service.pkg.name,
4216
+ service.node.id,
4217
+ relFile
4218
+ );
4219
+ nodesAdded += n;
4220
+ edgesAdded += e;
4221
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
4011
4222
  noteExtractedDropped({
4012
- source: service.node.id,
4223
+ source: fileNodeId,
4013
4224
  target: ep.infraId,
4014
4225
  type: edgeType,
4015
4226
  confidence,
@@ -4018,13 +4229,16 @@ async function addExternalEndpointEdges(graph, services) {
4018
4229
  });
4019
4230
  continue;
4020
4231
  }
4232
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
4233
+ if (seenEdges.has(edgeId)) continue;
4234
+ seenEdges.add(edgeId);
4021
4235
  if (!graph.hasEdge(edgeId)) {
4022
4236
  const edge = {
4023
4237
  id: edgeId,
4024
- source: service.node.id,
4238
+ source: fileNodeId,
4025
4239
  target: ep.infraId,
4026
4240
  type: edgeType,
4027
- provenance: import_types14.Provenance.EXTRACTED,
4241
+ provenance: import_types15.Provenance.EXTRACTED,
4028
4242
  confidence,
4029
4243
  evidence: ep.evidence
4030
4244
  };
@@ -4036,11 +4250,11 @@ async function addExternalEndpointEdges(graph, services) {
4036
4250
  return { nodesAdded, edgesAdded };
4037
4251
  }
4038
4252
  async function addCallEdges(graph, services) {
4039
- const httpEdges = await addHttpCallEdges(graph, services);
4253
+ const http2 = await addHttpCallEdges(graph, services);
4040
4254
  const ext = await addExternalEndpointEdges(graph, services);
4041
4255
  return {
4042
- nodesAdded: ext.nodesAdded,
4043
- edgesAdded: httpEdges + ext.edgesAdded
4256
+ nodesAdded: http2.nodesAdded + ext.nodesAdded,
4257
+ edgesAdded: http2.edgesAdded + ext.edgesAdded
4044
4258
  };
4045
4259
  }
4046
4260
 
@@ -4050,15 +4264,15 @@ init_cjs_shims();
4050
4264
  // src/extract/infra/docker-compose.ts
4051
4265
  init_cjs_shims();
4052
4266
  var import_node_path25 = __toESM(require("path"), 1);
4053
- var import_types16 = require("@neat.is/types");
4267
+ var import_types17 = require("@neat.is/types");
4054
4268
 
4055
4269
  // src/extract/infra/shared.ts
4056
4270
  init_cjs_shims();
4057
- var import_types15 = require("@neat.is/types");
4271
+ var import_types16 = require("@neat.is/types");
4058
4272
  function makeInfraNode(kind, name, provider = "self", extras) {
4059
4273
  return {
4060
- id: (0, import_types15.infraId)(kind, name),
4061
- type: import_types15.NodeType.InfraNode,
4274
+ id: (0, import_types16.infraId)(kind, name),
4275
+ type: import_types16.NodeType.InfraNode,
4062
4276
  name,
4063
4277
  provider,
4064
4278
  kind,
@@ -4137,15 +4351,15 @@ async function addComposeInfra(graph, scanPath, services) {
4137
4351
  for (const dep of dependsOnList(svc.depends_on)) {
4138
4352
  const targetId = composeNameToNodeId.get(dep);
4139
4353
  if (!targetId) continue;
4140
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
4354
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
4141
4355
  if (graph.hasEdge(edgeId)) continue;
4142
4356
  const edge = {
4143
4357
  id: edgeId,
4144
4358
  source: sourceId,
4145
4359
  target: targetId,
4146
- type: import_types16.EdgeType.DEPENDS_ON,
4147
- provenance: import_types16.Provenance.EXTRACTED,
4148
- confidence: (0, import_types16.confidenceForExtracted)("structural"),
4360
+ type: import_types17.EdgeType.DEPENDS_ON,
4361
+ provenance: import_types17.Provenance.EXTRACTED,
4362
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
4149
4363
  evidence: { file: evidenceFile }
4150
4364
  };
4151
4365
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4159,7 +4373,7 @@ async function addComposeInfra(graph, scanPath, services) {
4159
4373
  init_cjs_shims();
4160
4374
  var import_node_path26 = __toESM(require("path"), 1);
4161
4375
  var import_node_fs14 = require("fs");
4162
- var import_types17 = require("@neat.is/types");
4376
+ var import_types18 = require("@neat.is/types");
4163
4377
  function runtimeImage(content) {
4164
4378
  const lines = content.split("\n");
4165
4379
  let last = null;
@@ -4198,15 +4412,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4198
4412
  graph.addNode(node.id, node);
4199
4413
  nodesAdded++;
4200
4414
  }
4201
- const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
4415
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
4202
4416
  if (!graph.hasEdge(edgeId)) {
4203
4417
  const edge = {
4204
4418
  id: edgeId,
4205
4419
  source: service.node.id,
4206
4420
  target: node.id,
4207
- type: import_types17.EdgeType.RUNS_ON,
4208
- provenance: import_types17.Provenance.EXTRACTED,
4209
- confidence: (0, import_types17.confidenceForExtracted)("structural"),
4421
+ type: import_types18.EdgeType.RUNS_ON,
4422
+ provenance: import_types18.Provenance.EXTRACTED,
4423
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
4210
4424
  evidence: {
4211
4425
  file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
4212
4426
  }
@@ -4334,17 +4548,29 @@ var import_node_path30 = __toESM(require("path"), 1);
4334
4548
  init_cjs_shims();
4335
4549
  var import_node_fs17 = require("fs");
4336
4550
  var import_node_path29 = __toESM(require("path"), 1);
4337
- var import_types18 = require("@neat.is/types");
4551
+ var import_types19 = require("@neat.is/types");
4552
+ function dropOrphanedFileNodes(graph) {
4553
+ const orphans = [];
4554
+ graph.forEachNode((id, attrs) => {
4555
+ if (attrs.type !== import_types19.NodeType.FileNode) return;
4556
+ if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4557
+ orphans.push(id);
4558
+ }
4559
+ });
4560
+ for (const id of orphans) graph.dropNode(id);
4561
+ return orphans.length;
4562
+ }
4338
4563
  function retireEdgesByFile(graph, file) {
4339
4564
  const normalized = file.split("\\").join("/");
4340
4565
  const toDrop = [];
4341
4566
  graph.forEachEdge((id, attrs) => {
4342
4567
  const edge = attrs;
4343
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4568
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4344
4569
  if (!edge.evidence?.file) return;
4345
4570
  if (edge.evidence.file === normalized) toDrop.push(id);
4346
4571
  });
4347
4572
  for (const id of toDrop) graph.dropEdge(id);
4573
+ dropOrphanedFileNodes(graph);
4348
4574
  return toDrop.length;
4349
4575
  }
4350
4576
  function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
@@ -4352,7 +4578,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4352
4578
  const bases = [scanPath, ...serviceDirs];
4353
4579
  graph.forEachEdge((id, attrs) => {
4354
4580
  const edge = attrs;
4355
- if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4581
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4356
4582
  const evidenceFile = edge.evidence?.file;
4357
4583
  if (!evidenceFile) return;
4358
4584
  if (import_node_path29.default.isAbsolute(evidenceFile)) {
@@ -4363,6 +4589,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4363
4589
  if (!found) toDrop.push(id);
4364
4590
  });
4365
4591
  for (const id of toDrop) graph.dropEdge(id);
4592
+ dropOrphanedFileNodes(graph);
4366
4593
  return toDrop.length;
4367
4594
  }
4368
4595
 
@@ -4430,7 +4657,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4430
4657
 
4431
4658
  // src/divergences.ts
4432
4659
  init_cjs_shims();
4433
- var import_types19 = require("@neat.is/types");
4660
+ var import_types20 = require("@neat.is/types");
4434
4661
  function bucketKey(source, target, type) {
4435
4662
  return `${type}|${source}|${target}`;
4436
4663
  }
@@ -4438,22 +4665,22 @@ function bucketEdges(graph) {
4438
4665
  const buckets = /* @__PURE__ */ new Map();
4439
4666
  graph.forEachEdge((id, attrs) => {
4440
4667
  const e = attrs;
4441
- const parsed = (0, import_types19.parseEdgeId)(id);
4668
+ const parsed = (0, import_types20.parseEdgeId)(id);
4442
4669
  const provenance = parsed?.provenance ?? e.provenance;
4443
4670
  const key = bucketKey(e.source, e.target, e.type);
4444
4671
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
4445
4672
  switch (provenance) {
4446
- case import_types19.Provenance.EXTRACTED:
4673
+ case import_types20.Provenance.EXTRACTED:
4447
4674
  cur.extracted = e;
4448
4675
  break;
4449
- case import_types19.Provenance.OBSERVED:
4676
+ case import_types20.Provenance.OBSERVED:
4450
4677
  cur.observed = e;
4451
4678
  break;
4452
- case import_types19.Provenance.INFERRED:
4679
+ case import_types20.Provenance.INFERRED:
4453
4680
  cur.inferred = e;
4454
4681
  break;
4455
4682
  default:
4456
- if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
4683
+ if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
4457
4684
  }
4458
4685
  buckets.set(key, cur);
4459
4686
  });
@@ -4462,7 +4689,7 @@ function bucketEdges(graph) {
4462
4689
  function nodeIsFrontier(graph, nodeId) {
4463
4690
  if (!graph.hasNode(nodeId)) return false;
4464
4691
  const attrs = graph.getNodeAttributes(nodeId);
4465
- return attrs.type === import_types19.NodeType.FrontierNode;
4692
+ return attrs.type === import_types20.NodeType.FrontierNode;
4466
4693
  }
4467
4694
  function clampConfidence(n) {
4468
4695
  if (!Number.isFinite(n)) return 0;
@@ -4483,6 +4710,7 @@ function gradedConfidence(edge) {
4483
4710
  }
4484
4711
  function detectMissingDivergences(graph, bucket) {
4485
4712
  const out = [];
4713
+ if (bucket.type === import_types20.EdgeType.CONTAINS) return out;
4486
4714
  if (bucket.extracted && !bucket.observed) {
4487
4715
  if (!nodeIsFrontier(graph, bucket.target)) {
4488
4716
  out.push({
@@ -4523,7 +4751,7 @@ function declaredHostFor(svc) {
4523
4751
  function hasExtractedConfiguredBy(graph, svcId) {
4524
4752
  for (const edgeId of graph.outboundEdges(svcId)) {
4525
4753
  const e = graph.getEdgeAttributes(edgeId);
4526
- if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4754
+ if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
4527
4755
  return true;
4528
4756
  }
4529
4757
  }
@@ -4536,10 +4764,10 @@ function detectHostMismatch(graph, svcId, svc) {
4536
4764
  const out = [];
4537
4765
  for (const edgeId of graph.outboundEdges(svcId)) {
4538
4766
  const edge = graph.getEdgeAttributes(edgeId);
4539
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4540
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4767
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4768
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4541
4769
  const target = graph.getNodeAttributes(edge.target);
4542
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4770
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4543
4771
  const observedHost = target.host?.trim();
4544
4772
  if (!observedHost) continue;
4545
4773
  if (observedHost === declaredHost) continue;
@@ -4561,10 +4789,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4561
4789
  const deps = svc.dependencies ?? {};
4562
4790
  for (const edgeId of graph.outboundEdges(svcId)) {
4563
4791
  const edge = graph.getEdgeAttributes(edgeId);
4564
- if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4565
- if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4792
+ if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
4793
+ if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
4566
4794
  const target = graph.getNodeAttributes(edge.target);
4567
- if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4795
+ if (target.type !== import_types20.NodeType.DatabaseNode) continue;
4568
4796
  for (const pair of compatPairs()) {
4569
4797
  if (pair.engine !== target.engine) continue;
4570
4798
  const declared = deps[pair.driver];
@@ -4625,7 +4853,7 @@ function computeDivergences(graph, opts = {}) {
4625
4853
  }
4626
4854
  graph.forEachNode((nodeId, attrs) => {
4627
4855
  const n = attrs;
4628
- if (n.type !== import_types19.NodeType.ServiceNode) return;
4856
+ if (n.type !== import_types20.NodeType.ServiceNode) return;
4629
4857
  const svc = n;
4630
4858
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4631
4859
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4658,7 +4886,7 @@ function computeDivergences(graph, opts = {}) {
4658
4886
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4659
4887
  return a.target.localeCompare(b.target);
4660
4888
  });
4661
- return import_types19.DivergenceResultSchema.parse({
4889
+ return import_types20.DivergenceResultSchema.parse({
4662
4890
  divergences: filtered,
4663
4891
  totalAffected: filtered.length,
4664
4892
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4669,7 +4897,7 @@ function computeDivergences(graph, opts = {}) {
4669
4897
  init_cjs_shims();
4670
4898
  var import_node_fs18 = require("fs");
4671
4899
  var import_node_path31 = __toESM(require("path"), 1);
4672
- var import_types20 = require("@neat.is/types");
4900
+ var import_types21 = require("@neat.is/types");
4673
4901
  var SCHEMA_VERSION = 4;
4674
4902
  function migrateV1ToV2(payload) {
4675
4903
  const nodes = payload.graph.nodes;
@@ -4691,12 +4919,12 @@ function migrateV2ToV3(payload) {
4691
4919
  for (const edge of edges) {
4692
4920
  const attrs = edge.attributes;
4693
4921
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4694
- attrs.provenance = import_types20.Provenance.OBSERVED;
4922
+ attrs.provenance = import_types21.Provenance.OBSERVED;
4695
4923
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4696
4924
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4697
4925
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4698
4926
  if (type && source && target) {
4699
- const newId = (0, import_types20.observedEdgeId)(source, target, type);
4927
+ const newId = (0, import_types21.observedEdgeId)(source, target, type);
4700
4928
  attrs.id = newId;
4701
4929
  if (edge.key) edge.key = newId;
4702
4930
  }
@@ -4816,7 +5044,7 @@ ${NEAT_OUT_LINE}
4816
5044
 
4817
5045
  // src/summary.ts
4818
5046
  init_cjs_shims();
4819
- var import_types21 = require("@neat.is/types");
5047
+ var import_types22 = require("@neat.is/types");
4820
5048
  function renderOtelEnvBlock() {
4821
5049
  return [
4822
5050
  "for prod OTel routing, set these in your deploy platform's env:",
@@ -4826,19 +5054,19 @@ function renderOtelEnvBlock() {
4826
5054
  }
4827
5055
  function findIncompatServices(nodes) {
4828
5056
  return nodes.filter(
4829
- (n) => n.type === import_types21.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
5057
+ (n) => n.type === import_types22.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
4830
5058
  );
4831
5059
  }
4832
5060
  function servicesWithoutObserved(nodes, edges) {
4833
5061
  const seen = /* @__PURE__ */ new Set();
4834
5062
  for (const e of edges) {
4835
- if (e.provenance === import_types21.Provenance.OBSERVED) {
5063
+ if (e.provenance === import_types22.Provenance.OBSERVED) {
4836
5064
  seen.add(e.source);
4837
5065
  seen.add(e.target);
4838
5066
  }
4839
5067
  }
4840
5068
  return nodes.filter(
4841
- (n) => n.type === import_types21.NodeType.ServiceNode && !seen.has(n.id)
5069
+ (n) => n.type === import_types22.NodeType.ServiceNode && !seen.has(n.id)
4842
5070
  );
4843
5071
  }
4844
5072
  function formatDivergence(d) {
@@ -4920,7 +5148,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
4920
5148
  init_cjs_shims();
4921
5149
  var import_fastify = __toESM(require("fastify"), 1);
4922
5150
  var import_cors = __toESM(require("@fastify/cors"), 1);
4923
- var import_types23 = require("@neat.is/types");
5151
+ var import_types24 = require("@neat.is/types");
4924
5152
 
4925
5153
  // src/diff.ts
4926
5154
  init_cjs_shims();
@@ -5056,7 +5284,7 @@ init_cjs_shims();
5056
5284
  var import_node_fs21 = require("fs");
5057
5285
  var import_node_os2 = __toESM(require("os"), 1);
5058
5286
  var import_node_path34 = __toESM(require("path"), 1);
5059
- var import_types22 = require("@neat.is/types");
5287
+ var import_types23 = require("@neat.is/types");
5060
5288
  var LOCK_TIMEOUT_MS = 5e3;
5061
5289
  var LOCK_RETRY_MS = 50;
5062
5290
  function neatHome() {
@@ -5135,10 +5363,10 @@ async function readRegistry() {
5135
5363
  throw err;
5136
5364
  }
5137
5365
  const parsed = JSON.parse(raw);
5138
- return import_types22.RegistryFileSchema.parse(parsed);
5366
+ return import_types23.RegistryFileSchema.parse(parsed);
5139
5367
  }
5140
5368
  async function writeRegistry(reg) {
5141
- const validated = import_types22.RegistryFileSchema.parse(reg);
5369
+ const validated = import_types23.RegistryFileSchema.parse(reg);
5142
5370
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5143
5371
  }
5144
5372
  var ProjectNameCollisionError = class extends Error {
@@ -5401,11 +5629,11 @@ function registerRoutes(scope, ctx) {
5401
5629
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
5402
5630
  const parsed = [];
5403
5631
  for (const c of candidates) {
5404
- const r = import_types23.DivergenceTypeSchema.safeParse(c);
5632
+ const r = import_types24.DivergenceTypeSchema.safeParse(c);
5405
5633
  if (!r.success) {
5406
5634
  return reply.code(400).send({
5407
5635
  error: `unknown divergence type "${c}"`,
5408
- allowed: import_types23.DivergenceTypeSchema.options
5636
+ allowed: import_types24.DivergenceTypeSchema.options
5409
5637
  });
5410
5638
  }
5411
5639
  parsed.push(r.data);
@@ -5618,7 +5846,7 @@ function registerRoutes(scope, ctx) {
5618
5846
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
5619
5847
  let violations = await log.readAll();
5620
5848
  if (req.query.severity) {
5621
- const sev = import_types23.PolicySeveritySchema.safeParse(req.query.severity);
5849
+ const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
5622
5850
  if (!sev.success) {
5623
5851
  return reply.code(400).send({
5624
5852
  error: "invalid severity",
@@ -5635,7 +5863,7 @@ function registerRoutes(scope, ctx) {
5635
5863
  scope.post("/policies/check", async (req, reply) => {
5636
5864
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
5637
5865
  if (!proj) return;
5638
- const parsed = import_types23.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5866
+ const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5639
5867
  if (!parsed.success) {
5640
5868
  return reply.code(400).send({
5641
5869
  error: "invalid /policies/check body",
@@ -5672,14 +5900,18 @@ function registerRoutes(scope, ctx) {
5672
5900
  async function buildApi(opts) {
5673
5901
  const app = (0, import_fastify.default)({ logger: false });
5674
5902
  await app.register(import_cors.default, { origin: true });
5903
+ const env = readAuthEnv();
5904
+ const authToken = opts.authToken ?? env.authToken;
5905
+ const trustProxy = opts.trustProxy ?? env.trustProxy;
5906
+ const publicRead = opts.publicRead ?? env.publicRead;
5675
5907
  mountBearerAuth(app, {
5676
- token: opts.authToken,
5677
- trustProxy: opts.trustProxy,
5678
- publicRead: opts.publicRead
5908
+ token: authToken,
5909
+ trustProxy,
5910
+ publicRead
5679
5911
  });
5680
5912
  app.get("/api/config", async () => ({
5681
- publicRead: opts.publicRead === true,
5682
- authProxy: opts.trustProxy === true
5913
+ publicRead: publicRead === true,
5914
+ authProxy: trustProxy === true
5683
5915
  }));
5684
5916
  const startedAt = opts.startedAt ?? Date.now();
5685
5917
  const registry = buildLegacyRegistry(opts);
@@ -5769,6 +6001,7 @@ async function buildApi(opts) {
5769
6001
  }
5770
6002
 
5771
6003
  // src/watch.ts
6004
+ init_auth();
5772
6005
  init_otel();
5773
6006
  init_otel_grpc();
5774
6007
 
@@ -6253,7 +6486,9 @@ async function startWatch(graph, opts) {
6253
6486
  project: projectName,
6254
6487
  onPolicyTrigger
6255
6488
  });
6256
- const host = opts.host ?? "0.0.0.0";
6489
+ const auth = readAuthEnv();
6490
+ const host = opts.host ?? (auth.authToken ? "0.0.0.0" : "127.0.0.1");
6491
+ assertBindAuthority(host, auth.authToken);
6257
6492
  const port = opts.port ?? 8080;
6258
6493
  const otelPort = opts.otelPort ?? 4318;
6259
6494
  const cachePath = opts.embeddingsCachePath ?? import_node_path38.default.join(import_node_path38.default.dirname(opts.outPath), "embeddings.json");
@@ -6569,38 +6804,123 @@ var import_node_path40 = __toESM(require("path"), 1);
6569
6804
  // src/installers/templates.ts
6570
6805
  init_cjs_shims();
6571
6806
  var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
6807
+ var OTEL_INIT_STAMP = "// neat-template-version: 3 \u2014 file-first call-site capture (ADR-089) + OTLP bearer auth (#410).";
6808
+ var OTEL_OTLP_HEADERS_JS = "if (process.env.NEAT_OTEL_TOKEN) process.env.OTEL_EXPORTER_OTLP_HEADERS ||= 'Authorization=Bearer ' + process.env.NEAT_OTEL_TOKEN";
6809
+ function neatCallsiteProcessorSource(ts) {
6810
+ const stackT = ts ? ": string" : "";
6811
+ const spanT = ts ? ": any" : "";
6812
+ const strOpt = ts ? ": string | undefined" : "";
6813
+ return `function __neatPickUserFrame(stack${stackT}) {
6814
+ const lines = String(stack || '').split('\\n')
6815
+ for (let i = 0; i < lines.length; i++) {
6816
+ const raw = lines[i].trim()
6817
+ if (raw.indexOf('at ') !== 0) continue
6818
+ if (raw.indexOf('node_modules') !== -1) continue
6819
+ if (raw.indexOf('@opentelemetry') !== -1) continue
6820
+ if (raw.indexOf('node:') !== -1) continue
6821
+ if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
6822
+ const bodyText = raw.slice(3)
6823
+ const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
6824
+ if (!loc) continue
6825
+ const paren = bodyText.lastIndexOf('(')
6826
+ let filepath${strOpt}
6827
+ let fn${strOpt}
6828
+ if (paren !== -1) {
6829
+ fn = bodyText.slice(0, paren).trim()
6830
+ if (fn.indexOf('async ') === 0) fn = fn.slice(6)
6831
+ if (fn.indexOf('new ') === 0) fn = fn.slice(4)
6832
+ filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
6833
+ } else {
6834
+ filepath = bodyText.slice(0, bodyText.length - loc[0].length)
6835
+ }
6836
+ if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
6837
+ if (!filepath) continue
6838
+ return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
6839
+ }
6840
+ return null
6841
+ }
6842
+
6843
+ class NeatCallSiteSpanProcessor {
6844
+ onStart(span${spanT}) {
6845
+ if (!span || (span.kind !== 2 && span.kind !== 3)) return
6846
+ const frame = __neatPickUserFrame(new Error().stack)
6847
+ if (!frame) return
6848
+ span.setAttribute('code.filepath', frame.filepath)
6849
+ if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
6850
+ if (frame.function) span.setAttribute('code.function', frame.function)
6851
+ }
6852
+ onEnd() {}
6853
+ forceFlush() { return Promise.resolve() }
6854
+ shutdown() { return Promise.resolve() }
6855
+ }`;
6856
+ }
6857
+ var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
6858
+ var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
6859
+ function neatRegisterCallsiteSource(ts) {
6860
+ const providerT = ts ? ": any" : "";
6861
+ return `try {
6862
+ const provider${providerT} = trace.getTracerProvider()
6863
+ const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
6864
+ if (delegate && typeof delegate.addSpanProcessor === 'function') {
6865
+ delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
6866
+ }
6867
+ } catch {
6868
+ // capture is best-effort; span export is unaffected
6869
+ }`;
6870
+ }
6572
6871
  var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
6872
+ ${OTEL_INIT_STAMP}
6573
6873
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6574
6874
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6875
+ ${OTEL_OTLP_HEADERS_JS}
6575
6876
 
6576
6877
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6577
6878
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
6879
+ const { trace } = require('@opentelemetry/api')
6880
+
6881
+ ${CALLSITE_PROCESSOR_JS}
6578
6882
 
6579
6883
  const instrumentations = [getNodeAutoInstrumentations()]
6580
6884
  __INSTRUMENTATION_BLOCK__
6581
- new NodeSDK({ instrumentations }).start()
6885
+ const sdk = new NodeSDK({ instrumentations })
6886
+ sdk.start()
6887
+ ${neatRegisterCallsiteSource(false)}
6582
6888
  `;
6583
6889
  var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
6890
+ ${OTEL_INIT_STAMP}
6584
6891
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6585
6892
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6893
+ ${OTEL_OTLP_HEADERS_JS}
6586
6894
 
6587
6895
  import { NodeSDK } from '@opentelemetry/sdk-node'
6588
6896
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6897
+ import { trace } from '@opentelemetry/api'
6898
+
6899
+ ${CALLSITE_PROCESSOR_JS}
6589
6900
 
6590
6901
  const instrumentations = [getNodeAutoInstrumentations()]
6591
6902
  __INSTRUMENTATION_BLOCK__
6592
- new NodeSDK({ instrumentations }).start()
6903
+ const sdk = new NodeSDK({ instrumentations })
6904
+ sdk.start()
6905
+ ${neatRegisterCallsiteSource(false)}
6593
6906
  `;
6594
6907
  var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
6908
+ ${OTEL_INIT_STAMP}
6595
6909
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6596
6910
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6911
+ ${OTEL_OTLP_HEADERS_JS}
6597
6912
 
6598
6913
  import { NodeSDK } from '@opentelemetry/sdk-node'
6599
6914
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
6915
+ import { trace } from '@opentelemetry/api'
6916
+
6917
+ ${CALLSITE_PROCESSOR_TS}
6600
6918
 
6601
6919
  const instrumentations = [getNodeAutoInstrumentations()]
6602
6920
  __INSTRUMENTATION_BLOCK__
6603
- new NodeSDK({ instrumentations }).start()
6921
+ const sdk = new NodeSDK({ instrumentations })
6922
+ sdk.start()
6923
+ ${neatRegisterCallsiteSource(true)}
6604
6924
  `;
6605
6925
  function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
6606
6926
  const block = registrations.length === 0 ? "" : `
@@ -6613,6 +6933,8 @@ function renderEnvNeat(serviceName, projectName) {
6613
6933
  "# Generated by `neat init --apply` (ADR-069).",
6614
6934
  `OTEL_SERVICE_NAME=${serviceName}`,
6615
6935
  `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/projects/${projectName}/v1/traces`,
6936
+ "# Set NEAT_OTEL_TOKEN to the daemon's OTLP secret to authenticate exported spans (#410).",
6937
+ "# NEAT_OTEL_TOKEN=",
6616
6938
  ""
6617
6939
  ].join("\n");
6618
6940
  }
@@ -6634,6 +6956,7 @@ export async function register() {
6634
6956
  var NEXT_INSTRUMENTATION_NODE_TS = `${NEXT_INSTRUMENTATION_HEADER}
6635
6957
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6636
6958
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6959
+ ${OTEL_OTLP_HEADERS_JS}
6637
6960
 
6638
6961
  import { NodeSDK } from '@opentelemetry/sdk-node'
6639
6962
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
@@ -6645,6 +6968,7 @@ new NodeSDK({ instrumentations }).start()
6645
6968
  var NEXT_INSTRUMENTATION_NODE_JS = `${NEXT_INSTRUMENTATION_HEADER}
6646
6969
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6647
6970
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6971
+ ${OTEL_OTLP_HEADERS_JS}
6648
6972
 
6649
6973
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6650
6974
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
@@ -6663,6 +6987,7 @@ var FRAMEWORK_OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-074).
6663
6987
  var FRAMEWORK_OTEL_INIT_TS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6664
6988
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6665
6989
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
6990
+ ${OTEL_OTLP_HEADERS_JS}
6666
6991
 
6667
6992
  import { NodeSDK } from '@opentelemetry/sdk-node'
6668
6993
  import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
@@ -6676,6 +7001,7 @@ sdk.start()
6676
7001
  var FRAMEWORK_OTEL_INIT_JS_BODY = `${FRAMEWORK_OTEL_INIT_HEADER}
6677
7002
  process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
6678
7003
  process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
7004
+ ${OTEL_OTLP_HEADERS_JS}
6679
7005
 
6680
7006
  const { NodeSDK } = require('@opentelemetry/sdk-node')
6681
7007
  const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
@@ -6824,6 +7150,23 @@ async function exists2(p) {
6824
7150
  return false;
6825
7151
  }
6826
7152
  }
7153
+ async function readFileMaybe(p) {
7154
+ try {
7155
+ return await import_node_fs25.promises.readFile(p, "utf8");
7156
+ } catch {
7157
+ return null;
7158
+ }
7159
+ }
7160
+ async function planOtelInitGeneration(file, contents) {
7161
+ const existing = await readFileMaybe(file);
7162
+ if (existing === null) {
7163
+ return { file, contents, skipIfExists: true };
7164
+ }
7165
+ if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
7166
+ return { file, contents, skipIfExists: false };
7167
+ }
7168
+ return null;
7169
+ }
6827
7170
  async function detect(serviceDir) {
6828
7171
  const pkg = await readPackageJson(serviceDir);
6829
7172
  return pkg !== null && typeof pkg.name === "string";
@@ -7554,18 +7897,11 @@ async function plan(serviceDir, opts) {
7554
7897
  const projectName = projectToken(pkg, serviceDir, project);
7555
7898
  const registrations = nonBundled.map((i) => i.registration);
7556
7899
  const generatedFiles = [];
7557
- if (!await exists2(otelInitFile)) {
7558
- generatedFiles.push({
7559
- file: otelInitFile,
7560
- contents: renderNodeOtelInit(
7561
- otelInitContents(flavor),
7562
- svcName,
7563
- projectName,
7564
- registrations
7565
- ),
7566
- skipIfExists: true
7567
- });
7568
- }
7900
+ const otelInitGen = await planOtelInitGeneration(
7901
+ otelInitFile,
7902
+ renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
7903
+ );
7904
+ if (otelInitGen) generatedFiles.push(otelInitGen);
7569
7905
  if (!await exists2(envNeatFile)) {
7570
7906
  generatedFiles.push({
7571
7907
  file: envNeatFile,
@@ -8608,7 +8944,7 @@ var import_node_path44 = __toESM(require("path"), 1);
8608
8944
 
8609
8945
  // src/cli-client.ts
8610
8946
  init_cjs_shims();
8611
- var import_types24 = require("@neat.is/types");
8947
+ var import_types25 = require("@neat.is/types");
8612
8948
  var HttpError = class extends Error {
8613
8949
  constructor(status2, message, responseBody = "") {
8614
8950
  super(message);
@@ -8746,7 +9082,7 @@ async function runBlastRadius(client, input) {
8746
9082
  }
8747
9083
  }
8748
9084
  function formatBlastEntry(n) {
8749
- const tag = n.edgeProvenance === import_types24.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
9085
+ const tag = n.edgeProvenance === import_types25.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
8750
9086
  return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
8751
9087
  }
8752
9088
  async function runDependencies(client, input) {
@@ -8792,9 +9128,9 @@ async function runObservedDependencies(client, input) {
8792
9128
  const edges = await client.get(
8793
9129
  projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
8794
9130
  );
8795
- const observed = edges.outbound.filter((e) => e.provenance === import_types24.Provenance.OBSERVED);
9131
+ const observed = edges.outbound.filter((e) => e.provenance === import_types25.Provenance.OBSERVED);
8796
9132
  if (observed.length === 0) {
8797
- const hasExtracted = edges.outbound.some((e) => e.provenance === import_types24.Provenance.EXTRACTED);
9133
+ const hasExtracted = edges.outbound.some((e) => e.provenance === import_types25.Provenance.EXTRACTED);
8798
9134
  const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
8799
9135
  return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
8800
9136
  }
@@ -8802,7 +9138,7 @@ async function runObservedDependencies(client, input) {
8802
9138
  return {
8803
9139
  summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
8804
9140
  block: blockLines.join("\n"),
8805
- provenance: import_types24.Provenance.OBSERVED
9141
+ provenance: import_types25.Provenance.OBSERVED
8806
9142
  };
8807
9143
  } catch (err) {
8808
9144
  if (err instanceof HttpError && err.status === 404) {
@@ -8856,7 +9192,7 @@ async function runIncidents(client, input) {
8856
9192
  return {
8857
9193
  summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
8858
9194
  block: blockLines.join("\n"),
8859
- provenance: import_types24.Provenance.OBSERVED
9195
+ provenance: import_types25.Provenance.OBSERVED
8860
9196
  };
8861
9197
  } catch (err) {
8862
9198
  if (err instanceof HttpError && err.status === 404) {
@@ -8965,7 +9301,7 @@ async function runStaleEdges(client, input) {
8965
9301
  return {
8966
9302
  summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
8967
9303
  block: blockLines.join("\n"),
8968
- provenance: import_types24.Provenance.STALE
9304
+ provenance: import_types25.Provenance.STALE
8969
9305
  };
8970
9306
  }
8971
9307
  async function runPolicies(client, input) {
@@ -9279,7 +9615,7 @@ async function runSync(opts) {
9279
9615
  }
9280
9616
 
9281
9617
  // src/cli.ts
9282
- var import_types25 = require("@neat.is/types");
9618
+ var import_types26 = require("@neat.is/types");
9283
9619
  function usage() {
9284
9620
  console.log("usage: neat <command> [args] [--project <name>]");
9285
9621
  console.log("");
@@ -9519,7 +9855,7 @@ function printBanner() {
9519
9855
  console.log("\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D ");
9520
9856
  console.log("");
9521
9857
  console.log(" Network Expressive Architecting Tool");
9522
- console.log(" neat.is \xB7 v0.4.0 \xB7 Apache 2.0");
9858
+ console.log(` neat.is \xB7 v${readPackageVersion()} \xB7 Apache 2.0`);
9523
9859
  console.log("");
9524
9860
  }
9525
9861
  function printDiscoveryReport(opts, services) {
@@ -10112,10 +10448,10 @@ async function runQueryVerb(cmd, parsed) {
10112
10448
  const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
10113
10449
  const out = [];
10114
10450
  for (const p of parts) {
10115
- const r = import_types25.DivergenceTypeSchema.safeParse(p);
10451
+ const r = import_types26.DivergenceTypeSchema.safeParse(p);
10116
10452
  if (!r.success) {
10117
10453
  console.error(
10118
- `neat divergences: unknown --type "${p}". allowed: ${import_types25.DivergenceTypeSchema.options.join(", ")}`
10454
+ `neat divergences: unknown --type "${p}". allowed: ${import_types26.DivergenceTypeSchema.options.join(", ")}`
10119
10455
  );
10120
10456
  return 2;
10121
10457
  }
@@ -10164,6 +10500,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
10164
10500
  CLAUDE_SKILL_CONFIG,
10165
10501
  QUERY_VERBS,
10166
10502
  parseArgs,
10503
+ printBanner,
10167
10504
  readPackageVersion,
10168
10505
  runInit,
10169
10506
  runQueryVerb,