@neat.is/core 0.4.16 → 0.4.18

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 }
@@ -473,8 +473,10 @@ async function buildOtelReceiver(opts) {
473
473
  for (const s of spans) projectQueue.push({ project, span: s });
474
474
  projectDrainPromise = projectDrainPromise.then(() => drainProject());
475
475
  };
476
+ const offersProjectRouting = opts.onProjectSpan !== void 0;
476
477
  const legacyEndpointWarned = /* @__PURE__ */ new Set();
477
478
  function warnLegacyEndpoint(serviceName) {
479
+ if (!offersProjectRouting) return;
478
480
  if (legacyEndpointWarned.has(serviceName)) return;
479
481
  legacyEndpointWarned.add(serviceName);
480
482
  console.warn(
@@ -579,12 +581,12 @@ function logSpanHandler(span) {
579
581
  `otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
580
582
  );
581
583
  }
582
- var import_node_path39, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
584
+ var import_node_path40, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody;
583
585
  var init_otel = __esm({
584
586
  "src/otel.ts"() {
585
587
  "use strict";
586
588
  init_cjs_shims();
587
- import_node_path39 = __toESM(require("path"), 1);
589
+ import_node_path40 = __toESM(require("path"), 1);
588
590
  import_node_url2 = require("url");
589
591
  import_fastify2 = __toESM(require("fastify"), 1);
590
592
  import_protobufjs = __toESM(require("protobufjs"), 1);
@@ -1172,19 +1174,19 @@ function confidenceFromMix(edges, now = Date.now()) {
1172
1174
  function longestIncomingWalk(graph, start, maxDepth) {
1173
1175
  let best = { path: [start], edges: [] };
1174
1176
  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] };
1177
+ function step(node, path43, edges) {
1178
+ if (path43.length > best.path.length) {
1179
+ best = { path: [...path43], edges: [...edges] };
1178
1180
  }
1179
- if (path42.length - 1 >= maxDepth) return;
1181
+ if (path43.length - 1 >= maxDepth) return;
1180
1182
  const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
1181
1183
  for (const [srcId, edge] of incoming) {
1182
1184
  if (visited.has(srcId)) continue;
1183
1185
  visited.add(srcId);
1184
- path42.push(srcId);
1186
+ path43.push(srcId);
1185
1187
  edges.push(edge);
1186
- step(srcId, path42, edges);
1187
- path42.pop();
1188
+ step(srcId, path43, edges);
1189
+ path43.pop();
1188
1190
  edges.pop();
1189
1191
  visited.delete(srcId);
1190
1192
  }
@@ -1724,6 +1726,41 @@ function thresholdForEdgeType(edgeType, overrides) {
1724
1726
  const map = overrides ?? loadStaleThresholdsFromEnv();
1725
1727
  return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
1726
1728
  }
1729
+ var DEFAULT_INCIDENT_THRESHOLDS = {
1730
+ threshold: 5,
1731
+ windowMs: 6e4
1732
+ };
1733
+ function loadIncidentThresholdsFromEnv() {
1734
+ const raw = process.env.NEAT_INCIDENT_THRESHOLDS;
1735
+ if (!raw) return DEFAULT_INCIDENT_THRESHOLDS;
1736
+ try {
1737
+ const overrides = JSON.parse(raw);
1738
+ const merged = { ...DEFAULT_INCIDENT_THRESHOLDS };
1739
+ if (typeof overrides.threshold === "number" && Number.isFinite(overrides.threshold) && overrides.threshold >= 1) {
1740
+ merged.threshold = Math.floor(overrides.threshold);
1741
+ }
1742
+ if (typeof overrides.windowMs === "number" && Number.isFinite(overrides.windowMs) && overrides.windowMs >= 0) {
1743
+ merged.windowMs = overrides.windowMs;
1744
+ }
1745
+ return merged;
1746
+ } catch (err) {
1747
+ console.warn(
1748
+ `[neat] NEAT_INCIDENT_THRESHOLDS could not be parsed (${err.message}); using defaults`
1749
+ );
1750
+ return DEFAULT_INCIDENT_THRESHOLDS;
1751
+ }
1752
+ }
1753
+ function httpResponseStatus(span) {
1754
+ for (const key of ["http.response.status_code", "http.status_code"]) {
1755
+ const v = span.attributes[key];
1756
+ if (typeof v === "number" && Number.isFinite(v)) return v;
1757
+ if (typeof v === "string") {
1758
+ const n = Number(v);
1759
+ if (Number.isFinite(n)) return n;
1760
+ }
1761
+ }
1762
+ return void 0;
1763
+ }
1727
1764
  function nowIso(ctx) {
1728
1765
  return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1729
1766
  }
@@ -2137,6 +2174,71 @@ function makeErrorSpanWriter(errorsPath) {
2137
2174
  await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
2138
2175
  };
2139
2176
  }
2177
+ async function recordFailingResponseIncident(ctx, span, affectedNode, timestamp, statusCode, count, firstTimestamp) {
2178
+ const attrs = sanitizeAttributes(span.attributes);
2179
+ const first = firstTimestamp ?? timestamp;
2180
+ const peer = pickAddress(span);
2181
+ const message = count > 1 ? `${count} consecutive HTTP ${statusCode} responses` + (peer ? ` to ${peer}` : "") : `HTTP ${statusCode} response` + (peer ? ` from ${peer}` : "");
2182
+ const ev = {
2183
+ id: `${span.traceId}:${span.spanId}`,
2184
+ timestamp,
2185
+ service: span.service,
2186
+ traceId: span.traceId,
2187
+ spanId: span.spanId,
2188
+ errorType: "http-failure",
2189
+ errorMessage: message,
2190
+ ...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
2191
+ affectedNode,
2192
+ httpStatusCode: statusCode,
2193
+ incidentCount: count,
2194
+ firstTimestamp: first,
2195
+ lastTimestamp: timestamp
2196
+ };
2197
+ await appendErrorEvent(ctx, ev);
2198
+ }
2199
+ async function advance4xxBurst(ctx, span, affectedNode, ts, nowMs, status2) {
2200
+ const { threshold, windowMs } = loadIncidentThresholdsFromEnv();
2201
+ if (!ctx.burstState) ctx.burstState = /* @__PURE__ */ new Map();
2202
+ const peer = pickAddress(span) ?? span.spanId;
2203
+ const key = `${span.service}->${peer}`;
2204
+ const existing = ctx.burstState.get(key);
2205
+ let state;
2206
+ if (existing && nowMs - existing.lastMs <= windowMs) {
2207
+ existing.count += 1;
2208
+ existing.lastTs = ts;
2209
+ existing.lastMs = nowMs;
2210
+ existing.codes.set(status2, (existing.codes.get(status2) ?? 0) + 1);
2211
+ state = existing;
2212
+ } else {
2213
+ state = {
2214
+ count: 1,
2215
+ firstTs: ts,
2216
+ lastTs: ts,
2217
+ lastMs: nowMs,
2218
+ codes: /* @__PURE__ */ new Map([[status2, 1]])
2219
+ };
2220
+ ctx.burstState.set(key, state);
2221
+ }
2222
+ if (state.count < threshold) return;
2223
+ let dominant = status2;
2224
+ let max = 0;
2225
+ for (const [code, n] of state.codes) {
2226
+ if (n > max) {
2227
+ max = n;
2228
+ dominant = code;
2229
+ }
2230
+ }
2231
+ await recordFailingResponseIncident(
2232
+ ctx,
2233
+ span,
2234
+ affectedNode,
2235
+ state.lastTs,
2236
+ dominant,
2237
+ state.count,
2238
+ state.firstTs
2239
+ );
2240
+ ctx.burstState.delete(key);
2241
+ }
2140
2242
  async function handleSpan(ctx, span) {
2141
2243
  const ts = span.startTimeIso ?? nowIso(ctx);
2142
2244
  const nowMs = ctx.now ? ctx.now() : Date.now();
@@ -2235,6 +2337,14 @@ async function handleSpan(ctx, span) {
2235
2337
  await appendErrorEvent(ctx, ev);
2236
2338
  }
2237
2339
  }
2340
+ if (span.statusCode !== 2) {
2341
+ const status2 = httpResponseStatus(span);
2342
+ if (status2 !== void 0 && status2 >= 500) {
2343
+ await recordFailingResponseIncident(ctx, span, sourceId, ts, status2, 1);
2344
+ } else if (status2 !== void 0 && status2 >= 400 && spanMintsObservedEdge(span.kind)) {
2345
+ await advance4xxBurst(ctx, span, sourceId, ts, nowMs, status2);
2346
+ }
2347
+ }
2238
2348
  void affectedNode;
2239
2349
  if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
2240
2350
  }
@@ -4254,7 +4364,7 @@ async function addConfigNodes(graph, services, scanPath) {
4254
4364
 
4255
4365
  // src/extract/calls/index.ts
4256
4366
  init_cjs_shims();
4257
- var import_types16 = require("@neat.is/types");
4367
+ var import_types17 = require("@neat.is/types");
4258
4368
 
4259
4369
  // src/extract/calls/http.ts
4260
4370
  init_cjs_shims();
@@ -4607,15 +4717,75 @@ function grpcEndpointsFromFile(file, serviceDir) {
4607
4717
  return out;
4608
4718
  }
4609
4719
 
4720
+ // src/extract/calls/supabase.ts
4721
+ init_cjs_shims();
4722
+ var import_node_path27 = __toESM(require("path"), 1);
4723
+ var import_types16 = require("@neat.is/types");
4724
+ var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
4725
+ var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
4726
+ var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
4727
+ function hostFromLiteral(literal) {
4728
+ if (!literal) return null;
4729
+ const m = /^https?:\/\/([^/:'"`\s]+)/.exec(literal.trim());
4730
+ if (!m) return null;
4731
+ const host = m[1];
4732
+ if (!/\.supabase\.(co|in)$/i.test(host)) return null;
4733
+ return host;
4734
+ }
4735
+ function readImports2(content) {
4736
+ return {
4737
+ hasSupabaseJs: SUPABASE_JS_IMPORT_RE.test(content),
4738
+ hasSupabaseSsr: SUPABASE_SSR_IMPORT_RE.test(content)
4739
+ };
4740
+ }
4741
+ function constructorMatchesImport(name, ctx) {
4742
+ if (name === "createClient") return ctx.hasSupabaseJs;
4743
+ return ctx.hasSupabaseSsr;
4744
+ }
4745
+ function supabaseEndpointsFromFile(file, serviceDir) {
4746
+ const ctx = readImports2(file.content);
4747
+ if (!ctx.hasSupabaseJs && !ctx.hasSupabaseSsr) return [];
4748
+ const out = [];
4749
+ const seen = /* @__PURE__ */ new Set();
4750
+ SUPABASE_CLIENT_RE.lastIndex = 0;
4751
+ let m;
4752
+ while ((m = SUPABASE_CLIENT_RE.exec(file.content)) !== null) {
4753
+ const ctor = m[1];
4754
+ if (!constructorMatchesImport(ctor, ctx)) continue;
4755
+ const host = hostFromLiteral(m[2]);
4756
+ const name = host ?? "env";
4757
+ if (seen.has(name)) continue;
4758
+ seen.add(name);
4759
+ const line = lineOf(file.content, m[0]);
4760
+ out.push({
4761
+ infraId: (0, import_types16.infraId)("supabase", name),
4762
+ name,
4763
+ kind: "supabase",
4764
+ edgeType: "CALLS",
4765
+ // `createClient(...)` from @supabase/supabase-js (or createServerClient /
4766
+ // createBrowserClient from @supabase/ssr) with the import in scope — a
4767
+ // framework-aware recognizer matched the SDK shape. Verified-call-site
4768
+ // tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
4769
+ confidenceKind: "verified-call-site",
4770
+ evidence: {
4771
+ file: import_node_path27.default.relative(serviceDir, file.path),
4772
+ line,
4773
+ snippet: snippet(file.content, line)
4774
+ }
4775
+ });
4776
+ }
4777
+ return out;
4778
+ }
4779
+
4610
4780
  // src/extract/calls/index.ts
