@neat.is/core 0.2.7 → 0.2.9

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
@@ -24,12 +24,12 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  ));
25
25
 
26
26
  // src/neatd.ts
27
- var import_node_fs18 = require("fs");
28
- var import_node_path31 = __toESM(require("path"), 1);
27
+ var import_node_fs19 = require("fs");
28
+ var import_node_path32 = __toESM(require("path"), 1);
29
29
 
30
30
  // src/daemon.ts
31
- var import_node_fs17 = require("fs");
32
- var import_node_path30 = __toESM(require("path"), 1);
31
+ var import_node_fs18 = require("fs");
32
+ var import_node_path31 = __toESM(require("path"), 1);
33
33
 
34
34
  // src/graph.ts
35
35
  var import_graphology = __toESM(require("graphology"), 1);
@@ -376,6 +376,17 @@ function deprecatedApis() {
376
376
  return currentMatrix().deprecatedApis ?? [];
377
377
  }
378
378
 
379
+ // src/events.ts
380
+ var import_node_events = require("events");
381
+ var EVENT_BUS_CHANNEL = "event";
382
+ var NeatEventBus = class extends import_node_events.EventEmitter {
383
+ };
384
+ var eventBus = new NeatEventBus();
385
+ eventBus.setMaxListeners(0);
386
+ function emitNeatEvent(envelope) {
387
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
388
+ }
389
+
379
390
  // src/traverse.ts
380
391
  var import_types = require("@neat.is/types");
381
392
  var BLAST_RADIUS_DEFAULT_DEPTH = 10;
@@ -918,10 +929,10 @@ function pickLater(a, b) {
918
929
  }
919
930
 
920
931
  // src/extract/services.ts
921
- var import_node_fs6 = require("fs");
922
- var import_node_path6 = __toESM(require("path"), 1);
932
+ var import_node_fs7 = require("fs");
933
+ var import_node_path7 = __toESM(require("path"), 1);
923
934
  var import_ignore = __toESM(require("ignore"), 1);
924
- var import_minimatch = require("minimatch");
935
+ var import_minimatch2 = require("minimatch");
925
936
  var import_types5 = require("@neat.is/types");
926
937
 
927
938
  // src/extract/shared.ts
@@ -1032,6 +1043,72 @@ function pythonToPackage(service) {
1032
1043
  };
1033
1044
  }
1034
1045
 
1046
+ // src/extract/owners.ts
1047
+ var import_node_fs6 = require("fs");
1048
+ var import_node_path6 = __toESM(require("path"), 1);
1049
+ var import_minimatch = require("minimatch");
1050
+ async function loadCodeowners(scanPath) {
1051
+ const candidates = [
1052
+ import_node_path6.default.join(scanPath, "CODEOWNERS"),
1053
+ import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
1054
+ ];
1055
+ for (const file of candidates) {
1056
+ if (await exists(file)) {
1057
+ const raw = await import_node_fs6.promises.readFile(file, "utf8");
1058
+ return parseCodeowners(raw);
1059
+ }
1060
+ }
1061
+ return null;
1062
+ }
1063
+ function parseCodeowners(raw) {
1064
+ const rules = [];
1065
+ for (const line of raw.split("\n")) {
1066
+ const trimmed = line.trim();
1067
+ if (!trimmed || trimmed.startsWith("#")) continue;
1068
+ const match = /^(\S+)\s+(.+)$/.exec(trimmed);
1069
+ if (!match) continue;
1070
+ rules.push({ pattern: match[1], owners: match[2].trim() });
1071
+ }
1072
+ return { rules };
1073
+ }
1074
+ function matchOwner(file, repoPath) {
1075
+ const normalized = repoPath.split(import_node_path6.default.sep).join("/");
1076
+ for (const rule of file.rules) {
1077
+ if (matchesPattern(rule.pattern, normalized)) return rule.owners;
1078
+ }
1079
+ return null;
1080
+ }
1081
+ function matchesPattern(rawPattern, repoPath) {
1082
+ let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
1083
+ if (pattern === "*") return !repoPath.includes("/");
1084
+ if (pattern === "**" || pattern === "") return true;
1085
+ if (pattern.endsWith("/")) pattern = pattern + "**";
1086
+ if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
1087
+ if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
1088
+ return false;
1089
+ }
1090
+ async function readPackageJsonAuthor(serviceDir) {
1091
+ const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
1092
+ if (!await exists(pkgPath)) return null;
1093
+ try {
1094
+ const pkg = await readJson(pkgPath);
1095
+ if (!pkg.author) return null;
1096
+ if (typeof pkg.author === "string") return pkg.author;
1097
+ if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
1098
+ return null;
1099
+ } catch {
1100
+ return null;
1101
+ }
1102
+ }
1103
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
1104
+ if (codeowners && repoPath !== void 0) {
1105
+ const owner = matchOwner(codeowners, repoPath);
1106
+ if (owner) return owner;
1107
+ }
1108
+ const author = await readPackageJsonAuthor(serviceDir);
1109
+ return author ?? void 0;
1110
+ }
1111
+
1035
1112
  // src/extract/services.ts
1036
1113
  var DEFAULT_SCAN_DEPTH = 5;
