@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/index.cjs CHANGED
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
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 }
@@ -362,12 +362,12 @@ function logSpanHandler(span) {
362
362
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
363
363
  );
364
364
  }
365
- var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
365
+ var import_node_path35, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
366
366
  var init_otel = __esm({
367
367
  "src/otel.ts"() {
368
368
  "use strict";
369
369
  init_cjs_shims();
370
- import_node_path32 = __toESM(require("path"), 1);
370
+ import_node_path35 = __toESM(require("path"), 1);
371
371
  import_node_url2 = require("url");
372
372
  import_fastify2 = __toESM(require("fastify"), 1);
373
373
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -876,19 +876,19 @@ function confidenceFromMix(edges, now = Date.now()) {
876
876
  function longestIncomingWalk(graph, start, maxDepth) {
877
877
  let best = { path: [start], edges: [] };
878
878
  const visited = /* @__PURE__ */ new Set([start]);
879
- function step(node, path34, edges) {
880
- if (path34.length > best.path.length) {
881
- best = { path: [...path34], edges: [...edges] };
879
+ function step(node, path37, edges) {
880
+ if (path37.length > best.path.length) {
881
+ best = { path: [...path37], edges: [...edges] };
882
882
  }
883
- if (path34.length - 1 >= maxDepth) return;
883
+ if (path37.length - 1 >= maxDepth) return;
884
884
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
885
885
  for (const [srcId, edge] of incoming) {
886
886
  if (visited.has(srcId)) continue;
887
887
  visited.add(srcId);
888
- path34.push(srcId);
888
+ path37.push(srcId);
889
889
  edges.push(edge);
890
- step(srcId, path34, edges);
891
- path34.pop();
890
+ step(srcId, path37, edges);
891
+ path37.pop();
892
892
  edges.pop();
893
893
  visited.delete(srcId);
894
894
  }
@@ -1577,35 +1577,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1577
1577
  const existing = graph.getEdgeAttributes(id);
1578
1578
  const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
1579
1579
  const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
1580
+ const newSignal = {
1581
+ spanCount: newSpanCount,
1582
+ errorCount: newErrorCount,
1583
+ lastObservedAgeMs: 0
1584
+ };
1580
1585
  const updated = {
1581
1586
  ...existing,
1582
1587
  provenance: import_types3.Provenance.OBSERVED,
1583
1588
  lastObserved: ts,
1584
1589
  callCount: newSpanCount,
1585
- signal: {
1586
- spanCount: newSpanCount,
1587
- errorCount: newErrorCount,
1588
- lastObservedAgeMs: 0
1589
- },
1590
- confidence: 1
1590
+ signal: newSignal,
1591
+ confidence: (0, import_types3.confidenceForObservedSignal)(newSignal)
1591
1592
  };
1592
1593
  graph.replaceEdgeAttributes(id, updated);
1593
1594
  return { edge: updated, created: false };
1594
1595
  }
1596
+ const signal = {
1597
+ spanCount: 1,
1598
+ errorCount: isError ? 1 : 0,
1599
+ lastObservedAgeMs: 0
1600
+ };
1595
1601
  const edge = {
1596
1602
  id,
1597
1603
  source,
1598
1604
  target,
1599
1605
  type,
1600
1606
  provenance: import_types3.Provenance.OBSERVED,
1601
- confidence: 1,
1607
+ confidence: (0, import_types3.confidenceForObservedSignal)(signal),
1602
1608
  lastObserved: ts,
1603
1609
  callCount: 1,
1604
- signal: {
1605
- spanCount: 1,
1606
- errorCount: isError ? 1 : 0,
1607
- lastObservedAgeMs: 0
1608
- }
1610
+ signal
1609
1611
  };
1610
1612
  graph.addEdgeWithKey(id, source, target, edge);
1611
1613
  return { edge, created: true };
@@ -1925,8 +1927,8 @@ async function readErrorEvents(errorsPath) {
1925
1927
 
1926
1928
  // src/extract/services.ts
1927
1929
  init_cjs_shims();
1928
- var import_node_fs7 = require("fs");
1929
- var import_node_path7 = __toESM(require("path"), 1);
1930
+ var import_node_fs8 = require("fs");
1931
+ var import_node_path8 = __toESM(require("path"), 1);
1930
1932
  var import_ignore = __toESM(require("ignore"), 1);
1931
1933
  var import_minimatch2 = require("minimatch");
1932
1934
  var import_types5 = require("@neat.is/types");
@@ -1950,9 +1952,107 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
1950
1952
  function isConfigFile(name) {
1951
1953
  const ext = import_node_path4.default.extname(name);
1952
1954
  if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
1953
- if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
1955
+ if (name === ".env" || name.startsWith(".env.")) {
1956
+ if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
1957
+ return { match: true, fileType: "env" };
1958
+ }
1954
1959
  return { match: false, fileType: "" };
1955
1960
  }
1961
+ function isTestPath(filePath) {
1962
+ const normalised = filePath.replace(/\\/g, "/");
1963
+ const segments = normalised.split("/");
1964
+ for (const seg of segments) {
1965
+ if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
1966
+ return true;
1967
+ }
1968
+ }
1969
+ const base = segments[segments.length - 1] ?? "";
1970
+ return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
1971
+ }
1972
+ function isEnvTemplateFile(name) {
1973
+ if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
1974
+ return true;
1975
+ }
1976
+ return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
1977
+ }
1978
+ function maskCommentsInSource(src) {
1979
+ const len = src.length;
1980
+ const out = new Array(len);
1981
+ let i = 0;
1982
+ let inString = 0;
1983
+ let escaped = false;
1984
+ while (i < len) {
1985
+ const c = src[i];
1986
+ if (inString !== 0) {
1987
+ out[i] = c;
1988
+ if (escaped) {
1989
+ escaped = false;
1990
+ } else if (c === "\\") {
1991
+ escaped = true;
1992
+ } else if (c === inString) {
1993
+ inString = 0;
1994
+ }
1995
+ i++;
1996
+ continue;
1997
+ }
1998
+ if (c === "/" && i + 1 < len) {
1999
+ const next = src[i + 1];
2000
+ if (next === "/") {
2001
+ out[i] = " ";
2002
+ out[i + 1] = " ";
2003
+ let j = i + 2;
2004
+ while (j < len && src[j] !== "\n") {
2005
+ out[j] = " ";
2006
+ j++;
2007
+ }
2008
+ i = j;
2009
+ continue;
2010
+ }
2011
+ if (next === "*") {
2012
+ out[i] = " ";
2013
+ out[i + 1] = " ";
2014
+ let j = i + 2;
2015
+ while (j < len) {
2016
+ if (src[j] === "\n") {
2017
+ out[j] = "\n";
2018
+ j++;
2019
+ continue;
2020
+ }
2021
+ if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
2022
+ out[j] = " ";
2023
+ out[j + 1] = " ";
2024
+ j += 2;
2025
+ break;
2026
+ }
2027
+ out[j] = " ";
2028
+ j++;
2029
+ }
2030
+ i = j;
2031
+ continue;
2032
+ }
2033
+ }
2034
+ out[i] = c;
2035
+ if (c === "'" || c === '"' || c === "`") inString = c;
2036
+ i++;
2037
+ }
2038
+ return out.join("");
2039
+ }
2040
+ var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
2041
+ function urlMatchesHost(urlString, host) {
2042
+ if (typeof urlString !== "string" || urlString.length === 0) return false;
2043
+ if (!URL_LIKE.test(urlString)) return false;
2044
+ const [wantedHost, wantedPort] = host.split(":");
2045
+ let parsed;
2046
+ try {
2047
+ const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
2048
+ parsed = new URL(candidate);
2049
+ } catch {
2050
+ return false;
2051
+ }
2052
+ if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
2053
+ if (wantedPort && parsed.port !== wantedPort) return false;
2054
+ return true;
2055
+ }
1956
2056
  function cleanVersion(raw) {
1957
2057
  if (!raw) return void 0;
1958
2058
  return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
@@ -2108,6 +2208,50 @@ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2108
2208
  return author ?? void 0;
2109
2209
  }
2110
2210
 
2211
+ // src/extract/errors.ts
2212
+ init_cjs_shims();
2213
+ var import_node_fs7 = require("fs");
2214
+ var import_node_path7 = __toESM(require("path"), 1);
2215
+ var sink = [];
2216
+ function recordExtractionError(producer, file, err) {
2217
+ const e = err instanceof Error ? err : new Error(String(err));
2218
+ sink.push({
2219
+ producer,
2220
+ file,
2221
+ error: e.message,
2222
+ stack: e.stack,
2223
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
2224
+ source: "extract"
2225
+ });
2226
+ console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
2227
+ }
2228
+ function drainExtractionErrors() {
2229
+ return sink.splice(0, sink.length);
2230
+ }
2231
+ async function writeExtractionErrors(errors, errorsPath) {
2232
+ if (errors.length === 0) return;
2233
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(errorsPath), { recursive: true });
2234
+ const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
2235
+ await import_node_fs7.promises.appendFile(errorsPath, lines, "utf8");
2236
+ }
2237
+ var droppedSink = [];
2238
+ function noteExtractedDropped(edge) {
2239
+ droppedSink.push(edge);
2240
+ }
2241
+ function drainDroppedExtracted() {
2242
+ return droppedSink.splice(0, droppedSink.length);
2243
+ }
2244
+ function isRejectedLogEnabled() {
2245
+ const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
2246
+ return raw === "1" || raw === "true";
2247
+ }
2248
+ async function writeRejectedExtracted(drops, rejectedPath) {
2249
+ if (drops.length === 0) return;
2250
+ await import_node_fs7.promises.mkdir(import_node_path7.default.dirname(rejectedPath), { recursive: true });
2251
+ const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
2252
+ await import_node_fs7.promises.appendFile(rejectedPath, lines, "utf8");
2253
+ }
2254
+
2111
2255
  // src/extract/services.ts
2112
2256
  var DEFAULT_SCAN_DEPTH = 5;
2113
2257
  function parseScanDepth() {
@@ -2124,21 +2268,21 @@ function workspaceGlobs(pkg) {
2124
2268
  return null;
2125
2269
  }
2126
2270
  async function loadGitignore(scanPath) {
2127
- const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2271
+ const gitignorePath = import_node_path8.default.join(scanPath, ".gitignore");
2128
2272
  if (!await exists(gitignorePath)) return null;
2129
- const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2273
+ const raw = await import_node_fs8.promises.readFile(gitignorePath, "utf8");
2130
2274
  return (0, import_ignore.default)().add(raw);
2131
2275
  }
2132
2276
  async function walkDirs(start, scanPath, options, visit) {
2133
2277
  async function recurse(current, depth) {
2134
2278
  if (depth > options.maxDepth) return;
2135
- const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2279
+ const entries = await import_node_fs8.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2136
2280
  for (const entry of entries) {
2137
2281
  if (!entry.isDirectory()) continue;
2138
2282
  if (IGNORED_DIRS.has(entry.name)) continue;
2139
- const child = import_node_path7.default.join(current, entry.name);
2283
+ const child = import_node_path8.default.join(current, entry.name);
2140
2284
  if (options.ig) {
2141
- const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2285
+ const rel = import_node_path8.default.relative(scanPath, child).split(import_node_path8.default.sep).join("/");
2142
2286
  if (rel && options.ig.ignores(rel + "/")) continue;
2143
2287
  }
2144
2288
  await visit(child);
@@ -2153,8 +2297,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2153
2297
  for (const raw of globs) {
2154
2298
  const pattern = raw.replace(/^\.\//, "");
2155
2299
  if (!pattern.includes("*")) {
2156
- const candidate = import_node_path7.default.join(scanPath, pattern);
2157
- if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2300
+ const candidate = import_node_path8.default.join(scanPath, pattern);
2301
+ if (await exists(import_node_path8.default.join(candidate, "package.json"))) found.add(candidate);
2158
2302
  continue;
2159
2303
  }
2160
2304
  const segments = pattern.split("/");
@@ -2163,13 +2307,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2163
2307
  if (seg.includes("*")) break;
2164
2308
  staticSegments.push(seg);
2165
2309
  }
2166
- const start = import_node_path7.default.join(scanPath, ...staticSegments);
2310
+ const start = import_node_path8.default.join(scanPath, ...staticSegments);
2167
2311
  if (!await exists(start)) continue;
2168
2312
  const hasDoubleStar = pattern.includes("**");
2169
2313
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2170
2314
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2171
- const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2172
- if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2315
+ const rel = import_node_path8.default.relative(scanPath, dir).split(import_node_path8.default.sep).join("/");
2316
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path8.default.join(dir, "package.json"))) {
2173
2317
  found.add(dir);
2174
2318
  }
2175
2319
  });
@@ -2177,15 +2321,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2177
2321
  return [...found];
2178
2322
  }
2179
2323
  async function discoverNodeService(scanPath, dir) {
2180
- const pkgPath = import_node_path7.default.join(dir, "package.json");
2324
+ const pkgPath = import_node_path8.default.join(dir, "package.json");
2181
2325
  if (!await exists(pkgPath)) return null;
2182
2326
  let pkg;
2183
2327
  try {
2184
2328
  pkg = await readJson(pkgPath);
2185
2329
  } catch (err) {
2186
- console.warn(
2187
- `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2188
- );
2330
+ recordExtractionError("services", import_node_path8.default.relative(scanPath, pkgPath), err);
2189
2331
  return null;
2190
2332
  }
2191
2333
  if (!pkg.name) return null;
@@ -2196,7 +2338,7 @@ async function discoverNodeService(scanPath, dir) {
2196
2338
  language: "javascript",
2197
2339
  version: pkg.version,
2198
2340
  dependencies: pkg.dependencies ?? {},
2199
- repoPath: import_node_path7.default.relative(scanPath, dir),
2341
+ repoPath: import_node_path8.default.relative(scanPath, dir),
2200
2342
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2201
2343
  };
2202
2344
  return { pkg, dir, node };
@@ -2212,19 +2354,21 @@ async function discoverPyService(scanPath, dir) {
2212
2354
  language: "python",
2213
2355
  version: py.version,
2214
2356
  dependencies: py.dependencies,
2215
- repoPath: import_node_path7.default.relative(scanPath, dir)
2357
+ repoPath: import_node_path8.default.relative(scanPath, dir)
2216
2358
  };
2217
2359
  return { pkg, dir, node };
2218
2360
  }
2219
2361
  async function discoverServices(scanPath) {
2220
- const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2362
+ const rootPkgPath = import_node_path8.default.join(scanPath, "package.json");
2221
2363
  let rootPkg = null;
2222
2364
  if (await exists(rootPkgPath)) {
2223
2365
  try {
2224
2366
  rootPkg = await readJson(rootPkgPath);
2225
2367
  } catch (err) {
2226
- console.warn(
2227
- `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2368
+ recordExtractionError(
2369
+ "services workspaces",
2370
+ import_node_path8.default.relative(scanPath, rootPkgPath),
2371
+ err
2228
2372
  );
2229
2373
  }
2230
2374
  }
@@ -2240,9 +2384,9 @@ async function discoverServices(scanPath) {
2240
2384
  scanPath,
2241
2385
  { maxDepth: parseScanDepth(), ig },
2242
2386
  async (dir) => {
2243
- if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2387
+ if (await exists(import_node_path8.default.join(dir, "package.json"))) {
2244
2388
  candidateDirs.push(dir);
2245
- } else if (await exists(import_node_path7.default.join(dir, "pyproject.toml")) || await exists(import_node_path7.default.join(dir, "requirements.txt")) || await exists(import_node_path7.default.join(dir, "setup.py"))) {
2389
+ } else if (await exists(import_node_path8.default.join(dir, "pyproject.toml")) || await exists(import_node_path8.default.join(dir, "requirements.txt")) || await exists(import_node_path8.default.join(dir, "setup.py"))) {
2246
2390
  candidateDirs.push(dir);
2247
2391
  }
2248
2392
  }
@@ -2256,8 +2400,8 @@ async function discoverServices(scanPath) {
2256
2400
  if (!service) continue;
2257
2401
  const existingDir = seen.get(service.node.name);
2258
2402
  if (existingDir !== void 0) {
2259
- const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2260
- const b = import_node_path7.default.relative(scanPath, dir) || ".";
2403
+ const a = import_node_path8.default.relative(scanPath, existingDir) || ".";
2404
+ const b = import_node_path8.default.relative(scanPath, dir) || ".";
2261
2405
  console.warn(
2262
2406
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2263
2407
  );
@@ -2294,8 +2438,8 @@ function addServiceNodes(graph, services) {
2294
2438
 
2295
2439
  // src/extract/aliases.ts
2296
2440
  init_cjs_shims();
2297
- var import_node_path8 = __toESM(require("path"), 1);
2298
- var import_node_fs8 = require("fs");
2441
+ var import_node_path9 = __toESM(require("path"), 1);
2442
+ var import_node_fs9 = require("fs");
2299
2443
  var import_yaml2 = require("yaml");
2300
2444
  var import_types6 = require("@neat.is/types");
2301
2445
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2322,14 +2466,14 @@ function indexServicesByName(services) {
2322
2466
  const map = /* @__PURE__ */ new Map();
2323
2467
  for (const s of services) {
2324
2468
  map.set(s.node.name, s.node.id);
2325
- map.set(import_node_path8.default.basename(s.dir), s.node.id);
2469
+ map.set(import_node_path9.default.basename(s.dir), s.node.id);
2326
2470
  }
2327
2471
  return map;
2328
2472
  }
2329
2473
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2330
2474
  let composePath = null;
2331
2475
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2332
- const abs = import_node_path8.default.join(scanPath, name);
2476
+ const abs = import_node_path9.default.join(scanPath, name);
2333
2477
  if (await exists(abs)) {
2334
2478
  composePath = abs;
2335
2479
  break;
@@ -2340,8 +2484,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
2340
2484
  try {
2341
2485
  compose = await readYaml(composePath);
2342
2486
  } catch (err) {
2343
- console.warn(
2344
- `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2487
+ recordExtractionError(
2488
+ "aliases compose",
2489
+ import_node_path9.default.relative(scanPath, composePath),
2490
+ err
2345
2491
  );
2346
2492
  return;
2347
2493
  }
@@ -2383,15 +2529,13 @@ function parseDockerfileLabels(content) {
2383
2529
  }
2384
2530
  async function collectDockerfileAliases(graph, services) {
2385
2531
  for (const service of services) {
2386
- const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2532
+ const dockerfilePath = import_node_path9.default.join(service.dir, "Dockerfile");
2387
2533
  if (!await exists(dockerfilePath)) continue;
2388
2534
  let content;
2389
2535
  try {
2390
- content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2536
+ content = await import_node_fs9.promises.readFile(dockerfilePath, "utf8");
2391
2537
  } catch (err) {
2392
- console.warn(
2393
- `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2394
- );
2538
+ recordExtractionError("aliases dockerfile", dockerfilePath, err);
2395
2539
  continue;
2396
2540
  }
2397
2541
  const aliases = parseDockerfileLabels(content);
@@ -2401,13 +2545,13 @@ async function collectDockerfileAliases(graph, services) {
2401
2545
  async function walkYamlFiles(start, depth = 0, max = 5) {
2402
2546
  if (depth > max) return [];
2403
2547
  const out = [];
2404
- const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2548
+ const entries = await import_node_fs9.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2405
2549
  for (const entry of entries) {
2406
2550
  if (entry.isDirectory()) {
2407
2551
  if (IGNORED_DIRS.has(entry.name)) continue;
2408
- out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
2409
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
2410
- out.push(import_node_path8.default.join(start, entry.name));
2552
+ out.push(...await walkYamlFiles(import_node_path9.default.join(start, entry.name), depth + 1, max));
2553
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path9.default.extname(entry.name))) {
2554
+ out.push(import_node_path9.default.join(start, entry.name));
2411
2555
  }
2412
2556
  }
2413
2557
  return out;
@@ -2434,7 +2578,7 @@ function k8sServiceTarget(doc, byName) {
2434
2578
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2435
2579
  const files = await walkYamlFiles(scanPath);
2436
2580
  for (const file of files) {
2437
- const content = await import_node_fs8.promises.readFile(file, "utf8");
2581
+ const content = await import_node_fs9.promises.readFile(file, "utf8");
2438
2582
  let docs;
2439
2583
  try {
2440
2584
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2459,14 +2603,14 @@ async function addServiceAliases(graph, scanPath, services) {
2459
2603
 
2460
2604
  // src/extract/databases/index.ts
2461
2605
  init_cjs_shims();
2462
- var import_node_path16 = __toESM(require("path"), 1);
2606
+ var import_node_path17 = __toESM(require("path"), 1);
2463
2607
  var import_types7 = require("@neat.is/types");
2464
2608
 
2465
2609
  // src/extract/databases/db-config-yaml.ts
2466
2610
  init_cjs_shims();
2467
- var import_node_path9 = __toESM(require("path"), 1);
2611
+ var import_node_path10 = __toESM(require("path"), 1);
2468
2612
  async function parse(serviceDir) {
2469
- const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2613
+ const yamlPath = import_node_path10.default.join(serviceDir, "db-config.yaml");
2470
2614
  if (!await exists(yamlPath)) return [];
2471
2615
  const raw = await readYaml(yamlPath);
2472
2616
  return [
@@ -2484,13 +2628,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2484
2628
 
2485
2629
  // src/extract/databases/dotenv.ts
2486
2630
  init_cjs_shims();
2487
- var import_node_fs10 = require("fs");
2488
- var import_node_path11 = __toESM(require("path"), 1);
2631
+ var import_node_fs11 = require("fs");
2632
+ var import_node_path12 = __toESM(require("path"), 1);
2489
2633
 
2490
2634
  // src/extract/databases/shared.ts
2491
2635
  init_cjs_shims();
2492
- var import_node_fs9 = require("fs");
2493
- var import_node_path10 = __toESM(require("path"), 1);
2636
+ var import_node_fs10 = require("fs");
2637
+ var import_node_path11 = __toESM(require("path"), 1);
2494
2638
  function schemeToEngine(scheme) {
2495
2639
  const s = scheme.toLowerCase().split("+")[0];
2496
2640
  switch (s) {
@@ -2529,14 +2673,14 @@ function parseConnectionString(url) {
2529
2673
  }
2530
2674
  async function readIfExists(filePath) {
2531
2675
  try {
2532
- return await import_node_fs9.promises.readFile(filePath, "utf8");
2676
+ return await import_node_fs10.promises.readFile(filePath, "utf8");
2533
2677
  } catch {
2534
2678
  return null;
2535
2679
  }
2536
2680
  }
2537
2681
  async function findFirst(serviceDir, candidates) {
2538
2682
  for (const rel of candidates) {
2539
- const abs = import_node_path10.default.join(serviceDir, rel);
2683
+ const abs = import_node_path11.default.join(serviceDir, rel);
2540
2684
  const content = await readIfExists(abs);
2541
2685
  if (content !== null) return abs;
2542
2686
  }
@@ -2587,15 +2731,15 @@ function parseDotenvLine(line) {
2587
2731
  return { key, value };
2588
2732
  }
2589
2733
  async function parse2(serviceDir) {
2590
- const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2734
+ const entries = await import_node_fs11.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2591
2735
  const configs = [];
2592
2736
  const seen = /* @__PURE__ */ new Set();
2593
2737
  for (const entry of entries) {
2594
2738
  if (!entry.isFile()) continue;
2595
2739
  const match = isConfigFile(entry.name);
2596
2740
  if (!match.match || match.fileType !== "env") continue;
2597
- const filePath = import_node_path11.default.join(serviceDir, entry.name);
2598
- const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2741
+ const filePath = import_node_path12.default.join(serviceDir, entry.name);
2742
+ const content = await import_node_fs11.promises.readFile(filePath, "utf8");
2599
2743
  for (const line of content.split("\n")) {
2600
2744
  const parsed = parseDotenvLine(line);
2601
2745
  if (!parsed) continue;
@@ -2614,9 +2758,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2614
2758
 
2615
2759
  // src/extract/databases/prisma.ts
2616
2760
  init_cjs_shims();
2617
- var import_node_path12 = __toESM(require("path"), 1);
2761
+ var import_node_path13 = __toESM(require("path"), 1);
2618
2762
  async function parse3(serviceDir) {
2619
- const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2763
+ const schemaPath = import_node_path13.default.join(serviceDir, "prisma", "schema.prisma");
2620
2764
  const content = await readIfExists(schemaPath);
2621
2765
  if (!content) return [];
2622
2766
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2748,10 +2892,10 @@ var knexParser = { name: "knex", parse: parse5 };
2748
2892
 
2749
2893
  // src/extract/databases/ormconfig.ts
2750
2894
  init_cjs_shims();
2751
- var import_node_path13 = __toESM(require("path"), 1);
2895
+ var import_node_path14 = __toESM(require("path"), 1);
2752
2896
  async function parse6(serviceDir) {
2753
2897
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2754
- const abs = import_node_path13.default.join(serviceDir, candidate);
2898
+ const abs = import_node_path14.default.join(serviceDir, candidate);
2755
2899
  if (!await exists(abs)) continue;
2756
2900
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2757
2901
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2811,9 +2955,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2811
2955
 
2812
2956
  // src/extract/databases/sequelize.ts
2813
2957
  init_cjs_shims();
2814
- var import_node_path14 = __toESM(require("path"), 1);
2958
+ var import_node_path15 = __toESM(require("path"), 1);
2815
2959
  async function parse8(serviceDir) {
2816
- const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2960
+ const configPath = import_node_path15.default.join(serviceDir, "config", "config.json");
2817
2961
  if (!await exists(configPath)) return [];
2818
2962
  const raw = await readJson(configPath);
2819
2963
  const out = [];
@@ -2840,7 +2984,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2840
2984
 
2841
2985
  // src/extract/databases/docker-compose.ts
2842
2986
  init_cjs_shims();
2843
- var import_node_path15 = __toESM(require("path"), 1);
2987
+ var import_node_path16 = __toESM(require("path"), 1);
2844
2988
  function portFromService(svc) {
2845
2989
  for (const raw of svc.ports ?? []) {
2846
2990
  const str = String(raw);
@@ -2867,7 +3011,7 @@ function databaseFromEnv(svc) {
2867
3011
  }
2868
3012
  async function parse9(serviceDir) {
2869
3013
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2870
- const abs = import_node_path15.default.join(serviceDir, name);
3014
+ const abs = import_node_path16.default.join(serviceDir, name);
2871
3015
  if (!await exists(abs)) continue;
2872
3016
  const raw = await readYaml(abs);
2873
3017
  if (!raw?.services) return [];
@@ -3045,11 +3189,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3045
3189
  target: dbNode.id,
3046
3190
  type: import_types7.EdgeType.CONNECTS_TO,
3047
3191
  provenance: import_types7.Provenance.EXTRACTED,
3048
- ...config.sourceFile ? {
3049
- evidence: {
3050
- file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
3051
- }
3052
- } : {}
3192
+ confidence: (0, import_types7.confidenceForExtracted)("structural"),
3193
+ // ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
3194
+ // Ghost-edge cleanup keys retirement on this; the conditional
3195
+ // sourceFile spread that used to live here was a v0.1.x leftover.
3196
+ evidence: {
3197
+ file: import_node_path17.default.relative(scanPath, config.sourceFile).split(import_node_path17.default.sep).join("/")
3198
+ }
3053
3199
  };
3054
3200
  if (!graph.hasEdge(edge.id)) {
3055
3201
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3075,15 +3221,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
3075
3221
 
3076
3222
  // src/extract/configs.ts
3077
3223
  init_cjs_shims();
3078
- var import_node_fs11 = require("fs");
3079
- var import_node_path17 = __toESM(require("path"), 1);
3224
+ var import_node_fs12 = require("fs");
3225
+ var import_node_path18 = __toESM(require("path"), 1);
3080
3226
  var import_types8 = require("@neat.is/types");
3081
3227
  async function walkConfigFiles(dir) {
3082
3228
  const out = [];
3083
3229
  async function walk(current) {
3084
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
3230
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true });
3085
3231
  for (const entry of entries) {
3086
- const full = import_node_path17.default.join(current, entry.name);
3232
+ const full = import_node_path18.default.join(current, entry.name);
3087
3233
  if (entry.isDirectory()) {
3088
3234
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3089
3235
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -3100,13 +3246,13 @@ async function addConfigNodes(graph, services, scanPath) {
3100
3246
  for (const service of services) {
3101
3247
  const configFiles = await walkConfigFiles(service.dir);
3102
3248
  for (const file of configFiles) {
3103
- const relPath = import_node_path17.default.relative(scanPath, file);
3249
+ const relPath = import_node_path18.default.relative(scanPath, file);
3104
3250
  const node = {
3105
3251
  id: (0, import_types8.configId)(relPath),
3106
3252
  type: import_types8.NodeType.ConfigNode,
3107
- name: import_node_path17.default.basename(file),
3253
+ name: import_node_path18.default.basename(file),
3108
3254
  path: relPath,
3109
- fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
3255
+ fileType: isConfigFile(import_node_path18.default.basename(file)).fileType
3110
3256
  };
3111
3257
  if (!graph.hasNode(node.id)) {
3112
3258
  graph.addNode(node.id, node);
@@ -3118,7 +3264,8 @@ async function addConfigNodes(graph, services, scanPath) {
3118
3264
  target: node.id,
3119
3265
  type: import_types8.EdgeType.CONFIGURED_BY,
3120
3266
  provenance: import_types8.Provenance.EXTRACTED,
3121
- evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
3267
+ confidence: (0, import_types8.confidenceForExtracted)("structural"),
3268
+ evidence: { file: relPath.split(import_node_path18.default.sep).join("/") }
3122
3269
  };
3123
3270
  if (!graph.hasEdge(edge.id)) {
3124
3271
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3135,7 +3282,7 @@ var import_types14 = require("@neat.is/types");
3135
3282
 
3136
3283
  // src/extract/calls/http.ts
3137
3284
  init_cjs_shims();
3138
- var import_node_path19 = __toESM(require("path"), 1);
3285
+ var import_node_path20 = __toESM(require("path"), 1);
3139
3286
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
3140
3287
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
3141
3288
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -3143,17 +3290,17 @@ var import_types9 = require("@neat.is/types");
3143
3290
 
3144
3291
  // src/extract/calls/shared.ts
3145
3292
  init_cjs_shims();
3146
- var import_node_fs12 = require("fs");
3147
- var import_node_path18 = __toESM(require("path"), 1);
3293
+ var import_node_fs13 = require("fs");
3294
+ var import_node_path19 = __toESM(require("path"), 1);
3148
3295
  async function walkSourceFiles(dir) {
3149
3296
  const out = [];
3150
3297
  async function walk(current) {
3151
- const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3298
+ const entries = await import_node_fs13.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3152
3299
  for (const entry of entries) {
3153
- const full = import_node_path18.default.join(current, entry.name);
3300
+ const full = import_node_path19.default.join(current, entry.name);
3154
3301
  if (entry.isDirectory()) {
3155
3302
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
3156
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
3303
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path19.default.extname(entry.name))) {
3157
3304
  out.push(full);
3158
3305
  }
3159
3306
  }
@@ -3166,7 +3313,7 @@ async function loadSourceFiles(dir) {
3166
3313
  const out = [];
3167
3314
  for (const p of paths) {
3168
3315
  try {
3169
- const content = await import_node_fs12.promises.readFile(p, "utf8");
3316
+ const content = await import_node_fs13.promises.readFile(p, "utf8");
3170
3317
  out.push({ path: p, content });
3171
3318
  } catch {
3172
3319
  }
@@ -3185,8 +3332,27 @@ function snippet(text, line) {
3185
3332
 
3186
3333
  // src/extract/calls/http.ts
3187
3334
  var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
3335
+ var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
3336
+ function isInsideJsxExternalLink(node) {
3337
+ let cursor = node.parent;
3338
+ while (cursor) {
3339
+ if (cursor.type === "jsx_attribute") {
3340
+ let owner = cursor.parent;
3341
+ while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
3342
+ owner = owner.parent;
3343
+ }
3344
+ if (!owner) return false;
3345
+ const tagNode = owner.namedChild(0);
3346
+ const tagName = tagNode?.text ?? "";
3347
+ const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
3348
+ return JSX_EXTERNAL_LINK_TAGS.has(right);
3349
+ }
3350
+ cursor = cursor.parent;
3351
+ }
3352
+ return false;
3353
+ }
3188
3354
  function collectStringLiterals(node, out) {
3189
- if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
3355
+ if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
3190
3356
  for (let i = 0; i < node.namedChildCount; i++) {
3191
3357
  const child = node.namedChild(i);
3192
3358
  if (child) collectStringLiterals(child, out);
@@ -3198,8 +3364,9 @@ function callsFromSource(source, parser, knownHosts) {
3198
3364
  collectStringLiterals(tree.rootNode, literals);
3199
3365
  const targets = /* @__PURE__ */ new Set();
3200
3366
  for (const lit of literals) {
3367
+ if (isInsideJsxExternalLink(lit.node)) continue;
3201
3368
  for (const host of knownHosts) {
3202
- if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
3369
+ if (urlMatchesHost(lit.text, host)) {
3203
3370
  targets.add(host);
3204
3371
  }
3205
3372
  }
@@ -3222,9 +3389,9 @@ async function addHttpCallEdges(graph, services) {
3222
3389
  const knownHosts = /* @__PURE__ */ new Set();
3223
3390
  const hostToNodeId = /* @__PURE__ */ new Map();
3224
3391
  for (const service of services) {
3225
- knownHosts.add(import_node_path19.default.basename(service.dir));
3392
+ knownHosts.add(import_node_path20.default.basename(service.dir));
3226
3393
  knownHosts.add(service.pkg.name);
3227
- hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3394
+ hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
3228
3395
  hostToNodeId.set(service.pkg.name, service.node.id);
3229
3396
  }
3230
3397
  let edgesAdded = 0;
@@ -3232,14 +3399,13 @@ async function addHttpCallEdges(graph, services) {
3232
3399
  const files = await loadSourceFiles(service.dir);
3233
3400
  const seenTargets = /* @__PURE__ */ new Map();
3234
3401
  for (const file of files) {
3235
- const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3402
+ if (isTestPath(file.path)) continue;
3403
+ const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
3236
3404
  let targets;
3237
3405
  try {
3238
3406
  targets = callsFromSource(file.content, parser, knownHosts);
3239
3407
  } catch (err) {
3240
- console.warn(
3241
- `[neat] http call extraction skipped ${file.path}: ${err.message}`
3242
- );
3408
+ recordExtractionError("http call extraction", file.path, err);
3243
3409
  continue;
3244
3410
  }
3245
3411
  for (const t of targets) {
@@ -3253,17 +3419,32 @@ async function addHttpCallEdges(graph, services) {
3253
3419
  for (const [targetId, evidenceFile] of seenTargets) {
3254
3420
  const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
3255
3421
  const line = lineOf(fileContent, `//${evidenceFile.host}`);
3422
+ const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
3423
+ const ev = {
3424
+ file: import_node_path20.default.relative(service.dir, evidenceFile.file),
3425
+ line,
3426
+ snippet: snippet(fileContent, line)
3427
+ };
3428
+ const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
3429
+ if (!(0, import_types9.passesExtractedFloor)(confidence)) {
3430
+ noteExtractedDropped({
3431
+ source: service.node.id,
3432
+ target: targetId,
3433
+ type: import_types9.EdgeType.CALLS,
3434
+ confidence,
3435
+ confidenceKind: "hostname-shape-match",
3436
+ evidence: ev
3437
+ });
3438
+ continue;
3439
+ }
3256
3440
  const edge = {
3257
- id: (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS),
3441
+ id: edgeId,
3258
3442
  source: service.node.id,
3259
3443
  target: targetId,
3260
3444
  type: import_types9.EdgeType.CALLS,
3261
3445
  provenance: import_types9.Provenance.EXTRACTED,
3262
- evidence: {
3263
- file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3264
- line,
3265
- snippet: snippet(fileContent, line)
3266
- }
3446
+ confidence,
3447
+ evidence: ev
3267
3448
  };
3268
3449
  if (!graph.hasEdge(edge.id)) {
3269
3450
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -3276,7 +3457,7 @@ async function addHttpCallEdges(graph, services) {
3276
3457
 
3277
3458
  // src/extract/calls/kafka.ts
3278
3459
  init_cjs_shims();
3279
- var import_node_path20 = __toESM(require("path"), 1);
3460
+ var import_node_path21 = __toESM(require("path"), 1);
3280
3461
  var import_types10 = require("@neat.is/types");
3281
3462
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3282
3463
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3302,8 +3483,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3302
3483
  name: topic,
3303
3484
  kind: "kafka-topic",
3304
3485
  edgeType,
3486
+ // `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
3487
+ // framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
3488
+ // tier (ADR-066).
3489
+ confidenceKind: "verified-call-site",
3305
3490
  evidence: {
3306
- file: import_node_path20.default.relative(serviceDir, file.path),
3491
+ file: import_node_path21.default.relative(serviceDir, file.path),
3307
3492
  line,
3308
3493
  snippet: snippet(file.content, line)
3309
3494
  }
@@ -3316,7 +3501,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3316
3501
 
3317
3502
  // src/extract/calls/redis.ts
3318
3503
  init_cjs_shims();
3319
- var import_node_path21 = __toESM(require("path"), 1);
3504
+ var import_node_path22 = __toESM(require("path"), 1);
3320
3505
  var import_types11 = require("@neat.is/types");
3321
3506
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3322
3507
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3334,8 +3519,12 @@ function redisEndpointsFromFile(file, serviceDir) {
3334
3519
  name: host,
3335
3520
  kind: "redis",
3336
3521
  edgeType: "CALLS",
3522
+ // `redis://host` URL literal — the scheme is structural support, but no
3523
+ // call expression is verified to wire it through. URL-with-structural-
3524
+ // support tier (ADR-066).
3525
+ confidenceKind: "url-with-structural-support",
3337
3526
  evidence: {
3338
- file: import_node_path21.default.relative(serviceDir, file.path),
3527
+ file: import_node_path22.default.relative(serviceDir, file.path),
3339
3528
  line,
3340
3529
  snippet: snippet(file.content, line)
3341
3530
  }
@@ -3346,7 +3535,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3346
3535
 
3347
3536
  // src/extract/calls/aws.ts
3348
3537
  init_cjs_shims();
3349
- var import_node_path22 = __toESM(require("path"), 1);
3538
+ var import_node_path23 = __toESM(require("path"), 1);
3350
3539
  var import_types12 = require("@neat.is/types");
3351
3540
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3352
3541
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3375,8 +3564,12 @@ function awsEndpointsFromFile(file, serviceDir) {
3375
3564
  name,
3376
3565
  kind,
3377
3566
  edgeType: "CALLS",
3567
+ // SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
3568
+ // literal — framework-aware recognizer, verified-call-site tier
3569
+ // (ADR-066).
3570
+ confidenceKind: "verified-call-site",
3378
3571
  evidence: {
3379
- file: import_node_path22.default.relative(serviceDir, file.path),
3572
+ file: import_node_path23.default.relative(serviceDir, file.path),
3380
3573
  line,
3381
3574
  snippet: snippet(file.content, line)
3382
3575
  }
@@ -3401,16 +3594,42 @@ function awsEndpointsFromFile(file, serviceDir) {
3401
3594
 
3402
3595
  // src/extract/calls/grpc.ts
3403
3596
  init_cjs_shims();
3404
- var import_node_path23 = __toESM(require("path"), 1);
3597
+ var import_node_path24 = __toESM(require("path"), 1);
3405
3598
  var import_types13 = require("@neat.is/types");
3406
3599
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3600
+ var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
3601
+ var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
3407
3602
  function isLikelyAddress(value) {
3408
3603
  if (!value) return false;
3409
3604
  return /:\d{2,5}$/.test(value) || value.includes(".");
3410
3605
  }
3606
+ function normaliseForMatch(s) {
3607
+ return s.toLowerCase().replace(/[^a-z0-9]/g, "");
3608
+ }
3609
+ function readImports(content) {
3610
+ const awsSdkSuffixes = /* @__PURE__ */ new Map();
3611
+ AWS_SDK_IMPORT_RE.lastIndex = 0;
3612
+ let m;
3613
+ while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
3614
+ const raw = m[1];
3615
+ awsSdkSuffixes.set(normaliseForMatch(raw), raw);
3616
+ }
3617
+ return {
3618
+ awsSdkSuffixes,
3619
+ hasGrpcImport: GRPC_IMPORT_RE.test(content)
3620
+ };
3621
+ }
3622
+ function classifyClient(symbol, ctx) {
3623
+ const key = normaliseForMatch(symbol);
3624
+ const awsRaw = ctx.awsSdkSuffixes.get(key);
3625
+ if (awsRaw) return { kind: `aws-${awsRaw}` };
3626
+ if (ctx.hasGrpcImport) return { kind: "grpc-service" };
3627
+ return null;
3628
+ }
3411
3629
  function grpcEndpointsFromFile(file, serviceDir) {
3412
3630
  const out = [];
3413
3631
  const seen = /* @__PURE__ */ new Set();
3632
+ const ctx = readImports(file.content);
3414
3633
  GRPC_CLIENT_RE.lastIndex = 0;
3415
3634
  let m;
3416
3635
  while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
@@ -3418,15 +3637,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
3418
3637
  const addr = m[2]?.trim();
3419
3638
  const name = isLikelyAddress(addr) ? addr : symbol;
3420
3639
  if (seen.has(name)) continue;
3640
+ const classified = classifyClient(symbol, ctx);
3641
+ if (!classified) continue;
3421
3642
  seen.add(name);
3643
+ const { kind } = classified;
3422
3644
  const line = lineOf(file.content, m[0]);
3423
3645
  out.push({
3424
- infraId: (0, import_types13.infraId)("grpc-service", name),
3646
+ infraId: (0, import_types13.infraId)(kind, name),
3425
3647
  name,
3426
- kind: "grpc-service",
3648
+ kind,
3427
3649
  edgeType: "CALLS",
3650
+ // `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
3651
+ // context — import-aware classification per #238. Verified-call-site
3652
+ // tier (ADR-066).
3653
+ confidenceKind: "verified-call-site",
3428
3654
  evidence: {
3429
- file: import_node_path23.default.relative(serviceDir, file.path),
3655
+ file: import_node_path24.default.relative(serviceDir, file.path),
3430
3656
  line,
3431
3657
  snippet: snippet(file.content, line)
3432
3658
  }
@@ -3446,6 +3672,9 @@ function edgeTypeFromEndpoint(ep) {
3446
3672
  return import_types14.EdgeType.CALLS;
3447
3673
  }
3448
3674
  }
3675
+ function isAwsKind(kind) {
3676
+ return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
3677
+ }
3449
3678
  async function addExternalEndpointEdges(graph, services) {
3450
3679
  let nodesAdded = 0;
3451
3680
  let edgesAdded = 0;
@@ -3453,10 +3682,13 @@ async function addExternalEndpointEdges(graph, services) {
3453
3682
  const files = await loadSourceFiles(service.dir);
3454
3683
  const endpoints = [];
3455
3684
  for (const file of files) {
3456
- endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
3457
- endpoints.push(...redisEndpointsFromFile(file, service.dir));
3458
- endpoints.push(...awsEndpointsFromFile(file, service.dir));
3459
- endpoints.push(...grpcEndpointsFromFile(file, service.dir));
3685
+ if (isTestPath(file.path)) continue;
3686
+ const masked = maskCommentsInSource(file.content);
3687
+ const maskedFile = { path: file.path, content: masked };
3688
+ endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
3689
+ endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
3690
+ endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
3691
+ endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
3460
3692
  }
3461
3693
  if (endpoints.length === 0) continue;
3462
3694
  const seenEdges = /* @__PURE__ */ new Set();
@@ -3466,7 +3698,10 @@ async function addExternalEndpointEdges(graph, services) {
3466
3698
  id: ep.infraId,
3467
3699
  type: import_types14.NodeType.InfraNode,
3468
3700
  name: ep.name,
3469
- provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
3701
+ // #238 `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
3702
+ // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
3703
+ // bucket / table kinds from aws.ts.
3704
+ provider: isAwsKind(ep.kind) ? "aws" : "self",
3470
3705
  kind: ep.kind
3471
3706
  };
3472
3707
  graph.addNode(node.id, node);
@@ -3476,6 +3711,18 @@ async function addExternalEndpointEdges(graph, services) {
3476
3711
  const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
3477
3712
  if (seenEdges.has(edgeId)) continue;
3478
3713
  seenEdges.add(edgeId);
3714
+ const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
3715
+ if (!(0, import_types14.passesExtractedFloor)(confidence)) {
3716
+ noteExtractedDropped({
3717
+ source: service.node.id,
3718
+ target: ep.infraId,
3719
+ type: edgeType,
3720
+ confidence,
3721
+ confidenceKind: ep.confidenceKind,
3722
+ evidence: ep.evidence
3723
+ });
3724
+ continue;
3725
+ }
3479
3726
  if (!graph.hasEdge(edgeId)) {
3480
3727
  const edge = {
3481
3728
  id: edgeId,
@@ -3483,6 +3730,7 @@ async function addExternalEndpointEdges(graph, services) {
3483
3730
  target: ep.infraId,
3484
3731
  type: edgeType,
3485
3732
  provenance: import_types14.Provenance.EXTRACTED,
3733
+ confidence,
3486
3734
  evidence: ep.evidence
3487
3735
  };
3488
3736
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3506,7 +3754,7 @@ init_cjs_shims();
3506
3754
 
3507
3755
  // src/extract/infra/docker-compose.ts
3508
3756
  init_cjs_shims();
3509
- var import_node_path24 = __toESM(require("path"), 1);
3757
+ var import_node_path25 = __toESM(require("path"), 1);
3510
3758
  var import_types16 = require("@neat.is/types");
3511
3759
 
3512
3760
  // src/extract/infra/shared.ts
@@ -3544,7 +3792,7 @@ function dependsOnList(value) {
3544
3792
  }
3545
3793
  function serviceNameToServiceNode(name, services) {
3546
3794
  for (const s of services) {
3547
- if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3795
+ if (s.node.name === name || import_node_path25.default.basename(s.dir) === name) return s.node.id;
3548
3796
  }
3549
3797
  return null;
3550
3798
  }
@@ -3553,7 +3801,7 @@ async function addComposeInfra(graph, scanPath, services) {
3553
3801
  let edgesAdded = 0;
3554
3802
  let composePath = null;
3555
3803
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3556
- const abs = import_node_path24.default.join(scanPath, name);
3804
+ const abs = import_node_path25.default.join(scanPath, name);
3557
3805
  if (await exists(abs)) {
3558
3806
  composePath = abs;
3559
3807
  break;
@@ -3564,13 +3812,15 @@ async function addComposeInfra(graph, scanPath, services) {
3564
3812
  try {
3565
3813
  compose = await readYaml(composePath);
3566
3814
  } catch (err) {
3567
- console.warn(
3568
- `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3815
+ recordExtractionError(
3816
+ "infra docker-compose",
3817
+ import_node_path25.default.relative(scanPath, composePath),
3818
+ err
3569
3819
  );
3570
3820
  return { nodesAdded, edgesAdded };
3571
3821
  }
3572
3822
  if (!compose?.services) return { nodesAdded, edgesAdded };
3573
- const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3823
+ const evidenceFile = import_node_path25.default.relative(scanPath, composePath).split(import_node_path25.default.sep).join("/");
3574
3824
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3575
3825
  for (const [composeName, svc] of Object.entries(compose.services)) {
3576
3826
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3600,6 +3850,7 @@ async function addComposeInfra(graph, scanPath, services) {
3600
3850
  target: targetId,
3601
3851
  type: import_types16.EdgeType.DEPENDS_ON,
3602
3852
  provenance: import_types16.Provenance.EXTRACTED,
3853
+ confidence: (0, import_types16.confidenceForExtracted)("structural"),
3603
3854
  evidence: { file: evidenceFile }
3604
3855
  };
3605
3856
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3611,8 +3862,8 @@ async function addComposeInfra(graph, scanPath, services) {
3611
3862
 
3612
3863
  // src/extract/infra/dockerfile.ts
3613
3864
  init_cjs_shims();
3614
- var import_node_path25 = __toESM(require("path"), 1);
3615
- var import_node_fs13 = require("fs");
3865
+ var import_node_path26 = __toESM(require("path"), 1);
3866
+ var import_node_fs14 = require("fs");
3616
3867
  var import_types17 = require("@neat.is/types");
3617
3868
  function runtimeImage(content) {
3618
3869
  const lines = content.split("\n");
@@ -3632,14 +3883,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3632
3883
  let nodesAdded = 0;
3633
3884
  let edgesAdded = 0;
3634
3885
  for (const service of services) {
3635
- const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3886
+ const dockerfilePath = import_node_path26.default.join(service.dir, "Dockerfile");
3636
3887
  if (!await exists(dockerfilePath)) continue;
3637
3888
  let content;
3638
3889
  try {
3639
- content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3890
+ content = await import_node_fs14.promises.readFile(dockerfilePath, "utf8");
3640
3891
  } catch (err) {
3641
- console.warn(
3642
- `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3892
+ recordExtractionError(
3893
+ "infra dockerfile",
3894
+ import_node_path26.default.relative(scanPath, dockerfilePath),
3895
+ err
3643
3896
  );
3644
3897
  continue;
3645
3898
  }
@@ -3658,8 +3911,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3658
3911
  target: node.id,
3659
3912
  type: import_types17.EdgeType.RUNS_ON,
3660
3913
  provenance: import_types17.Provenance.EXTRACTED,
3914
+ confidence: (0, import_types17.confidenceForExtracted)("structural"),
3661
3915
  evidence: {
3662
- file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3916
+ file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
3663
3917
  }
3664
3918
  };
3665
3919
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3671,19 +3925,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3671
3925
 
3672
3926
  // src/extract/infra/terraform.ts
3673
3927
  init_cjs_shims();
3674
- var import_node_fs14 = require("fs");
3675
- var import_node_path26 = __toESM(require("path"), 1);
3928
+ var import_node_fs15 = require("fs");
3929
+ var import_node_path27 = __toESM(require("path"), 1);
3676
3930
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3677
3931
  async function walkTfFiles(start, depth = 0, max = 5) {
3678
3932
  if (depth > max) return [];
3679
3933
  const out = [];
3680
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3934
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3681
3935
  for (const entry of entries) {
3682
3936
  if (entry.isDirectory()) {
3683
3937
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3684
- out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
3938
+ out.push(...await walkTfFiles(import_node_path27.default.join(start, entry.name), depth + 1, max));
3685
3939
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3686
- out.push(import_node_path26.default.join(start, entry.name));
3940
+ out.push(import_node_path27.default.join(start, entry.name));
3687
3941
  }
3688
3942
  }
3689
3943
  return out;
@@ -3692,7 +3946,7 @@ async function addTerraformResources(graph, scanPath) {
3692
3946
  let nodesAdded = 0;
3693
3947
  const files = await walkTfFiles(scanPath);
3694
3948
  for (const file of files) {
3695
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3949
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3696
3950
  RESOURCE_RE.lastIndex = 0;
3697
3951
  let m;
3698
3952
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3710,8 +3964,8 @@ async function addTerraformResources(graph, scanPath) {
3710
3964
 
3711
3965
  // src/extract/infra/k8s.ts
3712
3966
  init_cjs_shims();
3713
- var import_node_fs15 = require("fs");
3714
- var import_node_path27 = __toESM(require("path"), 1);
3967
+ var import_node_fs16 = require("fs");
3968
+ var import_node_path28 = __toESM(require("path"), 1);
3715
3969
  var import_yaml3 = require("yaml");
3716
3970
  var K8S_KIND_TO_INFRA_KIND = {
3717
3971
  Service: "k8s-service",
@@ -3725,13 +3979,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3725
3979
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3726
3980
  if (depth > max) return [];
3727
3981
  const out = [];
3728
- const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3982
+ const entries = await import_node_fs16.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3729
3983
  for (const entry of entries) {
3730
3984
  if (entry.isDirectory()) {
3731
3985
  if (IGNORED_DIRS.has(entry.name)) continue;
3732
- out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
3733
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
3734
- out.push(import_node_path27.default.join(start, entry.name));
3986
+ out.push(...await walkYamlFiles2(import_node_path28.default.join(start, entry.name), depth + 1, max));
3987
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path28.default.extname(entry.name))) {
3988
+ out.push(import_node_path28.default.join(start, entry.name));
3735
3989
  }
3736
3990
  }
3737
3991
  return out;
@@ -3740,7 +3994,7 @@ async function addK8sResources(graph, scanPath) {
3740
3994
  let nodesAdded = 0;
3741
3995
  const files = await walkYamlFiles2(scanPath);
3742
3996
  for (const file of files) {
3743
- const content = await import_node_fs15.promises.readFile(file, "utf8");
3997
+ const content = await import_node_fs16.promises.readFile(file, "utf8");
3744
3998
  let docs;
3745
3999
  try {
3746
4000
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3774,9 +4028,37 @@ async function addInfra(graph, scanPath, services) {
3774
4028
  };
3775
4029
  }
3776
4030
 
4031
+ // src/extract/index.ts
4032
+ var import_node_path30 = __toESM(require("path"), 1);
4033
+
4034
+ // src/extract/retire.ts
4035
+ init_cjs_shims();
4036
+ var import_node_fs17 = require("fs");
4037
+ var import_node_path29 = __toESM(require("path"), 1);
4038
+ var import_types18 = require("@neat.is/types");
4039
+ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4040
+ const toDrop = [];
4041
+ const bases = [scanPath, ...serviceDirs];
4042
+ graph.forEachEdge((id, attrs) => {
4043
+ const edge = attrs;
4044
+ if (edge.provenance !== import_types18.Provenance.EXTRACTED) return;
4045
+ const evidenceFile = edge.evidence?.file;
4046
+ if (!evidenceFile) return;
4047
+ if (import_node_path29.default.isAbsolute(evidenceFile)) {
4048
+ if (!(0, import_node_fs17.existsSync)(evidenceFile)) toDrop.push(id);
4049
+ return;
4050
+ }
4051
+ const found = bases.some((base) => (0, import_node_fs17.existsSync)(import_node_path29.default.join(base, evidenceFile)));
4052
+ if (!found) toDrop.push(id);
4053
+ });
4054
+ for (const id of toDrop) graph.dropEdge(id);
4055
+ return toDrop.length;
4056
+ }
4057
+
3777
4058
  // src/extract/index.ts
3778
4059
  async function extractFromDirectory(graph, scanPath, opts = {}) {
3779
4060
  await ensureCompatLoaded();
4061
+ drainExtractionErrors();
3780
4062
  const services = await discoverServices(scanPath);
3781
4063
  const phase1Nodes = addServiceNodes(graph, services);
3782
4064
  await addServiceAliases(graph, scanPath, services);
@@ -3784,12 +4066,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3784
4066
  const phase3 = await addConfigNodes(graph, services, scanPath);
3785
4067
  const phase4 = await addCallEdges(graph, services);
3786
4068
  const phase5 = await addInfra(graph, scanPath, services);
4069
+ const ghostsRetired = retireExtractedEdgesByMissingFile(
4070
+ graph,
4071
+ scanPath,
4072
+ services.map((s) => s.dir)
4073
+ );
3787
4074
  const frontiersPromoted = promoteFrontierNodes(graph);
3788
4075
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
4076
+ const errorEntries = drainExtractionErrors();
4077
+ if (opts.errorsPath && errorEntries.length > 0) {
4078
+ try {
4079
+ await writeExtractionErrors(errorEntries, opts.errorsPath);
4080
+ } catch (err) {
4081
+ console.warn(
4082
+ `[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
4083
+ );
4084
+ }
4085
+ }
4086
+ const droppedEntries = drainDroppedExtracted();
4087
+ if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4088
+ const rejectedPath = import_node_path30.default.join(import_node_path30.default.dirname(opts.errorsPath), "rejected.ndjson");
4089
+ try {
4090
+ await writeRejectedExtracted(droppedEntries, rejectedPath);
4091
+ } catch (err) {
4092
+ console.warn(
4093
+ `[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
4094
+ );
4095
+ }
4096
+ }
3789
4097
  const result = {
3790
4098
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3791
4099
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3792
- frontiersPromoted
4100
+ frontiersPromoted,
4101
+ extractionErrors: errorEntries.length,
4102
+ errorEntries,
4103
+ ghostsRetired,
4104
+ extractedDropped: droppedEntries.length,
4105
+ droppedEntries
3793
4106
  };
3794
4107
  emitNeatEvent({
3795
4108
  type: "extraction-complete",
@@ -3806,8 +4119,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3806
4119
 
3807
4120
  // src/persist.ts
3808
4121
  init_cjs_shims();
3809
- var import_node_fs16 = require("fs");
3810
- var import_node_path28 = __toESM(require("path"), 1);
4122
+ var import_node_fs18 = require("fs");
4123
+ var import_node_path31 = __toESM(require("path"), 1);
3811
4124
  var SCHEMA_VERSION = 2;
3812
4125
  function migrateV1ToV2(payload) {
3813
4126
  const nodes = payload.graph.nodes;
@@ -3821,7 +4134,7 @@ function migrateV1ToV2(payload) {
3821
4134
  return { ...payload, schemaVersion: 2 };
3822
4135
  }
3823
4136
  async function ensureDir(filePath) {
3824
- await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
4137
+ await import_node_fs18.promises.mkdir(import_node_path31.default.dirname(filePath), { recursive: true });
3825
4138
  }
3826
4139
  async function saveGraphToDisk(graph, outPath) {
3827
4140
  await ensureDir(outPath);
@@ -3831,13 +4144,13 @@ async function saveGraphToDisk(graph, outPath) {
3831
4144
  graph: graph.export()
3832
4145
  };
3833
4146
  const tmp = `${outPath}.tmp`;
3834
- await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3835
- await import_node_fs16.promises.rename(tmp, outPath);
4147
+ await import_node_fs18.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
4148
+ await import_node_fs18.promises.rename(tmp, outPath);
3836
4149
  }
3837
4150
  async function loadGraphFromDisk(graph, outPath) {
3838
4151
  let raw;
3839
4152
  try {
3840
- raw = await import_node_fs16.promises.readFile(outPath, "utf8");
4153
+ raw = await import_node_fs18.promises.readFile(outPath, "utf8");
3841
4154
  } catch (err) {
3842
4155
  if (err.code === "ENOENT") return;
3843
4156
  throw err;
@@ -3892,11 +4205,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3892
4205
  init_cjs_shims();
3893
4206
  var import_fastify = __toESM(require("fastify"), 1);
3894
4207
  var import_cors = __toESM(require("@fastify/cors"), 1);
3895
- var import_types20 = require("@neat.is/types");
4208
+ var import_types21 = require("@neat.is/types");
3896
4209
 
3897
4210
  // src/divergences.ts
3898
4211
  init_cjs_shims();
3899
- var import_types18 = require("@neat.is/types");
4212
+ var import_types19 = require("@neat.is/types");
3900
4213
  function bucketKey(source, target, type) {
3901
4214
  return `${type}|${source}|${target}`;
3902
4215
  }
@@ -3904,25 +4217,25 @@ function bucketEdges(graph) {
3904
4217
  const buckets = /* @__PURE__ */ new Map();
3905
4218
  graph.forEachEdge((id, attrs) => {
3906
4219
  const e = attrs;
3907
- const parsed = (0, import_types18.parseEdgeId)(id);
4220
+ const parsed = (0, import_types19.parseEdgeId)(id);
3908
4221
  const provenance = parsed?.provenance ?? e.provenance;
3909
4222
  const key = bucketKey(e.source, e.target, e.type);
3910
4223
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3911
4224
  switch (provenance) {
3912
- case import_types18.Provenance.EXTRACTED:
4225
+ case import_types19.Provenance.EXTRACTED:
3913
4226
  cur.extracted = e;
3914
4227
  break;
3915
- case import_types18.Provenance.OBSERVED:
4228
+ case import_types19.Provenance.OBSERVED:
3916
4229
  cur.observed = e;
3917
4230
  break;
3918
- case import_types18.Provenance.INFERRED:
4231
+ case import_types19.Provenance.INFERRED:
3919
4232
  cur.inferred = e;
3920
4233
  break;
3921
- case import_types18.Provenance.FRONTIER:
4234
+ case import_types19.Provenance.FRONTIER:
3922
4235
  cur.frontier = e;
3923
4236
  break;
3924
4237
  default:
3925
- if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
4238
+ if (e.provenance === import_types19.Provenance.STALE) cur.stale = e;
3926
4239
  }
3927
4240
  buckets.set(key, cur);
3928
4241
  });
@@ -3931,15 +4244,7 @@ function bucketEdges(graph) {
3931
4244
  function nodeIsFrontier(graph, nodeId) {
3932
4245
  if (!graph.hasNode(nodeId)) return false;
3933
4246
  const attrs = graph.getNodeAttributes(nodeId);
3934
- return attrs.type === import_types18.NodeType.FrontierNode;
3935
- }
3936
- function hasAnyObservedFromSource(graph, sourceId) {
3937
- if (!graph.hasNode(sourceId)) return false;
3938
- for (const edgeId of graph.outboundEdges(sourceId)) {
3939
- const e = graph.getEdgeAttributes(edgeId);
3940
- if (e.provenance === import_types18.Provenance.OBSERVED) return true;
3941
- }
3942
- return false;
4247
+ return attrs.type === import_types19.NodeType.FrontierNode;
3943
4248
  }
3944
4249
  function clampConfidence(n) {
3945
4250
  if (!Number.isFinite(n)) return 0;
@@ -3954,32 +4259,34 @@ function reasonForMissingExtracted(source, target, type) {
3954
4259
  var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
3955
4260
  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.";
3956
4261
  var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
4262
+ function gradedConfidence(edge) {
4263
+ if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
4264
+ return clampConfidence(confidenceForEdge(edge));
4265
+ }
3957
4266
  function detectMissingDivergences(graph, bucket) {
3958
4267
  const out = [];
3959
4268
  if (bucket.extracted && !bucket.observed) {
3960
4269
  if (!nodeIsFrontier(graph, bucket.target)) {
3961
- const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
3962
4270
  out.push({
3963
4271
  type: "missing-observed",
3964
4272
  source: bucket.source,
3965
4273
  target: bucket.target,
3966
4274
  edgeType: bucket.type,
3967
4275
  extracted: bucket.extracted,
3968
- confidence: sourceHasTraffic ? 1 : 0.5,
4276
+ confidence: gradedConfidence(bucket.extracted),
3969
4277
  reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
3970
4278
  recommendation: RECOMMENDATION_MISSING_OBSERVED
3971
4279
  });
3972
4280
  }
3973
4281
  }
3974
4282
  if (bucket.observed && !bucket.extracted) {
3975
- const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
3976
4283
  out.push({
3977
4284
  type: "missing-extracted",
3978
4285
  source: bucket.source,
3979
4286
  target: bucket.target,
3980
4287
  edgeType: bucket.type,
3981
4288
  observed: bucket.observed,
3982
- confidence: cascaded,
4289
+ confidence: gradedConfidence(bucket.observed),
3983
4290
  reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
3984
4291
  recommendation: RECOMMENDATION_MISSING_EXTRACTED
3985
4292
  });
@@ -3998,7 +4305,7 @@ function declaredHostFor(svc) {
3998
4305
  function hasExtractedConfiguredBy(graph, svcId) {
3999
4306
  for (const edgeId of graph.outboundEdges(svcId)) {
4000
4307
  const e = graph.getEdgeAttributes(edgeId);
4001
- if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
4308
+ if (e.type === import_types19.EdgeType.CONFIGURED_BY && e.provenance === import_types19.Provenance.EXTRACTED) {
4002
4309
  return true;
4003
4310
  }
4004
4311
  }
@@ -4011,10 +4318,10 @@ function detectHostMismatch(graph, svcId, svc) {
4011
4318
  const out = [];
4012
4319
  for (const edgeId of graph.outboundEdges(svcId)) {
4013
4320
  const edge = graph.getEdgeAttributes(edgeId);
4014
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4015
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4321
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4322
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4016
4323
  const target = graph.getNodeAttributes(edge.target);
4017
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4324
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4018
4325
  const observedHost = target.host?.trim();
4019
4326
  if (!observedHost) continue;
4020
4327
  if (observedHost === declaredHost) continue;
@@ -4036,10 +4343,10 @@ function detectCompatDivergences(graph, svcId, svc) {
4036
4343
  const deps = svc.dependencies ?? {};
4037
4344
  for (const edgeId of graph.outboundEdges(svcId)) {
4038
4345
  const edge = graph.getEdgeAttributes(edgeId);
4039
- if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
4040
- if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
4346
+ if (edge.type !== import_types19.EdgeType.CONNECTS_TO) continue;
4347
+ if (edge.provenance !== import_types19.Provenance.OBSERVED) continue;
4041
4348
  const target = graph.getNodeAttributes(edge.target);
4042
- if (target.type !== import_types18.NodeType.DatabaseNode) continue;
4349
+ if (target.type !== import_types19.NodeType.DatabaseNode) continue;
4043
4350
  for (const pair of compatPairs()) {
4044
4351
  if (pair.engine !== target.engine) continue;
4045
4352
  const declared = deps[pair.driver];
@@ -4100,7 +4407,7 @@ function computeDivergences(graph, opts = {}) {
4100
4407
  }
4101
4408
  graph.forEachNode((nodeId, attrs) => {
4102
4409
  const n = attrs;
4103
- if (n.type !== import_types18.NodeType.ServiceNode) return;
4410
+ if (n.type !== import_types19.NodeType.ServiceNode) return;
4104
4411
  const svc = n;
4105
4412
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4106
4413
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -4118,13 +4425,22 @@ function computeDivergences(graph, opts = {}) {
4118
4425
  const target = opts.node;
4119
4426
  filtered = filtered.filter((d) => involvesNode(d, target));
4120
4427
  }
4428
+ const TYPE_LEADERSHIP = {
4429
+ "missing-extracted": 0,
4430
+ "missing-observed": 1,
4431
+ "version-mismatch": 2,
4432
+ "host-mismatch": 3,
4433
+ "compat-violation": 4
4434
+ };
4121
4435
  filtered.sort((a, b) => {
4122
4436
  if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4437
+ const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
4438
+ if (lead !== 0) return lead;
4123
4439
  if (a.type !== b.type) return a.type.localeCompare(b.type);
4124
4440
  if (a.source !== b.source) return a.source.localeCompare(b.source);
4125
4441
  return a.target.localeCompare(b.target);
4126
4442
  });
4127
- return import_types18.DivergenceResultSchema.parse({
4443
+ return import_types19.DivergenceResultSchema.parse({
4128
4444
  divergences: filtered,
4129
4445
  totalAffected: filtered.length,
4130
4446
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -4133,7 +4449,7 @@ function computeDivergences(graph, opts = {}) {
4133
4449
 
4134
4450
  // src/diff.ts
4135
4451
  init_cjs_shims();
4136
- var import_node_fs17 = require("fs");
4452
+ var import_node_fs19 = require("fs");
4137
4453
  async function loadSnapshotForDiff(target) {
4138
4454
  if (/^https?:\/\//i.test(target)) {
4139
4455
  const res = await fetch(target);
@@ -4142,7 +4458,7 @@ async function loadSnapshotForDiff(target) {
4142
4458
  }
4143
4459
  return await res.json();
4144
4460
  }
4145
- const raw = await import_node_fs17.promises.readFile(target, "utf8");
4461
+ const raw = await import_node_fs19.promises.readFile(target, "utf8");
4146
4462
  return JSON.parse(raw);
4147
4463
  }
4148
4464
  function indexEntries(entries) {
@@ -4210,23 +4526,23 @@ function canonicalJson(value) {
4210
4526
 
4211
4527
  // src/projects.ts
4212
4528
  init_cjs_shims();
4213
- var import_node_path29 = __toESM(require("path"), 1);
4529
+ var import_node_path32 = __toESM(require("path"), 1);
4214
4530
  function pathsForProject(project, baseDir) {
4215
4531
  if (project === DEFAULT_PROJECT) {
4216
4532
  return {
4217
- snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
4218
- errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
4219
- staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
4220
- embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
4221
- policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
4533
+ snapshotPath: import_node_path32.default.join(baseDir, "graph.json"),
4534
+ errorsPath: import_node_path32.default.join(baseDir, "errors.ndjson"),
4535
+ staleEventsPath: import_node_path32.default.join(baseDir, "stale-events.ndjson"),
4536
+ embeddingsCachePath: import_node_path32.default.join(baseDir, "embeddings.json"),
4537
+ policyViolationsPath: import_node_path32.default.join(baseDir, "policy-violations.ndjson")
4222
4538
  };
4223
4539
  }
4224
4540
  return {
4225
- snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
4226
- errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
4227
- staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
4228
- embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
4229
- policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
4541
+ snapshotPath: import_node_path32.default.join(baseDir, `${project}.json`),
4542
+ errorsPath: import_node_path32.default.join(baseDir, `errors.${project}.ndjson`),
4543
+ staleEventsPath: import_node_path32.default.join(baseDir, `stale-events.${project}.ndjson`),
4544
+ embeddingsCachePath: import_node_path32.default.join(baseDir, `embeddings.${project}.json`),
4545
+ policyViolationsPath: import_node_path32.default.join(baseDir, `policy-violations.${project}.ndjson`)
4230
4546
  };
4231
4547
  }
4232
4548
  var Projects = class {
@@ -4262,49 +4578,49 @@ var Projects = class {
4262
4578
 
4263
4579
  // src/registry.ts
4264
4580
  init_cjs_shims();
4265
- var import_node_fs18 = require("fs");
4581
+ var import_node_fs20 = require("fs");
4266
4582
  var import_node_os2 = __toESM(require("os"), 1);
4267
- var import_node_path30 = __toESM(require("path"), 1);
4268
- var import_types19 = require("@neat.is/types");
4583
+ var import_node_path33 = __toESM(require("path"), 1);
4584
+ var import_types20 = require("@neat.is/types");
4269
4585
  var LOCK_TIMEOUT_MS = 5e3;
4270
4586
  var LOCK_RETRY_MS = 50;
4271
4587
  function neatHome() {
4272
4588
  const override = process.env.NEAT_HOME;
4273
- if (override && override.length > 0) return import_node_path30.default.resolve(override);
4274
- return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4589
+ if (override && override.length > 0) return import_node_path33.default.resolve(override);
4590
+ return import_node_path33.default.join(import_node_os2.default.homedir(), ".neat");
4275
4591
  }
4276
4592
  function registryPath() {
4277
- return import_node_path30.default.join(neatHome(), "projects.json");
4593
+ return import_node_path33.default.join(neatHome(), "projects.json");
4278
4594
  }
4279
4595
  function registryLockPath() {
4280
- return import_node_path30.default.join(neatHome(), "projects.json.lock");
4596
+ return import_node_path33.default.join(neatHome(), "projects.json.lock");
4281
4597
  }
4282
4598
  async function normalizeProjectPath(input) {
4283
- const resolved = import_node_path30.default.resolve(input);
4599
+ const resolved = import_node_path33.default.resolve(input);
4284
4600
  try {
4285
- return await import_node_fs18.promises.realpath(resolved);
4601
+ return await import_node_fs20.promises.realpath(resolved);
4286
4602
  } catch {
4287
4603
  return resolved;
4288
4604
  }
4289
4605
  }
4290
4606
  async function writeAtomically(target, contents) {
4291
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4607
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(target), { recursive: true });
4292
4608
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4293
- const fd = await import_node_fs18.promises.open(tmp, "w");
4609
+ const fd = await import_node_fs20.promises.open(tmp, "w");
4294
4610
  try {
4295
4611
  await fd.writeFile(contents, "utf8");
4296
4612
  await fd.sync();
4297
4613
  } finally {
4298
4614
  await fd.close();
4299
4615
  }
4300
- await import_node_fs18.promises.rename(tmp, target);
4616
+ await import_node_fs20.promises.rename(tmp, target);
4301
4617
  }
4302
4618
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4303
4619
  const deadline = Date.now() + timeoutMs;
4304
- await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4620
+ await import_node_fs20.promises.mkdir(import_node_path33.default.dirname(lockPath), { recursive: true });
4305
4621
  while (true) {
4306
4622
  try {
4307
- const fd = await import_node_fs18.promises.open(lockPath, "wx");
4623
+ const fd = await import_node_fs20.promises.open(lockPath, "wx");
4308
4624
  await fd.close();
4309
4625
  return;
4310
4626
  } catch (err) {
@@ -4320,7 +4636,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4320
4636
  }
4321
4637
  }
4322
4638
  async function releaseLock(lockPath) {
4323
- await import_node_fs18.promises.unlink(lockPath).catch(() => {
4639
+ await import_node_fs20.promises.unlink(lockPath).catch(() => {
4324
4640
  });
4325
4641
  }
4326
4642
  async function withLock(fn) {
@@ -4336,7 +4652,7 @@ async function readRegistry() {
4336
4652
  const file = registryPath();
4337
4653
  let raw;
4338
4654
  try {
4339
- raw = await import_node_fs18.promises.readFile(file, "utf8");
4655
+ raw = await import_node_fs20.promises.readFile(file, "utf8");
4340
4656
  } catch (err) {
4341
4657
  if (err.code === "ENOENT") {
4342
4658
  return { version: 1, projects: [] };
@@ -4344,10 +4660,10 @@ async function readRegistry() {
4344
4660
  throw err;
4345
4661
  }
4346
4662
  const parsed = JSON.parse(raw);
4347
- return import_types19.RegistryFileSchema.parse(parsed);
4663
+ return import_types20.RegistryFileSchema.parse(parsed);
4348
4664
  }
4349
4665
  async function writeRegistry(reg) {
4350
- const validated = import_types19.RegistryFileSchema.parse(reg);
4666
+ const validated = import_types20.RegistryFileSchema.parse(reg);
4351
4667
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4352
4668
  }
4353
4669
  var ProjectNameCollisionError = class extends Error {
@@ -4607,11 +4923,11 @@ function registerRoutes(scope, ctx) {
4607
4923
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4608
4924
  const parsed = [];
4609
4925
  for (const c of candidates) {
4610
- const r = import_types20.DivergenceTypeSchema.safeParse(c);
4926
+ const r = import_types21.DivergenceTypeSchema.safeParse(c);
4611
4927
  if (!r.success) {
4612
4928
  return reply.code(400).send({
4613
4929
  error: `unknown divergence type "${c}"`,
4614
- allowed: import_types20.DivergenceTypeSchema.options
4930
+ allowed: import_types21.DivergenceTypeSchema.options
4615
4931
  });
4616
4932
  }
4617
4933
  parsed.push(r.data);
@@ -4795,7 +5111,7 @@ function registerRoutes(scope, ctx) {
4795
5111
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4796
5112
  let violations = await log.readAll();
4797
5113
  if (req.query.severity) {
4798
- const sev = import_types20.PolicySeveritySchema.safeParse(req.query.severity);
5114
+ const sev = import_types21.PolicySeveritySchema.safeParse(req.query.severity);
4799
5115
  if (!sev.success) {
4800
5116
  return reply.code(400).send({
4801
5117
  error: "invalid severity",
@@ -4812,7 +5128,7 @@ function registerRoutes(scope, ctx) {
4812
5128
  scope.post("/policies/check", async (req, reply) => {
4813
5129
  const proj = resolveProject(registry, req, reply);
4814
5130
  if (!proj) return;
4815
- const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req.body ?? {});
5131
+ const parsed = import_types21.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4816
5132
  if (!parsed.success) {
4817
5133
  return reply.code(400).send({
4818
5134
  error: "invalid /policies/check body",
@@ -4916,15 +5232,15 @@ init_otel_grpc();
4916
5232
 
4917
5233
  // src/daemon.ts
4918
5234
  init_cjs_shims();
4919
- var import_node_fs19 = require("fs");
4920
- var import_node_path33 = __toESM(require("path"), 1);
5235
+ var import_node_fs21 = require("fs");
5236
+ var import_node_path36 = __toESM(require("path"), 1);
4921
5237
  init_otel();
4922
5238
  function neatHomeFor(opts) {
4923
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
5239
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path36.default.resolve(opts.neatHome);
4924
5240
  const env = process.env.NEAT_HOME;
4925
- if (env && env.length > 0) return import_node_path33.default.resolve(env);
5241
+ if (env && env.length > 0) return import_node_path36.default.resolve(env);
4926
5242
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4927
- return import_node_path33.default.join(home, ".neat");
5243
+ return import_node_path36.default.join(home, ".neat");
4928
5244
  }
4929
5245
  function routeSpanToProject(serviceName, projects) {
4930
5246
  if (!serviceName) return DEFAULT_PROJECT;
@@ -4937,9 +5253,9 @@ function routeSpanToProject(serviceName, projects) {
4937
5253
  return DEFAULT_PROJECT;
4938
5254
  }
4939
5255
  async function bootstrapProject(entry) {
4940
- const paths = pathsForProject(entry.name, import_node_path33.default.join(entry.path, "neat-out"));
5256
+ const paths = pathsForProject(entry.name, import_node_path36.default.join(entry.path, "neat-out"));
4941
5257
  try {
4942
- const stat = await import_node_fs19.promises.stat(entry.path);
5258
+ const stat = await import_node_fs21.promises.stat(entry.path);
4943
5259
  if (!stat.isDirectory()) {
4944
5260
  throw new Error(`registered path ${entry.path} is not a directory`);
4945
5261
  }
@@ -5004,13 +5320,13 @@ async function startDaemon(opts = {}) {
5004
5320
  const home = neatHomeFor(opts);
5005
5321
  const regPath = registryPath();
5006
5322
  try {
5007
- await import_node_fs19.promises.access(regPath);
5323
+ await import_node_fs21.promises.access(regPath);
5008
5324
  } catch {
5009
5325
  throw new Error(
5010
5326
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
5011
5327
  );
5012
5328
  }
5013
- const pidPath = import_node_path33.default.join(home, "neatd.pid");
5329
+ const pidPath = import_node_path36.default.join(home, "neatd.pid");
5014
5330
  await writeAtomically(pidPath, `${process.pid}
5015
5331
  `);
5016
5332
  const slots = /* @__PURE__ */ new Map();
@@ -5079,7 +5395,7 @@ async function startDaemon(opts = {}) {
5079
5395
  }
5080
5396
  if (restApp) await restApp.close().catch(() => {
5081
5397
  });
5082
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5398
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
5083
5399
  });
5084
5400
  throw new Error(
5085
5401
  `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
@@ -5124,7 +5440,7 @@ async function startDaemon(opts = {}) {
5124
5440
  });
5125
5441
  if (otlpApp) await otlpApp.close().catch(() => {
5126
5442
  });
5127
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5443
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
5128
5444
  });
5129
5445
  throw new Error(
5130
5446
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
@@ -5164,7 +5480,7 @@ async function startDaemon(opts = {}) {
5164
5480
  } catch {
5165
5481
  }
5166
5482
  }
5167
- await import_node_fs19.promises.unlink(pidPath).catch(() => {
5483
+ await import_node_fs21.promises.unlink(pidPath).catch(() => {
5168
5484
  });
5169
5485
  };
5170
5486
  return { slots, reload, stop, pidPath, restAddress, otlpAddress };