@neat.is/core 0.3.1 → 0.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -101,8 +101,8 @@ function reshapeGrpcRequest(req) {
101
101
  };
102
102
  }
103
103
  function resolveProtoRoot() {
104
- const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
- return import_node_path31.default.resolve(here, "..", "proto");
104
+ const here = import_node_path33.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
+ return import_node_path33.default.resolve(here, "..", "proto");
106
106
  }
107
107
  function loadTraceService() {
108
108
  const protoRoot = resolveProtoRoot();
@@ -157,13 +157,13 @@ async function startOtelGrpcReceiver(opts) {
157
157
  })
158
158
  };
159
159
  }
160
- var import_node_url, import_node_path31, grpc, protoLoader;
160
+ var import_node_url, import_node_path33, grpc, protoLoader;
161
161
  var init_otel_grpc = __esm({
162
162
  "src/otel-grpc.ts"() {
163
163
  "use strict";
164
164
  init_cjs_shims();
165
165
  import_node_url = require("url");
166
- import_node_path31 = __toESM(require("path"), 1);
166
+ import_node_path33 = __toESM(require("path"), 1);
167
167
  grpc = __toESM(require("@grpc/grpc-js"), 1);
168
168
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
169
169
  init_otel();
@@ -260,10 +260,10 @@ function parseOtlpRequest(body) {
260
260
  }
261
261
  function loadProtobufDecoder() {
262
262
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
263
- const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
- const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
263
+ const here = import_node_path34.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
+ const protoRoot = import_node_path34.default.resolve(here, "..", "proto");
265
265
  const root = new import_protobufjs.default.Root();
266
- root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
266
+ root.resolvePath = (_origin, target) => import_node_path34.default.resolve(protoRoot, target);
267
267
  root.loadSync(
268
268
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
269
269
  { keepCase: true }
@@ -355,12 +355,12 @@ async function buildOtelReceiver(opts) {
355
355
  };
356
356
  return decorated;
357
357
  }
358
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
358
+ var import_node_path34, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
359
359
  var init_otel = __esm({
360
360
  "src/otel.ts"() {
361
361
  "use strict";
362
362
  init_cjs_shims();
363
- import_node_path32 = __toESM(require("path"), 1);
363
+ import_node_path34 = __toESM(require("path"), 1);
364
364
  import_node_url2 = require("url");
365
365
  import_fastify2 = __toESM(require("fastify"), 1);
366
366
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -380,8 +380,8 @@ __export(cli_exports, {
380
380
  });
381
381
  module.exports = __toCommonJS(cli_exports);
382
382
  init_cjs_shims();
383
- var import_node_path37 = __toESM(require("path"), 1);
384
- var import_node_fs22 = require("fs");
383
+ var import_node_path39 = __toESM(require("path"), 1);
384
+ var import_node_fs25 = require("fs");
385
385
 
386
386
  // src/graph.ts
387
387
  init_cjs_shims();
@@ -887,19 +887,19 @@ function confidenceFromMix(edges, now = Date.now()) {
887
887
  function longestIncomingWalk(graph, start, maxDepth) {
888
888
  let best = { path: [start], edges: [] };
889
889
  const visited = /* @__PURE__ */ new Set([start]);
890
- function step(node, path38, edges) {
891
- if (path38.length > best.path.length) {
892
- best = { path: [...path38], edges: [...edges] };
890
+ function step(node, path40, edges) {
891
+ if (path40.length > best.path.length) {
892
+ best = { path: [...path40], edges: [...edges] };
893
893
  }
894
- if (path38.length - 1 >= maxDepth) return;
894
+ if (path40.length - 1 >= maxDepth) return;
895
895
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
896
896
  for (const [srcId, edge] of incoming) {
897
897
  if (visited.has(srcId)) continue;
898
898
  visited.add(srcId);
899
- path38.push(srcId);
899
+ path40.push(srcId);
900
900
  edges.push(edge);
901
- step(srcId, path38, edges);
902
- path38.pop();
901
+ step(srcId, path40, edges);
902
+ path40.pop();
903
903
  edges.pop();
904
904
  visited.delete(srcId);
905
905
  }
@@ -1936,8 +1936,8 @@ async function readErrorEvents(errorsPath) {
1936
1936
 
1937
1937
  // src/extract/services.ts
1938
1938
  init_cjs_shims();
1939
- var import_node_fs7 = require("fs");
1940
- var import_node_path7 = __toESM(require("path"), 1);
1939
+ var import_node_fs8 = require("fs");
1940
+ var import_node_path8 = __toESM(require("path"), 1);
1941
1941
  var import_ignore = __toESM(require("ignore"), 1);
1942
1942
  var import_minimatch2 = require("minimatch");
1943
1943
  var import_types5 = require("@neat.is/types");
@@ -1961,9 +1961,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1961
1961
  function isConfigFile(name) {
1962
1962
  const ext = import_node_path4.default.extname(name);
1963
1963
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
1964
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
1964
+ if (name === ".env" || name.startsWith(".env.")) {
1965
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1966
+ return { match: true, fileType: "env" };
1967
+ }
1965
1968
  return { match: false, fileType: "" };
1966
1969
  }
1970
+ function isTestPath(filePath) {
1971
+ const normalised = filePath.replace(/\\/g, "/");
1972
+ const segments = normalised.split("/");
1973
+ for (const seg of segments) {
1974
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
1975
+ return true;
1976
+ }
1977
+ }
1978
+ const base = segments[segments.length - 1] ?? "";
1979
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
1980
+ }
1981
+ function isEnvTemplateFile(name) {
1982
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
1983
+ return true;
1984
+ }
1985
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
1986
+ }
1987
+ function maskCommentsInSource(src) {
1988
+ const len = src.length;
1989
+ const out = new Array(len);
1990
+ let i = 0;
1991
+ let inString = 0;
1992
+ let escaped = false;
1993
+ while (i < len) {
1994
+ const c = src[i];
1995
+ if (inString !== 0) {
1996
+ out[i] = c;
1997
+ if (escaped) {
1998
+ escaped = false;
1999
+ } else if (c === "\\") {
2000
+ escaped = true;
2001
+ } else if (c === inString) {
2002
+ inString = 0;
2003
+ }
2004
+ i++;
2005
+ continue;
2006
+ }
2007
+ if (c === "/" && i + 1 < len) {
2008
+ const next = src[i + 1];
2009
+ if (next === "/") {
2010
+ out[i] = " ";
2011
+ out[i + 1] = " ";
2012
+ let j = i + 2;
2013
+ while (j < len && src[j] !== "\n") {
2014
+ out[j] = " ";
2015
+ j++;
2016
+ }
2017
+ i = j;
2018
+ continue;
2019
+ }
2020
+ if (next === "*") {
2021
+ out[i] = " ";
2022
+ out[i + 1] = " ";
2023
+ let j = i + 2;
2024
+ while (j < len) {
2025
+ if (src[j] === "\n") {
2026
+ out[j] = "\n";
2027
+ j++;
2028
+ continue;
2029
+ }
2030
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
2031
+ out[j] = " ";
2032
+ out[j + 1] = " ";
2033
+ j += 2;
2034
+ break;
2035
+ }
2036
+ out[j] = " ";
2037
+ j++;
2038
+ }
2039
+ i = j;
2040
+ continue;
2041
+ }
2042
+ }
2043
+ out[i] = c;
2044
+ if (c === "'" || c === '"' || c === "`") inString = c;
2045
+ i++;
2046
+ }
2047
+ return out.join("");
2048
+ }
2049
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
2050
+ function urlMatchesHost(urlString, host) {
2051
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
2052
+ if (!URL_LIKE.test(urlString)) return false;
2053
+ const [wantedHost, wantedPort] = host.split(":");
2054
+ let parsed;
2055
+ try {
2056
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
2057
+ parsed = new URL(candidate);
2058
+ } catch {
2059
+ return false;
2060
+ }
2061
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
2062
+ if (wantedPort && parsed.port !== wantedPort) return false;
2063
+ return true;
2064
+ }
1967
2065
  function cleanVersion(raw) {
1968
2066
  if (!raw) return void 0;
1969
2067
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -2119,6 +2217,41 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2119
2217
  return author ?? void 0;
2120
2218
  }
2121
2219
 
2220
+ // src/extract/errors.ts
2221
+ init_cjs_shims();
2222
+ var import_node_fs7 = require("fs");
2223
+ var import_node_path7 = __toESM(require("path"), 1);
2224
+ var sink = [];
2225
+ function recordExtractionError(producer, file, err) {
2226
+ const e = err instanceof Error ? err : new Error(String(err));
2227
+ sink.push({
2228
+ producer,
2229
+ file,
2230
+ error: e.message,
2231
+ stack: e.stack,
2232
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2233
+ source: "extract"
2234
+ });
2235
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2236
+ }
2237
+ function drainExtractionErrors() {
2238
+ return sink.splice(0, sink.length);
2239
+ }
2240
+ async function writeExtractionErrors(errors, errorsPath) {
2241
+ if (errors.length === 0) return;
2242
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2243
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2244
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2245
+ }
2246
+ function isStrictExtractionEnabled() {
2247
+ const raw = process.env.NEAT_STRICT_EXTRACTION;
2248
+ return raw === "1" || raw === "true";
2249
+ }
2250
+ function formatExtractionBanner(count) {
2251
+ if (count === 1) return `[neat] 1 file skipped due to parse errors`;
2252
+ return `[neat] ${count} files skipped due to parse errors`;
2253
+ }
2254
+
2122
2255
  // src/extract/services.ts
2123
2256
  var DEFAULT_SCAN_DEPTH = 5;
2124
2257
  function parseScanDepth() {
@@ -2135,21 +2268,21 @@ function workspaceGlobs(pkg) {
2135
2268
  return null;
2136
2269
  }
2137
2270
  async function loadGitignore(scanPath) {
2138
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2271
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2139
2272
  if (!await exists(gitignorePath)) return null;
2140
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2273
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
2141
2274
  return (0, import_ignore.default)().add(raw);
2142
2275
  }
2143
2276
  async function walkDirs(start, scanPath, options, visit) {
2144
2277
  async function recurse(current, depth) {
2145
2278
  if (depth > options.maxDepth) return;
2146
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2279
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2147
2280
  for (const entry2 of entries) {
2148
2281
  if (!entry2.isDirectory()) continue;
2149
2282
  if (IGNORED_DIRS.has(entry2.name)) continue;
2150
- const child = import_node_path7.default.join(current, entry2.name);
2283
+ const child = import_node_path8.default.join(current, entry2.name);
2151
2284
  if (options.ig) {
2152
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2285
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2153
2286
  if (rel && options.ig.ignores(rel + "/")) continue;
2154
2287
  }
2155
2288
  await visit(child);
@@ -2164,8 +2297,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2164
2297
  for (const raw of globs) {
2165
2298
  const pattern = raw.replace(/^\.\//, "");
2166
2299
  if (!pattern.includes("*")) {
2167
- const candidate = import_node_path7.default.join(scanPath, pattern);
2168
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2300
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2301
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2169
2302
  continue;
2170
2303
  }
2171
2304
  const segments = pattern.split("/");
@@ -2174,13 +2307,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2174
2307
  if (seg.includes("*")) break;
2175
2308
  staticSegments.push(seg);
2176
2309
  }
2177
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2310
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2178
2311
  if (!await exists(start)) continue;
2179
2312
  const hasDoubleStar = pattern.includes("**");
2180
2313
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2181
2314
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2182
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2183
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2315
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2316
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2184
2317
  found.add(dir);
2185
2318
  }
2186
2319
  });
@@ -2188,15 +2321,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2188
2321
  return [...found];
2189
2322
  }
2190
2323
  async function discoverNodeService(scanPath, dir) {
2191
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2324
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2192
2325
  if (!await exists(pkgPath)) return null;
2193
2326
  let pkg;
2194
2327
  try {
2195
2328
  pkg = await readJson(pkgPath);
2196
2329
  } catch (err) {
2197
- console.warn(
2198
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2199
- );
2330
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2200
2331
  return null;
2201
2332
  }
2202
2333
  if (!pkg.name) return null;
@@ -2207,7 +2338,7 @@ async function discoverNodeService(scanPath, dir) {
2207
2338
  language: "javascript",
2208
2339
  version: pkg.version,
2209
2340
  dependencies: pkg.dependencies ?? {},
2210
- repoPath: import_node_path7.default.relative(scanPath, dir),
2341
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2211
2342
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2212
2343
  };
2213
2344
  return { pkg, dir, node };
@@ -2223,19 +2354,21 @@ async function discoverPyService(scanPath, dir) {
2223
2354
  language: "python",
2224
2355
  version: py.version,
2225
2356
  dependencies: py.dependencies,
2226
- repoPath: import_node_path7.default.relative(scanPath, dir)
2357
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2227
2358
  };
2228
2359
  return { pkg, dir, node };
2229
2360
  }
2230
2361
  async function discoverServices(scanPath) {
2231
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2362
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2232
2363
  let rootPkg = null;
2233
2364
  if (await exists(rootPkgPath)) {
2234
2365
  try {
2235
2366
  rootPkg = await readJson(rootPkgPath);
2236
2367
  } catch (err) {
2237
- console.warn(
2238
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2368
+ recordExtractionError(
2369
+ "services workspaces",
2370
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2371
+ err
2239
2372
  );
2240
2373
  }
2241
2374
  }
@@ -2251,9 +2384,9 @@ async function discoverServices(scanPath) {
2251
2384
  scanPath,
2252
2385
  { maxDepth: parseScanDepth(), ig },
2253
2386
  async (dir) => {
2254
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2387
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2255
2388
  candidateDirs.push(dir);
2256
- } 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"))) {
2389
+ } 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"))) {
2257
2390
  candidateDirs.push(dir);
2258
2391
  }
2259
2392
  }
@@ -2267,8 +2400,8 @@ async function discoverServices(scanPath) {
2267
2400
  if (!service) continue;
2268
2401
  const existingDir = seen.get(service.node.name);
2269
2402
  if (existingDir !== void 0) {
2270
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2271
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2403
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2404
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2272
2405
  console.warn(
2273
2406
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2274
2407
  );
@@ -2305,8 +2438,8 @@ function addServiceNodes(graph, services) {
2305
2438
 
2306
2439
  // src/extract/aliases.ts
2307
2440
  init_cjs_shims();
2308
- var import_node_path8 = __toESM(require("path"), 1);
2309
- var import_node_fs8 = require("fs");
2441
+ var import_node_path9 = __toESM(require("path"), 1);
2442
+ var import_node_fs9 = require("fs");
2310
2443
  var import_yaml2 = require("yaml");
2311
2444
  var import_types6 = require("@neat.is/types");
2312
2445
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2333,14 +2466,14 @@ function indexServicesByName(services) {
2333
2466
  const map = /* @__PURE__ */ new Map();
2334
2467
  for (const s of services) {
2335
2468
  map.set(s.node.name, s.node.id);
2336
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2469
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2337
2470
  }
2338
2471
  return map;
2339
2472
  }
2340
2473
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2341
2474
  let composePath = null;
2342
2475
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2343
- const abs = import_node_path8.default.join(scanPath, name);
2476
+ const abs = import_node_path9.default.join(scanPath, name);
2344
2477
  if (await exists(abs)) {
2345
2478
  composePath = abs;
2346
2479
  break;
@@ -2351,8 +2484,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2351
2484
  try {
2352
2485
  compose = await readYaml(composePath);
2353
2486
  } catch (err) {
2354
- console.warn(
2355
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2487
+ recordExtractionError(
2488
+ "aliases compose",
2489
+ import_node_path9.default.relative(scanPath, composePath),
2490
+ err
2356
2491
  );
2357
2492
  return;
2358
2493
  }
@@ -2394,15 +2529,13 @@ function parseDockerfileLabels(content) {
2394
2529
  }
2395
2530
  async function collectDockerfileAliases(graph, services) {
2396
2531
  for (const service of services) {
2397
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2532
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2398
2533
  if (!await exists(dockerfilePath)) continue;
2399
2534
  let content;
2400
2535
  try {
2401
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2536
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2402
2537
  } catch (err) {
2403
- console.warn(
2404
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2405
- );
2538
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2406
2539
  continue;
2407
2540
  }
2408
2541
  const aliases = parseDockerfileLabels(content);
@@ -2412,13 +2545,13 @@ async function collectDockerfileAliases(graph, services) {
2412
2545
  async function walkYamlFiles(start, depth = 0, max = 5) {
2413
2546
  if (depth > max) return [];
2414
2547
  const out = [];
2415
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2548
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2416
2549
  for (const entry2 of entries) {
2417
2550
  if (entry2.isDirectory()) {
2418
2551
  if (IGNORED_DIRS.has(entry2.name)) continue;
2419
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
2420
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
2421
- out.push(import_node_path8.default.join(start, entry2.name));
2552
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
2553
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
2554
+ out.push(import_node_path9.default.join(start, entry2.name));
2422
2555
  }
2423
2556
  }
2424
2557
  return out;
@@ -2445,7 +2578,7 @@ function k8sServiceTarget(doc, byName) {
2445
2578
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2446
2579
  const files = await walkYamlFiles(scanPath);
2447
2580
  for (const file of files) {
2448
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2581
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2449
2582
  let docs;
2450
2583
  try {
2451
2584
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2470,14 +2603,14 @@ async function addServiceAliases(graph, scanPath, services) {
2470
2603
 
2471
2604
  // src/extract/databases/index.ts
2472
2605
  init_cjs_shims();
2473
- var import_node_path16 = __toESM(require("path"), 1);
2606
+ var import_node_path17 = __toESM(require("path"), 1);
2474
2607
  var import_types7 = require("@neat.is/types");
2475
2608
 
2476
2609
  // src/extract/databases/db-config-yaml.ts
2477
2610
  init_cjs_shims();
2478
- var import_node_path9 = __toESM(require("path"), 1);
2611
+ var import_node_path10 = __toESM(require("path"), 1);
2479
2612
  async function parse(serviceDir) {
2480
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2613
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2481
2614
  if (!await exists(yamlPath)) return [];
2482
2615
  const raw = await readYaml(yamlPath);
2483
2616
  return [
@@ -2495,13 +2628,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2495
2628
 
2496
2629
  // src/extract/databases/dotenv.ts
2497
2630
  init_cjs_shims();
2498
- var import_node_fs10 = require("fs");
2499
- var import_node_path11 = __toESM(require("path"), 1);
2631
+ var import_node_fs11 = require("fs");
2632
+ var import_node_path12 = __toESM(require("path"), 1);
2500
2633
 
2501
2634
  // src/extract/databases/shared.ts
2502
2635
  init_cjs_shims();
2503
- var import_node_fs9 = require("fs");
2504
- var import_node_path10 = __toESM(require("path"), 1);
2636
+ var import_node_fs10 = require("fs");
2637
+ var import_node_path11 = __toESM(require("path"), 1);
2505
2638
  function schemeToEngine(scheme) {
2506
2639
  const s = scheme.toLowerCase().split("+")[0];
2507
2640
  switch (s) {
@@ -2540,14 +2673,14 @@ function parseConnectionString(url) {
2540
2673
  }
2541
2674
  async function readIfExists(filePath) {
2542
2675
  try {
2543
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2676
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2544
2677
  } catch {
2545
2678
  return null;
2546
2679
  }
2547
2680
  }
2548
2681
  async function findFirst(serviceDir, candidates) {
2549
2682
  for (const rel of candidates) {
2550
- const abs = import_node_path10.default.join(serviceDir, rel);
2683
+ const abs = import_node_path11.default.join(serviceDir, rel);
2551
2684
  const content = await readIfExists(abs);
2552
2685
  if (content !== null) return abs;
2553
2686
  }
@@ -2598,15 +2731,15 @@ function parseDotenvLine(line) {
2598
2731
  return { key, value };
2599
2732
  }
2600
2733
  async function parse2(serviceDir) {
2601
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2734
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2602
2735
  const configs = [];
2603
2736
  const seen = /* @__PURE__ */ new Set();
2604
2737
  for (const entry2 of entries) {
2605
2738
  if (!entry2.isFile()) continue;
2606
2739
  const match = isConfigFile(entry2.name);
2607
2740
  if (!match.match || match.fileType !== "env") continue;
2608
- const filePath = import_node_path11.default.join(serviceDir, entry2.name);
2609
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2741
+ const filePath = import_node_path12.default.join(serviceDir, entry2.name);
2742
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2610
2743
  for (const line of content.split("\n")) {
2611
2744
  const parsed = parseDotenvLine(line);
2612
2745
  if (!parsed) continue;
@@ -2625,9 +2758,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2625
2758
 
2626
2759
  // src/extract/databases/prisma.ts
2627
2760
  init_cjs_shims();
2628
- var import_node_path12 = __toESM(require("path"), 1);
2761
+ var import_node_path13 = __toESM(require("path"), 1);
2629
2762
  async function parse3(serviceDir) {
2630
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2763
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2631
2764
  const content = await readIfExists(schemaPath);
2632
2765
  if (!content) return [];
2633
2766
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2759,10 +2892,10 @@ var knexParser = { name: "knex", parse: parse5 };
2759
2892
 
2760
2893
  // src/extract/databases/ormconfig.ts
2761
2894
  init_cjs_shims();
2762
- var import_node_path13 = __toESM(require("path"), 1);
2895
+ var import_node_path14 = __toESM(require("path"), 1);
2763
2896
  async function parse6(serviceDir) {
2764
2897
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2765
- const abs = import_node_path13.default.join(serviceDir, candidate);
2898
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2766
2899
  if (!await exists(abs)) continue;
2767
2900
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2768
2901
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2822,9 +2955,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2822
2955
 
2823
2956
  // src/extract/databases/sequelize.ts
2824
2957
  init_cjs_shims();
2825
- var import_node_path14 = __toESM(require("path"), 1);
2958
+ var import_node_path15 = __toESM(require("path"), 1);
2826
2959
  async function parse8(serviceDir) {
2827
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2960
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2828
2961
  if (!await exists(configPath)) return [];
2829
2962
  const raw = await readJson(configPath);
2830
2963
  const out = [];
@@ -2851,7 +2984,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2851
2984
 
2852
2985
  // src/extract/databases/docker-compose.ts
2853
2986
  init_cjs_shims();
2854
- var import_node_path15 = __toESM(require("path"), 1);
2987
+ var import_node_path16 = __toESM(require("path"), 1);
2855
2988
  function portFromService(svc) {
2856
2989
  for (const raw of svc.ports ?? []) {
2857
2990
  const str = String(raw);
@@ -2878,7 +3011,7 @@ function databaseFromEnv(svc) {
2878
3011
  }
2879
3012
  async function parse9(serviceDir) {
2880
3013
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2881
- const abs = import_node_path15.default.join(serviceDir, name);
3014
+ const abs = import_node_path16.default.join(serviceDir, name);
2882
3015
  if (!await exists(abs)) continue;
2883
3016
  const raw = await readYaml(abs);
2884
3017
  if (!raw?.services) return [];
@@ -3056,11 +3189,12 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3056
3189
  target: dbNode.id,
3057
3190
  type: import_types7.EdgeType.CONNECTS_TO,
3058
3191
  provenance: import_types7.Provenance.EXTRACTED,
3059
- ...config.sourceFile ? {
3060
- evidence: {
3061
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
3062
- }
3063
- } : {}
3192
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3193
+ // Ghost-edge cleanup keys retirement on this; the conditional
3194
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3195
+ evidence: {
3196
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3197
+ }
3064
3198
  };
3065
3199
  if (!graph.hasEdge(edge.id)) {
3066
3200
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3086,15 +3220,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3086
3220
 
3087
3221
  // src/extract/configs.ts
3088
3222
  init_cjs_shims();
3089
- var import_node_fs11 = require("fs");
3090
- var import_node_path17 = __toESM(require("path"), 1);
3223
+ var import_node_fs12 = require("fs");
3224
+ var import_node_path18 = __toESM(require("path"), 1);
3091
3225
  var import_types8 = require("@neat.is/types");
3092
3226
  async function walkConfigFiles(dir) {
3093
3227
  const out = [];
3094
3228
  async function walk(current) {
3095
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3229
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3096
3230
  for (const entry2 of entries) {
3097
- const full = import_node_path17.default.join(current, entry2.name);
3231
+ const full = import_node_path18.default.join(current, entry2.name);
3098
3232
  if (entry2.isDirectory()) {
3099
3233
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3100
3234
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -3111,13 +3245,13 @@ async function addConfigNodes(graph, services, scanPath) {
3111
3245
  for (const service of services) {
3112
3246
  const configFiles = await walkConfigFiles(service.dir);
3113
3247
  for (const file of configFiles) {
3114
- const relPath = import_node_path17.default.relative(scanPath, file);
3248
+ const relPath = import_node_path18.default.relative(scanPath, file);
3115
3249
  const node = {
3116
3250
  id: (0, import_types8.configId)(relPath),
3117
3251
  type: import_types8.NodeType.ConfigNode,
3118
- name: import_node_path17.default.basename(file),
3252
+ name: import_node_path18.default.basename(file),
3119
3253
  path: relPath,
3120
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3254
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3121
3255
  };
3122
3256
  if (!graph.hasNode(node.id)) {
3123
3257
  graph.addNode(node.id, node);
@@ -3129,7 +3263,7 @@ async function addConfigNodes(graph, services, scanPath) {
3129
3263
  target: node.id,
3130
3264
  type: import_types8.EdgeType.CONFIGURED_BY,
3131
3265
  provenance: import_types8.Provenance.EXTRACTED,
3132
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3266
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3133
3267
  };
3134
3268
  if (!graph.hasEdge(edge.id)) {
3135
3269
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3146,7 +3280,7 @@ var import_types14 = require("@neat.is/types");
3146
3280
 
3147
3281
  // src/extract/calls/http.ts
3148
3282
  init_cjs_shims();
3149
- var import_node_path19 = __toESM(require("path"), 1);
3283
+ var import_node_path20 = __toESM(require("path"), 1);
3150
3284
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3151
3285
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3152
3286
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3154,17 +3288,17 @@ var import_types9 = require("@neat.is/types");
3154
3288
 
3155
3289
  // src/extract/calls/shared.ts
3156
3290
  init_cjs_shims();
3157
- var import_node_fs12 = require("fs");
3158
- var import_node_path18 = __toESM(require("path"), 1);
3291
+ var import_node_fs13 = require("fs");
3292
+ var import_node_path19 = __toESM(require("path"), 1);
3159
3293
  async function walkSourceFiles(dir) {
3160
3294
  const out = [];
3161
3295
  async function walk(current) {
3162
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3296
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3163
3297
  for (const entry2 of entries) {
3164
- const full = import_node_path18.default.join(current, entry2.name);
3298
+ const full = import_node_path19.default.join(current, entry2.name);
3165
3299
  if (entry2.isDirectory()) {
3166
3300
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3167
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
3301
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3168
3302
  out.push(full);
3169
3303
  }
3170
3304
  }
@@ -3177,7 +3311,7 @@ async function loadSourceFiles(dir) {
3177
3311
  const out = [];
3178
3312
  for (const p of paths) {
3179
3313
  try {
3180
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3314
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3181
3315
  out.push({ path: p, content });
3182
3316
  } catch {
3183
3317
  }
@@ -3196,8 +3330,27 @@ function snippet(text, line) {
3196
3330
 
3197
3331
  // src/extract/calls/http.ts
3198
3332
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3333
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3334
+ function isInsideJsxExternalLink(node) {
3335
+ let cursor = node.parent;
3336
+ while (cursor) {
3337
+ if (cursor.type === "jsx_attribute") {
3338
+ let owner = cursor.parent;
3339
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3340
+ owner = owner.parent;
3341
+ }
3342
+ if (!owner) return false;
3343
+ const tagNode = owner.namedChild(0);
3344
+ const tagName = tagNode?.text ?? "";
3345
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3346
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3347
+ }
3348
+ cursor = cursor.parent;
3349
+ }
3350
+ return false;
3351
+ }
3199
3352
  function collectStringLiterals(node, out) {
3200
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3353
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3201
3354
  for (let i = 0; i < node.namedChildCount; i++) {
3202
3355
  const child = node.namedChild(i);
3203
3356
  if (child) collectStringLiterals(child, out);
@@ -3209,8 +3362,9 @@ function callsFromSource(source, parser, knownHosts) {
3209
3362
  collectStringLiterals(tree.rootNode, literals);
3210
3363
  const targets = /* @__PURE__ */ new Set();
3211
3364
  for (const lit of literals) {
3365
+ if (isInsideJsxExternalLink(lit.node)) continue;
3212
3366
  for (const host of knownHosts) {
3213
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3367
+ if (urlMatchesHost(lit.text, host)) {
3214
3368
  targets.add(host);
3215
3369
  }
3216
3370
  }
@@ -3233,9 +3387,9 @@ async function addHttpCallEdges(graph, services) {
3233
3387
  const knownHosts = /* @__PURE__ */ new Set();
3234
3388
  const hostToNodeId = /* @__PURE__ */ new Map();
3235
3389
  for (const service of services) {
3236
- knownHosts.add(import_node_path19.default.basename(service.dir));
3390
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3237
3391
  knownHosts.add(service.pkg.name);
3238
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3392
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3239
3393
  hostToNodeId.set(service.pkg.name, service.node.id);
3240
3394
  }
3241
3395
  let edgesAdded = 0;
@@ -3243,14 +3397,13 @@ async function addHttpCallEdges(graph, services) {
3243
3397
  const files = await loadSourceFiles(service.dir);
3244
3398
  const seenTargets = /* @__PURE__ */ new Map();
3245
3399
  for (const file of files) {
3246
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3400
+ if (isTestPath(file.path)) continue;
3401
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3247
3402
  let targets;
3248
3403
  try {
3249
3404
  targets = callsFromSource(file.content, parser, knownHosts);
3250
3405
  } catch (err) {
3251
- console.warn(
3252
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3253
- );
3406
+ recordExtractionError("http call extraction", file.path, err);
3254
3407
  continue;
3255
3408
  }
3256
3409
  for (const t of targets) {
@@ -3271,7 +3424,7 @@ async function addHttpCallEdges(graph, services) {
3271
3424
  type: import_types9.EdgeType.CALLS,
3272
3425
  provenance: import_types9.Provenance.EXTRACTED,
3273
3426
  evidence: {
3274
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3427
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3275
3428
  line,
3276
3429
  snippet: snippet(fileContent, line)
3277
3430
  }
@@ -3287,7 +3440,7 @@ async function addHttpCallEdges(graph, services) {
3287
3440
 
3288
3441
  // src/extract/calls/kafka.ts
3289
3442
  init_cjs_shims();
3290
- var import_node_path20 = __toESM(require("path"), 1);
3443
+ var import_node_path21 = __toESM(require("path"), 1);
3291
3444
  var import_types10 = require("@neat.is/types");
3292
3445
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3293
3446
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3314,7 +3467,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3314
3467
  kind: "kafka-topic",
3315
3468
  edgeType,
3316
3469
  evidence: {
3317
- file: import_node_path20.default.relative(serviceDir, file.path),
3470
+ file: import_node_path21.default.relative(serviceDir, file.path),
3318
3471
  line,
3319
3472
  snippet: snippet(file.content, line)
3320
3473
  }
@@ -3327,7 +3480,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3327
3480
 
3328
3481
  // src/extract/calls/redis.ts
3329
3482
  init_cjs_shims();
3330
- var import_node_path21 = __toESM(require("path"), 1);
3483
+ var import_node_path22 = __toESM(require("path"), 1);
3331
3484
  var import_types11 = require("@neat.is/types");
3332
3485
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3333
3486
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3346,7 +3499,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3346
3499
  kind: "redis",
3347
3500
  edgeType: "CALLS",
3348
3501
  evidence: {
3349
- file: import_node_path21.default.relative(serviceDir, file.path),
3502
+ file: import_node_path22.default.relative(serviceDir, file.path),
3350
3503
  line,
3351
3504
  snippet: snippet(file.content, line)
3352
3505
  }
@@ -3357,7 +3510,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3357
3510
 
3358
3511
  // src/extract/calls/aws.ts
3359
3512
  init_cjs_shims();
3360
- var import_node_path22 = __toESM(require("path"), 1);
3513
+ var import_node_path23 = __toESM(require("path"), 1);
3361
3514
  var import_types12 = require("@neat.is/types");
3362
3515
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3363
3516
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3387,7 +3540,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3387
3540
  kind,
3388
3541
  edgeType: "CALLS",
3389
3542
  evidence: {
3390
- file: import_node_path22.default.relative(serviceDir, file.path),
3543
+ file: import_node_path23.default.relative(serviceDir, file.path),
3391
3544
  line,
3392
3545
  snippet: snippet(file.content, line)
3393
3546
  }
@@ -3412,16 +3565,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3412
3565
 
3413
3566
  // src/extract/calls/grpc.ts
3414
3567
  init_cjs_shims();
3415
- var import_node_path23 = __toESM(require("path"), 1);
3568
+ var import_node_path24 = __toESM(require("path"), 1);
3416
3569
  var import_types13 = require("@neat.is/types");
3417
3570
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3571
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3572
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3418
3573
  function isLikelyAddress(value) {
3419
3574
  if (!value) return false;
3420
3575
  return /:\d{2,5}$/.test(value) || value.includes(".");
3421
3576
  }
3577
+ function normaliseForMatch(s) {
3578
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3579
+ }
3580
+ function readImports(content) {
3581
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3582
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3583
+ let m;
3584
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3585
+ const raw = m[1];
3586
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3587
+ }
3588
+ return {
3589
+ awsSdkSuffixes,
3590
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3591
+ };
3592
+ }
3593
+ function classifyClient(symbol, ctx) {
3594
+ const key = normaliseForMatch(symbol);
3595
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3596
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3597
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3598
+ return null;
3599
+ }
3422
3600
  function grpcEndpointsFromFile(file, serviceDir) {
3423
3601
  const out = [];
3424
3602
  const seen = /* @__PURE__ */ new Set();
3603
+ const ctx = readImports(file.content);
3425
3604
  GRPC_CLIENT_RE.lastIndex = 0;
3426
3605
  let m;
3427
3606
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3429,15 +3608,18 @@ function grpcEndpointsFromFile(file, serviceDir) {
3429
3608
  const addr = m[2]?.trim();
3430
3609
  const name = isLikelyAddress(addr) ? addr : symbol;
3431
3610
  if (seen.has(name)) continue;
3611
+ const classified = classifyClient(symbol, ctx);
3612
+ if (!classified) continue;
3432
3613
  seen.add(name);
3614
+ const { kind } = classified;
3433
3615
  const line = lineOf(file.content, m[0]);
3434
3616
  out.push({
3435
- infraId: (0, import_types13.infraId)("grpc-service", name),
3617
+ infraId: (0, import_types13.infraId)(kind, name),
3436
3618
  name,
3437
- kind: "grpc-service",
3619
+ kind,
3438
3620
  edgeType: "CALLS",
3439
3621
  evidence: {
3440
- file: import_node_path23.default.relative(serviceDir, file.path),
3622
+ file: import_node_path24.default.relative(serviceDir, file.path),
3441
3623
  line,
3442
3624
  snippet: snippet(file.content, line)
3443
3625
  }
@@ -3457,6 +3639,9 @@ function edgeTypeFromEndpoint(ep) {
3457
3639
  return import_types14.EdgeType.CALLS;
3458
3640
  }
3459
3641
  }
3642
+ function isAwsKind(kind) {
3643
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3644
+ }
3460
3645
  async function addExternalEndpointEdges(graph, services) {
3461
3646
  let nodesAdded = 0;
3462
3647
  let edgesAdded = 0;
@@ -3464,10 +3649,13 @@ async function addExternalEndpointEdges(graph, services) {
3464
3649
  const files = await loadSourceFiles(service.dir);
3465
3650
  const endpoints = [];
3466
3651
  for (const file of files) {
3467
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3468
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3469
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3470
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3652
+ if (isTestPath(file.path)) continue;
3653
+ const masked = maskCommentsInSource(file.content);
3654
+ const maskedFile = { path: file.path, content: masked };
3655
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3656
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3657
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3658
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3471
3659
  }
3472
3660
  if (endpoints.length === 0) continue;
3473
3661
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3477,7 +3665,10 @@ async function addExternalEndpointEdges(graph, services) {
3477
3665
  id: ep.infraId,
3478
3666
  type: import_types14.NodeType.InfraNode,
3479
3667
  name: ep.name,
3480
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3668
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3669
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3670
+ // bucket / table kinds from aws.ts.
3671
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3481
3672
  kind: ep.kind
3482
3673
  };
3483
3674
  graph.addNode(node.id, node);
@@ -3517,7 +3708,7 @@ init_cjs_shims();
3517
3708
 
3518
3709
  // src/extract/infra/docker-compose.ts
3519
3710
  init_cjs_shims();
3520
- var import_node_path24 = __toESM(require("path"), 1);
3711
+ var import_node_path25 = __toESM(require("path"), 1);
3521
3712
  var import_types16 = require("@neat.is/types");
3522
3713
 
3523
3714
  // src/extract/infra/shared.ts
@@ -3555,7 +3746,7 @@ function dependsOnList(value) {
3555
3746
  }
3556
3747
  function serviceNameToServiceNode(name, services) {
3557
3748
  for (const s of services) {
3558
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3749
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3559
3750
  }
3560
3751
  return null;
3561
3752
  }
@@ -3564,7 +3755,7 @@ async function addComposeInfra(graph, scanPath, services) {
3564
3755
  let edgesAdded = 0;
3565
3756
  let composePath = null;
3566
3757
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3567
- const abs = import_node_path24.default.join(scanPath, name);
3758
+ const abs = import_node_path25.default.join(scanPath, name);
3568
3759
  if (await exists(abs)) {
3569
3760
  composePath = abs;
3570
3761
  break;
@@ -3575,13 +3766,15 @@ async function addComposeInfra(graph, scanPath, services) {
3575
3766
  try {
3576
3767
  compose = await readYaml(composePath);
3577
3768
  } catch (err) {
3578
- console.warn(
3579
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3769
+ recordExtractionError(
3770
+ "infra docker-compose",
3771
+ import_node_path25.default.relative(scanPath, composePath),
3772
+ err
3580
3773
  );
3581
3774
  return { nodesAdded, edgesAdded };
3582
3775
  }
3583
3776
  if (!compose?.services) return { nodesAdded, edgesAdded };
3584
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3777
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3585
3778
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3586
3779
  for (const [composeName, svc] of Object.entries(compose.services)) {
3587
3780
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3622,8 +3815,8 @@ async function addComposeInfra(graph, scanPath, services) {
3622
3815
 
3623
3816
  // src/extract/infra/dockerfile.ts
3624
3817
  init_cjs_shims();
3625
- var import_node_path25 = __toESM(require("path"), 1);
3626
- var import_node_fs13 = require("fs");
3818
+ var import_node_path26 = __toESM(require("path"), 1);
3819
+ var import_node_fs14 = require("fs");
3627
3820
  var import_types17 = require("@neat.is/types");
3628
3821
  function runtimeImage(content) {
3629
3822
  const lines = content.split("\n");
@@ -3643,14 +3836,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3643
3836
  let nodesAdded = 0;
3644
3837
  let edgesAdded = 0;
3645
3838
  for (const service of services) {
3646
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3839
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3647
3840
  if (!await exists(dockerfilePath)) continue;
3648
3841
  let content;
3649
3842
  try {
3650
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3843
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3651
3844
  } catch (err) {
3652
- console.warn(
3653
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3845
+ recordExtractionError(
3846
+ "infra dockerfile",
3847
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3848
+ err
3654
3849
  );
3655
3850
  continue;
3656
3851
  }
@@ -3670,7 +3865,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3670
3865
  type: import_types17.EdgeType.RUNS_ON,
3671
3866
  provenance: import_types17.Provenance.EXTRACTED,
3672
3867
  evidence: {
3673
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3868
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3674
3869
  }
3675
3870
  };
3676
3871
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3682,19 +3877,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3682
3877
 
3683
3878
  // src/extract/infra/terraform.ts
3684
3879
  init_cjs_shims();
3685
- var import_node_fs14 = require("fs");
3686
- var import_node_path26 = __toESM(require("path"), 1);
3880
+ var import_node_fs15 = require("fs");
3881
+ var import_node_path27 = __toESM(require("path"), 1);
3687
3882
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3688
3883
  async function walkTfFiles(start, depth = 0, max = 5) {
3689
3884
  if (depth > max) return [];
3690
3885
  const out = [];
3691
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3886
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3692
3887
  for (const entry2 of entries) {
3693
3888
  if (entry2.isDirectory()) {
3694
3889
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
3695
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
3890
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3696
3891
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
3697
- out.push(import_node_path26.default.join(start, entry2.name));
3892
+ out.push(import_node_path27.default.join(start, entry2.name));
3698
3893
  }
3699
3894
  }
3700
3895
  return out;
@@ -3703,7 +3898,7 @@ async function addTerraformResources(graph, scanPath) {
3703
3898
  let nodesAdded = 0;
3704
3899
  const files = await walkTfFiles(scanPath);
3705
3900
  for (const file of files) {
3706
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3901
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3707
3902
  RESOURCE_RE.lastIndex = 0;
3708
3903
  let m;
3709
3904
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3721,8 +3916,8 @@ async function addTerraformResources(graph, scanPath) {
3721
3916
 
3722
3917
  // src/extract/infra/k8s.ts
3723
3918
  init_cjs_shims();
3724
- var import_node_fs15 = require("fs");
3725
- var import_node_path27 = __toESM(require("path"), 1);
3919
+ var import_node_fs16 = require("fs");
3920
+ var import_node_path28 = __toESM(require("path"), 1);
3726
3921
  var import_yaml3 = require("yaml");
3727
3922
  var K8S_KIND_TO_INFRA_KIND = {
3728
3923
  Service: "k8s-service",
@@ -3736,13 +3931,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3736
3931
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3737
3932
  if (depth > max) return [];
3738
3933
  const out = [];
3739
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3934
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3740
3935
  for (const entry2 of entries) {
3741
3936
  if (entry2.isDirectory()) {
3742
3937
  if (IGNORED_DIRS.has(entry2.name)) continue;
3743
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3744
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
3745
- out.push(import_node_path27.default.join(start, entry2.name));
3938
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
3939
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
3940
+ out.push(import_node_path28.default.join(start, entry2.name));
3746
3941
  }
3747
3942
  }
3748
3943
  return out;
@@ -3751,7 +3946,7 @@ async function addK8sResources(graph, scanPath) {
3751
3946
  let nodesAdded = 0;
3752
3947
  const files = await walkYamlFiles2(scanPath);
3753
3948
  for (const file of files) {
3754
- const content = await import_node_fs15.promises.readFile(file, "utf8");
3949
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3755
3950
  let docs;
3756
3951
  try {
3757
3952
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3785,9 +3980,46 @@ async function addInfra(graph, scanPath, services) {
3785
3980
  };
3786
3981
  }
3787
3982
 
3983
+ // src/extract/retire.ts
3984
+ init_cjs_shims();
3985
+ var import_node_fs17 = require("fs");
3986
+ var import_node_path29 = __toESM(require("path"), 1);
3987
+ var import_types18 = require("@neat.is/types");
3988
+ function retireEdgesByFile(graph, file) {
3989
+ const normalized = file.split("\\").join("/");
3990
+ const toDrop = [];
3991
+ graph.forEachEdge((id, attrs) => {
3992
+ const edge = attrs;
3993
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
3994
+ if (!edge.evidence?.file) return;
3995
+ if (edge.evidence.file === normalized) toDrop.push(id);
3996
+ });
3997
+ for (const id of toDrop) graph.dropEdge(id);
3998
+ return toDrop.length;
3999
+ }
4000
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4001
+ const toDrop = [];
4002
+ const bases = [scanPath, ...serviceDirs];
4003
+ graph.forEachEdge((id, attrs) => {
4004
+ const edge = attrs;
4005
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4006
+ const evidenceFile = edge.evidence?.file;
4007
+ if (!evidenceFile) return;
4008
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
4009
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4010
+ return;
4011
+ }
4012
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4013
+ if (!found) toDrop.push(id);
4014
+ });
4015
+ for (const id of toDrop) graph.dropEdge(id);
4016
+ return toDrop.length;
4017
+ }
4018
+
3788
4019
  // src/extract/index.ts
3789
4020
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3790
4021
  await ensureCompatLoaded();
4022
+ drainExtractionErrors();
3791
4023
  const services = await discoverServices(scanPath);
3792
4024
  const phase1Nodes = addServiceNodes(graph, services);
3793
4025
  await addServiceAliases(graph, scanPath, services);
@@ -3795,12 +4027,30 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3795
4027
  const phase3 = await addConfigNodes(graph, services, scanPath);
3796
4028
  const phase4 = await addCallEdges(graph, services);
3797
4029
  const phase5 = await addInfra(graph, scanPath, services);
4030
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
4031
+ graph,
4032
+ scanPath,
4033
+ services.map((s) => s.dir)
4034
+ );
3798
4035
  const frontiersPromoted = promoteFrontierNodes(graph);
3799
4036
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
4037
+ const errorEntries = drainExtractionErrors();
4038
+ if (opts.errorsPath && errorEntries.length > 0) {
4039
+ try {
4040
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
4041
+ } catch (err) {
4042
+ console.warn(
4043
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
4044
+ );
4045
+ }
4046
+ }
3800
4047
  const result = {
3801
4048
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3802
4049
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3803
- frontiersPromoted
4050
+ frontiersPromoted,
4051
+ extractionErrors: errorEntries.length,
4052
+ errorEntries,
4053
+ ghostsRetired
3804
4054
  };
3805
4055
  emitNeatEvent({
3806
4056
  type: "extraction-complete",
@@ -3817,8 +4067,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3817
4067
 
3818
4068
  // src/persist.ts
3819
4069
  init_cjs_shims();
3820
- var import_node_fs16 = require("fs");
3821
- var import_node_path28 = __toESM(require("path"), 1);
4070
+ var import_node_fs18 = require("fs");
4071
+ var import_node_path30 = __toESM(require("path"), 1);
3822
4072
  var SCHEMA_VERSION = 2;
3823
4073
  function migrateV1ToV2(payload) {
3824
4074
  const nodes = payload.graph.nodes;
@@ -3832,7 +4082,7 @@ function migrateV1ToV2(payload) {
3832
4082
  return { ...payload, schemaVersion: 2 };
3833
4083
  }
3834
4084
  async function ensureDir(filePath) {
3835
- await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
4085
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
3836
4086
  }
3837
4087
  async function saveGraphToDisk(graph, outPath) {
3838
4088
  await ensureDir(outPath);
@@ -3842,13 +4092,13 @@ async function saveGraphToDisk(graph, outPath) {
3842
4092
  graph: graph.export()
3843
4093
  };
3844
4094
  const tmp = `${outPath}.tmp`;
3845
- await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3846
- await import_node_fs16.promises.rename(tmp, outPath);
4095
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4096
+ await import_node_fs18.promises.rename(tmp, outPath);
3847
4097
  }
3848
4098
  async function loadGraphFromDisk(graph, outPath) {
3849
4099
  let raw;
3850
4100
  try {
3851
- raw = await import_node_fs16.promises.readFile(outPath, "utf8");
4101
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
3852
4102
  } catch (err) {
3853
4103
  if (err.code === "ENOENT") return;
3854
4104
  throw err;
@@ -3901,18 +4151,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3901
4151
 
3902
4152
  // src/watch.ts
3903
4153
  init_cjs_shims();
3904
- var import_node_path34 = __toESM(require("path"), 1);
4154
+ var import_node_fs22 = __toESM(require("fs"), 1);
4155
+ var import_node_path36 = __toESM(require("path"), 1);
3905
4156
  var import_chokidar = __toESM(require("chokidar"), 1);
3906
4157
 
3907
4158
  // src/api.ts
3908
4159
  init_cjs_shims();
3909
4160
  var import_fastify = __toESM(require("fastify"), 1);
3910
4161
  var import_cors = __toESM(require("@fastify/cors"), 1);
3911
- var import_types20 = require("@neat.is/types");
4162
+ var import_types21 = require("@neat.is/types");
3912
4163
 
3913
4164
  // src/divergences.ts
3914
4165
  init_cjs_shims();
3915
- var import_types18 = require("@neat.is/types");
4166
+ var import_types19 = require("@neat.is/types");
3916
4167
  function bucketKey(source, target, type) {
3917
4168
  return `${type}|${source}|${target}`;
3918
4169
  }
@@ -3920,25 +4171,25 @@ function bucketEdges(graph) {
3920
4171
  const buckets = /* @__PURE__ */ new Map();
3921
4172
  graph.forEachEdge((id, attrs) => {
3922
4173
  const e = attrs;
3923
- const parsed = (0, import_types18.parseEdgeId)(id);
4174
+ const parsed = (0, import_types19.parseEdgeId)(id);
3924
4175
  const provenance = parsed?.provenance ?? e.provenance;
3925
4176
  const key = bucketKey(e.source, e.target, e.type);
3926
4177
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3927
4178
  switch (provenance) {
3928
- case import_types18.Provenance.EXTRACTED:
4179
+ case import_types19.Provenance.EXTRACTED:
3929
4180
  cur.extracted = e;
3930
4181
  break;
3931
- case import_types18.Provenance.OBSERVED:
4182
+ case import_types19.Provenance.OBSERVED:
3932
4183
  cur.observed = e;
3933
4184
  break;
3934
- case import_types18.Provenance.INFERRED:
4185
+ case import_types19.Provenance.INFERRED:
3935
4186
  cur.inferred = e;
3936
4187
  break;
3937
- case import_types18.Provenance.FRONTIER:
4188
+ case import_types19.Provenance.FRONTIER:
3938
4189
  cur.frontier = e;
3939
4190
  break;
3940
4191
  default:
3941
- if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
4192
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
3942
4193
  }
3943
4194
  buckets.set(key, cur);
3944
4195
  });
@@ -3947,13 +4198,13 @@ function bucketEdges(graph) {
3947
4198
  function nodeIsFrontier(graph, nodeId) {
3948
4199
  if (!graph.hasNode(nodeId)) return false;
3949
4200
  const attrs = graph.getNodeAttributes(nodeId);
3950
- return attrs.type === import_types18.NodeType.FrontierNode;
4201
+ return attrs.type === import_types19.NodeType.FrontierNode;
3951
4202
  }
3952
4203
  function hasAnyObservedFromSource(graph, sourceId) {
3953
4204
  if (!graph.hasNode(sourceId)) return false;
3954
4205
  for (const edgeId of graph.outboundEdges(sourceId)) {
3955
4206
  const e = graph.getEdgeAttributes(edgeId);
3956
- if (e.provenance === import_types18.Provenance.OBSERVED) return true;
4207
+ if (e.provenance === import_types19.Provenance.OBSERVED) return true;
3957
4208
  }
3958
4209
  return false;
3959
4210
  }
@@ -4014,7 +4265,7 @@ function declaredHostFor(svc) {
4014
4265
  function hasExtractedConfiguredBy(graph, svcId) {
4015
4266
  for (const edgeId of graph.outboundEdges(svcId)) {
4016
4267
  const e = graph.getEdgeAttributes(edgeId);
4017
- if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
4268
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4018
4269
  return true;
4019
4270
  }
4020
4271
  }
@@ -4027,10 +4278,10 @@ function detectHostMismatch(graph, svcId, svc) {
4027
4278
  const out = [];
4028
4279
  for (const edgeId of graph.outboundEdges(svcId)) {
4029
4280
  const edge = graph.getEdgeAttributes(edgeId);
4030
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4031
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4281
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4282
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4032
4283
  const target = graph.getNodeAttributes(edge.target);
4033
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4284
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4034
4285
  const observedHost = target.host?.trim();
4035
4286
  if (!observedHost) continue;
4036
4287
  if (observedHost === declaredHost) continue;
@@ -4052,10 +4303,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4052
4303
  const deps = svc.dependencies ?? {};
4053
4304
  for (const edgeId of graph.outboundEdges(svcId)) {
4054
4305
  const edge = graph.getEdgeAttributes(edgeId);
4055
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4056
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4306
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4307
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4057
4308
  const target = graph.getNodeAttributes(edge.target);
4058
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4309
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4059
4310
  for (const pair of compatPairs()) {
4060
4311
  if (pair.engine !== target.engine) continue;
4061
4312
  const declared = deps[pair.driver];
@@ -4116,7 +4367,7 @@ function computeDivergences(graph, opts = {}) {
4116
4367
  }
4117
4368
  graph.forEachNode((nodeId, attrs) => {
4118
4369
  const n = attrs;
4119
- if (n.type !== import_types18.NodeType.ServiceNode) return;
4370
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4120
4371
  const svc = n;
4121
4372
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4122
4373
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4140,7 +4391,7 @@ function computeDivergences(graph, opts = {}) {
4140
4391
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4141
4392
  return a.target.localeCompare(b.target);
4142
4393
  });
4143
- return import_types18.DivergenceResultSchema.parse({
4394
+ return import_types19.DivergenceResultSchema.parse({
4144
4395
  divergences: filtered,
4145
4396
  totalAffected: filtered.length,
4146
4397
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4149,7 +4400,7 @@ function computeDivergences(graph, opts = {}) {
4149
4400
 
4150
4401
  // src/diff.ts
4151
4402
  init_cjs_shims();
4152
- var import_node_fs17 = require("fs");
4403
+ var import_node_fs19 = require("fs");
4153
4404
  async function loadSnapshotForDiff(target) {
4154
4405
  if (/^https?:\/\//i.test(target)) {
4155
4406
  const res = await fetch(target);
@@ -4158,7 +4409,7 @@ async function loadSnapshotForDiff(target) {
4158
4409
  }
4159
4410
  return await res.json();
4160
4411
  }
4161
- const raw = await import_node_fs17.promises.readFile(target, "utf8");
4412
+ const raw = await import_node_fs19.promises.readFile(target, "utf8");
4162
4413
  return JSON.parse(raw);
4163
4414
  }
4164
4415
  function indexEntries(entries) {
@@ -4226,23 +4477,23 @@ function canonicalJson(value) {
4226
4477
 
4227
4478
  // src/projects.ts
4228
4479
  init_cjs_shims();
4229
- var import_node_path29 = __toESM(require("path"), 1);
4480
+ var import_node_path31 = __toESM(require("path"), 1);
4230
4481
  function pathsForProject(project, baseDir) {
4231
4482
  if (project === DEFAULT_PROJECT) {
4232
4483
  return {
4233
- snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
4234
- errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
4235
- staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
4236
- embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
4237
- policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
4484
+ snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
4485
+ errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
4486
+ staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
4487
+ embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
4488
+ policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
4238
4489
  };
4239
4490
  }
4240
4491
  return {
4241
- snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
4242
- errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
4243
- staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
4244
- embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
4245
- policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
4492
+ snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
4493
+ errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
4494
+ staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
4495
+ embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
4496
+ policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
4246
4497
  };
4247
4498
  }
4248
4499
  var Projects = class {
@@ -4278,49 +4529,49 @@ var Projects = class {
4278
4529
 
4279
4530
  // src/registry.ts
4280
4531
  init_cjs_shims();
4281
- var import_node_fs18 = require("fs");
4532
+ var import_node_fs20 = require("fs");
4282
4533
  var import_node_os2 = __toESM(require("os"), 1);
4283
- var import_node_path30 = __toESM(require("path"), 1);
4284
- var import_types19 = require("@neat.is/types");
4534
+ var import_node_path32 = __toESM(require("path"), 1);
4535
+ var import_types20 = require("@neat.is/types");
4285
4536
  var LOCK_TIMEOUT_MS = 5e3;
4286
4537
  var LOCK_RETRY_MS = 50;
4287
4538
  function neatHome() {
4288
4539
  const override = process.env.NEAT_HOME;
4289
- if (override && override.length > 0) return import_node_path30.default.resolve(override);
4290
- return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4540
+ if (override && override.length > 0) return import_node_path32.default.resolve(override);
4541
+ return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
4291
4542
  }
4292
4543
  function registryPath() {
4293
- return import_node_path30.default.join(neatHome(), "projects.json");
4544
+ return import_node_path32.default.join(neatHome(), "projects.json");
4294
4545
  }
4295
4546
  function registryLockPath() {
4296
- return import_node_path30.default.join(neatHome(), "projects.json.lock");
4547
+ return import_node_path32.default.join(neatHome(), "projects.json.lock");
4297
4548
  }
4298
4549
  async function normalizeProjectPath(input) {
4299
- const resolved = import_node_path30.default.resolve(input);
4550
+ const resolved = import_node_path32.default.resolve(input);
4300
4551
  try {
4301
- return await import_node_fs18.promises.realpath(resolved);
4552
+ return await import_node_fs20.promises.realpath(resolved);
4302
4553
  } catch {
4303
4554
  return resolved;
4304
4555
  }
4305
4556
  }
4306
4557
  async function writeAtomically(target, contents) {
4307
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4558
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(target), { recursive: true });
4308
4559
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4309
- const fd = await import_node_fs18.promises.open(tmp, "w");
4560
+ const fd = await import_node_fs20.promises.open(tmp, "w");
4310
4561
  try {
4311
4562
  await fd.writeFile(contents, "utf8");
4312
4563
  await fd.sync();
4313
4564
  } finally {
4314
4565
  await fd.close();
4315
4566
  }
4316
- await import_node_fs18.promises.rename(tmp, target);
4567
+ await import_node_fs20.promises.rename(tmp, target);
4317
4568
  }
4318
4569
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4319
4570
  const deadline = Date.now() + timeoutMs;
4320
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4571
+ await import_node_fs20.promises.mkdir(import_node_path32.default.dirname(lockPath), { recursive: true });
4321
4572
  while (true) {
4322
4573
  try {
4323
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4574
+ const fd = await import_node_fs20.promises.open(lockPath, "wx");
4324
4575
  await fd.close();
4325
4576
  return;
4326
4577
  } catch (err) {
@@ -4336,7 +4587,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4336
4587
  }
4337
4588
  }
4338
4589
  async function releaseLock(lockPath) {
4339
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4590
+ await import_node_fs20.promises.unlink(lockPath).catch(() => {
4340
4591
  });
4341
4592
  }
4342
4593
  async function withLock(fn) {
@@ -4352,7 +4603,7 @@ async function readRegistry() {
4352
4603
  const file = registryPath();
4353
4604
  let raw;
4354
4605
  try {
4355
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4606
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4356
4607
  } catch (err) {
4357
4608
  if (err.code === "ENOENT") {
4358
4609
  return { version: 1, projects: [] };
@@ -4360,10 +4611,10 @@ async function readRegistry() {
4360
4611
  throw err;
4361
4612
  }
4362
4613
  const parsed = JSON.parse(raw);
4363
- return import_types19.RegistryFileSchema.parse(parsed);
4614
+ return import_types20.RegistryFileSchema.parse(parsed);
4364
4615
  }
4365
4616
  async function writeRegistry(reg) {
4366
- const validated = import_types19.RegistryFileSchema.parse(reg);
4617
+ const validated = import_types20.RegistryFileSchema.parse(reg);
4367
4618
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4368
4619
  }
4369
4620
  var ProjectNameCollisionError = class extends Error {
@@ -4614,11 +4865,11 @@ function registerRoutes(scope, ctx) {
4614
4865
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4615
4866
  const parsed = [];
4616
4867
  for (const c of candidates) {
4617
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4868
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4618
4869
  if (!r.success) {
4619
4870
  return reply.code(400).send({
4620
4871
  error: `unknown divergence type "${c}"`,
4621
- allowed: import_types20.DivergenceTypeSchema.options
4872
+ allowed: import_types21.DivergenceTypeSchema.options
4622
4873
  });
4623
4874
  }
4624
4875
  parsed.push(r.data);
@@ -4802,7 +5053,7 @@ function registerRoutes(scope, ctx) {
4802
5053
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4803
5054
  let violations = await log.readAll();
4804
5055
  if (req.query.severity) {
4805
- const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
5056
+ const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
4806
5057
  if (!sev.success) {
4807
5058
  return reply.code(400).send({
4808
5059
  error: "invalid severity",
@@ -4819,7 +5070,7 @@ function registerRoutes(scope, ctx) {
4819
5070
  scope.post("/policies/check", async (req, reply) => {
4820
5071
  const proj = resolveProject(registry, req, reply);
4821
5072
  if (!proj) return;
4822
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5073
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4823
5074
  if (!parsed.success) {
4824
5075
  return reply.code(400).send({
4825
5076
  error: "invalid /policies/check body",
@@ -4917,30 +5168,14 @@ async function buildApi(opts) {
4917
5168
  return app;
4918
5169
  }
4919
5170
 
4920
- // src/extract/retire.ts
4921
- init_cjs_shims();
4922
- var import_types21 = require("@neat.is/types");
4923
- function retireEdgesByFile(graph, file) {
4924
- const normalized = file.split("\\").join("/");
4925
- const toDrop = [];
4926
- graph.forEachEdge((id, attrs) => {
4927
- const edge = attrs;
4928
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
4929
- if (!edge.evidence?.file) return;
4930
- if (edge.evidence.file === normalized) toDrop.push(id);
4931
- });
4932
- for (const id of toDrop) graph.dropEdge(id);
4933
- return toDrop.length;
4934
- }
4935
-
4936
5171
  // src/watch.ts
4937
5172
  init_otel();
4938
5173
  init_otel_grpc();
4939
5174
 
4940
5175
  // src/search.ts
4941
5176
  init_cjs_shims();
4942
- var import_node_fs19 = require("fs");
4943
- var import_node_path33 = __toESM(require("path"), 1);
5177
+ var import_node_fs21 = require("fs");
5178
+ var import_node_path35 = __toESM(require("path"), 1);
4944
5179
  var import_node_crypto = require("crypto");
4945
5180
  var DEFAULT_LIMIT = 10;
4946
5181
  var NOMIC_DIM = 768;
@@ -5070,7 +5305,7 @@ async function pickEmbedder() {
5070
5305
  }
5071
5306
  async function readCache(cachePath) {
5072
5307
  try {
5073
- const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
5308
+ const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
5074
5309
  const parsed = JSON.parse(raw);
5075
5310
  if (parsed.version !== 1) return null;
5076
5311
  return parsed;
@@ -5079,8 +5314,8 @@ async function readCache(cachePath) {
5079
5314
  }
5080
5315
  }
5081
5316
  async function writeCache(cachePath, cache) {
5082
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
5083
- await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
5317
+ await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(cachePath), { recursive: true });
5318
+ await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
5084
5319
  }
5085
5320
  var VectorIndex = class {
5086
5321
  constructor(embedder, cachePath) {
@@ -5235,8 +5470,8 @@ var ALL_PHASES = [
5235
5470
  ];
5236
5471
  function classifyChange(relPath) {
5237
5472
  const phases = /* @__PURE__ */ new Set();
5238
- const base = import_node_path34.default.basename(relPath).toLowerCase();
5239
- const segments = relPath.split(import_node_path34.default.sep).map((s) => s.toLowerCase());
5473
+ const base = import_node_path36.default.basename(relPath).toLowerCase();
5474
+ const segments = relPath.split(import_node_path36.default.sep).map((s) => s.toLowerCase());
5240
5475
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
5241
5476
  phases.add("services");
5242
5477
  phases.add("aliases");
@@ -5301,6 +5536,16 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
5301
5536
  durationMs: Date.now() - started
5302
5537
  };
5303
5538
  }
5539
+ var IGNORED_WATCH_GLOBS = [
5540
+ "**/node_modules/**",
5541
+ "**/.git/**",
5542
+ "**/dist/**",
5543
+ "**/build/**",
5544
+ "**/.turbo/**",
5545
+ "**/.next/**",
5546
+ "**/neat-out/**",
5547
+ "**/.DS_Store"
5548
+ ];
5304
5549
  var IGNORED_WATCH_PATHS = [
5305
5550
  /(?:^|[\\/])node_modules[\\/]/,
5306
5551
  /(?:^|[\\/])\.git[\\/]/,
@@ -5314,13 +5559,42 @@ var IGNORED_WATCH_PATHS = [
5314
5559
  function shouldIgnore(absPath) {
5315
5560
  return IGNORED_WATCH_PATHS.some((re) => re.test(absPath));
5316
5561
  }
5562
+ var DARWIN_POLLING_DIR_THRESHOLD = 400;
5563
+ function countWatchableDirs(scanPath, limit) {
5564
+ let count = 0;
5565
+ const visit = (dir, depth) => {
5566
+ if (count >= limit) return;
5567
+ let entries;
5568
+ try {
5569
+ entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
5570
+ } catch {
5571
+ return;
5572
+ }
5573
+ for (const e of entries) {
5574
+ if (count >= limit) return;
5575
+ if (!e.isDirectory()) continue;
5576
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path36.default.join(dir, e.name) + import_node_path36.default.sep))) continue;
5577
+ count++;
5578
+ if (depth < 2) visit(import_node_path36.default.join(dir, e.name), depth + 1);
5579
+ }
5580
+ };
5581
+ visit(scanPath, 0);
5582
+ return count;
5583
+ }
5584
+ function shouldUsePolling(scanPath) {
5585
+ const env = process.env.NEAT_WATCH_POLLING;
5586
+ if (env === "1" || env === "true") return true;
5587
+ if (env === "0" || env === "false") return false;
5588
+ if (process.platform !== "darwin") return false;
5589
+ return countWatchableDirs(scanPath, DARWIN_POLLING_DIR_THRESHOLD) >= DARWIN_POLLING_DIR_THRESHOLD;
5590
+ }
5317
5591
  async function startWatch(graph, opts) {
5318
5592
  const debounceMs = opts.debounceMs ?? 1e3;
5319
5593
  const projectName = opts.project ?? DEFAULT_PROJECT;
5320
5594
  await loadGraphFromDisk(graph, opts.outPath);
5321
5595
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
5322
- const policyFilePath = import_node_path34.default.join(opts.scanPath, "policy.json");
5323
- const policyViolationsPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "policy-violations.ndjson");
5596
+ const policyFilePath = import_node_path36.default.join(opts.scanPath, "policy.json");
5597
+ const policyViolationsPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.outPath), "policy-violations.ndjson");
5324
5598
  let policies = [];
5325
5599
  try {
5326
5600
  policies = await loadPolicyFile(policyFilePath);
@@ -5369,7 +5643,7 @@ async function startWatch(graph, opts) {
5369
5643
  const host = opts.host ?? "0.0.0.0";
5370
5644
  const port = opts.port ?? 8080;
5371
5645
  const otelPort = opts.otelPort ?? 4318;
5372
- const cachePath = opts.embeddingsCachePath ?? import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "embeddings.json");
5646
+ const cachePath = opts.embeddingsCachePath ?? import_node_path36.default.join(import_node_path36.default.dirname(opts.outPath), "embeddings.json");
5373
5647
  let searchIndex;
5374
5648
  try {
5375
5649
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -5387,7 +5661,7 @@ async function startWatch(graph, opts) {
5387
5661
  // Paths are derived from the explicit options the watch caller passes
5388
5662
  // — pathsForProject is only used to fill in the embeddings/snapshot
5389
5663
  // fields so the registry shape is complete.
5390
- ...pathsForProject(projectName, import_node_path34.default.dirname(opts.outPath)),
5664
+ ...pathsForProject(projectName, import_node_path36.default.dirname(opts.outPath)),
5391
5665
  snapshotPath: opts.outPath,
5392
5666
  errorsPath: opts.errorsPath,
5393
5667
  staleEventsPath: opts.staleEventsPath
@@ -5472,9 +5746,9 @@ async function startWatch(graph, opts) {
5472
5746
  };
5473
5747
  const onPath = (absPath) => {
5474
5748
  if (shouldIgnore(absPath)) return;
5475
- const rel = import_node_path34.default.relative(opts.scanPath, absPath);
5749
+ const rel = import_node_path36.default.relative(opts.scanPath, absPath);
5476
5750
  if (!rel || rel.startsWith("..")) return;
5477
- pendingPaths.add(rel.split(import_node_path34.default.sep).join("/"));
5751
+ pendingPaths.add(rel.split(import_node_path36.default.sep).join("/"));
5478
5752
  const phases = classifyChange(rel);
5479
5753
  if (phases.size === 0) {
5480
5754
  for (const p of ALL_PHASES) pending.add(p);
@@ -5483,10 +5757,19 @@ async function startWatch(graph, opts) {
5483
5757
  }
5484
5758
  schedule();
5485
5759
  };
5760
+ const usePolling = shouldUsePolling(opts.scanPath);
5761
+ if (usePolling) {
5762
+ const reason = process.env.NEAT_WATCH_POLLING === "1" || process.env.NEAT_WATCH_POLLING === "true" ? "NEAT_WATCH_POLLING env override" : "darwin heuristic \u2014 large scan root, kqueue cap risk";
5763
+ console.log(`[${projectName}] watch: usePolling=true (${reason})`);
5764
+ }
5486
5765
  const watcher = import_chokidar.default.watch(opts.scanPath, {
5487
5766
  ignoreInitial: true,
5488
- ignored: (p) => shouldIgnore(p),
5767
+ // Glob array prunes at descent time (#233) so chokidar never opens a
5768
+ // kqueue handle for `node_modules` and friends. The function backstop
5769
+ // catches any path that slipped through and matches the regex set.
5770
+ ignored: [...IGNORED_WATCH_GLOBS, (p) => shouldIgnore(p)],
5489
5771
  persistent: true,
5772
+ usePolling,
5490
5773
  awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 }
5491
5774
  });
5492
5775
  watcher.on("add", onPath);
@@ -5522,8 +5805,8 @@ init_cjs_shims();
5522
5805
 
5523
5806
  // src/installers/javascript.ts
5524
5807
  init_cjs_shims();
5525
- var import_node_fs20 = require("fs");
5526
- var import_node_path35 = __toESM(require("path"), 1);
5808
+ var import_node_fs23 = require("fs");
5809
+ var import_node_path37 = __toESM(require("path"), 1);
5527
5810
  var SDK_PACKAGES = [
5528
5811
  { name: "@opentelemetry/api", version: "^1.9.0" },
5529
5812
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
@@ -5539,7 +5822,7 @@ var OTEL_ENV = {
5539
5822
  };
5540
5823
  async function readPackageJson(serviceDir) {
5541
5824
  try {
5542
- const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(serviceDir, "package.json"), "utf8");
5825
+ const raw = await import_node_fs23.promises.readFile(import_node_path37.default.join(serviceDir, "package.json"), "utf8");
5543
5826
  return JSON.parse(raw);
5544
5827
  } catch {
5545
5828
  return null;
@@ -5558,7 +5841,7 @@ function rewriteStartScript(start) {
5558
5841
  }
5559
5842
  async function plan(serviceDir) {
5560
5843
  const pkg = await readPackageJson(serviceDir);
5561
- const manifestPath = import_node_path35.default.join(serviceDir, "package.json");
5844
+ const manifestPath = import_node_path37.default.join(serviceDir, "package.json");
5562
5845
  const empty = {
5563
5846
  language: "javascript",
5564
5847
  serviceDir,
@@ -5605,7 +5888,7 @@ async function apply(installPlan) {
5605
5888
  const originals = /* @__PURE__ */ new Map();
5606
5889
  for (const file of touched) {
5607
5890
  try {
5608
- originals.set(file, await import_node_fs20.promises.readFile(file, "utf8"));
5891
+ originals.set(file, await import_node_fs23.promises.readFile(file, "utf8"));
5609
5892
  } catch {
5610
5893
  }
5611
5894
  }
@@ -5631,8 +5914,8 @@ async function apply(installPlan) {
5631
5914
  }
5632
5915
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
5633
5916
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
5634
- await import_node_fs20.promises.writeFile(tmp, newRaw, "utf8");
5635
- await import_node_fs20.promises.rename(tmp, file);
5917
+ await import_node_fs23.promises.writeFile(tmp, newRaw, "utf8");
5918
+ await import_node_fs23.promises.rename(tmp, file);
5636
5919
  }
5637
5920
  } catch (err) {
5638
5921
  await rollback(installPlan, originals);
@@ -5643,7 +5926,7 @@ async function rollback(installPlan, originals) {
5643
5926
  const restored = [];
5644
5927
  for (const [file, raw] of originals.entries()) {
5645
5928
  try {
5646
- await import_node_fs20.promises.writeFile(file, raw, "utf8");
5929
+ await import_node_fs23.promises.writeFile(file, raw, "utf8");
5647
5930
  restored.push(file);
5648
5931
  } catch {
5649
5932
  }
@@ -5657,8 +5940,8 @@ async function rollback(installPlan, originals) {
5657
5940
  ...restored.map((f) => `restored: ${f}`),
5658
5941
  ""
5659
5942
  ];
5660
- const rollbackPath = import_node_path35.default.join(installPlan.serviceDir, "neat-rollback.patch");
5661
- await import_node_fs20.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5943
+ const rollbackPath = import_node_path37.default.join(installPlan.serviceDir, "neat-rollback.patch");
5944
+ await import_node_fs23.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5662
5945
  }
5663
5946
  var javascriptInstaller = {
5664
5947
  name: "javascript",
@@ -5669,8 +5952,8 @@ var javascriptInstaller = {
5669
5952
 
5670
5953
  // src/installers/python.ts
5671
5954
  init_cjs_shims();
5672
- var import_node_fs21 = require("fs");
5673
- var import_node_path36 = __toESM(require("path"), 1);
5955
+ var import_node_fs24 = require("fs");
5956
+ var import_node_path38 = __toESM(require("path"), 1);
5674
5957
  var SDK_PACKAGES2 = [
5675
5958
  { name: "opentelemetry-distro", version: ">=0.49b0" },
5676
5959
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -5682,7 +5965,7 @@ var OTEL_ENV2 = {
5682
5965
  };
5683
5966
  async function exists2(p) {
5684
5967
  try {
5685
- await import_node_fs21.promises.stat(p);
5968
+ await import_node_fs24.promises.stat(p);
5686
5969
  return true;
5687
5970
  } catch {
5688
5971
  return false;
@@ -5691,7 +5974,7 @@ async function exists2(p) {
5691
5974
  async function detect2(serviceDir) {
5692
5975
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
5693
5976
  for (const m of markers) {
5694
- if (await exists2(import_node_path36.default.join(serviceDir, m))) return true;
5977
+ if (await exists2(import_node_path38.default.join(serviceDir, m))) return true;
5695
5978
  }
5696
5979
  return false;
5697
5980
  }
@@ -5701,9 +5984,9 @@ function reqPackageName(line) {
5701
5984
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
5702
5985
  }
5703
5986
  async function planRequirementsTxtEdits(serviceDir) {
5704
- const file = import_node_path36.default.join(serviceDir, "requirements.txt");
5987
+ const file = import_node_path38.default.join(serviceDir, "requirements.txt");
5705
5988
  if (!await exists2(file)) return null;
5706
- const raw = await import_node_fs21.promises.readFile(file, "utf8");
5989
+ const raw = await import_node_fs24.promises.readFile(file, "utf8");
5707
5990
  const presentNames = new Set(
5708
5991
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
5709
5992
  );
@@ -5711,9 +5994,9 @@ async function planRequirementsTxtEdits(serviceDir) {
5711
5994
  return { manifest: file, missing: [...missing] };
5712
5995
  }
5713
5996
  async function planProcfileEdits(serviceDir) {
5714
- const procfile = import_node_path36.default.join(serviceDir, "Procfile");
5997
+ const procfile = import_node_path38.default.join(serviceDir, "Procfile");
5715
5998
  if (!await exists2(procfile)) return [];
5716
- const raw = await import_node_fs21.promises.readFile(procfile, "utf8");
5999
+ const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
5717
6000
  const edits = [];
5718
6001
  for (const line of raw.split(/\r?\n/)) {
5719
6002
  if (line.length === 0) continue;
@@ -5765,8 +6048,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
5765
6048
  const next = `${original}${trailing}${newlines.join("\n")}
5766
6049
  `;
5767
6050
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
5768
- await import_node_fs21.promises.writeFile(tmp, next, "utf8");
5769
- await import_node_fs21.promises.rename(tmp, manifest);
6051
+ await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6052
+ await import_node_fs24.promises.rename(tmp, manifest);
5770
6053
  }
5771
6054
  async function applyProcfile(procfile, edits, original) {
5772
6055
  let next = original;
@@ -5775,8 +6058,8 @@ async function applyProcfile(procfile, edits, original) {
5775
6058
  next = next.replace(e.before, e.after);
5776
6059
  }
5777
6060
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
5778
- await import_node_fs21.promises.writeFile(tmp, next, "utf8");
5779
- await import_node_fs21.promises.rename(tmp, procfile);
6061
+ await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6062
+ await import_node_fs24.promises.rename(tmp, procfile);
5780
6063
  }
5781
6064
  async function apply2(installPlan) {
5782
6065
  const touched = /* @__PURE__ */ new Set();
@@ -5786,7 +6069,7 @@ async function apply2(installPlan) {
5786
6069
  const originals = /* @__PURE__ */ new Map();
5787
6070
  for (const file of touched) {
5788
6071
  try {
5789
- originals.set(file, await import_node_fs21.promises.readFile(file, "utf8"));
6072
+ originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
5790
6073
  } catch {
5791
6074
  }
5792
6075
  }
@@ -5796,7 +6079,7 @@ async function apply2(installPlan) {
5796
6079
  if (raw === void 0) {
5797
6080
  throw new Error(`python installer: cannot read ${file} during apply`);
5798
6081
  }
5799
- const base = import_node_path36.default.basename(file);
6082
+ const base = import_node_path38.default.basename(file);
5800
6083
  if (base === "requirements.txt") {
5801
6084
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
5802
6085
  if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
@@ -5814,7 +6097,7 @@ async function rollback2(installPlan, originals) {
5814
6097
  const restored = [];
5815
6098
  for (const [file, raw] of originals.entries()) {
5816
6099
  try {
5817
- await import_node_fs21.promises.writeFile(file, raw, "utf8");
6100
+ await import_node_fs24.promises.writeFile(file, raw, "utf8");
5818
6101
  restored.push(file);
5819
6102
  } catch {
5820
6103
  }
@@ -5828,8 +6111,8 @@ async function rollback2(installPlan, originals) {
5828
6111
  ...restored.map((f) => `restored: ${f}`),
5829
6112
  ""
5830
6113
  ];
5831
- const rollbackPath = import_node_path36.default.join(installPlan.serviceDir, "neat-rollback.patch");
5832
- await import_node_fs21.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6114
+ const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
6115
+ await import_node_fs24.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5833
6116
  }
5834
6117
  var pythonInstaller = {
5835
6118
  name: "python",
@@ -5942,10 +6225,10 @@ var TransportError = class extends Error {
5942
6225
  function createHttpClient(baseUrl) {
5943
6226
  const root = baseUrl.replace(/\/$/, "");
5944
6227
  return {
5945
- async get(path38) {
6228
+ async get(path40) {
5946
6229
  let res;
5947
6230
  try {
5948
- res = await fetch(`${root}${path38}`);
6231
+ res = await fetch(`${root}${path40}`);
5949
6232
  } catch (err) {
5950
6233
  throw new TransportError(
5951
6234
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -5955,16 +6238,16 @@ function createHttpClient(baseUrl) {
5955
6238
  const body = await res.text().catch(() => "");
5956
6239
  throw new HttpError(
5957
6240
  res.status,
5958
- `${res.status} ${res.statusText} on GET ${path38}: ${body}`,
6241
+ `${res.status} ${res.statusText} on GET ${path40}: ${body}`,
5959
6242
  body
5960
6243
  );
5961
6244
  }
5962
6245
  return await res.json();
5963
6246
  },
5964
- async post(path38, body) {
6247
+ async post(path40, body) {
5965
6248
  let res;
5966
6249
  try {
5967
- res = await fetch(`${root}${path38}`, {
6250
+ res = await fetch(`${root}${path40}`, {
5968
6251
  method: "POST",
5969
6252
  headers: { "content-type": "application/json" },
5970
6253
  body: JSON.stringify(body)
@@ -5978,7 +6261,7 @@ function createHttpClient(baseUrl) {
5978
6261
  const text = await res.text().catch(() => "");
5979
6262
  throw new HttpError(
5980
6263
  res.status,
5981
- `${res.status} ${res.statusText} on POST ${path38}: ${text}`,
6264
+ `${res.status} ${res.statusText} on POST ${path40}: ${text}`,
5982
6265
  text
5983
6266
  );
5984
6267
  }
@@ -5992,12 +6275,12 @@ function projectPath(project, suffix) {
5992
6275
  }
5993
6276
  async function runRootCause(client, input) {
5994
6277
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
5995
- const path38 = projectPath(
6278
+ const path40 = projectPath(
5996
6279
  input.project,
5997
6280
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
5998
6281
  );
5999
6282
  try {
6000
- const result = await client.get(path38);
6283
+ const result = await client.get(path40);
6001
6284
  const arrowPath = result.traversalPath.join(" \u2190 ");
6002
6285
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
6003
6286
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -6023,12 +6306,12 @@ async function runRootCause(client, input) {
6023
6306
  }
6024
6307
  async function runBlastRadius(client, input) {
6025
6308
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
6026
- const path38 = projectPath(
6309
+ const path40 = projectPath(
6027
6310
  input.project,
6028
6311
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6029
6312
  );
6030
6313
  try {
6031
- const result = await client.get(path38);
6314
+ const result = await client.get(path40);
6032
6315
  if (result.totalAffected === 0) {
6033
6316
  return {
6034
6317
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -6062,12 +6345,12 @@ function formatBlastEntry(n) {
6062
6345
  }
6063
6346
  async function runDependencies(client, input) {
6064
6347
  const depth = input.depth ?? 3;
6065
- const path38 = projectPath(
6348
+ const path40 = projectPath(
6066
6349
  input.project,
6067
6350
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
6068
6351
  );
6069
6352
  try {
6070
- const result = await client.get(path38);
6353
+ const result = await client.get(path40);
6071
6354
  if (result.total === 0) {
6072
6355
  return {
6073
6356
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -6148,9 +6431,9 @@ function formatDuration(ms) {
6148
6431
  return `${Math.round(h / 24)}d`;
6149
6432
  }
6150
6433
  async function runIncidents(client, input) {
6151
- const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6434
+ const path40 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6152
6435
  try {
6153
- const body = await client.get(path38);
6436
+ const body = await client.get(path40);
6154
6437
  const events = body.events;
6155
6438
  if (events.length === 0) {
6156
6439
  return {
@@ -6665,7 +6948,7 @@ async function buildPatchSections(services) {
6665
6948
  }
6666
6949
  async function runInit(opts) {
6667
6950
  const written = [];
6668
- const stat = await import_node_fs22.promises.stat(opts.scanPath).catch(() => null);
6951
+ const stat = await import_node_fs25.promises.stat(opts.scanPath).catch(() => null);
6669
6952
  if (!stat || !stat.isDirectory()) {
6670
6953
  console.error(`neat init: ${opts.scanPath} is not a directory`);
6671
6954
  return { exitCode: 2, writtenFiles: written };
@@ -6674,9 +6957,9 @@ async function runInit(opts) {
6674
6957
  printDiscoveryReport(opts, services);
6675
6958
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
6676
6959
  const patch = renderPatch(sections);
6677
- const patchPath = import_node_path37.default.join(opts.scanPath, "neat.patch");
6960
+ const patchPath = import_node_path39.default.join(opts.scanPath, "neat.patch");
6678
6961
  if (opts.dryRun) {
6679
- await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
6962
+ await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
6680
6963
  written.push(patchPath);
6681
6964
  console.log(`dry-run: patch written to ${patchPath}`);
6682
6965
  console.log("rerun without --dry-run to register and snapshot.");
@@ -6685,7 +6968,12 @@ async function runInit(opts) {
6685
6968
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
6686
6969
  resetGraph(graphKey);
6687
6970
  const graph = getGraph(graphKey);
6688
- const result = await extractFromDirectory(graph, opts.scanPath);
6971
+ const projectPaths = pathsForProject(
6972
+ graphKey,
6973
+ import_node_path39.default.join(opts.scanPath, "neat-out")
6974
+ );
6975
+ const errorsPath = import_node_path39.default.join(import_node_path39.default.dirname(opts.outPath), import_node_path39.default.basename(projectPaths.errorsPath));
6976
+ const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
6689
6977
  await saveGraphToDisk(graph, opts.outPath);
6690
6978
  written.push(opts.outPath);
6691
6979
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
@@ -6716,7 +7004,7 @@ async function runInit(opts) {
6716
7004
  console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
6717
7005
  }
6718
7006
  } else {
6719
- await import_node_fs22.promises.writeFile(patchPath, patch, "utf8");
7007
+ await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
6720
7008
  written.push(patchPath);
6721
7009
  }
6722
7010
  }
@@ -6730,6 +7018,10 @@ async function runInit(opts) {
6730
7018
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
6731
7019
  console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
6732
7020
  console.log(summarise(nodes, edges));
7021
+ console.log(formatExtractionBanner(result.extractionErrors));
7022
+ if (result.extractionErrors > 0) {
7023
+ console.log(`errors: ${errorsPath}`);
7024
+ }
6733
7025
  const incompatibilities = findIncompatibilities(nodes);
6734
7026
  if (incompatibilities.length > 0) {
6735
7027
  console.log("");
@@ -6740,6 +7032,9 @@ async function runInit(opts) {
6740
7032
  }
6741
7033
  }
6742
7034
  }
7035
+ if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
7036
+ return { exitCode: 4, writtenFiles: written };
7037
+ }
6743
7038
  return { exitCode: 0, writtenFiles: written };
6744
7039
  }
6745
7040
  var CLAUDE_SKILL_CONFIG = {
@@ -6756,9 +7051,9 @@ var CLAUDE_SKILL_CONFIG = {
6756
7051
  };
6757
7052
  function claudeConfigPath() {
6758
7053
  const override = process.env.NEAT_CLAUDE_CONFIG;
6759
- if (override && override.length > 0) return import_node_path37.default.resolve(override);
7054
+ if (override && override.length > 0) return import_node_path39.default.resolve(override);
6760
7055
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6761
- return import_node_path37.default.join(home, ".claude.json");
7056
+ return import_node_path39.default.join(home, ".claude.json");
6762
7057
  }
6763
7058
  async function runSkill(opts) {
6764
7059
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -6770,7 +7065,7 @@ async function runSkill(opts) {
6770
7065
  const target = claudeConfigPath();
6771
7066
  let existing = {};
6772
7067
  try {
6773
- existing = JSON.parse(await import_node_fs22.promises.readFile(target, "utf8"));
7068
+ existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
6774
7069
  } catch (err) {
6775
7070
  if (err.code !== "ENOENT") {
6776
7071
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -6782,8 +7077,8 @@ async function runSkill(opts) {
6782
7077
  ...existing,
6783
7078
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
6784
7079
  };
6785
- await import_node_fs22.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6786
- await import_node_fs22.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
7080
+ await import_node_fs25.promises.mkdir(import_node_path39.default.dirname(target), { recursive: true });
7081
+ await import_node_fs25.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
6787
7082
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
6788
7083
  console.log("restart Claude Code to pick up the new MCP server.");
6789
7084
  return { exitCode: 0 };
@@ -6817,12 +7112,12 @@ async function main() {
6817
7112
  console.error("neat init: --apply and --dry-run are mutually exclusive");
6818
7113
  process.exit(2);
6819
7114
  }
6820
- const scanPath = import_node_path37.default.resolve(target);
7115
+ const scanPath = import_node_path39.default.resolve(target);
6821
7116
  const projectExplicit = parsed.project !== null;
6822
- const projectName = projectExplicit ? project : import_node_path37.default.basename(scanPath);
7117
+ const projectName = projectExplicit ? project : import_node_path39.default.basename(scanPath);
6823
7118
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
6824
- const fallback = pathsForProject(projectKey, import_node_path37.default.join(scanPath, "neat-out")).snapshotPath;
6825
- const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
7119
+ const fallback = pathsForProject(projectKey, import_node_path39.default.join(scanPath, "neat-out")).snapshotPath;
7120
+ const outPath = import_node_path39.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
6826
7121
  const result = await runInit({
6827
7122
  scanPath,
6828
7123
  outPath,
@@ -6842,21 +7137,21 @@ async function main() {
6842
7137
  usage();
6843
7138
  process.exit(2);
6844
7139
  }
6845
- const scanPath = import_node_path37.default.resolve(target);
6846
- const stat = await import_node_fs22.promises.stat(scanPath).catch(() => null);
7140
+ const scanPath = import_node_path39.default.resolve(target);
7141
+ const stat = await import_node_fs25.promises.stat(scanPath).catch(() => null);
6847
7142
  if (!stat || !stat.isDirectory()) {
6848
7143
  console.error(`neat watch: ${scanPath} is not a directory`);
6849
7144
  process.exit(2);
6850
7145
  }
6851
- const projectPaths = pathsForProject(project, import_node_path37.default.join(scanPath, "neat-out"));
6852
- const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
6853
- const errorsPath = import_node_path37.default.resolve(
6854
- process.env.NEAT_ERRORS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.errorsPath))
7146
+ const projectPaths = pathsForProject(project, import_node_path39.default.join(scanPath, "neat-out"));
7147
+ const outPath = import_node_path39.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
7148
+ const errorsPath = import_node_path39.default.resolve(
7149
+ process.env.NEAT_ERRORS_PATH ?? import_node_path39.default.join(import_node_path39.default.dirname(outPath), import_node_path39.default.basename(projectPaths.errorsPath))
6855
7150
  );
6856
- const staleEventsPath = import_node_path37.default.resolve(
6857
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.staleEventsPath))
7151
+ const staleEventsPath = import_node_path39.default.resolve(
7152
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path39.default.join(import_node_path39.default.dirname(outPath), import_node_path39.default.basename(projectPaths.staleEventsPath))
6858
7153
  );
6859
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path37.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
7154
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path39.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
6860
7155
  const handle = await startWatch(getGraph(project), {
6861
7156
  scanPath,
6862
7157
  outPath,