4611
4781
  function edgeTypeFromEndpoint(ep) {
4612
4782
  switch (ep.edgeType) {
4613
4783
  case "PUBLISHES_TO":
4614
- return import_types16.EdgeType.PUBLISHES_TO;
4784
+ return import_types17.EdgeType.PUBLISHES_TO;
4615
4785
  case "CONSUMES_FROM":
4616
- return import_types16.EdgeType.CONSUMES_FROM;
4786
+ return import_types17.EdgeType.CONSUMES_FROM;
4617
4787
  default:
4618
- return import_types16.EdgeType.CALLS;
4788
+ return import_types17.EdgeType.CALLS;
4619
4789
  }
4620
4790
  }
4621
4791
  function isAwsKind(kind) {
@@ -4635,6 +4805,7 @@ async function addExternalEndpointEdges(graph, services) {
4635
4805
  endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
4636
4806
  endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
4637
4807
  endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
4808
+ endpoints.push(...supabaseEndpointsFromFile(maskedFile, service.dir));
4638
4809
  }
4639
4810
  if (endpoints.length === 0) continue;
4640
4811
  const seenEdges = /* @__PURE__ */ new Set();
@@ -4642,7 +4813,7 @@ async function addExternalEndpointEdges(graph, services) {
4642
4813
  if (!graph.hasNode(ep.infraId)) {
4643
4814
  const node = {
4644
4815
  id: ep.infraId,
4645
- type: import_types16.NodeType.InfraNode,
4816
+ type: import_types17.NodeType.InfraNode,
4646
4817
  name: ep.name,
4647
4818
  // #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
4648
4819
  // aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
@@ -4654,7 +4825,7 @@ async function addExternalEndpointEdges(graph, services) {
4654
4825
  nodesAdded++;
4655
4826
  }
4656
4827
  const edgeType = edgeTypeFromEndpoint(ep);
4657
- const confidence = (0, import_types16.confidenceForExtracted)(ep.confidenceKind);
4828
+ const confidence = (0, import_types17.confidenceForExtracted)(ep.confidenceKind);
4658
4829
  const relFile = toPosix2(ep.evidence.file);
4659
4830
  const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
4660
4831
  graph,
@@ -4664,7 +4835,7 @@ async function addExternalEndpointEdges(graph, services) {
4664
4835
  );
4665
4836
  nodesAdded += n;
4666
4837
  edgesAdded += e;
4667
- if (!(0, import_types16.passesExtractedFloor)(confidence)) {
4838
+ if (!(0, import_types17.passesExtractedFloor)(confidence)) {
4668
4839
  noteExtractedDropped({
4669
4840
  source: fileNodeId,
4670
4841
  target: ep.infraId,
@@ -4684,7 +4855,7 @@ async function addExternalEndpointEdges(graph, services) {
4684
4855
  source: fileNodeId,
4685
4856
  target: ep.infraId,
4686
4857
  type: edgeType,
4687
- provenance: import_types16.Provenance.EXTRACTED,
4858
+ provenance: import_types17.Provenance.EXTRACTED,
4688
4859
  confidence,
4689
4860
  evidence: ep.evidence
4690
4861
  };
@@ -4709,16 +4880,16 @@ init_cjs_shims();
4709
4880
 
4710
4881
  // src/extract/infra/docker-compose.ts
4711
4882
  init_cjs_shims();
4712
- var import_node_path27 = __toESM(require("path"), 1);
4713
- var import_types18 = require("@neat.is/types");
4883
+ var import_node_path28 = __toESM(require("path"), 1);
4884
+ var import_types19 = require("@neat.is/types");
4714
4885
 
4715
4886
  // src/extract/infra/shared.ts
4716
4887
  init_cjs_shims();
4717
- var import_types17 = require("@neat.is/types");
4888
+ var import_types18 = require("@neat.is/types");
4718
4889
  function makeInfraNode(kind, name, provider = "self", extras) {
4719
4890
  return {
4720
- id: (0, import_types17.infraId)(kind, name),
4721
- type: import_types17.NodeType.InfraNode,
4891
+ id: (0, import_types18.infraId)(kind, name),
4892
+ type: import_types18.NodeType.InfraNode,
4722
4893
  name,
4723
4894
  provider,
4724
4895
  kind,
@@ -4747,7 +4918,7 @@ function dependsOnList(value) {
4747
4918
  }
4748
4919
  function serviceNameToServiceNode(name, services) {
4749
4920
  for (const s of services) {
4750
- if (s.node.name === name || import_node_path27.default.basename(s.dir) === name) return s.node.id;
4921
+ if (s.node.name === name || import_node_path28.default.basename(s.dir) === name) return s.node.id;
4751
4922
  }
4752
4923
  return null;
4753
4924
  }
@@ -4756,7 +4927,7 @@ async function addComposeInfra(graph, scanPath, services) {
4756
4927
  let edgesAdded = 0;
4757
4928
  let composePath = null;
4758
4929
  for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
4759
- const abs = import_node_path27.default.join(scanPath, name);
4930
+ const abs = import_node_path28.default.join(scanPath, name);
4760
4931
  if (await exists(abs)) {
4761
4932
  composePath = abs;
4762
4933
  break;
@@ -4769,13 +4940,13 @@ async function addComposeInfra(graph, scanPath, services) {
4769
4940
  } catch (err) {
4770
4941
  recordExtractionError(
4771
4942
  "infra docker-compose",
4772
- import_node_path27.default.relative(scanPath, composePath),
4943
+ import_node_path28.default.relative(scanPath, composePath),
4773
4944
  err
4774
4945
  );
4775
4946
  return { nodesAdded, edgesAdded };
4776
4947
  }
4777
4948
  if (!compose?.services) return { nodesAdded, edgesAdded };
4778
- const evidenceFile = import_node_path27.default.relative(scanPath, composePath).split(import_node_path27.default.sep).join("/");
4949
+ const evidenceFile = import_node_path28.default.relative(scanPath, composePath).split(import_node_path28.default.sep).join("/");
4779
4950
  const composeNameToNodeId = /* @__PURE__ */ new Map();
4780
4951
  for (const [composeName, svc] of Object.entries(compose.services)) {
4781
4952
  const matchedServiceId = serviceNameToServiceNode(composeName, services);
@@ -4797,15 +4968,15 @@ async function addComposeInfra(graph, scanPath, services) {
4797
4968
  for (const dep of dependsOnList(svc.depends_on)) {
4798
4969
  const targetId = composeNameToNodeId.get(dep);
4799
4970
  if (!targetId) continue;
4800
- const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types18.EdgeType.DEPENDS_ON);
4971
+ const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types19.EdgeType.DEPENDS_ON);
4801
4972
  if (graph.hasEdge(edgeId)) continue;
4802
4973
  const edge = {
4803
4974
  id: edgeId,
4804
4975
  source: sourceId,
4805
4976
  target: targetId,
4806
- type: import_types18.EdgeType.DEPENDS_ON,
4807
- provenance: import_types18.Provenance.EXTRACTED,
4808
- confidence: (0, import_types18.confidenceForExtracted)("structural"),
4977
+ type: import_types19.EdgeType.DEPENDS_ON,
4978
+ provenance: import_types19.Provenance.EXTRACTED,
4979
+ confidence: (0, import_types19.confidenceForExtracted)("structural"),
4809
4980
  evidence: { file: evidenceFile }
4810
4981
  };
4811
4982
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4817,9 +4988,9 @@ async function addComposeInfra(graph, scanPath, services) {
4817
4988
 
4818
4989
  // src/extract/infra/dockerfile.ts
4819
4990
  init_cjs_shims();
4820
- var import_node_path28 = __toESM(require("path"), 1);
4991
+ var import_node_path29 = __toESM(require("path"), 1);
4821
4992
  var import_node_fs15 = require("fs");
4822
- var import_types19 = require("@neat.is/types");
4993
+ var import_types20 = require("@neat.is/types");
4823
4994
  function runtimeImage(content) {
4824
4995
  const lines = content.split("\n");
4825
4996
  let last = null;
@@ -4838,7 +5009,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4838
5009
  let nodesAdded = 0;
4839
5010
  let edgesAdded = 0;
4840
5011
  for (const service of services) {
4841
- const dockerfilePath = import_node_path28.default.join(service.dir, "Dockerfile");
5012
+ const dockerfilePath = import_node_path29.default.join(service.dir, "Dockerfile");
4842
5013
  if (!await exists(dockerfilePath)) continue;
4843
5014
  let content;
4844
5015
  try {
@@ -4846,7 +5017,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4846
5017
  } catch (err) {
4847
5018
  recordExtractionError(
4848
5019
  "infra dockerfile",
4849
- import_node_path28.default.relative(scanPath, dockerfilePath),
5020
+ import_node_path29.default.relative(scanPath, dockerfilePath),
4850
5021
  err
4851
5022
  );
4852
5023
  continue;
@@ -4858,7 +5029,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4858
5029
  graph.addNode(node.id, node);
4859
5030
  nodesAdded++;
4860
5031
  }
4861
- const relDockerfile = toPosix2(import_node_path28.default.relative(service.dir, dockerfilePath));
5032
+ const relDockerfile = toPosix2(import_node_path29.default.relative(service.dir, dockerfilePath));
4862
5033
  const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
4863
5034
  graph,
4864
5035
  service.pkg.name,
@@ -4867,17 +5038,17 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4867
5038
  );
4868
5039
  nodesAdded += fn;
4869
5040
  edgesAdded += fe;
4870
- const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types19.EdgeType.RUNS_ON);
5041
+ const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types20.EdgeType.RUNS_ON);
4871
5042
  if (!graph.hasEdge(edgeId)) {
4872
5043
  const edge = {
4873
5044
  id: edgeId,
4874
5045
  source: fileNodeId,
4875
5046
  target: node.id,
4876
- type: import_types19.EdgeType.RUNS_ON,
4877
- provenance: import_types19.Provenance.EXTRACTED,
4878
- confidence: (0, import_types19.confidenceForExtracted)("structural"),
5047
+ type: import_types20.EdgeType.RUNS_ON,
5048
+ provenance: import_types20.Provenance.EXTRACTED,
5049
+ confidence: (0, import_types20.confidenceForExtracted)("structural"),
4879
5050
  evidence: {
4880
- file: toPosix2(import_node_path28.default.relative(scanPath, dockerfilePath))
5051
+ file: toPosix2(import_node_path29.default.relative(scanPath, dockerfilePath))
4881
5052
  }
4882
5053
  };
4883
5054
  graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
@@ -4890,7 +5061,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
4890
5061
  // src/extract/infra/terraform.ts
4891
5062
  init_cjs_shims();
4892
5063
  var import_node_fs16 = require("fs");
4893
- var import_node_path29 = __toESM(require("path"), 1);
5064
+ var import_node_path30 = __toESM(require("path"), 1);
4894
5065
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
4895
5066
  async function walkTfFiles(start, depth = 0, max = 5) {
4896
5067
  if (depth > max) return [];
@@ -4899,11 +5070,11 @@ async function walkTfFiles(start, depth = 0, max = 5) {
4899
5070
  for (const entry of entries) {
4900
5071
  if (entry.isDirectory()) {
4901
5072
  if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
4902
- const child = import_node_path29.default.join(start, entry.name);
5073
+ const child = import_node_path30.default.join(start, entry.name);
4903
5074
  if (await isPythonVenvDir(child)) continue;
4904
5075
  out.push(...await walkTfFiles(child, depth + 1, max));
4905
5076
  } else if (entry.isFile() && entry.name.endsWith(".tf")) {
4906
- out.push(import_node_path29.default.join(start, entry.name));
5077
+ out.push(import_node_path30.default.join(start, entry.name));
4907
5078
  }
4908
5079
  }
4909
5080
  return out;
@@ -4931,7 +5102,7 @@ async function addTerraformResources(graph, scanPath) {
4931
5102
  // src/extract/infra/k8s.ts
4932
5103
  init_cjs_shims();
4933
5104
  var import_node_fs17 = require("fs");
4934
- var import_node_path30 = __toESM(require("path"), 1);
5105
+ var import_node_path31 = __toESM(require("path"), 1);
4935
5106
  var import_yaml3 = require("yaml");
4936
5107
  var K8S_KIND_TO_INFRA_KIND = {
4937
5108
  Service: "k8s-service",
@@ -4949,11 +5120,11 @@ async function walkYamlFiles2(start, depth = 0, max = 5) {
4949
5120
  for (const entry of entries) {
4950
5121
  if (entry.isDirectory()) {
4951
5122
  if (IGNORED_DIRS.has(entry.name)) continue;
4952
- const child = import_node_path30.default.join(start, entry.name);
5123
+ const child = import_node_path31.default.join(start, entry.name);
4953
5124
  if (await isPythonVenvDir(child)) continue;
4954
5125
  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));
5126
+ } else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path31.default.extname(entry.name))) {
5127
+ out.push(import_node_path31.default.join(start, entry.name));
4957
5128
  }
4958
5129
  }
4959
5130
  return out;
@@ -4997,17 +5168,17 @@ async function addInfra(graph, scanPath, services) {
4997
5168
  }
4998
5169
 
4999
5170
  // src/extract/index.ts
5000
- var import_node_path32 = __toESM(require("path"), 1);
5171
+ var import_node_path33 = __toESM(require("path"), 1);
5001
5172
 
5002
5173
  // src/extract/retire.ts
5003
5174
  init_cjs_shims();
5004
5175
  var import_node_fs18 = require("fs");
5005
- var import_node_path31 = __toESM(require("path"), 1);
5006
- var import_types20 = require("@neat.is/types");
5176
+ var import_node_path32 = __toESM(require("path"), 1);
5177
+ var import_types21 = require("@neat.is/types");
5007
5178
  function dropOrphanedFileNodes(graph) {
5008
5179
  const orphans = [];
5009
5180
  graph.forEachNode((id, attrs) => {
5010
- if (attrs.type !== import_types20.NodeType.FileNode) return;
5181
+ if (attrs.type !== import_types21.NodeType.FileNode) return;
5011
5182
  if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
5012
5183
  orphans.push(id);
5013
5184
  }
@@ -5020,14 +5191,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
5020
5191
  const bases = [scanPath, ...serviceDirs];
5021
5192
  graph.forEachEdge((id, attrs) => {
5022
5193
  const edge = attrs;
5023
- if (edge.provenance !== import_types20.Provenance.EXTRACTED) return;
5194
+ if (edge.provenance !== import_types21.Provenance.EXTRACTED) return;
5024
5195
  const evidenceFile = edge.evidence?.file;
5025
5196
  if (!evidenceFile) return;
5026
- if (import_node_path31.default.isAbsolute(evidenceFile)) {
5197
+ if (import_node_path32.default.isAbsolute(evidenceFile)) {
5027
5198
  if (!(0, import_node_fs18.existsSync)(evidenceFile)) toDrop.push(id);
5028
5199
  return;
5029
5200
  }
5030
- const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path31.default.join(base, evidenceFile)));
5201
+ const found = bases.some((base) => (0, import_node_fs18.existsSync)(import_node_path32.default.join(base, evidenceFile)));
5031
5202
  if (!found) toDrop.push(id);
5032
5203
  });
5033
5204
  for (const id of toDrop) graph.dropEdge(id);
@@ -5067,7 +5238,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5067
5238
  }
5068
5239
  const droppedEntries = drainDroppedExtracted();
5069
5240
  if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
5070
- const rejectedPath = import_node_path32.default.join(import_node_path32.default.dirname(opts.errorsPath), "rejected.ndjson");
5241
+ const rejectedPath = import_node_path33.default.join(import_node_path33.default.dirname(opts.errorsPath), "rejected.ndjson");
5071
5242
  try {
5072
5243
  await writeRejectedExtracted(droppedEntries, rejectedPath);
5073
5244
  } catch (err) {
@@ -5102,8 +5273,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
5102
5273
  // src/persist.ts
5103
5274
  init_cjs_shims();
5104
5275
  var import_node_fs19 = require("fs");
5105
- var import_node_path33 = __toESM(require("path"), 1);
5106
- var import_types21 = require("@neat.is/types");
5276
+ var import_node_path34 = __toESM(require("path"), 1);
5277
+ var import_types22 = require("@neat.is/types");
5107
5278
  var SCHEMA_VERSION = 4;
5108
5279
  function migrateV1ToV2(payload) {
5109
5280
  const nodes = payload.graph.nodes;
@@ -5125,12 +5296,12 @@ function migrateV2ToV3(payload) {
5125
5296
  for (const edge of edges) {
5126
5297
  const attrs = edge.attributes;
5127
5298
  if (!attrs || attrs.provenance !== "FRONTIER") continue;
5128
- attrs.provenance = import_types21.Provenance.OBSERVED;
5299
+ attrs.provenance = import_types22.Provenance.OBSERVED;
5129
5300
  const type = typeof attrs.type === "string" ? attrs.type : void 0;
5130
5301
  const source = typeof attrs.source === "string" ? attrs.source : void 0;
5131
5302
  const target = typeof attrs.target === "string" ? attrs.target : void 0;
5132
5303
  if (type && source && target) {
5133
- const newId = (0, import_types21.observedEdgeId)(source, target, type);
5304
+ const newId = (0, import_types22.observedEdgeId)(source, target, type);
5134
5305
  attrs.id = newId;
5135
5306
  if (edge.key) edge.key = newId;
5136
5307
  }
@@ -5139,7 +5310,7 @@ function migrateV2ToV3(payload) {
5139
5310
  return { ...payload, schemaVersion: 3 };
5140
5311
  }
5141
5312
  async function ensureDir(filePath) {
5142
- await import_node_fs19.promises.mkdir(import_node_path33.default.dirname(filePath), { recursive: true });
5313
+ await import_node_fs19.promises.mkdir(import_node_path34.default.dirname(filePath), { recursive: true });
5143
5314
  }
5144
5315
  async function saveGraphToDisk(graph, outPath) {
5145
5316
  await ensureDir(outPath);
@@ -5216,19 +5387,19 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
5216
5387
  init_cjs_shims();
5217
5388
  var import_fastify = __toESM(require("fastify"), 1);
5218
5389
  var import_cors = __toESM(require("@fastify/cors"), 1);
5219
- var import_types24 = require("@neat.is/types");
5390
+ var import_types25 = require("@neat.is/types");
5220
5391
 
5221
5392
  // src/extend/index.ts
5222
5393
  init_cjs_shims();
5223
5394
  var import_node_fs21 = require("fs");
5224
- var import_node_path35 = __toESM(require("path"), 1);
5395
+ var import_node_path36 = __toESM(require("path"), 1);
5225
5396
  var import_node_os2 = __toESM(require("os"), 1);
5226
5397
  var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
5227
5398
 
5228
5399
  // src/installers/package-manager.ts
5229
5400
  init_cjs_shims();
5230
5401
  var import_node_fs20 = require("fs");
5231
- var import_node_path34 = __toESM(require("path"), 1);
5402
+ var import_node_path35 = __toESM(require("path"), 1);
5232
5403
  var import_node_child_process = require("child_process");
5233
5404
  var LOCKFILE_PRIORITY = [
5234
5405
  { lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
@@ -5250,22 +5421,22 @@ async function exists2(p) {
5250
5421
  }
5251
5422
  }
5252
5423
  async function detectPackageManager(serviceDir) {
5253
- let dir = import_node_path34.default.resolve(serviceDir);
5424
+ let dir = import_node_path35.default.resolve(serviceDir);
5254
5425
  const stops = /* @__PURE__ */ new Set();
5255
5426
  for (let i = 0; i < 64; i++) {
5256
5427
  if (stops.has(dir)) break;
5257
5428
  stops.add(dir);
5258
5429
  for (const candidate of LOCKFILE_PRIORITY) {
5259
- const lockPath = import_node_path34.default.join(dir, candidate.lockfile);
5430
+ const lockPath = import_node_path35.default.join(dir, candidate.lockfile);
5260
5431
  if (await exists2(lockPath)) {
5261
5432
  return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
5262
5433
  }
5263
5434
  }
5264
- const parent = import_node_path34.default.dirname(dir);
5435
+ const parent = import_node_path35.default.dirname(dir);
5265
5436
  if (parent === dir) break;
5266
5437
  dir = parent;
5267
5438
  }
5268
- return { pm: "npm", cwd: import_node_path34.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5439
+ return { pm: "npm", cwd: import_node_path35.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
5269
5440
  }
5270
5441
  async function runPackageManagerInstall(cmd) {
5271
5442
  return new Promise((resolve) => {
@@ -5314,7 +5485,7 @@ async function fileExists2(p) {
5314
5485
  }
5315
5486
  }
5316
5487
  async function readPackageJson(scanPath) {
5317
- const pkgPath = import_node_path35.default.join(scanPath, "package.json");
5488
+ const pkgPath = import_node_path36.default.join(scanPath, "package.json");
5318
5489
  const raw = await import_node_fs21.promises.readFile(pkgPath, "utf8");
5319
5490
  return JSON.parse(raw);
5320
5491
  }
@@ -5325,11 +5496,11 @@ async function findHookFiles(scanPath) {
5325
5496
  ).sort();
5326
5497
  }
5327
5498
  function extendLogPath() {
5328
- return process.env.NEAT_EXTEND_LOG ?? import_node_path35.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5499
+ return process.env.NEAT_EXTEND_LOG ?? import_node_path36.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
5329
5500
  }
5330
5501
  async function appendExtendLog(entry) {
5331
5502
  const logPath = extendLogPath();
5332
- await import_node_fs21.promises.mkdir(import_node_path35.default.dirname(logPath), { recursive: true });
5503
+ await import_node_fs21.promises.mkdir(import_node_path36.default.dirname(logPath), { recursive: true });
5333
5504
  await import_node_fs21.promises.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
5334
5505
  }
5335
5506
  function splicedContent(fileContent, snippet2) {
@@ -5388,7 +5559,7 @@ function lookupInstrumentation(library, installedVersion) {
5388
5559
  }
5389
5560
  async function describeProjectInstrumentation(ctx) {
5390
5561
  const hookFiles = await findHookFiles(ctx.scanPath);
5391
- const envNeat = await fileExists2(import_node_path35.default.join(ctx.scanPath, ".env.neat"));
5562
+ const envNeat = await fileExists2(import_node_path36.default.join(ctx.scanPath, ".env.neat"));
5392
5563
  const registryInstrPackages = new Set(
5393
5564
  (0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
5394
5565
  );
@@ -5410,16 +5581,16 @@ async function applyExtension(ctx, args, options) {
5410
5581
  );
5411
5582
  }
5412
5583
  for (const file of hookFiles) {
5413
- const content = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, file), "utf8");
5584
+ const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5414
5585
  if (content.includes(args.registration_snippet)) {
5415
5586
  return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
5416
5587
  }
5417
5588
  }
5418
5589
  const primaryFile = hookFiles[0];
5419
- const primaryPath = import_node_path35.default.join(ctx.scanPath, primaryFile);
5590
+ const primaryPath = import_node_path36.default.join(ctx.scanPath, primaryFile);
5420
5591
  const filesTouched = [];
5421
5592
  const depsAdded = [];
5422
- const pkgPath = import_node_path35.default.join(ctx.scanPath, "package.json");
5593
+ const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5423
5594
  const pkg = await readPackageJson(ctx.scanPath);
5424
5595
  if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
5425
5596
  pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
@@ -5465,7 +5636,7 @@ async function dryRunExtension(ctx, args) {
5465
5636
  };
5466
5637
  }
5467
5638
  for (const file of hookFiles) {
5468
- const content = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, file), "utf8");
5639
+ const content = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, file), "utf8");
5469
5640
  if (content.includes(args.registration_snippet)) {
5470
5641
  return {
5471
5642
  library: args.library,
@@ -5487,7 +5658,7 @@ async function dryRunExtension(ctx, args) {
5487
5658
  depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
5488
5659
  filesTouched.push("package.json");
5489
5660
  }
5490
- const hookContent = await import_node_fs21.promises.readFile(import_node_path35.default.join(ctx.scanPath, primaryFile), "utf8");
5661
+ const hookContent = await import_node_fs21.promises.readFile(import_node_path36.default.join(ctx.scanPath, primaryFile), "utf8");
5491
5662
  const patched = splicedContent(hookContent, args.registration_snippet);
5492
5663
  if (patched) {
5493
5664
  filesTouched.push(primaryFile);
@@ -5508,7 +5679,7 @@ async function rollbackExtension(ctx, args) {
5508
5679
  if (!match) {
5509
5680
  return { undone: false, message: "no apply found for library" };
5510
5681
  }
5511
- const pkgPath = import_node_path35.default.join(ctx.scanPath, "package.json");
5682
+ const pkgPath = import_node_path36.default.join(ctx.scanPath, "package.json");
5512
5683
  if (await fileExists2(pkgPath)) {
5513
5684
  const pkg = await readPackageJson(ctx.scanPath);
5514
5685
  if (pkg.dependencies?.[match.instrumentation_package]) {
@@ -5519,7 +5690,7 @@ async function rollbackExtension(ctx, args) {
5519
5690
  }
5520
5691
  const hookFiles = await findHookFiles(ctx.scanPath);
5521
5692
  for (const file of hookFiles) {
5522
- const filePath = import_node_path35.default.join(ctx.scanPath, file);
5693
+ const filePath = import_node_path36.default.join(ctx.scanPath, file);
5523
5694
  const content = await import_node_fs21.promises.readFile(filePath, "utf8");
5524
5695
  if (content.includes(match.registration_snippet)) {
5525
5696
  const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
@@ -5535,7 +5706,7 @@ async function rollbackExtension(ctx, args) {
5535
5706
 
5536
5707
  // src/divergences.ts
5537
5708
  init_cjs_shims();
5538
- var import_types22 = require("@neat.is/types");
5709
+ var import_types23 = require("@neat.is/types");
5539
5710
  function bucketKey(source, target, type) {
5540
5711
  return `${type}|${source}|${target}`;
5541
5712
  }
@@ -5543,22 +5714,22 @@ function bucketEdges(graph) {
5543
5714
  const buckets = /* @__PURE__ */ new Map();
5544
5715
  graph.forEachEdge((id, attrs) => {
5545
5716
  const e = attrs;
5546
- const parsed = (0, import_types22.parseEdgeId)(id);
5717
+ const parsed = (0, import_types23.parseEdgeId)(id);
5547
5718
  const provenance = parsed?.provenance ?? e.provenance;
5548
5719
  const key = bucketKey(e.source, e.target, e.type);
5549
5720
  const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
5550
5721
  switch (provenance) {
5551
- case import_types22.Provenance.EXTRACTED:
5722
+ case import_types23.Provenance.EXTRACTED:
5552
5723
  cur.extracted = e;
5553
5724
  break;
5554
- case import_types22.Provenance.OBSERVED:
5725
+ case import_types23.Provenance.OBSERVED:
5555
5726
  cur.observed = e;
5556
5727
  break;
5557
- case import_types22.Provenance.INFERRED:
5728
+ case import_types23.Provenance.INFERRED:
5558
5729
  cur.inferred = e;
5559
5730
  break;
5560
5731
  default:
5561
- if (e.provenance === import_types22.Provenance.STALE) cur.stale = e;
5732
+ if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
5562
5733
  }
5563
5734
  buckets.set(key, cur);
5564
5735
  });
@@ -5567,7 +5738,7 @@ function bucketEdges(graph) {
5567
5738
  function nodeIsFrontier(graph, nodeId) {
5568
5739
  if (!graph.hasNode(nodeId)) return false;
5569
5740
  const attrs = graph.getNodeAttributes(nodeId);
5570
- return attrs.type === import_types22.NodeType.FrontierNode;
5741
+ return attrs.type === import_types23.NodeType.FrontierNode;
5571
5742
  }
5572
5743
  function clampConfidence(n) {
5573
5744
  if (!Number.isFinite(n)) return 0;
@@ -5588,7 +5759,7 @@ function gradedConfidence(edge) {
5588
5759
  }
5589
5760
  function detectMissingDivergences(graph, bucket) {
5590
5761
  const out = [];
5591
- if (bucket.type === import_types22.EdgeType.CONTAINS) return out;
5762
+ if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
5592
5763
  if (bucket.extracted && !bucket.observed) {
5593
5764
  if (!nodeIsFrontier(graph, bucket.target)) {
5594
5765
  out.push({
@@ -5629,7 +5800,7 @@ function declaredHostFor(svc) {
5629
5800
  function hasExtractedConfiguredBy(graph, svcId) {
5630
5801
  for (const edgeId of graph.outboundEdges(svcId)) {
5631
5802
  const e = graph.getEdgeAttributes(edgeId);
5632
- if (e.type === import_types22.EdgeType.CONFIGURED_BY && e.provenance === import_types22.Provenance.EXTRACTED) {
5803
+ if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
5633
5804
  return true;
5634
5805
  }
5635
5806
  }
@@ -5642,10 +5813,10 @@ function detectHostMismatch(graph, svcId, svc) {
5642
5813
  const out = [];
5643
5814
  for (const edgeId of graph.outboundEdges(svcId)) {
5644
5815
  const edge = graph.getEdgeAttributes(edgeId);
5645
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5646
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5816
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5817
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5647
5818
  const target = graph.getNodeAttributes(edge.target);
5648
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5819
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5649
5820
  const observedHost = target.host?.trim();
5650
5821
  if (!observedHost) continue;
5651
5822
  if (observedHost === declaredHost) continue;
@@ -5667,10 +5838,10 @@ function detectCompatDivergences(graph, svcId, svc) {
5667
5838
  const deps = svc.dependencies ?? {};
5668
5839
  for (const edgeId of graph.outboundEdges(svcId)) {
5669
5840
  const edge = graph.getEdgeAttributes(edgeId);
5670
- if (edge.type !== import_types22.EdgeType.CONNECTS_TO) continue;
5671
- if (edge.provenance !== import_types22.Provenance.OBSERVED) continue;
5841
+ if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
5842
+ if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
5672
5843
  const target = graph.getNodeAttributes(edge.target);
5673
- if (target.type !== import_types22.NodeType.DatabaseNode) continue;
5844
+ if (target.type !== import_types23.NodeType.DatabaseNode) continue;
5674
5845
  for (const pair of compatPairs()) {
5675
5846
  if (pair.engine !== target.engine) continue;
5676
5847
  const declared = deps[pair.driver];
@@ -5731,7 +5902,7 @@ function computeDivergences(graph, opts = {}) {
5731
5902
  }
5732
5903
  graph.forEachNode((nodeId, attrs) => {
5733
5904
  const n = attrs;
5734
- if (n.type !== import_types22.NodeType.ServiceNode) return;
5905
+ if (n.type !== import_types23.NodeType.ServiceNode) return;
5735
5906
  const svc = n;
5736
5907
  for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
5737
5908
  for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
@@ -5764,7 +5935,7 @@ function computeDivergences(graph, opts = {}) {
5764
5935
  if (a.source !== b.source) return a.source.localeCompare(b.source);
5765
5936
  return a.target.localeCompare(b.target);
5766
5937
  });
5767
- return import_types22.DivergenceResultSchema.parse({
5938
+ return import_types23.DivergenceResultSchema.parse({
5768
5939
  divergences: filtered,
5769
5940
  totalAffected: filtered.length,
5770
5941
  computedAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -5850,23 +6021,23 @@ function canonicalJson(value) {
5850
6021
 
5851
6022
  // src/projects.ts
5852
6023
  init_cjs_shims();
5853
- var import_node_path36 = __toESM(require("path"), 1);
6024
+ var import_node_path37 = __toESM(require("path"), 1);
5854
6025
  function pathsForProject(project, baseDir) {
5855
6026
  if (project === DEFAULT_PROJECT) {
5856
6027
  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")
6028
+ snapshotPath: import_node_path37.default.join(baseDir, "graph.json"),
6029
+ errorsPath: import_node_path37.default.join(baseDir, "errors.ndjson"),
6030
+ staleEventsPath: import_node_path37.default.join(baseDir, "stale-events.ndjson"),
6031
+ embeddingsCachePath: import_node_path37.default.join(baseDir, "embeddings.json"),
6032
+ policyViolationsPath: import_node_path37.default.join(baseDir, "policy-violations.ndjson")
5862
6033
  };
5863
6034
  }
5864
6035
  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`)
6036
+ snapshotPath: import_node_path37.default.join(baseDir, `${project}.json`),
6037
+ errorsPath: import_node_path37.default.join(baseDir, `errors.${project}.ndjson`),
6038
+ staleEventsPath: import_node_path37.default.join(baseDir, `stale-events.${project}.ndjson`),
6039
+ embeddingsCachePath: import_node_path37.default.join(baseDir, `embeddings.${project}.json`),
6040
+ policyViolationsPath: import_node_path37.default.join(baseDir, `policy-violations.${project}.ndjson`)
5870
6041
  };
5871
6042
  }
5872
6043
  var Projects = class {
@@ -5904,23 +6075,23 @@ var Projects = class {
5904
6075
  init_cjs_shims();
5905
6076
  var import_node_fs23 = require("fs");
5906
6077
  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");
6078
+ var import_node_path38 = __toESM(require("path"), 1);
6079
+ var import_types24 = require("@neat.is/types");
5909
6080
  var LOCK_TIMEOUT_MS = 5e3;
5910
6081
  var LOCK_RETRY_MS = 50;
5911
6082
  function neatHome() {
5912
6083
  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");
6084
+ if (override && override.length > 0) return import_node_path38.default.resolve(override);
6085
+ return import_node_path38.default.join(import_node_os3.default.homedir(), ".neat");
5915
6086
  }
5916
6087
  function registryPath() {
5917
- return import_node_path37.default.join(neatHome(), "projects.json");
6088
+ return import_node_path38.default.join(neatHome(), "projects.json");
5918
6089
  }
5919
6090
  function registryLockPath() {
5920
- return import_node_path37.default.join(neatHome(), "projects.json.lock");
6091
+ return import_node_path38.default.join(neatHome(), "projects.json.lock");
5921
6092
  }
5922
6093
  function daemonPidPath() {
5923
- return import_node_path37.default.join(neatHome(), "neatd.pid");
6094
+ return import_node_path38.default.join(neatHome(), "neatd.pid");
5924
6095
  }
5925
6096
  function isPidAliveDefault(pid) {
5926
6097
  try {
@@ -5980,7 +6151,7 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
5980
6151
  }
5981
6152
  }
5982
6153
  async function normalizeProjectPath(input) {
5983
- const resolved = import_node_path37.default.resolve(input);
6154
+ const resolved = import_node_path38.default.resolve(input);
5984
6155
  try {
5985
6156
  return await import_node_fs23.promises.realpath(resolved);
5986
6157
  } catch {
@@ -5988,7 +6159,7 @@ async function normalizeProjectPath(input) {
5988
6159
  }
5989
6160
  }
5990
6161
  async function writeAtomically(target, contents) {
5991
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(target), { recursive: true });
6162
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(target), { recursive: true });
5992
6163
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
5993
6164
  const fd = await import_node_fs23.promises.open(tmp, "w");
5994
6165
  try {
@@ -6001,7 +6172,7 @@ async function writeAtomically(target, contents) {
6001
6172
  }
6002
6173
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
6003
6174
  const deadline = Date.now() + timeoutMs;
6004
- await import_node_fs23.promises.mkdir(import_node_path37.default.dirname(lockPath), { recursive: true });
6175
+ await import_node_fs23.promises.mkdir(import_node_path38.default.dirname(lockPath), { recursive: true });
6005
6176
  let probedHolder = false;
6006
6177
  while (true) {
6007
6178
  try {
@@ -6054,10 +6225,10 @@ async function readRegistry() {
6054
6225
  throw err;
6055
6226
  }
6056
6227
  const parsed = JSON.parse(raw);
6057
- return import_types23.RegistryFileSchema.parse(parsed);
6228
+ return import_types24.RegistryFileSchema.parse(parsed);
6058
6229
  }
6059
6230
  async function writeRegistry(reg) {
6060
- const validated = import_types23.RegistryFileSchema.parse(reg);
6231
+ const validated = import_types24.RegistryFileSchema.parse(reg);
6061
6232
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
6062
6233
  }
6063
6234
  var ProjectNameCollisionError = class extends Error {
@@ -6137,6 +6308,58 @@ async function removeProject(name) {
6137
6308
  return removed;
6138
6309
  });
6139
6310
  }
6311
+ var DAY_MS2 = 24 * 60 * 60 * 1e3;
6312
+ var DEFAULT_PRUNE_TTL_MS = 7 * DAY_MS2;
6313
+ function pruneTtlMs() {
6314
+ const raw = process.env.NEAT_REGISTRY_PRUNE_TTL_MS;
6315
+ if (!raw) return DEFAULT_PRUNE_TTL_MS;
6316
+ const n = Number.parseInt(raw, 10);
6317
+ if (Number.isFinite(n) && n >= 0) return n;
6318
+ console.warn(
6319
+ `[neat] NEAT_REGISTRY_PRUNE_TTL_MS could not be parsed (${raw}); using default ${DEFAULT_PRUNE_TTL_MS}ms`
6320
+ );
6321
+ return DEFAULT_PRUNE_TTL_MS;
6322
+ }
6323
+ async function statPathStatus(p) {
6324
+ try {
6325
+ const stat = await import_node_fs23.promises.stat(p);
6326
+ return stat.isDirectory() ? "present" : "unknown";
6327
+ } catch (err) {
6328
+ return err.code === "ENOENT" ? "gone" : "unknown";
6329
+ }
6330
+ }
6331
+ async function pruneRegistry(opts = {}) {
6332
+ const ttlMs = opts.ttlMs ?? pruneTtlMs();
6333
+ const statPath = opts.statPath ?? statPathStatus;
6334
+ const now = opts.now ?? Date.now;
6335
+ return withLock(async () => {
6336
+ const reg = await readRegistry();
6337
+ const removed = [];
6338
+ const kept = [];
6339
+ for (const entry of reg.projects) {
6340
+ const status2 = await statPath(entry.path);
6341
+ if (status2 !== "gone") {
6342
+ kept.push(entry);
6343
+ continue;
6344
+ }
6345
+ if (ttlMs <= 0) {
6346
+ removed.push(entry);
6347
+ continue;
6348
+ }
6349
+ const lastSeen = Date.parse(entry.lastSeenAt ?? entry.registeredAt);
6350
+ const age = now() - (Number.isFinite(lastSeen) ? lastSeen : 0);
6351
+ if (age > ttlMs) {
6352
+ removed.push(entry);
6353
+ } else {
6354
+ kept.push(entry);
6355
+ }
6356
+ }
6357
+ if (removed.length === 0) return [];
6358
+ reg.projects = kept;
6359
+ await writeRegistry(reg);
6360
+ return removed;
6361
+ });
6362
+ }
6140
6363
 
6141
6364
  // src/streaming.ts
6142
6365
  init_cjs_shims();
@@ -6150,6 +6373,7 @@ function handleSse(req, reply, opts) {
6150
6373
  reply.raw.setHeader("Connection", "keep-alive");
6151
6374
  reply.raw.setHeader("X-Accel-Buffering", "no");
6152
6375
  reply.raw.flushHeaders?.();
6376
+ reply.raw.write(":open\n\n");
6153
6377
  let pending = 0;
6154
6378
  let dropped = false;
6155
6379
  const closeConnection = () => {
@@ -6329,11 +6553,11 @@ function registerRoutes(scope, ctx) {
6329
6553
  const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
6330
6554
  const parsed = [];
6331
6555
  for (const c of candidates) {
6332
- const r = import_types24.DivergenceTypeSchema.safeParse(c);
6556
+ const r = import_types25.DivergenceTypeSchema.safeParse(c);
6333
6557
  if (!r.success) {
6334
6558
  return reply.code(400).send({
6335
6559
  error: `unknown divergence type "${c}"`,
6336
- allowed: import_types24.DivergenceTypeSchema.options
6560
+ allowed: import_types25.DivergenceTypeSchema.options
6337
6561
  });
6338
6562
  }
6339
6563
  parsed.push(r.data);
@@ -6546,7 +6770,7 @@ function registerRoutes(scope, ctx) {
6546
6770
  const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
6547
6771
  let violations = await log.readAll();
6548
6772
  if (req.query.severity) {
6549
- const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
6773
+ const sev = import_types25.PolicySeveritySchema.safeParse(req.query.severity);
6550
6774
  if (!sev.success) {
6551
6775
  return reply.code(400).send({
6552
6776
  error: "invalid severity",
@@ -6563,7 +6787,7 @@ function registerRoutes(scope, ctx) {
6563
6787
  scope.post("/policies/check", async (req, reply) => {
6564
6788
  const proj = resolveProject(registry, req, reply, ctx.bootstrap);
6565
6789
  if (!proj) return;
6566
- const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6790
+ const parsed = import_types25.PoliciesCheckBodySchema.safeParse(req.body ?? {});
6567
6791
  if (!parsed.success) {
6568
6792
  return reply.code(400).send({
6569
6793
  error: "invalid /policies/check body",
@@ -6806,14 +7030,15 @@ init_otel_grpc();
6806
7030
  // src/daemon.ts
6807
7031
  init_cjs_shims();
6808
7032
  var import_node_fs25 = require("fs");
6809
- var import_node_path41 = __toESM(require("path"), 1);
7033
+ var import_node_path42 = __toESM(require("path"), 1);
7034
+ var import_node_module = require("module");
6810
7035
  init_otel();
6811
7036
  init_auth();
6812
7037
 
6813
7038
  // src/unrouted.ts
6814
7039
  init_cjs_shims();
6815
7040
  var import_node_fs24 = require("fs");
6816
- var import_node_path40 = __toESM(require("path"), 1);
7041
+ var import_node_path41 = __toESM(require("path"), 1);
6817
7042
  function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
6818
7043
  return {
6819
7044
  timestamp: now.toISOString(),
@@ -6823,15 +7048,68 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
6823
7048
  };
6824
7049
  }
6825
7050
  async function appendUnroutedSpan(neatHome2, record) {
6826
- const target = import_node_path40.default.join(neatHome2, "errors.ndjson");
7051
+ const target = import_node_path41.default.join(neatHome2, "errors.ndjson");
6827
7052
  await import_node_fs24.promises.mkdir(neatHome2, { recursive: true });
6828
7053
  await import_node_fs24.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
6829
7054
  }
6830
7055
  function unroutedErrorsPath(neatHome2) {
6831
- return import_node_path40.default.join(neatHome2, "errors.ndjson");
7056
+ return import_node_path41.default.join(neatHome2, "errors.ndjson");
6832
7057
  }
6833
7058
 
6834
7059
  // src/daemon.ts
7060
+ function daemonJsonPath(scanPath) {
7061
+ return import_node_path42.default.join(scanPath, "neat-out", "daemon.json");
7062
+ }
7063
+ function daemonsDiscoveryDir(home) {
7064
+ const base = home && home.length > 0 ? home : neatHomeFromEnv();
7065
+ return import_node_path42.default.join(base, "daemons");
7066
+ }
7067
+ function daemonDiscoveryPath(project, home) {
7068
+ return import_node_path42.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
7069
+ }
7070
+ function sanitizeDiscoveryName(project) {
7071
+ return project.replace(/[^A-Za-z0-9._-]/g, "_");
7072
+ }
7073
+ function neatHomeFromEnv() {
7074
+ const env = process.env.NEAT_HOME;
7075
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
7076
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7077
+ return import_node_path42.default.join(home, ".neat");
7078
+ }
7079
+ function resolveNeatVersion() {
7080
+ if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
7081
+ return process.env.NEAT_LOCAL_VERSION;
7082
+ }
7083
+ try {
7084
+ const req = (0, import_node_module.createRequire)(importMetaUrl);
7085
+ const pkg = req("../package.json");
7086
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
7087
+ } catch {
7088
+ return "0.0.0";
7089
+ }
7090
+ }
7091
+ async function writeDaemonRecord(record, home) {
7092
+ const body = JSON.stringify(record, null, 2) + "\n";
7093
+ await writeAtomically(daemonJsonPath(record.projectPath), body);
7094
+ try {
7095
+ await writeAtomically(daemonDiscoveryPath(record.project, home), body);
7096
+ } catch (err) {
7097
+ console.warn(
7098
+ `neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
7099
+ );
7100
+ }
7101
+ }
7102
+ async function clearDaemonRecord(record, home) {
7103
+ try {
7104
+ const stopped = { ...record, status: "stopped" };
7105
+ await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
7106
+ } catch {
7107
+ }
7108
+ try {
7109
+ await import_node_fs25.promises.unlink(daemonDiscoveryPath(record.project, home));
7110
+ } catch {
7111
+ }
7112
+ }
6835
7113
  function teardownSlot(slot) {
6836
7114
  try {
6837
7115
  slot.stopPersist();
@@ -6843,11 +7121,11 @@ function teardownSlot(slot) {
6843
7121
  }
6844
7122
  }
6845
7123
  function neatHomeFor(opts) {
6846
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path41.default.resolve(opts.neatHome);
7124
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path42.default.resolve(opts.neatHome);
6847
7125
  const env = process.env.NEAT_HOME;
6848
- if (env && env.length > 0) return import_node_path41.default.resolve(env);
7126
+ if (env && env.length > 0) return import_node_path42.default.resolve(env);
6849
7127
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
6850
- return import_node_path41.default.join(home, ".neat");
7128
+ return import_node_path42.default.join(home, ".neat");
6851
7129
  }
6852
7130
  function routeSpanToProject(serviceName, projects) {
6853
7131
  if (!serviceName) return DEFAULT_PROJECT;
@@ -6882,7 +7160,7 @@ function isTokenContained(needle, haystack) {
6882
7160
  return tokens.includes(needle);
6883
7161
  }
6884
7162
  async function bootstrapProject(entry) {
6885
- const paths = pathsForProject(entry.name, import_node_path41.default.join(entry.path, "neat-out"));
7163
+ const paths = pathsForProject(entry.name, import_node_path42.default.join(entry.path, "neat-out"));
6886
7164
  try {
6887
7165
  const stat = await import_node_fs25.promises.stat(entry.path);
6888
7166
  if (!stat.isDirectory()) {
@@ -6948,6 +7226,23 @@ function resolveOtlpPort(opts) {
6948
7226
  }
6949
7227
  return 4318;
6950
7228
  }
7229
+ function resolveWebPort() {
7230
+ const env = process.env.NEAT_WEB_PORT;
7231
+ if (env && env.length > 0) {
7232
+ const n = Number.parseInt(env, 10);
7233
+ if (Number.isFinite(n)) return n;
7234
+ }
7235
+ return 6328;
7236
+ }
7237
+ function portFromListenAddress(address, fallback) {
7238
+ try {
7239
+ const port = new URL(address).port;
7240
+ const n = Number.parseInt(port, 10);
7241
+ if (Number.isFinite(n) && n > 0) return n;
7242
+ } catch {
7243
+ }
7244
+ return fallback;
7245
+ }
6951
7246
  function resolveHost(opts, authTokenSet) {
6952
7247
  if (opts.host && opts.host.length > 0) return opts.host;
6953
7248
  const env = process.env.HOST;
@@ -6958,14 +7253,25 @@ function resolveHost(opts, authTokenSet) {
6958
7253
  async function startDaemon(opts = {}) {
6959
7254
  const home = neatHomeFor(opts);
6960
7255
  const regPath = registryPath();
6961
- try {
6962
- await import_node_fs25.promises.access(regPath);
6963
- } catch {
7256
+ const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
7257
+ const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
7258
+ const singleProject = projectArg;
7259
+ const singleProjectPath = singleProject && projectPathArg ? import_node_path42.default.resolve(projectPathArg) : null;
7260
+ if (singleProject && !singleProjectPath) {
6964
7261
  throw new Error(
6965
- `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7262
+ `neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
6966
7263
  );
6967
7264
  }
6968
- const pidPath = import_node_path41.default.join(home, "neatd.pid");
7265
+ if (!singleProject) {
7266
+ try {
7267
+ await import_node_fs25.promises.access(regPath);
7268
+ } catch {
7269
+ throw new Error(
7270
+ `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
7271
+ );
7272
+ }
7273
+ }
7274
+ const pidPath = import_node_path42.default.join(home, "neatd.pid");
6969
7275
  await writeAtomically(pidPath, `${process.pid}
6970
7276
  `);
6971
7277
  const slots = /* @__PURE__ */ new Map();
@@ -7053,8 +7359,37 @@ async function startDaemon(opts = {}) {
7053
7359
  });
7054
7360
  }
7055
7361
  }
7362
+ async function enumerateProjects() {
7363
+ if (singleProject && singleProjectPath) {
7364
+ return [
7365
+ {
7366
+ name: singleProject,
7367
+ path: singleProjectPath,
7368
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
7369
+ languages: [],
7370
+ status: "active"
7371
+ }
7372
+ ];
7373
+ }
7374
+ return listProjects();
7375
+ }
7056
7376
  async function loadAll() {
7057
- const projects = await listProjects();
7377
+ if (!singleProject) {
7378
+ try {
7379
+ const pruned = await pruneRegistry();
7380
+ for (const entry of pruned) {
7381
+ console.log(
7382
+ `neatd: pruned project "${entry.name}" \u2014 registered path ${entry.path} is gone`
7383
+ );
7384
+ slots.delete(entry.name);
7385
+ bootstrapStatus.delete(entry.name);
7386
+ bootstrapStartedAt.delete(entry.name);
7387
+ }
7388
+ } catch (err) {
7389
+ console.warn(`neatd: registry prune skipped \u2014 ${err.message}`);
7390
+ }
7391
+ }
7392
+ const projects = await enumerateProjects();
7058
7393
  const seen = /* @__PURE__ */ new Set();
7059
7394
  const pending = [];
7060
7395
  for (const entry of projects) {
@@ -7079,7 +7414,7 @@ async function startDaemon(opts = {}) {
7079
7414
  }
7080
7415
  await Promise.allSettled(pending);
7081
7416
  }
7082
- const initialEntries = await listProjects().catch(() => []);
7417
+ const initialEntries = await enumerateProjects().catch(() => []);
7083
7418
  for (const entry of initialEntries) {
7084
7419
  bootstrapStatus.set(entry.name, "bootstrapping");
7085
7420
  bootstrapStartedAt.set(entry.name, Date.now());
@@ -7089,6 +7424,7 @@ async function startDaemon(opts = {}) {
7089
7424
  let otlpApp = null;
7090
7425
  let restAddress = "";
7091
7426
  let otlpAddress = "";
7427
+ let daemonRecord = null;
7092
7428
  if (bind) {
7093
7429
  const auth = readAuthEnv();
7094
7430
  const host = resolveHost(opts, Boolean(auth.authToken));
@@ -7113,6 +7449,20 @@ async function startDaemon(opts = {}) {
7113
7449
  }
7114
7450
  }
7115
7451
  });
7452
+ if (singleProject) {
7453
+ restApp.addHook("onSend", async (req, _reply, payload) => {
7454
+ if (req.url.split("?")[0] !== "/health") return payload;
7455
+ if (typeof payload !== "string") return payload;
7456
+ try {
7457
+ const body = JSON.parse(payload);
7458
+ if (typeof body !== "object" || body === null) return payload;
7459
+ body.project = singleProject;
7460
+ return JSON.stringify(body);
7461
+ } catch {
7462
+ return payload;
7463
+ }
7464
+ });
7465
+ }
7116
7466
  restAddress = await restApp.listen({ port: restPort, host });
7117
7467
  console.log(`neatd: REST listening on ${restAddress}`);
7118
7468
  } catch (err) {
@@ -7128,6 +7478,25 @@ async function startDaemon(opts = {}) {
7128
7478
  );
7129
7479
  }
7130
7480
  async function resolveTargetSlot(serviceName, traceId) {
7481
+ if (singleProject) {
7482
+ let slot2 = slots.get(singleProject);
7483
+ if (!slot2) {
7484
+ slot2 = await tryRecoverSlot({
7485
+ name: singleProject,
7486
+ path: singleProjectPath,
7487
+ registeredAt: (/* @__PURE__ */ new Date()).toISOString(),
7488
+ languages: [],
7489
+ status: "active"
7490
+ });
7491
+ } else if (slot2.status === "broken") {
7492
+ slot2 = await tryRecoverSlot(slot2.entry);
7493
+ }
7494
+ if (!slot2 || slot2.status !== "active") {
7495
+ warnDroppedSpan(singleProject, slot2?.errorReason ?? "unknown");
7496
+ return null;
7497
+ }
7498
+ return slot2;
7499
+ }
7131
7500
  const liveEntries = await listProjects().catch(() => []);
7132
7501
  const target = routeSpanToProject(serviceName, liveEntries);
7133
7502
  let slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
@@ -7230,6 +7599,42 @@ async function startDaemon(opts = {}) {
7230
7599
  `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
7231
7600
  );
7232
7601
  }
7602
+ if (singleProject && singleProjectPath) {
7603
+ const ports = {
7604
+ rest: portFromListenAddress(restAddress, restPort),
7605
+ otlp: portFromListenAddress(otlpAddress, otlpPort),
7606
+ // The daemon doesn't bind the web port itself (neatd spawns the web
7607
+ // child); it records the allocated value passed through so the
7608
+ // dashboard and `neat ps` agree on where to look.
7609
+ web: typeof opts.webPort === "number" ? opts.webPort : resolveWebPort()
7610
+ };
7611
+ daemonRecord = {
7612
+ project: singleProject,
7613
+ projectPath: singleProjectPath,
7614
+ pid: process.pid,
7615
+ status: "running",
7616
+ ports,
7617
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
7618
+ neatVersion: resolveNeatVersion()
7619
+ };
7620
+ try {
7621
+ await writeDaemonRecord(daemonRecord, home);
7622
+ console.log(
7623
+ `neatd: project "${singleProject}" \u2192 REST ${ports.rest} / OTLP ${ports.otlp} / web ${ports.web} (daemon.json written)`
7624
+ );
7625
+ } catch (err) {
7626
+ for (const slot of slots.values()) teardownSlot(slot);
7627
+ if (restApp) await restApp.close().catch(() => {
7628
+ });
7629
+ if (otlpApp) await otlpApp.close().catch(() => {
7630
+ });
7631
+ await import_node_fs25.promises.unlink(pidPath).catch(() => {
7632
+ });
7633
+ throw new Error(
7634
+ `neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
7635
+ );
7636
+ }
7637
+ }
7233
7638
  }
7234
7639
  const initialBootstrap = loadAll().catch((err) => {
7235
7640
  console.warn(`neatd: initial bootstrap pass failed \u2014 ${err.message}`);
@@ -7269,9 +7674,9 @@ async function startDaemon(opts = {}) {
7269
7674
  const REGISTRY_RELOAD_DEBOUNCE_MS = 500;
7270
7675
  let registryWatcher = null;
7271
7676
  let reloadTimer = null;
7272
- try {
7273
- const regDir = import_node_path41.default.dirname(regPath);
7274
- const regBase = import_node_path41.default.basename(regPath);
7677
+ if (!singleProject) try {
7678
+ const regDir = import_node_path42.default.dirname(regPath);
7679
+ const regBase = import_node_path42.default.basename(regPath);
7275
7680
  registryWatcher = (0, import_node_fs25.watch)(regDir, (_eventType, filename) => {
7276
7681
  if (filename !== null && filename !== regBase) return;
7277
7682
  if (reloadTimer) clearTimeout(reloadTimer);
@@ -7312,6 +7717,9 @@ async function startDaemon(opts = {}) {
7312
7717
  for (const slot of slots.values()) {
7313
7718
  teardownSlot(slot);
7314
7719
  }
7720
+ if (daemonRecord) {
7721
+ await clearDaemonRecord(daemonRecord, home);
7722
+ }
7315
7723
  await import_node_fs25.promises.unlink(pidPath).catch(() => {
7316
7724
  });
7317
7725
  };
@@ -7323,7 +7731,8 @@ async function startDaemon(opts = {}) {
7323
7731
  restAddress,
7324
7732
  otlpAddress,
7325
7733
  bootstrap: tracker,
7326
- initialBootstrap
7734
+ initialBootstrap,
7735
+ daemonRecord
7327
7736
  };
7328
7737
  }
7329
7738
  // Annotate the CommonJS export names for ESM import in node: