@neat.is/core 0.4.26-dev.20260702 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZX7PCMGZ.js → chunk-A3322JYS.js} +57 -3
- package/dist/chunk-A3322JYS.js.map +1 -0
- package/dist/{chunk-XOOCA5T7.js → chunk-BIY46Q6U.js} +25 -1
- package/dist/chunk-BIY46Q6U.js.map +1 -0
- package/dist/{chunk-O25KZNZK.js → chunk-QM6BMPVJ.js} +1200 -280
- package/dist/chunk-QM6BMPVJ.js.map +1 -0
- package/dist/{chunk-GAFTW2OX.js → chunk-UV5WSM7M.js} +2 -2
- package/dist/{chunk-IABNGQT2.js → chunk-XV4D7A3Z.js} +117 -6
- package/dist/chunk-XV4D7A3Z.js.map +1 -0
- package/dist/cli.cjs +1565 -546
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +49 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1382 -326
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +1543 -395
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-QYHUJ4OY.js → otel-grpc-ET5Z6KI6.js} +3 -3
- package/dist/server.cjs +1206 -235
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-IABNGQT2.js.map +0 -1
- package/dist/chunk-O25KZNZK.js.map +0 -1
- package/dist/chunk-XOOCA5T7.js.map +0 -1
- package/dist/chunk-ZX7PCMGZ.js.map +0 -1
- /package/dist/{chunk-GAFTW2OX.js.map → chunk-UV5WSM7M.js.map} +0 -0
- /package/dist/{otel-grpc-QYHUJ4OY.js.map → otel-grpc-ET5Z6KI6.js.map} +0 -0
package/dist/neatd.cjs
CHANGED
|
@@ -58,9 +58,9 @@ function mountBearerAuth(app, opts) {
|
|
|
58
58
|
const suffixes = [...DEFAULT_UNAUTH_SUFFIXES, ...opts.extraUnauthenticatedSuffixes ?? []];
|
|
59
59
|
const publicRead = opts.publicRead === true;
|
|
60
60
|
app.addHook("preHandler", (req2, reply, done) => {
|
|
61
|
-
const
|
|
61
|
+
const path48 = (req2.url.split("?")[0] ?? "").replace(/\/+$/, "");
|
|
62
62
|
for (const suffix of suffixes) {
|
|
63
|
-
if (
|
|
63
|
+
if (path48 === suffix || path48.endsWith(suffix)) {
|
|
64
64
|
done();
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
@@ -71,11 +71,13 @@ function mountBearerAuth(app, opts) {
|
|
|
71
71
|
}
|
|
72
72
|
const header = req2.headers.authorization;
|
|
73
73
|
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
74
|
+
opts.onReject?.();
|
|
74
75
|
void reply.code(401).send({ error: "unauthorized" });
|
|
75
76
|
return;
|
|
76
77
|
}
|
|
77
78
|
const provided = Buffer.from(header.slice("Bearer ".length).trim(), "utf8");
|
|
78
79
|
if (provided.length !== expected.length || !(0, import_node_crypto.timingSafeEqual)(provided, expected)) {
|
|
80
|
+
opts.onReject?.();
|
|
79
81
|
void reply.code(401).send({ error: "unauthorized" });
|
|
80
82
|
return;
|
|
81
83
|
}
|
|
@@ -188,8 +190,8 @@ function reshapeGrpcRequest(req2) {
|
|
|
188
190
|
};
|
|
189
191
|
}
|
|
190
192
|
function resolveProtoRoot() {
|
|
191
|
-
const here =
|
|
192
|
-
return
|
|
193
|
+
const here = import_node_path42.default.dirname((0, import_node_url.fileURLToPath)(importMetaUrl));
|
|
194
|
+
return import_node_path42.default.resolve(here, "..", "proto");
|
|
193
195
|
}
|
|
194
196
|
function loadTraceService() {
|
|
195
197
|
const protoRoot = resolveProtoRoot();
|
|
@@ -257,13 +259,13 @@ async function startOtelGrpcReceiver(opts) {
|
|
|
257
259
|
})
|
|
258
260
|
};
|
|
259
261
|
}
|
|
260
|
-
var import_node_url,
|
|
262
|
+
var import_node_url, import_node_path42, import_node_crypto2, grpc, protoLoader;
|
|
261
263
|
var init_otel_grpc = __esm({
|
|
262
264
|
"src/otel-grpc.ts"() {
|
|
263
265
|
"use strict";
|
|
264
266
|
init_cjs_shims();
|
|
265
267
|
import_node_url = require("url");
|
|
266
|
-
|
|
268
|
+
import_node_path42 = __toESM(require("path"), 1);
|
|
267
269
|
import_node_crypto2 = require("crypto");
|
|
268
270
|
grpc = __toESM(require("@grpc/grpc-js"), 1);
|
|
269
271
|
protoLoader = __toESM(require("@grpc/proto-loader"), 1);
|
|
@@ -336,6 +338,36 @@ function pickEnv(spanAttrs, resourceAttrs) {
|
|
|
336
338
|
}
|
|
337
339
|
return ENV_FALLBACK;
|
|
338
340
|
}
|
|
341
|
+
function messagingDestinationOf(attrs) {
|
|
342
|
+
for (const key of ["messaging.destination.name", "messaging.destination"]) {
|
|
343
|
+
const v = attrs[key];
|
|
344
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
345
|
+
}
|
|
346
|
+
return void 0;
|
|
347
|
+
}
|
|
348
|
+
function hasWebsocketUpgradeHeader(attrs) {
|
|
349
|
+
const v = attrs["http.request.header.upgrade"];
|
|
350
|
+
const matches = (s) => typeof s === "string" && s.trim().toLowerCase() === "websocket";
|
|
351
|
+
if (Array.isArray(v)) return v.some(matches);
|
|
352
|
+
return matches(v);
|
|
353
|
+
}
|
|
354
|
+
function websocketChannelPathOf(attrs) {
|
|
355
|
+
const route = attrs["http.route"];
|
|
356
|
+
if (typeof route === "string" && route.length > 0) return route;
|
|
357
|
+
for (const key of ["url.path", "http.target"]) {
|
|
358
|
+
const v = attrs[key];
|
|
359
|
+
if (typeof v === "string" && v.length > 0) {
|
|
360
|
+
const q = v.indexOf("?");
|
|
361
|
+
const path48 = q === -1 ? v : v.slice(0, q);
|
|
362
|
+
if (path48.length > 0) return path48;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return void 0;
|
|
366
|
+
}
|
|
367
|
+
function websocketChannelOf(attrs) {
|
|
368
|
+
if (!hasWebsocketUpgradeHeader(attrs)) return void 0;
|
|
369
|
+
return websocketChannelPathOf(attrs);
|
|
370
|
+
}
|
|
339
371
|
function parseOtlpRequest(body) {
|
|
340
372
|
const out = [];
|
|
341
373
|
for (const rs of body.resourceSpans ?? []) {
|
|
@@ -362,6 +394,14 @@ function parseOtlpRequest(body) {
|
|
|
362
394
|
attributes: attrs,
|
|
363
395
|
dbSystem: typeof attrs["db.system"] === "string" ? attrs["db.system"] : void 0,
|
|
364
396
|
dbName: typeof attrs["db.name"] === "string" ? attrs["db.name"] : void 0,
|
|
397
|
+
messagingSystem: typeof attrs["messaging.system"] === "string" ? attrs["messaging.system"] : void 0,
|
|
398
|
+
messagingDestination: messagingDestinationOf(attrs),
|
|
399
|
+
graphqlOperationName: typeof attrs["graphql.operation.name"] === "string" && attrs["graphql.operation.name"].length > 0 ? attrs["graphql.operation.name"] : void 0,
|
|
400
|
+
graphqlOperationType: typeof attrs["graphql.operation.type"] === "string" && attrs["graphql.operation.type"].length > 0 ? attrs["graphql.operation.type"] : void 0,
|
|
401
|
+
rpcSystem: typeof attrs["rpc.system"] === "string" && attrs["rpc.system"].length > 0 ? attrs["rpc.system"] : void 0,
|
|
402
|
+
rpcService: typeof attrs["rpc.service"] === "string" && attrs["rpc.service"].length > 0 ? attrs["rpc.service"] : void 0,
|
|
403
|
+
rpcMethod: typeof attrs["rpc.method"] === "string" && attrs["rpc.method"].length > 0 ? attrs["rpc.method"] : void 0,
|
|
404
|
+
websocketChannel: websocketChannelOf(attrs),
|
|
365
405
|
statusCode: span.status?.code,
|
|
366
406
|
errorMessage: span.status?.message,
|
|
367
407
|
exception: extractExceptionFromEvents(span.events)
|
|
@@ -373,10 +413,10 @@ function parseOtlpRequest(body) {
|
|
|
373
413
|
return out;
|
|
374
414
|
}
|
|
375
415
|
function loadProtoRoot() {
|
|
376
|
-
const here =
|
|
377
|
-
const protoRoot =
|
|
416
|
+
const here = import_node_path43.default.dirname((0, import_node_url2.fileURLToPath)(importMetaUrl));
|
|
417
|
+
const protoRoot = import_node_path43.default.resolve(here, "..", "proto");
|
|
378
418
|
const root = new import_protobufjs.default.Root();
|
|
379
|
-
root.resolvePath = (_origin, target) =>
|
|
419
|
+
root.resolvePath = (_origin, target) => import_node_path43.default.resolve(protoRoot, target);
|
|
380
420
|
root.loadSync(
|
|
381
421
|
"opentelemetry/proto/collector/trace/v1/trace_service.proto",
|
|
382
422
|
{ keepCase: true }
|
|
@@ -421,7 +461,21 @@ async function buildOtelReceiver(opts) {
|
|
|
421
461
|
logger: false,
|
|
422
462
|
bodyLimit: opts.bodyLimit ?? 16 * 1024 * 1024
|
|
423
463
|
});
|
|
424
|
-
|
|
464
|
+
const REJECT_WARN_INTERVAL_MS = 6e4;
|
|
465
|
+
let lastRejectWarnAt = 0;
|
|
466
|
+
const warnRejectedOtlp = () => {
|
|
467
|
+
const now = Date.now();
|
|
468
|
+
if (now - lastRejectWarnAt < REJECT_WARN_INTERVAL_MS) return;
|
|
469
|
+
lastRejectWarnAt = now;
|
|
470
|
+
console.warn(
|
|
471
|
+
"[neatd] rejecting OTLP spans on /v1/traces \u2014 missing or invalid bearer token (set NEAT_OTEL_TOKEN on the instrumented app)"
|
|
472
|
+
);
|
|
473
|
+
};
|
|
474
|
+
mountBearerAuth(app, {
|
|
475
|
+
token: opts.authToken,
|
|
476
|
+
trustProxy: opts.trustProxy,
|
|
477
|
+
onReject: warnRejectedOtlp
|
|
478
|
+
});
|
|
425
479
|
const queue = [];
|
|
426
480
|
let draining = false;
|
|
427
481
|
let drainPromise = Promise.resolve();
|
|
@@ -590,12 +644,12 @@ async function listenSteppingOtlp(app, requestedPort, host) {
|
|
|
590
644
|
}
|
|
591
645
|
}
|
|
592
646
|
}
|
|
593
|
-
var
|
|
647
|
+
var import_node_path43, import_node_url2, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
|
|
594
648
|
var init_otel = __esm({
|
|
595
649
|
"src/otel.ts"() {
|
|
596
650
|
"use strict";
|
|
597
651
|
init_cjs_shims();
|
|
598
|
-
|
|
652
|
+
import_node_path43 = __toESM(require("path"), 1);
|
|
599
653
|
import_node_url2 = require("url");
|
|
600
654
|
import_fastify2 = __toESM(require("fastify"), 1);
|
|
601
655
|
import_protobufjs = __toESM(require("protobufjs"), 1);
|
|
@@ -618,14 +672,14 @@ __export(neatd_exports, {
|
|
|
618
672
|
});
|
|
619
673
|
module.exports = __toCommonJS(neatd_exports);
|
|
620
674
|
init_cjs_shims();
|
|
621
|
-
var
|
|
622
|
-
var
|
|
675
|
+
var import_node_fs28 = require("fs");
|
|
676
|
+
var import_node_path47 = __toESM(require("path"), 1);
|
|
623
677
|
var import_node_module2 = require("module");
|
|
624
678
|
|
|
625
679
|
// src/daemon.ts
|
|
626
680
|
init_cjs_shims();
|
|
627
|
-
var
|
|
628
|
-
var
|
|
681
|
+
var import_node_fs26 = require("fs");
|
|
682
|
+
var import_node_path45 = __toESM(require("path"), 1);
|
|
629
683
|
var import_node_module = require("module");
|
|
630
684
|
|
|
631
685
|
// src/graph.ts
|
|
@@ -1155,19 +1209,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
1155
1209
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
1156
1210
|
let best = { path: [start], edges: [] };
|
|
1157
1211
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
1158
|
-
function step(node,
|
|
1159
|
-
if (
|
|
1160
|
-
best = { path: [...
|
|
1212
|
+
function step(node, path48, edges) {
|
|
1213
|
+
if (path48.length > best.path.length) {
|
|
1214
|
+
best = { path: [...path48], edges: [...edges] };
|
|
1161
1215
|
}
|
|
1162
|
-
if (
|
|
1216
|
+
if (path48.length - 1 >= maxDepth) return;
|
|
1163
1217
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
1164
1218
|
for (const [srcId, edge] of incoming) {
|
|
1165
1219
|
if (visited.has(srcId)) continue;
|
|
1166
1220
|
visited.add(srcId);
|
|
1167
|
-
|
|
1221
|
+
path48.push(srcId);
|
|
1168
1222
|
edges.push(edge);
|
|
1169
|
-
step(srcId,
|
|
1170
|
-
|
|
1223
|
+
step(srcId, path48, edges);
|
|
1224
|
+
path48.pop();
|
|
1171
1225
|
edges.pop();
|
|
1172
1226
|
visited.delete(srcId);
|
|
1173
1227
|
}
|
|
@@ -1175,14 +1229,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
|
|
|
1175
1229
|
step(start, [start], []);
|
|
1176
1230
|
return best;
|
|
1177
1231
|
}
|
|
1178
|
-
function databaseRootCauseShape(graph, origin,
|
|
1232
|
+
function databaseRootCauseShape(graph, origin, walk3) {
|
|
1179
1233
|
const targetDb = origin;
|
|
1180
1234
|
const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
|
|
1181
1235
|
if (candidatePairs.length === 0) return null;
|
|
1182
|
-
for (const id of
|
|
1236
|
+
for (const id of walk3.path) {
|
|
1183
1237
|
const owner = resolveOwningService(graph, id);
|
|
1184
1238
|
if (!owner) continue;
|
|
1185
|
-
const { id:
|
|
1239
|
+
const { id: serviceId4, svc } = owner;
|
|
1186
1240
|
const deps = svc.dependencies ?? {};
|
|
1187
1241
|
for (const pair of candidatePairs) {
|
|
1188
1242
|
const declared = deps[pair.driver];
|
|
@@ -1195,7 +1249,7 @@ function databaseRootCauseShape(graph, origin, walk) {
|
|
|
1195
1249
|
);
|
|
1196
1250
|
if (!result.compatible) {
|
|
1197
1251
|
return {
|
|
1198
|
-
rootCauseNode:
|
|
1252
|
+
rootCauseNode: serviceId4,
|
|
1199
1253
|
rootCauseReason: result.reason ?? "incompatible driver",
|
|
1200
1254
|
...result.minDriverVersion ? {
|
|
1201
1255
|
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
@@ -1206,11 +1260,11 @@ function databaseRootCauseShape(graph, origin, walk) {
|
|
|
1206
1260
|
}
|
|
1207
1261
|
return null;
|
|
1208
1262
|
}
|
|
1209
|
-
function serviceRootCauseShape(graph, _origin,
|
|
1210
|
-
for (const id of
|
|
1263
|
+
function serviceRootCauseShape(graph, _origin, walk3) {
|
|
1264
|
+
for (const id of walk3.path) {
|
|
1211
1265
|
const owner = resolveOwningService(graph, id);
|
|
1212
1266
|
if (!owner) continue;
|
|
1213
|
-
const { id:
|
|
1267
|
+
const { id: serviceId4, svc } = owner;
|
|
1214
1268
|
const deps = svc.dependencies ?? {};
|
|
1215
1269
|
const serviceNodeEngine = svc.nodeEngine;
|
|
1216
1270
|
for (const constraint of nodeEngineConstraints()) {
|
|
@@ -1219,7 +1273,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
|
|
|
1219
1273
|
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
1220
1274
|
if (!result.compatible && result.reason) {
|
|
1221
1275
|
return {
|
|
1222
|
-
rootCauseNode:
|
|
1276
|
+
rootCauseNode: serviceId4,
|
|
1223
1277
|
rootCauseReason: result.reason,
|
|
1224
1278
|
...result.requiredNodeVersion ? {
|
|
1225
1279
|
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
@@ -1234,7 +1288,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
|
|
|
1234
1288
|
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
1235
1289
|
if (!result.compatible && result.reason) {
|
|
1236
1290
|
return {
|
|
1237
|
-
rootCauseNode:
|
|
1291
|
+
rootCauseNode: serviceId4,
|
|
1238
1292
|
rootCauseReason: result.reason,
|
|
1239
1293
|
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
1240
1294
|
};
|
|
@@ -1243,10 +1297,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
|
|
|
1243
1297
|
}
|
|
1244
1298
|
return null;
|
|
1245
1299
|
}
|
|
1246
|
-
function fileRootCauseShape(graph, origin,
|
|
1300
|
+
function fileRootCauseShape(graph, origin, walk3) {
|
|
1247
1301
|
const owner = resolveOwningService(graph, origin.id);
|
|
1248
1302
|
if (!owner) return null;
|
|
1249
|
-
return serviceRootCauseShape(graph, owner.svc,
|
|
1303
|
+
return serviceRootCauseShape(graph, owner.svc, walk3);
|
|
1250
1304
|
}
|
|
1251
1305
|
var rootCauseShapes = {
|
|
1252
1306
|
[import_types.NodeType.DatabaseNode]: databaseRootCauseShape,
|
|
@@ -1258,16 +1312,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
|
1258
1312
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1259
1313
|
const shape = rootCauseShapes[origin.type];
|
|
1260
1314
|
if (shape) {
|
|
1261
|
-
const
|
|
1262
|
-
const match = shape(graph, origin,
|
|
1315
|
+
const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1316
|
+
const match = shape(graph, origin, walk3);
|
|
1263
1317
|
if (match) {
|
|
1264
1318
|
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1265
1319
|
return import_types.RootCauseResultSchema.parse({
|
|
1266
1320
|
rootCauseNode: match.rootCauseNode,
|
|
1267
1321
|
rootCauseReason: reason,
|
|
1268
|
-
traversalPath:
|
|
1269
|
-
edgeProvenances:
|
|
1270
|
-
confidence: confidenceFromMix(
|
|
1322
|
+
traversalPath: walk3.path,
|
|
1323
|
+
edgeProvenances: walk3.edges.map((e) => e.provenance),
|
|
1324
|
+
confidence: confidenceFromMix(walk3.edges),
|
|
1271
1325
|
fixRecommendation: match.fixRecommendation
|
|
1272
1326
|
});
|
|
1273
1327
|
}
|
|
@@ -1325,9 +1379,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
1325
1379
|
function isFailingCallEdge(e) {
|
|
1326
1380
|
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1327
1381
|
}
|
|
1328
|
-
function callSourcesForService(graph,
|
|
1329
|
-
const ids = [
|
|
1330
|
-
for (const edgeId of graph.outboundEdges(
|
|
1382
|
+
function callSourcesForService(graph, serviceId4) {
|
|
1383
|
+
const ids = [serviceId4];
|
|
1384
|
+
for (const edgeId of graph.outboundEdges(serviceId4)) {
|
|
1331
1385
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1332
1386
|
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1333
1387
|
const tgt = graph.getNodeAttributes(e.target);
|
|
@@ -1344,9 +1398,9 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
1344
1398
|
}
|
|
1345
1399
|
return id < curId;
|
|
1346
1400
|
}
|
|
1347
|
-
function dominantFailingCall(graph,
|
|
1401
|
+
function dominantFailingCall(graph, serviceId4, visited) {
|
|
1348
1402
|
let best = null;
|
|
1349
|
-
for (const src of callSourcesForService(graph,
|
|
1403
|
+
for (const src of callSourcesForService(graph, serviceId4)) {
|
|
1350
1404
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
1351
1405
|
const e = graph.getEdgeAttributes(edgeId);
|
|
1352
1406
|
if (!isFailingCallEdge(e)) continue;
|
|
@@ -1361,26 +1415,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
|
|
|
1361
1415
|
return best;
|
|
1362
1416
|
}
|
|
1363
1417
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1364
|
-
const
|
|
1418
|
+
const path48 = [originServiceId];
|
|
1365
1419
|
const edges = [];
|
|
1366
1420
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1367
1421
|
let current = originServiceId;
|
|
1368
1422
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1369
1423
|
const hop = dominantFailingCall(graph, current, visited);
|
|
1370
1424
|
if (!hop) break;
|
|
1371
|
-
|
|
1425
|
+
path48.push(hop.nextService);
|
|
1372
1426
|
edges.push(hop.edge);
|
|
1373
1427
|
visited.add(hop.nextService);
|
|
1374
1428
|
current = hop.nextService;
|
|
1375
1429
|
}
|
|
1376
1430
|
if (edges.length === 0) return null;
|
|
1377
|
-
return { path:
|
|
1431
|
+
return { path: path48, edges, culprit: current };
|
|
1378
1432
|
}
|
|
1379
1433
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1380
1434
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1381
1435
|
if (!chain) return null;
|
|
1382
1436
|
const culprit = chain.culprit;
|
|
1383
|
-
const
|
|
1437
|
+
const path48 = [...chain.path];
|
|
1384
1438
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1385
1439
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1386
1440
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -1388,14 +1442,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1388
1442
|
if (loc) {
|
|
1389
1443
|
let rootCauseNode = culprit;
|
|
1390
1444
|
if (loc.fileNode) {
|
|
1391
|
-
|
|
1445
|
+
path48.push(loc.fileNode);
|
|
1392
1446
|
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1393
1447
|
rootCauseNode = loc.fileNode;
|
|
1394
1448
|
}
|
|
1395
1449
|
return import_types.RootCauseResultSchema.parse({
|
|
1396
1450
|
rootCauseNode,
|
|
1397
1451
|
rootCauseReason: loc.rootCauseReason,
|
|
1398
|
-
traversalPath:
|
|
1452
|
+
traversalPath: path48,
|
|
1399
1453
|
edgeProvenances,
|
|
1400
1454
|
confidence,
|
|
1401
1455
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -1407,7 +1461,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
1407
1461
|
return import_types.RootCauseResultSchema.parse({
|
|
1408
1462
|
rootCauseNode: culprit,
|
|
1409
1463
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1410
|
-
traversalPath:
|
|
1464
|
+
traversalPath: path48,
|
|
1411
1465
|
edgeProvenances,
|
|
1412
1466
|
confidence,
|
|
1413
1467
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -2007,6 +2061,28 @@ var DEFAULT_STALE_THRESHOLDS = {
|
|
|
2007
2061
|
CONFIGURED_BY: DAY_MS,
|
|
2008
2062
|
RUNS_ON: DAY_MS
|
|
2009
2063
|
};
|
|
2064
|
+
var FALLBACK_STALE_THRESHOLD_MS = DAY_MS;
|
|
2065
|
+
function loadStaleThresholdsFromEnv() {
|
|
2066
|
+
const raw = process.env.NEAT_STALE_THRESHOLDS;
|
|
2067
|
+
if (!raw) return DEFAULT_STALE_THRESHOLDS;
|
|
2068
|
+
try {
|
|
2069
|
+
const overrides = JSON.parse(raw);
|
|
2070
|
+
const merged = { ...DEFAULT_STALE_THRESHOLDS };
|
|
2071
|
+
for (const [k, v] of Object.entries(overrides)) {
|
|
2072
|
+
if (typeof v === "number" && Number.isFinite(v) && v >= 0) merged[k] = v;
|
|
2073
|
+
}
|
|
2074
|
+
return merged;
|
|
2075
|
+
} catch (err) {
|
|
2076
|
+
console.warn(
|
|
2077
|
+
`[neat] NEAT_STALE_THRESHOLDS could not be parsed (${err.message}); using defaults`
|
|
2078
|
+
);
|
|
2079
|
+
return DEFAULT_STALE_THRESHOLDS;
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
function thresholdForEdgeType(edgeType, overrides) {
|
|
2083
|
+
const map = overrides ?? loadStaleThresholdsFromEnv();
|
|
2084
|
+
return map[edgeType] ?? FALLBACK_STALE_THRESHOLD_MS;
|
|
2085
|
+
}
|
|
2010
2086
|
var DEFAULT_INCIDENT_THRESHOLDS = {
|
|
2011
2087
|
threshold: 5,
|
|
2012
2088
|
windowMs: 6e4
|
|
@@ -2309,10 +2385,82 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
2309
2385
|
]);
|
|
2310
2386
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
2311
2387
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
2388
|
+
var WIRE_SPAN_KIND_CONSUMER = 5;
|
|
2312
2389
|
function spanMintsObservedEdge(kind) {
|
|
2313
2390
|
if (kind === void 0 || kind === 0) return true;
|
|
2314
2391
|
return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
|
|
2315
2392
|
}
|
|
2393
|
+
function spanMintsMessagingEdge(kind) {
|
|
2394
|
+
return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
|
|
2395
|
+
}
|
|
2396
|
+
function spanServesGraphqlOperation(kind) {
|
|
2397
|
+
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
2398
|
+
}
|
|
2399
|
+
function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
|
|
2400
|
+
const id = (0, import_types3.graphqlOperationId)(serviceName, operationType, operationName);
|
|
2401
|
+
if (graph.hasNode(id)) return id;
|
|
2402
|
+
const node = {
|
|
2403
|
+
id,
|
|
2404
|
+
type: import_types3.NodeType.GraphQLOperationNode,
|
|
2405
|
+
name: operationName,
|
|
2406
|
+
service: serviceName,
|
|
2407
|
+
operationType: operationType.toLowerCase(),
|
|
2408
|
+
operationName,
|
|
2409
|
+
discoveredVia: "otel"
|
|
2410
|
+
};
|
|
2411
|
+
graph.addNode(id, node);
|
|
2412
|
+
return id;
|
|
2413
|
+
}
|
|
2414
|
+
function spanServesGrpcMethod(kind) {
|
|
2415
|
+
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
2416
|
+
}
|
|
2417
|
+
function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
|
|
2418
|
+
const id = (0, import_types3.grpcMethodId)(rpcService, rpcMethod);
|
|
2419
|
+
if (graph.hasNode(id)) return id;
|
|
2420
|
+
const node = {
|
|
2421
|
+
id,
|
|
2422
|
+
type: import_types3.NodeType.GrpcMethodNode,
|
|
2423
|
+
name: `${rpcService}/${rpcMethod}`,
|
|
2424
|
+
rpcService,
|
|
2425
|
+
rpcMethod,
|
|
2426
|
+
discoveredVia: "otel"
|
|
2427
|
+
};
|
|
2428
|
+
graph.addNode(id, node);
|
|
2429
|
+
return id;
|
|
2430
|
+
}
|
|
2431
|
+
function spanServesWebsocketChannel(kind) {
|
|
2432
|
+
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
2433
|
+
}
|
|
2434
|
+
function ensureWebsocketChannelNode(graph, serviceName, channel) {
|
|
2435
|
+
const id = (0, import_types3.websocketChannelId)(serviceName, channel);
|
|
2436
|
+
if (graph.hasNode(id)) return id;
|
|
2437
|
+
const node = {
|
|
2438
|
+
id,
|
|
2439
|
+
type: import_types3.NodeType.WebSocketChannelNode,
|
|
2440
|
+
name: channel,
|
|
2441
|
+
service: serviceName,
|
|
2442
|
+
channel,
|
|
2443
|
+
discoveredVia: "otel"
|
|
2444
|
+
};
|
|
2445
|
+
graph.addNode(id, node);
|
|
2446
|
+
return id;
|
|
2447
|
+
}
|
|
2448
|
+
function messagingDestinationKind(system) {
|
|
2449
|
+
return `${system}-topic`;
|
|
2450
|
+
}
|
|
2451
|
+
function ensureMessagingDestinationNode(graph, system, destination) {
|
|
2452
|
+
const id = (0, import_types3.infraId)(messagingDestinationKind(system), destination);
|
|
2453
|
+
if (graph.hasNode(id)) return id;
|
|
2454
|
+
const node = {
|
|
2455
|
+
id,
|
|
2456
|
+
type: import_types3.NodeType.InfraNode,
|
|
2457
|
+
name: destination,
|
|
2458
|
+
provider: "self",
|
|
2459
|
+
kind: messagingDestinationKind(system)
|
|
2460
|
+
};
|
|
2461
|
+
graph.addNode(id, node);
|
|
2462
|
+
return id;
|
|
2463
|
+
}
|
|
2316
2464
|
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
2317
2465
|
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
2318
2466
|
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
@@ -2406,6 +2554,21 @@ function ensureDatabaseNode(graph, host, engine) {
|
|
|
2406
2554
|
graph.addNode(id, node);
|
|
2407
2555
|
return id;
|
|
2408
2556
|
}
|
|
2557
|
+
function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
|
|
2558
|
+
const id = (0, import_types3.localDatabaseId)(serviceName, name);
|
|
2559
|
+
if (graph.hasNode(id)) return id;
|
|
2560
|
+
const node = {
|
|
2561
|
+
id,
|
|
2562
|
+
type: import_types3.NodeType.DatabaseNode,
|
|
2563
|
+
name,
|
|
2564
|
+
engine,
|
|
2565
|
+
engineVersion: "unknown",
|
|
2566
|
+
compatibleDrivers: [],
|
|
2567
|
+
discoveredVia: "otel"
|
|
2568
|
+
};
|
|
2569
|
+
graph.addNode(id, node);
|
|
2570
|
+
return id;
|
|
2571
|
+
}
|
|
2409
2572
|
function ensureFrontierNode(graph, host, ts) {
|
|
2410
2573
|
const id = frontierIdFor(host);
|
|
2411
2574
|
if (graph.hasNode(id)) {
|
|
@@ -2657,10 +2820,21 @@ async function handleSpan(ctx, span) {
|
|
|
2657
2820
|
let affectedNode = sourceId;
|
|
2658
2821
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2659
2822
|
if (span.dbSystem) {
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2823
|
+
if (mintsFromCallerSide) {
|
|
2824
|
+
const host = pickAddress(span);
|
|
2825
|
+
let targetId;
|
|
2826
|
+
if (host) {
|
|
2827
|
+
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
2828
|
+
targetId = (0, import_types3.databaseId)(host);
|
|
2829
|
+
} else {
|
|
2830
|
+
const localName = span.dbName ?? span.dbSystem;
|
|
2831
|
+
targetId = ensureLocalDatabaseNode(
|
|
2832
|
+
ctx.graph,
|
|
2833
|
+
span.service,
|
|
2834
|
+
localName,
|
|
2835
|
+
span.dbSystem
|
|
2836
|
+
);
|
|
2837
|
+
}
|
|
2664
2838
|
const result = upsertObservedEdge(
|
|
2665
2839
|
ctx.graph,
|
|
2666
2840
|
import_types3.EdgeType.CONNECTS_TO,
|
|
@@ -2672,6 +2846,68 @@ async function handleSpan(ctx, span) {
|
|
|
2672
2846
|
);
|
|
2673
2847
|
if (result) affectedNode = targetId;
|
|
2674
2848
|
}
|
|
2849
|
+
} else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
|
|
2850
|
+
const targetId = ensureMessagingDestinationNode(
|
|
2851
|
+
ctx.graph,
|
|
2852
|
+
span.messagingSystem,
|
|
2853
|
+
span.messagingDestination
|
|
2854
|
+
);
|
|
2855
|
+
const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? import_types3.EdgeType.CONSUMES_FROM : import_types3.EdgeType.PUBLISHES_TO;
|
|
2856
|
+
const result = upsertObservedEdge(
|
|
2857
|
+
ctx.graph,
|
|
2858
|
+
edgeType,
|
|
2859
|
+
observedSource(),
|
|
2860
|
+
targetId,
|
|
2861
|
+
ts,
|
|
2862
|
+
isError,
|
|
2863
|
+
callSiteEvidence
|
|
2864
|
+
);
|
|
2865
|
+
if (result) affectedNode = targetId;
|
|
2866
|
+
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
2867
|
+
const targetId = ensureGraphqlOperationNode(
|
|
2868
|
+
ctx.graph,
|
|
2869
|
+
span.service,
|
|
2870
|
+
span.graphqlOperationType,
|
|
2871
|
+
span.graphqlOperationName
|
|
2872
|
+
);
|
|
2873
|
+
const result = upsertObservedEdge(
|
|
2874
|
+
ctx.graph,
|
|
2875
|
+
import_types3.EdgeType.CONTAINS,
|
|
2876
|
+
observedSource(),
|
|
2877
|
+
targetId,
|
|
2878
|
+
ts,
|
|
2879
|
+
isError,
|
|
2880
|
+
callSiteEvidence
|
|
2881
|
+
);
|
|
2882
|
+
if (result) affectedNode = targetId;
|
|
2883
|
+
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
2884
|
+
const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
|
|
2885
|
+
const result = upsertObservedEdge(
|
|
2886
|
+
ctx.graph,
|
|
2887
|
+
import_types3.EdgeType.CONTAINS,
|
|
2888
|
+
observedSource(),
|
|
2889
|
+
targetId,
|
|
2890
|
+
ts,
|
|
2891
|
+
isError,
|
|
2892
|
+
callSiteEvidence
|
|
2893
|
+
);
|
|
2894
|
+
if (result) affectedNode = targetId;
|
|
2895
|
+
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
2896
|
+
const targetId = ensureWebsocketChannelNode(
|
|
2897
|
+
ctx.graph,
|
|
2898
|
+
span.service,
|
|
2899
|
+
span.websocketChannel
|
|
2900
|
+
);
|
|
2901
|
+
const result = upsertObservedEdge(
|
|
2902
|
+
ctx.graph,
|
|
2903
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
2904
|
+
observedSource(),
|
|
2905
|
+
targetId,
|
|
2906
|
+
ts,
|
|
2907
|
+
isError,
|
|
2908
|
+
callSiteEvidence
|
|
2909
|
+
);
|
|
2910
|
+
if (result) affectedNode = targetId;
|
|
2675
2911
|
} else {
|
|
2676
2912
|
const host = pickAddress(span);
|
|
2677
2913
|
let resolvedViaAddress = false;
|
|
@@ -2781,29 +3017,29 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
2781
3017
|
toPromote.push({ frontierId: id, serviceId: target });
|
|
2782
3018
|
});
|
|
2783
3019
|
let promoted = 0;
|
|
2784
|
-
for (const { frontierId: frontierId2, serviceId:
|
|
3020
|
+
for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
|
|
2785
3021
|
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
2786
3022
|
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
2787
3023
|
if (!gate.allowed) {
|
|
2788
3024
|
continue;
|
|
2789
3025
|
}
|
|
2790
3026
|
}
|
|
2791
|
-
rewireFrontierEdges(graph, frontierId2,
|
|
3027
|
+
rewireFrontierEdges(graph, frontierId2, serviceId4);
|
|
2792
3028
|
graph.dropNode(frontierId2);
|
|
2793
3029
|
promoted++;
|
|
2794
3030
|
}
|
|
2795
3031
|
return promoted;
|
|
2796
3032
|
}
|
|
2797
|
-
function rewireFrontierEdges(graph, frontierId2,
|
|
3033
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId4) {
|
|
2798
3034
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
2799
3035
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
2800
3036
|
for (const edgeId of inbound) {
|
|
2801
3037
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2802
|
-
rebuildEdge(graph, edge, edge.source,
|
|
3038
|
+
rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
|
|
2803
3039
|
}
|
|
2804
3040
|
for (const edgeId of outbound) {
|
|
2805
3041
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2806
|
-
rebuildEdge(graph, edge,
|
|
3042
|
+
rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
|
|
2807
3043
|
}
|
|
2808
3044
|
}
|
|
2809
3045
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
@@ -2832,6 +3068,51 @@ function pickLater(a, b) {
|
|
|
2832
3068
|
if (!b) return a;
|
|
2833
3069
|
return new Date(a).getTime() >= new Date(b).getTime() ? a : b;
|
|
2834
3070
|
}
|
|
3071
|
+
async function markStaleEdges(graph, options = {}) {
|
|
3072
|
+
const thresholds = options.thresholds ?? loadStaleThresholdsFromEnv();
|
|
3073
|
+
const now = options.now ?? Date.now();
|
|
3074
|
+
const events = [];
|
|
3075
|
+
const project = options.project ?? DEFAULT_PROJECT;
|
|
3076
|
+
graph.forEachEdge((id, attrs) => {
|
|
3077
|
+
const e = attrs;
|
|
3078
|
+
if (e.provenance !== import_types3.Provenance.OBSERVED) return;
|
|
3079
|
+
if (!e.lastObserved) return;
|
|
3080
|
+
const threshold = thresholdForEdgeType(e.type, thresholds);
|
|
3081
|
+
const age = now - new Date(e.lastObserved).getTime();
|
|
3082
|
+
if (age > threshold) {
|
|
3083
|
+
const updated = { ...e, provenance: import_types3.Provenance.STALE, confidence: 0.3 };
|
|
3084
|
+
graph.replaceEdgeAttributes(id, updated);
|
|
3085
|
+
events.push({
|
|
3086
|
+
edgeId: id,
|
|
3087
|
+
source: e.source,
|
|
3088
|
+
target: e.target,
|
|
3089
|
+
edgeType: e.type,
|
|
3090
|
+
thresholdMs: threshold,
|
|
3091
|
+
ageMs: age,
|
|
3092
|
+
lastObserved: e.lastObserved,
|
|
3093
|
+
transitionedAt: new Date(now).toISOString()
|
|
3094
|
+
});
|
|
3095
|
+
emitNeatEvent({
|
|
3096
|
+
type: "stale-transition",
|
|
3097
|
+
project,
|
|
3098
|
+
payload: {
|
|
3099
|
+
edgeId: id,
|
|
3100
|
+
from: import_types3.Provenance.OBSERVED,
|
|
3101
|
+
to: import_types3.Provenance.STALE
|
|
3102
|
+
}
|
|
3103
|
+
});
|
|
3104
|
+
}
|
|
3105
|
+
});
|
|
3106
|
+
if (options.staleEventsPath && events.length > 0) {
|
|
3107
|
+
await appendStaleEvents(options.staleEventsPath, events);
|
|
3108
|
+
}
|
|
3109
|
+
return { count: events.length, events };
|
|
3110
|
+
}
|
|
3111
|
+
async function appendStaleEvents(staleEventsPath, events) {
|
|
3112
|
+
await import_node_fs3.promises.mkdir(import_node_path3.default.dirname(staleEventsPath), { recursive: true });
|
|
3113
|
+
const lines = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
3114
|
+
await import_node_fs3.promises.appendFile(staleEventsPath, lines, "utf8");
|
|
3115
|
+
}
|
|
2835
3116
|
async function readStaleEvents(staleEventsPath) {
|
|
2836
3117
|
try {
|
|
2837
3118
|
const raw = await import_node_fs3.promises.readFile(staleEventsPath, "utf8");
|
|
@@ -2841,6 +3122,31 @@ async function readStaleEvents(staleEventsPath) {
|
|
|
2841
3122
|
throw err;
|
|
2842
3123
|
}
|
|
2843
3124
|
}
|
|
3125
|
+
function startStalenessLoop(graph, options = {}) {
|
|
3126
|
+
let stopped = false;
|
|
3127
|
+
const intervalMs = options.intervalMs ?? 6e4;
|
|
3128
|
+
const tick = () => {
|
|
3129
|
+
if (stopped) return;
|
|
3130
|
+
void (async () => {
|
|
3131
|
+
try {
|
|
3132
|
+
await markStaleEdges(graph, {
|
|
3133
|
+
thresholds: options.thresholds,
|
|
3134
|
+
staleEventsPath: options.staleEventsPath,
|
|
3135
|
+
project: options.project
|
|
3136
|
+
});
|
|
3137
|
+
if (options.onPolicyTrigger) await options.onPolicyTrigger(graph);
|
|
3138
|
+
} catch (err) {
|
|
3139
|
+
console.error("staleness tick failed", err);
|
|
3140
|
+
}
|
|
3141
|
+
})();
|
|
3142
|
+
};
|
|
3143
|
+
const interval = setInterval(tick, intervalMs);
|
|
3144
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
3145
|
+
return () => {
|
|
3146
|
+
stopped = true;
|
|
3147
|
+
clearInterval(interval);
|
|
3148
|
+
};
|
|
3149
|
+
}
|
|
2844
3150
|
async function readErrorEvents(errorsPath) {
|
|
2845
3151
|
try {
|
|
2846
3152
|
const raw = await import_node_fs3.promises.readFile(errorsPath, "utf8");
|
|
@@ -3492,9 +3798,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
3492
3798
|
"StatefulSet",
|
|
3493
3799
|
"DaemonSet"
|
|
3494
3800
|
]);
|
|
3495
|
-
function addAliases(graph,
|
|
3496
|
-
if (!graph.hasNode(
|
|
3497
|
-
const node = graph.getNodeAttributes(
|
|
3801
|
+
function addAliases(graph, serviceId4, candidates) {
|
|
3802
|
+
if (!graph.hasNode(serviceId4)) return;
|
|
3803
|
+
const node = graph.getNodeAttributes(serviceId4);
|
|
3498
3804
|
if (node.type !== import_types6.NodeType.ServiceNode) return;
|
|
3499
3805
|
const set = new Set(node.aliases ?? []);
|
|
3500
3806
|
for (const c of candidates) {
|
|
@@ -3504,7 +3810,7 @@ function addAliases(graph, serviceId3, candidates) {
|
|
|
3504
3810
|
}
|
|
3505
3811
|
if (set.size === 0) return;
|
|
3506
3812
|
const updated = { ...node, aliases: [...set].sort() };
|
|
3507
|
-
graph.replaceNodeAttributes(
|
|
3813
|
+
graph.replaceNodeAttributes(serviceId4, updated);
|
|
3508
3814
|
}
|
|
3509
3815
|
function indexServicesByName(services) {
|
|
3510
3816
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -3537,12 +3843,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
3537
3843
|
}
|
|
3538
3844
|
if (!compose?.services) return;
|
|
3539
3845
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3540
|
-
const
|
|
3541
|
-
if (!
|
|
3846
|
+
const serviceId4 = serviceIndex.get(composeName);
|
|
3847
|
+
if (!serviceId4) continue;
|
|
3542
3848
|
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
3543
3849
|
if (svc.container_name) aliases.add(svc.container_name);
|
|
3544
3850
|
if (svc.hostname) aliases.add(svc.hostname);
|
|
3545
|
-
addAliases(graph,
|
|
3851
|
+
addAliases(graph, serviceId4, aliases);
|
|
3546
3852
|
}
|
|
3547
3853
|
}
|
|
3548
3854
|
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -3655,16 +3961,28 @@ init_cjs_shims();
|
|
|
3655
3961
|
var import_node_fs10 = require("fs");
|
|
3656
3962
|
var import_node_path10 = __toESM(require("path"), 1);
|
|
3657
3963
|
var import_types7 = require("@neat.is/types");
|
|
3964
|
+
function buildServiceHostIndex(services) {
|
|
3965
|
+
const knownHosts = /* @__PURE__ */ new Set();
|
|
3966
|
+
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3967
|
+
for (const service of services) {
|
|
3968
|
+
const base = import_node_path10.default.basename(service.dir);
|
|
3969
|
+
knownHosts.add(base);
|
|
3970
|
+
knownHosts.add(service.pkg.name);
|
|
3971
|
+
hostToNodeId.set(base, service.node.id);
|
|
3972
|
+
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3973
|
+
}
|
|
3974
|
+
return { knownHosts, hostToNodeId };
|
|
3975
|
+
}
|
|
3658
3976
|
async function walkSourceFiles(dir) {
|
|
3659
3977
|
const out = [];
|
|
3660
|
-
async function
|
|
3978
|
+
async function walk3(current) {
|
|
3661
3979
|
const entries = await import_node_fs10.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3662
3980
|
for (const entry2 of entries) {
|
|
3663
3981
|
const full = import_node_path10.default.join(current, entry2.name);
|
|
3664
3982
|
if (entry2.isDirectory()) {
|
|
3665
3983
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3666
3984
|
if (await isPythonVenvDir(full)) continue;
|
|
3667
|
-
await
|
|
3985
|
+
await walk3(full);
|
|
3668
3986
|
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path10.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3669
3987
|
// would attribute our instrumentation imports to the user's service.
|
|
3670
3988
|
!isNeatAuthoredSourceFile(entry2.name)) {
|
|
@@ -3672,7 +3990,7 @@ async function walkSourceFiles(dir) {
|
|
|
3672
3990
|
}
|
|
3673
3991
|
}
|
|
3674
3992
|
}
|
|
3675
|
-
await
|
|
3993
|
+
await walk3(dir);
|
|
3676
3994
|
return out;
|
|
3677
3995
|
}
|
|
3678
3996
|
async function loadSourceFiles(dir) {
|
|
@@ -4766,20 +5084,20 @@ var import_node_path21 = __toESM(require("path"), 1);
|
|
|
4766
5084
|
var import_types10 = require("@neat.is/types");
|
|
4767
5085
|
async function walkConfigFiles(dir) {
|
|
4768
5086
|
const out = [];
|
|
4769
|
-
async function
|
|
5087
|
+
async function walk3(current) {
|
|
4770
5088
|
const entries = await import_node_fs14.promises.readdir(current, { withFileTypes: true });
|
|
4771
5089
|
for (const entry2 of entries) {
|
|
4772
5090
|
const full = import_node_path21.default.join(current, entry2.name);
|
|
4773
5091
|
if (entry2.isDirectory()) {
|
|
4774
5092
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
4775
5093
|
if (await isPythonVenvDir(full)) continue;
|
|
4776
|
-
await
|
|
5094
|
+
await walk3(full);
|
|
4777
5095
|
} else if (entry2.isFile() && isConfigFile(entry2.name).match) {
|
|
4778
5096
|
out.push(full);
|
|
4779
5097
|
}
|
|
4780
5098
|
}
|
|
4781
5099
|
}
|
|
4782
|
-
await
|
|
5100
|
+
await walk3(dir);
|
|
4783
5101
|
return out;
|
|
4784
5102
|
}
|
|
4785
5103
|
async function addConfigNodes(graph, services, scanPath) {
|
|
@@ -4827,142 +5145,586 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
4827
5145
|
return { nodesAdded, edgesAdded };
|
|
4828
5146
|
}
|
|
4829
5147
|
|
|
4830
|
-
// src/extract/
|
|
4831
|
-
init_cjs_shims();
|
|
4832
|
-
var import_types17 = require("@neat.is/types");
|
|
4833
|
-
|
|
4834
|
-
// src/extract/calls/http.ts
|
|
5148
|
+
// src/extract/routes.ts
|
|
4835
5149
|
init_cjs_shims();
|
|
4836
5150
|
var import_node_path22 = __toESM(require("path"), 1);
|
|
4837
5151
|
var import_tree_sitter2 = __toESM(require("tree-sitter"), 1);
|
|
4838
5152
|
var import_tree_sitter_javascript2 = __toESM(require("tree-sitter-javascript"), 1);
|
|
4839
|
-
var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
|
|
4840
5153
|
var import_types11 = require("@neat.is/types");
|
|
4841
|
-
var
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
5154
|
+
var PARSE_CHUNK2 = 16384;
|
|
5155
|
+
function parseSource2(parser, source) {
|
|
5156
|
+
return parser.parse(
|
|
5157
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
|
|
5158
|
+
);
|
|
5159
|
+
}
|
|
5160
|
+
function makeJsParser2() {
|
|
5161
|
+
const p = new import_tree_sitter2.default();
|
|
5162
|
+
p.setLanguage(import_tree_sitter_javascript2.default);
|
|
5163
|
+
return p;
|
|
5164
|
+
}
|
|
5165
|
+
var ROUTER_METHODS = /* @__PURE__ */ new Set([
|
|
5166
|
+
"get",
|
|
5167
|
+
"post",
|
|
5168
|
+
"put",
|
|
5169
|
+
"patch",
|
|
5170
|
+
"delete",
|
|
5171
|
+
"options",
|
|
5172
|
+
"head",
|
|
5173
|
+
"all"
|
|
5174
|
+
]);
|
|
5175
|
+
var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
5176
|
+
var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
5177
|
+
function canonicalizeTemplate(raw) {
|
|
5178
|
+
let p = raw.split("?")[0].split("#")[0];
|
|
5179
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
5180
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
5181
|
+
return p;
|
|
5182
|
+
}
|
|
5183
|
+
function isDynamicSegment(seg) {
|
|
5184
|
+
if (seg.length === 0) return false;
|
|
5185
|
+
if (seg.includes(":")) return true;
|
|
5186
|
+
if (seg.startsWith("{") || seg.startsWith("[")) return true;
|
|
5187
|
+
if (/^\d+$/.test(seg)) return true;
|
|
5188
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
|
|
5189
|
+
if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
|
|
4859
5190
|
return false;
|
|
4860
5191
|
}
|
|
4861
|
-
function
|
|
4862
|
-
|
|
5192
|
+
function normalizePathTemplate(raw) {
|
|
5193
|
+
const canonical = canonicalizeTemplate(raw);
|
|
5194
|
+
const segments = canonical.split("/").filter((s) => s.length > 0);
|
|
5195
|
+
const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
|
|
5196
|
+
return "/" + normalised.join("/");
|
|
5197
|
+
}
|
|
5198
|
+
function walk(node, visit) {
|
|
5199
|
+
visit(node);
|
|
4863
5200
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4864
5201
|
const child = node.namedChild(i);
|
|
4865
|
-
if (child)
|
|
5202
|
+
if (child) walk(child, visit);
|
|
4866
5203
|
}
|
|
4867
5204
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
5205
|
+
function staticStringText(node) {
|
|
5206
|
+
if (node.type === "string") {
|
|
5207
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5208
|
+
const child = node.namedChild(i);
|
|
5209
|
+
if (child?.type === "string_fragment") return child.text;
|
|
5210
|
+
}
|
|
5211
|
+
return "";
|
|
5212
|
+
}
|
|
5213
|
+
if (node.type === "template_string") {
|
|
5214
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5215
|
+
if (node.namedChild(i)?.type === "template_substitution") return null;
|
|
5216
|
+
}
|
|
5217
|
+
const raw = node.text;
|
|
5218
|
+
return raw.length >= 2 ? raw.slice(1, -1) : "";
|
|
5219
|
+
}
|
|
5220
|
+
return null;
|
|
4873
5221
|
}
|
|
4874
|
-
function
|
|
5222
|
+
function objectStringProp(objNode, key) {
|
|
5223
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
5224
|
+
const pair = objNode.namedChild(i);
|
|
5225
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5226
|
+
const k = pair.childForFieldName("key");
|
|
5227
|
+
if (!k) continue;
|
|
5228
|
+
const kText = k.type === "string" ? staticStringText(k) : k.text;
|
|
5229
|
+
if (kText !== key) continue;
|
|
5230
|
+
const v = pair.childForFieldName("value");
|
|
5231
|
+
if (v) return staticStringText(v);
|
|
5232
|
+
}
|
|
5233
|
+
return null;
|
|
5234
|
+
}
|
|
5235
|
+
function fastifyRouteMethods(objNode) {
|
|
5236
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
5237
|
+
const pair = objNode.namedChild(i);
|
|
5238
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5239
|
+
const k = pair.childForFieldName("key");
|
|
5240
|
+
const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
|
|
5241
|
+
if (kText !== "method") continue;
|
|
5242
|
+
const v = pair.childForFieldName("value");
|
|
5243
|
+
if (!v) return [];
|
|
5244
|
+
if (v.type === "string" || v.type === "template_string") {
|
|
5245
|
+
const s = staticStringText(v);
|
|
5246
|
+
return s ? [s.toUpperCase()] : [];
|
|
5247
|
+
}
|
|
5248
|
+
if (v.type === "array") {
|
|
5249
|
+
const out = [];
|
|
5250
|
+
for (let j = 0; j < v.namedChildCount; j++) {
|
|
5251
|
+
const el = v.namedChild(j);
|
|
5252
|
+
if (el && (el.type === "string" || el.type === "template_string")) {
|
|
5253
|
+
const s = staticStringText(el);
|
|
5254
|
+
if (s) out.push(s.toUpperCase());
|
|
5255
|
+
}
|
|
5256
|
+
}
|
|
5257
|
+
return out;
|
|
5258
|
+
}
|
|
5259
|
+
}
|
|
5260
|
+
return [];
|
|
5261
|
+
}
|
|
5262
|
+
function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
|
|
4875
5263
|
const tree = parseSource2(parser, source);
|
|
4876
|
-
const literals = [];
|
|
4877
|
-
collectStringLiterals(tree.rootNode, literals);
|
|
4878
5264
|
const out = [];
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
5265
|
+
const framework = hasExpress ? "express" : "fastify";
|
|
5266
|
+
walk(tree.rootNode, (node) => {
|
|
5267
|
+
if (node.type !== "call_expression") return;
|
|
5268
|
+
const fn = node.childForFieldName("function");
|
|
5269
|
+
if (!fn || fn.type !== "member_expression") return;
|
|
5270
|
+
const prop = fn.childForFieldName("property");
|
|
5271
|
+
if (!prop) return;
|
|
5272
|
+
const method = prop.text.toLowerCase();
|
|
5273
|
+
const args = node.childForFieldName("arguments");
|
|
5274
|
+
const first = args?.namedChild(0);
|
|
5275
|
+
if (!first) return;
|
|
5276
|
+
const line = node.startPosition.row + 1;
|
|
5277
|
+
if (ROUTER_METHODS.has(method)) {
|
|
5278
|
+
const p = staticStringText(first);
|
|
5279
|
+
if (p && p.startsWith("/")) {
|
|
5280
|
+
out.push({
|
|
5281
|
+
method: method === "all" ? "ALL" : method.toUpperCase(),
|
|
5282
|
+
pathTemplate: canonicalizeTemplate(p),
|
|
5283
|
+
line,
|
|
5284
|
+
framework
|
|
5285
|
+
});
|
|
4884
5286
|
}
|
|
5287
|
+
return;
|
|
4885
5288
|
}
|
|
4886
|
-
|
|
5289
|
+
if (method === "route" && hasFastify && first.type === "object") {
|
|
5290
|
+
const url = objectStringProp(first, "url");
|
|
5291
|
+
if (!url || !url.startsWith("/")) return;
|
|
5292
|
+
const methods = fastifyRouteMethods(first);
|
|
5293
|
+
const list = methods.length > 0 ? methods : ["ALL"];
|
|
5294
|
+
for (const m of list) {
|
|
5295
|
+
out.push({
|
|
5296
|
+
method: m === "ALL" ? "ALL" : m.toUpperCase(),
|
|
5297
|
+
pathTemplate: canonicalizeTemplate(url),
|
|
5298
|
+
line,
|
|
5299
|
+
framework: "fastify"
|
|
5300
|
+
});
|
|
5301
|
+
}
|
|
5302
|
+
}
|
|
5303
|
+
});
|
|
4887
5304
|
return out;
|
|
4888
5305
|
}
|
|
4889
|
-
function
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
5306
|
+
function segmentsOf(relFile) {
|
|
5307
|
+
return toPosix2(relFile).split("/").filter((s) => s.length > 0);
|
|
5308
|
+
}
|
|
5309
|
+
function isNextAppRouteFile(relFile) {
|
|
5310
|
+
const segs = segmentsOf(relFile);
|
|
5311
|
+
if (!segs.includes("app")) return false;
|
|
5312
|
+
const base = segs[segs.length - 1] ?? "";
|
|
5313
|
+
return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
|
|
5314
|
+
}
|
|
5315
|
+
function isNextPagesApiFile(relFile) {
|
|
5316
|
+
const segs = segmentsOf(relFile);
|
|
5317
|
+
const pagesIdx = segs.indexOf("pages");
|
|
5318
|
+
if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
|
|
5319
|
+
const base = segs[segs.length - 1] ?? "";
|
|
5320
|
+
if (/^_(app|document|middleware)\./.test(base)) return false;
|
|
5321
|
+
return JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(base));
|
|
5322
|
+
}
|
|
5323
|
+
function nextSegment(seg) {
|
|
5324
|
+
if (seg.startsWith("(") && seg.endsWith(")")) return null;
|
|
5325
|
+
const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
|
|
5326
|
+
if (catchAll) return ":" + catchAll[1];
|
|
5327
|
+
const dynamic = seg.match(/^\[(.+?)\]$/);
|
|
5328
|
+
if (dynamic) return ":" + dynamic[1];
|
|
5329
|
+
return seg;
|
|
5330
|
+
}
|
|
5331
|
+
function nextAppPathTemplate(relFile) {
|
|
5332
|
+
const segs = segmentsOf(relFile);
|
|
5333
|
+
const appIdx = segs.lastIndexOf("app");
|
|
5334
|
+
const between = segs.slice(appIdx + 1, segs.length - 1);
|
|
5335
|
+
const parts = [];
|
|
5336
|
+
for (const seg of between) {
|
|
5337
|
+
const mapped = nextSegment(seg);
|
|
5338
|
+
if (mapped !== null) parts.push(mapped);
|
|
5339
|
+
}
|
|
5340
|
+
return "/" + parts.join("/");
|
|
5341
|
+
}
|
|
5342
|
+
function nextPagesApiPathTemplate(relFile) {
|
|
5343
|
+
const segs = segmentsOf(relFile);
|
|
5344
|
+
const pagesIdx = segs.indexOf("pages");
|
|
5345
|
+
const rest = segs.slice(pagesIdx + 1);
|
|
5346
|
+
const parts = [];
|
|
5347
|
+
for (let i = 0; i < rest.length; i++) {
|
|
5348
|
+
let seg = rest[i];
|
|
5349
|
+
if (i === rest.length - 1) {
|
|
5350
|
+
seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
|
|
5351
|
+
if (seg === "index") continue;
|
|
5352
|
+
}
|
|
5353
|
+
const mapped = nextSegment(seg);
|
|
5354
|
+
if (mapped !== null) parts.push(mapped);
|
|
5355
|
+
}
|
|
5356
|
+
return "/" + parts.join("/");
|
|
5357
|
+
}
|
|
5358
|
+
function nextAppMethods(root) {
|
|
5359
|
+
const out = [];
|
|
5360
|
+
walk(root, (node) => {
|
|
5361
|
+
if (node.type !== "export_statement") return;
|
|
5362
|
+
const decl = node.childForFieldName("declaration");
|
|
5363
|
+
if (!decl) return;
|
|
5364
|
+
const line = node.startPosition.row + 1;
|
|
5365
|
+
if (decl.type === "function_declaration") {
|
|
5366
|
+
const name = decl.childForFieldName("name")?.text;
|
|
5367
|
+
if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
|
|
5368
|
+
return;
|
|
5369
|
+
}
|
|
5370
|
+
if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
|
|
5371
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
5372
|
+
const d = decl.namedChild(i);
|
|
5373
|
+
if (d?.type !== "variable_declarator") continue;
|
|
5374
|
+
const name = d.childForFieldName("name")?.text;
|
|
5375
|
+
if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
|
|
5376
|
+
}
|
|
5377
|
+
}
|
|
5378
|
+
});
|
|
5379
|
+
return out;
|
|
4893
5380
|
}
|
|
4894
|
-
function
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
5381
|
+
function nextRoutesFromFile(source, relFile, parser) {
|
|
5382
|
+
if (isNextAppRouteFile(relFile)) {
|
|
5383
|
+
const tree = parseSource2(parser, source);
|
|
5384
|
+
const template = nextAppPathTemplate(relFile);
|
|
5385
|
+
return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
|
|
5386
|
+
method,
|
|
5387
|
+
pathTemplate: canonicalizeTemplate(template),
|
|
5388
|
+
line,
|
|
5389
|
+
framework: "next"
|
|
5390
|
+
}));
|
|
5391
|
+
}
|
|
5392
|
+
if (isNextPagesApiFile(relFile)) {
|
|
5393
|
+
return [
|
|
5394
|
+
{
|
|
5395
|
+
method: "ALL",
|
|
5396
|
+
pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
|
|
5397
|
+
line: 1,
|
|
5398
|
+
framework: "next"
|
|
5399
|
+
}
|
|
5400
|
+
];
|
|
5401
|
+
}
|
|
5402
|
+
return [];
|
|
4898
5403
|
}
|
|
4899
|
-
async function
|
|
5404
|
+
async function addRoutes(graph, services) {
|
|
4900
5405
|
const jsParser = makeJsParser2();
|
|
4901
|
-
const pyParser = makePyParser2();
|
|
4902
|
-
const knownHosts = /* @__PURE__ */ new Set();
|
|
4903
|
-
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
4904
|
-
for (const service of services) {
|
|
4905
|
-
knownHosts.add(import_node_path22.default.basename(service.dir));
|
|
4906
|
-
knownHosts.add(service.pkg.name);
|
|
4907
|
-
hostToNodeId.set(import_node_path22.default.basename(service.dir), service.node.id);
|
|
4908
|
-
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
4909
|
-
}
|
|
4910
5406
|
let nodesAdded = 0;
|
|
4911
5407
|
let edgesAdded = 0;
|
|
4912
5408
|
for (const service of services) {
|
|
5409
|
+
const deps = {
|
|
5410
|
+
...service.pkg.dependencies ?? {},
|
|
5411
|
+
...service.pkg.devDependencies ?? {}
|
|
5412
|
+
};
|
|
5413
|
+
const hasExpress = deps["express"] !== void 0;
|
|
5414
|
+
const hasFastify = deps["fastify"] !== void 0;
|
|
5415
|
+
const hasNext = deps["next"] !== void 0;
|
|
5416
|
+
if (!hasExpress && !hasFastify && !hasNext) continue;
|
|
4913
5417
|
const files = await loadSourceFiles(service.dir);
|
|
4914
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4915
5418
|
for (const file of files) {
|
|
4916
5419
|
if (isTestPath(file.path)) continue;
|
|
4917
|
-
|
|
4918
|
-
|
|
5420
|
+
if (!JS_ROUTE_EXTENSIONS.has(import_node_path22.default.extname(file.path))) continue;
|
|
5421
|
+
const relFile = toPosix2(import_node_path22.default.relative(service.dir, file.path));
|
|
5422
|
+
let routes;
|
|
4919
5423
|
try {
|
|
4920
|
-
|
|
5424
|
+
if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
|
|
5425
|
+
routes = nextRoutesFromFile(file.content, relFile, jsParser);
|
|
5426
|
+
} else if (hasExpress || hasFastify) {
|
|
5427
|
+
routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
|
|
5428
|
+
} else {
|
|
5429
|
+
routes = [];
|
|
5430
|
+
}
|
|
4921
5431
|
} catch (err) {
|
|
4922
|
-
recordExtractionError("
|
|
5432
|
+
recordExtractionError("route extraction", file.path, err);
|
|
4923
5433
|
continue;
|
|
4924
5434
|
}
|
|
4925
|
-
if (
|
|
4926
|
-
const
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
5435
|
+
if (routes.length === 0) continue;
|
|
5436
|
+
for (const route of routes) {
|
|
5437
|
+
const rid = (0, import_types11.routeId)(service.pkg.name, route.method, route.pathTemplate);
|
|
5438
|
+
if (!graph.hasNode(rid)) {
|
|
5439
|
+
const node = {
|
|
5440
|
+
id: rid,
|
|
5441
|
+
type: import_types11.NodeType.RouteNode,
|
|
5442
|
+
name: `${route.method} ${route.pathTemplate}`,
|
|
5443
|
+
service: service.pkg.name,
|
|
5444
|
+
method: route.method,
|
|
5445
|
+
pathTemplate: route.pathTemplate,
|
|
5446
|
+
path: relFile,
|
|
5447
|
+
line: route.line,
|
|
5448
|
+
framework: route.framework,
|
|
5449
|
+
discoveredVia: "static"
|
|
5450
|
+
};
|
|
5451
|
+
graph.addNode(rid, node);
|
|
5452
|
+
nodesAdded++;
|
|
5453
|
+
}
|
|
5454
|
+
const containsId = (0, import_types11.extractedEdgeId)(service.node.id, rid, import_types11.EdgeType.CONTAINS);
|
|
5455
|
+
if (!graph.hasEdge(containsId)) {
|
|
5456
|
+
const edge = {
|
|
5457
|
+
id: containsId,
|
|
5458
|
+
source: service.node.id,
|
|
5459
|
+
target: rid,
|
|
5460
|
+
type: import_types11.EdgeType.CONTAINS,
|
|
5461
|
+
provenance: import_types11.Provenance.EXTRACTED,
|
|
5462
|
+
confidence: (0, import_types11.confidenceForExtracted)("structural"),
|
|
5463
|
+
evidence: {
|
|
5464
|
+
file: relFile,
|
|
5465
|
+
line: route.line,
|
|
5466
|
+
snippet: snippet(file.content, route.line)
|
|
5467
|
+
}
|
|
5468
|
+
};
|
|
5469
|
+
graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
|
|
5470
|
+
edgesAdded++;
|
|
5471
|
+
}
|
|
5472
|
+
}
|
|
5473
|
+
}
|
|
5474
|
+
}
|
|
5475
|
+
return { nodesAdded, edgesAdded };
|
|
5476
|
+
}
|
|
5477
|
+
|
|
5478
|
+
// src/extract/proto.ts
|
|
5479
|
+
init_cjs_shims();
|
|
5480
|
+
var import_node_fs15 = require("fs");
|
|
5481
|
+
var import_node_path23 = __toESM(require("path"), 1);
|
|
5482
|
+
var import_types12 = require("@neat.is/types");
|
|
5483
|
+
var PROTO_EXTENSION = ".proto";
|
|
5484
|
+
function packageOf(content) {
|
|
5485
|
+
const m = content.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/m);
|
|
5486
|
+
return m ? m[1] : null;
|
|
5487
|
+
}
|
|
5488
|
+
function lineAt(content, offset) {
|
|
5489
|
+
return content.slice(0, offset).split("\n").length;
|
|
5490
|
+
}
|
|
5491
|
+
function grpcMethodsFromProto(content, fqPackage) {
|
|
5492
|
+
const out = [];
|
|
5493
|
+
const serviceRe = /\bservice\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g;
|
|
5494
|
+
let sm;
|
|
5495
|
+
while ((sm = serviceRe.exec(content)) !== null) {
|
|
5496
|
+
const serviceName = sm[1];
|
|
5497
|
+
const rpcService = fqPackage ? `${fqPackage}.${serviceName}` : serviceName;
|
|
5498
|
+
const bodyStart = serviceRe.lastIndex;
|
|
5499
|
+
let depth = 1;
|
|
5500
|
+
let i = bodyStart;
|
|
5501
|
+
for (; i < content.length && depth > 0; i++) {
|
|
5502
|
+
const ch = content[i];
|
|
5503
|
+
if (ch === "{") depth++;
|
|
5504
|
+
else if (ch === "}") depth--;
|
|
5505
|
+
}
|
|
5506
|
+
const body = content.slice(bodyStart, i - 1);
|
|
5507
|
+
const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
|
5508
|
+
let rm;
|
|
5509
|
+
while ((rm = rpcRe.exec(body)) !== null) {
|
|
5510
|
+
const rpcMethod = rm[1];
|
|
5511
|
+
const line = lineAt(content, bodyStart + rm.index);
|
|
5512
|
+
out.push({ rpcService, rpcMethod, line });
|
|
5513
|
+
}
|
|
5514
|
+
serviceRe.lastIndex = i;
|
|
5515
|
+
}
|
|
5516
|
+
return out;
|
|
5517
|
+
}
|
|
5518
|
+
async function walkProtoFiles(dir) {
|
|
5519
|
+
const out = [];
|
|
5520
|
+
async function walk3(current) {
|
|
5521
|
+
const entries = await import_node_fs15.promises.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
5522
|
+
for (const entry2 of entries) {
|
|
5523
|
+
const full = import_node_path23.default.join(current, entry2.name);
|
|
5524
|
+
if (entry2.isDirectory()) {
|
|
5525
|
+
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
5526
|
+
if (await isPythonVenvDir(full)) continue;
|
|
5527
|
+
await walk3(full);
|
|
5528
|
+
} else if (entry2.isFile() && import_node_path23.default.extname(entry2.name) === PROTO_EXTENSION) {
|
|
5529
|
+
out.push(full);
|
|
5530
|
+
}
|
|
5531
|
+
}
|
|
5532
|
+
}
|
|
5533
|
+
await walk3(dir);
|
|
5534
|
+
return out;
|
|
5535
|
+
}
|
|
5536
|
+
async function addGrpcMethods(graph, services) {
|
|
5537
|
+
let nodesAdded = 0;
|
|
5538
|
+
let edgesAdded = 0;
|
|
5539
|
+
for (const service of services) {
|
|
5540
|
+
const protoPaths = await walkProtoFiles(service.dir);
|
|
5541
|
+
for (const protoPath of protoPaths) {
|
|
5542
|
+
if (isTestPath(protoPath)) continue;
|
|
5543
|
+
const relFile = toPosix2(import_node_path23.default.relative(service.dir, protoPath));
|
|
5544
|
+
let content;
|
|
5545
|
+
try {
|
|
5546
|
+
content = await import_node_fs15.promises.readFile(protoPath, "utf8");
|
|
5547
|
+
} catch (err) {
|
|
5548
|
+
recordExtractionError("proto extraction", protoPath, err);
|
|
5549
|
+
continue;
|
|
5550
|
+
}
|
|
5551
|
+
let methods;
|
|
5552
|
+
try {
|
|
5553
|
+
methods = grpcMethodsFromProto(content, packageOf(content));
|
|
5554
|
+
} catch (err) {
|
|
5555
|
+
recordExtractionError("proto extraction", protoPath, err);
|
|
5556
|
+
continue;
|
|
5557
|
+
}
|
|
5558
|
+
if (methods.length === 0) continue;
|
|
5559
|
+
for (const method of methods) {
|
|
5560
|
+
const mid = (0, import_types12.grpcMethodId)(method.rpcService, method.rpcMethod);
|
|
5561
|
+
if (!graph.hasNode(mid)) {
|
|
5562
|
+
const node = {
|
|
5563
|
+
id: mid,
|
|
5564
|
+
type: import_types12.NodeType.GrpcMethodNode,
|
|
5565
|
+
name: `${method.rpcService}/${method.rpcMethod}`,
|
|
5566
|
+
rpcService: method.rpcService,
|
|
5567
|
+
rpcMethod: method.rpcMethod,
|
|
5568
|
+
path: relFile,
|
|
5569
|
+
line: method.line,
|
|
5570
|
+
discoveredVia: "static"
|
|
5571
|
+
};
|
|
5572
|
+
graph.addNode(mid, node);
|
|
5573
|
+
nodesAdded++;
|
|
5574
|
+
}
|
|
5575
|
+
const containsId = (0, import_types12.extractedEdgeId)(service.node.id, mid, import_types12.EdgeType.CONTAINS);
|
|
5576
|
+
if (!graph.hasEdge(containsId)) {
|
|
5577
|
+
const edge = {
|
|
5578
|
+
id: containsId,
|
|
5579
|
+
source: service.node.id,
|
|
5580
|
+
target: mid,
|
|
5581
|
+
type: import_types12.EdgeType.CONTAINS,
|
|
5582
|
+
provenance: import_types12.Provenance.EXTRACTED,
|
|
5583
|
+
confidence: (0, import_types12.confidenceForExtracted)("structural"),
|
|
5584
|
+
evidence: {
|
|
5585
|
+
file: relFile,
|
|
5586
|
+
line: method.line,
|
|
5587
|
+
snippet: snippet(content, method.line)
|
|
5588
|
+
}
|
|
5589
|
+
};
|
|
5590
|
+
graph.addEdgeWithKey(containsId, service.node.id, mid, edge);
|
|
5591
|
+
edgesAdded++;
|
|
5592
|
+
}
|
|
5593
|
+
}
|
|
5594
|
+
}
|
|
5595
|
+
}
|
|
5596
|
+
return { nodesAdded, edgesAdded };
|
|
5597
|
+
}
|
|
5598
|
+
|
|
5599
|
+
// src/extract/calls/index.ts
|
|
5600
|
+
init_cjs_shims();
|
|
5601
|
+
var import_types20 = require("@neat.is/types");
|
|
5602
|
+
|
|
5603
|
+
// src/extract/calls/http.ts
|
|
5604
|
+
init_cjs_shims();
|
|
5605
|
+
var import_node_path24 = __toESM(require("path"), 1);
|
|
5606
|
+
var import_tree_sitter3 = __toESM(require("tree-sitter"), 1);
|
|
5607
|
+
var import_tree_sitter_javascript3 = __toESM(require("tree-sitter-javascript"), 1);
|
|
5608
|
+
var import_tree_sitter_python2 = __toESM(require("tree-sitter-python"), 1);
|
|
5609
|
+
var import_types13 = require("@neat.is/types");
|
|
5610
|
+
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
5611
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
5612
|
+
function isInsideJsxExternalLink(node) {
|
|
5613
|
+
let cursor = node.parent;
|
|
5614
|
+
while (cursor) {
|
|
5615
|
+
if (cursor.type === "jsx_attribute") {
|
|
5616
|
+
let owner = cursor.parent;
|
|
5617
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
5618
|
+
owner = owner.parent;
|
|
5619
|
+
}
|
|
5620
|
+
if (!owner) return false;
|
|
5621
|
+
const tagNode = owner.namedChild(0);
|
|
5622
|
+
const tagName = tagNode?.text ?? "";
|
|
5623
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
5624
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
5625
|
+
}
|
|
5626
|
+
cursor = cursor.parent;
|
|
5627
|
+
}
|
|
5628
|
+
return false;
|
|
5629
|
+
}
|
|
5630
|
+
function collectStringLiterals(node, out) {
|
|
5631
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
5632
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5633
|
+
const child = node.namedChild(i);
|
|
5634
|
+
if (child) collectStringLiterals(child, out);
|
|
5635
|
+
}
|
|
5636
|
+
}
|
|
5637
|
+
var PARSE_CHUNK3 = 16384;
|
|
5638
|
+
function parseSource3(parser, source) {
|
|
5639
|
+
return parser.parse(
|
|
5640
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
|
|
5641
|
+
);
|
|
5642
|
+
}
|
|
5643
|
+
function callsFromSource(source, parser, knownHosts) {
|
|
5644
|
+
const tree = parseSource3(parser, source);
|
|
5645
|
+
const literals = [];
|
|
5646
|
+
collectStringLiterals(tree.rootNode, literals);
|
|
5647
|
+
const out = [];
|
|
5648
|
+
for (const lit of literals) {
|
|
5649
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
5650
|
+
for (const host of knownHosts) {
|
|
5651
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
5652
|
+
out.push({ host, line: lit.node.startPosition.row + 1 });
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5655
|
+
}
|
|
5656
|
+
return out;
|
|
5657
|
+
}
|
|
5658
|
+
function makeJsParser3() {
|
|
5659
|
+
const p = new import_tree_sitter3.default();
|
|
5660
|
+
p.setLanguage(import_tree_sitter_javascript3.default);
|
|
5661
|
+
return p;
|
|
5662
|
+
}
|
|
5663
|
+
function makePyParser2() {
|
|
5664
|
+
const p = new import_tree_sitter3.default();
|
|
5665
|
+
p.setLanguage(import_tree_sitter_python2.default);
|
|
5666
|
+
return p;
|
|
5667
|
+
}
|
|
5668
|
+
async function addHttpCallEdges(graph, services) {
|
|
5669
|
+
const jsParser = makeJsParser3();
|
|
5670
|
+
const pyParser = makePyParser2();
|
|
5671
|
+
const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
|
|
5672
|
+
let nodesAdded = 0;
|
|
5673
|
+
let edgesAdded = 0;
|
|
5674
|
+
for (const service of services) {
|
|
5675
|
+
const files = await loadSourceFiles(service.dir);
|
|
5676
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5677
|
+
for (const file of files) {
|
|
5678
|
+
if (isTestPath(file.path)) continue;
|
|
5679
|
+
const parser = import_node_path24.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
5680
|
+
let sites;
|
|
5681
|
+
try {
|
|
5682
|
+
sites = callsFromSource(file.content, parser, knownHosts);
|
|
5683
|
+
} catch (err) {
|
|
5684
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
5685
|
+
continue;
|
|
5686
|
+
}
|
|
5687
|
+
if (sites.length === 0) continue;
|
|
5688
|
+
const relFile = toPosix2(import_node_path24.default.relative(service.dir, file.path));
|
|
5689
|
+
for (const site of sites) {
|
|
5690
|
+
const targetId = hostToNodeId.get(site.host);
|
|
5691
|
+
if (!targetId || targetId === service.node.id) continue;
|
|
5692
|
+
const dedupKey = `${relFile}|${targetId}`;
|
|
5693
|
+
if (seen.has(dedupKey)) continue;
|
|
5694
|
+
seen.add(dedupKey);
|
|
5695
|
+
const confidence = (0, import_types13.confidenceForExtracted)("url-literal-service-target");
|
|
5696
|
+
const ev = {
|
|
5697
|
+
file: relFile,
|
|
5698
|
+
line: site.line,
|
|
5699
|
+
snippet: snippet(file.content, site.line)
|
|
5700
|
+
};
|
|
5701
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
5702
|
+
graph,
|
|
5703
|
+
service.pkg.name,
|
|
4942
5704
|
service.node.id,
|
|
4943
5705
|
relFile
|
|
4944
5706
|
);
|
|
4945
5707
|
nodesAdded += n;
|
|
4946
5708
|
edgesAdded += e;
|
|
4947
|
-
if (!(0,
|
|
5709
|
+
if (!(0, import_types13.passesExtractedFloor)(confidence)) {
|
|
4948
5710
|
noteExtractedDropped({
|
|
4949
5711
|
source: fileNodeId,
|
|
4950
5712
|
target: targetId,
|
|
4951
|
-
type:
|
|
5713
|
+
type: import_types13.EdgeType.CALLS,
|
|
4952
5714
|
confidence,
|
|
4953
5715
|
confidenceKind: "url-literal-service-target",
|
|
4954
5716
|
evidence: ev
|
|
4955
5717
|
});
|
|
4956
5718
|
continue;
|
|
4957
5719
|
}
|
|
4958
|
-
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId,
|
|
5720
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types13.EdgeType.CALLS);
|
|
4959
5721
|
if (!graph.hasEdge(edgeId)) {
|
|
4960
5722
|
const edge = {
|
|
4961
5723
|
id: edgeId,
|
|
4962
5724
|
source: fileNodeId,
|
|
4963
5725
|
target: targetId,
|
|
4964
|
-
type:
|
|
4965
|
-
provenance:
|
|
5726
|
+
type: import_types13.EdgeType.CALLS,
|
|
5727
|
+
provenance: import_types13.Provenance.EXTRACTED,
|
|
4966
5728
|
confidence,
|
|
4967
5729
|
evidence: ev
|
|
4968
5730
|
};
|
|
@@ -4975,10 +5737,279 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4975
5737
|
return { nodesAdded, edgesAdded };
|
|
4976
5738
|
}
|
|
4977
5739
|
|
|
5740
|
+
// src/extract/calls/route-match.ts
|
|
5741
|
+
init_cjs_shims();
|
|
5742
|
+
var import_node_path25 = __toESM(require("path"), 1);
|
|
5743
|
+
var import_tree_sitter4 = __toESM(require("tree-sitter"), 1);
|
|
5744
|
+
var import_tree_sitter_javascript4 = __toESM(require("tree-sitter-javascript"), 1);
|
|
5745
|
+
var import_types14 = require("@neat.is/types");
|
|
5746
|
+
var PARSE_CHUNK4 = 16384;
|
|
5747
|
+
function parseSource4(parser, source) {
|
|
5748
|
+
return parser.parse(
|
|
5749
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
|
|
5750
|
+
);
|
|
5751
|
+
}
|
|
5752
|
+
function makeJsParser4() {
|
|
5753
|
+
const p = new import_tree_sitter4.default();
|
|
5754
|
+
p.setLanguage(import_tree_sitter_javascript4.default);
|
|
5755
|
+
return p;
|
|
5756
|
+
}
|
|
5757
|
+
var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
5758
|
+
var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
|
|
5759
|
+
function walk2(node, visit) {
|
|
5760
|
+
visit(node);
|
|
5761
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5762
|
+
const child = node.namedChild(i);
|
|
5763
|
+
if (child) walk2(child, visit);
|
|
5764
|
+
}
|
|
5765
|
+
}
|
|
5766
|
+
function reconstructUrl(node) {
|
|
5767
|
+
if (node.type === "string") {
|
|
5768
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5769
|
+
const child = node.namedChild(i);
|
|
5770
|
+
if (child?.type === "string_fragment") return child.text;
|
|
5771
|
+
}
|
|
5772
|
+
return "";
|
|
5773
|
+
}
|
|
5774
|
+
if (node.type === "template_string") {
|
|
5775
|
+
let out = "";
|
|
5776
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5777
|
+
const child = node.namedChild(i);
|
|
5778
|
+
if (!child) continue;
|
|
5779
|
+
if (child.type === "string_fragment") out += child.text;
|
|
5780
|
+
else if (child.type === "template_substitution") out += ":param";
|
|
5781
|
+
}
|
|
5782
|
+
if (out.length === 0) {
|
|
5783
|
+
const raw = node.text;
|
|
5784
|
+
return raw.length >= 2 ? raw.slice(1, -1) : "";
|
|
5785
|
+
}
|
|
5786
|
+
return out;
|
|
5787
|
+
}
|
|
5788
|
+
return null;
|
|
5789
|
+
}
|
|
5790
|
+
function methodFromOptions(objNode) {
|
|
5791
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
5792
|
+
const pair = objNode.namedChild(i);
|
|
5793
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5794
|
+
const k = pair.childForFieldName("key");
|
|
5795
|
+
const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
|
|
5796
|
+
if (kText !== "method") continue;
|
|
5797
|
+
const v = pair.childForFieldName("value");
|
|
5798
|
+
if (v && (v.type === "string" || v.type === "template_string")) {
|
|
5799
|
+
const s = stringText(v);
|
|
5800
|
+
return s ? s.toUpperCase() : void 0;
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
return void 0;
|
|
5804
|
+
}
|
|
5805
|
+
function stringText(node) {
|
|
5806
|
+
if (node.type === "string") {
|
|
5807
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5808
|
+
const child = node.namedChild(i);
|
|
5809
|
+
if (child?.type === "string_fragment") return child.text;
|
|
5810
|
+
}
|
|
5811
|
+
return "";
|
|
5812
|
+
}
|
|
5813
|
+
return null;
|
|
5814
|
+
}
|
|
5815
|
+
function urlNodeFromConfig(objNode) {
|
|
5816
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
5817
|
+
const pair = objNode.namedChild(i);
|
|
5818
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5819
|
+
const k = pair.childForFieldName("key");
|
|
5820
|
+
const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
|
|
5821
|
+
if (kText === "url") return pair.childForFieldName("value");
|
|
5822
|
+
}
|
|
5823
|
+
return null;
|
|
5824
|
+
}
|
|
5825
|
+
function pathOf(urlStr) {
|
|
5826
|
+
try {
|
|
5827
|
+
const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
|
|
5828
|
+
const parsed = new URL(candidate);
|
|
5829
|
+
return parsed.pathname || "/";
|
|
5830
|
+
} catch {
|
|
5831
|
+
return null;
|
|
5832
|
+
}
|
|
5833
|
+
}
|
|
5834
|
+
function matchHost(urlStr, knownHosts) {
|
|
5835
|
+
for (const host of knownHosts) {
|
|
5836
|
+
if (urlMatchesHost(urlStr, host)) return host;
|
|
5837
|
+
}
|
|
5838
|
+
return null;
|
|
5839
|
+
}
|
|
5840
|
+
function clientCallSitesFromSource(source, parser, knownHosts) {
|
|
5841
|
+
const tree = parseSource4(parser, source);
|
|
5842
|
+
const out = [];
|
|
5843
|
+
const push = (urlNode, method, callNode) => {
|
|
5844
|
+
const urlStr = reconstructUrl(urlNode);
|
|
5845
|
+
if (!urlStr) return;
|
|
5846
|
+
const host = matchHost(urlStr, knownHosts);
|
|
5847
|
+
if (!host) return;
|
|
5848
|
+
const p = pathOf(urlStr);
|
|
5849
|
+
if (p === null) return;
|
|
5850
|
+
const line = callNode.startPosition.row + 1;
|
|
5851
|
+
out.push({
|
|
5852
|
+
host,
|
|
5853
|
+
method,
|
|
5854
|
+
pathTemplate: p,
|
|
5855
|
+
line,
|
|
5856
|
+
snippet: snippet(source, line)
|
|
5857
|
+
});
|
|
5858
|
+
};
|
|
5859
|
+
walk2(tree.rootNode, (node) => {
|
|
5860
|
+
if (node.type !== "call_expression") return;
|
|
5861
|
+
const fn = node.childForFieldName("function");
|
|
5862
|
+
if (!fn) return;
|
|
5863
|
+
const args = node.childForFieldName("arguments");
|
|
5864
|
+
const first = args?.namedChild(0);
|
|
5865
|
+
if (!first) return;
|
|
5866
|
+
const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
|
|
5867
|
+
if (fn.type === "identifier" && fnName === "fetch") {
|
|
5868
|
+
const opts = args?.namedChild(1);
|
|
5869
|
+
const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
|
|
5870
|
+
push(first, method, node);
|
|
5871
|
+
return;
|
|
5872
|
+
}
|
|
5873
|
+
if (fn.type === "identifier" && fnName === "axios") {
|
|
5874
|
+
if (first.type === "object") {
|
|
5875
|
+
const urlNode = urlNodeFromConfig(first);
|
|
5876
|
+
if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
|
|
5877
|
+
} else {
|
|
5878
|
+
const opts = args?.namedChild(1);
|
|
5879
|
+
const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
|
|
5880
|
+
push(first, method, node);
|
|
5881
|
+
}
|
|
5882
|
+
return;
|
|
5883
|
+
}
|
|
5884
|
+
if (fn.type === "member_expression") {
|
|
5885
|
+
const obj = fn.childForFieldName("object");
|
|
5886
|
+
const objName = obj?.text ?? "";
|
|
5887
|
+
if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
|
|
5888
|
+
if (fnName === "request" && first.type === "object") {
|
|
5889
|
+
const urlNode = urlNodeFromConfig(first);
|
|
5890
|
+
if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
|
|
5891
|
+
} else {
|
|
5892
|
+
push(first, fnName.toUpperCase(), node);
|
|
5893
|
+
}
|
|
5894
|
+
return;
|
|
5895
|
+
}
|
|
5896
|
+
if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
|
|
5897
|
+
const opts = args?.namedChild(1);
|
|
5898
|
+
const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
|
|
5899
|
+
push(first, method, node);
|
|
5900
|
+
return;
|
|
5901
|
+
}
|
|
5902
|
+
}
|
|
5903
|
+
});
|
|
5904
|
+
return out;
|
|
5905
|
+
}
|
|
5906
|
+
function buildRouteIndex(graph) {
|
|
5907
|
+
const index = /* @__PURE__ */ new Map();
|
|
5908
|
+
graph.forEachNode((_id, attrs) => {
|
|
5909
|
+
const node = attrs;
|
|
5910
|
+
if (node.type !== import_types14.NodeType.RouteNode) return;
|
|
5911
|
+
const route = attrs;
|
|
5912
|
+
const owner = (0, import_types14.serviceId)(route.service);
|
|
5913
|
+
const entry2 = {
|
|
5914
|
+
method: route.method.toUpperCase(),
|
|
5915
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
5916
|
+
routeNodeId: route.id
|
|
5917
|
+
};
|
|
5918
|
+
const list = index.get(owner);
|
|
5919
|
+
if (list) list.push(entry2);
|
|
5920
|
+
else index.set(owner, [entry2]);
|
|
5921
|
+
});
|
|
5922
|
+
return index;
|
|
5923
|
+
}
|
|
5924
|
+
function findRoute(entries, method, normalizedPath) {
|
|
5925
|
+
return entries.find(
|
|
5926
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
|
|
5927
|
+
);
|
|
5928
|
+
}
|
|
5929
|
+
async function addRouteCallEdges(graph, services) {
|
|
5930
|
+
const jsParser = makeJsParser4();
|
|
5931
|
+
const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
|
|
5932
|
+
const routeIndex = buildRouteIndex(graph);
|
|
5933
|
+
if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
|
|
5934
|
+
let nodesAdded = 0;
|
|
5935
|
+
let edgesAdded = 0;
|
|
5936
|
+
for (const service of services) {
|
|
5937
|
+
const files = await loadSourceFiles(service.dir);
|
|
5938
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5939
|
+
for (const file of files) {
|
|
5940
|
+
if (isTestPath(file.path)) continue;
|
|
5941
|
+
if (!JS_CLIENT_EXTENSIONS.has(import_node_path25.default.extname(file.path))) continue;
|
|
5942
|
+
let sites;
|
|
5943
|
+
try {
|
|
5944
|
+
sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
|
|
5945
|
+
} catch (err) {
|
|
5946
|
+
recordExtractionError("route-match call extraction", file.path, err);
|
|
5947
|
+
continue;
|
|
5948
|
+
}
|
|
5949
|
+
if (sites.length === 0) continue;
|
|
5950
|
+
const relFile = toPosix2(import_node_path25.default.relative(service.dir, file.path));
|
|
5951
|
+
for (const site of sites) {
|
|
5952
|
+
const serverServiceId = hostToNodeId.get(site.host);
|
|
5953
|
+
if (!serverServiceId || serverServiceId === service.node.id) continue;
|
|
5954
|
+
const entries = routeIndex.get(serverServiceId);
|
|
5955
|
+
if (!entries) continue;
|
|
5956
|
+
const normalizedPath = normalizePathTemplate(site.pathTemplate);
|
|
5957
|
+
const match = findRoute(entries, site.method, normalizedPath);
|
|
5958
|
+
if (!match) continue;
|
|
5959
|
+
const dedupKey = `${relFile}|${match.routeNodeId}`;
|
|
5960
|
+
if (seen.has(dedupKey)) continue;
|
|
5961
|
+
seen.add(dedupKey);
|
|
5962
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
5963
|
+
graph,
|
|
5964
|
+
service.pkg.name,
|
|
5965
|
+
service.node.id,
|
|
5966
|
+
relFile
|
|
5967
|
+
);
|
|
5968
|
+
nodesAdded += n;
|
|
5969
|
+
edgesAdded += e;
|
|
5970
|
+
const confidence = (0, import_types14.confidenceForExtracted)("verified-call-site");
|
|
5971
|
+
const ev = {
|
|
5972
|
+
file: relFile,
|
|
5973
|
+
line: site.line,
|
|
5974
|
+
snippet: site.snippet,
|
|
5975
|
+
method: site.method ?? match.method,
|
|
5976
|
+
pathTemplate: site.pathTemplate
|
|
5977
|
+
};
|
|
5978
|
+
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
5979
|
+
noteExtractedDropped({
|
|
5980
|
+
source: fileNodeId,
|
|
5981
|
+
target: match.routeNodeId,
|
|
5982
|
+
type: import_types14.EdgeType.CALLS,
|
|
5983
|
+
confidence,
|
|
5984
|
+
confidenceKind: "verified-call-site",
|
|
5985
|
+
evidence: ev
|
|
5986
|
+
});
|
|
5987
|
+
continue;
|
|
5988
|
+
}
|
|
5989
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, match.routeNodeId, import_types14.EdgeType.CALLS);
|
|
5990
|
+
if (!graph.hasEdge(edgeId)) {
|
|
5991
|
+
const edge = {
|
|
5992
|
+
id: edgeId,
|
|
5993
|
+
source: fileNodeId,
|
|
5994
|
+
target: match.routeNodeId,
|
|
5995
|
+
type: import_types14.EdgeType.CALLS,
|
|
5996
|
+
provenance: import_types14.Provenance.EXTRACTED,
|
|
5997
|
+
confidence,
|
|
5998
|
+
evidence: ev
|
|
5999
|
+
};
|
|
6000
|
+
graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
|
|
6001
|
+
edgesAdded++;
|
|
6002
|
+
}
|
|
6003
|
+
}
|
|
6004
|
+
}
|
|
6005
|
+
}
|
|
6006
|
+
return { nodesAdded, edgesAdded };
|
|
6007
|
+
}
|
|
6008
|
+
|
|
4978
6009
|
// src/extract/calls/kafka.ts
|
|
4979
6010
|
init_cjs_shims();
|
|
4980
|
-
var
|
|
4981
|
-
var
|
|
6011
|
+
var import_node_path26 = __toESM(require("path"), 1);
|
|
6012
|
+
var import_types15 = require("@neat.is/types");
|
|
4982
6013
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
4983
6014
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
4984
6015
|
function findAll(re, text) {
|
|
@@ -4999,7 +6030,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
4999
6030
|
seen.add(key);
|
|
5000
6031
|
const line = lineOf(file.content, topic);
|
|
5001
6032
|
out.push({
|
|
5002
|
-
infraId: (0,
|
|
6033
|
+
infraId: (0, import_types15.infraId)("kafka-topic", topic),
|
|
5003
6034
|
name: topic,
|
|
5004
6035
|
kind: "kafka-topic",
|
|
5005
6036
|
edgeType,
|
|
@@ -5008,7 +6039,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
5008
6039
|
// tier (ADR-066).
|
|
5009
6040
|
confidenceKind: "verified-call-site",
|
|
5010
6041
|
evidence: {
|
|
5011
|
-
file:
|
|
6042
|
+
file: import_node_path26.default.relative(serviceDir, file.path),
|
|
5012
6043
|
line,
|
|
5013
6044
|
snippet: snippet(file.content, line)
|
|
5014
6045
|
}
|
|
@@ -5021,8 +6052,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
5021
6052
|
|
|
5022
6053
|
// src/extract/calls/redis.ts
|
|
5023
6054
|
init_cjs_shims();
|
|
5024
|
-
var
|
|
5025
|
-
var
|
|
6055
|
+
var import_node_path27 = __toESM(require("path"), 1);
|
|
6056
|
+
var import_types16 = require("@neat.is/types");
|
|
5026
6057
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
5027
6058
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
5028
6059
|
const out = [];
|
|
@@ -5035,7 +6066,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
5035
6066
|
seen.add(host);
|
|
5036
6067
|
const line = lineOf(file.content, host);
|
|
5037
6068
|
out.push({
|
|
5038
|
-
infraId: (0,
|
|
6069
|
+
infraId: (0, import_types16.infraId)("redis", host),
|
|
5039
6070
|
name: host,
|
|
5040
6071
|
kind: "redis",
|
|
5041
6072
|
edgeType: "CALLS",
|
|
@@ -5044,7 +6075,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
5044
6075
|
// support tier (ADR-066).
|
|
5045
6076
|
confidenceKind: "url-with-structural-support",
|
|
5046
6077
|
evidence: {
|
|
5047
|
-
file:
|
|
6078
|
+
file: import_node_path27.default.relative(serviceDir, file.path),
|
|
5048
6079
|
line,
|
|
5049
6080
|
snippet: snippet(file.content, line)
|
|
5050
6081
|
}
|
|
@@ -5055,8 +6086,8 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
5055
6086
|
|
|
5056
6087
|
// src/extract/calls/aws.ts
|
|
5057
6088
|
init_cjs_shims();
|
|
5058
|
-
var
|
|
5059
|
-
var
|
|
6089
|
+
var import_node_path28 = __toESM(require("path"), 1);
|
|
6090
|
+
var import_types17 = require("@neat.is/types");
|
|
5060
6091
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
5061
6092
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
5062
6093
|
function hasMarker(text, markers) {
|
|
@@ -5080,7 +6111,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
5080
6111
|
seen.add(key);
|
|
5081
6112
|
const line = lineOf(file.content, name);
|
|
5082
6113
|
out.push({
|
|
5083
|
-
infraId: (0,
|
|
6114
|
+
infraId: (0, import_types17.infraId)(kind, name),
|
|
5084
6115
|
name,
|
|
5085
6116
|
kind,
|
|
5086
6117
|
edgeType: "CALLS",
|
|
@@ -5089,7 +6120,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
5089
6120
|
// (ADR-066).
|
|
5090
6121
|
confidenceKind: "verified-call-site",
|
|
5091
6122
|
evidence: {
|
|
5092
|
-
file:
|
|
6123
|
+
file: import_node_path28.default.relative(serviceDir, file.path),
|
|
5093
6124
|
line,
|
|
5094
6125
|
snippet: snippet(file.content, line)
|
|
5095
6126
|
}
|
|
@@ -5114,8 +6145,8 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
5114
6145
|
|
|
5115
6146
|
// src/extract/calls/grpc.ts
|
|
5116
6147
|
init_cjs_shims();
|
|
5117
|
-
var
|
|
5118
|
-
var
|
|
6148
|
+
var import_node_path29 = __toESM(require("path"), 1);
|
|
6149
|
+
var import_types18 = require("@neat.is/types");
|
|
5119
6150
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
5120
6151
|
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
5121
6152
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
@@ -5164,7 +6195,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
5164
6195
|
const { kind } = classified;
|
|
5165
6196
|
const line = lineOf(file.content, m[0]);
|
|
5166
6197
|
out.push({
|
|
5167
|
-
infraId: (0,
|
|
6198
|
+
infraId: (0, import_types18.infraId)(kind, name),
|
|
5168
6199
|
name,
|
|
5169
6200
|
kind,
|
|
5170
6201
|
edgeType: "CALLS",
|
|
@@ -5173,7 +6204,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
5173
6204
|
// tier (ADR-066).
|
|
5174
6205
|
confidenceKind: "verified-call-site",
|
|
5175
6206
|
evidence: {
|
|
5176
|
-
file:
|
|
6207
|
+
file: import_node_path29.default.relative(serviceDir, file.path),
|
|
5177
6208
|
line,
|
|
5178
6209
|
snippet: snippet(file.content, line)
|
|
5179
6210
|
}
|
|
@@ -5184,8 +6215,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
5184
6215
|
|
|
5185
6216
|
// src/extract/calls/supabase.ts
|
|
5186
6217
|
init_cjs_shims();
|
|
5187
|
-
var
|
|
5188
|
-
var
|
|
6218
|
+
var import_node_path30 = __toESM(require("path"), 1);
|
|
6219
|
+
var import_types19 = require("@neat.is/types");
|
|
5189
6220
|
var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
|
|
5190
6221
|
var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
|
|
5191
6222
|
var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
|
|
@@ -5223,7 +6254,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
5223
6254
|
seen.add(name);
|
|
5224
6255
|
const line = lineOf(file.content, m[0]);
|
|
5225
6256
|
out.push({
|
|
5226
|
-
infraId: (0,
|
|
6257
|
+
infraId: (0, import_types19.infraId)("supabase", name),
|
|
5227
6258
|
name,
|
|
5228
6259
|
kind: "supabase",
|
|
5229
6260
|
edgeType: "CALLS",
|
|
@@ -5233,7 +6264,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
5233
6264
|
// tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
|
|
5234
6265
|
confidenceKind: "verified-call-site",
|
|
5235
6266
|
evidence: {
|
|
5236
|
-
file:
|
|
6267
|
+
file: import_node_path30.default.relative(serviceDir, file.path),
|
|
5237
6268
|
line,
|
|
5238
6269
|
snippet: snippet(file.content, line)
|
|
5239
6270
|
}
|
|
@@ -5246,11 +6277,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
5246
6277
|
function edgeTypeFromEndpoint(ep) {
|
|
5247
6278
|
switch (ep.edgeType) {
|
|
5248
6279
|
case "PUBLISHES_TO":
|
|
5249
|
-
return
|
|
6280
|
+
return import_types20.EdgeType.PUBLISHES_TO;
|
|
5250
6281
|
case "CONSUMES_FROM":
|
|
5251
|
-
return
|
|
6282
|
+
return import_types20.EdgeType.CONSUMES_FROM;
|
|
5252
6283
|
default:
|
|
5253
|
-
return
|
|
6284
|
+
return import_types20.EdgeType.CALLS;
|
|
5254
6285
|
}
|
|
5255
6286
|
}
|
|
5256
6287
|
function isAwsKind(kind) {
|
|
@@ -5278,7 +6309,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
5278
6309
|
if (!graph.hasNode(ep.infraId)) {
|
|
5279
6310
|
const node = {
|
|
5280
6311
|
id: ep.infraId,
|
|
5281
|
-
type:
|
|
6312
|
+
type: import_types20.NodeType.InfraNode,
|
|
5282
6313
|
name: ep.name,
|
|
5283
6314
|
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
5284
6315
|
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
@@ -5290,7 +6321,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
5290
6321
|
nodesAdded++;
|
|
5291
6322
|
}
|
|
5292
6323
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
5293
|
-
const confidence = (0,
|
|
6324
|
+
const confidence = (0, import_types20.confidenceForExtracted)(ep.confidenceKind);
|
|
5294
6325
|
const relFile = toPosix2(ep.evidence.file);
|
|
5295
6326
|
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
5296
6327
|
graph,
|
|
@@ -5300,7 +6331,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
5300
6331
|
);
|
|
5301
6332
|
nodesAdded += n;
|
|
5302
6333
|
edgesAdded += e;
|
|
5303
|
-
if (!(0,
|
|
6334
|
+
if (!(0, import_types20.passesExtractedFloor)(confidence)) {
|
|
5304
6335
|
noteExtractedDropped({
|
|
5305
6336
|
source: fileNodeId,
|
|
5306
6337
|
target: ep.infraId,
|
|
@@ -5320,7 +6351,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
5320
6351
|
source: fileNodeId,
|
|
5321
6352
|
target: ep.infraId,
|
|
5322
6353
|
type: edgeType,
|
|
5323
|
-
provenance:
|
|
6354
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5324
6355
|
confidence,
|
|
5325
6356
|
evidence: ep.evidence
|
|
5326
6357
|
};
|
|
@@ -5334,9 +6365,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
5334
6365
|
async function addCallEdges(graph, services) {
|
|
5335
6366
|
const http = await addHttpCallEdges(graph, services);
|
|
5336
6367
|
const ext = await addExternalEndpointEdges(graph, services);
|
|
6368
|
+
const routes = await addRouteCallEdges(graph, services);
|
|
5337
6369
|
return {
|
|
5338
|
-
nodesAdded: http.nodesAdded + ext.nodesAdded,
|
|
5339
|
-
edgesAdded: http.edgesAdded + ext.edgesAdded
|
|
6370
|
+
nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
|
|
6371
|
+
edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
|
|
5340
6372
|
};
|
|
5341
6373
|
}
|
|
5342
6374
|
|
|
@@ -5345,16 +6377,16 @@ init_cjs_shims();
|
|
|
5345
6377
|
|
|
5346
6378
|
// src/extract/infra/docker-compose.ts
|
|
5347
6379
|
init_cjs_shims();
|
|
5348
|
-
var
|
|
5349
|
-
var
|
|
6380
|
+
var import_node_path31 = __toESM(require("path"), 1);
|
|
6381
|
+
var import_types22 = require("@neat.is/types");
|
|
5350
6382
|
|
|
5351
6383
|
// src/extract/infra/shared.ts
|
|
5352
6384
|
init_cjs_shims();
|
|
5353
|
-
var
|
|
6385
|
+
var import_types21 = require("@neat.is/types");
|
|
5354
6386
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
5355
6387
|
return {
|
|
5356
|
-
id: (0,
|
|
5357
|
-
type:
|
|
6388
|
+
id: (0, import_types21.infraId)(kind, name),
|
|
6389
|
+
type: import_types21.NodeType.InfraNode,
|
|
5358
6390
|
name,
|
|
5359
6391
|
provider,
|
|
5360
6392
|
kind,
|
|
@@ -5383,7 +6415,7 @@ function dependsOnList(value) {
|
|
|
5383
6415
|
}
|
|
5384
6416
|
function serviceNameToServiceNode(name, services) {
|
|
5385
6417
|
for (const s of services) {
|
|
5386
|
-
if (s.node.name === name ||
|
|
6418
|
+
if (s.node.name === name || import_node_path31.default.basename(s.dir) === name) return s.node.id;
|
|
5387
6419
|
}
|
|
5388
6420
|
return null;
|
|
5389
6421
|
}
|
|
@@ -5392,7 +6424,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
5392
6424
|
let edgesAdded = 0;
|
|
5393
6425
|
let composePath = null;
|
|
5394
6426
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
5395
|
-
const abs =
|
|
6427
|
+
const abs = import_node_path31.default.join(scanPath, name);
|
|
5396
6428
|
if (await exists(abs)) {
|
|
5397
6429
|
composePath = abs;
|
|
5398
6430
|
break;
|
|
@@ -5405,13 +6437,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
5405
6437
|
} catch (err) {
|
|
5406
6438
|
recordExtractionError(
|
|
5407
6439
|
"infra docker-compose",
|
|
5408
|
-
|
|
6440
|
+
import_node_path31.default.relative(scanPath, composePath),
|
|
5409
6441
|
err
|
|
5410
6442
|
);
|
|
5411
6443
|
return { nodesAdded, edgesAdded };
|
|
5412
6444
|
}
|
|
5413
6445
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
5414
|
-
const evidenceFile =
|
|
6446
|
+
const evidenceFile = import_node_path31.default.relative(scanPath, composePath).split(import_node_path31.default.sep).join("/");
|
|
5415
6447
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
5416
6448
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
5417
6449
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -5433,15 +6465,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
5433
6465
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
5434
6466
|
const targetId = composeNameToNodeId.get(dep);
|
|
5435
6467
|
if (!targetId) continue;
|
|
5436
|
-
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId,
|
|
6468
|
+
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types22.EdgeType.DEPENDS_ON);
|
|
5437
6469
|
if (graph.hasEdge(edgeId)) continue;
|
|
5438
6470
|
const edge = {
|
|
5439
6471
|
id: edgeId,
|
|
5440
6472
|
source: sourceId,
|
|
5441
6473
|
target: targetId,
|
|
5442
|
-
type:
|
|
5443
|
-
provenance:
|
|
5444
|
-
confidence: (0,
|
|
6474
|
+
type: import_types22.EdgeType.DEPENDS_ON,
|
|
6475
|
+
provenance: import_types22.Provenance.EXTRACTED,
|
|
6476
|
+
confidence: (0, import_types22.confidenceForExtracted)("structural"),
|
|
5445
6477
|
evidence: { file: evidenceFile }
|
|
5446
6478
|
};
|
|
5447
6479
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -5453,9 +6485,9 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
5453
6485
|
|
|
5454
6486
|
// src/extract/infra/dockerfile.ts
|
|
5455
6487
|
init_cjs_shims();
|
|
5456
|
-
var
|
|
5457
|
-
var
|
|
5458
|
-
var
|
|
6488
|
+
var import_node_path32 = __toESM(require("path"), 1);
|
|
6489
|
+
var import_node_fs16 = require("fs");
|
|
6490
|
+
var import_types23 = require("@neat.is/types");
|
|
5459
6491
|
function readDockerfile(content) {
|
|
5460
6492
|
let image = null;
|
|
5461
6493
|
const ports = [];
|
|
@@ -5484,15 +6516,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5484
6516
|
let nodesAdded = 0;
|
|
5485
6517
|
let edgesAdded = 0;
|
|
5486
6518
|
for (const service of services) {
|
|
5487
|
-
const dockerfilePath =
|
|
6519
|
+
const dockerfilePath = import_node_path32.default.join(service.dir, "Dockerfile");
|
|
5488
6520
|
if (!await exists(dockerfilePath)) continue;
|
|
5489
6521
|
let content;
|
|
5490
6522
|
try {
|
|
5491
|
-
content = await
|
|
6523
|
+
content = await import_node_fs16.promises.readFile(dockerfilePath, "utf8");
|
|
5492
6524
|
} catch (err) {
|
|
5493
6525
|
recordExtractionError(
|
|
5494
6526
|
"infra dockerfile",
|
|
5495
|
-
|
|
6527
|
+
import_node_path32.default.relative(scanPath, dockerfilePath),
|
|
5496
6528
|
err
|
|
5497
6529
|
);
|
|
5498
6530
|
continue;
|
|
@@ -5504,8 +6536,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5504
6536
|
graph.addNode(node.id, node);
|
|
5505
6537
|
nodesAdded++;
|
|
5506
6538
|
}
|
|
5507
|
-
const relDockerfile = toPosix2(
|
|
5508
|
-
const evidenceFile = toPosix2(
|
|
6539
|
+
const relDockerfile = toPosix2(import_node_path32.default.relative(service.dir, dockerfilePath));
|
|
6540
|
+
const evidenceFile = toPosix2(import_node_path32.default.relative(scanPath, dockerfilePath));
|
|
5509
6541
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5510
6542
|
graph,
|
|
5511
6543
|
service.pkg.name,
|
|
@@ -5514,15 +6546,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5514
6546
|
);
|
|
5515
6547
|
nodesAdded += fn;
|
|
5516
6548
|
edgesAdded += fe;
|
|
5517
|
-
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id,
|
|
6549
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, node.id, import_types23.EdgeType.RUNS_ON);
|
|
5518
6550
|
if (!graph.hasEdge(edgeId)) {
|
|
5519
6551
|
const edge = {
|
|
5520
6552
|
id: edgeId,
|
|
5521
6553
|
source: fileNodeId,
|
|
5522
6554
|
target: node.id,
|
|
5523
|
-
type:
|
|
5524
|
-
provenance:
|
|
5525
|
-
confidence: (0,
|
|
6555
|
+
type: import_types23.EdgeType.RUNS_ON,
|
|
6556
|
+
provenance: import_types23.Provenance.EXTRACTED,
|
|
6557
|
+
confidence: (0, import_types23.confidenceForExtracted)("structural"),
|
|
5526
6558
|
evidence: {
|
|
5527
6559
|
file: evidenceFile,
|
|
5528
6560
|
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
@@ -5537,15 +6569,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5537
6569
|
graph.addNode(portNode.id, portNode);
|
|
5538
6570
|
nodesAdded++;
|
|
5539
6571
|
}
|
|
5540
|
-
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id,
|
|
6572
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types23.EdgeType.CONNECTS_TO);
|
|
5541
6573
|
if (graph.hasEdge(portEdgeId)) continue;
|
|
5542
6574
|
const portEdge = {
|
|
5543
6575
|
id: portEdgeId,
|
|
5544
6576
|
source: fileNodeId,
|
|
5545
6577
|
target: portNode.id,
|
|
5546
|
-
type:
|
|
5547
|
-
provenance:
|
|
5548
|
-
confidence: (0,
|
|
6578
|
+
type: import_types23.EdgeType.CONNECTS_TO,
|
|
6579
|
+
provenance: import_types23.Provenance.EXTRACTED,
|
|
6580
|
+
confidence: (0, import_types23.confidenceForExtracted)("structural"),
|
|
5549
6581
|
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5550
6582
|
};
|
|
5551
6583
|
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
@@ -5557,23 +6589,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5557
6589
|
|
|
5558
6590
|
// src/extract/infra/terraform.ts
|
|
5559
6591
|
init_cjs_shims();
|
|
5560
|
-
var
|
|
5561
|
-
var
|
|
5562
|
-
var
|
|
6592
|
+
var import_node_fs17 = require("fs");
|
|
6593
|
+
var import_node_path33 = __toESM(require("path"), 1);
|
|
6594
|
+
var import_types24 = require("@neat.is/types");
|
|
5563
6595
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5564
6596
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5565
6597
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5566
6598
|
if (depth > max) return [];
|
|
5567
6599
|
const out = [];
|
|
5568
|
-
const entries = await
|
|
6600
|
+
const entries = await import_node_fs17.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
5569
6601
|
for (const entry2 of entries) {
|
|
5570
6602
|
if (entry2.isDirectory()) {
|
|
5571
6603
|
if (IGNORED_DIRS.has(entry2.name) || entry2.name === ".terraform") continue;
|
|
5572
|
-
const child =
|
|
6604
|
+
const child = import_node_path33.default.join(start, entry2.name);
|
|
5573
6605
|
if (await isPythonVenvDir(child)) continue;
|
|
5574
6606
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
5575
6607
|
} else if (entry2.isFile() && entry2.name.endsWith(".tf")) {
|
|
5576
|
-
out.push(
|
|
6608
|
+
out.push(import_node_path33.default.join(start, entry2.name));
|
|
5577
6609
|
}
|
|
5578
6610
|
}
|
|
5579
6611
|
return out;
|
|
@@ -5592,7 +6624,7 @@ function blockBody(content, from) {
|
|
|
5592
6624
|
}
|
|
5593
6625
|
return null;
|
|
5594
6626
|
}
|
|
5595
|
-
function
|
|
6627
|
+
function lineAt2(content, index) {
|
|
5596
6628
|
let line = 1;
|
|
5597
6629
|
for (let i = 0; i < index && i < content.length; i++) {
|
|
5598
6630
|
if (content[i] === "\n") line++;
|
|
@@ -5604,8 +6636,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
5604
6636
|
let edgesAdded = 0;
|
|
5605
6637
|
const files = await walkTfFiles(scanPath);
|
|
5606
6638
|
for (const file of files) {
|
|
5607
|
-
const content = await
|
|
5608
|
-
const evidenceFile = toPosix2(
|
|
6639
|
+
const content = await import_node_fs17.promises.readFile(file, "utf8");
|
|
6640
|
+
const evidenceFile = toPosix2(import_node_path33.default.relative(scanPath, file));
|
|
5609
6641
|
const resources = [];
|
|
5610
6642
|
const byKey = /* @__PURE__ */ new Map();
|
|
5611
6643
|
RESOURCE_RE.lastIndex = 0;
|
|
@@ -5640,16 +6672,16 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
5640
6672
|
if (!target) continue;
|
|
5641
6673
|
if (seen.has(target.nodeId)) continue;
|
|
5642
6674
|
seen.add(target.nodeId);
|
|
5643
|
-
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId,
|
|
6675
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types24.EdgeType.DEPENDS_ON);
|
|
5644
6676
|
if (graph.hasEdge(edgeId)) continue;
|
|
5645
|
-
const line =
|
|
6677
|
+
const line = lineAt2(content, resource.bodyOffset + ref.index);
|
|
5646
6678
|
const edge = {
|
|
5647
6679
|
id: edgeId,
|
|
5648
6680
|
source: resource.nodeId,
|
|
5649
6681
|
target: target.nodeId,
|
|
5650
|
-
type:
|
|
5651
|
-
provenance:
|
|
5652
|
-
confidence: (0,
|
|
6682
|
+
type: import_types24.EdgeType.DEPENDS_ON,
|
|
6683
|
+
provenance: import_types24.Provenance.EXTRACTED,
|
|
6684
|
+
confidence: (0, import_types24.confidenceForExtracted)("structural"),
|
|
5653
6685
|
evidence: { file: evidenceFile, line, snippet: key }
|
|
5654
6686
|
};
|
|
5655
6687
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -5662,8 +6694,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
5662
6694
|
|
|
5663
6695
|
// src/extract/infra/k8s.ts
|
|
5664
6696
|
init_cjs_shims();
|
|
5665
|
-
var
|
|
5666
|
-
var
|
|
6697
|
+
var import_node_fs18 = require("fs");
|
|
6698
|
+
var import_node_path34 = __toESM(require("path"), 1);
|
|
5667
6699
|
var import_yaml3 = require("yaml");
|
|
5668
6700
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
5669
6701
|
Service: "k8s-service",
|
|
@@ -5677,15 +6709,15 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
5677
6709
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
5678
6710
|
if (depth > max) return [];
|
|
5679
6711
|
const out = [];
|
|
5680
|
-
const entries = await
|
|
6712
|
+
const entries = await import_node_fs18.promises.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
5681
6713
|
for (const entry2 of entries) {
|
|
5682
6714
|
if (entry2.isDirectory()) {
|
|
5683
6715
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
5684
|
-
const child =
|
|
6716
|
+
const child = import_node_path34.default.join(start, entry2.name);
|
|
5685
6717
|
if (await isPythonVenvDir(child)) continue;
|
|
5686
6718
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
5687
|
-
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
5688
|
-
out.push(
|
|
6719
|
+
} else if (entry2.isFile() && CONFIG_FILE_EXTENSIONS.has(import_node_path34.default.extname(entry2.name))) {
|
|
6720
|
+
out.push(import_node_path34.default.join(start, entry2.name));
|
|
5689
6721
|
}
|
|
5690
6722
|
}
|
|
5691
6723
|
return out;
|
|
@@ -5694,7 +6726,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
5694
6726
|
let nodesAdded = 0;
|
|
5695
6727
|
const files = await walkYamlFiles2(scanPath);
|
|
5696
6728
|
for (const file of files) {
|
|
5697
|
-
const content = await
|
|
6729
|
+
const content = await import_node_fs18.promises.readFile(file, "utf8");
|
|
5698
6730
|
let docs;
|
|
5699
6731
|
try {
|
|
5700
6732
|
docs = (0, import_yaml3.parseAllDocuments)(content).map((d) => d.toJSON());
|
|
@@ -5729,17 +6761,17 @@ async function addInfra(graph, scanPath, services) {
|
|
|
5729
6761
|
}
|
|
5730
6762
|
|
|
5731
6763
|
// src/extract/index.ts
|
|
5732
|
-
var
|
|
6764
|
+
var import_node_path36 = __toESM(require("path"), 1);
|
|
5733
6765
|
|
|
5734
6766
|
// src/extract/retire.ts
|
|
5735
6767
|
init_cjs_shims();
|
|
5736
|
-
var
|
|
5737
|
-
var
|
|
5738
|
-
var
|
|
6768
|
+
var import_node_fs19 = require("fs");
|
|
6769
|
+
var import_node_path35 = __toESM(require("path"), 1);
|
|
6770
|
+
var import_types25 = require("@neat.is/types");
|
|
5739
6771
|
function dropOrphanedFileNodes(graph) {
|
|
5740
6772
|
const orphans = [];
|
|
5741
6773
|
graph.forEachNode((id, attrs) => {
|
|
5742
|
-
if (attrs.type !==
|
|
6774
|
+
if (attrs.type !== import_types25.NodeType.FileNode) return;
|
|
5743
6775
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5744
6776
|
orphans.push(id);
|
|
5745
6777
|
}
|
|
@@ -5752,14 +6784,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5752
6784
|
const bases = [scanPath, ...serviceDirs];
|
|
5753
6785
|
graph.forEachEdge((id, attrs) => {
|
|
5754
6786
|
const edge = attrs;
|
|
5755
|
-
if (edge.provenance !==
|
|
6787
|
+
if (edge.provenance !== import_types25.Provenance.EXTRACTED) return;
|
|
5756
6788
|
const evidenceFile = edge.evidence?.file;
|
|
5757
6789
|
if (!evidenceFile) return;
|
|
5758
|
-
if (
|
|
5759
|
-
if (!(0,
|
|
6790
|
+
if (import_node_path35.default.isAbsolute(evidenceFile)) {
|
|
6791
|
+
if (!(0, import_node_fs19.existsSync)(evidenceFile)) toDrop.push(id);
|
|
5760
6792
|
return;
|
|
5761
6793
|
}
|
|
5762
|
-
const found = bases.some((base) => (0,
|
|
6794
|
+
const found = bases.some((base) => (0, import_node_fs19.existsSync)(import_node_path35.default.join(base, evidenceFile)));
|
|
5763
6795
|
if (!found) toDrop.push(id);
|
|
5764
6796
|
});
|
|
5765
6797
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -5778,6 +6810,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5778
6810
|
const importGraph = await addImports(graph, services);
|
|
5779
6811
|
const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
|
|
5780
6812
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
6813
|
+
const routePhase = await addRoutes(graph, services);
|
|
6814
|
+
const grpcPhase = await addGrpcMethods(graph, services);
|
|
5781
6815
|
const phase4 = await addCallEdges(graph, services);
|
|
5782
6816
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
5783
6817
|
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
@@ -5799,7 +6833,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5799
6833
|
}
|
|
5800
6834
|
const droppedEntries = drainDroppedExtracted();
|
|
5801
6835
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
5802
|
-
const rejectedPath =
|
|
6836
|
+
const rejectedPath = import_node_path36.default.join(import_node_path36.default.dirname(opts.errorsPath), "rejected.ndjson");
|
|
5803
6837
|
try {
|
|
5804
6838
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
5805
6839
|
} catch (err) {
|
|
@@ -5809,8 +6843,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5809
6843
|
}
|
|
5810
6844
|
}
|
|
5811
6845
|
const result = {
|
|
5812
|
-
nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
5813
|
-
edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
6846
|
+
nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
6847
|
+
edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
5814
6848
|
frontiersPromoted,
|
|
5815
6849
|
extractionErrors: errorEntries.length,
|
|
5816
6850
|
errorEntries,
|
|
@@ -5833,9 +6867,9 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5833
6867
|
|
|
5834
6868
|
// src/persist.ts
|
|
5835
6869
|
init_cjs_shims();
|
|
5836
|
-
var
|
|
5837
|
-
var
|
|
5838
|
-
var
|
|
6870
|
+
var import_node_fs20 = require("fs");
|
|
6871
|
+
var import_node_path37 = __toESM(require("path"), 1);
|
|
6872
|
+
var import_types26 = require("@neat.is/types");
|
|
5839
6873
|
var SCHEMA_VERSION = 4;
|
|
5840
6874
|
function migrateV1ToV2(payload) {
|
|
5841
6875
|
const nodes = payload.graph.nodes;
|
|
@@ -5857,12 +6891,12 @@ function migrateV2ToV3(payload) {
|
|
|
5857
6891
|
for (const edge of edges) {
|
|
5858
6892
|
const attrs = edge.attributes;
|
|
5859
6893
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5860
|
-
attrs.provenance =
|
|
6894
|
+
attrs.provenance = import_types26.Provenance.OBSERVED;
|
|
5861
6895
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5862
6896
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5863
6897
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5864
6898
|
if (type && source && target) {
|
|
5865
|
-
const newId = (0,
|
|
6899
|
+
const newId = (0, import_types26.observedEdgeId)(source, target, type);
|
|
5866
6900
|
attrs.id = newId;
|
|
5867
6901
|
if (edge.key) edge.key = newId;
|
|
5868
6902
|
}
|
|
@@ -5871,7 +6905,7 @@ function migrateV2ToV3(payload) {
|
|
|
5871
6905
|
return { ...payload, schemaVersion: 3 };
|
|
5872
6906
|
}
|
|
5873
6907
|
async function ensureDir(filePath) {
|
|
5874
|
-
await
|
|
6908
|
+
await import_node_fs20.promises.mkdir(import_node_path37.default.dirname(filePath), { recursive: true });
|
|
5875
6909
|
}
|
|
5876
6910
|
async function saveGraphToDisk(graph, outPath) {
|
|
5877
6911
|
await ensureDir(outPath);
|
|
@@ -5881,13 +6915,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
5881
6915
|
graph: graph.export()
|
|
5882
6916
|
};
|
|
5883
6917
|
const tmp = `${outPath}.tmp`;
|
|
5884
|
-
await
|
|
5885
|
-
await
|
|
6918
|
+
await import_node_fs20.promises.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
6919
|
+
await import_node_fs20.promises.rename(tmp, outPath);
|
|
5886
6920
|
}
|
|
5887
6921
|
async function loadGraphFromDisk(graph, outPath) {
|
|
5888
6922
|
let raw;
|
|
5889
6923
|
try {
|
|
5890
|
-
raw = await
|
|
6924
|
+
raw = await import_node_fs20.promises.readFile(outPath, "utf8");
|
|
5891
6925
|
} catch (err) {
|
|
5892
6926
|
if (err.code === "ENOENT") return;
|
|
5893
6927
|
throw err;
|
|
@@ -5952,23 +6986,23 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
5952
6986
|
|
|
5953
6987
|
// src/projects.ts
|
|
5954
6988
|
init_cjs_shims();
|
|
5955
|
-
var
|
|
6989
|
+
var import_node_path38 = __toESM(require("path"), 1);
|
|
5956
6990
|
function pathsForProject(project, baseDir) {
|
|
5957
6991
|
if (project === DEFAULT_PROJECT) {
|
|
5958
6992
|
return {
|
|
5959
|
-
snapshotPath:
|
|
5960
|
-
errorsPath:
|
|
5961
|
-
staleEventsPath:
|
|
5962
|
-
embeddingsCachePath:
|
|
5963
|
-
policyViolationsPath:
|
|
6993
|
+
snapshotPath: import_node_path38.default.join(baseDir, "graph.json"),
|
|
6994
|
+
errorsPath: import_node_path38.default.join(baseDir, "errors.ndjson"),
|
|
6995
|
+
staleEventsPath: import_node_path38.default.join(baseDir, "stale-events.ndjson"),
|
|
6996
|
+
embeddingsCachePath: import_node_path38.default.join(baseDir, "embeddings.json"),
|
|
6997
|
+
policyViolationsPath: import_node_path38.default.join(baseDir, "policy-violations.ndjson")
|
|
5964
6998
|
};
|
|
5965
6999
|
}
|
|
5966
7000
|
return {
|
|
5967
|
-
snapshotPath:
|
|
5968
|
-
errorsPath:
|
|
5969
|
-
staleEventsPath:
|
|
5970
|
-
embeddingsCachePath:
|
|
5971
|
-
policyViolationsPath:
|
|
7001
|
+
snapshotPath: import_node_path38.default.join(baseDir, `${project}.json`),
|
|
7002
|
+
errorsPath: import_node_path38.default.join(baseDir, `errors.${project}.ndjson`),
|
|
7003
|
+
staleEventsPath: import_node_path38.default.join(baseDir, `stale-events.${project}.ndjson`),
|
|
7004
|
+
embeddingsCachePath: import_node_path38.default.join(baseDir, `embeddings.${project}.json`),
|
|
7005
|
+
policyViolationsPath: import_node_path38.default.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
5972
7006
|
};
|
|
5973
7007
|
}
|
|
5974
7008
|
var Projects = class {
|
|
@@ -6006,19 +7040,19 @@ var Projects = class {
|
|
|
6006
7040
|
init_cjs_shims();
|
|
6007
7041
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
6008
7042
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
6009
|
-
var
|
|
7043
|
+
var import_types29 = require("@neat.is/types");
|
|
6010
7044
|
|
|
6011
7045
|
// src/extend/index.ts
|
|
6012
7046
|
init_cjs_shims();
|
|
6013
|
-
var
|
|
6014
|
-
var
|
|
7047
|
+
var import_node_fs22 = require("fs");
|
|
7048
|
+
var import_node_path40 = __toESM(require("path"), 1);
|
|
6015
7049
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
6016
7050
|
var import_instrumentation_registry = require("@neat.is/instrumentation-registry");
|
|
6017
7051
|
|
|
6018
7052
|
// src/installers/package-manager.ts
|
|
6019
7053
|
init_cjs_shims();
|
|
6020
|
-
var
|
|
6021
|
-
var
|
|
7054
|
+
var import_node_fs21 = require("fs");
|
|
7055
|
+
var import_node_path39 = __toESM(require("path"), 1);
|
|
6022
7056
|
var import_node_child_process = require("child_process");
|
|
6023
7057
|
var LOCKFILE_PRIORITY = [
|
|
6024
7058
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -6033,29 +7067,29 @@ var LOCKFILE_PRIORITY = [
|
|
|
6033
7067
|
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
6034
7068
|
async function exists2(p) {
|
|
6035
7069
|
try {
|
|
6036
|
-
await
|
|
7070
|
+
await import_node_fs21.promises.access(p);
|
|
6037
7071
|
return true;
|
|
6038
7072
|
} catch {
|
|
6039
7073
|
return false;
|
|
6040
7074
|
}
|
|
6041
7075
|
}
|
|
6042
7076
|
async function detectPackageManager(serviceDir) {
|
|
6043
|
-
let dir =
|
|
7077
|
+
let dir = import_node_path39.default.resolve(serviceDir);
|
|
6044
7078
|
const stops = /* @__PURE__ */ new Set();
|
|
6045
7079
|
for (let i = 0; i < 64; i++) {
|
|
6046
7080
|
if (stops.has(dir)) break;
|
|
6047
7081
|
stops.add(dir);
|
|
6048
7082
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
6049
|
-
const lockPath =
|
|
7083
|
+
const lockPath = import_node_path39.default.join(dir, candidate.lockfile);
|
|
6050
7084
|
if (await exists2(lockPath)) {
|
|
6051
7085
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
6052
7086
|
}
|
|
6053
7087
|
}
|
|
6054
|
-
const parent =
|
|
7088
|
+
const parent = import_node_path39.default.dirname(dir);
|
|
6055
7089
|
if (parent === dir) break;
|
|
6056
7090
|
dir = parent;
|
|
6057
7091
|
}
|
|
6058
|
-
return { pm: "npm", cwd:
|
|
7092
|
+
return { pm: "npm", cwd: import_node_path39.default.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
6059
7093
|
}
|
|
6060
7094
|
async function runPackageManagerInstall(cmd) {
|
|
6061
7095
|
return new Promise((resolve) => {
|
|
@@ -6097,30 +7131,30 @@ ${err.message}`
|
|
|
6097
7131
|
// src/extend/index.ts
|
|
6098
7132
|
async function fileExists2(p) {
|
|
6099
7133
|
try {
|
|
6100
|
-
await
|
|
7134
|
+
await import_node_fs22.promises.access(p);
|
|
6101
7135
|
return true;
|
|
6102
7136
|
} catch {
|
|
6103
7137
|
return false;
|
|
6104
7138
|
}
|
|
6105
7139
|
}
|
|
6106
7140
|
async function readPackageJson(scanPath) {
|
|
6107
|
-
const pkgPath =
|
|
6108
|
-
const raw = await
|
|
7141
|
+
const pkgPath = import_node_path40.default.join(scanPath, "package.json");
|
|
7142
|
+
const raw = await import_node_fs22.promises.readFile(pkgPath, "utf8");
|
|
6109
7143
|
return JSON.parse(raw);
|
|
6110
7144
|
}
|
|
6111
7145
|
async function findHookFiles(scanPath) {
|
|
6112
|
-
const entries = await
|
|
7146
|
+
const entries = await import_node_fs22.promises.readdir(scanPath);
|
|
6113
7147
|
return entries.filter(
|
|
6114
7148
|
(e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
|
|
6115
7149
|
).sort();
|
|
6116
7150
|
}
|
|
6117
7151
|
function extendLogPath() {
|
|
6118
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
7152
|
+
return process.env.NEAT_EXTEND_LOG ?? import_node_path40.default.join(import_node_os2.default.homedir(), ".neat", "extend-log.ndjson");
|
|
6119
7153
|
}
|
|
6120
7154
|
async function appendExtendLog(entry2) {
|
|
6121
7155
|
const logPath = extendLogPath();
|
|
6122
|
-
await
|
|
6123
|
-
await
|
|
7156
|
+
await import_node_fs22.promises.mkdir(import_node_path40.default.dirname(logPath), { recursive: true });
|
|
7157
|
+
await import_node_fs22.promises.appendFile(logPath, JSON.stringify(entry2) + "\n", "utf8");
|
|
6124
7158
|
}
|
|
6125
7159
|
function splicedContent(fileContent, snippet2) {
|
|
6126
7160
|
if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
|
|
@@ -6178,7 +7212,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
6178
7212
|
}
|
|
6179
7213
|
async function describeProjectInstrumentation(ctx) {
|
|
6180
7214
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
6181
|
-
const envNeat = await fileExists2(
|
|
7215
|
+
const envNeat = await fileExists2(import_node_path40.default.join(ctx.scanPath, ".env.neat"));
|
|
6182
7216
|
const registryInstrPackages = new Set(
|
|
6183
7217
|
(0, import_instrumentation_registry.list)().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
6184
7218
|
);
|
|
@@ -6200,31 +7234,31 @@ async function applyExtension(ctx, args, options) {
|
|
|
6200
7234
|
);
|
|
6201
7235
|
}
|
|
6202
7236
|
for (const file of hookFiles) {
|
|
6203
|
-
const content = await
|
|
7237
|
+
const content = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
|
|
6204
7238
|
if (content.includes(args.registration_snippet)) {
|
|
6205
7239
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
6206
7240
|
}
|
|
6207
7241
|
}
|
|
6208
7242
|
const primaryFile = hookFiles[0];
|
|
6209
|
-
const primaryPath =
|
|
7243
|
+
const primaryPath = import_node_path40.default.join(ctx.scanPath, primaryFile);
|
|
6210
7244
|
const filesTouched = [];
|
|
6211
7245
|
const depsAdded = [];
|
|
6212
|
-
const pkgPath =
|
|
7246
|
+
const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
|
|
6213
7247
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
6214
7248
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
6215
7249
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
6216
|
-
await
|
|
7250
|
+
await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6217
7251
|
filesTouched.push("package.json");
|
|
6218
7252
|
depsAdded.push(`${args.instrumentation_package}@${args.version}`);
|
|
6219
7253
|
}
|
|
6220
|
-
const hookContent = await
|
|
7254
|
+
const hookContent = await import_node_fs22.promises.readFile(primaryPath, "utf8");
|
|
6221
7255
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
6222
7256
|
if (!patched) {
|
|
6223
7257
|
throw new Error(
|
|
6224
7258
|
`Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
|
|
6225
7259
|
);
|
|
6226
7260
|
}
|
|
6227
|
-
await
|
|
7261
|
+
await import_node_fs22.promises.writeFile(primaryPath, patched, "utf8");
|
|
6228
7262
|
filesTouched.push(primaryFile);
|
|
6229
7263
|
const cmd = await detectPackageManager(ctx.scanPath);
|
|
6230
7264
|
const installer = options?.runInstall ?? runPackageManagerInstall;
|
|
@@ -6255,7 +7289,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
6255
7289
|
};
|
|
6256
7290
|
}
|
|
6257
7291
|
for (const file of hookFiles) {
|
|
6258
|
-
const content = await
|
|
7292
|
+
const content = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, file), "utf8");
|
|
6259
7293
|
if (content.includes(args.registration_snippet)) {
|
|
6260
7294
|
return {
|
|
6261
7295
|
library: args.library,
|
|
@@ -6277,7 +7311,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
6277
7311
|
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
6278
7312
|
filesTouched.push("package.json");
|
|
6279
7313
|
}
|
|
6280
|
-
const hookContent = await
|
|
7314
|
+
const hookContent = await import_node_fs22.promises.readFile(import_node_path40.default.join(ctx.scanPath, primaryFile), "utf8");
|
|
6281
7315
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
6282
7316
|
if (patched) {
|
|
6283
7317
|
filesTouched.push(primaryFile);
|
|
@@ -6292,28 +7326,28 @@ async function rollbackExtension(ctx, args) {
|
|
|
6292
7326
|
if (!await fileExists2(logPath)) {
|
|
6293
7327
|
return { undone: false, message: "no apply found for library" };
|
|
6294
7328
|
}
|
|
6295
|
-
const raw = await
|
|
7329
|
+
const raw = await import_node_fs22.promises.readFile(logPath, "utf8");
|
|
6296
7330
|
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
6297
7331
|
const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
|
|
6298
7332
|
if (!match) {
|
|
6299
7333
|
return { undone: false, message: "no apply found for library" };
|
|
6300
7334
|
}
|
|
6301
|
-
const pkgPath =
|
|
7335
|
+
const pkgPath = import_node_path40.default.join(ctx.scanPath, "package.json");
|
|
6302
7336
|
if (await fileExists2(pkgPath)) {
|
|
6303
7337
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
6304
7338
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
6305
7339
|
const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
|
|
6306
7340
|
pkg.dependencies = rest;
|
|
6307
|
-
await
|
|
7341
|
+
await import_node_fs22.promises.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6308
7342
|
}
|
|
6309
7343
|
}
|
|
6310
7344
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
6311
7345
|
for (const file of hookFiles) {
|
|
6312
|
-
const filePath =
|
|
6313
|
-
const content = await
|
|
7346
|
+
const filePath = import_node_path40.default.join(ctx.scanPath, file);
|
|
7347
|
+
const content = await import_node_fs22.promises.readFile(filePath, "utf8");
|
|
6314
7348
|
if (content.includes(match.registration_snippet)) {
|
|
6315
7349
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
6316
|
-
await
|
|
7350
|
+
await import_node_fs22.promises.writeFile(filePath, filtered, "utf8");
|
|
6317
7351
|
break;
|
|
6318
7352
|
}
|
|
6319
7353
|
}
|
|
@@ -6325,7 +7359,7 @@ async function rollbackExtension(ctx, args) {
|
|
|
6325
7359
|
|
|
6326
7360
|
// src/divergences.ts
|
|
6327
7361
|
init_cjs_shims();
|
|
6328
|
-
var
|
|
7362
|
+
var import_types27 = require("@neat.is/types");
|
|
6329
7363
|
function bucketKey(source, target, type) {
|
|
6330
7364
|
return `${type}|${source}|${target}`;
|
|
6331
7365
|
}
|
|
@@ -6333,22 +7367,22 @@ function bucketEdges(graph) {
|
|
|
6333
7367
|
const buckets = /* @__PURE__ */ new Map();
|
|
6334
7368
|
graph.forEachEdge((id, attrs) => {
|
|
6335
7369
|
const e = attrs;
|
|
6336
|
-
const parsed = (0,
|
|
7370
|
+
const parsed = (0, import_types27.parseEdgeId)(id);
|
|
6337
7371
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
6338
7372
|
const key = bucketKey(e.source, e.target, e.type);
|
|
6339
7373
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
6340
7374
|
switch (provenance) {
|
|
6341
|
-
case
|
|
7375
|
+
case import_types27.Provenance.EXTRACTED:
|
|
6342
7376
|
cur.extracted = e;
|
|
6343
7377
|
break;
|
|
6344
|
-
case
|
|
7378
|
+
case import_types27.Provenance.OBSERVED:
|
|
6345
7379
|
cur.observed = e;
|
|
6346
7380
|
break;
|
|
6347
|
-
case
|
|
7381
|
+
case import_types27.Provenance.INFERRED:
|
|
6348
7382
|
cur.inferred = e;
|
|
6349
7383
|
break;
|
|
6350
7384
|
default:
|
|
6351
|
-
if (e.provenance ===
|
|
7385
|
+
if (e.provenance === import_types27.Provenance.STALE) cur.stale = e;
|
|
6352
7386
|
}
|
|
6353
7387
|
buckets.set(key, cur);
|
|
6354
7388
|
});
|
|
@@ -6357,7 +7391,12 @@ function bucketEdges(graph) {
|
|
|
6357
7391
|
function nodeIsFrontier(graph, nodeId) {
|
|
6358
7392
|
if (!graph.hasNode(nodeId)) return false;
|
|
6359
7393
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
6360
|
-
return attrs.type ===
|
|
7394
|
+
return attrs.type === import_types27.NodeType.FrontierNode;
|
|
7395
|
+
}
|
|
7396
|
+
function nodeIsWebsocketChannel(graph, nodeId) {
|
|
7397
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
7398
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
7399
|
+
return attrs.type === import_types27.NodeType.WebSocketChannelNode;
|
|
6361
7400
|
}
|
|
6362
7401
|
function clampConfidence(n) {
|
|
6363
7402
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -6377,14 +7416,14 @@ function gradedConfidence(edge) {
|
|
|
6377
7416
|
return clampConfidence(confidenceForEdge(edge));
|
|
6378
7417
|
}
|
|
6379
7418
|
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
7419
|
+
import_types27.EdgeType.CALLS,
|
|
7420
|
+
import_types27.EdgeType.CONNECTS_TO,
|
|
7421
|
+
import_types27.EdgeType.PUBLISHES_TO,
|
|
7422
|
+
import_types27.EdgeType.CONSUMES_FROM
|
|
6384
7423
|
]);
|
|
6385
7424
|
function detectMissingDivergences(graph, bucket) {
|
|
6386
7425
|
const out = [];
|
|
6387
|
-
if (bucket.type ===
|
|
7426
|
+
if (bucket.type === import_types27.EdgeType.CONTAINS) return out;
|
|
6388
7427
|
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
6389
7428
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
6390
7429
|
out.push({
|
|
@@ -6399,7 +7438,7 @@ function detectMissingDivergences(graph, bucket) {
|
|
|
6399
7438
|
});
|
|
6400
7439
|
}
|
|
6401
7440
|
}
|
|
6402
|
-
if (bucket.observed && !bucket.extracted) {
|
|
7441
|
+
if (bucket.observed && !bucket.extracted && !nodeIsWebsocketChannel(graph, bucket.target)) {
|
|
6403
7442
|
out.push({
|
|
6404
7443
|
type: "missing-extracted",
|
|
6405
7444
|
source: bucket.source,
|
|
@@ -6425,7 +7464,7 @@ function declaredHostFor(svc) {
|
|
|
6425
7464
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
6426
7465
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
6427
7466
|
const e = graph.getEdgeAttributes(edgeId);
|
|
6428
|
-
if (e.type ===
|
|
7467
|
+
if (e.type === import_types27.EdgeType.CONFIGURED_BY && e.provenance === import_types27.Provenance.EXTRACTED) {
|
|
6429
7468
|
return true;
|
|
6430
7469
|
}
|
|
6431
7470
|
}
|
|
@@ -6438,10 +7477,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
6438
7477
|
const out = [];
|
|
6439
7478
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
6440
7479
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
6441
|
-
if (edge.type !==
|
|
6442
|
-
if (edge.provenance !==
|
|
7480
|
+
if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
|
|
7481
|
+
if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
|
|
6443
7482
|
const target = graph.getNodeAttributes(edge.target);
|
|
6444
|
-
if (target.type !==
|
|
7483
|
+
if (target.type !== import_types27.NodeType.DatabaseNode) continue;
|
|
6445
7484
|
const observedHost = target.host?.trim();
|
|
6446
7485
|
if (!observedHost) continue;
|
|
6447
7486
|
if (observedHost === declaredHost) continue;
|
|
@@ -6463,10 +7502,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
6463
7502
|
const deps = svc.dependencies ?? {};
|
|
6464
7503
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
6465
7504
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
6466
|
-
if (edge.type !==
|
|
6467
|
-
if (edge.provenance !==
|
|
7505
|
+
if (edge.type !== import_types27.EdgeType.CONNECTS_TO) continue;
|
|
7506
|
+
if (edge.provenance !== import_types27.Provenance.OBSERVED) continue;
|
|
6468
7507
|
const target = graph.getNodeAttributes(edge.target);
|
|
6469
|
-
if (target.type !==
|
|
7508
|
+
if (target.type !== import_types27.NodeType.DatabaseNode) continue;
|
|
6470
7509
|
for (const pair of compatPairs()) {
|
|
6471
7510
|
if (pair.engine !== target.engine) continue;
|
|
6472
7511
|
const declared = deps[pair.driver];
|
|
@@ -6525,7 +7564,7 @@ function suppressHostMismatchHalves(all) {
|
|
|
6525
7564
|
for (const d of all) {
|
|
6526
7565
|
if (d.type !== "host-mismatch") continue;
|
|
6527
7566
|
observedHalf.add(`${d.source}->${d.target}`);
|
|
6528
|
-
declaredHalf.add((0,
|
|
7567
|
+
declaredHalf.add((0, import_types27.databaseId)(d.extractedHost));
|
|
6529
7568
|
}
|
|
6530
7569
|
if (observedHalf.size === 0) return all;
|
|
6531
7570
|
return all.filter((d) => {
|
|
@@ -6544,7 +7583,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
6544
7583
|
}
|
|
6545
7584
|
graph.forEachNode((nodeId, attrs) => {
|
|
6546
7585
|
const n = attrs;
|
|
6547
|
-
if (n.type !==
|
|
7586
|
+
if (n.type !== import_types27.NodeType.ServiceNode) return;
|
|
6548
7587
|
const svc = n;
|
|
6549
7588
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
6550
7589
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -6578,7 +7617,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
6578
7617
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
6579
7618
|
return a.target.localeCompare(b.target);
|
|
6580
7619
|
});
|
|
6581
|
-
return
|
|
7620
|
+
return import_types27.DivergenceResultSchema.parse({
|
|
6582
7621
|
divergences: filtered,
|
|
6583
7622
|
totalAffected: filtered.length,
|
|
6584
7623
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -6587,7 +7626,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
6587
7626
|
|
|
6588
7627
|
// src/diff.ts
|
|
6589
7628
|
init_cjs_shims();
|
|
6590
|
-
var
|
|
7629
|
+
var import_node_fs23 = require("fs");
|
|
6591
7630
|
async function loadSnapshotForDiff(target) {
|
|
6592
7631
|
if (/^https?:\/\//i.test(target)) {
|
|
6593
7632
|
const res = await fetch(target);
|
|
@@ -6596,7 +7635,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
6596
7635
|
}
|
|
6597
7636
|
return await res.json();
|
|
6598
7637
|
}
|
|
6599
|
-
const raw = await
|
|
7638
|
+
const raw = await import_node_fs23.promises.readFile(target, "utf8");
|
|
6600
7639
|
return JSON.parse(raw);
|
|
6601
7640
|
}
|
|
6602
7641
|
function indexEntries(entries) {
|
|
@@ -6664,25 +7703,25 @@ function canonicalJson(value) {
|
|
|
6664
7703
|
|
|
6665
7704
|
// src/registry.ts
|
|
6666
7705
|
init_cjs_shims();
|
|
6667
|
-
var
|
|
7706
|
+
var import_node_fs24 = require("fs");
|
|
6668
7707
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
6669
|
-
var
|
|
6670
|
-
var
|
|
7708
|
+
var import_node_path41 = __toESM(require("path"), 1);
|
|
7709
|
+
var import_types28 = require("@neat.is/types");
|
|
6671
7710
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
6672
7711
|
var LOCK_RETRY_MS = 50;
|
|
6673
7712
|
function neatHome() {
|
|
6674
7713
|
const override = process.env.NEAT_HOME;
|
|
6675
|
-
if (override && override.length > 0) return
|
|
6676
|
-
return
|
|
7714
|
+
if (override && override.length > 0) return import_node_path41.default.resolve(override);
|
|
7715
|
+
return import_node_path41.default.join(import_node_os3.default.homedir(), ".neat");
|
|
6677
7716
|
}
|
|
6678
7717
|
function registryPath() {
|
|
6679
|
-
return
|
|
7718
|
+
return import_node_path41.default.join(neatHome(), "projects.json");
|
|
6680
7719
|
}
|
|
6681
7720
|
function registryLockPath() {
|
|
6682
|
-
return
|
|
7721
|
+
return import_node_path41.default.join(neatHome(), "projects.json.lock");
|
|
6683
7722
|
}
|
|
6684
7723
|
function daemonPidPath() {
|
|
6685
|
-
return
|
|
7724
|
+
return import_node_path41.default.join(neatHome(), "neatd.pid");
|
|
6686
7725
|
}
|
|
6687
7726
|
function isPidAliveDefault(pid) {
|
|
6688
7727
|
try {
|
|
@@ -6694,7 +7733,7 @@ function isPidAliveDefault(pid) {
|
|
|
6694
7733
|
}
|
|
6695
7734
|
async function readPidFile(file) {
|
|
6696
7735
|
try {
|
|
6697
|
-
const raw = await
|
|
7736
|
+
const raw = await import_node_fs24.promises.readFile(file, "utf8");
|
|
6698
7737
|
const pid = Number.parseInt(raw.trim(), 10);
|
|
6699
7738
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
6700
7739
|
} catch {
|
|
@@ -6742,24 +7781,24 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
6742
7781
|
}
|
|
6743
7782
|
}
|
|
6744
7783
|
async function writeAtomically(target, contents) {
|
|
6745
|
-
await
|
|
7784
|
+
await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(target), { recursive: true });
|
|
6746
7785
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
6747
|
-
const fd = await
|
|
7786
|
+
const fd = await import_node_fs24.promises.open(tmp, "w");
|
|
6748
7787
|
try {
|
|
6749
7788
|
await fd.writeFile(contents, "utf8");
|
|
6750
7789
|
await fd.sync();
|
|
6751
7790
|
} finally {
|
|
6752
7791
|
await fd.close();
|
|
6753
7792
|
}
|
|
6754
|
-
await
|
|
7793
|
+
await import_node_fs24.promises.rename(tmp, target);
|
|
6755
7794
|
}
|
|
6756
7795
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
6757
7796
|
const deadline = Date.now() + timeoutMs;
|
|
6758
|
-
await
|
|
7797
|
+
await import_node_fs24.promises.mkdir(import_node_path41.default.dirname(lockPath), { recursive: true });
|
|
6759
7798
|
let probedHolder = false;
|
|
6760
7799
|
while (true) {
|
|
6761
7800
|
try {
|
|
6762
|
-
const fd = await
|
|
7801
|
+
const fd = await import_node_fs24.promises.open(lockPath, "wx");
|
|
6763
7802
|
try {
|
|
6764
7803
|
await fd.writeFile(`${process.pid}
|
|
6765
7804
|
`, "utf8");
|
|
@@ -6784,7 +7823,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
|
|
|
6784
7823
|
}
|
|
6785
7824
|
}
|
|
6786
7825
|
async function releaseLock(lockPath) {
|
|
6787
|
-
await
|
|
7826
|
+
await import_node_fs24.promises.unlink(lockPath).catch(() => {
|
|
6788
7827
|
});
|
|
6789
7828
|
}
|
|
6790
7829
|
async function withLock(fn) {
|
|
@@ -6800,7 +7839,7 @@ async function readRegistry() {
|
|
|
6800
7839
|
const file = registryPath();
|
|
6801
7840
|
let raw;
|
|
6802
7841
|
try {
|
|
6803
|
-
raw = await
|
|
7842
|
+
raw = await import_node_fs24.promises.readFile(file, "utf8");
|
|
6804
7843
|
} catch (err) {
|
|
6805
7844
|
if (err.code === "ENOENT") {
|
|
6806
7845
|
return { version: 1, projects: [] };
|
|
@@ -6808,10 +7847,10 @@ async function readRegistry() {
|
|
|
6808
7847
|
throw err;
|
|
6809
7848
|
}
|
|
6810
7849
|
const parsed = JSON.parse(raw);
|
|
6811
|
-
return
|
|
7850
|
+
return import_types28.RegistryFileSchema.parse(parsed);
|
|
6812
7851
|
}
|
|
6813
7852
|
async function writeRegistry(reg) {
|
|
6814
|
-
const validated =
|
|
7853
|
+
const validated = import_types28.RegistryFileSchema.parse(reg);
|
|
6815
7854
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6816
7855
|
}
|
|
6817
7856
|
async function getProject(name) {
|
|
@@ -6855,7 +7894,7 @@ function pruneTtlMs() {
|
|
|
6855
7894
|
}
|
|
6856
7895
|
async function statPathStatus(p) {
|
|
6857
7896
|
try {
|
|
6858
|
-
const stat = await
|
|
7897
|
+
const stat = await import_node_fs24.promises.stat(p);
|
|
6859
7898
|
return stat.isDirectory() ? "present" : "unknown";
|
|
6860
7899
|
} catch (err) {
|
|
6861
7900
|
return err.code === "ENOENT" ? "gone" : "unknown";
|
|
@@ -7102,11 +8141,11 @@ function registerRoutes(scope, ctx) {
|
|
|
7102
8141
|
const candidates = req2.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
7103
8142
|
const parsed = [];
|
|
7104
8143
|
for (const c of candidates) {
|
|
7105
|
-
const r =
|
|
8144
|
+
const r = import_types29.DivergenceTypeSchema.safeParse(c);
|
|
7106
8145
|
if (!r.success) {
|
|
7107
8146
|
return reply.code(400).send({
|
|
7108
8147
|
error: `unknown divergence type "${c}"`,
|
|
7109
|
-
allowed:
|
|
8148
|
+
allowed: import_types29.DivergenceTypeSchema.options
|
|
7110
8149
|
});
|
|
7111
8150
|
}
|
|
7112
8151
|
parsed.push(r.data);
|
|
@@ -7325,7 +8364,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7325
8364
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
7326
8365
|
let violations = await log.readAll();
|
|
7327
8366
|
if (req2.query.severity) {
|
|
7328
|
-
const sev =
|
|
8367
|
+
const sev = import_types29.PolicySeveritySchema.safeParse(req2.query.severity);
|
|
7329
8368
|
if (!sev.success) {
|
|
7330
8369
|
return reply.code(400).send({
|
|
7331
8370
|
error: "invalid severity",
|
|
@@ -7364,7 +8403,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7364
8403
|
scope.post("/policies/check", async (req2, reply) => {
|
|
7365
8404
|
const proj = resolveProject(registry, req2, reply, ctx.bootstrap, ctx.singleProject);
|
|
7366
8405
|
if (!proj) return;
|
|
7367
|
-
const parsed =
|
|
8406
|
+
const parsed = import_types29.PoliciesCheckBodySchema.safeParse(req2.body ?? {});
|
|
7368
8407
|
if (!parsed.success) {
|
|
7369
8408
|
return reply.code(400).send({
|
|
7370
8409
|
error: "invalid /policies/check body",
|
|
@@ -7622,12 +8661,91 @@ async function buildApi(opts) {
|
|
|
7622
8661
|
|
|
7623
8662
|
// src/daemon.ts
|
|
7624
8663
|
init_otel();
|
|
8664
|
+
|
|
8665
|
+
// src/connectors/index.ts
|
|
8666
|
+
init_cjs_shims();
|
|
8667
|
+
var NO_ENV = "unknown";
|
|
8668
|
+
async function runConnectorPoll(connector, ctx, graph, resolveTarget) {
|
|
8669
|
+
const signals = await connector.poll(ctx);
|
|
8670
|
+
let edgesCreated = 0;
|
|
8671
|
+
let edgesUpdated = 0;
|
|
8672
|
+
let unresolved = 0;
|
|
8673
|
+
for (const signal of signals) {
|
|
8674
|
+
const resolved = resolveTarget(signal, ctx);
|
|
8675
|
+
if (!resolved) {
|
|
8676
|
+
unresolved++;
|
|
8677
|
+
continue;
|
|
8678
|
+
}
|
|
8679
|
+
const serviceNodeId = ensureServiceNode(graph, resolved.serviceName, NO_ENV);
|
|
8680
|
+
const callSite = signal.callSite ? { relPath: signal.callSite.file, line: signal.callSite.line } : void 0;
|
|
8681
|
+
const sourceId = callSite ? ensureObservedFileNode(graph, resolved.serviceName, serviceNodeId, callSite) : serviceNodeId;
|
|
8682
|
+
const evidence = callSite ? {
|
|
8683
|
+
file: reconcileObservedRelPath(graph, resolved.serviceName, callSite.relPath),
|
|
8684
|
+
line: callSite.line
|
|
8685
|
+
} : void 0;
|
|
8686
|
+
const calls = Math.trunc(signal.callCount);
|
|
8687
|
+
if (calls < 1) continue;
|
|
8688
|
+
const errors = Math.min(Math.max(Math.trunc(signal.errorCount), 0), calls);
|
|
8689
|
+
let created = false;
|
|
8690
|
+
let ok = true;
|
|
8691
|
+
for (let i = 0; i < calls; i++) {
|
|
8692
|
+
const result = upsertObservedEdge(
|
|
8693
|
+
graph,
|
|
8694
|
+
resolved.edgeType,
|
|
8695
|
+
sourceId,
|
|
8696
|
+
resolved.targetNodeId,
|
|
8697
|
+
signal.lastObservedIso,
|
|
8698
|
+
i < errors,
|
|
8699
|
+
evidence
|
|
8700
|
+
);
|
|
8701
|
+
if (!result) {
|
|
8702
|
+
ok = false;
|
|
8703
|
+
break;
|
|
8704
|
+
}
|
|
8705
|
+
if (i === 0) created = result.created;
|
|
8706
|
+
}
|
|
8707
|
+
if (!ok) {
|
|
8708
|
+
unresolved++;
|
|
8709
|
+
continue;
|
|
8710
|
+
}
|
|
8711
|
+
if (created) edgesCreated++;
|
|
8712
|
+
else edgesUpdated++;
|
|
8713
|
+
}
|
|
8714
|
+
return { signalCount: signals.length, edgesCreated, edgesUpdated, unresolved };
|
|
8715
|
+
}
|
|
8716
|
+
var DEFAULT_POLL_INTERVAL_MS = 6e4;
|
|
8717
|
+
function startConnectorPollLoop(connector, ctx, graph, resolveTarget, options = {}) {
|
|
8718
|
+
let stopped = false;
|
|
8719
|
+
let since = ctx.since;
|
|
8720
|
+
const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
8721
|
+
const onError = options.onError ?? ((err) => console.error(`[neatd] connector poll failed (${connector.provider})`, err));
|
|
8722
|
+
const tick = () => {
|
|
8723
|
+
if (stopped) return;
|
|
8724
|
+
void (async () => {
|
|
8725
|
+
const tickStartedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
8726
|
+
try {
|
|
8727
|
+
await runConnectorPoll(connector, { ...ctx, since }, graph, resolveTarget);
|
|
8728
|
+
since = tickStartedAt;
|
|
8729
|
+
} catch (err) {
|
|
8730
|
+
onError(err);
|
|
8731
|
+
}
|
|
8732
|
+
})();
|
|
8733
|
+
};
|
|
8734
|
+
const interval = setInterval(tick, intervalMs);
|
|
8735
|
+
if (typeof interval.unref === "function") interval.unref();
|
|
8736
|
+
return () => {
|
|
8737
|
+
stopped = true;
|
|
8738
|
+
clearInterval(interval);
|
|
8739
|
+
};
|
|
8740
|
+
}
|
|
8741
|
+
|
|
8742
|
+
// src/daemon.ts
|
|
7625
8743
|
init_auth();
|
|
7626
8744
|
|
|
7627
8745
|
// src/unrouted.ts
|
|
7628
8746
|
init_cjs_shims();
|
|
7629
|
-
var
|
|
7630
|
-
var
|
|
8747
|
+
var import_node_fs25 = require("fs");
|
|
8748
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
7631
8749
|
function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new Date()) {
|
|
7632
8750
|
return {
|
|
7633
8751
|
timestamp: now.toISOString(),
|
|
@@ -7637,34 +8755,34 @@ function buildUnroutedSpanRecord(serviceName, traceId, now = /* @__PURE__ */ new
|
|
|
7637
8755
|
};
|
|
7638
8756
|
}
|
|
7639
8757
|
async function appendUnroutedSpan(neatHome3, record) {
|
|
7640
|
-
const target =
|
|
7641
|
-
await
|
|
7642
|
-
await
|
|
8758
|
+
const target = import_node_path44.default.join(neatHome3, "errors.ndjson");
|
|
8759
|
+
await import_node_fs25.promises.mkdir(neatHome3, { recursive: true });
|
|
8760
|
+
await import_node_fs25.promises.appendFile(target, JSON.stringify(record) + "\n", "utf8");
|
|
7643
8761
|
}
|
|
7644
8762
|
function unroutedErrorsPath(neatHome3) {
|
|
7645
|
-
return
|
|
8763
|
+
return import_node_path44.default.join(neatHome3, "errors.ndjson");
|
|
7646
8764
|
}
|
|
7647
8765
|
|
|
7648
8766
|
// src/daemon.ts
|
|
7649
|
-
var
|
|
8767
|
+
var import_types30 = require("@neat.is/types");
|
|
7650
8768
|
function daemonJsonPath(scanPath) {
|
|
7651
|
-
return
|
|
8769
|
+
return import_node_path45.default.join(scanPath, "neat-out", "daemon.json");
|
|
7652
8770
|
}
|
|
7653
8771
|
function daemonsDiscoveryDir(home) {
|
|
7654
8772
|
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
7655
|
-
return
|
|
8773
|
+
return import_node_path45.default.join(base, "daemons");
|
|
7656
8774
|
}
|
|
7657
8775
|
function daemonDiscoveryPath(project, home) {
|
|
7658
|
-
return
|
|
8776
|
+
return import_node_path45.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
7659
8777
|
}
|
|
7660
8778
|
function sanitizeDiscoveryName(project) {
|
|
7661
8779
|
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
7662
8780
|
}
|
|
7663
8781
|
function neatHomeFromEnv() {
|
|
7664
8782
|
const env = process.env.NEAT_HOME;
|
|
7665
|
-
if (env && env.length > 0) return
|
|
8783
|
+
if (env && env.length > 0) return import_node_path45.default.resolve(env);
|
|
7666
8784
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7667
|
-
return
|
|
8785
|
+
return import_node_path45.default.join(home, ".neat");
|
|
7668
8786
|
}
|
|
7669
8787
|
function resolveNeatVersion() {
|
|
7670
8788
|
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
@@ -7696,7 +8814,7 @@ async function clearDaemonRecord(record, home) {
|
|
|
7696
8814
|
} catch {
|
|
7697
8815
|
}
|
|
7698
8816
|
try {
|
|
7699
|
-
await
|
|
8817
|
+
await import_node_fs26.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
7700
8818
|
} catch {
|
|
7701
8819
|
}
|
|
7702
8820
|
}
|
|
@@ -7705,12 +8823,12 @@ function reconcileDaemonRecordSync(record, home) {
|
|
|
7705
8823
|
const stopped = { ...record, status: "stopped" };
|
|
7706
8824
|
const target = daemonJsonPath(record.projectPath);
|
|
7707
8825
|
const tmp = `${target}.${process.pid}.tmp`;
|
|
7708
|
-
(0,
|
|
7709
|
-
(0,
|
|
8826
|
+
(0, import_node_fs26.writeFileSync)(tmp, JSON.stringify(stopped, null, 2) + "\n");
|
|
8827
|
+
(0, import_node_fs26.renameSync)(tmp, target);
|
|
7710
8828
|
} catch {
|
|
7711
8829
|
}
|
|
7712
8830
|
try {
|
|
7713
|
-
(0,
|
|
8831
|
+
(0, import_node_fs26.unlinkSync)(daemonDiscoveryPath(record.project, home));
|
|
7714
8832
|
} catch {
|
|
7715
8833
|
}
|
|
7716
8834
|
}
|
|
@@ -7719,17 +8837,25 @@ function teardownSlot(slot) {
|
|
|
7719
8837
|
slot.stopPersist();
|
|
7720
8838
|
} catch {
|
|
7721
8839
|
}
|
|
8840
|
+
try {
|
|
8841
|
+
slot.stopStaleness();
|
|
8842
|
+
} catch {
|
|
8843
|
+
}
|
|
8844
|
+
try {
|
|
8845
|
+
slot.stopConnectors();
|
|
8846
|
+
} catch {
|
|
8847
|
+
}
|
|
7722
8848
|
try {
|
|
7723
8849
|
slot.detachEvents();
|
|
7724
8850
|
} catch {
|
|
7725
8851
|
}
|
|
7726
8852
|
}
|
|
7727
8853
|
function neatHomeFor(opts) {
|
|
7728
|
-
if (opts.neatHome && opts.neatHome.length > 0) return
|
|
8854
|
+
if (opts.neatHome && opts.neatHome.length > 0) return import_node_path45.default.resolve(opts.neatHome);
|
|
7729
8855
|
const env = process.env.NEAT_HOME;
|
|
7730
|
-
if (env && env.length > 0) return
|
|
8856
|
+
if (env && env.length > 0) return import_node_path45.default.resolve(env);
|
|
7731
8857
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7732
|
-
return
|
|
8858
|
+
return import_node_path45.default.join(home, ".neat");
|
|
7733
8859
|
}
|
|
7734
8860
|
function routeSpanToProject(serviceName, projects) {
|
|
7735
8861
|
if (!serviceName) return DEFAULT_PROJECT;
|
|
@@ -7773,13 +8899,13 @@ function spanBelongsToSingleProject(graph, project, serviceName) {
|
|
|
7773
8899
|
if (!serviceName) return true;
|
|
7774
8900
|
if (serviceNameMatchesProject(serviceName, project)) return true;
|
|
7775
8901
|
return graph.someNode(
|
|
7776
|
-
(_id, attrs) => attrs.type ===
|
|
8902
|
+
(_id, attrs) => attrs.type === import_types30.NodeType.ServiceNode && attrs.name === serviceName
|
|
7777
8903
|
);
|
|
7778
8904
|
}
|
|
7779
|
-
async function bootstrapProject(entry2) {
|
|
7780
|
-
const paths = pathsForProject(entry2.name,
|
|
8905
|
+
async function bootstrapProject(entry2, connectors = []) {
|
|
8906
|
+
const paths = pathsForProject(entry2.name, import_node_path45.default.join(entry2.path, "neat-out"));
|
|
7781
8907
|
try {
|
|
7782
|
-
const stat = await
|
|
8908
|
+
const stat = await import_node_fs26.promises.stat(entry2.path);
|
|
7783
8909
|
if (!stat.isDirectory()) {
|
|
7784
8910
|
throw new Error(`registered path ${entry2.path} is not a directory`);
|
|
7785
8911
|
}
|
|
@@ -7795,6 +8921,10 @@ async function bootstrapProject(entry2) {
|
|
|
7795
8921
|
paths,
|
|
7796
8922
|
stopPersist: () => {
|
|
7797
8923
|
},
|
|
8924
|
+
stopStaleness: () => {
|
|
8925
|
+
},
|
|
8926
|
+
stopConnectors: () => {
|
|
8927
|
+
},
|
|
7798
8928
|
detachEvents: () => {
|
|
7799
8929
|
},
|
|
7800
8930
|
status: "broken",
|
|
@@ -7809,6 +8939,22 @@ async function bootstrapProject(entry2) {
|
|
|
7809
8939
|
try {
|
|
7810
8940
|
await extractFromDirectory(graph, entry2.path);
|
|
7811
8941
|
const stopPersist = startPersistLoop(graph, outPath, { exitOnSignal: false });
|
|
8942
|
+
const stopStaleness = startStalenessLoop(graph, {
|
|
8943
|
+
staleEventsPath: paths.staleEventsPath,
|
|
8944
|
+
project: entry2.name
|
|
8945
|
+
});
|
|
8946
|
+
const stopFns = connectors.map(
|
|
8947
|
+
(registration) => startConnectorPollLoop(
|
|
8948
|
+
registration.connector,
|
|
8949
|
+
{ projectDir: entry2.path, credentials: registration.credentials },
|
|
8950
|
+
graph,
|
|
8951
|
+
registration.resolveTarget,
|
|
8952
|
+
{ intervalMs: registration.intervalMs }
|
|
8953
|
+
)
|
|
8954
|
+
);
|
|
8955
|
+
const stopConnectors = () => {
|
|
8956
|
+
for (const stop of stopFns) stop();
|
|
8957
|
+
};
|
|
7812
8958
|
await touchLastSeen(entry2.name).catch(() => {
|
|
7813
8959
|
});
|
|
7814
8960
|
return {
|
|
@@ -7817,6 +8963,8 @@ async function bootstrapProject(entry2) {
|
|
|
7817
8963
|
outPath,
|
|
7818
8964
|
paths,
|
|
7819
8965
|
stopPersist,
|
|
8966
|
+
stopStaleness,
|
|
8967
|
+
stopConnectors,
|
|
7820
8968
|
detachEvents,
|
|
7821
8969
|
status: "active"
|
|
7822
8970
|
};
|
|
@@ -7873,7 +9021,7 @@ async function startDaemon(opts = {}) {
|
|
|
7873
9021
|
const projectArg = typeof opts.project === "string" && opts.project.length > 0 ? opts.project : process.env.NEAT_PROJECT && process.env.NEAT_PROJECT.length > 0 ? process.env.NEAT_PROJECT : null;
|
|
7874
9022
|
const projectPathArg = opts.projectPath && opts.projectPath.length > 0 ? opts.projectPath : process.env.NEAT_PROJECT_PATH && process.env.NEAT_PROJECT_PATH.length > 0 ? process.env.NEAT_PROJECT_PATH : null;
|
|
7875
9023
|
const singleProject = projectArg;
|
|
7876
|
-
const singleProjectPath = singleProject && projectPathArg ?
|
|
9024
|
+
const singleProjectPath = singleProject && projectPathArg ? import_node_path45.default.resolve(projectPathArg) : null;
|
|
7877
9025
|
if (singleProject && !singleProjectPath) {
|
|
7878
9026
|
throw new Error(
|
|
7879
9027
|
`neatd: project "${singleProject}" given without a projectPath; pass NEAT_PROJECT_PATH alongside NEAT_PROJECT.`
|
|
@@ -7881,14 +9029,14 @@ async function startDaemon(opts = {}) {
|
|
|
7881
9029
|
}
|
|
7882
9030
|
if (!singleProject) {
|
|
7883
9031
|
try {
|
|
7884
|
-
await
|
|
9032
|
+
await import_node_fs26.promises.access(regPath);
|
|
7885
9033
|
} catch {
|
|
7886
9034
|
throw new Error(
|
|
7887
9035
|
`neatd: registry not found at ${regPath}. Run \`neat init <path>\` to register a project before starting the daemon.`
|
|
7888
9036
|
);
|
|
7889
9037
|
}
|
|
7890
9038
|
}
|
|
7891
|
-
const pidPath =
|
|
9039
|
+
const pidPath = import_node_path45.default.join(home, "neatd.pid");
|
|
7892
9040
|
await writeAtomically(pidPath, `${process.pid}
|
|
7893
9041
|
`);
|
|
7894
9042
|
const slots = /* @__PURE__ */ new Map();
|
|
@@ -7932,7 +9080,7 @@ async function startDaemon(opts = {}) {
|
|
|
7932
9080
|
}
|
|
7933
9081
|
async function tryRecoverSlot(entry2) {
|
|
7934
9082
|
try {
|
|
7935
|
-
const fresh = await bootstrapProject(entry2);
|
|
9083
|
+
const fresh = await bootstrapProject(entry2, opts.connectors ?? []);
|
|
7936
9084
|
const prior = slots.get(entry2.name);
|
|
7937
9085
|
if (prior) teardownSlot(prior);
|
|
7938
9086
|
slots.set(entry2.name, fresh);
|
|
@@ -7956,7 +9104,7 @@ async function startDaemon(opts = {}) {
|
|
|
7956
9104
|
bootstrapStatus.set(entry2.name, "bootstrapping");
|
|
7957
9105
|
bootstrapStartedAt.set(entry2.name, Date.now());
|
|
7958
9106
|
try {
|
|
7959
|
-
const slot = await bootstrapProject(entry2);
|
|
9107
|
+
const slot = await bootstrapProject(entry2, opts.connectors ?? []);
|
|
7960
9108
|
const prior = slots.get(entry2.name);
|
|
7961
9109
|
if (prior) teardownSlot(prior);
|
|
7962
9110
|
slots.set(entry2.name, slot);
|
|
@@ -8082,7 +9230,7 @@ async function startDaemon(opts = {}) {
|
|
|
8082
9230
|
}
|
|
8083
9231
|
if (restApp) await restApp.close().catch(() => {
|
|
8084
9232
|
});
|
|
8085
|
-
await
|
|
9233
|
+
await import_node_fs26.promises.unlink(pidPath).catch(() => {
|
|
8086
9234
|
});
|
|
8087
9235
|
throw new Error(
|
|
8088
9236
|
`neatd: failed to bind REST on port ${restPort} \u2014 ${err.message}`
|
|
@@ -8208,7 +9356,7 @@ async function startDaemon(opts = {}) {
|
|
|
8208
9356
|
});
|
|
8209
9357
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
8210
9358
|
});
|
|
8211
|
-
await
|
|
9359
|
+
await import_node_fs26.promises.unlink(pidPath).catch(() => {
|
|
8212
9360
|
});
|
|
8213
9361
|
throw new Error(
|
|
8214
9362
|
`neatd: failed to bind OTLP on port ${otlpPort} \u2014 ${err.message}`
|
|
@@ -8243,7 +9391,7 @@ async function startDaemon(opts = {}) {
|
|
|
8243
9391
|
});
|
|
8244
9392
|
if (otlpApp) await otlpApp.close().catch(() => {
|
|
8245
9393
|
});
|
|
8246
|
-
await
|
|
9394
|
+
await import_node_fs26.promises.unlink(pidPath).catch(() => {
|
|
8247
9395
|
});
|
|
8248
9396
|
throw new Error(
|
|
8249
9397
|
`neatd: failed to write daemon.json for "${singleProject}" \u2014 ${err.message}`
|
|
@@ -8290,9 +9438,9 @@ async function startDaemon(opts = {}) {
|
|
|
8290
9438
|
let registryWatcher = null;
|
|
8291
9439
|
let reloadTimer = null;
|
|
8292
9440
|
if (!singleProject) try {
|
|
8293
|
-
const regDir =
|
|
8294
|
-
const regBase =
|
|
8295
|
-
registryWatcher = (0,
|
|
9441
|
+
const regDir = import_node_path45.default.dirname(regPath);
|
|
9442
|
+
const regBase = import_node_path45.default.basename(regPath);
|
|
9443
|
+
registryWatcher = (0, import_node_fs26.watch)(regDir, (_eventType, filename) => {
|
|
8296
9444
|
if (filename !== null && filename !== regBase) return;
|
|
8297
9445
|
if (reloadTimer) clearTimeout(reloadTimer);
|
|
8298
9446
|
reloadTimer = setTimeout(() => {
|
|
@@ -8341,7 +9489,7 @@ async function startDaemon(opts = {}) {
|
|
|
8341
9489
|
if (daemonRecord) {
|
|
8342
9490
|
await clearDaemonRecord(daemonRecord, home);
|
|
8343
9491
|
}
|
|
8344
|
-
await
|
|
9492
|
+
await import_node_fs26.promises.unlink(pidPath).catch(() => {
|
|
8345
9493
|
});
|
|
8346
9494
|
};
|
|
8347
9495
|
return {
|
|
@@ -8364,9 +9512,9 @@ init_auth();
|
|
|
8364
9512
|
// src/web-spawn.ts
|
|
8365
9513
|
init_cjs_shims();
|
|
8366
9514
|
var import_node_child_process2 = require("child_process");
|
|
8367
|
-
var
|
|
9515
|
+
var import_node_fs27 = require("fs");
|
|
8368
9516
|
var import_node_net = __toESM(require("net"), 1);
|
|
8369
|
-
var
|
|
9517
|
+
var import_node_path46 = __toESM(require("path"), 1);
|
|
8370
9518
|
var DEFAULT_WEB_PORT = 6328;
|
|
8371
9519
|
var DEFAULT_REST_PORT = 8080;
|
|
8372
9520
|
function asValidPort(value) {
|
|
@@ -8375,11 +9523,11 @@ function asValidPort(value) {
|
|
|
8375
9523
|
}
|
|
8376
9524
|
function projectRoot() {
|
|
8377
9525
|
const fromEnv = process.env.NEAT_SCAN_PATH;
|
|
8378
|
-
return
|
|
9526
|
+
return import_node_path46.default.resolve(fromEnv && fromEnv.length > 0 ? fromEnv : process.cwd());
|
|
8379
9527
|
}
|
|
8380
9528
|
async function readDaemonPorts(root) {
|
|
8381
9529
|
try {
|
|
8382
|
-
const raw = await
|
|
9530
|
+
const raw = await import_node_fs27.promises.readFile(import_node_path46.default.join(root, "neat-out", "daemon.json"), "utf8");
|
|
8383
9531
|
const parsed = JSON.parse(raw);
|
|
8384
9532
|
const ports = parsed?.ports ?? {};
|
|
8385
9533
|
return { web: asValidPort(ports.web), rest: asValidPort(ports.rest) };
|
|
@@ -8424,10 +9572,10 @@ function resolveWebPackageDir() {
|
|
|
8424
9572
|
eval("require")
|
|
8425
9573
|
);
|
|
8426
9574
|
const pkgJsonPath = req.resolve("@neat.is/web/package.json");
|
|
8427
|
-
return
|
|
9575
|
+
return import_node_path46.default.dirname(pkgJsonPath);
|
|
8428
9576
|
}
|
|
8429
9577
|
function resolveStandaloneServerEntry(webDir) {
|
|
8430
|
-
return
|
|
9578
|
+
return import_node_path46.default.join(webDir, ".next/standalone/packages/web/server.js");
|
|
8431
9579
|
}
|
|
8432
9580
|
async function pickInternalPort() {
|
|
8433
9581
|
return new Promise((resolve, reject) => {
|
|
@@ -8480,7 +9628,7 @@ async function spawnWebUI(restPort, opts = {}) {
|
|
|
8480
9628
|
NEAT_API_URL: apiUrl
|
|
8481
9629
|
};
|
|
8482
9630
|
child = (0, import_node_child_process2.spawn)(process.execPath, [serverEntry], {
|
|
8483
|
-
cwd:
|
|
9631
|
+
cwd: import_node_path46.default.dirname(serverEntry),
|
|
8484
9632
|
env,
|
|
8485
9633
|
stdio: ["ignore", "inherit", "inherit"],
|
|
8486
9634
|
detached: false
|
|
@@ -8656,14 +9804,14 @@ function localVersion() {
|
|
|
8656
9804
|
}
|
|
8657
9805
|
function neatHome2() {
|
|
8658
9806
|
if (process.env.NEAT_HOME && process.env.NEAT_HOME.length > 0) {
|
|
8659
|
-
return
|
|
9807
|
+
return import_node_path47.default.resolve(process.env.NEAT_HOME);
|
|
8660
9808
|
}
|
|
8661
9809
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8662
|
-
return
|
|
9810
|
+
return import_node_path47.default.join(home, ".neat");
|
|
8663
9811
|
}
|
|
8664
9812
|
async function readPid() {
|
|
8665
9813
|
try {
|
|
8666
|
-
const raw = await
|
|
9814
|
+
const raw = await import_node_fs28.promises.readFile(import_node_path47.default.join(neatHome2(), "neatd.pid"), "utf8");
|
|
8667
9815
|
const n = Number.parseInt(raw.trim(), 10);
|
|
8668
9816
|
return Number.isFinite(n) ? n : null;
|
|
8669
9817
|
} catch {
|