@neat.is/core 0.2.5
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/compat.json +120 -0
- package/dist/chunk-6JT6L2OV.js +164 -0
- package/dist/chunk-6JT6L2OV.js.map +1 -0
- package/dist/chunk-6SFEITLJ.js +3371 -0
- package/dist/chunk-6SFEITLJ.js.map +1 -0
- package/dist/chunk-I5IMCXRO.js +325 -0
- package/dist/chunk-I5IMCXRO.js.map +1 -0
- package/dist/chunk-T2U4U256.js +462 -0
- package/dist/chunk-T2U4U256.js.map +1 -0
- package/dist/chunk-WX55TLUT.js +184 -0
- package/dist/chunk-WX55TLUT.js.map +1 -0
- package/dist/chunk-XOOCA5T7.js +290 -0
- package/dist/chunk-XOOCA5T7.js.map +1 -0
- package/dist/cli.cjs +5754 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +36 -0
- package/dist/cli.d.ts +36 -0
- package/dist/cli.js +1175 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +4552 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +408 -0
- package/dist/index.d.ts +408 -0
- package/dist/index.js +93 -0
- package/dist/index.js.map +1 -0
- package/dist/neatd.cjs +3070 -0
- package/dist/neatd.cjs.map +1 -0
- package/dist/neatd.d.cts +1 -0
- package/dist/neatd.d.ts +1 -0
- package/dist/neatd.js +114 -0
- package/dist/neatd.js.map +1 -0
- package/dist/otel-grpc-B4XBSI4W.js +9 -0
- package/dist/otel-grpc-B4XBSI4W.js.map +1 -0
- package/dist/server.cjs +4499 -0
- package/dist/server.cjs.map +1 -0
- package/dist/server.d.cts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +97 -0
- package/dist/server.js.map +1 -0
- package/package.json +77 -0
- package/proto/opentelemetry/proto/collector/trace/v1/trace_service.proto +31 -0
- package/proto/opentelemetry/proto/common/v1/common.proto +46 -0
- package/proto/opentelemetry/proto/resource/v1/resource.proto +19 -0
- package/proto/opentelemetry/proto/trace/v1/trace.proto +93 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,4552 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
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(req) {
|
|
78
|
+
return {
|
|
79
|
+
resourceSpans: (req.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_path29.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
104
|
+
return import_node_path29.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_path29, 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_path29 = __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_path30.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
263
|
+
const protoRoot = import_node_path30.default.resolve(here, "..", "proto");
|
|
264
|
+
const root = new import_protobufjs.default.Root();
|
|
265
|
+
root.resolvePath = (_origin, target) => import_node_path30.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 (req, reply) => {
|
|
319
|
+
const ct = (req.headers["content-type"] ?? "").toString().split(";")[0].trim().toLowerCase();
|
|
320
|
+
let body;
|
|
321
|
+
if (ct === "application/x-protobuf") {
|
|
322
|
+
try {
|
|
323
|
+
body = await decodeProtobufBody(req.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 = req.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
|
+
function logSpanHandler(span) {
|
|
358
|
+
const parent = span.parentSpanId ? span.parentSpanId.slice(0, 8) : "<root>";
|
|
359
|
+
const status2 = span.statusCode === 2 ? "ERROR" : "OK";
|
|
360
|
+
const db = span.dbSystem ? ` db=${span.dbSystem}/${span.dbName ?? "?"}` : "";
|
|
361
|
+
console.log(
|
|
362
|
+
`otel: ${span.service} ${span.name} parent=${parent} status=${status2}${db}`
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
var import_node_path30, import_node_url2, import_fastify2, import_protobufjs, exportTraceServiceRequestType;
|
|
366
|
+
var init_otel = __esm({
|
|
367
|
+
"src/otel.ts"() {
|
|
368
|
+
"use strict";
|
|
369
|
+
init_cjs_shims();
|
|
370
|
+
import_node_path30 = __toESM(require("path"), 1);
|
|
371
|
+
import_node_url2 = require("url");
|
|
372
|
+
import_fastify2 = __toESM(require("fastify"), 1);
|
|
373
|
+
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
374
|
+
exportTraceServiceRequestType = null;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
// src/index.ts
|
|
379
|
+
var index_exports = {};
|
|
380
|
+
__export(index_exports, {
|
|
381
|
+
ProjectNameCollisionError: () => ProjectNameCollisionError,
|
|
382
|
+
addProject: () => addProject,
|
|
383
|
+
buildApi: () => buildApi,
|
|
384
|
+
buildOtelReceiver: () => buildOtelReceiver,
|
|
385
|
+
checkCompatibility: () => checkCompatibility,
|
|
386
|
+
compatPairs: () => compatPairs,
|
|
387
|
+
computeGraphDiff: () => computeGraphDiff,
|
|
388
|
+
confidenceForEdge: () => confidenceForEdge,
|
|
389
|
+
extractFromDirectory: () => extractFromDirectory,
|
|
390
|
+
getBlastRadius: () => getBlastRadius,
|
|
391
|
+
getGraph: () => getGraph,
|
|
392
|
+
getProject: () => getProject,
|
|
393
|
+
getRootCause: () => getRootCause,
|
|
394
|
+
handleSpan: () => handleSpan,
|
|
395
|
+
listProjects: () => listProjects,
|
|
396
|
+
loadGraphFromDisk: () => loadGraphFromDisk,
|
|
397
|
+
loadSnapshotForDiff: () => loadSnapshotForDiff,
|
|
398
|
+
logSpanHandler: () => logSpanHandler,
|
|
399
|
+
makeSpanHandler: () => makeSpanHandler,
|
|
400
|
+
markStaleEdges: () => markStaleEdges,
|
|
401
|
+
normalizeProjectPath: () => normalizeProjectPath,
|
|
402
|
+
parseOtlpRequest: () => parseOtlpRequest,
|
|
403
|
+
readErrorEvents: () => readErrorEvents,
|
|
404
|
+
readRegistry: () => readRegistry,
|
|
405
|
+
readStaleEvents: () => readStaleEvents,
|
|
406
|
+
registryLockPath: () => registryLockPath,
|
|
407
|
+
registryPath: () => registryPath,
|
|
408
|
+
removeProject: () => removeProject,
|
|
409
|
+
resetGraph: () => resetGraph,
|
|
410
|
+
routeSpanToProject: () => routeSpanToProject,
|
|
411
|
+
saveGraphToDisk: () => saveGraphToDisk,
|
|
412
|
+
setStatus: () => setStatus,
|
|
413
|
+
startDaemon: () => startDaemon,
|
|
414
|
+
startOtelGrpcReceiver: () => startOtelGrpcReceiver,
|
|
415
|
+
startPersistLoop: () => startPersistLoop,
|
|
416
|
+
startStalenessLoop: () => startStalenessLoop,
|
|
417
|
+
stitchTrace: () => stitchTrace,
|
|
418
|
+
thresholdForEdgeType: () => thresholdForEdgeType,
|
|
419
|
+
touchLastSeen: () => touchLastSeen,
|
|
420
|
+
writeAtomically: () => writeAtomically
|
|
421
|
+
});
|
|
422
|
+
module.exports = __toCommonJS(index_exports);
|
|
423
|
+
init_cjs_shims();
|
|
424
|
+
|
|
425
|
+
// src/graph.ts
|
|
426
|
+
init_cjs_shims();
|
|
427
|
+
var import_graphology = __toESM(require("graphology"), 1);
|
|
428
|
+
var MultiDirectedGraph = import_graphology.default.MultiDirectedGraph;
|
|
429
|
+
var DEFAULT_PROJECT = "default";
|
|
430
|
+
var graphs = /* @__PURE__ */ new Map();
|
|
431
|
+
function makeGraph() {
|
|
432
|
+
return new MultiDirectedGraph({ allowSelfLoops: false });
|
|
433
|
+
}
|
|
434
|
+
function getGraph(project = DEFAULT_PROJECT) {
|
|
435
|
+
let g = graphs.get(project);
|
|
436
|
+
if (!g) {
|
|
437
|
+
g = makeGraph();
|
|
438
|
+
graphs.set(project, g);
|
|
439
|
+
}
|
|
440
|
+
return g;
|
|
441
|
+
}
|
|
442
|
+
function resetGraph(project) {
|
|
443
|
+
if (project === void 0) {
|
|
444
|
+
graphs.clear();
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
graphs.delete(project);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// src/extract.ts
|
|
451
|
+
init_cjs_shims();
|
|
452
|
+
|
|
453
|
+
// src/extract/index.ts
|
|
454
|
+
init_cjs_shims();
|
|
455
|
+
|
|
456
|
+
// src/ingest.ts
|
|
457
|
+
init_cjs_shims();
|
|
458
|
+
var import_node_fs3 = require("fs");
|
|
459
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
460
|
+
|
|
461
|
+
// src/policy.ts
|
|
462
|
+
init_cjs_shims();
|
|
463
|
+
var import_node_fs2 = require("fs");
|
|
464
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
465
|
+
var import_types2 = require("@neat.is/types");
|
|
466
|
+
|
|
467
|
+
// src/compat.ts
|
|
468
|
+
init_cjs_shims();
|
|
469
|
+
var import_node_fs = require("fs");
|
|
470
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
471
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
472
|
+
var import_semver = __toESM(require("semver"), 1);
|
|
473
|
+
|
|
474
|
+
// compat.json
|
|
475
|
+
var compat_default = {
|
|
476
|
+
pairs: [
|
|
477
|
+
{
|
|
478
|
+
kind: "driver-engine",
|
|
479
|
+
driver: "pg",
|
|
480
|
+
engine: "postgresql",
|
|
481
|
+
minDriverVersion: "8.0.0",
|
|
482
|
+
minEngineVersion: "14",
|
|
483
|
+
reason: "PostgreSQL 14+ requires scram-sha-256 auth by default; pg < 8.0.0 only speaks md5."
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
kind: "driver-engine",
|
|
487
|
+
driver: "mysql2",
|
|
488
|
+
engine: "mysql",
|
|
489
|
+
minDriverVersion: "3.0.0",
|
|
490
|
+
minEngineVersion: "8",
|
|
491
|
+
reason: "MySQL 8 defaults to caching_sha2_password; mysql2 < 3.0.0 doesn't negotiate it."
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
kind: "driver-engine",
|
|
495
|
+
driver: "mongoose",
|
|
496
|
+
engine: "mongodb",
|
|
497
|
+
minDriverVersion: "7.0.0",
|
|
498
|
+
minEngineVersion: "7",
|
|
499
|
+
reason: "MongoDB 7 drops legacy wire-protocol opcodes that mongoose < 7.0.0 still emits."
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
kind: "driver-engine",
|
|
503
|
+
driver: "psycopg2",
|
|
504
|
+
engine: "postgresql",
|
|
505
|
+
minDriverVersion: "2.9.0",
|
|
506
|
+
minEngineVersion: "14",
|
|
507
|
+
reason: "PostgreSQL 14+ requires scram-sha-256 auth by default; psycopg2 < 2.9.0 only speaks md5."
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
kind: "driver-engine",
|
|
511
|
+
driver: "pymongo",
|
|
512
|
+
engine: "mongodb",
|
|
513
|
+
minDriverVersion: "4.0.0",
|
|
514
|
+
minEngineVersion: "7",
|
|
515
|
+
reason: "MongoDB 7 drops legacy wire-protocol opcodes that pymongo < 4.0.0 still emits."
|
|
516
|
+
},
|
|
517
|
+
{
|
|
518
|
+
kind: "driver-engine",
|
|
519
|
+
driver: "mysql-connector-python",
|
|
520
|
+
engine: "mysql",
|
|
521
|
+
minDriverVersion: "8.0.0",
|
|
522
|
+
minEngineVersion: "8",
|
|
523
|
+
reason: "MySQL 8 defaults to caching_sha2_password; mysql-connector-python < 8.0.0 doesn't negotiate it."
|
|
524
|
+
}
|
|
525
|
+
],
|
|
526
|
+
nodeEngineConstraints: [
|
|
527
|
+
{
|
|
528
|
+
kind: "node-engine",
|
|
529
|
+
package: "vitest",
|
|
530
|
+
packageMinVersion: "2.0.0",
|
|
531
|
+
minNodeVersion: "18.0.0",
|
|
532
|
+
reason: "vitest >= 2.0 drops Node 16 support; requires Node 18+."
|
|
533
|
+
},
|
|
534
|
+
{
|
|
535
|
+
kind: "node-engine",
|
|
536
|
+
package: "next",
|
|
537
|
+
packageMinVersion: "14.0.0",
|
|
538
|
+
minNodeVersion: "18.17.0",
|
|
539
|
+
reason: "Next 14+ requires Node 18.17+ (uses APIs introduced in that minor)."
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
kind: "node-engine",
|
|
543
|
+
package: "@modelcontextprotocol/sdk",
|
|
544
|
+
packageMinVersion: "1.0.0",
|
|
545
|
+
minNodeVersion: "18.0.0",
|
|
546
|
+
reason: "@modelcontextprotocol/sdk >= 1 requires Node 18+ (web-streams polyfill removed)."
|
|
547
|
+
}
|
|
548
|
+
],
|
|
549
|
+
packageConflicts: [
|
|
550
|
+
{
|
|
551
|
+
kind: "package-conflict",
|
|
552
|
+
package: "@tanstack/react-query",
|
|
553
|
+
packageMinVersion: "5.0.0",
|
|
554
|
+
requires: {
|
|
555
|
+
name: "react",
|
|
556
|
+
minVersion: "18.0.0"
|
|
557
|
+
},
|
|
558
|
+
reason: "@tanstack/react-query 5+ uses useSyncExternalStore \u2014 only available in React 18+."
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
kind: "package-conflict",
|
|
562
|
+
package: "react-router-dom",
|
|
563
|
+
packageMinVersion: "7.0.0",
|
|
564
|
+
requires: {
|
|
565
|
+
name: "react",
|
|
566
|
+
minVersion: "18.0.0"
|
|
567
|
+
},
|
|
568
|
+
reason: "react-router-dom 7+ requires React 18+."
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
kind: "package-conflict",
|
|
572
|
+
package: "next",
|
|
573
|
+
packageMinVersion: "14.0.0",
|
|
574
|
+
requires: {
|
|
575
|
+
name: "react",
|
|
576
|
+
minVersion: "18.2.0"
|
|
577
|
+
},
|
|
578
|
+
reason: "Next.js 14+ requires React 18.2+."
|
|
579
|
+
}
|
|
580
|
+
],
|
|
581
|
+
deprecatedApis: [
|
|
582
|
+
{
|
|
583
|
+
kind: "deprecated-api",
|
|
584
|
+
package: "request",
|
|
585
|
+
packageMaxVersion: "2.88.2",
|
|
586
|
+
reason: "request is deprecated; use undici, node-fetch, or axios instead."
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
kind: "deprecated-api",
|
|
590
|
+
package: "node-uuid",
|
|
591
|
+
reason: "node-uuid is deprecated; use the `uuid` package."
|
|
592
|
+
}
|
|
593
|
+
]
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// src/compat.ts
|
|
597
|
+
var bundledMatrix = compat_default;
|
|
598
|
+
var mergedMatrix = null;
|
|
599
|
+
var remoteLoadAttempted = false;
|
|
600
|
+
var REMOTE_CACHE_DIR = import_node_path.default.join(import_node_os.default.homedir(), ".neat");
|
|
601
|
+
var REMOTE_CACHE_PATH = import_node_path.default.join(REMOTE_CACHE_DIR, "compat-cache.json");
|
|
602
|
+
var REMOTE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
603
|
+
function engineMeetsThreshold(engineVersion, threshold) {
|
|
604
|
+
const e = parseInt(engineVersion, 10);
|
|
605
|
+
const t = parseInt(threshold, 10);
|
|
606
|
+
if (Number.isFinite(e) && Number.isFinite(t)) return e >= t;
|
|
607
|
+
const ec = import_semver.default.coerce(engineVersion);
|
|
608
|
+
const tc = import_semver.default.coerce(threshold);
|
|
609
|
+
if (ec && tc) return import_semver.default.gte(ec, tc);
|
|
610
|
+
return false;
|
|
611
|
+
}
|
|
612
|
+
function checkCompatibility(driver, driverVersion, engine, engineVersion) {
|
|
613
|
+
const matrix = currentMatrix();
|
|
614
|
+
const pair = matrix.pairs.find((p) => p.driver === driver && p.engine === engine);
|
|
615
|
+
if (!pair) return { compatible: true };
|
|
616
|
+
if (pair.minEngineVersion && !engineMeetsThreshold(engineVersion, pair.minEngineVersion)) {
|
|
617
|
+
return { compatible: true };
|
|
618
|
+
}
|
|
619
|
+
const driverCoerced = import_semver.default.coerce(driverVersion);
|
|
620
|
+
if (!driverCoerced) return { compatible: true };
|
|
621
|
+
if (import_semver.default.lt(driverCoerced, pair.minDriverVersion)) {
|
|
622
|
+
return {
|
|
623
|
+
compatible: false,
|
|
624
|
+
reason: pair.reason,
|
|
625
|
+
minDriverVersion: pair.minDriverVersion
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return { compatible: true };
|
|
629
|
+
}
|
|
630
|
+
function rangeAdmitsVersion(serviceNodeRange, requiredNodeVersion) {
|
|
631
|
+
try {
|
|
632
|
+
const required = import_semver.default.coerce(requiredNodeVersion);
|
|
633
|
+
if (!required) return true;
|
|
634
|
+
return import_semver.default.subset(serviceNodeRange, `>=${required.version}`, {
|
|
635
|
+
includePrerelease: false
|
|
636
|
+
});
|
|
637
|
+
} catch {
|
|
638
|
+
return true;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
function checkNodeEngineConstraint(constraint, declaredPackageVersion, serviceNodeRange) {
|
|
642
|
+
if (constraint.packageMinVersion && declaredPackageVersion) {
|
|
643
|
+
const v = import_semver.default.coerce(declaredPackageVersion);
|
|
644
|
+
if (v && import_semver.default.lt(v, constraint.packageMinVersion)) {
|
|
645
|
+
return { compatible: true };
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
if (!serviceNodeRange) {
|
|
649
|
+
return { compatible: true };
|
|
650
|
+
}
|
|
651
|
+
if (rangeAdmitsVersion(serviceNodeRange, constraint.minNodeVersion)) {
|
|
652
|
+
return { compatible: true };
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
compatible: false,
|
|
656
|
+
reason: constraint.reason,
|
|
657
|
+
requiredNodeVersion: constraint.minNodeVersion
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
function checkPackageConflict(conflict, declaredPackageVersion, declaredRequiredVersion) {
|
|
661
|
+
if (!declaredPackageVersion) return { compatible: true };
|
|
662
|
+
if (conflict.packageMinVersion) {
|
|
663
|
+
const v = import_semver.default.coerce(declaredPackageVersion);
|
|
664
|
+
if (v && import_semver.default.lt(v, conflict.packageMinVersion)) {
|
|
665
|
+
return { compatible: true };
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
if (!declaredRequiredVersion) {
|
|
669
|
+
return {
|
|
670
|
+
compatible: false,
|
|
671
|
+
reason: conflict.reason,
|
|
672
|
+
requires: conflict.requires
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
const requiredCoerced = import_semver.default.coerce(declaredRequiredVersion);
|
|
676
|
+
if (!requiredCoerced) return { compatible: true };
|
|
677
|
+
if (import_semver.default.lt(requiredCoerced, conflict.requires.minVersion)) {
|
|
678
|
+
return {
|
|
679
|
+
compatible: false,
|
|
680
|
+
reason: conflict.reason,
|
|
681
|
+
requires: conflict.requires,
|
|
682
|
+
foundVersion: declaredRequiredVersion
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
return { compatible: true };
|
|
686
|
+
}
|
|
687
|
+
function checkDeprecatedApi(rule, declaredVersion) {
|
|
688
|
+
if (declaredVersion === void 0) return { compatible: true };
|
|
689
|
+
if (rule.packageMaxVersion) {
|
|
690
|
+
const v = import_semver.default.coerce(declaredVersion);
|
|
691
|
+
const max = import_semver.default.coerce(rule.packageMaxVersion);
|
|
692
|
+
if (v && max && import_semver.default.gt(v, max)) return { compatible: true };
|
|
693
|
+
}
|
|
694
|
+
return { compatible: false, reason: rule.reason };
|
|
695
|
+
}
|
|
696
|
+
function currentMatrix() {
|
|
697
|
+
return mergedMatrix ?? bundledMatrix;
|
|
698
|
+
}
|
|
699
|
+
function mergeMatrices(a, b) {
|
|
700
|
+
return {
|
|
701
|
+
pairs: [...a.pairs, ...b.pairs ?? []],
|
|
702
|
+
nodeEngineConstraints: [
|
|
703
|
+
...a.nodeEngineConstraints ?? [],
|
|
704
|
+
...b.nodeEngineConstraints ?? []
|
|
705
|
+
],
|
|
706
|
+
packageConflicts: [...a.packageConflicts ?? [], ...b.packageConflicts ?? []],
|
|
707
|
+
deprecatedApis: [...a.deprecatedApis ?? [], ...b.deprecatedApis ?? []]
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
async function readRemoteCache(url) {
|
|
711
|
+
try {
|
|
712
|
+
const raw = await import_node_fs.promises.readFile(REMOTE_CACHE_PATH, "utf8");
|
|
713
|
+
const parsed = JSON.parse(raw);
|
|
714
|
+
if (parsed.url !== url) return null;
|
|
715
|
+
const age = Date.now() - new Date(parsed.fetchedAt).getTime();
|
|
716
|
+
if (age > REMOTE_TTL_MS) return null;
|
|
717
|
+
return parsed.matrix;
|
|
718
|
+
} catch {
|
|
719
|
+
return null;
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
async function writeRemoteCache(url, matrix) {
|
|
723
|
+
const file = {
|
|
724
|
+
fetchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
725
|
+
url,
|
|
726
|
+
matrix
|
|
727
|
+
};
|
|
728
|
+
try {
|
|
729
|
+
await import_node_fs.promises.mkdir(REMOTE_CACHE_DIR, { recursive: true });
|
|
730
|
+
await import_node_fs.promises.writeFile(REMOTE_CACHE_PATH, JSON.stringify(file), "utf8");
|
|
731
|
+
} catch (err) {
|
|
732
|
+
console.warn(`[neat] failed to cache compat matrix: ${err.message}`);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
async function ensureCompatLoaded() {
|
|
736
|
+
if (mergedMatrix) return mergedMatrix;
|
|
737
|
+
if (remoteLoadAttempted) {
|
|
738
|
+
mergedMatrix = bundledMatrix;
|
|
739
|
+
return mergedMatrix;
|
|
740
|
+
}
|
|
741
|
+
remoteLoadAttempted = true;
|
|
742
|
+
const url = process.env.NEAT_COMPAT_URL;
|
|
743
|
+
if (!url) {
|
|
744
|
+
mergedMatrix = bundledMatrix;
|
|
745
|
+
return mergedMatrix;
|
|
746
|
+
}
|
|
747
|
+
const cached = await readRemoteCache(url);
|
|
748
|
+
if (cached) {
|
|
749
|
+
mergedMatrix = mergeMatrices(bundledMatrix, cached);
|
|
750
|
+
return mergedMatrix;
|
|
751
|
+
}
|
|
752
|
+
try {
|
|
753
|
+
const res = await fetch(url);
|
|
754
|
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
755
|
+
const remote = await res.json();
|
|
756
|
+
await writeRemoteCache(url, remote);
|
|
757
|
+
mergedMatrix = mergeMatrices(bundledMatrix, remote);
|
|
758
|
+
return mergedMatrix;
|
|
759
|
+
} catch (err) {
|
|
760
|
+
console.warn(
|
|
761
|
+
`[neat] NEAT_COMPAT_URL fetch failed (${err.message}); using bundled matrix only`
|
|
762
|
+
);
|
|
763
|
+
mergedMatrix = bundledMatrix;
|
|
764
|
+
return mergedMatrix;
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
function compatPairs() {
|
|
768
|
+
return currentMatrix().pairs;
|
|
769
|
+
}
|
|
770
|
+
function nodeEngineConstraints() {
|
|
771
|
+
return currentMatrix().nodeEngineConstraints ?? [];
|
|
772
|
+
}
|
|
773
|
+
function packageConflicts() {
|
|
774
|
+
return currentMatrix().packageConflicts ?? [];
|
|
775
|
+
}
|
|
776
|
+
function deprecatedApis() {
|
|
777
|
+
return currentMatrix().deprecatedApis ?? [];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// src/traverse.ts
|
|
781
|
+
init_cjs_shims();
|
|
782
|
+
var import_types = require("@neat.is/types");
|
|
783
|
+
var ROOT_CAUSE_MAX_DEPTH = 5;
|
|
784
|
+
var BLAST_RADIUS_DEFAULT_DEPTH = 10;
|
|
785
|
+
function bestEdgeBySource(graph, edgeIds) {
|
|
786
|
+
const best = /* @__PURE__ */ new Map();
|
|
787
|
+
for (const id of edgeIds) {
|
|
788
|
+
const e = graph.getEdgeAttributes(id);
|
|
789
|
+
if (e.provenance === import_types.Provenance.FRONTIER) continue;
|
|
790
|
+
const cur = best.get(e.source);
|
|
791
|
+
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
792
|
+
best.set(e.source, e);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
return best;
|
|
796
|
+
}
|
|
797
|
+
function bestEdgeByTarget(graph, edgeIds) {
|
|
798
|
+
const best = /* @__PURE__ */ new Map();
|
|
799
|
+
for (const id of edgeIds) {
|
|
800
|
+
const e = graph.getEdgeAttributes(id);
|
|
801
|
+
if (e.provenance === import_types.Provenance.FRONTIER) continue;
|
|
802
|
+
const cur = best.get(e.target);
|
|
803
|
+
if (!cur || import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[cur.provenance]) {
|
|
804
|
+
best.set(e.target, e);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
return best;
|
|
808
|
+
}
|
|
809
|
+
var PROVENANCE_CEILING = {
|
|
810
|
+
OBSERVED: 1,
|
|
811
|
+
INFERRED: 0.7,
|
|
812
|
+
EXTRACTED: 0.5,
|
|
813
|
+
STALE: 0.3,
|
|
814
|
+
FRONTIER: 0.3
|
|
815
|
+
};
|
|
816
|
+
function volumeWeight(spanCount) {
|
|
817
|
+
if (!spanCount || spanCount <= 0) return 0.5;
|
|
818
|
+
const w = 0.5 + Math.log10(spanCount + 1) / 3;
|
|
819
|
+
return Math.min(1, w);
|
|
820
|
+
}
|
|
821
|
+
function recencyWeight(ageMs) {
|
|
822
|
+
if (ageMs === void 0) return 0.8;
|
|
823
|
+
const hour = 60 * 60 * 1e3;
|
|
824
|
+
if (ageMs <= hour) return 1;
|
|
825
|
+
if (ageMs <= 24 * hour) {
|
|
826
|
+
const t = (ageMs - hour) / (23 * hour);
|
|
827
|
+
return 1 - 0.5 * t;
|
|
828
|
+
}
|
|
829
|
+
return 0.3;
|
|
830
|
+
}
|
|
831
|
+
function cleanlinessWeight(spanCount, errorCount) {
|
|
832
|
+
if (!spanCount || spanCount <= 0) return 1;
|
|
833
|
+
const rate = (errorCount ?? 0) / spanCount;
|
|
834
|
+
if (rate <= 0.01) return 1;
|
|
835
|
+
if (rate >= 0.5) return 0.3;
|
|
836
|
+
return 1 - rate * 1.4;
|
|
837
|
+
}
|
|
838
|
+
function confidenceForEdge(edge, now = Date.now()) {
|
|
839
|
+
const ceiling = PROVENANCE_CEILING[edge.provenance] ?? 0.5;
|
|
840
|
+
const spanCount = edge.signal?.spanCount ?? edge.callCount;
|
|
841
|
+
const ageMs = edge.signal?.lastObservedAgeMs ?? lastObservedAge(edge, now);
|
|
842
|
+
if (spanCount === void 0 && ageMs === void 0 && edge.signal === void 0) {
|
|
843
|
+
return ceiling;
|
|
844
|
+
}
|
|
845
|
+
const v = volumeWeight(spanCount);
|
|
846
|
+
const r = recencyWeight(ageMs);
|
|
847
|
+
const c = cleanlinessWeight(spanCount, edge.signal?.errorCount);
|
|
848
|
+
return Math.max(0, Math.min(1, ceiling * v * r * c));
|
|
849
|
+
}
|
|
850
|
+
function lastObservedAge(edge, now) {
|
|
851
|
+
if (!edge.lastObserved) return void 0;
|
|
852
|
+
const t = Date.parse(edge.lastObserved);
|
|
853
|
+
if (!Number.isFinite(t)) return void 0;
|
|
854
|
+
return Math.max(0, now - t);
|
|
855
|
+
}
|
|
856
|
+
function confidenceFromMix(edges, now = Date.now()) {
|
|
857
|
+
if (edges.length === 0) return 1;
|
|
858
|
+
let product = 1;
|
|
859
|
+
for (const e of edges) {
|
|
860
|
+
product *= confidenceForEdge(e, now);
|
|
861
|
+
}
|
|
862
|
+
return Math.max(0, Math.min(1, product));
|
|
863
|
+
}
|
|
864
|
+
function longestIncomingWalk(graph, start, maxDepth) {
|
|
865
|
+
let best = { path: [start], edges: [] };
|
|
866
|
+
const visited = /* @__PURE__ */ new Set([start]);
|
|
867
|
+
function step(node, path33, edges) {
|
|
868
|
+
if (path33.length > best.path.length) {
|
|
869
|
+
best = { path: [...path33], edges: [...edges] };
|
|
870
|
+
}
|
|
871
|
+
if (path33.length - 1 >= maxDepth) return;
|
|
872
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
873
|
+
for (const [srcId, edge] of incoming) {
|
|
874
|
+
if (visited.has(srcId)) continue;
|
|
875
|
+
visited.add(srcId);
|
|
876
|
+
path33.push(srcId);
|
|
877
|
+
edges.push(edge);
|
|
878
|
+
step(srcId, path33, edges);
|
|
879
|
+
path33.pop();
|
|
880
|
+
edges.pop();
|
|
881
|
+
visited.delete(srcId);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
step(start, [start], []);
|
|
885
|
+
return best;
|
|
886
|
+
}
|
|
887
|
+
function databaseRootCauseShape(graph, origin, walk) {
|
|
888
|
+
const targetDb = origin;
|
|
889
|
+
const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
|
|
890
|
+
if (candidatePairs.length === 0) return null;
|
|
891
|
+
for (const id of walk.path) {
|
|
892
|
+
const attrs = graph.getNodeAttributes(id);
|
|
893
|
+
if (attrs.type !== import_types.NodeType.ServiceNode) continue;
|
|
894
|
+
const svc = attrs;
|
|
895
|
+
const deps = svc.dependencies ?? {};
|
|
896
|
+
for (const pair of candidatePairs) {
|
|
897
|
+
const declared = deps[pair.driver];
|
|
898
|
+
if (!declared) continue;
|
|
899
|
+
const result = checkCompatibility(
|
|
900
|
+
pair.driver,
|
|
901
|
+
declared,
|
|
902
|
+
targetDb.engine,
|
|
903
|
+
targetDb.engineVersion
|
|
904
|
+
);
|
|
905
|
+
if (!result.compatible) {
|
|
906
|
+
return {
|
|
907
|
+
rootCauseNode: id,
|
|
908
|
+
rootCauseReason: result.reason ?? "incompatible driver",
|
|
909
|
+
...result.minDriverVersion ? {
|
|
910
|
+
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
911
|
+
} : {}
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return null;
|
|
917
|
+
}
|
|
918
|
+
function serviceRootCauseShape(graph, _origin, walk) {
|
|
919
|
+
for (const id of walk.path) {
|
|
920
|
+
const attrs = graph.getNodeAttributes(id);
|
|
921
|
+
if (attrs.type !== import_types.NodeType.ServiceNode) continue;
|
|
922
|
+
const svc = attrs;
|
|
923
|
+
const deps = svc.dependencies ?? {};
|
|
924
|
+
const serviceNodeEngine = svc.nodeEngine;
|
|
925
|
+
for (const constraint of nodeEngineConstraints()) {
|
|
926
|
+
const declared = deps[constraint.package];
|
|
927
|
+
if (!declared) continue;
|
|
928
|
+
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
929
|
+
if (!result.compatible && result.reason) {
|
|
930
|
+
return {
|
|
931
|
+
rootCauseNode: id,
|
|
932
|
+
rootCauseReason: result.reason,
|
|
933
|
+
...result.requiredNodeVersion ? {
|
|
934
|
+
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
935
|
+
} : {}
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
for (const conflict of packageConflicts()) {
|
|
940
|
+
const declared = deps[conflict.package];
|
|
941
|
+
if (!declared) continue;
|
|
942
|
+
const requiredDeclared = deps[conflict.requires.name];
|
|
943
|
+
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
944
|
+
if (!result.compatible && result.reason) {
|
|
945
|
+
return {
|
|
946
|
+
rootCauseNode: id,
|
|
947
|
+
rootCauseReason: result.reason,
|
|
948
|
+
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
var rootCauseShapes = {
|
|
956
|
+
[import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
|
|
957
|
+
[import_types.NodeType.ServiceNode]: serviceRootCauseShape
|
|
958
|
+
};
|
|
959
|
+
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
960
|
+
if (!graph.hasNode(errorNodeId)) return null;
|
|
961
|
+
const origin = graph.getNodeAttributes(errorNodeId);
|
|
962
|
+
const shape = rootCauseShapes[origin.type];
|
|
963
|
+
if (!shape) return null;
|
|
964
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
965
|
+
const match = shape(graph, origin, walk);
|
|
966
|
+
if (!match) return null;
|
|
967
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
968
|
+
return import_types.RootCauseResultSchema.parse({
|
|
969
|
+
rootCauseNode: match.rootCauseNode,
|
|
970
|
+
rootCauseReason: reason,
|
|
971
|
+
traversalPath: walk.path,
|
|
972
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
973
|
+
confidence: confidenceFromMix(walk.edges),
|
|
974
|
+
fixRecommendation: match.fixRecommendation
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
978
|
+
if (!graph.hasNode(nodeId)) {
|
|
979
|
+
return import_types.BlastRadiusResultSchema.parse({ origin: nodeId, affectedNodes: [], totalAffected: 0 });
|
|
980
|
+
}
|
|
981
|
+
const seen = /* @__PURE__ */ new Map();
|
|
982
|
+
const queue = [{ nodeId, distance: 0, path: [nodeId], pathEdges: [] }];
|
|
983
|
+
const enqueued = /* @__PURE__ */ new Set([nodeId]);
|
|
984
|
+
while (queue.length > 0) {
|
|
985
|
+
const frame = queue.shift();
|
|
986
|
+
if (frame.distance > 0 && frame.pathEdges.length > 0) {
|
|
987
|
+
const lastEdge = frame.pathEdges[frame.pathEdges.length - 1];
|
|
988
|
+
seen.set(frame.nodeId, {
|
|
989
|
+
nodeId: frame.nodeId,
|
|
990
|
+
distance: frame.distance,
|
|
991
|
+
edgeProvenance: lastEdge.provenance,
|
|
992
|
+
path: frame.path,
|
|
993
|
+
confidence: confidenceFromMix(frame.pathEdges)
|
|
994
|
+
});
|
|
995
|
+
}
|
|
996
|
+
if (frame.distance >= maxDepth) continue;
|
|
997
|
+
const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
998
|
+
for (const [tgtId, edge] of outgoing) {
|
|
999
|
+
if (enqueued.has(tgtId)) continue;
|
|
1000
|
+
enqueued.add(tgtId);
|
|
1001
|
+
queue.push({
|
|
1002
|
+
nodeId: tgtId,
|
|
1003
|
+
distance: frame.distance + 1,
|
|
1004
|
+
path: [...frame.path, tgtId],
|
|
1005
|
+
pathEdges: [...frame.pathEdges, edge]
|
|
1006
|
+
});
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const affectedNodes = [...seen.values()].sort(
|
|
1010
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
1011
|
+
);
|
|
1012
|
+
return import_types.BlastRadiusResultSchema.parse({
|
|
1013
|
+
origin: nodeId,
|
|
1014
|
+
affectedNodes,
|
|
1015
|
+
totalAffected: affectedNodes.length
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
var TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH = 3;
|
|
1019
|
+
var TRANSITIVE_DEPENDENCIES_MAX_DEPTH = 10;
|
|
1020
|
+
function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH) {
|
|
1021
|
+
if (!graph.hasNode(nodeId)) {
|
|
1022
|
+
return import_types.TransitiveDependenciesResultSchema.parse({
|
|
1023
|
+
origin: nodeId,
|
|
1024
|
+
depth,
|
|
1025
|
+
dependencies: [],
|
|
1026
|
+
total: 0
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1030
|
+
const queue = [{ nodeId, distance: 0, edge: null }];
|
|
1031
|
+
const enqueued = /* @__PURE__ */ new Set([nodeId]);
|
|
1032
|
+
while (queue.length > 0) {
|
|
1033
|
+
const frame = queue.shift();
|
|
1034
|
+
if (frame.distance > 0 && frame.edge) {
|
|
1035
|
+
seen.set(frame.nodeId, {
|
|
1036
|
+
nodeId: frame.nodeId,
|
|
1037
|
+
distance: frame.distance,
|
|
1038
|
+
edgeType: frame.edge.type,
|
|
1039
|
+
provenance: frame.edge.provenance
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
if (frame.distance >= depth) continue;
|
|
1043
|
+
const outgoing = bestEdgeByTarget(graph, graph.outboundEdges(frame.nodeId));
|
|
1044
|
+
for (const [tgtId, edge] of outgoing) {
|
|
1045
|
+
if (enqueued.has(tgtId)) continue;
|
|
1046
|
+
enqueued.add(tgtId);
|
|
1047
|
+
queue.push({ nodeId: tgtId, distance: frame.distance + 1, edge });
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
const dependencies = [...seen.values()].sort(
|
|
1051
|
+
(a, b) => a.distance - b.distance || a.nodeId.localeCompare(b.nodeId)
|
|
1052
|
+
);
|
|
1053
|
+
return import_types.TransitiveDependenciesResultSchema.parse({
|
|
1054
|
+
origin: nodeId,
|
|
1055
|
+
depth,
|
|
1056
|
+
dependencies,
|
|
1057
|
+
total: dependencies.length
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// src/policy.ts
|
|
1062
|
+
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
1063
|
+
info: "log",
|
|
1064
|
+
warning: "alert",
|
|
1065
|
+
error: "alert",
|
|
1066
|
+
critical: "block"
|
|
1067
|
+
};
|
|
1068
|
+
function resolveOnViolation(policy) {
|
|
1069
|
+
return policy.onViolation ?? DEFAULT_ACTION_BY_SEVERITY[policy.severity];
|
|
1070
|
+
}
|
|
1071
|
+
function makeViolation(policy, rule, contextSuffix, message, subject, ctx) {
|
|
1072
|
+
return {
|
|
1073
|
+
id: `${policy.id}:${contextSuffix}`,
|
|
1074
|
+
policyId: policy.id,
|
|
1075
|
+
policyName: policy.name,
|
|
1076
|
+
severity: policy.severity,
|
|
1077
|
+
onViolation: resolveOnViolation(policy),
|
|
1078
|
+
ruleType: rule.type,
|
|
1079
|
+
subject,
|
|
1080
|
+
message,
|
|
1081
|
+
observedAt: new Date(ctx.now()).toISOString()
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
var evaluateStructural = ({
|
|
1085
|
+
graph,
|
|
1086
|
+
policy,
|
|
1087
|
+
rule,
|
|
1088
|
+
ctx
|
|
1089
|
+
}) => {
|
|
1090
|
+
const violations = [];
|
|
1091
|
+
graph.forEachNode((id, attrs) => {
|
|
1092
|
+
const a = attrs;
|
|
1093
|
+
if (a.type !== rule.fromNodeType) return;
|
|
1094
|
+
let satisfied = false;
|
|
1095
|
+
for (const edgeId of graph.outboundEdges(id)) {
|
|
1096
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1097
|
+
if (e.type !== rule.edgeType) continue;
|
|
1098
|
+
if (e.provenance === import_types2.Provenance.FRONTIER) continue;
|
|
1099
|
+
const target = graph.getNodeAttributes(e.target);
|
|
1100
|
+
if (target.type === rule.toNodeType) {
|
|
1101
|
+
satisfied = true;
|
|
1102
|
+
break;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
if (!satisfied) {
|
|
1106
|
+
violations.push(
|
|
1107
|
+
makeViolation(
|
|
1108
|
+
policy,
|
|
1109
|
+
rule,
|
|
1110
|
+
id,
|
|
1111
|
+
`${rule.fromNodeType} ${id} has no ${rule.edgeType} edge to a ${rule.toNodeType}`,
|
|
1112
|
+
{ nodeId: id },
|
|
1113
|
+
ctx
|
|
1114
|
+
)
|
|
1115
|
+
);
|
|
1116
|
+
}
|
|
1117
|
+
});
|
|
1118
|
+
return violations;
|
|
1119
|
+
};
|
|
1120
|
+
var evaluateOwnership = ({
|
|
1121
|
+
graph,
|
|
1122
|
+
policy,
|
|
1123
|
+
rule,
|
|
1124
|
+
ctx
|
|
1125
|
+
}) => {
|
|
1126
|
+
const violations = [];
|
|
1127
|
+
graph.forEachNode((id, attrs) => {
|
|
1128
|
+
const a = attrs;
|
|
1129
|
+
if (a.type !== rule.nodeType) return;
|
|
1130
|
+
const value = a[rule.field];
|
|
1131
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1132
|
+
violations.push(
|
|
1133
|
+
makeViolation(
|
|
1134
|
+
policy,
|
|
1135
|
+
rule,
|
|
1136
|
+
id,
|
|
1137
|
+
`${rule.nodeType} ${id} is missing required field "${rule.field}"`,
|
|
1138
|
+
{ nodeId: id },
|
|
1139
|
+
ctx
|
|
1140
|
+
)
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
});
|
|
1144
|
+
return violations;
|
|
1145
|
+
};
|
|
1146
|
+
var evaluateProvenance = ({
|
|
1147
|
+
graph,
|
|
1148
|
+
policy,
|
|
1149
|
+
rule,
|
|
1150
|
+
ctx
|
|
1151
|
+
}) => {
|
|
1152
|
+
const required = Array.isArray(rule.required) ? new Set(rule.required) : /* @__PURE__ */ new Set([rule.required]);
|
|
1153
|
+
const violations = [];
|
|
1154
|
+
graph.forEachEdge((edgeId, attrs) => {
|
|
1155
|
+
const e = attrs;
|
|
1156
|
+
if (e.type !== rule.edgeType) return;
|
|
1157
|
+
if (rule.targetNodeId && e.target !== rule.targetNodeId) return;
|
|
1158
|
+
if (!required.has(e.provenance)) {
|
|
1159
|
+
const requiredList = [...required].join(" | ");
|
|
1160
|
+
violations.push(
|
|
1161
|
+
makeViolation(
|
|
1162
|
+
policy,
|
|
1163
|
+
rule,
|
|
1164
|
+
edgeId,
|
|
1165
|
+
`${rule.edgeType} edge ${edgeId} has provenance ${e.provenance}; required ${requiredList}`,
|
|
1166
|
+
{ edgeId },
|
|
1167
|
+
ctx
|
|
1168
|
+
)
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
return violations;
|
|
1173
|
+
};
|
|
1174
|
+
var evaluateBlastRadius = ({
|
|
1175
|
+
graph,
|
|
1176
|
+
policy,
|
|
1177
|
+
rule,
|
|
1178
|
+
ctx
|
|
1179
|
+
}) => {
|
|
1180
|
+
const violations = [];
|
|
1181
|
+
const depth = rule.depth;
|
|
1182
|
+
graph.forEachNode((id, attrs) => {
|
|
1183
|
+
const a = attrs;
|
|
1184
|
+
if (a.type !== rule.nodeType) return;
|
|
1185
|
+
const result = depth !== void 0 ? getBlastRadius(graph, id, depth) : getBlastRadius(graph, id);
|
|
1186
|
+
if (result.totalAffected > rule.maxAffected) {
|
|
1187
|
+
violations.push(
|
|
1188
|
+
makeViolation(
|
|
1189
|
+
policy,
|
|
1190
|
+
rule,
|
|
1191
|
+
id,
|
|
1192
|
+
`${rule.nodeType} ${id} has blast radius ${result.totalAffected} > ${rule.maxAffected}`,
|
|
1193
|
+
{ nodeId: id, path: [id] },
|
|
1194
|
+
ctx
|
|
1195
|
+
)
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
return violations;
|
|
1200
|
+
};
|
|
1201
|
+
var evaluateCompatibility = ({
|
|
1202
|
+
graph,
|
|
1203
|
+
policy,
|
|
1204
|
+
rule,
|
|
1205
|
+
ctx
|
|
1206
|
+
}) => {
|
|
1207
|
+
const violations = [];
|
|
1208
|
+
const wantsKind = (kind) => rule.kind === void 0 || rule.kind === kind;
|
|
1209
|
+
graph.forEachNode((svcId, attrs) => {
|
|
1210
|
+
const a = attrs;
|
|
1211
|
+
if (a.type !== import_types2.NodeType.ServiceNode) return;
|
|
1212
|
+
const svc = a;
|
|
1213
|
+
const deps = svc.dependencies ?? {};
|
|
1214
|
+
if (wantsKind("driver-engine")) {
|
|
1215
|
+
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
1216
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1217
|
+
if (e.type !== import_types2.EdgeType.CONNECTS_TO) continue;
|
|
1218
|
+
if (e.provenance === import_types2.Provenance.FRONTIER) continue;
|
|
1219
|
+
const dbAttrs = graph.getNodeAttributes(e.target);
|
|
1220
|
+
if (dbAttrs.type !== import_types2.NodeType.DatabaseNode) continue;
|
|
1221
|
+
const db = dbAttrs;
|
|
1222
|
+
for (const pair of compatPairs()) {
|
|
1223
|
+
if (pair.engine !== db.engine) continue;
|
|
1224
|
+
const declared = deps[pair.driver];
|
|
1225
|
+
if (!declared) continue;
|
|
1226
|
+
const result = checkCompatibility(pair.driver, declared, db.engine, db.engineVersion);
|
|
1227
|
+
if (!result.compatible && result.reason) {
|
|
1228
|
+
violations.push(
|
|
1229
|
+
makeViolation(
|
|
1230
|
+
policy,
|
|
1231
|
+
rule,
|
|
1232
|
+
`${svcId}:driver-engine:${pair.driver}@${declared}:${db.engine}@${db.engineVersion}`,
|
|
1233
|
+
result.reason,
|
|
1234
|
+
{ nodeId: svcId, edgeId },
|
|
1235
|
+
ctx
|
|
1236
|
+
)
|
|
1237
|
+
);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
if (wantsKind("node-engine")) {
|
|
1243
|
+
const serviceNodeRange = svc.nodeEngine;
|
|
1244
|
+
for (const constraint of nodeEngineConstraints()) {
|
|
1245
|
+
const declared = deps[constraint.package];
|
|
1246
|
+
if (!declared) continue;
|
|
1247
|
+
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeRange);
|
|
1248
|
+
if (!result.compatible && result.reason) {
|
|
1249
|
+
violations.push(
|
|
1250
|
+
makeViolation(
|
|
1251
|
+
policy,
|
|
1252
|
+
rule,
|
|
1253
|
+
`${svcId}:node-engine:${constraint.package}@${declared}`,
|
|
1254
|
+
result.reason,
|
|
1255
|
+
{ nodeId: svcId },
|
|
1256
|
+
ctx
|
|
1257
|
+
)
|
|
1258
|
+
);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (wantsKind("package-conflict")) {
|
|
1263
|
+
for (const conflict of packageConflicts()) {
|
|
1264
|
+
const declared = deps[conflict.package];
|
|
1265
|
+
if (!declared) continue;
|
|
1266
|
+
const requiredDeclared = deps[conflict.requires.name];
|
|
1267
|
+
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
1268
|
+
if (!result.compatible && result.reason) {
|
|
1269
|
+
violations.push(
|
|
1270
|
+
makeViolation(
|
|
1271
|
+
policy,
|
|
1272
|
+
rule,
|
|
1273
|
+
`${svcId}:package-conflict:${conflict.package}@${declared}`,
|
|
1274
|
+
result.reason,
|
|
1275
|
+
{ nodeId: svcId },
|
|
1276
|
+
ctx
|
|
1277
|
+
)
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (wantsKind("deprecated-api")) {
|
|
1283
|
+
for (const dep of deprecatedApis()) {
|
|
1284
|
+
const declared = deps[dep.package];
|
|
1285
|
+
if (!declared) continue;
|
|
1286
|
+
const result = checkDeprecatedApi(dep, declared);
|
|
1287
|
+
if (!result.compatible && result.reason) {
|
|
1288
|
+
violations.push(
|
|
1289
|
+
makeViolation(
|
|
1290
|
+
policy,
|
|
1291
|
+
rule,
|
|
1292
|
+
`${svcId}:deprecated-api:${dep.package}@${declared}`,
|
|
1293
|
+
result.reason,
|
|
1294
|
+
{ nodeId: svcId },
|
|
1295
|
+
ctx
|
|
1296
|
+
)
|
|
1297
|
+
);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
});
|
|
1302
|
+
return violations;
|
|
1303
|
+
};
|
|
1304
|
+
var policyEvaluators = {
|
|
1305
|
+
structural: evaluateStructural,
|
|
1306
|
+
ownership: evaluateOwnership,
|
|
1307
|
+
provenance: evaluateProvenance,
|
|
1308
|
+
"blast-radius": evaluateBlastRadius,
|
|
1309
|
+
compatibility: evaluateCompatibility
|
|
1310
|
+
};
|
|
1311
|
+
function canPromoteFrontier(graph, frontierId2, policies, ctx) {
|
|
1312
|
+
if (policies.length === 0) return { allowed: true, violations: [] };
|
|
1313
|
+
const all = evaluateAllPolicies(graph, policies, ctx);
|
|
1314
|
+
const blocking = all.filter((v) => {
|
|
1315
|
+
if (v.onViolation !== "block") return false;
|
|
1316
|
+
return v.subject.nodeId === frontierId2 || v.subject.path?.includes(frontierId2) === true;
|
|
1317
|
+
});
|
|
1318
|
+
return { allowed: blocking.length === 0, violations: blocking };
|
|
1319
|
+
}
|
|
1320
|
+
function evaluateAllPolicies(graph, policies, ctx) {
|
|
1321
|
+
const out = [];
|
|
1322
|
+
for (const policy of policies) {
|
|
1323
|
+
const evaluator = policyEvaluators[policy.rule.type];
|
|
1324
|
+
const violations = evaluator({ graph, policy, rule: policy.rule, ctx });
|
|
1325
|
+
for (const v of violations) out.push(v);
|
|
1326
|
+
}
|
|
1327
|
+
return out;
|
|
1328
|
+
}
|
|
1329
|
+
async function loadPolicyFile(policyPath) {
|
|
1330
|
+
let raw;
|
|
1331
|
+
try {
|
|
1332
|
+
raw = await import_node_fs2.promises.readFile(policyPath, "utf8");
|
|
1333
|
+
} catch (err) {
|
|
1334
|
+
if (err.code === "ENOENT") return [];
|
|
1335
|
+
throw err;
|
|
1336
|
+
}
|
|
1337
|
+
const json = JSON.parse(raw);
|
|
1338
|
+
const file = import_types2.PolicyFileSchema.parse(json);
|
|
1339
|
+
return file.policies;
|
|
1340
|
+
}
|
|
1341
|
+
var PolicyViolationsLog = class {
|
|
1342
|
+
path;
|
|
1343
|
+
seen = null;
|
|
1344
|
+
constructor(logPath) {
|
|
1345
|
+
this.path = logPath;
|
|
1346
|
+
}
|
|
1347
|
+
async append(v) {
|
|
1348
|
+
if (!this.seen) await this.hydrate();
|
|
1349
|
+
if (this.seen.has(v.id)) return false;
|
|
1350
|
+
this.seen.add(v.id);
|
|
1351
|
+
await import_node_fs2.promises.mkdir(import_node_path2.default.dirname(this.path), { recursive: true });
|
|
1352
|
+
await import_node_fs2.promises.appendFile(this.path, JSON.stringify(v) + "\n", "utf8");
|
|
1353
|
+
return true;
|
|
1354
|
+
}
|
|
1355
|
+
async readAll() {
|
|
1356
|
+
try {
|
|
1357
|
+
const raw = await import_node_fs2.promises.readFile(this.path, "utf8");
|
|
1358
|
+
return raw.split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
1359
|
+
} catch (err) {
|
|
1360
|
+
if (err.code === "ENOENT") return [];
|
|
1361
|
+
throw err;
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
async hydrate() {
|
|
1365
|
+
this.seen = /* @__PURE__ */ new Set();
|
|
1366
|
+
const existing = await this.readAll();
|
|
1367
|
+
for (const v of existing) this.seen.add(v.id);
|
|
1368
|
+
}
|
|
1369
|
+
};
|
|
1370
|
+
|
|
1371
|
+
// src/ingest.ts
|
|
1372
|
+
var import_types3 = require("@neat.is/types");
|
|
1373
|
+
var HOUR_MS = 60 * 60 * 1e3;
|
|
1374
|
+
var DAY_MS = 24 * HOUR_MS;
|
|
1375
|
+
var DEFAULT_STALE_THRESHOLDS = {
|
|
1376
|
+
CALLS: HOUR_MS,
|
|
1377
|
+
CONNECTS_TO: 4 * HOUR_MS,
|
|
1378
|
+
PUBLISHES_TO: 4 * HOUR_MS,
|
|
1379
|
+
CONSUMES_FROM: 4 * HOUR_MS,
|
|
1380
|
+
DEPENDS_ON: DAY_MS,
|
|
1381
|
+
CONFIGURED_BY: DAY_MS,
|
|
1382
|
+
RUNS_ON: DAY_MS
|
|
1383
|
+
};
|
|
1384
|
+
var FALLBACK_STALE_THRESHOLD_MS = DAY_MS;
|
|
1385
|
+
function loadStaleThresholdsFromEnv() {
|
|
1386
|
+
const raw = process.env.NEAT_STALE_THRESHOLDS;
|
|
1387
|
+
if (!raw) return DEFAULT_STALE_THRESHOLDS;
|
|
1388
|
+
try {
|
|
1389
|
+
const overrides = JSON.parse(raw);
|
|
1390
|
+
const merged = { ...DEFAULT_STALE_THRESHOLDS };
|
|
1391
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
1392
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) merged[k] = v;
|
|
1393
|
+
}
|
|
1394
|
+
return merged;
|
|
1395
|
+
} catch (err) {
|
|
1396
|
+
console.warn(
|
|
1397
|
+
`[neat] NEAT_STALE_THRESHOLDS could not be parsed (${err.message}); using defaults`
|
|
1398
|
+
);
|
|
1399
|
+
return DEFAULT_STALE_THRESHOLDS;
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
function thresholdForEdgeType(edgeType, overrides) {
|
|
1403
|
+
const map = overrides ?? loadStaleThresholdsFromEnv();
|
|
1404
|
+
return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
|
|
1405
|
+
}
|
|
1406
|
+
function nowIso(ctx) {
|
|
1407
|
+
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1408
|
+
}
|
|
1409
|
+
function pickAttr(span, ...keys) {
|
|
1410
|
+
for (const k of keys) {
|
|
1411
|
+
const v = span.attributes[k];
|
|
1412
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
1413
|
+
}
|
|
1414
|
+
return void 0;
|
|
1415
|
+
}
|
|
1416
|
+
function hostFromUrl(u) {
|
|
1417
|
+
if (!u) return void 0;
|
|
1418
|
+
try {
|
|
1419
|
+
return new URL(u).hostname;
|
|
1420
|
+
} catch {
|
|
1421
|
+
return void 0;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
function pickAddress(span) {
|
|
1425
|
+
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1426
|
+
}
|
|
1427
|
+
function makeObservedEdgeId(type, source, target) {
|
|
1428
|
+
return (0, import_types3.observedEdgeId)(source, target, type);
|
|
1429
|
+
}
|
|
1430
|
+
function makeInferredEdgeId(type, source, target) {
|
|
1431
|
+
return (0, import_types3.inferredEdgeId)(source, target, type);
|
|
1432
|
+
}
|
|
1433
|
+
var INFERRED_CONFIDENCE = 0.6;
|
|
1434
|
+
var STITCH_MAX_DEPTH = 2;
|
|
1435
|
+
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
1436
|
+
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1437
|
+
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
1438
|
+
function parentSpanKey(traceId, spanId) {
|
|
1439
|
+
return `${traceId}:${spanId}`;
|
|
1440
|
+
}
|
|
1441
|
+
function cacheSpanService(span, now) {
|
|
1442
|
+
if (!span.traceId || !span.spanId) return;
|
|
1443
|
+
const key = parentSpanKey(span.traceId, span.spanId);
|
|
1444
|
+
parentSpanCache.delete(key);
|
|
1445
|
+
parentSpanCache.set(key, { service: span.service, expiresAt: now + PARENT_SPAN_CACHE_TTL_MS });
|
|
1446
|
+
while (parentSpanCache.size > PARENT_SPAN_CACHE_SIZE) {
|
|
1447
|
+
const oldest = parentSpanCache.keys().next().value;
|
|
1448
|
+
if (!oldest) break;
|
|
1449
|
+
parentSpanCache.delete(oldest);
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
function lookupParentSpanService(traceId, parentSpanId, now) {
|
|
1453
|
+
const entry = parentSpanCache.get(parentSpanKey(traceId, parentSpanId));
|
|
1454
|
+
if (!entry) return null;
|
|
1455
|
+
if (entry.expiresAt <= now) {
|
|
1456
|
+
parentSpanCache.delete(parentSpanKey(traceId, parentSpanId));
|
|
1457
|
+
return null;
|
|
1458
|
+
}
|
|
1459
|
+
return entry.service;
|
|
1460
|
+
}
|
|
1461
|
+
function resolveServiceId(graph, host) {
|
|
1462
|
+
const direct = (0, import_types3.serviceId)(host);
|
|
1463
|
+
if (graph.hasNode(direct)) return direct;
|
|
1464
|
+
let found = null;
|
|
1465
|
+
graph.forEachNode((id, attrs) => {
|
|
1466
|
+
if (found) return;
|
|
1467
|
+
const a = attrs;
|
|
1468
|
+
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1469
|
+
if (a.name === host) {
|
|
1470
|
+
found = id;
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
if (a.aliases && a.aliases.includes(host)) {
|
|
1474
|
+
found = id;
|
|
1475
|
+
}
|
|
1476
|
+
});
|
|
1477
|
+
return found;
|
|
1478
|
+
}
|
|
1479
|
+
function frontierIdFor(host) {
|
|
1480
|
+
return (0, import_types3.frontierId)(host);
|
|
1481
|
+
}
|
|
1482
|
+
function ensureServiceNode(graph, serviceName) {
|
|
1483
|
+
const id = (0, import_types3.serviceId)(serviceName);
|
|
1484
|
+
if (graph.hasNode(id)) return id;
|
|
1485
|
+
const node = {
|
|
1486
|
+
id,
|
|
1487
|
+
type: import_types3.NodeType.ServiceNode,
|
|
1488
|
+
name: serviceName,
|
|
1489
|
+
language: "unknown",
|
|
1490
|
+
discoveredVia: "otel"
|
|
1491
|
+
};
|
|
1492
|
+
graph.addNode(id, node);
|
|
1493
|
+
return id;
|
|
1494
|
+
}
|
|
1495
|
+
function ensureDatabaseNode(graph, host, engine) {
|
|
1496
|
+
const id = (0, import_types3.databaseId)(host);
|
|
1497
|
+
if (graph.hasNode(id)) return id;
|
|
1498
|
+
const node = {
|
|
1499
|
+
id,
|
|
1500
|
+
type: import_types3.NodeType.DatabaseNode,
|
|
1501
|
+
name: host,
|
|
1502
|
+
engine,
|
|
1503
|
+
engineVersion: "unknown",
|
|
1504
|
+
compatibleDrivers: [],
|
|
1505
|
+
host,
|
|
1506
|
+
discoveredVia: "otel"
|
|
1507
|
+
};
|
|
1508
|
+
graph.addNode(id, node);
|
|
1509
|
+
return id;
|
|
1510
|
+
}
|
|
1511
|
+
function ensureFrontierNode(graph, host, ts) {
|
|
1512
|
+
const id = frontierIdFor(host);
|
|
1513
|
+
if (graph.hasNode(id)) {
|
|
1514
|
+
const existing = graph.getNodeAttributes(id);
|
|
1515
|
+
graph.replaceNodeAttributes(id, { ...existing, lastObserved: ts });
|
|
1516
|
+
return id;
|
|
1517
|
+
}
|
|
1518
|
+
const node = {
|
|
1519
|
+
id,
|
|
1520
|
+
type: import_types3.NodeType.FrontierNode,
|
|
1521
|
+
name: host,
|
|
1522
|
+
host,
|
|
1523
|
+
firstObserved: ts,
|
|
1524
|
+
lastObserved: ts
|
|
1525
|
+
};
|
|
1526
|
+
graph.addNode(id, node);
|
|
1527
|
+
return id;
|
|
1528
|
+
}
|
|
1529
|
+
function upsertFrontierEdge(graph, type, source, target, ts) {
|
|
1530
|
+
const id = (0, import_types3.frontierEdgeId)(source, target, type);
|
|
1531
|
+
if (graph.hasEdge(id)) {
|
|
1532
|
+
const existing = graph.getEdgeAttributes(id);
|
|
1533
|
+
const updated = {
|
|
1534
|
+
...existing,
|
|
1535
|
+
provenance: import_types3.Provenance.FRONTIER,
|
|
1536
|
+
lastObserved: ts,
|
|
1537
|
+
callCount: (existing.callCount ?? 0) + 1
|
|
1538
|
+
};
|
|
1539
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
const edge = {
|
|
1543
|
+
id,
|
|
1544
|
+
source,
|
|
1545
|
+
target,
|
|
1546
|
+
type,
|
|
1547
|
+
provenance: import_types3.Provenance.FRONTIER,
|
|
1548
|
+
confidence: 1,
|
|
1549
|
+
lastObserved: ts,
|
|
1550
|
+
callCount: 1
|
|
1551
|
+
};
|
|
1552
|
+
graph.addEdgeWithKey(id, source, target, edge);
|
|
1553
|
+
}
|
|
1554
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1555
|
+
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1556
|
+
const id = makeObservedEdgeId(type, source, target);
|
|
1557
|
+
if (graph.hasEdge(id)) {
|
|
1558
|
+
const existing = graph.getEdgeAttributes(id);
|
|
1559
|
+
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1560
|
+
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1561
|
+
const updated = {
|
|
1562
|
+
...existing,
|
|
1563
|
+
provenance: import_types3.Provenance.OBSERVED,
|
|
1564
|
+
lastObserved: ts,
|
|
1565
|
+
callCount: newSpanCount,
|
|
1566
|
+
signal: {
|
|
1567
|
+
spanCount: newSpanCount,
|
|
1568
|
+
errorCount: newErrorCount,
|
|
1569
|
+
lastObservedAgeMs: 0
|
|
1570
|
+
},
|
|
1571
|
+
confidence: 1
|
|
1572
|
+
};
|
|
1573
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1574
|
+
return { edge: updated, created: false };
|
|
1575
|
+
}
|
|
1576
|
+
const edge = {
|
|
1577
|
+
id,
|
|
1578
|
+
source,
|
|
1579
|
+
target,
|
|
1580
|
+
type,
|
|
1581
|
+
provenance: import_types3.Provenance.OBSERVED,
|
|
1582
|
+
confidence: 1,
|
|
1583
|
+
lastObserved: ts,
|
|
1584
|
+
callCount: 1,
|
|
1585
|
+
signal: {
|
|
1586
|
+
spanCount: 1,
|
|
1587
|
+
errorCount: isError ? 1 : 0,
|
|
1588
|
+
lastObservedAgeMs: 0
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
graph.addEdgeWithKey(id, source, target, edge);
|
|
1592
|
+
return { edge, created: true };
|
|
1593
|
+
}
|
|
1594
|
+
function stitchTrace(graph, sourceServiceId, ts) {
|
|
1595
|
+
if (!graph.hasNode(sourceServiceId)) return;
|
|
1596
|
+
const visited = /* @__PURE__ */ new Set([sourceServiceId]);
|
|
1597
|
+
const queue = [{ nodeId: sourceServiceId, depth: 0 }];
|
|
1598
|
+
while (queue.length > 0) {
|
|
1599
|
+
const { nodeId, depth } = queue.shift();
|
|
1600
|
+
if (depth >= STITCH_MAX_DEPTH) continue;
|
|
1601
|
+
const outbound = graph.outboundEdges(nodeId);
|
|
1602
|
+
for (const edgeId of outbound) {
|
|
1603
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
1604
|
+
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
1605
|
+
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
1606
|
+
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
1607
|
+
if (!visited.has(edge.target)) {
|
|
1608
|
+
visited.add(edge.target);
|
|
1609
|
+
queue.push({ nodeId: edge.target, depth: depth + 1 });
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
function upsertInferredEdge(graph, type, source, target, ts) {
|
|
1615
|
+
const id = makeInferredEdgeId(type, source, target);
|
|
1616
|
+
if (graph.hasEdge(id)) {
|
|
1617
|
+
const existing = graph.getEdgeAttributes(id);
|
|
1618
|
+
const updated = { ...existing, lastObserved: ts };
|
|
1619
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
const edge = {
|
|
1623
|
+
id,
|
|
1624
|
+
source,
|
|
1625
|
+
target,
|
|
1626
|
+
type,
|
|
1627
|
+
provenance: import_types3.Provenance.INFERRED,
|
|
1628
|
+
confidence: INFERRED_CONFIDENCE,
|
|
1629
|
+
lastObserved: ts
|
|
1630
|
+
};
|
|
1631
|
+
graph.addEdgeWithKey(id, source, target, edge);
|
|
1632
|
+
}
|
|
1633
|
+
async function appendErrorEvent(ctx, ev) {
|
|
1634
|
+
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(ctx.errorsPath), { recursive: true });
|
|
1635
|
+
await import_node_fs3.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
1636
|
+
}
|
|
1637
|
+
async function handleSpan(ctx, span) {
|
|
1638
|
+
const ts = span.startTimeIso ?? nowIso(ctx);
|
|
1639
|
+
const nowMs = ctx.now ? ctx.now() : Date.now();
|
|
1640
|
+
const sourceId = ensureServiceNode(ctx.graph, span.service);
|
|
1641
|
+
const isError = span.statusCode === 2;
|
|
1642
|
+
cacheSpanService(span, nowMs);
|
|
1643
|
+
let affectedNode = sourceId;
|
|
1644
|
+
if (span.dbSystem) {
|
|
1645
|
+
const host = pickAddress(span);
|
|
1646
|
+
if (host) {
|
|
1647
|
+
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
1648
|
+
const targetId = (0, import_types3.databaseId)(host);
|
|
1649
|
+
const result = upsertObservedEdge(
|
|
1650
|
+
ctx.graph,
|
|
1651
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
1652
|
+
sourceId,
|
|
1653
|
+
targetId,
|
|
1654
|
+
ts,
|
|
1655
|
+
isError
|
|
1656
|
+
);
|
|
1657
|
+
if (result) affectedNode = targetId;
|
|
1658
|
+
}
|
|
1659
|
+
} else {
|
|
1660
|
+
const host = pickAddress(span);
|
|
1661
|
+
let resolvedViaAddress = false;
|
|
1662
|
+
if (host && host !== span.service) {
|
|
1663
|
+
const targetId = resolveServiceId(ctx.graph, host);
|
|
1664
|
+
if (targetId && targetId !== sourceId) {
|
|
1665
|
+
upsertObservedEdge(
|
|
1666
|
+
ctx.graph,
|
|
1667
|
+
import_types3.EdgeType.CALLS,
|
|
1668
|
+
sourceId,
|
|
1669
|
+
targetId,
|
|
1670
|
+
ts,
|
|
1671
|
+
isError
|
|
1672
|
+
);
|
|
1673
|
+
affectedNode = targetId;
|
|
1674
|
+
resolvedViaAddress = true;
|
|
1675
|
+
} else if (!targetId) {
|
|
1676
|
+
const frontierId2 = ensureFrontierNode(ctx.graph, host, ts);
|
|
1677
|
+
if (ctx.graph.hasNode(sourceId)) {
|
|
1678
|
+
upsertFrontierEdge(ctx.graph, import_types3.EdgeType.CALLS, sourceId, frontierId2, ts);
|
|
1679
|
+
}
|
|
1680
|
+
affectedNode = frontierId2;
|
|
1681
|
+
resolvedViaAddress = true;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (!resolvedViaAddress && span.parentSpanId) {
|
|
1685
|
+
const parentService = lookupParentSpanService(span.traceId, span.parentSpanId, nowMs);
|
|
1686
|
+
if (parentService && parentService !== span.service) {
|
|
1687
|
+
const parentId = ensureServiceNode(ctx.graph, parentService);
|
|
1688
|
+
upsertObservedEdge(
|
|
1689
|
+
ctx.graph,
|
|
1690
|
+
import_types3.EdgeType.CALLS,
|
|
1691
|
+
parentId,
|
|
1692
|
+
sourceId,
|
|
1693
|
+
ts,
|
|
1694
|
+
isError
|
|
1695
|
+
);
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (span.statusCode === 2) {
|
|
1700
|
+
stitchTrace(ctx.graph, sourceId, ts);
|
|
1701
|
+
if (ctx.writeErrorEventInline !== false) {
|
|
1702
|
+
const ev = {
|
|
1703
|
+
id: `${span.traceId}:${span.spanId}`,
|
|
1704
|
+
timestamp: ts,
|
|
1705
|
+
service: span.service,
|
|
1706
|
+
traceId: span.traceId,
|
|
1707
|
+
spanId: span.spanId,
|
|
1708
|
+
errorMessage: span.exception?.message ?? span.errorMessage ?? span.name ?? "unknown error",
|
|
1709
|
+
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
1710
|
+
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
1711
|
+
affectedNode
|
|
1712
|
+
};
|
|
1713
|
+
await appendErrorEvent(ctx, ev);
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
void affectedNode;
|
|
1717
|
+
if (ctx.onPolicyTrigger) await ctx.onPolicyTrigger(ctx.graph);
|
|
1718
|
+
}
|
|
1719
|
+
function promoteFrontierNodes(graph, opts = {}) {
|
|
1720
|
+
const aliasIndex = /* @__PURE__ */ new Map();
|
|
1721
|
+
graph.forEachNode((id, attrs) => {
|
|
1722
|
+
const a = attrs;
|
|
1723
|
+
if (a.type !== import_types3.NodeType.ServiceNode) return;
|
|
1724
|
+
aliasIndex.set(a.name, id);
|
|
1725
|
+
if (a.aliases) {
|
|
1726
|
+
for (const alias of a.aliases) aliasIndex.set(alias, id);
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
const toPromote = [];
|
|
1730
|
+
graph.forEachNode((id, attrs) => {
|
|
1731
|
+
const a = attrs;
|
|
1732
|
+
if (a.type !== import_types3.NodeType.FrontierNode) return;
|
|
1733
|
+
const target = aliasIndex.get(a.host);
|
|
1734
|
+
if (!target) return;
|
|
1735
|
+
if (target === id) return;
|
|
1736
|
+
toPromote.push({ frontierId: id, serviceId: target });
|
|
1737
|
+
});
|
|
1738
|
+
let promoted = 0;
|
|
1739
|
+
for (const { frontierId: frontierId2, serviceId: serviceId3 } of toPromote) {
|
|
1740
|
+
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
1741
|
+
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
1742
|
+
if (!gate.allowed) {
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
rewireFrontierEdges(graph, frontierId2, serviceId3);
|
|
1747
|
+
graph.dropNode(frontierId2);
|
|
1748
|
+
promoted++;
|
|
1749
|
+
}
|
|
1750
|
+
return promoted;
|
|
1751
|
+
}
|
|
1752
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId3) {
|
|
1753
|
+
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
1754
|
+
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
1755
|
+
for (const edgeId of inbound) {
|
|
1756
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
1757
|
+
rebuildEdge(graph, edge, edge.source, serviceId3, edgeId);
|
|
1758
|
+
}
|
|
1759
|
+
for (const edgeId of outbound) {
|
|
1760
|
+
const edge = graph.getEdgeAttributes(edgeId);
|
|
1761
|
+
rebuildEdge(graph, edge, serviceId3, edge.target, edgeId);
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
1765
|
+
graph.dropEdge(oldEdgeId);
|
|
1766
|
+
const promotedProvenance = edge.provenance === import_types3.Provenance.FRONTIER ? import_types3.Provenance.OBSERVED : edge.provenance;
|
|
1767
|
+
const newId = promotedProvenance === import_types3.Provenance.OBSERVED ? (0, import_types3.observedEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.INFERRED ? (0, import_types3.inferredEdgeId)(newSource, newTarget, edge.type) : promotedProvenance === import_types3.Provenance.EXTRACTED ? (0, import_types3.extractedEdgeId)(newSource, newTarget, edge.type) : (0, import_types3.frontierEdgeId)(newSource, newTarget, edge.type);
|
|
1768
|
+
if (graph.hasEdge(newId)) {
|
|
1769
|
+
const existing = graph.getEdgeAttributes(newId);
|
|
1770
|
+
const merged = {
|
|
1771
|
+
...existing,
|
|
1772
|
+
callCount: (existing.callCount ?? 0) + (edge.callCount ?? 0),
|
|
1773
|
+
lastObserved: pickLater(existing.lastObserved, edge.lastObserved)
|
|
1774
|
+
};
|
|
1775
|
+
graph.replaceEdgeAttributes(newId, merged);
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
const rebuilt = {
|
|
1779
|
+
...edge,
|
|
1780
|
+
id: newId,
|
|
1781
|
+
source: newSource,
|
|
1782
|
+
target: newTarget,
|
|
1783
|
+
provenance: promotedProvenance
|
|
1784
|
+
};
|
|
1785
|
+
graph.addEdgeWithKey(newId, newSource, newTarget, rebuilt);
|
|
1786
|
+
}
|
|
1787
|
+
function pickLater(a, b) {
|
|
1788
|
+
if (!a) return b;
|
|
1789
|
+
if (!b) return a;
|
|
1790
|
+
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
|
|
1791
|
+
}
|
|
1792
|
+
function makeSpanHandler(ctx) {
|
|
1793
|
+
return (span) => handleSpan(ctx, span);
|
|
1794
|
+
}
|
|
1795
|
+
async function markStaleEdges(graph, options = {}) {
|
|
1796
|
+
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
1797
|
+
const now = options.now ?? Date.now();
|
|
1798
|
+
const events = [];
|
|
1799
|
+
graph.forEachEdge((id, attrs) => {
|
|
1800
|
+
const e = attrs;
|
|
1801
|
+
if (e.provenance !== import_types3.Provenance.OBSERVED) return;
|
|
1802
|
+
if (!e.lastObserved) return;
|
|
1803
|
+
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
1804
|
+
const age = now - new Date(e.lastObserved).getTime();
|
|
1805
|
+
if (age > threshold) {
|
|
1806
|
+
const updated = { ...e, provenance: import_types3.Provenance.STALE, confidence: 0.3 };
|
|
1807
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
1808
|
+
events.push({
|
|
1809
|
+
edgeId: id,
|
|
1810
|
+
source: e.source,
|
|
1811
|
+
target: e.target,
|
|
1812
|
+
edgeType: e.type,
|
|
1813
|
+
thresholdMs: threshold,
|
|
1814
|
+
ageMs: age,
|
|
1815
|
+
lastObserved: e.lastObserved,
|
|
1816
|
+
transitionedAt: new Date(now).toISOString()
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
if (options.staleEventsPath && events.length > 0) {
|
|
1821
|
+
await appendStaleEvents(options.staleEventsPath, events);
|
|
1822
|
+
}
|
|
1823
|
+
return { count: events.length, events };
|
|
1824
|
+
}
|
|
1825
|
+
async function appendStaleEvents(staleEventsPath, events) {
|
|
1826
|
+
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(staleEventsPath), { recursive: true });
|
|
1827
|
+
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
1828
|
+
await import_node_fs3.promises.appendFile(staleEventsPath, lines, "utf8");
|
|
1829
|
+
}
|
|
1830
|
+
async function readStaleEvents(staleEventsPath) {
|
|
1831
|
+
try {
|
|
1832
|
+
const raw = await import_node_fs3.promises.readFile(staleEventsPath, "utf8");
|
|
1833
|
+
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
1834
|
+
} catch (err) {
|
|
1835
|
+
if (err.code === "ENOENT") return [];
|
|
1836
|
+
throw err;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function startStalenessLoop(graph, options = {}) {
|
|
1840
|
+
let stopped = false;
|
|
1841
|
+
const intervalMs = options.intervalMs ?? 6e4;
|
|
1842
|
+
const tick = () => {
|
|
1843
|
+
if (stopped) return;
|
|
1844
|
+
void (async () => {
|
|
1845
|
+
try {
|
|
1846
|
+
await markStaleEdges(graph, {
|
|
1847
|
+
thresholds: options.thresholds,
|
|
1848
|
+
staleEventsPath: options.staleEventsPath
|
|
1849
|
+
});
|
|
1850
|
+
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
console.error("staleness tick failed", err);
|
|
1853
|
+
}
|
|
1854
|
+
})();
|
|
1855
|
+
};
|
|
1856
|
+
const interval = setInterval(tick, intervalMs);
|
|
1857
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
1858
|
+
return () => {
|
|
1859
|
+
stopped = true;
|
|
1860
|
+
clearInterval(interval);
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
async function readErrorEvents(errorsPath) {
|
|
1864
|
+
try {
|
|
1865
|
+
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
1866
|
+
return raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
1867
|
+
} catch (err) {
|
|
1868
|
+
if (err.code === "ENOENT") return [];
|
|
1869
|
+
throw err;
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// src/extract/services.ts
|
|
1874
|
+
init_cjs_shims();
|
|
1875
|
+
var import_node_fs6 = require("fs");
|
|
1876
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1877
|
+
var import_ignore = __toESM(require("ignore"), 1);
|
|
1878
|
+
var import_minimatch = require("minimatch");
|
|
1879
|
+
var import_types5 = require("@neat.is/types");
|
|
1880
|
+
|
|
1881
|
+
// src/extract/shared.ts
|
|
1882
|
+
init_cjs_shims();
|
|
1883
|
+
var import_node_fs4 = require("fs");
|
|
1884
|
+
var import_node_path4 = __toESM(require("path"), 1);
|
|
1885
|
+
var import_yaml = require("yaml");
|
|
1886
|
+
var import_types4 = require("@neat.is/types");
|
|
1887
|
+
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py"]);
|
|
1888
|
+
var CONFIG_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml"]);
|
|
1889
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
1890
|
+
"node_modules",
|
|
1891
|
+
".git",
|
|
1892
|
+
".turbo",
|
|
1893
|
+
"dist",
|
|
1894
|
+
"build",
|
|
1895
|
+
".next"
|
|
1896
|
+
]);
|
|
1897
|
+
function isConfigFile(name) {
|
|
1898
|
+
const ext = import_node_path4.default.extname(name);
|
|
1899
|
+
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1900
|
+
if (name === ".env" || name.startsWith(".env.")) return { match: true, fileType: "env" };
|
|
1901
|
+
return { match: false, fileType: "" };
|
|
1902
|
+
}
|
|
1903
|
+
function cleanVersion(raw) {
|
|
1904
|
+
if (!raw) return void 0;
|
|
1905
|
+
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
1906
|
+
}
|
|
1907
|
+
async function readJson(filePath) {
|
|
1908
|
+
const raw = await import_node_fs4.promises.readFile(filePath, "utf8");
|
|
1909
|
+
return JSON.parse(raw);
|
|
1910
|
+
}
|
|
1911
|
+
async function readYaml(filePath) {
|
|
1912
|
+
const raw = await import_node_fs4.promises.readFile(filePath, "utf8");
|
|
1913
|
+
return (0, import_yaml.parse)(raw);
|
|
1914
|
+
}
|
|
1915
|
+
async function exists(p) {
|
|
1916
|
+
try {
|
|
1917
|
+
await import_node_fs4.promises.access(p);
|
|
1918
|
+
return true;
|
|
1919
|
+
} catch {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
// src/extract/python.ts
|
|
1925
|
+
init_cjs_shims();
|
|
1926
|
+
var import_node_fs5 = require("fs");
|
|
1927
|
+
var import_node_path5 = __toESM(require("path"), 1);
|
|
1928
|
+
var import_smol_toml = require("smol-toml");
|
|
1929
|
+
var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
|
|
1930
|
+
function parseRequirementsTxt(content) {
|
|
1931
|
+
const out = {};
|
|
1932
|
+
for (const rawLine of content.split("\n")) {
|
|
1933
|
+
const line = rawLine.split("#")[0]?.trim();
|
|
1934
|
+
if (!line) continue;
|
|
1935
|
+
if (line.startsWith("-")) continue;
|
|
1936
|
+
const match = REQUIREMENT_LINE.exec(line);
|
|
1937
|
+
if (!match) continue;
|
|
1938
|
+
const name = match[1].toLowerCase();
|
|
1939
|
+
const version = match[3] ?? "";
|
|
1940
|
+
out[name] = version;
|
|
1941
|
+
}
|
|
1942
|
+
return out;
|
|
1943
|
+
}
|
|
1944
|
+
function depsFromPyProject(pyproject) {
|
|
1945
|
+
const out = {};
|
|
1946
|
+
for (const entry of pyproject.project?.dependencies ?? []) {
|
|
1947
|
+
const match = REQUIREMENT_LINE.exec(entry);
|
|
1948
|
+
if (!match) continue;
|
|
1949
|
+
out[match[1].toLowerCase()] = match[3] ?? "";
|
|
1950
|
+
}
|
|
1951
|
+
const poetryDeps = pyproject.tool?.poetry?.dependencies ?? {};
|
|
1952
|
+
for (const [name, value] of Object.entries(poetryDeps)) {
|
|
1953
|
+
if (name.toLowerCase() === "python") continue;
|
|
1954
|
+
const raw = typeof value === "string" ? value : value?.version ?? "";
|
|
1955
|
+
out[name.toLowerCase()] = raw.replace(/^[\^~><=v\s]+/, "");
|
|
1956
|
+
}
|
|
1957
|
+
return out;
|
|
1958
|
+
}
|
|
1959
|
+
async function discoverPythonService(serviceDir) {
|
|
1960
|
+
const pyprojectPath = import_node_path5.default.join(serviceDir, "pyproject.toml");
|
|
1961
|
+
const requirementsPath = import_node_path5.default.join(serviceDir, "requirements.txt");
|
|
1962
|
+
const setupPath = import_node_path5.default.join(serviceDir, "setup.py");
|
|
1963
|
+
const hasPyproject = await exists(pyprojectPath);
|
|
1964
|
+
const hasRequirements = await exists(requirementsPath);
|
|
1965
|
+
const hasSetup = await exists(setupPath);
|
|
1966
|
+
if (!hasPyproject && !hasRequirements && !hasSetup) return null;
|
|
1967
|
+
let name = import_node_path5.default.basename(serviceDir);
|
|
1968
|
+
let version;
|
|
1969
|
+
const dependencies = {};
|
|
1970
|
+
if (hasPyproject) {
|
|
1971
|
+
const raw = await import_node_fs5.promises.readFile(pyprojectPath, "utf8");
|
|
1972
|
+
const pyproject = (0, import_smol_toml.parse)(raw);
|
|
1973
|
+
name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
|
|
1974
|
+
version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
|
|
1975
|
+
Object.assign(dependencies, depsFromPyProject(pyproject));
|
|
1976
|
+
}
|
|
1977
|
+
if (hasRequirements) {
|
|
1978
|
+
const raw = await import_node_fs5.promises.readFile(requirementsPath, "utf8");
|
|
1979
|
+
Object.assign(dependencies, parseRequirementsTxt(raw));
|
|
1980
|
+
}
|
|
1981
|
+
return { name, version, dependencies };
|
|
1982
|
+
}
|
|
1983
|
+
function pythonToPackage(service) {
|
|
1984
|
+
return {
|
|
1985
|
+
name: service.name,
|
|
1986
|
+
version: service.version,
|
|
1987
|
+
dependencies: service.dependencies
|
|
1988
|
+
};
|
|
1989
|
+
}
|
|
1990
|
+
|
|
1991
|
+
// src/extract/services.ts
|
|
1992
|
+
var DEFAULT_SCAN_DEPTH = 5;
|
|
1993
|
+
function parseScanDepth() {
|
|
1994
|
+
const raw = process.env.NEAT_SCAN_DEPTH;
|
|
1995
|
+
if (!raw) return DEFAULT_SCAN_DEPTH;
|
|
1996
|
+
const n = Number.parseInt(raw, 10);
|
|
1997
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_SCAN_DEPTH;
|
|
1998
|
+
}
|
|
1999
|
+
function workspaceGlobs(pkg) {
|
|
2000
|
+
const ws = pkg.workspaces;
|
|
2001
|
+
if (!ws) return null;
|
|
2002
|
+
if (Array.isArray(ws)) return ws.length > 0 ? ws : null;
|
|
2003
|
+
if (Array.isArray(ws.packages)) return ws.packages.length > 0 ? ws.packages : null;
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
2006
|
+
async function loadGitignore(scanPath) {
|
|
2007
|
+
const gitignorePath = import_node_path6.default.join(scanPath, ".gitignore");
|
|
2008
|
+
if (!await exists(gitignorePath)) return null;
|
|
2009
|
+
const raw = await import_node_fs6.promises.readFile(gitignorePath, "utf8");
|
|
2010
|
+
return (0, import_ignore.default)().add(raw);
|
|
2011
|
+
}
|
|
2012
|
+
async function walkDirs(start, scanPath, options, visit) {
|
|
2013
|
+
async function recurse(current, depth) {
|
|
2014
|
+
if (depth > options.maxDepth) return;
|
|
2015
|
+
const entries = await import_node_fs6.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2016
|
+
for (const entry of entries) {
|
|
2017
|
+
if (!entry.isDirectory()) continue;
|
|
2018
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2019
|
+
const child = import_node_path6.default.join(current, entry.name);
|
|
2020
|
+
if (options.ig) {
|
|
2021
|
+
const rel = import_node_path6.default.relative(scanPath, child).split(import_node_path6.default.sep).join("/");
|
|
2022
|
+
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
2023
|
+
}
|
|
2024
|
+
await visit(child);
|
|
2025
|
+
await recurse(child, depth + 1);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
await recurse(start, 0);
|
|
2029
|
+
}
|
|
2030
|
+
async function expandWorkspaceGlobs(scanPath, globs) {
|
|
2031
|
+
const found = /* @__PURE__ */ new Set();
|
|
2032
|
+
const scanDepth = parseScanDepth();
|
|
2033
|
+
for (const raw of globs) {
|
|
2034
|
+
const pattern = raw.replace(/^\.\//, "");
|
|
2035
|
+
if (!pattern.includes("*")) {
|
|
2036
|
+
const candidate = import_node_path6.default.join(scanPath, pattern);
|
|
2037
|
+
if (await exists(import_node_path6.default.join(candidate, "package.json"))) found.add(candidate);
|
|
2038
|
+
continue;
|
|
2039
|
+
}
|
|
2040
|
+
const segments = pattern.split("/");
|
|
2041
|
+
const staticSegments = [];
|
|
2042
|
+
for (const seg of segments) {
|
|
2043
|
+
if (seg.includes("*")) break;
|
|
2044
|
+
staticSegments.push(seg);
|
|
2045
|
+
}
|
|
2046
|
+
const start = import_node_path6.default.join(scanPath, ...staticSegments);
|
|
2047
|
+
if (!await exists(start)) continue;
|
|
2048
|
+
const hasDoubleStar = pattern.includes("**");
|
|
2049
|
+
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
2050
|
+
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
2051
|
+
const rel = import_node_path6.default.relative(scanPath, dir).split(import_node_path6.default.sep).join("/");
|
|
2052
|
+
if ((0, import_minimatch.minimatch)(rel, pattern) && await exists(import_node_path6.default.join(dir, "package.json"))) {
|
|
2053
|
+
found.add(dir);
|
|
2054
|
+
}
|
|
2055
|
+
});
|
|
2056
|
+
}
|
|
2057
|
+
return [...found];
|
|
2058
|
+
}
|
|
2059
|
+
async function discoverNodeService(scanPath, dir) {
|
|
2060
|
+
const pkgPath = import_node_path6.default.join(dir, "package.json");
|
|
2061
|
+
if (!await exists(pkgPath)) return null;
|
|
2062
|
+
const pkg = await readJson(pkgPath);
|
|
2063
|
+
if (!pkg.name) return null;
|
|
2064
|
+
const node = {
|
|
2065
|
+
id: (0, import_types5.serviceId)(pkg.name),
|
|
2066
|
+
type: import_types5.NodeType.ServiceNode,
|
|
2067
|
+
name: pkg.name,
|
|
2068
|
+
language: "javascript",
|
|
2069
|
+
version: pkg.version,
|
|
2070
|
+
dependencies: pkg.dependencies ?? {},
|
|
2071
|
+
repoPath: import_node_path6.default.relative(scanPath, dir),
|
|
2072
|
+
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
2073
|
+
};
|
|
2074
|
+
return { pkg, dir, node };
|
|
2075
|
+
}
|
|
2076
|
+
async function discoverPyService(scanPath, dir) {
|
|
2077
|
+
const py = await discoverPythonService(dir);
|
|
2078
|
+
if (!py) return null;
|
|
2079
|
+
const pkg = pythonToPackage(py);
|
|
2080
|
+
const node = {
|
|
2081
|
+
id: (0, import_types5.serviceId)(py.name),
|
|
2082
|
+
type: import_types5.NodeType.ServiceNode,
|
|
2083
|
+
name: py.name,
|
|
2084
|
+
language: "python",
|
|
2085
|
+
version: py.version,
|
|
2086
|
+
dependencies: py.dependencies,
|
|
2087
|
+
repoPath: import_node_path6.default.relative(scanPath, dir)
|
|
2088
|
+
};
|
|
2089
|
+
return { pkg, dir, node };
|
|
2090
|
+
}
|
|
2091
|
+
async function discoverServices(scanPath) {
|
|
2092
|
+
const rootPkgPath = import_node_path6.default.join(scanPath, "package.json");
|
|
2093
|
+
const rootPkg = await exists(rootPkgPath) ? await readJson(rootPkgPath) : null;
|
|
2094
|
+
const wsGlobs = rootPkg ? workspaceGlobs(rootPkg) : null;
|
|
2095
|
+
const candidateDirs = [];
|
|
2096
|
+
if (wsGlobs) {
|
|
2097
|
+
candidateDirs.push(...await expandWorkspaceGlobs(scanPath, wsGlobs));
|
|
2098
|
+
} else {
|
|
2099
|
+
if (rootPkg && rootPkg.name) candidateDirs.push(scanPath);
|
|
2100
|
+
const ig = await loadGitignore(scanPath);
|
|
2101
|
+
await walkDirs(
|
|
2102
|
+
scanPath,
|
|
2103
|
+
scanPath,
|
|
2104
|
+
{ maxDepth: parseScanDepth(), ig },
|
|
2105
|
+
async (dir) => {
|
|
2106
|
+
if (await exists(import_node_path6.default.join(dir, "package.json"))) {
|
|
2107
|
+
candidateDirs.push(dir);
|
|
2108
|
+
} else if (await exists(import_node_path6.default.join(dir, "pyproject.toml")) || await exists(import_node_path6.default.join(dir, "requirements.txt")) || await exists(import_node_path6.default.join(dir, "setup.py"))) {
|
|
2109
|
+
candidateDirs.push(dir);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
);
|
|
2113
|
+
}
|
|
2114
|
+
candidateDirs.sort();
|
|
2115
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2116
|
+
const out = [];
|
|
2117
|
+
for (const dir of candidateDirs) {
|
|
2118
|
+
const service = await discoverNodeService(scanPath, dir) ?? await discoverPyService(scanPath, dir);
|
|
2119
|
+
if (!service) continue;
|
|
2120
|
+
const existingDir = seen.get(service.node.name);
|
|
2121
|
+
if (existingDir !== void 0) {
|
|
2122
|
+
const a = import_node_path6.default.relative(scanPath, existingDir) || ".";
|
|
2123
|
+
const b = import_node_path6.default.relative(scanPath, dir) || ".";
|
|
2124
|
+
console.warn(
|
|
2125
|
+
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
2126
|
+
);
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
seen.set(service.node.name, dir);
|
|
2130
|
+
out.push(service);
|
|
2131
|
+
}
|
|
2132
|
+
return out;
|
|
2133
|
+
}
|
|
2134
|
+
function addServiceNodes(graph, services) {
|
|
2135
|
+
let nodesAdded = 0;
|
|
2136
|
+
for (const service of services) {
|
|
2137
|
+
if (!graph.hasNode(service.node.id)) {
|
|
2138
|
+
graph.addNode(service.node.id, { ...service.node, discoveredVia: "static" });
|
|
2139
|
+
nodesAdded++;
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
const existing = graph.getNodeAttributes(service.node.id);
|
|
2143
|
+
const mergedDiscoveredVia = existing.discoveredVia === "otel" ? "merged" : "static";
|
|
2144
|
+
graph.replaceNodeAttributes(service.node.id, {
|
|
2145
|
+
...existing,
|
|
2146
|
+
...service.node,
|
|
2147
|
+
discoveredVia: mergedDiscoveredVia
|
|
2148
|
+
});
|
|
2149
|
+
}
|
|
2150
|
+
return nodesAdded;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/extract/aliases.ts
|
|
2154
|
+
init_cjs_shims();
|
|
2155
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
2156
|
+
var import_node_fs7 = require("fs");
|
|
2157
|
+
var import_yaml2 = require("yaml");
|
|
2158
|
+
var import_types6 = require("@neat.is/types");
|
|
2159
|
+
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2160
|
+
"Service",
|
|
2161
|
+
"Deployment",
|
|
2162
|
+
"StatefulSet",
|
|
2163
|
+
"DaemonSet"
|
|
2164
|
+
]);
|
|
2165
|
+
function addAliases(graph, serviceId3, candidates) {
|
|
2166
|
+
if (!graph.hasNode(serviceId3)) return;
|
|
2167
|
+
const node = graph.getNodeAttributes(serviceId3);
|
|
2168
|
+
if (node.type !== import_types6.NodeType.ServiceNode) return;
|
|
2169
|
+
const set = new Set(node.aliases ?? []);
|
|
2170
|
+
for (const c of candidates) {
|
|
2171
|
+
if (!c) continue;
|
|
2172
|
+
if (c === node.name) continue;
|
|
2173
|
+
set.add(c);
|
|
2174
|
+
}
|
|
2175
|
+
if (set.size === 0) return;
|
|
2176
|
+
const updated = { ...node, aliases: [...set].sort() };
|
|
2177
|
+
graph.replaceNodeAttributes(serviceId3, updated);
|
|
2178
|
+
}
|
|
2179
|
+
function indexServicesByName(services) {
|
|
2180
|
+
const map = /* @__PURE__ */ new Map();
|
|
2181
|
+
for (const s of services) {
|
|
2182
|
+
map.set(s.node.name, s.node.id);
|
|
2183
|
+
map.set(import_node_path7.default.basename(s.dir), s.node.id);
|
|
2184
|
+
}
|
|
2185
|
+
return map;
|
|
2186
|
+
}
|
|
2187
|
+
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
2188
|
+
let composePath = null;
|
|
2189
|
+
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2190
|
+
const abs = import_node_path7.default.join(scanPath, name);
|
|
2191
|
+
if (await exists(abs)) {
|
|
2192
|
+
composePath = abs;
|
|
2193
|
+
break;
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
if (!composePath) return;
|
|
2197
|
+
const compose = await readYaml(composePath);
|
|
2198
|
+
if (!compose?.services) return;
|
|
2199
|
+
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
2200
|
+
const serviceId3 = serviceIndex.get(composeName);
|
|
2201
|
+
if (!serviceId3) continue;
|
|
2202
|
+
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
2203
|
+
if (svc.container_name) aliases.add(svc.container_name);
|
|
2204
|
+
if (svc.hostname) aliases.add(svc.hostname);
|
|
2205
|
+
addAliases(graph, serviceId3, aliases);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
2209
|
+
"service",
|
|
2210
|
+
"service.name",
|
|
2211
|
+
"app",
|
|
2212
|
+
"app.name",
|
|
2213
|
+
"com.docker.compose.service",
|
|
2214
|
+
"org.opencontainers.image.title"
|
|
2215
|
+
]);
|
|
2216
|
+
function parseDockerfileLabels(content) {
|
|
2217
|
+
const out = [];
|
|
2218
|
+
const lineRegex = /^\s*label\s+(.+)$/i;
|
|
2219
|
+
for (const raw of content.split("\n")) {
|
|
2220
|
+
const m = lineRegex.exec(raw);
|
|
2221
|
+
if (!m) continue;
|
|
2222
|
+
const rest = m[1];
|
|
2223
|
+
const pairRegex = /([\w.-]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s]+))/g;
|
|
2224
|
+
let pair;
|
|
2225
|
+
while ((pair = pairRegex.exec(rest)) !== null) {
|
|
2226
|
+
const key = pair[1].toLowerCase();
|
|
2227
|
+
if (!LABEL_KEYS.has(key)) continue;
|
|
2228
|
+
const value = pair[3] ?? pair[4] ?? pair[5] ?? "";
|
|
2229
|
+
if (value) out.push(value);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
return out;
|
|
2233
|
+
}
|
|
2234
|
+
async function collectDockerfileAliases(graph, services) {
|
|
2235
|
+
for (const service of services) {
|
|
2236
|
+
const dockerfilePath = import_node_path7.default.join(service.dir, "Dockerfile");
|
|
2237
|
+
if (!await exists(dockerfilePath)) continue;
|
|
2238
|
+
const content = await import_node_fs7.promises.readFile(dockerfilePath, "utf8");
|
|
2239
|
+
const aliases = parseDockerfileLabels(content);
|
|
2240
|
+
if (aliases.length > 0) addAliases(graph, service.node.id, aliases);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2244
|
+
if (depth > max) return [];
|
|
2245
|
+
const out = [];
|
|
2246
|
+
const entries = await import_node_fs7.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2247
|
+
for (const entry of entries) {
|
|
2248
|
+
if (entry.isDirectory()) {
|
|
2249
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2250
|
+
out.push(...await walkYamlFiles(import_node_path7.default.join(start, entry.name), depth + 1, max));
|
|
2251
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path7.default.extname(entry.name))) {
|
|
2252
|
+
out.push(import_node_path7.default.join(start, entry.name));
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
return out;
|
|
2256
|
+
}
|
|
2257
|
+
function k8sHostnames(name, namespace) {
|
|
2258
|
+
const ns = namespace ?? "default";
|
|
2259
|
+
return [
|
|
2260
|
+
name,
|
|
2261
|
+
`${name}.${ns}`,
|
|
2262
|
+
`${name}.${ns}.svc`,
|
|
2263
|
+
`${name}.${ns}.svc.cluster.local`
|
|
2264
|
+
];
|
|
2265
|
+
}
|
|
2266
|
+
function k8sServiceTarget(doc, byName) {
|
|
2267
|
+
const selector = doc.spec?.selector;
|
|
2268
|
+
const selectorApp = selector?.app ?? selector?.matchLabels?.app;
|
|
2269
|
+
if (selectorApp && byName.has(selectorApp)) return byName.get(selectorApp);
|
|
2270
|
+
const labelApp = doc.metadata?.labels?.app;
|
|
2271
|
+
if (labelApp && byName.has(labelApp)) return byName.get(labelApp);
|
|
2272
|
+
const metaName = doc.metadata?.name;
|
|
2273
|
+
if (metaName && byName.has(metaName)) return byName.get(metaName);
|
|
2274
|
+
return null;
|
|
2275
|
+
}
|
|
2276
|
+
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2277
|
+
const files = await walkYamlFiles(scanPath);
|
|
2278
|
+
for (const file of files) {
|
|
2279
|
+
const content = await import_node_fs7.promises.readFile(file, "utf8");
|
|
2280
|
+
let docs;
|
|
2281
|
+
try {
|
|
2282
|
+
docs = (0, import_yaml2.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
2283
|
+
} catch {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
for (const doc of docs) {
|
|
2287
|
+
if (!doc?.kind || !doc.metadata?.name) continue;
|
|
2288
|
+
if (!K8S_KINDS_WITH_HOSTNAMES.has(doc.kind)) continue;
|
|
2289
|
+
const target = k8sServiceTarget(doc, serviceIndex);
|
|
2290
|
+
if (!target) continue;
|
|
2291
|
+
addAliases(graph, target, k8sHostnames(doc.metadata.name, doc.metadata.namespace));
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
async function addServiceAliases(graph, scanPath, services) {
|
|
2296
|
+
const byName = indexServicesByName(services);
|
|
2297
|
+
await collectComposeAliases(graph, scanPath, byName);
|
|
2298
|
+
await collectDockerfileAliases(graph, services);
|
|
2299
|
+
await collectK8sAliases(graph, scanPath, byName);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// src/extract/databases/index.ts
|
|
2303
|
+
init_cjs_shims();
|
|
2304
|
+
var import_node_path15 = __toESM(require("path"), 1);
|
|
2305
|
+
var import_types7 = require("@neat.is/types");
|
|
2306
|
+
|
|
2307
|
+
// src/extract/databases/db-config-yaml.ts
|
|
2308
|
+
init_cjs_shims();
|
|
2309
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
2310
|
+
async function parse(serviceDir) {
|
|
2311
|
+
const yamlPath = import_node_path8.default.join(serviceDir, "db-config.yaml");
|
|
2312
|
+
if (!await exists(yamlPath)) return [];
|
|
2313
|
+
const raw = await readYaml(yamlPath);
|
|
2314
|
+
return [
|
|
2315
|
+
{
|
|
2316
|
+
host: raw.host,
|
|
2317
|
+
port: raw.port,
|
|
2318
|
+
database: raw.database,
|
|
2319
|
+
engine: raw.engine,
|
|
2320
|
+
engineVersion: raw.engineVersion !== void 0 ? String(raw.engineVersion) : "unknown",
|
|
2321
|
+
sourceFile: yamlPath
|
|
2322
|
+
}
|
|
2323
|
+
];
|
|
2324
|
+
}
|
|
2325
|
+
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
2326
|
+
|
|
2327
|
+
// src/extract/databases/dotenv.ts
|
|
2328
|
+
init_cjs_shims();
|
|
2329
|
+
var import_node_fs9 = require("fs");
|
|
2330
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
2331
|
+
|
|
2332
|
+
// src/extract/databases/shared.ts
|
|
2333
|
+
init_cjs_shims();
|
|
2334
|
+
var import_node_fs8 = require("fs");
|
|
2335
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
2336
|
+
function schemeToEngine(scheme) {
|
|
2337
|
+
const s = scheme.toLowerCase().split("+")[0];
|
|
2338
|
+
switch (s) {
|
|
2339
|
+
case "postgres":
|
|
2340
|
+
case "postgresql":
|
|
2341
|
+
return "postgresql";
|
|
2342
|
+
case "mysql":
|
|
2343
|
+
case "mariadb":
|
|
2344
|
+
return "mysql";
|
|
2345
|
+
case "mongodb":
|
|
2346
|
+
case "mongodb+srv":
|
|
2347
|
+
return "mongodb";
|
|
2348
|
+
case "redis":
|
|
2349
|
+
case "rediss":
|
|
2350
|
+
return "redis";
|
|
2351
|
+
case "sqlite":
|
|
2352
|
+
return "sqlite";
|
|
2353
|
+
default:
|
|
2354
|
+
return null;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
function parseConnectionString(url) {
|
|
2358
|
+
const m = url.match(
|
|
2359
|
+
/^(?<scheme>[a-z][a-z+]*):\/\/(?:[^@/]+(?::[^@]*)?@)?(?<host>[^:/?]+)(?::(?<port>\d+))?(?:\/(?<db>[^?#]*))?/i
|
|
2360
|
+
);
|
|
2361
|
+
if (!m || !m.groups) return null;
|
|
2362
|
+
const engine = schemeToEngine(m.groups.scheme);
|
|
2363
|
+
if (!engine) return null;
|
|
2364
|
+
return {
|
|
2365
|
+
host: m.groups.host,
|
|
2366
|
+
port: m.groups.port ? Number(m.groups.port) : void 0,
|
|
2367
|
+
database: m.groups.db ?? "",
|
|
2368
|
+
engine,
|
|
2369
|
+
engineVersion: "unknown"
|
|
2370
|
+
};
|
|
2371
|
+
}
|
|
2372
|
+
async function readIfExists(filePath) {
|
|
2373
|
+
try {
|
|
2374
|
+
return await import_node_fs8.promises.readFile(filePath, "utf8");
|
|
2375
|
+
} catch {
|
|
2376
|
+
return null;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
async function findFirst(serviceDir, candidates) {
|
|
2380
|
+
for (const rel of candidates) {
|
|
2381
|
+
const abs = import_node_path9.default.join(serviceDir, rel);
|
|
2382
|
+
const content = await readIfExists(abs);
|
|
2383
|
+
if (content !== null) return abs;
|
|
2384
|
+
}
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
function engineFromImage(image) {
|
|
2388
|
+
const lower = image.toLowerCase();
|
|
2389
|
+
const colon = lower.lastIndexOf(":");
|
|
2390
|
+
const repo = colon >= 0 ? lower.slice(0, colon) : lower;
|
|
2391
|
+
const tag = colon >= 0 ? lower.slice(colon + 1) : "latest";
|
|
2392
|
+
const last = repo.split("/").pop() ?? repo;
|
|
2393
|
+
let engine = null;
|
|
2394
|
+
if (last.startsWith("postgres")) engine = "postgresql";
|
|
2395
|
+
else if (last.startsWith("mysql") || last.startsWith("mariadb")) engine = "mysql";
|
|
2396
|
+
else if (last.startsWith("mongo")) engine = "mongodb";
|
|
2397
|
+
else if (last.startsWith("redis")) engine = "redis";
|
|
2398
|
+
else if (last.startsWith("sqlite")) engine = "sqlite";
|
|
2399
|
+
if (!engine) return null;
|
|
2400
|
+
const versionMatch = tag.match(/^(\d+(?:\.\d+){0,2})/);
|
|
2401
|
+
return {
|
|
2402
|
+
engine,
|
|
2403
|
+
engineVersion: versionMatch ? versionMatch[1] : "unknown"
|
|
2404
|
+
};
|
|
2405
|
+
}
|
|
2406
|
+
|
|
2407
|
+
// src/extract/databases/dotenv.ts
|
|
2408
|
+
var CONNECTION_KEYS = /* @__PURE__ */ new Set([
|
|
2409
|
+
"DATABASE_URL",
|
|
2410
|
+
"DB_URL",
|
|
2411
|
+
"POSTGRES_URL",
|
|
2412
|
+
"POSTGRESQL_URL",
|
|
2413
|
+
"MYSQL_URL",
|
|
2414
|
+
"MONGODB_URI",
|
|
2415
|
+
"MONGO_URL",
|
|
2416
|
+
"MONGO_URI",
|
|
2417
|
+
"REDIS_URL"
|
|
2418
|
+
]);
|
|
2419
|
+
function parseDotenvLine(line) {
|
|
2420
|
+
const trimmed = line.trim();
|
|
2421
|
+
if (!trimmed || trimmed.startsWith("#")) return null;
|
|
2422
|
+
const eq = trimmed.indexOf("=");
|
|
2423
|
+
if (eq < 0) return null;
|
|
2424
|
+
const key = trimmed.slice(0, eq).trim();
|
|
2425
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
2426
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
2427
|
+
value = value.slice(1, -1);
|
|
2428
|
+
}
|
|
2429
|
+
return { key, value };
|
|
2430
|
+
}
|
|
2431
|
+
async function parse2(serviceDir) {
|
|
2432
|
+
const entries = await import_node_fs9.promises.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2433
|
+
const configs = [];
|
|
2434
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2435
|
+
for (const entry of entries) {
|
|
2436
|
+
if (!entry.isFile()) continue;
|
|
2437
|
+
const match = isConfigFile(entry.name);
|
|
2438
|
+
if (!match.match || match.fileType !== "env") continue;
|
|
2439
|
+
const filePath = import_node_path10.default.join(serviceDir, entry.name);
|
|
2440
|
+
const content = await import_node_fs9.promises.readFile(filePath, "utf8");
|
|
2441
|
+
for (const line of content.split("\n")) {
|
|
2442
|
+
const parsed = parseDotenvLine(line);
|
|
2443
|
+
if (!parsed) continue;
|
|
2444
|
+
if (!CONNECTION_KEYS.has(parsed.key.toUpperCase())) continue;
|
|
2445
|
+
const config = parseConnectionString(parsed.value);
|
|
2446
|
+
if (!config) continue;
|
|
2447
|
+
const key = `${config.engine}://${config.host}:${config.port ?? ""}/${config.database}`;
|
|
2448
|
+
if (seen.has(key)) continue;
|
|
2449
|
+
seen.add(key);
|
|
2450
|
+
configs.push({ ...config, sourceFile: filePath });
|
|
2451
|
+
}
|
|
2452
|
+
}
|
|
2453
|
+
return configs;
|
|
2454
|
+
}
|
|
2455
|
+
var dotenvParser = { name: ".env", parse: parse2 };
|
|
2456
|
+
|
|
2457
|
+
// src/extract/databases/prisma.ts
|
|
2458
|
+
init_cjs_shims();
|
|
2459
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
2460
|
+
async function parse3(serviceDir) {
|
|
2461
|
+
const schemaPath = import_node_path11.default.join(serviceDir, "prisma", "schema.prisma");
|
|
2462
|
+
const content = await readIfExists(schemaPath);
|
|
2463
|
+
if (!content) return [];
|
|
2464
|
+
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
2465
|
+
if (!block) return [];
|
|
2466
|
+
const body = block[1] ?? "";
|
|
2467
|
+
const providerMatch = body.match(/provider\s*=\s*"([^"]+)"/);
|
|
2468
|
+
if (!providerMatch) return [];
|
|
2469
|
+
const engine = schemeToEngine(providerMatch[1]);
|
|
2470
|
+
if (!engine) return [];
|
|
2471
|
+
const urlMatch = body.match(/url\s*=\s*"([^"]+)"/);
|
|
2472
|
+
if (urlMatch) {
|
|
2473
|
+
const config = parseConnectionString(urlMatch[1]);
|
|
2474
|
+
if (config) return [{ ...config, sourceFile: schemaPath }];
|
|
2475
|
+
}
|
|
2476
|
+
return [
|
|
2477
|
+
{
|
|
2478
|
+
host: `${engine}-prisma`,
|
|
2479
|
+
database: "",
|
|
2480
|
+
engine,
|
|
2481
|
+
engineVersion: "unknown",
|
|
2482
|
+
sourceFile: schemaPath
|
|
2483
|
+
}
|
|
2484
|
+
];
|
|
2485
|
+
}
|
|
2486
|
+
var prismaParser = { name: "prisma", parse: parse3 };
|
|
2487
|
+
|
|
2488
|
+
// src/extract/databases/drizzle.ts
|
|
2489
|
+
init_cjs_shims();
|
|
2490
|
+
var DIALECT_TO_ENGINE = {
|
|
2491
|
+
postgresql: "postgresql",
|
|
2492
|
+
postgres: "postgresql",
|
|
2493
|
+
pg: "postgresql",
|
|
2494
|
+
mysql: "mysql",
|
|
2495
|
+
mysql2: "mysql",
|
|
2496
|
+
sqlite: "sqlite",
|
|
2497
|
+
"better-sqlite": "sqlite"
|
|
2498
|
+
};
|
|
2499
|
+
async function parse4(serviceDir) {
|
|
2500
|
+
const filePath = await findFirst(serviceDir, [
|
|
2501
|
+
"drizzle.config.ts",
|
|
2502
|
+
"drizzle.config.js",
|
|
2503
|
+
"drizzle.config.mjs"
|
|
2504
|
+
]);
|
|
2505
|
+
if (!filePath) return [];
|
|
2506
|
+
const content = await readIfExists(filePath);
|
|
2507
|
+
if (!content) return [];
|
|
2508
|
+
const dialectMatch = content.match(/dialect\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2509
|
+
if (!dialectMatch) return [];
|
|
2510
|
+
const engine = DIALECT_TO_ENGINE[dialectMatch[1].toLowerCase()] ?? schemeToEngine(dialectMatch[1]);
|
|
2511
|
+
if (!engine) return [];
|
|
2512
|
+
const urlMatch = content.match(
|
|
2513
|
+
/(?:url|connectionString)\s*:\s*['"`]([a-z][a-z+]*:\/\/[^'"`]+)['"`]/i
|
|
2514
|
+
);
|
|
2515
|
+
if (urlMatch) {
|
|
2516
|
+
const config = parseConnectionString(urlMatch[1]);
|
|
2517
|
+
if (config) return [{ ...config, sourceFile: filePath }];
|
|
2518
|
+
}
|
|
2519
|
+
const hostMatch = content.match(/host\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2520
|
+
if (hostMatch) {
|
|
2521
|
+
const portMatch = content.match(/port\s*:\s*(\d+)/);
|
|
2522
|
+
const dbMatch = content.match(/database\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2523
|
+
return [
|
|
2524
|
+
{
|
|
2525
|
+
host: hostMatch[1],
|
|
2526
|
+
port: portMatch ? Number(portMatch[1]) : void 0,
|
|
2527
|
+
database: dbMatch?.[1] ?? "",
|
|
2528
|
+
engine,
|
|
2529
|
+
engineVersion: "unknown",
|
|
2530
|
+
sourceFile: filePath
|
|
2531
|
+
}
|
|
2532
|
+
];
|
|
2533
|
+
}
|
|
2534
|
+
return [
|
|
2535
|
+
{ host: `${engine}-drizzle`, database: "", engine, engineVersion: "unknown", sourceFile: filePath }
|
|
2536
|
+
];
|
|
2537
|
+
}
|
|
2538
|
+
var drizzleParser = { name: "drizzle", parse: parse4 };
|
|
2539
|
+
|
|
2540
|
+
// src/extract/databases/knex.ts
|
|
2541
|
+
init_cjs_shims();
|
|
2542
|
+
var CLIENT_TO_ENGINE = {
|
|
2543
|
+
pg: "postgresql",
|
|
2544
|
+
postgres: "postgresql",
|
|
2545
|
+
postgresql: "postgresql",
|
|
2546
|
+
mysql: "mysql",
|
|
2547
|
+
mysql2: "mysql",
|
|
2548
|
+
sqlite3: "sqlite",
|
|
2549
|
+
"better-sqlite3": "sqlite"
|
|
2550
|
+
};
|
|
2551
|
+
async function parse5(serviceDir) {
|
|
2552
|
+
const filePath = await findFirst(serviceDir, [
|
|
2553
|
+
"knexfile.js",
|
|
2554
|
+
"knexfile.ts",
|
|
2555
|
+
"knexfile.cjs",
|
|
2556
|
+
"knexfile.mjs"
|
|
2557
|
+
]);
|
|
2558
|
+
if (!filePath) return [];
|
|
2559
|
+
const content = await readIfExists(filePath);
|
|
2560
|
+
if (!content) return [];
|
|
2561
|
+
const clientMatch = content.match(/client\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2562
|
+
if (!clientMatch) return [];
|
|
2563
|
+
const engine = CLIENT_TO_ENGINE[clientMatch[1].toLowerCase()];
|
|
2564
|
+
if (!engine) return [];
|
|
2565
|
+
const urlMatch = content.match(
|
|
2566
|
+
/connection\s*:\s*['"`]([a-z][a-z+]*:\/\/[^'"`]+)['"`]/i
|
|
2567
|
+
);
|
|
2568
|
+
if (urlMatch) {
|
|
2569
|
+
const config = parseConnectionString(urlMatch[1]);
|
|
2570
|
+
if (config) return [{ ...config, sourceFile: filePath }];
|
|
2571
|
+
}
|
|
2572
|
+
const host = content.match(/host\s*:\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
2573
|
+
if (host) {
|
|
2574
|
+
const port = content.match(/port\s*:\s*(\d+)/)?.[1];
|
|
2575
|
+
const database = content.match(/database\s*:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? "";
|
|
2576
|
+
return [
|
|
2577
|
+
{
|
|
2578
|
+
host,
|
|
2579
|
+
port: port ? Number(port) : void 0,
|
|
2580
|
+
database,
|
|
2581
|
+
engine,
|
|
2582
|
+
engineVersion: "unknown",
|
|
2583
|
+
sourceFile: filePath
|
|
2584
|
+
}
|
|
2585
|
+
];
|
|
2586
|
+
}
|
|
2587
|
+
return [{ host: `${engine}-knex`, database: "", engine, engineVersion: "unknown", sourceFile: filePath }];
|
|
2588
|
+
}
|
|
2589
|
+
var knexParser = { name: "knex", parse: parse5 };
|
|
2590
|
+
|
|
2591
|
+
// src/extract/databases/ormconfig.ts
|
|
2592
|
+
init_cjs_shims();
|
|
2593
|
+
var import_node_path12 = __toESM(require("path"), 1);
|
|
2594
|
+
async function parse6(serviceDir) {
|
|
2595
|
+
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2596
|
+
const abs = import_node_path12.default.join(serviceDir, candidate);
|
|
2597
|
+
if (!await exists(abs)) continue;
|
|
2598
|
+
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2599
|
+
const entries = Array.isArray(raw) ? raw : [raw];
|
|
2600
|
+
const out = [];
|
|
2601
|
+
for (const entry of entries) {
|
|
2602
|
+
if (!entry?.type || !entry.host) continue;
|
|
2603
|
+
const engine = schemeToEngine(entry.type);
|
|
2604
|
+
if (!engine) continue;
|
|
2605
|
+
out.push({
|
|
2606
|
+
host: entry.host,
|
|
2607
|
+
port: entry.port,
|
|
2608
|
+
database: entry.database ?? "",
|
|
2609
|
+
engine,
|
|
2610
|
+
engineVersion: "unknown",
|
|
2611
|
+
sourceFile: abs
|
|
2612
|
+
});
|
|
2613
|
+
}
|
|
2614
|
+
if (out.length > 0) return out;
|
|
2615
|
+
}
|
|
2616
|
+
return [];
|
|
2617
|
+
}
|
|
2618
|
+
var ormconfigParser = { name: "ormconfig", parse: parse6 };
|
|
2619
|
+
|
|
2620
|
+
// src/extract/databases/typeorm.ts
|
|
2621
|
+
init_cjs_shims();
|
|
2622
|
+
async function parse7(serviceDir) {
|
|
2623
|
+
const filePath = await findFirst(serviceDir, [
|
|
2624
|
+
"data-source.ts",
|
|
2625
|
+
"data-source.js",
|
|
2626
|
+
"src/data-source.ts",
|
|
2627
|
+
"src/data-source.js"
|
|
2628
|
+
]);
|
|
2629
|
+
if (!filePath) return [];
|
|
2630
|
+
const content = await readIfExists(filePath);
|
|
2631
|
+
if (!content) return [];
|
|
2632
|
+
const block = content.match(/new\s+DataSource\s*\(\s*\{([\s\S]*?)\}\s*\)/);
|
|
2633
|
+
const body = block ? block[1] : content;
|
|
2634
|
+
const typeMatch = body.match(/type\s*:\s*['"`]([^'"`]+)['"`]/);
|
|
2635
|
+
const host = body.match(/host\s*:\s*['"`]([^'"`]+)['"`]/)?.[1];
|
|
2636
|
+
if (!typeMatch || !host) return [];
|
|
2637
|
+
const engine = schemeToEngine(typeMatch[1]);
|
|
2638
|
+
if (!engine) return [];
|
|
2639
|
+
const port = body.match(/port\s*:\s*(\d+)/)?.[1];
|
|
2640
|
+
const database = body.match(/database\s*:\s*['"`]([^'"`]+)['"`]/)?.[1] ?? "";
|
|
2641
|
+
return [
|
|
2642
|
+
{
|
|
2643
|
+
host,
|
|
2644
|
+
port: port ? Number(port) : void 0,
|
|
2645
|
+
database,
|
|
2646
|
+
engine,
|
|
2647
|
+
engineVersion: "unknown",
|
|
2648
|
+
sourceFile: filePath
|
|
2649
|
+
}
|
|
2650
|
+
];
|
|
2651
|
+
}
|
|
2652
|
+
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
2653
|
+
|
|
2654
|
+
// src/extract/databases/sequelize.ts
|
|
2655
|
+
init_cjs_shims();
|
|
2656
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
2657
|
+
async function parse8(serviceDir) {
|
|
2658
|
+
const configPath = import_node_path13.default.join(serviceDir, "config", "config.json");
|
|
2659
|
+
if (!await exists(configPath)) return [];
|
|
2660
|
+
const raw = await readJson(configPath);
|
|
2661
|
+
const out = [];
|
|
2662
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2663
|
+
for (const entry of Object.values(raw)) {
|
|
2664
|
+
if (!entry?.dialect || !entry.host) continue;
|
|
2665
|
+
const engine = schemeToEngine(entry.dialect);
|
|
2666
|
+
if (!engine) continue;
|
|
2667
|
+
const key = `${engine}://${entry.host}:${entry.port ?? ""}/${entry.database ?? ""}`;
|
|
2668
|
+
if (seen.has(key)) continue;
|
|
2669
|
+
seen.add(key);
|
|
2670
|
+
out.push({
|
|
2671
|
+
host: entry.host,
|
|
2672
|
+
port: entry.port,
|
|
2673
|
+
database: entry.database ?? "",
|
|
2674
|
+
engine,
|
|
2675
|
+
engineVersion: "unknown",
|
|
2676
|
+
sourceFile: configPath
|
|
2677
|
+
});
|
|
2678
|
+
}
|
|
2679
|
+
return out;
|
|
2680
|
+
}
|
|
2681
|
+
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
2682
|
+
|
|
2683
|
+
// src/extract/databases/docker-compose.ts
|
|
2684
|
+
init_cjs_shims();
|
|
2685
|
+
var import_node_path14 = __toESM(require("path"), 1);
|
|
2686
|
+
function portFromService(svc) {
|
|
2687
|
+
for (const raw of svc.ports ?? []) {
|
|
2688
|
+
const str = String(raw);
|
|
2689
|
+
const last = str.split(":").pop();
|
|
2690
|
+
const n = Number(last);
|
|
2691
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
2692
|
+
}
|
|
2693
|
+
return void 0;
|
|
2694
|
+
}
|
|
2695
|
+
function databaseFromEnv(svc) {
|
|
2696
|
+
const env = svc.environment;
|
|
2697
|
+
const get = (key) => {
|
|
2698
|
+
if (!env) return void 0;
|
|
2699
|
+
if (Array.isArray(env)) {
|
|
2700
|
+
for (const line of env) {
|
|
2701
|
+
const [k, v] = line.split("=");
|
|
2702
|
+
if (k === key) return v;
|
|
2703
|
+
}
|
|
2704
|
+
return void 0;
|
|
2705
|
+
}
|
|
2706
|
+
return env[key];
|
|
2707
|
+
};
|
|
2708
|
+
return get("POSTGRES_DB") ?? get("MYSQL_DATABASE") ?? get("MONGO_INITDB_DATABASE") ?? "";
|
|
2709
|
+
}
|
|
2710
|
+
async function parse9(serviceDir) {
|
|
2711
|
+
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2712
|
+
const abs = import_node_path14.default.join(serviceDir, name);
|
|
2713
|
+
if (!await exists(abs)) continue;
|
|
2714
|
+
const raw = await readYaml(abs);
|
|
2715
|
+
if (!raw?.services) return [];
|
|
2716
|
+
const out = [];
|
|
2717
|
+
for (const [serviceName, svc] of Object.entries(raw.services)) {
|
|
2718
|
+
if (!svc.image) continue;
|
|
2719
|
+
const meta = engineFromImage(svc.image);
|
|
2720
|
+
if (!meta) continue;
|
|
2721
|
+
out.push({
|
|
2722
|
+
host: serviceName,
|
|
2723
|
+
port: portFromService(svc),
|
|
2724
|
+
database: databaseFromEnv(svc),
|
|
2725
|
+
engine: meta.engine,
|
|
2726
|
+
engineVersion: meta.engineVersion,
|
|
2727
|
+
sourceFile: abs
|
|
2728
|
+
});
|
|
2729
|
+
}
|
|
2730
|
+
return out;
|
|
2731
|
+
}
|
|
2732
|
+
return [];
|
|
2733
|
+
}
|
|
2734
|
+
var dockerComposeParser = { name: "docker-compose", parse: parse9 };
|
|
2735
|
+
|
|
2736
|
+
// src/extract/databases/index.ts
|
|
2737
|
+
var DB_PARSERS = [
|
|
2738
|
+
dbConfigYamlParser,
|
|
2739
|
+
dotenvParser,
|
|
2740
|
+
prismaParser,
|
|
2741
|
+
drizzleParser,
|
|
2742
|
+
knexParser,
|
|
2743
|
+
ormconfigParser,
|
|
2744
|
+
typeormParser,
|
|
2745
|
+
sequelizeParser,
|
|
2746
|
+
dockerComposeParser
|
|
2747
|
+
];
|
|
2748
|
+
function compatibleDriversFor(engine) {
|
|
2749
|
+
return compatPairs().filter((p) => p.engine === engine).map((p) => ({ name: p.driver, minVersion: p.minDriverVersion }));
|
|
2750
|
+
}
|
|
2751
|
+
function toDatabaseNode(config) {
|
|
2752
|
+
return {
|
|
2753
|
+
id: (0, import_types7.databaseId)(config.host),
|
|
2754
|
+
type: import_types7.NodeType.DatabaseNode,
|
|
2755
|
+
name: config.database || config.host,
|
|
2756
|
+
engine: config.engine,
|
|
2757
|
+
engineVersion: config.engineVersion,
|
|
2758
|
+
compatibleDrivers: compatibleDriversFor(config.engine),
|
|
2759
|
+
host: config.host,
|
|
2760
|
+
port: config.port
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
function attachIncompatibilities(service, configs) {
|
|
2764
|
+
const deps = { ...service.pkg.dependencies ?? {}, ...service.pkg.devDependencies ?? {} };
|
|
2765
|
+
const incompatibilities = [];
|
|
2766
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2767
|
+
for (const config of configs) {
|
|
2768
|
+
for (const pair of compatPairs()) {
|
|
2769
|
+
if (pair.engine !== config.engine) continue;
|
|
2770
|
+
const declaredVersion = cleanVersion(deps[pair.driver]);
|
|
2771
|
+
if (!declaredVersion) continue;
|
|
2772
|
+
const result = checkCompatibility(
|
|
2773
|
+
pair.driver,
|
|
2774
|
+
declaredVersion,
|
|
2775
|
+
config.engine,
|
|
2776
|
+
config.engineVersion
|
|
2777
|
+
);
|
|
2778
|
+
if (!result.compatible && result.reason) {
|
|
2779
|
+
const key = `driver-engine|${pair.driver}@${declaredVersion}|${config.engine}@${config.engineVersion}`;
|
|
2780
|
+
if (seen.has(key)) continue;
|
|
2781
|
+
seen.add(key);
|
|
2782
|
+
incompatibilities.push({
|
|
2783
|
+
kind: "driver-engine",
|
|
2784
|
+
driver: pair.driver,
|
|
2785
|
+
driverVersion: declaredVersion,
|
|
2786
|
+
engine: config.engine,
|
|
2787
|
+
engineVersion: config.engineVersion,
|
|
2788
|
+
reason: result.reason
|
|
2789
|
+
});
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
const serviceNodeEngine = service.node.nodeEngine ?? service.pkg.engines?.node;
|
|
2794
|
+
for (const constraint of nodeEngineConstraints()) {
|
|
2795
|
+
const declared = cleanVersion(deps[constraint.package]);
|
|
2796
|
+
if (!declared) continue;
|
|
2797
|
+
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
2798
|
+
if (!result.compatible && result.reason) {
|
|
2799
|
+
const key = `node-engine|${constraint.package}@${declared}|${serviceNodeEngine ?? ""}`;
|
|
2800
|
+
if (seen.has(key)) continue;
|
|
2801
|
+
seen.add(key);
|
|
2802
|
+
incompatibilities.push({
|
|
2803
|
+
kind: "node-engine",
|
|
2804
|
+
package: constraint.package,
|
|
2805
|
+
packageVersion: declared,
|
|
2806
|
+
requiredNodeVersion: result.requiredNodeVersion ?? constraint.minNodeVersion,
|
|
2807
|
+
...serviceNodeEngine ? { declaredNodeEngine: serviceNodeEngine } : {},
|
|
2808
|
+
reason: result.reason
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
for (const conflict of packageConflicts()) {
|
|
2813
|
+
const declared = cleanVersion(deps[conflict.package]);
|
|
2814
|
+
if (!declared) continue;
|
|
2815
|
+
const requiredVersion = cleanVersion(deps[conflict.requires.name]);
|
|
2816
|
+
const result = checkPackageConflict(conflict, declared, requiredVersion);
|
|
2817
|
+
if (!result.compatible && result.reason) {
|
|
2818
|
+
const key = `package-conflict|${conflict.package}@${declared}|${conflict.requires.name}@${requiredVersion ?? "missing"}`;
|
|
2819
|
+
if (seen.has(key)) continue;
|
|
2820
|
+
seen.add(key);
|
|
2821
|
+
incompatibilities.push({
|
|
2822
|
+
kind: "package-conflict",
|
|
2823
|
+
package: conflict.package,
|
|
2824
|
+
packageVersion: declared,
|
|
2825
|
+
requires: conflict.requires,
|
|
2826
|
+
...requiredVersion ? { foundVersion: requiredVersion } : {},
|
|
2827
|
+
reason: result.reason
|
|
2828
|
+
});
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
for (const rule of deprecatedApis()) {
|
|
2832
|
+
const declared = cleanVersion(deps[rule.package]);
|
|
2833
|
+
if (declared === void 0) continue;
|
|
2834
|
+
const result = checkDeprecatedApi(rule, declared);
|
|
2835
|
+
if (!result.compatible && result.reason) {
|
|
2836
|
+
const key = `deprecated-api|${rule.package}@${declared}`;
|
|
2837
|
+
if (seen.has(key)) continue;
|
|
2838
|
+
seen.add(key);
|
|
2839
|
+
incompatibilities.push({
|
|
2840
|
+
kind: "deprecated-api",
|
|
2841
|
+
package: rule.package,
|
|
2842
|
+
packageVersion: declared,
|
|
2843
|
+
reason: result.reason
|
|
2844
|
+
});
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
if (incompatibilities.length > 0) service.node.incompatibilities = incompatibilities;
|
|
2848
|
+
}
|
|
2849
|
+
async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
2850
|
+
let nodesAdded = 0;
|
|
2851
|
+
let edgesAdded = 0;
|
|
2852
|
+
for (const service of services) {
|
|
2853
|
+
const merged = /* @__PURE__ */ new Map();
|
|
2854
|
+
for (const parser of DB_PARSERS) {
|
|
2855
|
+
let configs;
|
|
2856
|
+
try {
|
|
2857
|
+
configs = await parser.parse(service.dir);
|
|
2858
|
+
} catch (err) {
|
|
2859
|
+
console.warn(
|
|
2860
|
+
`[neat] ${parser.name} parser failed on ${service.node.name}: ${err.message}`
|
|
2861
|
+
);
|
|
2862
|
+
continue;
|
|
2863
|
+
}
|
|
2864
|
+
for (const config of configs) {
|
|
2865
|
+
if (!config.host) continue;
|
|
2866
|
+
if (!merged.has(config.host)) merged.set(config.host, config);
|
|
2867
|
+
}
|
|
2868
|
+
}
|
|
2869
|
+
const allConfigs = [...merged.values()];
|
|
2870
|
+
for (const config of allConfigs) {
|
|
2871
|
+
const dbNode = toDatabaseNode(config);
|
|
2872
|
+
if (!graph.hasNode(dbNode.id)) {
|
|
2873
|
+
graph.addNode(dbNode.id, { ...dbNode, discoveredVia: "static" });
|
|
2874
|
+
nodesAdded++;
|
|
2875
|
+
} else {
|
|
2876
|
+
const existing = graph.getNodeAttributes(dbNode.id);
|
|
2877
|
+
const mergedDiscoveredVia = existing.discoveredVia === "otel" ? "merged" : "static";
|
|
2878
|
+
graph.replaceNodeAttributes(dbNode.id, {
|
|
2879
|
+
...existing,
|
|
2880
|
+
...dbNode,
|
|
2881
|
+
discoveredVia: mergedDiscoveredVia
|
|
2882
|
+
});
|
|
2883
|
+
}
|
|
2884
|
+
const edge = {
|
|
2885
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, dbNode.id, import_types7.EdgeType.CONNECTS_TO),
|
|
2886
|
+
source: service.node.id,
|
|
2887
|
+
target: dbNode.id,
|
|
2888
|
+
type: import_types7.EdgeType.CONNECTS_TO,
|
|
2889
|
+
provenance: import_types7.Provenance.EXTRACTED,
|
|
2890
|
+
...config.sourceFile ? {
|
|
2891
|
+
evidence: {
|
|
2892
|
+
file: import_node_path15.default.relative(scanPath, config.sourceFile).split(import_node_path15.default.sep).join("/")
|
|
2893
|
+
}
|
|
2894
|
+
} : {}
|
|
2895
|
+
};
|
|
2896
|
+
if (!graph.hasEdge(edge.id)) {
|
|
2897
|
+
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
2898
|
+
edgesAdded++;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
attachIncompatibilities(service, allConfigs);
|
|
2902
|
+
if (graph.hasNode(service.node.id)) {
|
|
2903
|
+
const current = graph.getNodeAttributes(service.node.id);
|
|
2904
|
+
const updated = {
|
|
2905
|
+
...current,
|
|
2906
|
+
...service.node,
|
|
2907
|
+
...current.aliases ? { aliases: current.aliases } : {}
|
|
2908
|
+
};
|
|
2909
|
+
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
2910
|
+
delete updated.incompatibilities;
|
|
2911
|
+
}
|
|
2912
|
+
graph.replaceNodeAttributes(service.node.id, updated);
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
return { nodesAdded, edgesAdded };
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
// src/extract/configs.ts
|
|
2919
|
+
init_cjs_shims();
|
|
2920
|
+
var import_node_fs10 = require("fs");
|
|
2921
|
+
var import_node_path16 = __toESM(require("path"), 1);
|
|
2922
|
+
var import_types8 = require("@neat.is/types");
|
|
2923
|
+
async function walkConfigFiles(dir) {
|
|
2924
|
+
const out = [];
|
|
2925
|
+
async function walk(current) {
|
|
2926
|
+
const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true });
|
|
2927
|
+
for (const entry of entries) {
|
|
2928
|
+
const full = import_node_path16.default.join(current, entry.name);
|
|
2929
|
+
if (entry.isDirectory()) {
|
|
2930
|
+
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2931
|
+
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
2932
|
+
out.push(full);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
await walk(dir);
|
|
2937
|
+
return out;
|
|
2938
|
+
}
|
|
2939
|
+
async function addConfigNodes(graph, services, scanPath) {
|
|
2940
|
+
let nodesAdded = 0;
|
|
2941
|
+
let edgesAdded = 0;
|
|
2942
|
+
for (const service of services) {
|
|
2943
|
+
const configFiles = await walkConfigFiles(service.dir);
|
|
2944
|
+
for (const file of configFiles) {
|
|
2945
|
+
const relPath = import_node_path16.default.relative(scanPath, file);
|
|
2946
|
+
const node = {
|
|
2947
|
+
id: (0, import_types8.configId)(relPath),
|
|
2948
|
+
type: import_types8.NodeType.ConfigNode,
|
|
2949
|
+
name: import_node_path16.default.basename(file),
|
|
2950
|
+
path: relPath,
|
|
2951
|
+
fileType: isConfigFile(import_node_path16.default.basename(file)).fileType
|
|
2952
|
+
};
|
|
2953
|
+
if (!graph.hasNode(node.id)) {
|
|
2954
|
+
graph.addNode(node.id, node);
|
|
2955
|
+
nodesAdded++;
|
|
2956
|
+
}
|
|
2957
|
+
const edge = {
|
|
2958
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types8.EdgeType.CONFIGURED_BY),
|
|
2959
|
+
source: service.node.id,
|
|
2960
|
+
target: node.id,
|
|
2961
|
+
type: import_types8.EdgeType.CONFIGURED_BY,
|
|
2962
|
+
provenance: import_types8.Provenance.EXTRACTED,
|
|
2963
|
+
evidence: { file: relPath.split(import_node_path16.default.sep).join("/") }
|
|
2964
|
+
};
|
|
2965
|
+
if (!graph.hasEdge(edge.id)) {
|
|
2966
|
+
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
2967
|
+
edgesAdded++;
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
return { nodesAdded, edgesAdded };
|
|
2972
|
+
}
|
|
2973
|
+
|
|
2974
|
+
// src/extract/calls/index.ts
|
|
2975
|
+
init_cjs_shims();
|
|
2976
|
+
var import_types14 = require("@neat.is/types");
|
|
2977
|
+
|
|
2978
|
+
// src/extract/calls/http.ts
|
|
2979
|
+
init_cjs_shims();
|
|
2980
|
+
var import_node_path18 = __toESM(require("path"), 1);
|
|
2981
|
+
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
2982
|
+
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
2983
|
+
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
2984
|
+
var import_types9 = require("@neat.is/types");
|
|
2985
|
+
|
|
2986
|
+
// src/extract/calls/shared.ts
|
|
2987
|
+
init_cjs_shims();
|
|
2988
|
+
var import_node_fs11 = require("fs");
|
|
2989
|
+
var import_node_path17 = __toESM(require("path"), 1);
|
|
2990
|
+
async function walkSourceFiles(dir) {
|
|
2991
|
+
const out = [];
|
|
2992
|
+
async function walk(current) {
|
|
2993
|
+
const entries = await import_node_fs11.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2994
|
+
for (const entry of entries) {
|
|
2995
|
+
const full = import_node_path17.default.join(current, entry.name);
|
|
2996
|
+
if (entry.isDirectory()) {
|
|
2997
|
+
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2998
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path17.default.extname(entry.name))) {
|
|
2999
|
+
out.push(full);
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
}
|
|
3003
|
+
await walk(dir);
|
|
3004
|
+
return out;
|
|
3005
|
+
}
|
|
3006
|
+
async function loadSourceFiles(dir) {
|
|
3007
|
+
const paths = await walkSourceFiles(dir);
|
|
3008
|
+
const out = [];
|
|
3009
|
+
for (const p of paths) {
|
|
3010
|
+
try {
|
|
3011
|
+
const content = await import_node_fs11.promises.readFile(p, "utf8");
|
|
3012
|
+
out.push({ path: p, content });
|
|
3013
|
+
} catch {
|
|
3014
|
+
}
|
|
3015
|
+
}
|
|
3016
|
+
return out;
|
|
3017
|
+
}
|
|
3018
|
+
function lineOf(text, needle) {
|
|
3019
|
+
const idx = text.indexOf(needle);
|
|
3020
|
+
if (idx < 0) return 1;
|
|
3021
|
+
return text.slice(0, idx).split("\n").length;
|
|
3022
|
+
}
|
|
3023
|
+
function snippet(text, line) {
|
|
3024
|
+
const lines = text.split("\n");
|
|
3025
|
+
return (lines[line - 1] ?? "").trim();
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
// src/extract/calls/http.ts
|
|
3029
|
+
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3030
|
+
function collectStringLiterals(node, out) {
|
|
3031
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3032
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3033
|
+
const child = node.namedChild(i);
|
|
3034
|
+
if (child) collectStringLiterals(child, out);
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
function callsFromSource(source, parser, knownHosts) {
|
|
3038
|
+
const tree = parser.parse(source);
|
|
3039
|
+
const literals = [];
|
|
3040
|
+
collectStringLiterals(tree.rootNode, literals);
|
|
3041
|
+
const targets = /* @__PURE__ */ new Set();
|
|
3042
|
+
for (const lit of literals) {
|
|
3043
|
+
for (const host of knownHosts) {
|
|
3044
|
+
if (lit.includes(`//${host}`) || lit.includes(`//${host}:`)) {
|
|
3045
|
+
targets.add(host);
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
return targets;
|
|
3050
|
+
}
|
|
3051
|
+
function makeJsParser() {
|
|
3052
|
+
const p = new import_tree_sitter.default();
|
|
3053
|
+
p.setLanguage(import_tree_sitter_javascript.default);
|
|
3054
|
+
return p;
|
|
3055
|
+
}
|
|
3056
|
+
function makePyParser() {
|
|
3057
|
+
const p = new import_tree_sitter.default();
|
|
3058
|
+
p.setLanguage(import_tree_sitter_python.default);
|
|
3059
|
+
return p;
|
|
3060
|
+
}
|
|
3061
|
+
async function addHttpCallEdges(graph, services) {
|
|
3062
|
+
const jsParser = makeJsParser();
|
|
3063
|
+
const pyParser = makePyParser();
|
|
3064
|
+
const knownHosts = /* @__PURE__ */ new Set();
|
|
3065
|
+
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3066
|
+
for (const service of services) {
|
|
3067
|
+
knownHosts.add(import_node_path18.default.basename(service.dir));
|
|
3068
|
+
knownHosts.add(service.pkg.name);
|
|
3069
|
+
hostToNodeId.set(import_node_path18.default.basename(service.dir), service.node.id);
|
|
3070
|
+
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3071
|
+
}
|
|
3072
|
+
let edgesAdded = 0;
|
|
3073
|
+
for (const service of services) {
|
|
3074
|
+
const files = await loadSourceFiles(service.dir);
|
|
3075
|
+
const seenTargets = /* @__PURE__ */ new Map();
|
|
3076
|
+
for (const file of files) {
|
|
3077
|
+
const parser = import_node_path18.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3078
|
+
const targets = callsFromSource(file.content, parser, knownHosts);
|
|
3079
|
+
for (const t of targets) {
|
|
3080
|
+
const targetId = hostToNodeId.get(t);
|
|
3081
|
+
if (!targetId || targetId === service.node.id) continue;
|
|
3082
|
+
if (!seenTargets.has(targetId)) {
|
|
3083
|
+
seenTargets.set(targetId, { file: file.path, host: t });
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3088
|
+
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3089
|
+
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3090
|
+
const edge = {
|
|
3091
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS),
|
|
3092
|
+
source: service.node.id,
|
|
3093
|
+
target: targetId,
|
|
3094
|
+
type: import_types9.EdgeType.CALLS,
|
|
3095
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
3096
|
+
evidence: {
|
|
3097
|
+
file: import_node_path18.default.relative(service.dir, evidenceFile.file),
|
|
3098
|
+
line,
|
|
3099
|
+
snippet: snippet(fileContent, line)
|
|
3100
|
+
}
|
|
3101
|
+
};
|
|
3102
|
+
if (!graph.hasEdge(edge.id)) {
|
|
3103
|
+
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
3104
|
+
edgesAdded++;
|
|
3105
|
+
}
|
|
3106
|
+
}
|
|
3107
|
+
}
|
|
3108
|
+
return edgesAdded;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
// src/extract/calls/kafka.ts
|
|
3112
|
+
init_cjs_shims();
|
|
3113
|
+
var import_node_path19 = __toESM(require("path"), 1);
|
|
3114
|
+
var import_types10 = require("@neat.is/types");
|
|
3115
|
+
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3116
|
+
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
3117
|
+
function findAll(re, text) {
|
|
3118
|
+
re.lastIndex = 0;
|
|
3119
|
+
const out = [];
|
|
3120
|
+
let m;
|
|
3121
|
+
while ((m = re.exec(text)) !== null) {
|
|
3122
|
+
out.push({ topic: m[1], index: m.index });
|
|
3123
|
+
}
|
|
3124
|
+
return out;
|
|
3125
|
+
}
|
|
3126
|
+
function kafkaEndpointsFromFile(file, serviceDir) {
|
|
3127
|
+
const out = [];
|
|
3128
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3129
|
+
const make = (topic, edgeType) => {
|
|
3130
|
+
const key = `${edgeType}|${topic}`;
|
|
3131
|
+
if (seen.has(key)) return;
|
|
3132
|
+
seen.add(key);
|
|
3133
|
+
const line = lineOf(file.content, topic);
|
|
3134
|
+
out.push({
|
|
3135
|
+
infraId: (0, import_types10.infraId)("kafka-topic", topic),
|
|
3136
|
+
name: topic,
|
|
3137
|
+
kind: "kafka-topic",
|
|
3138
|
+
edgeType,
|
|
3139
|
+
evidence: {
|
|
3140
|
+
file: import_node_path19.default.relative(serviceDir, file.path),
|
|
3141
|
+
line,
|
|
3142
|
+
snippet: snippet(file.content, line)
|
|
3143
|
+
}
|
|
3144
|
+
});
|
|
3145
|
+
};
|
|
3146
|
+
for (const { topic } of findAll(PRODUCER_TOPIC_RE, file.content)) make(topic, "PUBLISHES_TO");
|
|
3147
|
+
for (const { topic } of findAll(CONSUMER_TOPIC_RE, file.content)) make(topic, "CONSUMES_FROM");
|
|
3148
|
+
return out;
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// src/extract/calls/redis.ts
|
|
3152
|
+
init_cjs_shims();
|
|
3153
|
+
var import_node_path20 = __toESM(require("path"), 1);
|
|
3154
|
+
var import_types11 = require("@neat.is/types");
|
|
3155
|
+
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3156
|
+
function redisEndpointsFromFile(file, serviceDir) {
|
|
3157
|
+
const out = [];
|
|
3158
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3159
|
+
REDIS_URL_RE.lastIndex = 0;
|
|
3160
|
+
let m;
|
|
3161
|
+
while ((m = REDIS_URL_RE.exec(file.content)) !== null) {
|
|
3162
|
+
const host = m[1];
|
|
3163
|
+
if (seen.has(host)) continue;
|
|
3164
|
+
seen.add(host);
|
|
3165
|
+
const line = lineOf(file.content, host);
|
|
3166
|
+
out.push({
|
|
3167
|
+
infraId: (0, import_types11.infraId)("redis", host),
|
|
3168
|
+
name: host,
|
|
3169
|
+
kind: "redis",
|
|
3170
|
+
edgeType: "CALLS",
|
|
3171
|
+
evidence: {
|
|
3172
|
+
file: import_node_path20.default.relative(serviceDir, file.path),
|
|
3173
|
+
line,
|
|
3174
|
+
snippet: snippet(file.content, line)
|
|
3175
|
+
}
|
|
3176
|
+
});
|
|
3177
|
+
}
|
|
3178
|
+
return out;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
// src/extract/calls/aws.ts
|
|
3182
|
+
init_cjs_shims();
|
|
3183
|
+
var import_node_path21 = __toESM(require("path"), 1);
|
|
3184
|
+
var import_types12 = require("@neat.is/types");
|
|
3185
|
+
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3186
|
+
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3187
|
+
function hasMarker(text, markers) {
|
|
3188
|
+
return markers.some((m) => text.includes(m));
|
|
3189
|
+
}
|
|
3190
|
+
function findAll2(re, text) {
|
|
3191
|
+
re.lastIndex = 0;
|
|
3192
|
+
const out = [];
|
|
3193
|
+
let m;
|
|
3194
|
+
while ((m = re.exec(text)) !== null) {
|
|
3195
|
+
out.push({ name: m[1], index: m.index });
|
|
3196
|
+
}
|
|
3197
|
+
return out;
|
|
3198
|
+
}
|
|
3199
|
+
function awsEndpointsFromFile(file, serviceDir) {
|
|
3200
|
+
const out = [];
|
|
3201
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3202
|
+
const make = (kind, name) => {
|
|
3203
|
+
const key = `${kind}|${name}`;
|
|
3204
|
+
if (seen.has(key)) return;
|
|
3205
|
+
seen.add(key);
|
|
3206
|
+
const line = lineOf(file.content, name);
|
|
3207
|
+
out.push({
|
|
3208
|
+
infraId: (0, import_types12.infraId)(kind, name),
|
|
3209
|
+
name,
|
|
3210
|
+
kind,
|
|
3211
|
+
edgeType: "CALLS",
|
|
3212
|
+
evidence: {
|
|
3213
|
+
file: import_node_path21.default.relative(serviceDir, file.path),
|
|
3214
|
+
line,
|
|
3215
|
+
snippet: snippet(file.content, line)
|
|
3216
|
+
}
|
|
3217
|
+
});
|
|
3218
|
+
};
|
|
3219
|
+
if (hasMarker(file.content, ["S3Client", "PutObjectCommand", "GetObjectCommand", "DeleteObjectCommand"])) {
|
|
3220
|
+
for (const { name } of findAll2(S3_BUCKET_RE, file.content)) make("s3-bucket", name);
|
|
3221
|
+
}
|
|
3222
|
+
if (hasMarker(file.content, [
|
|
3223
|
+
"DynamoDBClient",
|
|
3224
|
+
"DynamoDBDocumentClient",
|
|
3225
|
+
"GetCommand",
|
|
3226
|
+
"PutCommand",
|
|
3227
|
+
"QueryCommand",
|
|
3228
|
+
"UpdateCommand",
|
|
3229
|
+
"DeleteCommand"
|
|
3230
|
+
])) {
|
|
3231
|
+
for (const { name } of findAll2(DYNAMO_TABLE_RE, file.content)) make("dynamodb-table", name);
|
|
3232
|
+
}
|
|
3233
|
+
return out;
|
|
3234
|
+
}
|
|
3235
|
+
|
|
3236
|
+
// src/extract/calls/grpc.ts
|
|
3237
|
+
init_cjs_shims();
|
|
3238
|
+
var import_node_path22 = __toESM(require("path"), 1);
|
|
3239
|
+
var import_types13 = require("@neat.is/types");
|
|
3240
|
+
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3241
|
+
function isLikelyAddress(value) {
|
|
3242
|
+
if (!value) return false;
|
|
3243
|
+
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3244
|
+
}
|
|
3245
|
+
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3246
|
+
const out = [];
|
|
3247
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3248
|
+
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3249
|
+
let m;
|
|
3250
|
+
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
3251
|
+
const symbol = m[1];
|
|
3252
|
+
const addr = m[2]?.trim();
|
|
3253
|
+
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3254
|
+
if (seen.has(name)) continue;
|
|
3255
|
+
seen.add(name);
|
|
3256
|
+
const line = lineOf(file.content, m[0]);
|
|
3257
|
+
out.push({
|
|
3258
|
+
infraId: (0, import_types13.infraId)("grpc-service", name),
|
|
3259
|
+
name,
|
|
3260
|
+
kind: "grpc-service",
|
|
3261
|
+
edgeType: "CALLS",
|
|
3262
|
+
evidence: {
|
|
3263
|
+
file: import_node_path22.default.relative(serviceDir, file.path),
|
|
3264
|
+
line,
|
|
3265
|
+
snippet: snippet(file.content, line)
|
|
3266
|
+
}
|
|
3267
|
+
});
|
|
3268
|
+
}
|
|
3269
|
+
return out;
|
|
3270
|
+
}
|
|
3271
|
+
|
|
3272
|
+
// src/extract/calls/index.ts
|
|
3273
|
+
function edgeTypeFromEndpoint(ep) {
|
|
3274
|
+
switch (ep.edgeType) {
|
|
3275
|
+
case "PUBLISHES_TO":
|
|
3276
|
+
return import_types14.EdgeType.PUBLISHES_TO;
|
|
3277
|
+
case "CONSUMES_FROM":
|
|
3278
|
+
return import_types14.EdgeType.CONSUMES_FROM;
|
|
3279
|
+
default:
|
|
3280
|
+
return import_types14.EdgeType.CALLS;
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
async function addExternalEndpointEdges(graph, services) {
|
|
3284
|
+
let nodesAdded = 0;
|
|
3285
|
+
let edgesAdded = 0;
|
|
3286
|
+
for (const service of services) {
|
|
3287
|
+
const files = await loadSourceFiles(service.dir);
|
|
3288
|
+
const endpoints = [];
|
|
3289
|
+
for (const file of files) {
|
|
3290
|
+
endpoints.push(...kafkaEndpointsFromFile(file, service.dir));
|
|
3291
|
+
endpoints.push(...redisEndpointsFromFile(file, service.dir));
|
|
3292
|
+
endpoints.push(...awsEndpointsFromFile(file, service.dir));
|
|
3293
|
+
endpoints.push(...grpcEndpointsFromFile(file, service.dir));
|
|
3294
|
+
}
|
|
3295
|
+
if (endpoints.length === 0) continue;
|
|
3296
|
+
const seenEdges = /* @__PURE__ */ new Set();
|
|
3297
|
+
for (const ep of endpoints) {
|
|
3298
|
+
if (!graph.hasNode(ep.infraId)) {
|
|
3299
|
+
const node = {
|
|
3300
|
+
id: ep.infraId,
|
|
3301
|
+
type: import_types14.NodeType.InfraNode,
|
|
3302
|
+
name: ep.name,
|
|
3303
|
+
provider: ep.kind.startsWith("s3") || ep.kind.startsWith("dynamodb") ? "aws" : "self",
|
|
3304
|
+
kind: ep.kind
|
|
3305
|
+
};
|
|
3306
|
+
graph.addNode(node.id, node);
|
|
3307
|
+
nodesAdded++;
|
|
3308
|
+
}
|
|
3309
|
+
const edgeType = edgeTypeFromEndpoint(ep);
|
|
3310
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, ep.infraId, edgeType);
|
|
3311
|
+
if (seenEdges.has(edgeId)) continue;
|
|
3312
|
+
seenEdges.add(edgeId);
|
|
3313
|
+
if (!graph.hasEdge(edgeId)) {
|
|
3314
|
+
const edge = {
|
|
3315
|
+
id: edgeId,
|
|
3316
|
+
source: service.node.id,
|
|
3317
|
+
target: ep.infraId,
|
|
3318
|
+
type: edgeType,
|
|
3319
|
+
provenance: import_types14.Provenance.EXTRACTED,
|
|
3320
|
+
evidence: ep.evidence
|
|
3321
|
+
};
|
|
3322
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
3323
|
+
edgesAdded++;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
}
|
|
3327
|
+
return { nodesAdded, edgesAdded };
|
|
3328
|
+
}
|
|
3329
|
+
async function addCallEdges(graph, services) {
|
|
3330
|
+
const httpEdges = await addHttpCallEdges(graph, services);
|
|
3331
|
+
const ext = await addExternalEndpointEdges(graph, services);
|
|
3332
|
+
return {
|
|
3333
|
+
nodesAdded: ext.nodesAdded,
|
|
3334
|
+
edgesAdded: httpEdges + ext.edgesAdded
|
|
3335
|
+
};
|
|
3336
|
+
}
|
|
3337
|
+
|
|
3338
|
+
// src/extract/infra/index.ts
|
|
3339
|
+
init_cjs_shims();
|
|
3340
|
+
|
|
3341
|
+
// src/extract/infra/docker-compose.ts
|
|
3342
|
+
init_cjs_shims();
|
|
3343
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
3344
|
+
var import_types16 = require("@neat.is/types");
|
|
3345
|
+
|
|
3346
|
+
// src/extract/infra/shared.ts
|
|
3347
|
+
init_cjs_shims();
|
|
3348
|
+
var import_types15 = require("@neat.is/types");
|
|
3349
|
+
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
3350
|
+
return {
|
|
3351
|
+
id: (0, import_types15.infraId)(kind, name),
|
|
3352
|
+
type: import_types15.NodeType.InfraNode,
|
|
3353
|
+
name,
|
|
3354
|
+
provider,
|
|
3355
|
+
kind,
|
|
3356
|
+
...extras?.region ? { region: extras.region } : {}
|
|
3357
|
+
};
|
|
3358
|
+
}
|
|
3359
|
+
function classifyImage(image) {
|
|
3360
|
+
const lower = image.toLowerCase();
|
|
3361
|
+
const repo = lower.split(":")[0];
|
|
3362
|
+
const last = repo.split("/").pop() ?? repo;
|
|
3363
|
+
if (last.startsWith("postgres")) return "postgres";
|
|
3364
|
+
if (last.startsWith("mysql") || last.startsWith("mariadb")) return "mysql";
|
|
3365
|
+
if (last.startsWith("mongo")) return "mongodb";
|
|
3366
|
+
if (last.startsWith("redis")) return "redis";
|
|
3367
|
+
if (last.startsWith("rabbitmq")) return "rabbitmq";
|
|
3368
|
+
if (last.startsWith("kafka") || last.includes("kafka")) return "kafka";
|
|
3369
|
+
if (last.startsWith("memcached")) return "memcached";
|
|
3370
|
+
return "container";
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
// src/extract/infra/docker-compose.ts
|
|
3374
|
+
function dependsOnList(value) {
|
|
3375
|
+
if (!value) return [];
|
|
3376
|
+
if (Array.isArray(value)) return value;
|
|
3377
|
+
return Object.keys(value);
|
|
3378
|
+
}
|
|
3379
|
+
function serviceNameToServiceNode(name, services) {
|
|
3380
|
+
for (const s of services) {
|
|
3381
|
+
if (s.node.name === name || import_node_path23.default.basename(s.dir) === name) return s.node.id;
|
|
3382
|
+
}
|
|
3383
|
+
return null;
|
|
3384
|
+
}
|
|
3385
|
+
async function addComposeInfra(graph, scanPath, services) {
|
|
3386
|
+
let nodesAdded = 0;
|
|
3387
|
+
let edgesAdded = 0;
|
|
3388
|
+
let composePath = null;
|
|
3389
|
+
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3390
|
+
const abs = import_node_path23.default.join(scanPath, name);
|
|
3391
|
+
if (await exists(abs)) {
|
|
3392
|
+
composePath = abs;
|
|
3393
|
+
break;
|
|
3394
|
+
}
|
|
3395
|
+
}
|
|
3396
|
+
if (!composePath) return { nodesAdded, edgesAdded };
|
|
3397
|
+
const compose = await readYaml(composePath);
|
|
3398
|
+
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3399
|
+
const evidenceFile = import_node_path23.default.relative(scanPath, composePath).split(import_node_path23.default.sep).join("/");
|
|
3400
|
+
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3401
|
+
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3402
|
+
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
3403
|
+
if (matchedServiceId) {
|
|
3404
|
+
composeNameToNodeId.set(composeName, matchedServiceId);
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3407
|
+
const kind = svc.image ? classifyImage(svc.image) : "container";
|
|
3408
|
+
const node = makeInfraNode(kind, composeName);
|
|
3409
|
+
if (!graph.hasNode(node.id)) {
|
|
3410
|
+
graph.addNode(node.id, node);
|
|
3411
|
+
nodesAdded++;
|
|
3412
|
+
}
|
|
3413
|
+
composeNameToNodeId.set(composeName, node.id);
|
|
3414
|
+
}
|
|
3415
|
+
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3416
|
+
const sourceId = composeNameToNodeId.get(composeName);
|
|
3417
|
+
if (!sourceId) continue;
|
|
3418
|
+
for (const dep of dependsOnList(svc.depends_on)) {
|
|
3419
|
+
const targetId = composeNameToNodeId.get(dep);
|
|
3420
|
+
if (!targetId) continue;
|
|
3421
|
+
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types16.EdgeType.DEPENDS_ON);
|
|
3422
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
3423
|
+
const edge = {
|
|
3424
|
+
id: edgeId,
|
|
3425
|
+
source: sourceId,
|
|
3426
|
+
target: targetId,
|
|
3427
|
+
type: import_types16.EdgeType.DEPENDS_ON,
|
|
3428
|
+
provenance: import_types16.Provenance.EXTRACTED,
|
|
3429
|
+
evidence: { file: evidenceFile }
|
|
3430
|
+
};
|
|
3431
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
3432
|
+
edgesAdded++;
|
|
3433
|
+
}
|
|
3434
|
+
}
|
|
3435
|
+
return { nodesAdded, edgesAdded };
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
// src/extract/infra/dockerfile.ts
|
|
3439
|
+
init_cjs_shims();
|
|
3440
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
3441
|
+
var import_node_fs12 = require("fs");
|
|
3442
|
+
var import_types17 = require("@neat.is/types");
|
|
3443
|
+
function runtimeImage(content) {
|
|
3444
|
+
const lines = content.split("\n");
|
|
3445
|
+
let last = null;
|
|
3446
|
+
for (const raw of lines) {
|
|
3447
|
+
const line = raw.trim();
|
|
3448
|
+
if (!line || line.startsWith("#")) continue;
|
|
3449
|
+
if (!/^from\s+/i.test(line)) continue;
|
|
3450
|
+
const tokens = line.split(/\s+/);
|
|
3451
|
+
const image = tokens[1];
|
|
3452
|
+
if (!image || image.toLowerCase() === "scratch") continue;
|
|
3453
|
+
last = image;
|
|
3454
|
+
}
|
|
3455
|
+
return last;
|
|
3456
|
+
}
|
|
3457
|
+
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
3458
|
+
let nodesAdded = 0;
|
|
3459
|
+
let edgesAdded = 0;
|
|
3460
|
+
for (const service of services) {
|
|
3461
|
+
const dockerfilePath = import_node_path24.default.join(service.dir, "Dockerfile");
|
|
3462
|
+
if (!await exists(dockerfilePath)) continue;
|
|
3463
|
+
const content = await import_node_fs12.promises.readFile(dockerfilePath, "utf8");
|
|
3464
|
+
const image = runtimeImage(content);
|
|
3465
|
+
if (!image) continue;
|
|
3466
|
+
const node = makeInfraNode("container-image", image);
|
|
3467
|
+
if (!graph.hasNode(node.id)) {
|
|
3468
|
+
graph.addNode(node.id, node);
|
|
3469
|
+
nodesAdded++;
|
|
3470
|
+
}
|
|
3471
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types17.EdgeType.RUNS_ON);
|
|
3472
|
+
if (!graph.hasEdge(edgeId)) {
|
|
3473
|
+
const edge = {
|
|
3474
|
+
id: edgeId,
|
|
3475
|
+
source: service.node.id,
|
|
3476
|
+
target: node.id,
|
|
3477
|
+
type: import_types17.EdgeType.RUNS_ON,
|
|
3478
|
+
provenance: import_types17.Provenance.EXTRACTED,
|
|
3479
|
+
evidence: {
|
|
3480
|
+
file: import_node_path24.default.relative(scanPath, dockerfilePath).split(import_node_path24.default.sep).join("/")
|
|
3481
|
+
}
|
|
3482
|
+
};
|
|
3483
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
3484
|
+
edgesAdded++;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
return { nodesAdded, edgesAdded };
|
|
3488
|
+
}
|
|
3489
|
+
|
|
3490
|
+
// src/extract/infra/terraform.ts
|
|
3491
|
+
init_cjs_shims();
|
|
3492
|
+
var import_node_fs13 = require("fs");
|
|
3493
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
3494
|
+
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3495
|
+
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3496
|
+
if (depth > max) return [];
|
|
3497
|
+
const out = [];
|
|
3498
|
+
const entries = await import_node_fs13.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3499
|
+
for (const entry of entries) {
|
|
3500
|
+
if (entry.isDirectory()) {
|
|
3501
|
+
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3502
|
+
out.push(...await walkTfFiles(import_node_path25.default.join(start, entry.name), depth + 1, max));
|
|
3503
|
+
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3504
|
+
out.push(import_node_path25.default.join(start, entry.name));
|
|
3505
|
+
}
|
|
3506
|
+
}
|
|
3507
|
+
return out;
|
|
3508
|
+
}
|
|
3509
|
+
async function addTerraformResources(graph, scanPath) {
|
|
3510
|
+
let nodesAdded = 0;
|
|
3511
|
+
const files = await walkTfFiles(scanPath);
|
|
3512
|
+
for (const file of files) {
|
|
3513
|
+
const content = await import_node_fs13.promises.readFile(file, "utf8");
|
|
3514
|
+
RESOURCE_RE.lastIndex = 0;
|
|
3515
|
+
let m;
|
|
3516
|
+
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
3517
|
+
const kind = m[1];
|
|
3518
|
+
const name = m[2];
|
|
3519
|
+
const node = makeInfraNode(kind, name, "aws");
|
|
3520
|
+
if (!graph.hasNode(node.id)) {
|
|
3521
|
+
graph.addNode(node.id, node);
|
|
3522
|
+
nodesAdded++;
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
return { nodesAdded, edgesAdded: 0 };
|
|
3527
|
+
}
|
|
3528
|
+
|
|
3529
|
+
// src/extract/infra/k8s.ts
|
|
3530
|
+
init_cjs_shims();
|
|
3531
|
+
var import_node_fs14 = require("fs");
|
|
3532
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
3533
|
+
var import_yaml3 = require("yaml");
|
|
3534
|
+
var K8S_KIND_TO_INFRA_KIND = {
|
|
3535
|
+
Service: "k8s-service",
|
|
3536
|
+
Deployment: "k8s-deployment",
|
|
3537
|
+
StatefulSet: "k8s-statefulset",
|
|
3538
|
+
DaemonSet: "k8s-daemonset",
|
|
3539
|
+
CronJob: "k8s-cronjob",
|
|
3540
|
+
Job: "k8s-job",
|
|
3541
|
+
Ingress: "k8s-ingress"
|
|
3542
|
+
};
|
|
3543
|
+
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3544
|
+
if (depth > max) return [];
|
|
3545
|
+
const out = [];
|
|
3546
|
+
const entries = await import_node_fs14.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3547
|
+
for (const entry of entries) {
|
|
3548
|
+
if (entry.isDirectory()) {
|
|
3549
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3550
|
+
out.push(...await walkYamlFiles2(import_node_path26.default.join(start, entry.name), depth + 1, max));
|
|
3551
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path26.default.extname(entry.name))) {
|
|
3552
|
+
out.push(import_node_path26.default.join(start, entry.name));
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
return out;
|
|
3556
|
+
}
|
|
3557
|
+
async function addK8sResources(graph, scanPath) {
|
|
3558
|
+
let nodesAdded = 0;
|
|
3559
|
+
const files = await walkYamlFiles2(scanPath);
|
|
3560
|
+
for (const file of files) {
|
|
3561
|
+
const content = await import_node_fs14.promises.readFile(file, "utf8");
|
|
3562
|
+
let docs;
|
|
3563
|
+
try {
|
|
3564
|
+
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
3565
|
+
} catch {
|
|
3566
|
+
continue;
|
|
3567
|
+
}
|
|
3568
|
+
for (const doc of docs) {
|
|
3569
|
+
if (!doc?.kind || !doc.metadata?.name) continue;
|
|
3570
|
+
const infraKind = K8S_KIND_TO_INFRA_KIND[doc.kind];
|
|
3571
|
+
if (!infraKind) continue;
|
|
3572
|
+
const namespaced = doc.metadata.namespace ? `${doc.metadata.namespace}/${doc.metadata.name}` : doc.metadata.name;
|
|
3573
|
+
const node = makeInfraNode(infraKind, namespaced, "kubernetes");
|
|
3574
|
+
if (!graph.hasNode(node.id)) {
|
|
3575
|
+
graph.addNode(node.id, node);
|
|
3576
|
+
nodesAdded++;
|
|
3577
|
+
}
|
|
3578
|
+
}
|
|
3579
|
+
}
|
|
3580
|
+
return { nodesAdded, edgesAdded: 0 };
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// src/extract/infra/index.ts
|
|
3584
|
+
async function addInfra(graph, scanPath, services) {
|
|
3585
|
+
const compose = await addComposeInfra(graph, scanPath, services);
|
|
3586
|
+
const dockerfile = await addDockerfileRuntimes(graph, services, scanPath);
|
|
3587
|
+
const terraform = await addTerraformResources(graph, scanPath);
|
|
3588
|
+
const k8s = await addK8sResources(graph, scanPath);
|
|
3589
|
+
return {
|
|
3590
|
+
nodesAdded: compose.nodesAdded + dockerfile.nodesAdded + terraform.nodesAdded + k8s.nodesAdded,
|
|
3591
|
+
edgesAdded: compose.edgesAdded + dockerfile.edgesAdded + terraform.edgesAdded + k8s.edgesAdded
|
|
3592
|
+
};
|
|
3593
|
+
}
|
|
3594
|
+
|
|
3595
|
+
// src/extract/index.ts
|
|
3596
|
+
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3597
|
+
await ensureCompatLoaded();
|
|
3598
|
+
const services = await discoverServices(scanPath);
|
|
3599
|
+
const phase1Nodes = addServiceNodes(graph, services);
|
|
3600
|
+
await addServiceAliases(graph, scanPath, services);
|
|
3601
|
+
const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
|
|
3602
|
+
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3603
|
+
const phase4 = await addCallEdges(graph, services);
|
|
3604
|
+
const phase5 = await addInfra(graph, scanPath, services);
|
|
3605
|
+
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3606
|
+
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3607
|
+
return {
|
|
3608
|
+
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3609
|
+
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3610
|
+
frontiersPromoted
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// src/persist.ts
|
|
3615
|
+
init_cjs_shims();
|
|
3616
|
+
var import_node_fs15 = require("fs");
|
|
3617
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
3618
|
+
var SCHEMA_VERSION = 2;
|
|
3619
|
+
function migrateV1ToV2(payload) {
|
|
3620
|
+
const nodes = payload.graph.nodes;
|
|
3621
|
+
if (Array.isArray(nodes)) {
|
|
3622
|
+
for (const node of nodes) {
|
|
3623
|
+
if (node.attributes && "pgDriverVersion" in node.attributes) {
|
|
3624
|
+
delete node.attributes.pgDriverVersion;
|
|
3625
|
+
}
|
|
3626
|
+
}
|
|
3627
|
+
}
|
|
3628
|
+
return { ...payload, schemaVersion: 2 };
|
|
3629
|
+
}
|
|
3630
|
+
async function ensureDir(filePath) {
|
|
3631
|
+
await import_node_fs15.promises.mkdir(import_node_path27.default.dirname(filePath), { recursive: true });
|
|
3632
|
+
}
|
|
3633
|
+
async function saveGraphToDisk(graph, outPath) {
|
|
3634
|
+
await ensureDir(outPath);
|
|
3635
|
+
const payload = {
|
|
3636
|
+
schemaVersion: SCHEMA_VERSION,
|
|
3637
|
+
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3638
|
+
graph: graph.export()
|
|
3639
|
+
};
|
|
3640
|
+
const tmp = `${outPath}.tmp`;
|
|
3641
|
+
await import_node_fs15.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
3642
|
+
await import_node_fs15.promises.rename(tmp, outPath);
|
|
3643
|
+
}
|
|
3644
|
+
async function loadGraphFromDisk(graph, outPath) {
|
|
3645
|
+
let raw;
|
|
3646
|
+
try {
|
|
3647
|
+
raw = await import_node_fs15.promises.readFile(outPath, "utf8");
|
|
3648
|
+
} catch (err) {
|
|
3649
|
+
if (err.code === "ENOENT") return;
|
|
3650
|
+
throw err;
|
|
3651
|
+
}
|
|
3652
|
+
let payload = JSON.parse(raw);
|
|
3653
|
+
if (payload.schemaVersion === 1) {
|
|
3654
|
+
payload = migrateV1ToV2(payload);
|
|
3655
|
+
}
|
|
3656
|
+
if (payload.schemaVersion !== SCHEMA_VERSION) {
|
|
3657
|
+
throw new Error(
|
|
3658
|
+
`persist: unsupported snapshot schemaVersion ${payload.schemaVersion} (expected ${SCHEMA_VERSION})`
|
|
3659
|
+
);
|
|
3660
|
+
}
|
|
3661
|
+
graph.clear();
|
|
3662
|
+
graph.import(payload.graph);
|
|
3663
|
+
}
|
|
3664
|
+
function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
3665
|
+
let stopped = false;
|
|
3666
|
+
const tick = async () => {
|
|
3667
|
+
if (stopped) return;
|
|
3668
|
+
try {
|
|
3669
|
+
await saveGraphToDisk(graph, outPath);
|
|
3670
|
+
} catch (err) {
|
|
3671
|
+
console.error("persist: periodic save failed", err);
|
|
3672
|
+
}
|
|
3673
|
+
};
|
|
3674
|
+
const interval = setInterval(() => {
|
|
3675
|
+
void tick();
|
|
3676
|
+
}, intervalMs);
|
|
3677
|
+
const onSignal = (signal) => {
|
|
3678
|
+
void (async () => {
|
|
3679
|
+
try {
|
|
3680
|
+
await saveGraphToDisk(graph, outPath);
|
|
3681
|
+
} catch (err) {
|
|
3682
|
+
console.error(`persist: ${signal} save failed`, err);
|
|
3683
|
+
} finally {
|
|
3684
|
+
process.exit(0);
|
|
3685
|
+
}
|
|
3686
|
+
})();
|
|
3687
|
+
};
|
|
3688
|
+
process.on("SIGTERM", onSignal);
|
|
3689
|
+
process.on("SIGINT", onSignal);
|
|
3690
|
+
return () => {
|
|
3691
|
+
stopped = true;
|
|
3692
|
+
clearInterval(interval);
|
|
3693
|
+
process.off("SIGTERM", onSignal);
|
|
3694
|
+
process.off("SIGINT", onSignal);
|
|
3695
|
+
};
|
|
3696
|
+
}
|
|
3697
|
+
|
|
3698
|
+
// src/api.ts
|
|
3699
|
+
init_cjs_shims();
|
|
3700
|
+
var import_fastify = __toESM(require("fastify"), 1);
|
|
3701
|
+
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
3702
|
+
var import_types18 = require("@neat.is/types");
|
|
3703
|
+
|
|
3704
|
+
// src/diff.ts
|
|
3705
|
+
init_cjs_shims();
|
|
3706
|
+
var import_node_fs16 = require("fs");
|
|
3707
|
+
async function loadSnapshotForDiff(target) {
|
|
3708
|
+
if (/^https?:\/\//i.test(target)) {
|
|
3709
|
+
const res = await fetch(target);
|
|
3710
|
+
if (!res.ok) {
|
|
3711
|
+
throw new Error(`fetch ${target} failed: ${res.status} ${res.statusText}`);
|
|
3712
|
+
}
|
|
3713
|
+
return await res.json();
|
|
3714
|
+
}
|
|
3715
|
+
const raw = await import_node_fs16.promises.readFile(target, "utf8");
|
|
3716
|
+
return JSON.parse(raw);
|
|
3717
|
+
}
|
|
3718
|
+
function indexEntries(entries) {
|
|
3719
|
+
const m = /* @__PURE__ */ new Map();
|
|
3720
|
+
if (!entries) return m;
|
|
3721
|
+
for (const entry of entries) {
|
|
3722
|
+
const id = entry.attributes?.id ?? entry.key;
|
|
3723
|
+
if (!id) continue;
|
|
3724
|
+
m.set(id, entry.attributes);
|
|
3725
|
+
}
|
|
3726
|
+
return m;
|
|
3727
|
+
}
|
|
3728
|
+
function computeGraphDiff(liveGraph, baseSnapshot, currentExportedAt = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
3729
|
+
const baseNodes = indexEntries(baseSnapshot.graph?.nodes);
|
|
3730
|
+
const baseEdges = indexEntries(baseSnapshot.graph?.edges);
|
|
3731
|
+
const liveNodes = /* @__PURE__ */ new Map();
|
|
3732
|
+
liveGraph.forEachNode((id, attrs) => liveNodes.set(id, attrs));
|
|
3733
|
+
const liveEdges = /* @__PURE__ */ new Map();
|
|
3734
|
+
liveGraph.forEachEdge((id, attrs) => liveEdges.set(id, attrs));
|
|
3735
|
+
const result = {
|
|
3736
|
+
base: { exportedAt: baseSnapshot.exportedAt },
|
|
3737
|
+
current: { exportedAt: currentExportedAt },
|
|
3738
|
+
added: { nodes: [], edges: [] },
|
|
3739
|
+
removed: { nodes: [], edges: [] },
|
|
3740
|
+
changed: { nodes: [], edges: [] }
|
|
3741
|
+
};
|
|
3742
|
+
for (const [id, after] of liveNodes) {
|
|
3743
|
+
const before = baseNodes.get(id);
|
|
3744
|
+
if (!before) {
|
|
3745
|
+
result.added.nodes.push(after);
|
|
3746
|
+
} else if (!shallowEqual(before, after)) {
|
|
3747
|
+
result.changed.nodes.push({ id, before, after });
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
for (const [id, before] of baseNodes) {
|
|
3751
|
+
if (!liveNodes.has(id)) result.removed.nodes.push(before);
|
|
3752
|
+
}
|
|
3753
|
+
for (const [id, after] of liveEdges) {
|
|
3754
|
+
const before = baseEdges.get(id);
|
|
3755
|
+
if (!before) {
|
|
3756
|
+
result.added.edges.push(after);
|
|
3757
|
+
} else if (!shallowEqual(before, after)) {
|
|
3758
|
+
result.changed.edges.push({ id, before, after });
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
for (const [id, before] of baseEdges) {
|
|
3762
|
+
if (!liveEdges.has(id)) result.removed.edges.push(before);
|
|
3763
|
+
}
|
|
3764
|
+
return result;
|
|
3765
|
+
}
|
|
3766
|
+
function shallowEqual(a, b) {
|
|
3767
|
+
return canonicalJson(a) === canonicalJson(b);
|
|
3768
|
+
}
|
|
3769
|
+
function canonicalJson(value) {
|
|
3770
|
+
return JSON.stringify(value, (_key, v) => {
|
|
3771
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
3772
|
+
return Object.keys(v).sort().reduce((acc, k) => {
|
|
3773
|
+
acc[k] = v[k];
|
|
3774
|
+
return acc;
|
|
3775
|
+
}, {});
|
|
3776
|
+
}
|
|
3777
|
+
return v;
|
|
3778
|
+
});
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
// src/projects.ts
|
|
3782
|
+
init_cjs_shims();
|
|
3783
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
3784
|
+
function pathsForProject(project, baseDir) {
|
|
3785
|
+
if (project === DEFAULT_PROJECT) {
|
|
3786
|
+
return {
|
|
3787
|
+
snapshotPath: import_node_path28.default.join(baseDir, "graph.json"),
|
|
3788
|
+
errorsPath: import_node_path28.default.join(baseDir, "errors.ndjson"),
|
|
3789
|
+
staleEventsPath: import_node_path28.default.join(baseDir, "stale-events.ndjson"),
|
|
3790
|
+
embeddingsCachePath: import_node_path28.default.join(baseDir, "embeddings.json"),
|
|
3791
|
+
policyViolationsPath: import_node_path28.default.join(baseDir, "policy-violations.ndjson")
|
|
3792
|
+
};
|
|
3793
|
+
}
|
|
3794
|
+
return {
|
|
3795
|
+
snapshotPath: import_node_path28.default.join(baseDir, `${project}.json`),
|
|
3796
|
+
errorsPath: import_node_path28.default.join(baseDir, `errors.${project}.ndjson`),
|
|
3797
|
+
staleEventsPath: import_node_path28.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
3798
|
+
embeddingsCachePath: import_node_path28.default.join(baseDir, `embeddings.${project}.json`),
|
|
3799
|
+
policyViolationsPath: import_node_path28.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3800
|
+
};
|
|
3801
|
+
}
|
|
3802
|
+
var Projects = class {
|
|
3803
|
+
contexts = /* @__PURE__ */ new Map();
|
|
3804
|
+
upsert(ctx) {
|
|
3805
|
+
this.contexts.set(ctx.name, ctx);
|
|
3806
|
+
}
|
|
3807
|
+
set(name, init) {
|
|
3808
|
+
const ctx = {
|
|
3809
|
+
name,
|
|
3810
|
+
graph: init.graph ?? getGraph(name),
|
|
3811
|
+
scanPath: init.scanPath,
|
|
3812
|
+
paths: init.paths,
|
|
3813
|
+
searchIndex: init.searchIndex
|
|
3814
|
+
};
|
|
3815
|
+
this.contexts.set(name, ctx);
|
|
3816
|
+
return ctx;
|
|
3817
|
+
}
|
|
3818
|
+
get(name) {
|
|
3819
|
+
return this.contexts.get(name);
|
|
3820
|
+
}
|
|
3821
|
+
has(name) {
|
|
3822
|
+
return this.contexts.has(name);
|
|
3823
|
+
}
|
|
3824
|
+
list() {
|
|
3825
|
+
return [...this.contexts.keys()].sort();
|
|
3826
|
+
}
|
|
3827
|
+
attachSearchIndex(name, index) {
|
|
3828
|
+
const ctx = this.contexts.get(name);
|
|
3829
|
+
if (ctx) ctx.searchIndex = index;
|
|
3830
|
+
}
|
|
3831
|
+
};
|
|
3832
|
+
|
|
3833
|
+
// src/api.ts
|
|
3834
|
+
function serializeGraph(graph) {
|
|
3835
|
+
const nodes = [];
|
|
3836
|
+
graph.forEachNode((_id, attrs) => {
|
|
3837
|
+
nodes.push(attrs);
|
|
3838
|
+
});
|
|
3839
|
+
const edges = [];
|
|
3840
|
+
graph.forEachEdge((_id, attrs) => {
|
|
3841
|
+
edges.push(attrs);
|
|
3842
|
+
});
|
|
3843
|
+
return { nodes, edges };
|
|
3844
|
+
}
|
|
3845
|
+
function projectFromReq(req) {
|
|
3846
|
+
const params = req.params;
|
|
3847
|
+
return params.project ?? DEFAULT_PROJECT;
|
|
3848
|
+
}
|
|
3849
|
+
function resolveProject(registry, req, reply) {
|
|
3850
|
+
const name = projectFromReq(req);
|
|
3851
|
+
const ctx = registry.get(name);
|
|
3852
|
+
if (!ctx) {
|
|
3853
|
+
void reply.code(404).send({ error: "project not found", project: name });
|
|
3854
|
+
return null;
|
|
3855
|
+
}
|
|
3856
|
+
return ctx;
|
|
3857
|
+
}
|
|
3858
|
+
function buildLegacyRegistry(opts) {
|
|
3859
|
+
if (opts.projects) return opts.projects;
|
|
3860
|
+
if (!opts.graph) {
|
|
3861
|
+
throw new Error("buildApi: either `projects` or `graph` must be provided");
|
|
3862
|
+
}
|
|
3863
|
+
const registry = new Projects();
|
|
3864
|
+
const paths = pathsForProject(DEFAULT_PROJECT, "");
|
|
3865
|
+
registry.set(DEFAULT_PROJECT, {
|
|
3866
|
+
graph: opts.graph,
|
|
3867
|
+
scanPath: opts.scanPath,
|
|
3868
|
+
paths: {
|
|
3869
|
+
snapshotPath: paths.snapshotPath,
|
|
3870
|
+
errorsPath: opts.errorsPath ?? paths.errorsPath,
|
|
3871
|
+
staleEventsPath: opts.staleEventsPath ?? paths.staleEventsPath,
|
|
3872
|
+
embeddingsCachePath: paths.embeddingsCachePath,
|
|
3873
|
+
policyViolationsPath: paths.policyViolationsPath
|
|
3874
|
+
},
|
|
3875
|
+
searchIndex: opts.searchIndex
|
|
3876
|
+
});
|
|
3877
|
+
return registry;
|
|
3878
|
+
}
|
|
3879
|
+
function registerRoutes(scope, ctx) {
|
|
3880
|
+
const { registry, startedAt, errorsPathFor, staleEventsPathFor } = ctx;
|
|
3881
|
+
scope.get("/health", async (req, reply) => {
|
|
3882
|
+
const proj = resolveProject(registry, req, reply);
|
|
3883
|
+
if (!proj) return;
|
|
3884
|
+
return {
|
|
3885
|
+
uptime: Math.floor((Date.now() - startedAt) / 1e3),
|
|
3886
|
+
project: proj.name,
|
|
3887
|
+
nodeCount: proj.graph.order,
|
|
3888
|
+
edgeCount: proj.graph.size,
|
|
3889
|
+
lastUpdated: (/* @__PURE__ */ new Date()).toISOString()
|
|
3890
|
+
};
|
|
3891
|
+
});
|
|
3892
|
+
scope.get("/graph", async (req, reply) => {
|
|
3893
|
+
const proj = resolveProject(registry, req, reply);
|
|
3894
|
+
if (!proj) return;
|
|
3895
|
+
return serializeGraph(proj.graph);
|
|
3896
|
+
});
|
|
3897
|
+
scope.get(
|
|
3898
|
+
"/graph/node/:id",
|
|
3899
|
+
async (req, reply) => {
|
|
3900
|
+
const proj = resolveProject(registry, req, reply);
|
|
3901
|
+
if (!proj) return;
|
|
3902
|
+
const { id } = req.params;
|
|
3903
|
+
if (!proj.graph.hasNode(id)) {
|
|
3904
|
+
return reply.code(404).send({ error: "node not found", id });
|
|
3905
|
+
}
|
|
3906
|
+
return proj.graph.getNodeAttributes(id);
|
|
3907
|
+
}
|
|
3908
|
+
);
|
|
3909
|
+
scope.get(
|
|
3910
|
+
"/graph/edges/:id",
|
|
3911
|
+
async (req, reply) => {
|
|
3912
|
+
const proj = resolveProject(registry, req, reply);
|
|
3913
|
+
if (!proj) return;
|
|
3914
|
+
const { id } = req.params;
|
|
3915
|
+
if (!proj.graph.hasNode(id)) {
|
|
3916
|
+
return reply.code(404).send({ error: "node not found", id });
|
|
3917
|
+
}
|
|
3918
|
+
const inbound = proj.graph.inboundEdges(id).map((e) => proj.graph.getEdgeAttributes(e));
|
|
3919
|
+
const outbound = proj.graph.outboundEdges(id).map((e) => proj.graph.getEdgeAttributes(e));
|
|
3920
|
+
return { inbound, outbound };
|
|
3921
|
+
}
|
|
3922
|
+
);
|
|
3923
|
+
scope.get("/graph/node/:id/dependencies", async (req, reply) => {
|
|
3924
|
+
const proj = resolveProject(registry, req, reply);
|
|
3925
|
+
if (!proj) return;
|
|
3926
|
+
const { id } = req.params;
|
|
3927
|
+
if (!proj.graph.hasNode(id)) {
|
|
3928
|
+
return reply.code(404).send({ error: "node not found", id });
|
|
3929
|
+
}
|
|
3930
|
+
const depth = req.query.depth ? Number(req.query.depth) : TRANSITIVE_DEPENDENCIES_DEFAULT_DEPTH;
|
|
3931
|
+
if (!Number.isFinite(depth) || depth < 1 || depth > TRANSITIVE_DEPENDENCIES_MAX_DEPTH) {
|
|
3932
|
+
return reply.code(400).send({
|
|
3933
|
+
error: `depth must be an integer in [1, ${TRANSITIVE_DEPENDENCIES_MAX_DEPTH}]`
|
|
3934
|
+
});
|
|
3935
|
+
}
|
|
3936
|
+
return getTransitiveDependencies(proj.graph, id, depth);
|
|
3937
|
+
});
|
|
3938
|
+
scope.get("/incidents", async (req, reply) => {
|
|
3939
|
+
const proj = resolveProject(registry, req, reply);
|
|
3940
|
+
if (!proj) return;
|
|
3941
|
+
const epath = errorsPathFor(proj);
|
|
3942
|
+
if (!epath) return [];
|
|
3943
|
+
return readErrorEvents(epath);
|
|
3944
|
+
});
|
|
3945
|
+
scope.get("/incidents/stale", async (req, reply) => {
|
|
3946
|
+
const proj = resolveProject(registry, req, reply);
|
|
3947
|
+
if (!proj) return;
|
|
3948
|
+
const spath = staleEventsPathFor(proj);
|
|
3949
|
+
if (!spath) return [];
|
|
3950
|
+
const events = await readStaleEvents(spath);
|
|
3951
|
+
const filtered = req.query.edgeType ? events.filter((e) => e.edgeType === req.query.edgeType) : events;
|
|
3952
|
+
const ordered = [...filtered].reverse();
|
|
3953
|
+
const limit = req.query.limit ? Number(req.query.limit) : 50;
|
|
3954
|
+
return ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
3955
|
+
});
|
|
3956
|
+
scope.get(
|
|
3957
|
+
"/incidents/:nodeId",
|
|
3958
|
+
async (req, reply) => {
|
|
3959
|
+
const proj = resolveProject(registry, req, reply);
|
|
3960
|
+
if (!proj) return;
|
|
3961
|
+
const { nodeId } = req.params;
|
|
3962
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
3963
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
3964
|
+
}
|
|
3965
|
+
const epath = errorsPathFor(proj);
|
|
3966
|
+
if (!epath) return [];
|
|
3967
|
+
const events = await readErrorEvents(epath);
|
|
3968
|
+
return events.filter(
|
|
3969
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
3970
|
+
);
|
|
3971
|
+
}
|
|
3972
|
+
);
|
|
3973
|
+
scope.get("/traverse/root-cause/:nodeId", async (req, reply) => {
|
|
3974
|
+
const proj = resolveProject(registry, req, reply);
|
|
3975
|
+
if (!proj) return;
|
|
3976
|
+
const { nodeId } = req.params;
|
|
3977
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
3978
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
3979
|
+
}
|
|
3980
|
+
let errorEvent;
|
|
3981
|
+
const epath = errorsPathFor(proj);
|
|
3982
|
+
if (req.query.errorId && epath) {
|
|
3983
|
+
const events = await readErrorEvents(epath);
|
|
3984
|
+
errorEvent = events.find((e) => e.id === req.query.errorId);
|
|
3985
|
+
if (!errorEvent) {
|
|
3986
|
+
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
3990
|
+
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
3991
|
+
return result;
|
|
3992
|
+
});
|
|
3993
|
+
scope.get("/traverse/blast-radius/:nodeId", async (req, reply) => {
|
|
3994
|
+
const proj = resolveProject(registry, req, reply);
|
|
3995
|
+
if (!proj) return;
|
|
3996
|
+
const { nodeId } = req.params;
|
|
3997
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
3998
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
3999
|
+
}
|
|
4000
|
+
const depth = req.query.depth ? Number(req.query.depth) : void 0;
|
|
4001
|
+
if (depth !== void 0 && (!Number.isFinite(depth) || depth < 0)) {
|
|
4002
|
+
return reply.code(400).send({ error: "depth must be a non-negative number" });
|
|
4003
|
+
}
|
|
4004
|
+
return getBlastRadius(proj.graph, nodeId, depth);
|
|
4005
|
+
});
|
|
4006
|
+
scope.get("/search", async (req, reply) => {
|
|
4007
|
+
const proj = resolveProject(registry, req, reply);
|
|
4008
|
+
if (!proj) return;
|
|
4009
|
+
const raw = (req.query.q ?? "").trim();
|
|
4010
|
+
if (!raw) return reply.code(400).send({ error: "query parameter `q` is required" });
|
|
4011
|
+
const limit = req.query.limit ? Number(req.query.limit) : void 0;
|
|
4012
|
+
const safeLimit = limit !== void 0 && Number.isFinite(limit) && limit > 0 ? limit : void 0;
|
|
4013
|
+
if (proj.searchIndex) {
|
|
4014
|
+
const result = await proj.searchIndex.search(raw, safeLimit);
|
|
4015
|
+
return {
|
|
4016
|
+
query: result.query,
|
|
4017
|
+
provider: result.provider,
|
|
4018
|
+
matches: result.matches.map((m) => ({ ...m.node, score: m.score }))
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
const q = raw.toLowerCase();
|
|
4022
|
+
const matches = [];
|
|
4023
|
+
proj.graph.forEachNode((id, attrs) => {
|
|
4024
|
+
const name = attrs.name ?? "";
|
|
4025
|
+
if (id.toLowerCase().includes(q) || name.toLowerCase().includes(q)) {
|
|
4026
|
+
matches.push({ ...attrs, score: 1 });
|
|
4027
|
+
}
|
|
4028
|
+
});
|
|
4029
|
+
return {
|
|
4030
|
+
query: q,
|
|
4031
|
+
provider: "substring",
|
|
4032
|
+
matches: matches.slice(0, safeLimit)
|
|
4033
|
+
};
|
|
4034
|
+
});
|
|
4035
|
+
scope.get(
|
|
4036
|
+
"/graph/diff",
|
|
4037
|
+
async (req, reply) => {
|
|
4038
|
+
const proj = resolveProject(registry, req, reply);
|
|
4039
|
+
if (!proj) return;
|
|
4040
|
+
const against = req.query.against;
|
|
4041
|
+
if (!against) {
|
|
4042
|
+
return reply.code(400).send({ error: "query parameter `against` is required" });
|
|
4043
|
+
}
|
|
4044
|
+
try {
|
|
4045
|
+
const snapshot = await loadSnapshotForDiff(against);
|
|
4046
|
+
return computeGraphDiff(proj.graph, snapshot);
|
|
4047
|
+
} catch (err) {
|
|
4048
|
+
return reply.code(400).send({ error: "failed to load snapshot", against, detail: err.message });
|
|
4049
|
+
}
|
|
4050
|
+
}
|
|
4051
|
+
);
|
|
4052
|
+
scope.post("/graph/scan", async (req, reply) => {
|
|
4053
|
+
const proj = resolveProject(registry, req, reply);
|
|
4054
|
+
if (!proj) return;
|
|
4055
|
+
if (!proj.scanPath) {
|
|
4056
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
4057
|
+
}
|
|
4058
|
+
const result = await extractFromDirectory(proj.graph, proj.scanPath);
|
|
4059
|
+
return {
|
|
4060
|
+
project: proj.name,
|
|
4061
|
+
scanned: proj.scanPath,
|
|
4062
|
+
nodesAdded: result.nodesAdded,
|
|
4063
|
+
edgesAdded: result.edgesAdded,
|
|
4064
|
+
nodeCount: proj.graph.order,
|
|
4065
|
+
edgeCount: proj.graph.size
|
|
4066
|
+
};
|
|
4067
|
+
});
|
|
4068
|
+
scope.get("/policies", async (req, reply) => {
|
|
4069
|
+
const proj = resolveProject(registry, req, reply);
|
|
4070
|
+
if (!proj) return;
|
|
4071
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
4072
|
+
if (!policyPath) {
|
|
4073
|
+
return { version: 1, policies: [] };
|
|
4074
|
+
}
|
|
4075
|
+
try {
|
|
4076
|
+
const policies = await loadPolicyFile(policyPath);
|
|
4077
|
+
return { version: 1, policies };
|
|
4078
|
+
} catch (err) {
|
|
4079
|
+
return reply.code(400).send({
|
|
4080
|
+
error: "policy.json failed to parse",
|
|
4081
|
+
details: err.message
|
|
4082
|
+
});
|
|
4083
|
+
}
|
|
4084
|
+
});
|
|
4085
|
+
scope.get("/policies/violations", async (req, reply) => {
|
|
4086
|
+
const proj = resolveProject(registry, req, reply);
|
|
4087
|
+
if (!proj) return;
|
|
4088
|
+
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
4089
|
+
let violations = await log.readAll();
|
|
4090
|
+
if (req.query.severity) {
|
|
4091
|
+
const sev = import_types18.PolicySeveritySchema.safeParse(req.query.severity);
|
|
4092
|
+
if (!sev.success) {
|
|
4093
|
+
return reply.code(400).send({
|
|
4094
|
+
error: "invalid severity",
|
|
4095
|
+
details: sev.error.format()
|
|
4096
|
+
});
|
|
4097
|
+
}
|
|
4098
|
+
violations = violations.filter((v) => v.severity === sev.data);
|
|
4099
|
+
}
|
|
4100
|
+
if (req.query.policyId) {
|
|
4101
|
+
violations = violations.filter((v) => v.policyId === req.query.policyId);
|
|
4102
|
+
}
|
|
4103
|
+
return violations;
|
|
4104
|
+
});
|
|
4105
|
+
scope.post("/policies/check", async (req, reply) => {
|
|
4106
|
+
const proj = resolveProject(registry, req, reply);
|
|
4107
|
+
if (!proj) return;
|
|
4108
|
+
const parsed = import_types18.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
4109
|
+
if (!parsed.success) {
|
|
4110
|
+
return reply.code(400).send({
|
|
4111
|
+
error: "invalid /policies/check body",
|
|
4112
|
+
details: parsed.error.format()
|
|
4113
|
+
});
|
|
4114
|
+
}
|
|
4115
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
4116
|
+
let policies = [];
|
|
4117
|
+
if (policyPath) {
|
|
4118
|
+
try {
|
|
4119
|
+
policies = await loadPolicyFile(policyPath);
|
|
4120
|
+
} catch (err) {
|
|
4121
|
+
return reply.code(400).send({
|
|
4122
|
+
error: "policy.json failed to parse",
|
|
4123
|
+
details: err.message
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
}
|
|
4127
|
+
const evalCtx = { now: () => Date.now() };
|
|
4128
|
+
if (!parsed.data.hypotheticalAction) {
|
|
4129
|
+
const violations2 = evaluateAllPolicies(proj.graph, policies, evalCtx);
|
|
4130
|
+
const blocking2 = violations2.filter((v) => v.onViolation === "block");
|
|
4131
|
+
return { allowed: blocking2.length === 0, violations: violations2 };
|
|
4132
|
+
}
|
|
4133
|
+
const violations = evaluateAllPolicies(proj.graph, policies, evalCtx);
|
|
4134
|
+
const blocking = violations.filter((v) => v.onViolation === "block");
|
|
4135
|
+
return {
|
|
4136
|
+
allowed: blocking.length === 0,
|
|
4137
|
+
hypotheticalAction: parsed.data.hypotheticalAction,
|
|
4138
|
+
violations
|
|
4139
|
+
};
|
|
4140
|
+
});
|
|
4141
|
+
}
|
|
4142
|
+
async function buildApi(opts) {
|
|
4143
|
+
const app = (0, import_fastify.default)({ logger: false });
|
|
4144
|
+
await app.register(import_cors.default, { origin: true });
|
|
4145
|
+
const startedAt = opts.startedAt ?? Date.now();
|
|
4146
|
+
const registry = buildLegacyRegistry(opts);
|
|
4147
|
+
const legacyErrorsExplicit = !opts.projects && opts.errorsPath !== void 0;
|
|
4148
|
+
const legacyStaleExplicit = !opts.projects && opts.staleEventsPath !== void 0;
|
|
4149
|
+
const errorsPathFor = (proj) => {
|
|
4150
|
+
if (proj.name === DEFAULT_PROJECT && !opts.projects) {
|
|
4151
|
+
return legacyErrorsExplicit ? opts.errorsPath : void 0;
|
|
4152
|
+
}
|
|
4153
|
+
return proj.paths.errorsPath;
|
|
4154
|
+
};
|
|
4155
|
+
const staleEventsPathFor = (proj) => {
|
|
4156
|
+
if (proj.name === DEFAULT_PROJECT && !opts.projects) {
|
|
4157
|
+
return legacyStaleExplicit ? opts.staleEventsPath : void 0;
|
|
4158
|
+
}
|
|
4159
|
+
return proj.paths.staleEventsPath;
|
|
4160
|
+
};
|
|
4161
|
+
const policyFilePathFor = (proj) => {
|
|
4162
|
+
if (!proj.scanPath) return void 0;
|
|
4163
|
+
return `${proj.scanPath}/policy.json`;
|
|
4164
|
+
};
|
|
4165
|
+
const routeCtx = {
|
|
4166
|
+
registry,
|
|
4167
|
+
startedAt,
|
|
4168
|
+
errorsPathFor,
|
|
4169
|
+
staleEventsPathFor,
|
|
4170
|
+
policyFilePathFor
|
|
4171
|
+
};
|
|
4172
|
+
app.get("/projects", async () => ({
|
|
4173
|
+
projects: registry.list().map((name) => {
|
|
4174
|
+
const proj = registry.get(name);
|
|
4175
|
+
return {
|
|
4176
|
+
name,
|
|
4177
|
+
nodeCount: proj.graph.order,
|
|
4178
|
+
edgeCount: proj.graph.size,
|
|
4179
|
+
scanPath: proj.scanPath
|
|
4180
|
+
};
|
|
4181
|
+
})
|
|
4182
|
+
}));
|
|
4183
|
+
registerRoutes(app, routeCtx);
|
|
4184
|
+
await app.register(
|
|
4185
|
+
async (scope) => {
|
|
4186
|
+
registerRoutes(scope, routeCtx);
|
|
4187
|
+
},
|
|
4188
|
+
{ prefix: "/projects/:project" }
|
|
4189
|
+
);
|
|
4190
|
+
return app;
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
// src/index.ts
|
|
4194
|
+
init_otel();
|
|
4195
|
+
init_otel_grpc();
|
|
4196
|
+
|
|
4197
|
+
// src/daemon.ts
|
|
4198
|
+
init_cjs_shims();
|
|
4199
|
+
var import_node_fs18 = require("fs");
|
|
4200
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
4201
|
+
|
|
4202
|
+
// src/registry.ts
|
|
4203
|
+
init_cjs_shims();
|
|
4204
|
+
var import_node_fs17 = require("fs");
|
|
4205
|
+
var import_node_os2 = __toESM(require("os"), 1);
|
|
4206
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
4207
|
+
var import_types19 = require("@neat.is/types");
|
|
4208
|
+
var LOCK_TIMEOUT_MS = 5e3;
|
|
4209
|
+
var LOCK_RETRY_MS = 50;
|
|
4210
|
+
function neatHome() {
|
|
4211
|
+
const override = process.env.NEAT_HOME;
|
|
4212
|
+
if (override && override.length > 0) return import_node_path31.default.resolve(override);
|
|
4213
|
+
return import_node_path31.default.join(import_node_os2.default.homedir(), ".neat");
|
|
4214
|
+
}
|
|
4215
|
+
function registryPath() {
|
|
4216
|
+
return import_node_path31.default.join(neatHome(), "projects.json");
|
|
4217
|
+
}
|
|
4218
|
+
function registryLockPath() {
|
|
4219
|
+
return import_node_path31.default.join(neatHome(), "projects.json.lock");
|
|
4220
|
+
}
|
|
4221
|
+
async function normalizeProjectPath(input) {
|
|
4222
|
+
const resolved = import_node_path31.default.resolve(input);
|
|
4223
|
+
try {
|
|
4224
|
+
return await import_node_fs17.promises.realpath(resolved);
|
|
4225
|
+
} catch {
|
|
4226
|
+
return resolved;
|
|
4227
|
+
}
|
|
4228
|
+
}
|
|
4229
|
+
async function writeAtomically(target, contents) {
|
|
4230
|
+
await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(target), { recursive: true });
|
|
4231
|
+
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4232
|
+
const fd = await import_node_fs17.promises.open(tmp, "w");
|
|
4233
|
+
try {
|
|
4234
|
+
await fd.writeFile(contents, "utf8");
|
|
4235
|
+
await fd.sync();
|
|
4236
|
+
} finally {
|
|
4237
|
+
await fd.close();
|
|
4238
|
+
}
|
|
4239
|
+
await import_node_fs17.promises.rename(tmp, target);
|
|
4240
|
+
}
|
|
4241
|
+
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
4242
|
+
const deadline = Date.now() + timeoutMs;
|
|
4243
|
+
await import_node_fs17.promises.mkdir(import_node_path31.default.dirname(lockPath), { recursive: true });
|
|
4244
|
+
while (true) {
|
|
4245
|
+
try {
|
|
4246
|
+
const fd = await import_node_fs17.promises.open(lockPath, "wx");
|
|
4247
|
+
await fd.close();
|
|
4248
|
+
return;
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
const code = err.code;
|
|
4251
|
+
if (code !== "EEXIST") throw err;
|
|
4252
|
+
if (Date.now() >= deadline) {
|
|
4253
|
+
throw new Error(
|
|
4254
|
+
`neat registry: timed out after ${timeoutMs}ms waiting for ${lockPath}. Another neat process is holding the lock; if no such process exists, remove the file by hand.`
|
|
4255
|
+
);
|
|
4256
|
+
}
|
|
4257
|
+
await new Promise((r) => setTimeout(r, LOCK_RETRY_MS));
|
|
4258
|
+
}
|
|
4259
|
+
}
|
|
4260
|
+
}
|
|
4261
|
+
async function releaseLock(lockPath) {
|
|
4262
|
+
await import_node_fs17.promises.unlink(lockPath).catch(() => {
|
|
4263
|
+
});
|
|
4264
|
+
}
|
|
4265
|
+
async function withLock(fn) {
|
|
4266
|
+
const lock = registryLockPath();
|
|
4267
|
+
await acquireLock(lock);
|
|
4268
|
+
try {
|
|
4269
|
+
return await fn();
|
|
4270
|
+
} finally {
|
|
4271
|
+
await releaseLock(lock);
|
|
4272
|
+
}
|
|
4273
|
+
}
|
|
4274
|
+
async function readRegistry() {
|
|
4275
|
+
const file = registryPath();
|
|
4276
|
+
let raw;
|
|
4277
|
+
try {
|
|
4278
|
+
raw = await import_node_fs17.promises.readFile(file, "utf8");
|
|
4279
|
+
} catch (err) {
|
|
4280
|
+
if (err.code === "ENOENT") {
|
|
4281
|
+
return { version: 1, projects: [] };
|
|
4282
|
+
}
|
|
4283
|
+
throw err;
|
|
4284
|
+
}
|
|
4285
|
+
const parsed = JSON.parse(raw);
|
|
4286
|
+
return import_types19.RegistryFileSchema.parse(parsed);
|
|
4287
|
+
}
|
|
4288
|
+
async function writeRegistry(reg) {
|
|
4289
|
+
const validated = import_types19.RegistryFileSchema.parse(reg);
|
|
4290
|
+
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
4291
|
+
}
|
|
4292
|
+
var ProjectNameCollisionError = class extends Error {
|
|
4293
|
+
projectName;
|
|
4294
|
+
constructor(name) {
|
|
4295
|
+
super(`neat registry: a project named "${name}" is already registered`);
|
|
4296
|
+
this.name = "ProjectNameCollisionError";
|
|
4297
|
+
this.projectName = name;
|
|
4298
|
+
}
|
|
4299
|
+
};
|
|
4300
|
+
async function addProject(opts) {
|
|
4301
|
+
const resolvedPath = await normalizeProjectPath(opts.path);
|
|
4302
|
+
return withLock(async () => {
|
|
4303
|
+
const reg = await readRegistry();
|
|
4304
|
+
const byName = reg.projects.find((p) => p.name === opts.name);
|
|
4305
|
+
const byPath = reg.projects.find((p) => p.path === resolvedPath);
|
|
4306
|
+
if (byName && byName.path !== resolvedPath) {
|
|
4307
|
+
throw new ProjectNameCollisionError(opts.name);
|
|
4308
|
+
}
|
|
4309
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
4310
|
+
if (byName && byName.path === resolvedPath) {
|
|
4311
|
+
byName.lastSeenAt = now;
|
|
4312
|
+
if (opts.languages) byName.languages = opts.languages;
|
|
4313
|
+
if (opts.status) byName.status = opts.status;
|
|
4314
|
+
await writeRegistry(reg);
|
|
4315
|
+
return byName;
|
|
4316
|
+
}
|
|
4317
|
+
if (byPath && byPath.name !== opts.name) {
|
|
4318
|
+
throw new ProjectNameCollisionError(byPath.name);
|
|
4319
|
+
}
|
|
4320
|
+
const entry = {
|
|
4321
|
+
name: opts.name,
|
|
4322
|
+
path: resolvedPath,
|
|
4323
|
+
registeredAt: now,
|
|
4324
|
+
languages: opts.languages ?? [],
|
|
4325
|
+
status: opts.status ?? "active"
|
|
4326
|
+
};
|
|
4327
|
+
reg.projects.push(entry);
|
|
4328
|
+
await writeRegistry(reg);
|
|
4329
|
+
return entry;
|
|
4330
|
+
});
|
|
4331
|
+
}
|
|
4332
|
+
async function getProject(name) {
|
|
4333
|
+
const reg = await readRegistry();
|
|
4334
|
+
return reg.projects.find((p) => p.name === name);
|
|
4335
|
+
}
|
|
4336
|
+
async function listProjects() {
|
|
4337
|
+
const reg = await readRegistry();
|
|
4338
|
+
return reg.projects;
|
|
4339
|
+
}
|
|
4340
|
+
async function setStatus(name, status2) {
|
|
4341
|
+
return withLock(async () => {
|
|
4342
|
+
const reg = await readRegistry();
|
|
4343
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
4344
|
+
if (!entry) throw new Error(`neat registry: no project named "${name}"`);
|
|
4345
|
+
entry.status = status2;
|
|
4346
|
+
await writeRegistry(reg);
|
|
4347
|
+
return entry;
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4350
|
+
async function touchLastSeen(name, at = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
4351
|
+
await withLock(async () => {
|
|
4352
|
+
const reg = await readRegistry();
|
|
4353
|
+
const entry = reg.projects.find((p) => p.name === name);
|
|
4354
|
+
if (!entry) return;
|
|
4355
|
+
entry.lastSeenAt = at;
|
|
4356
|
+
await writeRegistry(reg);
|
|
4357
|
+
});
|
|
4358
|
+
}
|
|
4359
|
+
async function removeProject(name) {
|
|
4360
|
+
return withLock(async () => {
|
|
4361
|
+
const reg = await readRegistry();
|
|
4362
|
+
const idx = reg.projects.findIndex((p) => p.name === name);
|
|
4363
|
+
if (idx < 0) return void 0;
|
|
4364
|
+
const [removed] = reg.projects.splice(idx, 1);
|
|
4365
|
+
await writeRegistry(reg);
|
|
4366
|
+
return removed;
|
|
4367
|
+
});
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4370
|
+
// src/daemon.ts
|
|
4371
|
+
function neatHomeFor(opts) {
|
|
4372
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path32.default.resolve(opts.neatHome);
|
|
4373
|
+
const env = process.env.NEAT_HOME;
|
|
4374
|
+
if (env && env.length > 0) return import_node_path32.default.resolve(env);
|
|
4375
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
4376
|
+
return import_node_path32.default.join(home, ".neat");
|
|
4377
|
+
}
|
|
4378
|
+
function routeSpanToProject(serviceName, projects) {
|
|
4379
|
+
if (!serviceName) return DEFAULT_PROJECT;
|
|
4380
|
+
for (const entry of projects) {
|
|
4381
|
+
if (entry.status !== "active") continue;
|
|
4382
|
+
if (entry.languages.length === 0) {
|
|
4383
|
+
}
|
|
4384
|
+
if (entry.name === serviceName) return entry.name;
|
|
4385
|
+
}
|
|
4386
|
+
return DEFAULT_PROJECT;
|
|
4387
|
+
}
|
|
4388
|
+
async function bootstrapProject(entry) {
|
|
4389
|
+
try {
|
|
4390
|
+
const stat = await import_node_fs18.promises.stat(entry.path);
|
|
4391
|
+
if (!stat.isDirectory()) {
|
|
4392
|
+
throw new Error(`registered path ${entry.path} is not a directory`);
|
|
4393
|
+
}
|
|
4394
|
+
} catch (err) {
|
|
4395
|
+
await setStatus(entry.name, "broken").catch(() => {
|
|
4396
|
+
});
|
|
4397
|
+
return {
|
|
4398
|
+
entry,
|
|
4399
|
+
// Empty graph is fine — `slots` keeps the entry visible in `status`
|
|
4400
|
+
// output; nothing routes to it because it's not 'active'.
|
|
4401
|
+
graph: getGraph(`__broken__:${entry.name}`),
|
|
4402
|
+
outPath: "",
|
|
4403
|
+
stopPersist: () => {
|
|
4404
|
+
},
|
|
4405
|
+
status: "broken",
|
|
4406
|
+
errorReason: err.message
|
|
4407
|
+
};
|
|
4408
|
+
}
|
|
4409
|
+
resetGraph(entry.name);
|
|
4410
|
+
const graph = getGraph(entry.name);
|
|
4411
|
+
const outPath = pathsForProject(
|
|
4412
|
+
entry.name,
|
|
4413
|
+
import_node_path32.default.join(entry.path, "neat-out")
|
|
4414
|
+
).snapshotPath;
|
|
4415
|
+
await loadGraphFromDisk(graph, outPath);
|
|
4416
|
+
await extractFromDirectory(graph, entry.path);
|
|
4417
|
+
const stopPersist = startPersistLoop(graph, outPath);
|
|
4418
|
+
await touchLastSeen(entry.name).catch(() => {
|
|
4419
|
+
});
|
|
4420
|
+
return {
|
|
4421
|
+
entry,
|
|
4422
|
+
graph,
|
|
4423
|
+
outPath,
|
|
4424
|
+
stopPersist,
|
|
4425
|
+
status: "active"
|
|
4426
|
+
};
|
|
4427
|
+
}
|
|
4428
|
+
async function startDaemon(opts = {}) {
|
|
4429
|
+
const home = neatHomeFor(opts);
|
|
4430
|
+
const regPath = registryPath();
|
|
4431
|
+
try {
|
|
4432
|
+
await import_node_fs18.promises.access(regPath);
|
|
4433
|
+
} catch {
|
|
4434
|
+
throw new Error(
|
|
4435
|
+
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
4436
|
+
);
|
|
4437
|
+
}
|
|
4438
|
+
const pidPath = import_node_path32.default.join(home, "neatd.pid");
|
|
4439
|
+
await writeAtomically(pidPath, `${process.pid}
|
|
4440
|
+
`);
|
|
4441
|
+
const slots = /* @__PURE__ */ new Map();
|
|
4442
|
+
async function loadAll() {
|
|
4443
|
+
const projects = await listProjects();
|
|
4444
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4445
|
+
for (const entry of projects) {
|
|
4446
|
+
seen.add(entry.name);
|
|
4447
|
+
if (slots.has(entry.name)) continue;
|
|
4448
|
+
try {
|
|
4449
|
+
const slot = await bootstrapProject(entry);
|
|
4450
|
+
slots.set(entry.name, slot);
|
|
4451
|
+
if (slot.status === "broken") {
|
|
4452
|
+
console.warn(`neatd: project "${entry.name}" broken \u2014 ${slot.errorReason}`);
|
|
4453
|
+
} else {
|
|
4454
|
+
console.log(`neatd: project "${entry.name}" active (${entry.path})`);
|
|
4455
|
+
}
|
|
4456
|
+
} catch (err) {
|
|
4457
|
+
console.warn(
|
|
4458
|
+
`neatd: project "${entry.name}" failed to bootstrap \u2014 ${err.message}`
|
|
4459
|
+
);
|
|
4460
|
+
await setStatus(entry.name, "broken").catch(() => {
|
|
4461
|
+
});
|
|
4462
|
+
}
|
|
4463
|
+
}
|
|
4464
|
+
for (const [name, slot] of [...slots.entries()]) {
|
|
4465
|
+
if (seen.has(name)) continue;
|
|
4466
|
+
try {
|
|
4467
|
+
slot.stopPersist();
|
|
4468
|
+
} catch {
|
|
4469
|
+
}
|
|
4470
|
+
slots.delete(name);
|
|
4471
|
+
console.log(`neatd: project "${name}" removed from registry \u2014 stopped`);
|
|
4472
|
+
}
|
|
4473
|
+
}
|
|
4474
|
+
await loadAll();
|
|
4475
|
+
let reloading = null;
|
|
4476
|
+
const reload = async () => {
|
|
4477
|
+
if (reloading) return reloading;
|
|
4478
|
+
reloading = (async () => {
|
|
4479
|
+
try {
|
|
4480
|
+
await loadAll();
|
|
4481
|
+
} finally {
|
|
4482
|
+
reloading = null;
|
|
4483
|
+
}
|
|
4484
|
+
})();
|
|
4485
|
+
return reloading;
|
|
4486
|
+
};
|
|
4487
|
+
const sighupHandler = () => {
|
|
4488
|
+
void reload().catch((err) => {
|
|
4489
|
+
console.warn(`neatd: SIGHUP reload failed \u2014 ${err.message}`);
|
|
4490
|
+
});
|
|
4491
|
+
};
|
|
4492
|
+
process.on("SIGHUP", sighupHandler);
|
|
4493
|
+
let stopped = false;
|
|
4494
|
+
const stop = async () => {
|
|
4495
|
+
if (stopped) return;
|
|
4496
|
+
stopped = true;
|
|
4497
|
+
process.off("SIGHUP", sighupHandler);
|
|
4498
|
+
for (const slot of slots.values()) {
|
|
4499
|
+
try {
|
|
4500
|
+
slot.stopPersist();
|
|
4501
|
+
} catch {
|
|
4502
|
+
}
|
|
4503
|
+
}
|
|
4504
|
+
await import_node_fs18.promises.unlink(pidPath).catch(() => {
|
|
4505
|
+
});
|
|
4506
|
+
};
|
|
4507
|
+
return { slots, reload, stop, pidPath };
|
|
4508
|
+
}
|
|
4509
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
4510
|
+
0 && (module.exports = {
|
|
4511
|
+
ProjectNameCollisionError,
|
|
4512
|
+
addProject,
|
|
4513
|
+
buildApi,
|
|
4514
|
+
buildOtelReceiver,
|
|
4515
|
+
checkCompatibility,
|
|
4516
|
+
compatPairs,
|
|
4517
|
+
computeGraphDiff,
|
|
4518
|
+
confidenceForEdge,
|
|
4519
|
+
extractFromDirectory,
|
|
4520
|
+
getBlastRadius,
|
|
4521
|
+
getGraph,
|
|
4522
|
+
getProject,
|
|
4523
|
+
getRootCause,
|
|
4524
|
+
handleSpan,
|
|
4525
|
+
listProjects,
|
|
4526
|
+
loadGraphFromDisk,
|
|
4527
|
+
loadSnapshotForDiff,
|
|
4528
|
+
logSpanHandler,
|
|
4529
|
+
makeSpanHandler,
|
|
4530
|
+
markStaleEdges,
|
|
4531
|
+
normalizeProjectPath,
|
|
4532
|
+
parseOtlpRequest,
|
|
4533
|
+
readErrorEvents,
|
|
4534
|
+
readRegistry,
|
|
4535
|
+
readStaleEvents,
|
|
4536
|
+
registryLockPath,
|
|
4537
|
+
registryPath,
|
|
4538
|
+
removeProject,
|
|
4539
|
+
resetGraph,
|
|
4540
|
+
routeSpanToProject,
|
|
4541
|
+
saveGraphToDisk,
|
|
4542
|
+
setStatus,
|
|
4543
|
+
startDaemon,
|
|
4544
|
+
startOtelGrpcReceiver,
|
|
4545
|
+
startPersistLoop,
|
|
4546
|
+
startStalenessLoop,
|
|
4547
|
+
stitchTrace,
|
|
4548
|
+
thresholdForEdgeType,
|
|
4549
|
+
touchLastSeen,
|
|
4550
|
+
writeAtomically
|
|
4551
|
+
});
|
|
4552
|
+
//# sourceMappingURL=index.cjs.map
|