1037
1114
  function parseScanDepth() {
@@ -1048,21 +1125,21 @@ function workspaceGlobs(pkg) {
1048
1125
  return null;
1049
1126
  }
1050
1127
  async function loadGitignore(scanPath) {
1051
- const gitignorePath = import_node_path6.default.join(scanPath, ".gitignore");
1128
+ const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
1052
1129
  if (!await exists(gitignorePath)) return null;
1053
- const raw = await import_node_fs6.promises.readFile(gitignorePath, "utf8");
1130
+ const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
1054
1131
  return (0, import_ignore.default)().add(raw);
1055
1132
  }
1056
1133
  async function walkDirs(start, scanPath, options, visit) {
1057
1134
  async function recurse(current, depth) {
1058
1135
  if (depth > options.maxDepth) return;
1059
- const entries = await import_node_fs6.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1136
+ const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
1060
1137
  for (const entry2 of entries) {
1061
1138
  if (!entry2.isDirectory()) continue;
1062
1139
  if (IGNORED_DIRS.has(entry2.name)) continue;
1063
- const child = import_node_path6.default.join(current, entry2.name);
1140
+ const child = import_node_path7.default.join(current, entry2.name);
1064
1141
  if (options.ig) {
1065
- const rel = import_node_path6.default.relative(scanPath, child).split(import_node_path6.default.sep).join("/");
1142
+ const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
1066
1143
  if (rel && options.ig.ignores(rel + "/")) continue;
1067
1144
  }
1068
1145
  await visit(child);
@@ -1077,8 +1154,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1077
1154
  for (const raw of globs) {
1078
1155
  const pattern = raw.replace(/^\.\//, "");
1079
1156
  if (!pattern.includes("*")) {
1080
- const candidate = import_node_path6.default.join(scanPath, pattern);
1081
- if (await exists(import_node_path6.default.join(candidate, "package.json"))) found.add(candidate);
1157
+ const candidate = import_node_path7.default.join(scanPath, pattern);
1158
+ if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
1082
1159
  continue;
1083
1160
  }
1084
1161
  const segments = pattern.split("/");
@@ -1087,13 +1164,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1087
1164
  if (seg.includes("*")) break;
1088
1165
  staticSegments.push(seg);
1089
1166
  }
1090
- const start = import_node_path6.default.join(scanPath, ...staticSegments);
1167
+ const start = import_node_path7.default.join(scanPath, ...staticSegments);
1091
1168
  if (!await exists(start)) continue;
1092
1169
  const hasDoubleStar = pattern.includes("**");
1093
1170
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
1094
1171
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
1095
- const rel = import_node_path6.default.relative(scanPath, dir).split(import_node_path6.default.sep).join("/");
1096
- if ((0, import_minimatch.minimatch)(rel, pattern) && await exists(import_node_path6.default.join(dir, "package.json"))) {
1172
+ const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
1173
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
1097
1174
  found.add(dir);
1098
1175
  }
1099
1176
  });
@@ -1101,9 +1178,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
1101
1178
  return [...found];
1102
1179
  }
1103
1180
  async function discoverNodeService(scanPath, dir) {
1104
- const pkgPath = import_node_path6.default.join(dir, "package.json");
1181
+ const pkgPath = import_node_path7.default.join(dir, "package.json");
1105
1182
  if (!await exists(pkgPath)) return null;
1106
- const pkg = await readJson(pkgPath);
1183
+ let pkg;
1184
+ try {
1185
+ pkg = await readJson(pkgPath);
1186
+ } catch (err) {
1187
+ console.warn(
1188
+ `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
1189
+ );
1190
+ return null;
1191
+ }
1107
1192
  if (!pkg.name) return null;
1108
1193
  const node = {
1109
1194
  id: (0, import_types5.serviceId)(pkg.name),
@@ -1112,7 +1197,7 @@ async function discoverNodeService(scanPath, dir) {
1112
1197
  language: "javascript",
1113
1198
  version: pkg.version,
1114
1199
  dependencies: pkg.dependencies ?? {},
1115
- repoPath: import_node_path6.default.relative(scanPath, dir),
1200
+ repoPath: import_node_path7.default.relative(scanPath, dir),
1116
1201
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
1117
1202
  };
1118
1203
  return { pkg, dir, node };
@@ -1128,13 +1213,22 @@ async function discoverPyService(scanPath, dir) {
1128
1213
  language: "python",
1129
1214
  version: py.version,
1130
1215
  dependencies: py.dependencies,
1131
- repoPath: import_node_path6.default.relative(scanPath, dir)
1216
+ repoPath: import_node_path7.default.relative(scanPath, dir)
1132
1217
  };
1133
1218
  return { pkg, dir, node };
1134
1219
  }
1135
1220
  async function discoverServices(scanPath) {
1136
- const rootPkgPath = import_node_path6.default.join(scanPath, "package.json");
1137
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
1221
+ const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
1222
+ let rootPkg = null;
1223
+ if (await exists(rootPkgPath)) {
1224
+ try {
1225
+ rootPkg = await readJson(rootPkgPath);
1226
+ } catch (err) {
1227
+ console.warn(
1228
+ `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
1229
+ );
1230
+ }
1231
+ }
1138
1232
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
1139
1233
  const candidateDirs = [];
1140
1234
  if (wsGlobs) {
@@ -1147,9 +1241,9 @@ async function discoverServices(scanPath) {
1147
1241
  scanPath,
1148
1242
  { maxDepth: parseScanDepth(), ig },
1149
1243
  async (dir) => {
1150
- if (await exists(import_node_path6.default.join(dir, "package.json"))) {
1244
+ if (await exists(import_node_path7.default.join(dir, "package.json"))) {
1151
1245
  candidateDirs.push(dir);
1152
- } else if (await exists(import_node_path6.default.join(dir, "pyproject.toml")) || await exists(import_node_path6.default.join(dir, "requirements.txt")) || await exists(import_node_path6.default.join(dir, "setup.py"))) {
1246
+ } 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"))) {
1153
1247
  candidateDirs.push(dir);
1154
1248
  }
1155
1249
  }
@@ -1163,8 +1257,8 @@ async function discoverServices(scanPath) {
1163
1257
  if (!service) continue;
1164
1258
  const existingDir = seen.get(service.node.name);
1165
1259
  if (existingDir !== void 0) {
1166
- const a = import_node_path6.default.relative(scanPath, existingDir) || ".";
1167
- const b = import_node_path6.default.relative(scanPath, dir) || ".";
1260
+ const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
1261
+ const b = import_node_path7.default.relative(scanPath, dir) || ".";
1168
1262
  console.warn(
1169
1263
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
1170
1264
  );
@@ -1173,6 +1267,11 @@ async function discoverServices(scanPath) {
1173
1267
  seen.set(service.node.name, dir);
1174
1268
  out.push(service);
1175
1269
  }
1270
+ const codeowners = await loadCodeowners(scanPath);
1271
+ for (const service of out) {
1272
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
1273
+ if (owner !== void 0) service.node.owner = owner;
1274
+ }
1176
1275
  return out;
1177
1276
  }
1178
1277
  function addServiceNodes(graph, services) {
@@ -1195,8 +1294,8 @@ function addServiceNodes(graph, services) {
1195
1294
  }
1196
1295
 
1197
1296
  // src/extract/aliases.ts
1198
- var import_node_path7 = __toESM(require("path"), 1);
1199
- var import_node_fs7 = require("fs");
1297
+ var import_node_path8 = __toESM(require("path"), 1);
1298
+ var import_node_fs8 = require("fs");
1200
1299
  var import_yaml2 = require("yaml");
1201
1300
  var import_types6 = require("@neat.is/types");
1202
1301
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -1223,21 +1322,29 @@ function indexServicesByName(services) {
1223
1322
  const map = /* @__PURE__ */ new Map();
1224
1323
  for (const s of services) {
1225
1324
  map.set(s.node.name, s.node.id);
1226
- map.set(import_node_path7.default.basename(s.dir), s.node.id);
1325
+ map.set(import_node_path8.default.basename(s.dir), s.node.id);
1227
1326
  }
1228
1327
  return map;
1229
1328
  }
1230
1329
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
1231
1330
  let composePath = null;
1232
1331
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
1233
- const abs = import_node_path7.default.join(scanPath, name);
1332
+ const abs = import_node_path8.default.join(scanPath, name);
1234
1333
  if (await exists(abs)) {
1235
1334
  composePath = abs;
1236
1335
  break;
1237
1336
  }
1238
1337
  }
1239
1338
  if (!composePath) return;
1240
- const compose = await readYaml(composePath);
1339
+ let compose;
1340
+ try {
1341
+ compose = await readYaml(composePath);
1342
+ } catch (err) {
1343
+ console.warn(
1344
+ `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
1345
+ );
1346
+ return;
1347
+ }
1241
1348
  if (!compose?.services) return;
1242
1349
  for (const [composeName, svc] of Object.entries(compose.services)) {
1243
1350
  const serviceId3 = serviceIndex.get(composeName);
@@ -1276,9 +1383,17 @@ function parseDockerfileLabels(content) {
1276
1383
  }
1277
1384
  async function collectDockerfileAliases(graph, services) {
1278
1385
  for (const service of services) {
1279
- const dockerfilePath = import_node_path7.default.join(service.dir, "Dockerfile");
1386
+ const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
1280
1387
  if (!await exists(dockerfilePath)) continue;
1281
- const content = await import_node_fs7.promises.readFile(dockerfilePath, "utf8");
1388
+ let content;
1389
+ try {
1390
+ content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
1391
+ } catch (err) {
1392
+ console.warn(
1393
+ `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
1394
+ );
1395
+ continue;
1396
+ }
1282
1397
  const aliases = parseDockerfileLabels(content);
1283
1398
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
1284
1399
  }
@@ -1286,13 +1401,13 @@ async function collectDockerfileAliases(graph, services) {
1286
1401
  async function walkYamlFiles(start, depth = 0, max = 5) {
1287
1402
  if (depth > max) return [];
1288
1403
  const out = [];
1289
- const entries = await import_node_fs7.promises.readdir(start, { withFileTypes: true }).catch(() => []);
1404
+ const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
1290
1405
  for (const entry2 of entries) {
1291
1406
  if (entry2.isDirectory()) {
1292
1407
  if (IGNORED_DIRS.has(entry2.name)) continue;
1293
- out.push(...await walkYamlFiles(import_node_path7.default.join(start, entry2.name), depth + 1, max));
1294
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path7.default.extname(entry2.name))) {
1295
- out.push(import_node_path7.default.join(start, entry2.name));
1408
+ out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry2.name), depth + 1, max));
1409
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry2.name))) {
1410
+ out.push(import_node_path8.default.join(start, entry2.name));
1296
1411
  }
1297
1412
  }
