@neat.is/core 0.4.15 → 0.4.17

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
@@ -57,9 +57,9 @@ function mountBearerAuth(app, opts) {
57
57
  const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
58
58
  const publicRead = opts.publicRead === true;
59
59
  app.addHook("preHandler", (req, reply, done) => {
60
- const path42 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path43 = (req.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path42 === suffix || path42.endsWith(suffix)) {
62
+ if (path43 === suffix || path43.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -187,8 +187,8 @@ function reshapeGrpcRequest(req) {
187
187
  };
188
188
  }
189
189
  function resolveProtoRoot() {
190
- const here = import_node_path38.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
191
- return import_node_path38.default.resolve(here, "..", "proto");
190
+ const here = import_node_path39.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
191
+ return import_node_path39.default.resolve(here, "..", "proto");
192
192
  }
193
193
  function loadTraceService() {
194
194
  const protoRoot = resolveProtoRoot();
@@ -256,13 +256,13 @@ async function startOtelGrpcReceiver(opts) {
256
256
  })
257
257
  };
258
258
  }
259
- var import_node_url, import_node_path38, import_node_crypto2, grpc, protoLoader;
259
+ var import_node_url, import_node_path39, import_node_crypto2, grpc, protoLoader;
260
260
  var init_otel_grpc = __esm({
261
261
  "src/otel-grpc.ts"() {
262
262
  "use strict";
263
263
  init_cjs_shims();
264
264
  import_node_url = require("url");
265
- import_node_path38 = __toESM(require("path"), 1);
265
+ import_node_path39 = __toESM(require("path"), 1);
266
266
  import_node_crypto2 = require("crypto");
267
267
  grpc = __toESM(require("@grpc/grpc-js"), 1);
268
268
  protoLoader = __toESM(require("@grpc/proto-loader"), 1);
@@ -372,10 +372,10 @@ function parseOtlpRequest(body) {
372
372
  return out;
373
373
  }
374
374
  function loadProtoRoot() {
375
- const here = import_node_path39.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
376
- const protoRoot = import_node_path39.default.resolve(here, "..", "proto");
375
+ const here = import_node_path40.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
376
+ const protoRoot = import_node_path40.default.resolve(here, "..", "proto");
377
377
  const root = new import_protobufjs.default.Root();
378
- root.resolvePath = (_origin, target) => import_node_path39.default.resolve(protoRoot, target);
378
+ root.resolvePath = (_origin, target) => import_node_path40.default.resolve(protoRoot, target);
379
379
  root.loadSync(
380
380
  "opentelemetry/proto/collector/trace/v1/trace_service.proto",
381
381
  { keepCase: true }
@@ -579,12 +579,12 @@ function logSpanHandler(span) {
579
579
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
580
580
  );
581
581
  }
582
- var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
582
+ var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
583
583
  var init_otel = __esm({
584
584
  "src/otel.ts"() {
585
585
  "use strict";
586
586
  init_cjs_shims();
587
- import_node_path39 = __toESM(require("path"), 1);
587
+ import_node_path40 = __toESM(require("path"), 1);
588
588
  import_node_url2 = require("url");
589
589
  import_fastify2 = __toESM(require("fastify"), 1);
590
590
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1172,19 +1172,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1172
1172
  function longestIncomingWalk(graph, start, maxDepth) {
1173
1173
  let best = { path: [start], edges: [] };
1174
1174
  const visited = /* @__PURE__ */ new Set([start]);
1175
- function step(node, path42, edges) {
1176
- if (path42.length > best.path.length) {
1177
- best = { path: [...path42], edges: [...edges] };
1175
+ function step(node, path43, edges) {
1176
+ if (path43.length > best.path.length) {
1177
+ best = { path: [...path43], edges: [...edges] };
1178
1178
  }
1179
- if (path42.length - 1 >= maxDepth) return;
1179
+ if (path43.length - 1 >= maxDepth) return;
1180
1180
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1181
1181
  for (const [srcId, edge] of incoming) {
1182
1182
  if (visited.has(srcId)) continue;
1183
1183
  visited.add(srcId);
1184
- path42.push(srcId);
1184
+ path43.push(srcId);
1185
1185
  edges.push(edge);
1186
- step(srcId, path42, edges);
1187
- path42.pop();
1186
+ step(srcId, path43, edges);
1187
+ path43.pop();
1188
1188
  edges.pop();
1189
1189
  visited.delete(srcId);
1190
1190
  }
@@ -1724,6 +1724,41 @@ function thresholdForEdgeType(edgeType, overrides) {
1724
1724
  const map = overrides ?? loadStaleThresholdsFromEnv();
1725
1725
  return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
1726
1726
  }
1727
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1728
+ threshold: 5,
1729
+ windowMs: 6e4
1730
+ };
1731
+ function loadIncidentThresholdsFromEnv() {
1732
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1733
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1734
+ try {
1735
+ const overrides = JSON.parse(raw);
1736
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1737
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1738
+ merged.threshold = Math.floor(overrides.threshold);
1739
+ }
1740
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1741
+ merged.windowMs = overrides.windowMs;
1742
+ }
1743
+ return merged;
1744
+ } catch (err) {
1745
+ console.warn(
1746
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1747
+ );
1748
+ return DEFAULT_INCIDENT_THRESHOLDS;
1749
+ }
1750
+ }
1751
+ function httpResponseStatus(span) {
1752
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1753
+ const v = span.attributes[key];
1754
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1755
+ if (typeof v === "string") {
1756
+ const n = Number(v);
1757
+ if (Number.isFinite(n)) return n;
1758
+ }
1759
+ }
1760
+ return void 0;
1761
+ }
1727
1762
  function nowIso(ctx) {
1728
1763
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1729
1764
  }
@@ -2137,6 +2172,71 @@ function makeErrorSpanWriter(errorsPath) {
2137
2172
  await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
2138
2173
  };
2139
2174
  }
2175
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
2176
+ const attrs = sanitizeAttributes(span.attributes);
2177
+ const first = firstTimestamp ?? timestamp;
2178
+ const peer = pickAddress(span);
2179
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
2180
+ const ev = {
2181
+ id: `${span.traceId}:${span.spanId}`,
2182
+ timestamp,
2183
+ service: span.service,
2184
+ traceId: span.traceId,
2185
+ spanId: span.spanId,
2186
+ errorType: "http-failure",
2187
+ errorMessage: message,
2188
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2189
+ affectedNode,
2190
+ httpStatusCode: statusCode,
2191
+ incidentCount: count,
2192
+ firstTimestamp: first,
2193
+ lastTimestamp: timestamp
2194
+ };
2195
+ await appendErrorEvent(ctx, ev);
2196
+ }
2197
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2198
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2199
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
2200
+ const peer = pickAddress(span) ?? span.spanId;
2201
+ const key = `${span.service}->${peer}`;
2202
+ const existing = ctx.burstState.get(key);
2203
+ let state;
2204
+ if (existing && nowMs - existing.lastMs <= windowMs) {
2205
+ existing.count += 1;
2206
+ existing.lastTs = ts;
2207
+ existing.lastMs = nowMs;
2208
+ existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
2209
+ state = existing;
2210
+ } else {
2211
+ state = {
2212
+ count: 1,
2213
+ firstTs: ts,
2214
+ lastTs: ts,
2215
+ lastMs: nowMs,
2216
+ codes: /* @__PURE__ */ new Map([[status2, 1]])
2217
+ };
2218
+ ctx.burstState.set(key, state);
2219
+ }
2220
+ if (state.count < threshold) return;
2221
+ let dominant = status2;
2222
+ let max = 0;
2223
+ for (const [code, n] of state.codes) {
2224
+ if (n > max) {
2225
+ max = n;
2226
+ dominant = code;
2227
+ }
2228
+ }
2229
+ await recordFailingResponseIncident(
2230
+ ctx,
2231
+ span,
2232
+ affectedNode,
2233
+ state.lastTs,
2234
+ dominant,
2235
+ state.count,
2236
+ state.firstTs
2237
+ );
2238
+ ctx.burstState.delete(key);
2239
+ }
2140
2240
  async function handleSpan(ctx, span) {
2141
2241
  const ts = span.startTimeIso ?? nowIso(ctx);
2142
2242
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -2235,6 +2335,14 @@ async function handleSpan(ctx, span) {
2235
2335
  await appendErrorEvent(ctx, ev);
2236
2336
  }
2237
2337
  }
2338
+ if (span.statusCode !== 2) {
2339
+ const status2 = httpResponseStatus(span);
2340
+ if (status2 !== void 0 && status2 >= 500) {
2341
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2342
+ } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2343
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
2344
+ }
2345
+ }
2238
2346
  void affectedNode;
2239
2347
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2240
2348
  }
@@ -4254,7 +4362,7 @@ async function addConfigNodes(graph, services, scanPath) {
4254
4362
 
4255
4363
  // src/extract/calls/index.ts
4256
4364
  init_cjs_shims();
4257
- var import_types16 = require("@neat.is/types");
4365
+ var import_types17 = require("@neat.is/types");
4258
4366
 
4259
4367
  // src/extract/calls/http.ts
4260
4368
  init_cjs_shims();
@@ -4607,15 +4715,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
4607
4715
  return out;
4608
4716
  }
4609
4717
 
4718
+ // src/extract/calls/supabase.ts
4719
+ init_cjs_shims();
4720
+ var import_node_path27 = __toESM(require("path"), 1);
4721
+ var import_types16 = require("@neat.is/types");
4722
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4723
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4724
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4725
+ function hostFromLiteral(literal) {
4726
+ if (!literal) return null;
4727
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4728
+ if (!m) return null;
4729
+ const host = m[1];
4730
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4731
+ return host;
4732
+ }
4733
+ function readImports2(content) {
4734
+ return {
4735
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4736
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4737
+ };
4738
+ }
4739
+ function constructorMatchesImport(name, ctx) {
4740
+ if (name === "createClient") return ctx.hasSupabaseJs;
4741
+ return ctx.hasSupabaseSsr;
4742
+ }
4743
+ function supabaseEndpointsFromFile(file, serviceDir) {
4744
+ const ctx = readImports2(file.content);
4745
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4746
+ const out = [];
4747
+ const seen = /* @__PURE__ */ new Set();
4748
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4749
+ let m;
4750
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4751
+ const ctor = m[1];
4752
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4753
+ const host = hostFromLiteral(m[2]);
4754
+ const name = host ?? "env";
4755
+ if (seen.has(name)) continue;
4756
+ seen.add(name);
4757
+ const line = lineOf(file.content, m[0]);
4758
+ out.push({
4759
+ infraId: (0, import_types16.infraId)("supabase", name),
4760
+ name,
4761
+ kind: "supabase",
4762
+ edgeType: "CALLS",
4763
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4764
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4765
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4766
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4767
+ confidenceKind: "verified-call-site",
4768
+ evidence: {
4769
+ file: import_node_path27.default.relative(serviceDir, file.path),
4770
+ line,
4771
+ snippet: snippet(file.content, line)
4772
+ }
4773
+ });
4774
+ }
4775
+ return out;
4776
+ }
4777
+
4610
4778
  // src/extract/calls/index.ts
4611
4779
  function edgeTypeFromEndpoint(ep) {
4612
4780
  switch (ep.edgeType) {
4613
4781
  case "PUBLISHES_TO":
4614
- return import_types16.EdgeType.PUBLISHES_TO;
4782
+ return import_types17.EdgeType.PUBLISHES_TO;
4615
4783
  case "CONSUMES_FROM":
4616
- return import_types16.EdgeType.CONSUMES_FROM;
4784
+ return import_types17.EdgeType.CONSUMES_FROM;
4617
4785
  default:
4618
- return import_types16.EdgeType.CALLS;
4786
+ return import_types17.EdgeType.CALLS;
4619
4787
  }
4620
4788
  }
4621
4789
  function isAwsKind(kind) {
@@ -4635,6 +4803,7 @@ async function addExternalEndpointEdges(graph, services) {
4635
4803
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4636
4804
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4637
4805
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4806
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4638
4807
  }
4639
4808
  if (endpoints.length === 0) continue;
4640
4809
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4642,7 +4811,7 @@ async function addExternalEndpointEdges(graph, services) {
4642
4811
  if (!graph.hasNode(ep.infraId)) {
4643
4812
  const node = {
4644
4813
  id: ep.infraId,
4645
- type: import_types16.NodeType.InfraNode,
4814
+ type: import_types17.NodeType.InfraNode,
4646
4815
  name: ep.name,
4647
4816
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4648
4817
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4654,7 +4823,7 @@ async function addExternalEndpointEdges(graph, services) {
4654
4823
  nodesAdded++;
4655
4824
  }
4656
4825
  const edgeType = edgeTypeFromEndpoint(ep);
4657
- const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4826
+ const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
4658
4827
  const relFile = toPosix2(ep.evidence.file);
4659
4828
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4660
4829
  graph,
@@ -4664,7 +4833,7 @@ async function addExternalEndpointEdges(graph, services) {
4664
4833
  );
4665
4834
  nodesAdded += n;
4666
4835
  edgesAdded += e;
4667
- if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4836
+ if (!(0, import_types17.passesExtractedFloor)(confidence)) {
4668
4837
  noteExtractedDropped({
4669
4838
  source: fileNodeId,
4670
4839
  target: ep.infraId,
@@ -4684,7 +4853,7 @@ async function addExternalEndpointEdges(graph, services) {
4684
4853
  source: fileNodeId,
4685
4854
  target: ep.infraId,
4686
4855
  type: edgeType,
4687
- provenance: import_types16.Provenance.EXTRACTED,
4856
+ provenance: import_types17.Provenance.EXTRACTED,
4688
4857
  confidence,
4689
4858
  evidence: ep.evidence
4690
4859
  };
@@ -4709,16 +4878,16 @@ init_cjs_shims();
4709
4878
 
4710
4879
  // src/extract/infra/docker-compose.ts
4711
4880
  init_cjs_shims();
4712
- var import_node_path27 = __toESM(require("path"), 1);
4713
- var import_types18 = require("@neat.is/types");
4881
+ var import_node_path28 = __toESM(require("path"), 1);
4882
+ var import_types19 = require("@neat.is/types");
4714
4883
 
4715
4884
  // src/extract/infra/shared.ts
4716
4885
  init_cjs_shims();
4717
- var import_types17 = require("@neat.is/types");
4886
+ var import_types18 = require("@neat.is/types");
4718
4887
  function makeInfraNode(kind, name, provider = "self", extras) {
4719
4888
  return {
4720
- id: (0, import_types17.infraId)(kind, name),
4721
- type: import_types17.NodeType.InfraNode,
4889
+ id: (0, import_types18.infraId)(kind, name),
4890
+ type: import_types18.NodeType.InfraNode,
4722
4891
  name,
4723
4892
  provider,
4724
4893
  kind,
@@ -4747,7 +4916,7 @@ function dependsOnList(value) {
4747
4916
  }
4748
4917
  function serviceNameToServiceNode(name, services) {
4749
4918
  for (const s of services) {
4750
- if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4919
+ if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
4751
4920
  }
4752
4921
  return null;
4753
4922
  }
@@ -4756,7 +4925,7 @@ async function addComposeInfra(graph, scanPath, services) {
4756
4925
  let edgesAdded = 0;
4757
4926
  let composePath = null;
4758
4927
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4759
- const abs = import_node_path27.default.join(scanPath, name);
4928
+ const abs = import_node_path28.default.join(scanPath, name);
4760
4929
  if (await exists(abs)) {
4761
4930
  composePath = abs;
4762
4931
  break;
@@ -4769,13 +4938,13 @@ async function addComposeInfra(graph, scanPath, services) {
4769
4938
  } catch (err) {
4770
4939
  recordExtractionError(
4771
4940
  "infra docker-compose",
4772
- import_node_path27.default.relative(scanPath, composePath),
4941
+ import_node_path28.default.relative(scanPath, composePath),
4773
4942
  err
4774
4943
  );
4775
4944
  return { nodesAdded, edgesAdded };
4776
4945
  }
4777
4946
  if (!compose?.services) return { nodesAdded, edgesAdded };
4778
- const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4947
+ const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
4779
4948
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4780
4949
  for (const [composeName, svc] of Object.entries(compose.services)) {
4781
4950
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4797,15 +4966,15 @@ async function addComposeInfra(graph, scanPath, services) {
4797
4966
  for (const dep of dependsOnList(svc.depends_on)) {
4798
4967
  const targetId = composeNameToNodeId.get(dep);
4799
4968
  if (!targetId) continue;
4800
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4969
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
4801
4970
  if (graph.hasEdge(edgeId)) continue;
4802
4971
  const edge = {
4803
4972
  id: edgeId,
4804
4973
  source: sourceId,
4805
4974
  target: targetId,
4806
- type: import_types18.EdgeType.DEPENDS_ON,
4807
- provenance: import_types18.Provenance.EXTRACTED,
4808
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4975
+ type: import_types19.EdgeType.DEPENDS_ON,
4976
+ provenance: import_types19.Provenance.EXTRACTED,
4977
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4809
4978
  evidence: { file: evidenceFile }
4810
4979
  };
4811
4980
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4817,9 +4986,9 @@ async function addComposeInfra(graph, scanPath, services) {
4817
4986
 
4818
4987
  // src/extract/infra/dockerfile.ts
4819
4988
  init_cjs_shims();
4820
- var import_node_path28 = __toESM(require("path"), 1);
4989
+ var import_node_path29 = __toESM(require("path"), 1);
4821
4990
  var import_node_fs15 = require("fs");
4822
- var import_types19 = require("@neat.is/types");
4991
+ var import_types20 = require("@neat.is/types");
4823
4992
  function runtimeImage(content) {
4824
4993
  const lines = content.split("\n");
4825
4994
  let last = null;
@@ -4838,7 +5007,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4838
5007
  let nodesAdded = 0;
4839
5008
  let edgesAdded = 0;
4840
5009
  for (const service of services) {
4841
- const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
5010
+ const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
4842
5011
  if (!await exists(dockerfilePath)) continue;
4843
5012
  let content;
4844
5013
  try {
@@ -4846,7 +5015,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4846
5015
  } catch (err) {
4847
5016
  recordExtractionError(
4848
5017
  "infra dockerfile",
4849
- import_node_path28.default.relative(scanPath, dockerfilePath),
5018
+ import_node_path29.default.relative(scanPath, dockerfilePath),
4850
5019
  err
4851
5020
  );
4852
5021
  continue;
@@ -4858,7 +5027,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4858
5027
  graph.addNode(node.id, node);
4859
5028
  nodesAdded++;
4860
5029
  }
4861
- const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
5030
+ const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
4862
5031
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4863
5032
  graph,
4864
5033
  service.pkg.name,
@@ -4867,17 +5036,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4867
5036
  );
4868
5037
  nodesAdded += fn;
4869
5038
  edgesAdded += fe;
4870
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
5039
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
4871
5040
  if (!graph.hasEdge(edgeId)) {
4872
5041
  const edge = {
4873
5042
  id: edgeId,
4874
5043
  source: fileNodeId,
4875
5044
  target: node.id,
4876
- type: import_types19.EdgeType.RUNS_ON,
4877
- provenance: import_types19.Provenance.EXTRACTED,
4878
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
5045
+ type: import_types20.EdgeType.RUNS_ON,
5046
+ provenance: import_types20.Provenance.EXTRACTED,
5047
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
4879
5048
  evidence: {
4880
- file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
5049
+ file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
4881
5050
  }
4882
5051
  };
4883
5052
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4890,7 +5059,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4890
5059
  // src/extract/infra/terraform.ts
4891
5060
  init_cjs_shims();
4892
5061
  var import_node_fs16 = require("fs");
4893
- var import_node_path29 = __toESM(require("path"), 1);
5062
+ var import_node_path30 = __toESM(require("path"), 1);
4894
5063
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4895
5064
  async function walkTfFiles(start, depth = 0, max = 5) {
4896
5065
  if (depth > max) return [];
@@ -4899,11 +5068,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4899
5068
  for (const entry of entries) {
4900
5069
  if (entry.isDirectory()) {
4901
5070
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4902
- const child = import_node_path29.default.join(start, entry.name);
5071
+ const child = import_node_path30.default.join(start, entry.name);
4903
5072
  if (await isPythonVenvDir(child)) continue;
4904
5073
  out.push(...await walkTfFiles(child, depth + 1, max));
4905
5074
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4906
- out.push(import_node_path29.default.join(start, entry.name));
5075
+ out.push(import_node_path30.default.join(start, entry.name));
4907
5076
  }
4908
5077
  }
4909
5078
  return out;
@@ -4931,7 +5100,7 @@ async function addTerraformResources(graph, scanPath) {
4931
5100
  // src/extract/infra/k8s.ts
4932
5101
  init_cjs_shims();
4933
5102
  var import_node_fs17 = require("fs");
4934
- var import_node_path30 = __toESM(require("path"), 1);
5103
+ var import_node_path31 = __toESM(require("path"), 1);
4935
5104
  var import_yaml3 = require("yaml");
4936
5105
  var K8S_KIND_TO_INFRA_KIND = {
4937
5106
  Service: "k8s-service",
@@ -4949,11 +5118,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4949
5118
  for (const entry of entries) {
4950
5119
  if (entry.isDirectory()) {
4951
5120
  if (IGNORED_DIRS.has(entry.name)) continue;
4952
- const child = import_node_path30.default.join(start, entry.name);
5121
+ const child = import_node_path31.default.join(start, entry.name);
4953
5122
  if (await isPythonVenvDir(child)) continue;
4954
5123
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4955
- } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path30.default.extname(entry.name))) {
4956
- out.push(import_node_path30.default.join(start, entry.name));
5124
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry.name))) {
5125
+ out.push(import_node_path31.default.join(start, entry.name));
4957
5126
  }
4958
5127
  }
4959
5128
  return out;
@@ -4997,17 +5166,17 @@ async function addInfra(graph, scanPath, services) {
4997
5166
  }
4998
5167
 
4999
5168
  // src/extract/index.ts
5000
- var import_node_path32 = __toESM(require("path"), 1);
5169
+ var import_node_path33 = __toESM(require("path"), 1);
5001
5170
 
5002
5171
  // src/extract/retire.ts
5003
5172
  init_cjs_shims();
5004
5173
  var import_node_fs18 = require("fs");
5005
- var import_node_path31 = __toESM(require("path"), 1);
5006
- var import_types20 = require("@neat.is/types");
5174
+ var import_node_path32 = __toESM(require("path"), 1);
5175
+ var import_types21 = require("@neat.is/types");
5007
5176
  function dropOrphanedFileNodes(graph) {
5008
5177
  const orphans = [];
5009
5178
  graph.forEachNode((id, attrs) => {
5010
- if (attrs.type !== import_types20.NodeType.FileNode) return;
5179
+ if (attrs.type !== import_types21.NodeType.FileNode) return;
5011
5180
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5012
5181
  orphans.push(id);
5013
5182
  }
@@ -5020,14 +5189,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5020
5189
  const bases = [scanPath, ...serviceDirs];
5021
5190
  graph.forEachEdge((id, attrs) => {
5022
5191
  const edge = attrs;
5023
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5192
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5024
5193
  const evidenceFile = edge.evidence?.file;
5025
5194
  if (!evidenceFile) return;
5026
- if (import_node_path31.default.isAbsolute(evidenceFile)) {
5195
+ if (import_node_path32.default.isAbsolute(evidenceFile)) {
5027
5196
  if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
5028
5197
  return;
5029
5198
  }
5030
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path31.default.join(base, evidenceFile)));
5199
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
5031
5200
  if (!found) toDrop.push(id);
5032
5201
  });
5033
5202
  for (const id of toDrop) graph.dropEdge(id);
@@ -5067,7 +5236,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5067
5236
  }
5068
5237
  const droppedEntries = drainDroppedExtracted();
5069
5238
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5070
- const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
5239
+ const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
5071
5240
  try {
5072
5241
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5073
5242
  } catch (err) {
@@ -5102,8 +5271,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5102
5271
  // src/persist.ts
5103
5272
  init_cjs_shims();
5104
5273
  var import_node_fs19 = require("fs");
5105
- var import_node_path33 = __toESM(require("path"), 1);
5106
- var import_types21 = require("@neat.is/types");
5274
+ var import_node_path34 = __toESM(require("path"), 1);
5275
+ var import_types22 = require("@neat.is/types");
5107
5276
  var SCHEMA_VERSION = 4;
5108
5277
  function migrateV1ToV2(payload) {
5109
5278
  const nodes = payload.graph.nodes;
@@ -5125,12 +5294,12 @@ function migrateV2ToV3(payload) {
5125
5294
  for (const edge of edges) {
5126
5295
  const attrs = edge.attributes;
5127
5296
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5128
- attrs.provenance = import_types21.Provenance.OBSERVED;
5297
+ attrs.provenance = import_types22.Provenance.OBSERVED;
5129
5298
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5130
5299
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5131
5300
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5132
5301
  if (type && source && target) {
5133
- const newId = (0, import_types21.observedEdgeId)(source, target, type);
5302
+ const newId = (0, import_types22.observedEdgeId)(source, target, type);
5134
5303
  attrs.id = newId;
5135
5304
  if (edge.key) edge.key = newId;
5136
5305
  }
@@ -5139,7 +5308,7 @@ function migrateV2ToV3(payload) {
5139
5308
  return { ...payload, schemaVersion: 3 };
5140
5309
  }
5141
5310
  async function ensureDir(filePath) {
5142
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
5311
+ await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
5143
5312
  }
5144
5313
  async function saveGraphToDisk(graph, outPath) {
5145
5314
  await ensureDir(outPath);
@@ -5216,19 +5385,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5216
5385
  init_cjs_shims();
5217
5386
  var import_fastify = __toESM(require("fastify"), 1);
5218
5387
  var import_cors = __toESM(require("@fastify/cors"), 1);
5219
- var import_types24 = require("@neat.is/types");
5388
+ var import_types25 = require("@neat.is/types");
5220
5389
 
5221
5390
  // src/extend/index.ts
5222
5391
  init_cjs_shims();
5223
5392
  var import_node_fs21 = require("fs");
5224
- var import_node_path35 = __toESM(require("path"), 1);
5393
+ var import_node_path36 = __toESM(require("path"), 1);
5225
5394
  var import_node_os2 = __toESM(require("os"), 1);
5226
5395
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5227
5396
 
5228
5397
  // src/installers/package-manager.ts
5229
5398
  init_cjs_shims();
5230
5399
  var import_node_fs20 = require("fs");
5231
- var import_node_path34 = __toESM(require("path"), 1);
5400
+ var import_node_path35 = __toESM(require("path"), 1);
5232
5401
  var import_node_child_process = require("child_process");
5233
5402
  var LOCKFILE_PRIORITY = [
5234
5403
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5250,22 +5419,22 @@ async function exists2(p) {
5250
5419
  }
5251
5420
  }
5252
5421
  async function detectPackageManager(serviceDir) {
5253
- let dir = import_node_path34.default.resolve(serviceDir);
5422
+ let dir = import_node_path35.default.resolve(serviceDir);
5254
5423
  const stops = /* @__PURE__ */ new Set();
5255
5424
  for (let i = 0; i < 64; i++) {
5256
5425
  if (stops.has(dir)) break;
5257
5426
  stops.add(dir);
5258
5427
  for (const candidate of LOCKFILE_PRIORITY) {
5259
- const lockPath = import_node_path34.default.join(dir, candidate.lockfile);
5428
+ const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
5260
5429
  if (await exists2(lockPath)) {
5261
5430
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5262
5431
  }
5263
5432
  }
5264
- const parent = import_node_path34.default.dirname(dir);
5433
+ const parent = import_node_path35.default.dirname(dir);
5265
5434
  if (parent === dir) break;
5266
5435
  dir = parent;
5267
5436
  }
5268
- return { pm: "npm", cwd: import_node_path34.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5437
+ return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5269
5438
  }
5270
5439
  async function runPackageManagerInstall(cmd) {
5271
5440
  return new Promise((resolve) => {
@@ -5314,7 +5483,7 @@ async function fileExists2(p) {
5314
5483
  }
5315
5484
  }
5316
5485
  async function readPackageJson(scanPath) {
5317
- const pkgPath = import_node_path35.default.join(scanPath, "package.json");
5486
+ const pkgPath = import_node_path36.default.join(scanPath, "package.json");
5318
5487
  const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
5319
5488
  return JSON.parse(raw);
5320
5489
  }
@@ -5325,11 +5494,11 @@ async function findHookFiles(scanPath) {
5325
5494
  ).sort();
5326
5495
  }
5327
5496
  function extendLogPath() {
5328
- return process.env.NEAT_EXTEND_LOG ?? import_node_path35.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5497
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5329
5498
  }
5330
5499
  async function appendExtendLog(entry) {
5331
5500
  const logPath = extendLogPath();
5332
- await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(logPath), { recursive: true });
5501
+ await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
5333
5502
  await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5334
5503
  }
5335
5504
  function splicedContent(fileContent, snippet2) {
@@ -5388,7 +5557,7 @@ function lookupInstrumentation(library, installedVersion) {
5388
5557
  }
5389
5558
  async function describeProjectInstrumentation(ctx) {
5390
5559
  const hookFiles = await findHookFiles(ctx.scanPath);
5391
- const envNeat = await fileExists2(import_node_path35.default.join(ctx.scanPath, ".env.neat"));
5560
+ const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
5392
5561
  const registryInstrPackages = new Set(
5393
5562
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5394
5563
  );
@@ -5410,16 +5579,16 @@ async function applyExtension(ctx, args, options) {
5410
5579
  );
5411
5580
  }
5412
5581
  for (const file of hookFiles) {
5413
- const content = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, file), "utf8");
5582
+ const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5414
5583
  if (content.includes(args.registration_snippet)) {
5415
5584
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5416
5585
  }
5417
5586
  }
5418
5587
  const primaryFile = hookFiles[0];
5419
- const primaryPath = import_node_path35.default.join(ctx.scanPath, primaryFile);
5588
+ const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
5420
5589
  const filesTouched = [];
5421
5590
  const depsAdded = [];
5422
- const pkgPath = import_node_path35.default.join(ctx.scanPath, "package.json");
5591
+ const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5423
5592
  const pkg = await readPackageJson(ctx.scanPath);
5424
5593
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5425
5594
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5465,7 +5634,7 @@ async function dryRunExtension(ctx, args) {
5465
5634
  };
5466
5635
  }
5467
5636
  for (const file of hookFiles) {
5468
- const content = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, file), "utf8");
5637
+ const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5469
5638
  if (content.includes(args.registration_snippet)) {
5470
5639
  return {
5471
5640
  library: args.library,
@@ -5487,7 +5656,7 @@ async function dryRunExtension(ctx, args) {
5487
5656
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5488
5657
  filesTouched.push("package.json");
5489
5658
  }
5490
- const hookContent = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, primaryFile), "utf8");
5659
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
5491
5660
  const patched = splicedContent(hookContent, args.registration_snippet);
5492
5661
  if (patched) {
5493
5662
  filesTouched.push(primaryFile);
@@ -5508,7 +5677,7 @@ async function rollbackExtension(ctx, args) {
5508
5677
  if (!match) {
5509
5678
  return { undone: false, message: "no apply found for library" };
5510
5679
  }
5511
- const pkgPath = import_node_path35.default.join(ctx.scanPath, "package.json");
5680
+ const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5512
5681
  if (await fileExists2(pkgPath)) {
5513
5682
  const pkg = await readPackageJson(ctx.scanPath);
5514
5683
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5519,7 +5688,7 @@ async function rollbackExtension(ctx, args) {
5519
5688
  }
5520
5689
  const hookFiles = await findHookFiles(ctx.scanPath);
5521
5690
  for (const file of hookFiles) {
5522
- const filePath = import_node_path35.default.join(ctx.scanPath, file);
5691
+ const filePath = import_node_path36.default.join(ctx.scanPath, file);
5523
5692
  const content = await import_node_fs21.promises.readFile(filePath, "utf8");
5524
5693
  if (content.includes(match.registration_snippet)) {
5525
5694
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -5535,7 +5704,7 @@ async function rollbackExtension(ctx, args) {
5535
5704
 
5536
5705
  // src/divergences.ts
5537
5706
  init_cjs_shims();
5538
- var import_types22 = require("@neat.is/types");
5707
+ var import_types23 = require("@neat.is/types");
5539
5708
  function bucketKey(source, target, type) {
5540
5709
  return `${type}|${source}|${target}`;
5541
5710
  }
@@ -5543,22 +5712,22 @@ function bucketEdges(graph) {
5543
5712
  const buckets = /* @__PURE__ */ new Map();
5544
5713
  graph.forEachEdge((id, attrs) => {
5545
5714
  const e = attrs;
5546
- const parsed = (0, import_types22.parseEdgeId)(id);
5715
+ const parsed = (0, import_types23.parseEdgeId)(id);
5547
5716
  const provenance = parsed?.provenance ?? e.provenance;
5548
5717
  const key = bucketKey(e.source, e.target, e.type);
5549
5718
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5550
5719
  switch (provenance) {
5551
- case import_types22.Provenance.EXTRACTED:
5720
+ case import_types23.Provenance.EXTRACTED:
5552
5721
  cur.extracted = e;
5553
5722
  break;
5554
- case import_types22.Provenance.OBSERVED:
5723
+ case import_types23.Provenance.OBSERVED:
5555
5724
  cur.observed = e;
5556
5725
  break;
5557
- case import_types22.Provenance.INFERRED:
5726
+ case import_types23.Provenance.INFERRED:
5558
5727
  cur.inferred = e;
5559
5728
  break;
5560
5729
  default:
5561
- if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
5730
+ if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
5562
5731
  }
5563
5732
  buckets.set(key, cur);
5564
5733
  });
@@ -5567,7 +5736,7 @@ function bucketEdges(graph) {
5567
5736
  function nodeIsFrontier(graph, nodeId) {
5568
5737
  if (!graph.hasNode(nodeId)) return false;
5569
5738
  const attrs = graph.getNodeAttributes(nodeId);
5570
- return attrs.type === import_types22.NodeType.FrontierNode;
5739
+ return attrs.type === import_types23.NodeType.FrontierNode;
5571
5740
  }
5572
5741
  function clampConfidence(n) {
5573
5742
  if (!Number.isFinite(n)) return 0;
@@ -5588,7 +5757,7 @@ function gradedConfidence(edge) {
5588
5757
  }
5589
5758
  function detectMissingDivergences(graph, bucket) {
5590
5759
  const out = [];
5591
- if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5760
+ if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5592
5761
  if (bucket.extracted && !bucket.observed) {
5593
5762
  if (!nodeIsFrontier(graph, bucket.target)) {
5594
5763
  out.push({
@@ -5629,7 +5798,7 @@ function declaredHostFor(svc) {
5629
5798
  function hasExtractedConfiguredBy(graph, svcId) {
5630
5799
  for (const edgeId of graph.outboundEdges(svcId)) {
5631
5800
  const e = graph.getEdgeAttributes(edgeId);
5632
- if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
5801
+ if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
5633
5802
  return true;
5634
5803
  }
5635
5804
  }
@@ -5642,10 +5811,10 @@ function detectHostMismatch(graph, svcId, svc) {
5642
5811
  const out = [];
5643
5812
  for (const edgeId of graph.outboundEdges(svcId)) {
5644
5813
  const edge = graph.getEdgeAttributes(edgeId);
5645
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5646
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5814
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5815
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5647
5816
  const target = graph.getNodeAttributes(edge.target);
5648
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5817
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5649
5818
  const observedHost = target.host?.trim();
5650
5819
  if (!observedHost) continue;
5651
5820
  if (observedHost === declaredHost) continue;
@@ -5667,10 +5836,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5667
5836
  const deps = svc.dependencies ?? {};
5668
5837
  for (const edgeId of graph.outboundEdges(svcId)) {
5669
5838
  const edge = graph.getEdgeAttributes(edgeId);
5670
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5671
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5839
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5840
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5672
5841
  const target = graph.getNodeAttributes(edge.target);
5673
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5842
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5674
5843
  for (const pair of compatPairs()) {
5675
5844
  if (pair.engine !== target.engine) continue;
5676
5845
  const declared = deps[pair.driver];
@@ -5731,7 +5900,7 @@ function computeDivergences(graph, opts = {}) {
5731
5900
  }
5732
5901
  graph.forEachNode((nodeId, attrs) => {
5733
5902
  const n = attrs;
5734
- if (n.type !== import_types22.NodeType.ServiceNode) return;
5903
+ if (n.type !== import_types23.NodeType.ServiceNode) return;
5735
5904
  const svc = n;
5736
5905
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5737
5906
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5764,7 +5933,7 @@ function computeDivergences(graph, opts = {}) {
5764
5933
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5765
5934
  return a.target.localeCompare(b.target);
5766
5935
  });
5767
- return import_types22.DivergenceResultSchema.parse({
5936
+ return import_types23.DivergenceResultSchema.parse({
5768
5937
  divergences: filtered,
5769
5938
  totalAffected: filtered.length,
5770
5939
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5850,23 +6019,23 @@ function canonicalJson(value) {
5850
6019
 
5851
6020
  // src/projects.ts
5852
6021
  init_cjs_shims();
5853
- var import_node_path36 = __toESM(require("path"), 1);
6022
+ var import_node_path37 = __toESM(require("path"), 1);
5854
6023
  function pathsForProject(project, baseDir) {
5855
6024
  if (project === DEFAULT_PROJECT) {
5856
6025
  return {
5857
- snapshotPath: import_node_path36.default.join(baseDir, "graph.json"),
5858
- errorsPath: import_node_path36.default.join(baseDir, "errors.ndjson"),
5859
- staleEventsPath: import_node_path36.default.join(baseDir, "stale-events.ndjson"),
5860
- embeddingsCachePath: import_node_path36.default.join(baseDir, "embeddings.json"),
5861
- policyViolationsPath: import_node_path36.default.join(baseDir, "policy-violations.ndjson")
6026
+ snapshotPath: import_node_path37.default.join(baseDir, "graph.json"),
6027
+ errorsPath: import_node_path37.default.join(baseDir, "errors.ndjson"),
6028
+ staleEventsPath: import_node_path37.default.join(baseDir, "stale-events.ndjson"),
6029
+ embeddingsCachePath: import_node_path37.default.join(baseDir, "embeddings.json"),
6030
+ policyViolationsPath: import_node_path37.default.join(baseDir, "policy-violations.ndjson")
5862
6031
  };
5863
6032
  }
5864
6033
  return {
5865
- snapshotPath: import_node_path36.default.join(baseDir, `${project}.json`),
5866
- errorsPath: import_node_path36.default.join(baseDir, `errors.${project}.ndjson`),
5867
- staleEventsPath: import_node_path36.default.join(baseDir, `stale-events.${project}.ndjson`),
5868
- embeddingsCachePath: import_node_path36.default.join(baseDir, `embeddings.${project}.json`),
5869
- policyViolationsPath: import_node_path36.default.join(baseDir, `policy-violations.${project}.ndjson`)
6034
+ snapshotPath: import_node_path37.default.join(baseDir, `${project}.json`),
6035
+ errorsPath: import_node_path37.default.join(baseDir, `errors.${project}.ndjson`),
6036
+ staleEventsPath: import_node_path37.default.join(baseDir, `stale-events.${project}.ndjson`),
6037
+ embeddingsCachePath: import_node_path37.default.join(baseDir, `embeddings.${project}.json`),
6038
+ policyViolationsPath: import_node_path37.default.join(baseDir, `policy-violations.${project}.ndjson`)
5870
6039
  };
5871
6040
  }
5872
6041
  var Projects = class {
@@ -5904,23 +6073,23 @@ var Projects = class {
5904
6073
  init_cjs_shims();
5905
6074
  var import_node_fs23 = require("fs");
5906
6075
  var import_node_os3 = __toESM(require("os"), 1);
5907
- var import_node_path37 = __toESM(require("path"), 1);
5908
- var import_types23 = require("@neat.is/types");
6076
+ var import_node_path38 = __toESM(require("path"), 1);
6077
+ var import_types24 = require("@neat.is/types");
5909
6078
  var LOCK_TIMEOUT_MS = 5e3;
5910
6079
  var LOCK_RETRY_MS = 50;
5911
6080
  function neatHome() {
5912
6081
  const override = process.env.NEAT_HOME;
5913
- if (override && override.length > 0) return import_node_path37.default.resolve(override);
5914
- return import_node_path37.default.join(import_node_os3.default.homedir(), ".neat");
6082
+ if (override && override.length > 0) return import_node_path38.default.resolve(override);
6083
+ return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
5915
6084
  }
5916
6085
  function registryPath() {
5917
- return import_node_path37.default.join(neatHome(), "projects.json");
6086
+ return import_node_path38.default.join(neatHome(), "projects.json");
5918
6087
  }
5919
6088
  function registryLockPath() {
5920
- return import_node_path37.default.join(neatHome(), "projects.json.lock");
6089
+ return import_node_path38.default.join(neatHome(), "projects.json.lock");
5921
6090
  }
5922
6091
  function daemonPidPath() {
5923
- return import_node_path37.default.join(neatHome(), "neatd.pid");
6092
+ return import_node_path38.default.join(neatHome(), "neatd.pid");
5924
6093
  }
5925
6094
  function isPidAliveDefault(pid) {
5926
6095
  try {
@@ -5980,7 +6149,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5980
6149
  }
5981
6150
  }
5982
6151
  async function normalizeProjectPath(input) {
5983
- const resolved = import_node_path37.default.resolve(input);
6152
+ const resolved = import_node_path38.default.resolve(input);
5984
6153
  try {
5985
6154
  return await import_node_fs23.promises.realpath(resolved);
5986
6155
  } catch {
@@ -5988,7 +6157,7 @@ async function normalizeProjectPath(input) {
5988
6157
  }
5989
6158
  }
5990
6159
  async function writeAtomically(target, contents) {
5991
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6160
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
5992
6161
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5993
6162
  const fd = await import_node_fs23.promises.open(tmp, "w");
5994
6163
  try {
@@ -6001,7 +6170,7 @@ async function writeAtomically(target, contents) {
6001
6170
  }
6002
6171
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6003
6172
  const deadline = Date.now() + timeoutMs;
6004
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(lockPath), { recursive: true });
6173
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
6005
6174
  let probedHolder = false;
6006
6175
  while (true) {
6007
6176
  try {
@@ -6054,10 +6223,10 @@ async function readRegistry() {
6054
6223
  throw err;
6055
6224
  }
6056
6225
  const parsed = JSON.parse(raw);
6057
- return import_types23.RegistryFileSchema.parse(parsed);
6226
+ return import_types24.RegistryFileSchema.parse(parsed);
6058
6227
  }
6059
6228
  async function writeRegistry(reg) {
6060
- const validated = import_types23.RegistryFileSchema.parse(reg);
6229
+ const validated = import_types24.RegistryFileSchema.parse(reg);
6061
6230
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6062
6231
  }
6063
6232
  var ProjectNameCollisionError = class extends Error {
@@ -6137,6 +6306,58 @@ async function removeProject(name) {
6137
6306
  return removed;
6138
6307
  });
6139
6308
  }
6309
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
6310
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
6311
+ function pruneTtlMs() {
6312
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
6313
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
6314
+ const n = Number.parseInt(raw, 10);
6315
+ if (Number.isFinite(n) && n >= 0) return n;
6316
+ console.warn(
6317
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
6318
+ );
6319
+ return DEFAULT_PRUNE_TTL_MS;
6320
+ }
6321
+ async function statPathStatus(p) {
6322
+ try {
6323
+ const stat = await import_node_fs23.promises.stat(p);
6324
+ return stat.isDirectory() ? "present" : "unknown";
6325
+ } catch (err) {
6326
+ return err.code === "ENOENT" ? "gone" : "unknown";
6327
+ }
6328
+ }
6329
+ async function pruneRegistry(opts = {}) {
6330
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
6331
+ const statPath = opts.statPath ?? statPathStatus;
6332
+ const now = opts.now ?? Date.now;
6333
+ return withLock(async () => {
6334
+ const reg = await readRegistry();
6335
+ const removed = [];
6336
+ const kept = [];
6337
+ for (const entry of reg.projects) {
6338
+ const status2 = await statPath(entry.path);
6339
+ if (status2 !== "gone") {
6340
+ kept.push(entry);
6341
+ continue;
6342
+ }
6343
+ if (ttlMs <= 0) {
6344
+ removed.push(entry);
6345
+ continue;
6346
+ }
6347
+ const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt);
6348
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
6349
+ if (age > ttlMs) {
6350
+ removed.push(entry);
6351
+ } else {
6352
+ kept.push(entry);
6353
+ }
6354
+ }
6355
+ if (removed.length === 0) return [];
6356
+ reg.projects = kept;
6357
+ await writeRegistry(reg);
6358
+ return removed;
6359
+ });
6360
+ }
6140
6361
 
6141
6362
  // src/streaming.ts
6142
6363
  init_cjs_shims();
@@ -6150,6 +6371,7 @@ function handleSse(req, reply, opts) {
6150
6371
  reply.raw.setHeader("Connection", "keep-alive");
6151
6372
  reply.raw.setHeader("X-Accel-Buffering", "no");
6152
6373
  reply.raw.flushHeaders?.();
6374
+ reply.raw.write(":open\n\n");
6153
6375
  let pending = 0;
6154
6376
  let dropped = false;
6155
6377
  const closeConnection = () => {
@@ -6329,11 +6551,11 @@ function registerRoutes(scope, ctx) {
6329
6551
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6330
6552
  const parsed = [];
6331
6553
  for (const c of candidates) {
6332
- const r = import_types24.DivergenceTypeSchema.safeParse(c);
6554
+ const r = import_types25.DivergenceTypeSchema.safeParse(c);
6333
6555
  if (!r.success) {
6334
6556
  return reply.code(400).send({
6335
6557
  error: `unknown divergence type "${c}"`,
6336
- allowed: import_types24.DivergenceTypeSchema.options
6558
+ allowed: import_types25.DivergenceTypeSchema.options
6337
6559
  });
6338
6560
  }
6339
6561
  parsed.push(r.data);
@@ -6546,7 +6768,7 @@ function registerRoutes(scope, ctx) {
6546
6768
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6547
6769
  let violations = await log.readAll();
6548
6770
  if (req.query.severity) {
6549
- const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
6771
+ const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
6550
6772
  if (!sev.success) {
6551
6773
  return reply.code(400).send({
6552
6774
  error: "invalid severity",
@@ -6563,7 +6785,7 @@ function registerRoutes(scope, ctx) {
6563
6785
  scope.post("/policies/check", async (req, reply) => {
6564
6786
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6565
6787
  if (!proj) return;
6566
- const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6788
+ const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6567
6789
  if (!parsed.success) {
6568
6790
  return reply.code(400).send({
6569
6791
  error: "invalid /policies/check body",
@@ -6806,14 +7028,14 @@ init_otel_grpc();
6806
7028
  // src/daemon.ts
6807
7029
  init_cjs_shims();
6808
7030
  var import_node_fs25 = require("fs");
6809
- var import_node_path41 = __toESM(require("path"), 1);
7031
+ var import_node_path42 = __toESM(require("path"), 1);
6810
7032
  init_otel();
6811
7033
  init_auth();
6812
7034
 
6813
7035
  // src/unrouted.ts
6814
7036
  init_cjs_shims();
6815
7037
  var import_node_fs24 = require("fs");
6816
- var import_node_path40 = __toESM(require("path"), 1);
7038
+ var import_node_path41 = __toESM(require("path"), 1);
6817
7039
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
6818
7040
  return {
6819
7041
  timestamp: now.toISOString(),
@@ -6823,12 +7045,12 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
6823
7045
  };
6824
7046
  }
6825
7047
  async function appendUnroutedSpan(neatHome2, record) {
6826
- const target = import_node_path40.default.join(neatHome2, "errors.ndjson");
7048
+ const target = import_node_path41.default.join(neatHome2, "errors.ndjson");
6827
7049
  await import_node_fs24.promises.mkdir(neatHome2, { recursive: true });
6828
7050
  await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6829
7051
  }
6830
7052
  function unroutedErrorsPath(neatHome2) {
6831
- return import_node_path40.default.join(neatHome2, "errors.ndjson");
7053
+ return import_node_path41.default.join(neatHome2, "errors.ndjson");
6832
7054
  }
6833
7055
 
6834
7056
  // src/daemon.ts
@@ -6843,11 +7065,11 @@ function teardownSlot(slot) {
6843
7065
  }
6844
7066
  }
6845
7067
  function neatHomeFor(opts) {
6846
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path41.default.resolve(opts.neatHome);
7068
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
6847
7069
  const env = process.env.NEAT_HOME;
6848
- if (env && env.length > 0) return import_node_path41.default.resolve(env);
7070
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
6849
7071
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6850
- return import_node_path41.default.join(home, ".neat");
7072
+ return import_node_path42.default.join(home, ".neat");
6851
7073
  }
6852
7074
  function routeSpanToProject(serviceName, projects) {
6853
7075
  if (!serviceName) return DEFAULT_PROJECT;
@@ -6882,7 +7104,7 @@ function isTokenContained(needle, haystack) {
6882
7104
  return tokens.includes(needle);
6883
7105
  }
6884
7106
  async function bootstrapProject(entry) {
6885
- const paths = pathsForProject(entry.name, import_node_path41.default.join(entry.path, "neat-out"));
7107
+ const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
6886
7108
  try {
6887
7109
  const stat = await import_node_fs25.promises.stat(entry.path);
6888
7110
  if (!stat.isDirectory()) {
@@ -6965,7 +7187,7 @@ async function startDaemon(opts = {}) {
6965
7187
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
6966
7188
  );
6967
7189
  }
6968
- const pidPath = import_node_path41.default.join(home, "neatd.pid");
7190
+ const pidPath = import_node_path42.default.join(home, "neatd.pid");
6969
7191
  await writeAtomically(pidPath, `${process.pid}
6970
7192
  `);
6971
7193
  const slots = /* @__PURE__ */ new Map();
@@ -7054,6 +7276,19 @@ async function startDaemon(opts = {}) {
7054
7276
  }
7055
7277
  }
7056
7278
  async function loadAll() {
7279
+ try {
7280
+ const pruned = await pruneRegistry();
7281
+ for (const entry of pruned) {
7282
+ console.log(
7283
+ `neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
7284
+ );
7285
+ slots.delete(entry.name);
7286
+ bootstrapStatus.delete(entry.name);
7287
+ bootstrapStartedAt.delete(entry.name);
7288
+ }
7289
+ } catch (err) {
7290
+ console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
7291
+ }
7057
7292
  const projects = await listProjects();
7058
7293
  const seen = /* @__PURE__ */ new Set();
7059
7294
  const pending = [];
@@ -7270,8 +7505,8 @@ async function startDaemon(opts = {}) {
7270
7505
  let registryWatcher = null;
7271
7506
  let reloadTimer = null;
7272
7507
  try {
7273
- const regDir = import_node_path41.default.dirname(regPath);
7274
- const regBase = import_node_path41.default.basename(regPath);
7508
+ const regDir = import_node_path42.default.dirname(regPath);
7509
+ const regBase = import_node_path42.default.basename(regPath);
7275
7510
  registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
7276
7511
  if (filename !== null && filename !== regBase) return;
7277
7512
  if (reloadTimer) clearTimeout(reloadTimer);