@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/neatd.cjs CHANGED
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req2) {
100
100
  };
101
101
  }
102
102
  function resolveProtoRoot() {
103
- const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
- return import_node_path31.default.resolve(here, "..", "proto");
103
+ const here = import_node_path34.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
+ return import_node_path34.default.resolve(here, "..", "proto");
105
105
  }
106
106
  function loadTraceService() {
107
107
  const protoRoot = resolveProtoRoot();
@@ -156,13 +156,13 @@ async function startOtelGrpcReceiver(opts) {
156
156
  })
157
157
  };
158
158
  }
159
- var import_node_url, import_node_path31, grpc, protoLoader;
159
+ var import_node_url, import_node_path34, grpc, protoLoader;
160
160
  var init_otel_grpc = __esm({
161
161
  "src/otel-grpc.ts"() {
162
162
  "use strict";
163
163
  init_cjs_shims();
164
164
  import_node_url = require("url");
165
- import_node_path31 = __toESM(require("path"), 1);
165
+ import_node_path34 = __toESM(require("path"), 1);
166
166
  grpc = __toESM(require("@grpc/grpc-js"), 1);
167
167
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
168
168
  init_otel();
@@ -259,10 +259,10 @@ function parseOtlpRequest(body) {
259
259
  }
260
260
  function loadProtobufDecoder() {
261
261
  if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
262
- const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
- const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
262
+ const here = import_node_path35.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
+ const protoRoot = import_node_path35.default.resolve(here, "..", "proto");
264
264
  const root = new import_protobufjs.default.Root();
265
- root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
265
+ root.resolvePath = (_origin, target) => import_node_path35.default.resolve(protoRoot, target);
266
266
  root.loadSync(
267
267
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
268
268
  { keepCase: true }
@@ -354,12 +354,12 @@ async function buildOtelReceiver(opts) {
354
354
  };
355
355
  return decorated;
356
356
  }
357
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
357
+ var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
358
358
  var init_otel = __esm({
359
359
  "src/otel.ts"() {
360
360
  "use strict";
361
361
  init_cjs_shims();
362
- import_node_path32 = __toESM(require("path"), 1);
362
+ import_node_path35 = __toESM(require("path"), 1);
363
363
  import_node_url2 = require("url");
364
364
  import_fastify2 = __toESM(require("fastify"), 1);
365
365
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -369,13 +369,13 @@ var init_otel = __esm({
369
369
 
370
370
  // src/neatd.ts
371
371
  init_cjs_shims();
372
- var import_node_fs20 = require("fs");
373
- var import_node_path35 = __toESM(require("path"), 1);
372
+ var import_node_fs22 = require("fs");
373
+ var import_node_path38 = __toESM(require("path"), 1);
374
374
 
375
375
  // src/daemon.ts
376
376
  init_cjs_shims();
377
- var import_node_fs19 = require("fs");
378
- var import_node_path33 = __toESM(require("path"), 1);
377
+ var import_node_fs21 = require("fs");
378
+ var import_node_path36 = __toESM(require("path"), 1);
379
379
 
380
380
  // src/graph.ts
381
381
  init_cjs_shims();
@@ -831,19 +831,19 @@ function confidenceFromMix(edges, now = Date.now()) {
831
831
  function longestIncomingWalk(graph, start, maxDepth) {
832
832
  let best = { path: [start], edges: [] };
833
833
  const visited = /* @__PURE__ */ new Set([start]);
834
- function step(node, path36, edges) {
835
- if (path36.length > best.path.length) {
836
- best = { path: [...path36], edges: [...edges] };
834
+ function step(node, path39, edges) {
835
+ if (path39.length > best.path.length) {
836
+ best = { path: [...path39], edges: [...edges] };
837
837
  }
838
- if (path36.length - 1 >= maxDepth) return;
838
+ if (path39.length - 1 >= maxDepth) return;
839
839
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
840
840
  for (const [srcId, edge] of incoming) {
841
841
  if (visited.has(srcId)) continue;
842
842
  visited.add(srcId);
843
- path36.push(srcId);
843
+ path39.push(srcId);
844
844
  edges.push(edge);
845
- step(srcId, path36, edges);
846
- path36.pop();
845
+ step(srcId, path39, edges);
846
+ path39.pop();
847
847
  edges.pop();
848
848
  visited.delete(srcId);
849
849
  }
@@ -1510,35 +1510,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1510
1510
  const existing = graph.getEdgeAttributes(id);
1511
1511
  const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
1512
1512
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
1513
+ const newSignal = {
1514
+ spanCount: newSpanCount,
1515
+ errorCount: newErrorCount,
1516
+ lastObservedAgeMs: 0
1517
+ };
1513
1518
  const updated = {
1514
1519
  ...existing,
1515
1520
  provenance: import_types3.Provenance.OBSERVED,
1516
1521
  lastObserved: ts,
1517
1522
  callCount: newSpanCount,
1518
- signal: {
1519
- spanCount: newSpanCount,
1520
- errorCount: newErrorCount,
1521
- lastObservedAgeMs: 0
1522
- },
1523
- confidence: 1
1523
+ signal: newSignal,
1524
+ confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
1524
1525
  };
1525
1526
  graph.replaceEdgeAttributes(id, updated);
1526
1527
  return { edge: updated, created: false };
1527
1528
  }
1529
+ const signal = {
1530
+ spanCount: 1,
1531
+ errorCount: isError ? 1 : 0,
1532
+ lastObservedAgeMs: 0
1533
+ };
1528
1534
  const edge = {
1529
1535
  id,
1530
1536
  source,
1531
1537
  target,
1532
1538
  type,
1533
1539
  provenance: import_types3.Provenance.OBSERVED,
1534
- confidence: 1,
1540
+ confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1535
1541
  lastObserved: ts,
1536
1542
  callCount: 1,
1537
- signal: {
1538
- spanCount: 1,
1539
- errorCount: isError ? 1 : 0,
1540
- lastObservedAgeMs: 0
1541
- }
1543
+ signal
1542
1544
  };
1543
1545
  graph.addEdgeWithKey(id, source, target, edge);
1544
1546
  return { edge, created: true };
@@ -1785,8 +1787,8 @@ async function readErrorEvents(errorsPath) {
1785
1787
 
1786
1788
  // src/extract/services.ts
1787
1789
  init_cjs_shims();
1788
- var import_node_fs7 = require("fs");
1789
- var import_node_path7 = __toESM(require("path"), 1);
1790
+ var import_node_fs8 = require("fs");
1791
+ var import_node_path8 = __toESM(require("path"), 1);
1790
1792
  var import_ignore = __toESM(require("ignore"), 1);
1791
1793
  var import_minimatch2 = require("minimatch");
1792
1794
  var import_types5 = require("@neat.is/types");
@@ -1810,9 +1812,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1810
1812
  function isConfigFile(name) {
1811
1813
  const ext = import_node_path4.default.extname(name);
1812
1814
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
1813
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
1815
+ if (name === ".env" || name.startsWith(".env.")) {
1816
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1817
+ return { match: true, fileType: "env" };
1818
+ }
1814
1819
  return { match: false, fileType: "" };
1815
1820
  }
1821
+ function isTestPath(filePath) {
1822
+ const normalised = filePath.replace(/\\/g, "/");
1823
+ const segments = normalised.split("/");
1824
+ for (const seg of segments) {
1825
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
1826
+ return true;
1827
+ }
1828
+ }
1829
+ const base = segments[segments.length - 1] ?? "";
1830
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
1831
+ }
1832
+ function isEnvTemplateFile(name) {
1833
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
1834
+ return true;
1835
+ }
1836
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
1837
+ }
1838
+ function maskCommentsInSource(src) {
1839
+ const len = src.length;
1840
+ const out = new Array(len);
1841
+ let i = 0;
1842
+ let inString = 0;
1843
+ let escaped = false;
1844
+ while (i < len) {
1845
+ const c = src[i];
1846
+ if (inString !== 0) {
1847
+ out[i] = c;
1848
+ if (escaped) {
1849
+ escaped = false;
1850
+ } else if (c === "\\") {
1851
+ escaped = true;
1852
+ } else if (c === inString) {
1853
+ inString = 0;
1854
+ }
1855
+ i++;
1856
+ continue;
1857
+ }
1858
+ if (c === "/" && i + 1 < len) {
1859
+ const next = src[i + 1];
1860
+ if (next === "/") {
1861
+ out[i] = " ";
1862
+ out[i + 1] = " ";
1863
+ let j = i + 2;
1864
+ while (j < len && src[j] !== "\n") {
1865
+ out[j] = " ";
1866
+ j++;
1867
+ }
1868
+ i = j;
1869
+ continue;
1870
+ }
1871
+ if (next === "*") {
1872
+ out[i] = " ";
1873
+ out[i + 1] = " ";
1874
+ let j = i + 2;
1875
+ while (j < len) {
1876
+ if (src[j] === "\n") {
1877
+ out[j] = "\n";
1878
+ j++;
1879
+ continue;
1880
+ }
1881
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
1882
+ out[j] = " ";
1883
+ out[j + 1] = " ";
1884
+ j += 2;
1885
+ break;
1886
+ }
1887
+ out[j] = " ";
1888
+ j++;
1889
+ }
1890
+ i = j;
1891
+ continue;
1892
+ }
1893
+ }
1894
+ out[i] = c;
1895
+ if (c === "'" || c === '"' || c === "`") inString = c;
1896
+ i++;
1897
+ }
1898
+ return out.join("");
1899
+ }
1900
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
1901
+ function urlMatchesHost(urlString, host) {
1902
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
1903
+ if (!URL_LIKE.test(urlString)) return false;
1904
+ const [wantedHost, wantedPort] = host.split(":");
1905
+ let parsed;
1906
+ try {
1907
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
1908
+ parsed = new URL(candidate);
1909
+ } catch {
1910
+ return false;
1911
+ }
1912
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
1913
+ if (wantedPort && parsed.port !== wantedPort) return false;
1914
+ return true;
1915
+ }
1816
1916
  function cleanVersion(raw) {
1817
1917
  if (!raw) return void 0;
1818
1918
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -1968,6 +2068,50 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
1968
2068
  return author ?? void 0;
1969
2069
  }
1970
2070
 
2071
+ // src/extract/errors.ts
2072
+ init_cjs_shims();
2073
+ var import_node_fs7 = require("fs");
2074
+ var import_node_path7 = __toESM(require("path"), 1);
2075
+ var sink = [];
2076
+ function recordExtractionError(producer, file, err) {
2077
+ const e = err instanceof Error ? err : new Error(String(err));
2078
+ sink.push({
2079
+ producer,
2080
+ file,
2081
+ error: e.message,
2082
+ stack: e.stack,
2083
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2084
+ source: "extract"
2085
+ });
2086
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2087
+ }
2088
+ function drainExtractionErrors() {
2089
+ return sink.splice(0, sink.length);
2090
+ }
2091
+ async function writeExtractionErrors(errors, errorsPath) {
2092
+ if (errors.length === 0) return;
2093
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2094
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2095
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2096
+ }
2097
+ var droppedSink = [];
2098
+ function noteExtractedDropped(edge) {
2099
+ droppedSink.push(edge);
2100
+ }
2101
+ function drainDroppedExtracted() {
2102
+ return droppedSink.splice(0, droppedSink.length);
2103
+ }
2104
+ function isRejectedLogEnabled() {
2105
+ const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
2106
+ return raw === "1" || raw === "true";
2107
+ }
2108
+ async function writeRejectedExtracted(drops, rejectedPath) {
2109
+ if (drops.length === 0) return;
2110
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
2111
+ const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
2112
+ await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
2113
+ }
2114
+
1971
2115
  // src/extract/services.ts
1972
2116
  var DEFAULT_SCAN_DEPTH = 5;
1973
2117
  function parseScanDepth() {
@@ -1984,21 +2128,21 @@ function workspaceGlobs(pkg) {
1984
2128
  return null;
1985
2129
  }
1986
2130
  async function loadGitignore(scanPath) {
1987
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2131
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
1988
2132
  if (!await exists(gitignorePath)) return null;
1989
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2133
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
1990
2134
  return (0, import_ignore.default)().add(raw);
1991
2135
  }
1992
2136
  async function walkDirs(start, scanPath, options, visit) {
1993
2137
  async function recurse(current, depth) {
1994
2138
  if (depth > options.maxDepth) return;
1995
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2139
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1996
2140
  for (const entry2 of entries) {
1997
2141
  if (!entry2.isDirectory()) continue;
1998
2142
  if (IGNORED_DIRS.has(entry2.name)) continue;
1999
- const child = import_node_path7.default.join(current, entry2.name);
2143
+ const child = import_node_path8.default.join(current, entry2.name);
2000
2144
  if (options.ig) {
2001
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2145
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2002
2146
  if (rel && options.ig.ignores(rel + "/")) continue;
2003
2147
  }
2004
2148
  await visit(child);
@@ -2013,8 +2157,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2013
2157
  for (const raw of globs) {
2014
2158
  const pattern = raw.replace(/^\.\//, "");
2015
2159
  if (!pattern.includes("*")) {
2016
- const candidate = import_node_path7.default.join(scanPath, pattern);
2017
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2160
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2161
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2018
2162
  continue;
2019
2163
  }
2020
2164
  const segments = pattern.split("/");
@@ -2023,13 +2167,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2023
2167
  if (seg.includes("*")) break;
2024
2168
  staticSegments.push(seg);
2025
2169
  }
2026
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2170
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2027
2171
  if (!await exists(start)) continue;
2028
2172
  const hasDoubleStar = pattern.includes("**");
2029
2173
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2030
2174
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2031
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2032
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2175
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2176
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2033
2177
  found.add(dir);
2034
2178
  }
2035
2179
  });
@@ -2037,15 +2181,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2037
2181
  return [...found];
2038
2182
  }
2039
2183
  async function discoverNodeService(scanPath, dir) {
2040
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2184
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2041
2185
  if (!await exists(pkgPath)) return null;
2042
2186
  let pkg;
2043
2187
  try {
2044
2188
  pkg = await readJson(pkgPath);
2045
2189
  } catch (err) {
2046
- console.warn(
2047
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2048
- );
2190
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2049
2191
  return null;
2050
2192
  }
2051
2193
  if (!pkg.name) return null;
@@ -2056,7 +2198,7 @@ async function discoverNodeService(scanPath, dir) {
2056
2198
  language: "javascript",
2057
2199
  version: pkg.version,
2058
2200
  dependencies: pkg.dependencies ?? {},
2059
- repoPath: import_node_path7.default.relative(scanPath, dir),
2201
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2060
2202
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2061
2203
  };
2062
2204
  return { pkg, dir, node };
@@ -2072,19 +2214,21 @@ async function discoverPyService(scanPath, dir) {
2072
2214
  language: "python",
2073
2215
  version: py.version,
2074
2216
  dependencies: py.dependencies,
2075
- repoPath: import_node_path7.default.relative(scanPath, dir)
2217
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2076
2218
  };
2077
2219
  return { pkg, dir, node };
2078
2220
  }
2079
2221
  async function discoverServices(scanPath) {
2080
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2222
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2081
2223
  let rootPkg = null;
2082
2224
  if (await exists(rootPkgPath)) {
2083
2225
  try {
2084
2226
  rootPkg = await readJson(rootPkgPath);
2085
2227
  } catch (err) {
2086
- console.warn(
2087
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2228
+ recordExtractionError(
2229
+ "services workspaces",
2230
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2231
+ err
2088
2232
  );
2089
2233
  }
2090
2234
  }
@@ -2100,9 +2244,9 @@ async function discoverServices(scanPath) {
2100
2244
  scanPath,
2101
2245
  { maxDepth: parseScanDepth(), ig },
2102
2246
  async (dir) => {
2103
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2247
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2104
2248
  candidateDirs.push(dir);
2105
- } 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"))) {
2249
+ } 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"))) {
2106
2250
  candidateDirs.push(dir);
2107
2251
  }
2108
2252
  }
@@ -2116,8 +2260,8 @@ async function discoverServices(scanPath) {
2116
2260
  if (!service) continue;
2117
2261
  const existingDir = seen.get(service.node.name);
2118
2262
  if (existingDir !== void 0) {
2119
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2120
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2263
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2264
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2121
2265
  console.warn(
2122
2266
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2123
2267
  );
@@ -2154,8 +2298,8 @@ function addServiceNodes(graph, services) {
2154
2298
 
2155
2299
  // src/extract/aliases.ts
2156
2300
  init_cjs_shims();
2157
- var import_node_path8 = __toESM(require("path"), 1);
2158
- var import_node_fs8 = require("fs");
2301
+ var import_node_path9 = __toESM(require("path"), 1);
2302
+ var import_node_fs9 = require("fs");
2159
2303
  var import_yaml2 = require("yaml");
2160
2304
  var import_types6 = require("@neat.is/types");
2161
2305
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2182,14 +2326,14 @@ function indexServicesByName(services) {
2182
2326
  const map = /* @__PURE__ */ new Map();
2183
2327
  for (const s of services) {
2184
2328
  map.set(s.node.name, s.node.id);
2185
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2329
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2186
2330
  }
2187
2331
  return map;
2188
2332
  }
2189
2333
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2190
2334
  let composePath = null;
2191
2335
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2192
- const abs = import_node_path8.default.join(scanPath, name);
2336
+ const abs = import_node_path9.default.join(scanPath, name);
2193
2337
  if (await exists(abs)) {
2194
2338
  composePath = abs;
2195
2339
  break;
@@ -2200,8 +2344,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2200
2344
  try {
2201
2345
  compose = await readYaml(composePath);
2202
2346
  } catch (err) {
2203
- console.warn(
2204
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2347
+ recordExtractionError(
2348
+ "aliases compose",
2349
+ import_node_path9.default.relative(scanPath, composePath),
2350
+ err
2205
2351
  );
2206
2352
  return;
2207
2353
  }
@@ -2243,15 +2389,13 @@ function parseDockerfileLabels(content) {
2243
2389
  }
2244
2390
  async function collectDockerfileAliases(graph, services) {
2245
2391
  for (const service of services) {
2246
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2392
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2247
2393
  if (!await exists(dockerfilePath)) continue;
2248
2394
  let content;
2249
2395
  try {
2250
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2396
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2251
2397
  } catch (err) {
2252
- console.warn(
2253
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2254
- );
2398
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2255
2399
  continue;
2256
2400
  }
2257
2401
  const aliases = parseDockerfileLabels(content);
@@ -2261,13 +2405,13 @@ async function collectDockerfileAliases(graph, services) {
2261
2405
  async function walkYamlFiles(start, depth = 0, max = 5) {
2262
2406
  if (depth > max) return [];
2263
2407
  const out = [];
2264
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2408
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2265
2409
  for (const entry2 of entries) {
2266
2410
  if (entry2.isDirectory()) {
2267
2411
  if (IGNORED_DIRS.has(entry2.name)) continue;
2268
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
2269
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
2270
- out.push(import_node_path8.default.join(start, entry2.name));
2412
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry2.name), depth + 1, max));
2413
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry2.name))) {
2414
+ out.push(import_node_path9.default.join(start, entry2.name));
2271
2415
  }
2272
2416
  }
2273
2417
  return out;
@@ -2294,7 +2438,7 @@ function k8sServiceTarget(doc, byName) {
2294
2438
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2295
2439
  const files = await walkYamlFiles(scanPath);
2296
2440
  for (const file of files) {
2297
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2441
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2298
2442
  let docs;
2299
2443
  try {
2300
2444
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2319,14 +2463,14 @@ async function addServiceAliases(graph, scanPath, services) {
2319
2463
 
2320
2464
  // src/extract/databases/index.ts
2321
2465
  init_cjs_shims();
2322
- var import_node_path16 = __toESM(require("path"), 1);
2466
+ var import_node_path17 = __toESM(require("path"), 1);
2323
2467
  var import_types7 = require("@neat.is/types");
2324
2468
 
2325
2469
  // src/extract/databases/db-config-yaml.ts
2326
2470
  init_cjs_shims();
2327
- var import_node_path9 = __toESM(require("path"), 1);
2471
+ var import_node_path10 = __toESM(require("path"), 1);
2328
2472
  async function parse(serviceDir) {
2329
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2473
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2330
2474
  if (!await exists(yamlPath)) return [];
2331
2475
  const raw = await readYaml(yamlPath);
2332
2476
  return [
@@ -2344,13 +2488,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2344
2488
 
2345
2489
  // src/extract/databases/dotenv.ts
2346
2490
  init_cjs_shims();
2347
- var import_node_fs10 = require("fs");
2348
- var import_node_path11 = __toESM(require("path"), 1);
2491
+ var import_node_fs11 = require("fs");
2492
+ var import_node_path12 = __toESM(require("path"), 1);
2349
2493
 
2350
2494
  // src/extract/databases/shared.ts
2351
2495
  init_cjs_shims();
2352
- var import_node_fs9 = require("fs");
2353
- var import_node_path10 = __toESM(require("path"), 1);
2496
+ var import_node_fs10 = require("fs");
2497
+ var import_node_path11 = __toESM(require("path"), 1);
2354
2498
  function schemeToEngine(scheme) {
2355
2499
  const s = scheme.toLowerCase().split("+")[0];
2356
2500
  switch (s) {
@@ -2389,14 +2533,14 @@ function parseConnectionString(url) {
2389
2533
  }
2390
2534
  async function readIfExists(filePath) {
2391
2535
  try {
2392
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2536
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2393
2537
  } catch {
2394
2538
  return null;
2395
2539
  }
2396
2540
  }
2397
2541
  async function findFirst(serviceDir, candidates) {
2398
2542
  for (const rel of candidates) {
2399
- const abs = import_node_path10.default.join(serviceDir, rel);
2543
+ const abs = import_node_path11.default.join(serviceDir, rel);
2400
2544
  const content = await readIfExists(abs);
2401
2545
  if (content !== null) return abs;
2402
2546
  }
@@ -2447,15 +2591,15 @@ function parseDotenvLine(line) {
2447
2591
  return { key, value };
2448
2592
  }
2449
2593
  async function parse2(serviceDir) {
2450
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2594
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2451
2595
  const configs = [];
2452
2596
  const seen = /* @__PURE__ */ new Set();
2453
2597
  for (const entry2 of entries) {
2454
2598
  if (!entry2.isFile()) continue;
2455
2599
  const match = isConfigFile(entry2.name);
2456
2600
  if (!match.match || match.fileType !== "env") continue;
2457
- const filePath = import_node_path11.default.join(serviceDir, entry2.name);
2458
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2601
+ const filePath = import_node_path12.default.join(serviceDir, entry2.name);
2602
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2459
2603
  for (const line of content.split("\n")) {
2460
2604
  const parsed = parseDotenvLine(line);
2461
2605
  if (!parsed) continue;
@@ -2474,9 +2618,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2474
2618
 
2475
2619
  // src/extract/databases/prisma.ts
2476
2620
  init_cjs_shims();
2477
- var import_node_path12 = __toESM(require("path"), 1);
2621
+ var import_node_path13 = __toESM(require("path"), 1);
2478
2622
  async function parse3(serviceDir) {
2479
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2623
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2480
2624
  const content = await readIfExists(schemaPath);
2481
2625
  if (!content) return [];
2482
2626
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2608,10 +2752,10 @@ var knexParser = { name: "knex", parse: parse5 };
2608
2752
 
2609
2753
  // src/extract/databases/ormconfig.ts
2610
2754
  init_cjs_shims();
2611
- var import_node_path13 = __toESM(require("path"), 1);
2755
+ var import_node_path14 = __toESM(require("path"), 1);
2612
2756
  async function parse6(serviceDir) {
2613
2757
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2614
- const abs = import_node_path13.default.join(serviceDir, candidate);
2758
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2615
2759
  if (!await exists(abs)) continue;
2616
2760
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2617
2761
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2671,9 +2815,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2671
2815
 
2672
2816
  // src/extract/databases/sequelize.ts
2673
2817
  init_cjs_shims();
2674
- var import_node_path14 = __toESM(require("path"), 1);
2818
+ var import_node_path15 = __toESM(require("path"), 1);
2675
2819
  async function parse8(serviceDir) {
2676
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2820
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2677
2821
  if (!await exists(configPath)) return [];
2678
2822
  const raw = await readJson(configPath);
2679
2823
  const out = [];
@@ -2700,7 +2844,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2700
2844
 
2701
2845
  // src/extract/databases/docker-compose.ts
2702
2846
  init_cjs_shims();
2703
- var import_node_path15 = __toESM(require("path"), 1);
2847
+ var import_node_path16 = __toESM(require("path"), 1);
2704
2848
  function portFromService(svc) {
2705
2849
  for (const raw of svc.ports ?? []) {
2706
2850
  const str = String(raw);
@@ -2727,7 +2871,7 @@ function databaseFromEnv(svc) {
2727
2871
  }
2728
2872
  async function parse9(serviceDir) {
2729
2873
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2730
- const abs = import_node_path15.default.join(serviceDir, name);
2874
+ const abs = import_node_path16.default.join(serviceDir, name);
2731
2875
  if (!await exists(abs)) continue;
2732
2876
  const raw = await readYaml(abs);
2733
2877
  if (!raw?.services) return [];
@@ -2905,11 +3049,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2905
3049
  target: dbNode.id,
2906
3050
  type: import_types7.EdgeType.CONNECTS_TO,
2907
3051
  provenance: import_types7.Provenance.EXTRACTED,
2908
- ...config.sourceFile ? {
2909
- evidence: {
2910
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
2911
- }
2912
- } : {}
3052
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3053
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3054
+ // Ghost-edge cleanup keys retirement on this; the conditional
3055
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3056
+ evidence: {
3057
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3058
+ }
2913
3059
  };
2914
3060
  if (!graph.hasEdge(edge.id)) {
2915
3061
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2935,15 +3081,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2935
3081
 
2936
3082
  // src/extract/configs.ts
2937
3083
  init_cjs_shims();
2938
- var import_node_fs11 = require("fs");
2939
- var import_node_path17 = __toESM(require("path"), 1);
3084
+ var import_node_fs12 = require("fs");
3085
+ var import_node_path18 = __toESM(require("path"), 1);
2940
3086
  var import_types8 = require("@neat.is/types");
2941
3087
  async function walkConfigFiles(dir) {
2942
3088
  const out = [];
2943
3089
  async function walk(current) {
2944
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3090
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
2945
3091
  for (const entry2 of entries) {
2946
- const full = import_node_path17.default.join(current, entry2.name);
3092
+ const full = import_node_path18.default.join(current, entry2.name);
2947
3093
  if (entry2.isDirectory()) {
2948
3094
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
2949
3095
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -2960,13 +3106,13 @@ async function addConfigNodes(graph, services, scanPath) {
2960
3106
  for (const service of services) {
2961
3107
  const configFiles = await walkConfigFiles(service.dir);
2962
3108
  for (const file of configFiles) {
2963
- const relPath = import_node_path17.default.relative(scanPath, file);
3109
+ const relPath = import_node_path18.default.relative(scanPath, file);
2964
3110
  const node = {
2965
3111
  id: (0, import_types8.configId)(relPath),
2966
3112
  type: import_types8.NodeType.ConfigNode,
2967
- name: import_node_path17.default.basename(file),
3113
+ name: import_node_path18.default.basename(file),
2968
3114
  path: relPath,
2969
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3115
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
2970
3116
  };
2971
3117
  if (!graph.hasNode(node.id)) {
2972
3118
  graph.addNode(node.id, node);
@@ -2978,7 +3124,8 @@ async function addConfigNodes(graph, services, scanPath) {
2978
3124
  target: node.id,
2979
3125
  type: import_types8.EdgeType.CONFIGURED_BY,
2980
3126
  provenance: import_types8.Provenance.EXTRACTED,
2981
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3127
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3128
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
2982
3129
  };
2983
3130
  if (!graph.hasEdge(edge.id)) {
2984
3131
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2995,7 +3142,7 @@ var import_types14 = require("@neat.is/types");
2995
3142
 
2996
3143
  // src/extract/calls/http.ts
2997
3144
  init_cjs_shims();
2998
- var import_node_path19 = __toESM(require("path"), 1);
3145
+ var import_node_path20 = __toESM(require("path"), 1);
2999
3146
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3000
3147
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3001
3148
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3003,17 +3150,17 @@ var import_types9 = require("@neat.is/types");
3003
3150
 
3004
3151
  // src/extract/calls/shared.ts
3005
3152
  init_cjs_shims();
3006
- var import_node_fs12 = require("fs");
3007
- var import_node_path18 = __toESM(require("path"), 1);
3153
+ var import_node_fs13 = require("fs");
3154
+ var import_node_path19 = __toESM(require("path"), 1);
3008
3155
  async function walkSourceFiles(dir) {
3009
3156
  const out = [];
3010
3157
  async function walk(current) {
3011
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3158
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3012
3159
  for (const entry2 of entries) {
3013
- const full = import_node_path18.default.join(current, entry2.name);
3160
+ const full = import_node_path19.default.join(current, entry2.name);
3014
3161
  if (entry2.isDirectory()) {
3015
3162
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
3016
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
3163
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry2.name))) {
3017
3164
  out.push(full);
3018
3165
  }
3019
3166
  }
@@ -3026,7 +3173,7 @@ async function loadSourceFiles(dir) {
3026
3173
  const out = [];
3027
3174
  for (const p of paths) {
3028
3175
  try {
3029
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3176
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3030
3177
  out.push({ path: p, content });
3031
3178
  } catch {
3032
3179
  }
@@ -3045,8 +3192,27 @@ function snippet(text, line) {
3045
3192
 
3046
3193
  // src/extract/calls/http.ts
3047
3194
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3195
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3196
+ function isInsideJsxExternalLink(node) {
3197
+ let cursor = node.parent;
3198
+ while (cursor) {
3199
+ if (cursor.type === "jsx_attribute") {
3200
+ let owner = cursor.parent;
3201
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3202
+ owner = owner.parent;
3203
+ }
3204
+ if (!owner) return false;
3205
+ const tagNode = owner.namedChild(0);
3206
+ const tagName = tagNode?.text ?? "";
3207
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3208
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3209
+ }
3210
+ cursor = cursor.parent;
3211
+ }
3212
+ return false;
3213
+ }
3048
3214
  function collectStringLiterals(node, out) {
3049
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3215
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3050
3216
  for (let i = 0; i < node.namedChildCount; i++) {
3051
3217
  const child = node.namedChild(i);
3052
3218
  if (child) collectStringLiterals(child, out);
@@ -3058,8 +3224,9 @@ function callsFromSource(source, parser, knownHosts) {
3058
3224
  collectStringLiterals(tree.rootNode, literals);
3059
3225
  const targets = /* @__PURE__ */ new Set();
3060
3226
  for (const lit of literals) {
3227
+ if (isInsideJsxExternalLink(lit.node)) continue;
3061
3228
  for (const host of knownHosts) {
3062
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3229
+ if (urlMatchesHost(lit.text, host)) {
3063
3230
  targets.add(host);
3064
3231
  }
3065
3232
  }
@@ -3082,9 +3249,9 @@ async function addHttpCallEdges(graph, services) {
3082
3249
  const knownHosts = /* @__PURE__ */ new Set();
3083
3250
  const hostToNodeId = /* @__PURE__ */ new Map();
3084
3251
  for (const service of services) {
3085
- knownHosts.add(import_node_path19.default.basename(service.dir));
3252
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3086
3253
  knownHosts.add(service.pkg.name);
3087
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3254
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3088
3255
  hostToNodeId.set(service.pkg.name, service.node.id);
3089
3256
  }
3090
3257
  let edgesAdded = 0;
@@ -3092,14 +3259,13 @@ async function addHttpCallEdges(graph, services) {
3092
3259
  const files = await loadSourceFiles(service.dir);
3093
3260
  const seenTargets = /* @__PURE__ */ new Map();
3094
3261
  for (const file of files) {
3095
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3262
+ if (isTestPath(file.path)) continue;
3263
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3096
3264
  let targets;
3097
3265
  try {
3098
3266
  targets = callsFromSource(file.content, parser, knownHosts);
3099
3267
  } catch (err) {
3100
- console.warn(
3101
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3102
- );
3268
+ recordExtractionError("http call extraction", file.path, err);
3103
3269
  continue;
3104
3270
  }
3105
3271
  for (const t of targets) {
@@ -3113,17 +3279,32 @@ async function addHttpCallEdges(graph, services) {
3113
3279
  for (const [targetId, evidenceFile] of seenTargets) {
3114
3280
  const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3115
3281
  const line = lineOf(fileContent, `//${evidenceFile.host}`);
3282
+ const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3283
+ const ev = {
3284
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3285
+ line,
3286
+ snippet: snippet(fileContent, line)
3287
+ };
3288
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3289
+ if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3290
+ noteExtractedDropped({
3291
+ source: service.node.id,
3292
+ target: targetId,
3293
+ type: import_types9.EdgeType.CALLS,
3294
+ confidence,
3295
+ confidenceKind: "hostname-shape-match",
3296
+ evidence: ev
3297
+ });
3298
+ continue;
3299
+ }
3116
3300
  const edge = {
3117
- id: (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS),
3301
+ id: edgeId,
3118
3302
  source: service.node.id,
3119
3303
  target: targetId,
3120
3304
  type: import_types9.EdgeType.CALLS,
3121
3305
  provenance: import_types9.Provenance.EXTRACTED,
3122
- evidence: {
3123
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3124
- line,
3125
- snippet: snippet(fileContent, line)
3126
- }
3306
+ confidence,
3307
+ evidence: ev
3127
3308
  };
3128
3309
  if (!graph.hasEdge(edge.id)) {
3129
3310
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3136,7 +3317,7 @@ async function addHttpCallEdges(graph, services) {
3136
3317
 
3137
3318
  // src/extract/calls/kafka.ts
3138
3319
  init_cjs_shims();
3139
- var import_node_path20 = __toESM(require("path"), 1);
3320
+ var import_node_path21 = __toESM(require("path"), 1);
3140
3321
  var import_types10 = require("@neat.is/types");
3141
3322
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3142
3323
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3162,8 +3343,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3162
3343
  name: topic,
3163
3344
  kind: "kafka-topic",
3164
3345
  edgeType,
3346
+ // `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
3347
+ // framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
3348
+ // tier (ADR-066).
3349
+ confidenceKind: "verified-call-site",
3165
3350
  evidence: {
3166
- file: import_node_path20.default.relative(serviceDir, file.path),
3351
+ file: import_node_path21.default.relative(serviceDir, file.path),
3167
3352
  line,
3168
3353
  snippet: snippet(file.content, line)
3169
3354
  }
@@ -3176,7 +3361,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3176
3361
 
3177
3362
  // src/extract/calls/redis.ts
3178
3363
  init_cjs_shims();
3179
- var import_node_path21 = __toESM(require("path"), 1);
3364
+ var import_node_path22 = __toESM(require("path"), 1);
3180
3365
  var import_types11 = require("@neat.is/types");
3181
3366
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3182
3367
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3194,8 +3379,12 @@ function redisEndpointsFromFile(file, serviceDir) {
3194
3379
  name: host,
3195
3380
  kind: "redis",
3196
3381
  edgeType: "CALLS",
3382
+ // `redis://host` URL literal — the scheme is structural support, but no
3383
+ // call expression is verified to wire it through. URL-with-structural-
3384
+ // support tier (ADR-066).
3385
+ confidenceKind: "url-with-structural-support",
3197
3386
  evidence: {
3198
- file: import_node_path21.default.relative(serviceDir, file.path),
3387
+ file: import_node_path22.default.relative(serviceDir, file.path),
3199
3388
  line,
3200
3389
  snippet: snippet(file.content, line)
3201
3390
  }
@@ -3206,7 +3395,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3206
3395
 
3207
3396
  // src/extract/calls/aws.ts
3208
3397
  init_cjs_shims();
3209
- var import_node_path22 = __toESM(require("path"), 1);
3398
+ var import_node_path23 = __toESM(require("path"), 1);
3210
3399
  var import_types12 = require("@neat.is/types");
3211
3400
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3212
3401
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3235,8 +3424,12 @@ function awsEndpointsFromFile(file, serviceDir) {
3235
3424
  name,
3236
3425
  kind,
3237
3426
  edgeType: "CALLS",
3427
+ // SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
3428
+ // literal — framework-aware recognizer, verified-call-site tier
3429
+ // (ADR-066).
3430
+ confidenceKind: "verified-call-site",
3238
3431
  evidence: {
3239
- file: import_node_path22.default.relative(serviceDir, file.path),
3432
+ file: import_node_path23.default.relative(serviceDir, file.path),
3240
3433
  line,
3241
3434
  snippet: snippet(file.content, line)
3242
3435
  }
@@ -3261,16 +3454,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3261
3454
 
3262
3455
  // src/extract/calls/grpc.ts
3263
3456
  init_cjs_shims();
3264
- var import_node_path23 = __toESM(require("path"), 1);
3457
+ var import_node_path24 = __toESM(require("path"), 1);
3265
3458
  var import_types13 = require("@neat.is/types");
3266
3459
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3460
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3461
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3267
3462
  function isLikelyAddress(value) {
3268
3463
  if (!value) return false;
3269
3464
  return /:\d{2,5}$/.test(value) || value.includes(".");
3270
3465
  }
3466
+ function normaliseForMatch(s) {
3467
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3468
+ }
3469
+ function readImports(content) {
3470
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3471
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3472
+ let m;
3473
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3474
+ const raw = m[1];
3475
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3476
+ }
3477
+ return {
3478
+ awsSdkSuffixes,
3479
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3480
+ };
3481
+ }
3482
+ function classifyClient(symbol, ctx) {
3483
+ const key = normaliseForMatch(symbol);
3484
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3485
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3486
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3487
+ return null;
3488
+ }
3271
3489
  function grpcEndpointsFromFile(file, serviceDir) {
3272
3490
  const out = [];
3273
3491
  const seen = /* @__PURE__ */ new Set();
3492
+ const ctx = readImports(file.content);
3274
3493
  GRPC_CLIENT_RE.lastIndex = 0;
3275
3494
  let m;
3276
3495
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3278,15 +3497,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
3278
3497
  const addr = m[2]?.trim();
3279
3498
  const name = isLikelyAddress(addr) ? addr : symbol;
3280
3499
  if (seen.has(name)) continue;
3500
+ const classified = classifyClient(symbol, ctx);
3501
+ if (!classified) continue;
3281
3502
  seen.add(name);
3503
+ const { kind } = classified;
3282
3504
  const line = lineOf(file.content, m[0]);
3283
3505
  out.push({
3284
- infraId: (0, import_types13.infraId)("grpc-service", name),
3506
+ infraId: (0, import_types13.infraId)(kind, name),
3285
3507
  name,
3286
- kind: "grpc-service",
3508
+ kind,
3287
3509
  edgeType: "CALLS",
3510
+ // `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
3511
+ // context — import-aware classification per #238. Verified-call-site
3512
+ // tier (ADR-066).
3513
+ confidenceKind: "verified-call-site",
3288
3514
  evidence: {
3289
- file: import_node_path23.default.relative(serviceDir, file.path),
3515
+ file: import_node_path24.default.relative(serviceDir, file.path),
3290
3516
  line,
3291
3517
  snippet: snippet(file.content, line)
3292
3518
  }
@@ -3306,6 +3532,9 @@ function edgeTypeFromEndpoint(ep) {
3306
3532
  return import_types14.EdgeType.CALLS;
3307
3533
  }
3308
3534
  }
3535
+ function isAwsKind(kind) {
3536
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3537
+ }
3309
3538
  async function addExternalEndpointEdges(graph, services) {
3310
3539
  let nodesAdded = 0;
3311
3540
  let edgesAdded = 0;
@@ -3313,10 +3542,13 @@ async function addExternalEndpointEdges(graph, services) {
3313
3542
  const files = await loadSourceFiles(service.dir);
3314
3543
  const endpoints = [];
3315
3544
  for (const file of files) {
3316
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3317
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3318
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3319
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3545
+ if (isTestPath(file.path)) continue;
3546
+ const masked = maskCommentsInSource(file.content);
3547
+ const maskedFile = { path: file.path, content: masked };
3548
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3549
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3550
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3551
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3320
3552
  }
3321
3553
  if (endpoints.length === 0) continue;
3322
3554
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3326,7 +3558,10 @@ async function addExternalEndpointEdges(graph, services) {
3326
3558
  id: ep.infraId,
3327
3559
  type: import_types14.NodeType.InfraNode,
3328
3560
  name: ep.name,
3329
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3561
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3562
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3563
+ // bucket / table kinds from aws.ts.
3564
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3330
3565
  kind: ep.kind
3331
3566
  };
3332
3567
  graph.addNode(node.id, node);
@@ -3336,6 +3571,18 @@ async function addExternalEndpointEdges(graph, services) {
3336
3571
  const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3337
3572
  if (seenEdges.has(edgeId)) continue;
3338
3573
  seenEdges.add(edgeId);
3574
+ const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
3575
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
3576
+ noteExtractedDropped({
3577
+ source: service.node.id,
3578
+ target: ep.infraId,
3579
+ type: edgeType,
3580
+ confidence,
3581
+ confidenceKind: ep.confidenceKind,
3582
+ evidence: ep.evidence
3583
+ });
3584
+ continue;
3585
+ }
3339
3586
  if (!graph.hasEdge(edgeId)) {
3340
3587
  const edge = {
3341
3588
  id: edgeId,
@@ -3343,6 +3590,7 @@ async function addExternalEndpointEdges(graph, services) {
3343
3590
  target: ep.infraId,
3344
3591
  type: edgeType,
3345
3592
  provenance: import_types14.Provenance.EXTRACTED,
3593
+ confidence,
3346
3594
  evidence: ep.evidence
3347
3595
  };
3348
3596
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3366,7 +3614,7 @@ init_cjs_shims();
3366
3614
 
3367
3615
  // src/extract/infra/docker-compose.ts
3368
3616
  init_cjs_shims();
3369
- var import_node_path24 = __toESM(require("path"), 1);
3617
+ var import_node_path25 = __toESM(require("path"), 1);
3370
3618
  var import_types16 = require("@neat.is/types");
3371
3619
 
3372
3620
  // src/extract/infra/shared.ts
@@ -3404,7 +3652,7 @@ function dependsOnList(value) {
3404
3652
  }
3405
3653
  function serviceNameToServiceNode(name, services) {
3406
3654
  for (const s of services) {
3407
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3655
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3408
3656
  }
3409
3657
  return null;
3410
3658
  }
@@ -3413,7 +3661,7 @@ async function addComposeInfra(graph, scanPath, services) {
3413
3661
  let edgesAdded = 0;
3414
3662
  let composePath = null;
3415
3663
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3416
- const abs = import_node_path24.default.join(scanPath, name);
3664
+ const abs = import_node_path25.default.join(scanPath, name);
3417
3665
  if (await exists(abs)) {
3418
3666
  composePath = abs;
3419
3667
  break;
@@ -3424,13 +3672,15 @@ async function addComposeInfra(graph, scanPath, services) {
3424
3672
  try {
3425
3673
  compose = await readYaml(composePath);
3426
3674
  } catch (err) {
3427
- console.warn(
3428
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3675
+ recordExtractionError(
3676
+ "infra docker-compose",
3677
+ import_node_path25.default.relative(scanPath, composePath),
3678
+ err
3429
3679
  );
3430
3680
  return { nodesAdded, edgesAdded };
3431
3681
  }
3432
3682
  if (!compose?.services) return { nodesAdded, edgesAdded };
3433
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3683
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3434
3684
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3435
3685
  for (const [composeName, svc] of Object.entries(compose.services)) {
3436
3686
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3460,6 +3710,7 @@ async function addComposeInfra(graph, scanPath, services) {
3460
3710
  target: targetId,
3461
3711
  type: import_types16.EdgeType.DEPENDS_ON,
3462
3712
  provenance: import_types16.Provenance.EXTRACTED,
3713
+ confidence: (0, import_types16.confidenceForExtracted)("structural"),
3463
3714
  evidence: { file: evidenceFile }
3464
3715
  };
3465
3716
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3471,8 +3722,8 @@ async function addComposeInfra(graph, scanPath, services) {
3471
3722
 
3472
3723
  // src/extract/infra/dockerfile.ts
3473
3724
  init_cjs_shims();
3474
- var import_node_path25 = __toESM(require("path"), 1);
3475
- var import_node_fs13 = require("fs");
3725
+ var import_node_path26 = __toESM(require("path"), 1);
3726
+ var import_node_fs14 = require("fs");
3476
3727
  var import_types17 = require("@neat.is/types");
3477
3728
  function runtimeImage(content) {
3478
3729
  const lines = content.split("\n");
@@ -3492,14 +3743,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3492
3743
  let nodesAdded = 0;
3493
3744
  let edgesAdded = 0;
3494
3745
  for (const service of services) {
3495
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3746
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3496
3747
  if (!await exists(dockerfilePath)) continue;
3497
3748
  let content;
3498
3749
  try {
3499
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3750
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3500
3751
  } catch (err) {
3501
- console.warn(
3502
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3752
+ recordExtractionError(
3753
+ "infra dockerfile",
3754
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3755
+ err
3503
3756
  );
3504
3757
  continue;
3505
3758
  }
@@ -3518,8 +3771,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3518
3771
  target: node.id,
3519
3772
  type: import_types17.EdgeType.RUNS_ON,
3520
3773
  provenance: import_types17.Provenance.EXTRACTED,
3774
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
3521
3775
  evidence: {
3522
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3776
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3523
3777
  }
3524
3778
  };
3525
3779
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3531,19 +3785,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3531
3785
 
3532
3786
  // src/extract/infra/terraform.ts
3533
3787
  init_cjs_shims();
3534
- var import_node_fs14 = require("fs");
3535
- var import_node_path26 = __toESM(require("path"), 1);
3788
+ var import_node_fs15 = require("fs");
3789
+ var import_node_path27 = __toESM(require("path"), 1);
3536
3790
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3537
3791
  async function walkTfFiles(start, depth = 0, max = 5) {
3538
3792
  if (depth > max) return [];
3539
3793
  const out = [];
3540
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3794
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3541
3795
  for (const entry2 of entries) {
3542
3796
  if (entry2.isDirectory()) {
3543
3797
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
3544
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
3798
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3545
3799
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
3546
- out.push(import_node_path26.default.join(start, entry2.name));
3800
+ out.push(import_node_path27.default.join(start, entry2.name));
3547
3801
  }
3548
3802
  }
3549
3803
  return out;
@@ -3552,7 +3806,7 @@ async function addTerraformResources(graph, scanPath) {
3552
3806
  let nodesAdded = 0;
3553
3807
  const files = await walkTfFiles(scanPath);
3554
3808
  for (const file of files) {
3555
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3809
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3556
3810
  RESOURCE_RE.lastIndex = 0;
3557
3811
  let m;
3558
3812
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3570,8 +3824,8 @@ async function addTerraformResources(graph, scanPath) {
3570
3824
 
3571
3825
  // src/extract/infra/k8s.ts
3572
3826
  init_cjs_shims();
3573
- var import_node_fs15 = require("fs");
3574
- var import_node_path27 = __toESM(require("path"), 1);
3827
+ var import_node_fs16 = require("fs");
3828
+ var import_node_path28 = __toESM(require("path"), 1);
3575
3829
  var import_yaml3 = require("yaml");
3576
3830
  var K8S_KIND_TO_INFRA_KIND = {
3577
3831
  Service: "k8s-service",
@@ -3585,13 +3839,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3585
3839
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3586
3840
  if (depth > max) return [];
3587
3841
  const out = [];
3588
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3842
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3589
3843
  for (const entry2 of entries) {
3590
3844
  if (entry2.isDirectory()) {
3591
3845
  if (IGNORED_DIRS.has(entry2.name)) continue;
3592
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
3593
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
3594
- out.push(import_node_path27.default.join(start, entry2.name));
3846
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry2.name), depth + 1, max));
3847
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry2.name))) {
3848
+ out.push(import_node_path28.default.join(start, entry2.name));
3595
3849
  }
3596
3850
  }
3597
3851
  return out;
@@ -3600,7 +3854,7 @@ async function addK8sResources(graph, scanPath) {
3600
3854
  let nodesAdded = 0;
3601
3855
  const files = await walkYamlFiles2(scanPath);
3602
3856
  for (const file of files) {
3603
- const content = await import_node_fs15.promises.readFile(file, "utf8");
3857
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3604
3858
  let docs;
3605
3859
  try {
3606
3860
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3634,9 +3888,37 @@ async function addInfra(graph, scanPath, services) {
3634
3888
  };
3635
3889
  }
3636
3890
 
3891
+ // src/extract/index.ts
3892
+ var import_node_path30 = __toESM(require("path"), 1);
3893
+
3894
+ // src/extract/retire.ts
3895
+ init_cjs_shims();
3896
+ var import_node_fs17 = require("fs");
3897
+ var import_node_path29 = __toESM(require("path"), 1);
3898
+ var import_types18 = require("@neat.is/types");
3899
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
3900
+ const toDrop = [];
3901
+ const bases = [scanPath, ...serviceDirs];
3902
+ graph.forEachEdge((id, attrs) => {
3903
+ const edge = attrs;
3904
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
3905
+ const evidenceFile = edge.evidence?.file;
3906
+ if (!evidenceFile) return;
3907
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
3908
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
3909
+ return;
3910
+ }
3911
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
3912
+ if (!found) toDrop.push(id);
3913
+ });
3914
+ for (const id of toDrop) graph.dropEdge(id);
3915
+ return toDrop.length;
3916
+ }
3917
+
3637
3918
  // src/extract/index.ts
3638
3919
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3639
3920
  await ensureCompatLoaded();
3921
+ drainExtractionErrors();
3640
3922
  const services = await discoverServices(scanPath);
3641
3923
  const phase1Nodes = addServiceNodes(graph, services);
3642
3924
  await addServiceAliases(graph, scanPath, services);
@@ -3644,12 +3926,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3644
3926
  const phase3 = await addConfigNodes(graph, services, scanPath);
3645
3927
  const phase4 = await addCallEdges(graph, services);
3646
3928
  const phase5 = await addInfra(graph, scanPath, services);
3929
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
3930
+ graph,
3931
+ scanPath,
3932
+ services.map((s) => s.dir)
3933
+ );
3647
3934
  const frontiersPromoted = promoteFrontierNodes(graph);
3648
3935
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3936
+ const errorEntries = drainExtractionErrors();
3937
+ if (opts.errorsPath && errorEntries.length > 0) {
3938
+ try {
3939
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
3940
+ } catch (err) {
3941
+ console.warn(
3942
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
3943
+ );
3944
+ }
3945
+ }
3946
+ const droppedEntries = drainDroppedExtracted();
3947
+ if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
3948
+ const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
3949
+ try {
3950
+ await writeRejectedExtracted(droppedEntries, rejectedPath);
3951
+ } catch (err) {
3952
+ console.warn(
3953
+ `[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
3954
+ );
3955
+ }
3956
+ }
3649
3957
  const result = {
3650
3958
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3651
3959
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3652
- frontiersPromoted
3960
+ frontiersPromoted,
3961
+ extractionErrors: errorEntries.length,
3962
+ errorEntries,
3963
+ ghostsRetired,
3964
+ extractedDropped: droppedEntries.length,
3965
+ droppedEntries
3653
3966
  };
3654
3967
  emitNeatEvent({
3655
3968
  type: "extraction-complete",
@@ -3666,8 +3979,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3666
3979
 
3667
3980
  // src/persist.ts
3668
3981
  init_cjs_shims();
3669
- var import_node_fs16 = require("fs");
3670
- var import_node_path28 = __toESM(require("path"), 1);
3982
+ var import_node_fs18 = require("fs");
3983
+ var import_node_path31 = __toESM(require("path"), 1);
3671
3984
  var SCHEMA_VERSION = 2;
3672
3985
  function migrateV1ToV2(payload) {
3673
3986
  const nodes = payload.graph.nodes;
@@ -3681,7 +3994,7 @@ function migrateV1ToV2(payload) {
3681
3994
  return { ...payload, schemaVersion: 2 };
3682
3995
  }
3683
3996
  async function ensureDir(filePath) {
3684
- await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
3997
+ await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
3685
3998
  }
3686
3999
  async function saveGraphToDisk(graph, outPath) {
3687
4000
  await ensureDir(outPath);
@@ -3691,13 +4004,13 @@ async function saveGraphToDisk(graph, outPath) {
3691
4004
  graph: graph.export()
3692
4005
  };
3693
4006
  const tmp = `${outPath}.tmp`;
3694
- await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3695
- await import_node_fs16.promises.rename(tmp, outPath);
4007
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4008
+ await import_node_fs18.promises.rename(tmp, outPath);
3696
4009
  }
3697
4010
  async function loadGraphFromDisk(graph, outPath) {
3698
4011
  let raw;
3699
4012
  try {
3700
- raw = await import_node_fs16.promises.readFile(outPath, "utf8");
4013
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
3701
4014
  } catch (err) {
3702
4015
  if (err.code === "ENOENT") return;
3703
4016
  throw err;
@@ -3750,23 +4063,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3750
4063
 
3751
4064
  // src/projects.ts
3752
4065
  init_cjs_shims();
3753
- var import_node_path29 = __toESM(require("path"), 1);
4066
+ var import_node_path32 = __toESM(require("path"), 1);
3754
4067
  function pathsForProject(project, baseDir) {
3755
4068
  if (project === DEFAULT_PROJECT) {
3756
4069
  return {
3757
- snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
3758
- errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
3759
- staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
3760
- embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
3761
- policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
4070
+ snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4071
+ errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4072
+ staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4073
+ embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4074
+ policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
3762
4075
  };
3763
4076
  }
3764
4077
  return {
3765
- snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
3766
- errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
3767
- staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
3768
- embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
3769
- policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
4078
+ snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4079
+ errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4080
+ staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4081
+ embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4082
+ policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
3770
4083
  };
3771
4084
  }
3772
4085
  var Projects = class {
@@ -3804,11 +4117,11 @@ var Projects = class {
3804
4117
  init_cjs_shims();
3805
4118
  var import_fastify = __toESM(require("fastify"), 1);
3806
4119
  var import_cors = __toESM(require("@fastify/cors"), 1);
3807
- var import_types20 = require("@neat.is/types");
4120
+ var import_types21 = require("@neat.is/types");
3808
4121
 
3809
4122
  // src/divergences.ts
3810
4123
  init_cjs_shims();
3811
- var import_types18 = require("@neat.is/types");
4124
+ var import_types19 = require("@neat.is/types");
3812
4125
  function bucketKey(source, target, type) {
3813
4126
  return `${type}|${source}|${target}`;
3814
4127
  }
@@ -3816,25 +4129,25 @@ function bucketEdges(graph) {
3816
4129
  const buckets = /* @__PURE__ */ new Map();
3817
4130
  graph.forEachEdge((id, attrs) => {
3818
4131
  const e = attrs;
3819
- const parsed = (0, import_types18.parseEdgeId)(id);
4132
+ const parsed = (0, import_types19.parseEdgeId)(id);
3820
4133
  const provenance = parsed?.provenance ?? e.provenance;
3821
4134
  const key = bucketKey(e.source, e.target, e.type);
3822
4135
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3823
4136
  switch (provenance) {
3824
- case import_types18.Provenance.EXTRACTED:
4137
+ case import_types19.Provenance.EXTRACTED:
3825
4138
  cur.extracted = e;
3826
4139
  break;
3827
- case import_types18.Provenance.OBSERVED:
4140
+ case import_types19.Provenance.OBSERVED:
3828
4141
  cur.observed = e;
3829
4142
  break;
3830
- case import_types18.Provenance.INFERRED:
4143
+ case import_types19.Provenance.INFERRED:
3831
4144
  cur.inferred = e;
3832
4145
  break;
3833
- case import_types18.Provenance.FRONTIER:
4146
+ case import_types19.Provenance.FRONTIER:
3834
4147
  cur.frontier = e;
3835
4148
  break;
3836
4149
  default:
3837
- if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
4150
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
3838
4151
  }
3839
4152
  buckets.set(key, cur);
3840
4153
  });
@@ -3843,15 +4156,7 @@ function bucketEdges(graph) {
3843
4156
  function nodeIsFrontier(graph, nodeId) {
3844
4157
  if (!graph.hasNode(nodeId)) return false;
3845
4158
  const attrs = graph.getNodeAttributes(nodeId);
3846
- return attrs.type === import_types18.NodeType.FrontierNode;
3847
- }
3848
- function hasAnyObservedFromSource(graph, sourceId) {
3849
- if (!graph.hasNode(sourceId)) return false;
3850
- for (const edgeId of graph.outboundEdges(sourceId)) {
3851
- const e = graph.getEdgeAttributes(edgeId);
3852
- if (e.provenance === import_types18.Provenance.OBSERVED) return true;
3853
- }
3854
- return false;
4159
+ return attrs.type === import_types19.NodeType.FrontierNode;
3855
4160
  }
3856
4161
  function clampConfidence(n) {
3857
4162
  if (!Number.isFinite(n)) return 0;
@@ -3866,32 +4171,34 @@ function reasonForMissingExtracted(source, target, type) {
3866
4171
  var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
3867
4172
  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.";
3868
4173
  var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
4174
+ function gradedConfidence(edge) {
4175
+ if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
4176
+ return clampConfidence(confidenceForEdge(edge));
4177
+ }
3869
4178
  function detectMissingDivergences(graph, bucket) {
3870
4179
  const out = [];
3871
4180
  if (bucket.extracted && !bucket.observed) {
3872
4181
  if (!nodeIsFrontier(graph, bucket.target)) {
3873
- const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
3874
4182
  out.push({
3875
4183
  type: "missing-observed",
3876
4184
  source: bucket.source,
3877
4185
  target: bucket.target,
3878
4186
  edgeType: bucket.type,
3879
4187
  extracted: bucket.extracted,
3880
- confidence: sourceHasTraffic ? 1 : 0.5,
4188
+ confidence: gradedConfidence(bucket.extracted),
3881
4189
  reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
3882
4190
  recommendation: RECOMMENDATION_MISSING_OBSERVED
3883
4191
  });
3884
4192
  }
3885
4193
  }
3886
4194
  if (bucket.observed && !bucket.extracted) {
3887
- const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
3888
4195
  out.push({
3889
4196
  type: "missing-extracted",
3890
4197
  source: bucket.source,
3891
4198
  target: bucket.target,
3892
4199
  edgeType: bucket.type,
3893
4200
  observed: bucket.observed,
3894
- confidence: cascaded,
4201
+ confidence: gradedConfidence(bucket.observed),
3895
4202
  reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
3896
4203
  recommendation: RECOMMENDATION_MISSING_EXTRACTED
3897
4204
  });
@@ -3910,7 +4217,7 @@ function declaredHostFor(svc) {
3910
4217
  function hasExtractedConfiguredBy(graph, svcId) {
3911
4218
  for (const edgeId of graph.outboundEdges(svcId)) {
3912
4219
  const e = graph.getEdgeAttributes(edgeId);
3913
- if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
4220
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
3914
4221
  return true;
3915
4222
  }
3916
4223
  }
@@ -3923,10 +4230,10 @@ function detectHostMismatch(graph, svcId, svc) {
3923
4230
  const out = [];
3924
4231
  for (const edgeId of graph.outboundEdges(svcId)) {
3925
4232
  const edge = graph.getEdgeAttributes(edgeId);
3926
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3927
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4233
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4234
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
3928
4235
  const target = graph.getNodeAttributes(edge.target);
3929
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4236
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
3930
4237
  const observedHost = target.host?.trim();
3931
4238
  if (!observedHost) continue;
3932
4239
  if (observedHost === declaredHost) continue;
@@ -3948,10 +4255,10 @@ function detectCompatDivergences(graph, svcId, svc) {
3948
4255
  const deps = svc.dependencies ?? {};
3949
4256
  for (const edgeId of graph.outboundEdges(svcId)) {
3950
4257
  const edge = graph.getEdgeAttributes(edgeId);
3951
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3952
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4258
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4259
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
3953
4260
  const target = graph.getNodeAttributes(edge.target);
3954
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4261
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
3955
4262
  for (const pair of compatPairs()) {
3956
4263
  if (pair.engine !== target.engine) continue;
3957
4264
  const declared = deps[pair.driver];
@@ -4012,7 +4319,7 @@ function computeDivergences(graph, opts = {}) {
4012
4319
  }
4013
4320
  graph.forEachNode((nodeId, attrs) => {
4014
4321
  const n = attrs;
4015
- if (n.type !== import_types18.NodeType.ServiceNode) return;
4322
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4016
4323
  const svc = n;
4017
4324
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4018
4325
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4030,13 +4337,22 @@ function computeDivergences(graph, opts = {}) {
4030
4337
  const target = opts.node;
4031
4338
  filtered = filtered.filter((d) => involvesNode(d, target));
4032
4339
  }
4340
+ const TYPE_LEADERSHIP = {
4341
+ "missing-extracted": 0,
4342
+ "missing-observed": 1,
4343
+ "version-mismatch": 2,
4344
+ "host-mismatch": 3,
4345
+ "compat-violation": 4
4346
+ };
4033
4347
  filtered.sort((a, b) => {
4034
4348
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4349
+ const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
4350
+ if (lead !== 0) return lead;
4035
4351
  if (a.type !== b.type) return a.type.localeCompare(b.type);
4036
4352
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4037
4353
  return a.target.localeCompare(b.target);
4038
4354
  });
4039
- return import_types18.DivergenceResultSchema.parse({
4355
+ return import_types19.DivergenceResultSchema.parse({
4040
4356
  divergences: filtered,
4041
4357
  totalAffected: filtered.length,
4042
4358
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4045,7 +4361,7 @@ function computeDivergences(graph, opts = {}) {
4045
4361
 
4046
4362
  // src/diff.ts
4047
4363
  init_cjs_shims();
4048
- var import_node_fs17 = require("fs");
4364
+ var import_node_fs19 = require("fs");
4049
4365
  async function loadSnapshotForDiff(target) {
4050
4366
  if (/^https?:\/\//i.test(target)) {
4051
4367
  const res = await fetch(target);
@@ -4054,7 +4370,7 @@ async function loadSnapshotForDiff(target) {
4054
4370
  }
4055
4371
  return await res.json();
4056
4372
  }
4057
- const raw = await import_node_fs17.promises.readFile(target, "utf8");
4373
+ const raw = await import_node_fs19.promises.readFile(target, "utf8");
4058
4374
  return JSON.parse(raw);
4059
4375
  }
4060
4376
  function indexEntries(entries) {
@@ -4122,41 +4438,41 @@ function canonicalJson(value) {
4122
4438
 
4123
4439
  // src/registry.ts
4124
4440
  init_cjs_shims();
4125
- var import_node_fs18 = require("fs");
4441
+ var import_node_fs20 = require("fs");
4126
4442
  var import_node_os2 = __toESM(require("os"), 1);
4127
- var import_node_path30 = __toESM(require("path"), 1);
4128
- var import_types19 = require("@neat.is/types");
4443
+ var import_node_path33 = __toESM(require("path"), 1);
4444
+ var import_types20 = require("@neat.is/types");
4129
4445
  var LOCK_TIMEOUT_MS = 5e3;
4130
4446
  var LOCK_RETRY_MS = 50;
4131
4447
  function neatHome() {
4132
4448
  const override = process.env.NEAT_HOME;
4133
- if (override && override.length > 0) return import_node_path30.default.resolve(override);
4134
- return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4449
+ if (override && override.length > 0) return import_node_path33.default.resolve(override);
4450
+ return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
4135
4451
  }
4136
4452
  function registryPath() {
4137
- return import_node_path30.default.join(neatHome(), "projects.json");
4453
+ return import_node_path33.default.join(neatHome(), "projects.json");
4138
4454
  }
4139
4455
  function registryLockPath() {
4140
- return import_node_path30.default.join(neatHome(), "projects.json.lock");
4456
+ return import_node_path33.default.join(neatHome(), "projects.json.lock");
4141
4457
  }
4142
4458
  async function writeAtomically(target, contents) {
4143
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4459
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4144
4460
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4145
- const fd = await import_node_fs18.promises.open(tmp, "w");
4461
+ const fd = await import_node_fs20.promises.open(tmp, "w");
4146
4462
  try {
4147
4463
  await fd.writeFile(contents, "utf8");
4148
4464
  await fd.sync();
4149
4465
  } finally {
4150
4466
  await fd.close();
4151
4467
  }
4152
- await import_node_fs18.promises.rename(tmp, target);
4468
+ await import_node_fs20.promises.rename(tmp, target);
4153
4469
  }
4154
4470
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4155
4471
  const deadline = Date.now() + timeoutMs;
4156
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4472
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
4157
4473
  while (true) {
4158
4474
  try {
4159
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4475
+ const fd = await import_node_fs20.promises.open(lockPath, "wx");
4160
4476
  await fd.close();
4161
4477
  return;
4162
4478
  } catch (err) {
@@ -4172,7 +4488,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4172
4488
  }
4173
4489
  }
4174
4490
  async function releaseLock(lockPath) {
4175
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4491
+ await import_node_fs20.promises.unlink(lockPath).catch(() => {
4176
4492
  });
4177
4493
  }
4178
4494
  async function withLock(fn) {
@@ -4188,7 +4504,7 @@ async function readRegistry() {
4188
4504
  const file = registryPath();
4189
4505
  let raw;
4190
4506
  try {
4191
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4507
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4192
4508
  } catch (err) {
4193
4509
  if (err.code === "ENOENT") {
4194
4510
  return { version: 1, projects: [] };
@@ -4196,10 +4512,10 @@ async function readRegistry() {
4196
4512
  throw err;
4197
4513
  }
4198
4514
  const parsed = JSON.parse(raw);
4199
- return import_types19.RegistryFileSchema.parse(parsed);
4515
+ return import_types20.RegistryFileSchema.parse(parsed);
4200
4516
  }
4201
4517
  async function writeRegistry(reg) {
4202
- const validated = import_types19.RegistryFileSchema.parse(reg);
4518
+ const validated = import_types20.RegistryFileSchema.parse(reg);
4203
4519
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4204
4520
  }
4205
4521
  async function getProject(name) {
@@ -4409,11 +4725,11 @@ function registerRoutes(scope, ctx) {
4409
4725
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4410
4726
  const parsed = [];
4411
4727
  for (const c of candidates) {
4412
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4728
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4413
4729
  if (!r.success) {
4414
4730
  return reply.code(400).send({
4415
4731
  error: `unknown divergence type "${c}"`,
4416
- allowed: import_types20.DivergenceTypeSchema.options
4732
+ allowed: import_types21.DivergenceTypeSchema.options
4417
4733
  });
4418
4734
  }
4419
4735
  parsed.push(r.data);
@@ -4597,7 +4913,7 @@ function registerRoutes(scope, ctx) {
4597
4913
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4598
4914
  let violations = await log.readAll();
4599
4915
  if (req2.query.severity) {
4600
- const sev = import_types20.PolicySeveritySchema.safeParse(req2.query.severity);
4916
+ const sev = import_types21.PolicySeveritySchema.safeParse(req2.query.severity);
4601
4917
  if (!sev.success) {
4602
4918
  return reply.code(400).send({
4603
4919
  error: "invalid severity",
@@ -4614,7 +4930,7 @@ function registerRoutes(scope, ctx) {
4614
4930
  scope.post("/policies/check", async (req2, reply) => {
4615
4931
  const proj = resolveProject(registry, req2, reply);
4616
4932
  if (!proj) return;
4617
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4933
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4618
4934
  if (!parsed.success) {
4619
4935
  return reply.code(400).send({
4620
4936
  error: "invalid /policies/check body",
@@ -4715,11 +5031,11 @@ async function buildApi(opts) {
4715
5031
  // src/daemon.ts
4716
5032
  init_otel();
4717
5033
  function neatHomeFor(opts) {
4718
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
5034
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
4719
5035
  const env = process.env.NEAT_HOME;
4720
- if (env && env.length > 0) return import_node_path33.default.resolve(env);
5036
+ if (env && env.length > 0) return import_node_path36.default.resolve(env);
4721
5037
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4722
- return import_node_path33.default.join(home, ".neat");
5038
+ return import_node_path36.default.join(home, ".neat");
4723
5039
  }
4724
5040
  function routeSpanToProject(serviceName, projects) {
4725
5041
  if (!serviceName) return DEFAULT_PROJECT;
@@ -4732,9 +5048,9 @@ function routeSpanToProject(serviceName, projects) {
4732
5048
  return DEFAULT_PROJECT;
4733
5049
  }
4734
5050
  async function bootstrapProject(entry2) {
4735
- const paths = pathsForProject(entry2.name, import_node_path33.default.join(entry2.path, "neat-out"));
5051
+ const paths = pathsForProject(entry2.name, import_node_path36.default.join(entry2.path, "neat-out"));
4736
5052
  try {
4737
- const stat = await import_node_fs19.promises.stat(entry2.path);
5053
+ const stat = await import_node_fs21.promises.stat(entry2.path);
4738
5054
  if (!stat.isDirectory()) {
4739
5055
  throw new Error(`registered path ${entry2.path} is not a directory`);
4740
5056
  }
@@ -4799,13 +5115,13 @@ async function startDaemon(opts = {}) {
4799
5115
  const home = neatHomeFor(opts);
4800
5116
  const regPath = registryPath();
4801
5117
  try {
4802
- await import_node_fs19.promises.access(regPath);
5118
+ await import_node_fs21.promises.access(regPath);
4803
5119
  } catch {
4804
5120
  throw new Error(
4805
5121
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
4806
5122
  );
4807
5123
  }
4808
- const pidPath = import_node_path33.default.join(home, "neatd.pid");
5124
+ const pidPath = import_node_path36.default.join(home, "neatd.pid");
4809
5125
  await writeAtomically(pidPath, `${process.pid}
4810
5126
  `);
4811
5127
  const slots = /* @__PURE__ */ new Map();
@@ -4874,7 +5190,7 @@ async function startDaemon(opts = {}) {
4874
5190
  }
4875
5191
  if (restApp) await restApp.close().catch(() => {
4876
5192
  });
4877
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5193
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
4878
5194
  });
4879
5195
  throw new Error(
4880
5196
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -4919,7 +5235,7 @@ async function startDaemon(opts = {}) {
4919
5235
  });
4920
5236
  if (otlpApp) await otlpApp.close().catch(() => {
4921
5237
  });
4922
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5238
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
4923
5239
  });
4924
5240
  throw new Error(
4925
5241
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -4959,7 +5275,7 @@ async function startDaemon(opts = {}) {
4959
5275
  } catch {
4960
5276
  }
4961
5277
  }
4962
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5278
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
4963
5279
  });
4964
5280
  };
4965
5281
  return { slots, reload, stop, pidPath, restAddress, otlpAddress };
@@ -4969,7 +5285,7 @@ async function startDaemon(opts = {}) {
4969
5285
  init_cjs_shims();
4970
5286
  var import_node_child_process = require("child_process");
4971
5287
  var import_node_net = __toESM(require("net"), 1);
4972
- var import_node_path34 = __toESM(require("path"), 1);
5288
+ var import_node_path37 = __toESM(require("path"), 1);
4973
5289
  var DEFAULT_WEB_PORT = 6328;
4974
5290
  async function assertPortFree(port) {
4975
5291
  await new Promise((resolve, reject) => {
@@ -4997,10 +5313,10 @@ function resolveWebPackageDir() {
4997
5313
  eval("require")
4998
5314
  );
4999
5315
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
5000
- return import_node_path34.default.dirname(pkgJsonPath);
5316
+ return import_node_path37.default.dirname(pkgJsonPath);
5001
5317
  }
5002
5318
  function resolveStandaloneServerEntry(webDir) {
5003
- return import_node_path34.default.join(webDir, ".next/standalone/packages/web/server.js");
5319
+ return import_node_path37.default.join(webDir, ".next/standalone/packages/web/server.js");
5004
5320
  }
5005
5321
  async function spawnWebUI(restPort) {
5006
5322
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -5025,7 +5341,7 @@ async function spawnWebUI(restPort) {
5025
5341
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
5026
5342
  };
5027
5343
  const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
5028
- cwd: import_node_path34.default.dirname(serverEntry),
5344
+ cwd: import_node_path37.default.dirname(serverEntry),
5029
5345
  env,
5030
5346
  stdio: ["ignore", "inherit", "inherit"],
5031
5347
  detached: false
@@ -5062,14 +5378,14 @@ async function spawnWebUI(restPort) {
5062
5378
  // src/neatd.ts
5063
5379
  function neatHome2() {
5064
5380
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
5065
- return import_node_path35.default.resolve(process.env.NEAT_HOME);
5381
+ return import_node_path38.default.resolve(process.env.NEAT_HOME);
5066
5382
  }
5067
5383
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
5068
- return import_node_path35.default.join(home, ".neat");
5384
+ return import_node_path38.default.join(home, ".neat");
5069
5385
  }
5070
5386
  async function readPid() {
5071
5387
  try {
5072
- const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(neatHome2(), "neatd.pid"), "utf8");
5388
+ const raw = await import_node_fs22.promises.readFile(import_node_path38.default.join(neatHome2(), "neatd.pid"), "utf8");
5073
5389
  const n = Number.parseInt(raw.trim(), 10);
5074
5390
  return Number.isFinite(n) ? n : null;
5075
5391
  } catch {