@neat.is/core 0.3.1 → 0.3.3

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/neatd.cjs CHANGED
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req2) {
100
100
  };
101
101
  }
102
102
  function resolveProtoRoot() {
103
- const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
- return import_node_path31.default.resolve(here, "..", "proto");
103
+ const here = import_node_path33.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
+ return import_node_path33.default.resolve(here, "..", "proto");
105
105
  }
106
106
  function loadTraceService() {
107
107
  const protoRoot = resolveProtoRoot();
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
156
156
  })
157
157
  };
158
158
  }
159
- var import_node_url, import_node_path31, grpc, protoLoader;
159
+ var import_node_url, import_node_path33, grpc, protoLoader;
160
160
  var init_otel_grpc = __esm({
161
161
  "src/otel-grpc.ts"() {
162
162
  "use strict";
163
163
  init_cjs_shims();
164
164
  import_node_url = require("url");
165
- import_node_path31 = __toESM(require("path"), 1);
165
+ import_node_path33 = __toESM(require("path"), 1);
166
166
  grpc = __toESM(require("@grpc/grpc-js"), 1);
167
167
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
168
168
  init_otel();
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
259
259
  }
260
260
  function loadProtobufDecoder() {
261
261
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
262
- const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
- const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
262
+ const here = import_node_path34.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
+ const protoRoot = import_node_path34.default.resolve(here, "..", "proto");
264
264
  const root = new import_protobufjs.default.Root();
265
- root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
265
+ root.resolvePath = (_origin, target) => import_node_path34.default.resolve(protoRoot, target);
266
266
  root.loadSync(
267
267
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
268
268
  { keepCase: true }
@@ -354,12 +354,12 @@ async function buildOtelReceiver(opts) {
354
354
  };
355
355
  return decorated;
356
356
  }
357
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
357
+ var import_node_path34, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
358
358
  var init_otel = __esm({
359
359
  "src/otel.ts"() {
360
360
  "use strict";
361
361
  init_cjs_shims();
362
- import_node_path32 = __toESM(require("path"), 1);
362
+ import_node_path34 = __toESM(require("path"), 1);
363
363
  import_node_url2 = require("url");
364
364
  import_fastify2 = __toESM(require("fastify"), 1);
365
365
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -369,13 +369,13 @@ var init_otel = __esm({
369
369
 
370
370
  // src/neatd.ts
371
371
  init_cjs_shims();
372
- var import_node_fs20 = require("fs");
373
- var import_node_path35 = __toESM(require("path"), 1);
372
+ var import_node_fs22 = require("fs");
373
+ var import_node_path37 = __toESM(require("path"), 1);
374
374
 
375
375
  // src/daemon.ts
376
376
  init_cjs_shims();
377
- var import_node_fs19 = require("fs");
378
- var import_node_path33 = __toESM(require("path"), 1);
377
+ var import_node_fs21 = require("fs");
378
+ var import_node_path35 = __toESM(require("path"), 1);
379
379
 
380
380
  // src/graph.ts
381
381
  init_cjs_shims();
@@ -831,19 +831,19 @@ function confidenceFromMix(edges, now = Date.now()) {
831
831
  function longestIncomingWalk(graph, start, maxDepth) {
832
832
  let best = { path: [start], edges: [] };
833
833
  const visited = /* @__PURE__ */ new Set([start]);
834
- function step(node, path36, edges) {
835
- if (path36.length > best.path.length) {
836
- best = { path: [...path36], edges: [...edges] };
834
+ function step(node, path38, edges) {
835
+ if (path38.length > best.path.length) {
836
+ best = { path: [...path38], edges: [...edges] };
837
837
  }
838
- if (path36.length - 1 >= maxDepth) return;
838
+ if (path38.length - 1 >= maxDepth) return;
839
839
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
840
840
  for (const [srcId, edge] of incoming) {
841
841
  if (visited.has(srcId)) continue;
842
842
  visited.add(srcId);
843
- path36.push(srcId);
843
+ path38.push(srcId);
844
844
  edges.push(edge);
845
- step(srcId, path36, edges);
846
- path36.pop();
845
+ step(srcId, path38, edges);
846
+ path38.pop();
847
847
  edges.pop();
848
848
  visited.delete(srcId);
849
849
  }
@@ -1785,8 +1785,8 @@ async function readErrorEvents(errorsPath) {
1785
1785
 
1786
1786
  // src/extract/services.ts
1787
1787
  init_cjs_shims();
1788
- var import_node_fs7 = require("fs");
1789
- var import_node_path7 = __toESM(require("path"), 1);
1788
+ var import_node_fs8 = require("fs");
1789
+ var import_node_path8 = __toESM(require("path"), 1);
1790
1790
  var import_ignore = __toESM(require("ignore"), 1);
1791
1791
  var import_minimatch2 = require("minimatch");
1792
1792
  var import_types5 = require("@neat.is/types");
@@ -1810,9 +1810,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1810
1810
  function isConfigFile(name) {
1811
1811
  const ext = import_node_path4.default.extname(name);
1812
1812
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
1813
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
1813
+ if (name === ".env" || name.startsWith(".env.")) {
1814
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1815
+ return { match: true, fileType: "env" };
1816
+ }
1814
1817
  return { match: false, fileType: "" };
1815
1818
  }
1819
+ function isTestPath(filePath) {
1820
+ const normalised = filePath.replace(/\\/g, "/");
1821
+ const segments = normalised.split("/");
1822
+ for (const seg of segments) {
1823
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
1824
+ return true;
1825
+ }
1826
+ }
1827
+ const base = segments[segments.length - 1] ?? "";
1828
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
1829
+ }
1830
+ function isEnvTemplateFile(name) {
1831
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
1832
+ return true;
1833
+ }
1834
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
1835
+ }
1836
+ function maskCommentsInSource(src) {
1837
+ const len = src.length;
1838
+ const out = new Array(len);
1839
+ let i = 0;
1840
+ let inString = 0;
1841
+ let escaped = false;
1842
+ while (i < len) {
1843
+ const c = src[i];
1844
+ if (inString !== 0) {
1845
+ out[i] = c;
1846
+ if (escaped) {
1847
+ escaped = false;
1848
+ } else if (c === "\\") {
1849
+ escaped = true;
1850
+ } else if (c === inString) {
1851
+ inString = 0;
1852
+ }
1853
+ i++;
1854
+ continue;
1855
+ }
1856
+ if (c === "/" && i + 1 < len) {
1857
+ const next = src[i + 1];
1858
+ if (next === "/") {
1859
+ out[i] = " ";
1860
+ out[i + 1] = " ";
1861
+ let j = i + 2;
1862
+ while (j < len && src[j] !== "\n") {
1863
+ out[j] = " ";
1864
+ j++;
1865
+ }
1866
+ i = j;
1867
+ continue;
1868
+ }
1869
+ if (next === "*") {
1870
+ out[i] = " ";
1871
+ out[i + 1] = " ";
1872
+ let j = i + 2;
1873
+ while (j < len) {
1874
+ if (src[j] === "\n") {
1875
+ out[j] = "\n";
1876
+ j++;
1877
+ continue;
1878
+ }
1879
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
1880
+ out[j] = " ";
1881
+ out[j + 1] = " ";
1882
+ j += 2;
1883
+ break;
1884
+ }
1885
+ out[j] = " ";
1886
+ j++;
1887
+ }
1888
+ i = j;
1889
+ continue;
1890
+ }
1891
+ }
1892
+ out[i] = c;
1893
+ if (c === "'" || c === '"' || c === "`") inString = c;
1894
+ i++;
1895
+ }
1896
+ return out.join("");
1897
+ }
1898
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
1899
+ function urlMatchesHost(urlString, host) {
1900
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
1901
+ if (!URL_LIKE.test(urlString)) return false;
1902
+ const [wantedHost, wantedPort] = host.split(":");
1903
+ let parsed;
1904
+ try {
1905
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
1906
+ parsed = new URL(candidate);
1907
+ } catch {
1908
+ return false;
1909
+ }
1910
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
1911
+ if (wantedPort && parsed.port !== wantedPort) return false;
1912
+ return true;
1913
+ }
1816
1914
  function cleanVersion(raw) {
1817
1915
  if (!raw) return void 0;
1818
1916
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -1968,6 +2066,33 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
1968
2066
  return author ?? void 0;
1969
2067
  }
1970
2068
 
2069
+ // src/extract/errors.ts
2070
+ init_cjs_shims();
2071
+ var import_node_fs7 = require("fs");
2072
+ var import_node_path7 = __toESM(require("path"), 1);
2073
+ var sink = [];
2074
+ function recordExtractionError(producer, file, err) {
2075
+ const e = err instanceof Error ? err : new Error(String(err));
2076
+ sink.push({
2077
+ producer,
2078
+ file,
2079
+ error: e.message,
2080
+ stack: e.stack,
2081
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2082
+ source: "extract"
2083
+ });
2084
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2085
+ }
2086
+ function drainExtractionErrors() {
2087
+ return sink.splice(0, sink.length);
2088
+ }
2089
+ async function writeExtractionErrors(errors, errorsPath) {
2090
+ if (errors.length === 0) return;
2091
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2092
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2093
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2094
+ }
2095
+
1971
2096
  // src/extract/services.ts
1972
2097
  var DEFAULT_SCAN_DEPTH = 5;
1973
2098
  function parseScanDepth() {
@@ -1984,21 +2109,21 @@ function workspaceGlobs(pkg) {
1984
2109
  return null;
1985
2110
  }
1986
2111
  async function loadGitignore(scanPath) {
1987
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2112
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
1988
2113
  if (!await exists(gitignorePath)) return null;
1989
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2114
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
1990
2115
  return (0, import_ignore.default)().add(raw);
1991
2116
  }
1992
2117
  async function walkDirs(start, scanPath, options, visit) {
1993
2118
  async function recurse(current, depth) {
1994
2119
  if (depth > options.maxDepth) return;
1995
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2120
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1996
2121
  for (const entry2 of entries) {
1997
2122
  if (!entry2.isDirectory()) continue;
1998
2123
  if (IGNORED_DIRS.has(entry2.name)) continue;
1999
- const child = import_node_path7.default.join(current, entry2.name);
2124
+ const child = import_node_path8.default.join(current, entry2.name);
2000
2125
  if (options.ig) {
2001
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2126
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2002
2127
  if (rel && options.ig.ignores(rel + "/")) continue;
2003
2128
  }
2004
2129
  await visit(child);
@@ -2013,8 +2138,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2013
2138
  for (const raw of globs) {
2014
2139
  const pattern = raw.replace(/^\.\//, "");
2015
2140
  if (!pattern.includes("*")) {
2016
- const candidate = import_node_path7.default.join(scanPath, pattern);
2017
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2141
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2142
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2018
2143
  continue;
2019
2144
  }
2020
2145
  const segments = pattern.split("/");
@@ -2023,13 +2148,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2023
2148
  if (seg.includes("*")) break;
2024
2149
  staticSegments.push(seg);
2025
2150
  }
2026
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2151
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2027
2152
  if (!await exists(start)) continue;
2028
2153
  const hasDoubleStar = pattern.includes("**");
2029
2154
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2030
2155
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2031
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2032
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2156
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2157
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2033
2158
  found.add(dir);
2034
2159
  }
2035
2160
  });
@@ -2037,15 +2162,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2037
2162
  return [...found];
2038
2163
  }
2039
2164
  async function discoverNodeService(scanPath, dir) {
2040
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2165
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2041
2166
  if (!await exists(pkgPath)) return null;
2042
2167
  let pkg;
2043
2168
  try {
2044
2169
  pkg = await readJson(pkgPath);
2045
2170
  } catch (err) {
2046
- console.warn(
2047
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2048
- );
2171
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2049
2172
  return null;
2050
2173
  }
2051
2174
  if (!pkg.name) return null;
@@ -2056,7 +2179,7 @@ async function discoverNodeService(scanPath, dir) {
2056
2179
  language: "javascript",
2057
2180
  version: pkg.version,
2058
2181
  dependencies: pkg.dependencies ?? {},
2059
- repoPath: import_node_path7.default.relative(scanPath, dir),
2182
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2060
2183
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2061
2184
  };
2062
2185
  return { pkg, dir, node };
@@ -2072,19 +2195,21 @@ async function discoverPyService(scanPath, dir) {
2072
2195
  language: "python",
2073
2196
  version: py.version,
2074
2197
  dependencies: py.dependencies,
2075
- repoPath: import_node_path7.default.relative(scanPath, dir)
2198
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2076
2199
  };
2077
2200
  return { pkg, dir, node };
2078
2201
  }
2079
2202
  async function discoverServices(scanPath) {
2080
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2203
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2081
2204
  let rootPkg = null;
2082
2205
  if (await exists(rootPkgPath)) {
2083
2206
  try {
2084
2207
  rootPkg = await readJson(rootPkgPath);
2085
2208
  } catch (err) {
2086
- console.warn(
2087
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2209
+ recordExtractionError(
2210
+ "services workspaces",
2211
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2212
+ err
2088
2213
  );
2089
2214
  }
2090
2215
  }
@@ -2100,9 +2225,9 @@ async function discoverServices(scanPath) {
2100
2225
  scanPath,
2101
2226
  { maxDepth: parseScanDepth(), ig },
2102
2227
  async (dir) => {
2103
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2228
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2104
2229
  candidateDirs.push(dir);
2105
- } else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
2230
+ } else if (await exists(import_node_path8.default.join(dir, "pyproject.toml")) || await exists(import_node_path8.default.join(dir, "requirements.txt")) || await exists(import_node_path8.default.join(dir, "setup.py"))) {
2106
2231
  candidateDirs.push(dir);
2107
2232
  }
2108
2233
  }
@@ -2116,8 +2241,8 @@ async function discoverServices(scanPath) {
2116
2241
  if (!service) continue;
2117
2242
  const existingDir = seen.get(service.node.name);
2118
2243
  if (existingDir !== void 0) {
2119
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2120
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2244
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2245
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2121
2246
  console.warn(
2122
2247
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2123
2248
  );
@@ -2154,8 +2279,8 @@ function addServiceNodes(graph, services) {
2154
2279
 
2155
2280
  // src/extract/aliases.ts
2156
2281
  init_cjs_shims();
2157
- var import_node_path8 = __toESM(require("path"), 1);
2158
- var import_node_fs8 = require("fs");
2282
+ var import_node_path9 = __toESM(require("path"), 1);
2283
+ var import_node_fs9 = require("fs");
2159
2284
  var import_yaml2 = require("yaml");
2160
2285
  var import_types6 = require("@neat.is/types");
2161
2286
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2182,14 +2307,14 @@ function indexServicesByName(services) {
2182
2307
  const map = /* @__PURE__ */ new Map();
2183
2308
  for (const s of services) {
2184
2309
  map.set(s.node.name, s.node.id);
2185
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2310
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2186
2311
  }
2187
2312
  return map;
2188
2313
  }
2189
2314
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2190
2315
  let composePath = null;
2191
2316
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2192
- const abs = import_node_path8.default.join(scanPath, name);
2317
+ const abs = import_node_path9.default.join(scanPath, name);
2193
2318
  if (await exists(abs)) {
2194
2319
  composePath = abs;
2195
2320
  break;
@@ -2200,8 +2325,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2200
2325
  try {
2201
2326
  compose = await readYaml(composePath);
2202
2327
  } catch (err) {
2203
- console.warn(
2204
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2328
+ recordExtractionError(
2329
+ "aliases compose",
2330
+ import_node_path9.default.relative(scanPath, composePath),
2331
+ err
2205
2332
  );
2206
2333
  return;
2207
2334
  }
@@ -2243,15 +2370,13 @@ function parseDockerfileLabels(content) {
2243
2370
  }
2244
2371
  async function collectDockerfileAliases(graph, services) {
2245
2372
  for (const service of services) {
2246
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2373
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2247
2374
  if (!await exists(dockerfilePath)) continue;
2248
2375
  let content;
2249
2376
  try {
2250
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2377
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2251
2378
  } catch (err) {
2252
- console.warn(
2253
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2254
- );
2379
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2255
2380
  continue;
2256
2381
  }
2257
2382
  const aliases = parseDockerfileLabels(content);
@@ -2261,13 +2386,13 @@ async function collectDockerfileAliases(graph, services) {
2261
2386
  async function walkYamlFiles(start, depth = 0, max = 5) {
2262
2387
  if (depth > max) return [];
2263
2388
  const out = [];
2264
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2389
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2265
2390
  for (const entry2 of entries) {
2266
2391
  if (entry2.isDirectory()) {
2267
2392
  if (IGNORED_DIRS.has(entry2.name)) continue;
2268
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
2269
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
2270
- out.push(import_node_path8.default.join(start, entry2.name));
2393
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
2394
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
2395
+ out.push(import_node_path9.default.join(start, entry2.name));
2271
2396
  }
2272
2397
  }
2273
2398
  return out;
@@ -2294,7 +2419,7 @@ function k8sServiceTarget(doc, byName) {
2294
2419
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2295
2420
  const files = await walkYamlFiles(scanPath);
2296
2421
  for (const file of files) {
2297
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2422
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2298
2423
  let docs;
2299
2424
  try {
2300
2425
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2319,14 +2444,14 @@ async function addServiceAliases(graph, scanPath, services) {
2319
2444
 
2320
2445
  // src/extract/databases/index.ts
2321
2446
  init_cjs_shims();
2322
- var import_node_path16 = __toESM(require("path"), 1);
2447
+ var import_node_path17 = __toESM(require("path"), 1);
2323
2448
  var import_types7 = require("@neat.is/types");
2324
2449
 
2325
2450
  // src/extract/databases/db-config-yaml.ts
2326
2451
  init_cjs_shims();
2327
- var import_node_path9 = __toESM(require("path"), 1);
2452
+ var import_node_path10 = __toESM(require("path"), 1);
2328
2453
  async function parse(serviceDir) {
2329
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2454
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2330
2455
  if (!await exists(yamlPath)) return [];
2331
2456
  const raw = await readYaml(yamlPath);
2332
2457
  return [
@@ -2344,13 +2469,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2344
2469
 
2345
2470
  // src/extract/databases/dotenv.ts
2346
2471
  init_cjs_shims();
2347
- var import_node_fs10 = require("fs");
2348
- var import_node_path11 = __toESM(require("path"), 1);
2472
+ var import_node_fs11 = require("fs");
2473
+ var import_node_path12 = __toESM(require("path"), 1);
2349
2474
 
2350
2475
  // src/extract/databases/shared.ts
2351
2476
  init_cjs_shims();
2352
- var import_node_fs9 = require("fs");
2353
- var import_node_path10 = __toESM(require("path"), 1);
2477
+ var import_node_fs10 = require("fs");
2478
+ var import_node_path11 = __toESM(require("path"), 1);
2354
2479
  function schemeToEngine(scheme) {
2355
2480
  const s = scheme.toLowerCase().split("+")[0];
2356
2481
  switch (s) {
@@ -2389,14 +2514,14 @@ function parseConnectionString(url) {
2389
2514
  }
2390
2515
  async function readIfExists(filePath) {
2391
2516
  try {
2392
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2517
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2393
2518
  } catch {
2394
2519
  return null;
2395
2520
  }
2396
2521
  }
2397
2522
  async function findFirst(serviceDir, candidates) {
2398
2523
  for (const rel of candidates) {
2399
- const abs = import_node_path10.default.join(serviceDir, rel);
2524
+ const abs = import_node_path11.default.join(serviceDir, rel);
2400
2525
  const content = await readIfExists(abs);
2401
2526
  if (content !== null) return abs;
2402
2527
  }
@@ -2447,15 +2572,15 @@ function parseDotenvLine(line) {
2447
2572
  return { key, value };
2448
2573
  }
2449
2574
  async function parse2(serviceDir) {
2450
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2575
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2451
2576
  const configs = [];
2452
2577
  const seen = /* @__PURE__ */ new Set();
2453
2578
  for (const entry2 of entries) {
2454
2579
  if (!entry2.isFile()) continue;
2455
2580
  const match = isConfigFile(entry2.name);
2456
2581
  if (!match.match || match.fileType !== "env") continue;
2457
- const filePath = import_node_path11.default.join(serviceDir, entry2.name);
2458
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2582
+ const filePath = import_node_path12.default.join(serviceDir, entry2.name);
2583
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2459
2584
  for (const line of content.split("\n")) {
2460
2585
  const parsed = parseDotenvLine(line);
2461
2586
  if (!parsed) continue;
@@ -2474,9 +2599,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2474
2599
 
2475
2600
  // src/extract/databases/prisma.ts
2476
2601
  init_cjs_shims();
2477
- var import_node_path12 = __toESM(require("path"), 1);
2602
+ var import_node_path13 = __toESM(require("path"), 1);
2478
2603
  async function parse3(serviceDir) {
2479
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2604
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2480
2605
  const content = await readIfExists(schemaPath);
2481
2606
  if (!content) return [];
2482
2607
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2608,10 +2733,10 @@ var knexParser = { name: "knex", parse: parse5 };
2608
2733
 
2609
2734
  // src/extract/databases/ormconfig.ts
2610
2735
  init_cjs_shims();
2611
- var import_node_path13 = __toESM(require("path"), 1);
2736
+ var import_node_path14 = __toESM(require("path"), 1);
2612
2737
  async function parse6(serviceDir) {
2613
2738
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2614
- const abs = import_node_path13.default.join(serviceDir, candidate);
2739
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2615
2740
  if (!await exists(abs)) continue;
2616
2741
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2617
2742
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2671,9 +2796,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2671
2796
 
2672
2797
  // src/extract/databases/sequelize.ts
2673
2798
  init_cjs_shims();
2674
- var import_node_path14 = __toESM(require("path"), 1);
2799
+ var import_node_path15 = __toESM(require("path"), 1);
2675
2800
  async function parse8(serviceDir) {
2676
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2801
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2677
2802
  if (!await exists(configPath)) return [];
2678
2803
  const raw = await readJson(configPath);
2679
2804
  const out = [];
@@ -2700,7 +2825,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2700
2825
 
2701
2826
  // src/extract/databases/docker-compose.ts
2702
2827
  init_cjs_shims();
2703
- var import_node_path15 = __toESM(require("path"), 1);
2828
+ var import_node_path16 = __toESM(require("path"), 1);
2704
2829
  function portFromService(svc) {
2705
2830
  for (const raw of svc.ports ?? []) {
2706
2831
  const str = String(raw);
@@ -2727,7 +2852,7 @@ function databaseFromEnv(svc) {
2727
2852
  }
2728
2853
  async function parse9(serviceDir) {
2729
2854
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2730
- const abs = import_node_path15.default.join(serviceDir, name);
2855
+ const abs = import_node_path16.default.join(serviceDir, name);
2731
2856
  if (!await exists(abs)) continue;
2732
2857
  const raw = await readYaml(abs);
2733
2858
  if (!raw?.services) return [];
@@ -2905,11 +3030,12 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2905
3030
  target: dbNode.id,
2906
3031
  type: import_types7.EdgeType.CONNECTS_TO,
2907
3032
  provenance: import_types7.Provenance.EXTRACTED,
2908
- ...config.sourceFile ? {
2909
- evidence: {
2910
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
2911
- }
2912
- } : {}
3033
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3034
+ // Ghost-edge cleanup keys retirement on this; the conditional
3035
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3036
+ evidence: {
3037
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3038
+ }
2913
3039
  };
2914
3040
  if (!graph.hasEdge(edge.id)) {
2915
3041
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2935,15 +3061,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2935
3061
 
2936
3062
  // src/extract/configs.ts
2937
3063
  init_cjs_shims();
2938
- var import_node_fs11 = require("fs");
2939
- var import_node_path17 = __toESM(require("path"), 1);
3064
+ var import_node_fs12 = require("fs");
3065
+ var import_node_path18 = __toESM(require("path"), 1);
2940
3066
  var import_types8 = require("@neat.is/types");
2941
3067
  async function walkConfigFiles(dir) {
2942
3068
  const out = [];
2943
3069
  async function walk(current) {
2944
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3070
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
2945
3071
  for (const entry2 of entries) {
2946
- const full = import_node_path17.default.join(current, entry2.name);
3072
+ const full = import_node_path18.default.join(current, entry2.name);
2947
3073
  if (entry2.isDirectory()) {
2948
3074
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
2949
3075
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -2960,13 +3086,13 @@ async function addConfigNodes(graph, services, scanPath) {
2960
3086
  for (const service of services) {
2961
3087
  const configFiles = await walkConfigFiles(service.dir);
2962
3088
  for (const file of configFiles) {
2963
- const relPath = import_node_path17.default.relative(scanPath, file);
3089
+ const relPath = import_node_path18.default.relative(scanPath, file);
2964
3090
  const node = {
2965
3091
  id: (0, import_types8.configId)(relPath),
2966
3092
  type: import_types8.NodeType.ConfigNode,
2967
- name: import_node_path17.default.basename(file),
3093
+ name: import_node_path18.default.basename(file),
2968
3094
  path: relPath,
2969
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3095
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
2970
3096
  };
2971
3097
  if (!graph.hasNode(node.id)) {
2972
3098
  graph.addNode(node.id, node);
@@ -2978,7 +3104,7 @@ async function addConfigNodes(graph, services, scanPath) {
2978
3104
  target: node.id,
2979
3105
  type: import_types8.EdgeType.CONFIGURED_BY,
2980
3106
  provenance: import_types8.Provenance.EXTRACTED,
2981
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3107
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
2982
3108
  };
2983
3109
  if (!graph.hasEdge(edge.id)) {
2984
3110
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2995,7 +3121,7 @@ var import_types14 = require("@neat.is/types");
2995
3121
 
2996
3122
  // src/extract/calls/http.ts
2997
3123
  init_cjs_shims();
2998
- var import_node_path19 = __toESM(require("path"), 1);
3124
+ var import_node_path20 = __toESM(require("path"), 1);
2999
3125
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3000
3126
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3001
3127
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3003,17 +3129,17 @@ var import_types9 = require("@neat.is/types");
3003
3129
 
3004
3130
  // src/extract/calls/shared.ts
3005
3131
  init_cjs_shims();
3006
- var import_node_fs12 = require("fs");
3007
- var import_node_path18 = __toESM(require("path"), 1);
3132
+ var import_node_fs13 = require("fs");
3133
+ var import_node_path19 = __toESM(require("path"), 1);
3008
3134
  async function walkSourceFiles(dir) {
3009
3135
  const out = [];
3010
3136
  async function walk(current) {
3011
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3137
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3012
3138
  for (const entry2 of entries) {
3013
- const full = import_node_path18.default.join(current, entry2.name);
3139
+ const full = import_node_path19.default.join(current, entry2.name);
3014
3140
  if (entry2.isDirectory()) {
3015
3141
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3016
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
3142
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3017
3143
  out.push(full);
3018
3144
  }
3019
3145
  }
@@ -3026,7 +3152,7 @@ async function loadSourceFiles(dir) {
3026
3152
  const out = [];
3027
3153
  for (const p of paths) {
3028
3154
  try {
3029
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3155
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3030
3156
  out.push({ path: p, content });
3031
3157
  } catch {
3032
3158
  }
@@ -3045,8 +3171,27 @@ function snippet(text, line) {
3045
3171
 
3046
3172
  // src/extract/calls/http.ts
3047
3173
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3174
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3175
+ function isInsideJsxExternalLink(node) {
3176
+ let cursor = node.parent;
3177
+ while (cursor) {
3178
+ if (cursor.type === "jsx_attribute") {
3179
+ let owner = cursor.parent;
3180
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3181
+ owner = owner.parent;
3182
+ }
3183
+ if (!owner) return false;
3184
+ const tagNode = owner.namedChild(0);
3185
+ const tagName = tagNode?.text ?? "";
3186
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3187
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3188
+ }
3189
+ cursor = cursor.parent;
3190
+ }
3191
+ return false;
3192
+ }
3048
3193
  function collectStringLiterals(node, out) {
3049
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3194
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3050
3195
  for (let i = 0; i < node.namedChildCount; i++) {
3051
3196
  const child = node.namedChild(i);
3052
3197
  if (child) collectStringLiterals(child, out);
@@ -3058,8 +3203,9 @@ function callsFromSource(source, parser, knownHosts) {
3058
3203
  collectStringLiterals(tree.rootNode, literals);
3059
3204
  const targets = /* @__PURE__ */ new Set();
3060
3205
  for (const lit of literals) {
3206
+ if (isInsideJsxExternalLink(lit.node)) continue;
3061
3207
  for (const host of knownHosts) {
3062
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3208
+ if (urlMatchesHost(lit.text, host)) {
3063
3209
  targets.add(host);
3064
3210
  }
3065
3211
  }
@@ -3082,9 +3228,9 @@ async function addHttpCallEdges(graph, services) {
3082
3228
  const knownHosts = /* @__PURE__ */ new Set();
3083
3229
  const hostToNodeId = /* @__PURE__ */ new Map();
3084
3230
  for (const service of services) {
3085
- knownHosts.add(import_node_path19.default.basename(service.dir));
3231
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3086
3232
  knownHosts.add(service.pkg.name);
3087
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3233
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3088
3234
  hostToNodeId.set(service.pkg.name, service.node.id);
3089
3235
  }
3090
3236
  let edgesAdded = 0;
@@ -3092,14 +3238,13 @@ async function addHttpCallEdges(graph, services) {
3092
3238
  const files = await loadSourceFiles(service.dir);
3093
3239
  const seenTargets = /* @__PURE__ */ new Map();
3094
3240
  for (const file of files) {
3095
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3241
+ if (isTestPath(file.path)) continue;
3242
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3096
3243
  let targets;
3097
3244
  try {
3098
3245
  targets = callsFromSource(file.content, parser, knownHosts);
3099
3246
  } catch (err) {
3100
- console.warn(
3101
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3102
- );
3247
+ recordExtractionError("http call extraction", file.path, err);
3103
3248
  continue;
3104
3249
  }
3105
3250
  for (const t of targets) {
@@ -3120,7 +3265,7 @@ async function addHttpCallEdges(graph, services) {
3120
3265
  type: import_types9.EdgeType.CALLS,
3121
3266
  provenance: import_types9.Provenance.EXTRACTED,
3122
3267
  evidence: {
3123
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3268
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3124
3269
  line,
3125
3270
  snippet: snippet(fileContent, line)
3126
3271
  }
@@ -3136,7 +3281,7 @@ async function addHttpCallEdges(graph, services) {
3136
3281
 
3137
3282
  // src/extract/calls/kafka.ts
3138
3283
  init_cjs_shims();
3139
- var import_node_path20 = __toESM(require("path"), 1);
3284
+ var import_node_path21 = __toESM(require("path"), 1);
3140
3285
  var import_types10 = require("@neat.is/types");
3141
3286
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3142
3287
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3163,7 +3308,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3163
3308
  kind: "kafka-topic",
3164
3309
  edgeType,
3165
3310
  evidence: {
3166
- file: import_node_path20.default.relative(serviceDir, file.path),
3311
+ file: import_node_path21.default.relative(serviceDir, file.path),
3167
3312
  line,
3168
3313
  snippet: snippet(file.content, line)
3169
3314
  }
@@ -3176,7 +3321,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3176
3321
 
3177
3322
  // src/extract/calls/redis.ts
3178
3323
  init_cjs_shims();
3179
- var import_node_path21 = __toESM(require("path"), 1);
3324
+ var import_node_path22 = __toESM(require("path"), 1);
3180
3325
  var import_types11 = require("@neat.is/types");
3181
3326
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3182
3327
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3195,7 +3340,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3195
3340
  kind: "redis",
3196
3341
  edgeType: "CALLS",
3197
3342
  evidence: {
3198
- file: import_node_path21.default.relative(serviceDir, file.path),
3343
+ file: import_node_path22.default.relative(serviceDir, file.path),
3199
3344
  line,
3200
3345
  snippet: snippet(file.content, line)
3201
3346
  }
@@ -3206,7 +3351,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3206
3351
 
3207
3352
  // src/extract/calls/aws.ts
3208
3353
  init_cjs_shims();
3209
- var import_node_path22 = __toESM(require("path"), 1);
3354
+ var import_node_path23 = __toESM(require("path"), 1);
3210
3355
  var import_types12 = require("@neat.is/types");
3211
3356
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3212
3357
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3236,7 +3381,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3236
3381
  kind,
3237
3382
  edgeType: "CALLS",
3238
3383
  evidence: {
3239
- file: import_node_path22.default.relative(serviceDir, file.path),
3384
+ file: import_node_path23.default.relative(serviceDir, file.path),
3240
3385
  line,
3241
3386
  snippet: snippet(file.content, line)
3242
3387
  }
@@ -3261,16 +3406,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3261
3406
 
3262
3407
  // src/extract/calls/grpc.ts
3263
3408
  init_cjs_shims();
3264
- var import_node_path23 = __toESM(require("path"), 1);
3409
+ var import_node_path24 = __toESM(require("path"), 1);
3265
3410
  var import_types13 = require("@neat.is/types");
3266
3411
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3412
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3413
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3267
3414
  function isLikelyAddress(value) {
3268
3415
  if (!value) return false;
3269
3416
  return /:\d{2,5}$/.test(value) || value.includes(".");
3270
3417
  }
3418
+ function normaliseForMatch(s) {
3419
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3420
+ }
3421
+ function readImports(content) {
3422
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3423
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3424
+ let m;
3425
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3426
+ const raw = m[1];
3427
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3428
+ }
3429
+ return {
3430
+ awsSdkSuffixes,
3431
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3432
+ };
3433
+ }
3434
+ function classifyClient(symbol, ctx) {
3435
+ const key = normaliseForMatch(symbol);
3436
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3437
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3438
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3439
+ return null;
3440
+ }
3271
3441
  function grpcEndpointsFromFile(file, serviceDir) {
3272
3442
  const out = [];
3273
3443
  const seen = /* @__PURE__ */ new Set();
3444
+ const ctx = readImports(file.content);
3274
3445
  GRPC_CLIENT_RE.lastIndex = 0;
3275
3446
  let m;
3276
3447
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3278,15 +3449,18 @@ function grpcEndpointsFromFile(file, serviceDir) {
3278
3449
  const addr = m[2]?.trim();
3279
3450
  const name = isLikelyAddress(addr) ? addr : symbol;
3280
3451
  if (seen.has(name)) continue;
3452
+ const classified = classifyClient(symbol, ctx);
3453
+ if (!classified) continue;
3281
3454
  seen.add(name);
3455
+ const { kind } = classified;
3282
3456
  const line = lineOf(file.content, m[0]);
3283
3457
  out.push({
3284
- infraId: (0, import_types13.infraId)("grpc-service", name),
3458
+ infraId: (0, import_types13.infraId)(kind, name),
3285
3459
  name,
3286
- kind: "grpc-service",
3460
+ kind,
3287
3461
  edgeType: "CALLS",
3288
3462
  evidence: {
3289
- file: import_node_path23.default.relative(serviceDir, file.path),
3463
+ file: import_node_path24.default.relative(serviceDir, file.path),
3290
3464
  line,
3291
3465
  snippet: snippet(file.content, line)
3292
3466
  }
@@ -3306,6 +3480,9 @@ function edgeTypeFromEndpoint(ep) {
3306
3480
  return import_types14.EdgeType.CALLS;
3307
3481
  }
3308
3482
  }
3483
+ function isAwsKind(kind) {
3484
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3485
+ }
3309
3486
  async function addExternalEndpointEdges(graph, services) {
3310
3487
  let nodesAdded = 0;
3311
3488
  let edgesAdded = 0;
@@ -3313,10 +3490,13 @@ async function addExternalEndpointEdges(graph, services) {
3313
3490
  const files = await loadSourceFiles(service.dir);
3314
3491
  const endpoints = [];
3315
3492
  for (const file of files) {
3316
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3317
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3318
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3319
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3493
+ if (isTestPath(file.path)) continue;
3494
+ const masked = maskCommentsInSource(file.content);
3495
+ const maskedFile = { path: file.path, content: masked };
3496
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3497
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3498
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3499
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3320
3500
  }
3321
3501
  if (endpoints.length === 0) continue;
3322
3502
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3326,7 +3506,10 @@ async function addExternalEndpointEdges(graph, services) {
3326
3506
  id: ep.infraId,
3327
3507
  type: import_types14.NodeType.InfraNode,
3328
3508
  name: ep.name,
3329
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3509
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3510
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3511
+ // bucket / table kinds from aws.ts.
3512
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3330
3513
  kind: ep.kind
3331
3514
  };
3332
3515
  graph.addNode(node.id, node);
@@ -3366,7 +3549,7 @@ init_cjs_shims();
3366
3549
 
3367
3550
  // src/extract/infra/docker-compose.ts
3368
3551
  init_cjs_shims();
3369
- var import_node_path24 = __toESM(require("path"), 1);
3552
+ var import_node_path25 = __toESM(require("path"), 1);
3370
3553
  var import_types16 = require("@neat.is/types");
3371
3554
 
3372
3555
  // src/extract/infra/shared.ts
@@ -3404,7 +3587,7 @@ function dependsOnList(value) {
3404
3587
  }
3405
3588
  function serviceNameToServiceNode(name, services) {
3406
3589
  for (const s of services) {
3407
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3590
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3408
3591
  }
3409
3592
  return null;
3410
3593
  }
@@ -3413,7 +3596,7 @@ async function addComposeInfra(graph, scanPath, services) {
3413
3596
  let edgesAdded = 0;
3414
3597
  let composePath = null;
3415
3598
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3416
- const abs = import_node_path24.default.join(scanPath, name);
3599
+ const abs = import_node_path25.default.join(scanPath, name);
3417
3600
  if (await exists(abs)) {
3418
3601
  composePath = abs;
3419
3602
  break;
@@ -3424,13 +3607,15 @@ async function addComposeInfra(graph, scanPath, services) {
3424
3607
  try {
3425
3608
  compose = await readYaml(composePath);
3426
3609
  } catch (err) {
3427
- console.warn(
3428
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3610
+ recordExtractionError(
3611
+ "infra docker-compose",
3612
+ import_node_path25.default.relative(scanPath, composePath),
3613
+ err
3429
3614
  );
3430
3615
  return { nodesAdded, edgesAdded };
3431
3616
  }
3432
3617
  if (!compose?.services) return { nodesAdded, edgesAdded };
3433
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3618
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3434
3619
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3435
3620
  for (const [composeName, svc] of Object.entries(compose.services)) {
3436
3621
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3471,8 +3656,8 @@ async function addComposeInfra(graph, scanPath, services) {
3471
3656
 
3472
3657
  // src/extract/infra/dockerfile.ts
3473
3658
  init_cjs_shims();
3474
- var import_node_path25 = __toESM(require("path"), 1);
3475
- var import_node_fs13 = require("fs");
3659
+ var import_node_path26 = __toESM(require("path"), 1);
3660
+ var import_node_fs14 = require("fs");
3476
3661
  var import_types17 = require("@neat.is/types");
3477
3662
  function runtimeImage(content) {
3478
3663
  const lines = content.split("\n");
@@ -3492,14 +3677,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3492
3677
  let nodesAdded = 0;
3493
3678
  let edgesAdded = 0;
3494
3679
  for (const service of services) {
3495
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3680
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3496
3681
  if (!await exists(dockerfilePath)) continue;
3497
3682
  let content;
3498
3683
  try {
3499
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3684
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3500
3685
  } catch (err) {
3501
- console.warn(
3502
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3686
+ recordExtractionError(
3687
+ "infra dockerfile",
3688
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3689
+ err
3503
3690
  );
3504
3691
  continue;
3505
3692
  }
@@ -3519,7 +3706,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3519
3706
  type: import_types17.EdgeType.RUNS_ON,
3520
3707
  provenance: import_types17.Provenance.EXTRACTED,
3521
3708
  evidence: {
3522
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3709
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3523
3710
  }
3524
3711
  };
3525
3712
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3531,19 +3718,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3531
3718
 
3532
3719
  // src/extract/infra/terraform.ts
3533
3720
  init_cjs_shims();
3534
- var import_node_fs14 = require("fs");
3535
- var import_node_path26 = __toESM(require("path"), 1);
3721
+ var import_node_fs15 = require("fs");
3722
+ var import_node_path27 = __toESM(require("path"), 1);
3536
3723
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3537
3724
  async function walkTfFiles(start, depth = 0, max = 5) {
3538
3725
  if (depth > max) return [];
3539
3726
  const out = [];
3540
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3727
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3541
3728
  for (const entry2 of entries) {
3542
3729
  if (entry2.isDirectory()) {
3543
3730
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
3544
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
3731
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3545
3732
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
3546
- out.push(import_node_path26.default.join(start, entry2.name));
3733
+ out.push(import_node_path27.default.join(start, entry2.name));
3547
3734
  }
3548
3735
  }
3549
3736
  return out;
@@ -3552,7 +3739,7 @@ async function addTerraformResources(graph, scanPath) {
3552
3739
  let nodesAdded = 0;
3553
3740
  const files = await walkTfFiles(scanPath);
3554
3741
  for (const file of files) {
3555
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3742
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3556
3743
  RESOURCE_RE.lastIndex = 0;
3557
3744
  let m;
3558
3745
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3570,8 +3757,8 @@ async function addTerraformResources(graph, scanPath) {
3570
3757
 
3571
3758
  // src/extract/infra/k8s.ts
3572
3759
  init_cjs_shims();
3573
- var import_node_fs15 = require("fs");
3574
- var import_node_path27 = __toESM(require("path"), 1);
3760
+ var import_node_fs16 = require("fs");
3761
+ var import_node_path28 = __toESM(require("path"), 1);
3575
3762
  var import_yaml3 = require("yaml");
3576
3763
  var K8S_KIND_TO_INFRA_KIND = {
3577
3764
  Service: "k8s-service",
@@ -3585,13 +3772,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3585
3772
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3586
3773
  if (depth > max) return [];
3587
3774
  const out = [];
3588
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3775
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3589
3776
  for (const entry2 of entries) {
3590
3777
  if (entry2.isDirectory()) {
3591
3778
  if (IGNORED_DIRS.has(entry2.name)) continue;
3592
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3593
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
3594
- out.push(import_node_path27.default.join(start, entry2.name));
3779
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
3780
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
3781
+ out.push(import_node_path28.default.join(start, entry2.name));
3595
3782
  }
3596
3783
  }
3597
3784
  return out;
@@ -3600,7 +3787,7 @@ async function addK8sResources(graph, scanPath) {
3600
3787
  let nodesAdded = 0;
3601
3788
  const files = await walkYamlFiles2(scanPath);
3602
3789
  for (const file of files) {
3603
- const content = await import_node_fs15.promises.readFile(file, "utf8");
3790
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3604
3791
  let docs;
3605
3792
  try {
3606
3793
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3634,9 +3821,34 @@ async function addInfra(graph, scanPath, services) {
3634
3821
  };
3635
3822
  }
3636
3823
 
3824
+ // src/extract/retire.ts
3825
+ init_cjs_shims();
3826
+ var import_node_fs17 = require("fs");
3827
+ var import_node_path29 = __toESM(require("path"), 1);
3828
+ var import_types18 = require("@neat.is/types");
3829
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
3830
+ const toDrop = [];
3831
+ const bases = [scanPath, ...serviceDirs];
3832
+ graph.forEachEdge((id, attrs) => {
3833
+ const edge = attrs;
3834
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
3835
+ const evidenceFile = edge.evidence?.file;
3836
+ if (!evidenceFile) return;
3837
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
3838
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
3839
+ return;
3840
+ }
3841
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
3842
+ if (!found) toDrop.push(id);
3843
+ });
3844
+ for (const id of toDrop) graph.dropEdge(id);
3845
+ return toDrop.length;
3846
+ }
3847
+
3637
3848
  // src/extract/index.ts
3638
3849
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3639
3850
  await ensureCompatLoaded();
3851
+ drainExtractionErrors();
3640
3852
  const services = await discoverServices(scanPath);
3641
3853
  const phase1Nodes = addServiceNodes(graph, services);
3642
3854
  await addServiceAliases(graph, scanPath, services);
@@ -3644,12 +3856,30 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3644
3856
  const phase3 = await addConfigNodes(graph, services, scanPath);
3645
3857
  const phase4 = await addCallEdges(graph, services);
3646
3858
  const phase5 = await addInfra(graph, scanPath, services);
3859
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
3860
+ graph,
3861
+ scanPath,
3862
+ services.map((s) => s.dir)
3863
+ );
3647
3864
  const frontiersPromoted = promoteFrontierNodes(graph);
3648
3865
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3866
+ const errorEntries = drainExtractionErrors();
3867
+ if (opts.errorsPath && errorEntries.length > 0) {
3868
+ try {
3869
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
3870
+ } catch (err) {
3871
+ console.warn(
3872
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
3873
+ );
3874
+ }
3875
+ }
3649
3876
  const result = {
3650
3877
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3651
3878
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3652
- frontiersPromoted
3879
+ frontiersPromoted,
3880
+ extractionErrors: errorEntries.length,
3881
+ errorEntries,
3882
+ ghostsRetired
3653
3883
  };
3654
3884
  emitNeatEvent({
3655
3885
  type: "extraction-complete",
@@ -3666,8 +3896,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3666
3896
 
3667
3897
  // src/persist.ts
3668
3898
  init_cjs_shims();
3669
- var import_node_fs16 = require("fs");
3670
- var import_node_path28 = __toESM(require("path"), 1);
3899
+ var import_node_fs18 = require("fs");
3900
+ var import_node_path30 = __toESM(require("path"), 1);
3671
3901
  var SCHEMA_VERSION = 2;
3672
3902
  function migrateV1ToV2(payload) {
3673
3903
  const nodes = payload.graph.nodes;
@@ -3681,7 +3911,7 @@ function migrateV1ToV2(payload) {
3681
3911
  return { ...payload, schemaVersion: 2 };
3682
3912
  }
3683
3913
  async function ensureDir(filePath) {
3684
- await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
3914
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
3685
3915
  }
3686
3916
  async function saveGraphToDisk(graph, outPath) {
3687
3917
  await ensureDir(outPath);
@@ -3691,13 +3921,13 @@ async function saveGraphToDisk(graph, outPath) {
3691
3921
  graph: graph.export()
3692
3922
  };
3693
3923
  const tmp = `${outPath}.tmp`;
3694
- await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3695
- await import_node_fs16.promises.rename(tmp, outPath);
3924
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3925
+ await import_node_fs18.promises.rename(tmp, outPath);
3696
3926
  }
3697
3927
  async function loadGraphFromDisk(graph, outPath) {
3698
3928
  let raw;
3699
3929
  try {
3700
- raw = await import_node_fs16.promises.readFile(outPath, "utf8");
3930
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
3701
3931
  } catch (err) {
3702
3932
  if (err.code === "ENOENT") return;
3703
3933
  throw err;
@@ -3750,23 +3980,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3750
3980
 
3751
3981
  // src/projects.ts
3752
3982
  init_cjs_shims();
3753
- var import_node_path29 = __toESM(require("path"), 1);
3983
+ var import_node_path31 = __toESM(require("path"), 1);
3754
3984
  function pathsForProject(project, baseDir) {
3755
3985
  if (project === DEFAULT_PROJECT) {
3756
3986
  return {
3757
- snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
3758
- errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
3759
- staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
3760
- embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
3761
- policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
3987
+ snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
3988
+ errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
3989
+ staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
3990
+ embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
3991
+ policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
3762
3992
  };
3763
3993
  }
3764
3994
  return {
3765
- snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
3766
- errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
3767
- staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
3768
- embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
3769
- policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
3995
+ snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
3996
+ errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
3997
+ staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
3998
+ embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
3999
+ policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
3770
4000
  };
3771
4001
  }
3772
4002
  var Projects = class {
@@ -3804,11 +4034,11 @@ var Projects = class {
3804
4034
  init_cjs_shims();
3805
4035
  var import_fastify = __toESM(require("fastify"), 1);
3806
4036
  var import_cors = __toESM(require("@fastify/cors"), 1);
3807
- var import_types20 = require("@neat.is/types");
4037
+ var import_types21 = require("@neat.is/types");
3808
4038
 
3809
4039
  // src/divergences.ts
3810
4040
  init_cjs_shims();
3811
- var import_types18 = require("@neat.is/types");
4041
+ var import_types19 = require("@neat.is/types");
3812
4042
  function bucketKey(source, target, type) {
3813
4043
  return `${type}|${source}|${target}`;
3814
4044
  }
@@ -3816,25 +4046,25 @@ function bucketEdges(graph) {
3816
4046
  const buckets = /* @__PURE__ */ new Map();
3817
4047
  graph.forEachEdge((id, attrs) => {
3818
4048
  const e = attrs;
3819
- const parsed = (0, import_types18.parseEdgeId)(id);
4049
+ const parsed = (0, import_types19.parseEdgeId)(id);
3820
4050
  const provenance = parsed?.provenance ?? e.provenance;
3821
4051
  const key = bucketKey(e.source, e.target, e.type);
3822
4052
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3823
4053
  switch (provenance) {
3824
- case import_types18.Provenance.EXTRACTED:
4054
+ case import_types19.Provenance.EXTRACTED:
3825
4055
  cur.extracted = e;
3826
4056
  break;
3827
- case import_types18.Provenance.OBSERVED:
4057
+ case import_types19.Provenance.OBSERVED:
3828
4058
  cur.observed = e;
3829
4059
  break;
3830
- case import_types18.Provenance.INFERRED:
4060
+ case import_types19.Provenance.INFERRED:
3831
4061
  cur.inferred = e;
3832
4062
  break;
3833
- case import_types18.Provenance.FRONTIER:
4063
+ case import_types19.Provenance.FRONTIER:
3834
4064
  cur.frontier = e;
3835
4065
  break;
3836
4066
  default:
3837
- if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
4067
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
3838
4068
  }
3839
4069
  buckets.set(key, cur);
3840
4070
  });
@@ -3843,13 +4073,13 @@ function bucketEdges(graph) {
3843
4073
  function nodeIsFrontier(graph, nodeId) {
3844
4074
  if (!graph.hasNode(nodeId)) return false;
3845
4075
  const attrs = graph.getNodeAttributes(nodeId);
3846
- return attrs.type === import_types18.NodeType.FrontierNode;
4076
+ return attrs.type === import_types19.NodeType.FrontierNode;
3847
4077
  }
3848
4078
  function hasAnyObservedFromSource(graph, sourceId) {
3849
4079
  if (!graph.hasNode(sourceId)) return false;
3850
4080
  for (const edgeId of graph.outboundEdges(sourceId)) {
3851
4081
  const e = graph.getEdgeAttributes(edgeId);
3852
- if (e.provenance === import_types18.Provenance.OBSERVED) return true;
4082
+ if (e.provenance === import_types19.Provenance.OBSERVED) return true;
3853
4083
  }
3854
4084
  return false;
3855
4085
  }
@@ -3910,7 +4140,7 @@ function declaredHostFor(svc) {
3910
4140
  function hasExtractedConfiguredBy(graph, svcId) {
3911
4141
  for (const edgeId of graph.outboundEdges(svcId)) {
3912
4142
  const e = graph.getEdgeAttributes(edgeId);
3913
- if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
4143
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
3914
4144
  return true;
3915
4145
  }
3916
4146
  }
@@ -3923,10 +4153,10 @@ function detectHostMismatch(graph, svcId, svc) {
3923
4153
  const out = [];
3924
4154
  for (const edgeId of graph.outboundEdges(svcId)) {
3925
4155
  const edge = graph.getEdgeAttributes(edgeId);
3926
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3927
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4156
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4157
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
3928
4158
  const target = graph.getNodeAttributes(edge.target);
3929
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4159
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
3930
4160
  const observedHost = target.host?.trim();
3931
4161
  if (!observedHost) continue;
3932
4162
  if (observedHost === declaredHost) continue;
@@ -3948,10 +4178,10 @@ function detectCompatDivergences(graph, svcId, svc) {
3948
4178
  const deps = svc.dependencies ?? {};
3949
4179
  for (const edgeId of graph.outboundEdges(svcId)) {
3950
4180
  const edge = graph.getEdgeAttributes(edgeId);
3951
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3952
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4181
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4182
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
3953
4183
  const target = graph.getNodeAttributes(edge.target);
3954
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4184
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
3955
4185
  for (const pair of compatPairs()) {
3956
4186
  if (pair.engine !== target.engine) continue;
3957
4187
  const declared = deps[pair.driver];
@@ -4012,7 +4242,7 @@ function computeDivergences(graph, opts = {}) {
4012
4242
  }
4013
4243
  graph.forEachNode((nodeId, attrs) => {
4014
4244
  const n = attrs;
4015
- if (n.type !== import_types18.NodeType.ServiceNode) return;
4245
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4016
4246
  const svc = n;
4017
4247
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4018
4248
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4036,7 +4266,7 @@ function computeDivergences(graph, opts = {}) {
4036
4266
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4037
4267
  return a.target.localeCompare(b.target);
4038
4268
  });
4039
- return import_types18.DivergenceResultSchema.parse({
4269
+ return import_types19.DivergenceResultSchema.parse({
4040
4270
  divergences: filtered,
4041
4271
  totalAffected: filtered.length,
4042
4272
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4045,7 +4275,7 @@ function computeDivergences(graph, opts = {}) {
4045
4275
 
4046
4276
  // src/diff.ts
4047
4277
  init_cjs_shims();
4048
- var import_node_fs17 = require("fs");
4278
+ var import_node_fs19 = require("fs");
4049
4279
  async function loadSnapshotForDiff(target) {
4050
4280
  if (/^https?:\/\//i.test(target)) {
4051
4281
  const res = await fetch(target);
@@ -4054,7 +4284,7 @@ async function loadSnapshotForDiff(target) {
4054
4284
  }
4055
4285
  return await res.json();
4056
4286
  }
4057
- const raw = await import_node_fs17.promises.readFile(target, "utf8");
4287
+ const raw = await import_node_fs19.promises.readFile(target, "utf8");
4058
4288
  return JSON.parse(raw);
4059
4289
  }
4060
4290
  function indexEntries(entries) {
@@ -4122,41 +4352,41 @@ function canonicalJson(value) {
4122
4352
 
4123
4353
  // src/registry.ts
4124
4354
  init_cjs_shims();
4125
- var import_node_fs18 = require("fs");
4355
+ var import_node_fs20 = require("fs");
4126
4356
  var import_node_os2 = __toESM(require("os"), 1);
4127
- var import_node_path30 = __toESM(require("path"), 1);
4128
- var import_types19 = require("@neat.is/types");
4357
+ var import_node_path32 = __toESM(require("path"), 1);
4358
+ var import_types20 = require("@neat.is/types");
4129
4359
  var LOCK_TIMEOUT_MS = 5e3;
4130
4360
  var LOCK_RETRY_MS = 50;
4131
4361
  function neatHome() {
4132
4362
  const override = process.env.NEAT_HOME;
4133
- if (override && override.length > 0) return import_node_path30.default.resolve(override);
4134
- return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4363
+ if (override && override.length > 0) return import_node_path32.default.resolve(override);
4364
+ return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
4135
4365
  }
4136
4366
  function registryPath() {
4137
- return import_node_path30.default.join(neatHome(), "projects.json");
4367
+ return import_node_path32.default.join(neatHome(), "projects.json");
4138
4368
  }
4139
4369
  function registryLockPath() {
4140
- return import_node_path30.default.join(neatHome(), "projects.json.lock");
4370
+ return import_node_path32.default.join(neatHome(), "projects.json.lock");
4141
4371
  }
4142
4372
  async function writeAtomically(target, contents) {
4143
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4373
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(target), { recursive: true });
4144
4374
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4145
- const fd = await import_node_fs18.promises.open(tmp, "w");
4375
+ const fd = await import_node_fs20.promises.open(tmp, "w");
4146
4376
  try {
4147
4377
  await fd.writeFile(contents, "utf8");
4148
4378
  await fd.sync();
4149
4379
  } finally {
4150
4380
  await fd.close();
4151
4381
  }
4152
- await import_node_fs18.promises.rename(tmp, target);
4382
+ await import_node_fs20.promises.rename(tmp, target);
4153
4383
  }
4154
4384
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4155
4385
  const deadline = Date.now() + timeoutMs;
4156
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4386
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(lockPath), { recursive: true });
4157
4387
  while (true) {
4158
4388
  try {
4159
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4389
+ const fd = await import_node_fs20.promises.open(lockPath, "wx");
4160
4390
  await fd.close();
4161
4391
  return;
4162
4392
  } catch (err) {
@@ -4172,7 +4402,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4172
4402
  }
4173
4403
  }
4174
4404
  async function releaseLock(lockPath) {
4175
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4405
+ await import_node_fs20.promises.unlink(lockPath).catch(() => {
4176
4406
  });
4177
4407
  }
4178
4408
  async function withLock(fn) {
@@ -4188,7 +4418,7 @@ async function readRegistry() {
4188
4418
  const file = registryPath();
4189
4419
  let raw;
4190
4420
  try {
4191
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4421
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4192
4422
  } catch (err) {
4193
4423
  if (err.code === "ENOENT") {
4194
4424
  return { version: 1, projects: [] };
@@ -4196,10 +4426,10 @@ async function readRegistry() {
4196
4426
  throw err;
4197
4427
  }
4198
4428
  const parsed = JSON.parse(raw);
4199
- return import_types19.RegistryFileSchema.parse(parsed);
4429
+ return import_types20.RegistryFileSchema.parse(parsed);
4200
4430
  }
4201
4431
  async function writeRegistry(reg) {
4202
- const validated = import_types19.RegistryFileSchema.parse(reg);
4432
+ const validated = import_types20.RegistryFileSchema.parse(reg);
4203
4433
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4204
4434
  }
4205
4435
  async function getProject(name) {
@@ -4409,11 +4639,11 @@ function registerRoutes(scope, ctx) {
4409
4639
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4410
4640
  const parsed = [];
4411
4641
  for (const c of candidates) {
4412
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4642
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4413
4643
  if (!r.success) {
4414
4644
  return reply.code(400).send({
4415
4645
  error: `unknown divergence type "${c}"`,
4416
- allowed: import_types20.DivergenceTypeSchema.options
4646
+ allowed: import_types21.DivergenceTypeSchema.options
4417
4647
  });
4418
4648
  }
4419
4649
  parsed.push(r.data);
@@ -4597,7 +4827,7 @@ function registerRoutes(scope, ctx) {
4597
4827
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4598
4828
  let violations = await log.readAll();
4599
4829
  if (req2.query.severity) {
4600
- const sev = import_types20.PolicySeveritySchema.safeParse(req2.query.severity);
4830
+ const sev = import_types21.PolicySeveritySchema.safeParse(req2.query.severity);
4601
4831
  if (!sev.success) {
4602
4832
  return reply.code(400).send({
4603
4833
  error: "invalid severity",
@@ -4614,7 +4844,7 @@ function registerRoutes(scope, ctx) {
4614
4844
  scope.post("/policies/check", async (req2, reply) => {
4615
4845
  const proj = resolveProject(registry, req2, reply);
4616
4846
  if (!proj) return;
4617
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4847
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4618
4848
  if (!parsed.success) {
4619
4849
  return reply.code(400).send({
4620
4850
  error: "invalid /policies/check body",
@@ -4715,11 +4945,11 @@ async function buildApi(opts) {
4715
4945
  // src/daemon.ts
4716
4946
  init_otel();
4717
4947
  function neatHomeFor(opts) {
4718
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
4948
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path35.default.resolve(opts.neatHome);
4719
4949
  const env = process.env.NEAT_HOME;
4720
- if (env && env.length > 0) return import_node_path33.default.resolve(env);
4950
+ if (env && env.length > 0) return import_node_path35.default.resolve(env);
4721
4951
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4722
- return import_node_path33.default.join(home, ".neat");
4952
+ return import_node_path35.default.join(home, ".neat");
4723
4953
  }
4724
4954
  function routeSpanToProject(serviceName, projects) {
4725
4955
  if (!serviceName) return DEFAULT_PROJECT;
@@ -4732,9 +4962,9 @@ function routeSpanToProject(serviceName, projects) {
4732
4962
  return DEFAULT_PROJECT;
4733
4963
  }
4734
4964
  async function bootstrapProject(entry2) {
4735
- const paths = pathsForProject(entry2.name, import_node_path33.default.join(entry2.path, "neat-out"));
4965
+ const paths = pathsForProject(entry2.name, import_node_path35.default.join(entry2.path, "neat-out"));
4736
4966
  try {
4737
- const stat = await import_node_fs19.promises.stat(entry2.path);
4967
+ const stat = await import_node_fs21.promises.stat(entry2.path);
4738
4968
  if (!stat.isDirectory()) {
4739
4969
  throw new Error(`registered path ${entry2.path} is not a directory`);
4740
4970
  }
@@ -4799,13 +5029,13 @@ async function startDaemon(opts = {}) {
4799
5029
  const home = neatHomeFor(opts);
4800
5030
  const regPath = registryPath();
4801
5031
  try {
4802
- await import_node_fs19.promises.access(regPath);
5032
+ await import_node_fs21.promises.access(regPath);
4803
5033
  } catch {
4804
5034
  throw new Error(
4805
5035
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
4806
5036
  );
4807
5037
  }
4808
- const pidPath = import_node_path33.default.join(home, "neatd.pid");
5038
+ const pidPath = import_node_path35.default.join(home, "neatd.pid");
4809
5039
  await writeAtomically(pidPath, `${process.pid}
4810
5040
  `);
4811
5041
  const slots = /* @__PURE__ */ new Map();
@@ -4874,7 +5104,7 @@ async function startDaemon(opts = {}) {
4874
5104
  }
4875
5105
  if (restApp) await restApp.close().catch(() => {
4876
5106
  });
4877
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5107
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
4878
5108
  });
4879
5109
  throw new Error(
4880
5110
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -4919,7 +5149,7 @@ async function startDaemon(opts = {}) {
4919
5149
  });
4920
5150
  if (otlpApp) await otlpApp.close().catch(() => {
4921
5151
  });
4922
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5152
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
4923
5153
  });
4924
5154
  throw new Error(
4925
5155
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -4959,7 +5189,7 @@ async function startDaemon(opts = {}) {
4959
5189
  } catch {
4960
5190
  }
4961
5191
  }
4962
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5192
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
4963
5193
  });
4964
5194
  };
4965
5195
  return { slots, reload, stop, pidPath, restAddress, otlpAddress };
@@ -4969,7 +5199,7 @@ async function startDaemon(opts = {}) {
4969
5199
  init_cjs_shims();
4970
5200
  var import_node_child_process = require("child_process");
4971
5201
  var import_node_net = __toESM(require("net"), 1);
4972
- var import_node_path34 = __toESM(require("path"), 1);
5202
+ var import_node_path36 = __toESM(require("path"), 1);
4973
5203
  var DEFAULT_WEB_PORT = 6328;
4974
5204
  async function assertPortFree(port) {
4975
5205
  await new Promise((resolve, reject) => {
@@ -4997,7 +5227,10 @@ function resolveWebPackageDir() {
4997
5227
  eval("require")
4998
5228
  );
4999
5229
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
5000
- return import_node_path34.default.dirname(pkgJsonPath);
5230
+ return import_node_path36.default.dirname(pkgJsonPath);
5231
+ }
5232
+ function resolveStandaloneServerEntry(webDir) {
5233
+ return import_node_path36.default.join(webDir, ".next/standalone/packages/web/server.js");
5001
5234
  }
5002
5235
  async function spawnWebUI(restPort) {
5003
5236
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -5007,13 +5240,22 @@ async function spawnWebUI(restPort) {
5007
5240
  }
5008
5241
  await assertPortFree(port);
5009
5242
  const cwd = resolveWebPackageDir();
5243
+ const serverEntry = resolveStandaloneServerEntry(cwd);
5244
+ try {
5245
+ require.resolve(serverEntry);
5246
+ } catch {
5247
+ throw new Error(
5248
+ `neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
5249
+ );
5250
+ }
5010
5251
  const env = {
5011
5252
  ...process.env,
5012
5253
  PORT: String(port),
5254
+ HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
5013
5255
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
5014
5256
  };
5015
- const child = (0, import_node_child_process.spawn)("npm", ["exec", "--", "next", "start", "-p", String(port)], {
5016
- cwd,
5257
+ const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
5258
+ cwd: import_node_path36.default.dirname(serverEntry),
5017
5259
  env,
5018
5260
  stdio: ["ignore", "inherit", "inherit"],
5019
5261
  detached: false
@@ -5050,14 +5292,14 @@ async function spawnWebUI(restPort) {
5050
5292
  // src/neatd.ts
5051
5293
  function neatHome2() {
5052
5294
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
5053
- return import_node_path35.default.resolve(process.env.NEAT_HOME);
5295
+ return import_node_path37.default.resolve(process.env.NEAT_HOME);
5054
5296
  }
5055
5297
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5056
- return import_node_path35.default.join(home, ".neat");
5298
+ return import_node_path37.default.join(home, ".neat");
5057
5299
  }
5058
5300
  async function readPid() {
5059
5301
  try {
5060
- const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(neatHome2(), "neatd.pid"), "utf8");
5302
+ const raw = await import_node_fs22.promises.readFile(import_node_path37.default.join(neatHome2(), "neatd.pid"), "utf8");
5061
5303
  const n = Number.parseInt(raw.trim(), 10);
5062
5304
  return Number.isFinite(n) ? n : null;
5063
5305
  } catch {