@neat.is/core 0.3.2 → 0.3.4

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_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
105
+ return import_node_path34.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_path34, 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_path34 = __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_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
264
+ const protoRoot = import_node_path35.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_path35.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_path35, 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_path35 = __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_fs23 = require("fs");
383
+ var import_node_path40 = __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, path41, edges) {
891
+ if (path41.length > best.path.length) {
892
+ best = { path: [...path41], edges: [...edges] };
893
893
  }
894
- if (path38.length - 1 >= maxDepth) return;
894
+ if (path41.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
+ path41.push(srcId);
900
900
  edges.push(edge);
901
- step(srcId, path38, edges);
902
- path38.pop();
901
+ step(srcId, path41, edges);
902
+ path41.pop();
903
903
  edges.pop();
904
904
  visited.delete(srcId);
905
905
  }
@@ -1588,35 +1588,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1588
1588
  const existing = graph.getEdgeAttributes(id);
1589
1589
  const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
1590
1590
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
1591
+ const newSignal = {
1592
+ spanCount: newSpanCount,
1593
+ errorCount: newErrorCount,
1594
+ lastObservedAgeMs: 0
1595
+ };
1591
1596
  const updated = {
1592
1597
  ...existing,
1593
1598
  provenance: import_types3.Provenance.OBSERVED,
1594
1599
  lastObserved: ts,
1595
1600
  callCount: newSpanCount,
1596
- signal: {
1597
- spanCount: newSpanCount,
1598
- errorCount: newErrorCount,
1599
- lastObservedAgeMs: 0
1600
- },
1601
- confidence: 1
1601
+ signal: newSignal,
1602
+ confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
1602
1603
  };
1603
1604
  graph.replaceEdgeAttributes(id, updated);
1604
1605
  return { edge: updated, created: false };
1605
1606
  }
1607
+ const signal = {
1608
+ spanCount: 1,
1609
+ errorCount: isError ? 1 : 0,
1610
+ lastObservedAgeMs: 0
1611
+ };
1606
1612
  const edge = {
1607
1613
  id,
1608
1614
  source,
1609
1615
  target,
1610
1616
  type,
1611
1617
  provenance: import_types3.Provenance.OBSERVED,
1612
- confidence: 1,
1618
+ confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1613
1619
  lastObserved: ts,
1614
1620
  callCount: 1,
1615
- signal: {
1616
- spanCount: 1,
1617
- errorCount: isError ? 1 : 0,
1618
- lastObservedAgeMs: 0
1619
- }
1621
+ signal
1620
1622
  };
1621
1623
  graph.addEdgeWithKey(id, source, target, edge);
1622
1624
  return { edge, created: true };
@@ -1936,8 +1938,8 @@ async function readErrorEvents(errorsPath) {
1936
1938
 
1937
1939
  // src/extract/services.ts
1938
1940
  init_cjs_shims();
1939
- var import_node_fs7 = require("fs");
1940
- var import_node_path7 = __toESM(require("path"), 1);
1941
+ var import_node_fs8 = require("fs");
1942
+ var import_node_path8 = __toESM(require("path"), 1);
1941
1943
  var import_ignore = __toESM(require("ignore"), 1);
1942
1944
  var import_minimatch2 = require("minimatch");
1943
1945
  var import_types5 = require("@neat.is/types");
@@ -1961,9 +1963,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1961
1963
  function isConfigFile(name) {
1962
1964
  const ext = import_node_path4.default.extname(name);
1963
1965
  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" };
1966
+ if (name === ".env" || name.startsWith(".env.")) {
1967
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1968
+ return { match: true, fileType: "env" };
1969
+ }
1965
1970
  return { match: false, fileType: "" };
1966
1971
  }
1972
+ function isTestPath(filePath) {
1973
+ const normalised = filePath.replace(/\\/g, "/");
1974
+ const segments = normalised.split("/");
1975
+ for (const seg of segments) {
1976
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
1977
+ return true;
1978
+ }
1979
+ }
1980
+ const base = segments[segments.length - 1] ?? "";
1981
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
1982
+ }
1983
+ function isEnvTemplateFile(name) {
1984
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
1985
+ return true;
1986
+ }
1987
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
1988
+ }
1989
+ function maskCommentsInSource(src) {
1990
+ const len = src.length;
1991
+ const out = new Array(len);
1992
+ let i = 0;
1993
+ let inString = 0;
1994
+ let escaped = false;
1995
+ while (i < len) {
1996
+ const c = src[i];
1997
+ if (inString !== 0) {
1998
+ out[i] = c;
1999
+ if (escaped) {
2000
+ escaped = false;
2001
+ } else if (c === "\\") {
2002
+ escaped = true;
2003
+ } else if (c === inString) {
2004
+ inString = 0;
2005
+ }
2006
+ i++;
2007
+ continue;
2008
+ }
2009
+ if (c === "/" && i + 1 < len) {
2010
+ const next = src[i + 1];
2011
+ if (next === "/") {
2012
+ out[i] = " ";
2013
+ out[i + 1] = " ";
2014
+ let j = i + 2;
2015
+ while (j < len && src[j] !== "\n") {
2016
+ out[j] = " ";
2017
+ j++;
2018
+ }
2019
+ i = j;
2020
+ continue;
2021
+ }
2022
+ if (next === "*") {
2023
+ out[i] = " ";
2024
+ out[i + 1] = " ";
2025
+ let j = i + 2;
2026
+ while (j < len) {
2027
+ if (src[j] === "\n") {
2028
+ out[j] = "\n";
2029
+ j++;
2030
+ continue;
2031
+ }
2032
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
2033
+ out[j] = " ";
2034
+ out[j + 1] = " ";
2035
+ j += 2;
2036
+ break;
2037
+ }
2038
+ out[j] = " ";
2039
+ j++;
2040
+ }
2041
+ i = j;
2042
+ continue;
2043
+ }
2044
+ }
2045
+ out[i] = c;
2046
+ if (c === "'" || c === '"' || c === "`") inString = c;
2047
+ i++;
2048
+ }
2049
+ return out.join("");
2050
+ }
2051
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
2052
+ function urlMatchesHost(urlString, host) {
2053
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
2054
+ if (!URL_LIKE.test(urlString)) return false;
2055
+ const [wantedHost, wantedPort] = host.split(":");
2056
+ let parsed;
2057
+ try {
2058
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
2059
+ parsed = new URL(candidate);
2060
+ } catch {
2061
+ return false;
2062
+ }
2063
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
2064
+ if (wantedPort && parsed.port !== wantedPort) return false;
2065
+ return true;
2066
+ }
1967
2067
  function cleanVersion(raw) {
1968
2068
  if (!raw) return void 0;
1969
2069
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -2119,6 +2219,62 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2119
2219
  return author ?? void 0;
2120
2220
  }
2121
2221
 
2222
+ // src/extract/errors.ts
2223
+ init_cjs_shims();
2224
+ var import_node_fs7 = require("fs");
2225
+ var import_node_path7 = __toESM(require("path"), 1);
2226
+ var sink = [];
2227
+ function recordExtractionError(producer, file, err) {
2228
+ const e = err instanceof Error ? err : new Error(String(err));
2229
+ sink.push({
2230
+ producer,
2231
+ file,
2232
+ error: e.message,
2233
+ stack: e.stack,
2234
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2235
+ source: "extract"
2236
+ });
2237
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2238
+ }
2239
+ function drainExtractionErrors() {
2240
+ return sink.splice(0, sink.length);
2241
+ }
2242
+ async function writeExtractionErrors(errors, errorsPath) {
2243
+ if (errors.length === 0) return;
2244
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2245
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2246
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2247
+ }
2248
+ function isStrictExtractionEnabled() {
2249
+ const raw = process.env.NEAT_STRICT_EXTRACTION;
2250
+ return raw === "1" || raw === "true";
2251
+ }
2252
+ function formatExtractionBanner(count) {
2253
+ if (count === 1) return `[neat] 1 file skipped due to parse errors`;
2254
+ return `[neat] ${count} files skipped due to parse errors`;
2255
+ }
2256
+ var droppedSink = [];
2257
+ function noteExtractedDropped(edge) {
2258
+ droppedSink.push(edge);
2259
+ }
2260
+ function drainDroppedExtracted() {
2261
+ return droppedSink.splice(0, droppedSink.length);
2262
+ }
2263
+ function isRejectedLogEnabled() {
2264
+ const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
2265
+ return raw === "1" || raw === "true";
2266
+ }
2267
+ async function writeRejectedExtracted(drops, rejectedPath) {
2268
+ if (drops.length === 0) return;
2269
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
2270
+ const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
2271
+ await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
2272
+ }
2273
+ function formatPrecisionFloorBanner(count) {
2274
+ if (count === 1) return `[neat] 1 extracted edge dropped below precision floor`;
2275
+ return `[neat] ${count} extracted edges dropped below precision floor`;
2276
+ }
2277
+
2122
2278
  // src/extract/services.ts
2123
2279
  var DEFAULT_SCAN_DEPTH = 5;
2124
2280
  function parseScanDepth() {
@@ -2135,21 +2291,21 @@ function workspaceGlobs(pkg) {
2135
2291
  return null;
2136
2292
  }
2137
2293
  async function loadGitignore(scanPath) {
2138
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2294
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2139
2295
  if (!await exists(gitignorePath)) return null;
2140
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2296
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
2141
2297
  return (0, import_ignore.default)().add(raw);
2142
2298
  }
2143
2299
  async function walkDirs(start, scanPath, options, visit) {
2144
2300
  async function recurse(current, depth) {
2145
2301
  if (depth > options.maxDepth) return;
2146
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2302
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2147
2303
  for (const entry2 of entries) {
2148
2304
  if (!entry2.isDirectory()) continue;
2149
2305
  if (IGNORED_DIRS.has(entry2.name)) continue;
2150
- const child = import_node_path7.default.join(current, entry2.name);
2306
+ const child = import_node_path8.default.join(current, entry2.name);
2151
2307
  if (options.ig) {
2152
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2308
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2153
2309
  if (rel && options.ig.ignores(rel + "/")) continue;
2154
2310
  }
2155
2311
  await visit(child);
@@ -2164,8 +2320,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2164
2320
  for (const raw of globs) {
2165
2321
  const pattern = raw.replace(/^\.\//, "");
2166
2322
  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);
2323
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2324
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2169
2325
  continue;
2170
2326
  }
2171
2327
  const segments = pattern.split("/");
@@ -2174,13 +2330,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2174
2330
  if (seg.includes("*")) break;
2175
2331
  staticSegments.push(seg);
2176
2332
  }
2177
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2333
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2178
2334
  if (!await exists(start)) continue;
2179
2335
  const hasDoubleStar = pattern.includes("**");
2180
2336
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2181
2337
  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"))) {
2338
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2339
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2184
2340
  found.add(dir);
2185
2341
  }
2186
2342
  });
@@ -2188,15 +2344,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2188
2344
  return [...found];
2189
2345
  }
2190
2346
  async function discoverNodeService(scanPath, dir) {
2191
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2347
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2192
2348
  if (!await exists(pkgPath)) return null;
2193
2349
  let pkg;
2194
2350
  try {
2195
2351
  pkg = await readJson(pkgPath);
2196
2352
  } catch (err) {
2197
- console.warn(
2198
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2199
- );
2353
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2200
2354
  return null;
2201
2355
  }
2202
2356
  if (!pkg.name) return null;
@@ -2207,7 +2361,7 @@ async function discoverNodeService(scanPath, dir) {
2207
2361
  language: "javascript",
2208
2362
  version: pkg.version,
2209
2363
  dependencies: pkg.dependencies ?? {},
2210
- repoPath: import_node_path7.default.relative(scanPath, dir),
2364
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2211
2365
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2212
2366
  };
2213
2367
  return { pkg, dir, node };
@@ -2223,19 +2377,21 @@ async function discoverPyService(scanPath, dir) {
2223
2377
  language: "python",
2224
2378
  version: py.version,
2225
2379
  dependencies: py.dependencies,
2226
- repoPath: import_node_path7.default.relative(scanPath, dir)
2380
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2227
2381
  };
2228
2382
  return { pkg, dir, node };
2229
2383
  }
