@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/index.cjs CHANGED
@@ -100,8 +100,8 @@ function reshapeGrpcRequest(req) {
100
100
  };
101
101
  }
102
102
  function resolveProtoRoot() {
103
- const here = import_node_path29.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
- return import_node_path29.default.resolve(here, "..", "proto");
103
+ const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
+ return import_node_path31.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_path29, grpc, protoLoader;
159
+ var import_node_url, import_node_path31, 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_path29 = __toESM(require("path"), 1);
165
+ import_node_path31 = __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_path30.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
- const protoRoot = import_node_path30.default.resolve(here, "..", "proto");
262
+ const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
+ const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
264
264
  const root = new import_protobufjs.default.Root();
265
- root.resolvePath = (_origin, target) => import_node_path30.default.resolve(protoRoot, target);
265
+ root.resolvePath = (_origin, target) => import_node_path32.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_path30, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
365
+ var import_node_path32, 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_path30 = __toESM(require("path"), 1);
370
+ import_node_path32 = __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);
@@ -777,6 +777,18 @@ function deprecatedApis() {
777
777
  return currentMatrix().deprecatedApis ?? [];
778
778
  }
779
779
 
780
+ // src/events.ts
781
+ init_cjs_shims();
782
+ var import_node_events = require("events");
783
+ var EVENT_BUS_CHANNEL = "event";
784
+ var NeatEventBus = class extends import_node_events.EventEmitter {
785
+ };
786
+ var eventBus = new NeatEventBus();
787
+ eventBus.setMaxListeners(0);
788
+ function emitNeatEvent(envelope) {
789
+ eventBus.emit(EVENT_BUS_CHANNEL, envelope);
790
+ }
791
+
780
792
  // src/traverse.ts
781
793
  init_cjs_shims();
782
794
  var import_types = require("@neat.is/types");
