@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/server.cjs CHANGED
@@ -99,8 +99,8 @@ function reshapeGrpcRequest(req) {
99
99
  };
100
100
  }
101
101
  function resolveProtoRoot() {
102
- const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
- return import_node_path31.default.resolve(here, "..", "proto");
102
+ const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
103
+ return import_node_path34.default.resolve(here, "..", "proto");
104
104
  }
105
105
  function loadTraceService() {
106
106
  const protoRoot = resolveProtoRoot();
@@ -155,13 +155,13 @@ async function startOtelGrpcReceiver(opts) {
155
155
  })
156
156
  };
157
157
  }
158
- var import_node_url, import_node_path31, grpc, protoLoader;
158
+ var import_node_url, import_node_path34, grpc, protoLoader;
159
159
  var init_otel_grpc = __esm({
160
160
  "src/otel-grpc.ts"() {
161
161
  "use strict";
162
162
  init_cjs_shims();
163
163
  import_node_url = require("url");
164
- import_node_path31 = __toESM(require("path"), 1);
164
+ import_node_path34 = __toESM(require("path"), 1);
165
165
  grpc = __toESM(require("@grpc/grpc-js"), 1);
166
166
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
167
167
  init_otel();
@@ -258,10 +258,10 @@ function parseOtlpRequest(body) {
258
258
  }
259
259
  function loadProtobufDecoder() {
260
260
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
261
- const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
262
- const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
261
+ const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
262
+ const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
263
263
  const root = new import_protobufjs.default.Root();
264
- root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
264
+ root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
265
265
  root.loadSync(
266
266
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
267
267
  { keepCase: true }
@@ -353,12 +353,12 @@ async function buildOtelReceiver(opts) {
353
353
  };
354
354
  return decorated;
355
355
  }
356
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
356
+ var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
357
357
  var init_otel = __esm({
358
358
  "src/otel.ts"() {
359
359
  "use strict";
360
360
  init_cjs_shims();
361
- import_node_path32 = __toESM(require("path"), 1);
361
+ import_node_path35 = __toESM(require("path"), 1);
362
362
  import_node_url2 = require("url");
363
363
  import_fastify2 = __toESM(require("fastify"), 1);
364
364
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -368,7 +368,7 @@ var init_otel = __esm({
368
368
 
369
369
  // src/server.ts
370
370
  init_cjs_shims();
371
- var import_node_path34 = __toESM(require("path"), 1);
371
+ var import_node_path37 = __toESM(require("path"), 1);
372
372
 
373
373
  // src/graph.ts
374
374
  init_cjs_shims();
@@ -392,7 +392,7 @@ function getGraph(project = DEFAULT_PROJECT) {
392
392
  init_cjs_shims();
393
393
  var import_fastify = __toESM(require("fastify"), 1);
394
394
  var import_cors = __toESM(require("@fastify/cors"), 1);
395
- var import_types20 = require("@neat.is/types");
395
+ var import_types21 = require("@neat.is/types");
396
396
 
397
397
  // src/divergences.ts
398
398
  init_cjs_shims();
@@ -798,19 +798,19 @@ function confidenceFromMix(edges, now = Date.now()) {
798
798
  function longestIncomingWalk(graph, start, maxDepth) {
799
799
  let best = { path: [start], edges: [] };
800
800
  const visited = /* @__PURE__ */ new Set([start]);
801
- function step(node, path35, edges) {
802
- if (path35.length > best.path.length) {
803
- best = { path: [...path35], edges: [...edges] };
801
+ function step(node, path38, edges) {
802
+ if (path38.length > best.path.length) {
803
+ best = { path: [...path38], edges: [...edges] };
804
804
  }
805
- if (path35.length - 1 >= maxDepth) return;
805
+ if (path38.length - 1 >= maxDepth) return;
806
806
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
807
807
  for (const [srcId, edge] of incoming) {
808
808
  if (visited.has(srcId)) continue;
809
809
  visited.add(srcId);
810
- path35.push(srcId);
810
+ path38.push(srcId);
811
811
  edges.push(edge);
812
- step(srcId, path35, edges);
813
- path35.pop();
812
+ step(srcId, path38, edges);
813
+ path38.pop();
814
814
  edges.pop();
815
815
  visited.delete(srcId);
816
816
  }
@@ -1029,14 +1029,6 @@ function nodeIsFrontier(graph, nodeId) {
1029
1029
  const attrs = graph.getNodeAttributes(nodeId);
1030
1030
  return attrs.type === import_types2.NodeType.FrontierNode;
1031
1031
  }
1032
- function hasAnyObservedFromSource(graph, sourceId) {
1033
- if (!graph.hasNode(sourceId)) return false;
1034
- for (const edgeId of graph.outboundEdges(sourceId)) {
1035
- const e = graph.getEdgeAttributes(edgeId);
1036
- if (e.provenance === import_types2.Provenance.OBSERVED) return true;
1037
- }
1038
- return false;
1039
- }
1040
1032
  function clampConfidence(n) {
1041
1033
  if (!Number.isFinite(n)) return 0;
1042
1034
  return Math.max(0, Math.min(1, n));
@@ -1050,32 +1042,34 @@ function reasonForMissingExtracted(source, target, type) {
1050
1042
  var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
1051
1043
  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.";
1052
1044
  var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
1045
+ function gradedConfidence(edge) {
1046
+ if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
1047
+ return clampConfidence(confidenceForEdge(edge));
1048
+ }
1053
1049
  function detectMissingDivergences(graph, bucket) {
1054
1050
  const out = [];
1055
1051
  if (bucket.extracted && !bucket.observed) {
1056
1052
  if (!nodeIsFrontier(graph, bucket.target)) {
1057
- const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
1058
1053
  out.push({
1059
1054
  type: "missing-observed",
1060
1055
  source: bucket.source,
1061
1056
  target: bucket.target,
1062
1057
  edgeType: bucket.type,
1063
1058
  extracted: bucket.extracted,
1064
- confidence: sourceHasTraffic ? 1 : 0.5,
1059
+ confidence: gradedConfidence(bucket.extracted),
1065
1060
  reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
1066
1061
  recommendation: RECOMMENDATION_MISSING_OBSERVED
1067
1062
  });
1068
1063
  }
1069
1064
  }
1070
1065
  if (bucket.observed && !bucket.extracted) {
1071
- const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
1072
1066
  out.push({
1073
1067
  type: "missing-extracted",
1074
1068
  source: bucket.source,
1075
1069
  target: bucket.target,
1076
1070
  edgeType: bucket.type,
1077
1071
  observed: bucket.observed,
1078
- confidence: cascaded,
1072
+ confidence: gradedConfidence(bucket.observed),
1079
1073
  reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
1080
1074
  recommendation: RECOMMENDATION_MISSING_EXTRACTED
1081
1075
  });
@@ -1214,8 +1208,17 @@ function computeDivergences(graph, opts = {}) {
1214
1208
  const target = opts.node;
1215
1209
  filtered = filtered.filter((d) => involvesNode(d, target));
1216
1210
  }
1211
+ const TYPE_LEADERSHIP = {
1212
+ "missing-extracted": 0,
1213
+ "missing-observed": 1,
1214
+ "version-mismatch": 2,
1215
+ "host-mismatch": 3,
1216
+ "compat-violation": 4
1217
+ };
1217
1218
  filtered.sort((a, b) => {
1218
1219
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
1220
+ const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
1221
+ if (lead !== 0) return lead;
1219
1222
  if (a.type !== b.type) return a.type.localeCompare(b.type);
1220
1223
  if (a.source !== b.source) return a.source.localeCompare(b.source);
1221
1224
  return a.target.localeCompare(b.target);
@@ -1761,35 +1764,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1761
1764
  const existing = graph.getEdgeAttributes(id);
1762
1765
  const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
1763
1766
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
1767
+ const newSignal = {
1768
+ spanCount: newSpanCount,
1769
+ errorCount: newErrorCount,
1770
+ lastObservedAgeMs: 0
1771
+ };
1764
1772
  const updated = {
1765
1773
  ...existing,
1766
1774
  provenance: import_types4.Provenance.OBSERVED,
1767
1775
  lastObserved: ts,
1768
1776
  callCount: newSpanCount,
1769
- signal: {
1770
- spanCount: newSpanCount,
1771
- errorCount: newErrorCount,
1772
- lastObservedAgeMs: 0
1773
- },
1774
- confidence: 1
1777
+ signal: newSignal,
1778
+ confidence: (0, import_types4.confidenceForObservedSignal)(newSignal)
1775
1779
  };
1776
1780
  graph.replaceEdgeAttributes(id, updated);
1777
1781
  return { edge: updated, created: false };
1778
1782
  }
1783
+ const signal = {
1784
+ spanCount: 1,
1785
+ errorCount: isError ? 1 : 0,
1786
+ lastObservedAgeMs: 0
1787
+ };
1779
1788
  const edge = {
1780
1789
  id,
1781
1790
  source,
1782
1791
  target,
1783
1792
  type,
1784
1793
  provenance: import_types4.Provenance.OBSERVED,
1785
- confidence: 1,
1794
+ confidence: (0, import_types4.confidenceForObservedSignal)(signal),
1786
1795
  lastObserved: ts,
1787
1796
  callCount: 1,
1788
- signal: {
1789
- spanCount: 1,
1790
- errorCount: isError ? 1 : 0,
1791
- lastObservedAgeMs: 0
1792
- }
1797
+ signal
1793
1798
  };
1794
1799
  graph.addEdgeWithKey(id, source, target, edge);
1795
1800
  return { edge, created: true };
@@ -2086,8 +2091,8 @@ async function readErrorEvents(errorsPath) {
2086
2091
 
2087
2092
  // src/extract/services.ts
2088
2093
  init_cjs_shims();
2089
- var import_node_fs7 = require("fs");
2090
- var import_node_path7 = __toESM(require("path"), 1);
2094
+ var import_node_fs8 = require("fs");
2095
+ var import_node_path8 = __toESM(require("path"), 1);
2091
2096
  var import_ignore = __toESM(require("ignore"), 1);
2092
2097
  var import_minimatch2 = require("minimatch");
2093
2098
  var import_types6 = require("@neat.is/types");
@@ -2111,9 +2116,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
2111
2116
  function isConfigFile(name) {
2112
2117
  const ext = import_node_path4.default.extname(name);
2113
2118
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
2114
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
2119
+ if (name === ".env" || name.startsWith(".env.")) {
2120
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
2121
+ return { match: true, fileType: "env" };
2122
+ }
2115
2123
  return { match: false, fileType: "" };
2116
2124
  }
2125
+ function isTestPath(filePath) {
2126
+ const normalised = filePath.replace(/\\/g, "/");
2127
+ const segments = normalised.split("/");
2128
+ for (const seg of segments) {
2129
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
2130
+ return true;
2131
+ }
2132
+ }
2133
+ const base = segments[segments.length - 1] ?? "";
2134
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
2135
+ }
2136
+ function isEnvTemplateFile(name) {
2137
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
2138
+ return true;
2139
+ }
2140
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
2141
+ }
2142
+ function maskCommentsInSource(src) {
2143
+ const len = src.length;
2144
+ const out = new Array(len);
2145
+ let i = 0;
2146
+ let inString = 0;
2147
+ let escaped = false;
2148
+ while (i < len) {
2149
+ const c = src[i];
2150
+ if (inString !== 0) {
2151
+ out[i] = c;
2152
+ if (escaped) {
2153
+ escaped = false;
2154
+ } else if (c === "\\") {
2155
+ escaped = true;
2156
+ } else if (c === inString) {
2157
+ inString = 0;
2158
+ }
2159
+ i++;
2160
+ continue;
2161
+ }
2162
+ if (c === "/" && i + 1 < len) {
2163
+ const next = src[i + 1];
2164
+ if (next === "/") {
2165
+ out[i] = " ";
2166
+ out[i + 1] = " ";
2167
+ let j = i + 2;
2168
+ while (j < len && src[j] !== "\n") {
2169
+ out[j] = " ";
2170
+ j++;
2171
+ }
2172
+ i = j;
2173
+ continue;
2174
+ }
2175
+ if (next === "*") {
2176
+ out[i] = " ";
2177
+ out[i + 1] = " ";
2178
+ let j = i + 2;
2179
+ while (j < len) {
2180
+ if (src[j] === "\n") {
2181
+ out[j] = "\n";
2182
+ j++;
2183
+ continue;
2184
+ }
2185
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
2186
+ out[j] = " ";
2187
+ out[j + 1] = " ";
2188
+ j += 2;
2189
+ break;
2190
+ }
2191
+ out[j] = " ";
2192
+ j++;
2193
+ }
2194
+ i = j;
2195
+ continue;
2196
+ }
2197
+ }
2198
+ out[i] = c;
2199
+ if (c === "'" || c === '"' || c === "`") inString = c;
2200
+ i++;
2201
+ }
2202
+ return out.join("");
2203
+ }
2204
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
2205
+ function urlMatchesHost(urlString, host) {
2206
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
2207
+ if (!URL_LIKE.test(urlString)) return false;
2208
+ const [wantedHost, wantedPort] = host.split(":");
2209
+ let parsed;
2210
+ try {
2211
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
2212
+ parsed = new URL(candidate);
2213
+ } catch {
2214
+ return false;
2215
+ }
2216
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
2217
+ if (wantedPort && parsed.port !== wantedPort) return false;
2218
+ return true;
2219
+ }
2117
2220
  function cleanVersion(raw) {
2118
2221
  if (!raw) return void 0;
2119
2222
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -2269,6 +2372,50 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2269
2372
  return author ?? void 0;
2270
2373
  }
2271
2374
 
2375
+ // src/extract/errors.ts
2376
+ init_cjs_shims();
2377
+ var import_node_fs7 = require("fs");
2378
+ var import_node_path7 = __toESM(require("path"), 1);
2379
+ var sink = [];
2380
+ function recordExtractionError(producer, file, err) {
2381
+ const e = err instanceof Error ? err : new Error(String(err));
2382
+ sink.push({
2383
+ producer,
2384
+ file,
2385
+ error: e.message,
2386
+ stack: e.stack,
2387
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2388
+ source: "extract"
2389
+ });
2390
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2391
+ }
2392
+ function drainExtractionErrors() {
2393
+ return sink.splice(0, sink.length);
2394
+ }
2395
+ async function writeExtractionErrors(errors, errorsPath) {
2396
+ if (errors.length === 0) return;
2397
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2398
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2399
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2400
+ }
2401
+ var droppedSink = [];
2402
+ function noteExtractedDropped(edge) {
2403
+ droppedSink.push(edge);
2404
+ }
2405
+ function drainDroppedExtracted() {
2406
+ return droppedSink.splice(0, droppedSink.length);
2407
+ }
2408
+ function isRejectedLogEnabled() {
2409
+ const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
2410
+ return raw === "1" || raw === "true";
2411
+ }
2412
+ async function writeRejectedExtracted(drops, rejectedPath) {
2413
+ if (drops.length === 0) return;
2414
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
2415
+ const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
2416
+ await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
2417
+ }
2418
+
2272
2419
  // src/extract/services.ts
2273
2420
  var DEFAULT_SCAN_DEPTH = 5;
2274
2421
  function parseScanDepth() {
@@ -2285,21 +2432,21 @@ function workspaceGlobs(pkg) {
2285
2432
  return null;
2286
2433
  }
2287
2434
  async function loadGitignore(scanPath) {
2288
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2435
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2289
2436
  if (!await exists(gitignorePath)) return null;
2290
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2437
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
2291
2438
  return (0, import_ignore.default)().add(raw);
2292
2439
  }
2293
2440
  async function walkDirs(start, scanPath, options, visit) {
2294
2441
  async function recurse(current, depth) {
2295
2442
  if (depth > options.maxDepth) return;
2296
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2443
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2297
2444
  for (const entry of entries) {
2298
2445
  if (!entry.isDirectory()) continue;
2299
2446
  if (IGNORED_DIRS.has(entry.name)) continue;
2300
- const child = import_node_path7.default.join(current, entry.name);
2447
+ const child = import_node_path8.default.join(current, entry.name);
2301
2448
  if (options.ig) {
2302
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2449
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2303
2450
  if (rel && options.ig.ignores(rel + "/")) continue;
2304
2451
  }
2305
2452
  await visit(child);
@@ -2314,8 +2461,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2314
2461
  for (const raw of globs) {
2315
2462
  const pattern = raw.replace(/^\.\//, "");
2316
2463
  if (!pattern.includes("*")) {
2317
- const candidate = import_node_path7.default.join(scanPath, pattern);
2318
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2464
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2465
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2319
2466
  continue;
2320
2467
  }
2321
2468
  const segments = pattern.split("/");
@@ -2324,13 +2471,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2324
2471
  if (seg.includes("*")) break;
2325
2472
  staticSegments.push(seg);
2326
2473
  }
2327
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2474
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2328
2475
  if (!await exists(start)) continue;
2329
2476
  const hasDoubleStar = pattern.includes("**");
2330
2477
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2331
2478
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2332
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2333
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2479
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2480
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2334
2481
  found.add(dir);
2335
2482
  }
2336
2483
  });
@@ -2338,15 +2485,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2338
2485
  return [...found];
2339
2486
  }
2340
2487
  async function discoverNodeService(scanPath, dir) {
2341
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2488
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2342
2489
  if (!await exists(pkgPath)) return null;
2343
2490
  let pkg;
2344
2491
  try {
2345
2492
  pkg = await readJson(pkgPath);
2346
2493
  } catch (err) {
2347
- console.warn(
2348
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2349
- );
2494
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2350
2495
  return null;
2351
2496
  }
2352
2497
  if (!pkg.name) return null;
@@ -2357,7 +2502,7 @@ async function discoverNodeService(scanPath, dir) {
2357
2502
  language: "javascript",
2358
2503
  version: pkg.version,
2359
2504
  dependencies: pkg.dependencies ?? {},
2360
- repoPath: import_node_path7.default.relative(scanPath, dir),
2505
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2361
2506
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2362
2507
  };
2363
2508
  return { pkg, dir, node };
@@ -2373,19 +2518,21 @@ async function discoverPyService(scanPath, dir) {
2373
2518
  language: "python",
2374
2519
  version: py.version,
2375
2520
  dependencies: py.dependencies,
2376
- repoPath: import_node_path7.default.relative(scanPath, dir)
2521
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2377
2522
  };
2378
2523
  return { pkg, dir, node };
2379
2524
  }
2380
2525
  async function discoverServices(scanPath) {
2381
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2526
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2382
2527
  let rootPkg = null;
2383
2528
  if (await exists(rootPkgPath)) {
2384
2529
  try {
2385
2530
  rootPkg = await readJson(rootPkgPath);
2386
2531
  } catch (err) {
2387
- console.warn(
2388
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2532
+ recordExtractionError(
2533
+ "services workspaces",
2534
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2535
+ err
2389
2536
  );
2390
2537
  }
2391
2538
  }
@@ -2401,9 +2548,9 @@ async function discoverServices(scanPath) {
2401
2548
  scanPath,
2402
2549
  { maxDepth: parseScanDepth(), ig },
2403
2550
  async (dir) => {
2404
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2551
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2405
2552
  candidateDirs.push(dir);
2406
- } else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
2553
+ } else if (await exists(import_node_path8.default.join(dir, "pyproject.toml")) || await exists(import_node_path8.default.join(dir, "requirements.txt")) || await exists(import_node_path8.default.join(dir, "setup.py"))) {
2407
2554
  candidateDirs.push(dir);
2408
2555
  }
2409
2556
  }
@@ -2417,8 +2564,8 @@ async function discoverServices(scanPath) {
2417
2564
  if (!service) continue;
2418
2565
  const existingDir = seen.get(service.node.name);
2419
2566
  if (existingDir !== void 0) {
2420
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2421
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2567
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2568
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2422
2569
  console.warn(
2423
2570
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2424
2571
  );
@@ -2455,8 +2602,8 @@ function addServiceNodes(graph, services) {
2455
2602
 
2456
2603
  // src/extract/aliases.ts
2457
2604
  init_cjs_shims();
2458
- var import_node_path8 = __toESM(require("path"), 1);
2459
- var import_node_fs8 = require("fs");
2605
+ var import_node_path9 = __toESM(require("path"), 1);
2606
+ var import_node_fs9 = require("fs");
2460
2607
  var import_yaml2 = require("yaml");
2461
2608
  var import_types7 = require("@neat.is/types");
2462
2609
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2483,14 +2630,14 @@ function indexServicesByName(services) {
2483
2630
  const map = /* @__PURE__ */ new Map();
2484
2631
  for (const s of services) {
2485
2632
  map.set(s.node.name, s.node.id);
2486
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2633
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2487
2634
  }
2488
2635
  return map;
2489
2636
  }
2490
2637
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2491
2638
  let composePath = null;
2492
2639
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2493
- const abs = import_node_path8.default.join(scanPath, name);
2640
+ const abs = import_node_path9.default.join(scanPath, name);
2494
2641
  if (await exists(abs)) {
2495
2642
  composePath = abs;
2496
2643
  break;
@@ -2501,8 +2648,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2501
2648
  try {
2502
2649
  compose = await readYaml(composePath);
2503
2650
  } catch (err) {
2504
- console.warn(
2505
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2651
+ recordExtractionError(
2652
+ "aliases compose",
2653
+ import_node_path9.default.relative(scanPath, composePath),
2654
+ err
2506
2655
  );
2507
2656
  return;
2508
2657
  }
@@ -2544,15 +2693,13 @@ function parseDockerfileLabels(content) {
2544
2693
  }
2545
2694
  async function collectDockerfileAliases(graph, services) {
2546
2695
  for (const service of services) {
2547
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2696
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2548
2697
  if (!await exists(dockerfilePath)) continue;
2549
2698
  let content;
2550
2699
  try {
2551
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2700
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2552
2701
  } catch (err) {
2553
- console.warn(
2554
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2555
- );
2702
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2556
2703
  continue;
2557
2704
  }
2558
2705
  const aliases = parseDockerfileLabels(content);
@@ -2562,13 +2709,13 @@ async function collectDockerfileAliases(graph, services) {
2562
2709
  async function walkYamlFiles(start, depth = 0, max = 5) {
2563
2710
  if (depth > max) return [];
2564
2711
  const out = [];
2565
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2712
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2566
2713
  for (const entry of entries) {
2567
2714
  if (entry.isDirectory()) {
2568
2715
  if (IGNORED_DIRS.has(entry.name)) continue;
2569
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
2570
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
2571
- out.push(import_node_path8.default.join(start, entry.name));
2716
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
2717
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
2718
+ out.push(import_node_path9.default.join(start, entry.name));
2572
2719
  }
2573
2720
  }
2574
2721
  return out;
@@ -2595,7 +2742,7 @@ function k8sServiceTarget(doc, byName) {
2595
2742
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2596
2743
  const files = await walkYamlFiles(scanPath);
2597
2744
  for (const file of files) {
2598
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2745
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2599
2746
  let docs;
2600
2747
  try {
2601
2748
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2620,14 +2767,14 @@ async function addServiceAliases(graph, scanPath, services) {
2620
2767
 
2621
2768
  // src/extract/databases/index.ts
2622
2769
  init_cjs_shims();
2623
- var import_node_path16 = __toESM(require("path"), 1);
2770
+ var import_node_path17 = __toESM(require("path"), 1);
2624
2771
  var import_types8 = require("@neat.is/types");
2625
2772
 
2626
2773
  // src/extract/databases/db-config-yaml.ts
2627
2774
  init_cjs_shims();
2628
- var import_node_path9 = __toESM(require("path"), 1);
2775
+ var import_node_path10 = __toESM(require("path"), 1);
2629
2776
  async function parse(serviceDir) {
2630
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2777
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2631
2778
  if (!await exists(yamlPath)) return [];
2632
2779
  const raw = await readYaml(yamlPath);
2633
2780
  return [
@@ -2645,13 +2792,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2645
2792
 
2646
2793
  // src/extract/databases/dotenv.ts
2647
2794
  init_cjs_shims();
2648
- var import_node_fs10 = require("fs");
2649
- var import_node_path11 = __toESM(require("path"), 1);
2795
+ var import_node_fs11 = require("fs");
2796
+ var import_node_path12 = __toESM(require("path"), 1);
2650
2797
 
2651
2798
  // src/extract/databases/shared.ts
2652
2799
  init_cjs_shims();
2653
- var import_node_fs9 = require("fs");
2654
- var import_node_path10 = __toESM(require("path"), 1);
2800
+ var import_node_fs10 = require("fs");
2801
+ var import_node_path11 = __toESM(require("path"), 1);
2655
2802
  function schemeToEngine(scheme) {
2656
2803
  const s = scheme.toLowerCase().split("+")[0];
2657
2804
  switch (s) {
@@ -2690,14 +2837,14 @@ function parseConnectionString(url) {
2690
2837
  }
2691
2838
  async function readIfExists(filePath) {
2692
2839
  try {
2693
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2840
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2694
2841
  } catch {
2695
2842
  return null;
2696
2843
  }
2697
2844
  }
2698
2845
  async function findFirst(serviceDir, candidates) {
2699
2846
  for (const rel of candidates) {
2700
- const abs = import_node_path10.default.join(serviceDir, rel);
2847
+ const abs = import_node_path11.default.join(serviceDir, rel);
2701
2848
  const content = await readIfExists(abs);
2702
2849
  if (content !== null) return abs;
2703
2850
  }
@@ -2748,15 +2895,15 @@ function parseDotenvLine(line) {
2748
2895
  return { key, value };
2749
2896
  }
2750
2897
  async function parse2(serviceDir) {
2751
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2898
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2752
2899
  const configs = [];
2753
2900
  const seen = /* @__PURE__ */ new Set();
2754
2901
  for (const entry of entries) {
2755
2902
  if (!entry.isFile()) continue;
2756
2903
  const match = isConfigFile(entry.name);
2757
2904
  if (!match.match || match.fileType !== "env") continue;
2758
- const filePath = import_node_path11.default.join(serviceDir, entry.name);
2759
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2905
+ const filePath = import_node_path12.default.join(serviceDir, entry.name);
2906
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2760
2907
  for (const line of content.split("\n")) {
2761
2908
  const parsed = parseDotenvLine(line);
2762
2909
  if (!parsed) continue;
@@ -2775,9 +2922,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2775
2922
 
2776
2923
  // src/extract/databases/prisma.ts
2777
2924
  init_cjs_shims();
2778
- var import_node_path12 = __toESM(require("path"), 1);
2925
+ var import_node_path13 = __toESM(require("path"), 1);
2779
2926
  async function parse3(serviceDir) {
2780
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2927
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2781
2928
  const content = await readIfExists(schemaPath);
2782
2929
  if (!content) return [];
2783
2930
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2909,10 +3056,10 @@ var knexParser = { name: "knex", parse: parse5 };
2909
3056
 
2910
3057
  // src/extract/databases/ormconfig.ts
2911
3058
  init_cjs_shims();
2912
- var import_node_path13 = __toESM(require("path"), 1);
3059
+ var import_node_path14 = __toESM(require("path"), 1);
2913
3060
  async function parse6(serviceDir) {
2914
3061
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2915
- const abs = import_node_path13.default.join(serviceDir, candidate);
3062
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2916
3063
  if (!await exists(abs)) continue;
2917
3064
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2918
3065
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2972,9 +3119,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2972
3119
 
2973
3120
  // src/extract/databases/sequelize.ts
2974
3121
  init_cjs_shims();
2975
- var import_node_path14 = __toESM(require("path"), 1);
3122
+ var import_node_path15 = __toESM(require("path"), 1);
2976
3123
  async function parse8(serviceDir) {
2977
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
3124
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2978
3125
  if (!await exists(configPath)) return [];
2979
3126
  const raw = await readJson(configPath);
2980
3127
  const out = [];
@@ -3001,7 +3148,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
3001
3148
 
3002
3149
  // src/extract/databases/docker-compose.ts
3003
3150
  init_cjs_shims();
3004
- var import_node_path15 = __toESM(require("path"), 1);
3151
+ var import_node_path16 = __toESM(require("path"), 1);
3005
3152
  function portFromService(svc) {
3006
3153
  for (const raw of svc.ports ?? []) {
3007
3154
  const str = String(raw);
@@ -3028,7 +3175,7 @@ function databaseFromEnv(svc) {
3028
3175
  }
3029
3176
  async function parse9(serviceDir) {
3030
3177
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3031
- const abs = import_node_path15.default.join(serviceDir, name);
3178
+ const abs = import_node_path16.default.join(serviceDir, name);
3032
3179
  if (!await exists(abs)) continue;
3033
3180
  const raw = await readYaml(abs);
3034
3181
  if (!raw?.services) return [];
@@ -3206,11 +3353,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3206
3353
  target: dbNode.id,
3207
3354
  type: import_types8.EdgeType.CONNECTS_TO,
3208
3355
  provenance: import_types8.Provenance.EXTRACTED,
3209
- ...config.sourceFile ? {
3210
- evidence: {
3211
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
3212
- }
3213
- } : {}
3356
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3357
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3358
+ // Ghost-edge cleanup keys retirement on this; the conditional
3359
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3360
+ evidence: {
3361
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3362
+ }
3214
3363
  };
3215
3364
  if (!graph.hasEdge(edge.id)) {
3216
3365
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3236,15 +3385,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3236
3385
 
3237
3386
  // src/extract/configs.ts
3238
3387
  init_cjs_shims();
3239
- var import_node_fs11 = require("fs");
3240
- var import_node_path17 = __toESM(require("path"), 1);
3388
+ var import_node_fs12 = require("fs");
3389
+ var import_node_path18 = __toESM(require("path"), 1);
3241
3390
  var import_types9 = require("@neat.is/types");
3242
3391
  async function walkConfigFiles(dir) {
3243
3392
  const out = [];
3244
3393
  async function walk(current) {
3245
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3394
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3246
3395
  for (const entry of entries) {
3247
- const full = import_node_path17.default.join(current, entry.name);
3396
+ const full = import_node_path18.default.join(current, entry.name);
3248
3397
  if (entry.isDirectory()) {
3249
3398
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3250
3399
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -3261,13 +3410,13 @@ async function addConfigNodes(graph, services, scanPath) {
3261
3410
  for (const service of services) {
3262
3411
  const configFiles = await walkConfigFiles(service.dir);
3263
3412
  for (const file of configFiles) {
3264
- const relPath = import_node_path17.default.relative(scanPath, file);
3413
+ const relPath = import_node_path18.default.relative(scanPath, file);
3265
3414
  const node = {
3266
3415
  id: (0, import_types9.configId)(relPath),
3267
3416
  type: import_types9.NodeType.ConfigNode,
3268
- name: import_node_path17.default.basename(file),
3417
+ name: import_node_path18.default.basename(file),
3269
3418
  path: relPath,
3270
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3419
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3271
3420
  };
3272
3421
  if (!graph.hasNode(node.id)) {
3273
3422
  graph.addNode(node.id, node);
@@ -3279,7 +3428,8 @@ async function addConfigNodes(graph, services, scanPath) {
3279
3428
  target: node.id,
3280
3429
  type: import_types9.EdgeType.CONFIGURED_BY,
3281
3430
  provenance: import_types9.Provenance.EXTRACTED,
3282
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3431
+ confidence: (0, import_types9.confidenceForExtracted)("structural"),
3432
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3283
3433
  };
3284
3434
  if (!graph.hasEdge(edge.id)) {
3285
3435
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3296,7 +3446,7 @@ var import_types15 = require("@neat.is/types");
3296
3446
 
3297
3447
  // src/extract/calls/http.ts
3298
3448
  init_cjs_shims();
3299
- var import_node_path19 = __toESM(require("path"), 1);
3449
+ var import_node_path20 = __toESM(require("path"), 1);
3300
3450
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3301
3451
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3302
3452
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3304,17 +3454,17 @@ var import_types10 = require("@neat.is/types");
3304
3454
 
3305
3455
  // src/extract/calls/shared.ts
3306
3456
  init_cjs_shims();
3307
- var import_node_fs12 = require("fs");
3308
- var import_node_path18 = __toESM(require("path"), 1);
3457
+ var import_node_fs13 = require("fs");
3458
+ var import_node_path19 = __toESM(require("path"), 1);
3309
3459
  async function walkSourceFiles(dir) {
3310
3460
  const out = [];
3311
3461
  async function walk(current) {
3312
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3462
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3313
3463
  for (const entry of entries) {
3314
- const full = import_node_path18.default.join(current, entry.name);
3464
+ const full = import_node_path19.default.join(current, entry.name);
3315
3465
  if (entry.isDirectory()) {
3316
3466
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3317
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
3467
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3318
3468
  out.push(full);
3319
3469
  }
3320
3470
  }
@@ -3327,7 +3477,7 @@ async function loadSourceFiles(dir) {
3327
3477
  const out = [];
3328
3478
  for (const p of paths) {
3329
3479
  try {
3330
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3480
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3331
3481
  out.push({ path: p, content });
3332
3482
  } catch {
3333
3483
  }
@@ -3346,8 +3496,27 @@ function snippet(text, line) {
3346
3496
 
3347
3497
  // src/extract/calls/http.ts
3348
3498
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3499
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3500
+ function isInsideJsxExternalLink(node) {
3501
+ let cursor = node.parent;
3502
+ while (cursor) {
3503
+ if (cursor.type === "jsx_attribute") {
3504
+ let owner = cursor.parent;
3505
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3506
+ owner = owner.parent;
3507
+ }
3508
+ if (!owner) return false;
3509
+ const tagNode = owner.namedChild(0);
3510
+ const tagName = tagNode?.text ?? "";
3511
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3512
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3513
+ }
3514
+ cursor = cursor.parent;
3515
+ }
3516
+ return false;
3517
+ }
3349
3518
  function collectStringLiterals(node, out) {
3350
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3519
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3351
3520
  for (let i = 0; i < node.namedChildCount; i++) {
3352
3521
  const child = node.namedChild(i);
3353
3522
  if (child) collectStringLiterals(child, out);
@@ -3359,8 +3528,9 @@ function callsFromSource(source, parser, knownHosts) {
3359
3528
  collectStringLiterals(tree.rootNode, literals);
3360
3529
  const targets = /* @__PURE__ */ new Set();
3361
3530
  for (const lit of literals) {
3531
+ if (isInsideJsxExternalLink(lit.node)) continue;
3362
3532
  for (const host of knownHosts) {
3363
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3533
+ if (urlMatchesHost(lit.text, host)) {
3364
3534
  targets.add(host);
3365
3535
  }
3366
3536
  }
@@ -3383,9 +3553,9 @@ async function addHttpCallEdges(graph, services) {
3383
3553
  const knownHosts = /* @__PURE__ */ new Set();
3384
3554
  const hostToNodeId = /* @__PURE__ */ new Map();
3385
3555
  for (const service of services) {
3386
- knownHosts.add(import_node_path19.default.basename(service.dir));
3556
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3387
3557
  knownHosts.add(service.pkg.name);
3388
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3558
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3389
3559
  hostToNodeId.set(service.pkg.name, service.node.id);
3390
3560
  }
3391
3561
  let edgesAdded = 0;
@@ -3393,14 +3563,13 @@ async function addHttpCallEdges(graph, services) {
3393
3563
  const files = await loadSourceFiles(service.dir);
3394
3564
  const seenTargets = /* @__PURE__ */ new Map();
3395
3565
  for (const file of files) {
3396
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3566
+ if (isTestPath(file.path)) continue;
3567
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3397
3568
  let targets;
3398
3569
  try {
3399
3570
  targets = callsFromSource(file.content, parser, knownHosts);
3400
3571
  } catch (err) {
3401
- console.warn(
3402
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3403
- );
3572
+ recordExtractionError("http call extraction", file.path, err);
3404
3573
  continue;
3405
3574
  }
3406
3575
  for (const t of targets) {
@@ -3414,17 +3583,32 @@ async function addHttpCallEdges(graph, services) {
3414
3583
  for (const [targetId, evidenceFile] of seenTargets) {
3415
3584
  const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3416
3585
  const line = lineOf(fileContent, `//${evidenceFile.host}`);
3586
+ const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
3587
+ const ev = {
3588
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3589
+ line,
3590
+ snippet: snippet(fileContent, line)
3591
+ };
3592
+ const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, targetId, import_types10.EdgeType.CALLS);
3593
+ if (!(0, import_types10.passesExtractedFloor)(confidence)) {
3594
+ noteExtractedDropped({
3595
+ source: service.node.id,
3596
+ target: targetId,
3597
+ type: import_types10.EdgeType.CALLS,
3598
+ confidence,
3599
+ confidenceKind: "hostname-shape-match",
3600
+ evidence: ev
3601
+ });
3602
+ continue;
3603
+ }
3417
3604
  const edge = {
3418
- id: (0, import_types5.extractedEdgeId)(service.node.id, targetId, import_types10.EdgeType.CALLS),
3605
+ id: edgeId,
3419
3606
  source: service.node.id,
3420
3607
  target: targetId,
3421
3608
  type: import_types10.EdgeType.CALLS,
3422
3609
  provenance: import_types10.Provenance.EXTRACTED,
3423
- evidence: {
3424
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3425
- line,
3426
- snippet: snippet(fileContent, line)
3427
- }
3610
+ confidence,
3611
+ evidence: ev
3428
3612
  };
3429
3613
  if (!graph.hasEdge(edge.id)) {
3430
3614
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3437,7 +3621,7 @@ async function addHttpCallEdges(graph, services) {
3437
3621
 
3438
3622
  // src/extract/calls/kafka.ts
3439
3623
  init_cjs_shims();
3440
- var import_node_path20 = __toESM(require("path"), 1);
3624
+ var import_node_path21 = __toESM(require("path"), 1);
3441
3625
  var import_types11 = require("@neat.is/types");
3442
3626
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3443
3627
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3463,8 +3647,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3463
3647
  name: topic,
3464
3648
  kind: "kafka-topic",
3465
3649
  edgeType,
3650
+ // `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
3651
+ // framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
3652
+ // tier (ADR-066).
3653
+ confidenceKind: "verified-call-site",
3466
3654
  evidence: {
3467
- file: import_node_path20.default.relative(serviceDir, file.path),
3655
+ file: import_node_path21.default.relative(serviceDir, file.path),
3468
3656
  line,
3469
3657
  snippet: snippet(file.content, line)
3470
3658
  }
@@ -3477,7 +3665,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3477
3665
 
3478
3666
  // src/extract/calls/redis.ts
3479
3667
  init_cjs_shims();
3480
- var import_node_path21 = __toESM(require("path"), 1);
3668
+ var import_node_path22 = __toESM(require("path"), 1);
3481
3669
  var import_types12 = require("@neat.is/types");
3482
3670
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3483
3671
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3495,8 +3683,12 @@ function redisEndpointsFromFile(file, serviceDir) {
3495
3683
  name: host,
3496
3684
  kind: "redis",
3497
3685
  edgeType: "CALLS",
3686
+ // `redis://host` URL literal — the scheme is structural support, but no
3687
+ // call expression is verified to wire it through. URL-with-structural-
3688
+ // support tier (ADR-066).
3689
+ confidenceKind: "url-with-structural-support",
3498
3690
  evidence: {
3499
- file: import_node_path21.default.relative(serviceDir, file.path),
3691
+ file: import_node_path22.default.relative(serviceDir, file.path),
3500
3692
  line,
3501
3693
  snippet: snippet(file.content, line)
3502
3694
  }
@@ -3507,7 +3699,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3507
3699
 
3508
3700
  // src/extract/calls/aws.ts
3509
3701
  init_cjs_shims();
3510
- var import_node_path22 = __toESM(require("path"), 1);
3702
+ var import_node_path23 = __toESM(require("path"), 1);
3511
3703
  var import_types13 = require("@neat.is/types");
3512
3704
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3513
3705
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3536,8 +3728,12 @@ function awsEndpointsFromFile(file, serviceDir) {
3536
3728
  name,
3537
3729
  kind,
3538
3730
  edgeType: "CALLS",
3731
+ // SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
3732
+ // literal — framework-aware recognizer, verified-call-site tier
3733
+ // (ADR-066).
3734
+ confidenceKind: "verified-call-site",
3539
3735
  evidence: {
3540
- file: import_node_path22.default.relative(serviceDir, file.path),
3736
+ file: import_node_path23.default.relative(serviceDir, file.path),
3541
3737
  line,
3542
3738
  snippet: snippet(file.content, line)
3543
3739
  }
@@ -3562,16 +3758,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3562
3758
 
3563
3759
  // src/extract/calls/grpc.ts
3564
3760
  init_cjs_shims();
3565
- var import_node_path23 = __toESM(require("path"), 1);
3761
+ var import_node_path24 = __toESM(require("path"), 1);
3566
3762
  var import_types14 = require("@neat.is/types");
3567
3763
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3764
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3765
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3568
3766
  function isLikelyAddress(value) {
3569
3767
  if (!value) return false;
3570
3768
  return /:\d{2,5}$/.test(value) || value.includes(".");
3571
3769
  }
3770
+ function normaliseForMatch(s) {
3771
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3772
+ }
3773
+ function readImports(content) {
3774
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3775
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3776
+ let m;
3777
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3778
+ const raw = m[1];
3779
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3780
+ }
3781
+ return {
3782
+ awsSdkSuffixes,
3783
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3784
+ };
3785
+ }
3786
+ function classifyClient(symbol, ctx) {
3787
+ const key = normaliseForMatch(symbol);
3788
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3789
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3790
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3791
+ return null;
3792
+ }
3572
3793
  function grpcEndpointsFromFile(file, serviceDir) {
3573
3794
  const out = [];
3574
3795
  const seen = /* @__PURE__ */ new Set();
3796
+ const ctx = readImports(file.content);
3575
3797
  GRPC_CLIENT_RE.lastIndex = 0;
3576
3798
  let m;
3577
3799
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3579,15 +3801,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
3579
3801
  const addr = m[2]?.trim();
3580
3802
  const name = isLikelyAddress(addr) ? addr : symbol;
3581
3803
  if (seen.has(name)) continue;
3804
+ const classified = classifyClient(symbol, ctx);
3805
+ if (!classified) continue;
3582
3806
  seen.add(name);
3807
+ const { kind } = classified;
3583
3808
  const line = lineOf(file.content, m[0]);
3584
3809
  out.push({
3585
- infraId: (0, import_types14.infraId)("grpc-service", name),
3810
+ infraId: (0, import_types14.infraId)(kind, name),
3586
3811
  name,
3587
- kind: "grpc-service",
3812
+ kind,
3588
3813
  edgeType: "CALLS",
3814
+ // `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
3815
+ // context — import-aware classification per #238. Verified-call-site
3816
+ // tier (ADR-066).
3817
+ confidenceKind: "verified-call-site",
3589
3818
  evidence: {
3590
- file: import_node_path23.default.relative(serviceDir, file.path),
3819
+ file: import_node_path24.default.relative(serviceDir, file.path),
3591
3820
  line,
3592
3821
  snippet: snippet(file.content, line)
3593
3822
  }
@@ -3607,6 +3836,9 @@ function edgeTypeFromEndpoint(ep) {
3607
3836
  return import_types15.EdgeType.CALLS;
3608
3837
  }
3609
3838
  }
3839
+ function isAwsKind(kind) {
3840
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3841
+ }
3610
3842
  async function addExternalEndpointEdges(graph, services) {
3611
3843
  let nodesAdded = 0;
3612
3844
  let edgesAdded = 0;
@@ -3614,10 +3846,13 @@ async function addExternalEndpointEdges(graph, services) {
3614
3846
  const files = await loadSourceFiles(service.dir);
3615
3847
  const endpoints = [];
3616
3848
  for (const file of files) {
3617
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3618
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3619
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3620
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3849
+ if (isTestPath(file.path)) continue;
3850
+ const masked = maskCommentsInSource(file.content);
3851
+ const maskedFile = { path: file.path, content: masked };
3852
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3853
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3854
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3855
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3621
3856
  }
3622
3857
  if (endpoints.length === 0) continue;
3623
3858
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3627,7 +3862,10 @@ async function addExternalEndpointEdges(graph, services) {
3627
3862
  id: ep.infraId,
3628
3863
  type: import_types15.NodeType.InfraNode,
3629
3864
  name: ep.name,
3630
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3865
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3866
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3867
+ // bucket / table kinds from aws.ts.
3868
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3631
3869
  kind: ep.kind
3632
3870
  };
3633
3871
  graph.addNode(node.id, node);
@@ -3637,6 +3875,18 @@ async function addExternalEndpointEdges(graph, services) {
3637
3875
  const edgeId = (0, import_types5.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3638
3876
  if (seenEdges.has(edgeId)) continue;
3639
3877
  seenEdges.add(edgeId);
3878
+ const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
3879
+ if (!(0, import_types15.passesExtractedFloor)(confidence)) {
3880
+ noteExtractedDropped({
3881
+ source: service.node.id,
3882
+ target: ep.infraId,
3883
+ type: edgeType,
3884
+ confidence,
3885
+ confidenceKind: ep.confidenceKind,
3886
+ evidence: ep.evidence
3887
+ });
3888
+ continue;
3889
+ }
3640
3890
  if (!graph.hasEdge(edgeId)) {
3641
3891
  const edge = {
3642
3892
  id: edgeId,
@@ -3644,6 +3894,7 @@ async function addExternalEndpointEdges(graph, services) {
3644
3894
  target: ep.infraId,
3645
3895
  type: edgeType,
3646
3896
  provenance: import_types15.Provenance.EXTRACTED,
3897
+ confidence,
3647
3898
  evidence: ep.evidence
3648
3899
  };
3649
3900
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3667,7 +3918,7 @@ init_cjs_shims();
3667
3918
 
3668
3919
  // src/extract/infra/docker-compose.ts
3669
3920
  init_cjs_shims();
3670
- var import_node_path24 = __toESM(require("path"), 1);
3921
+ var import_node_path25 = __toESM(require("path"), 1);
3671
3922
  var import_types17 = require("@neat.is/types");
3672
3923
 
3673
3924
  // src/extract/infra/shared.ts
@@ -3705,7 +3956,7 @@ function dependsOnList(value) {
3705
3956
  }
3706
3957
  function serviceNameToServiceNode(name, services) {
3707
3958
  for (const s of services) {
3708
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3959
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3709
3960
  }
3710
3961
  return null;
3711
3962
  }
@@ -3714,7 +3965,7 @@ async function addComposeInfra(graph, scanPath, services) {
3714
3965
  let edgesAdded = 0;
3715
3966
  let composePath = null;
3716
3967
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3717
- const abs = import_node_path24.default.join(scanPath, name);
3968
+ const abs = import_node_path25.default.join(scanPath, name);
3718
3969
  if (await exists(abs)) {
3719
3970
  composePath = abs;
3720
3971
  break;
@@ -3725,13 +3976,15 @@ async function addComposeInfra(graph, scanPath, services) {
3725
3976
  try {
3726
3977
  compose = await readYaml(composePath);
3727
3978
  } catch (err) {
3728
- console.warn(
3729
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3979
+ recordExtractionError(
3980
+ "infra docker-compose",
3981
+ import_node_path25.default.relative(scanPath, composePath),
3982
+ err
3730
3983
  );
3731
3984
  return { nodesAdded, edgesAdded };
3732
3985
  }
3733
3986
  if (!compose?.services) return { nodesAdded, edgesAdded };
3734
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3987
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3735
3988
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3736
3989
  for (const [composeName, svc] of Object.entries(compose.services)) {
3737
3990
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3761,6 +4014,7 @@ async function addComposeInfra(graph, scanPath, services) {
3761
4014
  target: targetId,
3762
4015
  type: import_types17.EdgeType.DEPENDS_ON,
3763
4016
  provenance: import_types17.Provenance.EXTRACTED,
4017
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
3764
4018
  evidence: { file: evidenceFile }
3765
4019
  };
3766
4020
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3772,8 +4026,8 @@ async function addComposeInfra(graph, scanPath, services) {
3772
4026
 
3773
4027
  // src/extract/infra/dockerfile.ts
3774
4028
  init_cjs_shims();
3775
- var import_node_path25 = __toESM(require("path"), 1);
3776
- var import_node_fs13 = require("fs");
4029
+ var import_node_path26 = __toESM(require("path"), 1);
4030
+ var import_node_fs14 = require("fs");
3777
4031
  var import_types18 = require("@neat.is/types");
3778
4032
  function runtimeImage(content) {
3779
4033
  const lines = content.split("\n");
@@ -3793,14 +4047,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3793
4047
  let nodesAdded = 0;
3794
4048
  let edgesAdded = 0;
3795
4049
  for (const service of services) {
3796
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
4050
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3797
4051
  if (!await exists(dockerfilePath)) continue;
3798
4052
  let content;
3799
4053
  try {
3800
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
4054
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3801
4055
  } catch (err) {
3802
- console.warn(
3803
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
4056
+ recordExtractionError(
4057
+ "infra dockerfile",
4058
+ import_node_path26.default.relative(scanPath, dockerfilePath),
4059
+ err
3804
4060
  );
3805
4061
  continue;
3806
4062
  }
@@ -3819,8 +4075,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3819
4075
  target: node.id,
3820
4076
  type: import_types18.EdgeType.RUNS_ON,
3821
4077
  provenance: import_types18.Provenance.EXTRACTED,
4078
+ confidence: (0, import_types18.confidenceForExtracted)("structural"),
3822
4079
  evidence: {
3823
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
4080
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3824
4081
  }
3825
4082
  };
3826
4083
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3832,19 +4089,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3832
4089
 
3833
4090
  // src/extract/infra/terraform.ts
3834
4091
  init_cjs_shims();
3835
- var import_node_fs14 = require("fs");
3836
- var import_node_path26 = __toESM(require("path"), 1);
4092
+ var import_node_fs15 = require("fs");
4093
+ var import_node_path27 = __toESM(require("path"), 1);
3837
4094
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3838
4095
  async function walkTfFiles(start, depth = 0, max = 5) {
3839
4096
  if (depth > max) return [];
3840
4097
  const out = [];
3841
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4098
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3842
4099
  for (const entry of entries) {
3843
4100
  if (entry.isDirectory()) {
3844
4101
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3845
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
4102
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
3846
4103
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3847
- out.push(import_node_path26.default.join(start, entry.name));
4104
+ out.push(import_node_path27.default.join(start, entry.name));
3848
4105
  }
3849
4106
  }
3850
4107
  return out;
@@ -3853,7 +4110,7 @@ async function addTerraformResources(graph, scanPath) {
3853
4110
  let nodesAdded = 0;
3854
4111
  const files = await walkTfFiles(scanPath);
3855
4112
  for (const file of files) {
3856
- const content = await import_node_fs14.promises.readFile(file, "utf8");
4113
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3857
4114
  RESOURCE_RE.lastIndex = 0;
3858
4115
  let m;
3859
4116
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3871,8 +4128,8 @@ async function addTerraformResources(graph, scanPath) {
3871
4128
 
3872
4129
  // src/extract/infra/k8s.ts
3873
4130
  init_cjs_shims();
3874
- var import_node_fs15 = require("fs");
3875
- var import_node_path27 = __toESM(require("path"), 1);
4131
+ var import_node_fs16 = require("fs");
4132
+ var import_node_path28 = __toESM(require("path"), 1);
3876
4133
  var import_yaml3 = require("yaml");
3877
4134
  var K8S_KIND_TO_INFRA_KIND = {
3878
4135
  Service: "k8s-service",
@@ -3886,13 +4143,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3886
4143
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3887
4144
  if (depth > max) return [];
3888
4145
  const out = [];
3889
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
4146
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3890
4147
  for (const entry of entries) {
3891
4148
  if (entry.isDirectory()) {
3892
4149
  if (IGNORED_DIRS.has(entry.name)) continue;
3893
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
3894
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
3895
- out.push(import_node_path27.default.join(start, entry.name));
4150
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
4151
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
4152
+ out.push(import_node_path28.default.join(start, entry.name));
3896
4153
  }
3897
4154
  }
3898
4155
  return out;
@@ -3901,7 +4158,7 @@ async function addK8sResources(graph, scanPath) {
3901
4158
  let nodesAdded = 0;
3902
4159
  const files = await walkYamlFiles2(scanPath);
3903
4160
  for (const file of files) {
3904
- const content = await import_node_fs15.promises.readFile(file, "utf8");
4161
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3905
4162
  let docs;
3906
4163
  try {
3907
4164
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3935,9 +4192,37 @@ async function addInfra(graph, scanPath, services) {
3935
4192
  };
3936
4193
  }
3937
4194
 
4195
+ // src/extract/index.ts
4196
+ var import_node_path30 = __toESM(require("path"), 1);
4197
+
4198
+ // src/extract/retire.ts
4199
+ init_cjs_shims();
4200
+ var import_node_fs17 = require("fs");
4201
+ var import_node_path29 = __toESM(require("path"), 1);
4202
+ var import_types19 = require("@neat.is/types");
4203
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4204
+ const toDrop = [];
4205
+ const bases = [scanPath, ...serviceDirs];
4206
+ graph.forEachEdge((id, attrs) => {
4207
+ const edge = attrs;
4208
+ if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
4209
+ const evidenceFile = edge.evidence?.file;
4210
+ if (!evidenceFile) return;
4211
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
4212
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4213
+ return;
4214
+ }
4215
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4216
+ if (!found) toDrop.push(id);
4217
+ });
4218
+ for (const id of toDrop) graph.dropEdge(id);
4219
+ return toDrop.length;
4220
+ }
4221
+
3938
4222
  // src/extract/index.ts
3939
4223
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3940
4224
  await ensureCompatLoaded();
4225
+ drainExtractionErrors();
3941
4226
  const services = await discoverServices(scanPath);
3942
4227
  const phase1Nodes = addServiceNodes(graph, services);
3943
4228
  await addServiceAliases(graph, scanPath, services);
@@ -3945,12 +4230,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3945
4230
  const phase3 = await addConfigNodes(graph, services, scanPath);
3946
4231
  const phase4 = await addCallEdges(graph, services);
3947
4232
  const phase5 = await addInfra(graph, scanPath, services);
4233
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
4234
+ graph,
4235
+ scanPath,
4236
+ services.map((s) => s.dir)
4237
+ );
3948
4238
  const frontiersPromoted = promoteFrontierNodes(graph);
3949
4239
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
4240
+ const errorEntries = drainExtractionErrors();
4241
+ if (opts.errorsPath && errorEntries.length > 0) {
4242
+ try {
4243
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
4244
+ } catch (err) {
4245
+ console.warn(
4246
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
4247
+ );
4248
+ }
4249
+ }
4250
+ const droppedEntries = drainDroppedExtracted();
4251
+ if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4252
+ const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
4253
+ try {
4254
+ await writeRejectedExtracted(droppedEntries, rejectedPath);
4255
+ } catch (err) {
4256
+ console.warn(
4257
+ `[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
4258
+ );
4259
+ }
4260
+ }
3950
4261
  const result = {
3951
4262
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3952
4263
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3953
- frontiersPromoted
4264
+ frontiersPromoted,
4265
+ extractionErrors: errorEntries.length,
4266
+ errorEntries,
4267
+ ghostsRetired,
4268
+ extractedDropped: droppedEntries.length,
4269
+ droppedEntries
3954
4270
  };
3955
4271
  emitNeatEvent({
3956
4272
  type: "extraction-complete",
@@ -3967,7 +4283,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3967
4283
 
3968
4284
  // src/diff.ts
3969
4285
  init_cjs_shims();
3970
- var import_node_fs16 = require("fs");
4286
+ var import_node_fs18 = require("fs");
3971
4287
  async function loadSnapshotForDiff(target) {
3972
4288
  if (/^https?:\/\//i.test(target)) {
3973
4289
  const res = await fetch(target);
@@ -3976,7 +4292,7 @@ async function loadSnapshotForDiff(target) {
3976
4292
  }
3977
4293
  return await res.json();
3978
4294
  }
3979
- const raw = await import_node_fs16.promises.readFile(target, "utf8");
4295
+ const raw = await import_node_fs18.promises.readFile(target, "utf8");
3980
4296
  return JSON.parse(raw);
3981
4297
  }
3982
4298
  function indexEntries(entries) {
@@ -4044,23 +4360,23 @@ function canonicalJson(value) {
4044
4360
 
4045
4361
  // src/projects.ts
4046
4362
  init_cjs_shims();
4047
- var import_node_path28 = __toESM(require("path"), 1);
4363
+ var import_node_path31 = __toESM(require("path"), 1);
4048
4364
  function pathsForProject(project, baseDir) {
4049
4365
  if (project === DEFAULT_PROJECT) {
4050
4366
  return {
4051
- snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
4052
- errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
4053
- staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
4054
- embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
4055
- policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
4367
+ snapshotPath: import_node_path31.default.join(baseDir, "graph.json"),
4368
+ errorsPath: import_node_path31.default.join(baseDir, "errors.ndjson"),
4369
+ staleEventsPath: import_node_path31.default.join(baseDir, "stale-events.ndjson"),
4370
+ embeddingsCachePath: import_node_path31.default.join(baseDir, "embeddings.json"),
4371
+ policyViolationsPath: import_node_path31.default.join(baseDir, "policy-violations.ndjson")
4056
4372
  };
4057
4373
  }
4058
4374
  return {
4059
- snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
4060
- errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
4061
- staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
4062
- embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
4063
- policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
4375
+ snapshotPath: import_node_path31.default.join(baseDir, `${project}.json`),
4376
+ errorsPath: import_node_path31.default.join(baseDir, `errors.${project}.ndjson`),
4377
+ staleEventsPath: import_node_path31.default.join(baseDir, `stale-events.${project}.ndjson`),
4378
+ embeddingsCachePath: import_node_path31.default.join(baseDir, `embeddings.${project}.json`),
4379
+ policyViolationsPath: import_node_path31.default.join(baseDir, `policy-violations.${project}.ndjson`)
4064
4380
  };
4065
4381
  }
4066
4382
  var Projects = class {
@@ -4100,23 +4416,23 @@ function parseExtraProjects(raw) {
4100
4416
 
4101
4417
  // src/registry.ts
4102
4418
  init_cjs_shims();
4103
- var import_node_fs17 = require("fs");
4419
+ var import_node_fs19 = require("fs");
4104
4420
  var import_node_os2 = __toESM(require("os"), 1);
4105
- var import_node_path29 = __toESM(require("path"), 1);
4106
- var import_types19 = require("@neat.is/types");
4421
+ var import_node_path32 = __toESM(require("path"), 1);
4422
+ var import_types20 = require("@neat.is/types");
4107
4423
  function neatHome() {
4108
4424
  const override = process.env.NEAT_HOME;
4109
- if (override && override.length > 0) return import_node_path29.default.resolve(override);
4110
- return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
4425
+ if (override && override.length > 0) return import_node_path32.default.resolve(override);
4426
+ return import_node_path32.default.join(import_node_os2.default.homedir(), ".neat");
4111
4427
  }
4112
4428
  function registryPath() {
4113
- return import_node_path29.default.join(neatHome(), "projects.json");
4429
+ return import_node_path32.default.join(neatHome(), "projects.json");
4114
4430
  }
4115
4431
  async function readRegistry() {
4116
4432
  const file = registryPath();
4117
4433
  let raw;
4118
4434
  try {
4119
- raw = await import_node_fs17.promises.readFile(file, "utf8");
4435
+ raw = await import_node_fs19.promises.readFile(file, "utf8");
4120
4436
  } catch (err) {
4121
4437
  if (err.code === "ENOENT") {
4122
4438
  return { version: 1, projects: [] };
@@ -4124,7 +4440,7 @@ async function readRegistry() {
4124
4440
  throw err;
4125
4441
  }
4126
4442
  const parsed = JSON.parse(raw);
4127
- return import_types19.RegistryFileSchema.parse(parsed);
4443
+ return import_types20.RegistryFileSchema.parse(parsed);
4128
4444
  }
4129
4445
  async function getProject(name) {
4130
4446
  const reg = await readRegistry();
@@ -4314,11 +4630,11 @@ function registerRoutes(scope, ctx) {
4314
4630
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4315
4631
  const parsed = [];
4316
4632
  for (const c of candidates) {
4317
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4633
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4318
4634
  if (!r.success) {
4319
4635
  return reply.code(400).send({
4320
4636
  error: `unknown divergence type "${c}"`,
4321
- allowed: import_types20.DivergenceTypeSchema.options
4637
+ allowed: import_types21.DivergenceTypeSchema.options
4322
4638
  });
4323
4639
  }
4324
4640
  parsed.push(r.data);
@@ -4502,7 +4818,7 @@ function registerRoutes(scope, ctx) {
4502
4818
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4503
4819
  let violations = await log.readAll();
4504
4820
  if (req.query.severity) {
4505
- const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
4821
+ const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
4506
4822
  if (!sev.success) {
4507
4823
  return reply.code(400).send({
4508
4824
  error: "invalid severity",
@@ -4519,7 +4835,7 @@ function registerRoutes(scope, ctx) {
4519
4835
  scope.post("/policies/check", async (req, reply) => {
4520
4836
  const proj = resolveProject(registry, req, reply);
4521
4837
  if (!proj) return;
4522
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4838
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4523
4839
  if (!parsed.success) {
4524
4840
  return reply.code(400).send({
4525
4841
  error: "invalid /policies/check body",
@@ -4619,8 +4935,8 @@ async function buildApi(opts) {
4619
4935
 
4620
4936
  // src/persist.ts
4621
4937
  init_cjs_shims();
4622
- var import_node_fs18 = require("fs");
4623
- var import_node_path30 = __toESM(require("path"), 1);
4938
+ var import_node_fs20 = require("fs");
4939
+ var import_node_path33 = __toESM(require("path"), 1);
4624
4940
  var SCHEMA_VERSION = 2;
4625
4941
  function migrateV1ToV2(payload) {
4626
4942
  const nodes = payload.graph.nodes;
@@ -4634,7 +4950,7 @@ function migrateV1ToV2(payload) {
4634
4950
  return { ...payload, schemaVersion: 2 };
4635
4951
  }
4636
4952
  async function ensureDir(filePath) {
4637
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(filePath), { recursive: true });
4953
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
4638
4954
  }
4639
4955
  async function saveGraphToDisk(graph, outPath) {
4640
4956
  await ensureDir(outPath);
@@ -4644,13 +4960,13 @@ async function saveGraphToDisk(graph, outPath) {
4644
4960
  graph: graph.export()
4645
4961
  };
4646
4962
  const tmp = `${outPath}.tmp`;
4647
- await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4648
- await import_node_fs18.promises.rename(tmp, outPath);
4963
+ await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4964
+ await import_node_fs20.promises.rename(tmp, outPath);
4649
4965
  }
4650
4966
  async function loadGraphFromDisk(graph, outPath) {
4651
4967
  let raw;
4652
4968
  try {
4653
- raw = await import_node_fs18.promises.readFile(outPath, "utf8");
4969
+ raw = await import_node_fs20.promises.readFile(outPath, "utf8");
4654
4970
  } catch (err) {
4655
4971
  if (err.code === "ENOENT") return;
4656
4972
  throw err;
@@ -4707,8 +5023,8 @@ init_otel_grpc();
4707
5023
 
4708
5024
  // src/search.ts
4709
5025
  init_cjs_shims();
4710
- var import_node_fs19 = require("fs");
4711
- var import_node_path33 = __toESM(require("path"), 1);
5026
+ var import_node_fs21 = require("fs");
5027
+ var import_node_path36 = __toESM(require("path"), 1);
4712
5028
  var import_node_crypto = require("crypto");
4713
5029
  var DEFAULT_LIMIT = 10;
4714
5030
  var NOMIC_DIM = 768;
@@ -4838,7 +5154,7 @@ async function pickEmbedder() {
4838
5154
  }
4839
5155
  async function readCache(cachePath) {
4840
5156
  try {
4841
- const raw = await import_node_fs19.promises.readFile(cachePath, "utf8");
5157
+ const raw = await import_node_fs21.promises.readFile(cachePath, "utf8");
4842
5158
  const parsed = JSON.parse(raw);
4843
5159
  if (parsed.version !== 1) return null;
4844
5160
  return parsed;
@@ -4847,8 +5163,8 @@ async function readCache(cachePath) {
4847
5163
  }
4848
5164
  }
4849
5165
  async function writeCache(cachePath, cache) {
4850
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(cachePath), { recursive: true });
4851
- await import_node_fs19.promises.writeFile(cachePath, JSON.stringify(cache));
5166
+ await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(cachePath), { recursive: true });
5167
+ await import_node_fs21.promises.writeFile(cachePath, JSON.stringify(cache));
4852
5168
  }
4853
5169
  var VectorIndex = class {
4854
5170
  constructor(embedder, cachePath) {
@@ -5027,14 +5343,14 @@ async function bootProject(registry, name, scanPath, baseDir) {
5027
5343
  async function main() {
5028
5344
  const baseDirEnv = process.env.NEAT_OUT_DIR;
5029
5345
  const legacyOutPath = process.env.NEAT_OUT_PATH;
5030
- const baseDir = baseDirEnv ? import_node_path34.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path34.default.resolve(import_node_path34.default.dirname(legacyOutPath)) : import_node_path34.default.resolve("./neat-out");
5031
- const defaultScanPath = import_node_path34.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
5346
+ const baseDir = baseDirEnv ? import_node_path37.default.resolve(baseDirEnv) : legacyOutPath ? import_node_path37.default.resolve(import_node_path37.default.dirname(legacyOutPath)) : import_node_path37.default.resolve("./neat-out");
5347
+ const defaultScanPath = import_node_path37.default.resolve(process.env.NEAT_SCAN_PATH ?? "./demo");
5032
5348
  const registry = new Projects();
5033
5349
  await bootProject(registry, DEFAULT_PROJECT, defaultScanPath, baseDir);
5034
5350
  for (const name of parseExtraProjects(process.env.NEAT_PROJECTS)) {
5035
5351
  const envKey = `NEAT_PROJECT_SCAN_PATH_${name.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
5036
5352
  const projectScan = process.env[envKey];
5037
- await bootProject(registry, name, projectScan ? import_node_path34.default.resolve(projectScan) : void 0, baseDir);
5353
+ await bootProject(registry, name, projectScan ? import_node_path37.default.resolve(projectScan) : void 0, baseDir);
5038
5354
  }
5039
5355
  const host = process.env.HOST ?? "0.0.0.0";
5040
5356
  const port = Number(process.env.PORT ?? 8080);