@neat.is/core 0.3.0 → 0.3.2

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
@@ -6,6 +6,13 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
11
+ };
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
9
16
  var __copyProps = (to, from, except, desc) => {
10
17
  if (from && typeof from === "object" || typeof from === "function") {
11
18
  for (let key of __getOwnPropNames(from))
@@ -23,15 +30,355 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
30
  mod
24
31
  ));
25
32
 
33
+ // ../../node_modules/tsup/assets/cjs_shims.js
34
+ var getImportMetaUrl, importMetaUrl;
35
+ var init_cjs_shims = __esm({
36
+ "../../node_modules/tsup/assets/cjs_shims.js"() {
37
+ "use strict";
38
+ getImportMetaUrl = () => typeof document === "undefined" ? new URL(`file:${__filename}`).href : document.currentScript && document.currentScript.tagName.toUpperCase() === "SCRIPT" ? document.currentScript.src : new URL("main.js", document.baseURI).href;
39
+ importMetaUrl = /* @__PURE__ */ getImportMetaUrl();
40
+ }
41
+ });
42
+
43
+ // src/otel-grpc.ts
44
+ var otel_grpc_exports = {};
45
+ __export(otel_grpc_exports, {
46
+ reshapeGrpcRequest: () => reshapeGrpcRequest,
47
+ startOtelGrpcReceiver: () => startOtelGrpcReceiver
48
+ });
49
+ function bytesToHex(buf) {
50
+ if (!buf) return "";
51
+ return Buffer.isBuffer(buf) ? buf.toString("hex") : "";
52
+ }
53
+ function nanosToString(n) {
54
+ if (n === void 0 || n === null) return "0";
55
+ return typeof n === "string" ? n : String(n);
56
+ }
57
+ function reshapeAttributes(attrs) {
58
+ const out = (attrs ?? []).map((kv) => ({
59
+ key: kv.key ?? "",
60
+ value: kv.value ? {
61
+ stringValue: kv.value.string_value,
62
+ boolValue: kv.value.bool_value,
63
+ intValue: kv.value.int_value,
64
+ doubleValue: kv.value.double_value,
65
+ arrayValue: kv.value.array_value ? {
66
+ values: (kv.value.array_value.values ?? []).map((v) => ({
67
+ stringValue: v.string_value,
68
+ boolValue: v.bool_value,
69
+ intValue: v.int_value,
70
+ doubleValue: v.double_value
71
+ }))
72
+ } : void 0
73
+ } : void 0
74
+ }));
75
+ return out;
76
+ }
77
+ function reshapeGrpcRequest(req2) {
78
+ return {
79
+ resourceSpans: (req2.resource_spans ?? []).map((rs) => ({
80
+ resource: rs.resource ? { attributes: reshapeAttributes(rs.resource.attributes) } : void 0,
81
+ scopeSpans: (rs.scope_spans ?? []).map((ss) => ({
82
+ spans: (ss.spans ?? []).map((s) => ({
83
+ traceId: bytesToHex(s.trace_id),
84
+ spanId: bytesToHex(s.span_id),
85
+ parentSpanId: s.parent_span_id ? bytesToHex(s.parent_span_id) : void 0,
86
+ name: s.name,
87
+ kind: s.kind,
88
+ startTimeUnixNano: nanosToString(s.start_time_unix_nano),
89
+ endTimeUnixNano: nanosToString(s.end_time_unix_nano),
90
+ attributes: reshapeAttributes(s.attributes),
91
+ events: (s.events ?? []).map((e) => ({
92
+ name: e.name,
93
+ timeUnixNano: nanosToString(e.time_unix_nano),
94
+ attributes: reshapeAttributes(e.attributes)
95
+ })),
96
+ status: s.status ? { code: s.status.code, message: s.status.message } : void 0
97
+ }))
98
+ }))
99
+ }))
100
+ };
101
+ }
102
+ function resolveProtoRoot() {
103
+ const here = import_node_path31.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
104
+ return import_node_path31.default.resolve(here, "..", "proto");
105
+ }
106
+ function loadTraceService() {
107
+ const protoRoot = resolveProtoRoot();
108
+ const def = protoLoader.loadSync(
109
+ "opentelemetry/proto/collector/trace/v1/trace_service.proto",
110
+ {
111
+ keepCase: true,
112
+ longs: String,
113
+ enums: Number,
114
+ defaults: true,
115
+ oneofs: true,
116
+ includeDirs: [protoRoot]
117
+ }
118
+ );
119
+ const pkg = grpc.loadPackageDefinition(def);
120
+ return pkg.opentelemetry.proto.collector.trace.v1.TraceService.service;
121
+ }
122
+ async function startOtelGrpcReceiver(opts) {
123
+ const server = new grpc.Server();
124
+ const service = loadTraceService();
125
+ server.addService(service, {
126
+ Export: (call, callback) => {
127
+ void (async () => {
128
+ try {
129
+ const reshaped = reshapeGrpcRequest(call.request ?? {});
130
+ const spans = parseOtlpRequest(reshaped);
131
+ for (const span of spans) {
132
+ await opts.onSpan(span);
133
+ }
134
+ callback(null, { partial_success: {} });
135
+ } catch (err) {
136
+ callback({
137
+ code: grpc.status.INTERNAL,
138
+ message: err instanceof Error ? err.message : String(err)
139
+ });
140
+ }
141
+ })();
142
+ }
143
+ });
144
+ const host = opts.host ?? "0.0.0.0";
145
+ const port = opts.port ?? 4317;
146
+ const boundPort = await new Promise((resolve, reject) => {
147
+ server.bindAsync(`${host}:${port}`, grpc.ServerCredentials.createInsecure(), (err, p) => {
148
+ if (err) return reject(err);
149
+ resolve(p);
150
+ });
151
+ });
152
+ return {
153
+ address: `${host}:${boundPort}`,
154
+ stop: () => new Promise((resolve) => {
155
+ server.tryShutdown(() => resolve());
156
+ })
157
+ };
158
+ }
159
+ var import_node_url, import_node_path31, grpc, protoLoader;
160
+ var init_otel_grpc = __esm({
161
+ "src/otel-grpc.ts"() {
162
+ "use strict";
163
+ init_cjs_shims();
164
+ import_node_url = require("url");
165
+ import_node_path31 = __toESM(require("path"), 1);
166
+ grpc = __toESM(require("@grpc/grpc-js"), 1);
167
+ protoLoader = __toESM(require("@grpc/proto-loader"), 1);
168
+ init_otel();
169
+ }
170
+ });
171
+
172
+ // src/otel.ts
173
+ function extractExceptionFromEvents(events) {
174
+ if (!events) return void 0;
175
+ for (const ev of events) {
176
+ if (ev.name !== "exception") continue;
177
+ const attrs = attrsToRecord(ev.attributes);
178
+ const out = {};
179
+ const t = attrs["exception.type"];
180
+ const m = attrs["exception.message"];
181
+ const s = attrs["exception.stacktrace"];
182
+ if (typeof t === "string") out.type = t;
183
+ if (typeof m === "string") out.message = m;
184
+ if (typeof s === "string") out.stacktrace = s;
185
+ if (out.type || out.message || out.stacktrace) return out;
186
+ }
187
+ return void 0;
188
+ }
189
+ function flattenAttribute(v) {
190
+ if (!v) return null;
191
+ if (v.stringValue !== void 0) return v.stringValue;
192
+ if (v.boolValue !== void 0) return v.boolValue;
193
+ if (v.intValue !== void 0) {
194
+ return typeof v.intValue === "string" ? Number(v.intValue) : v.intValue;
195
+ }
196
+ if (v.doubleValue !== void 0) return v.doubleValue;
197
+ if (v.arrayValue?.values) {
198
+ return v.arrayValue.values.map((x) => flattenAttribute(x));
199
+ }
200
+ return null;
201
+ }
202
+ function attrsToRecord(attrs) {
203
+ const out = {};
204
+ if (!attrs) return out;
205
+ for (const kv of attrs) {
206
+ if (kv.key) out[kv.key] = flattenAttribute(kv.value);
207
+ }
208
+ return out;
209
+ }
210
+ function durationNanos(start, end) {
211
+ if (!start || !end) return 0n;
212
+ try {
213
+ return BigInt(end) - BigInt(start);
214
+ } catch {
215
+ return 0n;
216
+ }
217
+ }
218
+ function isoFromUnixNano(nanos) {
219
+ if (!nanos || nanos === "0") return void 0;
220
+ try {
221
+ const ms = Number(BigInt(nanos) / 1000000n);
222
+ if (!Number.isFinite(ms)) return void 0;
223
+ return new Date(ms).toISOString();
224
+ } catch {
225
+ return void 0;
226
+ }
227
+ }
228
+ function parseOtlpRequest(body) {
229
+ const out = [];
230
+ for (const rs of body.resourceSpans ?? []) {
231
+ const resourceAttrs = attrsToRecord(rs.resource?.attributes);
232
+ const service = typeof resourceAttrs["service.name"] === "string" ? resourceAttrs["service.name"] : "unknown";
233
+ for (const ss of rs.scopeSpans ?? []) {
234
+ for (const span of ss.spans ?? []) {
235
+ const attrs = attrsToRecord(span.attributes);
236
+ const parsed = {
237
+ service,
238
+ traceId: span.traceId ?? "",
239
+ spanId: span.spanId ?? "",
240
+ parentSpanId: span.parentSpanId || void 0,
241
+ name: span.name ?? "",
242
+ kind: span.kind,
243
+ startTimeUnixNano: span.startTimeUnixNano ?? "0",
244
+ endTimeUnixNano: span.endTimeUnixNano ?? "0",
245
+ startTimeIso: isoFromUnixNano(span.startTimeUnixNano),
246
+ durationNanos: durationNanos(span.startTimeUnixNano, span.endTimeUnixNano),
247
+ attributes: attrs,
248
+ dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
249
+ dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
250
+ statusCode: span.status?.code,
251
+ errorMessage: span.status?.message,
252
+ exception: extractExceptionFromEvents(span.events)
253
+ };
254
+ out.push(parsed);
255
+ }
256
+ }
257
+ }
258
+ return out;
259
+ }
260
+ function loadProtobufDecoder() {
261
+ if (exportTraceServiceRequestType) return exportTraceServiceRequestType;
262
+ const here = import_node_path32.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
263
+ const protoRoot = import_node_path32.default.resolve(here, "..", "proto");
264
+ const root = new import_protobufjs.default.Root();
265
+ root.resolvePath = (_origin, target) => import_node_path32.default.resolve(protoRoot, target);
266
+ root.loadSync(
267
+ "opentelemetry/proto/collector/trace/v1/trace_service.proto",
268
+ { keepCase: true }
269
+ );
270
+ exportTraceServiceRequestType = root.lookupType(
271
+ "opentelemetry.proto.collector.trace.v1.ExportTraceServiceRequest"
272
+ );
273
+ return exportTraceServiceRequestType;
274
+ }
275
+ async function decodeProtobufBody(buf) {
276
+ const Type = loadProtobufDecoder();
277
+ const decoded = Type.decode(buf).toJSON();
278
+ const { reshapeGrpcRequest: reshapeGrpcRequest2 } = await Promise.resolve().then(() => (init_otel_grpc(), otel_grpc_exports));
279
+ return reshapeGrpcRequest2(decoded);
280
+ }
281
+ async function buildOtelReceiver(opts) {
282
+ const app = (0, import_fastify2.default)({
283
+ logger: false,
284
+ bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
285
+ });
286
+ const queue = [];
287
+ let draining = false;
288
+ let drainPromise = Promise.resolve();
289
+ const drain = async () => {
290
+ if (draining) return;
291
+ draining = true;
292
+ try {
293
+ while (queue.length > 0) {
294
+ const span = queue.shift();
295
+ try {
296
+ await opts.onSpan(span);
297
+ } catch (err) {
298
+ console.warn(`[neat] otel handler error: ${err.message}`);
299
+ }
300
+ }
301
+ } finally {
302
+ draining = false;
303
+ }
304
+ };
305
+ const enqueue = (spans) => {
306
+ if (spans.length === 0) return;
307
+ for (const s of spans) queue.push(s);
308
+ drainPromise = drainPromise.then(() => drain());
309
+ };
310
+ app.addContentTypeParser(
311
+ "application/x-protobuf",
312
+ { parseAs: "buffer", bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024 },
313
+ (_req, body, done) => {
314
+ done(null, body);
315
+ }
316
+ );
317
+ app.get("/health", async () => ({ ok: true }));
318
+ app.post("/v1/traces", async (req2, reply) => {
319
+ const ct = (req2.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
320
+ let body;
321
+ if (ct === "application/x-protobuf") {
322
+ try {
323
+ body = await decodeProtobufBody(req2.body);
324
+ } catch (err) {
325
+ return reply.code(400).send({
326
+ error: `protobuf decode failed: ${err.message}`
327
+ });
328
+ }
329
+ } else if (!ct || ct === "application/json") {
330
+ body = req2.body ?? {};
331
+ } else {
332
+ return reply.code(415).send({ error: `unsupported content-type: ${ct}` });
333
+ }
334
+ const spans = parseOtlpRequest(body);
335
+ if (opts.onErrorSpanSync) {
336
+ try {
337
+ for (const span of spans) {
338
+ if (span.statusCode === 2) await opts.onErrorSpanSync(span);
339
+ }
340
+ } catch (err) {
341
+ return reply.code(500).send({
342
+ error: `error-event write failed: ${err.message}`
343
+ });
344
+ }
345
+ }
346
+ enqueue(spans);
347
+ return reply.code(200).send({ partialSuccess: {} });
348
+ });
349
+ const decorated = app;
350
+ decorated.flushPending = async () => {
351
+ while (queue.length > 0 || draining) {
352
+ await drainPromise;
353
+ }
354
+ };
355
+ return decorated;
356
+ }
357
+ var import_node_path32, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
358
+ var init_otel = __esm({
359
+ "src/otel.ts"() {
360
+ "use strict";
361
+ init_cjs_shims();
362
+ import_node_path32 = __toESM(require("path"), 1);
363
+ import_node_url2 = require("url");
364
+ import_fastify2 = __toESM(require("fastify"), 1);
365
+ import_protobufjs = __toESM(require("protobufjs"), 1);
366
+ exportTraceServiceRequestType = null;
367
+ }
368
+ });
369
+
26
370
  // src/neatd.ts
27
- var import_node_fs19 = require("fs");
28
- var import_node_path33 = __toESM(require("path"), 1);
371
+ init_cjs_shims();
372
+ var import_node_fs20 = require("fs");
373
+ var import_node_path35 = __toESM(require("path"), 1);
29
374
 
30
375
  // src/daemon.ts
31
- var import_node_fs18 = require("fs");
32
- var import_node_path31 = __toESM(require("path"), 1);
376
+ init_cjs_shims();
377
+ var import_node_fs19 = require("fs");
378
+ var import_node_path33 = __toESM(require("path"), 1);
33
379
 
34
380
  // src/graph.ts
381
+ init_cjs_shims();
35
382
  var import_graphology = __toESM(require("graphology"), 1);
36
383
  var MultiDirectedGraph = import_graphology.default.MultiDirectedGraph;
37
384
  var DEFAULT_PROJECT = "default";
@@ -55,16 +402,25 @@ function resetGraph(project) {
55
402
  graphs.delete(project);
56
403
  }
57
404
 
405
+ // src/extract.ts
406
+ init_cjs_shims();
407
+
408
+ // src/extract/index.ts
409
+ init_cjs_shims();
410
+
58
411
  // src/ingest.ts
412
+ init_cjs_shims();
59
413
  var import_node_fs3 = require("fs");
60
414
  var import_node_path3 = __toESM(require("path"), 1);
61
415
 
62
416
  // src/policy.ts
417
+ init_cjs_shims();
63
418
  var import_node_fs2 = require("fs");
64
419
  var import_node_path2 = __toESM(require("path"), 1);
65
420
  var import_types2 = require("@neat.is/types");
66
421
 
67
422
  // src/compat.ts
423
+ init_cjs_shims();
68
424
  var import_node_fs = require("fs");
69
425
  var import_node_os = __toESM(require("os"), 1);
70
426
  var import_node_path = __toESM(require("path"), 1);
@@ -377,6 +733,7 @@ function deprecatedApis() {
377
733
  }
378
734
 
379
735
  // src/events.ts
736
+ init_cjs_shims();
380
737
  var import_node_events = require("events");
381
738
  var EVENT_BUS_CHANNEL = "event";
382
739
  var NeatEventBus = class extends import_node_events.EventEmitter {
@@ -388,8 +745,22 @@ function emitNeatEvent(envelope) {
388
745
  }
389
746
 
390
747
  // src/traverse.ts
748
+ init_cjs_shims();
391
749
  var import_types = require("@neat.is/types");
750
+ var ROOT_CAUSE_MAX_DEPTH = 5;
392
751
  var BLAST_RADIUS_DEFAULT_DEPTH = 10;
752
+ function bestEdgeBySource(graph, edgeIds) {
753
+ const best = /* @__PURE__ */ new Map();
754
+ for (const id of edgeIds) {
755
+ const e = graph.getEdgeAttributes(id);
756
+ if (e.provenance === import_types.Provenance.FRONTIER) continue;
757
+ const cur = best.get(e.source);
758
+ if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
759
+ best.set(e.source, e);
760
+ }
761
+ }
762
+ return best;
763
+ }
393
764
  function bestEdgeByTarget(graph, edgeIds) {
394
765
  const best = /* @__PURE__ */ new Map();
395
766
  for (const id of edgeIds) {
@@ -457,6 +828,29 @@ function confidenceFromMix(edges, now = Date.now()) {
457
828
  }
458
829
  return Math.max(0, Math.min(1, product));
459
830
  }
831
+ function longestIncomingWalk(graph, start, maxDepth) {
832
+ let best = { path: [start], edges: [] };
833
+ const visited = /* @__PURE__ */ new Set([start]);
834
+ function step(node, path36, edges) {
835
+ if (path36.length > best.path.length) {
836
+ best = { path: [...path36], edges: [...edges] };
837
+ }
838
+ if (path36.length - 1 >= maxDepth) return;
839
+ const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
840
+ for (const [srcId, edge] of incoming) {
841
+ if (visited.has(srcId)) continue;
842
+ visited.add(srcId);
843
+ path36.push(srcId);
844
+ edges.push(edge);
845
+ step(srcId, path36, edges);
846
+ path36.pop();
847
+ edges.pop();
848
+ visited.delete(srcId);
849
+ }
850
+ }
851
+ step(start, [start], []);
852
+ return best;
853
+ }
460
854
  function databaseRootCauseShape(graph, origin, walk) {
461
855
  const targetDb = origin;
462
856
  const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
@@ -529,6 +923,24 @@ var rootCauseShapes = {
529
923
  [import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
530
924
  [import_types.NodeType.ServiceNode]: serviceRootCauseShape
531
925
  };
926
+ function getRootCause(graph, errorNodeId, errorEvent) {
927
+ if (!graph.hasNode(errorNodeId)) return null;
928
+ const origin = graph.getNodeAttributes(errorNodeId);
929
+ const shape = rootCauseShapes[origin.type];
930
+ if (!shape) return null;
931
+ const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
932
+ const match = shape(graph, origin, walk);
933
+ if (!match) return null;
934
+ const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
935
+ return import_types.RootCauseResultSchema.parse({
936
+ rootCauseNode: match.rootCauseNode,
937
+ rootCauseReason: reason,
938
+ traversalPath: walk.path,
939
+ edgeProvenances: walk.edges.map((e) => e.provenance),
940
+ confidence: confidenceFromMix(walk.edges),
941
+ fixRecommendation: match.fixRecommendation
942
+ });
943
+ }
532
944
  function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
533
945
  if (!graph.hasNode(nodeId)) {
534
946
  return import_types.BlastRadiusResultSchema.parse({ origin: nodeId, affectedNodes: [], totalAffected: 0 });
@@ -570,6 +982,48 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
570
982
  totalAffected: affectedNodes.length
571
983
  });
572
984
  }
985
+ var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
986
+ var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
987
+ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
988
+ if (!graph.hasNode(nodeId)) {
989
+ return import_types.TransitiveDependenciesResultSchema.parse({
990
+ origin: nodeId,
991
+ depth,
992
+ dependencies: [],
993
+ total: 0
994
+ });
995
+ }
996
+ const seen = /* @__PURE__ */ new Map();
997
+ const queue = [{ nodeId, distance: 0, edge: null }];
998
+ const enqueued = /* @__PURE__ */ new Set([nodeId]);
999
+ while (queue.length > 0) {
1000
+ const frame = queue.shift();
1001
+ if (frame.distance > 0 && frame.edge) {
1002
+ seen.set(frame.nodeId, {
1003
+ nodeId: frame.nodeId,
1004
+ distance: frame.distance,
1005
+ edgeType: frame.edge.type,
1006
+ provenance: frame.edge.provenance
1007
+ });
1008
+ }
1009
+ if (frame.distance >= depth) continue;
1010
+ const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
1011
+ for (const [tgtId, edge] of outgoing) {
1012
+ if (enqueued.has(tgtId)) continue;
1013
+ enqueued.add(tgtId);
1014
+ queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
1015
+ }
1016
+ }
1017
+ const dependencies = [...seen.values()].sort(
1018
+ (a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
1019
+ );
1020
+ return import_types.TransitiveDependenciesResultSchema.parse({
1021
+ origin: nodeId,
1022
+ depth,
1023
+ dependencies,
1024
+ total: dependencies.length
1025
+ });
1026
+ }
573
1027
 
574
1028
  // src/policy.ts
575
1029
  var DEFAULT_ACTION_BY_SEVERITY = {
@@ -839,6 +1293,54 @@ function evaluateAllPolicies(graph, policies, ctx) {
839
1293
  }
840
1294
  return out;
841
1295
  }
1296
+ async function loadPolicyFile(policyPath) {
1297
+ let raw;
1298
+ try {
1299
+ raw = await import_node_fs2.promises.readFile(policyPath, "utf8");
1300
+ } catch (err) {
1301
+ if (err.code === "ENOENT") return [];
1302
+ throw err;
1303
+ }
1304
+ const json = JSON.parse(raw);
1305
+ const file = import_types2.PolicyFileSchema.parse(json);
1306
+ return file.policies;
1307
+ }
1308
+ var PolicyViolationsLog = class {
1309
+ path;
1310
+ project;
1311
+ seen = null;
1312
+ constructor(logPath, project = DEFAULT_PROJECT) {
1313
+ this.path = logPath;
1314
+ this.project = project;
1315
+ }
1316
+ async append(v) {
1317
+ if (!this.seen) await this.hydrate();
1318
+ if (this.seen.has(v.id)) return false;
1319
+ this.seen.add(v.id);
1320
+ await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
1321
+ await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
1322
+ emitNeatEvent({
1323
+ type: "policy-violation",
1324
+ project: this.project,
1325
+ payload: { violation: v }
1326
+ });
1327
+ return true;
1328
+ }
1329
+ async readAll() {
1330
+ try {
1331
+ const raw = await import_node_fs2.promises.readFile(this.path, "utf8");
1332
+ return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
1333
+ } catch (err) {
1334
+ if (err.code === "ENOENT") return [];
1335
+ throw err;
1336
+ }
1337
+ }
1338
+ async hydrate() {
1339
+ this.seen = /* @__PURE__ */ new Set();
1340
+ const existing = await this.readAll();
1341
+ for (const v of existing) this.seen.add(v.id);
1342
+ }
1343
+ };
842
1344
 
843
1345
  // src/ingest.ts
844
1346
  var import_types3 = require("@neat.is/types");
@@ -853,7 +1355,342 @@ var DEFAULT_STALE_THRESHOLDS = {
853
1355
  CONFIGURED_BY: DAY_MS,
854
1356
  RUNS_ON: DAY_MS
855
1357
  };
1358
+ function nowIso(ctx) {
1359
+ return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
1360
+ }
1361
+ function pickAttr(span, ...keys) {
1362
+ for (const k of keys) {
1363
+ const v = span.attributes[k];
1364
+ if (typeof v === "string" && v.length > 0) return v;
1365
+ }
1366
+ return void 0;
1367
+ }
1368
+ function hostFromUrl(u) {
1369
+ if (!u) return void 0;
1370
+ try {
1371
+ return new URL(u).hostname;
1372
+ } catch {
1373
+ return void 0;
1374
+ }
1375
+ }
1376
+ function pickAddress(span) {
1377
+ return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
1378
+ }
1379
+ function makeObservedEdgeId(type, source, target) {
1380
+ return (0, import_types3.observedEdgeId)(source, target, type);
1381
+ }
1382
+ function makeInferredEdgeId(type, source, target) {
1383
+ return (0, import_types3.inferredEdgeId)(source, target, type);
1384
+ }
1385
+ var INFERRED_CONFIDENCE = 0.6;
1386
+ var STITCH_MAX_DEPTH = 2;
1387
+ var PARENT_SPAN_CACHE_SIZE = 1e4;
856
1388
  var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
1389
+ var parentSpanCache = /* @__PURE__ */ new Map();
1390
+ function parentSpanKey(traceId, spanId) {
1391
+ return `${traceId}:${spanId}`;
1392
+ }
1393
+ function cacheSpanService(span, now) {
1394
+ if (!span.traceId || !span.spanId) return;
1395
+ const key = parentSpanKey(span.traceId, span.spanId);
1396
+ parentSpanCache.delete(key);
1397
+ parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
1398
+ while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
1399
+ const oldest = parentSpanCache.keys().next().value;
1400
+ if (!oldest) break;
1401
+ parentSpanCache.delete(oldest);
1402
+ }
1403
+ }
1404
+ function lookupParentSpanService(traceId, parentSpanId, now) {
1405
+ const entry2 = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
1406
+ if (!entry2) return null;
1407
+ if (entry2.expiresAt <= now) {
1408
+ parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
1409
+ return null;
1410
+ }
1411
+ return entry2.service;
1412
+ }
1413
+ function resolveServiceId(graph, host) {
1414
+ const direct = (0, import_types3.serviceId)(host);
1415
+ if (graph.hasNode(direct)) return direct;
1416
+ let found = null;
1417
+ graph.forEachNode((id, attrs) => {
1418
+ if (found) return;
1419
+ const a = attrs;
1420
+ if (a.type !== import_types3.NodeType.ServiceNode) return;
1421
+ if (a.name === host) {
1422
+ found = id;
1423
+ return;
1424
+ }
1425
+ if (a.aliases && a.aliases.includes(host)) {
1426
+ found = id;
1427
+ }
1428
+ });
1429
+ return found;
1430
+ }
1431
+ function frontierIdFor(host) {
1432
+ return (0, import_types3.frontierId)(host);
1433
+ }
1434
+ function ensureServiceNode(graph, serviceName) {
1435
+ const id = (0, import_types3.serviceId)(serviceName);
1436
+ if (graph.hasNode(id)) return id;
1437
+ const node = {
1438
+ id,
1439
+ type: import_types3.NodeType.ServiceNode,
1440
+ name: serviceName,
1441
+ language: "unknown",
1442
+ discoveredVia: "otel"
1443
+ };
1444
+ graph.addNode(id, node);
1445
+ return id;
1446
+ }
1447
+ function ensureDatabaseNode(graph, host, engine) {
1448
+ const id = (0, import_types3.databaseId)(host);
1449
+ if (graph.hasNode(id)) return id;
1450
+ const node = {
1451
+ id,
1452
+ type: import_types3.NodeType.DatabaseNode,
1453
+ name: host,
1454
+ engine,
1455
+ engineVersion: "unknown",
1456
+ compatibleDrivers: [],
1457
+ host,
1458
+ discoveredVia: "otel"
1459
+ };
1460
+ graph.addNode(id, node);
1461
+ return id;
1462
+ }
1463
+ function ensureFrontierNode(graph, host, ts) {
1464
+ const id = frontierIdFor(host);
1465
+ if (graph.hasNode(id)) {
1466
+ const existing = graph.getNodeAttributes(id);
1467
+ graph.replaceNodeAttributes(id, { ...existing, lastObserved: ts });
1468
+ return id;
1469
+ }
1470
+ const node = {
1471
+ id,
1472
+ type: import_types3.NodeType.FrontierNode,
1473
+ name: host,
1474
+ host,
1475
+ firstObserved: ts,
1476
+ lastObserved: ts
1477
+ };
1478
+ graph.addNode(id, node);
1479
+ return id;
1480
+ }
1481
+ function upsertFrontierEdge(graph, type, source, target, ts) {
1482
+ const id = (0, import_types3.frontierEdgeId)(source, target, type);
1483
+ if (graph.hasEdge(id)) {
1484
+ const existing = graph.getEdgeAttributes(id);
1485
+ const updated = {
1486
+ ...existing,
1487
+ provenance: import_types3.Provenance.FRONTIER,
1488
+ lastObserved: ts,
1489
+ callCount: (existing.callCount ?? 0) + 1
1490
+ };
1491
+ graph.replaceEdgeAttributes(id, updated);
1492
+ return;
1493
+ }
1494
+ const edge = {
1495
+ id,
1496
+ source,
1497
+ target,
1498
+ type,
1499
+ provenance: import_types3.Provenance.FRONTIER,
1500
+ confidence: 1,
1501
+ lastObserved: ts,
1502
+ callCount: 1
1503
+ };
1504
+ graph.addEdgeWithKey(id, source, target, edge);
1505
+ }
1506
+ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
1507
+ if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
1508
+ const id = makeObservedEdgeId(type, source, target);
1509
+ if (graph.hasEdge(id)) {
1510
+ const existing = graph.getEdgeAttributes(id);
1511
+ const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
1512
+ const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
1513
+ const updated = {
1514
+ ...existing,
1515
+ provenance: import_types3.Provenance.OBSERVED,
1516
+ lastObserved: ts,
1517
+ callCount: newSpanCount,
1518
+ signal: {
1519
+ spanCount: newSpanCount,
1520
+ errorCount: newErrorCount,
1521
+ lastObservedAgeMs: 0
1522
+ },
1523
+ confidence: 1
1524
+ };
1525
+ graph.replaceEdgeAttributes(id, updated);
1526
+ return { edge: updated, created: false };
1527
+ }
1528
+ const edge = {
1529
+ id,
1530
+ source,
1531
+ target,
1532
+ type,
1533
+ provenance: import_types3.Provenance.OBSERVED,
1534
+ confidence: 1,
1535
+ lastObserved: ts,
1536
+ callCount: 1,
1537
+ signal: {
1538
+ spanCount: 1,
1539
+ errorCount: isError ? 1 : 0,
1540
+ lastObservedAgeMs: 0
1541
+ }
1542
+ };
1543
+ graph.addEdgeWithKey(id, source, target, edge);
1544
+ return { edge, created: true };
1545
+ }
1546
+ function stitchTrace(graph, sourceServiceId, ts) {
1547
+ if (!graph.hasNode(sourceServiceId)) return;
1548
+ const visited = /* @__PURE__ */ new Set([sourceServiceId]);
1549
+ const queue = [{ nodeId: sourceServiceId, depth: 0 }];
1550
+ while (queue.length > 0) {
1551
+ const { nodeId, depth } = queue.shift();
1552
+ if (depth >= STITCH_MAX_DEPTH) continue;
1553
+ const outbound = graph.outboundEdges(nodeId);
1554
+ for (const edgeId of outbound) {
1555
+ const edge = graph.getEdgeAttributes(edgeId);
1556
+ if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
1557
+ if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
1558
+ upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
1559
+ if (!visited.has(edge.target)) {
1560
+ visited.add(edge.target);
1561
+ queue.push({ nodeId: edge.target, depth: depth + 1 });
1562
+ }
1563
+ }
1564
+ }
1565
+ }
1566
+ function upsertInferredEdge(graph, type, source, target, ts) {
1567
+ const id = makeInferredEdgeId(type, source, target);
1568
+ if (graph.hasEdge(id)) {
1569
+ const existing = graph.getEdgeAttributes(id);
1570
+ const updated = { ...existing, lastObserved: ts };
1571
+ graph.replaceEdgeAttributes(id, updated);
1572
+ return;
1573
+ }
1574
+ const edge = {
1575
+ id,
1576
+ source,
1577
+ target,
1578
+ type,
1579
+ provenance: import_types3.Provenance.INFERRED,
1580
+ confidence: INFERRED_CONFIDENCE,
1581
+ lastObserved: ts
1582
+ };
1583
+ graph.addEdgeWithKey(id, source, target, edge);
1584
+ }
1585
+ async function appendErrorEvent(ctx, ev) {
1586
+ await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
1587
+ await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
1588
+ }
1589
+ function buildErrorEventForReceiver(span) {
1590
+ if (span.statusCode !== 2) return null;
1591
+ const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
1592
+ return {
1593
+ id: `${span.traceId}:${span.spanId}`,
1594
+ timestamp: ts,
1595
+ service: span.service,
1596
+ traceId: span.traceId,
1597
+ spanId: span.spanId,
1598
+ errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1599
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1600
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1601
+ affectedNode: (0, import_types3.serviceId)(span.service)
1602
+ };
1603
+ }
1604
+ function makeErrorSpanWriter(errorsPath) {
1605
+ return async (span) => {
1606
+ const ev = buildErrorEventForReceiver(span);
1607
+ if (!ev) return;
1608
+ await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(errorsPath), { recursive: true });
1609
+ await import_node_fs3.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
1610
+ };
1611
+ }
1612
+ async function handleSpan(ctx, span) {
1613
+ const ts = span.startTimeIso ?? nowIso(ctx);
1614
+ const nowMs = ctx.now ? ctx.now() : Date.now();
1615
+ const sourceId = ensureServiceNode(ctx.graph, span.service);
1616
+ const isError = span.statusCode === 2;
1617
+ cacheSpanService(span, nowMs);
1618
+ let affectedNode = sourceId;
1619
+ if (span.dbSystem) {
1620
+ const host = pickAddress(span);
1621
+ if (host) {
1622
+ ensureDatabaseNode(ctx.graph, host, span.dbSystem);
1623
+ const targetId = (0, import_types3.databaseId)(host);
1624
+ const result = upsertObservedEdge(
1625
+ ctx.graph,
1626
+ import_types3.EdgeType.CONNECTS_TO,
1627
+ sourceId,
1628
+ targetId,
1629
+ ts,
1630
+ isError
1631
+ );
1632
+ if (result) affectedNode = targetId;
1633
+ }
1634
+ } else {
1635
+ const host = pickAddress(span);
1636
+ let resolvedViaAddress = false;
1637
+ if (host && host !== span.service) {
1638
+ const targetId = resolveServiceId(ctx.graph, host);
1639
+ if (targetId && targetId !== sourceId) {
1640
+ upsertObservedEdge(
1641
+ ctx.graph,
1642
+ import_types3.EdgeType.CALLS,
1643
+ sourceId,
1644
+ targetId,
1645
+ ts,
1646
+ isError
1647
+ );
1648
+ affectedNode = targetId;
1649
+ resolvedViaAddress = true;
1650
+ } else if (!targetId) {
1651
+ const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
1652
+ if (ctx.graph.hasNode(sourceId)) {
1653
+ upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
1654
+ }
1655
+ affectedNode = frontierId2;
1656
+ resolvedViaAddress = true;
1657
+ }
1658
+ }
1659
+ if (!resolvedViaAddress && span.parentSpanId) {
1660
+ const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
1661
+ if (parentService && parentService !== span.service) {
1662
+ const parentId = ensureServiceNode(ctx.graph, parentService);
1663
+ upsertObservedEdge(
1664
+ ctx.graph,
1665
+ import_types3.EdgeType.CALLS,
1666
+ parentId,
1667
+ sourceId,
1668
+ ts,
1669
+ isError
1670
+ );
1671
+ }
1672
+ }
1673
+ }
1674
+ if (span.statusCode === 2) {
1675
+ stitchTrace(ctx.graph, sourceId, ts);
1676
+ if (ctx.writeErrorEventInline !== false) {
1677
+ const ev = {
1678
+ id: `${span.traceId}:${span.spanId}`,
1679
+ timestamp: ts,
1680
+ service: span.service,
1681
+ traceId: span.traceId,
1682
+ spanId: span.spanId,
1683
+ errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
1684
+ ...span.exception?.type ? { exceptionType: span.exception.type } : {},
1685
+ ...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
1686
+ affectedNode
1687
+ };
1688
+ await appendErrorEvent(ctx, ev);
1689
+ }
1690
+ }
1691
+ void affectedNode;
1692
+ if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
1693
+ }
857
1694
  function promoteFrontierNodes(graph, opts = {}) {
858
1695
  const aliasIndex = /* @__PURE__ */ new Map();
859
1696
  graph.forEachNode((id, attrs) => {
@@ -927,8 +1764,27 @@ function pickLater(a, b) {
927
1764
  if (!b) return a;
928
1765
  return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
929
1766
  }
1767
+ async function readStaleEvents(staleEventsPath) {
1768
+ try {
1769
+ const raw = await import_node_fs3.promises.readFile(staleEventsPath, "utf8");
1770
+ return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
1771
+ } catch (err) {
1772
+ if (err.code === "ENOENT") return [];
1773
+ throw err;
1774
+ }
1775
+ }
1776
+ async function readErrorEvents(errorsPath) {
1777
+ try {
1778
+ const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
1779
+ return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
1780
+ } catch (err) {
1781
+ if (err.code === "ENOENT") return [];
1782
+ throw err;
1783
+ }
1784
+ }
930
1785
 
931
1786
  // src/extract/services.ts
1787
+ init_cjs_shims();
932
1788
  var import_node_fs7 = require("fs");
933
1789
  var import_node_path7 = __toESM(require("path"), 1);
934
1790
  var import_ignore = __toESM(require("ignore"), 1);
@@ -936,6 +1792,7 @@ var import_minimatch2 = require("minimatch");
936
1792
  var import_types5 = require("@neat.is/types");
937
1793
 
938
1794
  // src/extract/shared.ts
1795
+ init_cjs_shims();
939
1796
  var import_node_fs4 = require("fs");
940
1797
  var import_node_path4 = __toESM(require("path"), 1);
941
1798
  var import_yaml = require("yaml");
@@ -978,6 +1835,7 @@ async function exists(p) {
978
1835
  }
979
1836
 
980
1837
  // src/extract/python.ts
1838
+ init_cjs_shims();
981
1839
  var import_node_fs5 = require("fs");
982
1840
  var import_node_path5 = __toESM(require("path"), 1);
983
1841
  var import_smol_toml = require("smol-toml");
@@ -1044,6 +1902,7 @@ function pythonToPackage(service) {
1044
1902
  }
1045
1903
 
1046
1904
  // src/extract/owners.ts
1905
+ init_cjs_shims();
1047
1906
  var import_node_fs6 = require("fs");
1048
1907
  var import_node_path6 = __toESM(require("path"), 1);
1049
1908
  var import_minimatch = require("minimatch");
@@ -1294,6 +2153,7 @@ function addServiceNodes(graph, services) {
1294
2153
  }
1295
2154
 
1296
2155
  // src/extract/aliases.ts
2156
+ init_cjs_shims();
1297
2157
  var import_node_path8 = __toESM(require("path"), 1);
1298
2158
  var import_node_fs8 = require("fs");
1299
2159
  var import_yaml2 = require("yaml");
@@ -1458,10 +2318,12 @@ async function addServiceAliases(graph, scanPath, services) {
1458
2318
  }
1459
2319
 
1460
2320
  // src/extract/databases/index.ts
2321
+ init_cjs_shims();
1461
2322
  var import_node_path16 = __toESM(require("path"), 1);
1462
2323
  var import_types7 = require("@neat.is/types");
1463
2324
 
1464
2325
  // src/extract/databases/db-config-yaml.ts
2326
+ init_cjs_shims();
1465
2327
  var import_node_path9 = __toESM(require("path"), 1);
1466
2328
  async function parse(serviceDir) {
1467
2329
  const yamlPath = import_node_path9.default.join(serviceDir, "db-config.yaml");
@@ -1481,10 +2343,12 @@ async function parse(serviceDir) {
1481
2343
  var dbConfigYamlParser = { name: "db-config.yaml", parse };
1482
2344
 
1483
2345
  // src/extract/databases/dotenv.ts
2346
+ init_cjs_shims();
1484
2347
  var import_node_fs10 = require("fs");
1485
2348
  var import_node_path11 = __toESM(require("path"), 1);
1486
2349
 
1487
2350
  // src/extract/databases/shared.ts
2351
+ init_cjs_shims();
1488
2352
  var import_node_fs9 = require("fs");
1489
2353
  var import_node_path10 = __toESM(require("path"), 1);
1490
2354
  function schemeToEngine(scheme) {
@@ -1609,6 +2473,7 @@ async function parse2(serviceDir) {
1609
2473
  var dotenvParser = { name: ".env", parse: parse2 };
1610
2474
 
1611
2475
  // src/extract/databases/prisma.ts
2476
+ init_cjs_shims();
1612
2477
  var import_node_path12 = __toESM(require("path"), 1);
1613
2478
  async function parse3(serviceDir) {
1614
2479
  const schemaPath = import_node_path12.default.join(serviceDir, "prisma", "schema.prisma");
@@ -1639,6 +2504,7 @@ async function parse3(serviceDir) {
1639
2504
  var prismaParser = { name: "prisma", parse: parse3 };
1640
2505
 
1641
2506
  // src/extract/databases/drizzle.ts
2507
+ init_cjs_shims();
1642
2508
  var DIALECT_TO_ENGINE = {
1643
2509
  postgresql: "postgresql",
1644
2510
  postgres: "postgresql",
@@ -1690,6 +2556,7 @@ async function parse4(serviceDir) {
1690
2556
  var drizzleParser = { name: "drizzle", parse: parse4 };
1691
2557
 
1692
2558
  // src/extract/databases/knex.ts
2559
+ init_cjs_shims();
1693
2560
  var CLIENT_TO_ENGINE = {
1694
2561
  pg: "postgresql",
1695
2562
  postgres: "postgresql",
@@ -1740,6 +2607,7 @@ async function parse5(serviceDir) {
1740
2607
  var knexParser = { name: "knex", parse: parse5 };
1741
2608
 
1742
2609
  // src/extract/databases/ormconfig.ts
2610
+ init_cjs_shims();
1743
2611
  var import_node_path13 = __toESM(require("path"), 1);
1744
2612
  async function parse6(serviceDir) {
1745
2613
  for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
@@ -1768,6 +2636,7 @@ async function parse6(serviceDir) {
1768
2636
  var ormconfigParser = { name: "ormconfig", parse: parse6 };
1769
2637
 
1770
2638
  // src/extract/databases/typeorm.ts
2639
+ init_cjs_shims();
1771
2640
  async function parse7(serviceDir) {
1772
2641
  const filePath = await findFirst(serviceDir, [
1773
2642
  "data-source.ts",
@@ -1801,6 +2670,7 @@ async function parse7(serviceDir) {
1801
2670
  var typeormParser = { name: "typeorm", parse: parse7 };
1802
2671
 
1803
2672
  // src/extract/databases/sequelize.ts
2673
+ init_cjs_shims();
1804
2674
  var import_node_path14 = __toESM(require("path"), 1);
1805
2675
  async function parse8(serviceDir) {
1806
2676
  const configPath = import_node_path14.default.join(serviceDir, "config", "config.json");
@@ -1829,6 +2699,7 @@ async function parse8(serviceDir) {
1829
2699
  var sequelizeParser = { name: "sequelize", parse: parse8 };
1830
2700
 
1831
2701
  // src/extract/databases/docker-compose.ts
2702
+ init_cjs_shims();
1832
2703
  var import_node_path15 = __toESM(require("path"), 1);
1833
2704
  function portFromService(svc) {
1834
2705
  for (const raw of svc.ports ?? []) {
@@ -2063,6 +2934,7 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
2063
2934
  }
2064
2935
 
2065
2936
  // src/extract/configs.ts
2937
+ init_cjs_shims();
2066
2938
  var import_node_fs11 = require("fs");
2067
2939
  var import_node_path17 = __toESM(require("path"), 1);
2068
2940
  var import_types8 = require("@neat.is/types");
@@ -2118,9 +2990,11 @@ async function addConfigNodes(graph, services, scanPath) {
2118
2990
  }
2119
2991
 
2120
2992
  // src/extract/calls/index.ts
2993
+ init_cjs_shims();
2121
2994
  var import_types14 = require("@neat.is/types");
2122
2995
 
2123
2996
  // src/extract/calls/http.ts
2997
+ init_cjs_shims();
2124
2998
  var import_node_path19 = __toESM(require("path"), 1);
2125
2999
  var import_tree_sitter = __toESM(require("tree-sitter"), 1);
2126
3000
  var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
@@ -2128,6 +3002,7 @@ var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
2128
3002
  var import_types9 = require("@neat.is/types");
2129
3003
 
2130
3004
  // src/extract/calls/shared.ts
3005
+ init_cjs_shims();
2131
3006
  var import_node_fs12 = require("fs");
2132
3007
  var import_node_path18 = __toESM(require("path"), 1);
2133
3008
  async function walkSourceFiles(dir) {
@@ -2260,6 +3135,7 @@ async function addHttpCallEdges(graph, services) {
2260
3135
  }
2261
3136
 
2262
3137
  // src/extract/calls/kafka.ts
3138
+ init_cjs_shims();
2263
3139
  var import_node_path20 = __toESM(require("path"), 1);
2264
3140
  var import_types10 = require("@neat.is/types");
2265
3141
  var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -2299,6 +3175,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
2299
3175
  }
2300
3176
 
2301
3177
  // src/extract/calls/redis.ts
3178
+ init_cjs_shims();
2302
3179
  var import_node_path21 = __toESM(require("path"), 1);
2303
3180
  var import_types11 = require("@neat.is/types");
2304
3181
  var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
@@ -2328,6 +3205,7 @@ function redisEndpointsFromFile(file, serviceDir) {
2328
3205
  }
2329
3206
 
2330
3207
  // src/extract/calls/aws.ts
3208
+ init_cjs_shims();
2331
3209
  var import_node_path22 = __toESM(require("path"), 1);
2332
3210
  var import_types12 = require("@neat.is/types");
2333
3211
  var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
@@ -2382,6 +3260,7 @@ function awsEndpointsFromFile(file, serviceDir) {
2382
3260
  }
2383
3261
 
2384
3262
  // src/extract/calls/grpc.ts
3263
+ init_cjs_shims();
2385
3264
  var import_node_path23 = __toESM(require("path"), 1);
2386
3265
  var import_types13 = require("@neat.is/types");
2387
3266
  var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
@@ -2482,11 +3361,16 @@ async function addCallEdges(graph, services) {
2482
3361
  };
2483
3362
  }
2484
3363
 
3364
+ // src/extract/infra/index.ts
3365
+ init_cjs_shims();
3366
+
2485
3367
  // src/extract/infra/docker-compose.ts
3368
+ init_cjs_shims();
2486
3369
  var import_node_path24 = __toESM(require("path"), 1);
2487
3370
  var import_types16 = require("@neat.is/types");
2488
3371
 
2489
3372
  // src/extract/infra/shared.ts
3373
+ init_cjs_shims();
2490
3374
  var import_types15 = require("@neat.is/types");
2491
3375
  function makeInfraNode(kind, name, provider = "self", extras) {
2492
3376
  return {
@@ -2586,6 +3470,7 @@ async function addComposeInfra(graph, scanPath, services) {
2586
3470
  }
2587
3471
 
2588
3472
  // src/extract/infra/dockerfile.ts
3473
+ init_cjs_shims();
2589
3474
  var import_node_path25 = __toESM(require("path"), 1);
2590
3475
  var import_node_fs13 = require("fs");
2591
3476
  var import_types17 = require("@neat.is/types");
@@ -2645,6 +3530,7 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
2645
3530
  }
2646
3531
 
2647
3532
  // src/extract/infra/terraform.ts
3533
+ init_cjs_shims();
2648
3534
  var import_node_fs14 = require("fs");
2649
3535
  var import_node_path26 = __toESM(require("path"), 1);
2650
3536
  var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
@@ -2683,6 +3569,7 @@ async function addTerraformResources(graph, scanPath) {
2683
3569
  }
2684
3570
 
2685
3571
  // src/extract/infra/k8s.ts
3572
+ init_cjs_shims();
2686
3573
  var import_node_fs15 = require("fs");
2687
3574
  var import_node_path27 = __toESM(require("path"), 1);
2688
3575
  var import_yaml3 = require("yaml");
@@ -2778,6 +3665,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
2778
3665
  }
2779
3666
 
2780
3667
  // src/persist.ts
3668
+ init_cjs_shims();
2781
3669
  var import_node_fs16 = require("fs");
2782
3670
  var import_node_path28 = __toESM(require("path"), 1);
2783
3671
  var SCHEMA_VERSION = 2;
@@ -2861,6 +3749,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
2861
3749
  }
2862
3750
 
2863
3751
  // src/projects.ts
3752
+ init_cjs_shims();
2864
3753
  var import_node_path29 = __toESM(require("path"), 1);
2865
3754
  function pathsForProject(project, baseDir) {
2866
3755
  if (project === DEFAULT_PROJECT) {
@@ -2880,12 +3769,363 @@ function pathsForProject(project, baseDir) {
2880
3769
  policyViolationsPath: import_node_path29.default.join(baseDir, `policy-violations.${project}.ndjson`)
2881
3770
  };
2882
3771
  }
3772
+ var Projects = class {
3773
+ contexts = /* @__PURE__ */ new Map();
3774
+ upsert(ctx) {
3775
+ this.contexts.set(ctx.name, ctx);
3776
+ }
3777
+ set(name, init) {
3778
+ const ctx = {
3779
+ name,
3780
+ graph: init.graph ?? getGraph(name),
3781
+ scanPath: init.scanPath,
3782
+ paths: init.paths,
3783
+ searchIndex: init.searchIndex
3784
+ };
3785
+ this.contexts.set(name, ctx);
3786
+ return ctx;
3787
+ }
3788
+ get(name) {
3789
+ return this.contexts.get(name);
3790
+ }
3791
+ has(name) {
3792
+ return this.contexts.has(name);
3793
+ }
3794
+ list() {
3795
+ return [...this.contexts.keys()].sort();
3796
+ }
3797
+ attachSearchIndex(name, index) {
3798
+ const ctx = this.contexts.get(name);
3799
+ if (ctx) ctx.searchIndex = index;
3800
+ }
3801
+ };
2883
3802
 
2884
- // src/registry.ts
3803
+ // src/api.ts
3804
+ init_cjs_shims();
3805
+ var import_fastify = __toESM(require("fastify"), 1);
3806
+ var import_cors = __toESM(require("@fastify/cors"), 1);
3807
+ var import_types20 = require("@neat.is/types");
3808
+
3809
+ // src/divergences.ts
3810
+ init_cjs_shims();
3811
+ var import_types18 = require("@neat.is/types");
3812
+ function bucketKey(source, target, type) {
3813
+ return `${type}|${source}|${target}`;
3814
+ }
3815
+ function bucketEdges(graph) {
3816
+ const buckets = /* @__PURE__ */ new Map();
3817
+ graph.forEachEdge((id, attrs) => {
3818
+ const e = attrs;
3819
+ const parsed = (0, import_types18.parseEdgeId)(id);
3820
+ const provenance = parsed?.provenance ?? e.provenance;
3821
+ const key = bucketKey(e.source, e.target, e.type);
3822
+ const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
3823
+ switch (provenance) {
3824
+ case import_types18.Provenance.EXTRACTED:
3825
+ cur.extracted = e;
3826
+ break;
3827
+ case import_types18.Provenance.OBSERVED:
3828
+ cur.observed = e;
3829
+ break;
3830
+ case import_types18.Provenance.INFERRED:
3831
+ cur.inferred = e;
3832
+ break;
3833
+ case import_types18.Provenance.FRONTIER:
3834
+ cur.frontier = e;
3835
+ break;
3836
+ default:
3837
+ if (e.provenance === import_types18.Provenance.STALE) cur.stale = e;
3838
+ }
3839
+ buckets.set(key, cur);
3840
+ });
3841
+ return buckets;
3842
+ }
3843
+ function nodeIsFrontier(graph, nodeId) {
3844
+ if (!graph.hasNode(nodeId)) return false;
3845
+ const attrs = graph.getNodeAttributes(nodeId);
3846
+ return attrs.type === import_types18.NodeType.FrontierNode;
3847
+ }
3848
+ function hasAnyObservedFromSource(graph, sourceId) {
3849
+ if (!graph.hasNode(sourceId)) return false;
3850
+ for (const edgeId of graph.outboundEdges(sourceId)) {
3851
+ const e = graph.getEdgeAttributes(edgeId);
3852
+ if (e.provenance === import_types18.Provenance.OBSERVED) return true;
3853
+ }
3854
+ return false;
3855
+ }
3856
+ function clampConfidence(n) {
3857
+ if (!Number.isFinite(n)) return 0;
3858
+ return Math.max(0, Math.min(1, n));
3859
+ }
3860
+ function reasonForMissingObserved(source, target, type) {
3861
+ return `Code declares ${source} \u2192 ${target} (${type}) but no production traffic has been observed for this edge.`;
3862
+ }
3863
+ function reasonForMissingExtracted(source, target, type) {
3864
+ return `Production observed ${source} \u2192 ${target} (${type}) but static analysis did not surface this edge.`;
3865
+ }
3866
+ var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
3867
+ var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
3868
+ var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
3869
+ function detectMissingDivergences(graph, bucket) {
3870
+ const out = [];
3871
+ if (bucket.extracted && !bucket.observed) {
3872
+ if (!nodeIsFrontier(graph, bucket.target)) {
3873
+ const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
3874
+ out.push({
3875
+ type: "missing-observed",
3876
+ source: bucket.source,
3877
+ target: bucket.target,
3878
+ edgeType: bucket.type,
3879
+ extracted: bucket.extracted,
3880
+ confidence: sourceHasTraffic ? 1 : 0.5,
3881
+ reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
3882
+ recommendation: RECOMMENDATION_MISSING_OBSERVED
3883
+ });
3884
+ }
3885
+ }
3886
+ if (bucket.observed && !bucket.extracted) {
3887
+ const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
3888
+ out.push({
3889
+ type: "missing-extracted",
3890
+ source: bucket.source,
3891
+ target: bucket.target,
3892
+ edgeType: bucket.type,
3893
+ observed: bucket.observed,
3894
+ confidence: cascaded,
3895
+ reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
3896
+ recommendation: RECOMMENDATION_MISSING_EXTRACTED
3897
+ });
3898
+ }
3899
+ return out;
3900
+ }
3901
+ function declaredHostFor(svc) {
3902
+ const raw = svc.dbConnectionTarget?.trim();
3903
+ if (!raw) return null;
3904
+ const colon = raw.lastIndexOf(":");
3905
+ if (colon === -1) return raw;
3906
+ const port = raw.slice(colon + 1);
3907
+ if (/^\d+$/.test(port)) return raw.slice(0, colon);
3908
+ return raw;
3909
+ }
3910
+ function hasExtractedConfiguredBy(graph, svcId) {
3911
+ for (const edgeId of graph.outboundEdges(svcId)) {
3912
+ const e = graph.getEdgeAttributes(edgeId);
3913
+ if (e.type === import_types18.EdgeType.CONFIGURED_BY && e.provenance === import_types18.Provenance.EXTRACTED) {
3914
+ return true;
3915
+ }
3916
+ }
3917
+ return false;
3918
+ }
3919
+ function detectHostMismatch(graph, svcId, svc) {
3920
+ const declaredHost = declaredHostFor(svc);
3921
+ if (!declaredHost) return [];
3922
+ if (!hasExtractedConfiguredBy(graph, svcId)) return [];
3923
+ const out = [];
3924
+ for (const edgeId of graph.outboundEdges(svcId)) {
3925
+ const edge = graph.getEdgeAttributes(edgeId);
3926
+ if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3927
+ if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
3928
+ const target = graph.getNodeAttributes(edge.target);
3929
+ if (target.type !== import_types18.NodeType.DatabaseNode) continue;
3930
+ const observedHost = target.host?.trim();
3931
+ if (!observedHost) continue;
3932
+ if (observedHost === declaredHost) continue;
3933
+ out.push({
3934
+ type: "host-mismatch",
3935
+ source: svcId,
3936
+ target: edge.target,
3937
+ extractedHost: declaredHost,
3938
+ observedHost,
3939
+ confidence: clampConfidence(confidenceForEdge(edge)),
3940
+ reason: `Config declares ${svcId} connects to ${declaredHost}; production connects to ${observedHost}.`,
3941
+ recommendation: RECOMMENDATION_HOST_MISMATCH
3942
+ });
3943
+ }
3944
+ return out;
3945
+ }
3946
+ function detectCompatDivergences(graph, svcId, svc) {
3947
+ const out = [];
3948
+ const deps = svc.dependencies ?? {};
3949
+ for (const edgeId of graph.outboundEdges(svcId)) {
3950
+ const edge = graph.getEdgeAttributes(edgeId);
3951
+ if (edge.type !== import_types18.EdgeType.CONNECTS_TO) continue;
3952
+ if (edge.provenance !== import_types18.Provenance.OBSERVED) continue;
3953
+ const target = graph.getNodeAttributes(edge.target);
3954
+ if (target.type !== import_types18.NodeType.DatabaseNode) continue;
3955
+ for (const pair of compatPairs()) {
3956
+ if (pair.engine !== target.engine) continue;
3957
+ const declared = deps[pair.driver];
3958
+ if (!declared) continue;
3959
+ const result = checkCompatibility(
3960
+ pair.driver,
3961
+ declared,
3962
+ target.engine,
3963
+ target.engineVersion
3964
+ );
3965
+ if (!result.compatible && result.reason) {
3966
+ out.push({
3967
+ type: "version-mismatch",
3968
+ source: svcId,
3969
+ target: edge.target,
3970
+ extractedVersion: declared,
3971
+ observedVersion: target.engineVersion,
3972
+ compatibility: "incompatible",
3973
+ confidence: 1,
3974
+ reason: result.reason,
3975
+ recommendation: result.minDriverVersion ? `Upgrade ${pair.driver} to >= ${result.minDriverVersion}.` : `Update the ${pair.driver} driver to a version compatible with ${target.engine} ${target.engineVersion}.`
3976
+ });
3977
+ }
3978
+ }
3979
+ for (const rule of deprecatedApis()) {
3980
+ const declared = deps[rule.package];
3981
+ if (!declared) continue;
3982
+ const result = checkDeprecatedApi(rule, declared);
3983
+ if (!result.compatible && result.reason) {
3984
+ const ruleRef = {
3985
+ kind: rule.kind ?? "deprecated-api",
3986
+ reason: result.reason,
3987
+ package: rule.package
3988
+ };
3989
+ out.push({
3990
+ type: "compat-violation",
3991
+ source: svcId,
3992
+ target: edge.target,
3993
+ rule: ruleRef,
3994
+ observed: edge,
3995
+ confidence: 1,
3996
+ reason: result.reason,
3997
+ recommendation: `Replace deprecated ${rule.package}@${declared} with a supported version.`
3998
+ });
3999
+ }
4000
+ }
4001
+ }
4002
+ return out;
4003
+ }
4004
+ function involvesNode(d, nodeId) {
4005
+ return d.source === nodeId || d.target === nodeId;
4006
+ }
4007
+ function computeDivergences(graph, opts = {}) {
4008
+ const all = [];
4009
+ const buckets = bucketEdges(graph);
4010
+ for (const bucket of buckets.values()) {
4011
+ for (const d of detectMissingDivergences(graph, bucket)) all.push(d);
4012
+ }
4013
+ graph.forEachNode((nodeId, attrs) => {
4014
+ const n = attrs;
4015
+ if (n.type !== import_types18.NodeType.ServiceNode) return;
4016
+ const svc = n;
4017
+ for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
4018
+ for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
4019
+ });
4020
+ let filtered = all;
4021
+ if (opts.type) {
4022
+ const allowed = opts.type;
4023
+ filtered = filtered.filter((d) => allowed.has(d.type));
4024
+ }
4025
+ if (opts.minConfidence !== void 0) {
4026
+ const threshold = opts.minConfidence;
4027
+ filtered = filtered.filter((d) => d.confidence >= threshold);
4028
+ }
4029
+ if (opts.node) {
4030
+ const target = opts.node;
4031
+ filtered = filtered.filter((d) => involvesNode(d, target));
4032
+ }
4033
+ filtered.sort((a, b) => {
4034
+ if (b.confidence !== a.confidence) return b.confidence - a.confidence;
4035
+ if (a.type !== b.type) return a.type.localeCompare(b.type);
4036
+ if (a.source !== b.source) return a.source.localeCompare(b.source);
4037
+ return a.target.localeCompare(b.target);
4038
+ });
4039
+ return import_types18.DivergenceResultSchema.parse({
4040
+ divergences: filtered,
4041
+ totalAffected: filtered.length,
4042
+ computedAt: (/* @__PURE__ */ new Date()).toISOString()
4043
+ });
4044
+ }
4045
+
4046
+ // src/diff.ts
4047
+ init_cjs_shims();
2885
4048
  var import_node_fs17 = require("fs");
4049
+ async function loadSnapshotForDiff(target) {
4050
+ if (/^https?:\/\//i.test(target)) {
4051
+ const res = await fetch(target);
4052
+ if (!res.ok) {
4053
+ throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
4054
+ }
4055
+ return await res.json();
4056
+ }
4057
+ const raw = await import_node_fs17.promises.readFile(target, "utf8");
4058
+ return JSON.parse(raw);
4059
+ }
4060
+ function indexEntries(entries) {
4061
+ const m = /* @__PURE__ */ new Map();
4062
+ if (!entries) return m;
4063
+ for (const entry2 of entries) {
4064
+ const id = entry2.attributes?.id ?? entry2.key;
4065
+ if (!id) continue;
4066
+ m.set(id, entry2.attributes);
4067
+ }
4068
+ return m;
4069
+ }
4070
+ function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
4071
+ const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
4072
+ const baseEdges = indexEntries(baseSnapshot.graph?.edges);
4073
+ const liveNodes = /* @__PURE__ */ new Map();
4074
+ liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
4075
+ const liveEdges = /* @__PURE__ */ new Map();
4076
+ liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
4077
+ const result = {
4078
+ base: { exportedAt: baseSnapshot.exportedAt },
4079
+ current: { exportedAt: currentExportedAt },
4080
+ added: { nodes: [], edges: [] },
4081
+ removed: { nodes: [], edges: [] },
4082
+ changed: { nodes: [], edges: [] }
4083
+ };
4084
+ for (const [id, after] of liveNodes) {
4085
+ const before = baseNodes.get(id);
4086
+ if (!before) {
4087
+ result.added.nodes.push(after);
4088
+ } else if (!shallowEqual(before, after)) {
4089
+ result.changed.nodes.push({ id, before, after });
4090
+ }
4091
+ }
4092
+ for (const [id, before] of baseNodes) {
4093
+ if (!liveNodes.has(id)) result.removed.nodes.push(before);
4094
+ }
4095
+ for (const [id, after] of liveEdges) {
4096
+ const before = baseEdges.get(id);
4097
+ if (!before) {
4098
+ result.added.edges.push(after);
4099
+ } else if (!shallowEqual(before, after)) {
4100
+ result.changed.edges.push({ id, before, after });
4101
+ }
4102
+ }
4103
+ for (const [id, before] of baseEdges) {
4104
+ if (!liveEdges.has(id)) result.removed.edges.push(before);
4105
+ }
4106
+ return result;
4107
+ }
4108
+ function shallowEqual(a, b) {
4109
+ return canonicalJson(a) === canonicalJson(b);
4110
+ }
4111
+ function canonicalJson(value) {
4112
+ return JSON.stringify(value, (_key, v) => {
4113
+ if (v && typeof v === "object" && !Array.isArray(v)) {
4114
+ return Object.keys(v).sort().reduce((acc, k) => {
4115
+ acc[k] = v[k];
4116
+ return acc;
4117
+ }, {});
4118
+ }
4119
+ return v;
4120
+ });
4121
+ }
4122
+
4123
+ // src/registry.ts
4124
+ init_cjs_shims();
4125
+ var import_node_fs18 = require("fs");
2886
4126
  var import_node_os2 = __toESM(require("os"), 1);
2887
4127
  var import_node_path30 = __toESM(require("path"), 1);
2888
- var import_types18 = require("@neat.is/types");
4128
+ var import_types19 = require("@neat.is/types");
2889
4129
  var LOCK_TIMEOUT_MS = 5e3;
2890
4130
  var LOCK_RETRY_MS = 50;
2891
4131
  function neatHome() {
@@ -2900,23 +4140,23 @@ function registryLockPath() {
2900
4140
  return import_node_path30.default.join(neatHome(), "projects.json.lock");
2901
4141
  }
2902
4142
  async function writeAtomically(target, contents) {
2903
- await import_node_fs17.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
4143
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(target), { recursive: true });
2904
4144
  const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
2905
- const fd = await import_node_fs17.promises.open(tmp, "w");
4145
+ const fd = await import_node_fs18.promises.open(tmp, "w");
2906
4146
  try {
2907
4147
  await fd.writeFile(contents, "utf8");
2908
4148
  await fd.sync();
2909
4149
  } finally {
2910
4150
  await fd.close();
2911
4151
  }
2912
- await import_node_fs17.promises.rename(tmp, target);
4152
+ await import_node_fs18.promises.rename(tmp, target);
2913
4153
  }
2914
4154
  async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
2915
4155
  const deadline = Date.now() + timeoutMs;
2916
- await import_node_fs17.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
4156
+ await import_node_fs18.promises.mkdir(import_node_path30.default.dirname(lockPath), { recursive: true });
2917
4157
  while (true) {
2918
4158
  try {
2919
- const fd = await import_node_fs17.promises.open(lockPath, "wx");
4159
+ const fd = await import_node_fs18.promises.open(lockPath, "wx");
2920
4160
  await fd.close();
2921
4161
  return;
2922
4162
  } catch (err) {
@@ -2932,7 +4172,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
2932
4172
  }
2933
4173
  }
2934
4174
  async function releaseLock(lockPath) {
2935
- await import_node_fs17.promises.unlink(lockPath).catch(() => {
4175
+ await import_node_fs18.promises.unlink(lockPath).catch(() => {
2936
4176
  });
2937
4177
  }
2938
4178
  async function withLock(fn) {
@@ -2948,7 +4188,7 @@ async function readRegistry() {
2948
4188
  const file = registryPath();
2949
4189
  let raw;
2950
4190
  try {
2951
- raw = await import_node_fs17.promises.readFile(file, "utf8");
4191
+ raw = await import_node_fs18.promises.readFile(file, "utf8");
2952
4192
  } catch (err) {
2953
4193
  if (err.code === "ENOENT") {
2954
4194
  return { version: 1, projects: [] };
@@ -2956,22 +4196,26 @@ async function readRegistry() {
2956
4196
  throw err;
2957
4197
  }
2958
4198
  const parsed = JSON.parse(raw);
2959
- return import_types18.RegistryFileSchema.parse(parsed);
4199
+ return import_types19.RegistryFileSchema.parse(parsed);
2960
4200
  }
2961
4201
  async function writeRegistry(reg) {
2962
- const validated = import_types18.RegistryFileSchema.parse(reg);
4202
+ const validated = import_types19.RegistryFileSchema.parse(reg);
2963
4203
  await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
2964
4204
  }
4205
+ async function getProject(name) {
4206
+ const reg = await readRegistry();
4207
+ return reg.projects.find((p) => p.name === name);
4208
+ }
2965
4209
  async function listProjects() {
2966
4210
  const reg = await readRegistry();
2967
4211
  return reg.projects;
2968
4212
  }
2969
- async function setStatus(name, status) {
4213
+ async function setStatus(name, status2) {
2970
4214
  return withLock(async () => {
2971
4215
  const reg = await readRegistry();
2972
4216
  const entry2 = reg.projects.find((p) => p.name === name);
2973
4217
  if (!entry2) throw new Error(`neat registry: no project named "${name}"`);
2974
- entry2.status = status;
4218
+ entry2.status = status2;
2975
4219
  await writeRegistry(reg);
2976
4220
  return entry2;
2977
4221
  });
@@ -2986,17 +4230,511 @@ async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString
2986
4230
  });
2987
4231
  }
2988
4232
 
4233
+ // src/streaming.ts
4234
+ init_cjs_shims();
4235
+ var SSE_HEARTBEAT_MS = 3e4;
4236
+ var SSE_BACKPRESSURE_CAP = 1e3;
4237
+ function handleSse(req2, reply, opts) {
4238
+ const heartbeatMs = opts.heartbeatMs ?? SSE_HEARTBEAT_MS;
4239
+ const backpressureCap = opts.backpressureCap ?? SSE_BACKPRESSURE_CAP;
4240
+ reply.raw.setHeader("Content-Type", "text/event-stream");
4241
+ reply.raw.setHeader("Cache-Control", "no-cache, no-transform");
4242
+ reply.raw.setHeader("Connection", "keep-alive");
4243
+ reply.raw.setHeader("X-Accel-Buffering", "no");
4244
+ reply.raw.flushHeaders?.();
4245
+ let pending = 0;
4246
+ let dropped = false;
4247
+ const closeConnection = () => {
4248
+ if (dropped) return;
4249
+ dropped = true;
4250
+ eventBus.off(EVENT_BUS_CHANNEL, listener);
4251
+ clearInterval(heartbeat);
4252
+ if (!reply.raw.writableEnded) reply.raw.end();
4253
+ };
4254
+ const writeFrame = (frame) => {
4255
+ if (dropped) return;
4256
+ if (pending >= backpressureCap) {
4257
+ const errFrame = `event: error
4258
+ data: ${JSON.stringify({ reason: "backpressure" })}
4259
+
4260
+ `;
4261
+ reply.raw.write(errFrame);
4262
+ closeConnection();
4263
+ return;
4264
+ }
4265
+ pending++;
4266
+ reply.raw.write(frame, () => {
4267
+ pending = Math.max(0, pending - 1);
4268
+ });
4269
+ };
4270
+ const listener = (envelope) => {
4271
+ if (envelope.project !== opts.project) return;
4272
+ writeFrame(`event: ${envelope.type}
4273
+ data: ${JSON.stringify(envelope.payload)}
4274
+
4275
+ `);
4276
+ };
4277
+ eventBus.on(EVENT_BUS_CHANNEL, listener);
4278
+ const heartbeat = setInterval(() => {
4279
+ if (dropped) return;
4280
+ reply.raw.write(":heartbeat\n\n");
4281
+ }, heartbeatMs);
4282
+ if (typeof heartbeat.unref === "function") heartbeat.unref();
4283
+ req2.raw.on("close", closeConnection);
4284
+ reply.raw.on("close", closeConnection);
4285
+ reply.raw.on("error", closeConnection);
4286
+ }
4287
+
4288
+ // src/api.ts
4289
+ function serializeGraph(graph) {
4290
+ const nodes = [];
4291
+ graph.forEachNode((_id, attrs) => {
4292
+ nodes.push(attrs);
4293
+ });
4294
+ const edges = [];
4295
+ graph.forEachEdge((_id, attrs) => {
4296
+ edges.push(attrs);
4297
+ });
4298
+ return { nodes, edges };
4299
+ }
4300
+ function projectFromReq(req2) {
4301
+ const params = req2.params;
4302
+ return params.project ?? DEFAULT_PROJECT;
4303
+ }
4304
+ function resolveProject(registry, req2, reply) {
4305
+ const name = projectFromReq(req2);
4306
+ const ctx = registry.get(name);
4307
+ if (!ctx) {
4308
+ void reply.code(404).send({ error: "project not found", project: name });
4309
+ return null;
4310
+ }
4311
+ return ctx;
4312
+ }
4313
+ function buildLegacyRegistry(opts) {
4314
+ if (opts.projects) return opts.projects;
4315
+ if (!opts.graph) {
4316
+ throw new Error("buildApi: either `projects` or `graph` must be provided");
4317
+ }
4318
+ const registry = new Projects();
4319
+ const paths = pathsForProject(DEFAULT_PROJECT, "");
4320
+ registry.set(DEFAULT_PROJECT, {
4321
+ graph: opts.graph,
4322
+ scanPath: opts.scanPath,
4323
+ paths: {
4324
+ snapshotPath: paths.snapshotPath,
4325
+ errorsPath: opts.errorsPath ?? paths.errorsPath,
4326
+ staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
4327
+ embeddingsCachePath: paths.embeddingsCachePath,
4328
+ policyViolationsPath: paths.policyViolationsPath
4329
+ },
4330
+ searchIndex: opts.searchIndex
4331
+ });
4332
+ return registry;
4333
+ }
4334
+ function registerRoutes(scope, ctx) {
4335
+ const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
4336
+ scope.get("/events", (req2, reply) => {
4337
+ const proj = resolveProject(registry, req2, reply);
4338
+ if (!proj) return;
4339
+ handleSse(req2, reply, { project: proj.name });
4340
+ });
4341
+ scope.get("/health", async (req2, reply) => {
4342
+ const proj = resolveProject(registry, req2, reply);
4343
+ if (!proj) return;
4344
+ const uptimeMs = Date.now() - startedAt;
4345
+ return {
4346
+ ok: true,
4347
+ project: proj.name,
4348
+ uptimeMs,
4349
+ // Legacy fields kept additively. The web shell's StatusBar reads
4350
+ // nodeCount / edgeCount; ADR-061's HealthResponseSchema validates
4351
+ // the canonical triple and lets the extras pass through.
4352
+ uptime: Math.floor(uptimeMs / 1e3),
4353
+ nodeCount: proj.graph.order,
4354
+ edgeCount: proj.graph.size,
4355
+ lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
4356
+ };
4357
+ });
4358
+ scope.get("/graph", async (req2, reply) => {
4359
+ const proj = resolveProject(registry, req2, reply);
4360
+ if (!proj) return;
4361
+ return serializeGraph(proj.graph);
4362
+ });
4363
+ scope.get(
4364
+ "/graph/node/:id",
4365
+ async (req2, reply) => {
4366
+ const proj = resolveProject(registry, req2, reply);
4367
+ if (!proj) return;
4368
+ const { id } = req2.params;
4369
+ if (!proj.graph.hasNode(id)) {
4370
+ return reply.code(404).send({ error: "node not found", id });
4371
+ }
4372
+ return { node: proj.graph.getNodeAttributes(id) };
4373
+ }
4374
+ );
4375
+ scope.get(
4376
+ "/graph/edges/:id",
4377
+ async (req2, reply) => {
4378
+ const proj = resolveProject(registry, req2, reply);
4379
+ if (!proj) return;
4380
+ const { id } = req2.params;
4381
+ if (!proj.graph.hasNode(id)) {
4382
+ return reply.code(404).send({ error: "node not found", id });
4383
+ }
4384
+ const inbound = proj.graph.inboundEdges(id).map((e) => proj.graph.getEdgeAttributes(e));
4385
+ const outbound = proj.graph.outboundEdges(id).map((e) => proj.graph.getEdgeAttributes(e));
4386
+ return { inbound, outbound };
4387
+ }
4388
+ );
4389
+ scope.get("/graph/dependencies/:nodeId", async (req2, reply) => {
4390
+ const proj = resolveProject(registry, req2, reply);
4391
+ if (!proj) return;
4392
+ const { nodeId } = req2.params;
4393
+ if (!proj.graph.hasNode(nodeId)) {
4394
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4395
+ }
4396
+ const depth = req2.query.depth ? Number(req2.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
4397
+ if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
4398
+ return reply.code(400).send({
4399
+ error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
4400
+ });
4401
+ }
4402
+ return getTransitiveDependencies(proj.graph, nodeId, depth);
4403
+ });
4404
+ scope.get("/graph/divergences", async (req2, reply) => {
4405
+ const proj = resolveProject(registry, req2, reply);
4406
+ if (!proj) return;
4407
+ let typeFilter;
4408
+ if (req2.query.type) {
4409
+ const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
4410
+ const parsed = [];
4411
+ for (const c of candidates) {
4412
+ const r = import_types20.DivergenceTypeSchema.safeParse(c);
4413
+ if (!r.success) {
4414
+ return reply.code(400).send({
4415
+ error: `unknown divergence type "${c}"`,
4416
+ allowed: import_types20.DivergenceTypeSchema.options
4417
+ });
4418
+ }
4419
+ parsed.push(r.data);
4420
+ }
4421
+ typeFilter = new Set(parsed);
4422
+ }
4423
+ let minConfidence;
4424
+ if (req2.query.minConfidence !== void 0) {
4425
+ const n = Number(req2.query.minConfidence);
4426
+ if (!Number.isFinite(n) || n < 0 || n > 1) {
4427
+ return reply.code(400).send({
4428
+ error: "minConfidence must be a number in [0, 1]"
4429
+ });
4430
+ }
4431
+ minConfidence = n;
4432
+ }
4433
+ return computeDivergences(proj.graph, {
4434
+ ...typeFilter ? { type: typeFilter } : {},
4435
+ ...minConfidence !== void 0 ? { minConfidence } : {},
4436
+ ...req2.query.node ? { node: req2.query.node } : {}
4437
+ });
4438
+ });
4439
+ scope.get("/incidents", async (req2, reply) => {
4440
+ const proj = resolveProject(registry, req2, reply);
4441
+ if (!proj) return;
4442
+ const epath = errorsPathFor(proj);
4443
+ if (!epath) return { count: 0, total: 0, events: [] };
4444
+ const events = await readErrorEvents(epath);
4445
+ const total = events.length;
4446
+ const limit = req2.query.limit ? Number(req2.query.limit) : 50;
4447
+ const safeLimit = Number.isFinite(limit) && limit > 0 ? Math.min(limit, 200) : 50;
4448
+ const sliced = events.slice(0, safeLimit);
4449
+ return { count: sliced.length, total, events: sliced };
4450
+ });
4451
+ scope.get("/stale-events", async (req2, reply) => {
4452
+ const proj = resolveProject(registry, req2, reply);
4453
+ if (!proj) return;
4454
+ const spath = staleEventsPathFor(proj);
4455
+ if (!spath) return { count: 0, total: 0, events: [] };
4456
+ const events = await readStaleEvents(spath);
4457
+ const filtered = req2.query.edgeType ? events.filter((e) => e.edgeType === req2.query.edgeType) : events;
4458
+ const ordered = [...filtered].reverse();
4459
+ const total = ordered.length;
4460
+ const limit = req2.query.limit ? Number(req2.query.limit) : 50;
4461
+ const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
4462
+ return { count: sliced.length, total, events: sliced };
4463
+ });
4464
+ scope.get(
4465
+ "/incidents/:nodeId",
4466
+ async (req2, reply) => {
4467
+ const proj = resolveProject(registry, req2, reply);
4468
+ if (!proj) return;
4469
+ const { nodeId } = req2.params;
4470
+ if (!proj.graph.hasNode(nodeId)) {
4471
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4472
+ }
4473
+ const epath = errorsPathFor(proj);
4474
+ if (!epath) return { count: 0, total: 0, events: [] };
4475
+ const events = await readErrorEvents(epath);
4476
+ const filtered = events.filter(
4477
+ (e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
4478
+ );
4479
+ return { count: filtered.length, total: filtered.length, events: filtered };
4480
+ }
4481
+ );
4482
+ scope.get("/graph/root-cause/:nodeId", async (req2, reply) => {
4483
+ const proj = resolveProject(registry, req2, reply);
4484
+ if (!proj) return;
4485
+ const { nodeId } = req2.params;
4486
+ if (!proj.graph.hasNode(nodeId)) {
4487
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4488
+ }
4489
+ let errorEvent;
4490
+ const epath = errorsPathFor(proj);
4491
+ if (req2.query.errorId && epath) {
4492
+ const events = await readErrorEvents(epath);
4493
+ errorEvent = events.find((e) => e.id === req2.query.errorId);
4494
+ if (!errorEvent) {
4495
+ return reply.code(404).send({ error: "error event not found", id: req2.query.errorId });
4496
+ }
4497
+ }
4498
+ const result = getRootCause(proj.graph, nodeId, errorEvent);
4499
+ if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
4500
+ return result;
4501
+ });
4502
+ scope.get("/graph/blast-radius/:nodeId", async (req2, reply) => {
4503
+ const proj = resolveProject(registry, req2, reply);
4504
+ if (!proj) return;
4505
+ const { nodeId } = req2.params;
4506
+ if (!proj.graph.hasNode(nodeId)) {
4507
+ return reply.code(404).send({ error: "node not found", id: nodeId });
4508
+ }
4509
+ const depth = req2.query.depth ? Number(req2.query.depth) : void 0;
4510
+ if (depth !== void 0 && (!Number.isFinite(depth) || depth < 0)) {
4511
+ return reply.code(400).send({ error: "depth must be a non-negative number" });
4512
+ }
4513
+ return getBlastRadius(proj.graph, nodeId, depth);
4514
+ });
4515
+ scope.get("/search", async (req2, reply) => {
4516
+ const proj = resolveProject(registry, req2, reply);
4517
+ if (!proj) return;
4518
+ const raw = (req2.query.q ?? "").trim();
4519
+ if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
4520
+ const limit = req2.query.limit ? Number(req2.query.limit) : void 0;
4521
+ const safeLimit = limit !== void 0 && Number.isFinite(limit) && limit > 0 ? limit : void 0;
4522
+ if (proj.searchIndex) {
4523
+ const result = await proj.searchIndex.search(raw, safeLimit);
4524
+ return {
4525
+ query: result.query,
4526
+ provider: result.provider,
4527
+ matches: result.matches.map((m) => ({ ...m.node, score: m.score }))
4528
+ };
4529
+ }
4530
+ const q = raw.toLowerCase();
4531
+ const matches = [];
4532
+ proj.graph.forEachNode((id, attrs) => {
4533
+ const name = attrs.name ?? "";
4534
+ if (id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {
4535
+ matches.push({ ...attrs, score: 1 });
4536
+ }
4537
+ });
4538
+ return {
4539
+ query: q,
4540
+ provider: "substring",
4541
+ matches: matches.slice(0, safeLimit)
4542
+ };
4543
+ });
4544
+ scope.get(
4545
+ "/graph/diff",
4546
+ async (req2, reply) => {
4547
+ const proj = resolveProject(registry, req2, reply);
4548
+ if (!proj) return;
4549
+ const against = req2.query.against;
4550
+ if (!against) {
4551
+ return reply.code(400).send({ error: "query parameter `against` is required" });
4552
+ }
4553
+ try {
4554
+ const snapshot = await loadSnapshotForDiff(against);
4555
+ return computeGraphDiff(proj.graph, snapshot);
4556
+ } catch (err) {
4557
+ return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
4558
+ }
4559
+ }
4560
+ );
4561
+ scope.post("/graph/scan", async (req2, reply) => {
4562
+ const proj = resolveProject(registry, req2, reply);
4563
+ if (!proj) return;
4564
+ if (!proj.scanPath) {
4565
+ return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
4566
+ }
4567
+ const result = await extractFromDirectory(proj.graph, proj.scanPath);
4568
+ return {
4569
+ project: proj.name,
4570
+ scanned: proj.scanPath,
4571
+ nodesAdded: result.nodesAdded,
4572
+ edgesAdded: result.edgesAdded,
4573
+ nodeCount: proj.graph.order,
4574
+ edgeCount: proj.graph.size
4575
+ };
4576
+ });
4577
+ scope.get("/policies", async (req2, reply) => {
4578
+ const proj = resolveProject(registry, req2, reply);
4579
+ if (!proj) return;
4580
+ const policyPath = ctx.policyFilePathFor(proj);
4581
+ if (!policyPath) {
4582
+ return { version: 1, policies: [] };
4583
+ }
4584
+ try {
4585
+ const policies = await loadPolicyFile(policyPath);
4586
+ return { version: 1, policies };
4587
+ } catch (err) {
4588
+ return reply.code(400).send({
4589
+ error: "policy.json failed to parse",
4590
+ details: err.message
4591
+ });
4592
+ }
4593
+ });
4594
+ scope.get("/policies/violations", async (req2, reply) => {
4595
+ const proj = resolveProject(registry, req2, reply);
4596
+ if (!proj) return;
4597
+ const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
4598
+ let violations = await log.readAll();
4599
+ if (req2.query.severity) {
4600
+ const sev = import_types20.PolicySeveritySchema.safeParse(req2.query.severity);
4601
+ if (!sev.success) {
4602
+ return reply.code(400).send({
4603
+ error: "invalid severity",
4604
+ details: sev.error.format()
4605
+ });
4606
+ }
4607
+ violations = violations.filter((v) => v.severity === sev.data);
4608
+ }
4609
+ if (req2.query.policyId) {
4610
+ violations = violations.filter((v) => v.policyId === req2.query.policyId);
4611
+ }
4612
+ return { violations };
4613
+ });
4614
+ scope.post("/policies/check", async (req2, reply) => {
4615
+ const proj = resolveProject(registry, req2, reply);
4616
+ if (!proj) return;
4617
+ const parsed = import_types20.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
4618
+ if (!parsed.success) {
4619
+ return reply.code(400).send({
4620
+ error: "invalid /policies/check body",
4621
+ details: parsed.error.format()
4622
+ });
4623
+ }
4624
+ const policyPath = ctx.policyFilePathFor(proj);
4625
+ let policies = [];
4626
+ if (policyPath) {
4627
+ try {
4628
+ policies = await loadPolicyFile(policyPath);
4629
+ } catch (err) {
4630
+ return reply.code(400).send({
4631
+ error: "policy.json failed to parse",
4632
+ details: err.message
4633
+ });
4634
+ }
4635
+ }
4636
+ const evalCtx = { now: () => Date.now() };
4637
+ if (!parsed.data.hypotheticalAction) {
4638
+ const violations2 = evaluateAllPolicies(proj.graph, policies, evalCtx);
4639
+ const blocking2 = violations2.filter((v) => v.onViolation === "block");
4640
+ return { allowed: blocking2.length === 0, violations: violations2 };
4641
+ }
4642
+ const violations = evaluateAllPolicies(proj.graph, policies, evalCtx);
4643
+ const blocking = violations.filter((v) => v.onViolation === "block");
4644
+ return {
4645
+ allowed: blocking.length === 0,
4646
+ hypotheticalAction: parsed.data.hypotheticalAction,
4647
+ violations
4648
+ };
4649
+ });
4650
+ }
4651
+ async function buildApi(opts) {
4652
+ const app = (0, import_fastify.default)({ logger: false });
4653
+ await app.register(import_cors.default, { origin: true });
4654
+ const startedAt = opts.startedAt ?? Date.now();
4655
+ const registry = buildLegacyRegistry(opts);
4656
+ const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
4657
+ const legacyStaleExplicit = !opts.projects && opts.staleEventsPath !== void 0;
4658
+ const errorsPathFor = (proj) => {
4659
+ if (proj.name === DEFAULT_PROJECT && !opts.projects) {
4660
+ return legacyErrorsExplicit ? opts.errorsPath : void 0;
4661
+ }
4662
+ return proj.paths.errorsPath;
4663
+ };
4664
+ const staleEventsPathFor = (proj) => {
4665
+ if (proj.name === DEFAULT_PROJECT && !opts.projects) {
4666
+ return legacyStaleExplicit ? opts.staleEventsPath : void 0;
4667
+ }
4668
+ return proj.paths.staleEventsPath;
4669
+ };
4670
+ const policyFilePathFor = (proj) => {
4671
+ if (!proj.scanPath) return void 0;
4672
+ return `${proj.scanPath}/policy.json`;
4673
+ };
4674
+ const routeCtx = {
4675
+ registry,
4676
+ startedAt,
4677
+ errorsPathFor,
4678
+ staleEventsPathFor,
4679
+ policyFilePathFor
4680
+ };
4681
+ app.get("/projects", async (_req, reply) => {
4682
+ try {
4683
+ return await listProjects();
4684
+ } catch (err) {
4685
+ return reply.code(500).send({
4686
+ error: "failed to read project registry",
4687
+ details: err.message
4688
+ });
4689
+ }
4690
+ });
4691
+ app.get("/projects/:project", async (req2, reply) => {
4692
+ try {
4693
+ const entry2 = await getProject(req2.params.project);
4694
+ if (!entry2) {
4695
+ return reply.code(404).send({ error: "project not found", project: req2.params.project });
4696
+ }
4697
+ return { project: entry2 };
4698
+ } catch (err) {
4699
+ return reply.code(500).send({
4700
+ error: "failed to read project registry",
4701
+ details: err.message
4702
+ });
4703
+ }
4704
+ });
4705
+ registerRoutes(app, routeCtx);
4706
+ await app.register(
4707
+ async (scope) => {
4708
+ registerRoutes(scope, routeCtx);
4709
+ },
4710
+ { prefix: "/projects/:project" }
4711
+ );
4712
+ return app;
4713
+ }
4714
+
2989
4715
  // src/daemon.ts
4716
+ init_otel();
2990
4717
  function neatHomeFor(opts) {
2991
- if (opts.neatHome && opts.neatHome.length > 0) return import_node_path31.default.resolve(opts.neatHome);
4718
+ if (opts.neatHome && opts.neatHome.length > 0) return import_node_path33.default.resolve(opts.neatHome);
2992
4719
  const env = process.env.NEAT_HOME;
2993
- if (env && env.length > 0) return import_node_path31.default.resolve(env);
4720
+ if (env && env.length > 0) return import_node_path33.default.resolve(env);
2994
4721
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
2995
- return import_node_path31.default.join(home, ".neat");
4722
+ return import_node_path33.default.join(home, ".neat");
4723
+ }
4724
+ function routeSpanToProject(serviceName, projects) {
4725
+ if (!serviceName) return DEFAULT_PROJECT;
4726
+ for (const entry2 of projects) {
4727
+ if (entry2.status !== "active") continue;
4728
+ if (entry2.languages.length === 0) {
4729
+ }
4730
+ if (entry2.name === serviceName) return entry2.name;
4731
+ }
4732
+ return DEFAULT_PROJECT;
2996
4733
  }
2997
4734
  async function bootstrapProject(entry2) {
4735
+ const paths = pathsForProject(entry2.name, import_node_path33.default.join(entry2.path, "neat-out"));
2998
4736
  try {
2999
- const stat = await import_node_fs18.promises.stat(entry2.path);
4737
+ const stat = await import_node_fs19.promises.stat(entry2.path);
3000
4738
  if (!stat.isDirectory()) {
3001
4739
  throw new Error(`registered path ${entry2.path} is not a directory`);
3002
4740
  }
@@ -3009,6 +4747,7 @@ async function bootstrapProject(entry2) {
3009
4747
  // output; nothing routes to it because it's not 'active'.
3010
4748
  graph: getGraph(`__broken__:${entry2.name}`),
3011
4749
  outPath: "",
4750
+ paths,
3012
4751
  stopPersist: () => {
3013
4752
  },
3014
4753
  status: "broken",
@@ -3017,10 +4756,7 @@ async function bootstrapProject(entry2) {
3017
4756
  }
3018
4757
  resetGraph(entry2.name);
3019
4758
  const graph = getGraph(entry2.name);
3020
- const outPath = pathsForProject(
3021
- entry2.name,
3022
- import_node_path31.default.join(entry2.path, "neat-out")
3023
- ).snapshotPath;
4759
+ const outPath = paths.snapshotPath;
3024
4760
  await loadGraphFromDisk(graph, outPath);
3025
4761
  await extractFromDirectory(graph, entry2.path);
3026
4762
  const stopPersist = startPersistLoop(graph, outPath);
@@ -3030,24 +4766,58 @@ async function bootstrapProject(entry2) {
3030
4766
  entry: entry2,
3031
4767
  graph,
3032
4768
  outPath,
4769
+ paths,
3033
4770
  stopPersist,
3034
4771
  status: "active"
3035
4772
  };
3036
4773
  }
4774
+ function resolveRestPort(opts) {
4775
+ if (typeof opts.restPort === "number") return opts.restPort;
4776
+ const env = process.env.PORT;
4777
+ if (env && env.length > 0) {
4778
+ const n = Number.parseInt(env, 10);
4779
+ if (Number.isFinite(n)) return n;
4780
+ }
4781
+ return 8080;
4782
+ }
4783
+ function resolveOtlpPort(opts) {
4784
+ if (typeof opts.otlpPort === "number") return opts.otlpPort;
4785
+ const env = process.env.OTEL_PORT;
4786
+ if (env && env.length > 0) {
4787
+ const n = Number.parseInt(env, 10);
4788
+ if (Number.isFinite(n)) return n;
4789
+ }
4790
+ return 4318;
4791
+ }
4792
+ function resolveHost(opts) {
4793
+ if (opts.host && opts.host.length > 0) return opts.host;
4794
+ const env = process.env.HOST;
4795
+ if (env && env.length > 0) return env;
4796
+ return "0.0.0.0";
4797
+ }
3037
4798
  async function startDaemon(opts = {}) {
3038
4799
  const home = neatHomeFor(opts);
3039
4800
  const regPath = registryPath();
3040
4801
  try {
3041
- await import_node_fs18.promises.access(regPath);
4802
+ await import_node_fs19.promises.access(regPath);
3042
4803
  } catch {
3043
4804
  throw new Error(
3044
4805
  `neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
3045
4806
  );
3046
4807
  }
3047
- const pidPath = import_node_path31.default.join(home, "neatd.pid");
4808
+ const pidPath = import_node_path33.default.join(home, "neatd.pid");
3048
4809
  await writeAtomically(pidPath, `${process.pid}
3049
4810
  `);
3050
4811
  const slots = /* @__PURE__ */ new Map();
4812
+ const registry = new Projects();
4813
+ function upsertRegistryFromSlot(slot) {
4814
+ if (slot.status !== "active") return;
4815
+ registry.set(slot.entry.name, {
4816
+ scanPath: slot.entry.path,
4817
+ paths: slot.paths,
4818
+ graph: slot.graph
4819
+ });
4820
+ }
3051
4821
  async function loadAll() {
3052
4822
  const projects = await listProjects();
3053
4823
  const seen = /* @__PURE__ */ new Set();
@@ -3057,6 +4827,7 @@ async function startDaemon(opts = {}) {
3057
4827
  try {
3058
4828
  const slot = await bootstrapProject(entry2);
3059
4829
  slots.set(entry2.name, slot);
4830
+ upsertRegistryFromSlot(slot);
3060
4831
  if (slot.status === "broken") {
3061
4832
  console.warn(`neatd: project "${entry2.name}" broken \u2014 ${slot.errorReason}`);
3062
4833
  } else {
@@ -3081,6 +4852,80 @@ async function startDaemon(opts = {}) {
3081
4852
  }
3082
4853
  }
3083
4854
  await loadAll();
4855
+ const bind = opts.bindListeners !== false;
4856
+ let restApp = null;
4857
+ let otlpApp = null;
4858
+ let restAddress = "";
4859
+ let otlpAddress = "";
4860
+ if (bind) {
4861
+ const host = resolveHost(opts);
4862
+ const restPort = resolveRestPort(opts);
4863
+ const otlpPort = resolveOtlpPort(opts);
4864
+ try {
4865
+ restApp = await buildApi({ projects: registry });
4866
+ restAddress = await restApp.listen({ port: restPort, host });
4867
+ console.log(`neatd: REST listening on ${restAddress}`);
4868
+ } catch (err) {
4869
+ for (const slot of slots.values()) {
4870
+ try {
4871
+ slot.stopPersist();
4872
+ } catch {
4873
+ }
4874
+ }
4875
+ if (restApp) await restApp.close().catch(() => {
4876
+ });
4877
+ await import_node_fs19.promises.unlink(pidPath).catch(() => {
4878
+ });
4879
+ throw new Error(
4880
+ `neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
4881
+ );
4882
+ }
4883
+ try {
4884
+ otlpApp = await buildOtelReceiver({
4885
+ onSpan: async (span) => {
4886
+ const liveEntries = await listProjects().catch(() => []);
4887
+ const target = routeSpanToProject(span.service, liveEntries);
4888
+ const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
4889
+ if (!slot || slot.status !== "active") return;
4890
+ await handleSpan(
4891
+ {
4892
+ graph: slot.graph,
4893
+ errorsPath: slot.paths.errorsPath,
4894
+ project: slot.entry.name,
4895
+ // Receiver already wrote the error event synchronously below.
4896
+ writeErrorEventInline: false
4897
+ },
4898
+ span
4899
+ );
4900
+ },
4901
+ onErrorSpanSync: async (span) => {
4902
+ const liveEntries = await listProjects().catch(() => []);
4903
+ const target = routeSpanToProject(span.service, liveEntries);
4904
+ const slot = slots.get(target) ?? slots.get(DEFAULT_PROJECT);
4905
+ if (!slot || slot.status !== "active") return;
4906
+ await makeErrorSpanWriter(slot.paths.errorsPath)(span);
4907
+ }
4908
+ });
4909
+ otlpAddress = await otlpApp.listen({ port: otlpPort, host });
4910
+ console.log(`neatd: OTLP listening on ${otlpAddress}/v1/traces`);
4911
+ } catch (err) {
4912
+ for (const slot of slots.values()) {
4913
+ try {
4914
+ slot.stopPersist();
4915
+ } catch {
4916
+ }
4917
+ }
4918
+ if (restApp) await restApp.close().catch(() => {
4919
+ });
4920
+ if (otlpApp) await otlpApp.close().catch(() => {
4921
+ });
4922
+ await import_node_fs19.promises.unlink(pidPath).catch(() => {
4923
+ });
4924
+ throw new Error(
4925
+ `neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
4926
+ );
4927
+ }
4928
+ }
3084
4929
  let reloading = null;
3085
4930
  const reload = async () => {
3086
4931
  if (reloading) return reloading;
@@ -3104,22 +4949,27 @@ async function startDaemon(opts = {}) {
3104
4949
  if (stopped) return;
3105
4950
  stopped = true;
3106
4951
  process.off("SIGHUP", sighupHandler);
4952
+ if (otlpApp) await otlpApp.close().catch(() => {
4953
+ });
4954
+ if (restApp) await restApp.close().catch(() => {
4955
+ });
3107
4956
  for (const slot of slots.values()) {
3108
4957
  try {
3109
4958
  slot.stopPersist();
3110
4959
  } catch {
3111
4960
  }
3112
4961
  }
3113
- await import_node_fs18.promises.unlink(pidPath).catch(() => {
4962
+ await import_node_fs19.promises.unlink(pidPath).catch(() => {
3114
4963
  });
3115
4964
  };
3116
- return { slots, reload, stop, pidPath };
4965
+ return { slots, reload, stop, pidPath, restAddress, otlpAddress };
3117
4966
  }
3118
4967
 
3119
4968
  // src/web-spawn.ts
4969
+ init_cjs_shims();
3120
4970
  var import_node_child_process = require("child_process");
3121
4971
  var import_node_net = __toESM(require("net"), 1);
3122
- var import_node_path32 = __toESM(require("path"), 1);
4972
+ var import_node_path34 = __toESM(require("path"), 1);
3123
4973
  var DEFAULT_WEB_PORT = 6328;
3124
4974
  async function assertPortFree(port) {
3125
4975
  await new Promise((resolve, reject) => {
@@ -3147,7 +4997,10 @@ function resolveWebPackageDir() {
3147
4997
  eval("require")
3148
4998
  );
3149
4999
  const pkgJsonPath = req.resolve("@neat.is/web/package.json");
3150
- return import_node_path32.default.dirname(pkgJsonPath);
5000
+ return import_node_path34.default.dirname(pkgJsonPath);
5001
+ }
5002
+ function resolveStandaloneServerEntry(webDir) {
5003
+ return import_node_path34.default.join(webDir, ".next/standalone/packages/web/server.js");
3151
5004
  }
3152
5005
  async function spawnWebUI(restPort) {
3153
5006
  const portRaw = process.env.NEAT_WEB_PORT;
@@ -3157,13 +5010,22 @@ async function spawnWebUI(restPort) {
3157
5010
  }
3158
5011
  await assertPortFree(port);
3159
5012
  const cwd = resolveWebPackageDir();
5013
+ const serverEntry = resolveStandaloneServerEntry(cwd);
5014
+ try {
5015
+ require.resolve(serverEntry);
5016
+ } catch {
5017
+ throw new Error(
5018
+ `neatd: web UI standalone build missing at ${serverEntry}. The published @neat.is/web tarball should include it; if you're running from a monorepo checkout, run \`npm run build --workspace @neat.is/web\` first, or set NEAT_WEB_DISABLED=1 to skip the web UI.`
5019
+ );
5020
+ }
3160
5021
  const env = {
3161
5022
  ...process.env,
3162
5023
  PORT: String(port),
5024
+ HOSTNAME: process.env.HOSTNAME ?? "0.0.0.0",
3163
5025
  NEAT_API_URL: process.env.NEAT_API_URL ?? `http://localhost:${restPort}`
3164
5026
  };
3165
- const child = (0, import_node_child_process.spawn)("npm", ["exec", "--", "next", "start", "-p", String(port)], {
3166
- cwd,
5027
+ const child = (0, import_node_child_process.spawn)(process.execPath, [serverEntry], {
5028
+ cwd: import_node_path34.default.dirname(serverEntry),
3167
5029
  env,
3168
5030
  stdio: ["ignore", "inherit", "inherit"],
3169
5031
  detached: false
@@ -3200,14 +5062,14 @@ async function spawnWebUI(restPort) {
3200
5062
  // src/neatd.ts
3201
5063
  function neatHome2() {
3202
5064
  if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
3203
- return import_node_path33.default.resolve(process.env.NEAT_HOME);
5065
+ return import_node_path35.default.resolve(process.env.NEAT_HOME);
3204
5066
  }
3205
5067
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
3206
- return import_node_path33.default.join(home, ".neat");
5068
+ return import_node_path35.default.join(home, ".neat");
3207
5069
  }
3208
5070
  async function readPid() {
3209
5071
  try {
3210
- const raw = await import_node_fs19.promises.readFile(import_node_path33.default.join(neatHome2(), "neatd.pid"), "utf8");
5072
+ const raw = await import_node_fs20.promises.readFile(import_node_path35.default.join(neatHome2(), "neatd.pid"), "utf8");
3211
5073
  const n = Number.parseInt(raw.trim(), 10);
3212
5074
  return Number.isFinite(n) ? n : null;
3213
5075
  } catch {
@@ -3225,6 +5087,8 @@ async function cmdStart() {
3225
5087
  const handle = await startDaemon();
3226
5088
  console.log(`neatd: started, PID ${process.pid}, ${handle.slots.size} project(s)`);
3227
5089
  console.log(`neatd: registry at ${registryPath()}`);
5090
+ if (handle.restAddress) console.log(`neatd: REST \u2192 ${handle.restAddress}`);
5091
+ if (handle.otlpAddress) console.log(`neatd: OTLP \u2192 ${handle.otlpAddress}`);
3228
5092
  const skipWeb = process.env.NEAT_WEB_DISABLED === "1";
3229
5093
  let web = null;
3230
5094
  if (!skipWeb) {