@@ -864,19 +876,19 @@ function confidenceFromMix(edges, now = Date.now()) {
864
876
  function longestIncomingWalk(graph, start, maxDepth) {
865
877
  let best = { path: [start], edges: [] };
866
878
  const visited = /* @__PURE__ */ new Set([start]);
867
- function step(node, path33, edges) {
868
- if (path33.length > best.path.length) {
869
- best = { path: [...path33], edges: [...edges] };
879
+ function step(node, path34, edges) {
880
+ if (path34.length > best.path.length) {
881
+ best = { path: [...path34], edges: [...edges] };
870
882
  }
871
- if (path33.length - 1 >= maxDepth) return;
883
+ if (path34.length - 1 >= maxDepth) return;
872
884
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
873
885
  for (const [srcId, edge] of incoming) {
874
886
  if (visited.has(srcId)) continue;
875
887
  visited.add(srcId);
876
- path33.push(srcId);
888
+ path34.push(srcId);
877
889
  edges.push(edge);
878
- step(srcId, path33, edges);
879
- path33.pop();
890
+ step(srcId, path34, edges);
891
+ path34.pop();
880
892
  edges.pop();
881
893
  visited.delete(srcId);
882
894
  }
@@ -1340,9 +1352,11 @@ async function loadPolicyFile(policyPath) {
1340
1352
  }
1341
1353
  var PolicyViolationsLog = class {
1342
1354
  path;
1355
+ project;
1343
1356
  seen = null;
1344
- constructor(logPath) {
1357
+ constructor(logPath, project = DEFAULT_PROJECT) {
1345
1358
  this.path = logPath;
1359
+ this.project = project;
1346
1360
  }
1347
1361
  async append(v) {
1348
1362
  if (!this.seen) await this.hydrate();
@@ -1350,6 +1364,11 @@ var PolicyViolationsLog = class {
1350
1364
  this.seen.add(v.id);
1351
1365
  await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
1352
1366
  await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
1367
+ emitNeatEvent({
1368
+ type: "policy-violation",
1369
+ project: this.project,
1370
+ payload: { violation: v }
1371
+ });
1353
1372
  return true;
1354
1373
  }
1355
1374
  async readAll() {
@@ -1796,6 +1815,7 @@ async function markStaleEdges(graph, options = {}) {
1796
1815
  const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
1797
1816
  const now = options.now ?? Date.now();
1798
1817
  const events = [];
1818
+ const project = options.project ?? DEFAULT_PROJECT;
1799
1819
  graph.forEachEdge((id, attrs) => {
1800
1820
  const e = attrs;
1801
1821
  if (e.provenance !== import_types3.Provenance.OBSERVED) return;
@@ -1815,6 +1835,15 @@ async function markStaleEdges(graph, options = {}) {
1815
1835
  lastObserved: e.lastObserved,
1816
1836
  transitionedAt: new Date(now).toISOString()
1817
1837
  });
1838
+ emitNeatEvent({
1839
+ type: "stale-transition",
1840
+ project,
1841
+ payload: {
1842
+ edgeId: id,
1843
+ from: import_types3.Provenance.OBSERVED,
1844
+ to: import_types3.Provenance.STALE
1845
+ }
1846
+ });
1818
1847
  }
1819
1848
  });
1820
1849
  if (options.staleEventsPath && events.length > 0) {
@@ -1845,7 +1874,8 @@ function startStalenessLoop(graph, options = {}) {
1845
1874
  try {
1846
1875
  await markStaleEdges(graph, {
1847
1876
  thresholds: options.thresholds,
1848
- staleEventsPath: options.staleEventsPath
1877
+ staleEventsPath: options.staleEventsPath,
1878
+ project: options.project
1849
1879
  });
1850
1880
  if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
1851
1881
  } catch (err) {
@@ -1872,10 +1902,10 @@ async function readErrorEvents(errorsPath) {
1872
1902
 
1873
1903
  // src/extract/services.ts
1874
1904
  init_cjs_shims();
1875
- var import_node_fs6 = require("fs");
1876
- var import_node_path6 = __toESM(require("path"), 1);
1905
+ var import_node_fs7 = require("fs");
1906
+ var import_node_path7 = __toESM(require("path"), 1);
1877
1907
  var import_ignore = __toESM(require("ignore"), 1);
1878
- var import_minimatch = require("minimatch");
1908
+ var import_minimatch2 = require("minimatch");
1879
1909
  var import_types5 = require("@neat.is/types");
1880
1910
 
1881
1911
  // src/extract/shared.ts
@@ -1988,6 +2018,73 @@ function pythonToPackage(service) {
1988
2018
  };
1989
2019
  }
1990
2020
 
2021
+ // src/extract/owners.ts
2022
+ init_cjs_shims();
2023
+ var import_node_fs6 = require("fs");
2024
+ var import_node_path6 = __toESM(require("path"), 1);
2025
+ var import_minimatch = require("minimatch");
2026
+ async function loadCodeowners(scanPath) {
2027
+ const candidates = [
2028
+ import_node_path6.default.join(scanPath, "CODEOWNERS"),
2029
+ import_node_path6.default.join(scanPath, ".github", "CODEOWNERS")
2030
+ ];
2031
+ for (const file of candidates) {
2032
+ if (await exists(file)) {
2033
+ const raw = await import_node_fs6.promises.readFile(file, "utf8");
2034
+ return parseCodeowners(raw);
2035
+ }
2036
+ }
2037
+ return null;
2038
+ }
2039
+ function parseCodeowners(raw) {
2040
+ const rules = [];
2041
+ for (const line of raw.split("\n")) {
2042
+ const trimmed = line.trim();
2043
+ if (!trimmed || trimmed.startsWith("#")) continue;
2044
+ const match = /^(\S+)\s+(.+)$/.exec(trimmed);
2045
+ if (!match) continue;
2046
+ rules.push({ pattern: match[1], owners: match[2].trim() });
2047
+ }
2048
+ return { rules };
2049
+ }
2050
+ function matchOwner(file, repoPath) {
2051
+ const normalized = repoPath.split(import_node_path6.default.sep).join("/");
2052
+ for (const rule of file.rules) {
2053
+ if (matchesPattern(rule.pattern, normalized)) return rule.owners;
2054
+ }
2055
+ return null;
2056
+ }
2057
+ function matchesPattern(rawPattern, repoPath) {
2058
+ let pattern = rawPattern.startsWith("/") ? rawPattern.slice(1) : rawPattern;
2059
+ if (pattern === "*") return !repoPath.includes("/");
2060
+ if (pattern === "**" || pattern === "") return true;
2061
+ if (pattern.endsWith("/")) pattern = pattern + "**";
2062
+ if ((0, import_minimatch.minimatch)(repoPath, pattern, { dot: true })) return true;
2063
+ if (!pattern.includes("*") && (0, import_minimatch.minimatch)(repoPath, pattern + "/**", { dot: true })) return true;
2064
+ return false;
2065
+ }
2066
+ async function readPackageJsonAuthor(serviceDir) {
2067
+ const pkgPath = import_node_path6.default.join(serviceDir, "package.json");
2068
+ if (!await exists(pkgPath)) return null;
2069
+ try {
2070
+ const pkg = await readJson(pkgPath);
2071
+ if (!pkg.author) return null;
2072
+ if (typeof pkg.author === "string") return pkg.author;
2073
+ if (typeof pkg.author === "object" && typeof pkg.author.name === "string") return pkg.author.name;
2074
+ return null;
2075
+ } catch {
2076
+ return null;
2077
+ }
2078
+ }
2079
+ async function computeServiceOwner(codeowners, repoPath, serviceDir) {
2080
+ if (codeowners && repoPath !== void 0) {
2081
+ const owner = matchOwner(codeowners, repoPath);
2082
+ if (owner) return owner;
2083
+ }
2084
+ const author = await readPackageJsonAuthor(serviceDir);
2085
+ return author ?? void 0;
2086
+ }
2087
+
1991
2088
  // src/extract/services.ts
1992
2089
  var DEFAULT_SCAN_DEPTH = 5;
1993
2090
  function parseScanDepth() {
@@ -2004,21 +2101,21 @@ function workspaceGlobs(pkg) {
2004
2101
  return null;
2005
2102
  }
2006
2103
  async function loadGitignore(scanPath) {
2007
- const gitignorePath = import_node_path6.default.join(scanPath, ".gitignore");
2104
+ const gitignorePath = import_node_path7.default.join(scanPath, ".gitignore");
2008
2105
  if (!await exists(gitignorePath)) return null;
2009
- const raw = await import_node_fs6.promises.readFile(gitignorePath, "utf8");
2106
+ const raw = await import_node_fs7.promises.readFile(gitignorePath, "utf8");
2010
2107
  return (0, import_ignore.default)().add(raw);
2011
2108
  }
2012
2109
  async function walkDirs(start, scanPath, options, visit) {
2013
2110
  async function recurse(current, depth) {
2014
2111
  if (depth > options.maxDepth) return;
2015
- const entries = await import_node_fs6.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2112
+ const entries = await import_node_fs7.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2016
2113
  for (const entry of entries) {
2017
2114
  if (!entry.isDirectory()) continue;
2018
2115
  if (IGNORED_DIRS.has(entry.name)) continue;
2019
- const child = import_node_path6.default.join(current, entry.name);
2116
+ const child = import_node_path7.default.join(current, entry.name);
2020
2117
  if (options.ig) {
2021
- const rel = import_node_path6.default.relative(scanPath, child).split(import_node_path6.default.sep).join("/");
2118
+ const rel = import_node_path7.default.relative(scanPath, child).split(import_node_path7.default.sep).join("/");
2022
2119
  if (rel && options.ig.ignores(rel + "/")) continue;
2023
2120
  }
2024
2121
  await visit(child);
@@ -2033,8 +2130,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2033
2130
  for (const raw of globs) {
2034
2131
  const pattern = raw.replace(/^\.\//, "");
2035
2132
  if (!pattern.includes("*")) {
2036
- const candidate = import_node_path6.default.join(scanPath, pattern);
2037
- if (await exists(import_node_path6.default.join(candidate, "package.json"))) found.add(candidate);
2133
+ const candidate = import_node_path7.default.join(scanPath, pattern);
2134
+ if (await exists(import_node_path7.default.join(candidate, "package.json"))) found.add(candidate);
2038
2135
  continue;
2039
2136
  }
2040
2137
  const segments = pattern.split("/");
@@ -2043,13 +2140,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2043
2140
  if (seg.includes("*")) break;
2044
2141
  staticSegments.push(seg);
2045
2142
  }
2046
- const start = import_node_path6.default.join(scanPath, ...staticSegments);
2143
+ const start = import_node_path7.default.join(scanPath, ...staticSegments);
2047
2144
  if (!await exists(start)) continue;
2048
2145
  const hasDoubleStar = pattern.includes("**");
2049
2146
  const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
2050
2147
  await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
2051
- const rel = import_node_path6.default.relative(scanPath, dir).split(import_node_path6.default.sep).join("/");
2052
- if ((0, import_minimatch.minimatch)(rel, pattern) && await exists(import_node_path6.default.join(dir, "package.json"))) {
2148
+ const rel = import_node_path7.default.relative(scanPath, dir).split(import_node_path7.default.sep).join("/");
2149
+ if ((0, import_minimatch2.minimatch)(rel, pattern) && await exists(import_node_path7.default.join(dir, "package.json"))) {
2053
2150
  found.add(dir);
2054
2151
  }
2055
2152
  });
@@ -2057,9 +2154,17 @@ async function expandWorkspaceGlobs(scanPath, globs) {
2057
2154
  return [...found];
2058
2155
  }
2059
2156
  async function discoverNodeService(scanPath, dir) {
2060
- const pkgPath = import_node_path6.default.join(dir, "package.json");
2157
+ const pkgPath = import_node_path7.default.join(dir, "package.json");
2061
2158
  if (!await exists(pkgPath)) return null;
2062
- const pkg = await readJson(pkgPath);
2159
+ let pkg;
2160
+ try {
2161
+ pkg = await readJson(pkgPath);
2162
+ } catch (err) {
2163
+ console.warn(
2164
+ `[neat] services skipped ${import_node_path7.default.relative(scanPath, pkgPath)}: ${err.message}`
2165
+ );
2166
+ return null;
2167
+ }
2063
2168
  if (!pkg.name) return null;
2064
2169
  const node = {
2065
2170
  id: (0, import_types5.serviceId)(pkg.name),
@@ -2068,7 +2173,7 @@ async function discoverNodeService(scanPath, dir) {
2068
2173
  language: "javascript",
2069
2174
  version: pkg.version,
2070
2175
  dependencies: pkg.dependencies ?? {},
2071
- repoPath: import_node_path6.default.relative(scanPath, dir),
2176
+ repoPath: import_node_path7.default.relative(scanPath, dir),
2072
2177
  ...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
2073
2178
  };
2074
2179
  return { pkg, dir, node };
@@ -2084,13 +2189,22 @@ async function discoverPyService(scanPath, dir) {
2084
2189
  language: "python",
2085
2190
  version: py.version,
2086
2191
  dependencies: py.dependencies,
2087
- repoPath: import_node_path6.default.relative(scanPath, dir)
2192
+ repoPath: import_node_path7.default.relative(scanPath, dir)
2088
2193
  };
2089
2194
  return { pkg, dir, node };
2090
2195
  }
2091
2196
  async function discoverServices(scanPath) {
2092
- const rootPkgPath = import_node_path6.default.join(scanPath, "package.json");
2093
- const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
2197
+ const rootPkgPath = import_node_path7.default.join(scanPath, "package.json");
2198
+ let rootPkg = null;
2199
+ if (await exists(rootPkgPath)) {
2200
+ try {
2201
+ rootPkg = await readJson(rootPkgPath);
2202
+ } catch (err) {
2203
+ console.warn(
2204
+ `[neat] services workspaces skipped ${import_node_path7.default.relative(scanPath, rootPkgPath)}: ${err.message}`
2205
+ );
2206
+ }
2207
+ }
2094
2208
  const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
2095
2209
  const candidateDirs = [];
2096
2210
  if (wsGlobs) {
@@ -2103,9 +2217,9 @@ async function discoverServices(scanPath) {
2103
2217
  scanPath,
2104
2218
  { maxDepth: parseScanDepth(), ig },
2105
2219
  async (dir) => {
2106
- if (await exists(import_node_path6.default.join(dir, "package.json"))) {
2220
+ if (await exists(import_node_path7.default.join(dir, "package.json"))) {
2107
2221
  candidateDirs.push(dir);
2108
- } 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"))) {
2222
+ } 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"))) {
2109
2223
  candidateDirs.push(dir);
2110
2224
  }
2111
2225
  }
@@ -2119,8 +2233,8 @@ async function discoverServices(scanPath) {
2119
2233
  if (!service) continue;
2120
2234
  const existingDir = seen.get(service.node.name);
2121
2235
  if (existingDir !== void 0) {
2122
- const a = import_node_path6.default.relative(scanPath, existingDir) || ".";
2123
- const b = import_node_path6.default.relative(scanPath, dir) || ".";
2236
+ const a = import_node_path7.default.relative(scanPath, existingDir) || ".";
2237
+ const b = import_node_path7.default.relative(scanPath, dir) || ".";
2124
2238
  console.warn(
2125
2239
  `[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
2126
2240
  );
@@ -2129,6 +2243,11 @@ async function discoverServices(scanPath) {
2129
2243
  seen.set(service.node.name, dir);
2130
2244
  out.push(service);
2131
2245
  }
2246
+ const codeowners = await loadCodeowners(scanPath);
2247
+ for (const service of out) {
2248
+ const owner = await computeServiceOwner(codeowners, service.node.repoPath, service.dir);
2249
+ if (owner !== void 0) service.node.owner = owner;
2250
+ }
2132
2251
  return out;
2133
2252
  }
2134
2253
  function addServiceNodes(graph, services) {
@@ -2152,8 +2271,8 @@ function addServiceNodes(graph, services) {
2152
2271
 
2153
2272
  // src/extract/aliases.ts
2154
2273
  init_cjs_shims();
2155
- var import_node_path7 = __toESM(require("path"), 1);
2156
- var import_node_fs7 = require("fs");
2274
+ var import_node_path8 = __toESM(require("path"), 1);
2275
+ var import_node_fs8 = require("fs");
2157
2276
  var import_yaml2 = require("yaml");
2158
2277
  var import_types6 = require("@neat.is/types");
2159
2278
  var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
@@ -2180,21 +2299,29 @@ function indexServicesByName(services) {
2180
2299
  const map = /* @__PURE__ */ new Map();
2181
2300
  for (const s of services) {
2182
2301
  map.set(s.node.name, s.node.id);
2183
- map.set(import_node_path7.default.basename(s.dir), s.node.id);
2302
+ map.set(import_node_path8.default.basename(s.dir), s.node.id);
2184
2303
  }
2185
2304
  return map;
2186
2305
  }
2187
2306
  async function collectComposeAliases(graph, scanPath, serviceIndex) {
2188
2307
  let composePath = null;
2189
2308
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2190
- const abs = import_node_path7.default.join(scanPath, name);
2309
+ const abs = import_node_path8.default.join(scanPath, name);
2191
2310
  if (await exists(abs)) {
2192
2311
  composePath = abs;
2193
2312
  break;
2194
2313
  }
2195
2314
  }
2196
2315
  if (!composePath) return;
2197
- const compose = await readYaml(composePath);
2316
+ let compose;
2317
+ try {
2318
+ compose = await readYaml(composePath);
2319
+ } catch (err) {
2320
+ console.warn(
2321
+ `[neat] aliases compose skipped ${import_node_path8.default.relative(scanPath, composePath)}: ${err.message}`
2322
+ );
2323
+ return;
2324
+ }
2198
2325
  if (!compose?.services) return;
2199
2326
  for (const [composeName, svc] of Object.entries(compose.services)) {
2200
2327
  const serviceId3 = serviceIndex.get(composeName);
@@ -2233,9 +2360,17 @@ function parseDockerfileLabels(content) {
2233
2360
  }
2234
2361
  async function collectDockerfileAliases(graph, services) {
2235
2362
  for (const service of services) {
2236
- const dockerfilePath = import_node_path7.default.join(service.dir, "Dockerfile");
2363
+ const dockerfilePath = import_node_path8.default.join(service.dir, "Dockerfile");
2237
2364
  if (!await exists(dockerfilePath)) continue;
2238
- const content = await import_node_fs7.promises.readFile(dockerfilePath, "utf8");
2365
+ let content;
2366
+ try {
2367
+ content = await import_node_fs8.promises.readFile(dockerfilePath, "utf8");
2368
+ } catch (err) {
2369
+ console.warn(
2370
+ `[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
2371
+ );
2372
+ continue;
2373
+ }
2239
2374
  const aliases = parseDockerfileLabels(content);
2240
2375
  if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
2241
2376
  }
@@ -2243,13 +2378,13 @@ async function collectDockerfileAliases(graph, services) {
2243
2378
  async function walkYamlFiles(start, depth = 0, max = 5) {
2244
2379
  if (depth > max) return [];
2245
2380
  const out = [];
2246
- const entries = await import_node_fs7.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2381
+ const entries = await import_node_fs8.promises.readdir(start, { withFileTypes: true }).catch(() => []);
2247
2382
  for (const entry of entries) {
2248
2383
  if (entry.isDirectory()) {
2249
2384
  if (IGNORED_DIRS.has(entry.name)) continue;
2250
- out.push(...await walkYamlFiles(import_node_path7.default.join(start, entry.name), depth + 1, max));
2251
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path7.default.extname(entry.name))) {
2252
- out.push(import_node_path7.default.join(start, entry.name));
2385
+ out.push(...await walkYamlFiles(import_node_path8.default.join(start, entry.name), depth + 1, max));
2386
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path8.default.extname(entry.name))) {
2387
+ out.push(import_node_path8.default.join(start, entry.name));
2253
2388
  }
2254
2389
  }
2255
2390
  return out;
@@ -2276,7 +2411,7 @@ function k8sServiceTarget(doc, byName) {
2276
2411
  async function collectK8sAliases(graph, scanPath, serviceIndex) {
2277
2412
  const files = await walkYamlFiles(scanPath);
2278
2413
  for (const file of files) {
2279
- const content = await import_node_fs7.promises.readFile(file, "utf8");
2414
+ const content = await import_node_fs8.promises.readFile(file, "utf8");
2280
2415
  let docs;
2281
2416
  try {
2282
2417
  docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -2301,14 +2436,14 @@ async function addServiceAliases(graph, scanPath, services) {
2301
2436
 
2302
2437
  // src/extract/databases/index.ts
2303
2438
  init_cjs_shims();
2304
- var import_node_path15 = __toESM(require("path"), 1);
2439
+ var import_node_path16 = __toESM(require("path"), 1);
2305
2440
  var import_types7 = require("@neat.is/types");
2306
2441
 
2307
2442
  // src/extract/databases/db-config-yaml.ts
2308
2443
  init_cjs_shims();
2309
- var import_node_path8 = __toESM(require("path"), 1);
2444
+ var import_node_path9 = __toESM(require("path"), 1);
2310
2445
  async function parse(serviceDir) {
2311
- const yamlPath = import_node_path8.default.join(serviceDir, "db-config.yaml");
2446
+ const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
2312
2447
  if (!await exists(yamlPath)) return [];
2313
2448
  const raw = await readYaml(yamlPath);
2314
2449
  return [
@@ -2326,13 +2461,13 @@ var dbConfigYamlParser = { name: "db-config.yaml", parse };
2326
2461
 
2327
2462
  // src/extract/databases/dotenv.ts
2328
2463
  init_cjs_shims();
2329
- var import_node_fs9 = require("fs");
2330
- var import_node_path10 = __toESM(require("path"), 1);
2464
+ var import_node_fs10 = require("fs");
2465
+ var import_node_path11 = __toESM(require("path"), 1);
2331
2466
 
2332
2467
  // src/extract/databases/shared.ts
2333
2468
  init_cjs_shims();
2334
- var import_node_fs8 = require("fs");
2335
- var import_node_path9 = __toESM(require("path"), 1);
2469
+ var import_node_fs9 = require("fs");
2470
+ var import_node_path10 = __toESM(require("path"), 1);
2336
2471
  function schemeToEngine(scheme) {
2337
2472
  const s = scheme.toLowerCase().split("+")[0];
2338
2473
  switch (s) {
@@ -2371,14 +2506,14 @@ function parseConnectionString(url) {
2371
2506
  }
2372
2507
  async function readIfExists(filePath) {
2373
2508
  try {
2374
- return await import_node_fs8.promises.readFile(filePath, "utf8");
2509
+ return await import_node_fs9.promises.readFile(filePath, "utf8");
2375
2510
  } catch {
2376
2511
  return null;
2377
2512
  }
2378
2513
  }
2379
2514
  async function findFirst(serviceDir, candidates) {
2380
2515
  for (const rel of candidates) {
2381
- const abs = import_node_path9.default.join(serviceDir, rel);
2516
+ const abs = import_node_path10.default.join(serviceDir, rel);
2382
2517
  const content = await readIfExists(abs);
2383
2518
  if (content !== null) return abs;
2384
2519
  }
@@ -2429,15 +2564,15 @@ function parseDotenvLine(line) {
2429
2564
  return { key, value };
2430
2565
  }
2431
2566
  async function parse2(serviceDir) {
2432
- const entries = await import_node_fs9.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2567
+ const entries = await import_node_fs10.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
2433
2568
  const configs = [];
2434
2569
  const seen = /* @__PURE__ */ new Set();
2435
2570
  for (const entry of entries) {
2436
2571
  if (!entry.isFile()) continue;
2437
2572
  const match = isConfigFile(entry.name);
2438
2573
  if (!match.match || match.fileType !== "env") continue;
2439
- const filePath = import_node_path10.default.join(serviceDir, entry.name);
2440
- const content = await import_node_fs9.promises.readFile(filePath, "utf8");
2574
+ const filePath = import_node_path11.default.join(serviceDir, entry.name);
2575
+ const content = await import_node_fs10.promises.readFile(filePath, "utf8");
2441
2576
  for (const line of content.split("\n")) {
2442
2577
  const parsed = parseDotenvLine(line);
2443
2578
  if (!parsed) continue;
@@ -2456,9 +2591,9 @@ var dotenvParser = { name: ".env", parse: parse2 };
2456
2591
 
2457
2592
  // src/extract/databases/prisma.ts
2458
2593
  init_cjs_shims();
2459
- var import_node_path11 = __toESM(require("path"), 1);
2594
+ var import_node_path12 = __toESM(require("path"), 1);
2460
2595
  async function parse3(serviceDir) {
2461
- const schemaPath = import_node_path11.default.join(serviceDir, "prisma", "schema.prisma");
2596
+ const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
2462
2597
  const content = await readIfExists(schemaPath);
2463
2598
  if (!content) return [];
2464
2599
  const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
@@ -2590,10 +2725,10 @@ var knexParser = { name: "knex", parse: parse5 };
2590
2725
 
2591
2726
  // src/extract/databases/ormconfig.ts
2592
2727
  init_cjs_shims();
2593
- var import_node_path12 = __toESM(require("path"), 1);
2728
+ var import_node_path13 = __toESM(require("path"), 1);
2594
2729
  async function parse6(serviceDir) {
2595
2730
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
2596
- const abs = import_node_path12.default.join(serviceDir, candidate);
2731
+ const abs = import_node_path13.default.join(serviceDir, candidate);
2597
2732
  if (!await exists(abs)) continue;
2598
2733
  const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
2599
2734
  const entries = Array.isArray(raw) ? raw : [raw];
@@ -2653,9 +2788,9 @@ var typeormParser = { name: "typeorm", parse: parse7 };
2653
2788
 
2654
2789
  // src/extract/databases/sequelize.ts
2655
2790
  init_cjs_shims();
2656
- var import_node_path13 = __toESM(require("path"), 1);
2791
+ var import_node_path14 = __toESM(require("path"), 1);
2657
2792
  async function parse8(serviceDir) {
2658
- const configPath = import_node_path13.default.join(serviceDir, "config", "config.json");
2793
+ const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
2659
2794
  if (!await exists(configPath)) return [];
2660
2795
  const raw = await readJson(configPath);
2661
2796
  const out = [];
@@ -2682,7 +2817,7 @@ var sequelizeParser = { name: "sequelize", parse: parse8 };
2682
2817
 
2683
2818
  // src/extract/databases/docker-compose.ts
2684
2819
  init_cjs_shims();
2685
- var import_node_path14 = __toESM(require("path"), 1);
2820
+ var import_node_path15 = __toESM(require("path"), 1);
2686
2821
  function portFromService(svc) {
2687
2822
  for (const raw of svc.ports ?? []) {
2688
2823
  const str = String(raw);
@@ -2709,7 +2844,7 @@ function databaseFromEnv(svc) {
2709
2844
  }
2710
2845
  async function parse9(serviceDir) {
2711
2846
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
2712
- const abs = import_node_path14.default.join(serviceDir, name);
2847
+ const abs = import_node_path15.default.join(serviceDir, name);
2713
2848
  if (!await exists(abs)) continue;
2714
2849
  const raw = await readYaml(abs);
2715
2850
  if (!raw?.services) return [];
@@ -2889,7 +3024,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2889
3024
  provenance: import_types7.Provenance.EXTRACTED,
2890
3025
  ...config.sourceFile ? {
2891
3026
  evidence: {
2892
- file: import_node_path15.default.relative(scanPath, config.sourceFile).split(import_node_path15.default.sep).join("/")
3027
+ file: import_node_path16.default.relative(scanPath, config.sourceFile).split(import_node_path16.default.sep).join("/")
2893
3028
  }
2894
3029
  } : {}
2895
3030
  };
@@ -2917,15 +3052,15 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2917
3052
 
2918
3053
  // src/extract/configs.ts
2919
3054
  init_cjs_shims();
2920
- var import_node_fs10 = require("fs");
2921
- var import_node_path16 = __toESM(require("path"), 1);
3055
+ var import_node_fs11 = require("fs");
3056
+ var import_node_path17 = __toESM(require("path"), 1);
2922
3057
  var import_types8 = require("@neat.is/types");
2923
3058
  async function walkConfigFiles(dir) {
2924
3059
  const out = [];
2925
3060
  async function walk(current) {
2926
- const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true });
3061
+ const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true });
2927
3062
  for (const entry of entries) {
2928
- const full = import_node_path16.default.join(current, entry.name);
3063
+ const full = import_node_path17.default.join(current, entry.name);
2929
3064
  if (entry.isDirectory()) {
2930
3065
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2931
3066
  } else if (entry.isFile() && isConfigFile(entry.name).match) {
@@ -2942,13 +3077,13 @@ async function addConfigNodes(graph, services, scanPath) {
2942
3077
  for (const service of services) {
2943
3078
  const configFiles = await walkConfigFiles(service.dir);
2944
3079
  for (const file of configFiles) {
2945
- const relPath = import_node_path16.default.relative(scanPath, file);
3080
+ const relPath = import_node_path17.default.relative(scanPath, file);
2946
3081
  const node = {
2947
3082
  id: (0, import_types8.configId)(relPath),
2948
3083
  type: import_types8.NodeType.ConfigNode,
2949
- name: import_node_path16.default.basename(file),
3084
+ name: import_node_path17.default.basename(file),
2950
3085
  path: relPath,
2951
- fileType: isConfigFile(import_node_path16.default.basename(file)).fileType
3086
+ fileType: isConfigFile(import_node_path17.default.basename(file)).fileType
2952
3087
  };
2953
3088
  if (!graph.hasNode(node.id)) {
2954
3089
  graph.addNode(node.id, node);
@@ -2960,7 +3095,7 @@ async function addConfigNodes(graph, services, scanPath) {
2960
3095
  target: node.id,
2961
3096
  type: import_types8.EdgeType.CONFIGURED_BY,
2962
3097
  provenance: import_types8.Provenance.EXTRACTED,
2963
- evidence: { file: relPath.split(import_node_path16.default.sep).join("/") }
3098
+ evidence: { file: relPath.split(import_node_path17.default.sep).join("/") }
2964
3099
  };
2965
3100
  if (!graph.hasEdge(edge.id)) {
2966
3101
  graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
@@ -2977,7 +3112,7 @@ var import_types14 = require("@neat.is/types");
2977
3112
 
2978
3113
  // src/extract/calls/http.ts
2979
3114
  init_cjs_shims();
2980
- var import_node_path18 = __toESM(require("path"), 1);
3115
+ var import_node_path19 = __toESM(require("path"), 1);
2981
3116
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2982
3117
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
2983
3118
  var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
@@ -2985,17 +3120,17 @@ var import_types9 = require("@neat.is/types");
2985
3120
 
2986
3121
  // src/extract/calls/shared.ts
2987
3122
  init_cjs_shims();
2988
- var import_node_fs11 = require("fs");
2989
- var import_node_path17 = __toESM(require("path"), 1);
3123
+ var import_node_fs12 = require("fs");
3124
+ var import_node_path18 = __toESM(require("path"), 1);
2990
3125
  async function walkSourceFiles(dir) {
2991
3126
  const out = [];
2992
3127
  async function walk(current) {
2993
- const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
3128
+ const entries = await import_node_fs12.promises.readdir(current, { withFileTypes: true }).catch(() => []);
2994
3129
  for (const entry of entries) {
2995
- const full = import_node_path17.default.join(current, entry.name);
3130
+ const full = import_node_path18.default.join(current, entry.name);
2996
3131
  if (entry.isDirectory()) {
2997
3132
  if (!IGNORED_DIRS.has(entry.name)) await walk(full);
2998
- } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path17.default.extname(entry.name))) {
3133
+ } else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path18.default.extname(entry.name))) {
2999
3134
  out.push(full);
3000
3135
  }
3001
3136
  }
@@ -3008,7 +3143,7 @@ async function loadSourceFiles(dir) {
3008
3143
  const out = [];
3009
3144
  for (const p of paths) {
3010
3145
  try {
3011
- const content = await import_node_fs11.promises.readFile(p, "utf8");
3146
+ const content = await import_node_fs12.promises.readFile(p, "utf8");
3012
3147
  out.push({ path: p, content });
3013
3148
  } catch {
3014
3149
  }
@@ -3064,9 +3199,9 @@ async function addHttpCallEdges(graph, services) {
3064
3199
  const knownHosts = /* @__PURE__ */ new Set();
3065
3200
  const hostToNodeId = /* @__PURE__ */ new Map();
3066
3201
  for (const service of services) {
3067
- knownHosts.add(import_node_path18.default.basename(service.dir));
3202
+ knownHosts.add(import_node_path19.default.basename(service.dir));
3068
3203
  knownHosts.add(service.pkg.name);
3069
- hostToNodeId.set(import_node_path18.default.basename(service.dir), service.node.id);
3204
+ hostToNodeId.set(import_node_path19.default.basename(service.dir), service.node.id);
3070
3205
  hostToNodeId.set(service.pkg.name, service.node.id);
3071
3206
  }
3072
3207
  let edgesAdded = 0;
@@ -3074,8 +3209,16 @@ async function addHttpCallEdges(graph, services) {
3074
3209
  const files = await loadSourceFiles(service.dir);
3075
3210
  const seenTargets = /* @__PURE__ */ new Map();
3076
3211
  for (const file of files) {
3077
- const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
3078
- const targets = callsFromSource(file.content, parser, knownHosts);
3212
+ const parser = import_node_path19.default.extname(file.path) === ".py" ? pyParser : jsParser;
3213
+ let targets;
3214
+ try {
3215
+ targets = callsFromSource(file.content, parser, knownHosts);
3216
+ } catch (err) {
3217
+ console.warn(
3218
+ `[neat] http call extraction skipped ${file.path}: ${err.message}`
3219
+ );
3220
+ continue;
3221
+ }
3079
3222
  for (const t of targets) {
3080
3223
  const targetId = hostToNodeId.get(t);
3081
3224
  if (!targetId || targetId === service.node.id) continue;
@@ -3094,7 +3237,7 @@ async function addHttpCallEdges(graph, services) {
3094
3237
  type: import_types9.EdgeType.CALLS,
3095
3238
  provenance: import_types9.Provenance.EXTRACTED,
3096
3239
  evidence: {
3097
- file: import_node_path18.default.relative(service.dir, evidenceFile.file),
3240
+ file: import_node_path19.default.relative(service.dir, evidenceFile.file),
3098
3241
  line,
3099
3242
  snippet: snippet(fileContent, line)
3100
3243
  }
@@ -3110,7 +3253,7 @@ async function addHttpCallEdges(graph, services) {
3110
3253
 
3111
3254
  // src/extract/calls/kafka.ts
3112
3255
  init_cjs_shims();
3113
- var import_node_path19 = __toESM(require("path"), 1);
3256
+ var import_node_path20 = __toESM(require("path"), 1);
3114
3257
  var import_types10 = require("@neat.is/types");
3115
3258
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
3116
3259
  var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
@@ -3137,7 +3280,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3137
3280
  kind: "kafka-topic",
3138
3281
  edgeType,
3139
3282
  evidence: {
3140
- file: import_node_path19.default.relative(serviceDir, file.path),
3283
+ file: import_node_path20.default.relative(serviceDir, file.path),
3141
3284
  line,
3142
3285
  snippet: snippet(file.content, line)
3143
3286
  }
@@ -3150,7 +3293,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
3150
3293
 
3151
3294
  // src/extract/calls/redis.ts
3152
3295
  init_cjs_shims();
3153
- var import_node_path20 = __toESM(require("path"), 1);
3296
+ var import_node_path21 = __toESM(require("path"), 1);
3154
3297
  var import_types11 = require("@neat.is/types");
3155
3298
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
3156
3299
  function redisEndpointsFromFile(file, serviceDir) {
@@ -3169,7 +3312,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3169
3312
  kind: "redis",
3170
3313
  edgeType: "CALLS",
3171
3314
  evidence: {
3172
- file: import_node_path20.default.relative(serviceDir, file.path),
3315
+ file: import_node_path21.default.relative(serviceDir, file.path),
3173
3316
  line,
3174
3317
  snippet: snippet(file.content, line)
3175
3318
  }
@@ -3180,7 +3323,7 @@ function redisEndpointsFromFile(file, serviceDir) {
3180
3323
 
3181
3324
  // src/extract/calls/aws.ts
3182
3325
  init_cjs_shims();
3183
- var import_node_path21 = __toESM(require("path"), 1);
3326
+ var import_node_path22 = __toESM(require("path"), 1);
3184
3327
  var import_types12 = require("@neat.is/types");
3185
3328
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
3186
3329
  var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -3210,7 +3353,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3210
3353
  kind,
3211
3354
  edgeType: "CALLS",
3212
3355
  evidence: {
3213
- file: import_node_path21.default.relative(serviceDir, file.path),
3356
+ file: import_node_path22.default.relative(serviceDir, file.path),
3214
3357
  line,
3215
3358
  snippet: snippet(file.content, line)
3216
3359
  }
@@ -3235,7 +3378,7 @@ function awsEndpointsFromFile(file, serviceDir) {
3235
3378
 
3236
3379
  // src/extract/calls/grpc.ts
3237
3380
  init_cjs_shims();
3238
- var import_node_path22 = __toESM(require("path"), 1);
3381
+ var import_node_path23 = __toESM(require("path"), 1);
3239
3382
  var import_types13 = require("@neat.is/types");
3240
3383
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
3241
3384
  function isLikelyAddress(value) {
@@ -3260,7 +3403,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
3260
3403
  kind: "grpc-service",
3261
3404
  edgeType: "CALLS",
3262
3405
  evidence: {
3263
- file: import_node_path22.default.relative(serviceDir, file.path),
3406
+ file: import_node_path23.default.relative(serviceDir, file.path),
3264
3407
  line,
3265
3408
  snippet: snippet(file.content, line)
3266
3409
  }
@@ -3340,7 +3483,7 @@ init_cjs_shims();
3340
3483
 
3341
3484
  // src/extract/infra/docker-compose.ts
3342
3485
  init_cjs_shims();
3343
- var import_node_path23 = __toESM(require("path"), 1);
3486
+ var import_node_path24 = __toESM(require("path"), 1);
3344
3487
  var import_types16 = require("@neat.is/types");
3345
3488
 
3346
3489
  // src/extract/infra/shared.ts
@@ -3378,7 +3521,7 @@ function dependsOnList(value) {
3378
3521
  }
3379
3522
  function serviceNameToServiceNode(name, services) {
3380
3523
  for (const s of services) {
3381
- if (s.node.name === name || import_node_path23.default.basename(s.dir) === name) return s.node.id;
3524
+ if (s.node.name === name || import_node_path24.default.basename(s.dir) === name) return s.node.id;
3382
3525
  }
3383
3526
  return null;
3384
3527
  }
@@ -3387,16 +3530,24 @@ async function addComposeInfra(graph, scanPath, services) {
3387
3530
  let edgesAdded = 0;
3388
3531
  let composePath = null;
3389
3532
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
3390
- const abs = import_node_path23.default.join(scanPath, name);
3533
+ const abs = import_node_path24.default.join(scanPath, name);
3391
3534
  if (await exists(abs)) {
3392
3535
  composePath = abs;
3393
3536
  break;
3394
3537
  }
3395
3538
  }
3396
3539
  if (!composePath) return { nodesAdded, edgesAdded };
3397
- const compose = await readYaml(composePath);
3540
+ let compose;
3541
+ try {
3542
+ compose = await readYaml(composePath);
3543
+ } catch (err) {
3544
+ console.warn(
3545
+ `[neat] infra docker-compose skipped ${import_node_path24.default.relative(scanPath, composePath)}: ${err.message}`
3546
+ );
3547
+ return { nodesAdded, edgesAdded };
3548
+ }
3398
3549
  if (!compose?.services) return { nodesAdded, edgesAdded };
3399
- const evidenceFile = import_node_path23.default.relative(scanPath, composePath).split(import_node_path23.default.sep).join("/");
3550
+ const evidenceFile = import_node_path24.default.relative(scanPath, composePath).split(import_node_path24.default.sep).join("/");
3400
3551
  const composeNameToNodeId = /* @__PURE__ */ new Map();
3401
3552
  for (const [composeName, svc] of Object.entries(compose.services)) {
3402
3553
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -3437,8 +3588,8 @@ async function addComposeInfra(graph, scanPath, services) {
3437
3588
 
3438
3589
  // src/extract/infra/dockerfile.ts
3439
3590
  init_cjs_shims();
3440
- var import_node_path24 = __toESM(require("path"), 1);
3441
- var import_node_fs12 = require("fs");
3591
+ var import_node_path25 = __toESM(require("path"), 1);
3592
+ var import_node_fs13 = require("fs");
3442
3593
  var import_types17 = require("@neat.is/types");
3443
3594
  function runtimeImage(content) {
3444
3595
  const lines = content.split("\n");
@@ -3458,9 +3609,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3458
3609
  let nodesAdded = 0;
3459
3610
  let edgesAdded = 0;
3460
3611
  for (const service of services) {
3461
- const dockerfilePath = import_node_path24.default.join(service.dir, "Dockerfile");
3612
+ const dockerfilePath = import_node_path25.default.join(service.dir, "Dockerfile");
3462
3613
  if (!await exists(dockerfilePath)) continue;
3463
- const content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
3614
+ let content;
3615
+ try {
3616
+ content = await import_node_fs13.promises.readFile(dockerfilePath, "utf8");
3617
+ } catch (err) {
3618
+ console.warn(
3619
+ `[neat] infra dockerfile skipped ${import_node_path25.default.relative(scanPath, dockerfilePath)}: ${err.message}`
3620
+ );
3621
+ continue;
3622
+ }
3464
3623
  const image = runtimeImage(content);
3465
3624
  if (!image) continue;
3466
3625
  const node = makeInfraNode("container-image", image);
@@ -3477,7 +3636,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3477
3636
  type: import_types17.EdgeType.RUNS_ON,
3478
3637
  provenance: import_types17.Provenance.EXTRACTED,
3479
3638
  evidence: {
3480
- file: import_node_path24.default.relative(scanPath, dockerfilePath).split(import_node_path24.default.sep).join("/")
3639
+ file: import_node_path25.default.relative(scanPath, dockerfilePath).split(import_node_path25.default.sep).join("/")
3481
3640
  }
3482
3641
  };
3483
3642
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -3489,19 +3648,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
3489
3648
 
3490
3649
  // src/extract/infra/terraform.ts
3491
3650
  init_cjs_shims();
3492
- var import_node_fs13 = require("fs");
3493
- var import_node_path25 = __toESM(require("path"), 1);
3651
+ var import_node_fs14 = require("fs");
3652
+ var import_node_path26 = __toESM(require("path"), 1);
3494
3653
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
3495
3654
  async function walkTfFiles(start, depth = 0, max = 5) {
3496
3655
  if (depth > max) return [];
3497
3656
  const out = [];
3498
- const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3657
+ const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3499
3658
  for (const entry of entries) {
3500
3659
  if (entry.isDirectory()) {
3501
3660
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
3502
- out.push(...await walkTfFiles(import_node_path25.default.join(start, entry.name), depth + 1, max));
3661
+ out.push(...await walkTfFiles(import_node_path26.default.join(start, entry.name), depth + 1, max));
3503
3662
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
3504
- out.push(import_node_path25.default.join(start, entry.name));
3663
+ out.push(import_node_path26.default.join(start, entry.name));
3505
3664
  }
3506
3665
  }
3507
3666
  return out;
@@ -3510,7 +3669,7 @@ async function addTerraformResources(graph, scanPath) {
3510
3669
  let nodesAdded = 0;
3511
3670
  const files = await walkTfFiles(scanPath);
3512
3671
  for (const file of files) {
3513
- const content = await import_node_fs13.promises.readFile(file, "utf8");
3672
+ const content = await import_node_fs14.promises.readFile(file, "utf8");
3514
3673
  RESOURCE_RE.lastIndex = 0;
3515
3674
  let m;
3516
3675
  while ((m = RESOURCE_RE.exec(content)) !== null) {
@@ -3528,8 +3687,8 @@ async function addTerraformResources(graph, scanPath) {
3528
3687
 
3529
3688
  // src/extract/infra/k8s.ts
3530
3689
  init_cjs_shims();
3531
- var import_node_fs14 = require("fs");
3532
- var import_node_path26 = __toESM(require("path"), 1);
3690
+ var import_node_fs15 = require("fs");
3691
+ var import_node_path27 = __toESM(require("path"), 1);
3533
3692
  var import_yaml3 = require("yaml");
3534
3693
  var K8S_KIND_TO_INFRA_KIND = {
3535
3694
  Service: "k8s-service",
@@ -3543,13 +3702,13 @@ var K8S_KIND_TO_INFRA_KIND = {
3543
3702
  async function walkYamlFiles2(start, depth = 0, max = 5) {
3544
3703
  if (depth > max) return [];
3545
3704
  const out = [];
3546
- const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3705
+ const entries = await import_node_fs15.promises.readdir(start, { withFileTypes: true }).catch(() => []);
3547
3706
  for (const entry of entries) {
3548
3707
  if (entry.isDirectory()) {
3549
3708
  if (IGNORED_DIRS.has(entry.name)) continue;
3550
- out.push(...await walkYamlFiles2(import_node_path26.default.join(start, entry.name), depth + 1, max));
3551
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path26.default.extname(entry.name))) {
3552
- out.push(import_node_path26.default.join(start, entry.name));
3709
+ out.push(...await walkYamlFiles2(import_node_path27.default.join(start, entry.name), depth + 1, max));
3710
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path27.default.extname(entry.name))) {
3711
+ out.push(import_node_path27.default.join(start, entry.name));
3553
3712
  }
3554
3713
  }
3555
3714
  return out;
@@ -3558,7 +3717,7 @@ async function addK8sResources(graph, scanPath) {
3558
3717
  let nodesAdded = 0;
3559
3718
  const files = await walkYamlFiles2(scanPath);
3560
3719
  for (const file of files) {
3561
- const content = await import_node_fs14.promises.readFile(file, "utf8");
3720
+ const content = await import_node_fs15.promises.readFile(file, "utf8");
3562
3721
  let docs;
3563
3722
  try {
3564
3723
  docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
@@ -3604,17 +3763,28 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
3604
3763
  const phase5 = await addInfra(graph, scanPath, services);
3605
3764
  const frontiersPromoted = promoteFrontierNodes(graph);
3606
3765
  if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
3607
- return {
3766
+ const result = {
3608
3767
  nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
3609
3768
  edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
3610
3769
  frontiersPromoted
3611
3770
  };
3771
+ emitNeatEvent({
3772
+ type: "extraction-complete",
3773
+ project: opts.project ?? DEFAULT_PROJECT,
3774
+ payload: {
3775
+ project: opts.project ?? DEFAULT_PROJECT,
3776
+ fileCount: services.length,
3777
+ nodesAdded: result.nodesAdded,
3778
+ edgesAdded: result.edgesAdded
3779
+ }
3780
+ });
3781
+ return result;
3612
3782
  }
3613
3783
 
3614
3784
  // src/persist.ts
3615
3785
  init_cjs_shims();
3616
- var import_node_fs15 = require("fs");
3617
- var import_node_path27 = __toESM(require("path"), 1);
3786
+ var import_node_fs16 = require("fs");
3787
+ var import_node_path28 = __toESM(require("path"), 1);
3618
3788
  var SCHEMA_VERSION = 2;
3619
3789
  function migrateV1ToV2(payload) {
3620
3790
  const nodes = payload.graph.nodes;
@@ -3628,7 +3798,7 @@ function migrateV1ToV2(payload) {
3628
3798
  return { ...payload, schemaVersion: 2 };
3629
3799
  }
3630
3800
  async function ensureDir(filePath) {
3631
- await import_node_fs15.promises.mkdir(import_node_path27.default.dirname(filePath), { recursive: true });
3801
+ await import_node_fs16.promises.mkdir(import_node_path28.default.dirname(filePath), { recursive: true });
3632
3802
  }
3633
3803
  async function saveGraphToDisk(graph, outPath) {
3634
3804
  await ensureDir(outPath);
@@ -3638,13 +3808,13 @@ async function saveGraphToDisk(graph, outPath) {
3638
3808
  graph: graph.export()
3639
3809
  };
3640
3810
  const tmp = `${outPath}.tmp`;
3641
- await import_node_fs15.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3642
- await import_node_fs15.promises.rename(tmp, outPath);
3811
+ await import_node_fs16.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
3812
+ await import_node_fs16.promises.rename(tmp, outPath);
3643
3813
  }
3644
3814
  async function loadGraphFromDisk(graph, outPath) {
3645
3815
  let raw;
3646
3816
  try {
3647
- raw = await import_node_fs15.promises.readFile(outPath, "utf8");
3817
+ raw = await import_node_fs16.promises.readFile(outPath, "utf8");
3648
3818
  } catch (err) {
3649
3819
  if (err.code === "ENOENT") return;
3650
3820
  throw err;
@@ -3699,11 +3869,11 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
3699
3869
  init_cjs_shims();
3700
3870
  var import_fastify = __toESM(require("fastify"), 1);
3701
3871
  var import_cors = __toESM(require("@fastify/cors"), 1);
3702
- var import_types18 = require("@neat.is/types");
3872
+ var import_types19 = require("@neat.is/types");
3703
3873
 
3704
3874
  // src/diff.ts
3705
3875
  init_cjs_shims();
3706
- var import_node_fs16 = require("fs");
3876
+ var import_node_fs17 = require("fs");
3707
3877
  async function loadSnapshotForDiff(target) {
3708
3878
  if (/^https?:\/\//i.test(target)) {
3709
3879
  const res = await fetch(target);
@@ -3712,7 +3882,7 @@ async function loadSnapshotForDiff(target) {
3712
3882
  }
3713
3883
  return await res.json();
3714
3884
  }
3715
- const raw = await import_node_fs16.promises.readFile(target, "utf8");
3885
+ const raw = await import_node_fs17.promises.readFile(target, "utf8");
3716
3886
  return JSON.parse(raw);
3717
3887
  }
3718
3888
  function indexEntries(entries) {
@@ -3780,23 +3950,23 @@ function canonicalJson(value) {
3780
3950
 
3781
3951
  // src/projects.ts
3782
3952
  init_cjs_shims();
3783
- var import_node_path28 = __toESM(require("path"), 1);
3953
+ var import_node_path29 = __toESM(require("path"), 1);
3784
3954
  function pathsForProject(project, baseDir) {
3785
3955
  if (project === DEFAULT_PROJECT) {
3786
3956
  return {
3787
- snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
3788
- errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
3789
- staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
3790
- embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
3791
- policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
3957
+ snapshotPath: import_node_path29.default.join(baseDir, "graph.json"),
3958
+ errorsPath: import_node_path29.default.join(baseDir, "errors.ndjson"),
3959
+ staleEventsPath: import_node_path29.default.join(baseDir, "stale-events.ndjson"),
3960
+ embeddingsCachePath: import_node_path29.default.join(baseDir, "embeddings.json"),
3961
+ policyViolationsPath: import_node_path29.default.join(baseDir, "policy-violations.ndjson")
3792
3962
  };
3793
3963
  }
3794
3964
  return {
3795
- snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
3796
- errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
3797
- staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
3798
- embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
3799
- policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
3965
+ snapshotPath: import_node_path29.default.join(baseDir, `${project}.json`),
3966
+ errorsPath: import_node_path29.default.join(baseDir, `errors.${project}.ndjson`),
3967
+ staleEventsPath: import_node_path29.default.join(baseDir, `stale-events.${project}.ndjson`),
3968
+ embeddingsCachePath: import_node_path29.default.join(baseDir, `embeddings.${project}.json`),
3969
+ policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
3800
3970
  };
3801
3971
  }
3802
3972
  var Projects = class {
@@ -3830,6 +4000,229 @@ var Projects = class {
3830
4000
  }
3831
4001
  };
3832
4002
 
4003
+ // src/registry.ts
4004
+ init_cjs_shims();
4005
+ var import_node_fs18 = require("fs");
4006
+ var import_node_os2 = __toESM(require("os"), 1);
4007
+ var import_node_path30 = __toESM(require("path"), 1);
4008
+ var import_types18 = require("@neat.is/types");
4009
+ var LOCK_TIMEOUT_MS = 5e3;
4010
+ var LOCK_RETRY_MS = 50;
4011
+ function neatHome() {
4012
+ const override = process.env.NEAT_HOME;
4013
+ if (override && override.length > 0) return import_node_path30.default.resolve(override);
4014
+ return import_node_path30.default.join(import_node_os2.default.homedir(), ".neat");
4015
+ }
4016
+ function registryPath() {
4017
+ return import_node_path30.default.join(neatHome(), "projects.json");
4018
+ }
4019
+ function registryLockPath() {
4020
+ return import_node_path30.default.join(neatHome(), "projects.json.lock");
4021
+ }
4022
+ async function normalizeProjectPath(input) {
4023
+ const resolved = import_node_path30.default.resolve(input);
4024
+ try {
4025
+ return await import_node_fs18.promises.realpath(resolved);
4026
+ } catch {
4027
+ return resolved;
4028
+ }
4029
+ }
4030
+ async function writeAtomically(target, contents) {
4031
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4032
+ const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4033
+ const fd = await import_node_fs18.promises.open(tmp, "w");
4034
+ try {
4035
+ await fd.writeFile(contents, "utf8");
4036
+ await fd.sync();
4037
+ } finally {
4038
+ await fd.close();
4039
+ }
4040
+ await import_node_fs18.promises.rename(tmp, target);
4041
+ }
4042
+ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4043
+ const deadline = Date.now() + timeoutMs;
4044
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4045
+ while (true) {
4046
+ try {
4047
+ const fd = await import_node_fs18.promises.open(lockPath, "wx");
4048
+ await fd.close();
4049
+ return;
4050
+ } catch (err) {
4051
+ const code = err.code;
4052
+ if (code !== "EEXIST") throw err;
4053
+ if (Date.now() >= deadline) {
4054
+ throw new Error(
4055
+ `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
4056
+ );
4057
+ }
4058
+ await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
4059
+ }
4060
+ }
4061
+ }
4062
+ async function releaseLock(lockPath) {
4063
+ await import_node_fs18.promises.unlink(lockPath).catch(() => {
4064
+ });
4065
+ }
4066
+ async function withLock(fn) {
4067
+ const lock = registryLockPath();
4068
+ await acquireLock(lock);
4069
+ try {
4070
+ return await fn();
4071
+ } finally {
4072
+ await releaseLock(lock);
4073
+ }
4074
+ }
4075
+ async function readRegistry() {
4076
+ const file = registryPath();
4077
+ let raw;
4078
+ try {
4079
+ raw = await import_node_fs18.promises.readFile(file, "utf8");
4080
+ } catch (err) {
4081
+ if (err.code === "ENOENT") {
4082
+ return { version: 1, projects: [] };
4083
+ }
4084
+ throw err;
4085
+ }
4086
+ const parsed = JSON.parse(raw);
4087
+ return import_types18.RegistryFileSchema.parse(parsed);
4088
+ }
4089
+ async function writeRegistry(reg) {
4090
+ const validated = import_types18.RegistryFileSchema.parse(reg);
4091
+ await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4092
+ }
4093
+ var ProjectNameCollisionError = class extends Error {
4094
+ projectName;
4095
+ constructor(name) {
4096
+ super(`neat registry: a project named "${name}" is already registered`);
4097
+ this.name = "ProjectNameCollisionError";
4098
+ this.projectName = name;
4099
+ }
4100
+ };
4101
+ async function addProject(opts) {
4102
+ const resolvedPath = await normalizeProjectPath(opts.path);
4103
+ return withLock(async () => {
4104
+ const reg = await readRegistry();
4105
+ const byName = reg.projects.find((p) => p.name === opts.name);
4106
+ const byPath = reg.projects.find((p) => p.path === resolvedPath);
4107
+ if (byName && byName.path !== resolvedPath) {
4108
+ throw new ProjectNameCollisionError(opts.name);
4109
+ }
4110
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4111
+ if (byName && byName.path === resolvedPath) {
4112
+ byName.lastSeenAt = now;
4113
+ if (opts.languages) byName.languages = opts.languages;
4114
+ if (opts.status) byName.status = opts.status;
4115
+ await writeRegistry(reg);
4116
+ return byName;
4117
+ }
4118
+ if (byPath && byPath.name !== opts.name) {
4119
+ throw new ProjectNameCollisionError(byPath.name);
4120
+ }
4121
+ const entry = {
4122
+ name: opts.name,
4123
+ path: resolvedPath,
4124
+ registeredAt: now,
4125
+ languages: opts.languages ?? [],
4126
+ status: opts.status ?? "active"
4127
+ };
4128
+ reg.projects.push(entry);
4129
+ await writeRegistry(reg);
4130
+ return entry;
4131
+ });
4132
+ }
4133
+ async function getProject(name) {
4134
+ const reg = await readRegistry();
4135
+ return reg.projects.find((p) => p.name === name);
4136
+ }
4137
+ async function listProjects() {
4138
+ const reg = await readRegistry();
4139
+ return reg.projects;
4140
+ }
4141
+ async function setStatus(name, status2) {
4142
+ return withLock(async () => {
4143
+ const reg = await readRegistry();
4144
+ const entry = reg.projects.find((p) => p.name === name);
4145
+ if (!entry) throw new Error(`neat registry: no project named "${name}"`);
4146
+ entry.status = status2;
4147
+ await writeRegistry(reg);
4148
+ return entry;
4149
+ });
4150
+ }
4151
+ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
4152
+ await withLock(async () => {
4153
+ const reg = await readRegistry();
4154
+ const entry = reg.projects.find((p) => p.name === name);
4155
+ if (!entry) return;
4156
+ entry.lastSeenAt = at;
4157
+ await writeRegistry(reg);
4158
+ });
4159
+ }
4160
+ async function removeProject(name) {
4161
+ return withLock(async () => {
4162
+ const reg = await readRegistry();
4163
+ const idx = reg.projects.findIndex((p) => p.name === name);
4164
+ if (idx < 0) return void 0;
4165
+ const [removed] = reg.projects.splice(idx, 1);
4166
+ await writeRegistry(reg);
4167
+ return removed;
4168
+ });
4169
+ }
4170
+
4171
+ // src/streaming.ts
4172
+ init_cjs_shims();
4173
+ var SSE_HEARTBEAT_MS = 3e4;
4174
+ var SSE_BACKPRESSURE_CAP = 1e3;
4175
+ function handleSse(req, reply, opts) {
4176
+ const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
4177
+ const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
4178
+ reply.raw.setHeader("Content-Type", "text/event-stream");
4179
+ reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
4180
+ reply.raw.setHeader("Connection", "keep-alive");
4181
+ reply.raw.setHeader("X-Accel-Buffering", "no");
4182
+ reply.raw.flushHeaders?.();
4183
+ let pending = 0;
4184
+ let dropped = false;
4185
+ const closeConnection = () => {
4186
+ if (dropped) return;
4187
+ dropped = true;
4188
+ eventBus.off(EVENT_BUS_CHANNEL, listener);
4189
+ clearInterval(heartbeat);
4190
+ if (!reply.raw.writableEnded) reply.raw.end();
4191
+ };
4192
+ const writeFrame = (frame) => {
4193
+ if (dropped) return;
4194
+ if (pending >= backpressureCap) {
4195
+ const errFrame = `event: error
4196
+ data: ${JSON.stringify({ reason: "backpressure" })}
4197
+
4198
+ `;
4199
+ reply.raw.write(errFrame);
4200
+ closeConnection();
4201
+ return;
4202
+ }
4203
+ pending++;
4204
+ reply.raw.write(frame, () => {
4205
+ pending = Math.max(0, pending - 1);
4206
+ });
4207
+ };
4208
+ const listener = (envelope) => {
4209
+ if (envelope.project !== opts.project) return;
4210
+ writeFrame(`event: ${envelope.type}
4211
+ data: ${JSON.stringify(envelope.payload)}
4212
+
4213
+ `);
4214
+ };
4215
+ eventBus.on(EVENT_BUS_CHANNEL, listener);
4216
+ const heartbeat = setInterval(() => {
4217
+ if (dropped) return;
4218
+ reply.raw.write(":heartbeat\n\n");
4219
+ }, heartbeatMs);
4220
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
4221
+ req.raw.on("close", closeConnection);
4222
+ reply.raw.on("close", closeConnection);
4223
+ reply.raw.on("error", closeConnection);
4224
+ }
4225
+
3833
4226
  // src/api.ts
3834
4227
  function serializeGraph(graph) {
3835
4228
  const nodes = [];
@@ -3878,6 +4271,11 @@ function buildLegacyRegistry(opts) {
3878
4271
  }
3879
4272
  function registerRoutes(scope, ctx) {
3880
4273
  const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4274
+ scope.get("/events", (req, reply) => {
4275
+ const proj = resolveProject(registry, req, reply);
4276
+ if (!proj) return;
4277
+ handleSse(req, reply, { project: proj.name });
4278
+ });
3881
4279
  scope.get("/health", async (req, reply) => {
3882
4280
  const proj = resolveProject(registry, req, reply);
3883
4281
  if (!proj) return;
@@ -4088,7 +4486,7 @@ function registerRoutes(scope, ctx) {
4088
4486
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4089
4487
  let violations = await log.readAll();
4090
4488
  if (req.query.severity) {
4091
- const sev = import_types18.PolicySeveritySchema.safeParse(req.query.severity);
4489
+ const sev = import_types19.PolicySeveritySchema.safeParse(req.query.severity);
4092
4490
  if (!sev.success) {
4093
4491
  return reply.code(400).send({
4094
4492
  error: "invalid severity",
@@ -4105,7 +4503,7 @@ function registerRoutes(scope, ctx) {
4105
4503
  scope.post("/policies/check", async (req, reply) => {
4106
4504
  const proj = resolveProject(registry, req, reply);
4107
4505
  if (!proj) return;
4108
- const parsed = import_types18.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4506
+ const parsed = import_types19.PoliciesCheckBodySchema.safeParse(req.body ?? {});
4109
4507
  if (!parsed.success) {
4110
4508
  return reply.code(400).send({
4111
4509
  error: "invalid /policies/check body",
@@ -4169,17 +4567,16 @@ async function buildApi(opts) {
4169
4567
  staleEventsPathFor,
4170
4568
  policyFilePathFor
4171
4569
  };
4172
- app.get("/projects", async () => ({
4173
- projects: registry.list().map((name) => {
4174
- const proj = registry.get(name);
4175
- return {
4176
- name,
4177
- nodeCount: proj.graph.order,
4178
- edgeCount: proj.graph.size,
4179
- scanPath: proj.scanPath
4180
- };
4181
- })
4182
- }));
4570
+ app.get("/projects", async (_req, reply) => {
4571
+ try {
4572
+ return await listProjects();
4573
+ } catch (err) {
4574
+ return reply.code(500).send({
4575
+ error: "failed to read project registry",
4576
+ details: err.message
4577
+ });
4578
+ }
4579
+ });
4183
4580
  registerRoutes(app, routeCtx);
4184
4581
  await app.register(
4185
4582
  async (scope) => {
@@ -4196,184 +4593,14 @@ init_otel_grpc();
4196
4593
 
4197
4594
  // src/daemon.ts
4198
4595
  init_cjs_shims();
4199
- var import_node_fs18 = require("fs");
4200
- var import_node_path32 = __toESM(require("path"), 1);
4201
-
4202
- // src/registry.ts
4203
- init_cjs_shims();
4204
- var import_node_fs17 = require("fs");
4205
- var import_node_os2 = __toESM(require("os"), 1);
4206
- var import_node_path31 = __toESM(require("path"), 1);
4207
- var import_types19 = require("@neat.is/types");
4208
- var LOCK_TIMEOUT_MS = 5e3;
4209
- var LOCK_RETRY_MS = 50;
4210
- function neatHome() {
4211
- const override = process.env.NEAT_HOME;
4212
- if (override && override.length > 0) return import_node_path31.default.resolve(override);
4213
- return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
4214
- }
4215
- function registryPath() {
4216
- return import_node_path31.default.join(neatHome(), "projects.json");
4217
- }
4218
- function registryLockPath() {
4219
- return import_node_path31.default.join(neatHome(), "projects.json.lock");
4220
- }
4221
- async function normalizeProjectPath(input) {
4222
- const resolved = import_node_path31.default.resolve(input);
4223
- try {
4224
- return await import_node_fs17.promises.realpath(resolved);
4225
- } catch {
4226
- return resolved;
4227
- }
4228
- }
4229
- async function writeAtomically(target, contents) {
4230
- await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(target), { recursive: true });
4231
- const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
4232
- const fd = await import_node_fs17.promises.open(tmp, "w");
4233
- try {
4234
- await fd.writeFile(contents, "utf8");
4235
- await fd.sync();
4236
- } finally {
4237
- await fd.close();
4238
- }
4239
- await import_node_fs17.promises.rename(tmp, target);
4240
- }
4241
- async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
4242
- const deadline = Date.now() + timeoutMs;
4243
- await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(lockPath), { recursive: true });
4244
- while (true) {
4245
- try {
4246
- const fd = await import_node_fs17.promises.open(lockPath, "wx");
4247
- await fd.close();
4248
- return;
4249
- } catch (err) {
4250
- const code = err.code;
4251
- if (code !== "EEXIST") throw err;
4252
- if (Date.now() >= deadline) {
4253
- throw new Error(
4254
- `neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
4255
- );
4256
- }
4257
- await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
4258
- }
4259
- }
4260
- }
4261
- async function releaseLock(lockPath) {
4262
- await import_node_fs17.promises.unlink(lockPath).catch(() => {
4263
- });
4264
- }
4265
- async function withLock(fn) {
4266
- const lock = registryLockPath();
4267
- await acquireLock(lock);
4268
- try {
4269
- return await fn();
4270
- } finally {
4271
- await releaseLock(lock);
4272
- }
4273
- }
4274
- async function readRegistry() {
4275
- const file = registryPath();
4276
- let raw;
4277
- try {
4278
- raw = await import_node_fs17.promises.readFile(file, "utf8");
4279
- } catch (err) {
4280
- if (err.code === "ENOENT") {
4281
- return { version: 1, projects: [] };
4282
- }
4283
- throw err;
4284
- }
4285
- const parsed = JSON.parse(raw);
4286
- return import_types19.RegistryFileSchema.parse(parsed);
4287
- }
4288
- async function writeRegistry(reg) {
4289
- const validated = import_types19.RegistryFileSchema.parse(reg);
4290
- await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
4291
- }
4292
- var ProjectNameCollisionError = class extends Error {
4293
- projectName;
4294
- constructor(name) {
4295
- super(`neat registry: a project named "${name}" is already registered`);
4296
- this.name = "ProjectNameCollisionError";
4297
- this.projectName = name;
4298
- }
4299
- };
4300
- async function addProject(opts) {
4301
- const resolvedPath = await normalizeProjectPath(opts.path);
4302
- return withLock(async () => {
4303
- const reg = await readRegistry();
4304
- const byName = reg.projects.find((p) => p.name === opts.name);
4305
- const byPath = reg.projects.find((p) => p.path === resolvedPath);
4306
- if (byName && byName.path !== resolvedPath) {
4307
- throw new ProjectNameCollisionError(opts.name);
4308
- }
4309
- const now = (/* @__PURE__ */ new Date()).toISOString();
4310
- if (byName && byName.path === resolvedPath) {
4311
- byName.lastSeenAt = now;
4312
- if (opts.languages) byName.languages = opts.languages;
4313
- if (opts.status) byName.status = opts.status;
4314
- await writeRegistry(reg);
4315
- return byName;
4316
- }
4317
- if (byPath && byPath.name !== opts.name) {
4318
- throw new ProjectNameCollisionError(byPath.name);
4319
- }
4320
- const entry = {
4321
- name: opts.name,
4322
- path: resolvedPath,
4323
- registeredAt: now,
4324
- languages: opts.languages ?? [],
4325
- status: opts.status ?? "active"
4326
- };
4327
- reg.projects.push(entry);
4328
- await writeRegistry(reg);
4329
- return entry;
4330
- });
4331
- }
4332
- async function getProject(name) {
4333
- const reg = await readRegistry();
4334
- return reg.projects.find((p) => p.name === name);
4335
- }
4336
- async function listProjects() {
4337
- const reg = await readRegistry();
4338
- return reg.projects;
4339
- }
4340
- async function setStatus(name, status2) {
4341
- return withLock(async () => {
4342
- const reg = await readRegistry();
4343
- const entry = reg.projects.find((p) => p.name === name);
4344
- if (!entry) throw new Error(`neat registry: no project named "${name}"`);
4345
- entry.status = status2;
4346
- await writeRegistry(reg);
4347
- return entry;
4348
- });
4349
- }
4350
- async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
4351
- await withLock(async () => {
4352
- const reg = await readRegistry();
4353
- const entry = reg.projects.find((p) => p.name === name);
4354
- if (!entry) return;
4355
- entry.lastSeenAt = at;
4356
- await writeRegistry(reg);
4357
- });
4358
- }
4359
- async function removeProject(name) {
4360
- return withLock(async () => {
4361
- const reg = await readRegistry();
4362
- const idx = reg.projects.findIndex((p) => p.name === name);
4363
- if (idx < 0) return void 0;
4364
- const [removed] = reg.projects.splice(idx, 1);
4365
- await writeRegistry(reg);
4366
- return removed;
4367
- });
4368
- }
4369
-
4370
- // src/daemon.ts
4596
+ var import_node_fs19 = require("fs");
4597
+ var import_node_path33 = __toESM(require("path"), 1);
4371
4598
  function neatHomeFor(opts) {
4372
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path32.default.resolve(opts.neatHome);
4599
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
4373
4600
  const env = process.env.NEAT_HOME;
4374
- if (env && env.length > 0) return import_node_path32.default.resolve(env);
4601
+ if (env && env.length > 0) return import_node_path33.default.resolve(env);
4375
4602
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
4376
- return import_node_path32.default.join(home, ".neat");
4603
+ return import_node_path33.default.join(home, ".neat");
4377
4604
  }
4378
4605
  function routeSpanToProject(serviceName, projects) {
4379
4606
  if (!serviceName) return DEFAULT_PROJECT;
@@ -4387,7 +4614,7 @@ function routeSpanToProject(serviceName, projects) {
4387
4614
  }
4388
4615
  async function bootstrapProject(entry) {
4389
4616
  try {
4390
- const stat = await import_node_fs18.promises.stat(entry.path);
4617
+ const stat = await import_node_fs19.promises.stat(entry.path);
4391
4618
  if (!stat.isDirectory()) {
4392
4619
  throw new Error(`registered path ${entry.path} is not a directory`);
4393
4620
  }
@@ -4410,7 +4637,7 @@ async function bootstrapProject(entry) {
4410
4637
  const graph = getGraph(entry.name);
4411
4638
  const outPath = pathsForProject(
4412
4639
  entry.name,
4413
- import_node_path32.default.join(entry.path, "neat-out")
4640
+ import_node_path33.default.join(entry.path, "neat-out")
4414
4641
  ).snapshotPath;
4415
4642
  await loadGraphFromDisk(graph, outPath);
4416
4643
  await extractFromDirectory(graph, entry.path);
@@ -4429,13 +4656,13 @@ async function startDaemon(opts = {}) {
4429
4656
  const home = neatHomeFor(opts);
4430
4657
  const regPath = registryPath();
4431
4658
  try {
4432
- await import_node_fs18.promises.access(regPath);
4659
+ await import_node_fs19.promises.access(regPath);
4433
4660
  } catch {
4434
4661
  throw new Error(
4435
4662
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
4436
4663
  );
4437
4664
  }
4438
- const pidPath = import_node_path32.default.join(home, "neatd.pid");
4665
+ const pidPath = import_node_path33.default.join(home, "neatd.pid");
4439
4666
  await writeAtomically(pidPath, `${process.pid}
4440
4667
  `);
4441
4668
  const slots = /* @__PURE__ */ new Map();
@@ -4501,7 +4728,7 @@ async function startDaemon(opts = {}) {
4501
4728
  } catch {
4502
4729
  }
4503
4730
  }
4504
- await import_node_fs18.promises.unlink(pidPath).catch(() => {
4731
+ await import_node_fs19.promises.unlink(pidPath).catch(() => {
4505
4732
  });
4506
4733
  };
4507
4734
  return { slots, reload, stop, pidPath };