2230
2384
  async function discoverServices(scanPath) {
2231
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2385
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2232
2386
  let rootPkg = null;
2233
2387
  if (await exists(rootPkgPath)) {
2234
2388
  try {
2235
2389
  rootPkg = await readJson(rootPkgPath);
2236
2390
  } catch (err) {
2237
- console.warn(
2238
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2391
+ recordExtractionError(
2392
+ "services workspaces",
2393
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2394
+ err
2239
2395
  );
2240
2396
  }
2241
2397
  }
@@ -2251,9 +2407,9 @@ async function discoverServices(scanPath) {
2251
2407
  scanPath,
2252
2408
  { maxDepth: parseScanDepth(), ig },
2253
2409
  async (dir) => {
2254
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2410
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2255
2411
  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"))) {
2412
+ } 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
2413
  candidateDirs.push(dir);
2258
2414
  }
2259
2415
  }
@@ -2267,8 +2423,8 @@ async function discoverServices(scanPath) {
2267
2423
  if (!service) continue;
2268
2424
  const existingDir = seen.get(service.node.name);
2269
2425
  if (existingDir !== void 0) {
2270
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2271
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2426
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2427
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2272
2428
  console.warn(
2273
2429
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2274
2430
  );
@@ -2305,8 +2461,8 @@ function addServiceNodes(graph, services) {
2305
2461
 
2306
2462
  // src/extract/aliases.ts
2307
2463
  init_cjs_shims();
2308
- var import_node_path8 = __toESM(require("path"), 1);
2309
- var import_node_fs8 = require("fs");
2464
+ var import_node_path9 = __toESM(require("path"), 1);
2465
+ var import_node_fs9 = require("fs");
2310
2466
  var import_yaml2 = require("yaml");
2311
2467
  var import_types6 = require("@neat.is/types");
2312
2468
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2333,14 +2489,14 @@ function indexServicesByName(services) {
2333
2489
  const map = /* @__PURE__ */ new Map();
2334
2490
  for (const s of services) {
2335
2491
  map.set(s.node.name, s.node.id);
2336
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2492
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2337
2493
  }
2338
2494
  return map;
2339
2495
  }
2340
2496
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2341
2497
  let composePath = null;
2342
2498
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2343
- const abs = import_node_path8.default.join(scanPath, name);
2499
+ const abs = import_node_path9.default.join(scanPath, name);
2344
2500
  if (await exists(abs)) {
2345
2501
  composePath = abs;
2346
2502
  break;
@@ -2351,8 +2507,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2351
2507
  try {
2352
2508
  compose = await readYaml(composePath);
2353
2509
  } catch (err) {
2354
- console.warn(
2355
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2510
+ recordExtractionError(
2511
+ "aliases compose",
2512
+ import_node_path9.default.relative(scanPath, composePath),
2513
+ err
2356
2514
  );
2357
2515
  return;
2358
2516
  }
@@ -2394,15 +2552,13 @@ function parseDockerfileLabels(content) {
2394
2552
  }
2395
2553
  async function collectDockerfileAliases(graph, services) {
2396
2554
  for (const service of services) {
2397
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2555
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2398
2556
  if (!await exists(dockerfilePath)) continue;
2399
2557
  let content;
2400
2558
  try {
2401
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2559
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2402
2560
  } catch (err) {
2403
- console.warn(
2404
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2405
- );
2561
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2406
2562
  continue;
2407
2563
  }
2408
2564
  const aliases = parseDockerfileLabels(content);
@@ -2412,13 +2568,13 @@ async function collectDockerfileAliases(graph, services) {
2412
2568
  async function walkYamlFiles(start, depth = 0, max = 5) {
2413
2569
  if (depth > max) return [];
2414
2570
  const out = [];
2415
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2571
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2416
2572
  for (const entry2 of entries) {
2417
2573
  if (entry2.isDirectory()) {
2418
2574
  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));
2575
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
2576
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
2577
+ out.push(import_node_path9.default.join(start, entry2.name));
2422
2578
  }
2423
2579
  }
2424
2580
  return out;
@@ -2445,7 +2601,7 @@ function k8sServiceTarget(doc, byName) {
2445
2601
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2446
2602
  const files = await walkYamlFiles(scanPath);
2447
2603
  for (const file of files) {
2448
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2604
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2449
2605
  let docs;
2450
2606
  try {
2451
2607
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2470,14 +2626,14 @@ async function addServiceAliases(graph, scanPath, services) {
2470
2626
 
2471
2627
  // src/extract/databases/index.ts
2472
2628
  init_cjs_shims();
2473
- var import_node_path16 = __toESM(require("path"), 1);
2629
+ var import_node_path17 = __toESM(require("path"), 1);
2474
2630
  var import_types7 = require("@neat.is/types");
2475
2631
 
2476
2632
  // src/extract/databases/db-config-yaml.ts
2477
2633
  init_cjs_shims();
2478
- var import_node_path9 = __toESM(require("path"), 1);
2634
+ var import_node_path10 = __toESM(require("path"), 1);
2479
2635
  async function parse(serviceDir) {
2480
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2636
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2481
2637
  if (!await exists(yamlPath)) return [];
2482
2638
  const raw = await readYaml(yamlPath);
2483
2639
  return [
@@ -2495,13 +2651,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2495
2651
 
2496
2652
  // src/extract/databases/dotenv.ts
2497
2653
  init_cjs_shims();
2498
- var import_node_fs10 = require("fs");
2499
- var import_node_path11 = __toESM(require("path"), 1);
2654
+ var import_node_fs11 = require("fs");
2655
+ var import_node_path12 = __toESM(require("path"), 1);
2500
2656
 
2501
2657
  // src/extract/databases/shared.ts
2502
2658
  init_cjs_shims();
2503
- var import_node_fs9 = require("fs");
2504
- var import_node_path10 = __toESM(require("path"), 1);
2659
+ var import_node_fs10 = require("fs");
2660
+ var import_node_path11 = __toESM(require("path"), 1);
2505
2661
  function schemeToEngine(scheme) {
2506
2662
  const s = scheme.toLowerCase().split("+")[0];
2507
2663
  switch (s) {
@@ -2540,14 +2696,14 @@ function parseConnectionString(url) {
2540
2696
  }
2541
2697
  async function readIfExists(filePath) {
2542
2698
  try {
2543
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2699
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2544
2700
  } catch {
2545
2701
  return null;
2546
2702
  }
2547
2703
  }
2548
2704
  async function findFirst(serviceDir, candidates) {
2549
2705
  for (const rel of candidates) {
2550
- const abs = import_node_path10.default.join(serviceDir, rel);
2706
+ const abs = import_node_path11.default.join(serviceDir, rel);
2551
2707
  const content = await readIfExists(abs);
2552
2708
  if (content !== null) return abs;
2553
2709
  }
@@ -2598,15 +2754,15 @@ function parseDotenvLine(line) {
2598
2754
  return { key, value };
2599
2755
  }
2600
2756
  async function parse2(serviceDir) {
2601
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2757
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2602
2758
  const configs = [];
2603
2759
  const seen = /* @__PURE__ */ new Set();
2604
2760
  for (const entry2 of entries) {
2605
2761
  if (!entry2.isFile()) continue;
2606
2762
  const match = isConfigFile(entry2.name);
2607
2763
  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");
2764
+ const filePath = import_node_path12.default.join(serviceDir, entry2.name);
2765
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2610
2766
  for (const line of content.split("\n")) {
2611
2767
  const parsed = parseDotenvLine(line);
2612
2768
  if (!parsed) continue;
@@ -2625,9 +2781,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2625
2781
 
2626
2782
  // src/extract/databases/prisma.ts
2627
2783
  init_cjs_shims();
2628
- var import_node_path12 = __toESM(require("path"), 1);
2784
+ var import_node_path13 = __toESM(require("path"), 1);
2629
2785
  async function parse3(serviceDir) {
2630
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2786
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2631
2787
  const content = await readIfExists(schemaPath);
2632
2788
  if (!content) return [];
2633
2789
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2759,10 +2915,10 @@ var knexParser = { name: "knex", parse: parse5 };
2759
2915
 
2760
2916
  // src/extract/databases/ormconfig.ts
2761
2917
  init_cjs_shims();
2762
- var import_node_path13 = __toESM(require("path"), 1);
2918
+ var import_node_path14 = __toESM(require("path"), 1);
2763
2919
  async function parse6(serviceDir) {
2764
2920
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2765
- const abs = import_node_path13.default.join(serviceDir, candidate);
2921
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2766
2922
  if (!await exists(abs)) continue;
2767
2923
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2768
2924
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2822,9 +2978,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2822
2978
 
2823
2979
  // src/extract/databases/sequelize.ts
2824
2980
  init_cjs_shims();
2825
- var import_node_path14 = __toESM(require("path"), 1);
2981
+ var import_node_path15 = __toESM(require("path"), 1);
2826
2982
  async function parse8(serviceDir) {
2827
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2983
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2828
2984
  if (!await exists(configPath)) return [];
2829
2985
  const raw = await readJson(configPath);
2830
2986
  const out = [];
@@ -2851,7 +3007,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2851
3007
 
2852
3008
  // src/extract/databases/docker-compose.ts
2853
3009
  init_cjs_shims();
2854
- var import_node_path15 = __toESM(require("path"), 1);
3010
+ var import_node_path16 = __toESM(require("path"), 1);
2855
3011
  function portFromService(svc) {
2856
3012
  for (const raw of svc.ports ?? []) {
2857
3013
  const str = String(raw);
@@ -2878,7 +3034,7 @@ function databaseFromEnv(svc) {
2878
3034
  }
2879
3035
  async function parse9(serviceDir) {
2880
3036
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2881
- const abs = import_node_path15.default.join(serviceDir, name);
3037
+ const abs = import_node_path16.default.join(serviceDir, name);
2882
3038
  if (!await exists(abs)) continue;
2883
3039
  const raw = await readYaml(abs);
2884
3040
  if (!raw?.services) return [];
@@ -3056,11 +3212,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3056
3212
  target: dbNode.id,
3057
3213
  type: import_types7.EdgeType.CONNECTS_TO,
3058
3214
  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
- } : {}
3215
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3216
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3217
+ // Ghost-edge cleanup keys retirement on this; the conditional
3218
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3219
+ evidence: {
3220
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3221
+ }
3064
3222
  };
3065
3223
  if (!graph.hasEdge(edge.id)) {
3066
3224
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3086,15 +3244,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3086
3244
 
3087
3245
  // src/extract/configs.ts
3088
3246
  init_cjs_shims();
3089
- var import_node_fs11 = require("fs");
3090
- var import_node_path17 = __toESM(require("path"), 1);
3247
+ var import_node_fs12 = require("fs");
3248
+ var import_node_path18 = __toESM(require("path"), 1);
3091
3249
  var import_types8 = require("@neat.is/types");
3092
3250
  async function walkConfigFiles(dir) {
3093
3251
  const out = [];
3094
3252
  async function walk(current) {
3095
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3253
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3096
3254
  for (const entry2 of entries) {
3097
- const full = import_node_path17.default.join(current, entry2.name);
3255
+ const full = import_node_path18.default.join(current, entry2.name);
3098
3256
  if (entry2.isDirectory()) {
3099
3257
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3100
3258
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -3111,13 +3269,13 @@ async function addConfigNodes(graph, services, scanPath) {
3111
3269
  for (const service of services) {
3112
3270
  const configFiles = await walkConfigFiles(service.dir);
3113
3271
  for (const file of configFiles) {
3114
- const relPath = import_node_path17.default.relative(scanPath, file);
3272
+ const relPath = import_node_path18.default.relative(scanPath, file);
3115
3273
  const node = {
3116
3274
  id: (0, import_types8.configId)(relPath),
3117
3275
  type: import_types8.NodeType.ConfigNode,
3118
- name: import_node_path17.default.basename(file),
3276
+ name: import_node_path18.default.basename(file),
3119
3277
  path: relPath,
3120
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3278
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3121
3279
  };
3122
3280
  if (!graph.hasNode(node.id)) {
3123
3281
  graph.addNode(node.id, node);
@@ -3129,7 +3287,8 @@ async function addConfigNodes(graph, services, scanPath) {
3129
3287
  target: node.id,
3130
3288
  type: import_types8.EdgeType.CONFIGURED_BY,
3131
3289
  provenance: import_types8.Provenance.EXTRACTED,
3132
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3290
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3291
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3133
3292
  };
3134
3293
  if (!graph.hasEdge(edge.id)) {
3135
3294
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3146,7 +3305,7 @@ var import_types14 = require("@neat.is/types");
3146
3305
 
3147
3306
  // src/extract/calls/http.ts
3148
3307
  init_cjs_shims();
3149
- var import_node_path19 = __toESM(require("path"), 1);
3308
+ var import_node_path20 = __toESM(require("path"), 1);
3150
3309
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3151
3310
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3152
3311
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3154,17 +3313,17 @@ var import_types9 = require("@neat.is/types");
3154
3313
 
3155
3314
  // src/extract/calls/shared.ts
3156
3315
  init_cjs_shims();
3157
- var import_node_fs12 = require("fs");
3158
- var import_node_path18 = __toESM(require("path"), 1);
3316
+ var import_node_fs13 = require("fs");
3317
+ var import_node_path19 = __toESM(require("path"), 1);
3159
3318
  async function walkSourceFiles(dir) {
3160
3319
  const out = [];
3161
3320
  async function walk(current) {
3162
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3321
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3163
3322
  for (const entry2 of entries) {
3164
- const full = import_node_path18.default.join(current, entry2.name);
3323
+ const full = import_node_path19.default.join(current, entry2.name);
3165
3324
  if (entry2.isDirectory()) {
3166
3325
  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))) {
3326
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3168
3327
  out.push(full);
3169
3328
  }
3170
3329
  }
@@ -3177,7 +3336,7 @@ async function loadSourceFiles(dir) {
3177
3336
  const out = [];
3178
3337
  for (const p of paths) {
3179
3338
  try {
3180
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3339
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3181
3340
  out.push({ path: p, content });
3182
3341
  } catch {
3183
3342
  }
@@ -3196,8 +3355,27 @@ function snippet(text, line) {
3196
3355
 
3197
3356
  // src/extract/calls/http.ts
3198
3357
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3358
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3359
+ function isInsideJsxExternalLink(node) {
3360
+ let cursor = node.parent;
3361
+ while (cursor) {
3362
+ if (cursor.type === "jsx_attribute") {
3363
+ let owner = cursor.parent;
3364
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3365
+ owner = owner.parent;
3366
+ }
3367
+ if (!owner) return false;
3368
+ const tagNode = owner.namedChild(0);
3369
+ const tagName = tagNode?.text ?? "";
3370
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3371
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3372
+ }
3373
+ cursor = cursor.parent;
3374
+ }
3375
+ return false;
3376
+ }
3199
3377
  function collectStringLiterals(node, out) {
3200
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3378
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3201
3379
  for (let i = 0; i < node.namedChildCount; i++) {
3202
3380
  const child = node.namedChild(i);
3203
3381
  if (child) collectStringLiterals(child, out);
@@ -3209,8 +3387,9 @@ function callsFromSource(source, parser, knownHosts) {
3209
3387
  collectStringLiterals(tree.rootNode, literals);
3210
3388
  const targets = /* @__PURE__ */ new Set();
3211
3389
  for (const lit of literals) {
3390
+ if (isInsideJsxExternalLink(lit.node)) continue;
3212
3391
  for (const host of knownHosts) {
3213
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3392
+ if (urlMatchesHost(lit.text, host)) {
3214
3393
  targets.add(host);
3215
3394
  }
3216
3395
  }
@@ -3233,9 +3412,9 @@ async function addHttpCallEdges(graph, services) {
3233
3412
  const knownHosts = /* @__PURE__ */ new Set();
3234
3413
  const hostToNodeId = /* @__PURE__ */ new Map();
3235
3414
  for (const service of services) {
3236
- knownHosts.add(import_node_path19.default.basename(service.dir));
3415
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3237
3416
  knownHosts.add(service.pkg.name);
3238
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3417
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3239
3418
  hostToNodeId.set(service.pkg.name, service.node.id);
3240
3419
  }
3241
3420
  let edgesAdded = 0;
@@ -3243,14 +3422,13 @@ async function addHttpCallEdges(graph, services) {
3243
3422
  const files = await loadSourceFiles(service.dir);
3244
3423
  const seenTargets = /* @__PURE__ */ new Map();
3245
3424
  for (const file of files) {
3246
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3425
+ if (isTestPath(file.path)) continue;
3426
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3247
3427
  let targets;
3248
3428
  try {
3249
3429
  targets = callsFromSource(file.content, parser, knownHosts);
3250
3430
  } catch (err) {
3251
- console.warn(
3252
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3253
- );
3431
+ recordExtractionError("http call extraction", file.path, err);
3254
3432
  continue;
3255
3433
  }
3256
3434
  for (const t of targets) {
@@ -3264,17 +3442,32 @@ async function addHttpCallEdges(graph, services) {
3264
3442
  for (const [targetId, evidenceFile] of seenTargets) {
3265
3443
  const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3266
3444
  const line = lineOf(fileContent, `//${evidenceFile.host}`);
3445
+ const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3446
+ const ev = {
3447
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3448
+ line,
3449
+ snippet: snippet(fileContent, line)
3450
+ };
3451
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3452
+ if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3453
+ noteExtractedDropped({
3454
+ source: service.node.id,
3455
+ target: targetId,
3456
+ type: import_types9.EdgeType.CALLS,
3457
+ confidence,
3458
+ confidenceKind: "hostname-shape-match",
3459
+ evidence: ev
3460
+ });
3461
+ continue;
3462
+ }
3267
3463
  const edge = {
3268
- id: (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS),
3464
+ id: edgeId,
3269
3465
  source: service.node.id,
3270
3466
  target: targetId,
3271
3467
  type: import_types9.EdgeType.CALLS,
3272
3468
  provenance: import_types9.Provenance.EXTRACTED,
3273
- evidence: {
3274
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3275
- line,
3276
- snippet: snippet(fileContent, line)
3277
- }
3469
+ confidence,
3470
+ evidence: ev
3278
3471
  };
3279
3472
  if (!graph.hasEdge(edge.id)) {
3280
3473
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3287,7 +3480,7 @@ async function addHttpCallEdges(graph, services) {
3287
3480
 
3288
3481
  // src/extract/calls/kafka.ts
3289
3482
  init_cjs_shims();
3290
- var import_node_path20 = __toESM(require("path"), 1);
3483
+ var import_node_path21 = __toESM(require("path"), 1);
3291
3484
  var import_types10 = require("@neat.is/types");
3292
3485
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3293
3486
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3313,8 +3506,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3313
3506
  name: topic,
3314
3507
  kind: "kafka-topic",
3315
3508
  edgeType,
3509
+ // `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
3510
+ // framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
3511
+ // tier (ADR-066).
3512
+ confidenceKind: "verified-call-site",
3316
3513
  evidence: {
3317
- file: import_node_path20.default.relative(serviceDir, file.path),
3514
+ file: import_node_path21.default.relative(serviceDir, file.path),
3318
3515
  line,
3319
3516
  snippet: snippet(file.content, line)
3320
3517
  }
@@ -3327,7 +3524,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3327
3524
 
3328
3525
  // src/extract/calls/redis.ts
3329
3526
  init_cjs_shims();
3330
- var import_node_path21 = __toESM(require("path"), 1);
3527
+ var import_node_path22 = __toESM(require("path"), 1);
3331
3528
  var import_types11 = require("@neat.is/types");
3332
3529
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3333
3530
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3345,8 +3542,12 @@ function redisEndpointsFromFile(file, serviceDir) {
3345
3542
  name: host,
3346
3543
  kind: "redis",
3347
3544
  edgeType: "CALLS",
3545
+ // `redis://host` URL literal — the scheme is structural support, but no
3546
+ // call expression is verified to wire it through. URL-with-structural-
3547
+ // support tier (ADR-066).
3548
+ confidenceKind: "url-with-structural-support",
3348
3549
  evidence: {
3349
- file: import_node_path21.default.relative(serviceDir, file.path),
3550
+ file: import_node_path22.default.relative(serviceDir, file.path),
3350
3551
  line,
3351
3552
  snippet: snippet(file.content, line)
3352
3553
  }
@@ -3357,7 +3558,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3357
3558
 
3358
3559
  // src/extract/calls/aws.ts
3359
3560
  init_cjs_shims();
3360
- var import_node_path22 = __toESM(require("path"), 1);
3561
+ var import_node_path23 = __toESM(require("path"), 1);
3361
3562
  var import_types12 = require("@neat.is/types");
3362
3563
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3363
3564
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3386,8 +3587,12 @@ function awsEndpointsFromFile(file, serviceDir) {
3386
3587
  name,
3387
3588
  kind,
3388
3589
  edgeType: "CALLS",
3590
+ // SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
3591
+ // literal — framework-aware recognizer, verified-call-site tier
3592
+ // (ADR-066).
3593
+ confidenceKind: "verified-call-site",
3389
3594
  evidence: {
3390
- file: import_node_path22.default.relative(serviceDir, file.path),
3595
+ file: import_node_path23.default.relative(serviceDir, file.path),
3391
3596
  line,
3392
3597
  snippet: snippet(file.content, line)
3393
3598
  }
@@ -3412,16 +3617,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3412
3617
 
3413
3618
  // src/extract/calls/grpc.ts
3414
3619
  init_cjs_shims();
3415
- var import_node_path23 = __toESM(require("path"), 1);
3620
+ var import_node_path24 = __toESM(require("path"), 1);
3416
3621
  var import_types13 = require("@neat.is/types");
3417
3622
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3623
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3624
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3418
3625
  function isLikelyAddress(value) {
3419
3626
  if (!value) return false;
3420
3627
  return /:\d{2,5}$/.test(value) || value.includes(".");
3421
3628
  }
3629
+ function normaliseForMatch(s) {
3630
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3631
+ }
3632
+ function readImports(content) {
3633
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3634
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3635
+ let m;
3636
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3637
+ const raw = m[1];
3638
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3639
+ }
3640
+ return {
3641
+ awsSdkSuffixes,
3642
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3643
+ };
3644
+ }
3645
+ function classifyClient(symbol, ctx) {
3646
+ const key = normaliseForMatch(symbol);
3647
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3648
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3649
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3650
+ return null;
3651
+ }
3422
3652
  function grpcEndpointsFromFile(file, serviceDir) {
3423
3653
  const out = [];
3424
3654
  const seen = /* @__PURE__ */ new Set();
3655
+ const ctx = readImports(file.content);
3425
3656
  GRPC_CLIENT_RE.lastIndex = 0;
3426
3657
  let m;
3427
3658
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3429,15 +3660,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
3429
3660
  const addr = m[2]?.trim();
3430
3661
  const name = isLikelyAddress(addr) ? addr : symbol;
3431
3662
  if (seen.has(name)) continue;
3663
+ const classified = classifyClient(symbol, ctx);
3664
+ if (!classified) continue;
3432
3665
  seen.add(name);
3666
+ const { kind } = classified;
3433
3667
  const line = lineOf(file.content, m[0]);
3434
3668
  out.push({
3435
- infraId: (0, import_types13.infraId)("grpc-service", name),
3669
+ infraId: (0, import_types13.infraId)(kind, name),
3436
3670
  name,
3437
- kind: "grpc-service",
3671
+ kind,
3438
3672
  edgeType: "CALLS",
3673
+ // `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
3674
+ // context — import-aware classification per #238. Verified-call-site
3675
+ // tier (ADR-066).
3676
+ confidenceKind: "verified-call-site",
3439
3677
  evidence: {
3440
- file: import_node_path23.default.relative(serviceDir, file.path),
3678
+ file: import_node_path24.default.relative(serviceDir, file.path),
3441
3679
  line,
3442
3680
  snippet: snippet(file.content, line)
3443
3681
  }
@@ -3457,6 +3695,9 @@ function edgeTypeFromEndpoint(ep) {
3457
3695
  return import_types14.EdgeType.CALLS;
3458
3696
  }
3459
3697
  }
3698
+ function isAwsKind(kind) {
3699
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3700
+ }
3460
3701
  async function addExternalEndpointEdges(graph, services) {
3461
3702
  let nodesAdded = 0;
3462
3703
  let edgesAdded = 0;
@@ -3464,10 +3705,13 @@ async function addExternalEndpointEdges(graph, services) {
3464
3705
  const files = await loadSourceFiles(service.dir);
3465
3706
  const endpoints = [];
3466
3707
  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));
3708
+ if (isTestPath(file.path)) continue;
3709
+ const masked = maskCommentsInSource(file.content);
3710
+ const maskedFile = { path: file.path, content: masked };
3711
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3712
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3713
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3714
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3471
3715
  }
3472
3716
  if (endpoints.length === 0) continue;
3473
3717
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3477,7 +3721,10 @@ async function addExternalEndpointEdges(graph, services) {
3477
3721
  id: ep.infraId,
3478
3722
  type: import_types14.NodeType.InfraNode,
3479
3723
  name: ep.name,
3480
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3724
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3725
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3726
+ // bucket / table kinds from aws.ts.
3727
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3481
3728
  kind: ep.kind
3482
3729
  };
3483
3730
  graph.addNode(node.id, node);
@@ -3487,6 +3734,18 @@ async function addExternalEndpointEdges(graph, services) {
3487
3734
  const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3488
3735
  if (seenEdges.has(edgeId)) continue;
3489
3736
  seenEdges.add(edgeId);
3737
+ const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
3738
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
3739
+ noteExtractedDropped({
3740
+ source: service.node.id,
3741
+ target: ep.infraId,
3742
+ type: edgeType,
3743
+ confidence,
3744
+ confidenceKind: ep.confidenceKind,
3745
+ evidence: ep.evidence
3746
+ });
3747
+ continue;
3748
+ }
3490
3749
  if (!graph.hasEdge(edgeId)) {
3491
3750
  const edge = {
3492
3751
  id: edgeId,
@@ -3494,6 +3753,7 @@ async function addExternalEndpointEdges(graph, services) {
3494
3753
  target: ep.infraId,
3495
3754
  type: edgeType,
3496
3755
  provenance: import_types14.Provenance.EXTRACTED,
3756
+ confidence,
3497
3757
  evidence: ep.evidence
3498
3758
  };
3499
3759
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3517,7 +3777,7 @@ init_cjs_shims();
3517
3777
 
3518
3778
  // src/extract/infra/docker-compose.ts
3519
3779
  init_cjs_shims();
3520
- var import_node_path24 = __toESM(require("path"), 1);
3780
+ var import_node_path25 = __toESM(require("path"), 1);
3521
3781
  var import_types16 = require("@neat.is/types");
3522
3782
 
3523
3783
  // src/extract/infra/shared.ts
@@ -3555,7 +3815,7 @@ function dependsOnList(value) {
3555
3815
  }
3556
3816
  function serviceNameToServiceNode(name, services) {
3557
3817
  for (const s of services) {
3558
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3818
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3559
3819
  }
3560
3820
  return null;
3561
3821
  }
@@ -3564,7 +3824,7 @@ async function addComposeInfra(graph, scanPath, services) {
3564
3824
  let edgesAdded = 0;
3565
3825
  let composePath = null;
3566
3826
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3567
- const abs = import_node_path24.default.join(scanPath, name);
3827
+ const abs = import_node_path25.default.join(scanPath, name);
3568
3828
  if (await exists(abs)) {
3569
3829
  composePath = abs;
3570
3830
  break;
@@ -3575,13 +3835,15 @@ async function addComposeInfra(graph, scanPath, services) {
3575
3835
  try {
3576
3836
  compose = await readYaml(composePath);
3577
3837
  } catch (err) {
3578
- console.warn(
3579
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3838
+ recordExtractionError(
3839
+ "infra docker-compose",
3840
+ import_node_path25.default.relative(scanPath, composePath),
3841
+ err
3580
3842
  );
3581
3843
  return { nodesAdded, edgesAdded };
3582
3844
  }
3583
3845
  if (!compose?.services) return { nodesAdded, edgesAdded };
3584
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3846
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3585
3847
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3586
3848
  for (const [composeName, svc] of Object.entries(compose.services)) {
3587
3849
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3611,6 +3873,7 @@ async function addComposeInfra(graph, scanPath, services) {
3611
3873
  target: targetId,
3612
3874
  type: import_types16.EdgeType.DEPENDS_ON,
3613
3875
  provenance: import_types16.Provenance.EXTRACTED,
3876
+ confidence: (0, import_types16.confidenceForExtracted)("structural"),
3614
3877
  evidence: { file: evidenceFile }
3615
3878
  };
3616
3879
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3622,8 +3885,8 @@ async function addComposeInfra(graph, scanPath, services) {
3622
3885
 
3623
3886
  // src/extract/infra/dockerfile.ts
3624
3887
  init_cjs_shims();
3625
- var import_node_path25 = __toESM(require("path"), 1);
3626
- var import_node_fs13 = require("fs");
3888
+ var import_node_path26 = __toESM(require("path"), 1);
3889
+ var import_node_fs14 = require("fs");
3627
3890
  var import_types17 = require("@neat.is/types");
3628
3891
  function runtimeImage(content) {
3629
3892
  const lines = content.split("\n");
@@ -3643,14 +3906,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3643
3906
  let nodesAdded = 0;
3644
3907
  let edgesAdded = 0;
3645
3908
  for (const service of services) {
3646
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3909
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3647
3910
  if (!await exists(dockerfilePath)) continue;
3648
3911
  let content;
3649
3912
  try {
3650
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3913
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3651
3914
  } catch (err) {
3652
- console.warn(
3653
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3915
+ recordExtractionError(
3916
+ "infra dockerfile",
3917
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3918
+ err
3654
3919
  );
3655
3920
  continue;
3656
3921
  }
@@ -3669,8 +3934,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3669
3934
  target: node.id,
3670
3935
  type: import_types17.EdgeType.RUNS_ON,
3671
3936
  provenance: import_types17.Provenance.EXTRACTED,
3937
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
3672
3938
  evidence: {
3673
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3939
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3674
3940
  }
3675
3941
  };
3676
3942
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3682,19 +3948,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3682
3948
 
3683
3949
  // src/extract/infra/terraform.ts
3684
3950
  init_cjs_shims();
3685
- var import_node_fs14 = require("fs");
3686
- var import_node_path26 = __toESM(require("path"), 1);
3951
+ var import_node_fs15 = require("fs");
3952
+ var import_node_path27 = __toESM(require("path"), 1);
3687
3953
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3688
3954
  async function walkTfFiles(start, depth = 0, max = 5) {
3689
3955
  if (depth > max) return [];
3690
3956
  const out = [];
3691
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3957
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3692
3958
  for (const entry2 of entries) {
3693
3959
  if (entry2.isDirectory()) {
3694
3960
  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));
3961
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3696
3962
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
3697
- out.push(import_node_path26.default.join(start, entry2.name));
3963
+ out.push(import_node_path27.default.join(start, entry2.name));
3698
3964
  }
3699
3965
  }
3700
3966
  return out;
@@ -3703,7 +3969,7 @@ async function addTerraformResources(graph, scanPath) {
3703
3969
  let nodesAdded = 0;
3704
3970
  const files = await walkTfFiles(scanPath);
3705
3971
  for (const file of files) {
3706
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3972
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3707
3973
  RESOURCE_RE.lastIndex = 0;
3708
3974
  let m;
3709
3975
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3721,8 +3987,8 @@ async function addTerraformResources(graph, scanPath) {
3721
3987
 
3722
3988
  // src/extract/infra/k8s.ts
3723
3989
  init_cjs_shims();
3724
- var import_node_fs15 = require("fs");
3725
- var import_node_path27 = __toESM(require("path"), 1);
3990
+ var import_node_fs16 = require("fs");
3991
+ var import_node_path28 = __toESM(require("path"), 1);
3726
3992
  var import_yaml3 = require("yaml");
3727
3993
  var K8S_KIND_TO_INFRA_KIND = {
3728
3994
  Service: "k8s-service",
@@ -3736,13 +4002,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3736
4002
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3737
4003
  if (depth > max) return [];
3738
4004
  const out = [];
3739
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4005
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3740
4006
  for (const entry2 of entries) {
3741
4007
  if (entry2.isDirectory()) {
3742
4008
  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));
4009
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
4010
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
4011
+ out.push(import_node_path28.default.join(start, entry2.name));
3746
4012
  }
3747
4013
  }
3748
4014
  return out;
@@ -3751,7 +4017,7 @@ async function addK8sResources(graph, scanPath) {
3751
4017
  let nodesAdded = 0;
3752
4018
  const files = await walkYamlFiles2(scanPath);
3753
4019
  for (const file of files) {
3754
- const content = await import_node_fs15.promises.readFile(file, "utf8");
4020
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3755
4021
  let docs;
3756
4022
  try {
3757
4023
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3785,9 +4051,49 @@ async function addInfra(graph, scanPath, services) {
3785
4051
  };
3786
4052
  }
3787
4053
 
4054
+ // src/extract/index.ts
4055
+ var import_node_path30 = __toESM(require("path"), 1);
4056
+
4057
+ // src/extract/retire.ts
4058
+ init_cjs_shims();
4059
+ var import_node_fs17 = require("fs");
4060
+ var import_node_path29 = __toESM(require("path"), 1);
4061
+ var import_types18 = require("@neat.is/types");
4062
+ function retireEdgesByFile(graph, file) {
4063
+ const normalized = file.split("\\").join("/");
4064
+ const toDrop = [];
4065
+ graph.forEachEdge((id, attrs) => {
4066
+ const edge = attrs;
4067
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4068
+ if (!edge.evidence?.file) return;
4069
+ if (edge.evidence.file === normalized) toDrop.push(id);
4070
+ });
4071
+ for (const id of toDrop) graph.dropEdge(id);
4072
+ return toDrop.length;
4073
+ }
4074
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4075
+ const toDrop = [];
4076
+ const bases = [scanPath, ...serviceDirs];
4077
+ graph.forEachEdge((id, attrs) => {
4078
+ const edge = attrs;
4079
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4080
+ const evidenceFile = edge.evidence?.file;
4081
+ if (!evidenceFile) return;
4082
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
4083
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4084
+ return;
4085
+ }
4086
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4087
+ if (!found) toDrop.push(id);
4088
+ });
4089
+ for (const id of toDrop) graph.dropEdge(id);
4090
+ return toDrop.length;
4091
+ }
4092
+
3788
4093
  // src/extract/index.ts
3789
4094
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3790
4095
  await ensureCompatLoaded();
4096
+ drainExtractionErrors();
3791
4097
  const services = await discoverServices(scanPath);
3792
4098
  const phase1Nodes = addServiceNodes(graph, services);
3793
4099
  await addServiceAliases(graph, scanPath, services);
@@ -3795,12 +4101,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3795
4101
  const phase3 = await addConfigNodes(graph, services, scanPath);
3796
4102
  const phase4 = await addCallEdges(graph, services);
3797
4103
  const phase5 = await addInfra(graph, scanPath, services);
4104
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
4105
+ graph,
4106
+ scanPath,
4107
+ services.map((s) => s.dir)
4108
+ );
3798
4109
  const frontiersPromoted = promoteFrontierNodes(graph);
3799
4110
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
4111
+ const errorEntries = drainExtractionErrors();
4112
+ if (opts.errorsPath && errorEntries.length > 0) {
4113
+ try {
4114
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
4115
+ } catch (err) {
4116
+ console.warn(
4117
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
4118
+ );
4119
+ }
4120
+ }
4121
+ const droppedEntries = drainDroppedExtracted();
4122
+ if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4123
+ const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
4124
+ try {
4125
+ await writeRejectedExtracted(droppedEntries, rejectedPath);
4126
+ } catch (err) {
4127
+ console.warn(
4128
+ `[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
4129
+ );
4130
+ }
4131
+ }
3800
4132
  const result = {
3801
4133
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3802
4134
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3803
- frontiersPromoted
4135
+ frontiersPromoted,
4136
+ extractionErrors: errorEntries.length,
4137
+ errorEntries,
4138
+ ghostsRetired,
4139
+ extractedDropped: droppedEntries.length,
4140
+ droppedEntries
3804
4141
  };
3805
4142
  emitNeatEvent({
3806
4143
  type: "extraction-complete",
@@ -3817,8 +4154,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3817
4154
 
3818
4155
  // src/persist.ts
3819
4156
  init_cjs_shims();
3820
- var import_node_fs16 = require("fs");
3821
- var import_node_path28 = __toESM(require("path"), 1);
4157
+ var import_node_fs18 = require("fs");
4158
+ var import_node_path31 = __toESM(require("path"), 1);
3822
4159
  var SCHEMA_VERSION = 2;
3823
4160
  function migrateV1ToV2(payload) {
3824
4161
  const nodes = payload.graph.nodes;
@@ -3832,7 +4169,7 @@ function migrateV1ToV2(payload) {
3832
4169
  return { ...payload, schemaVersion: 2 };
3833
4170
  }
3834
4171
  async function ensureDir(filePath) {
3835
- await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
4172
+ await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
3836
4173
  }
3837
4174
  async function saveGraphToDisk(graph, outPath) {
3838
4175
  await ensureDir(outPath);
@@ -3842,13 +4179,13 @@ async function saveGraphToDisk(graph, outPath) {
3842
4179
  graph: graph.export()
3843
4180
  };
3844
4181
  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);
4182
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4183
+ await import_node_fs18.promises.rename(tmp, outPath);
3847
4184
  }
3848
4185
  async function loadGraphFromDisk(graph, outPath) {
3849
4186
  let raw;
3850
4187
  try {
3851
- raw = await import_node_fs16.promises.readFile(outPath, "utf8");
4188
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
3852
4189
  } catch (err) {
3853
4190
  if (err.code === "ENOENT") return;
3854
4191
  throw err;
@@ -3901,19 +4238,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3901
4238
 
3902
4239
  // src/watch.ts
3903
4240
  init_cjs_shims();
3904
- var import_node_fs20 = __toESM(require("fs"), 1);
3905
- var import_node_path34 = __toESM(require("path"), 1);
4241
+ var import_node_fs22 = __toESM(require("fs"), 1);
4242
+ var import_node_path37 = __toESM(require("path"), 1);
3906
4243
  var import_chokidar = __toESM(require("chokidar"), 1);
3907
4244
 
3908
4245
  // src/api.ts
3909
4246
  init_cjs_shims();
3910
4247
  var import_fastify = __toESM(require("fastify"), 1);
3911
4248
  var import_cors = __toESM(require("@fastify/cors"), 1);
3912
- var import_types20 = require("@neat.is/types");
4249
+ var import_types21 = require("@neat.is/types");
3913
4250
 
3914
4251
  // src/divergences.ts
3915
4252
  init_cjs_shims();
3916
- var import_types18 = require("@neat.is/types");
4253
+ var import_types19 = require("@neat.is/types");
3917
4254
  function bucketKey(source, target, type) {
3918
4255
  return `${type}|${source}|${target}`;
3919
4256
  }
@@ -3921,25 +4258,25 @@ function bucketEdges(graph) {
3921
4258
  const buckets = /* @__PURE__ */ new Map();
3922
4259
  graph.forEachEdge((id, attrs) => {
3923
4260
  const e = attrs;
3924
- const parsed = (0, import_types18.parseEdgeId)(id);
4261
+ const parsed = (0, import_types19.parseEdgeId)(id);
3925
4262
  const provenance = parsed?.provenance ?? e.provenance;
3926
4263
  const key = bucketKey(e.source, e.target, e.type);
3927
4264
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3928
4265
  switch (provenance) {
3929
- case import_types18.Provenance.EXTRACTED:
4266
+ case import_types19.Provenance.EXTRACTED:
3930
4267
  cur.extracted = e;
3931
4268
  break;
3932
- case import_types18.Provenance.OBSERVED:
4269
+ case import_types19.Provenance.OBSERVED:
3933
4270
  cur.observed = e;
3934
4271
  break;
3935
- case import_types18.Provenance.INFERRED:
4272
+ case import_types19.Provenance.INFERRED:
3936
4273
  cur.inferred = e;
3937
4274
  break;
3938
- case import_types18.Provenance.FRONTIER:
4275
+ case import_types19.Provenance.FRONTIER:
3939
4276
  cur.frontier = e;
3940
4277
  break;
3941
4278
  default:
3942
- if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
4279
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
3943
4280
  }
3944
4281
  buckets.set(key, cur);
3945
4282
  });
@@ -3948,15 +4285,7 @@ function bucketEdges(graph) {
3948
4285
  function nodeIsFrontier(graph, nodeId) {
3949
4286
  if (!graph.hasNode(nodeId)) return false;
3950
4287
  const attrs = graph.getNodeAttributes(nodeId);
3951
- return attrs.type === import_types18.NodeType.FrontierNode;
3952
- }
3953
- function hasAnyObservedFromSource(graph, sourceId) {
3954
- if (!graph.hasNode(sourceId)) return false;
3955
- for (const edgeId of graph.outboundEdges(sourceId)) {
3956
- const e = graph.getEdgeAttributes(edgeId);
3957
- if (e.provenance === import_types18.Provenance.OBSERVED) return true;
3958
- }
3959
- return false;
4288
+ return attrs.type === import_types19.NodeType.FrontierNode;
3960
4289
  }
3961
4290
  function clampConfidence(n) {
3962
4291
  if (!Number.isFinite(n)) return 0;
@@ -3971,32 +4300,34 @@ function reasonForMissingExtracted(source, target, type) {
3971
4300
  var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
3972
4301
  var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
3973
4302
  var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
4303
+ function gradedConfidence(edge) {
4304
+ if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
4305
+ return clampConfidence(confidenceForEdge(edge));
4306
+ }
3974
4307
  function detectMissingDivergences(graph, bucket) {
3975
4308
  const out = [];
3976
4309
  if (bucket.extracted && !bucket.observed) {
3977
4310
  if (!nodeIsFrontier(graph, bucket.target)) {
3978
- const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
3979
4311
  out.push({
3980
4312
  type: "missing-observed",
3981
4313
  source: bucket.source,
3982
4314
  target: bucket.target,
3983
4315
  edgeType: bucket.type,
3984
4316
  extracted: bucket.extracted,
3985
- confidence: sourceHasTraffic ? 1 : 0.5,
4317
+ confidence: gradedConfidence(bucket.extracted),
3986
4318
  reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
3987
4319
  recommendation: RECOMMENDATION_MISSING_OBSERVED
3988
4320
  });
3989
4321
  }
3990
4322
  }
3991
4323
  if (bucket.observed && !bucket.extracted) {
3992
- const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
3993
4324
  out.push({
3994
4325
  type: "missing-extracted",
3995
4326
  source: bucket.source,
3996
4327
  target: bucket.target,
3997
4328
  edgeType: bucket.type,
3998
4329
  observed: bucket.observed,
3999
- confidence: cascaded,
4330
+ confidence: gradedConfidence(bucket.observed),
4000
4331
  reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
4001
4332
  recommendation: RECOMMENDATION_MISSING_EXTRACTED
4002
4333
  });
@@ -4015,7 +4346,7 @@ function declaredHostFor(svc) {
4015
4346
  function hasExtractedConfiguredBy(graph, svcId) {
4016
4347
  for (const edgeId of graph.outboundEdges(svcId)) {
4017
4348
  const e = graph.getEdgeAttributes(edgeId);
4018
- if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
4349
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4019
4350
  return true;
4020
4351
  }
4021
4352
  }
@@ -4028,10 +4359,10 @@ function detectHostMismatch(graph, svcId, svc) {
4028
4359
  const out = [];
4029
4360
  for (const edgeId of graph.outboundEdges(svcId)) {
4030
4361
  const edge = graph.getEdgeAttributes(edgeId);
4031
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4032
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4362
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4363
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4033
4364
  const target = graph.getNodeAttributes(edge.target);
4034
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4365
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4035
4366
  const observedHost = target.host?.trim();
4036
4367
  if (!observedHost) continue;
4037
4368
  if (observedHost === declaredHost) continue;
@@ -4053,10 +4384,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4053
4384
  const deps = svc.dependencies ?? {};
4054
4385
  for (const edgeId of graph.outboundEdges(svcId)) {
4055
4386
  const edge = graph.getEdgeAttributes(edgeId);
4056
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4057
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4387
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4388
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4058
4389
  const target = graph.getNodeAttributes(edge.target);
4059
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4390
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4060
4391
  for (const pair of compatPairs()) {
4061
4392
  if (pair.engine !== target.engine) continue;
4062
4393
  const declared = deps[pair.driver];
@@ -4117,7 +4448,7 @@ function computeDivergences(graph, opts = {}) {
4117
4448
  }
4118
4449
  graph.forEachNode((nodeId, attrs) => {
4119
4450
  const n = attrs;
4120
- if (n.type !== import_types18.NodeType.ServiceNode) return;
4451
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4121
4452
  const svc = n;
4122
4453
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4123
4454
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4135,13 +4466,22 @@ function computeDivergences(graph, opts = {}) {
4135
4466
  const target = opts.node;
4136
4467
  filtered = filtered.filter((d) => involvesNode(d, target));
4137
4468
  }
4469
+ const TYPE_LEADERSHIP = {
4470
+ "missing-extracted": 0,
4471
+ "missing-observed": 1,
4472
+ "version-mismatch": 2,
4473
+ "host-mismatch": 3,
4474
+ "compat-violation": 4
4475
+ };
4138
4476
  filtered.sort((a, b) => {
4139
4477
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4478
+ const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
4479
+ if (lead !== 0) return lead;
4140
4480
  if (a.type !== b.type) return a.type.localeCompare(b.type);
4141
4481
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4142
4482
  return a.target.localeCompare(b.target);
4143
4483
  });
4144
- return import_types18.DivergenceResultSchema.parse({
4484
+ return import_types19.DivergenceResultSchema.parse({
4145
4485
  divergences: filtered,
4146
4486
  totalAffected: filtered.length,
4147
4487
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4150,7 +4490,7 @@ function computeDivergences(graph, opts = {}) {
4150
4490
 
4151
4491
  // src/diff.ts
4152
4492
  init_cjs_shims();
4153
- var import_node_fs17 = require("fs");
4493
+ var import_node_fs19 = require("fs");
4154
4494
  async function loadSnapshotForDiff(target) {
4155
4495
  if (/^https?:\/\//i.test(target)) {
4156
4496
  const res = await fetch(target);
@@ -4159,7 +4499,7 @@ async function loadSnapshotForDiff(target) {
4159
4499
  }
4160
4500
  return await res.json();
4161
4501
  }
4162
- const raw = await import_node_fs17.promises.readFile(target, "utf8");
4502
+ const raw = await import_node_fs19.promises.readFile(target, "utf8");
4163
4503
  return JSON.parse(raw);
4164
4504
  }
4165
4505
  function indexEntries(entries) {
@@ -4227,23 +4567,23 @@ function canonicalJson(value) {
4227
4567
 
4228
4568
  // src/projects.ts
4229
4569
  init_cjs_shims();
4230
- var import_node_path29 = __toESM(require("path"), 1);
4570
+ var import_node_path32 = __toESM(require("path"), 1);
4231
4571
  function pathsForProject(project, baseDir) {
4232
4572
  if (project === DEFAULT_PROJECT) {
4233
4573
  return {
4234
- snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
4235
- errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
4236
- staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
4237
- embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
4238
- policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
4574
+ snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4575
+ errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4576
+ staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4577
+ embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4578
+ policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
4239
4579
  };
4240
4580
  }
4241
4581
  return {
4242
- snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
4243
- errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
4244
- staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
4245
- embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
4246
- policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
4582
+ snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4583
+ errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4584
+ staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4585
+ embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4586
+ policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
4247
4587
  };
4248
4588
  }
4249
4589
  var Projects = class {
@@ -4279,49 +4619,49 @@ var Projects = class {
4279
4619
 
4280
4620
  // src/registry.ts
4281
4621
  init_cjs_shims();
4282
- var import_node_fs18 = require("fs");
4622
+ var import_node_fs20 = require("fs");
4283
4623
  var import_node_os2 = __toESM(require("os"), 1);
4284
- var import_node_path30 = __toESM(require("path"), 1);
4285
- var import_types19 = require("@neat.is/types");
4624
+ var import_node_path33 = __toESM(require("path"), 1);
4625
+ var import_types20 = require("@neat.is/types");
4286
4626
  var LOCK_TIMEOUT_MS = 5e3;
4287
4627
  var LOCK_RETRY_MS = 50;
4288
4628
  function neatHome() {
4289
4629
  const override = process.env.NEAT_HOME;
4290
- if (override && override.length > 0) return import_node_path30.default.resolve(override);
4291
- return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4630
+ if (override && override.length > 0) return import_node_path33.default.resolve(override);
4631
+ return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
4292
4632
  }
4293
4633
  function registryPath() {
4294
- return import_node_path30.default.join(neatHome(), "projects.json");
4634
+ return import_node_path33.default.join(neatHome(), "projects.json");
4295
4635
  }
4296
4636
  function registryLockPath() {
4297
- return import_node_path30.default.join(neatHome(), "projects.json.lock");
4637
+ return import_node_path33.default.join(neatHome(), "projects.json.lock");
4298
4638
  }
4299
4639
  async function normalizeProjectPath(input) {
4300
- const resolved = import_node_path30.default.resolve(input);
4640
+ const resolved = import_node_path33.default.resolve(input);
4301
4641
  try {
4302
- return await import_node_fs18.promises.realpath(resolved);
4642
+ return await import_node_fs20.promises.realpath(resolved);
4303
4643
  } catch {
4304
4644
  return resolved;
4305
4645
  }
4306
4646
  }
4307
4647
  async function writeAtomically(target, contents) {
4308
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4648
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4309
4649
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4310
- const fd = await import_node_fs18.promises.open(tmp, "w");
4650
+ const fd = await import_node_fs20.promises.open(tmp, "w");
4311
4651
  try {
4312
4652
  await fd.writeFile(contents, "utf8");
4313
4653
  await fd.sync();
4314
4654
  } finally {
4315
4655
  await fd.close();
4316
4656
  }
4317
- await import_node_fs18.promises.rename(tmp, target);
4657
+ await import_node_fs20.promises.rename(tmp, target);
4318
4658
  }
4319
4659
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4320
4660
  const deadline = Date.now() + timeoutMs;
4321
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4661
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
4322
4662
  while (true) {
4323
4663
  try {
4324
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4664
+ const fd = await import_node_fs20.promises.open(lockPath, "wx");
4325
4665
  await fd.close();
4326
4666
  return;
4327
4667
  } catch (err) {
@@ -4337,7 +4677,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4337
4677
  }
4338
4678
  }
4339
4679
  async function releaseLock(lockPath) {
4340
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4680
+ await import_node_fs20.promises.unlink(lockPath).catch(() => {
4341
4681
  });
4342
4682
  }
4343
4683
  async function withLock(fn) {
@@ -4353,7 +4693,7 @@ async function readRegistry() {
4353
4693
  const file = registryPath();
4354
4694
  let raw;
4355
4695
  try {
4356
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4696
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4357
4697
  } catch (err) {
4358
4698
  if (err.code === "ENOENT") {
4359
4699
  return { version: 1, projects: [] };
@@ -4361,10 +4701,10 @@ async function readRegistry() {
4361
4701
  throw err;
4362
4702
  }
4363
4703
  const parsed = JSON.parse(raw);
4364
- return import_types19.RegistryFileSchema.parse(parsed);
4704
+ return import_types20.RegistryFileSchema.parse(parsed);
4365
4705
  }
4366
4706
  async function writeRegistry(reg) {
4367
- const validated = import_types19.RegistryFileSchema.parse(reg);
4707
+ const validated = import_types20.RegistryFileSchema.parse(reg);
4368
4708
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4369
4709
  }
4370
4710
  var ProjectNameCollisionError = class extends Error {
@@ -4615,11 +4955,11 @@ function registerRoutes(scope, ctx) {
4615
4955
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4616
4956
  const parsed = [];
4617
4957
  for (const c of candidates) {
4618
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4958
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4619
4959
  if (!r.success) {
4620
4960
  return reply.code(400).send({
4621
4961
  error: `unknown divergence type "${c}"`,
4622
- allowed: import_types20.DivergenceTypeSchema.options
4962
+ allowed: import_types21.DivergenceTypeSchema.options
4623
4963
  });
4624
4964
  }
4625
4965
  parsed.push(r.data);
@@ -4803,7 +5143,7 @@ function registerRoutes(scope, ctx) {
4803
5143
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4804
5144
  let violations = await log.readAll();
4805
5145
  if (req.query.severity) {
4806
- const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
5146
+ const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
4807
5147
  if (!sev.success) {
4808
5148
  return reply.code(400).send({
4809
5149
  error: "invalid severity",
@@ -4820,7 +5160,7 @@ function registerRoutes(scope, ctx) {
4820
5160
  scope.post("/policies/check", async (req, reply) => {
4821
5161
  const proj = resolveProject(registry, req, reply);
4822
5162
  if (!proj) return;
4823
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5163
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4824
5164
  if (!parsed.success) {
4825
5165
  return reply.code(400).send({
4826
5166
  error: "invalid /policies/check body",
@@ -4918,30 +5258,14 @@ async function buildApi(opts) {
4918
5258
  return app;
4919
5259
  }
4920
5260
 
4921
- // src/extract/retire.ts
4922
- init_cjs_shims();
4923
- var import_types21 = require("@neat.is/types");
4924
- function retireEdgesByFile(graph, file) {
4925
- const normalized = file.split("\\").join("/");
4926
- const toDrop = [];
4927
- graph.forEachEdge((id, attrs) => {
4928
- const edge = attrs;
4929
- if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
4930
- if (!edge.evidence?.file) return;
4931
- if (edge.evidence.file === normalized) toDrop.push(id);
4932
- });
4933
- for (const id of toDrop) graph.dropEdge(id);
4934
- return toDrop.length;
4935
- }
4936
-
4937
5261
  // src/watch.ts
4938
5262
  init_otel();
4939
5263
  init_otel_grpc();
4940
5264
 
4941
5265
  // src/search.ts
4942
5266
  init_cjs_shims();
4943
- var import_node_fs19 = require("fs");
4944
- var import_node_path33 = __toESM(require("path"), 1);
5267
+ var import_node_fs21 = require("fs");
5268
+ var import_node_path36 = __toESM(require("path"), 1);
4945
5269
  var import_node_crypto = require("crypto");
4946
5270
  var DEFAULT_LIMIT = 10;
4947
5271
  var NOMIC_DIM = 768;
@@ -5071,7 +5395,7 @@ async function pickEmbedder() {
5071
5395
  }
5072
5396
  async function readCache(cachePath) {
5073
5397
  try {
5074
- const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
5398
+ const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
5075
5399
  const parsed = JSON.parse(raw);
5076
5400
  if (parsed.version !== 1) return null;
5077
5401
  return parsed;
@@ -5080,8 +5404,8 @@ async function readCache(cachePath) {
5080
5404
  }
5081
5405
  }
5082
5406
  async function writeCache(cachePath, cache) {
5083
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
5084
- await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
5407
+ await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(cachePath), { recursive: true });
5408
+ await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
5085
5409
  }
5086
5410
  var VectorIndex = class {
5087
5411
  constructor(embedder, cachePath) {
@@ -5236,8 +5560,8 @@ var ALL_PHASES = [
5236
5560
  ];
5237
5561
  function classifyChange(relPath) {
5238
5562
  const phases = /* @__PURE__ */ new Set();
5239
- const base = import_node_path34.default.basename(relPath).toLowerCase();
5240
- const segments = relPath.split(import_node_path34.default.sep).map((s) => s.toLowerCase());
5563
+ const base = import_node_path37.default.basename(relPath).toLowerCase();
5564
+ const segments = relPath.split(import_node_path37.default.sep).map((s) => s.toLowerCase());
5241
5565
  if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
5242
5566
  phases.add("services");
5243
5567
  phases.add("aliases");
@@ -5332,16 +5656,16 @@ function countWatchableDirs(scanPath, limit) {
5332
5656
  if (count >= limit) return;
5333
5657
  let entries;
5334
5658
  try {
5335
- entries = import_node_fs20.default.readdirSync(dir, { withFileTypes: true });
5659
+ entries = import_node_fs22.default.readdirSync(dir, { withFileTypes: true });
5336
5660
  } catch {
5337
5661
  return;
5338
5662
  }
5339
5663
  for (const e of entries) {
5340
5664
  if (count >= limit) return;
5341
5665
  if (!e.isDirectory()) continue;
5342
- if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path34.default.join(dir, e.name) + import_node_path34.default.sep))) continue;
5666
+ if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path37.default.join(dir, e.name) + import_node_path37.default.sep))) continue;
5343
5667
  count++;
5344
- if (depth < 2) visit(import_node_path34.default.join(dir, e.name), depth + 1);
5668
+ if (depth < 2) visit(import_node_path37.default.join(dir, e.name), depth + 1);
5345
5669
  }
5346
5670
  };
5347
5671
  visit(scanPath, 0);
@@ -5359,8 +5683,8 @@ async function startWatch(graph, opts) {
5359
5683
  const projectName = opts.project ?? DEFAULT_PROJECT;
5360
5684
  await loadGraphFromDisk(graph, opts.outPath);
5361
5685
  const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
5362
- const policyFilePath = import_node_path34.default.join(opts.scanPath, "policy.json");
5363
- const policyViolationsPath = import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "policy-violations.ndjson");
5686
+ const policyFilePath = import_node_path37.default.join(opts.scanPath, "policy.json");
5687
+ const policyViolationsPath = import_node_path37.default.join(import_node_path37.default.dirname(opts.outPath), "policy-violations.ndjson");
5364
5688
  let policies = [];
5365
5689
  try {
5366
5690
  policies = await loadPolicyFile(policyFilePath);
@@ -5409,7 +5733,7 @@ async function startWatch(graph, opts) {
5409
5733
  const host = opts.host ?? "0.0.0.0";
5410
5734
  const port = opts.port ?? 8080;
5411
5735
  const otelPort = opts.otelPort ?? 4318;
5412
- const cachePath = opts.embeddingsCachePath ?? import_node_path34.default.join(import_node_path34.default.dirname(opts.outPath), "embeddings.json");
5736
+ const cachePath = opts.embeddingsCachePath ?? import_node_path37.default.join(import_node_path37.default.dirname(opts.outPath), "embeddings.json");
5413
5737
  let searchIndex;
5414
5738
  try {
5415
5739
  searchIndex = await buildSearchIndex(graph, { cachePath });
@@ -5427,7 +5751,7 @@ async function startWatch(graph, opts) {
5427
5751
  // Paths are derived from the explicit options the watch caller passes
5428
5752
  // — pathsForProject is only used to fill in the embeddings/snapshot
5429
5753
  // fields so the registry shape is complete.
5430
- ...pathsForProject(projectName, import_node_path34.default.dirname(opts.outPath)),
5754
+ ...pathsForProject(projectName, import_node_path37.default.dirname(opts.outPath)),
5431
5755
  snapshotPath: opts.outPath,
5432
5756
  errorsPath: opts.errorsPath,
5433
5757
  staleEventsPath: opts.staleEventsPath
@@ -5512,9 +5836,9 @@ async function startWatch(graph, opts) {
5512
5836
  };
5513
5837
  const onPath = (absPath) => {
5514
5838
  if (shouldIgnore(absPath)) return;
5515
- const rel = import_node_path34.default.relative(opts.scanPath, absPath);
5839
+ const rel = import_node_path37.default.relative(opts.scanPath, absPath);
5516
5840
  if (!rel || rel.startsWith("..")) return;
5517
- pendingPaths.add(rel.split(import_node_path34.default.sep).join("/"));
5841
+ pendingPaths.add(rel.split(import_node_path37.default.sep).join("/"));
5518
5842
  const phases = classifyChange(rel);
5519
5843
  if (phases.size === 0) {
5520
5844
  for (const p of ALL_PHASES) pending.add(p);
@@ -5571,8 +5895,8 @@ init_cjs_shims();
5571
5895
 
5572
5896
  // src/installers/javascript.ts
5573
5897
  init_cjs_shims();
5574
- var import_node_fs21 = require("fs");
5575
- var import_node_path35 = __toESM(require("path"), 1);
5898
+ var import_node_fs23 = require("fs");
5899
+ var import_node_path38 = __toESM(require("path"), 1);
5576
5900
  var SDK_PACKAGES = [
5577
5901
  { name: "@opentelemetry/api", version: "^1.9.0" },
5578
5902
  { name: "@opentelemetry/sdk-node", version: "^0.57.0" },
@@ -5588,7 +5912,7 @@ var OTEL_ENV = {
5588
5912
  };
5589
5913
  async function readPackageJson(serviceDir) {
5590
5914
  try {
5591
- const raw = await import_node_fs21.promises.readFile(import_node_path35.default.join(serviceDir, "package.json"), "utf8");
5915
+ const raw = await import_node_fs23.promises.readFile(import_node_path38.default.join(serviceDir, "package.json"), "utf8");
5592
5916
  return JSON.parse(raw);
5593
5917
  } catch {
5594
5918
  return null;
@@ -5607,7 +5931,7 @@ function rewriteStartScript(start) {
5607
5931
  }
5608
5932
  async function plan(serviceDir) {
5609
5933
  const pkg = await readPackageJson(serviceDir);
5610
- const manifestPath = import_node_path35.default.join(serviceDir, "package.json");
5934
+ const manifestPath = import_node_path38.default.join(serviceDir, "package.json");
5611
5935
  const empty = {
5612
5936
  language: "javascript",
5613
5937
  serviceDir,
@@ -5654,7 +5978,7 @@ async function apply(installPlan) {
5654
5978
  const originals = /* @__PURE__ */ new Map();
5655
5979
  for (const file of touched) {
5656
5980
  try {
5657
- originals.set(file, await import_node_fs21.promises.readFile(file, "utf8"));
5981
+ originals.set(file, await import_node_fs23.promises.readFile(file, "utf8"));
5658
5982
  } catch {
5659
5983
  }
5660
5984
  }
@@ -5680,8 +6004,8 @@ async function apply(installPlan) {
5680
6004
  }
5681
6005
  const newRaw = JSON.stringify(pkg, null, 2) + "\n";
5682
6006
  const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
5683
- await import_node_fs21.promises.writeFile(tmp, newRaw, "utf8");
5684
- await import_node_fs21.promises.rename(tmp, file);
6007
+ await import_node_fs23.promises.writeFile(tmp, newRaw, "utf8");
6008
+ await import_node_fs23.promises.rename(tmp, file);
5685
6009
  }
5686
6010
  } catch (err) {
5687
6011
  await rollback(installPlan, originals);
@@ -5692,7 +6016,7 @@ async function rollback(installPlan, originals) {
5692
6016
  const restored = [];
5693
6017
  for (const [file, raw] of originals.entries()) {
5694
6018
  try {
5695
- await import_node_fs21.promises.writeFile(file, raw, "utf8");
6019
+ await import_node_fs23.promises.writeFile(file, raw, "utf8");
5696
6020
  restored.push(file);
5697
6021
  } catch {
5698
6022
  }
@@ -5706,8 +6030,8 @@ async function rollback(installPlan, originals) {
5706
6030
  ...restored.map((f) => `restored: ${f}`),
5707
6031
  ""
5708
6032
  ];
5709
- const rollbackPath = import_node_path35.default.join(installPlan.serviceDir, "neat-rollback.patch");
5710
- await import_node_fs21.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6033
+ const rollbackPath = import_node_path38.default.join(installPlan.serviceDir, "neat-rollback.patch");
6034
+ await import_node_fs23.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5711
6035
  }
5712
6036
  var javascriptInstaller = {
5713
6037
  name: "javascript",
@@ -5718,8 +6042,8 @@ var javascriptInstaller = {
5718
6042
 
5719
6043
  // src/installers/python.ts
5720
6044
  init_cjs_shims();
5721
- var import_node_fs22 = require("fs");
5722
- var import_node_path36 = __toESM(require("path"), 1);
6045
+ var import_node_fs24 = require("fs");
6046
+ var import_node_path39 = __toESM(require("path"), 1);
5723
6047
  var SDK_PACKAGES2 = [
5724
6048
  { name: "opentelemetry-distro", version: ">=0.49b0" },
5725
6049
  { name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
@@ -5731,7 +6055,7 @@ var OTEL_ENV2 = {
5731
6055
  };
5732
6056
  async function exists2(p) {
5733
6057
  try {
5734
- await import_node_fs22.promises.stat(p);
6058
+ await import_node_fs24.promises.stat(p);
5735
6059
  return true;
5736
6060
  } catch {
5737
6061
  return false;
@@ -5740,7 +6064,7 @@ async function exists2(p) {
5740
6064
  async function detect2(serviceDir) {
5741
6065
  const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
5742
6066
  for (const m of markers) {
5743
- if (await exists2(import_node_path36.default.join(serviceDir, m))) return true;
6067
+ if (await exists2(import_node_path39.default.join(serviceDir, m))) return true;
5744
6068
  }
5745
6069
  return false;
5746
6070
  }
@@ -5750,9 +6074,9 @@ function reqPackageName(line) {
5750
6074
  return head.replace(/[<>=!~].*$/, "").toLowerCase();
5751
6075
  }
5752
6076
  async function planRequirementsTxtEdits(serviceDir) {
5753
- const file = import_node_path36.default.join(serviceDir, "requirements.txt");
6077
+ const file = import_node_path39.default.join(serviceDir, "requirements.txt");
5754
6078
  if (!await exists2(file)) return null;
5755
- const raw = await import_node_fs22.promises.readFile(file, "utf8");
6079
+ const raw = await import_node_fs24.promises.readFile(file, "utf8");
5756
6080
  const presentNames = new Set(
5757
6081
  raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
5758
6082
  );
@@ -5760,9 +6084,9 @@ async function planRequirementsTxtEdits(serviceDir) {
5760
6084
  return { manifest: file, missing: [...missing] };
5761
6085
  }
5762
6086
  async function planProcfileEdits(serviceDir) {
5763
- const procfile = import_node_path36.default.join(serviceDir, "Procfile");
6087
+ const procfile = import_node_path39.default.join(serviceDir, "Procfile");
5764
6088
  if (!await exists2(procfile)) return [];
5765
- const raw = await import_node_fs22.promises.readFile(procfile, "utf8");
6089
+ const raw = await import_node_fs24.promises.readFile(procfile, "utf8");
5766
6090
  const edits = [];
5767
6091
  for (const line of raw.split(/\r?\n/)) {
5768
6092
  if (line.length === 0) continue;
@@ -5814,8 +6138,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
5814
6138
  const next = `${original}${trailing}${newlines.join("\n")}
5815
6139
  `;
5816
6140
  const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
5817
- await import_node_fs22.promises.writeFile(tmp, next, "utf8");
5818
- await import_node_fs22.promises.rename(tmp, manifest);
6141
+ await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6142
+ await import_node_fs24.promises.rename(tmp, manifest);
5819
6143
  }
5820
6144
  async function applyProcfile(procfile, edits, original) {
5821
6145
  let next = original;
@@ -5824,8 +6148,8 @@ async function applyProcfile(procfile, edits, original) {
5824
6148
  next = next.replace(e.before, e.after);
5825
6149
  }
5826
6150
  const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
5827
- await import_node_fs22.promises.writeFile(tmp, next, "utf8");
5828
- await import_node_fs22.promises.rename(tmp, procfile);
6151
+ await import_node_fs24.promises.writeFile(tmp, next, "utf8");
6152
+ await import_node_fs24.promises.rename(tmp, procfile);
5829
6153
  }
5830
6154
  async function apply2(installPlan) {
5831
6155
  const touched = /* @__PURE__ */ new Set();
@@ -5835,7 +6159,7 @@ async function apply2(installPlan) {
5835
6159
  const originals = /* @__PURE__ */ new Map();
5836
6160
  for (const file of touched) {
5837
6161
  try {
5838
- originals.set(file, await import_node_fs22.promises.readFile(file, "utf8"));
6162
+ originals.set(file, await import_node_fs24.promises.readFile(file, "utf8"));
5839
6163
  } catch {
5840
6164
  }
5841
6165
  }
@@ -5845,7 +6169,7 @@ async function apply2(installPlan) {
5845
6169
  if (raw === void 0) {
5846
6170
  throw new Error(`python installer: cannot read ${file} during apply`);
5847
6171
  }
5848
- const base = import_node_path36.default.basename(file);
6172
+ const base = import_node_path39.default.basename(file);
5849
6173
  if (base === "requirements.txt") {
5850
6174
  const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
5851
6175
  if (edits.length > 0) await applyRequirementsTxt(file, edits, raw);
@@ -5863,7 +6187,7 @@ async function rollback2(installPlan, originals) {
5863
6187
  const restored = [];
5864
6188
  for (const [file, raw] of originals.entries()) {
5865
6189
  try {
5866
- await import_node_fs22.promises.writeFile(file, raw, "utf8");
6190
+ await import_node_fs24.promises.writeFile(file, raw, "utf8");
5867
6191
  restored.push(file);
5868
6192
  } catch {
5869
6193
  }
@@ -5877,8 +6201,8 @@ async function rollback2(installPlan, originals) {
5877
6201
  ...restored.map((f) => `restored: ${f}`),
5878
6202
  ""
5879
6203
  ];
5880
- const rollbackPath = import_node_path36.default.join(installPlan.serviceDir, "neat-rollback.patch");
5881
- await import_node_fs22.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
6204
+ const rollbackPath = import_node_path39.default.join(installPlan.serviceDir, "neat-rollback.patch");
6205
+ await import_node_fs24.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
5882
6206
  }
5883
6207
  var pythonInstaller = {
5884
6208
  name: "python",
@@ -5991,10 +6315,10 @@ var TransportError = class extends Error {
5991
6315
  function createHttpClient(baseUrl) {
5992
6316
  const root = baseUrl.replace(/\/$/, "");
5993
6317
  return {
5994
- async get(path38) {
6318
+ async get(path41) {
5995
6319
  let res;
5996
6320
  try {
5997
- res = await fetch(`${root}${path38}`);
6321
+ res = await fetch(`${root}${path41}`);
5998
6322
  } catch (err) {
5999
6323
  throw new TransportError(
6000
6324
  `cannot reach neat-core at ${root}: ${err.message}`
@@ -6004,16 +6328,16 @@ function createHttpClient(baseUrl) {
6004
6328
  const body = await res.text().catch(() => "");
6005
6329
  throw new HttpError(
6006
6330
  res.status,
6007
- `${res.status} ${res.statusText} on GET ${path38}: ${body}`,
6331
+ `${res.status} ${res.statusText} on GET ${path41}: ${body}`,
6008
6332
  body
6009
6333
  );
6010
6334
  }
6011
6335
  return await res.json();
6012
6336
  },
6013
- async post(path38, body) {
6337
+ async post(path41, body) {
6014
6338
  let res;
6015
6339
  try {
6016
- res = await fetch(`${root}${path38}`, {
6340
+ res = await fetch(`${root}${path41}`, {
6017
6341
  method: "POST",
6018
6342
  headers: { "content-type": "application/json" },
6019
6343
  body: JSON.stringify(body)
@@ -6027,7 +6351,7 @@ function createHttpClient(baseUrl) {
6027
6351
  const text = await res.text().catch(() => "");
6028
6352
  throw new HttpError(
6029
6353
  res.status,
6030
- `${res.status} ${res.statusText} on POST ${path38}: ${text}`,
6354
+ `${res.status} ${res.statusText} on POST ${path41}: ${text}`,
6031
6355
  text
6032
6356
  );
6033
6357
  }
@@ -6041,12 +6365,12 @@ function projectPath(project, suffix) {
6041
6365
  }
6042
6366
  async function runRootCause(client, input) {
6043
6367
  const qs = input.errorId ? `?errorId=${encodeURIComponent(input.errorId)}` : "";
6044
- const path38 = projectPath(
6368
+ const path41 = projectPath(
6045
6369
  input.project,
6046
6370
  `/graph/root-cause/${encodeURIComponent(input.errorNode)}${qs}`
6047
6371
  );
6048
6372
  try {
6049
- const result = await client.get(path38);
6373
+ const result = await client.get(path41);
6050
6374
  const arrowPath = result.traversalPath.join(" \u2190 ");
6051
6375
  const provenances = result.edgeProvenances.length ? result.edgeProvenances.join(", ") : "(direct, no edges traversed)";
6052
6376
  const summary = `Root cause for ${input.errorNode} is ${result.rootCauseNode}. ` + result.rootCauseReason + (result.fixRecommendation ? ` Recommended fix: ${result.fixRecommendation}.` : "");
@@ -6072,12 +6396,12 @@ async function runRootCause(client, input) {
6072
6396
  }
6073
6397
  async function runBlastRadius(client, input) {
6074
6398
  const qs = input.depth !== void 0 ? `?depth=${input.depth}` : "";
6075
- const path38 = projectPath(
6399
+ const path41 = projectPath(
6076
6400
  input.project,
6077
6401
  `/graph/blast-radius/${encodeURIComponent(input.nodeId)}${qs}`
6078
6402
  );
6079
6403
  try {
6080
- const result = await client.get(path38);
6404
+ const result = await client.get(path41);
6081
6405
  if (result.totalAffected === 0) {
6082
6406
  return {
6083
6407
  summary: `${result.origin} has no downstream dependencies. Nothing else would break if it failed.`
@@ -6111,12 +6435,12 @@ function formatBlastEntry(n) {
6111
6435
  }
6112
6436
  async function runDependencies(client, input) {
6113
6437
  const depth = input.depth ?? 3;
6114
- const path38 = projectPath(
6438
+ const path41 = projectPath(
6115
6439
  input.project,
6116
6440
  `/graph/dependencies/${encodeURIComponent(input.nodeId)}?depth=${depth}`
6117
6441
  );
6118
6442
  try {
6119
- const result = await client.get(path38);
6443
+ const result = await client.get(path41);
6120
6444
  if (result.total === 0) {
6121
6445
  return {
6122
6446
  summary: depth === 1 ? `${input.nodeId} has no direct dependencies in the graph.` : `${input.nodeId} has no dependencies (BFS to depth ${depth}).`
@@ -6197,9 +6521,9 @@ function formatDuration(ms) {
6197
6521
  return `${Math.round(h / 24)}d`;
6198
6522
  }
6199
6523
  async function runIncidents(client, input) {
6200
- const path38 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6524
+ const path41 = input.nodeId ? projectPath(input.project, `/incidents/${encodeURIComponent(input.nodeId)}`) : projectPath(input.project, "/incidents");
6201
6525
  try {
6202
- const body = await client.get(path38);
6526
+ const body = await client.get(path41);
6203
6527
  const events = body.events;
6204
6528
  if (events.length === 0) {
6205
6529
  return {
@@ -6714,7 +7038,7 @@ async function buildPatchSections(services) {
6714
7038
  }
6715
7039
  async function runInit(opts) {
6716
7040
  const written = [];
6717
- const stat = await import_node_fs23.promises.stat(opts.scanPath).catch(() => null);
7041
+ const stat = await import_node_fs25.promises.stat(opts.scanPath).catch(() => null);
6718
7042
  if (!stat || !stat.isDirectory()) {
6719
7043
  console.error(`neat init: ${opts.scanPath} is not a directory`);
6720
7044
  return { exitCode: 2, writtenFiles: written };
@@ -6723,9 +7047,9 @@ async function runInit(opts) {
6723
7047
  printDiscoveryReport(opts, services);
6724
7048
  const sections = opts.noInstall ? [] : await buildPatchSections(services);
6725
7049
  const patch = renderPatch(sections);
6726
- const patchPath = import_node_path37.default.join(opts.scanPath, "neat.patch");
7050
+ const patchPath = import_node_path40.default.join(opts.scanPath, "neat.patch");
6727
7051
  if (opts.dryRun) {
6728
- await import_node_fs23.promises.writeFile(patchPath, patch, "utf8");
7052
+ await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
6729
7053
  written.push(patchPath);
6730
7054
  console.log(`dry-run: patch written to ${patchPath}`);
6731
7055
  console.log("rerun without --dry-run to register and snapshot.");
@@ -6734,7 +7058,12 @@ async function runInit(opts) {
6734
7058
  const graphKey = opts.projectExplicit ? opts.project : DEFAULT_PROJECT;
6735
7059
  resetGraph(graphKey);
6736
7060
  const graph = getGraph(graphKey);
6737
- const result = await extractFromDirectory(graph, opts.scanPath);
7061
+ const projectPaths = pathsForProject(
7062
+ graphKey,
7063
+ import_node_path40.default.join(opts.scanPath, "neat-out")
7064
+ );
7065
+ const errorsPath = import_node_path40.default.join(import_node_path40.default.dirname(opts.outPath), import_node_path40.default.basename(projectPaths.errorsPath));
7066
+ const result = await extractFromDirectory(graph, opts.scanPath, { errorsPath });
6738
7067
  await saveGraphToDisk(graph, opts.outPath);
6739
7068
  written.push(opts.outPath);
6740
7069
  const languages = [...new Set(services.map((s) => s.node.language))].sort();
@@ -6765,7 +7094,7 @@ async function runInit(opts) {
6765
7094
  console.log("patch applied. Run `npm install` (or your language equivalent) to refresh lockfiles.");
6766
7095
  }
6767
7096
  } else {
6768
- await import_node_fs23.promises.writeFile(patchPath, patch, "utf8");
7097
+ await import_node_fs25.promises.writeFile(patchPath, patch, "utf8");
6769
7098
  written.push(patchPath);
6770
7099
  }
6771
7100
  }
@@ -6779,6 +7108,11 @@ async function runInit(opts) {
6779
7108
  console.log(`added: ${result.nodesAdded} nodes, ${result.edgesAdded} edges`);
6780
7109
  console.log(`total: ${graph.order} nodes, ${graph.size} edges`);
6781
7110
  console.log(summarise(nodes, edges));
7111
+ console.log(formatExtractionBanner(result.extractionErrors));
7112
+ if (result.extractionErrors > 0) {
7113
+ console.log(`errors: ${errorsPath}`);
7114
+ }
7115
+ console.log(formatPrecisionFloorBanner(result.extractedDropped));
6782
7116
  const incompatibilities = findIncompatibilities(nodes);
6783
7117
  if (incompatibilities.length > 0) {
6784
7118
  console.log("");
@@ -6789,6 +7123,9 @@ async function runInit(opts) {
6789
7123
  }
6790
7124
  }
6791
7125
  }
7126
+ if (result.extractionErrors > 0 && isStrictExtractionEnabled()) {
7127
+ return { exitCode: 4, writtenFiles: written };
7128
+ }
6792
7129
  return { exitCode: 0, writtenFiles: written };
6793
7130
  }
6794
7131
  var CLAUDE_SKILL_CONFIG = {
@@ -6805,9 +7142,9 @@ var CLAUDE_SKILL_CONFIG = {
6805
7142
  };
6806
7143
  function claudeConfigPath() {
6807
7144
  const override = process.env.NEAT_CLAUDE_CONFIG;
6808
- if (override && override.length > 0) return import_node_path37.default.resolve(override);
7145
+ if (override && override.length > 0) return import_node_path40.default.resolve(override);
6809
7146
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6810
- return import_node_path37.default.join(home, ".claude.json");
7147
+ return import_node_path40.default.join(home, ".claude.json");
6811
7148
  }
6812
7149
  async function runSkill(opts) {
6813
7150
  const snippet2 = JSON.stringify(CLAUDE_SKILL_CONFIG, null, 2) + "\n";
@@ -6819,7 +7156,7 @@ async function runSkill(opts) {
6819
7156
  const target = claudeConfigPath();
6820
7157
  let existing = {};
6821
7158
  try {
6822
- existing = JSON.parse(await import_node_fs23.promises.readFile(target, "utf8"));
7159
+ existing = JSON.parse(await import_node_fs25.promises.readFile(target, "utf8"));
6823
7160
  } catch (err) {
6824
7161
  if (err.code !== "ENOENT") {
6825
7162
  console.error(`neat skill: failed to read ${target} \u2014 ${err.message}`);
@@ -6831,8 +7168,8 @@ async function runSkill(opts) {
6831
7168
  ...existing,
6832
7169
  mcpServers: { ...mcp, neat: CLAUDE_SKILL_CONFIG.mcpServers.neat }
6833
7170
  };
6834
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6835
- await import_node_fs23.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
7171
+ await import_node_fs25.promises.mkdir(import_node_path40.default.dirname(target), { recursive: true });
7172
+ await import_node_fs25.promises.writeFile(target, JSON.stringify(merged, null, 2) + "\n", "utf8");
6836
7173
  console.log(`neat skill: wrote mcpServers.neat to ${target}`);
6837
7174
  console.log("restart Claude Code to pick up the new MCP server.");
6838
7175
  return { exitCode: 0 };
@@ -6866,12 +7203,12 @@ async function main() {
6866
7203
  console.error("neat init: --apply and --dry-run are mutually exclusive");
6867
7204
  process.exit(2);
6868
7205
  }
6869
- const scanPath = import_node_path37.default.resolve(target);
7206
+ const scanPath = import_node_path40.default.resolve(target);
6870
7207
  const projectExplicit = parsed.project !== null;
6871
- const projectName = projectExplicit ? project : import_node_path37.default.basename(scanPath);
7208
+ const projectName = projectExplicit ? project : import_node_path40.default.basename(scanPath);
6872
7209
  const projectKey = projectExplicit ? project : DEFAULT_PROJECT;
6873
- const fallback = pathsForProject(projectKey, import_node_path37.default.join(scanPath, "neat-out")).snapshotPath;
6874
- const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
7210
+ const fallback = pathsForProject(projectKey, import_node_path40.default.join(scanPath, "neat-out")).snapshotPath;
7211
+ const outPath = import_node_path40.default.resolve(process.env.NEAT_OUT_PATH ?? fallback);
6875
7212
  const result = await runInit({
6876
7213
  scanPath,
6877
7214
  outPath,
@@ -6891,21 +7228,21 @@ async function main() {
6891
7228
  usage();
6892
7229
  process.exit(2);
6893
7230
  }
6894
- const scanPath = import_node_path37.default.resolve(target);
6895
- const stat = await import_node_fs23.promises.stat(scanPath).catch(() => null);
7231
+ const scanPath = import_node_path40.default.resolve(target);
7232
+ const stat = await import_node_fs25.promises.stat(scanPath).catch(() => null);
6896
7233
  if (!stat || !stat.isDirectory()) {
6897
7234
  console.error(`neat watch: ${scanPath} is not a directory`);
6898
7235
  process.exit(2);
6899
7236
  }
6900
- const projectPaths = pathsForProject(project, import_node_path37.default.join(scanPath, "neat-out"));
6901
- const outPath = import_node_path37.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
6902
- const errorsPath = import_node_path37.default.resolve(
6903
- process.env.NEAT_ERRORS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.errorsPath))
7237
+ const projectPaths = pathsForProject(project, import_node_path40.default.join(scanPath, "neat-out"));
7238
+ const outPath = import_node_path40.default.resolve(process.env.NEAT_OUT_PATH ?? projectPaths.snapshotPath);
7239
+ const errorsPath = import_node_path40.default.resolve(
7240
+ process.env.NEAT_ERRORS_PATH ?? import_node_path40.default.join(import_node_path40.default.dirname(outPath), import_node_path40.default.basename(projectPaths.errorsPath))
6904
7241
  );
6905
- const staleEventsPath = import_node_path37.default.resolve(
6906
- process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path37.default.join(import_node_path37.default.dirname(outPath), import_node_path37.default.basename(projectPaths.staleEventsPath))
7242
+ const staleEventsPath = import_node_path40.default.resolve(
7243
+ process.env.NEAT_STALE_EVENTS_PATH ?? import_node_path40.default.join(import_node_path40.default.dirname(outPath), import_node_path40.default.basename(projectPaths.staleEventsPath))
6907
7244
  );
6908
- const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path37.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
7245
+ const embeddingsCachePath = process.env.NEAT_EMBEDDINGS_CACHE_PATH ? import_node_path40.default.resolve(process.env.NEAT_EMBEDDINGS_CACHE_PATH) : void 0;
6909
7246
  const handle = await startWatch(getGraph(project), {
6910
7247
  scanPath,
6911
7248
  outPath,