@neat.is/core 0.4.16 → 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/neatd.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", (req2, reply, done) => {
60
- const path44 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
60
+ const path45 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
61
61
  for (const suffix of suffixes) {
62
- if (path44 === suffix || path44.endsWith(suffix)) {
62
+ if (path45 === suffix || path45.endsWith(suffix)) {
63
63
  done();
64
64
  return;
65
65
  }
@@ -187,8 +187,8 @@ function reshapeGrpcRequest(req2) {
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 }
@@ -571,12 +571,12 @@ async function buildOtelReceiver(opts) {
571
571
  };
572
572
  return decorated;
573
573
  }
574
- var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
574
+ var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
575
575
  var init_otel = __esm({
576
576
  "src/otel.ts"() {
577
577
  "use strict";
578
578
  init_cjs_shims();
579
- import_node_path39 = __toESM(require("path"), 1);
579
+ import_node_path40 = __toESM(require("path"), 1);
580
580
  import_node_url2 = require("url");
581
581
  import_fastify2 = __toESM(require("fastify"), 1);
582
582
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -593,13 +593,13 @@ var init_otel = __esm({
593
593
  // src/neatd.ts
594
594
  init_cjs_shims();
595
595
  var import_node_fs26 = require("fs");
596
- var import_node_path43 = __toESM(require("path"), 1);
596
+ var import_node_path44 = __toESM(require("path"), 1);
597
597
  var import_node_module = require("module");
598
598
 
599
599
  // src/daemon.ts
600
600
  init_cjs_shims();
601
601
  var import_node_fs25 = require("fs");
602
- var import_node_path41 = __toESM(require("path"), 1);
602
+ var import_node_path42 = __toESM(require("path"), 1);
603
603
 
604
604
  // src/graph.ts
605
605
  init_cjs_shims();
@@ -1128,19 +1128,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1128
1128
  function longestIncomingWalk(graph, start, maxDepth) {
1129
1129
  let best = { path: [start], edges: [] };
1130
1130
  const visited = /* @__PURE__ */ new Set([start]);
1131
- function step(node, path44, edges) {
1132
- if (path44.length > best.path.length) {
1133
- best = { path: [...path44], edges: [...edges] };
1131
+ function step(node, path45, edges) {
1132
+ if (path45.length > best.path.length) {
1133
+ best = { path: [...path45], edges: [...edges] };
1134
1134
  }
1135
- if (path44.length - 1 >= maxDepth) return;
1135
+ if (path45.length - 1 >= maxDepth) return;
1136
1136
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1137
1137
  for (const [srcId, edge] of incoming) {
1138
1138
  if (visited.has(srcId)) continue;
1139
1139
  visited.add(srcId);
1140
- path44.push(srcId);
1140
+ path45.push(srcId);
1141
1141
  edges.push(edge);
1142
- step(srcId, path44, edges);
1143
- path44.pop();
1142
+ step(srcId, path45, edges);
1143
+ path45.pop();
1144
1144
  edges.pop();
1145
1145
  visited.delete(srcId);
1146
1146
  }
@@ -1658,6 +1658,41 @@ var DEFAULT_STALE_THRESHOLDS = {
1658
1658
  CONFIGURED_BY: DAY_MS,
1659
1659
  RUNS_ON: DAY_MS
1660
1660
  };
1661
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1662
+ threshold: 5,
1663
+ windowMs: 6e4
1664
+ };
1665
+ function loadIncidentThresholdsFromEnv() {
1666
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1667
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1668
+ try {
1669
+ const overrides = JSON.parse(raw);
1670
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1671
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1672
+ merged.threshold = Math.floor(overrides.threshold);
1673
+ }
1674
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1675
+ merged.windowMs = overrides.windowMs;
1676
+ }
1677
+ return merged;
1678
+ } catch (err) {
1679
+ console.warn(
1680
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1681
+ );
1682
+ return DEFAULT_INCIDENT_THRESHOLDS;
1683
+ }
1684
+ }
1685
+ function httpResponseStatus(span) {
1686
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1687
+ const v = span.attributes[key];
1688
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1689
+ if (typeof v === "string") {
1690
+ const n = Number(v);
1691
+ if (Number.isFinite(n)) return n;
1692
+ }
1693
+ }
1694
+ return void 0;
1695
+ }
1661
1696
  function nowIso(ctx) {
1662
1697
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1663
1698
  }
@@ -2071,6 +2106,71 @@ function makeErrorSpanWriter(errorsPath) {
2071
2106
  await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
2072
2107
  };
2073
2108
  }
2109
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
2110
+ const attrs = sanitizeAttributes(span.attributes);
2111
+ const first = firstTimestamp ?? timestamp;
2112
+ const peer = pickAddress(span);
2113
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
2114
+ const ev = {
2115
+ id: `${span.traceId}:${span.spanId}`,
2116
+ timestamp,
2117
+ service: span.service,
2118
+ traceId: span.traceId,
2119
+ spanId: span.spanId,
2120
+ errorType: "http-failure",
2121
+ errorMessage: message,
2122
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2123
+ affectedNode,
2124
+ httpStatusCode: statusCode,
2125
+ incidentCount: count,
2126
+ firstTimestamp: first,
2127
+ lastTimestamp: timestamp
2128
+ };
2129
+ await appendErrorEvent(ctx, ev);
2130
+ }
2131
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2132
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2133
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
2134
+ const peer = pickAddress(span) ?? span.spanId;
2135
+ const key = `${span.service}->${peer}`;
2136
+ const existing = ctx.burstState.get(key);
2137
+ let state;
2138
+ if (existing && nowMs - existing.lastMs <= windowMs) {
2139
+ existing.count += 1;
2140
+ existing.lastTs = ts;
2141
+ existing.lastMs = nowMs;
2142
+ existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
2143
+ state = existing;
2144
+ } else {
2145
+ state = {
2146
+ count: 1,
2147
+ firstTs: ts,
2148
+ lastTs: ts,
2149
+ lastMs: nowMs,
2150
+ codes: /* @__PURE__ */ new Map([[status2, 1]])
2151
+ };
2152
+ ctx.burstState.set(key, state);
2153
+ }
2154
+ if (state.count < threshold) return;
2155
+ let dominant = status2;
2156
+ let max = 0;
2157
+ for (const [code, n] of state.codes) {
2158
+ if (n > max) {
2159
+ max = n;
2160
+ dominant = code;
2161
+ }
2162
+ }
2163
+ await recordFailingResponseIncident(
2164
+ ctx,
2165
+ span,
2166
+ affectedNode,
2167
+ state.lastTs,
2168
+ dominant,
2169
+ state.count,
2170
+ state.firstTs
2171
+ );
2172
+ ctx.burstState.delete(key);
2173
+ }
2074
2174
  async function handleSpan(ctx, span) {
2075
2175
  const ts = span.startTimeIso ?? nowIso(ctx);
2076
2176
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -2169,6 +2269,14 @@ async function handleSpan(ctx, span) {
2169
2269
  await appendErrorEvent(ctx, ev);
2170
2270
  }
2171
2271
  }
2272
+ if (span.statusCode !== 2) {
2273
+ const status2 = httpResponseStatus(span);
2274
+ if (status2 !== void 0 && status2 >= 500) {
2275
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2276
+ } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2277
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
2278
+ }
2279
+ }
2172
2280
  void affectedNode;
2173
2281
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2174
2282
  }
@@ -4115,7 +4223,7 @@ async function addConfigNodes(graph, services, scanPath) {
4115
4223
 
4116
4224
  // src/extract/calls/index.ts
4117
4225
  init_cjs_shims();
4118
- var import_types16 = require("@neat.is/types");
4226
+ var import_types17 = require("@neat.is/types");
4119
4227
 
4120
4228
  // src/extract/calls/http.ts
4121
4229
  init_cjs_shims();
@@ -4468,15 +4576,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
4468
4576
  return out;
4469
4577
  }
4470
4578
 
4579
+ // src/extract/calls/supabase.ts
4580
+ init_cjs_shims();
4581
+ var import_node_path27 = __toESM(require("path"), 1);
4582
+ var import_types16 = require("@neat.is/types");
4583
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4584
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4585
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4586
+ function hostFromLiteral(literal) {
4587
+ if (!literal) return null;
4588
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4589
+ if (!m) return null;
4590
+ const host = m[1];
4591
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4592
+ return host;
4593
+ }
4594
+ function readImports2(content) {
4595
+ return {
4596
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4597
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4598
+ };
4599
+ }
4600
+ function constructorMatchesImport(name, ctx) {
4601
+ if (name === "createClient") return ctx.hasSupabaseJs;
4602
+ return ctx.hasSupabaseSsr;
4603
+ }
4604
+ function supabaseEndpointsFromFile(file, serviceDir) {
4605
+ const ctx = readImports2(file.content);
4606
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4607
+ const out = [];
4608
+ const seen = /* @__PURE__ */ new Set();
4609
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4610
+ let m;
4611
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4612
+ const ctor = m[1];
4613
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4614
+ const host = hostFromLiteral(m[2]);
4615
+ const name = host ?? "env";
4616
+ if (seen.has(name)) continue;
4617
+ seen.add(name);
4618
+ const line = lineOf(file.content, m[0]);
4619
+ out.push({
4620
+ infraId: (0, import_types16.infraId)("supabase", name),
4621
+ name,
4622
+ kind: "supabase",
4623
+ edgeType: "CALLS",
4624
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4625
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4626
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4627
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4628
+ confidenceKind: "verified-call-site",
4629
+ evidence: {
4630
+ file: import_node_path27.default.relative(serviceDir, file.path),
4631
+ line,
4632
+ snippet: snippet(file.content, line)
4633
+ }
4634
+ });
4635
+ }
4636
+ return out;
4637
+ }
4638
+
4471
4639
  // src/extract/calls/index.ts
4472
4640
  function edgeTypeFromEndpoint(ep) {
4473
4641
  switch (ep.edgeType) {
4474
4642
  case "PUBLISHES_TO":
4475
- return import_types16.EdgeType.PUBLISHES_TO;
4643
+ return import_types17.EdgeType.PUBLISHES_TO;
4476
4644
  case "CONSUMES_FROM":
4477
- return import_types16.EdgeType.CONSUMES_FROM;
4645
+ return import_types17.EdgeType.CONSUMES_FROM;
4478
4646
  default:
4479
- return import_types16.EdgeType.CALLS;
4647
+ return import_types17.EdgeType.CALLS;
4480
4648
  }
4481
4649
  }
4482
4650
  function isAwsKind(kind) {
@@ -4496,6 +4664,7 @@ async function addExternalEndpointEdges(graph, services) {
4496
4664
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4497
4665
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4498
4666
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4667
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4499
4668
  }
4500
4669
  if (endpoints.length === 0) continue;
4501
4670
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4503,7 +4672,7 @@ async function addExternalEndpointEdges(graph, services) {
4503
4672
  if (!graph.hasNode(ep.infraId)) {
4504
4673
  const node = {
4505
4674
  id: ep.infraId,
4506
- type: import_types16.NodeType.InfraNode,
4675
+ type: import_types17.NodeType.InfraNode,
4507
4676
  name: ep.name,
4508
4677
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4509
4678
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4515,7 +4684,7 @@ async function addExternalEndpointEdges(graph, services) {
4515
4684
  nodesAdded++;
4516
4685
  }
4517
4686
  const edgeType = edgeTypeFromEndpoint(ep);
4518
- const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4687
+ const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
4519
4688
  const relFile = toPosix2(ep.evidence.file);
4520
4689
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4521
4690
  graph,
@@ -4525,7 +4694,7 @@ async function addExternalEndpointEdges(graph, services) {
4525
4694
  );
4526
4695
  nodesAdded += n;
4527
4696
  edgesAdded += e;
4528
- if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4697
+ if (!(0, import_types17.passesExtractedFloor)(confidence)) {
4529
4698
  noteExtractedDropped({
4530
4699
  source: fileNodeId,
4531
4700
  target: ep.infraId,
@@ -4545,7 +4714,7 @@ async function addExternalEndpointEdges(graph, services) {
4545
4714
  source: fileNodeId,
4546
4715
  target: ep.infraId,
4547
4716
  type: edgeType,
4548
- provenance: import_types16.Provenance.EXTRACTED,
4717
+ provenance: import_types17.Provenance.EXTRACTED,
4549
4718
  confidence,
4550
4719
  evidence: ep.evidence
4551
4720
  };
@@ -4570,16 +4739,16 @@ init_cjs_shims();
4570
4739
 
4571
4740
  // src/extract/infra/docker-compose.ts
4572
4741
  init_cjs_shims();
4573
- var import_node_path27 = __toESM(require("path"), 1);
4574
- var import_types18 = require("@neat.is/types");
4742
+ var import_node_path28 = __toESM(require("path"), 1);
4743
+ var import_types19 = require("@neat.is/types");
4575
4744
 
4576
4745
  // src/extract/infra/shared.ts
4577
4746
  init_cjs_shims();
4578
- var import_types17 = require("@neat.is/types");
4747
+ var import_types18 = require("@neat.is/types");
4579
4748
  function makeInfraNode(kind, name, provider = "self", extras) {
4580
4749
  return {
4581
- id: (0, import_types17.infraId)(kind, name),
4582
- type: import_types17.NodeType.InfraNode,
4750
+ id: (0, import_types18.infraId)(kind, name),
4751
+ type: import_types18.NodeType.InfraNode,
4583
4752
  name,
4584
4753
  provider,
4585
4754
  kind,
@@ -4608,7 +4777,7 @@ function dependsOnList(value) {
4608
4777
  }
4609
4778
  function serviceNameToServiceNode(name, services) {
4610
4779
  for (const s of services) {
4611
- if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4780
+ if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
4612
4781
  }
4613
4782
  return null;
4614
4783
  }
@@ -4617,7 +4786,7 @@ async function addComposeInfra(graph, scanPath, services) {
4617
4786
  let edgesAdded = 0;
4618
4787
  let composePath = null;
4619
4788
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4620
- const abs = import_node_path27.default.join(scanPath, name);
4789
+ const abs = import_node_path28.default.join(scanPath, name);
4621
4790
  if (await exists(abs)) {
4622
4791
  composePath = abs;
4623
4792
  break;
@@ -4630,13 +4799,13 @@ async function addComposeInfra(graph, scanPath, services) {
4630
4799
  } catch (err) {
4631
4800
  recordExtractionError(
4632
4801
  "infra docker-compose",
4633
- import_node_path27.default.relative(scanPath, composePath),
4802
+ import_node_path28.default.relative(scanPath, composePath),
4634
4803
  err
4635
4804
  );
4636
4805
  return { nodesAdded, edgesAdded };
4637
4806
  }
4638
4807
  if (!compose?.services) return { nodesAdded, edgesAdded };
4639
- const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4808
+ const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
4640
4809
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4641
4810
  for (const [composeName, svc] of Object.entries(compose.services)) {
4642
4811
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4658,15 +4827,15 @@ async function addComposeInfra(graph, scanPath, services) {
4658
4827
  for (const dep of dependsOnList(svc.depends_on)) {
4659
4828
  const targetId = composeNameToNodeId.get(dep);
4660
4829
  if (!targetId) continue;
4661
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4830
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
4662
4831
  if (graph.hasEdge(edgeId)) continue;
4663
4832
  const edge = {
4664
4833
  id: edgeId,
4665
4834
  source: sourceId,
4666
4835
  target: targetId,
4667
- type: import_types18.EdgeType.DEPENDS_ON,
4668
- provenance: import_types18.Provenance.EXTRACTED,
4669
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4836
+ type: import_types19.EdgeType.DEPENDS_ON,
4837
+ provenance: import_types19.Provenance.EXTRACTED,
4838
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4670
4839
  evidence: { file: evidenceFile }
4671
4840
  };
4672
4841
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4678,9 +4847,9 @@ async function addComposeInfra(graph, scanPath, services) {
4678
4847
 
4679
4848
  // src/extract/infra/dockerfile.ts
4680
4849
  init_cjs_shims();
4681
- var import_node_path28 = __toESM(require("path"), 1);
4850
+ var import_node_path29 = __toESM(require("path"), 1);
4682
4851
  var import_node_fs15 = require("fs");
4683
- var import_types19 = require("@neat.is/types");
4852
+ var import_types20 = require("@neat.is/types");
4684
4853
  function runtimeImage(content) {
4685
4854
  const lines = content.split("\n");
4686
4855
  let last = null;
@@ -4699,7 +4868,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4699
4868
  let nodesAdded = 0;
4700
4869
  let edgesAdded = 0;
4701
4870
  for (const service of services) {
4702
- const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
4871
+ const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
4703
4872
  if (!await exists(dockerfilePath)) continue;
4704
4873
  let content;
4705
4874
  try {
@@ -4707,7 +4876,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4707
4876
  } catch (err) {
4708
4877
  recordExtractionError(
4709
4878
  "infra dockerfile",
4710
- import_node_path28.default.relative(scanPath, dockerfilePath),
4879
+ import_node_path29.default.relative(scanPath, dockerfilePath),
4711
4880
  err
4712
4881
  );
4713
4882
  continue;
@@ -4719,7 +4888,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4719
4888
  graph.addNode(node.id, node);
4720
4889
  nodesAdded++;
4721
4890
  }
4722
- const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
4891
+ const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
4723
4892
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4724
4893
  graph,
4725
4894
  service.pkg.name,
@@ -4728,17 +4897,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4728
4897
  );
4729
4898
  nodesAdded += fn;
4730
4899
  edgesAdded += fe;
4731
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
4900
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
4732
4901
  if (!graph.hasEdge(edgeId)) {
4733
4902
  const edge = {
4734
4903
  id: edgeId,
4735
4904
  source: fileNodeId,
4736
4905
  target: node.id,
4737
- type: import_types19.EdgeType.RUNS_ON,
4738
- provenance: import_types19.Provenance.EXTRACTED,
4739
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
4906
+ type: import_types20.EdgeType.RUNS_ON,
4907
+ provenance: import_types20.Provenance.EXTRACTED,
4908
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
4740
4909
  evidence: {
4741
- file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
4910
+ file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
4742
4911
  }
4743
4912
  };
4744
4913
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4751,7 +4920,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4751
4920
  // src/extract/infra/terraform.ts
4752
4921
  init_cjs_shims();
4753
4922
  var import_node_fs16 = require("fs");
4754
- var import_node_path29 = __toESM(require("path"), 1);
4923
+ var import_node_path30 = __toESM(require("path"), 1);
4755
4924
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4756
4925
  async function walkTfFiles(start, depth = 0, max = 5) {
4757
4926
  if (depth > max) return [];
@@ -4760,11 +4929,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4760
4929
  for (const entry2 of entries) {
4761
4930
  if (entry2.isDirectory()) {
4762
4931
  if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
4763
- const child = import_node_path29.default.join(start, entry2.name);
4932
+ const child = import_node_path30.default.join(start, entry2.name);
4764
4933
  if (await isPythonVenvDir(child)) continue;
4765
4934
  out.push(...await walkTfFiles(child, depth + 1, max));
4766
4935
  } else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
4767
- out.push(import_node_path29.default.join(start, entry2.name));
4936
+ out.push(import_node_path30.default.join(start, entry2.name));
4768
4937
  }
4769
4938
  }
4770
4939
  return out;
@@ -4792,7 +4961,7 @@ async function addTerraformResources(graph, scanPath) {
4792
4961
  // src/extract/infra/k8s.ts
4793
4962
  init_cjs_shims();
4794
4963
  var import_node_fs17 = require("fs");
4795
- var import_node_path30 = __toESM(require("path"), 1);
4964
+ var import_node_path31 = __toESM(require("path"), 1);
4796
4965
  var import_yaml3 = require("yaml");
4797
4966
  var K8S_KIND_TO_INFRA_KIND = {
4798
4967
  Service: "k8s-service",
@@ -4810,11 +4979,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4810
4979
  for (const entry2 of entries) {
4811
4980
  if (entry2.isDirectory()) {
4812
4981
  if (IGNORED_DIRS.has(entry2.name)) continue;
4813
- const child = import_node_path30.default.join(start, entry2.name);
4982
+ const child = import_node_path31.default.join(start, entry2.name);
4814
4983
  if (await isPythonVenvDir(child)) continue;
4815
4984
  out.push(...await walkYamlFiles2(child, depth + 1, max));
4816
- } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path30.default.extname(entry2.name))) {
4817
- out.push(import_node_path30.default.join(start, entry2.name));
4985
+ } else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry2.name))) {
4986
+ out.push(import_node_path31.default.join(start, entry2.name));
4818
4987
  }
4819
4988
  }
4820
4989
  return out;
@@ -4858,17 +5027,17 @@ async function addInfra(graph, scanPath, services) {
4858
5027
  }
4859
5028
 
4860
5029
  // src/extract/index.ts
4861
- var import_node_path32 = __toESM(require("path"), 1);
5030
+ var import_node_path33 = __toESM(require("path"), 1);
4862
5031
 
4863
5032
  // src/extract/retire.ts
4864
5033
  init_cjs_shims();
4865
5034
  var import_node_fs18 = require("fs");
4866
- var import_node_path31 = __toESM(require("path"), 1);
4867
- var import_types20 = require("@neat.is/types");
5035
+ var import_node_path32 = __toESM(require("path"), 1);
5036
+ var import_types21 = require("@neat.is/types");
4868
5037
  function dropOrphanedFileNodes(graph) {
4869
5038
  const orphans = [];
4870
5039
  graph.forEachNode((id, attrs) => {
4871
- if (attrs.type !== import_types20.NodeType.FileNode) return;
5040
+ if (attrs.type !== import_types21.NodeType.FileNode) return;
4872
5041
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
4873
5042
  orphans.push(id);
4874
5043
  }
@@ -4881,14 +5050,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
4881
5050
  const bases = [scanPath, ...serviceDirs];
4882
5051
  graph.forEachEdge((id, attrs) => {
4883
5052
  const edge = attrs;
4884
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5053
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
4885
5054
  const evidenceFile = edge.evidence?.file;
4886
5055
  if (!evidenceFile) return;
4887
- if (import_node_path31.default.isAbsolute(evidenceFile)) {
5056
+ if (import_node_path32.default.isAbsolute(evidenceFile)) {
4888
5057
  if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
4889
5058
  return;
4890
5059
  }
4891
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path31.default.join(base, evidenceFile)));
5060
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
4892
5061
  if (!found) toDrop.push(id);
4893
5062
  });
4894
5063
  for (const id of toDrop) graph.dropEdge(id);
@@ -4928,7 +5097,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4928
5097
  }
4929
5098
  const droppedEntries = drainDroppedExtracted();
4930
5099
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
4931
- const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
5100
+ const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
4932
5101
  try {
4933
5102
  await writeRejectedExtracted(droppedEntries, rejectedPath);
4934
5103
  } catch (err) {
@@ -4963,8 +5132,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
4963
5132
  // src/persist.ts
4964
5133
  init_cjs_shims();
4965
5134
  var import_node_fs19 = require("fs");
4966
- var import_node_path33 = __toESM(require("path"), 1);
4967
- var import_types21 = require("@neat.is/types");
5135
+ var import_node_path34 = __toESM(require("path"), 1);
5136
+ var import_types22 = require("@neat.is/types");
4968
5137
  var SCHEMA_VERSION = 4;
4969
5138
  function migrateV1ToV2(payload) {
4970
5139
  const nodes = payload.graph.nodes;
@@ -4986,12 +5155,12 @@ function migrateV2ToV3(payload) {
4986
5155
  for (const edge of edges) {
4987
5156
  const attrs = edge.attributes;
4988
5157
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
4989
- attrs.provenance = import_types21.Provenance.OBSERVED;
5158
+ attrs.provenance = import_types22.Provenance.OBSERVED;
4990
5159
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
4991
5160
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
4992
5161
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
4993
5162
  if (type && source && target) {
4994
- const newId = (0, import_types21.observedEdgeId)(source, target, type);
5163
+ const newId = (0, import_types22.observedEdgeId)(source, target, type);
4995
5164
  attrs.id = newId;
4996
5165
  if (edge.key) edge.key = newId;
4997
5166
  }
@@ -5000,7 +5169,7 @@ function migrateV2ToV3(payload) {
5000
5169
  return { ...payload, schemaVersion: 3 };
5001
5170
  }
5002
5171
  async function ensureDir(filePath) {
5003
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
5172
+ await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
5004
5173
  }
5005
5174
  async function saveGraphToDisk(graph, outPath) {
5006
5175
  await ensureDir(outPath);
@@ -5075,23 +5244,23 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5075
5244
 
5076
5245
  // src/projects.ts
5077
5246
  init_cjs_shims();
5078
- var import_node_path34 = __toESM(require("path"), 1);
5247
+ var import_node_path35 = __toESM(require("path"), 1);
5079
5248
  function pathsForProject(project, baseDir) {
5080
5249
  if (project === DEFAULT_PROJECT) {
5081
5250
  return {
5082
- snapshotPath: import_node_path34.default.join(baseDir, "graph.json"),
5083
- errorsPath: import_node_path34.default.join(baseDir, "errors.ndjson"),
5084
- staleEventsPath: import_node_path34.default.join(baseDir, "stale-events.ndjson"),
5085
- embeddingsCachePath: import_node_path34.default.join(baseDir, "embeddings.json"),
5086
- policyViolationsPath: import_node_path34.default.join(baseDir, "policy-violations.ndjson")
5251
+ snapshotPath: import_node_path35.default.join(baseDir, "graph.json"),
5252
+ errorsPath: import_node_path35.default.join(baseDir, "errors.ndjson"),
5253
+ staleEventsPath: import_node_path35.default.join(baseDir, "stale-events.ndjson"),
5254
+ embeddingsCachePath: import_node_path35.default.join(baseDir, "embeddings.json"),
5255
+ policyViolationsPath: import_node_path35.default.join(baseDir, "policy-violations.ndjson")
5087
5256
  };
5088
5257
  }
5089
5258
  return {
5090
- snapshotPath: import_node_path34.default.join(baseDir, `${project}.json`),
5091
- errorsPath: import_node_path34.default.join(baseDir, `errors.${project}.ndjson`),
5092
- staleEventsPath: import_node_path34.default.join(baseDir, `stale-events.${project}.ndjson`),
5093
- embeddingsCachePath: import_node_path34.default.join(baseDir, `embeddings.${project}.json`),
5094
- policyViolationsPath: import_node_path34.default.join(baseDir, `policy-violations.${project}.ndjson`)
5259
+ snapshotPath: import_node_path35.default.join(baseDir, `${project}.json`),
5260
+ errorsPath: import_node_path35.default.join(baseDir, `errors.${project}.ndjson`),
5261
+ staleEventsPath: import_node_path35.default.join(baseDir, `stale-events.${project}.ndjson`),
5262
+ embeddingsCachePath: import_node_path35.default.join(baseDir, `embeddings.${project}.json`),
5263
+ policyViolationsPath: import_node_path35.default.join(baseDir, `policy-violations.${project}.ndjson`)
5095
5264
  };
5096
5265
  }
5097
5266
  var Projects = class {
@@ -5129,19 +5298,19 @@ var Projects = class {
5129
5298
  init_cjs_shims();
5130
5299
  var import_fastify = __toESM(require("fastify"), 1);
5131
5300
  var import_cors = __toESM(require("@fastify/cors"), 1);
5132
- var import_types24 = require("@neat.is/types");
5301
+ var import_types25 = require("@neat.is/types");
5133
5302
 
5134
5303
  // src/extend/index.ts
5135
5304
  init_cjs_shims();
5136
5305
  var import_node_fs21 = require("fs");
5137
- var import_node_path36 = __toESM(require("path"), 1);
5306
+ var import_node_path37 = __toESM(require("path"), 1);
5138
5307
  var import_node_os2 = __toESM(require("os"), 1);
5139
5308
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5140
5309
 
5141
5310
  // src/installers/package-manager.ts
5142
5311
  init_cjs_shims();
5143
5312
  var import_node_fs20 = require("fs");
5144
- var import_node_path35 = __toESM(require("path"), 1);
5313
+ var import_node_path36 = __toESM(require("path"), 1);
5145
5314
  var import_node_child_process = require("child_process");
5146
5315
  var LOCKFILE_PRIORITY = [
5147
5316
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5163,22 +5332,22 @@ async function exists2(p) {
5163
5332
  }
5164
5333
  }
5165
5334
  async function detectPackageManager(serviceDir) {
5166
- let dir = import_node_path35.default.resolve(serviceDir);
5335
+ let dir = import_node_path36.default.resolve(serviceDir);
5167
5336
  const stops = /* @__PURE__ */ new Set();
5168
5337
  for (let i = 0; i < 64; i++) {
5169
5338
  if (stops.has(dir)) break;
5170
5339
  stops.add(dir);
5171
5340
  for (const candidate of LOCKFILE_PRIORITY) {
5172
- const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
5341
+ const lockPath = import_node_path36.default.join(dir, candidate.lockfile);
5173
5342
  if (await exists2(lockPath)) {
5174
5343
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5175
5344
  }
5176
5345
  }
5177
- const parent = import_node_path35.default.dirname(dir);
5346
+ const parent = import_node_path36.default.dirname(dir);
5178
5347
  if (parent === dir) break;
5179
5348
  dir = parent;
5180
5349
  }
5181
- return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5350
+ return { pm: "npm", cwd: import_node_path36.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5182
5351
  }
5183
5352
  async function runPackageManagerInstall(cmd) {
5184
5353
  return new Promise((resolve) => {
@@ -5227,7 +5396,7 @@ async function fileExists2(p) {
5227
5396
  }
5228
5397
  }
5229
5398
  async function readPackageJson(scanPath) {
5230
- const pkgPath = import_node_path36.default.join(scanPath, "package.json");
5399
+ const pkgPath = import_node_path37.default.join(scanPath, "package.json");
5231
5400
  const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
5232
5401
  return JSON.parse(raw);
5233
5402
  }
@@ -5238,11 +5407,11 @@ async function findHookFiles(scanPath) {
5238
5407
  ).sort();
5239
5408
  }
5240
5409
  function extendLogPath() {
5241
- return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5410
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path37.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5242
5411
  }
5243
5412
  async function appendExtendLog(entry2) {
5244
5413
  const logPath = extendLogPath();
5245
- await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
5414
+ await import_node_fs21.promises.mkdir(import_node_path37.default.dirname(logPath), { recursive: true });
5246
5415
  await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
5247
5416
  }
5248
5417
  function splicedContent(fileContent, snippet2) {
@@ -5301,7 +5470,7 @@ function lookupInstrumentation(library, installedVersion) {
5301
5470
  }
5302
5471
  async function describeProjectInstrumentation(ctx) {
5303
5472
  const hookFiles = await findHookFiles(ctx.scanPath);
5304
- const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
5473
+ const envNeat = await fileExists2(import_node_path37.default.join(ctx.scanPath, ".env.neat"));
5305
5474
  const registryInstrPackages = new Set(
5306
5475
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5307
5476
  );
@@ -5323,16 +5492,16 @@ async function applyExtension(ctx, args, options) {
5323
5492
  );
5324
5493
  }
5325
5494
  for (const file of hookFiles) {
5326
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5495
+ const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
5327
5496
  if (content.includes(args.registration_snippet)) {
5328
5497
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5329
5498
  }
5330
5499
  }
5331
5500
  const primaryFile = hookFiles[0];
5332
- const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
5501
+ const primaryPath = import_node_path37.default.join(ctx.scanPath, primaryFile);
5333
5502
  const filesTouched = [];
5334
5503
  const depsAdded = [];
5335
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5504
+ const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
5336
5505
  const pkg = await readPackageJson(ctx.scanPath);
5337
5506
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5338
5507
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5378,7 +5547,7 @@ async function dryRunExtension(ctx, args) {
5378
5547
  };
5379
5548
  }
5380
5549
  for (const file of hookFiles) {
5381
- const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5550
+ const content = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, file), "utf8");
5382
5551
  if (content.includes(args.registration_snippet)) {
5383
5552
  return {
5384
5553
  library: args.library,
@@ -5400,7 +5569,7 @@ async function dryRunExtension(ctx, args) {
5400
5569
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5401
5570
  filesTouched.push("package.json");
5402
5571
  }
5403
- const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
5572
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path37.default.join(ctx.scanPath, primaryFile), "utf8");
5404
5573
  const patched = splicedContent(hookContent, args.registration_snippet);
5405
5574
  if (patched) {
5406
5575
  filesTouched.push(primaryFile);
@@ -5421,7 +5590,7 @@ async function rollbackExtension(ctx, args) {
5421
5590
  if (!match) {
5422
5591
  return { undone: false, message: "no apply found for library" };
5423
5592
  }
5424
- const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5593
+ const pkgPath = import_node_path37.default.join(ctx.scanPath, "package.json");
5425
5594
  if (await fileExists2(pkgPath)) {
5426
5595
  const pkg = await readPackageJson(ctx.scanPath);
5427
5596
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5432,7 +5601,7 @@ async function rollbackExtension(ctx, args) {
5432
5601
  }
5433
5602
  const hookFiles = await findHookFiles(ctx.scanPath);
5434
5603
  for (const file of hookFiles) {
5435
- const filePath = import_node_path36.default.join(ctx.scanPath, file);
5604
+ const filePath = import_node_path37.default.join(ctx.scanPath, file);
5436
5605
  const content = await import_node_fs21.promises.readFile(filePath, "utf8");
5437
5606
  if (content.includes(match.registration_snippet)) {
5438
5607
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -5448,7 +5617,7 @@ async function rollbackExtension(ctx, args) {
5448
5617
 
5449
5618
  // src/divergences.ts
5450
5619
  init_cjs_shims();
5451
- var import_types22 = require("@neat.is/types");
5620
+ var import_types23 = require("@neat.is/types");
5452
5621
  function bucketKey(source, target, type) {
5453
5622
  return `${type}|${source}|${target}`;
5454
5623
  }
@@ -5456,22 +5625,22 @@ function bucketEdges(graph) {
5456
5625
  const buckets = /* @__PURE__ */ new Map();
5457
5626
  graph.forEachEdge((id, attrs) => {
5458
5627
  const e = attrs;
5459
- const parsed = (0, import_types22.parseEdgeId)(id);
5628
+ const parsed = (0, import_types23.parseEdgeId)(id);
5460
5629
  const provenance = parsed?.provenance ?? e.provenance;
5461
5630
  const key = bucketKey(e.source, e.target, e.type);
5462
5631
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5463
5632
  switch (provenance) {
5464
- case import_types22.Provenance.EXTRACTED:
5633
+ case import_types23.Provenance.EXTRACTED:
5465
5634
  cur.extracted = e;
5466
5635
  break;
5467
- case import_types22.Provenance.OBSERVED:
5636
+ case import_types23.Provenance.OBSERVED:
5468
5637
  cur.observed = e;
5469
5638
  break;
5470
- case import_types22.Provenance.INFERRED:
5639
+ case import_types23.Provenance.INFERRED:
5471
5640
  cur.inferred = e;
5472
5641
  break;
5473
5642
  default:
5474
- if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
5643
+ if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
5475
5644
  }
5476
5645
  buckets.set(key, cur);
5477
5646
  });
@@ -5480,7 +5649,7 @@ function bucketEdges(graph) {
5480
5649
  function nodeIsFrontier(graph, nodeId) {
5481
5650
  if (!graph.hasNode(nodeId)) return false;
5482
5651
  const attrs = graph.getNodeAttributes(nodeId);
5483
- return attrs.type === import_types22.NodeType.FrontierNode;
5652
+ return attrs.type === import_types23.NodeType.FrontierNode;
5484
5653
  }
5485
5654
  function clampConfidence(n) {
5486
5655
  if (!Number.isFinite(n)) return 0;
@@ -5501,7 +5670,7 @@ function gradedConfidence(edge) {
5501
5670
  }
5502
5671
  function detectMissingDivergences(graph, bucket) {
5503
5672
  const out = [];
5504
- if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5673
+ if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5505
5674
  if (bucket.extracted && !bucket.observed) {
5506
5675
  if (!nodeIsFrontier(graph, bucket.target)) {
5507
5676
  out.push({
@@ -5542,7 +5711,7 @@ function declaredHostFor(svc) {
5542
5711
  function hasExtractedConfiguredBy(graph, svcId) {
5543
5712
  for (const edgeId of graph.outboundEdges(svcId)) {
5544
5713
  const e = graph.getEdgeAttributes(edgeId);
5545
- if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
5714
+ if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
5546
5715
  return true;
5547
5716
  }
5548
5717
  }
@@ -5555,10 +5724,10 @@ function detectHostMismatch(graph, svcId, svc) {
5555
5724
  const out = [];
5556
5725
  for (const edgeId of graph.outboundEdges(svcId)) {
5557
5726
  const edge = graph.getEdgeAttributes(edgeId);
5558
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5559
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5727
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5728
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5560
5729
  const target = graph.getNodeAttributes(edge.target);
5561
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5730
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5562
5731
  const observedHost = target.host?.trim();
5563
5732
  if (!observedHost) continue;
5564
5733
  if (observedHost === declaredHost) continue;
@@ -5580,10 +5749,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5580
5749
  const deps = svc.dependencies ?? {};
5581
5750
  for (const edgeId of graph.outboundEdges(svcId)) {
5582
5751
  const edge = graph.getEdgeAttributes(edgeId);
5583
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5584
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5752
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5753
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5585
5754
  const target = graph.getNodeAttributes(edge.target);
5586
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5755
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5587
5756
  for (const pair of compatPairs()) {
5588
5757
  if (pair.engine !== target.engine) continue;
5589
5758
  const declared = deps[pair.driver];
@@ -5644,7 +5813,7 @@ function computeDivergences(graph, opts = {}) {
5644
5813
  }
5645
5814
  graph.forEachNode((nodeId, attrs) => {
5646
5815
  const n = attrs;
5647
- if (n.type !== import_types22.NodeType.ServiceNode) return;
5816
+ if (n.type !== import_types23.NodeType.ServiceNode) return;
5648
5817
  const svc = n;
5649
5818
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5650
5819
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5677,7 +5846,7 @@ function computeDivergences(graph, opts = {}) {
5677
5846
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5678
5847
  return a.target.localeCompare(b.target);
5679
5848
  });
5680
- return import_types22.DivergenceResultSchema.parse({
5849
+ return import_types23.DivergenceResultSchema.parse({
5681
5850
  divergences: filtered,
5682
5851
  totalAffected: filtered.length,
5683
5852
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5765,23 +5934,23 @@ function canonicalJson(value) {
5765
5934
  init_cjs_shims();
5766
5935
  var import_node_fs23 = require("fs");
5767
5936
  var import_node_os3 = __toESM(require("os"), 1);
5768
- var import_node_path37 = __toESM(require("path"), 1);
5769
- var import_types23 = require("@neat.is/types");
5937
+ var import_node_path38 = __toESM(require("path"), 1);
5938
+ var import_types24 = require("@neat.is/types");
5770
5939
  var LOCK_TIMEOUT_MS = 5e3;
5771
5940
  var LOCK_RETRY_MS = 50;
5772
5941
  function neatHome() {
5773
5942
  const override = process.env.NEAT_HOME;
5774
- if (override && override.length > 0) return import_node_path37.default.resolve(override);
5775
- return import_node_path37.default.join(import_node_os3.default.homedir(), ".neat");
5943
+ if (override && override.length > 0) return import_node_path38.default.resolve(override);
5944
+ return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
5776
5945
  }
5777
5946
  function registryPath() {
5778
- return import_node_path37.default.join(neatHome(), "projects.json");
5947
+ return import_node_path38.default.join(neatHome(), "projects.json");
5779
5948
  }
5780
5949
  function registryLockPath() {
5781
- return import_node_path37.default.join(neatHome(), "projects.json.lock");
5950
+ return import_node_path38.default.join(neatHome(), "projects.json.lock");
5782
5951
  }
5783
5952
  function daemonPidPath() {
5784
- return import_node_path37.default.join(neatHome(), "neatd.pid");
5953
+ return import_node_path38.default.join(neatHome(), "neatd.pid");
5785
5954
  }
5786
5955
  function isPidAliveDefault(pid) {
5787
5956
  try {
@@ -5841,7 +6010,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5841
6010
  }
5842
6011
  }
5843
6012
  async function writeAtomically(target, contents) {
5844
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6013
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
5845
6014
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5846
6015
  const fd = await import_node_fs23.promises.open(tmp, "w");
5847
6016
  try {
@@ -5854,7 +6023,7 @@ async function writeAtomically(target, contents) {
5854
6023
  }
5855
6024
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
5856
6025
  const deadline = Date.now() + timeoutMs;
5857
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(lockPath), { recursive: true });
6026
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
5858
6027
  let probedHolder = false;
5859
6028
  while (true) {
5860
6029
  try {
@@ -5907,10 +6076,10 @@ async function readRegistry() {
5907
6076
  throw err;
5908
6077
  }
5909
6078
  const parsed = JSON.parse(raw);
5910
- return import_types23.RegistryFileSchema.parse(parsed);
6079
+ return import_types24.RegistryFileSchema.parse(parsed);
5911
6080
  }
5912
6081
  async function writeRegistry(reg) {
5913
- const validated = import_types23.RegistryFileSchema.parse(reg);
6082
+ const validated = import_types24.RegistryFileSchema.parse(reg);
5914
6083
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
5915
6084
  }
5916
6085
  async function getProject(name) {
@@ -5940,6 +6109,58 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
5940
6109
  await writeRegistry(reg);
5941
6110
  });
5942
6111
  }
6112
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
6113
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
6114
+ function pruneTtlMs() {
6115
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
6116
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
6117
+ const n = Number.parseInt(raw, 10);
6118
+ if (Number.isFinite(n) && n >= 0) return n;
6119
+ console.warn(
6120
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
6121
+ );
6122
+ return DEFAULT_PRUNE_TTL_MS;
6123
+ }
6124
+ async function statPathStatus(p) {
6125
+ try {
6126
+ const stat = await import_node_fs23.promises.stat(p);
6127
+ return stat.isDirectory() ? "present" : "unknown";
6128
+ } catch (err) {
6129
+ return err.code === "ENOENT" ? "gone" : "unknown";
6130
+ }
6131
+ }
6132
+ async function pruneRegistry(opts = {}) {
6133
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
6134
+ const statPath = opts.statPath ?? statPathStatus;
6135
+ const now = opts.now ?? Date.now;
6136
+ return withLock(async () => {
6137
+ const reg = await readRegistry();
6138
+ const removed = [];
6139
+ const kept = [];
6140
+ for (const entry2 of reg.projects) {
6141
+ const status2 = await statPath(entry2.path);
6142
+ if (status2 !== "gone") {
6143
+ kept.push(entry2);
6144
+ continue;
6145
+ }
6146
+ if (ttlMs <= 0) {
6147
+ removed.push(entry2);
6148
+ continue;
6149
+ }
6150
+ const lastSeen = Date.parse(entry2.lastSeenAt ?? entry2.registeredAt);
6151
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
6152
+ if (age > ttlMs) {
6153
+ removed.push(entry2);
6154
+ } else {
6155
+ kept.push(entry2);
6156
+ }
6157
+ }
6158
+ if (removed.length === 0) return [];
6159
+ reg.projects = kept;
6160
+ await writeRegistry(reg);
6161
+ return removed;
6162
+ });
6163
+ }
5943
6164
 
5944
6165
  // src/streaming.ts
5945
6166
  init_cjs_shims();
@@ -5953,6 +6174,7 @@ function handleSse(req2, reply, opts) {
5953
6174
  reply.raw.setHeader("Connection", "keep-alive");
5954
6175
  reply.raw.setHeader("X-Accel-Buffering", "no");
5955
6176
  reply.raw.flushHeaders?.();
6177
+ reply.raw.write(":open\n\n");
5956
6178
  let pending = 0;
5957
6179
  let dropped = false;
5958
6180
  const closeConnection = () => {
@@ -6132,11 +6354,11 @@ function registerRoutes(scope, ctx) {
6132
6354
  const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6133
6355
  const parsed = [];
6134
6356
  for (const c of candidates) {
6135
- const r = import_types24.DivergenceTypeSchema.safeParse(c);
6357
+ const r = import_types25.DivergenceTypeSchema.safeParse(c);
6136
6358
  if (!r.success) {
6137
6359
  return reply.code(400).send({
6138
6360
  error: `unknown divergence type "${c}"`,
6139
- allowed: import_types24.DivergenceTypeSchema.options
6361
+ allowed: import_types25.DivergenceTypeSchema.options
6140
6362
  });
6141
6363
  }
6142
6364
  parsed.push(r.data);
@@ -6349,7 +6571,7 @@ function registerRoutes(scope, ctx) {
6349
6571
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6350
6572
  let violations = await log.readAll();
6351
6573
  if (req2.query.severity) {
6352
- const sev = import_types24.PolicySeveritySchema.safeParse(req2.query.severity);
6574
+ const sev = import_types25.PolicySeveritySchema.safeParse(req2.query.severity);
6353
6575
  if (!sev.success) {
6354
6576
  return reply.code(400).send({
6355
6577
  error: "invalid severity",
@@ -6366,7 +6588,7 @@ function registerRoutes(scope, ctx) {
6366
6588
  scope.post("/policies/check", async (req2, reply) => {
6367
6589
  const proj = resolveProject(registry, req2, reply, ctx.bootstrap);
6368
6590
  if (!proj) return;
6369
- const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6591
+ const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
6370
6592
  if (!parsed.success) {
6371
6593
  return reply.code(400).send({
6372
6594
  error: "invalid /policies/check body",
@@ -6609,7 +6831,7 @@ init_auth();
6609
6831
  // src/unrouted.ts
6610
6832
  init_cjs_shims();
6611
6833
  var import_node_fs24 = require("fs");
6612
- var import_node_path40 = __toESM(require("path"), 1);
6834
+ var import_node_path41 = __toESM(require("path"), 1);
6613
6835
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
6614
6836
  return {
6615
6837
  timestamp: now.toISOString(),
@@ -6619,12 +6841,12 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
6619
6841
  };
6620
6842
  }
6621
6843
  async function appendUnroutedSpan(neatHome3, record) {
6622
- const target = import_node_path40.default.join(neatHome3, "errors.ndjson");
6844
+ const target = import_node_path41.default.join(neatHome3, "errors.ndjson");
6623
6845
  await import_node_fs24.promises.mkdir(neatHome3, { recursive: true });
6624
6846
  await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6625
6847
  }
6626
6848
  function unroutedErrorsPath(neatHome3) {
6627
- return import_node_path40.default.join(neatHome3, "errors.ndjson");
6849
+ return import_node_path41.default.join(neatHome3, "errors.ndjson");
6628
6850
  }
6629
6851
 
6630
6852
  // src/daemon.ts
@@ -6639,11 +6861,11 @@ function teardownSlot(slot) {
6639
6861
  }
6640
6862
  }
6641
6863
  function neatHomeFor(opts) {
6642
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path41.default.resolve(opts.neatHome);
6864
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
6643
6865
  const env = process.env.NEAT_HOME;
6644
- if (env && env.length > 0) return import_node_path41.default.resolve(env);
6866
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
6645
6867
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6646
- return import_node_path41.default.join(home, ".neat");
6868
+ return import_node_path42.default.join(home, ".neat");
6647
6869
  }
6648
6870
  function routeSpanToProject(serviceName, projects) {
6649
6871
  if (!serviceName) return DEFAULT_PROJECT;
@@ -6678,7 +6900,7 @@ function isTokenContained(needle, haystack) {
6678
6900
  return tokens.includes(needle);
6679
6901
  }
6680
6902
  async function bootstrapProject(entry2) {
6681
- const paths = pathsForProject(entry2.name, import_node_path41.default.join(entry2.path, "neat-out"));
6903
+ const paths = pathsForProject(entry2.name, import_node_path42.default.join(entry2.path, "neat-out"));
6682
6904
  try {
6683
6905
  const stat = await import_node_fs25.promises.stat(entry2.path);
6684
6906
  if (!stat.isDirectory()) {
@@ -6761,7 +6983,7 @@ async function startDaemon(opts = {}) {
6761
6983
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
6762
6984
  );
6763
6985
  }
6764
- const pidPath = import_node_path41.default.join(home, "neatd.pid");
6986
+ const pidPath = import_node_path42.default.join(home, "neatd.pid");
6765
6987
  await writeAtomically(pidPath, `${process.pid}
6766
6988
  `);
6767
6989
  const slots = /* @__PURE__ */ new Map();
@@ -6850,6 +7072,19 @@ async function startDaemon(opts = {}) {
6850
7072
  }
6851
7073
  }
6852
7074
  async function loadAll() {
7075
+ try {
7076
+ const pruned = await pruneRegistry();
7077
+ for (const entry2 of pruned) {
7078
+ console.log(
7079
+ `neatd: pruned project "${entry2.name}" \u2014 registered path ${entry2.path} is gone`
7080
+ );
7081
+ slots.delete(entry2.name);
7082
+ bootstrapStatus.delete(entry2.name);
7083
+ bootstrapStartedAt.delete(entry2.name);
7084
+ }
7085
+ } catch (err) {
7086
+ console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
7087
+ }
6853
7088
  const projects = await listProjects();
6854
7089
  const seen = /* @__PURE__ */ new Set();
6855
7090
  const pending = [];
@@ -7066,8 +7301,8 @@ async function startDaemon(opts = {}) {
7066
7301
  let registryWatcher = null;
7067
7302
  let reloadTimer = null;
7068
7303
  try {
7069
- const regDir = import_node_path41.default.dirname(regPath);
7070
- const regBase = import_node_path41.default.basename(regPath);
7304
+ const regDir = import_node_path42.default.dirname(regPath);
7305
+ const regBase = import_node_path42.default.basename(regPath);
7071
7306
  registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
7072
7307
  if (filename !== null && filename !== regBase) return;
7073
7308
  if (reloadTimer) clearTimeout(reloadTimer);
@@ -7130,7 +7365,7 @@ init_auth();
7130
7365
  init_cjs_shims();
7131
7366
  var import_node_child_process2 = require("child_process");
7132
7367
  var import_node_net = __toESM(require("net"), 1);
7133
- var import_node_path42 = __toESM(require("path"), 1);
7368
+ var import_node_path43 = __toESM(require("path"), 1);
7134
7369
  var DEFAULT_WEB_PORT = 6328;
7135
7370
  async function assertPortFree(port) {
7136
7371
  await new Promise((resolve, reject) => {
@@ -7158,10 +7393,10 @@ function resolveWebPackageDir() {
7158
7393
  eval("require")
7159
7394
  );
7160
7395
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
7161
- return import_node_path42.default.dirname(pkgJsonPath);
7396
+ return import_node_path43.default.dirname(pkgJsonPath);
7162
7397
  }
7163
7398
  function resolveStandaloneServerEntry(webDir) {
7164
- return import_node_path42.default.join(webDir, ".next/standalone/packages/web/server.js");
7399
+ return import_node_path43.default.join(webDir, ".next/standalone/packages/web/server.js");
7165
7400
  }
7166
7401
  async function spawnWebUI(restPort) {
7167
7402
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -7186,7 +7421,7 @@ async function spawnWebUI(restPort) {
7186
7421
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
7187
7422
  };
7188
7423
  const child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
7189
- cwd: import_node_path42.default.dirname(serverEntry),
7424
+ cwd: import_node_path43.default.dirname(serverEntry),
7190
7425
  env,
7191
7426
  stdio: ["ignore", "inherit", "inherit"],
7192
7427
  detached: false
@@ -7299,14 +7534,14 @@ function localVersion() {
7299
7534
  }
7300
7535
  function neatHome2() {
7301
7536
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
7302
- return import_node_path43.default.resolve(process.env.NEAT_HOME);
7537
+ return import_node_path44.default.resolve(process.env.NEAT_HOME);
7303
7538
  }
7304
7539
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7305
- return import_node_path43.default.join(home, ".neat");
7540
+ return import_node_path44.default.join(home, ".neat");
7306
7541
  }
7307
7542
  async function readPid() {
7308
7543
  try {
7309
- const raw = await import_node_fs26.promises.readFile(import_node_path43.default.join(neatHome2(), "neatd.pid"), "utf8");
7544
+ const raw = await import_node_fs26.promises.readFile(import_node_path44.default.join(neatHome2(), "neatd.pid"), "utf8");
7310
7545
  const n = Number.parseInt(raw.trim(), 10);
7311
7546
  return Number.isFinite(n) ? n : null;
7312
7547
  } catch {