@neat.is/core 0.3.2 → 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/index.cjs CHANGED
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
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 }
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
362
362
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
363
363
  );
364
364
  }
365
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
365
+ var import_node_path34, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
366
366
  var init_otel = __esm({
367
367
  "src/otel.ts"() {
368
368
  "use strict";
369
369
  init_cjs_shims();
370
- import_node_path32 = __toESM(require("path"), 1);
370
+ import_node_path34 = __toESM(require("path"), 1);
371
371
  import_node_url2 = require("url");
372
372
  import_fastify2 = __toESM(require("fastify"), 1);
373
373
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -876,19 +876,19 @@ function confidenceFromMix(edges, now = Date.now()) {
876
876
  function longestIncomingWalk(graph, start, maxDepth) {
877
877
  let best = { path: [start], edges: [] };
878
878
  const visited = /* @__PURE__ */ new Set([start]);
879
- function step(node, path34, edges) {
880
- if (path34.length > best.path.length) {
881
- best = { path: [...path34], edges: [...edges] };
879
+ function step(node, path36, edges) {
880
+ if (path36.length > best.path.length) {
881
+ best = { path: [...path36], edges: [...edges] };
882
882
  }
883
- if (path34.length - 1 >= maxDepth) return;
883
+ if (path36.length - 1 >= maxDepth) return;
884
884
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
885
885
  for (const [srcId, edge] of incoming) {
886
886
  if (visited.has(srcId)) continue;
887
887
  visited.add(srcId);
888
- path34.push(srcId);
888
+ path36.push(srcId);
889
889
  edges.push(edge);
890
- step(srcId, path34, edges);
891
- path34.pop();
890
+ step(srcId, path36, edges);
891
+ path36.pop();
892
892
  edges.pop();
893
893
  visited.delete(srcId);
894
894
  }
@@ -1925,8 +1925,8 @@ async function readErrorEvents(errorsPath) {
1925
1925
 
1926
1926
  // src/extract/services.ts
1927
1927
  init_cjs_shims();
1928
- var import_node_fs7 = require("fs");
1929
- var import_node_path7 = __toESM(require("path"), 1);
1928
+ var import_node_fs8 = require("fs");
1929
+ var import_node_path8 = __toESM(require("path"), 1);
1930
1930
  var import_ignore = __toESM(require("ignore"), 1);
1931
1931
  var import_minimatch2 = require("minimatch");
1932
1932
  var import_types5 = require("@neat.is/types");
@@ -1950,9 +1950,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1950
1950
  function isConfigFile(name) {
1951
1951
  const ext = import_node_path4.default.extname(name);
1952
1952
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
1953
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
1953
+ if (name === ".env" || name.startsWith(".env.")) {
1954
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1955
+ return { match: true, fileType: "env" };
1956
+ }
1954
1957
  return { match: false, fileType: "" };
1955
1958
  }
1959
+ function isTestPath(filePath) {
1960
+ const normalised = filePath.replace(/\\/g, "/");
1961
+ const segments = normalised.split("/");
1962
+ for (const seg of segments) {
1963
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
1964
+ return true;
1965
+ }
1966
+ }
1967
+ const base = segments[segments.length - 1] ?? "";
1968
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
1969
+ }
1970
+ function isEnvTemplateFile(name) {
1971
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
1972
+ return true;
1973
+ }
1974
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
1975
+ }
1976
+ function maskCommentsInSource(src) {
1977
+ const len = src.length;
1978
+ const out = new Array(len);
1979
+ let i = 0;
1980
+ let inString = 0;
1981
+ let escaped = false;
1982
+ while (i < len) {
1983
+ const c = src[i];
1984
+ if (inString !== 0) {
1985
+ out[i] = c;
1986
+ if (escaped) {
1987
+ escaped = false;
1988
+ } else if (c === "\\") {
1989
+ escaped = true;
1990
+ } else if (c === inString) {
1991
+ inString = 0;
1992
+ }
1993
+ i++;
1994
+ continue;
1995
+ }
1996
+ if (c === "/" && i + 1 < len) {
1997
+ const next = src[i + 1];
1998
+ if (next === "/") {
1999
+ out[i] = " ";
2000
+ out[i + 1] = " ";
2001
+ let j = i + 2;
2002
+ while (j < len && src[j] !== "\n") {
2003
+ out[j] = " ";
2004
+ j++;
2005
+ }
2006
+ i = j;
2007
+ continue;
2008
+ }
2009
+ if (next === "*") {
2010
+ out[i] = " ";
2011
+ out[i + 1] = " ";
2012
+ let j = i + 2;
2013
+ while (j < len) {
2014
+ if (src[j] === "\n") {
2015
+ out[j] = "\n";
2016
+ j++;
2017
+ continue;
2018
+ }
2019
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
2020
+ out[j] = " ";
2021
+ out[j + 1] = " ";
2022
+ j += 2;
2023
+ break;
2024
+ }
2025
+ out[j] = " ";
2026
+ j++;
2027
+ }
2028
+ i = j;
2029
+ continue;
2030
+ }
2031
+ }
2032
+ out[i] = c;
2033
+ if (c === "'" || c === '"' || c === "`") inString = c;
2034
+ i++;
2035
+ }
2036
+ return out.join("");
2037
+ }
2038
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
2039
+ function urlMatchesHost(urlString, host) {
2040
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
2041
+ if (!URL_LIKE.test(urlString)) return false;
2042
+ const [wantedHost, wantedPort] = host.split(":");
2043
+ let parsed;
2044
+ try {
2045
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
2046
+ parsed = new URL(candidate);
2047
+ } catch {
2048
+ return false;
2049
+ }
2050
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
2051
+ if (wantedPort && parsed.port !== wantedPort) return false;
2052
+ return true;
2053
+ }
1956
2054
  function cleanVersion(raw) {
1957
2055
  if (!raw) return void 0;
1958
2056
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -2108,6 +2206,33 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2108
2206
  return author ?? void 0;
2109
2207
  }
2110
2208
 
2209
+ // src/extract/errors.ts
2210
+ init_cjs_shims();
2211
+ var import_node_fs7 = require("fs");
2212
+ var import_node_path7 = __toESM(require("path"), 1);
2213
+ var sink = [];
2214
+ function recordExtractionError(producer, file, err) {
2215
+ const e = err instanceof Error ? err : new Error(String(err));
2216
+ sink.push({
2217
+ producer,
2218
+ file,
2219
+ error: e.message,
2220
+ stack: e.stack,
2221
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2222
+ source: "extract"
2223
+ });
2224
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2225
+ }
2226
+ function drainExtractionErrors() {
2227
+ return sink.splice(0, sink.length);
2228
+ }
2229
+ async function writeExtractionErrors(errors, errorsPath) {
2230
+ if (errors.length === 0) return;
2231
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2232
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2233
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2234
+ }
2235
+
2111
2236
  // src/extract/services.ts
2112
2237
  var DEFAULT_SCAN_DEPTH = 5;
2113
2238
  function parseScanDepth() {
@@ -2124,21 +2249,21 @@ function workspaceGlobs(pkg) {
2124
2249
  return null;
2125
2250
  }
2126
2251
  async function loadGitignore(scanPath) {
2127
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2252
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2128
2253
  if (!await exists(gitignorePath)) return null;
2129
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2254
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
2130
2255
  return (0, import_ignore.default)().add(raw);
2131
2256
  }
2132
2257
  async function walkDirs(start, scanPath, options, visit) {
2133
2258
  async function recurse(current, depth) {
2134
2259
  if (depth > options.maxDepth) return;
2135
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2260
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2136
2261
  for (const entry of entries) {
2137
2262
  if (!entry.isDirectory()) continue;
2138
2263
  if (IGNORED_DIRS.has(entry.name)) continue;
2139
- const child = import_node_path7.default.join(current, entry.name);
2264
+ const child = import_node_path8.default.join(current, entry.name);
2140
2265
  if (options.ig) {
2141
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2266
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2142
2267
  if (rel && options.ig.ignores(rel + "/")) continue;
2143
2268
  }
2144
2269
  await visit(child);
@@ -2153,8 +2278,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2153
2278
  for (const raw of globs) {
2154
2279
  const pattern = raw.replace(/^\.\//, "");
2155
2280
  if (!pattern.includes("*")) {
2156
- const candidate = import_node_path7.default.join(scanPath, pattern);
2157
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2281
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2282
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2158
2283
  continue;
2159
2284
  }
2160
2285
  const segments = pattern.split("/");
@@ -2163,13 +2288,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2163
2288
  if (seg.includes("*")) break;
2164
2289
  staticSegments.push(seg);
2165
2290
  }
2166
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2291
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2167
2292
  if (!await exists(start)) continue;
2168
2293
  const hasDoubleStar = pattern.includes("**");
2169
2294
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2170
2295
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2171
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2172
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2296
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2297
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2173
2298
  found.add(dir);
2174
2299
  }
2175
2300
  });
@@ -2177,15 +2302,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2177
2302
  return [...found];
2178
2303
  }
2179
2304
  async function discoverNodeService(scanPath, dir) {
2180
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2305
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2181
2306
  if (!await exists(pkgPath)) return null;
2182
2307
  let pkg;
2183
2308
  try {
2184
2309
  pkg = await readJson(pkgPath);
2185
2310
  } catch (err) {
2186
- console.warn(
2187
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2188
- );
2311
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2189
2312
  return null;
2190
2313
  }
2191
2314
  if (!pkg.name) return null;
@@ -2196,7 +2319,7 @@ async function discoverNodeService(scanPath, dir) {
2196
2319
  language: "javascript",
2197
2320
  version: pkg.version,
2198
2321
  dependencies: pkg.dependencies ?? {},
2199
- repoPath: import_node_path7.default.relative(scanPath, dir),
2322
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2200
2323
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2201
2324
  };
2202
2325
  return { pkg, dir, node };
@@ -2212,19 +2335,21 @@ async function discoverPyService(scanPath, dir) {
2212
2335
  language: "python",
2213
2336
  version: py.version,
2214
2337
  dependencies: py.dependencies,
2215
- repoPath: import_node_path7.default.relative(scanPath, dir)
2338
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2216
2339
  };
2217
2340
  return { pkg, dir, node };
2218
2341
  }
2219
2342
  async function discoverServices(scanPath) {
2220
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2343
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2221
2344
  let rootPkg = null;
2222
2345
  if (await exists(rootPkgPath)) {
2223
2346
  try {
2224
2347
  rootPkg = await readJson(rootPkgPath);
2225
2348
  } catch (err) {
2226
- console.warn(
2227
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2349
+ recordExtractionError(
2350
+ "services workspaces",
2351
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2352
+ err
2228
2353
  );
2229
2354
  }
2230
2355
  }
@@ -2240,9 +2365,9 @@ async function discoverServices(scanPath) {
2240
2365
  scanPath,
2241
2366
  { maxDepth: parseScanDepth(), ig },
2242
2367
  async (dir) => {
2243
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2368
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2244
2369
  candidateDirs.push(dir);
2245
- } 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"))) {
2370
+ } 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"))) {
2246
2371
  candidateDirs.push(dir);
2247
2372
  }
2248
2373
  }
@@ -2256,8 +2381,8 @@ async function discoverServices(scanPath) {
2256
2381
  if (!service) continue;
2257
2382
  const existingDir = seen.get(service.node.name);
2258
2383
  if (existingDir !== void 0) {
2259
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2260
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2384
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2385
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2261
2386
  console.warn(
2262
2387
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2263
2388
  );
@@ -2294,8 +2419,8 @@ function addServiceNodes(graph, services) {
2294
2419
 
2295
2420
  // src/extract/aliases.ts
2296
2421
  init_cjs_shims();
2297
- var import_node_path8 = __toESM(require("path"), 1);
2298
- var import_node_fs8 = require("fs");
2422
+ var import_node_path9 = __toESM(require("path"), 1);
2423
+ var import_node_fs9 = require("fs");
2299
2424
  var import_yaml2 = require("yaml");
2300
2425
  var import_types6 = require("@neat.is/types");
2301
2426
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2322,14 +2447,14 @@ function indexServicesByName(services) {
2322
2447
  const map = /* @__PURE__ */ new Map();
2323
2448
  for (const s of services) {
2324
2449
  map.set(s.node.name, s.node.id);
2325
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2450
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2326
2451
  }
2327
2452
  return map;
2328
2453
  }
2329
2454
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2330
2455
  let composePath = null;
2331
2456
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2332
- const abs = import_node_path8.default.join(scanPath, name);
2457
+ const abs = import_node_path9.default.join(scanPath, name);
2333
2458
  if (await exists(abs)) {
2334
2459
  composePath = abs;
2335
2460
  break;
@@ -2340,8 +2465,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2340
2465
  try {
2341
2466
  compose = await readYaml(composePath);
2342
2467
  } catch (err) {
2343
- console.warn(
2344
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2468
+ recordExtractionError(
2469
+ "aliases compose",
2470
+ import_node_path9.default.relative(scanPath, composePath),
2471
+ err
2345
2472
  );
2346
2473
  return;
2347
2474
  }
@@ -2383,15 +2510,13 @@ function parseDockerfileLabels(content) {
2383
2510
  }
2384
2511
  async function collectDockerfileAliases(graph, services) {
2385
2512
  for (const service of services) {
2386
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2513
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2387
2514
  if (!await exists(dockerfilePath)) continue;
2388
2515
  let content;
2389
2516
  try {
2390
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2517
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2391
2518
  } catch (err) {
2392
- console.warn(
2393
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2394
- );
2519
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2395
2520
  continue;
2396
2521
  }
2397
2522
  const aliases = parseDockerfileLabels(content);
@@ -2401,13 +2526,13 @@ async function collectDockerfileAliases(graph, services) {
2401
2526
  async function walkYamlFiles(start, depth = 0, max = 5) {
2402
2527
  if (depth > max) return [];
2403
2528
  const out = [];
2404
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2529
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2405
2530
  for (const entry of entries) {
2406
2531
  if (entry.isDirectory()) {
2407
2532
  if (IGNORED_DIRS.has(entry.name)) continue;
2408
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
2409
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
2410
- out.push(import_node_path8.default.join(start, entry.name));
2533
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
2534
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
2535
+ out.push(import_node_path9.default.join(start, entry.name));
2411
2536
  }
2412
2537
  }
2413
2538
  return out;
@@ -2434,7 +2559,7 @@ function k8sServiceTarget(doc, byName) {
2434
2559
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2435
2560
  const files = await walkYamlFiles(scanPath);
2436
2561
  for (const file of files) {
2437
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2562
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2438
2563
  let docs;
2439
2564
  try {
2440
2565
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2459,14 +2584,14 @@ async function addServiceAliases(graph, scanPath, services) {
2459
2584
 
2460
2585
  // src/extract/databases/index.ts
2461
2586
  init_cjs_shims();
2462
- var import_node_path16 = __toESM(require("path"), 1);
2587
+ var import_node_path17 = __toESM(require("path"), 1);
2463
2588
  var import_types7 = require("@neat.is/types");
2464
2589
 
2465
2590
  // src/extract/databases/db-config-yaml.ts
2466
2591
  init_cjs_shims();
2467
- var import_node_path9 = __toESM(require("path"), 1);
2592
+ var import_node_path10 = __toESM(require("path"), 1);
2468
2593
  async function parse(serviceDir) {
2469
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2594
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2470
2595
  if (!await exists(yamlPath)) return [];
2471
2596
  const raw = await readYaml(yamlPath);
2472
2597
  return [
@@ -2484,13 +2609,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2484
2609
 
2485
2610
  // src/extract/databases/dotenv.ts
2486
2611
  init_cjs_shims();
2487
- var import_node_fs10 = require("fs");
2488
- var import_node_path11 = __toESM(require("path"), 1);
2612
+ var import_node_fs11 = require("fs");
2613
+ var import_node_path12 = __toESM(require("path"), 1);
2489
2614
 
2490
2615
  // src/extract/databases/shared.ts
2491
2616
  init_cjs_shims();
2492
- var import_node_fs9 = require("fs");
2493
- var import_node_path10 = __toESM(require("path"), 1);
2617
+ var import_node_fs10 = require("fs");
2618
+ var import_node_path11 = __toESM(require("path"), 1);
2494
2619
  function schemeToEngine(scheme) {
2495
2620
  const s = scheme.toLowerCase().split("+")[0];
2496
2621
  switch (s) {
@@ -2529,14 +2654,14 @@ function parseConnectionString(url) {
2529
2654
  }
2530
2655
  async function readIfExists(filePath) {
2531
2656
  try {
2532
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2657
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2533
2658
  } catch {
2534
2659
  return null;
2535
2660
  }
2536
2661
  }
2537
2662
  async function findFirst(serviceDir, candidates) {
2538
2663
  for (const rel of candidates) {
2539
- const abs = import_node_path10.default.join(serviceDir, rel);
2664
+ const abs = import_node_path11.default.join(serviceDir, rel);
2540
2665
  const content = await readIfExists(abs);
2541
2666
  if (content !== null) return abs;
2542
2667
  }
@@ -2587,15 +2712,15 @@ function parseDotenvLine(line) {
2587
2712
  return { key, value };
2588
2713
  }
2589
2714
  async function parse2(serviceDir) {
2590
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2715
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2591
2716
  const configs = [];
2592
2717
  const seen = /* @__PURE__ */ new Set();
2593
2718
  for (const entry of entries) {
2594
2719
  if (!entry.isFile()) continue;
2595
2720
  const match = isConfigFile(entry.name);
2596
2721
  if (!match.match || match.fileType !== "env") continue;
2597
- const filePath = import_node_path11.default.join(serviceDir, entry.name);
2598
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2722
+ const filePath = import_node_path12.default.join(serviceDir, entry.name);
2723
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2599
2724
  for (const line of content.split("\n")) {
2600
2725
  const parsed = parseDotenvLine(line);
2601
2726
  if (!parsed) continue;
@@ -2614,9 +2739,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2614
2739
 
2615
2740
  // src/extract/databases/prisma.ts
2616
2741
  init_cjs_shims();
2617
- var import_node_path12 = __toESM(require("path"), 1);
2742
+ var import_node_path13 = __toESM(require("path"), 1);
2618
2743
  async function parse3(serviceDir) {
2619
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2744
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2620
2745
  const content = await readIfExists(schemaPath);
2621
2746
  if (!content) return [];
2622
2747
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2748,10 +2873,10 @@ var knexParser = { name: "knex", parse: parse5 };
2748
2873
 
2749
2874
  // src/extract/databases/ormconfig.ts
2750
2875
  init_cjs_shims();
2751
- var import_node_path13 = __toESM(require("path"), 1);
2876
+ var import_node_path14 = __toESM(require("path"), 1);
2752
2877
  async function parse6(serviceDir) {
2753
2878
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2754
- const abs = import_node_path13.default.join(serviceDir, candidate);
2879
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2755
2880
  if (!await exists(abs)) continue;
2756
2881
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2757
2882
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2811,9 +2936,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2811
2936
 
2812
2937
  // src/extract/databases/sequelize.ts
2813
2938
  init_cjs_shims();
2814
- var import_node_path14 = __toESM(require("path"), 1);
2939
+ var import_node_path15 = __toESM(require("path"), 1);
2815
2940
  async function parse8(serviceDir) {
2816
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2941
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2817
2942
  if (!await exists(configPath)) return [];
2818
2943
  const raw = await readJson(configPath);
2819
2944
  const out = [];
@@ -2840,7 +2965,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2840
2965
 
2841
2966
  // src/extract/databases/docker-compose.ts
2842
2967
  init_cjs_shims();
2843
- var import_node_path15 = __toESM(require("path"), 1);
2968
+ var import_node_path16 = __toESM(require("path"), 1);
2844
2969
  function portFromService(svc) {
2845
2970
  for (const raw of svc.ports ?? []) {
2846
2971
  const str = String(raw);
@@ -2867,7 +2992,7 @@ function databaseFromEnv(svc) {
2867
2992
  }
2868
2993
  async function parse9(serviceDir) {
2869
2994
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2870
- const abs = import_node_path15.default.join(serviceDir, name);
2995
+ const abs = import_node_path16.default.join(serviceDir, name);
2871
2996
  if (!await exists(abs)) continue;
2872
2997
  const raw = await readYaml(abs);
2873
2998
  if (!raw?.services) return [];
@@ -3045,11 +3170,12 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3045
3170
  target: dbNode.id,
3046
3171
  type: import_types7.EdgeType.CONNECTS_TO,
3047
3172
  provenance: import_types7.Provenance.EXTRACTED,
3048
- ...config.sourceFile ? {
3049
- evidence: {
3050
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
3051
- }
3052
- } : {}
3173
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3174
+ // Ghost-edge cleanup keys retirement on this; the conditional
3175
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3176
+ evidence: {
3177
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3178
+ }
3053
3179
  };
3054
3180
  if (!graph.hasEdge(edge.id)) {
3055
3181
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3075,15 +3201,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3075
3201
 
3076
3202
  // src/extract/configs.ts
3077
3203
  init_cjs_shims();
3078
- var import_node_fs11 = require("fs");
3079
- var import_node_path17 = __toESM(require("path"), 1);
3204
+ var import_node_fs12 = require("fs");
3205
+ var import_node_path18 = __toESM(require("path"), 1);
3080
3206
  var import_types8 = require("@neat.is/types");
3081
3207
  async function walkConfigFiles(dir) {
3082
3208
  const out = [];
3083
3209
  async function walk(current) {
3084
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3210
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3085
3211
  for (const entry of entries) {
3086
- const full = import_node_path17.default.join(current, entry.name);
3212
+ const full = import_node_path18.default.join(current, entry.name);
3087
3213
  if (entry.isDirectory()) {
3088
3214
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3089
3215
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -3100,13 +3226,13 @@ async function addConfigNodes(graph, services, scanPath) {
3100
3226
  for (const service of services) {
3101
3227
  const configFiles = await walkConfigFiles(service.dir);
3102
3228
  for (const file of configFiles) {
3103
- const relPath = import_node_path17.default.relative(scanPath, file);
3229
+ const relPath = import_node_path18.default.relative(scanPath, file);
3104
3230
  const node = {
3105
3231
  id: (0, import_types8.configId)(relPath),
3106
3232
  type: import_types8.NodeType.ConfigNode,
3107
- name: import_node_path17.default.basename(file),
3233
+ name: import_node_path18.default.basename(file),
3108
3234
  path: relPath,
3109
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3235
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3110
3236
  };
3111
3237
  if (!graph.hasNode(node.id)) {
3112
3238
  graph.addNode(node.id, node);
@@ -3118,7 +3244,7 @@ async function addConfigNodes(graph, services, scanPath) {
3118
3244
  target: node.id,
3119
3245
  type: import_types8.EdgeType.CONFIGURED_BY,
3120
3246
  provenance: import_types8.Provenance.EXTRACTED,
3121
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3247
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3122
3248
  };
3123
3249
  if (!graph.hasEdge(edge.id)) {
3124
3250
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3135,7 +3261,7 @@ var import_types14 = require("@neat.is/types");
3135
3261
 
3136
3262
  // src/extract/calls/http.ts
3137
3263
  init_cjs_shims();
3138
- var import_node_path19 = __toESM(require("path"), 1);
3264
+ var import_node_path20 = __toESM(require("path"), 1);
3139
3265
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3140
3266
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3141
3267
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3143,17 +3269,17 @@ var import_types9 = require("@neat.is/types");
3143
3269
 
3144
3270
  // src/extract/calls/shared.ts
3145
3271
  init_cjs_shims();
3146
- var import_node_fs12 = require("fs");
3147
- var import_node_path18 = __toESM(require("path"), 1);
3272
+ var import_node_fs13 = require("fs");
3273
+ var import_node_path19 = __toESM(require("path"), 1);
3148
3274
  async function walkSourceFiles(dir) {
3149
3275
  const out = [];
3150
3276
  async function walk(current) {
3151
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3277
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3152
3278
  for (const entry of entries) {
3153
- const full = import_node_path18.default.join(current, entry.name);
3279
+ const full = import_node_path19.default.join(current, entry.name);
3154
3280
  if (entry.isDirectory()) {
3155
3281
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3156
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
3282
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3157
3283
  out.push(full);
3158
3284
  }
3159
3285
  }
@@ -3166,7 +3292,7 @@ async function loadSourceFiles(dir) {
3166
3292
  const out = [];
3167
3293
  for (const p of paths) {
3168
3294
  try {
3169
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3295
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3170
3296
  out.push({ path: p, content });
3171
3297
  } catch {
3172
3298
  }
@@ -3185,8 +3311,27 @@ function snippet(text, line) {
3185
3311
 
3186
3312
  // src/extract/calls/http.ts
3187
3313
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3314
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3315
+ function isInsideJsxExternalLink(node) {
3316
+ let cursor = node.parent;
3317
+ while (cursor) {
3318
+ if (cursor.type === "jsx_attribute") {
3319
+ let owner = cursor.parent;
3320
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3321
+ owner = owner.parent;
3322
+ }
3323
+ if (!owner) return false;
3324
+ const tagNode = owner.namedChild(0);
3325
+ const tagName = tagNode?.text ?? "";
3326
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3327
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3328
+ }
3329
+ cursor = cursor.parent;
3330
+ }
3331
+ return false;
3332
+ }
3188
3333
  function collectStringLiterals(node, out) {
3189
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3334
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3190
3335
  for (let i = 0; i < node.namedChildCount; i++) {
3191
3336
  const child = node.namedChild(i);
3192
3337
  if (child) collectStringLiterals(child, out);
@@ -3198,8 +3343,9 @@ function callsFromSource(source, parser, knownHosts) {
3198
3343
  collectStringLiterals(tree.rootNode, literals);
3199
3344
  const targets = /* @__PURE__ */ new Set();
3200
3345
  for (const lit of literals) {
3346
+ if (isInsideJsxExternalLink(lit.node)) continue;
3201
3347
  for (const host of knownHosts) {
3202
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3348
+ if (urlMatchesHost(lit.text, host)) {
3203
3349
  targets.add(host);
3204
3350
  }
3205
3351
  }
@@ -3222,9 +3368,9 @@ async function addHttpCallEdges(graph, services) {
3222
3368
  const knownHosts = /* @__PURE__ */ new Set();
3223
3369
  const hostToNodeId = /* @__PURE__ */ new Map();
3224
3370
  for (const service of services) {
3225
- knownHosts.add(import_node_path19.default.basename(service.dir));
3371
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3226
3372
  knownHosts.add(service.pkg.name);
3227
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3373
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3228
3374
  hostToNodeId.set(service.pkg.name, service.node.id);
3229
3375
  }
3230
3376
  let edgesAdded = 0;
@@ -3232,14 +3378,13 @@ async function addHttpCallEdges(graph, services) {
3232
3378
  const files = await loadSourceFiles(service.dir);
3233
3379
  const seenTargets = /* @__PURE__ */ new Map();
3234
3380
  for (const file of files) {
3235
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3381
+ if (isTestPath(file.path)) continue;
3382
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3236
3383
  let targets;
3237
3384
  try {
3238
3385
  targets = callsFromSource(file.content, parser, knownHosts);
3239
3386
  } catch (err) {
3240
- console.warn(
3241
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3242
- );
3387
+ recordExtractionError("http call extraction", file.path, err);
3243
3388
  continue;
3244
3389
  }
3245
3390
  for (const t of targets) {
@@ -3260,7 +3405,7 @@ async function addHttpCallEdges(graph, services) {
3260
3405
  type: import_types9.EdgeType.CALLS,
3261
3406
  provenance: import_types9.Provenance.EXTRACTED,
3262
3407
  evidence: {
3263
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3408
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3264
3409
  line,
3265
3410
  snippet: snippet(fileContent, line)
3266
3411
  }
@@ -3276,7 +3421,7 @@ async function addHttpCallEdges(graph, services) {
3276
3421
 
3277
3422
  // src/extract/calls/kafka.ts
3278
3423
  init_cjs_shims();
3279
- var import_node_path20 = __toESM(require("path"), 1);
3424
+ var import_node_path21 = __toESM(require("path"), 1);
3280
3425
  var import_types10 = require("@neat.is/types");
3281
3426
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3282
3427
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3303,7 +3448,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3303
3448
  kind: "kafka-topic",
3304
3449
  edgeType,
3305
3450
  evidence: {
3306
- file: import_node_path20.default.relative(serviceDir, file.path),
3451
+ file: import_node_path21.default.relative(serviceDir, file.path),
3307
3452
  line,
3308
3453
  snippet: snippet(file.content, line)
3309
3454
  }
@@ -3316,7 +3461,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3316
3461
 
3317
3462
  // src/extract/calls/redis.ts
3318
3463
  init_cjs_shims();
3319
- var import_node_path21 = __toESM(require("path"), 1);
3464
+ var import_node_path22 = __toESM(require("path"), 1);
3320
3465
  var import_types11 = require("@neat.is/types");
3321
3466
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3322
3467
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3335,7 +3480,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3335
3480
  kind: "redis",
3336
3481
  edgeType: "CALLS",
3337
3482
  evidence: {
3338
- file: import_node_path21.default.relative(serviceDir, file.path),
3483
+ file: import_node_path22.default.relative(serviceDir, file.path),
3339
3484
  line,
3340
3485
  snippet: snippet(file.content, line)
3341
3486
  }
@@ -3346,7 +3491,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3346
3491
 
3347
3492
  // src/extract/calls/aws.ts
3348
3493
  init_cjs_shims();
3349
- var import_node_path22 = __toESM(require("path"), 1);
3494
+ var import_node_path23 = __toESM(require("path"), 1);
3350
3495
  var import_types12 = require("@neat.is/types");
3351
3496
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3352
3497
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3376,7 +3521,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3376
3521
  kind,
3377
3522
  edgeType: "CALLS",
3378
3523
  evidence: {
3379
- file: import_node_path22.default.relative(serviceDir, file.path),
3524
+ file: import_node_path23.default.relative(serviceDir, file.path),
3380
3525
  line,
3381
3526
  snippet: snippet(file.content, line)
3382
3527
  }
@@ -3401,16 +3546,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3401
3546
 
3402
3547
  // src/extract/calls/grpc.ts
3403
3548
  init_cjs_shims();
3404
- var import_node_path23 = __toESM(require("path"), 1);
3549
+ var import_node_path24 = __toESM(require("path"), 1);
3405
3550
  var import_types13 = require("@neat.is/types");
3406
3551
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3552
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3553
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3407
3554
  function isLikelyAddress(value) {
3408
3555
  if (!value) return false;
3409
3556
  return /:\d{2,5}$/.test(value) || value.includes(".");
3410
3557
  }
3558
+ function normaliseForMatch(s) {
3559
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3560
+ }
3561
+ function readImports(content) {
3562
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3563
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3564
+ let m;
3565
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3566
+ const raw = m[1];
3567
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3568
+ }
3569
+ return {
3570
+ awsSdkSuffixes,
3571
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3572
+ };
3573
+ }
3574
+ function classifyClient(symbol, ctx) {
3575
+ const key = normaliseForMatch(symbol);
3576
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3577
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3578
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3579
+ return null;
3580
+ }
3411
3581
  function grpcEndpointsFromFile(file, serviceDir) {
3412
3582
  const out = [];
3413
3583
  const seen = /* @__PURE__ */ new Set();
3584
+ const ctx = readImports(file.content);
3414
3585
  GRPC_CLIENT_RE.lastIndex = 0;
3415
3586
  let m;
3416
3587
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3418,15 +3589,18 @@ function grpcEndpointsFromFile(file, serviceDir) {
3418
3589
  const addr = m[2]?.trim();
3419
3590
  const name = isLikelyAddress(addr) ? addr : symbol;
3420
3591
  if (seen.has(name)) continue;
3592
+ const classified = classifyClient(symbol, ctx);
3593
+ if (!classified) continue;
3421
3594
  seen.add(name);
3595
+ const { kind } = classified;
3422
3596
  const line = lineOf(file.content, m[0]);
3423
3597
  out.push({
3424
- infraId: (0, import_types13.infraId)("grpc-service", name),
3598
+ infraId: (0, import_types13.infraId)(kind, name),
3425
3599
  name,
3426
- kind: "grpc-service",
3600
+ kind,
3427
3601
  edgeType: "CALLS",
3428
3602
  evidence: {
3429
- file: import_node_path23.default.relative(serviceDir, file.path),
3603
+ file: import_node_path24.default.relative(serviceDir, file.path),
3430
3604
  line,
3431
3605
  snippet: snippet(file.content, line)
3432
3606
  }
@@ -3446,6 +3620,9 @@ function edgeTypeFromEndpoint(ep) {
3446
3620
  return import_types14.EdgeType.CALLS;
3447
3621
  }
3448
3622
  }
3623
+ function isAwsKind(kind) {
3624
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3625
+ }
3449
3626
  async function addExternalEndpointEdges(graph, services) {
3450
3627
  let nodesAdded = 0;
3451
3628
  let edgesAdded = 0;
@@ -3453,10 +3630,13 @@ async function addExternalEndpointEdges(graph, services) {
3453
3630
  const files = await loadSourceFiles(service.dir);
3454
3631
  const endpoints = [];
3455
3632
  for (const file of files) {
3456
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3457
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3458
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3459
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3633
+ if (isTestPath(file.path)) continue;
3634
+ const masked = maskCommentsInSource(file.content);
3635
+ const maskedFile = { path: file.path, content: masked };
3636
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3637
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3638
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3639
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3460
3640
  }
3461
3641
  if (endpoints.length === 0) continue;
3462
3642
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3466,7 +3646,10 @@ async function addExternalEndpointEdges(graph, services) {
3466
3646
  id: ep.infraId,
3467
3647
  type: import_types14.NodeType.InfraNode,
3468
3648
  name: ep.name,
3469
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3649
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3650
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3651
+ // bucket / table kinds from aws.ts.
3652
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3470
3653
  kind: ep.kind
3471
3654
  };
3472
3655
  graph.addNode(node.id, node);
@@ -3506,7 +3689,7 @@ init_cjs_shims();
3506
3689
 
3507
3690
  // src/extract/infra/docker-compose.ts
3508
3691
  init_cjs_shims();
3509
- var import_node_path24 = __toESM(require("path"), 1);
3692
+ var import_node_path25 = __toESM(require("path"), 1);
3510
3693
  var import_types16 = require("@neat.is/types");
3511
3694
 
3512
3695
  // src/extract/infra/shared.ts
@@ -3544,7 +3727,7 @@ function dependsOnList(value) {
3544
3727
  }
3545
3728
  function serviceNameToServiceNode(name, services) {
3546
3729
  for (const s of services) {
3547
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3730
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3548
3731
  }
3549
3732
  return null;
3550
3733
  }
@@ -3553,7 +3736,7 @@ async function addComposeInfra(graph, scanPath, services) {
3553
3736
  let edgesAdded = 0;
3554
3737
  let composePath = null;
3555
3738
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3556
- const abs = import_node_path24.default.join(scanPath, name);
3739
+ const abs = import_node_path25.default.join(scanPath, name);
3557
3740
  if (await exists(abs)) {
3558
3741
  composePath = abs;
3559
3742
  break;
@@ -3564,13 +3747,15 @@ async function addComposeInfra(graph, scanPath, services) {
3564
3747
  try {
3565
3748
  compose = await readYaml(composePath);
3566
3749
  } catch (err) {
3567
- console.warn(
3568
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3750
+ recordExtractionError(
3751
+ "infra docker-compose",
3752
+ import_node_path25.default.relative(scanPath, composePath),
3753
+ err
3569
3754
  );
3570
3755
  return { nodesAdded, edgesAdded };
3571
3756
  }
3572
3757
  if (!compose?.services) return { nodesAdded, edgesAdded };
3573
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3758
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3574
3759
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3575
3760
  for (const [composeName, svc] of Object.entries(compose.services)) {
3576
3761
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3611,8 +3796,8 @@ async function addComposeInfra(graph, scanPath, services) {
3611
3796
 
3612
3797
  // src/extract/infra/dockerfile.ts
3613
3798
  init_cjs_shims();
3614
- var import_node_path25 = __toESM(require("path"), 1);
3615
- var import_node_fs13 = require("fs");
3799
+ var import_node_path26 = __toESM(require("path"), 1);
3800
+ var import_node_fs14 = require("fs");
3616
3801
  var import_types17 = require("@neat.is/types");
3617
3802
  function runtimeImage(content) {
3618
3803
  const lines = content.split("\n");
@@ -3632,14 +3817,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3632
3817
  let nodesAdded = 0;
3633
3818
  let edgesAdded = 0;
3634
3819
  for (const service of services) {
3635
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3820
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3636
3821
  if (!await exists(dockerfilePath)) continue;
3637
3822
  let content;
3638
3823
  try {
3639
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3824
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3640
3825
  } catch (err) {
3641
- console.warn(
3642
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3826
+ recordExtractionError(
3827
+ "infra dockerfile",
3828
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3829
+ err
3643
3830
  );
3644
3831
  continue;
3645
3832
  }
@@ -3659,7 +3846,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3659
3846
  type: import_types17.EdgeType.RUNS_ON,
3660
3847
  provenance: import_types17.Provenance.EXTRACTED,
3661
3848
  evidence: {
3662
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3849
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3663
3850
  }
3664
3851
  };
3665
3852
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3671,19 +3858,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3671
3858
 
3672
3859
  // src/extract/infra/terraform.ts
3673
3860
  init_cjs_shims();
3674
- var import_node_fs14 = require("fs");
3675
- var import_node_path26 = __toESM(require("path"), 1);
3861
+ var import_node_fs15 = require("fs");
3862
+ var import_node_path27 = __toESM(require("path"), 1);
3676
3863
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3677
3864
  async function walkTfFiles(start, depth = 0, max = 5) {
3678
3865
  if (depth > max) return [];
3679
3866
  const out = [];
3680
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3867
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3681
3868
  for (const entry of entries) {
3682
3869
  if (entry.isDirectory()) {
3683
3870
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3684
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
3871
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
3685
3872
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3686
- out.push(import_node_path26.default.join(start, entry.name));
3873
+ out.push(import_node_path27.default.join(start, entry.name));
3687
3874
  }
3688
3875
  }
3689
3876
  return out;
@@ -3692,7 +3879,7 @@ async function addTerraformResources(graph, scanPath) {
3692
3879
  let nodesAdded = 0;
3693
3880
  const files = await walkTfFiles(scanPath);
3694
3881
  for (const file of files) {
3695
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3882
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3696
3883
  RESOURCE_RE.lastIndex = 0;
3697
3884
  let m;
3698
3885
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3710,8 +3897,8 @@ async function addTerraformResources(graph, scanPath) {
3710
3897
 
3711
3898
  // src/extract/infra/k8s.ts
3712
3899
  init_cjs_shims();
3713
- var import_node_fs15 = require("fs");
3714
- var import_node_path27 = __toESM(require("path"), 1);
3900
+ var import_node_fs16 = require("fs");
3901
+ var import_node_path28 = __toESM(require("path"), 1);
3715
3902
  var import_yaml3 = require("yaml");
3716
3903
  var K8S_KIND_TO_INFRA_KIND = {
3717
3904
  Service: "k8s-service",
@@ -3725,13 +3912,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3725
3912
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3726
3913
  if (depth > max) return [];
3727
3914
  const out = [];
3728
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3915
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3729
3916
  for (const entry of entries) {
3730
3917
  if (entry.isDirectory()) {
3731
3918
  if (IGNORED_DIRS.has(entry.name)) continue;
3732
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
3733
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
3734
- out.push(import_node_path27.default.join(start, entry.name));
3919
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
3920
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
3921
+ out.push(import_node_path28.default.join(start, entry.name));
3735
3922
  }
3736
3923
  }
3737
3924
  return out;
@@ -3740,7 +3927,7 @@ async function addK8sResources(graph, scanPath) {
3740
3927
  let nodesAdded = 0;
3741
3928
  const files = await walkYamlFiles2(scanPath);
3742
3929
  for (const file of files) {
3743
- const content = await import_node_fs15.promises.readFile(file, "utf8");
3930
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3744
3931
  let docs;
3745
3932
  try {
3746
3933
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3774,9 +3961,34 @@ async function addInfra(graph, scanPath, services) {
3774
3961
  };
3775
3962
  }
3776
3963
 
3964
+ // src/extract/retire.ts
3965
+ init_cjs_shims();
3966
+ var import_node_fs17 = require("fs");
3967
+ var import_node_path29 = __toESM(require("path"), 1);
3968
+ var import_types18 = require("@neat.is/types");
3969
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
3970
+ const toDrop = [];
3971
+ const bases = [scanPath, ...serviceDirs];
3972
+ graph.forEachEdge((id, attrs) => {
3973
+ const edge = attrs;
3974
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
3975
+ const evidenceFile = edge.evidence?.file;
3976
+ if (!evidenceFile) return;
3977
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
3978
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
3979
+ return;
3980
+ }
3981
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
3982
+ if (!found) toDrop.push(id);
3983
+ });
3984
+ for (const id of toDrop) graph.dropEdge(id);
3985
+ return toDrop.length;
3986
+ }
3987
+
3777
3988
  // src/extract/index.ts
3778
3989
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3779
3990
  await ensureCompatLoaded();
3991
+ drainExtractionErrors();
3780
3992
  const services = await discoverServices(scanPath);
3781
3993
  const phase1Nodes = addServiceNodes(graph, services);
3782
3994
  await addServiceAliases(graph, scanPath, services);
@@ -3784,12 +3996,30 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3784
3996
  const phase3 = await addConfigNodes(graph, services, scanPath);
3785
3997
  const phase4 = await addCallEdges(graph, services);
3786
3998
  const phase5 = await addInfra(graph, scanPath, services);
3999
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
4000
+ graph,
4001
+ scanPath,
4002
+ services.map((s) => s.dir)
4003
+ );
3787
4004
  const frontiersPromoted = promoteFrontierNodes(graph);
3788
4005
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
4006
+ const errorEntries = drainExtractionErrors();
4007
+ if (opts.errorsPath && errorEntries.length > 0) {
4008
+ try {
4009
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
4010
+ } catch (err) {
4011
+ console.warn(
4012
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
4013
+ );
4014
+ }
4015
+ }
3789
4016
  const result = {
3790
4017
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3791
4018
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3792
- frontiersPromoted
4019
+ frontiersPromoted,
4020
+ extractionErrors: errorEntries.length,
4021
+ errorEntries,
4022
+ ghostsRetired
3793
4023
  };
3794
4024
  emitNeatEvent({
3795
4025
  type: "extraction-complete",
@@ -3806,8 +4036,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3806
4036
 
3807
4037
  // src/persist.ts
3808
4038
  init_cjs_shims();
3809
- var import_node_fs16 = require("fs");
3810
- var import_node_path28 = __toESM(require("path"), 1);
4039
+ var import_node_fs18 = require("fs");
4040
+ var import_node_path30 = __toESM(require("path"), 1);
3811
4041
  var SCHEMA_VERSION = 2;
3812
4042
  function migrateV1ToV2(payload) {
3813
4043
  const nodes = payload.graph.nodes;
@@ -3821,7 +4051,7 @@ function migrateV1ToV2(payload) {
3821
4051
  return { ...payload, schemaVersion: 2 };
3822
4052
  }
3823
4053
  async function ensureDir(filePath) {
3824
- await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
4054
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
3825
4055
  }
3826
4056
  async function saveGraphToDisk(graph, outPath) {
3827
4057
  await ensureDir(outPath);
@@ -3831,13 +4061,13 @@ async function saveGraphToDisk(graph, outPath) {
3831
4061
  graph: graph.export()
3832
4062
  };
3833
4063
  const tmp = `${outPath}.tmp`;
3834
- await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3835
- await import_node_fs16.promises.rename(tmp, outPath);
4064
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4065
+ await import_node_fs18.promises.rename(tmp, outPath);
3836
4066
  }
3837
4067
  async function loadGraphFromDisk(graph, outPath) {
3838
4068
  let raw;
3839
4069
  try {
3840
- raw = await import_node_fs16.promises.readFile(outPath, "utf8");
4070
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
3841
4071
  } catch (err) {
3842
4072
  if (err.code === "ENOENT") return;
3843
4073
  throw err;
@@ -3892,11 +4122,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3892
4122
  init_cjs_shims();
3893
4123
  var import_fastify = __toESM(require("fastify"), 1);
3894
4124
  var import_cors = __toESM(require("@fastify/cors"), 1);
3895
- var import_types20 = require("@neat.is/types");
4125
+ var import_types21 = require("@neat.is/types");
3896
4126
 
3897
4127
  // src/divergences.ts
3898
4128
  init_cjs_shims();
3899
- var import_types18 = require("@neat.is/types");
4129
+ var import_types19 = require("@neat.is/types");
3900
4130
  function bucketKey(source, target, type) {
3901
4131
  return `${type}|${source}|${target}`;
3902
4132
  }
@@ -3904,25 +4134,25 @@ function bucketEdges(graph) {
3904
4134
  const buckets = /* @__PURE__ */ new Map();
3905
4135
  graph.forEachEdge((id, attrs) => {
3906
4136
  const e = attrs;
3907
- const parsed = (0, import_types18.parseEdgeId)(id);
4137
+ const parsed = (0, import_types19.parseEdgeId)(id);
3908
4138
  const provenance = parsed?.provenance ?? e.provenance;
3909
4139
  const key = bucketKey(e.source, e.target, e.type);
3910
4140
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3911
4141
  switch (provenance) {
3912
- case import_types18.Provenance.EXTRACTED:
4142
+ case import_types19.Provenance.EXTRACTED:
3913
4143
  cur.extracted = e;
3914
4144
  break;
3915
- case import_types18.Provenance.OBSERVED:
4145
+ case import_types19.Provenance.OBSERVED:
3916
4146
  cur.observed = e;
3917
4147
  break;
3918
- case import_types18.Provenance.INFERRED:
4148
+ case import_types19.Provenance.INFERRED:
3919
4149
  cur.inferred = e;
3920
4150
  break;
3921
- case import_types18.Provenance.FRONTIER:
4151
+ case import_types19.Provenance.FRONTIER:
3922
4152
  cur.frontier = e;
3923
4153
  break;
3924
4154
  default:
3925
- if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
4155
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
3926
4156
  }
3927
4157
  buckets.set(key, cur);
3928
4158
  });
@@ -3931,13 +4161,13 @@ function bucketEdges(graph) {
3931
4161
  function nodeIsFrontier(graph, nodeId) {
3932
4162
  if (!graph.hasNode(nodeId)) return false;
3933
4163
  const attrs = graph.getNodeAttributes(nodeId);
3934
- return attrs.type === import_types18.NodeType.FrontierNode;
4164
+ return attrs.type === import_types19.NodeType.FrontierNode;
3935
4165
  }
3936
4166
  function hasAnyObservedFromSource(graph, sourceId) {
3937
4167
  if (!graph.hasNode(sourceId)) return false;
3938
4168
  for (const edgeId of graph.outboundEdges(sourceId)) {
3939
4169
  const e = graph.getEdgeAttributes(edgeId);
3940
- if (e.provenance === import_types18.Provenance.OBSERVED) return true;
4170
+ if (e.provenance === import_types19.Provenance.OBSERVED) return true;
3941
4171
  }
3942
4172
  return false;
3943
4173
  }
@@ -3998,7 +4228,7 @@ function declaredHostFor(svc) {
3998
4228
  function hasExtractedConfiguredBy(graph, svcId) {
3999
4229
  for (const edgeId of graph.outboundEdges(svcId)) {
4000
4230
  const e = graph.getEdgeAttributes(edgeId);
4001
- if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
4231
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4002
4232
  return true;
4003
4233
  }
4004
4234
  }
@@ -4011,10 +4241,10 @@ function detectHostMismatch(graph, svcId, svc) {
4011
4241
  const out = [];
4012
4242
  for (const edgeId of graph.outboundEdges(svcId)) {
4013
4243
  const edge = graph.getEdgeAttributes(edgeId);
4014
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4015
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4244
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4245
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4016
4246
  const target = graph.getNodeAttributes(edge.target);
4017
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4247
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4018
4248
  const observedHost = target.host?.trim();
4019
4249
  if (!observedHost) continue;
4020
4250
  if (observedHost === declaredHost) continue;
@@ -4036,10 +4266,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4036
4266
  const deps = svc.dependencies ?? {};
4037
4267
  for (const edgeId of graph.outboundEdges(svcId)) {
4038
4268
  const edge = graph.getEdgeAttributes(edgeId);
4039
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4040
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4269
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4270
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4041
4271
  const target = graph.getNodeAttributes(edge.target);
4042
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4272
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4043
4273
  for (const pair of compatPairs()) {
4044
4274
  if (pair.engine !== target.engine) continue;
4045
4275
  const declared = deps[pair.driver];
@@ -4100,7 +4330,7 @@ function computeDivergences(graph, opts = {}) {
4100
4330
  }
4101
4331
  graph.forEachNode((nodeId, attrs) => {
4102
4332
  const n = attrs;
4103
- if (n.type !== import_types18.NodeType.ServiceNode) return;
4333
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4104
4334
  const svc = n;
4105
4335
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4106
4336
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4124,7 +4354,7 @@ function computeDivergences(graph, opts = {}) {
4124
4354
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4125
4355
  return a.target.localeCompare(b.target);
4126
4356
  });
4127
- return import_types18.DivergenceResultSchema.parse({
4357
+ return import_types19.DivergenceResultSchema.parse({
4128
4358
  divergences: filtered,
4129
4359
  totalAffected: filtered.length,
4130
4360
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4133,7 +4363,7 @@ function computeDivergences(graph, opts = {}) {
4133
4363
 
4134
4364
  // src/diff.ts
4135
4365
  init_cjs_shims();
4136
- var import_node_fs17 = require("fs");
4366
+ var import_node_fs19 = require("fs");
4137
4367
  async function loadSnapshotForDiff(target) {
4138
4368
  if (/^https?:\/\//i.test(target)) {
4139
4369
  const res = await fetch(target);
@@ -4142,7 +4372,7 @@ async function loadSnapshotForDiff(target) {
4142
4372
  }
4143
4373
  return await res.json();
4144
4374
  }
4145
- const raw = await import_node_fs17.promises.readFile(target, "utf8");
4375
+ const raw = await import_node_fs19.promises.readFile(target, "utf8");
4146
4376
  return JSON.parse(raw);
4147
4377
  }
4148
4378
  function indexEntries(entries) {
@@ -4210,23 +4440,23 @@ function canonicalJson(value) {
4210
4440
 
4211
4441
  // src/projects.ts
4212
4442
  init_cjs_shims();
4213
- var import_node_path29 = __toESM(require("path"), 1);
4443
+ var import_node_path31 = __toESM(require("path"), 1);
4214
4444
  function pathsForProject(project, baseDir) {
4215
4445
  if (project === DEFAULT_PROJECT) {
4216
4446
  return {
4217
- snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
4218
- errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
4219
- staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
4220
- embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
4221
- policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
4447
+ snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
4448
+ errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
4449
+ staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
4450
+ embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
4451
+ policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
4222
4452
  };
4223
4453
  }
4224
4454
  return {
4225
- snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
4226
- errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
4227
- staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
4228
- embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
4229
- policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
4455
+ snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
4456
+ errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
4457
+ staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
4458
+ embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
4459
+ policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
4230
4460
  };
4231
4461
  }
4232
4462
  var Projects = class {
@@ -4262,49 +4492,49 @@ var Projects = class {
4262
4492
 
4263
4493
  // src/registry.ts
4264
4494
  init_cjs_shims();
4265
- var import_node_fs18 = require("fs");
4495
+ var import_node_fs20 = require("fs");
4266
4496
  var import_node_os2 = __toESM(require("os"), 1);
4267
- var import_node_path30 = __toESM(require("path"), 1);
4268
- var import_types19 = require("@neat.is/types");
4497
+ var import_node_path32 = __toESM(require("path"), 1);
4498
+ var import_types20 = require("@neat.is/types");
4269
4499
  var LOCK_TIMEOUT_MS = 5e3;
4270
4500
  var LOCK_RETRY_MS = 50;
4271
4501
  function neatHome() {
4272
4502
  const override = process.env.NEAT_HOME;
4273
- if (override && override.length > 0) return import_node_path30.default.resolve(override);
4274
- return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4503
+ if (override && override.length > 0) return import_node_path32.default.resolve(override);
4504
+ return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
4275
4505
  }
4276
4506
  function registryPath() {
4277
- return import_node_path30.default.join(neatHome(), "projects.json");
4507
+ return import_node_path32.default.join(neatHome(), "projects.json");
4278
4508
  }
4279
4509
  function registryLockPath() {
4280
- return import_node_path30.default.join(neatHome(), "projects.json.lock");
4510
+ return import_node_path32.default.join(neatHome(), "projects.json.lock");
4281
4511
  }
4282
4512
  async function normalizeProjectPath(input) {
4283
- const resolved = import_node_path30.default.resolve(input);
4513
+ const resolved = import_node_path32.default.resolve(input);
4284
4514
  try {
4285
- return await import_node_fs18.promises.realpath(resolved);
4515
+ return await import_node_fs20.promises.realpath(resolved);
4286
4516
  } catch {
4287
4517
  return resolved;
4288
4518
  }
4289
4519
  }
4290
4520
  async function writeAtomically(target, contents) {
4291
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4521
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(target), { recursive: true });
4292
4522
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4293
- const fd = await import_node_fs18.promises.open(tmp, "w");
4523
+ const fd = await import_node_fs20.promises.open(tmp, "w");
4294
4524
  try {
4295
4525
  await fd.writeFile(contents, "utf8");
4296
4526
  await fd.sync();
4297
4527
  } finally {
4298
4528
  await fd.close();
4299
4529
  }
4300
- await import_node_fs18.promises.rename(tmp, target);
4530
+ await import_node_fs20.promises.rename(tmp, target);
4301
4531
  }
4302
4532
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4303
4533
  const deadline = Date.now() + timeoutMs;
4304
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4534
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(lockPath), { recursive: true });
4305
4535
  while (true) {
4306
4536
  try {
4307
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4537
+ const fd = await import_node_fs20.promises.open(lockPath, "wx");
4308
4538
  await fd.close();
4309
4539
  return;
4310
4540
  } catch (err) {
@@ -4320,7 +4550,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4320
4550
  }
4321
4551
  }
4322
4552
  async function releaseLock(lockPath) {
4323
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4553
+ await import_node_fs20.promises.unlink(lockPath).catch(() => {
4324
4554
  });
4325
4555
  }
4326
4556
  async function withLock(fn) {
@@ -4336,7 +4566,7 @@ async function readRegistry() {
4336
4566
  const file = registryPath();
4337
4567
  let raw;
4338
4568
  try {
4339
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4569
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4340
4570
  } catch (err) {
4341
4571
  if (err.code === "ENOENT") {
4342
4572
  return { version: 1, projects: [] };
@@ -4344,10 +4574,10 @@ async function readRegistry() {
4344
4574
  throw err;
4345
4575
  }
4346
4576
  const parsed = JSON.parse(raw);
4347
- return import_types19.RegistryFileSchema.parse(parsed);
4577
+ return import_types20.RegistryFileSchema.parse(parsed);
4348
4578
  }
4349
4579
  async function writeRegistry(reg) {
4350
- const validated = import_types19.RegistryFileSchema.parse(reg);
4580
+ const validated = import_types20.RegistryFileSchema.parse(reg);
4351
4581
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4352
4582
  }
4353
4583
  var ProjectNameCollisionError = class extends Error {
@@ -4607,11 +4837,11 @@ function registerRoutes(scope, ctx) {
4607
4837
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4608
4838
  const parsed = [];
4609
4839
  for (const c of candidates) {
4610
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4840
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4611
4841
  if (!r.success) {
4612
4842
  return reply.code(400).send({
4613
4843
  error: `unknown divergence type "${c}"`,
4614
- allowed: import_types20.DivergenceTypeSchema.options
4844
+ allowed: import_types21.DivergenceTypeSchema.options
4615
4845
  });
4616
4846
  }
4617
4847
  parsed.push(r.data);
@@ -4795,7 +5025,7 @@ function registerRoutes(scope, ctx) {
4795
5025
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4796
5026
  let violations = await log.readAll();
4797
5027
  if (req.query.severity) {
4798
- const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
5028
+ const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
4799
5029
  if (!sev.success) {
4800
5030
  return reply.code(400).send({
4801
5031
  error: "invalid severity",
@@ -4812,7 +5042,7 @@ function registerRoutes(scope, ctx) {
4812
5042
  scope.post("/policies/check", async (req, reply) => {
4813
5043
  const proj = resolveProject(registry, req, reply);
4814
5044
  if (!proj) return;
4815
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5045
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4816
5046
  if (!parsed.success) {
4817
5047
  return reply.code(400).send({
4818
5048
  error: "invalid /policies/check body",
@@ -4916,15 +5146,15 @@ init_otel_grpc();
4916
5146
 
4917
5147
  // src/daemon.ts
4918
5148
  init_cjs_shims();
4919
- var import_node_fs19 = require("fs");
4920
- var import_node_path33 = __toESM(require("path"), 1);
5149
+ var import_node_fs21 = require("fs");
5150
+ var import_node_path35 = __toESM(require("path"), 1);
4921
5151
  init_otel();
4922
5152
  function neatHomeFor(opts) {
4923
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
5153
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path35.default.resolve(opts.neatHome);
4924
5154
  const env = process.env.NEAT_HOME;
4925
- if (env && env.length > 0) return import_node_path33.default.resolve(env);
5155
+ if (env && env.length > 0) return import_node_path35.default.resolve(env);
4926
5156
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4927
- return import_node_path33.default.join(home, ".neat");
5157
+ return import_node_path35.default.join(home, ".neat");
4928
5158
  }
4929
5159
  function routeSpanToProject(serviceName, projects) {
4930
5160
  if (!serviceName) return DEFAULT_PROJECT;
@@ -4937,9 +5167,9 @@ function routeSpanToProject(serviceName, projects) {
4937
5167
  return DEFAULT_PROJECT;
4938
5168
  }
4939
5169
  async function bootstrapProject(entry) {
4940
- const paths = pathsForProject(entry.name, import_node_path33.default.join(entry.path, "neat-out"));
5170
+ const paths = pathsForProject(entry.name, import_node_path35.default.join(entry.path, "neat-out"));
4941
5171
  try {
4942
- const stat = await import_node_fs19.promises.stat(entry.path);
5172
+ const stat = await import_node_fs21.promises.stat(entry.path);
4943
5173
  if (!stat.isDirectory()) {
4944
5174
  throw new Error(`registered path ${entry.path} is not a directory`);
4945
5175
  }
@@ -5004,13 +5234,13 @@ async function startDaemon(opts = {}) {
5004
5234
  const home = neatHomeFor(opts);
5005
5235
  const regPath = registryPath();
5006
5236
  try {
5007
- await import_node_fs19.promises.access(regPath);
5237
+ await import_node_fs21.promises.access(regPath);
5008
5238
  } catch {
5009
5239
  throw new Error(
5010
5240
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5011
5241
  );
5012
5242
  }
5013
- const pidPath = import_node_path33.default.join(home, "neatd.pid");
5243
+ const pidPath = import_node_path35.default.join(home, "neatd.pid");
5014
5244
  await writeAtomically(pidPath, `${process.pid}
5015
5245
  `);
5016
5246
  const slots = /* @__PURE__ */ new Map();
@@ -5079,7 +5309,7 @@ async function startDaemon(opts = {}) {
5079
5309
  }
5080
5310
  if (restApp) await restApp.close().catch(() => {
5081
5311
  });
5082
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5312
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
5083
5313
  });
5084
5314
  throw new Error(
5085
5315
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -5124,7 +5354,7 @@ async function startDaemon(opts = {}) {
5124
5354
  });
5125
5355
  if (otlpApp) await otlpApp.close().catch(() => {
5126
5356
  });
5127
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5357
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
5128
5358
  });
5129
5359
  throw new Error(
5130
5360
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -5164,7 +5394,7 @@ async function startDaemon(opts = {}) {
5164
5394
  } catch {
5165
5395
  }
5166
5396
  }
5167
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5397
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
5168
5398
  });
5169
5399
  };
5170
5400
  return { slots, reload, stop, pidPath, restAddress, otlpAddress };