@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/server.cjs CHANGED
@@ -99,8 +99,8 @@ function reshapeGrpcRequest(req) {
99
99
  };
100
100
  }
101
101
  function resolveProtoRoot() {
102
- const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
- return import_node_path31.default.resolve(here, "..", "proto");
102
+ const here = import_node_path33.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
+ return import_node_path33.default.resolve(here, "..", "proto");
104
104
  }
105
105
  function loadTraceService() {
106
106
  const protoRoot = resolveProtoRoot();
@@ -155,13 +155,13 @@ async function startOtelGrpcReceiver(opts) {
155
155
  })
156
156
  };
157
157
  }
158
- var import_node_url, import_node_path31, grpc, protoLoader;
158
+ var import_node_url, import_node_path33, grpc, protoLoader;
159
159
  var init_otel_grpc = __esm({
160
160
  "src/otel-grpc.ts"() {
161
161
  "use strict";
162
162
  init_cjs_shims();
163
163
  import_node_url = require("url");
164
- import_node_path31 = __toESM(require("path"), 1);
164
+ import_node_path33 = __toESM(require("path"), 1);
165
165
  grpc = __toESM(require("@grpc/grpc-js"), 1);
166
166
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
167
167
  init_otel();
@@ -258,10 +258,10 @@ function parseOtlpRequest(body) {
258
258
  }
259
259
  function loadProtobufDecoder() {
260
260
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
261
- const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
262
- const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
261
+ const here = import_node_path34.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
262
+ const protoRoot = import_node_path34.default.resolve(here, "..", "proto");
263
263
  const root = new import_protobufjs.default.Root();
264
- root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
264
+ root.resolvePath = (_origin, target) => import_node_path34.default.resolve(protoRoot, target);
265
265
  root.loadSync(
266
266
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
267
267
  { keepCase: true }
@@ -353,12 +353,12 @@ async function buildOtelReceiver(opts) {
353
353
  };
354
354
  return decorated;
355
355
  }
356
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
356
+ var import_node_path34, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
357
357
  var init_otel = __esm({
358
358
  "src/otel.ts"() {
359
359
  "use strict";
360
360
  init_cjs_shims();
361
- import_node_path32 = __toESM(require("path"), 1);
361
+ import_node_path34 = __toESM(require("path"), 1);
362
362
  import_node_url2 = require("url");
363
363
  import_fastify2 = __toESM(require("fastify"), 1);
364
364
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -368,7 +368,7 @@ var init_otel = __esm({
368
368
 
369
369
  // src/server.ts
370
370
  init_cjs_shims();
371
- var import_node_path34 = __toESM(require("path"), 1);
371
+ var import_node_path36 = __toESM(require("path"), 1);
372
372
 
373
373
  // src/graph.ts
374
374
  init_cjs_shims();
@@ -392,7 +392,7 @@ function getGraph(project = DEFAULT_PROJECT) {
392
392
  init_cjs_shims();
393
393
  var import_fastify = __toESM(require("fastify"), 1);
394
394
  var import_cors = __toESM(require("@fastify/cors"), 1);
395
- var import_types20 = require("@neat.is/types");
395
+ var import_types21 = require("@neat.is/types");
396
396
 
397
397
  // src/divergences.ts
398
398
  init_cjs_shims();
@@ -798,19 +798,19 @@ function confidenceFromMix(edges, now = Date.now()) {
798
798
  function longestIncomingWalk(graph, start, maxDepth) {
799
799
  let best = { path: [start], edges: [] };
800
800
  const visited = /* @__PURE__ */ new Set([start]);
801
- function step(node, path35, edges) {
802
- if (path35.length > best.path.length) {
803
- best = { path: [...path35], edges: [...edges] };
801
+ function step(node, path37, edges) {
802
+ if (path37.length > best.path.length) {
803
+ best = { path: [...path37], edges: [...edges] };
804
804
  }
805
- if (path35.length - 1 >= maxDepth) return;
805
+ if (path37.length - 1 >= maxDepth) return;
806
806
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
807
807
  for (const [srcId, edge] of incoming) {
808
808
  if (visited.has(srcId)) continue;
809
809
  visited.add(srcId);
810
- path35.push(srcId);
810
+ path37.push(srcId);
811
811
  edges.push(edge);
812
- step(srcId, path35, edges);
813
- path35.pop();
812
+ step(srcId, path37, edges);
813
+ path37.pop();
814
814
  edges.pop();
815
815
  visited.delete(srcId);
816
816
  }
@@ -2086,8 +2086,8 @@ async function readErrorEvents(errorsPath) {
2086
2086
 
2087
2087
  // src/extract/services.ts
2088
2088
  init_cjs_shims();
2089
- var import_node_fs7 = require("fs");
2090
- var import_node_path7 = __toESM(require("path"), 1);
2089
+ var import_node_fs8 = require("fs");
2090
+ var import_node_path8 = __toESM(require("path"), 1);
2091
2091
  var import_ignore = __toESM(require("ignore"), 1);
2092
2092
  var import_minimatch2 = require("minimatch");
2093
2093
  var import_types6 = require("@neat.is/types");
@@ -2111,9 +2111,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2111
2111
  function isConfigFile(name) {
2112
2112
  const ext = import_node_path4.default.extname(name);
2113
2113
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2114
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
2114
+ if (name === ".env" || name.startsWith(".env.")) {
2115
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2116
+ return { match: true, fileType: "env" };
2117
+ }
2115
2118
  return { match: false, fileType: "" };
2116
2119
  }
2120
+ function isTestPath(filePath) {
2121
+ const normalised = filePath.replace(/\\/g, "/");
2122
+ const segments = normalised.split("/");
2123
+ for (const seg of segments) {
2124
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
2125
+ return true;
2126
+ }
2127
+ }
2128
+ const base = segments[segments.length - 1] ?? "";
2129
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
2130
+ }
2131
+ function isEnvTemplateFile(name) {
2132
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
2133
+ return true;
2134
+ }
2135
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
2136
+ }
2137
+ function maskCommentsInSource(src) {
2138
+ const len = src.length;
2139
+ const out = new Array(len);
2140
+ let i = 0;
2141
+ let inString = 0;
2142
+ let escaped = false;
2143
+ while (i < len) {
2144
+ const c = src[i];
2145
+ if (inString !== 0) {
2146
+ out[i] = c;
2147
+ if (escaped) {
2148
+ escaped = false;
2149
+ } else if (c === "\\") {
2150
+ escaped = true;
2151
+ } else if (c === inString) {
2152
+ inString = 0;
2153
+ }
2154
+ i++;
2155
+ continue;
2156
+ }
2157
+ if (c === "/" && i + 1 < len) {
2158
+ const next = src[i + 1];
2159
+ if (next === "/") {
2160
+ out[i] = " ";
2161
+ out[i + 1] = " ";
2162
+ let j = i + 2;
2163
+ while (j < len && src[j] !== "\n") {
2164
+ out[j] = " ";
2165
+ j++;
2166
+ }
2167
+ i = j;
2168
+ continue;
2169
+ }
2170
+ if (next === "*") {
2171
+ out[i] = " ";
2172
+ out[i + 1] = " ";
2173
+ let j = i + 2;
2174
+ while (j < len) {
2175
+ if (src[j] === "\n") {
2176
+ out[j] = "\n";
2177
+ j++;
2178
+ continue;
2179
+ }
2180
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
2181
+ out[j] = " ";
2182
+ out[j + 1] = " ";
2183
+ j += 2;
2184
+ break;
2185
+ }
2186
+ out[j] = " ";
2187
+ j++;
2188
+ }
2189
+ i = j;
2190
+ continue;
2191
+ }
2192
+ }
2193
+ out[i] = c;
2194
+ if (c === "'" || c === '"' || c === "`") inString = c;
2195
+ i++;
2196
+ }
2197
+ return out.join("");
2198
+ }
2199
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
2200
+ function urlMatchesHost(urlString, host) {
2201
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
2202
+ if (!URL_LIKE.test(urlString)) return false;
2203
+ const [wantedHost, wantedPort] = host.split(":");
2204
+ let parsed;
2205
+ try {
2206
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
2207
+ parsed = new URL(candidate);
2208
+ } catch {
2209
+ return false;
2210
+ }
2211
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
2212
+ if (wantedPort && parsed.port !== wantedPort) return false;
2213
+ return true;
2214
+ }
2117
2215
  function cleanVersion(raw) {
2118
2216
  if (!raw) return void 0;
2119
2217
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -2269,6 +2367,33 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2269
2367
  return author ?? void 0;
2270
2368
  }
2271
2369
 
2370
+ // src/extract/errors.ts
2371
+ init_cjs_shims();
2372
+ var import_node_fs7 = require("fs");
2373
+ var import_node_path7 = __toESM(require("path"), 1);
2374
+ var sink = [];
2375
+ function recordExtractionError(producer, file, err) {
2376
+ const e = err instanceof Error ? err : new Error(String(err));
2377
+ sink.push({
2378
+ producer,
2379
+ file,
2380
+ error: e.message,
2381
+ stack: e.stack,
2382
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2383
+ source: "extract"
2384
+ });
2385
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2386
+ }
2387
+ function drainExtractionErrors() {
2388
+ return sink.splice(0, sink.length);
2389
+ }
2390
+ async function writeExtractionErrors(errors, errorsPath) {
2391
+ if (errors.length === 0) return;
2392
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2393
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2394
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2395
+ }
2396
+
2272
2397
  // src/extract/services.ts
2273
2398
  var DEFAULT_SCAN_DEPTH = 5;
2274
2399
  function parseScanDepth() {
@@ -2285,21 +2410,21 @@ function workspaceGlobs(pkg) {
2285
2410
  return null;
2286
2411
  }
2287
2412
  async function loadGitignore(scanPath) {
2288
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2413
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2289
2414
  if (!await exists(gitignorePath)) return null;
2290
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2415
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
2291
2416
  return (0, import_ignore.default)().add(raw);
2292
2417
  }
2293
2418
  async function walkDirs(start, scanPath, options, visit) {
2294
2419
  async function recurse(current, depth) {
2295
2420
  if (depth > options.maxDepth) return;
2296
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2421
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2297
2422
  for (const entry of entries) {
2298
2423
  if (!entry.isDirectory()) continue;
2299
2424
  if (IGNORED_DIRS.has(entry.name)) continue;
2300
- const child = import_node_path7.default.join(current, entry.name);
2425
+ const child = import_node_path8.default.join(current, entry.name);
2301
2426
  if (options.ig) {
2302
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2427
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2303
2428
  if (rel && options.ig.ignores(rel + "/")) continue;
2304
2429
  }
2305
2430
  await visit(child);
@@ -2314,8 +2439,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2314
2439
  for (const raw of globs) {
2315
2440
  const pattern = raw.replace(/^\.\//, "");
2316
2441
  if (!pattern.includes("*")) {
2317
- const candidate = import_node_path7.default.join(scanPath, pattern);
2318
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2442
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2443
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2319
2444
  continue;
2320
2445
  }
2321
2446
  const segments = pattern.split("/");
@@ -2324,13 +2449,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2324
2449
  if (seg.includes("*")) break;
2325
2450
  staticSegments.push(seg);
2326
2451
  }
2327
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2452
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2328
2453
  if (!await exists(start)) continue;
2329
2454
  const hasDoubleStar = pattern.includes("**");
2330
2455
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2331
2456
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2332
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2333
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2457
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2458
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2334
2459
  found.add(dir);
2335
2460
  }
2336
2461
  });
@@ -2338,15 +2463,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2338
2463
  return [...found];
2339
2464
  }
2340
2465
  async function discoverNodeService(scanPath, dir) {
2341
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2466
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2342
2467
  if (!await exists(pkgPath)) return null;
2343
2468
  let pkg;
2344
2469
  try {
2345
2470
  pkg = await readJson(pkgPath);
2346
2471
  } catch (err) {
2347
- console.warn(
2348
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2349
- );
2472
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2350
2473
  return null;
2351
2474
  }
2352
2475
  if (!pkg.name) return null;
@@ -2357,7 +2480,7 @@ async function discoverNodeService(scanPath, dir) {
2357
2480
  language: "javascript",
2358
2481
  version: pkg.version,
2359
2482
  dependencies: pkg.dependencies ?? {},
2360
- repoPath: import_node_path7.default.relative(scanPath, dir),
2483
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2361
2484
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2362
2485
  };
2363
2486
  return { pkg, dir, node };
@@ -2373,19 +2496,21 @@ async function discoverPyService(scanPath, dir) {
2373
2496
  language: "python",
2374
2497
  version: py.version,
2375
2498
  dependencies: py.dependencies,
2376
- repoPath: import_node_path7.default.relative(scanPath, dir)
2499
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2377
2500
  };
2378
2501
  return { pkg, dir, node };
2379
2502
  }
2380
2503
  async function discoverServices(scanPath) {
2381
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2504
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2382
2505
  let rootPkg = null;
2383
2506
  if (await exists(rootPkgPath)) {
2384
2507
  try {
2385
2508
  rootPkg = await readJson(rootPkgPath);
2386
2509
  } catch (err) {
2387
- console.warn(
2388
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2510
+ recordExtractionError(
2511
+ "services workspaces",
2512
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2513
+ err
2389
2514
  );
2390
2515
  }
2391
2516
  }
@@ -2401,9 +2526,9 @@ async function discoverServices(scanPath) {
2401
2526
  scanPath,
2402
2527
  { maxDepth: parseScanDepth(), ig },
2403
2528
  async (dir) => {
2404
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2529
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2405
2530
  candidateDirs.push(dir);
2406
- } 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"))) {
2531
+ } 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"))) {
2407
2532
  candidateDirs.push(dir);
2408
2533
  }
2409
2534
  }
@@ -2417,8 +2542,8 @@ async function discoverServices(scanPath) {
2417
2542
  if (!service) continue;
2418
2543
  const existingDir = seen.get(service.node.name);
2419
2544
  if (existingDir !== void 0) {
2420
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2421
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2545
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2546
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2422
2547
  console.warn(
2423
2548
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2424
2549
  );
@@ -2455,8 +2580,8 @@ function addServiceNodes(graph, services) {
2455
2580
 
2456
2581
  // src/extract/aliases.ts
2457
2582
  init_cjs_shims();
2458
- var import_node_path8 = __toESM(require("path"), 1);
2459
- var import_node_fs8 = require("fs");
2583
+ var import_node_path9 = __toESM(require("path"), 1);
2584
+ var import_node_fs9 = require("fs");
2460
2585
  var import_yaml2 = require("yaml");
2461
2586
  var import_types7 = require("@neat.is/types");
2462
2587
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2483,14 +2608,14 @@ function indexServicesByName(services) {
2483
2608
  const map = /* @__PURE__ */ new Map();
2484
2609
  for (const s of services) {
2485
2610
  map.set(s.node.name, s.node.id);
2486
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2611
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2487
2612
  }
2488
2613
  return map;
2489
2614
  }
2490
2615
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2491
2616
  let composePath = null;
2492
2617
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2493
- const abs = import_node_path8.default.join(scanPath, name);
2618
+ const abs = import_node_path9.default.join(scanPath, name);
2494
2619
  if (await exists(abs)) {
2495
2620
  composePath = abs;
2496
2621
  break;
@@ -2501,8 +2626,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2501
2626
  try {
2502
2627
  compose = await readYaml(composePath);
2503
2628
  } catch (err) {
2504
- console.warn(
2505
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2629
+ recordExtractionError(
2630
+ "aliases compose",
2631
+ import_node_path9.default.relative(scanPath, composePath),
2632
+ err
2506
2633
  );
2507
2634
  return;
2508
2635
  }
@@ -2544,15 +2671,13 @@ function parseDockerfileLabels(content) {
2544
2671
  }
2545
2672
  async function collectDockerfileAliases(graph, services) {
2546
2673
  for (const service of services) {
2547
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2674
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2548
2675
  if (!await exists(dockerfilePath)) continue;
2549
2676
  let content;
2550
2677
  try {
2551
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2678
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2552
2679
  } catch (err) {
2553
- console.warn(
2554
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2555
- );
2680
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2556
2681
  continue;
2557
2682
  }
2558
2683
  const aliases = parseDockerfileLabels(content);
@@ -2562,13 +2687,13 @@ async function collectDockerfileAliases(graph, services) {
2562
2687
  async function walkYamlFiles(start, depth = 0, max = 5) {
2563
2688
  if (depth > max) return [];
2564
2689
  const out = [];
2565
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2690
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2566
2691
  for (const entry of entries) {
2567
2692
  if (entry.isDirectory()) {
2568
2693
  if (IGNORED_DIRS.has(entry.name)) continue;
2569
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
2570
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
2571
- out.push(import_node_path8.default.join(start, entry.name));
2694
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
2695
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
2696
+ out.push(import_node_path9.default.join(start, entry.name));
2572
2697
  }
2573
2698
  }
2574
2699
  return out;
@@ -2595,7 +2720,7 @@ function k8sServiceTarget(doc, byName) {
2595
2720
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2596
2721
  const files = await walkYamlFiles(scanPath);
2597
2722
  for (const file of files) {
2598
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2723
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2599
2724
  let docs;
2600
2725
  try {
2601
2726
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2620,14 +2745,14 @@ async function addServiceAliases(graph, scanPath, services) {
2620
2745
 
2621
2746
  // src/extract/databases/index.ts
2622
2747
  init_cjs_shims();
2623
- var import_node_path16 = __toESM(require("path"), 1);
2748
+ var import_node_path17 = __toESM(require("path"), 1);
2624
2749
  var import_types8 = require("@neat.is/types");
2625
2750
 
2626
2751
  // src/extract/databases/db-config-yaml.ts
2627
2752
  init_cjs_shims();
2628
- var import_node_path9 = __toESM(require("path"), 1);
2753
+ var import_node_path10 = __toESM(require("path"), 1);
2629
2754
  async function parse(serviceDir) {
2630
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2755
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2631
2756
  if (!await exists(yamlPath)) return [];
2632
2757
  const raw = await readYaml(yamlPath);
2633
2758
  return [
@@ -2645,13 +2770,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2645
2770
 
2646
2771
  // src/extract/databases/dotenv.ts
2647
2772
  init_cjs_shims();
2648
- var import_node_fs10 = require("fs");
2649
- var import_node_path11 = __toESM(require("path"), 1);
2773
+ var import_node_fs11 = require("fs");
2774
+ var import_node_path12 = __toESM(require("path"), 1);
2650
2775
 
2651
2776
  // src/extract/databases/shared.ts
2652
2777
  init_cjs_shims();
2653
- var import_node_fs9 = require("fs");
2654
- var import_node_path10 = __toESM(require("path"), 1);
2778
+ var import_node_fs10 = require("fs");
2779
+ var import_node_path11 = __toESM(require("path"), 1);
2655
2780
  function schemeToEngine(scheme) {
2656
2781
  const s = scheme.toLowerCase().split("+")[0];
2657
2782
  switch (s) {
@@ -2690,14 +2815,14 @@ function parseConnectionString(url) {
2690
2815
  }
2691
2816
  async function readIfExists(filePath) {
2692
2817
  try {
2693
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2818
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2694
2819
  } catch {
2695
2820
  return null;
2696
2821
  }
2697
2822
  }
2698
2823
  async function findFirst(serviceDir, candidates) {
2699
2824
  for (const rel of candidates) {
2700
- const abs = import_node_path10.default.join(serviceDir, rel);
2825
+ const abs = import_node_path11.default.join(serviceDir, rel);
2701
2826
  const content = await readIfExists(abs);
2702
2827
  if (content !== null) return abs;
2703
2828
  }
@@ -2748,15 +2873,15 @@ function parseDotenvLine(line) {
2748
2873
  return { key, value };
2749
2874
  }
2750
2875
  async function parse2(serviceDir) {
2751
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2876
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2752
2877
  const configs = [];
2753
2878
  const seen = /* @__PURE__ */ new Set();
2754
2879
  for (const entry of entries) {
2755
2880
  if (!entry.isFile()) continue;
2756
2881
  const match = isConfigFile(entry.name);
2757
2882
  if (!match.match || match.fileType !== "env") continue;
2758
- const filePath = import_node_path11.default.join(serviceDir, entry.name);
2759
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2883
+ const filePath = import_node_path12.default.join(serviceDir, entry.name);
2884
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2760
2885
  for (const line of content.split("\n")) {
2761
2886
  const parsed = parseDotenvLine(line);
2762
2887
  if (!parsed) continue;
@@ -2775,9 +2900,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2775
2900
 
2776
2901
  // src/extract/databases/prisma.ts
2777
2902
  init_cjs_shims();
2778
- var import_node_path12 = __toESM(require("path"), 1);
2903
+ var import_node_path13 = __toESM(require("path"), 1);
2779
2904
  async function parse3(serviceDir) {
2780
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2905
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2781
2906
  const content = await readIfExists(schemaPath);
2782
2907
  if (!content) return [];
2783
2908
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2909,10 +3034,10 @@ var knexParser = { name: "knex", parse: parse5 };
2909
3034
 
2910
3035
  // src/extract/databases/ormconfig.ts
2911
3036
  init_cjs_shims();
2912
- var import_node_path13 = __toESM(require("path"), 1);
3037
+ var import_node_path14 = __toESM(require("path"), 1);
2913
3038
  async function parse6(serviceDir) {
2914
3039
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2915
- const abs = import_node_path13.default.join(serviceDir, candidate);
3040
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2916
3041
  if (!await exists(abs)) continue;
2917
3042
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2918
3043
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2972,9 +3097,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2972
3097
 
2973
3098
  // src/extract/databases/sequelize.ts
2974
3099
  init_cjs_shims();
2975
- var import_node_path14 = __toESM(require("path"), 1);
3100
+ var import_node_path15 = __toESM(require("path"), 1);
2976
3101
  async function parse8(serviceDir) {
2977
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
3102
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2978
3103
  if (!await exists(configPath)) return [];
2979
3104
  const raw = await readJson(configPath);
2980
3105
  const out = [];
@@ -3001,7 +3126,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3001
3126
 
3002
3127
  // src/extract/databases/docker-compose.ts
3003
3128
  init_cjs_shims();
3004
- var import_node_path15 = __toESM(require("path"), 1);
3129
+ var import_node_path16 = __toESM(require("path"), 1);
3005
3130
  function portFromService(svc) {
3006
3131
  for (const raw of svc.ports ?? []) {
3007
3132
  const str = String(raw);
@@ -3028,7 +3153,7 @@ function databaseFromEnv(svc) {
3028
3153
  }
3029
3154
  async function parse9(serviceDir) {
3030
3155
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3031
- const abs = import_node_path15.default.join(serviceDir, name);
3156
+ const abs = import_node_path16.default.join(serviceDir, name);
3032
3157
  if (!await exists(abs)) continue;
3033
3158
  const raw = await readYaml(abs);
3034
3159
  if (!raw?.services) return [];
@@ -3206,11 +3331,12 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3206
3331
  target: dbNode.id,
3207
3332
  type: import_types8.EdgeType.CONNECTS_TO,
3208
3333
  provenance: import_types8.Provenance.EXTRACTED,
3209
- ...config.sourceFile ? {
3210
- evidence: {
3211
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
3212
- }
3213
- } : {}
3334
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3335
+ // Ghost-edge cleanup keys retirement on this; the conditional
3336
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3337
+ evidence: {
3338
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3339
+ }
3214
3340
  };
3215
3341
  if (!graph.hasEdge(edge.id)) {
3216
3342
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3236,15 +3362,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3236
3362
 
3237
3363
  // src/extract/configs.ts
3238
3364
  init_cjs_shims();
3239
- var import_node_fs11 = require("fs");
3240
- var import_node_path17 = __toESM(require("path"), 1);
3365
+ var import_node_fs12 = require("fs");
3366
+ var import_node_path18 = __toESM(require("path"), 1);
3241
3367
  var import_types9 = require("@neat.is/types");
3242
3368
  async function walkConfigFiles(dir) {
3243
3369
  const out = [];
3244
3370
  async function walk(current) {
3245
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3371
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3246
3372
  for (const entry of entries) {
3247
- const full = import_node_path17.default.join(current, entry.name);
3373
+ const full = import_node_path18.default.join(current, entry.name);
3248
3374
  if (entry.isDirectory()) {
3249
3375
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3250
3376
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -3261,13 +3387,13 @@ async function addConfigNodes(graph, services, scanPath) {
3261
3387
  for (const service of services) {
3262
3388
  const configFiles = await walkConfigFiles(service.dir);
3263
3389
  for (const file of configFiles) {
3264
- const relPath = import_node_path17.default.relative(scanPath, file);
3390
+ const relPath = import_node_path18.default.relative(scanPath, file);
3265
3391
  const node = {
3266
3392
  id: (0, import_types9.configId)(relPath),
3267
3393
  type: import_types9.NodeType.ConfigNode,
3268
- name: import_node_path17.default.basename(file),
3394
+ name: import_node_path18.default.basename(file),
3269
3395
  path: relPath,
3270
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3396
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3271
3397
  };
3272
3398
  if (!graph.hasNode(node.id)) {
3273
3399
  graph.addNode(node.id, node);
@@ -3279,7 +3405,7 @@ async function addConfigNodes(graph, services, scanPath) {
3279
3405
  target: node.id,
3280
3406
  type: import_types9.EdgeType.CONFIGURED_BY,
3281
3407
  provenance: import_types9.Provenance.EXTRACTED,
3282
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3408
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3283
3409
  };
3284
3410
  if (!graph.hasEdge(edge.id)) {
3285
3411
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3296,7 +3422,7 @@ var import_types15 = require("@neat.is/types");
3296
3422
 
3297
3423
  // src/extract/calls/http.ts
3298
3424
  init_cjs_shims();
3299
- var import_node_path19 = __toESM(require("path"), 1);
3425
+ var import_node_path20 = __toESM(require("path"), 1);
3300
3426
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3301
3427
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3302
3428
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3304,17 +3430,17 @@ var import_types10 = require("@neat.is/types");
3304
3430
 
3305
3431
  // src/extract/calls/shared.ts
3306
3432
  init_cjs_shims();
3307
- var import_node_fs12 = require("fs");
3308
- var import_node_path18 = __toESM(require("path"), 1);
3433
+ var import_node_fs13 = require("fs");
3434
+ var import_node_path19 = __toESM(require("path"), 1);
3309
3435
  async function walkSourceFiles(dir) {
3310
3436
  const out = [];
3311
3437
  async function walk(current) {
3312
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3438
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3313
3439
  for (const entry of entries) {
3314
- const full = import_node_path18.default.join(current, entry.name);
3440
+ const full = import_node_path19.default.join(current, entry.name);
3315
3441
  if (entry.isDirectory()) {
3316
3442
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3317
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
3443
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3318
3444
  out.push(full);
3319
3445
  }
3320
3446
  }
@@ -3327,7 +3453,7 @@ async function loadSourceFiles(dir) {
3327
3453
  const out = [];
3328
3454
  for (const p of paths) {
3329
3455
  try {
3330
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3456
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3331
3457
  out.push({ path: p, content });
3332
3458
  } catch {
3333
3459
  }
@@ -3346,8 +3472,27 @@ function snippet(text, line) {
3346
3472
 
3347
3473
  // src/extract/calls/http.ts
3348
3474
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3475
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3476
+ function isInsideJsxExternalLink(node) {
3477
+ let cursor = node.parent;
3478
+ while (cursor) {
3479
+ if (cursor.type === "jsx_attribute") {
3480
+ let owner = cursor.parent;
3481
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3482
+ owner = owner.parent;
3483
+ }
3484
+ if (!owner) return false;
3485
+ const tagNode = owner.namedChild(0);
3486
+ const tagName = tagNode?.text ?? "";
3487
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3488
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3489
+ }
3490
+ cursor = cursor.parent;
3491
+ }
3492
+ return false;
3493
+ }
3349
3494
  function collectStringLiterals(node, out) {
3350
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3495
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3351
3496
  for (let i = 0; i < node.namedChildCount; i++) {
3352
3497
  const child = node.namedChild(i);
3353
3498
  if (child) collectStringLiterals(child, out);
@@ -3359,8 +3504,9 @@ function callsFromSource(source, parser, knownHosts) {
3359
3504
  collectStringLiterals(tree.rootNode, literals);
3360
3505
  const targets = /* @__PURE__ */ new Set();
3361
3506
  for (const lit of literals) {
3507
+ if (isInsideJsxExternalLink(lit.node)) continue;
3362
3508
  for (const host of knownHosts) {
3363
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3509
+ if (urlMatchesHost(lit.text, host)) {
3364
3510
  targets.add(host);
3365
3511
  }
3366
3512
  }
@@ -3383,9 +3529,9 @@ async function addHttpCallEdges(graph, services) {
3383
3529
  const knownHosts = /* @__PURE__ */ new Set();
3384
3530
  const hostToNodeId = /* @__PURE__ */ new Map();
3385
3531
  for (const service of services) {
3386
- knownHosts.add(import_node_path19.default.basename(service.dir));
3532
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3387
3533
  knownHosts.add(service.pkg.name);
3388
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3534
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3389
3535
  hostToNodeId.set(service.pkg.name, service.node.id);
3390
3536
  }
3391
3537
  let edgesAdded = 0;
@@ -3393,14 +3539,13 @@ async function addHttpCallEdges(graph, services) {
3393
3539
  const files = await loadSourceFiles(service.dir);
3394
3540
  const seenTargets = /* @__PURE__ */ new Map();
3395
3541
  for (const file of files) {
3396
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3542
+ if (isTestPath(file.path)) continue;
3543
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3397
3544
  let targets;
3398
3545
  try {
3399
3546
  targets = callsFromSource(file.content, parser, knownHosts);
3400
3547
  } catch (err) {
3401
- console.warn(
3402
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3403
- );
3548
+ recordExtractionError("http call extraction", file.path, err);
3404
3549
  continue;
3405
3550
  }
3406
3551
  for (const t of targets) {
@@ -3421,7 +3566,7 @@ async function addHttpCallEdges(graph, services) {
3421
3566
  type: import_types10.EdgeType.CALLS,
3422
3567
  provenance: import_types10.Provenance.EXTRACTED,
3423
3568
  evidence: {
3424
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3569
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3425
3570
  line,
3426
3571
  snippet: snippet(fileContent, line)
3427
3572
  }
@@ -3437,7 +3582,7 @@ async function addHttpCallEdges(graph, services) {
3437
3582
 
3438
3583
  // src/extract/calls/kafka.ts
3439
3584
  init_cjs_shims();
3440
- var import_node_path20 = __toESM(require("path"), 1);
3585
+ var import_node_path21 = __toESM(require("path"), 1);
3441
3586
  var import_types11 = require("@neat.is/types");
3442
3587
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3443
3588
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3464,7 +3609,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3464
3609
  kind: "kafka-topic",
3465
3610
  edgeType,
3466
3611
  evidence: {
3467
- file: import_node_path20.default.relative(serviceDir, file.path),
3612
+ file: import_node_path21.default.relative(serviceDir, file.path),
3468
3613
  line,
3469
3614
  snippet: snippet(file.content, line)
3470
3615
  }
@@ -3477,7 +3622,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3477
3622
 
3478
3623
  // src/extract/calls/redis.ts
3479
3624
  init_cjs_shims();
3480
- var import_node_path21 = __toESM(require("path"), 1);
3625
+ var import_node_path22 = __toESM(require("path"), 1);
3481
3626
  var import_types12 = require("@neat.is/types");
3482
3627
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3483
3628
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3496,7 +3641,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3496
3641
  kind: "redis",
3497
3642
  edgeType: "CALLS",
3498
3643
  evidence: {
3499
- file: import_node_path21.default.relative(serviceDir, file.path),
3644
+ file: import_node_path22.default.relative(serviceDir, file.path),
3500
3645
  line,
3501
3646
  snippet: snippet(file.content, line)
3502
3647
  }
@@ -3507,7 +3652,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3507
3652
 
3508
3653
  // src/extract/calls/aws.ts
3509
3654
  init_cjs_shims();
3510
- var import_node_path22 = __toESM(require("path"), 1);
3655
+ var import_node_path23 = __toESM(require("path"), 1);
3511
3656
  var import_types13 = require("@neat.is/types");
3512
3657
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3513
3658
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3537,7 +3682,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3537
3682
  kind,
3538
3683
  edgeType: "CALLS",
3539
3684
  evidence: {
3540
- file: import_node_path22.default.relative(serviceDir, file.path),
3685
+ file: import_node_path23.default.relative(serviceDir, file.path),
3541
3686
  line,
3542
3687
  snippet: snippet(file.content, line)
3543
3688
  }
@@ -3562,16 +3707,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3562
3707
 
3563
3708
  // src/extract/calls/grpc.ts
3564
3709
  init_cjs_shims();
3565
- var import_node_path23 = __toESM(require("path"), 1);
3710
+ var import_node_path24 = __toESM(require("path"), 1);
3566
3711
  var import_types14 = require("@neat.is/types");
3567
3712
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3713
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3714
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3568
3715
  function isLikelyAddress(value) {
3569
3716
  if (!value) return false;
3570
3717
  return /:\d{2,5}$/.test(value) || value.includes(".");
3571
3718
  }
3719
+ function normaliseForMatch(s) {
3720
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3721
+ }
3722
+ function readImports(content) {
3723
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3724
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3725
+ let m;
3726
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3727
+ const raw = m[1];
3728
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3729
+ }
3730
+ return {
3731
+ awsSdkSuffixes,
3732
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3733
+ };
3734
+ }
3735
+ function classifyClient(symbol, ctx) {
3736
+ const key = normaliseForMatch(symbol);
3737
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3738
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3739
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3740
+ return null;
3741
+ }
3572
3742
  function grpcEndpointsFromFile(file, serviceDir) {
3573
3743
  const out = [];
3574
3744
  const seen = /* @__PURE__ */ new Set();
3745
+ const ctx = readImports(file.content);
3575
3746
  GRPC_CLIENT_RE.lastIndex = 0;
3576
3747
  let m;
3577
3748
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3579,15 +3750,18 @@ function grpcEndpointsFromFile(file, serviceDir) {
3579
3750
  const addr = m[2]?.trim();
3580
3751
  const name = isLikelyAddress(addr) ? addr : symbol;
3581
3752
  if (seen.has(name)) continue;
3753
+ const classified = classifyClient(symbol, ctx);
3754
+ if (!classified) continue;
3582
3755
  seen.add(name);
3756
+ const { kind } = classified;
3583
3757
  const line = lineOf(file.content, m[0]);
3584
3758
  out.push({
3585
- infraId: (0, import_types14.infraId)("grpc-service", name),
3759
+ infraId: (0, import_types14.infraId)(kind, name),
3586
3760
  name,
3587
- kind: "grpc-service",
3761
+ kind,
3588
3762
  edgeType: "CALLS",
3589
3763
  evidence: {
3590
- file: import_node_path23.default.relative(serviceDir, file.path),
3764
+ file: import_node_path24.default.relative(serviceDir, file.path),
3591
3765
  line,
3592
3766
  snippet: snippet(file.content, line)
3593
3767
  }
@@ -3607,6 +3781,9 @@ function edgeTypeFromEndpoint(ep) {
3607
3781
  return import_types15.EdgeType.CALLS;
3608
3782
  }
3609
3783
  }
3784
+ function isAwsKind(kind) {
3785
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3786
+ }
3610
3787
  async function addExternalEndpointEdges(graph, services) {
3611
3788
  let nodesAdded = 0;
3612
3789
  let edgesAdded = 0;
@@ -3614,10 +3791,13 @@ async function addExternalEndpointEdges(graph, services) {
3614
3791
  const files = await loadSourceFiles(service.dir);
3615
3792
  const endpoints = [];
3616
3793
  for (const file of files) {
3617
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3618
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3619
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3620
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3794
+ if (isTestPath(file.path)) continue;
3795
+ const masked = maskCommentsInSource(file.content);
3796
+ const maskedFile = { path: file.path, content: masked };
3797
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3798
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3799
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3800
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3621
3801
  }
3622
3802
  if (endpoints.length === 0) continue;
3623
3803
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3627,7 +3807,10 @@ async function addExternalEndpointEdges(graph, services) {
3627
3807
  id: ep.infraId,
3628
3808
  type: import_types15.NodeType.InfraNode,
3629
3809
  name: ep.name,
3630
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3810
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3811
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3812
+ // bucket / table kinds from aws.ts.
3813
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3631
3814
  kind: ep.kind
3632
3815
  };
3633
3816
  graph.addNode(node.id, node);
@@ -3667,7 +3850,7 @@ init_cjs_shims();
3667
3850
 
3668
3851
  // src/extract/infra/docker-compose.ts
3669
3852
  init_cjs_shims();
3670
- var import_node_path24 = __toESM(require("path"), 1);
3853
+ var import_node_path25 = __toESM(require("path"), 1);
3671
3854
  var import_types17 = require("@neat.is/types");
3672
3855
 
3673
3856
  // src/extract/infra/shared.ts
@@ -3705,7 +3888,7 @@ function dependsOnList(value) {
3705
3888
  }
3706
3889
  function serviceNameToServiceNode(name, services) {
3707
3890
  for (const s of services) {
3708
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3891
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3709
3892
  }
3710
3893
  return null;
3711
3894
  }
@@ -3714,7 +3897,7 @@ async function addComposeInfra(graph, scanPath, services) {
3714
3897
  let edgesAdded = 0;
3715
3898
  let composePath = null;
3716
3899
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3717
- const abs = import_node_path24.default.join(scanPath, name);
3900
+ const abs = import_node_path25.default.join(scanPath, name);
3718
3901
  if (await exists(abs)) {
3719
3902
  composePath = abs;
3720
3903
  break;
@@ -3725,13 +3908,15 @@ async function addComposeInfra(graph, scanPath, services) {
3725
3908
  try {
3726
3909
  compose = await readYaml(composePath);
3727
3910
  } catch (err) {
3728
- console.warn(
3729
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3911
+ recordExtractionError(
3912
+ "infra docker-compose",
3913
+ import_node_path25.default.relative(scanPath, composePath),
3914
+ err
3730
3915
  );
3731
3916
  return { nodesAdded, edgesAdded };
3732
3917
  }
3733
3918
  if (!compose?.services) return { nodesAdded, edgesAdded };
3734
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3919
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3735
3920
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3736
3921
  for (const [composeName, svc] of Object.entries(compose.services)) {
3737
3922
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3772,8 +3957,8 @@ async function addComposeInfra(graph, scanPath, services) {
3772
3957
 
3773
3958
  // src/extract/infra/dockerfile.ts
3774
3959
  init_cjs_shims();
3775
- var import_node_path25 = __toESM(require("path"), 1);
3776
- var import_node_fs13 = require("fs");
3960
+ var import_node_path26 = __toESM(require("path"), 1);
3961
+ var import_node_fs14 = require("fs");
3777
3962
  var import_types18 = require("@neat.is/types");
3778
3963
  function runtimeImage(content) {
3779
3964
  const lines = content.split("\n");
@@ -3793,14 +3978,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3793
3978
  let nodesAdded = 0;
3794
3979
  let edgesAdded = 0;
3795
3980
  for (const service of services) {
3796
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3981
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3797
3982
  if (!await exists(dockerfilePath)) continue;
3798
3983
  let content;
3799
3984
  try {
3800
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3985
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3801
3986
  } catch (err) {
3802
- console.warn(
3803
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3987
+ recordExtractionError(
3988
+ "infra dockerfile",
3989
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3990
+ err
3804
3991
  );
3805
3992
  continue;
3806
3993
  }
@@ -3820,7 +4007,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3820
4007
  type: import_types18.EdgeType.RUNS_ON,
3821
4008
  provenance: import_types18.Provenance.EXTRACTED,
3822
4009
  evidence: {
3823
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
4010
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3824
4011
  }
3825
4012
  };
3826
4013
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3832,19 +4019,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3832
4019
 
3833
4020
  // src/extract/infra/terraform.ts
3834
4021
  init_cjs_shims();
3835
- var import_node_fs14 = require("fs");
3836
- var import_node_path26 = __toESM(require("path"), 1);
4022
+ var import_node_fs15 = require("fs");
4023
+ var import_node_path27 = __toESM(require("path"), 1);
3837
4024
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3838
4025
  async function walkTfFiles(start, depth = 0, max = 5) {
3839
4026
  if (depth > max) return [];
3840
4027
  const out = [];
3841
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4028
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3842
4029
  for (const entry of entries) {
3843
4030
  if (entry.isDirectory()) {
3844
4031
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3845
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
4032
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
3846
4033
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3847
- out.push(import_node_path26.default.join(start, entry.name));
4034
+ out.push(import_node_path27.default.join(start, entry.name));
3848
4035
  }
3849
4036
  }
3850
4037
  return out;
@@ -3853,7 +4040,7 @@ async function addTerraformResources(graph, scanPath) {
3853
4040
  let nodesAdded = 0;
3854
4041
  const files = await walkTfFiles(scanPath);
3855
4042
  for (const file of files) {
3856
- const content = await import_node_fs14.promises.readFile(file, "utf8");
4043
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3857
4044
  RESOURCE_RE.lastIndex = 0;
3858
4045
  let m;
3859
4046
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3871,8 +4058,8 @@ async function addTerraformResources(graph, scanPath) {
3871
4058
 
3872
4059
  // src/extract/infra/k8s.ts
3873
4060
  init_cjs_shims();
3874
- var import_node_fs15 = require("fs");
3875
- var import_node_path27 = __toESM(require("path"), 1);
4061
+ var import_node_fs16 = require("fs");
4062
+ var import_node_path28 = __toESM(require("path"), 1);
3876
4063
  var import_yaml3 = require("yaml");
3877
4064
  var K8S_KIND_TO_INFRA_KIND = {
3878
4065
  Service: "k8s-service",
@@ -3886,13 +4073,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3886
4073
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3887
4074
  if (depth > max) return [];
3888
4075
  const out = [];
3889
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4076
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3890
4077
  for (const entry of entries) {
3891
4078
  if (entry.isDirectory()) {
3892
4079
  if (IGNORED_DIRS.has(entry.name)) continue;
3893
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
3894
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
3895
- out.push(import_node_path27.default.join(start, entry.name));
4080
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
4081
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
4082
+ out.push(import_node_path28.default.join(start, entry.name));
3896
4083
  }
3897
4084
  }
3898
4085
  return out;
@@ -3901,7 +4088,7 @@ async function addK8sResources(graph, scanPath) {
3901
4088
  let nodesAdded = 0;
3902
4089
  const files = await walkYamlFiles2(scanPath);
3903
4090
  for (const file of files) {
3904
- const content = await import_node_fs15.promises.readFile(file, "utf8");
4091
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3905
4092
  let docs;
3906
4093
  try {
3907
4094
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3935,9 +4122,34 @@ async function addInfra(graph, scanPath, services) {
3935
4122
  };
3936
4123
  }
3937
4124
 
4125
+ // src/extract/retire.ts
4126
+ init_cjs_shims();
4127
+ var import_node_fs17 = require("fs");
4128
+ var import_node_path29 = __toESM(require("path"), 1);
4129
+ var import_types19 = require("@neat.is/types");
4130
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4131
+ const toDrop = [];
4132
+ const bases = [scanPath, ...serviceDirs];
4133
+ graph.forEachEdge((id, attrs) => {
4134
+ const edge = attrs;
4135
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4136
+ const evidenceFile = edge.evidence?.file;
4137
+ if (!evidenceFile) return;
4138
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
4139
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4140
+ return;
4141
+ }
4142
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4143
+ if (!found) toDrop.push(id);
4144
+ });
4145
+ for (const id of toDrop) graph.dropEdge(id);
4146
+ return toDrop.length;
4147
+ }
4148
+
3938
4149
  // src/extract/index.ts
3939
4150
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3940
4151
  await ensureCompatLoaded();
4152
+ drainExtractionErrors();
3941
4153
  const services = await discoverServices(scanPath);
3942
4154
  const phase1Nodes = addServiceNodes(graph, services);
3943
4155
  await addServiceAliases(graph, scanPath, services);
@@ -3945,12 +4157,30 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3945
4157
  const phase3 = await addConfigNodes(graph, services, scanPath);
3946
4158
  const phase4 = await addCallEdges(graph, services);
3947
4159
  const phase5 = await addInfra(graph, scanPath, services);
4160
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
4161
+ graph,
4162
+ scanPath,
4163
+ services.map((s) => s.dir)
4164
+ );
3948
4165
  const frontiersPromoted = promoteFrontierNodes(graph);
3949
4166
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
4167
+ const errorEntries = drainExtractionErrors();
4168
+ if (opts.errorsPath && errorEntries.length > 0) {
4169
+ try {
4170
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
4171
+ } catch (err) {
4172
+ console.warn(
4173
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
4174
+ );
4175
+ }
4176
+ }
3950
4177
  const result = {
3951
4178
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3952
4179
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3953
- frontiersPromoted
4180
+ frontiersPromoted,
4181
+ extractionErrors: errorEntries.length,
4182
+ errorEntries,
4183
+ ghostsRetired
3954
4184
  };
3955
4185
  emitNeatEvent({
3956
4186
  type: "extraction-complete",
@@ -3967,7 +4197,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3967
4197
 
3968
4198
  // src/diff.ts
3969
4199
  init_cjs_shims();
3970
- var import_node_fs16 = require("fs");
4200
+ var import_node_fs18 = require("fs");
3971
4201
  async function loadSnapshotForDiff(target) {
3972
4202
  if (/^https?:\/\//i.test(target)) {
3973
4203
  const res = await fetch(target);
@@ -3976,7 +4206,7 @@ async function loadSnapshotForDiff(target) {
3976
4206
  }
3977
4207
  return await res.json();
3978
4208
  }
3979
- const raw = await import_node_fs16.promises.readFile(target, "utf8");
4209
+ const raw = await import_node_fs18.promises.readFile(target, "utf8");
3980
4210
  return JSON.parse(raw);
3981
4211
  }
3982
4212
  function indexEntries(entries) {
@@ -4044,23 +4274,23 @@ function canonicalJson(value) {
4044
4274
 
4045
4275
  // src/projects.ts
4046
4276
  init_cjs_shims();
4047
- var import_node_path28 = __toESM(require("path"), 1);
4277
+ var import_node_path30 = __toESM(require("path"), 1);
4048
4278
  function pathsForProject(project, baseDir) {
4049
4279
  if (project === DEFAULT_PROJECT) {
4050
4280
  return {
4051
- snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
4052
- errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
4053
- staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
4054
- embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
4055
- policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
4281
+ snapshotPath: import_node_path30.default.join(baseDir, "graph.json"),
4282
+ errorsPath: import_node_path30.default.join(baseDir, "errors.ndjson"),
4283
+ staleEventsPath: import_node_path30.default.join(baseDir, "stale-events.ndjson"),
4284
+ embeddingsCachePath: import_node_path30.default.join(baseDir, "embeddings.json"),
4285
+ policyViolationsPath: import_node_path30.default.join(baseDir, "policy-violations.ndjson")
4056
4286
  };
4057
4287
  }
4058
4288
  return {
4059
- snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
4060
- errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
4061
- staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
4062
- embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
4063
- policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
4289
+ snapshotPath: import_node_path30.default.join(baseDir, `${project}.json`),
4290
+ errorsPath: import_node_path30.default.join(baseDir, `errors.${project}.ndjson`),
4291
+ staleEventsPath: import_node_path30.default.join(baseDir, `stale-events.${project}.ndjson`),
4292
+ embeddingsCachePath: import_node_path30.default.join(baseDir, `embeddings.${project}.json`),
4293
+ policyViolationsPath: import_node_path30.default.join(baseDir, `policy-violations.${project}.ndjson`)
4064
4294
  };
4065
4295
  }
4066
4296
  var Projects = class {
@@ -4100,23 +4330,23 @@ function parseExtraProjects(raw) {
4100
4330
 
4101
4331
  // src/registry.ts
4102
4332
  init_cjs_shims();
4103
- var import_node_fs17 = require("fs");
4333
+ var import_node_fs19 = require("fs");
4104
4334
  var import_node_os2 = __toESM(require("os"), 1);
4105
- var import_node_path29 = __toESM(require("path"), 1);
4106
- var import_types19 = require("@neat.is/types");
4335
+ var import_node_path31 = __toESM(require("path"), 1);
4336
+ var import_types20 = require("@neat.is/types");
4107
4337
  function neatHome() {
4108
4338
  const override = process.env.NEAT_HOME;
4109
- if (override && override.length > 0) return import_node_path29.default.resolve(override);
4110
- return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
4339
+ if (override && override.length > 0) return import_node_path31.default.resolve(override);
4340
+ return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
4111
4341
  }
4112
4342
  function registryPath() {
4113
- return import_node_path29.default.join(neatHome(), "projects.json");
4343
+ return import_node_path31.default.join(neatHome(), "projects.json");
4114
4344
  }
4115
4345
  async function readRegistry() {
4116
4346
  const file = registryPath();
4117
4347
  let raw;
4118
4348
  try {
4119
- raw = await import_node_fs17.promises.readFile(file, "utf8");
4349
+ raw = await import_node_fs19.promises.readFile(file, "utf8");
4120
4350
  } catch (err) {
4121
4351
  if (err.code === "ENOENT") {
4122
4352
  return { version: 1, projects: [] };
@@ -4124,7 +4354,7 @@ async function readRegistry() {
4124
4354
  throw err;
4125
4355
  }
4126
4356
  const parsed = JSON.parse(raw);
4127
- return import_types19.RegistryFileSchema.parse(parsed);
4357
+ return import_types20.RegistryFileSchema.parse(parsed);
4128
4358
  }
4129
4359
  async function getProject(name) {
4130
4360
  const reg = await readRegistry();
@@ -4314,11 +4544,11 @@ function registerRoutes(scope, ctx) {
4314
4544
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4315
4545
  const parsed = [];
4316
4546
  for (const c of candidates) {
4317
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4547
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4318
4548
  if (!r.success) {
4319
4549
  return reply.code(400).send({
4320
4550
  error: `unknown divergence type "${c}"`,
4321
- allowed: import_types20.DivergenceTypeSchema.options
4551
+ allowed: import_types21.DivergenceTypeSchema.options
4322
4552
  });
4323
4553
  }
4324
4554
  parsed.push(r.data);
@@ -4502,7 +4732,7 @@ function registerRoutes(scope, ctx) {
4502
4732
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4503
4733
  let violations = await log.readAll();
4504
4734
  if (req.query.severity) {
4505
- const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
4735
+ const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
4506
4736
  if (!sev.success) {
4507
4737
  return reply.code(400).send({
4508
4738
  error: "invalid severity",
@@ -4519,7 +4749,7 @@ function registerRoutes(scope, ctx) {
4519
4749
  scope.post("/policies/check", async (req, reply) => {
4520
4750
  const proj = resolveProject(registry, req, reply);
4521
4751
  if (!proj) return;
4522
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4752
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4523
4753
  if (!parsed.success) {
4524
4754
  return reply.code(400).send({
4525
4755
  error: "invalid /policies/check body",
@@ -4619,8 +4849,8 @@ async function buildApi(opts) {
4619
4849
 
4620
4850
  // src/persist.ts
4621
4851
  init_cjs_shims();
4622
- var import_node_fs18 = require("fs");
4623
- var import_node_path30 = __toESM(require("path"), 1);
4852
+ var import_node_fs20 = require("fs");
4853
+ var import_node_path32 = __toESM(require("path"), 1);
4624
4854
  var SCHEMA_VERSION = 2;
4625
4855
  function migrateV1ToV2(payload) {
4626
4856
  const nodes = payload.graph.nodes;
@@ -4634,7 +4864,7 @@ function migrateV1ToV2(payload) {
4634
4864
  return { ...payload, schemaVersion: 2 };
4635
4865
  }
4636
4866
  async function ensureDir(filePath) {
4637
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
4867
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(filePath), { recursive: true });
4638
4868
  }
4639
4869
  async function saveGraphToDisk(graph, outPath) {
4640
4870
  await ensureDir(outPath);
@@ -4644,13 +4874,13 @@ async function saveGraphToDisk(graph, outPath) {
4644
4874
  graph: graph.export()
4645
4875
  };
4646
4876
  const tmp = `${outPath}.tmp`;
4647
- await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4648
- await import_node_fs18.promises.rename(tmp, outPath);
4877
+ await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4878
+ await import_node_fs20.promises.rename(tmp, outPath);
4649
4879
  }
4650
4880
  async function loadGraphFromDisk(graph, outPath) {
4651
4881
  let raw;
4652
4882
  try {
4653
- raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4883
+ raw = await import_node_fs20.promises.readFile(outPath, "utf8");
4654
4884
  } catch (err) {
4655
4885
  if (err.code === "ENOENT") return;
4656
4886
  throw err;
@@ -4707,8 +4937,8 @@ init_otel_grpc();
4707
4937
 
4708
4938
  // src/search.ts
4709
4939
  init_cjs_shims();
4710
- var import_node_fs19 = require("fs");
4711
- var import_node_path33 = __toESM(require("path"), 1);
4940
+ var import_node_fs21 = require("fs");
4941
+ var import_node_path35 = __toESM(require("path"), 1);
4712
4942
  var import_node_crypto = require("crypto");
4713
4943
  var DEFAULT_LIMIT = 10;
4714
4944
  var NOMIC_DIM = 768;
@@ -4838,7 +5068,7 @@ async function pickEmbedder() {
4838
5068
  }
4839
5069
  async function readCache(cachePath) {
4840
5070
  try {
4841
- const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
5071
+ const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
4842
5072
  const parsed = JSON.parse(raw);
4843
5073
  if (parsed.version !== 1) return null;
4844
5074
  return parsed;
@@ -4847,8 +5077,8 @@ async function readCache(cachePath) {
4847
5077
  }
4848
5078
  }
4849
5079
  async function writeCache(cachePath, cache) {
4850
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
4851
- await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
5080
+ await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(cachePath), { recursive: true });
5081
+ await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
4852
5082
  }
4853
5083
  var VectorIndex = class {
4854
5084
  constructor(embedder, cachePath) {
@@ -5027,14 +5257,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
5027
5257
  async function main() {
5028
5258
  const baseDirEnv = process.env.NEAT_OUT_DIR;
5029
5259
  const legacyOutPath = process.env.NEAT_OUT_PATH;
5030
- const baseDir = baseDirEnv ? import_node_path34.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path34.default.resolve(import_node_path34.default.dirname(legacyOutPath)) : import_node_path34.default.resolve("./neat-out");
5031
- const defaultScanPath = import_node_path34.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
5260
+ const baseDir = baseDirEnv ? import_node_path36.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path36.default.resolve(import_node_path36.default.dirname(legacyOutPath)) : import_node_path36.default.resolve("./neat-out");
5261
+ const defaultScanPath = import_node_path36.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
5032
5262
  const registry = new Projects();
5033
5263
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
5034
5264
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
5035
5265
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
5036
5266
  const projectScan = process.env[envKey];
5037
- await bootProject(registry, name, projectScan ? import_node_path34.default.resolve(projectScan) : void 0, baseDir);
5267
+ await bootProject(registry, name, projectScan ? import_node_path36.default.resolve(projectScan) : void 0, baseDir);
5038
5268
  }
5039
5269
  const host = process.env.HOST ?? "0.0.0.0";
5040
5270
  const port = Number(process.env.PORT ?? 8080);