1298
1413
  return out;
@@ -1319,7 +1434,7 @@ function k8sServiceTarget(doc, byName) {
1319
1434
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
1320
1435
  const files = await walkYamlFiles(scanPath);
1321
1436
  for (const file of files) {
1322
- const content = await import_node_fs7.promises.readFile(file, "utf8");
1437
+ const content = await import_node_fs8.promises.readFile(file, "utf8");
1323
1438
  let docs;
1324
1439
  try {
1325
1440
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -1343,13 +1458,13 @@ async function addServiceAliases(graph, scanPath, services) {
1343
1458
  }
1344
1459
 
1345
1460
  // src/extract/databases/index.ts
1346
- var import_node_path15 = __toESM(require("path"), 1);
1461
+ var import_node_path16 = __toESM(require("path"), 1);
1347
1462
  var import_types7 = require("@neat.is/types");
1348
1463
 
1349
1464
  // src/extract/databases/db-config-yaml.ts
1350
- var import_node_path8 = __toESM(require("path"), 1);
1465
+ var import_node_path9 = __toESM(require("path"), 1);
1351
1466
  async function parse(serviceDir) {
1352
- const yamlPath = import_node_path8.default.join(serviceDir, "db-config.yaml");
1467
+ const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
1353
1468
  if (!await exists(yamlPath)) return [];
1354
1469
  const raw = await readYaml(yamlPath);
1355
1470
  return [
@@ -1366,12 +1481,12 @@ async function parse(serviceDir) {
1366
1481
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
1367
1482
 
1368
1483
  // src/extract/databases/dotenv.ts
1369
- var import_node_fs9 = require("fs");
1370
- var import_node_path10 = __toESM(require("path"), 1);
1484
+ var import_node_fs10 = require("fs");
1485
+ var import_node_path11 = __toESM(require("path"), 1);
1371
1486
 
1372
1487
  // src/extract/databases/shared.ts
1373
- var import_node_fs8 = require("fs");
1374
- var import_node_path9 = __toESM(require("path"), 1);
1488
+ var import_node_fs9 = require("fs");
1489
+ var import_node_path10 = __toESM(require("path"), 1);
1375
1490
  function schemeToEngine(scheme) {
1376
1491
  const s = scheme.toLowerCase().split("+")[0];
1377
1492
  switch (s) {
@@ -1410,14 +1525,14 @@ function parseConnectionString(url) {
1410
1525
  }
1411
1526
  async function readIfExists(filePath) {
1412
1527
  try {
1413
- return await import_node_fs8.promises.readFile(filePath, "utf8");
1528
+ return await import_node_fs9.promises.readFile(filePath, "utf8");
1414
1529
  } catch {
1415
1530
  return null;
1416
1531
  }
1417
1532
  }
1418
1533
  async function findFirst(serviceDir, candidates) {
1419
1534
  for (const rel of candidates) {
1420
- const abs = import_node_path9.default.join(serviceDir, rel);
1535
+ const abs = import_node_path10.default.join(serviceDir, rel);
1421
1536
  const content = await readIfExists(abs);
1422
1537
  if (content !== null) return abs;
1423
1538
  }
@@ -1468,15 +1583,15 @@ function parseDotenvLine(line) {
1468
1583
  return { key, value };
1469
1584
  }
1470
1585
  async function parse2(serviceDir) {
1471
- const entries = await import_node_fs9.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
1586
+ const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
1472
1587
  const configs = [];
1473
1588
  const seen = /* @__PURE__ */ new Set();
1474
1589
  for (const entry2 of entries) {
1475
1590
  if (!entry2.isFile()) continue;
1476
1591
  const match = isConfigFile(entry2.name);
1477
1592
  if (!match.match || match.fileType !== "env") continue;
1478
- const filePath = import_node_path10.default.join(serviceDir, entry2.name);
1479
- const content = await import_node_fs9.promises.readFile(filePath, "utf8");
1593
+ const filePath = import_node_path11.default.join(serviceDir, entry2.name);
1594
+ const content = await import_node_fs10.promises.readFile(filePath, "utf8");
1480
1595
  for (const line of content.split("\n")) {
1481
1596
  const parsed = parseDotenvLine(line);
1482
1597
  if (!parsed) continue;
@@ -1494,9 +1609,9 @@ async function parse2(serviceDir) {
1494
1609
  var dotenvParser = { name: ".env", parse: parse2 };
1495
1610
 
1496
1611
  // src/extract/databases/prisma.ts
1497
- var import_node_path11 = __toESM(require("path"), 1);
1612
+ var import_node_path12 = __toESM(require("path"), 1);
1498
1613
  async function parse3(serviceDir) {
1499
- const schemaPath = import_node_path11.default.join(serviceDir, "prisma", "schema.prisma");
1614
+ const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
1500
1615
  const content = await readIfExists(schemaPath);
1501
1616
  if (!content) return [];
1502
1617
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -1625,10 +1740,10 @@ async function parse5(serviceDir) {
1625
1740
  var knexParser = { name: "knex", parse: parse5 };
1626
1741
 
1627
1742
  // src/extract/databases/ormconfig.ts
1628
- var import_node_path12 = __toESM(require("path"), 1);
1743
+ var import_node_path13 = __toESM(require("path"), 1);
1629
1744
  async function parse6(serviceDir) {
1630
1745
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
1631
- const abs = import_node_path12.default.join(serviceDir, candidate);
1746
+ const abs = import_node_path13.default.join(serviceDir, candidate);
1632
1747
  if (!await exists(abs)) continue;
1633
1748
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
1634
1749
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -1686,9 +1801,9 @@ async function parse7(serviceDir) {
1686
1801
  var typeormParser = { name: "typeorm", parse: parse7 };
1687
1802
 
1688
1803
  // src/extract/databases/sequelize.ts
1689
- var import_node_path13 = __toESM(require("path"), 1);
1804
+ var import_node_path14 = __toESM(require("path"), 1);
1690
1805
  async function parse8(serviceDir) {
1691
- const configPath = import_node_path13.default.join(serviceDir, "config", "config.json");
1806
+ const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
1692
1807
  if (!await exists(configPath)) return [];
1693
1808
  const raw = await readJson(configPath);
1694
1809
  const out = [];
@@ -1714,7 +1829,7 @@ async function parse8(serviceDir) {
1714
1829
  var sequelizeParser = { name: "sequelize", parse: parse8 };
1715
1830
 
1716
1831
  // src/extract/databases/docker-compose.ts
1717
- var import_node_path14 = __toESM(require("path"), 1);
1832
+ var import_node_path15 = __toESM(require("path"), 1);
1718
1833
  function portFromService(svc) {
1719
1834
  for (const raw of svc.ports ?? []) {
1720
1835
  const str = String(raw);
@@ -1741,7 +1856,7 @@ function databaseFromEnv(svc) {
1741
1856
  }
1742
1857
  async function parse9(serviceDir) {
1743
1858
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
1744
- const abs = import_node_path14.default.join(serviceDir, name);
1859
+ const abs = import_node_path15.default.join(serviceDir, name);
1745
1860
  if (!await exists(abs)) continue;
1746
1861
  const raw = await readYaml(abs);
1747
1862
  if (!raw?.services) return [];
@@ -1921,7 +2036,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
1921
2036
  provenance: import_types7.Provenance.EXTRACTED,
1922
2037
  ...config.sourceFile ? {
1923
2038
  evidence: {
1924
- file: import_node_path15.default.relative(scanPath, config.sourceFile).split(import_node_path15.default.sep).join("/")
2039
+ file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
1925
2040
  }
1926
2041
  } : {}
1927
2042
  };
@@ -1948,15 +2063,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
1948
2063
  }
1949
2064
 
1950
2065
  // src/extract/configs.ts
1951
- var import_node_fs10 = require("fs");
1952
- var import_node_path16 = __toESM(require("path"), 1);
2066
+ var import_node_fs11 = require("fs");
2067
+ var import_node_path17 = __toESM(require("path"), 1);
1953
2068
  var import_types8 = require("@neat.is/types");
1954
2069
  async function walkConfigFiles(dir) {
1955
2070
  const out = [];
1956
2071
  async function walk(current) {
1957
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true });
2072
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
1958
2073
  for (const entry2 of entries) {
1959
- const full = import_node_path16.default.join(current, entry2.name);
2074
+ const full = import_node_path17.default.join(current, entry2.name);
1960
2075
  if (entry2.isDirectory()) {
1961
2076
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
1962
2077
  } else if (entry2.isFile() && isConfigFile(entry2.name).match) {
@@ -1973,13 +2088,13 @@ async function addConfigNodes(graph, services, scanPath) {
1973
2088
  for (const service of services) {
1974
2089
  const configFiles = await walkConfigFiles(service.dir);
1975
2090
  for (const file of configFiles) {
1976
- const relPath = import_node_path16.default.relative(scanPath, file);
2091
+ const relPath = import_node_path17.default.relative(scanPath, file);
1977
2092
  const node = {
1978
2093
  id: (0, import_types8.configId)(relPath),
1979
2094
  type: import_types8.NodeType.ConfigNode,
1980
- name: import_node_path16.default.basename(file),
2095
+ name: import_node_path17.default.basename(file),
1981
2096
  path: relPath,
1982
- fileType: isConfigFile(import_node_path16.default.basename(file)).fileType
2097
+ fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
1983
2098
  };
1984
2099
  if (!graph.hasNode(node.id)) {
1985
2100
  graph.addNode(node.id, node);
@@ -1991,7 +2106,7 @@ async function addConfigNodes(graph, services, scanPath) {
1991
2106
  target: node.id,
1992
2107
  type: import_types8.EdgeType.CONFIGURED_BY,
1993
2108
  provenance: import_types8.Provenance.EXTRACTED,
1994
- evidence: { file: relPath.split(import_node_path16.default.sep).join("/") }
2109
+ evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
1995
2110
  };
1996
2111
  if (!graph.hasEdge(edge.id)) {
1997
2112
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2006,24 +2121,24 @@ async function addConfigNodes(graph, services, scanPath) {
2006
2121
  var import_types14 = require("@neat.is/types");
2007
2122
 
2008
2123
  // src/extract/calls/http.ts
2009
- var import_node_path18 = __toESM(require("path"), 1);
2124
+ var import_node_path19 = __toESM(require("path"), 1);
2010
2125
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2011
2126
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2012
2127
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2013
2128
  var import_types9 = require("@neat.is/types");
2014
2129
 
2015
2130
  // src/extract/calls/shared.ts
2016
- var import_node_fs11 = require("fs");
2017
- var import_node_path17 = __toESM(require("path"), 1);
2131
+ var import_node_fs12 = require("fs");
2132
+ var import_node_path18 = __toESM(require("path"), 1);
2018
2133
  async function walkSourceFiles(dir) {
2019
2134
  const out = [];
2020
2135
  async function walk(current) {
2021
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2136
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2022
2137
  for (const entry2 of entries) {
2023
- const full = import_node_path17.default.join(current, entry2.name);
2138
+ const full = import_node_path18.default.join(current, entry2.name);
2024
2139
  if (entry2.isDirectory()) {
2025
2140
  if (!IGNORED_DIRS.has(entry2.name)) await walk(full);
2026
- } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path17.default.extname(entry2.name))) {
2141
+ } else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry2.name))) {
2027
2142
  out.push(full);
2028
2143
  }
2029
2144
  }
@@ -2036,7 +2151,7 @@ async function loadSourceFiles(dir) {
2036
2151
  const out = [];
2037
2152
  for (const p of paths) {
2038
2153
  try {
2039
- const content = await import_node_fs11.promises.readFile(p, "utf8");
2154
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
2040
2155
  out.push({ path: p, content });
2041
2156
  } catch {
2042
2157
  }
@@ -2092,9 +2207,9 @@ async function addHttpCallEdges(graph, services) {
2092
2207
  const knownHosts = /* @__PURE__ */ new Set();
2093
2208
  const hostToNodeId = /* @__PURE__ */ new Map();
2094
2209
  for (const service of services) {
2095
- knownHosts.add(import_node_path18.default.basename(service.dir));
2210
+ knownHosts.add(import_node_path19.default.basename(service.dir));
2096
2211
  knownHosts.add(service.pkg.name);
2097
- hostToNodeId.set(import_node_path18.default.basename(service.dir), service.node.id);
2212
+ hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
2098
2213
  hostToNodeId.set(service.pkg.name, service.node.id);
2099
2214
  }
2100
2215
  let edgesAdded = 0;
@@ -2102,8 +2217,16 @@ async function addHttpCallEdges(graph, services) {
2102
2217
  const files = await loadSourceFiles(service.dir);
2103
2218
  const seenTargets = /* @__PURE__ */ new Map();
2104
2219
  for (const file of files) {
2105
- const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
2106
- const targets = callsFromSource(file.content, parser, knownHosts);
2220
+ const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
2221
+ let targets;
2222
+ try {
2223
+ targets = callsFromSource(file.content, parser, knownHosts);
2224
+ } catch (err) {
2225
+ console.warn(
2226
+ `[neat] http call extraction skipped ${file.path}: ${err.message}`
2227
+ );
2228
+ continue;
2229
+ }
2107
2230
  for (const t of targets) {
2108
2231
  const targetId = hostToNodeId.get(t);
2109
2232
  if (!targetId || targetId === service.node.id) continue;
@@ -2122,7 +2245,7 @@ async function addHttpCallEdges(graph, services) {
2122
2245
  type: import_types9.EdgeType.CALLS,
2123
2246
  provenance: import_types9.Provenance.EXTRACTED,
2124
2247
  evidence: {
2125
- file: import_node_path18.default.relative(service.dir, evidenceFile.file),
2248
+ file: import_node_path19.default.relative(service.dir, evidenceFile.file),
2126
2249
  line,
2127
2250
  snippet: snippet(fileContent, line)
2128
2251
  }
@@ -2137,7 +2260,7 @@ async function addHttpCallEdges(graph, services) {
2137
2260
  }
2138
2261
 
2139
2262
  // src/extract/calls/kafka.ts
2140
- var import_node_path19 = __toESM(require("path"), 1);
2263
+ var import_node_path20 = __toESM(require("path"), 1);
2141
2264
  var import_types10 = require("@neat.is/types");
2142
2265
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
2143
2266
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -2164,7 +2287,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2164
2287
  kind: "kafka-topic",
2165
2288
  edgeType,
2166
2289
  evidence: {
2167
- file: import_node_path19.default.relative(serviceDir, file.path),
2290
+ file: import_node_path20.default.relative(serviceDir, file.path),
2168
2291
  line,
2169
2292
  snippet: snippet(file.content, line)
2170
2293
  }
@@ -2176,7 +2299,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2176
2299
  }
2177
2300
 
2178
2301
  // src/extract/calls/redis.ts
2179
- var import_node_path20 = __toESM(require("path"), 1);
2302
+ var import_node_path21 = __toESM(require("path"), 1);
2180
2303
  var import_types11 = require("@neat.is/types");
2181
2304
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
2182
2305
  function redisEndpointsFromFile(file, serviceDir) {
@@ -2195,7 +2318,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2195
2318
  kind: "redis",
2196
2319
  edgeType: "CALLS",
2197
2320
  evidence: {
2198
- file: import_node_path20.default.relative(serviceDir, file.path),
2321
+ file: import_node_path21.default.relative(serviceDir, file.path),
2199
2322
  line,
2200
2323
  snippet: snippet(file.content, line)
2201
2324
  }
@@ -2205,7 +2328,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2205
2328
  }
2206
2329
 
2207
2330
  // src/extract/calls/aws.ts
2208
- var import_node_path21 = __toESM(require("path"), 1);
2331
+ var import_node_path22 = __toESM(require("path"), 1);
2209
2332
  var import_types12 = require("@neat.is/types");
2210
2333
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
2211
2334
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -2235,7 +2358,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2235
2358
  kind,
2236
2359
  edgeType: "CALLS",
2237
2360
  evidence: {
2238
- file: import_node_path21.default.relative(serviceDir, file.path),
2361
+ file: import_node_path22.default.relative(serviceDir, file.path),
2239
2362
  line,
2240
2363
  snippet: snippet(file.content, line)
2241
2364
  }
@@ -2259,7 +2382,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2259
2382
  }
2260
2383
 
2261
2384
  // src/extract/calls/grpc.ts
2262
- var import_node_path22 = __toESM(require("path"), 1);
2385
+ var import_node_path23 = __toESM(require("path"), 1);
2263
2386
  var import_types13 = require("@neat.is/types");
2264
2387
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
2265
2388
  function isLikelyAddress(value) {
@@ -2284,7 +2407,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
2284
2407
  kind: "grpc-service",
2285
2408
  edgeType: "CALLS",
2286
2409
  evidence: {
2287
- file: import_node_path22.default.relative(serviceDir, file.path),
2410
+ file: import_node_path23.default.relative(serviceDir, file.path),
2288
2411
  line,
2289
2412
  snippet: snippet(file.content, line)
2290
2413
  }
@@ -2360,7 +2483,7 @@ async function addCallEdges(graph, services) {
2360
2483
  }
2361
2484
 
2362
2485
  // src/extract/infra/docker-compose.ts
2363
- var import_node_path23 = __toESM(require("path"), 1);
2486
+ var import_node_path24 = __toESM(require("path"), 1);
2364
2487
  var import_types16 = require("@neat.is/types");
2365
2488
 
2366
2489
  // src/extract/infra/shared.ts
@@ -2397,7 +2520,7 @@ function dependsOnList(value) {
2397
2520
  }
2398
2521
  function serviceNameToServiceNode(name, services) {
2399
2522
  for (const s of services) {
2400
- if (s.node.name === name || import_node_path23.default.basename(s.dir) === name) return s.node.id;
2523
+ if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
2401
2524
  }
2402
2525
  return null;
2403
2526
  }
@@ -2406,16 +2529,24 @@ async function addComposeInfra(graph, scanPath, services) {
2406
2529
  let edgesAdded = 0;
2407
2530
  let composePath = null;
2408
2531
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2409
- const abs = import_node_path23.default.join(scanPath, name);
2532
+ const abs = import_node_path24.default.join(scanPath, name);
2410
2533
  if (await exists(abs)) {
2411
2534
  composePath = abs;
2412
2535
  break;
2413
2536
  }
2414
2537
  }
2415
2538
  if (!composePath) return { nodesAdded, edgesAdded };
2416
- const compose = await readYaml(composePath);
2539
+ let compose;
2540
+ try {
2541
+ compose = await readYaml(composePath);
2542
+ } catch (err) {
2543
+ console.warn(
2544
+ `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
2545
+ );
2546
+ return { nodesAdded, edgesAdded };
2547
+ }
2417
2548
  if (!compose?.services) return { nodesAdded, edgesAdded };
2418
- const evidenceFile = import_node_path23.default.relative(scanPath, composePath).split(import_node_path23.default.sep).join("/");
2549
+ const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
2419
2550
  const composeNameToNodeId = /* @__PURE__ */ new Map();
2420
2551
  for (const [composeName, svc] of Object.entries(compose.services)) {
2421
2552
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -2455,8 +2586,8 @@ async function addComposeInfra(graph, scanPath, services) {
2455
2586
  }
2456
2587
 
2457
2588
  // src/extract/infra/dockerfile.ts
2458
- var import_node_path24 = __toESM(require("path"), 1);
2459
- var import_node_fs12 = require("fs");
2589
+ var import_node_path25 = __toESM(require("path"), 1);
2590
+ var import_node_fs13 = require("fs");
2460
2591
  var import_types17 = require("@neat.is/types");
2461
2592
  function runtimeImage(content) {
2462
2593
  const lines = content.split("\n");
@@ -2476,9 +2607,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2476
2607
  let nodesAdded = 0;
2477
2608
  let edgesAdded = 0;
2478
2609
  for (const service of services) {
2479
- const dockerfilePath = import_node_path24.default.join(service.dir, "Dockerfile");
2610
+ const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
2480
2611
  if (!await exists(dockerfilePath)) continue;
2481
- const content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
2612
+ let content;
2613
+ try {
2614
+ content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
2615
+ } catch (err) {
2616
+ console.warn(
2617
+ `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
2618
+ );
2619
+ continue;
2620
+ }
2482
2621
  const image = runtimeImage(content);
2483
2622
  if (!image) continue;
2484
2623
  const node = makeInfraNode("container-image", image);
@@ -2495,7 +2634,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2495
2634
  type: import_types17.EdgeType.RUNS_ON,
2496
2635
  provenance: import_types17.Provenance.EXTRACTED,
2497
2636
  evidence: {
2498
- file: import_node_path24.default.relative(scanPath, dockerfilePath).split(import_node_path24.default.sep).join("/")
2637
+ file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
2499
2638
  }
2500
2639
  };
2501
2640
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -2506,19 +2645,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2506
2645
  }
2507
2646
 
2508
2647
  // src/extract/infra/terraform.ts
2509
- var import_node_fs13 = require("fs");
2510
- var import_node_path25 = __toESM(require("path"), 1);
2648
+ var import_node_fs14 = require("fs");
2649
+ var import_node_path26 = __toESM(require("path"), 1);
2511
2650
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
2512
2651
  async function walkTfFiles(start, depth = 0, max = 5) {
2513
2652
  if (depth > max) return [];
2514
2653
  const out = [];
2515
- const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2654
+ const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2516
2655
  for (const entry2 of entries) {
2517
2656
  if (entry2.isDirectory()) {
2518
2657
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
2519
- out.push(...await walkTfFiles(import_node_path25.default.join(start, entry2.name), depth + 1, max));
2658
+ out.push(...await walkTfFiles(import_node_path26.default.join(start, entry2.name), depth + 1, max));
2520
2659
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
2521
- out.push(import_node_path25.default.join(start, entry2.name));
2660
+ out.push(import_node_path26.default.join(start, entry2.name));
2522
2661
  }
2523
2662
  }
2524
2663
  return out;
@@ -2527,7 +2666,7 @@ async function addTerraformResources(graph, scanPath) {
2527
2666
  let nodesAdded = 0;
2528
2667
  const files = await walkTfFiles(scanPath);
2529
2668
  for (const file of files) {
2530
- const content = await import_node_fs13.promises.readFile(file, "utf8");
2669
+ const content = await import_node_fs14.promises.readFile(file, "utf8");
2531
2670
  RESOURCE_RE.lastIndex = 0;
2532
2671
  let m;
2533
2672
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -2544,8 +2683,8 @@ async function addTerraformResources(graph, scanPath) {
2544
2683
  }
2545
2684
 
2546
2685
  // src/extract/infra/k8s.ts
2547
- var import_node_fs14 = require("fs");
2548
- var import_node_path26 = __toESM(require("path"), 1);
2686
+ var import_node_fs15 = require("fs");
2687
+ var import_node_path27 = __toESM(require("path"), 1);
2549
2688
  var import_yaml3 = require("yaml");
2550
2689
  var K8S_KIND_TO_INFRA_KIND = {
2551
2690
  Service: "k8s-service",
@@ -2559,13 +2698,13 @@ var K8S_KIND_TO_INFRA_KIND = {
2559
2698
  async function walkYamlFiles2(start, depth = 0, max = 5) {
2560
2699
  if (depth > max) return [];
2561
2700
  const out = [];
2562
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2701
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2563
2702
  for (const entry2 of entries) {
2564
2703
  if (entry2.isDirectory()) {
2565
2704
  if (IGNORED_DIRS.has(entry2.name)) continue;
2566
- out.push(...await walkYamlFiles2(import_node_path26.default.join(start, entry2.name), depth + 1, max));
2567
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path26.default.extname(entry2.name))) {
2568
- out.push(import_node_path26.default.join(start, entry2.name));
2705
+ out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry2.name), depth + 1, max));
2706
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry2.name))) {
2707
+ out.push(import_node_path27.default.join(start, entry2.name));
2569
2708
  }
2570
2709
  }
2571
2710
  return out;
@@ -2574,7 +2713,7 @@ async function addK8sResources(graph, scanPath) {
2574
2713
  let nodesAdded = 0;
2575
2714
  const files = await walkYamlFiles2(scanPath);
2576
2715
  for (const file of files) {
2577
- const content = await import_node_fs14.promises.readFile(file, "utf8");
2716
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
2578
2717
  let docs;
2579
2718
  try {
2580
2719
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2620,16 +2759,27 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
2620
2759
  const phase5 = await addInfra(graph, scanPath, services);
2621
2760
  const frontiersPromoted = promoteFrontierNodes(graph);
2622
2761
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
2623
- return {
2762
+ const result = {
2624
2763
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
2625
2764
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
2626
2765
  frontiersPromoted
2627
2766
  };
2767
+ emitNeatEvent({
2768
+ type: "extraction-complete",
2769
+ project: opts.project ?? DEFAULT_PROJECT,
2770
+ payload: {
2771
+ project: opts.project ?? DEFAULT_PROJECT,
2772
+ fileCount: services.length,
2773
+ nodesAdded: result.nodesAdded,
2774
+ edgesAdded: result.edgesAdded
2775
+ }
2776
+ });
2777
+ return result;
2628
2778
  }
2629
2779
 
2630
2780
  // src/persist.ts
2631
- var import_node_fs15 = require("fs");
2632
- var import_node_path27 = __toESM(require("path"), 1);
2781
+ var import_node_fs16 = require("fs");
2782
+ var import_node_path28 = __toESM(require("path"), 1);
2633
2783
  var SCHEMA_VERSION = 2;
2634
2784
  function migrateV1ToV2(payload) {
2635
2785
  const nodes = payload.graph.nodes;
@@ -2643,7 +2793,7 @@ function migrateV1ToV2(payload) {
2643
2793
  return { ...payload, schemaVersion: 2 };
2644
2794
  }
2645
2795
  async function ensureDir(filePath) {
2646
- await import_node_fs15.promises.mkdir(import_node_path27.default.dirname(filePath), { recursive: true });
2796
+ await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
2647
2797
  }
2648
2798
  async function saveGraphToDisk(graph, outPath) {
2649
2799
  await ensureDir(outPath);
@@ -2653,13 +2803,13 @@ async function saveGraphToDisk(graph, outPath) {
2653
2803
  graph: graph.export()
2654
2804
  };
2655
2805
  const tmp = `${outPath}.tmp`;
2656
- await import_node_fs15.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
2657
- await import_node_fs15.promises.rename(tmp, outPath);
2806
+ await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
2807
+ await import_node_fs16.promises.rename(tmp, outPath);
2658
2808
  }
2659
2809
  async function loadGraphFromDisk(graph, outPath) {
2660
2810
  let raw;
2661
2811
  try {
2662
- raw = await import_node_fs15.promises.readFile(outPath, "utf8");
2812
+ raw = await import_node_fs16.promises.readFile(outPath, "utf8");
2663
2813
  } catch (err) {
2664
2814
  if (err.code === "ENOENT") return;
2665
2815
  throw err;
@@ -2711,62 +2861,62 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
2711
2861
  }
2712
2862
 
2713
2863
  // src/projects.ts
2714
- var import_node_path28 = __toESM(require("path"), 1);
2864
+ var import_node_path29 = __toESM(require("path"), 1);
2715
2865
  function pathsForProject(project, baseDir) {
2716
2866
  if (project === DEFAULT_PROJECT) {
2717
2867
  return {
2718
- snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
2719
- errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
2720
- staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
2721
- embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
2722
- policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
2868
+ snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
2869
+ errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
2870
+ staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
2871
+ embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
2872
+ policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
2723
2873
  };
2724
2874
  }
2725
2875
  return {
2726
- snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
2727
- errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
2728
- staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
2729
- embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
2730
- policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
2876
+ snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
2877
+ errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
2878
+ staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
2879
+ embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
2880
+ policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
2731
2881
  };
2732
2882
  }
2733
2883
 
2734
2884
  // src/registry.ts
2735
- var import_node_fs16 = require("fs");
2885
+ var import_node_fs17 = require("fs");
2736
2886
  var import_node_os2 = __toESM(require("os"), 1);
2737
- var import_node_path29 = __toESM(require("path"), 1);
2887
+ var import_node_path30 = __toESM(require("path"), 1);
2738
2888
  var import_types18 = require("@neat.is/types");
2739
2889
  var LOCK_TIMEOUT_MS = 5e3;
2740
2890
  var LOCK_RETRY_MS = 50;
2741
2891
  function neatHome() {
2742
2892
  const override = process.env.NEAT_HOME;
2743
- if (override && override.length > 0) return import_node_path29.default.resolve(override);
2744
- return import_node_path29.default.join(import_node_os2.default.homedir(), ".neat");
2893
+ if (override && override.length > 0) return import_node_path30.default.resolve(override);
2894
+ return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
2745
2895
  }
2746
2896
  function registryPath() {
2747
- return import_node_path29.default.join(neatHome(), "projects.json");
2897
+ return import_node_path30.default.join(neatHome(), "projects.json");
2748
2898
  }
2749
2899
  function registryLockPath() {
2750
- return import_node_path29.default.join(neatHome(), "projects.json.lock");
2900
+ return import_node_path30.default.join(neatHome(), "projects.json.lock");
2751
2901
  }
2752
2902
  async function writeAtomically(target, contents) {
2753
- await import_node_fs16.promises.mkdir(import_node_path29.default.dirname(target), { recursive: true });
2903
+ await import_node_fs17.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
2754
2904
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
2755
- const fd = await import_node_fs16.promises.open(tmp, "w");
2905
+ const fd = await import_node_fs17.promises.open(tmp, "w");
2756
2906
  try {
2757
2907
  await fd.writeFile(contents, "utf8");
2758
2908
  await fd.sync();
2759
2909
  } finally {
2760
2910
  await fd.close();
2761
2911
  }
2762
- await import_node_fs16.promises.rename(tmp, target);
2912
+ await import_node_fs17.promises.rename(tmp, target);
2763
2913
  }
2764
2914
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
2765
2915
  const deadline = Date.now() + timeoutMs;
2766
- await import_node_fs16.promises.mkdir(import_node_path29.default.dirname(lockPath), { recursive: true });
2916
+ await import_node_fs17.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
2767
2917
  while (true) {
2768
2918
  try {
2769
- const fd = await import_node_fs16.promises.open(lockPath, "wx");
2919
+ const fd = await import_node_fs17.promises.open(lockPath, "wx");
2770
2920
  await fd.close();
2771
2921
  return;
2772
2922
  } catch (err) {
@@ -2782,7 +2932,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
2782
2932
  }
2783
2933
  }
2784
2934
  async function releaseLock(lockPath) {
2785
- await import_node_fs16.promises.unlink(lockPath).catch(() => {
2935
+ await import_node_fs17.promises.unlink(lockPath).catch(() => {
2786
2936
  });
2787
2937
  }
2788
2938
  async function withLock(fn) {
@@ -2798,7 +2948,7 @@ async function readRegistry() {
2798
2948
  const file = registryPath();
2799
2949
  let raw;
2800
2950
  try {
2801
- raw = await import_node_fs16.promises.readFile(file, "utf8");
2951
+ raw = await import_node_fs17.promises.readFile(file, "utf8");
2802
2952
  } catch (err) {
2803
2953
  if (err.code === "ENOENT") {
2804
2954
  return { version: 1, projects: [] };
@@ -2838,15 +2988,15 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
2838
2988
 
2839
2989
  // src/daemon.ts
2840
2990
  function neatHomeFor(opts) {
2841
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path30.default.resolve(opts.neatHome);
2991
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path31.default.resolve(opts.neatHome);
2842
2992
  const env = process.env.NEAT_HOME;
2843
- if (env && env.length > 0) return import_node_path30.default.resolve(env);
2993
+ if (env && env.length > 0) return import_node_path31.default.resolve(env);
2844
2994
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2845
- return import_node_path30.default.join(home, ".neat");
2995
+ return import_node_path31.default.join(home, ".neat");
2846
2996
  }
2847
2997
  async function bootstrapProject(entry2) {
2848
2998
  try {
2849
- const stat = await import_node_fs17.promises.stat(entry2.path);
2999
+ const stat = await import_node_fs18.promises.stat(entry2.path);
2850
3000
  if (!stat.isDirectory()) {
2851
3001
  throw new Error(`registered path ${entry2.path} is not a directory`);
2852
3002
  }
@@ -2869,7 +3019,7 @@ async function bootstrapProject(entry2) {
2869
3019
  const graph = getGraph(entry2.name);
2870
3020
  const outPath = pathsForProject(
2871
3021
  entry2.name,
2872
- import_node_path30.default.join(entry2.path, "neat-out")
3022
+ import_node_path31.default.join(entry2.path, "neat-out")
2873
3023
  ).snapshotPath;
2874
3024
  await loadGraphFromDisk(graph, outPath);
2875
3025
  await extractFromDirectory(graph, entry2.path);
@@ -2888,13 +3038,13 @@ async function startDaemon(opts = {}) {
2888
3038
  const home = neatHomeFor(opts);
2889
3039
  const regPath = registryPath();
2890
3040
  try {
2891
- await import_node_fs17.promises.access(regPath);
3041
+ await import_node_fs18.promises.access(regPath);
2892
3042
  } catch {
2893
3043
  throw new Error(
2894
3044
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
2895
3045
  );
2896
3046
  }
2897
- const pidPath = import_node_path30.default.join(home, "neatd.pid");
3047
+ const pidPath = import_node_path31.default.join(home, "neatd.pid");
2898
3048
  await writeAtomically(pidPath, `${process.pid}
2899
3049
  `);
2900
3050
  const slots = /* @__PURE__ */ new Map();
@@ -2960,7 +3110,7 @@ async function startDaemon(opts = {}) {
2960
3110
  } catch {
2961
3111
  }
2962
3112
  }
2963
- await import_node_fs17.promises.unlink(pidPath).catch(() => {
3113
+ await import_node_fs18.promises.unlink(pidPath).catch(() => {
2964
3114
  });
2965
3115
  };
2966
3116
  return { slots, reload, stop, pidPath };
@@ -2969,14 +3119,14 @@ async function startDaemon(opts = {}) {
2969
3119
  // src/neatd.ts
2970
3120
  function neatHome2() {
2971
3121
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
2972
- return import_node_path31.default.resolve(process.env.NEAT_HOME);
3122
+ return import_node_path32.default.resolve(process.env.NEAT_HOME);
2973
3123
  }
2974
3124
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2975
- return import_node_path31.default.join(home, ".neat");
3125
+ return import_node_path32.default.join(home, ".neat");
2976
3126
  }
2977
3127
  async function readPid() {
2978
3128
  try {
2979
- const raw = await import_node_fs18.promises.readFile(import_node_path31.default.join(neatHome2(), "neatd.pid"), "utf8");
3129
+ const raw = await import_node_fs19.promises.readFile(import_node_path32.default.join(neatHome2(), "neatd.pid"), "utf8");
2980
3130
  const n = Number.parseInt(raw.trim(), 10);
2981
3131
  return Number.isFinite(n) ? n : null;
2982
3132
  } catch {