@neat.is/core 0.4.20-dev.20260629 → 0.4.22
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-MTXF77TN.js → chunk-GAFTW2OX.js} +2 -2
- package/dist/{chunk-GNAX2CBF.js → chunk-OQ7CHOY2.js} +859 -159
- package/dist/chunk-OQ7CHOY2.js.map +1 -0
- package/dist/{chunk-62ELQVIP.js → chunk-XRE4OQWJ.js} +57 -9
- package/dist/chunk-XRE4OQWJ.js.map +1 -0
- package/dist/{chunk-BGPWBRLU.js → chunk-ZX7PCMGZ.js} +21 -2
- package/dist/{chunk-BGPWBRLU.js.map → chunk-ZX7PCMGZ.js.map} +1 -1
- package/dist/cli.cjs +1232 -354
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.d.cts +2 -1
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +164 -60
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +884 -150
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +928 -150
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.d.cts +17 -0
- package/dist/neatd.d.ts +17 -0
- package/dist/neatd.js +27 -3
- package/dist/neatd.js.map +1 -1
- package/dist/{otel-grpc-I67ONCRM.js → otel-grpc-QYHUJ4OY.js} +3 -3
- package/dist/server.cjs +806 -121
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +3 -3
- package/package.json +2 -2
- package/proto/opentelemetry/proto/trace/v1/trace.proto +8 -2
- package/dist/chunk-62ELQVIP.js.map +0 -1
- package/dist/chunk-GNAX2CBF.js.map +0 -1
- /package/dist/{chunk-MTXF77TN.js.map → chunk-GAFTW2OX.js.map} +0 -0
- /package/dist/{otel-grpc-I67ONCRM.js.map → otel-grpc-QYHUJ4OY.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -42,13 +42,13 @@ var init_cjs_shims = __esm({
|
|
|
42
42
|
});
|
|
43
43
|
|
|
44
44
|
// src/auth.ts
|
|
45
|
-
function
|
|
45
|
+
function isLoopbackHost2(host) {
|
|
46
46
|
if (!host) return false;
|
|
47
47
|
return LOOPBACK_HOSTS.has(host);
|
|
48
48
|
}
|
|
49
49
|
function assertBindAuthority(host, token) {
|
|
50
50
|
if (token && token.length > 0) return;
|
|
51
|
-
if (
|
|
51
|
+
if (isLoopbackHost2(host)) return;
|
|
52
52
|
throw new BindAuthorityError(host);
|
|
53
53
|
}
|
|
54
54
|
function mountBearerAuth(app, opts) {
|
|
@@ -574,7 +574,23 @@ async function buildOtelReceiver(opts) {
|
|
|
574
574
|
};
|
|
575
575
|
return decorated;
|
|
576
576
|
}
|
|
577
|
-
|
|
577
|
+
async function listenSteppingOtlp(app, requestedPort, host) {
|
|
578
|
+
let port = requestedPort;
|
|
579
|
+
for (let attempt = 0; ; attempt++) {
|
|
580
|
+
try {
|
|
581
|
+
return await app.listen({ port, host });
|
|
582
|
+
} catch (err) {
|
|
583
|
+
const code = err.code;
|
|
584
|
+
const canStep = requestedPort !== 0 && code === "EADDRINUSE" && attempt < OTLP_STEP_ATTEMPTS - 1;
|
|
585
|
+
if (!canStep) throw err;
|
|
586
|
+
console.warn(
|
|
587
|
+
`otel: OTLP port ${port} is in use, stepping to ${port + OTLP_STEP_STRIDE}`
|
|
588
|
+
);
|
|
589
|
+
port += OTLP_STEP_STRIDE;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
var import_node_path42, import_node_url3, import_fastify2, import_protobufjs, ENV_ATTR_CANONICAL, ENV_ATTR_COMPAT, ENV_FALLBACK, exportTraceServiceRequestType, exportTraceServiceResponseType, cachedProtobufResponseBody, OTLP_STEP_ATTEMPTS, OTLP_STEP_STRIDE;
|
|
578
594
|
var init_otel = __esm({
|
|
579
595
|
"src/otel.ts"() {
|
|
580
596
|
"use strict";
|
|
@@ -590,6 +606,8 @@ var init_otel = __esm({
|
|
|
590
606
|
exportTraceServiceRequestType = null;
|
|
591
607
|
exportTraceServiceResponseType = null;
|
|
592
608
|
cachedProtobufResponseBody = null;
|
|
609
|
+
OTLP_STEP_ATTEMPTS = 8;
|
|
610
|
+
OTLP_STEP_STRIDE = 1;
|
|
593
611
|
}
|
|
594
612
|
});
|
|
595
613
|
|
|
@@ -605,6 +623,7 @@ __export(cli_exports, {
|
|
|
605
623
|
parseArgs: () => parseArgs,
|
|
606
624
|
printBanner: () => printBanner,
|
|
607
625
|
readPackageVersion: () => readPackageVersion,
|
|
626
|
+
resolveDaemonUrl: () => resolveDaemonUrl,
|
|
608
627
|
resolveProjectForVerb: () => resolveProjectForVerb,
|
|
609
628
|
runInit: () => runInit,
|
|
610
629
|
runQueryVerb: () => runQueryVerb,
|
|
@@ -1277,22 +1296,164 @@ var rootCauseShapes = {
|
|
|
1277
1296
|
[import_types.NodeType.ServiceNode]: serviceRootCauseShape,
|
|
1278
1297
|
[import_types.NodeType.FileNode]: fileRootCauseShape
|
|
1279
1298
|
};
|
|
1280
|
-
function getRootCause(graph, errorNodeId, errorEvent) {
|
|
1299
|
+
function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
1281
1300
|
if (!graph.hasNode(errorNodeId)) return null;
|
|
1282
1301
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
1283
1302
|
const shape = rootCauseShapes[origin.type];
|
|
1284
|
-
if (
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1303
|
+
if (shape) {
|
|
1304
|
+
const walk = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
1305
|
+
const match = shape(graph, origin, walk);
|
|
1306
|
+
if (match) {
|
|
1307
|
+
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
1308
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1309
|
+
rootCauseNode: match.rootCauseNode,
|
|
1310
|
+
rootCauseReason: reason,
|
|
1311
|
+
traversalPath: walk.path,
|
|
1312
|
+
edgeProvenances: walk.edges.map((e) => e.provenance),
|
|
1313
|
+
confidence: confidenceFromMix(walk.edges),
|
|
1314
|
+
fixRecommendation: match.fixRecommendation
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
if (origin.type === import_types.NodeType.ServiceNode) {
|
|
1319
|
+
const crossService = crossServiceRootCause(graph, errorNodeId, incidents, errorEvent);
|
|
1320
|
+
if (crossService) return crossService;
|
|
1321
|
+
}
|
|
1322
|
+
return rootCauseFromIncidents(errorNodeId, incidents, errorEvent);
|
|
1323
|
+
}
|
|
1324
|
+
var INCIDENT_ROOT_CAUSE_CONFIDENCE = 0.6;
|
|
1325
|
+
function incidentMatchesNode(ev, nodeId) {
|
|
1326
|
+
return ev.affectedNode === nodeId || ev.service === nodeId.replace(/^service:/, "");
|
|
1327
|
+
}
|
|
1328
|
+
function localizeFromIncidents(nodeId, incidents, errorEvent) {
|
|
1329
|
+
const pool = incidents && incidents.length > 0 ? incidents : errorEvent ? [errorEvent] : [];
|
|
1330
|
+
const relevant = pool.filter((ev) => incidentMatchesNode(ev, nodeId));
|
|
1331
|
+
if (relevant.length === 0) return null;
|
|
1332
|
+
const latest = [...relevant].sort((a, b) => b.timestamp.localeCompare(a.timestamp))[0];
|
|
1333
|
+
const attrs = latest.attributes ?? {};
|
|
1334
|
+
const filepath = typeof attrs["code.filepath"] === "string" ? attrs["code.filepath"] : void 0;
|
|
1335
|
+
const lineno = typeof attrs["code.lineno"] === "number" ? attrs["code.lineno"] : void 0;
|
|
1336
|
+
const route = typeof attrs["http.route"] === "string" ? attrs["http.route"] : void 0;
|
|
1337
|
+
const location = filepath ? `${filepath}${lineno !== void 0 ? `:${lineno}` : ""}` : void 0;
|
|
1338
|
+
const sameMode = relevant.filter((ev) => ev.errorMessage === latest.errorMessage);
|
|
1339
|
+
const count = sameMode.length;
|
|
1340
|
+
const tail = count > 1 ? ` (${count} recorded incidents)` : " (1 recorded incident)";
|
|
1341
|
+
const reasonParts = [`${latest.service}: ${latest.errorMessage}`];
|
|
1342
|
+
if (location) reasonParts.push(`surfaced at ${location}`);
|
|
1343
|
+
const rootCauseReason = `${reasonParts.join(" \u2014 ")}${tail}`;
|
|
1344
|
+
const localizesToFile = latest.affectedNode !== nodeId && latest.affectedNode.startsWith("file:");
|
|
1345
|
+
const fileNode = localizesToFile ? latest.affectedNode : void 0;
|
|
1346
|
+
const fixRecommendation = location ? `Inspect ${location}${route ? ` handling ${route}` : ""}` : route ? `Inspect ${latest.service}'s handler for ${route}` : void 0;
|
|
1347
|
+
return {
|
|
1348
|
+
rootCauseNode: fileNode ?? nodeId,
|
|
1349
|
+
rootCauseReason,
|
|
1350
|
+
...fileNode ? { fileNode } : {},
|
|
1351
|
+
...fixRecommendation ? { fixRecommendation } : {}
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
1355
|
+
const loc = localizeFromIncidents(nodeId, incidents, errorEvent);
|
|
1356
|
+
if (!loc) return null;
|
|
1357
|
+
const traversalPath = loc.fileNode ? [nodeId, loc.fileNode] : [nodeId];
|
|
1358
|
+
const edgeProvenances = loc.fileNode ? [import_types.Provenance.OBSERVED] : [];
|
|
1289
1359
|
return import_types.RootCauseResultSchema.parse({
|
|
1290
|
-
rootCauseNode:
|
|
1291
|
-
rootCauseReason:
|
|
1292
|
-
traversalPath
|
|
1293
|
-
edgeProvenances
|
|
1294
|
-
confidence:
|
|
1295
|
-
fixRecommendation:
|
|
1360
|
+
rootCauseNode: loc.rootCauseNode,
|
|
1361
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1362
|
+
traversalPath,
|
|
1363
|
+
edgeProvenances,
|
|
1364
|
+
confidence: INCIDENT_ROOT_CAUSE_CONFIDENCE,
|
|
1365
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1366
|
+
});
|
|
1367
|
+
}
|
|
1368
|
+
function isFailingCallEdge(e) {
|
|
1369
|
+
return e.type === import_types.EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
1370
|
+
}
|
|
1371
|
+
function callSourcesForService(graph, serviceId3) {
|
|
1372
|
+
const ids = [serviceId3];
|
|
1373
|
+
for (const edgeId of graph.outboundEdges(serviceId3)) {
|
|
1374
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1375
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1376
|
+
const tgt = graph.getNodeAttributes(e.target);
|
|
1377
|
+
if (tgt.type === import_types.NodeType.FileNode) ids.push(e.target);
|
|
1378
|
+
}
|
|
1379
|
+
return ids;
|
|
1380
|
+
}
|
|
1381
|
+
function failingCallDominates(e, id, curEdge, curId) {
|
|
1382
|
+
const ec = e.signal?.errorCount ?? 0;
|
|
1383
|
+
const cc = curEdge.signal?.errorCount ?? 0;
|
|
1384
|
+
if (ec !== cc) return ec > cc;
|
|
1385
|
+
if (import_types.PROV_RANK[e.provenance] !== import_types.PROV_RANK[curEdge.provenance]) {
|
|
1386
|
+
return import_types.PROV_RANK[e.provenance] > import_types.PROV_RANK[curEdge.provenance];
|
|
1387
|
+
}
|
|
1388
|
+
return id < curId;
|
|
1389
|
+
}
|
|
1390
|
+
function dominantFailingCall(graph, serviceId3, visited) {
|
|
1391
|
+
let best = null;
|
|
1392
|
+
for (const src of callSourcesForService(graph, serviceId3)) {
|
|
1393
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1394
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1395
|
+
if (!isFailingCallEdge(e)) continue;
|
|
1396
|
+
if (isFrontierNode(graph, e.target)) continue;
|
|
1397
|
+
const owner = resolveOwningService(graph, e.target);
|
|
1398
|
+
if (!owner || visited.has(owner.id)) continue;
|
|
1399
|
+
if (!best || failingCallDominates(e, owner.id, best.edge, best.nextService)) {
|
|
1400
|
+
best = { nextService: owner.id, edge: e };
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return best;
|
|
1405
|
+
}
|
|
1406
|
+
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
1407
|
+
const path53 = [originServiceId];
|
|
1408
|
+
const edges = [];
|
|
1409
|
+
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
1410
|
+
let current = originServiceId;
|
|
1411
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
1412
|
+
const hop = dominantFailingCall(graph, current, visited);
|
|
1413
|
+
if (!hop) break;
|
|
1414
|
+
path53.push(hop.nextService);
|
|
1415
|
+
edges.push(hop.edge);
|
|
1416
|
+
visited.add(hop.nextService);
|
|
1417
|
+
current = hop.nextService;
|
|
1418
|
+
}
|
|
1419
|
+
if (edges.length === 0) return null;
|
|
1420
|
+
return { path: path53, edges, culprit: current };
|
|
1421
|
+
}
|
|
1422
|
+
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
1423
|
+
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
1424
|
+
if (!chain) return null;
|
|
1425
|
+
const culprit = chain.culprit;
|
|
1426
|
+
const path53 = [...chain.path];
|
|
1427
|
+
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
1428
|
+
const baseConfidence = confidenceFromMix(chain.edges);
|
|
1429
|
+
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
1430
|
+
const loc = localizeFromIncidents(culprit, incidents, errorEvent);
|
|
1431
|
+
if (loc) {
|
|
1432
|
+
let rootCauseNode = culprit;
|
|
1433
|
+
if (loc.fileNode) {
|
|
1434
|
+
path53.push(loc.fileNode);
|
|
1435
|
+
edgeProvenances.push(import_types.Provenance.OBSERVED);
|
|
1436
|
+
rootCauseNode = loc.fileNode;
|
|
1437
|
+
}
|
|
1438
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1439
|
+
rootCauseNode,
|
|
1440
|
+
rootCauseReason: loc.rootCauseReason,
|
|
1441
|
+
traversalPath: path53,
|
|
1442
|
+
edgeProvenances,
|
|
1443
|
+
confidence,
|
|
1444
|
+
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
const lastEdge = chain.edges[chain.edges.length - 1];
|
|
1448
|
+
const errs = lastEdge.signal?.errorCount ?? 0;
|
|
1449
|
+
const culpritName = culprit.replace(/^service:/, "");
|
|
1450
|
+
return import_types.RootCauseResultSchema.parse({
|
|
1451
|
+
rootCauseNode: culprit,
|
|
1452
|
+
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
1453
|
+
traversalPath: path53,
|
|
1454
|
+
edgeProvenances,
|
|
1455
|
+
confidence,
|
|
1456
|
+
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
1296
1457
|
});
|
|
1297
1458
|
}
|
|
1298
1459
|
function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
@@ -1315,14 +1476,14 @@ function getBlastRadius(graph, nodeId, maxDepth = BLAST_RADIUS_DEFAULT_DEPTH) {
|
|
|
1315
1476
|
});
|
|
1316
1477
|
}
|
|
1317
1478
|
if (frame.distance >= maxDepth) continue;
|
|
1318
|
-
const
|
|
1319
|
-
for (const [
|
|
1320
|
-
if (enqueued.has(
|
|
1321
|
-
enqueued.add(
|
|
1479
|
+
const incoming = bestEdgeBySource(graph, graph.inboundEdges(frame.nodeId));
|
|
1480
|
+
for (const [srcId, edge] of incoming) {
|
|
1481
|
+
if (enqueued.has(srcId)) continue;
|
|
1482
|
+
enqueued.add(srcId);
|
|
1322
1483
|
queue.push({
|
|
1323
|
-
nodeId:
|
|
1484
|
+
nodeId: srcId,
|
|
1324
1485
|
distance: frame.distance + 1,
|
|
1325
|
-
path: [...frame.path,
|
|
1486
|
+
path: [...frame.path, srcId],
|
|
1326
1487
|
pathEdges: [...frame.pathEdges, edge]
|
|
1327
1488
|
});
|
|
1328
1489
|
}
|
|
@@ -1378,6 +1539,62 @@ function getTransitiveDependencies(graph, nodeId, depth = TRANSITIVE_DEPENDENCIE
|
|
|
1378
1539
|
total: dependencies.length
|
|
1379
1540
|
});
|
|
1380
1541
|
}
|
|
1542
|
+
function getObservedDependencies(graph, nodeId) {
|
|
1543
|
+
if (!graph.hasNode(nodeId)) {
|
|
1544
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1545
|
+
origin: nodeId,
|
|
1546
|
+
dependencies: [],
|
|
1547
|
+
observed: false,
|
|
1548
|
+
inboundObservedCount: 0,
|
|
1549
|
+
hasExtractedOutbound: false
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
1553
|
+
const scope = [nodeId];
|
|
1554
|
+
if (attrs.type === import_types.NodeType.ServiceNode) {
|
|
1555
|
+
for (const edgeId of graph.outboundEdges(nodeId)) {
|
|
1556
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1557
|
+
if (e.type !== import_types.EdgeType.CONTAINS) continue;
|
|
1558
|
+
const owned = graph.getNodeAttributes(e.target);
|
|
1559
|
+
if (owned.type === import_types.NodeType.FileNode) scope.push(e.target);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
const dependencies = [];
|
|
1563
|
+
const seenEdge = /* @__PURE__ */ new Set();
|
|
1564
|
+
let hasExtractedOutbound = false;
|
|
1565
|
+
for (const src of scope) {
|
|
1566
|
+
for (const edgeId of graph.outboundEdges(src)) {
|
|
1567
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1568
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1569
|
+
if (e.provenance === import_types.Provenance.OBSERVED) {
|
|
1570
|
+
if (!seenEdge.has(e.id)) {
|
|
1571
|
+
seenEdge.add(e.id);
|
|
1572
|
+
dependencies.push(e);
|
|
1573
|
+
}
|
|
1574
|
+
} else if (e.provenance === import_types.Provenance.EXTRACTED) {
|
|
1575
|
+
hasExtractedOutbound = true;
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
let inboundObservedCount = 0;
|
|
1580
|
+
for (const tgt of scope) {
|
|
1581
|
+
for (const edgeId of graph.inboundEdges(tgt)) {
|
|
1582
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1583
|
+
if (e.type === import_types.EdgeType.CONTAINS) continue;
|
|
1584
|
+
if (e.provenance === import_types.Provenance.OBSERVED) inboundObservedCount += 1;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
dependencies.sort(
|
|
1588
|
+
(a, b) => a.target.localeCompare(b.target) || a.source.localeCompare(b.source) || a.id.localeCompare(b.id)
|
|
1589
|
+
);
|
|
1590
|
+
return import_types.ObservedDependenciesResultSchema.parse({
|
|
1591
|
+
origin: nodeId,
|
|
1592
|
+
dependencies,
|
|
1593
|
+
observed: dependencies.length > 0 || inboundObservedCount > 0,
|
|
1594
|
+
inboundObservedCount,
|
|
1595
|
+
hasExtractedOutbound
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1381
1598
|
|
|
1382
1599
|
// src/policy.ts
|
|
1383
1600
|
var DEFAULT_ACTION_BY_SEVERITY = {
|
|
@@ -1647,6 +1864,130 @@ function evaluateAllPolicies(graph, policies, ctx) {
|
|
|
1647
1864
|
}
|
|
1648
1865
|
return out;
|
|
1649
1866
|
}
|
|
1867
|
+
function selectApplicablePolicies(graph, policies, nodeId) {
|
|
1868
|
+
if (!graph.hasNode(nodeId)) return [];
|
|
1869
|
+
const node = graph.getNodeAttributes(nodeId);
|
|
1870
|
+
const out = [];
|
|
1871
|
+
for (const policy of policies) {
|
|
1872
|
+
const m = matchPolicyToNode(graph, policy, nodeId, node);
|
|
1873
|
+
if (!m) continue;
|
|
1874
|
+
out.push({
|
|
1875
|
+
policyId: policy.id,
|
|
1876
|
+
policyName: policy.name,
|
|
1877
|
+
...policy.description !== void 0 ? { description: policy.description } : {},
|
|
1878
|
+
severity: policy.severity,
|
|
1879
|
+
onViolation: resolveOnViolation(policy),
|
|
1880
|
+
ruleType: policy.rule.type,
|
|
1881
|
+
match: m.match,
|
|
1882
|
+
reason: m.reason
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
return out;
|
|
1886
|
+
}
|
|
1887
|
+
function requiredProvenanceList(required) {
|
|
1888
|
+
if (Array.isArray(required)) return required.join(" | ");
|
|
1889
|
+
return String(required);
|
|
1890
|
+
}
|
|
1891
|
+
function nodeTouchesEdgeType(graph, nodeId, edgeType, requiredOtherEnd) {
|
|
1892
|
+
const incident = [...graph.outboundEdges(nodeId), ...graph.inboundEdges(nodeId)];
|
|
1893
|
+
for (const edgeId of incident) {
|
|
1894
|
+
const e = graph.getEdgeAttributes(edgeId);
|
|
1895
|
+
if (e.type !== edgeType) continue;
|
|
1896
|
+
if (requiredOtherEnd === void 0) return true;
|
|
1897
|
+
if (e.source === requiredOtherEnd || e.target === requiredOtherEnd) return true;
|
|
1898
|
+
}
|
|
1899
|
+
return false;
|
|
1900
|
+
}
|
|
1901
|
+
function blastRadiusSubjectReaching(graph, rule, nodeId) {
|
|
1902
|
+
let found = null;
|
|
1903
|
+
graph.forEachNode((subjId, attrs) => {
|
|
1904
|
+
if (found !== null) return;
|
|
1905
|
+
if (subjId === nodeId) return;
|
|
1906
|
+
if (attrs.type !== rule.nodeType) return;
|
|
1907
|
+
const radius = rule.depth !== void 0 ? getBlastRadius(graph, subjId, rule.depth) : getBlastRadius(graph, subjId);
|
|
1908
|
+
if (radius.affectedNodes.some((n) => n.nodeId === nodeId)) found = subjId;
|
|
1909
|
+
});
|
|
1910
|
+
return found;
|
|
1911
|
+
}
|
|
1912
|
+
function matchPolicyToNode(graph, policy, nodeId, node) {
|
|
1913
|
+
const rule = policy.rule;
|
|
1914
|
+
switch (rule.type) {
|
|
1915
|
+
case "structural": {
|
|
1916
|
+
if (node.type === rule.fromNodeType) {
|
|
1917
|
+
return {
|
|
1918
|
+
match: "subject",
|
|
1919
|
+
reason: `every ${rule.fromNodeType} must have a ${rule.edgeType} edge to a ${rule.toNodeType}`
|
|
1920
|
+
};
|
|
1921
|
+
}
|
|
1922
|
+
if (node.type === rule.toNodeType) {
|
|
1923
|
+
return {
|
|
1924
|
+
match: "region",
|
|
1925
|
+
reason: `${rule.fromNodeType} nodes must reach a ${rule.toNodeType} like this one via a ${rule.edgeType} edge`
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
return null;
|
|
1929
|
+
}
|
|
1930
|
+
case "ownership": {
|
|
1931
|
+
if (node.type === rule.nodeType) {
|
|
1932
|
+
return {
|
|
1933
|
+
match: "subject",
|
|
1934
|
+
reason: `every ${rule.nodeType} must declare a non-empty "${rule.field}" field`
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
return null;
|
|
1938
|
+
}
|
|
1939
|
+
case "blast-radius": {
|
|
1940
|
+
const depthLabel = rule.depth !== void 0 ? ` at depth ${rule.depth}` : "";
|
|
1941
|
+
if (node.type === rule.nodeType) {
|
|
1942
|
+
return {
|
|
1943
|
+
match: "subject",
|
|
1944
|
+
reason: `no ${rule.nodeType} may exceed a blast radius of ${rule.maxAffected}${depthLabel}`
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
const subject = blastRadiusSubjectReaching(graph, rule, nodeId);
|
|
1948
|
+
if (subject) {
|
|
1949
|
+
return {
|
|
1950
|
+
match: "region",
|
|
1951
|
+
reason: `this node is in the blast radius of ${subject} (a ${rule.nodeType} held to a blast radius of ${rule.maxAffected}${depthLabel}) \u2014 it breaks if that ${rule.nodeType} changes`
|
|
1952
|
+
};
|
|
1953
|
+
}
|
|
1954
|
+
return null;
|
|
1955
|
+
}
|
|
1956
|
+
case "compatibility": {
|
|
1957
|
+
const kindLabel = rule.kind ?? "all compat shapes";
|
|
1958
|
+
if (node.type === import_types2.NodeType.ServiceNode) {
|
|
1959
|
+
return {
|
|
1960
|
+
match: "subject",
|
|
1961
|
+
reason: `this service's dependencies are compatibility-checked (${kindLabel})`
|
|
1962
|
+
};
|
|
1963
|
+
}
|
|
1964
|
+
const reachesDriverEngine = rule.kind === void 0 || rule.kind === "driver-engine";
|
|
1965
|
+
if (reachesDriverEngine && node.type === import_types2.NodeType.DatabaseNode && nodeTouchesEdgeType(graph, nodeId, import_types2.EdgeType.CONNECTS_TO)) {
|
|
1966
|
+
return {
|
|
1967
|
+
match: "region",
|
|
1968
|
+
reason: "services connecting to this database have their driver/engine compatibility checked against it"
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
return null;
|
|
1972
|
+
}
|
|
1973
|
+
case "provenance": {
|
|
1974
|
+
const requiredList = requiredProvenanceList(rule.required);
|
|
1975
|
+
if (rule.targetNodeId !== void 0 && nodeId === rule.targetNodeId) {
|
|
1976
|
+
return {
|
|
1977
|
+
match: "subject",
|
|
1978
|
+
reason: `every ${rule.edgeType} edge into ${rule.targetNodeId} must carry ${requiredList} provenance`
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
if (nodeTouchesEdgeType(graph, nodeId, rule.edgeType, rule.targetNodeId)) {
|
|
1982
|
+
return {
|
|
1983
|
+
match: "region",
|
|
1984
|
+
reason: rule.targetNodeId !== void 0 ? `this node sits on a ${rule.edgeType} edge to ${rule.targetNodeId}, which must carry ${requiredList} provenance` : `this node sits on a ${rule.edgeType} edge, which must carry ${requiredList} provenance`
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1650
1991
|
async function loadPolicyFile(policyPath) {
|
|
1651
1992
|
let raw;
|
|
1652
1993
|
try {
|
|
@@ -1755,9 +2096,9 @@ function loadIncidentThresholdsFromEnv() {
|
|
|
1755
2096
|
return DEFAULT_INCIDENT_THRESHOLDS;
|
|
1756
2097
|
}
|
|
1757
2098
|
}
|
|
1758
|
-
function
|
|
2099
|
+
function httpResponseStatusFromAttrs(attrs) {
|
|
1759
2100
|
for (const key of ["http.response.status_code", "http.status_code"]) {
|
|
1760
|
-
const v =
|
|
2101
|
+
const v = attrs[key];
|
|
1761
2102
|
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
1762
2103
|
if (typeof v === "string") {
|
|
1763
2104
|
const n = Number(v);
|
|
@@ -1766,6 +2107,63 @@ function httpResponseStatus(span) {
|
|
|
1766
2107
|
}
|
|
1767
2108
|
return void 0;
|
|
1768
2109
|
}
|
|
2110
|
+
function httpResponseStatus(span) {
|
|
2111
|
+
return httpResponseStatusFromAttrs(span.attributes);
|
|
2112
|
+
}
|
|
2113
|
+
function httpFailureMessageFromAttrs(attrs) {
|
|
2114
|
+
const status2 = httpResponseStatusFromAttrs(attrs);
|
|
2115
|
+
const route = pickAttrFrom(attrs, "http.route", "http.target", "url.path");
|
|
2116
|
+
const method = pickAttrFrom(attrs, "http.request.method", "http.method");
|
|
2117
|
+
const where = route ? `${method ? `${method} ` : ""}${route}` : void 0;
|
|
2118
|
+
if (status2 !== void 0 && where) return `${status2} on ${where}`;
|
|
2119
|
+
if (status2 !== void 0) return `HTTP ${status2}`;
|
|
2120
|
+
if (where) return `error on ${where}`;
|
|
2121
|
+
return void 0;
|
|
2122
|
+
}
|
|
2123
|
+
var GRPC_STATUS_NAMES = {
|
|
2124
|
+
1: "CANCELLED",
|
|
2125
|
+
2: "UNKNOWN",
|
|
2126
|
+
3: "INVALID_ARGUMENT",
|
|
2127
|
+
4: "DEADLINE_EXCEEDED",
|
|
2128
|
+
5: "NOT_FOUND",
|
|
2129
|
+
6: "ALREADY_EXISTS",
|
|
2130
|
+
7: "PERMISSION_DENIED",
|
|
2131
|
+
8: "RESOURCE_EXHAUSTED",
|
|
2132
|
+
9: "FAILED_PRECONDITION",
|
|
2133
|
+
10: "ABORTED",
|
|
2134
|
+
11: "OUT_OF_RANGE",
|
|
2135
|
+
12: "UNIMPLEMENTED",
|
|
2136
|
+
13: "INTERNAL",
|
|
2137
|
+
14: "UNAVAILABLE",
|
|
2138
|
+
15: "DATA_LOSS",
|
|
2139
|
+
16: "UNAUTHENTICATED"
|
|
2140
|
+
};
|
|
2141
|
+
function grpcStatusCodeFromAttrs(attrs) {
|
|
2142
|
+
const v = attrs["rpc.grpc.status_code"];
|
|
2143
|
+
if (typeof v === "number" && Number.isFinite(v)) return v;
|
|
2144
|
+
if (typeof v === "string") {
|
|
2145
|
+
const n = Number(v);
|
|
2146
|
+
if (Number.isFinite(n)) return n;
|
|
2147
|
+
}
|
|
2148
|
+
return void 0;
|
|
2149
|
+
}
|
|
2150
|
+
function nonHttpFailureMessageFromAttrs(attrs) {
|
|
2151
|
+
const grpc2 = grpcStatusCodeFromAttrs(attrs);
|
|
2152
|
+
if (grpc2 !== void 0 && grpc2 !== 0) {
|
|
2153
|
+
const name = GRPC_STATUS_NAMES[grpc2] ?? `status ${grpc2}`;
|
|
2154
|
+
const detail = pickAttrFrom(attrs, "rpc.grpc.status_message");
|
|
2155
|
+
return detail ? `gRPC ${name}: ${detail}` : `gRPC ${name}`;
|
|
2156
|
+
}
|
|
2157
|
+
const errType = pickAttrFrom(attrs, "error.type");
|
|
2158
|
+
if (errType && errType !== "_OTHER" && !/^\d+$/.test(errType)) {
|
|
2159
|
+
const peer = pickAttrFrom(attrs, "server.address", "net.peer.name", "net.host.name");
|
|
2160
|
+
return peer ? `${errType} connecting to ${peer}` : errType;
|
|
2161
|
+
}
|
|
2162
|
+
return void 0;
|
|
2163
|
+
}
|
|
2164
|
+
function incidentMessage(span) {
|
|
2165
|
+
return span.exception?.message ?? httpFailureMessageFromAttrs(span.attributes) ?? nonHttpFailureMessageFromAttrs(span.attributes) ?? "unknown error";
|
|
2166
|
+
}
|
|
1769
2167
|
function nowIso(ctx) {
|
|
1770
2168
|
return new Date(ctx.now ? ctx.now() : Date.now()).toISOString();
|
|
1771
2169
|
}
|
|
@@ -1785,13 +2183,16 @@ function warnNoSourceMaps(serviceName) {
|
|
|
1785
2183
|
`[neat] ${serviceName}: no .map files found under dist/; observed file edges will land on dist paths, not src. Set sourceMap: true in tsconfig to enable file-level reconciliation.`
|
|
1786
2184
|
);
|
|
1787
2185
|
}
|
|
1788
|
-
function
|
|
2186
|
+
function pickAttrFrom(attrs, ...keys) {
|
|
1789
2187
|
for (const k of keys) {
|
|
1790
|
-
const v =
|
|
2188
|
+
const v = attrs[k];
|
|
1791
2189
|
if (typeof v === "string" && v.length > 0) return v;
|
|
1792
2190
|
}
|
|
1793
2191
|
return void 0;
|
|
1794
2192
|
}
|
|
2193
|
+
function pickAttr(span, ...keys) {
|
|
2194
|
+
return pickAttrFrom(span.attributes, ...keys);
|
|
2195
|
+
}
|
|
1795
2196
|
function hostFromUrl(u) {
|
|
1796
2197
|
if (!u) return void 0;
|
|
1797
2198
|
try {
|
|
@@ -1803,6 +2204,10 @@ function hostFromUrl(u) {
|
|
|
1803
2204
|
function pickAddress(span) {
|
|
1804
2205
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1805
2206
|
}
|
|
2207
|
+
function isLoopbackHost(host) {
|
|
2208
|
+
const h = host.toLowerCase();
|
|
2209
|
+
return h === "localhost" || h === "ip6-localhost" || h === "::1" || h === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(h);
|
|
2210
|
+
}
|
|
1806
2211
|
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1807
2212
|
var CODE_LINENO_ATTR = "code.lineno";
|
|
1808
2213
|
var CODE_FUNCTION_ATTR = "code.function";
|
|
@@ -1910,15 +2315,31 @@ function callSiteFromSpan(span, serviceNode, scanPath) {
|
|
|
1910
2315
|
...originalRelPath && originalRelPath !== relPath ? { originalRelPath } : {}
|
|
1911
2316
|
};
|
|
1912
2317
|
}
|
|
2318
|
+
function reconcileObservedRelPath(graph, serviceName, relPath) {
|
|
2319
|
+
if (graph.hasNode((0, import_types3.fileId)(serviceName, relPath))) return relPath;
|
|
2320
|
+
let best = null;
|
|
2321
|
+
graph.forEachNode((_id, attrs) => {
|
|
2322
|
+
const a = attrs;
|
|
2323
|
+
if (a.type !== import_types3.NodeType.FileNode || a.service !== serviceName) return;
|
|
2324
|
+
if (a.discoveredVia === "otel") return;
|
|
2325
|
+
const p = a.path;
|
|
2326
|
+
if (!p) return;
|
|
2327
|
+
if ((relPath === p || relPath.endsWith(`/${p}`)) && (!best || p.length > best.length)) {
|
|
2328
|
+
best = p;
|
|
2329
|
+
}
|
|
2330
|
+
});
|
|
2331
|
+
return best ?? relPath;
|
|
2332
|
+
}
|
|
1913
2333
|
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1914
|
-
const
|
|
2334
|
+
const relPath = reconcileObservedRelPath(graph, serviceName, callSite.relPath);
|
|
2335
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, relPath);
|
|
1915
2336
|
if (!graph.hasNode(fileNodeId)) {
|
|
1916
|
-
const language = languageForExt(
|
|
2337
|
+
const language = languageForExt(relPath);
|
|
1917
2338
|
const node = {
|
|
1918
2339
|
id: fileNodeId,
|
|
1919
2340
|
type: import_types3.NodeType.FileNode,
|
|
1920
2341
|
service: serviceName,
|
|
1921
|
-
path:
|
|
2342
|
+
path: relPath,
|
|
1922
2343
|
...language ? { language } : {},
|
|
1923
2344
|
...callSite.originalRelPath ? { originalPath: callSite.originalRelPath } : {},
|
|
1924
2345
|
discoveredVia: "otel"
|
|
@@ -1946,6 +2367,11 @@ function makeInferredEdgeId(type, source, target) {
|
|
|
1946
2367
|
}
|
|
1947
2368
|
var INFERRED_CONFIDENCE = 0.6;
|
|
1948
2369
|
var STITCH_MAX_DEPTH = 2;
|
|
2370
|
+
var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
2371
|
+
import_types3.EdgeType.CALLS,
|
|
2372
|
+
import_types3.EdgeType.CONNECTS_TO,
|
|
2373
|
+
import_types3.EdgeType.DEPENDS_ON
|
|
2374
|
+
]);
|
|
1949
2375
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1950
2376
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1951
2377
|
function spanMintsObservedEdge(kind) {
|
|
@@ -2119,6 +2545,7 @@ function stitchTrace(graph, sourceServiceId, ts) {
|
|
|
2119
2545
|
for (const edgeId of outbound) {
|
|
2120
2546
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2121
2547
|
if (edge.provenance !== import_types3.Provenance.EXTRACTED) continue;
|
|
2548
|
+
if (!STITCH_EDGE_TYPES.has(edge.type)) continue;
|
|
2122
2549
|
if (graph.hasEdge((0, import_types3.observedEdgeId)(edge.source, edge.target, edge.type))) continue;
|
|
2123
2550
|
upsertInferredEdge(graph, edge.type, edge.source, edge.target, ts);
|
|
2124
2551
|
if (!visited.has(edge.target)) {
|
|
@@ -2151,6 +2578,16 @@ async function appendErrorEvent(ctx, ev) {
|
|
|
2151
2578
|
await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(ctx.errorsPath), { recursive: true });
|
|
2152
2579
|
await import_node_fs4.promises.appendFile(ctx.errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
2153
2580
|
}
|
|
2581
|
+
function incidentAffectedNode(span, graph, scanPath) {
|
|
2582
|
+
const sid = (0, import_types3.serviceId)(span.service, span.env);
|
|
2583
|
+
const serviceNode = graph && graph.hasNode(sid) ? graph.getNodeAttributes(sid) : void 0;
|
|
2584
|
+
const callSite = callSiteFromSpan(span, serviceNode, scanPath);
|
|
2585
|
+
if (callSite) {
|
|
2586
|
+
const relPath = graph ? reconcileObservedRelPath(graph, span.service, callSite.relPath) : callSite.relPath;
|
|
2587
|
+
return (0, import_types3.fileId)(span.service, relPath);
|
|
2588
|
+
}
|
|
2589
|
+
return sid;
|
|
2590
|
+
}
|
|
2154
2591
|
function sanitizeAttributes(attrs) {
|
|
2155
2592
|
const out = {};
|
|
2156
2593
|
for (const [k, v] of Object.entries(attrs)) {
|
|
@@ -2159,7 +2596,7 @@ function sanitizeAttributes(attrs) {
|
|
|
2159
2596
|
}
|
|
2160
2597
|
return out;
|
|
2161
2598
|
}
|
|
2162
|
-
function buildErrorEventForReceiver(span) {
|
|
2599
|
+
function buildErrorEventForReceiver(span, graph, scanPath) {
|
|
2163
2600
|
if (span.statusCode !== 2) return null;
|
|
2164
2601
|
const ts = span.startTimeIso ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2165
2602
|
const attrs = sanitizeAttributes(span.attributes);
|
|
@@ -2169,16 +2606,16 @@ function buildErrorEventForReceiver(span) {
|
|
|
2169
2606
|
service: span.service,
|
|
2170
2607
|
traceId: span.traceId,
|
|
2171
2608
|
spanId: span.spanId,
|
|
2172
|
-
errorMessage: span
|
|
2609
|
+
errorMessage: incidentMessage(span),
|
|
2173
2610
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2174
2611
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2175
2612
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
2176
|
-
affectedNode: (
|
|
2613
|
+
affectedNode: incidentAffectedNode(span, graph, scanPath)
|
|
2177
2614
|
};
|
|
2178
2615
|
}
|
|
2179
|
-
function makeErrorSpanWriter(errorsPath) {
|
|
2616
|
+
function makeErrorSpanWriter(errorsPath, graph, scanPath) {
|
|
2180
2617
|
return async (span) => {
|
|
2181
|
-
const ev = buildErrorEventForReceiver(span);
|
|
2618
|
+
const ev = buildErrorEventForReceiver(span, graph, scanPath);
|
|
2182
2619
|
if (!ev) return;
|
|
2183
2620
|
await import_node_fs4.promises.mkdir(import_node_path4.default.dirname(errorsPath), { recursive: true });
|
|
2184
2621
|
await import_node_fs4.promises.appendFile(errorsPath, JSON.stringify(ev) + "\n", "utf8");
|
|
@@ -2262,7 +2699,10 @@ async function handleSpan(ctx, span) {
|
|
|
2262
2699
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
2263
2700
|
cacheSpanService(span, nowMs, callSite);
|
|
2264
2701
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
2265
|
-
const callSiteEvidence = callSite ? {
|
|
2702
|
+
const callSiteEvidence = callSite ? {
|
|
2703
|
+
file: reconcileObservedRelPath(ctx.graph, span.service, callSite.relPath),
|
|
2704
|
+
...callSite.line !== void 0 ? { line: callSite.line } : {}
|
|
2705
|
+
} : void 0;
|
|
2266
2706
|
let affectedNode = sourceId;
|
|
2267
2707
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2268
2708
|
if (span.dbSystem) {
|
|
@@ -2284,7 +2724,7 @@ async function handleSpan(ctx, span) {
|
|
|
2284
2724
|
} else {
|
|
2285
2725
|
const host = pickAddress(span);
|
|
2286
2726
|
let resolvedViaAddress = false;
|
|
2287
|
-
if (mintsFromCallerSide && host && host !== span.service) {
|
|
2727
|
+
if (mintsFromCallerSide && host && host !== span.service && !isLoopbackHost(host)) {
|
|
2288
2728
|
const targetId = resolveServiceId(ctx.graph, host, env);
|
|
2289
2729
|
if (targetId && targetId !== sourceId) {
|
|
2290
2730
|
upsertObservedEdge(
|
|
@@ -2319,7 +2759,11 @@ async function handleSpan(ctx, span) {
|
|
|
2319
2759
|
const parentId = ensureServiceNode(ctx.graph, parent.service, parent.env);
|
|
2320
2760
|
const fallbackSource = parent.callSite ? ensureObservedFileNode(ctx.graph, parent.service, parentId, parent.callSite) : parentId;
|
|
2321
2761
|
const fallbackEvidence = parent.callSite ? {
|
|
2322
|
-
file:
|
|
2762
|
+
file: reconcileObservedRelPath(
|
|
2763
|
+
ctx.graph,
|
|
2764
|
+
parent.service,
|
|
2765
|
+
parent.callSite.relPath
|
|
2766
|
+
),
|
|
2323
2767
|
...parent.callSite.line !== void 0 ? { line: parent.callSite.line } : {}
|
|
2324
2768
|
} : void 0;
|
|
2325
2769
|
upsertObservedEdge(
|
|
@@ -2344,7 +2788,7 @@ async function handleSpan(ctx, span) {
|
|
|
2344
2788
|
service: span.service,
|
|
2345
2789
|
traceId: span.traceId,
|
|
2346
2790
|
spanId: span.spanId,
|
|
2347
|
-
errorMessage: span
|
|
2791
|
+
errorMessage: incidentMessage(span),
|
|
2348
2792
|
...span.exception?.type ? { exceptionType: span.exception.type } : {},
|
|
2349
2793
|
...span.exception?.stacktrace ? { exceptionStacktrace: span.exception.stacktrace } : {},
|
|
2350
2794
|
...Object.keys(attrs).length > 0 ? { attributes: attrs } : {},
|
|
@@ -2520,12 +2964,43 @@ function startStalenessLoop(graph, options = {}) {
|
|
|
2520
2964
|
async function readErrorEvents(errorsPath) {
|
|
2521
2965
|
try {
|
|
2522
2966
|
const raw = await import_node_fs4.promises.readFile(errorsPath, "utf8");
|
|
2523
|
-
|
|
2967
|
+
const events = raw.split("\n").filter((line) => line.length > 0).map((line) => JSON.parse(line));
|
|
2968
|
+
return dedupeIncidents(events);
|
|
2524
2969
|
} catch (err) {
|
|
2525
2970
|
if (err.code === "ENOENT") return [];
|
|
2526
2971
|
throw err;
|
|
2527
2972
|
}
|
|
2528
2973
|
}
|
|
2974
|
+
function isSynthesizedHttpIncident(ev) {
|
|
2975
|
+
if (ev.exceptionType || ev.exceptionStacktrace) return false;
|
|
2976
|
+
if (ev.errorType) return false;
|
|
2977
|
+
if (!ev.attributes) return false;
|
|
2978
|
+
const synth = httpFailureMessageFromAttrs(ev.attributes);
|
|
2979
|
+
return synth !== void 0 && synth === ev.errorMessage;
|
|
2980
|
+
}
|
|
2981
|
+
function dedupeIncidents(events) {
|
|
2982
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2983
|
+
const once = [];
|
|
2984
|
+
for (const ev of events) {
|
|
2985
|
+
const key = ev.id ?? (ev.traceId && ev.spanId ? `${ev.traceId}:${ev.spanId}` : void 0);
|
|
2986
|
+
if (key === void 0) {
|
|
2987
|
+
once.push(ev);
|
|
2988
|
+
continue;
|
|
2989
|
+
}
|
|
2990
|
+
if (seen.has(key)) continue;
|
|
2991
|
+
seen.add(key);
|
|
2992
|
+
once.push(ev);
|
|
2993
|
+
}
|
|
2994
|
+
const groupKey = (ev) => `${ev.traceId}\0${ev.affectedNode}`;
|
|
2995
|
+
const hasRealFailure = /* @__PURE__ */ new Set();
|
|
2996
|
+
for (const ev of once) {
|
|
2997
|
+
if (ev.traceId && !isSynthesizedHttpIncident(ev)) hasRealFailure.add(groupKey(ev));
|
|
2998
|
+
}
|
|
2999
|
+
return once.filter((ev) => {
|
|
3000
|
+
if (!ev.traceId || !isSynthesizedHttpIncident(ev)) return true;
|
|
3001
|
+
return !hasRealFailure.has(groupKey(ev));
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
2529
3004
|
function mergeSnapshot(graph, snapshot) {
|
|
2530
3005
|
const exported = snapshot.graph;
|
|
2531
3006
|
let nodesAdded = 0;
|
|
@@ -2606,10 +3081,17 @@ function isConfigFile(name) {
|
|
|
2606
3081
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
2607
3082
|
if (name === ".env" || name.startsWith(".env.")) {
|
|
2608
3083
|
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
3084
|
+
if (isNeatAuthoredEnvFile(name)) return { match: false, fileType: "" };
|
|
2609
3085
|
return { match: true, fileType: "env" };
|
|
2610
3086
|
}
|
|
2611
3087
|
return { match: false, fileType: "" };
|
|
2612
3088
|
}
|
|
3089
|
+
function isNeatAuthoredEnvFile(name) {
|
|
3090
|
+
return name === ".env.neat";
|
|
3091
|
+
}
|
|
3092
|
+
function isNeatAuthoredSourceFile(name) {
|
|
3093
|
+
return /^otel-init\.(?:js|cjs|mjs|ts|tsx)$/i.test(name);
|
|
3094
|
+
}
|
|
2613
3095
|
function isTestPath(filePath) {
|
|
2614
3096
|
const normalised = filePath.replace(/\\/g, "/");
|
|
2615
3097
|
const segments = normalised.split("/");
|
|
@@ -3315,7 +3797,9 @@ async function walkSourceFiles(dir) {
|
|
|
3315
3797
|
if (IGNORED_DIRS.has(entry2.name)) continue;
|
|
3316
3798
|
if (await isPythonVenvDir(full)) continue;
|
|
3317
3799
|
await walk(full);
|
|
3318
|
-
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name))
|
|
3800
|
+
} else if (entry2.isFile() && SERVICE_FILE_EXTENSIONS.has(import_node_path11.default.extname(entry2.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3801
|
+
// would attribute our instrumentation imports to the user's service.
|
|
3802
|
+
!isNeatAuthoredSourceFile(entry2.name)) {
|
|
3319
3803
|
out.push(full);
|
|
3320
3804
|
}
|
|
3321
3805
|
}
|
|
@@ -3485,11 +3969,28 @@ function collectJsImports(node, out) {
|
|
|
3485
3969
|
if (child) collectJsImports(child, out);
|
|
3486
3970
|
}
|
|
3487
3971
|
}
|
|
3972
|
+
function collectImportedNames(node, out) {
|
|
3973
|
+
if (node.type === "aliased_import") {
|
|
3974
|
+
const nameNode = node.childForFieldName("name");
|
|
3975
|
+
if (nameNode) out.push(nameNode.text);
|
|
3976
|
+
return;
|
|
3977
|
+
}
|
|
3978
|
+
if (node.type === "dotted_name") {
|
|
3979
|
+
out.push(node.text);
|
|
3980
|
+
return;
|
|
3981
|
+
}
|
|
3982
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
3983
|
+
const child = node.namedChild(i);
|
|
3984
|
+
if (child) collectImportedNames(child, out);
|
|
3985
|
+
}
|
|
3986
|
+
}
|
|
3488
3987
|
function collectPyImports(node, out) {
|
|
3489
3988
|
if (node.type === "import_from_statement") {
|
|
3490
3989
|
let level = 0;
|
|
3491
3990
|
let modulePath = "";
|
|
3991
|
+
const names = [];
|
|
3492
3992
|
let pastFrom = false;
|
|
3993
|
+
let pastImport = false;
|
|
3493
3994
|
for (let i = 0; i < node.childCount; i++) {
|
|
3494
3995
|
const child = node.child(i);
|
|
3495
3996
|
if (!child) continue;
|
|
@@ -3497,26 +3998,30 @@ function collectPyImports(node, out) {
|
|
|
3497
3998
|
if (child.type === "from") pastFrom = true;
|
|
3498
3999
|
continue;
|
|
3499
4000
|
}
|
|
3500
|
-
if (
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
if (!rc) continue;
|
|
3505
|
-
if (rc.type === "import_prefix") {
|
|
3506
|
-
for (let k = 0; k < rc.childCount; k++) {
|
|
3507
|
-
if (rc.child(k)?.type === ".") level++;
|
|
3508
|
-
}
|
|
3509
|
-
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4001
|
+
if (!pastImport) {
|
|
4002
|
+
if (child.type === "import") {
|
|
4003
|
+
pastImport = true;
|
|
4004
|
+
continue;
|
|
3510
4005
|
}
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
4006
|
+
if (child.type === "relative_import") {
|
|
4007
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
4008
|
+
const rc = child.child(j);
|
|
4009
|
+
if (!rc) continue;
|
|
4010
|
+
if (rc.type === "import_prefix") {
|
|
4011
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
4012
|
+
if (rc.child(k)?.type === ".") level++;
|
|
4013
|
+
}
|
|
4014
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
4015
|
+
}
|
|
4016
|
+
} else if (child.type === "dotted_name") {
|
|
4017
|
+
modulePath = child.text;
|
|
4018
|
+
}
|
|
4019
|
+
continue;
|
|
3516
4020
|
}
|
|
4021
|
+
collectImportedNames(child, names);
|
|
3517
4022
|
}
|
|
3518
4023
|
if (level > 0 || modulePath) {
|
|
3519
|
-
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
4024
|
+
out.push({ modulePath, level, names, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
3520
4025
|
}
|
|
3521
4026
|
}
|
|
3522
4027
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
@@ -3618,8 +4123,6 @@ async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
|
3618
4123
|
return null;
|
|
3619
4124
|
}
|
|
3620
4125
|
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
3621
|
-
if (!imp.modulePath) return null;
|
|
3622
|
-
const relPath = imp.modulePath.split(".").join("/");
|
|
3623
4126
|
let baseDir;
|
|
3624
4127
|
if (imp.level > 0) {
|
|
3625
4128
|
baseDir = import_node_path13.default.dirname(importerPath);
|
|
@@ -3627,13 +4130,30 @@ async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
|
3627
4130
|
} else {
|
|
3628
4131
|
baseDir = serviceDir;
|
|
3629
4132
|
}
|
|
3630
|
-
const
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
4133
|
+
const moduleBase = imp.modulePath ? import_node_path13.default.join(baseDir, imp.modulePath.split(".").join("/")) : baseDir;
|
|
4134
|
+
const resolved = /* @__PURE__ */ new Set();
|
|
4135
|
+
let needModuleFile = imp.names.length === 0;
|
|
4136
|
+
for (const name of imp.names) {
|
|
4137
|
+
const submoduleFile = import_node_path13.default.join(moduleBase, `${name}.py`);
|
|
4138
|
+
const subpackageInit = import_node_path13.default.join(moduleBase, name, "__init__.py");
|
|
4139
|
+
if (isWithinServiceDir(submoduleFile, serviceDir) && await fileExists(submoduleFile)) {
|
|
4140
|
+
resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, submoduleFile)));
|
|
4141
|
+
} else if (isWithinServiceDir(subpackageInit, serviceDir) && await fileExists(subpackageInit)) {
|
|
4142
|
+
resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, subpackageInit)));
|
|
4143
|
+
} else {
|
|
4144
|
+
needModuleFile = true;
|
|
3634
4145
|
}
|
|
3635
4146
|
}
|
|
3636
|
-
|
|
4147
|
+
if (needModuleFile) {
|
|
4148
|
+
const moduleFileCandidates = imp.modulePath ? [`${moduleBase}.py`, import_node_path13.default.join(moduleBase, "__init__.py")] : [import_node_path13.default.join(moduleBase, "__init__.py")];
|
|
4149
|
+
for (const candidate of moduleFileCandidates) {
|
|
4150
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
4151
|
+
resolved.add(toPosix2(import_node_path13.default.relative(serviceDir, candidate)));
|
|
4152
|
+
break;
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
return [...resolved];
|
|
3637
4157
|
}
|
|
3638
4158
|
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
3639
4159
|
const importeeFileId = (0, import_types8.fileId)(serviceName, importeeRelPath);
|
|
@@ -3674,17 +4194,18 @@ async function addImports(graph, services) {
|
|
|
3674
4194
|
continue;
|
|
3675
4195
|
}
|
|
3676
4196
|
for (const imp of pyImports) {
|
|
3677
|
-
const
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
4197
|
+
const resolvedPaths = await resolvePyImport(imp, file.path, service.dir);
|
|
4198
|
+
for (const resolved of resolvedPaths) {
|
|
4199
|
+
edgesAdded += emitImportEdge(
|
|
4200
|
+
graph,
|
|
4201
|
+
service.pkg.name,
|
|
4202
|
+
importerFileId,
|
|
4203
|
+
relFile,
|
|
4204
|
+
resolved,
|
|
4205
|
+
imp.line,
|
|
4206
|
+
imp.snippet
|
|
4207
|
+
);
|
|
4208
|
+
}
|
|
3688
4209
|
}
|
|
3689
4210
|
continue;
|
|
3690
4211
|
}
|
|
@@ -4320,6 +4841,36 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4320
4841
|
edgesAdded++;
|
|
4321
4842
|
}
|
|
4322
4843
|
}
|
|
4844
|
+
if (allConfigs.length === 1) {
|
|
4845
|
+
const primary = allConfigs[0];
|
|
4846
|
+
service.node.dbConnectionTarget = primary.port ? `${primary.host}:${primary.port}` : primary.host;
|
|
4847
|
+
const relPath = import_node_path21.default.relative(scanPath, primary.sourceFile);
|
|
4848
|
+
const cfgId = (0, import_types9.configId)(relPath);
|
|
4849
|
+
if (!graph.hasNode(cfgId)) {
|
|
4850
|
+
const cfgNode = {
|
|
4851
|
+
id: cfgId,
|
|
4852
|
+
type: import_types9.NodeType.ConfigNode,
|
|
4853
|
+
name: import_node_path21.default.basename(primary.sourceFile),
|
|
4854
|
+
path: relPath,
|
|
4855
|
+
fileType: isConfigFile(import_node_path21.default.basename(primary.sourceFile)).fileType || "config"
|
|
4856
|
+
};
|
|
4857
|
+
graph.addNode(cfgId, cfgNode);
|
|
4858
|
+
nodesAdded++;
|
|
4859
|
+
}
|
|
4860
|
+
const cfgEdge = {
|
|
4861
|
+
id: (0, import_types4.extractedEdgeId)(service.node.id, cfgId, import_types9.EdgeType.CONFIGURED_BY),
|
|
4862
|
+
source: service.node.id,
|
|
4863
|
+
target: cfgId,
|
|
4864
|
+
type: import_types9.EdgeType.CONFIGURED_BY,
|
|
4865
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
4866
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
4867
|
+
evidence: { file: toPosix2(relPath) }
|
|
4868
|
+
};
|
|
4869
|
+
if (!graph.hasEdge(cfgEdge.id)) {
|
|
4870
|
+
graph.addEdgeWithKey(cfgEdge.id, cfgEdge.source, cfgEdge.target, cfgEdge);
|
|
4871
|
+
edgesAdded++;
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4323
4874
|
attachIncompatibilities(service, allConfigs);
|
|
4324
4875
|
if (graph.hasNode(service.node.id)) {
|
|
4325
4876
|
const current = graph.getNodeAttributes(service.node.id);
|
|
@@ -4331,6 +4882,9 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
4331
4882
|
if (!service.node.incompatibilities || service.node.incompatibilities.length === 0) {
|
|
4332
4883
|
delete updated.incompatibilities;
|
|
4333
4884
|
}
|
|
4885
|
+
if (!service.node.dbConnectionTarget) {
|
|
4886
|
+
delete updated.dbConnectionTarget;
|
|
4887
|
+
}
|
|
4334
4888
|
graph.replaceNodeAttributes(service.node.id, updated);
|
|
4335
4889
|
}
|
|
4336
4890
|
}
|
|
@@ -4508,7 +5062,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4508
5062
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4509
5063
|
if (seen.has(dedupKey)) continue;
|
|
4510
5064
|
seen.add(dedupKey);
|
|
4511
|
-
const confidence = (0, import_types11.confidenceForExtracted)("
|
|
5065
|
+
const confidence = (0, import_types11.confidenceForExtracted)("url-literal-service-target");
|
|
4512
5066
|
const ev = {
|
|
4513
5067
|
file: relFile,
|
|
4514
5068
|
line: site.line,
|
|
@@ -4528,7 +5082,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4528
5082
|
target: targetId,
|
|
4529
5083
|
type: import_types11.EdgeType.CALLS,
|
|
4530
5084
|
confidence,
|
|
4531
|
-
confidenceKind: "
|
|
5085
|
+
confidenceKind: "url-literal-service-target",
|
|
4532
5086
|
evidence: ev
|
|
4533
5087
|
});
|
|
4534
5088
|
continue;
|
|
@@ -5034,19 +5588,29 @@ init_cjs_shims();
|
|
|
5034
5588
|
var import_node_path30 = __toESM(require("path"), 1);
|
|
5035
5589
|
var import_node_fs16 = require("fs");
|
|
5036
5590
|
var import_types20 = require("@neat.is/types");
|
|
5037
|
-
function
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5591
|
+
function readDockerfile(content) {
|
|
5592
|
+
let image = null;
|
|
5593
|
+
const ports = [];
|
|
5594
|
+
let cmd = null;
|
|
5595
|
+
let entrypoint = null;
|
|
5596
|
+
for (const raw of content.split("\n")) {
|
|
5041
5597
|
const line = raw.trim();
|
|
5042
5598
|
if (!line || line.startsWith("#")) continue;
|
|
5043
|
-
if (
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
if (
|
|
5047
|
-
|
|
5599
|
+
if (/^from\s+/i.test(line)) {
|
|
5600
|
+
const candidate = line.split(/\s+/)[1];
|
|
5601
|
+
if (candidate && candidate.toLowerCase() !== "scratch") image = candidate;
|
|
5602
|
+
} else if (/^expose\s+/i.test(line)) {
|
|
5603
|
+
for (const token of line.split(/\s+/).slice(1)) {
|
|
5604
|
+
const port = Number.parseInt(token.split("/")[0], 10);
|
|
5605
|
+
if (Number.isInteger(port) && !ports.includes(port)) ports.push(port);
|
|
5606
|
+
}
|
|
5607
|
+
} else if (/^entrypoint\s+/i.test(line)) {
|
|
5608
|
+
entrypoint = line;
|
|
5609
|
+
} else if (/^cmd\s+/i.test(line)) {
|
|
5610
|
+
cmd = line;
|
|
5611
|
+
}
|
|
5048
5612
|
}
|
|
5049
|
-
return
|
|
5613
|
+
return { image, ports, entrypoint: entrypoint ?? cmd };
|
|
5050
5614
|
}
|
|
5051
5615
|
async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
5052
5616
|
let nodesAdded = 0;
|
|
@@ -5065,14 +5629,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5065
5629
|
);
|
|
5066
5630
|
continue;
|
|
5067
5631
|
}
|
|
5068
|
-
const
|
|
5069
|
-
if (!image) continue;
|
|
5070
|
-
const node = makeInfraNode("container-image", image);
|
|
5632
|
+
const facts = readDockerfile(content);
|
|
5633
|
+
if (!facts.image) continue;
|
|
5634
|
+
const node = makeInfraNode("container-image", facts.image);
|
|
5071
5635
|
if (!graph.hasNode(node.id)) {
|
|
5072
5636
|
graph.addNode(node.id, node);
|
|
5073
5637
|
nodesAdded++;
|
|
5074
5638
|
}
|
|
5075
5639
|
const relDockerfile = toPosix2(import_node_path30.default.relative(service.dir, dockerfilePath));
|
|
5640
|
+
const evidenceFile = toPosix2(import_node_path30.default.relative(scanPath, dockerfilePath));
|
|
5076
5641
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5077
5642
|
graph,
|
|
5078
5643
|
service.pkg.name,
|
|
@@ -5091,12 +5656,33 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5091
5656
|
provenance: import_types20.Provenance.EXTRACTED,
|
|
5092
5657
|
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5093
5658
|
evidence: {
|
|
5094
|
-
file:
|
|
5659
|
+
file: evidenceFile,
|
|
5660
|
+
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
5095
5661
|
}
|
|
5096
5662
|
};
|
|
5097
5663
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5098
5664
|
edgesAdded++;
|
|
5099
5665
|
}
|
|
5666
|
+
for (const port of facts.ports) {
|
|
5667
|
+
const portNode = makeInfraNode("port", String(port));
|
|
5668
|
+
if (!graph.hasNode(portNode.id)) {
|
|
5669
|
+
graph.addNode(portNode.id, portNode);
|
|
5670
|
+
nodesAdded++;
|
|
5671
|
+
}
|
|
5672
|
+
const portEdgeId = (0, import_types4.extractedEdgeId)(fileNodeId, portNode.id, import_types20.EdgeType.CONNECTS_TO);
|
|
5673
|
+
if (graph.hasEdge(portEdgeId)) continue;
|
|
5674
|
+
const portEdge = {
|
|
5675
|
+
id: portEdgeId,
|
|
5676
|
+
source: fileNodeId,
|
|
5677
|
+
target: portNode.id,
|
|
5678
|
+
type: import_types20.EdgeType.CONNECTS_TO,
|
|
5679
|
+
provenance: import_types20.Provenance.EXTRACTED,
|
|
5680
|
+
confidence: (0, import_types20.confidenceForExtracted)("structural"),
|
|
5681
|
+
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5682
|
+
};
|
|
5683
|
+
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
5684
|
+
edgesAdded++;
|
|
5685
|
+
}
|
|
5100
5686
|
}
|
|
5101
5687
|
return { nodesAdded, edgesAdded };
|
|
5102
5688
|
}
|
|
@@ -5105,7 +5691,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5105
5691
|
init_cjs_shims();
|
|
5106
5692
|
var import_node_fs17 = require("fs");
|
|
5107
5693
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
5694
|
+
var import_types21 = require("@neat.is/types");
|
|
5108
5695
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5696
|
+
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5109
5697
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5110
5698
|
if (depth > max) return [];
|
|
5111
5699
|
const out = [];
|
|
@@ -5122,24 +5710,86 @@ async function walkTfFiles(start, depth = 0, max = 5) {
|
|
|
5122
5710
|
}
|
|
5123
5711
|
return out;
|
|
5124
5712
|
}
|
|
5713
|
+
function blockBody(content, from) {
|
|
5714
|
+
const open = content.indexOf("{", from);
|
|
5715
|
+
if (open === -1) return null;
|
|
5716
|
+
let depth = 0;
|
|
5717
|
+
for (let i = open; i < content.length; i++) {
|
|
5718
|
+
const ch = content[i];
|
|
5719
|
+
if (ch === "{") depth++;
|
|
5720
|
+
else if (ch === "}") {
|
|
5721
|
+
depth--;
|
|
5722
|
+
if (depth === 0) return { body: content.slice(open + 1, i), offset: open + 1 };
|
|
5723
|
+
}
|
|
5724
|
+
}
|
|
5725
|
+
return null;
|
|
5726
|
+
}
|
|
5727
|
+
function lineAt(content, index) {
|
|
5728
|
+
let line = 1;
|
|
5729
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
5730
|
+
if (content[i] === "\n") line++;
|
|
5731
|
+
}
|
|
5732
|
+
return line;
|
|
5733
|
+
}
|
|
5125
5734
|
async function addTerraformResources(graph, scanPath) {
|
|
5126
5735
|
let nodesAdded = 0;
|
|
5736
|
+
let edgesAdded = 0;
|
|
5127
5737
|
const files = await walkTfFiles(scanPath);
|
|
5128
5738
|
for (const file of files) {
|
|
5129
5739
|
const content = await import_node_fs17.promises.readFile(file, "utf8");
|
|
5740
|
+
const evidenceFile = toPosix2(import_node_path31.default.relative(scanPath, file));
|
|
5741
|
+
const resources = [];
|
|
5742
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
5130
5743
|
RESOURCE_RE.lastIndex = 0;
|
|
5131
5744
|
let m;
|
|
5132
5745
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
5133
|
-
const
|
|
5746
|
+
const type = m[1];
|
|
5134
5747
|
const name = m[2];
|
|
5135
|
-
const node = makeInfraNode(
|
|
5748
|
+
const node = makeInfraNode(type, name, "aws");
|
|
5136
5749
|
if (!graph.hasNode(node.id)) {
|
|
5137
5750
|
graph.addNode(node.id, node);
|
|
5138
5751
|
nodesAdded++;
|
|
5139
5752
|
}
|
|
5753
|
+
const span = blockBody(content, RESOURCE_RE.lastIndex);
|
|
5754
|
+
const resource = {
|
|
5755
|
+
type,
|
|
5756
|
+
name,
|
|
5757
|
+
nodeId: node.id,
|
|
5758
|
+
body: span?.body ?? "",
|
|
5759
|
+
bodyOffset: span?.offset ?? RESOURCE_RE.lastIndex
|
|
5760
|
+
};
|
|
5761
|
+
resources.push(resource);
|
|
5762
|
+
byKey.set(`${type}.${name}`, resource);
|
|
5763
|
+
}
|
|
5764
|
+
for (const resource of resources) {
|
|
5765
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5766
|
+
REFERENCE_RE.lastIndex = 0;
|
|
5767
|
+
let ref;
|
|
5768
|
+
while ((ref = REFERENCE_RE.exec(resource.body)) !== null) {
|
|
5769
|
+
const key = `${ref[1]}.${ref[2]}`;
|
|
5770
|
+
if (key === `${resource.type}.${resource.name}`) continue;
|
|
5771
|
+
const target = byKey.get(key);
|
|
5772
|
+
if (!target) continue;
|
|
5773
|
+
if (seen.has(target.nodeId)) continue;
|
|
5774
|
+
seen.add(target.nodeId);
|
|
5775
|
+
const edgeId = (0, import_types4.extractedEdgeId)(resource.nodeId, target.nodeId, import_types21.EdgeType.DEPENDS_ON);
|
|
5776
|
+
if (graph.hasEdge(edgeId)) continue;
|
|
5777
|
+
const line = lineAt(content, resource.bodyOffset + ref.index);
|
|
5778
|
+
const edge = {
|
|
5779
|
+
id: edgeId,
|
|
5780
|
+
source: resource.nodeId,
|
|
5781
|
+
target: target.nodeId,
|
|
5782
|
+
type: import_types21.EdgeType.DEPENDS_ON,
|
|
5783
|
+
provenance: import_types21.Provenance.EXTRACTED,
|
|
5784
|
+
confidence: (0, import_types21.confidenceForExtracted)("structural"),
|
|
5785
|
+
evidence: { file: evidenceFile, line, snippet: key }
|
|
5786
|
+
};
|
|
5787
|
+
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
5788
|
+
edgesAdded++;
|
|
5789
|
+
}
|
|
5140
5790
|
}
|
|
5141
5791
|
}
|
|
5142
|
-
return { nodesAdded, edgesAdded
|
|
5792
|
+
return { nodesAdded, edgesAdded };
|
|
5143
5793
|
}
|
|
5144
5794
|
|
|
5145
5795
|
// src/extract/infra/k8s.ts
|
|
@@ -5217,11 +5867,11 @@ var import_node_path34 = __toESM(require("path"), 1);
|
|
|
5217
5867
|
init_cjs_shims();
|
|
5218
5868
|
var import_node_fs19 = require("fs");
|
|
5219
5869
|
var import_node_path33 = __toESM(require("path"), 1);
|
|
5220
|
-
var
|
|
5870
|
+
var import_types22 = require("@neat.is/types");
|
|
5221
5871
|
function dropOrphanedFileNodes(graph) {
|
|
5222
5872
|
const orphans = [];
|
|
5223
5873
|
graph.forEachNode((id, attrs) => {
|
|
5224
|
-
if (attrs.type !==
|
|
5874
|
+
if (attrs.type !== import_types22.NodeType.FileNode) return;
|
|
5225
5875
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5226
5876
|
orphans.push(id);
|
|
5227
5877
|
}
|
|
@@ -5234,7 +5884,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
5234
5884
|
const toDrop = [];
|
|
5235
5885
|
graph.forEachEdge((id, attrs) => {
|
|
5236
5886
|
const edge = attrs;
|
|
5237
|
-
if (edge.provenance !==
|
|
5887
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5238
5888
|
if (!edge.evidence?.file) return;
|
|
5239
5889
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
5240
5890
|
});
|
|
@@ -5247,7 +5897,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5247
5897
|
const bases = [scanPath, ...serviceDirs];
|
|
5248
5898
|
graph.forEachEdge((id, attrs) => {
|
|
5249
5899
|
const edge = attrs;
|
|
5250
|
-
if (edge.provenance !==
|
|
5900
|
+
if (edge.provenance !== import_types22.Provenance.EXTRACTED) return;
|
|
5251
5901
|
const evidenceFile = edge.evidence?.file;
|
|
5252
5902
|
if (!evidenceFile) return;
|
|
5253
5903
|
if (import_node_path33.default.isAbsolute(evidenceFile)) {
|
|
@@ -5328,7 +5978,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5328
5978
|
|
|
5329
5979
|
// src/divergences.ts
|
|
5330
5980
|
init_cjs_shims();
|
|
5331
|
-
var
|
|
5981
|
+
var import_types23 = require("@neat.is/types");
|
|
5332
5982
|
function bucketKey(source, target, type) {
|
|
5333
5983
|
return `${type}|${source}|${target}`;
|
|
5334
5984
|
}
|
|
@@ -5336,22 +5986,22 @@ function bucketEdges(graph) {
|
|
|
5336
5986
|
const buckets = /* @__PURE__ */ new Map();
|
|
5337
5987
|
graph.forEachEdge((id, attrs) => {
|
|
5338
5988
|
const e = attrs;
|
|
5339
|
-
const parsed = (0,
|
|
5989
|
+
const parsed = (0, import_types23.parseEdgeId)(id);
|
|
5340
5990
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
5341
5991
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5342
5992
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5343
5993
|
switch (provenance) {
|
|
5344
|
-
case
|
|
5994
|
+
case import_types23.Provenance.EXTRACTED:
|
|
5345
5995
|
cur.extracted = e;
|
|
5346
5996
|
break;
|
|
5347
|
-
case
|
|
5997
|
+
case import_types23.Provenance.OBSERVED:
|
|
5348
5998
|
cur.observed = e;
|
|
5349
5999
|
break;
|
|
5350
|
-
case
|
|
6000
|
+
case import_types23.Provenance.INFERRED:
|
|
5351
6001
|
cur.inferred = e;
|
|
5352
6002
|
break;
|
|
5353
6003
|
default:
|
|
5354
|
-
if (e.provenance ===
|
|
6004
|
+
if (e.provenance === import_types23.Provenance.STALE) cur.stale = e;
|
|
5355
6005
|
}
|
|
5356
6006
|
buckets.set(key, cur);
|
|
5357
6007
|
});
|
|
@@ -5360,7 +6010,7 @@ function bucketEdges(graph) {
|
|
|
5360
6010
|
function nodeIsFrontier(graph, nodeId) {
|
|
5361
6011
|
if (!graph.hasNode(nodeId)) return false;
|
|
5362
6012
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5363
|
-
return attrs.type ===
|
|
6013
|
+
return attrs.type === import_types23.NodeType.FrontierNode;
|
|
5364
6014
|
}
|
|
5365
6015
|
function clampConfidence(n) {
|
|
5366
6016
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5379,10 +6029,16 @@ function gradedConfidence(edge) {
|
|
|
5379
6029
|
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
5380
6030
|
return clampConfidence(confidenceForEdge(edge));
|
|
5381
6031
|
}
|
|
6032
|
+
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
6033
|
+
import_types23.EdgeType.CALLS,
|
|
6034
|
+
import_types23.EdgeType.CONNECTS_TO,
|
|
6035
|
+
import_types23.EdgeType.PUBLISHES_TO,
|
|
6036
|
+
import_types23.EdgeType.CONSUMES_FROM
|
|
6037
|
+
]);
|
|
5382
6038
|
function detectMissingDivergences(graph, bucket) {
|
|
5383
6039
|
const out = [];
|
|
5384
|
-
if (bucket.type ===
|
|
5385
|
-
if (bucket.extracted && !bucket.observed) {
|
|
6040
|
+
if (bucket.type === import_types23.EdgeType.CONTAINS) return out;
|
|
6041
|
+
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5386
6042
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5387
6043
|
out.push({
|
|
5388
6044
|
type: "missing-observed",
|
|
@@ -5422,7 +6078,7 @@ function declaredHostFor(svc) {
|
|
|
5422
6078
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5423
6079
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5424
6080
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5425
|
-
if (e.type ===
|
|
6081
|
+
if (e.type === import_types23.EdgeType.CONFIGURED_BY && e.provenance === import_types23.Provenance.EXTRACTED) {
|
|
5426
6082
|
return true;
|
|
5427
6083
|
}
|
|
5428
6084
|
}
|
|
@@ -5435,10 +6091,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5435
6091
|
const out = [];
|
|
5436
6092
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5437
6093
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5438
|
-
if (edge.type !==
|
|
5439
|
-
if (edge.provenance !==
|
|
6094
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
6095
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5440
6096
|
const target = graph.getNodeAttributes(edge.target);
|
|
5441
|
-
if (target.type !==
|
|
6097
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5442
6098
|
const observedHost = target.host?.trim();
|
|
5443
6099
|
if (!observedHost) continue;
|
|
5444
6100
|
if (observedHost === declaredHost) continue;
|
|
@@ -5460,10 +6116,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5460
6116
|
const deps = svc.dependencies ?? {};
|
|
5461
6117
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5462
6118
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5463
|
-
if (edge.type !==
|
|
5464
|
-
if (edge.provenance !==
|
|
6119
|
+
if (edge.type !== import_types23.EdgeType.CONNECTS_TO) continue;
|
|
6120
|
+
if (edge.provenance !== import_types23.Provenance.OBSERVED) continue;
|
|
5465
6121
|
const target = graph.getNodeAttributes(edge.target);
|
|
5466
|
-
if (target.type !==
|
|
6122
|
+
if (target.type !== import_types23.NodeType.DatabaseNode) continue;
|
|
5467
6123
|
for (const pair of compatPairs()) {
|
|
5468
6124
|
if (pair.engine !== target.engine) continue;
|
|
5469
6125
|
const declared = deps[pair.driver];
|
|
@@ -5516,6 +6172,23 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5516
6172
|
function involvesNode(d, nodeId) {
|
|
5517
6173
|
return d.source === nodeId || d.target === nodeId;
|
|
5518
6174
|
}
|
|
6175
|
+
function suppressHostMismatchHalves(all) {
|
|
6176
|
+
const observedHalf = /* @__PURE__ */ new Set();
|
|
6177
|
+
const declaredHalf = /* @__PURE__ */ new Set();
|
|
6178
|
+
for (const d of all) {
|
|
6179
|
+
if (d.type !== "host-mismatch") continue;
|
|
6180
|
+
observedHalf.add(`${d.source}->${d.target}`);
|
|
6181
|
+
declaredHalf.add((0, import_types23.databaseId)(d.extractedHost));
|
|
6182
|
+
}
|
|
6183
|
+
if (observedHalf.size === 0) return all;
|
|
6184
|
+
return all.filter((d) => {
|
|
6185
|
+
if (d.type === "missing-extracted" && observedHalf.has(`${d.source}->${d.target}`)) {
|
|
6186
|
+
return false;
|
|
6187
|
+
}
|
|
6188
|
+
if (d.type === "missing-observed" && declaredHalf.has(d.target)) return false;
|
|
6189
|
+
return true;
|
|
6190
|
+
});
|
|
6191
|
+
}
|
|
5519
6192
|
function computeDivergences(graph, opts = {}) {
|
|
5520
6193
|
const all = [];
|
|
5521
6194
|
const buckets = bucketEdges(graph);
|
|
@@ -5524,12 +6197,13 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5524
6197
|
}
|
|
5525
6198
|
graph.forEachNode((nodeId, attrs) => {
|
|
5526
6199
|
const n = attrs;
|
|
5527
|
-
if (n.type !==
|
|
6200
|
+
if (n.type !== import_types23.NodeType.ServiceNode) return;
|
|
5528
6201
|
const svc = n;
|
|
5529
6202
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5530
6203
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
5531
6204
|
});
|
|
5532
|
-
|
|
6205
|
+
const reconciled = suppressHostMismatchHalves(all);
|
|
6206
|
+
let filtered = reconciled;
|
|
5533
6207
|
if (opts.type) {
|
|
5534
6208
|
const allowed = opts.type;
|
|
5535
6209
|
filtered = filtered.filter((d) => allowed.has(d.type));
|
|
@@ -5557,7 +6231,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5557
6231
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
5558
6232
|
return a.target.localeCompare(b.target);
|
|
5559
6233
|
});
|
|
5560
|
-
return
|
|
6234
|
+
return import_types23.DivergenceResultSchema.parse({
|
|
5561
6235
|
divergences: filtered,
|
|
5562
6236
|
totalAffected: filtered.length,
|
|
5563
6237
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -5568,7 +6242,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5568
6242
|
init_cjs_shims();
|
|
5569
6243
|
var import_node_fs20 = require("fs");
|
|
5570
6244
|
var import_node_path35 = __toESM(require("path"), 1);
|
|
5571
|
-
var
|
|
6245
|
+
var import_types24 = require("@neat.is/types");
|
|
5572
6246
|
var SCHEMA_VERSION = 4;
|
|
5573
6247
|
function migrateV1ToV2(payload) {
|
|
5574
6248
|
const nodes = payload.graph.nodes;
|
|
@@ -5590,12 +6264,12 @@ function migrateV2ToV3(payload) {
|
|
|
5590
6264
|
for (const edge of edges) {
|
|
5591
6265
|
const attrs = edge.attributes;
|
|
5592
6266
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5593
|
-
attrs.provenance =
|
|
6267
|
+
attrs.provenance = import_types24.Provenance.OBSERVED;
|
|
5594
6268
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5595
6269
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5596
6270
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
5597
6271
|
if (type && source && target) {
|
|
5598
|
-
const newId = (0,
|
|
6272
|
+
const newId = (0, import_types24.observedEdgeId)(source, target, type);
|
|
5599
6273
|
attrs.id = newId;
|
|
5600
6274
|
if (edge.key) edge.key = newId;
|
|
5601
6275
|
}
|
|
@@ -5721,7 +6395,7 @@ ${NEAT_OUT_LINE}
|
|
|
5721
6395
|
|
|
5722
6396
|
// src/summary.ts
|
|
5723
6397
|
init_cjs_shims();
|
|
5724
|
-
var
|
|
6398
|
+
var import_types25 = require("@neat.is/types");
|
|
5725
6399
|
function renderOtelEnvBlock() {
|
|
5726
6400
|
return [
|
|
5727
6401
|
"for prod OTel routing, set these in your deploy platform's env:",
|
|
@@ -5731,19 +6405,19 @@ function renderOtelEnvBlock() {
|
|
|
5731
6405
|
}
|
|
5732
6406
|
function findIncompatServices(nodes) {
|
|
5733
6407
|
return nodes.filter(
|
|
5734
|
-
(n) => n.type ===
|
|
6408
|
+
(n) => n.type === import_types25.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
5735
6409
|
);
|
|
5736
6410
|
}
|
|
5737
6411
|
function servicesWithoutObserved(nodes, edges) {
|
|
5738
6412
|
const seen = /* @__PURE__ */ new Set();
|
|
5739
6413
|
for (const e of edges) {
|
|
5740
|
-
if (e.provenance ===
|
|
6414
|
+
if (e.provenance === import_types25.Provenance.OBSERVED) {
|
|
5741
6415
|
seen.add(e.source);
|
|
5742
6416
|
seen.add(e.target);
|
|
5743
6417
|
}
|
|
5744
6418
|
}
|
|
5745
6419
|
return nodes.filter(
|
|
5746
|
-
(n) => n.type ===
|
|
6420
|
+
(n) => n.type === import_types25.NodeType.ServiceNode && !seen.has(n.id)
|
|
5747
6421
|
);
|
|
5748
6422
|
}
|
|
5749
6423
|
function formatDivergence(d) {
|
|
@@ -5817,15 +6491,15 @@ function formatIncompat(inc) {
|
|
|
5817
6491
|
|
|
5818
6492
|
// src/watch.ts
|
|
5819
6493
|
init_cjs_shims();
|
|
5820
|
-
var
|
|
5821
|
-
var
|
|
6494
|
+
var import_node_fs29 = __toESM(require("fs"), 1);
|
|
6495
|
+
var import_node_path46 = __toESM(require("path"), 1);
|
|
5822
6496
|
var import_chokidar = __toESM(require("chokidar"), 1);
|
|
5823
6497
|
|
|
5824
6498
|
// src/api.ts
|
|
5825
6499
|
init_cjs_shims();
|
|
5826
6500
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
5827
6501
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
5828
|
-
var
|
|
6502
|
+
var import_types27 = require("@neat.is/types");
|
|
5829
6503
|
|
|
5830
6504
|
// src/extend/index.ts
|
|
5831
6505
|
init_cjs_shims();
|
|
@@ -6276,7 +6950,7 @@ init_cjs_shims();
|
|
|
6276
6950
|
var import_node_fs25 = require("fs");
|
|
6277
6951
|
var import_node_os3 = __toESM(require("os"), 1);
|
|
6278
6952
|
var import_node_path40 = __toESM(require("path"), 1);
|
|
6279
|
-
var
|
|
6953
|
+
var import_types26 = require("@neat.is/types");
|
|
6280
6954
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
6281
6955
|
var LOCK_RETRY_MS = 50;
|
|
6282
6956
|
function neatHome() {
|
|
@@ -6530,10 +7204,10 @@ async function readRegistry() {
|
|
|
6530
7204
|
throw err;
|
|
6531
7205
|
}
|
|
6532
7206
|
const parsed = JSON.parse(raw);
|
|
6533
|
-
return
|
|
7207
|
+
return import_types26.RegistryFileSchema.parse(parsed);
|
|
6534
7208
|
}
|
|
6535
7209
|
async function writeRegistry(reg) {
|
|
6536
|
-
const validated =
|
|
7210
|
+
const validated = import_types26.RegistryFileSchema.parse(reg);
|
|
6537
7211
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
6538
7212
|
}
|
|
6539
7213
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -6845,6 +7519,18 @@ function registerRoutes(scope, ctx) {
|
|
|
6845
7519
|
}
|
|
6846
7520
|
return getTransitiveDependencies(proj.graph, nodeId, depth);
|
|
6847
7521
|
});
|
|
7522
|
+
scope.get(
|
|
7523
|
+
"/graph/observed-dependencies/:nodeId",
|
|
7524
|
+
async (req, reply) => {
|
|
7525
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7526
|
+
if (!proj) return;
|
|
7527
|
+
const { nodeId } = req.params;
|
|
7528
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7529
|
+
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7530
|
+
}
|
|
7531
|
+
return getObservedDependencies(proj.graph, nodeId);
|
|
7532
|
+
}
|
|
7533
|
+
);
|
|
6848
7534
|
scope.get("/graph/divergences", async (req, reply) => {
|
|
6849
7535
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
6850
7536
|
if (!proj) return;
|
|
@@ -6853,11 +7539,11 @@ function registerRoutes(scope, ctx) {
|
|
|
6853
7539
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
6854
7540
|
const parsed = [];
|
|
6855
7541
|
for (const c of candidates) {
|
|
6856
|
-
const r =
|
|
7542
|
+
const r = import_types27.DivergenceTypeSchema.safeParse(c);
|
|
6857
7543
|
if (!r.success) {
|
|
6858
7544
|
return reply.code(400).send({
|
|
6859
7545
|
error: `unknown divergence type "${c}"`,
|
|
6860
|
-
allowed:
|
|
7546
|
+
allowed: import_types27.DivergenceTypeSchema.options
|
|
6861
7547
|
});
|
|
6862
7548
|
}
|
|
6863
7549
|
parsed.push(r.data);
|
|
@@ -6905,23 +7591,29 @@ function registerRoutes(scope, ctx) {
|
|
|
6905
7591
|
const sliced = ordered.slice(0, Number.isFinite(limit) && limit > 0 ? limit : 50);
|
|
6906
7592
|
return { count: sliced.length, total, events: sliced };
|
|
6907
7593
|
});
|
|
7594
|
+
const incidentHistoryHandler = async (req, reply) => {
|
|
7595
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7596
|
+
if (!proj) return;
|
|
7597
|
+
const { nodeId } = req.params;
|
|
7598
|
+
if (!proj.graph.hasNode(nodeId)) {
|
|
7599
|
+
reply.code(404).send({ error: "node not found", id: nodeId });
|
|
7600
|
+
return;
|
|
7601
|
+
}
|
|
7602
|
+
const epath = errorsPathFor(proj);
|
|
7603
|
+
if (!epath) return { count: 0, total: 0, events: [] };
|
|
7604
|
+
const events = await readErrorEvents(epath);
|
|
7605
|
+
const filtered = events.filter(
|
|
7606
|
+
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
7607
|
+
);
|
|
7608
|
+
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
7609
|
+
};
|
|
6908
7610
|
scope.get(
|
|
6909
7611
|
"/incidents/:nodeId",
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6916
|
-
}
|
|
6917
|
-
const epath = errorsPathFor(proj);
|
|
6918
|
-
if (!epath) return { count: 0, total: 0, events: [] };
|
|
6919
|
-
const events = await readErrorEvents(epath);
|
|
6920
|
-
const filtered = events.filter(
|
|
6921
|
-
(e) => e.affectedNode === nodeId || e.service === nodeId.replace(/^service:/, "")
|
|
6922
|
-
);
|
|
6923
|
-
return { count: filtered.length, total: filtered.length, events: filtered };
|
|
6924
|
-
}
|
|
7612
|
+
incidentHistoryHandler
|
|
7613
|
+
);
|
|
7614
|
+
scope.get(
|
|
7615
|
+
"/graph/incident-history/:nodeId",
|
|
7616
|
+
incidentHistoryHandler
|
|
6925
7617
|
);
|
|
6926
7618
|
scope.get("/graph/root-cause/:nodeId", async (req, reply) => {
|
|
6927
7619
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
@@ -6930,16 +7622,16 @@ function registerRoutes(scope, ctx) {
|
|
|
6930
7622
|
if (!proj.graph.hasNode(nodeId)) {
|
|
6931
7623
|
return reply.code(404).send({ error: "node not found", id: nodeId });
|
|
6932
7624
|
}
|
|
6933
|
-
let errorEvent;
|
|
6934
7625
|
const epath = errorsPathFor(proj);
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
7626
|
+
const incidents = epath ? await readErrorEvents(epath) : [];
|
|
7627
|
+
let errorEvent;
|
|
7628
|
+
if (req.query.errorId) {
|
|
7629
|
+
errorEvent = incidents.find((e) => e.id === req.query.errorId);
|
|
6938
7630
|
if (!errorEvent) {
|
|
6939
7631
|
return reply.code(404).send({ error: "error event not found", id: req.query.errorId });
|
|
6940
7632
|
}
|
|
6941
7633
|
}
|
|
6942
|
-
const result = getRootCause(proj.graph, nodeId, errorEvent);
|
|
7634
|
+
const result = getRootCause(proj.graph, nodeId, errorEvent, incidents);
|
|
6943
7635
|
if (!result) return reply.code(404).send({ error: "no root cause found", id: nodeId });
|
|
6944
7636
|
return result;
|
|
6945
7637
|
});
|
|
@@ -7070,7 +7762,7 @@ function registerRoutes(scope, ctx) {
|
|
|
7070
7762
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
7071
7763
|
let violations = await log.readAll();
|
|
7072
7764
|
if (req.query.severity) {
|
|
7073
|
-
const sev =
|
|
7765
|
+
const sev = import_types27.PolicySeveritySchema.safeParse(req.query.severity);
|
|
7074
7766
|
if (!sev.success) {
|
|
7075
7767
|
return reply.code(400).send({
|
|
7076
7768
|
error: "invalid severity",
|
|
@@ -7084,10 +7776,32 @@ function registerRoutes(scope, ctx) {
|
|
|
7084
7776
|
}
|
|
7085
7777
|
return { violations };
|
|
7086
7778
|
});
|
|
7779
|
+
scope.get("/policies/applicable", async (req, reply) => {
|
|
7780
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7781
|
+
if (!proj) return;
|
|
7782
|
+
const nodeId = req.query.node;
|
|
7783
|
+
if (!nodeId) {
|
|
7784
|
+
return reply.code(400).send({ error: 'missing required query param "node"' });
|
|
7785
|
+
}
|
|
7786
|
+
const policyPath = ctx.policyFilePathFor(proj);
|
|
7787
|
+
let policies = [];
|
|
7788
|
+
if (policyPath) {
|
|
7789
|
+
try {
|
|
7790
|
+
policies = await loadPolicyFile(policyPath);
|
|
7791
|
+
} catch (err) {
|
|
7792
|
+
return reply.code(400).send({
|
|
7793
|
+
error: "policy.json failed to parse",
|
|
7794
|
+
details: err.message
|
|
7795
|
+
});
|
|
7796
|
+
}
|
|
7797
|
+
}
|
|
7798
|
+
const applicable = selectApplicablePolicies(proj.graph, policies, nodeId);
|
|
7799
|
+
return { node: nodeId, applicable };
|
|
7800
|
+
});
|
|
7087
7801
|
scope.post("/policies/check", async (req, reply) => {
|
|
7088
7802
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap, ctx.singleProject);
|
|
7089
7803
|
if (!proj) return;
|
|
7090
|
-
const parsed =
|
|
7804
|
+
const parsed = import_types27.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
7091
7805
|
if (!parsed.success) {
|
|
7092
7806
|
return reply.code(400).send({
|
|
7093
7807
|
error: "invalid /policies/check body",
|
|
@@ -7346,12 +8060,111 @@ async function buildApi(opts) {
|
|
|
7346
8060
|
// src/watch.ts
|
|
7347
8061
|
init_auth();
|
|
7348
8062
|
init_otel();
|
|
7349
|
-
init_otel_grpc();
|
|
7350
8063
|
|
|
7351
|
-
// src/
|
|
8064
|
+
// src/daemon.ts
|
|
8065
|
+
init_cjs_shims();
|
|
8066
|
+
var import_node_fs27 = require("fs");
|
|
8067
|
+
var import_node_path44 = __toESM(require("path"), 1);
|
|
8068
|
+
var import_node_module = require("module");
|
|
8069
|
+
init_otel();
|
|
8070
|
+
init_auth();
|
|
8071
|
+
|
|
8072
|
+
// src/unrouted.ts
|
|
7352
8073
|
init_cjs_shims();
|
|
7353
8074
|
var import_node_fs26 = require("fs");
|
|
7354
8075
|
var import_node_path43 = __toESM(require("path"), 1);
|
|
8076
|
+
|
|
8077
|
+
// src/daemon.ts
|
|
8078
|
+
var import_types28 = require("@neat.is/types");
|
|
8079
|
+
function daemonJsonPath(scanPath) {
|
|
8080
|
+
return import_node_path44.default.join(scanPath, "neat-out", "daemon.json");
|
|
8081
|
+
}
|
|
8082
|
+
function daemonsDiscoveryDir(home) {
|
|
8083
|
+
const base = home && home.length > 0 ? home : neatHomeFromEnv();
|
|
8084
|
+
return import_node_path44.default.join(base, "daemons");
|
|
8085
|
+
}
|
|
8086
|
+
function daemonDiscoveryPath(project, home) {
|
|
8087
|
+
return import_node_path44.default.join(daemonsDiscoveryDir(home), `${sanitizeDiscoveryName(project)}.json`);
|
|
8088
|
+
}
|
|
8089
|
+
function sanitizeDiscoveryName(project) {
|
|
8090
|
+
return project.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
8091
|
+
}
|
|
8092
|
+
function neatHomeFromEnv() {
|
|
8093
|
+
const env = process.env.NEAT_HOME;
|
|
8094
|
+
if (env && env.length > 0) return import_node_path44.default.resolve(env);
|
|
8095
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
8096
|
+
return import_node_path44.default.join(home, ".neat");
|
|
8097
|
+
}
|
|
8098
|
+
async function readDaemonRecord(scanPath) {
|
|
8099
|
+
try {
|
|
8100
|
+
const raw = await import_node_fs27.promises.readFile(daemonJsonPath(scanPath), "utf8");
|
|
8101
|
+
const parsed = JSON.parse(raw);
|
|
8102
|
+
if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
|
|
8103
|
+
return parsed;
|
|
8104
|
+
}
|
|
8105
|
+
return null;
|
|
8106
|
+
} catch {
|
|
8107
|
+
return null;
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
8110
|
+
function resolveNeatVersion() {
|
|
8111
|
+
if (process.env.NEAT_LOCAL_VERSION && process.env.NEAT_LOCAL_VERSION.length > 0) {
|
|
8112
|
+
return process.env.NEAT_LOCAL_VERSION;
|
|
8113
|
+
}
|
|
8114
|
+
try {
|
|
8115
|
+
const req = (0, import_node_module.createRequire)(importMetaUrl);
|
|
8116
|
+
const pkg = req("../package.json");
|
|
8117
|
+
return typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
8118
|
+
} catch {
|
|
8119
|
+
return "0.0.0";
|
|
8120
|
+
}
|
|
8121
|
+
}
|
|
8122
|
+
async function writeDaemonRecord(record, home) {
|
|
8123
|
+
const body = JSON.stringify(record, null, 2) + "\n";
|
|
8124
|
+
await writeAtomically(daemonJsonPath(record.projectPath), body);
|
|
8125
|
+
try {
|
|
8126
|
+
await writeAtomically(daemonDiscoveryPath(record.project, home), body);
|
|
8127
|
+
} catch (err) {
|
|
8128
|
+
console.warn(
|
|
8129
|
+
`neatd: could not write discovery copy for "${record.project}" \u2014 ${err.message}`
|
|
8130
|
+
);
|
|
8131
|
+
}
|
|
8132
|
+
}
|
|
8133
|
+
async function clearDaemonRecord(record, home) {
|
|
8134
|
+
try {
|
|
8135
|
+
const stopped = { ...record, status: "stopped" };
|
|
8136
|
+
await writeAtomically(daemonJsonPath(record.projectPath), JSON.stringify(stopped, null, 2) + "\n");
|
|
8137
|
+
} catch {
|
|
8138
|
+
}
|
|
8139
|
+
try {
|
|
8140
|
+
await import_node_fs27.promises.unlink(daemonDiscoveryPath(record.project, home));
|
|
8141
|
+
} catch {
|
|
8142
|
+
}
|
|
8143
|
+
}
|
|
8144
|
+
function portFromListenAddress(address, fallback) {
|
|
8145
|
+
try {
|
|
8146
|
+
const port = new URL(address).port;
|
|
8147
|
+
const n = Number.parseInt(port, 10);
|
|
8148
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
8149
|
+
} catch {
|
|
8150
|
+
}
|
|
8151
|
+
return fallback;
|
|
8152
|
+
}
|
|
8153
|
+
function resolveHost(opts, authTokenSet) {
|
|
8154
|
+
if (opts.host && opts.host.length > 0) return opts.host;
|
|
8155
|
+
const env = process.env.HOST;
|
|
8156
|
+
if (env && env.length > 0) return env;
|
|
8157
|
+
if (!authTokenSet) return "127.0.0.1";
|
|
8158
|
+
return "0.0.0.0";
|
|
8159
|
+
}
|
|
8160
|
+
|
|
8161
|
+
// src/watch.ts
|
|
8162
|
+
init_otel_grpc();
|
|
8163
|
+
|
|
8164
|
+
// src/search.ts
|
|
8165
|
+
init_cjs_shims();
|
|
8166
|
+
var import_node_fs28 = require("fs");
|
|
8167
|
+
var import_node_path45 = __toESM(require("path"), 1);
|
|
7355
8168
|
var import_node_crypto3 = require("crypto");
|
|
7356
8169
|
var DEFAULT_LIMIT = 10;
|
|
7357
8170
|
var NOMIC_DIM = 768;
|
|
@@ -7481,7 +8294,7 @@ async function pickEmbedder() {
|
|
|
7481
8294
|
}
|
|
7482
8295
|
async function readCache(cachePath) {
|
|
7483
8296
|
try {
|
|
7484
|
-
const raw = await
|
|
8297
|
+
const raw = await import_node_fs28.promises.readFile(cachePath, "utf8");
|
|
7485
8298
|
const parsed = JSON.parse(raw);
|
|
7486
8299
|
if (parsed.version !== 1) return null;
|
|
7487
8300
|
return parsed;
|
|
@@ -7490,8 +8303,8 @@ async function readCache(cachePath) {
|
|
|
7490
8303
|
}
|
|
7491
8304
|
}
|
|
7492
8305
|
async function writeCache(cachePath, cache) {
|
|
7493
|
-
await
|
|
7494
|
-
await
|
|
8306
|
+
await import_node_fs28.promises.mkdir(import_node_path45.default.dirname(cachePath), { recursive: true });
|
|
8307
|
+
await import_node_fs28.promises.writeFile(cachePath, JSON.stringify(cache));
|
|
7495
8308
|
}
|
|
7496
8309
|
var VectorIndex = class {
|
|
7497
8310
|
constructor(embedder, cachePath) {
|
|
@@ -7639,6 +8452,7 @@ async function buildSearchIndex(graph, options = {}) {
|
|
|
7639
8452
|
var ALL_PHASES = [
|
|
7640
8453
|
"services",
|
|
7641
8454
|
"aliases",
|
|
8455
|
+
"files",
|
|
7642
8456
|
"imports",
|
|
7643
8457
|
"databases",
|
|
7644
8458
|
"configs",
|
|
@@ -7647,8 +8461,8 @@ var ALL_PHASES = [
|
|
|
7647
8461
|
];
|
|
7648
8462
|
function classifyChange(relPath) {
|
|
7649
8463
|
const phases = /* @__PURE__ */ new Set();
|
|
7650
|
-
const base =
|
|
7651
|
-
const segments = relPath.split(
|
|
8464
|
+
const base = import_node_path46.default.basename(relPath).toLowerCase();
|
|
8465
|
+
const segments = relPath.split(import_node_path46.default.sep).map((s) => s.toLowerCase());
|
|
7652
8466
|
if (base === "package.json" || base === "requirements.txt" || base === "pyproject.toml" || base === "setup.py") {
|
|
7653
8467
|
phases.add("services");
|
|
7654
8468
|
phases.add("aliases");
|
|
@@ -7663,6 +8477,7 @@ function classifyChange(relPath) {
|
|
|
7663
8477
|
phases.add("aliases");
|
|
7664
8478
|
}
|
|
7665
8479
|
if (/\.(?:js|jsx|mjs|cjs|ts|tsx|py)$/.test(base)) {
|
|
8480
|
+
phases.add("files");
|
|
7666
8481
|
phases.add("imports");
|
|
7667
8482
|
phases.add("calls");
|
|
7668
8483
|
}
|
|
@@ -7685,6 +8500,11 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
7685
8500
|
if (phases.has("aliases")) {
|
|
7686
8501
|
await addServiceAliases(graph, scanPath, services);
|
|
7687
8502
|
}
|
|
8503
|
+
if (phases.has("files")) {
|
|
8504
|
+
const r = await addFiles(graph, services);
|
|
8505
|
+
nodesAdded += r.nodesAdded;
|
|
8506
|
+
edgesAdded += r.edgesAdded;
|
|
8507
|
+
}
|
|
7688
8508
|
if (phases.has("imports")) {
|
|
7689
8509
|
const r = await addImports(graph, services);
|
|
7690
8510
|
nodesAdded += r.nodesAdded;
|
|
@@ -7719,6 +8539,7 @@ async function runExtractPhases(graph, scanPath, phases, project = DEFAULT_PROJE
|
|
|
7719
8539
|
durationMs: Date.now() - started
|
|
7720
8540
|
};
|
|
7721
8541
|
}
|
|
8542
|
+
var DEFAULT_WATCH_WEB_PORT = 6328;
|
|
7722
8543
|
var IGNORED_WATCH_GLOBS = [
|
|
7723
8544
|
"**/node_modules/**",
|
|
7724
8545
|
"**/.git/**",
|
|
@@ -7762,16 +8583,16 @@ function countWatchableDirs(scanPath, limit) {
|
|
|
7762
8583
|
if (count >= limit) return;
|
|
7763
8584
|
let entries;
|
|
7764
8585
|
try {
|
|
7765
|
-
entries =
|
|
8586
|
+
entries = import_node_fs29.default.readdirSync(dir, { withFileTypes: true });
|
|
7766
8587
|
} catch {
|
|
7767
8588
|
return;
|
|
7768
8589
|
}
|
|
7769
8590
|
for (const e of entries) {
|
|
7770
8591
|
if (count >= limit) return;
|
|
7771
8592
|
if (!e.isDirectory()) continue;
|
|
7772
|
-
if (IGNORED_WATCH_PATHS.some((re) => re.test(
|
|
8593
|
+
if (IGNORED_WATCH_PATHS.some((re) => re.test(import_node_path46.default.join(dir, e.name) + import_node_path46.default.sep))) continue;
|
|
7773
8594
|
count++;
|
|
7774
|
-
if (depth < 2) visit(
|
|
8595
|
+
if (depth < 2) visit(import_node_path46.default.join(dir, e.name), depth + 1);
|
|
7775
8596
|
}
|
|
7776
8597
|
};
|
|
7777
8598
|
visit(scanPath, 0);
|
|
@@ -7789,8 +8610,8 @@ async function startWatch(graph, opts) {
|
|
|
7789
8610
|
const projectName = opts.project ?? DEFAULT_PROJECT;
|
|
7790
8611
|
await loadGraphFromDisk(graph, opts.outPath);
|
|
7791
8612
|
const detachEventBus = attachGraphToEventBus(graph, { project: projectName });
|
|
7792
|
-
const policyFilePath =
|
|
7793
|
-
const policyViolationsPath =
|
|
8613
|
+
const policyFilePath = import_node_path46.default.join(opts.scanPath, "policy.json");
|
|
8614
|
+
const policyViolationsPath = import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "policy-violations.ndjson");
|
|
7794
8615
|
let policies = [];
|
|
7795
8616
|
try {
|
|
7796
8617
|
policies = await loadPolicyFile(policyFilePath);
|
|
@@ -7841,7 +8662,7 @@ async function startWatch(graph, opts) {
|
|
|
7841
8662
|
assertBindAuthority(host, auth.authToken);
|
|
7842
8663
|
const port = opts.port ?? 8080;
|
|
7843
8664
|
const otelPort = opts.otelPort ?? 4318;
|
|
7844
|
-
const cachePath = opts.embeddingsCachePath ??
|
|
8665
|
+
const cachePath = opts.embeddingsCachePath ?? import_node_path46.default.join(import_node_path46.default.dirname(opts.outPath), "embeddings.json");
|
|
7845
8666
|
let searchIndex;
|
|
7846
8667
|
try {
|
|
7847
8668
|
searchIndex = await buildSearchIndex(graph, { cachePath });
|
|
@@ -7859,7 +8680,7 @@ async function startWatch(graph, opts) {
|
|
|
7859
8680
|
// Paths are derived from the explicit options the watch caller passes
|
|
7860
8681
|
// — pathsForProject is only used to fill in the embeddings/snapshot
|
|
7861
8682
|
// fields so the registry shape is complete.
|
|
7862
|
-
...pathsForProject(projectName,
|
|
8683
|
+
...pathsForProject(projectName, import_node_path46.default.dirname(opts.outPath)),
|
|
7863
8684
|
snapshotPath: opts.outPath,
|
|
7864
8685
|
errorsPath: opts.errorsPath,
|
|
7865
8686
|
staleEventsPath: opts.staleEventsPath
|
|
@@ -7867,7 +8688,7 @@ async function startWatch(graph, opts) {
|
|
|
7867
8688
|
searchIndex
|
|
7868
8689
|
});
|
|
7869
8690
|
const api = await buildApi({ projects: registry });
|
|
7870
|
-
await api.listen({ port, host });
|
|
8691
|
+
const restAddress = await api.listen({ port, host });
|
|
7871
8692
|
console.log(`neat-core listening on http://${host}:${port}`);
|
|
7872
8693
|
console.log(` scan path: ${opts.scanPath} (watching for changes)`);
|
|
7873
8694
|
console.log(` snapshot path: ${opts.outPath}`);
|
|
@@ -7880,10 +8701,41 @@ async function startWatch(graph, opts) {
|
|
|
7880
8701
|
writeErrorEventInline: false,
|
|
7881
8702
|
onPolicyTrigger
|
|
7882
8703
|
});
|
|
7883
|
-
const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath);
|
|
8704
|
+
const onErrorSpanSync = makeErrorSpanWriter(opts.errorsPath, graph, opts.scanPath);
|
|
7884
8705
|
const otelHttp = await buildOtelReceiver({ onSpan, onErrorSpanSync });
|
|
7885
|
-
await otelHttp
|
|
7886
|
-
|
|
8706
|
+
const otelAddress = await listenSteppingOtlp(otelHttp, otelPort, host);
|
|
8707
|
+
const boundOtelPort = portFromListenAddress(otelAddress, otelPort);
|
|
8708
|
+
console.log(`neat-core OTLP receiver on ${otelAddress}/v1/traces`);
|
|
8709
|
+
const boundRestPort = portFromListenAddress(restAddress, port);
|
|
8710
|
+
const daemonRecord = {
|
|
8711
|
+
project: projectName,
|
|
8712
|
+
projectPath: opts.scanPath,
|
|
8713
|
+
pid: process.pid,
|
|
8714
|
+
status: "running",
|
|
8715
|
+
ports: {
|
|
8716
|
+
rest: boundRestPort,
|
|
8717
|
+
otlp: boundOtelPort,
|
|
8718
|
+
// watch serves no dashboard of its own; record the canonical web port so
|
|
8719
|
+
// the record shape is valid. Only ports.otlp is load-bearing here.
|
|
8720
|
+
web: DEFAULT_WATCH_WEB_PORT
|
|
8721
|
+
},
|
|
8722
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8723
|
+
neatVersion: resolveNeatVersion()
|
|
8724
|
+
};
|
|
8725
|
+
try {
|
|
8726
|
+
await writeDaemonRecord(daemonRecord);
|
|
8727
|
+
console.log(
|
|
8728
|
+
`neat watch: wrote daemon.json (REST ${boundRestPort} / OTLP ${boundOtelPort})`
|
|
8729
|
+
);
|
|
8730
|
+
} catch (err) {
|
|
8731
|
+
await api.close().catch(() => {
|
|
8732
|
+
});
|
|
8733
|
+
await otelHttp.close().catch(() => {
|
|
8734
|
+
});
|
|
8735
|
+
throw new Error(
|
|
8736
|
+
`neat watch: failed to write daemon.json \u2014 ${err.message}`
|
|
8737
|
+
);
|
|
8738
|
+
}
|
|
7887
8739
|
let grpcReceiver = null;
|
|
7888
8740
|
if (opts.otelGrpc) {
|
|
7889
8741
|
const grpcPort = opts.otelGrpcPort ?? 4317;
|
|
@@ -7946,9 +8798,9 @@ async function startWatch(graph, opts) {
|
|
|
7946
8798
|
};
|
|
7947
8799
|
const onPath = (absPath) => {
|
|
7948
8800
|
if (shouldIgnore(absPath)) return;
|
|
7949
|
-
const rel =
|
|
8801
|
+
const rel = import_node_path46.default.relative(opts.scanPath, absPath);
|
|
7950
8802
|
if (!rel || rel.startsWith("..")) return;
|
|
7951
|
-
pendingPaths.add(rel.split(
|
|
8803
|
+
pendingPaths.add(rel.split(import_node_path46.default.sep).join("/"));
|
|
7952
8804
|
const phases = classifyChange(rel);
|
|
7953
8805
|
if (phases.size === 0) {
|
|
7954
8806
|
for (const p of ALL_PHASES) pending.add(p);
|
|
@@ -7996,14 +8848,16 @@ async function startWatch(graph, opts) {
|
|
|
7996
8848
|
await api.close();
|
|
7997
8849
|
await otelHttp.close();
|
|
7998
8850
|
if (grpcReceiver) await grpcReceiver.stop();
|
|
8851
|
+
await clearDaemonRecord(daemonRecord).catch(() => {
|
|
8852
|
+
});
|
|
7999
8853
|
};
|
|
8000
8854
|
return { api, stop };
|
|
8001
8855
|
}
|
|
8002
8856
|
|
|
8003
8857
|
// src/deploy/detect.ts
|
|
8004
8858
|
init_cjs_shims();
|
|
8005
|
-
var
|
|
8006
|
-
var
|
|
8859
|
+
var import_node_fs30 = require("fs");
|
|
8860
|
+
var import_node_path47 = __toESM(require("path"), 1);
|
|
8007
8861
|
var import_node_child_process2 = require("child_process");
|
|
8008
8862
|
var import_node_crypto4 = require("crypto");
|
|
8009
8863
|
function generateToken() {
|
|
@@ -8103,21 +8957,21 @@ async function runDeploy(opts = {}) {
|
|
|
8103
8957
|
const token = generateToken();
|
|
8104
8958
|
switch (substrate) {
|
|
8105
8959
|
case "docker-compose": {
|
|
8106
|
-
const artifactPath =
|
|
8960
|
+
const artifactPath = import_node_path47.default.join(cwd, "docker-compose.neat.yml");
|
|
8107
8961
|
const contents = emitDockerCompose(cwd);
|
|
8108
|
-
await
|
|
8962
|
+
await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
|
|
8109
8963
|
return {
|
|
8110
8964
|
substrate,
|
|
8111
8965
|
artifactPath,
|
|
8112
8966
|
token,
|
|
8113
8967
|
contents,
|
|
8114
|
-
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${
|
|
8968
|
+
startCommand: `NEAT_AUTH_TOKEN=${token} docker compose -f ${import_node_path47.default.basename(artifactPath)} up -d`
|
|
8115
8969
|
};
|
|
8116
8970
|
}
|
|
8117
8971
|
case "systemd": {
|
|
8118
|
-
const artifactPath =
|
|
8972
|
+
const artifactPath = import_node_path47.default.join(cwd, "neat.service");
|
|
8119
8973
|
const contents = emitSystemdUnit(cwd);
|
|
8120
|
-
await
|
|
8974
|
+
await import_node_fs30.promises.writeFile(artifactPath, contents, "utf8");
|
|
8121
8975
|
return {
|
|
8122
8976
|
substrate,
|
|
8123
8977
|
artifactPath,
|
|
@@ -8150,8 +9004,8 @@ init_cjs_shims();
|
|
|
8150
9004
|
|
|
8151
9005
|
// src/installers/javascript.ts
|
|
8152
9006
|
init_cjs_shims();
|
|
8153
|
-
var
|
|
8154
|
-
var
|
|
9007
|
+
var import_node_fs31 = require("fs");
|
|
9008
|
+
var import_node_path48 = __toESM(require("path"), 1);
|
|
8155
9009
|
var import_semver2 = __toESM(require("semver"), 1);
|
|
8156
9010
|
|
|
8157
9011
|
// src/installers/templates.ts
|
|
@@ -8747,15 +9601,15 @@ var OTEL_ENV = {
|
|
|
8747
9601
|
value: "http://localhost:4318/projects/<project>/v1/traces"
|
|
8748
9602
|
};
|
|
8749
9603
|
function serviceNodeName(pkg, serviceDir) {
|
|
8750
|
-
return pkg.name ??
|
|
9604
|
+
return pkg.name ?? import_node_path48.default.basename(serviceDir);
|
|
8751
9605
|
}
|
|
8752
9606
|
function projectToken(pkg, serviceDir, project) {
|
|
8753
9607
|
if (project && project.length > 0) return project;
|
|
8754
|
-
return pkg.name ??
|
|
9608
|
+
return pkg.name ?? import_node_path48.default.basename(serviceDir);
|
|
8755
9609
|
}
|
|
8756
9610
|
async function readJsonFile(p) {
|
|
8757
9611
|
try {
|
|
8758
|
-
const raw = await
|
|
9612
|
+
const raw = await import_node_fs31.promises.readFile(p, "utf8");
|
|
8759
9613
|
return JSON.parse(raw);
|
|
8760
9614
|
} catch {
|
|
8761
9615
|
return null;
|
|
@@ -8764,16 +9618,16 @@ async function readJsonFile(p) {
|
|
|
8764
9618
|
async function detectRuntimeKind(pkgRoot, pkg) {
|
|
8765
9619
|
const deps = allDeps(pkg);
|
|
8766
9620
|
if ("react-native" in deps || "expo" in deps) return "react-native";
|
|
8767
|
-
const appJson = await readJsonFile(
|
|
9621
|
+
const appJson = await readJsonFile(import_node_path48.default.join(pkgRoot, "app.json"));
|
|
8768
9622
|
if (appJson && typeof appJson === "object" && "expo" in appJson) {
|
|
8769
9623
|
return "react-native";
|
|
8770
9624
|
}
|
|
8771
|
-
if (await exists3(
|
|
9625
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "vite.config.js")) || await exists3(import_node_path48.default.join(pkgRoot, "vite.config.ts")) || await exists3(import_node_path48.default.join(pkgRoot, "vite.config.mjs")) || "vite" in deps) {
|
|
8772
9626
|
return "browser-bundle";
|
|
8773
9627
|
}
|
|
8774
|
-
if (await exists3(
|
|
8775
|
-
if (await exists3(
|
|
8776
|
-
if (await exists3(
|
|
9628
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "wrangler.toml"))) return "cloudflare-workers";
|
|
9629
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "bun.lockb"))) return "bun";
|
|
9630
|
+
if (await exists3(import_node_path48.default.join(pkgRoot, "deno.json")) || await exists3(import_node_path48.default.join(pkgRoot, "deno.lock"))) {
|
|
8777
9631
|
return "deno";
|
|
8778
9632
|
}
|
|
8779
9633
|
const engines = pkg.engines ?? {};
|
|
@@ -8782,7 +9636,7 @@ async function detectRuntimeKind(pkgRoot, pkg) {
|
|
|
8782
9636
|
}
|
|
8783
9637
|
async function readPackageJson2(serviceDir) {
|
|
8784
9638
|
try {
|
|
8785
|
-
const raw = await
|
|
9639
|
+
const raw = await import_node_fs31.promises.readFile(import_node_path48.default.join(serviceDir, "package.json"), "utf8");
|
|
8786
9640
|
return JSON.parse(raw);
|
|
8787
9641
|
} catch {
|
|
8788
9642
|
return null;
|
|
@@ -8790,7 +9644,7 @@ async function readPackageJson2(serviceDir) {
|
|
|
8790
9644
|
}
|
|
8791
9645
|
async function exists3(p) {
|
|
8792
9646
|
try {
|
|
8793
|
-
await
|
|
9647
|
+
await import_node_fs31.promises.stat(p);
|
|
8794
9648
|
return true;
|
|
8795
9649
|
} catch {
|
|
8796
9650
|
return false;
|
|
@@ -8798,7 +9652,7 @@ async function exists3(p) {
|
|
|
8798
9652
|
}
|
|
8799
9653
|
async function readFileMaybe(p) {
|
|
8800
9654
|
try {
|
|
8801
|
-
return await
|
|
9655
|
+
return await import_node_fs31.promises.readFile(p, "utf8");
|
|
8802
9656
|
} catch {
|
|
8803
9657
|
return null;
|
|
8804
9658
|
}
|
|
@@ -8826,7 +9680,7 @@ function needsVersionUpgrade(installed, expected) {
|
|
|
8826
9680
|
var NEXT_CONFIG_CANDIDATES = ["next.config.js", "next.config.ts", "next.config.mjs"];
|
|
8827
9681
|
async function findNextConfig(serviceDir) {
|
|
8828
9682
|
for (const name of NEXT_CONFIG_CANDIDATES) {
|
|
8829
|
-
const candidate =
|
|
9683
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
8830
9684
|
if (await exists3(candidate)) return candidate;
|
|
8831
9685
|
}
|
|
8832
9686
|
return null;
|
|
@@ -8852,9 +9706,23 @@ var WEB_FRAMEWORK_DEPS = [
|
|
|
8852
9706
|
"polka",
|
|
8853
9707
|
"micro"
|
|
8854
9708
|
];
|
|
8855
|
-
|
|
9709
|
+
var WORKER_FRAMEWORK_DEPS = [
|
|
9710
|
+
"bullmq",
|
|
9711
|
+
"bull",
|
|
9712
|
+
"bee-queue",
|
|
9713
|
+
"agenda",
|
|
9714
|
+
"kafkajs",
|
|
9715
|
+
"amqplib",
|
|
9716
|
+
"amqp-connection-manager",
|
|
9717
|
+
"nats",
|
|
9718
|
+
"node-resque",
|
|
9719
|
+
"pg-boss",
|
|
9720
|
+
"graphile-worker"
|
|
9721
|
+
];
|
|
9722
|
+
var APP_FRAMEWORK_DEPS = [...WEB_FRAMEWORK_DEPS, ...WORKER_FRAMEWORK_DEPS];
|
|
9723
|
+
function appFrameworkDependencies(pkg) {
|
|
8856
9724
|
const deps = allDeps(pkg);
|
|
8857
|
-
return
|
|
9725
|
+
return APP_FRAMEWORK_DEPS.filter((name) => name in deps);
|
|
8858
9726
|
}
|
|
8859
9727
|
function uninstrumentedLibraries(pkg) {
|
|
8860
9728
|
const deps = allDeps(pkg);
|
|
@@ -8881,7 +9749,7 @@ function hasRemixDependency(pkg) {
|
|
|
8881
9749
|
}
|
|
8882
9750
|
async function findRemixEntry(serviceDir) {
|
|
8883
9751
|
for (const rel of REMIX_ENTRY_CANDIDATES) {
|
|
8884
|
-
const candidate =
|
|
9752
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
8885
9753
|
if (await exists3(candidate)) return candidate;
|
|
8886
9754
|
}
|
|
8887
9755
|
return null;
|
|
@@ -8893,14 +9761,14 @@ function hasSvelteKitDependency(pkg) {
|
|
|
8893
9761
|
}
|
|
8894
9762
|
async function findSvelteKitHooks(serviceDir) {
|
|
8895
9763
|
for (const rel of SVELTEKIT_HOOKS_CANDIDATES) {
|
|
8896
|
-
const candidate =
|
|
9764
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
8897
9765
|
if (await exists3(candidate)) return candidate;
|
|
8898
9766
|
}
|
|
8899
9767
|
return null;
|
|
8900
9768
|
}
|
|
8901
9769
|
async function findSvelteKitConfig(serviceDir) {
|
|
8902
9770
|
for (const rel of SVELTEKIT_CONFIG_CANDIDATES) {
|
|
8903
|
-
const candidate =
|
|
9771
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
8904
9772
|
if (await exists3(candidate)) return candidate;
|
|
8905
9773
|
}
|
|
8906
9774
|
return null;
|
|
@@ -8911,7 +9779,7 @@ function hasNuxtDependency(pkg) {
|
|
|
8911
9779
|
}
|
|
8912
9780
|
async function findNuxtConfig(serviceDir) {
|
|
8913
9781
|
for (const name of NUXT_CONFIG_CANDIDATES) {
|
|
8914
|
-
const candidate =
|
|
9782
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
8915
9783
|
if (await exists3(candidate)) return candidate;
|
|
8916
9784
|
}
|
|
8917
9785
|
return null;
|
|
@@ -8922,7 +9790,7 @@ function hasAstroDependency(pkg) {
|
|
|
8922
9790
|
}
|
|
8923
9791
|
async function findAstroConfig(serviceDir) {
|
|
8924
9792
|
for (const name of ASTRO_CONFIG_CANDIDATES) {
|
|
8925
|
-
const candidate =
|
|
9793
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
8926
9794
|
if (await exists3(candidate)) return candidate;
|
|
8927
9795
|
}
|
|
8928
9796
|
return null;
|
|
@@ -8936,15 +9804,15 @@ function parseNextMajor(range) {
|
|
|
8936
9804
|
return Number.isFinite(n) ? n : null;
|
|
8937
9805
|
}
|
|
8938
9806
|
async function isTypeScriptProject(serviceDir) {
|
|
8939
|
-
return exists3(
|
|
9807
|
+
return exists3(import_node_path48.default.join(serviceDir, "tsconfig.json"));
|
|
8940
9808
|
}
|
|
8941
9809
|
var INDEX_EXTENSIONS = [".ts", ".tsx", ".js", ".mjs", ".cjs"];
|
|
8942
9810
|
var INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `index${ext}`);
|
|
8943
9811
|
var SRC_INDEX_CANDIDATES = INDEX_EXTENSIONS.map((ext) => `src/index${ext}`);
|
|
8944
|
-
var SRC_NAMED_CANDIDATES = ["server", "main", "app"].flatMap(
|
|
9812
|
+
var SRC_NAMED_CANDIDATES = ["server", "main", "app", "worker"].flatMap(
|
|
8945
9813
|
(name) => INDEX_EXTENSIONS.map((ext) => `src/${name}${ext}`)
|
|
8946
9814
|
);
|
|
8947
|
-
var ROOT_NAMED_CANDIDATES = ["server", "app", "main"].flatMap(
|
|
9815
|
+
var ROOT_NAMED_CANDIDATES = ["server", "app", "main", "worker"].flatMap(
|
|
8948
9816
|
(name) => INDEX_EXTENSIONS.map((ext) => `${name}${ext}`)
|
|
8949
9817
|
);
|
|
8950
9818
|
var SCRIPT_LAUNCHERS = /* @__PURE__ */ new Set([
|
|
@@ -8985,7 +9853,7 @@ function entryFromScript(script) {
|
|
|
8985
9853
|
}
|
|
8986
9854
|
async function resolveEntry(serviceDir, pkg) {
|
|
8987
9855
|
if (typeof pkg.main === "string" && pkg.main.length > 0) {
|
|
8988
|
-
const candidate =
|
|
9856
|
+
const candidate = import_node_path48.default.resolve(serviceDir, pkg.main);
|
|
8989
9857
|
if (await exists3(candidate)) return candidate;
|
|
8990
9858
|
}
|
|
8991
9859
|
if (pkg.bin) {
|
|
@@ -8999,40 +9867,40 @@ async function resolveEntry(serviceDir, pkg) {
|
|
|
8999
9867
|
if (typeof first === "string") binEntry = first;
|
|
9000
9868
|
}
|
|
9001
9869
|
if (binEntry) {
|
|
9002
|
-
const candidate =
|
|
9870
|
+
const candidate = import_node_path48.default.resolve(serviceDir, binEntry);
|
|
9003
9871
|
if (await exists3(candidate)) return candidate;
|
|
9004
9872
|
}
|
|
9005
9873
|
}
|
|
9006
9874
|
const startEntry = entryFromScript(pkg.scripts?.start);
|
|
9007
9875
|
if (startEntry) {
|
|
9008
|
-
const candidate =
|
|
9876
|
+
const candidate = import_node_path48.default.resolve(serviceDir, startEntry);
|
|
9009
9877
|
if (await exists3(candidate)) return candidate;
|
|
9010
9878
|
}
|
|
9011
9879
|
const devEntry = entryFromScript(pkg.scripts?.dev);
|
|
9012
9880
|
if (devEntry) {
|
|
9013
|
-
const candidate =
|
|
9881
|
+
const candidate = import_node_path48.default.resolve(serviceDir, devEntry);
|
|
9014
9882
|
if (await exists3(candidate)) return candidate;
|
|
9015
9883
|
}
|
|
9016
9884
|
for (const rel of SRC_INDEX_CANDIDATES) {
|
|
9017
|
-
const candidate =
|
|
9885
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9018
9886
|
if (await exists3(candidate)) return candidate;
|
|
9019
9887
|
}
|
|
9020
9888
|
for (const rel of SRC_NAMED_CANDIDATES) {
|
|
9021
|
-
const candidate =
|
|
9889
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9022
9890
|
if (await exists3(candidate)) return candidate;
|
|
9023
9891
|
}
|
|
9024
9892
|
for (const rel of ROOT_NAMED_CANDIDATES) {
|
|
9025
|
-
const candidate =
|
|
9893
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9026
9894
|
if (await exists3(candidate)) return candidate;
|
|
9027
9895
|
}
|
|
9028
9896
|
for (const name of INDEX_CANDIDATES) {
|
|
9029
|
-
const candidate =
|
|
9897
|
+
const candidate = import_node_path48.default.join(serviceDir, name);
|
|
9030
9898
|
if (await exists3(candidate)) return candidate;
|
|
9031
9899
|
}
|
|
9032
9900
|
return null;
|
|
9033
9901
|
}
|
|
9034
9902
|
function dispatchEntry(entryFile, pkg) {
|
|
9035
|
-
const ext =
|
|
9903
|
+
const ext = import_node_path48.default.extname(entryFile).toLowerCase();
|
|
9036
9904
|
if (ext === ".ts" || ext === ".tsx") return "ts";
|
|
9037
9905
|
if (ext === ".mjs") return "esm";
|
|
9038
9906
|
if (ext === ".cjs") return "cjs";
|
|
@@ -9049,9 +9917,9 @@ function otelInitContents(flavor) {
|
|
|
9049
9917
|
return OTEL_INIT_CJS;
|
|
9050
9918
|
}
|
|
9051
9919
|
function injectionLine(flavor, entryFile, otelInitFile) {
|
|
9052
|
-
let rel =
|
|
9920
|
+
let rel = import_node_path48.default.relative(import_node_path48.default.dirname(entryFile), otelInitFile);
|
|
9053
9921
|
if (!rel.startsWith(".")) rel = `./${rel}`;
|
|
9054
|
-
rel = rel.split(
|
|
9922
|
+
rel = rel.split(import_node_path48.default.sep).join("/");
|
|
9055
9923
|
if (flavor === "cjs") return `require('${rel}')`;
|
|
9056
9924
|
if (flavor === "esm") return `import '${rel}'`;
|
|
9057
9925
|
const tsRel = rel.replace(/\.ts$/, "");
|
|
@@ -9064,23 +9932,23 @@ function lineIsOtelInjection(line) {
|
|
|
9064
9932
|
}
|
|
9065
9933
|
async function detectsSrcLayout(serviceDir) {
|
|
9066
9934
|
const [hasSrcApp, hasSrcPages, hasRootApp, hasRootPages] = await Promise.all([
|
|
9067
|
-
exists3(
|
|
9068
|
-
exists3(
|
|
9069
|
-
exists3(
|
|
9070
|
-
exists3(
|
|
9935
|
+
exists3(import_node_path48.default.join(serviceDir, "src", "app")),
|
|
9936
|
+
exists3(import_node_path48.default.join(serviceDir, "src", "pages")),
|
|
9937
|
+
exists3(import_node_path48.default.join(serviceDir, "app")),
|
|
9938
|
+
exists3(import_node_path48.default.join(serviceDir, "pages"))
|
|
9071
9939
|
]);
|
|
9072
9940
|
return (hasSrcApp || hasSrcPages) && !hasRootApp && !hasRootPages;
|
|
9073
9941
|
}
|
|
9074
9942
|
async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project) {
|
|
9075
9943
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9076
9944
|
const srcLayout = await detectsSrcLayout(serviceDir);
|
|
9077
|
-
const baseDir = srcLayout ?
|
|
9078
|
-
const instrumentationFile =
|
|
9079
|
-
const instrumentationNodeFile =
|
|
9945
|
+
const baseDir = srcLayout ? import_node_path48.default.join(serviceDir, "src") : serviceDir;
|
|
9946
|
+
const instrumentationFile = import_node_path48.default.join(baseDir, useTs ? "instrumentation.ts" : "instrumentation.js");
|
|
9947
|
+
const instrumentationNodeFile = import_node_path48.default.join(
|
|
9080
9948
|
baseDir,
|
|
9081
9949
|
useTs ? "instrumentation.node.ts" : "instrumentation.node.js"
|
|
9082
9950
|
);
|
|
9083
|
-
const envNeatFile =
|
|
9951
|
+
const envNeatFile = import_node_path48.default.join(baseDir, ".env.neat");
|
|
9084
9952
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
9085
9953
|
const dependencyEdits = [];
|
|
9086
9954
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -9137,7 +10005,7 @@ async function planNext(serviceDir, pkg, manifestPath, nextConfigPath, project)
|
|
|
9137
10005
|
const nextMajor = parseNextMajor(nextRange);
|
|
9138
10006
|
if (nextMajor !== null && nextMajor < 15) {
|
|
9139
10007
|
try {
|
|
9140
|
-
const raw = await
|
|
10008
|
+
const raw = await import_node_fs31.promises.readFile(nextConfigPath, "utf8");
|
|
9141
10009
|
if (!raw.includes("instrumentationHook")) {
|
|
9142
10010
|
nextConfigEdit = {
|
|
9143
10011
|
file: nextConfigPath,
|
|
@@ -9185,7 +10053,7 @@ function buildDependencyEdits(pkg, manifestPath) {
|
|
|
9185
10053
|
return edits;
|
|
9186
10054
|
}
|
|
9187
10055
|
async function queueEnvNeat(serviceDir, pkg, project, generatedFiles) {
|
|
9188
|
-
const envNeatFile =
|
|
10056
|
+
const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
|
|
9189
10057
|
if (!await exists3(envNeatFile)) {
|
|
9190
10058
|
generatedFiles.push({
|
|
9191
10059
|
file: envNeatFile,
|
|
@@ -9220,7 +10088,7 @@ function fileImportsOtelHook(raw, specifiers) {
|
|
|
9220
10088
|
}
|
|
9221
10089
|
async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
9222
10090
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9223
|
-
const otelServerFile =
|
|
10091
|
+
const otelServerFile = import_node_path48.default.join(
|
|
9224
10092
|
serviceDir,
|
|
9225
10093
|
useTs ? "app/otel.server.ts" : "app/otel.server.js"
|
|
9226
10094
|
);
|
|
@@ -9241,7 +10109,7 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
|
9241
10109
|
await queueEnvNeat(serviceDir, pkg, project, generatedFiles);
|
|
9242
10110
|
const entrypointEdits = [];
|
|
9243
10111
|
try {
|
|
9244
|
-
const raw = await
|
|
10112
|
+
const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
|
|
9245
10113
|
if (!fileImportsOtelHook(raw, ["./otel.server"])) {
|
|
9246
10114
|
const lines = raw.split(/\r?\n/);
|
|
9247
10115
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -9277,11 +10145,11 @@ async function planRemix(serviceDir, pkg, manifestPath, entryFile, project) {
|
|
|
9277
10145
|
}
|
|
9278
10146
|
async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project) {
|
|
9279
10147
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9280
|
-
const otelInitFile =
|
|
10148
|
+
const otelInitFile = import_node_path48.default.join(
|
|
9281
10149
|
serviceDir,
|
|
9282
10150
|
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
9283
10151
|
);
|
|
9284
|
-
const resolvedHooksFile = hooksFile ??
|
|
10152
|
+
const resolvedHooksFile = hooksFile ?? import_node_path48.default.join(serviceDir, useTs ? "src/hooks.server.ts" : "src/hooks.server.js");
|
|
9285
10153
|
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
9286
10154
|
const generatedFiles = [];
|
|
9287
10155
|
const entrypointEdits = [];
|
|
@@ -9306,7 +10174,7 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
|
|
|
9306
10174
|
});
|
|
9307
10175
|
} else {
|
|
9308
10176
|
try {
|
|
9309
|
-
const raw = await
|
|
10177
|
+
const raw = await import_node_fs31.promises.readFile(hooksFile, "utf8");
|
|
9310
10178
|
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
9311
10179
|
const lines = raw.split(/\r?\n/);
|
|
9312
10180
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -9343,11 +10211,11 @@ async function planSvelteKit(serviceDir, pkg, manifestPath, hooksFile, project)
|
|
|
9343
10211
|
}
|
|
9344
10212
|
async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
9345
10213
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9346
|
-
const otelPluginFile =
|
|
10214
|
+
const otelPluginFile = import_node_path48.default.join(
|
|
9347
10215
|
serviceDir,
|
|
9348
10216
|
useTs ? "server/plugins/otel.ts" : "server/plugins/otel.js"
|
|
9349
10217
|
);
|
|
9350
|
-
const otelInitFile =
|
|
10218
|
+
const otelInitFile = import_node_path48.default.join(
|
|
9351
10219
|
serviceDir,
|
|
9352
10220
|
useTs ? "server/plugins/otel-init.ts" : "server/plugins/otel-init.js"
|
|
9353
10221
|
);
|
|
@@ -9398,19 +10266,19 @@ async function planNuxt(serviceDir, pkg, manifestPath, project) {
|
|
|
9398
10266
|
var ASTRO_MIDDLEWARE_CANDIDATES = ["src/middleware.ts", "src/middleware.js"];
|
|
9399
10267
|
async function findAstroMiddleware(serviceDir) {
|
|
9400
10268
|
for (const rel of ASTRO_MIDDLEWARE_CANDIDATES) {
|
|
9401
|
-
const candidate =
|
|
10269
|
+
const candidate = import_node_path48.default.join(serviceDir, rel);
|
|
9402
10270
|
if (await exists3(candidate)) return candidate;
|
|
9403
10271
|
}
|
|
9404
10272
|
return null;
|
|
9405
10273
|
}
|
|
9406
10274
|
async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
9407
10275
|
const useTs = await isTypeScriptProject(serviceDir);
|
|
9408
|
-
const otelInitFile =
|
|
10276
|
+
const otelInitFile = import_node_path48.default.join(
|
|
9409
10277
|
serviceDir,
|
|
9410
10278
|
useTs ? "src/otel-init.ts" : "src/otel-init.js"
|
|
9411
10279
|
);
|
|
9412
10280
|
const existingMiddleware = await findAstroMiddleware(serviceDir);
|
|
9413
|
-
const middlewareFile = existingMiddleware ??
|
|
10281
|
+
const middlewareFile = existingMiddleware ?? import_node_path48.default.join(serviceDir, useTs ? "src/middleware.ts" : "src/middleware.js");
|
|
9414
10282
|
const dependencyEdits = buildDependencyEdits(pkg, manifestPath);
|
|
9415
10283
|
const generatedFiles = [];
|
|
9416
10284
|
const entrypointEdits = [];
|
|
@@ -9435,7 +10303,7 @@ async function planAstro(serviceDir, pkg, manifestPath, project) {
|
|
|
9435
10303
|
});
|
|
9436
10304
|
} else {
|
|
9437
10305
|
try {
|
|
9438
|
-
const raw = await
|
|
10306
|
+
const raw = await import_node_fs31.promises.readFile(existingMiddleware, "utf8");
|
|
9439
10307
|
if (!fileImportsOtelHook(raw, ["./otel-init"])) {
|
|
9440
10308
|
const lines = raw.split(/\r?\n/);
|
|
9441
10309
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
@@ -9506,7 +10374,7 @@ async function findFrameworkDispatch(serviceDir, pkg, manifestPath, project) {
|
|
|
9506
10374
|
}
|
|
9507
10375
|
async function plan(serviceDir, opts) {
|
|
9508
10376
|
const pkg = await readPackageJson2(serviceDir);
|
|
9509
|
-
const manifestPath =
|
|
10377
|
+
const manifestPath = import_node_path48.default.join(serviceDir, "package.json");
|
|
9510
10378
|
const project = opts?.project;
|
|
9511
10379
|
const empty = {
|
|
9512
10380
|
language: "javascript",
|
|
@@ -9541,8 +10409,8 @@ async function plan(serviceDir, opts) {
|
|
|
9541
10409
|
return { ...empty, libOnly: true };
|
|
9542
10410
|
}
|
|
9543
10411
|
const flavor = dispatchEntry(entryFile, pkg);
|
|
9544
|
-
const otelInitFile =
|
|
9545
|
-
const envNeatFile =
|
|
10412
|
+
const otelInitFile = import_node_path48.default.join(import_node_path48.default.dirname(entryFile), otelInitFilename(flavor));
|
|
10413
|
+
const envNeatFile = import_node_path48.default.join(serviceDir, ".env.neat");
|
|
9546
10414
|
const existingDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
9547
10415
|
const dependencyEdits = [];
|
|
9548
10416
|
for (const sdk of SDK_PACKAGES) {
|
|
@@ -9566,7 +10434,7 @@ async function plan(serviceDir, opts) {
|
|
|
9566
10434
|
}
|
|
9567
10435
|
const entrypointEdits = [];
|
|
9568
10436
|
try {
|
|
9569
|
-
const raw = await
|
|
10437
|
+
const raw = await import_node_fs31.promises.readFile(entryFile, "utf8");
|
|
9570
10438
|
const lines = raw.split(/\r?\n/);
|
|
9571
10439
|
const firstReal = lines[0]?.startsWith("#!") ? lines[1] ?? "" : lines[0] ?? "";
|
|
9572
10440
|
if (!lineIsOtelInjection(firstReal)) {
|
|
@@ -9611,13 +10479,13 @@ async function plan(serviceDir, opts) {
|
|
|
9611
10479
|
};
|
|
9612
10480
|
}
|
|
9613
10481
|
function isAllowedWritePath(serviceDir, target) {
|
|
9614
|
-
const rel =
|
|
10482
|
+
const rel = import_node_path48.default.relative(serviceDir, target);
|
|
9615
10483
|
if (rel.startsWith("..")) return false;
|
|
9616
|
-
const base =
|
|
10484
|
+
const base = import_node_path48.default.basename(target);
|
|
9617
10485
|
if (base === "package.json") return true;
|
|
9618
10486
|
if (base === ".env.neat") return true;
|
|
9619
10487
|
if (/^otel-init\.(?:js|cjs|mjs|ts)$/.test(base)) return true;
|
|
9620
|
-
const relPosix = rel.split(
|
|
10488
|
+
const relPosix = rel.split(import_node_path48.default.sep).join("/");
|
|
9621
10489
|
if (/^instrumentation(?:\.node)?\.(?:js|cjs|mjs|ts)$/.test(base)) {
|
|
9622
10490
|
if (relPosix === base) return true;
|
|
9623
10491
|
if (relPosix === `src/${base}`) return true;
|
|
@@ -9634,10 +10502,10 @@ function isAllowedWritePath(serviceDir, target) {
|
|
|
9634
10502
|
return false;
|
|
9635
10503
|
}
|
|
9636
10504
|
async function writeAtomic(file, contents) {
|
|
9637
|
-
await
|
|
10505
|
+
await import_node_fs31.promises.mkdir(import_node_path48.default.dirname(file), { recursive: true });
|
|
9638
10506
|
const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
|
|
9639
|
-
await
|
|
9640
|
-
await
|
|
10507
|
+
await import_node_fs31.promises.writeFile(tmp, contents, "utf8");
|
|
10508
|
+
await import_node_fs31.promises.rename(tmp, file);
|
|
9641
10509
|
}
|
|
9642
10510
|
async function apply(installPlan) {
|
|
9643
10511
|
const { serviceDir } = installPlan;
|
|
@@ -9713,7 +10581,7 @@ async function apply(installPlan) {
|
|
|
9713
10581
|
for (const target of allTargets) {
|
|
9714
10582
|
if (await exists3(target)) {
|
|
9715
10583
|
try {
|
|
9716
|
-
originals.set(target, await
|
|
10584
|
+
originals.set(target, await import_node_fs31.promises.readFile(target, "utf8"));
|
|
9717
10585
|
} catch {
|
|
9718
10586
|
}
|
|
9719
10587
|
}
|
|
@@ -9796,14 +10664,14 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
9796
10664
|
const removed = [];
|
|
9797
10665
|
for (const [file, raw] of originals.entries()) {
|
|
9798
10666
|
try {
|
|
9799
|
-
await
|
|
10667
|
+
await import_node_fs31.promises.writeFile(file, raw, "utf8");
|
|
9800
10668
|
restored.push(file);
|
|
9801
10669
|
} catch {
|
|
9802
10670
|
}
|
|
9803
10671
|
}
|
|
9804
10672
|
for (const file of createdFiles) {
|
|
9805
10673
|
try {
|
|
9806
|
-
await
|
|
10674
|
+
await import_node_fs31.promises.unlink(file);
|
|
9807
10675
|
removed.push(file);
|
|
9808
10676
|
} catch {
|
|
9809
10677
|
}
|
|
@@ -9818,8 +10686,8 @@ async function rollback(installPlan, originals, createdFiles) {
|
|
|
9818
10686
|
...removed.map((f) => `removed: ${f}`),
|
|
9819
10687
|
""
|
|
9820
10688
|
];
|
|
9821
|
-
const rollbackPath =
|
|
9822
|
-
await
|
|
10689
|
+
const rollbackPath = import_node_path48.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
10690
|
+
await import_node_fs31.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
9823
10691
|
}
|
|
9824
10692
|
function injectInstrumentationHook(raw) {
|
|
9825
10693
|
if (raw.includes("instrumentationHook")) return raw;
|
|
@@ -9848,8 +10716,8 @@ var javascriptInstaller = {
|
|
|
9848
10716
|
|
|
9849
10717
|
// src/installers/python.ts
|
|
9850
10718
|
init_cjs_shims();
|
|
9851
|
-
var
|
|
9852
|
-
var
|
|
10719
|
+
var import_node_fs32 = require("fs");
|
|
10720
|
+
var import_node_path49 = __toESM(require("path"), 1);
|
|
9853
10721
|
var SDK_PACKAGES2 = [
|
|
9854
10722
|
{ name: "opentelemetry-distro", version: ">=0.49b0" },
|
|
9855
10723
|
{ name: "opentelemetry-exporter-otlp", version: ">=1.28.0" }
|
|
@@ -9861,7 +10729,7 @@ var OTEL_ENV2 = {
|
|
|
9861
10729
|
};
|
|
9862
10730
|
async function exists4(p) {
|
|
9863
10731
|
try {
|
|
9864
|
-
await
|
|
10732
|
+
await import_node_fs32.promises.stat(p);
|
|
9865
10733
|
return true;
|
|
9866
10734
|
} catch {
|
|
9867
10735
|
return false;
|
|
@@ -9870,7 +10738,7 @@ async function exists4(p) {
|
|
|
9870
10738
|
async function detect2(serviceDir) {
|
|
9871
10739
|
const markers = ["requirements.txt", "pyproject.toml", "setup.py"];
|
|
9872
10740
|
for (const m of markers) {
|
|
9873
|
-
if (await exists4(
|
|
10741
|
+
if (await exists4(import_node_path49.default.join(serviceDir, m))) return true;
|
|
9874
10742
|
}
|
|
9875
10743
|
return false;
|
|
9876
10744
|
}
|
|
@@ -9880,9 +10748,9 @@ function reqPackageName(line) {
|
|
|
9880
10748
|
return head.replace(/[<>=!~].*$/, "").toLowerCase();
|
|
9881
10749
|
}
|
|
9882
10750
|
async function planRequirementsTxtEdits(serviceDir) {
|
|
9883
|
-
const file =
|
|
10751
|
+
const file = import_node_path49.default.join(serviceDir, "requirements.txt");
|
|
9884
10752
|
if (!await exists4(file)) return null;
|
|
9885
|
-
const raw = await
|
|
10753
|
+
const raw = await import_node_fs32.promises.readFile(file, "utf8");
|
|
9886
10754
|
const presentNames = new Set(
|
|
9887
10755
|
raw.split(/\r?\n/).map(reqPackageName).filter((n) => n.length > 0)
|
|
9888
10756
|
);
|
|
@@ -9890,9 +10758,9 @@ async function planRequirementsTxtEdits(serviceDir) {
|
|
|
9890
10758
|
return { manifest: file, missing: [...missing] };
|
|
9891
10759
|
}
|
|
9892
10760
|
async function planProcfileEdits(serviceDir) {
|
|
9893
|
-
const procfile =
|
|
10761
|
+
const procfile = import_node_path49.default.join(serviceDir, "Procfile");
|
|
9894
10762
|
if (!await exists4(procfile)) return [];
|
|
9895
|
-
const raw = await
|
|
10763
|
+
const raw = await import_node_fs32.promises.readFile(procfile, "utf8");
|
|
9896
10764
|
const edits = [];
|
|
9897
10765
|
for (const line of raw.split(/\r?\n/)) {
|
|
9898
10766
|
if (line.length === 0) continue;
|
|
@@ -9944,8 +10812,8 @@ async function applyRequirementsTxt(manifest, edits, original) {
|
|
|
9944
10812
|
const next = `${original}${trailing}${newlines.join("\n")}
|
|
9945
10813
|
`;
|
|
9946
10814
|
const tmp = `${manifest}.${process.pid}.${Date.now()}.tmp`;
|
|
9947
|
-
await
|
|
9948
|
-
await
|
|
10815
|
+
await import_node_fs32.promises.writeFile(tmp, next, "utf8");
|
|
10816
|
+
await import_node_fs32.promises.rename(tmp, manifest);
|
|
9949
10817
|
}
|
|
9950
10818
|
async function applyProcfile(procfile, edits, original) {
|
|
9951
10819
|
let next = original;
|
|
@@ -9954,8 +10822,8 @@ async function applyProcfile(procfile, edits, original) {
|
|
|
9954
10822
|
next = next.replace(e.before, e.after);
|
|
9955
10823
|
}
|
|
9956
10824
|
const tmp = `${procfile}.${process.pid}.${Date.now()}.tmp`;
|
|
9957
|
-
await
|
|
9958
|
-
await
|
|
10825
|
+
await import_node_fs32.promises.writeFile(tmp, next, "utf8");
|
|
10826
|
+
await import_node_fs32.promises.rename(tmp, procfile);
|
|
9959
10827
|
}
|
|
9960
10828
|
async function apply2(installPlan) {
|
|
9961
10829
|
const { serviceDir } = installPlan;
|
|
@@ -9968,7 +10836,7 @@ async function apply2(installPlan) {
|
|
|
9968
10836
|
const originals = /* @__PURE__ */ new Map();
|
|
9969
10837
|
for (const file of touched) {
|
|
9970
10838
|
try {
|
|
9971
|
-
originals.set(file, await
|
|
10839
|
+
originals.set(file, await import_node_fs32.promises.readFile(file, "utf8"));
|
|
9972
10840
|
} catch {
|
|
9973
10841
|
}
|
|
9974
10842
|
}
|
|
@@ -9979,7 +10847,7 @@ async function apply2(installPlan) {
|
|
|
9979
10847
|
if (raw === void 0) {
|
|
9980
10848
|
throw new Error(`python installer: cannot read ${file} during apply`);
|
|
9981
10849
|
}
|
|
9982
|
-
const base =
|
|
10850
|
+
const base = import_node_path49.default.basename(file);
|
|
9983
10851
|
if (base === "requirements.txt") {
|
|
9984
10852
|
const edits = installPlan.dependencyEdits.filter((e) => e.file === file);
|
|
9985
10853
|
if (edits.length > 0) {
|
|
@@ -10004,7 +10872,7 @@ async function rollback2(installPlan, originals) {
|
|
|
10004
10872
|
const restored = [];
|
|
10005
10873
|
for (const [file, raw] of originals.entries()) {
|
|
10006
10874
|
try {
|
|
10007
|
-
await
|
|
10875
|
+
await import_node_fs32.promises.writeFile(file, raw, "utf8");
|
|
10008
10876
|
restored.push(file);
|
|
10009
10877
|
} catch {
|
|
10010
10878
|
}
|
|
@@ -10018,8 +10886,8 @@ async function rollback2(installPlan, originals) {
|
|
|
10018
10886
|
...restored.map((f) => `restored: ${f}`),
|
|
10019
10887
|
""
|
|
10020
10888
|
];
|
|
10021
|
-
const rollbackPath =
|
|
10022
|
-
await
|
|
10889
|
+
const rollbackPath = import_node_path49.default.join(installPlan.serviceDir, "neat-rollback.patch");
|
|
10890
|
+
await import_node_fs32.promises.writeFile(rollbackPath, lines.join("\n"), "utf8");
|
|
10023
10891
|
}
|
|
10024
10892
|
var pythonInstaller = {
|
|
10025
10893
|
name: "python",
|
|
@@ -10148,38 +11016,6 @@ var import_node_net = __toESM(require("net"), 1);
|
|
|
10148
11016
|
var import_node_path50 = __toESM(require("path"), 1);
|
|
10149
11017
|
var import_node_child_process3 = require("child_process");
|
|
10150
11018
|
var import_node_readline = __toESM(require("readline"), 1);
|
|
10151
|
-
|
|
10152
|
-
// src/daemon.ts
|
|
10153
|
-
init_cjs_shims();
|
|
10154
|
-
var import_node_fs32 = require("fs");
|
|
10155
|
-
var import_node_path49 = __toESM(require("path"), 1);
|
|
10156
|
-
var import_node_module = require("module");
|
|
10157
|
-
init_otel();
|
|
10158
|
-
init_auth();
|
|
10159
|
-
|
|
10160
|
-
// src/unrouted.ts
|
|
10161
|
-
init_cjs_shims();
|
|
10162
|
-
var import_node_fs31 = require("fs");
|
|
10163
|
-
var import_node_path48 = __toESM(require("path"), 1);
|
|
10164
|
-
|
|
10165
|
-
// src/daemon.ts
|
|
10166
|
-
function daemonJsonPath(scanPath) {
|
|
10167
|
-
return import_node_path49.default.join(scanPath, "neat-out", "daemon.json");
|
|
10168
|
-
}
|
|
10169
|
-
async function readDaemonRecord(scanPath) {
|
|
10170
|
-
try {
|
|
10171
|
-
const raw = await import_node_fs32.promises.readFile(daemonJsonPath(scanPath), "utf8");
|
|
10172
|
-
const parsed = JSON.parse(raw);
|
|
10173
|
-
if (typeof parsed.project === "string" && parsed.ports && typeof parsed.ports.rest === "number" && typeof parsed.ports.otlp === "number" && typeof parsed.ports.web === "number") {
|
|
10174
|
-
return parsed;
|
|
10175
|
-
}
|
|
10176
|
-
return null;
|
|
10177
|
-
} catch {
|
|
10178
|
-
return null;
|
|
10179
|
-
}
|
|
10180
|
-
}
|
|
10181
|
-
|
|
10182
|
-
// src/orchestrator.ts
|
|
10183
11019
|
async function extractAndPersist(opts) {
|
|
10184
11020
|
const services = await discoverServices(opts.scanPath);
|
|
10185
11021
|
const languages = [...new Set(services.map((s) => s.node.language))].sort();
|
|
@@ -10236,11 +11072,13 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
10236
11072
|
} else if (outcome.outcome === "already-instrumented") already++;
|
|
10237
11073
|
else if (outcome.outcome === "lib-only") {
|
|
10238
11074
|
libOnly++;
|
|
10239
|
-
|
|
11075
|
+
const appDeps = svc.pkg ? appFrameworkDependencies(svc.pkg) : [];
|
|
11076
|
+
if (appDeps.length > 0) {
|
|
10240
11077
|
const svcName = import_node_path50.default.basename(svc.dir);
|
|
11078
|
+
const list = appDeps.join(", ");
|
|
10241
11079
|
console.warn(
|
|
10242
11080
|
`neat: runtime layer won't engage for ${svcName}: no entry point found.
|
|
10243
|
-
${svc.dir} depends on a
|
|
11081
|
+
${svc.dir} depends on ${list} \u2014 a runnable app \u2014 but neat couldn't resolve an entry to instrument.
|
|
10244
11082
|
Add a "start" script to package.json, or point neat at the entry file directly.`
|
|
10245
11083
|
);
|
|
10246
11084
|
}
|
|
@@ -10308,10 +11146,11 @@ async function applyInstallersOver(services, project, options = {}) {
|
|
|
10308
11146
|
if (gaps.length > 0) {
|
|
10309
11147
|
const svcName = import_node_path50.default.basename(svc.dir);
|
|
10310
11148
|
const list = gaps.join(", ");
|
|
10311
|
-
const
|
|
11149
|
+
const subject = gaps.length === 1 ? "this library" : "these libraries";
|
|
11150
|
+
const aux = gaps.length === 1 ? "isn't" : "aren't";
|
|
10312
11151
|
console.warn(
|
|
10313
11152
|
`neat: calls to ${list} won't be observed by default in ${svcName}.
|
|
10314
|
-
${
|
|
11153
|
+
${subject} ${aux} in the default instrumentation set, so they produce no OBSERVED edges.
|
|
10315
11154
|
Run \`neat list-uninstrumented\` to review them, then \`neat extend\` to capture them.`
|
|
10316
11155
|
);
|
|
10317
11156
|
}
|
|
@@ -10405,29 +11244,22 @@ async function probeProjectHealth(restPort, name) {
|
|
|
10405
11244
|
});
|
|
10406
11245
|
});
|
|
10407
11246
|
}
|
|
10408
|
-
async function snapshotProjectStatus(restPort, body) {
|
|
11247
|
+
async function snapshotProjectStatus(restPort, project, body) {
|
|
10409
11248
|
if (body.projects && body.projects.length > 0) {
|
|
10410
|
-
|
|
10411
|
-
|
|
10412
|
-
status: p.status ?? "active"
|
|
10413
|
-
}
|
|
10414
|
-
}
|
|
10415
|
-
|
|
10416
|
-
if (entries.length === 0) return [];
|
|
10417
|
-
return Promise.all(
|
|
10418
|
-
entries.map(async (entry2) => ({
|
|
10419
|
-
name: entry2.name,
|
|
10420
|
-
status: await probeProjectHealth(restPort, entry2.name)
|
|
10421
|
-
}))
|
|
10422
|
-
);
|
|
11249
|
+
const mine = body.projects.filter((p) => p.name === project);
|
|
11250
|
+
if (mine.length > 0) {
|
|
11251
|
+
return mine.map((p) => ({ name: p.name, status: p.status ?? "active" }));
|
|
11252
|
+
}
|
|
11253
|
+
}
|
|
11254
|
+
return [{ name: project, status: await probeProjectHealth(restPort, project) }];
|
|
10423
11255
|
}
|
|
10424
|
-
async function waitForDaemonReady(restPort, timeoutMs) {
|
|
11256
|
+
async function waitForDaemonReady(restPort, project, timeoutMs) {
|
|
10425
11257
|
const deadline = Date.now() + timeoutMs;
|
|
10426
11258
|
let lastBootstrapping = [];
|
|
10427
11259
|
while (Date.now() < deadline) {
|
|
10428
11260
|
const body = await fetchDaemonHealth(restPort);
|
|
10429
11261
|
if (body !== null) {
|
|
10430
|
-
const projects2 = await snapshotProjectStatus(restPort, body);
|
|
11262
|
+
const projects2 = await snapshotProjectStatus(restPort, project, body);
|
|
10431
11263
|
const bootstrapping = projects2.filter((p) => p.status === "bootstrapping").map((p) => p.name);
|
|
10432
11264
|
const broken = projects2.filter((p) => p.status === "broken").map((p) => p.name);
|
|
10433
11265
|
if (bootstrapping.length === 0) {
|
|
@@ -10446,7 +11278,7 @@ async function waitForDaemonReady(restPort, timeoutMs) {
|
|
|
10446
11278
|
await new Promise((r) => setTimeout(r, PROBE_INTERVAL_MS));
|
|
10447
11279
|
}
|
|
10448
11280
|
const final = await fetchDaemonHealth(restPort);
|
|
10449
|
-
const projects = final ? await snapshotProjectStatus(restPort, final) : [];
|
|
11281
|
+
const projects = final ? await snapshotProjectStatus(restPort, project, final) : [];
|
|
10450
11282
|
return {
|
|
10451
11283
|
ready: false,
|
|
10452
11284
|
brokenProjects: projects.filter((p) => p.status === "broken").map((p) => p.name),
|
|
@@ -10454,14 +11286,37 @@ async function waitForDaemonReady(restPort, timeoutMs) {
|
|
|
10454
11286
|
};
|
|
10455
11287
|
}
|
|
10456
11288
|
var NEAT_PORTS = [8080, 4318, 6328];
|
|
10457
|
-
|
|
11289
|
+
function probeBind(port, host) {
|
|
10458
11290
|
return new Promise((resolve) => {
|
|
10459
11291
|
const server = import_node_net.default.createServer();
|
|
10460
|
-
server.once("error", () =>
|
|
10461
|
-
|
|
10462
|
-
|
|
11292
|
+
server.once("error", (err) => {
|
|
11293
|
+
resolve(err.code === "EADDRINUSE" ? "in-use" : "unavailable");
|
|
11294
|
+
});
|
|
11295
|
+
server.once("listening", () => server.close(() => resolve("free")));
|
|
11296
|
+
server.listen(port, host);
|
|
10463
11297
|
});
|
|
10464
11298
|
}
|
|
11299
|
+
function crossFamilyHost(host) {
|
|
11300
|
+
switch (host) {
|
|
11301
|
+
case "127.0.0.1":
|
|
11302
|
+
case "localhost":
|
|
11303
|
+
return "::1";
|
|
11304
|
+
case "::1":
|
|
11305
|
+
return "127.0.0.1";
|
|
11306
|
+
case "0.0.0.0":
|
|
11307
|
+
return "::";
|
|
11308
|
+
case "::":
|
|
11309
|
+
return "0.0.0.0";
|
|
11310
|
+
default:
|
|
11311
|
+
return null;
|
|
11312
|
+
}
|
|
11313
|
+
}
|
|
11314
|
+
async function isPortFree(port, host = "127.0.0.1") {
|
|
11315
|
+
if (await probeBind(port, host) !== "free") return false;
|
|
11316
|
+
const sibling = crossFamilyHost(host);
|
|
11317
|
+
if (sibling && await probeBind(port, sibling) === "in-use") return false;
|
|
11318
|
+
return true;
|
|
11319
|
+
}
|
|
10465
11320
|
function formatPortCollisionMessage(port) {
|
|
10466
11321
|
return [
|
|
10467
11322
|
`neat: port ${port} is in use; the NEAT daemon needs it.`,
|
|
@@ -10471,13 +11326,13 @@ function formatPortCollisionMessage(port) {
|
|
|
10471
11326
|
}
|
|
10472
11327
|
var PORT_ALLOCATION_ATTEMPTS = 8;
|
|
10473
11328
|
var PORT_STRIDE = 1;
|
|
10474
|
-
async function tripleFree(ports) {
|
|
11329
|
+
async function tripleFree(ports, host = "127.0.0.1") {
|
|
10475
11330
|
for (const p of [ports.rest, ports.otlp, ports.web]) {
|
|
10476
|
-
if (!await isPortFree(p)) return false;
|
|
11331
|
+
if (!await isPortFree(p, host)) return false;
|
|
10477
11332
|
}
|
|
10478
11333
|
return true;
|
|
10479
11334
|
}
|
|
10480
|
-
async function allocatePorts() {
|
|
11335
|
+
async function allocatePorts(host = "127.0.0.1") {
|
|
10481
11336
|
const [baseRest, baseOtlp, baseWeb] = NEAT_PORTS;
|
|
10482
11337
|
for (let i = 0; i < PORT_ALLOCATION_ATTEMPTS; i++) {
|
|
10483
11338
|
const candidate = {
|
|
@@ -10485,7 +11340,7 @@ async function allocatePorts() {
|
|
|
10485
11340
|
otlp: baseOtlp + i * PORT_STRIDE,
|
|
10486
11341
|
web: baseWeb + i * PORT_STRIDE
|
|
10487
11342
|
};
|
|
10488
|
-
if (await tripleFree(candidate)) return candidate;
|
|
11343
|
+
if (await tripleFree(candidate, host)) return candidate;
|
|
10489
11344
|
}
|
|
10490
11345
|
return null;
|
|
10491
11346
|
}
|
|
@@ -10691,16 +11546,20 @@ async function runOrchestrator(opts) {
|
|
|
10691
11546
|
}
|
|
10692
11547
|
}
|
|
10693
11548
|
const timeoutMs = opts.daemonReadyTimeoutMs ?? DEFAULT_DAEMON_READY_TIMEOUT_MS;
|
|
11549
|
+
const bindHost = resolveHost(
|
|
11550
|
+
{},
|
|
11551
|
+
typeof process.env.NEAT_AUTH_TOKEN === "string" && process.env.NEAT_AUTH_TOKEN.length > 0
|
|
11552
|
+
);
|
|
10694
11553
|
const persistedPorts = await persistedPortsFor(opts.scanPath);
|
|
10695
11554
|
let allocated = null;
|
|
10696
11555
|
if (persistedPorts && await healthIsForProject(persistedPorts.rest, currentProjectName)) {
|
|
10697
11556
|
result.steps.daemon = "already-running";
|
|
10698
11557
|
allocated = persistedPorts;
|
|
10699
11558
|
} else {
|
|
10700
|
-
if (persistedPorts && await isPortFree(persistedPorts.rest) && await tripleFree(persistedPorts)) {
|
|
11559
|
+
if (persistedPorts && await isPortFree(persistedPorts.rest, bindHost) && await tripleFree(persistedPorts, bindHost)) {
|
|
10701
11560
|
allocated = persistedPorts;
|
|
10702
11561
|
} else {
|
|
10703
|
-
allocated = await allocatePorts();
|
|
11562
|
+
allocated = await allocatePorts(bindHost);
|
|
10704
11563
|
}
|
|
10705
11564
|
if (!allocated) {
|
|
10706
11565
|
for (const line of formatPortCollisionMessage(NEAT_PORTS[0])) {
|
|
@@ -10729,7 +11588,7 @@ async function runOrchestrator(opts) {
|
|
|
10729
11588
|
projectPath: opts.scanPath,
|
|
10730
11589
|
ports: allocated
|
|
10731
11590
|
});
|
|
10732
|
-
const ready = await waitForDaemonReady(allocated.rest, timeoutMs);
|
|
11591
|
+
const ready = await waitForDaemonReady(allocated.rest, currentProjectName, timeoutMs);
|
|
10733
11592
|
result.steps.daemon = ready.ready ? "spawned" : "timed-out";
|
|
10734
11593
|
if (!ready.ready) {
|
|
10735
11594
|
console.error(`neat: daemon did not become ready within ${timeoutMs}ms`);
|
|
@@ -10797,7 +11656,7 @@ var import_node_path51 = __toESM(require("path"), 1);
|
|
|
10797
11656
|
|
|
10798
11657
|
// src/cli-client.ts
|
|
10799
11658
|
init_cjs_shims();
|
|
10800
|
-
var
|
|
11659
|
+
var import_types29 = require("@neat.is/types");
|
|
10801
11660
|
var HttpError = class extends Error {
|
|
10802
11661
|
constructor(status2, message, responseBody = "") {
|
|
10803
11662
|
super(message);
|
|
@@ -10913,7 +11772,7 @@ async function runBlastRadius(client, input) {
|
|
|
10913
11772
|
const result = await client.get(path53);
|
|
10914
11773
|
if (result.totalAffected === 0) {
|
|
10915
11774
|
return {
|
|
10916
|
-
summary: `${result.origin} has no
|
|
11775
|
+
summary: `${result.origin} has no dependents. Nothing else would break if it failed.`
|
|
10917
11776
|
};
|
|
10918
11777
|
}
|
|
10919
11778
|
const sorted = [...result.affectedNodes].sort(
|
|
@@ -10926,7 +11785,7 @@ async function runBlastRadius(client, input) {
|
|
|
10926
11785
|
);
|
|
10927
11786
|
const provenances = [...new Set(sorted.map((n) => n.edgeProvenance))];
|
|
10928
11787
|
return {
|
|
10929
|
-
summary: `Blast radius for ${result.origin}: ${result.totalAffected}
|
|
11788
|
+
summary: `Blast radius for ${result.origin}: ${result.totalAffected} dependent node${result.totalAffected === 1 ? "" : "s"} would break if it changed.`,
|
|
10930
11789
|
block: blockLines.join("\n"),
|
|
10931
11790
|
confidence: Number.isFinite(minConfidence) ? minConfidence : void 0,
|
|
10932
11791
|
provenance: provenances.length ? provenances : void 0
|
|
@@ -10939,7 +11798,7 @@ async function runBlastRadius(client, input) {
|
|
|
10939
11798
|
}
|
|
10940
11799
|
}
|
|
10941
11800
|
function formatBlastEntry(n) {
|
|
10942
|
-
const tag = n.edgeProvenance ===
|
|
11801
|
+
const tag = n.edgeProvenance === import_types29.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
10943
11802
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
10944
11803
|
}
|
|
10945
11804
|
async function runDependencies(client, input) {
|
|
@@ -10980,22 +11839,33 @@ async function runDependencies(client, input) {
|
|
|
10980
11839
|
throw err;
|
|
10981
11840
|
}
|
|
10982
11841
|
}
|
|
11842
|
+
function observedDepLine(nodeId, e) {
|
|
11843
|
+
const via = e.source !== nodeId ? ` (via ${e.source})` : "";
|
|
11844
|
+
return ` \u2022 ${e.target} \u2014 ${e.type}${via}${edgeMeta(e)}`;
|
|
11845
|
+
}
|
|
10983
11846
|
async function runObservedDependencies(client, input) {
|
|
10984
11847
|
try {
|
|
10985
|
-
const
|
|
10986
|
-
projectPath(
|
|
11848
|
+
const result = await client.get(
|
|
11849
|
+
projectPath(
|
|
11850
|
+
input.project,
|
|
11851
|
+
`/graph/observed-dependencies/${encodeURIComponent(input.nodeId)}`
|
|
11852
|
+
)
|
|
10987
11853
|
);
|
|
10988
|
-
|
|
10989
|
-
|
|
10990
|
-
|
|
10991
|
-
|
|
11854
|
+
if (result.dependencies.length === 0) {
|
|
11855
|
+
if (result.observed) {
|
|
11856
|
+
return {
|
|
11857
|
+
summary: `${input.nodeId} makes no outbound runtime calls, but OTel has observed it receiving traffic on ${result.inboundObservedCount} inbound call path${result.inboundObservedCount === 1 ? "" : "s"} \u2014 it's a pure receiver.`,
|
|
11858
|
+
provenance: import_types29.Provenance.OBSERVED
|
|
11859
|
+
};
|
|
11860
|
+
}
|
|
11861
|
+
const note = result.hasExtractedOutbound ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
10992
11862
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
10993
11863
|
}
|
|
10994
|
-
const blockLines =
|
|
11864
|
+
const blockLines = result.dependencies.map((e) => observedDepLine(input.nodeId, e));
|
|
10995
11865
|
return {
|
|
10996
|
-
summary: `${input.nodeId} has ${
|
|
11866
|
+
summary: `${input.nodeId} has ${result.dependencies.length} runtime dependenc${result.dependencies.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
10997
11867
|
block: blockLines.join("\n"),
|
|
10998
|
-
provenance:
|
|
11868
|
+
provenance: import_types29.Provenance.OBSERVED
|
|
10999
11869
|
};
|
|
11000
11870
|
} catch (err) {
|
|
11001
11871
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -11049,7 +11919,7 @@ async function runIncidents(client, input) {
|
|
|
11049
11919
|
return {
|
|
11050
11920
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
11051
11921
|
block: blockLines.join("\n"),
|
|
11052
|
-
provenance:
|
|
11922
|
+
provenance: import_types29.Provenance.OBSERVED
|
|
11053
11923
|
};
|
|
11054
11924
|
} catch (err) {
|
|
11055
11925
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -11158,7 +12028,7 @@ async function runStaleEdges(client, input) {
|
|
|
11158
12028
|
return {
|
|
11159
12029
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
11160
12030
|
block: blockLines.join("\n"),
|
|
11161
|
-
provenance:
|
|
12031
|
+
provenance: import_types29.Provenance.STALE
|
|
11162
12032
|
};
|
|
11163
12033
|
}
|
|
11164
12034
|
async function runPolicies(client, input) {
|
|
@@ -11475,7 +12345,7 @@ async function runSync(opts) {
|
|
|
11475
12345
|
}
|
|
11476
12346
|
|
|
11477
12347
|
// src/cli.ts
|
|
11478
|
-
var
|
|
12348
|
+
var import_types30 = require("@neat.is/types");
|
|
11479
12349
|
function isNpxInvocation() {
|
|
11480
12350
|
if (process.env.npm_command === "exec") return true;
|
|
11481
12351
|
const execpath = process.env.npm_execpath ?? "";
|
|
@@ -11540,7 +12410,7 @@ function usage() {
|
|
|
11540
12410
|
console.log("query commands (mirror the MCP tools, ADR-050):");
|
|
11541
12411
|
console.log(" root-cause <node-id> Walk inbound edges to find what broke first.");
|
|
11542
12412
|
console.log(` example: ${neat} root-cause service:<name>`);
|
|
11543
|
-
console.log(" blast-radius <node-id> BFS
|
|
12413
|
+
console.log(" blast-radius <node-id> BFS inbound \u2014 the dependents that break if this dies.");
|
|
11544
12414
|
console.log(` example: ${neat} blast-radius database:<host>`);
|
|
11545
12415
|
console.log(" dependencies <node-id> Transitive outbound dependencies.");
|
|
11546
12416
|
console.log(" Flags: --depth N (default 3, max 10)");
|
|
@@ -12267,11 +13137,18 @@ Pass --project <name> to choose:
|
|
|
12267
13137
|
${names}`
|
|
12268
13138
|
);
|
|
12269
13139
|
}
|
|
12270
|
-
function resolveDaemonUrl() {
|
|
12271
|
-
|
|
13140
|
+
async function resolveDaemonUrl(project) {
|
|
13141
|
+
const explicit = process.env.NEAT_API_URL ?? process.env.NEAT_CORE_URL;
|
|
13142
|
+
if (explicit) return explicit;
|
|
13143
|
+
if (project) {
|
|
13144
|
+
const daemon = await findDaemonByProject(project);
|
|
13145
|
+
if (daemon) return `http://localhost:${daemon.record.ports.rest}`;
|
|
13146
|
+
}
|
|
13147
|
+
return "http://localhost:8080";
|
|
12272
13148
|
}
|
|
12273
13149
|
async function runQueryVerb(cmd, parsed) {
|
|
12274
|
-
const
|
|
13150
|
+
const requestedProject = resolveProjectFlag(parsed);
|
|
13151
|
+
const baseUrl = await resolveDaemonUrl(requestedProject);
|
|
12275
13152
|
const client = createHttpClient(baseUrl, resolveAuthToken());
|
|
12276
13153
|
const positional = parsed.positional;
|
|
12277
13154
|
let makeWork;
|
|
@@ -12389,10 +13266,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
12389
13266
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
12390
13267
|
const out = [];
|
|
12391
13268
|
for (const p of parts) {
|
|
12392
|
-
const r =
|
|
13269
|
+
const r = import_types30.DivergenceTypeSchema.safeParse(p);
|
|
12393
13270
|
if (!r.success) {
|
|
12394
13271
|
console.error(
|
|
12395
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
13272
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types30.DivergenceTypeSchema.options.join(", ")}`
|
|
12396
13273
|
);
|
|
12397
13274
|
return 2;
|
|
12398
13275
|
}
|
|
@@ -12427,7 +13304,7 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
12427
13304
|
const detail = err.responseBody.length > 0 ? err.responseBody : err.message;
|
|
12428
13305
|
console.error(`neat ${cmd}: ${detail.trim()}`);
|
|
12429
13306
|
} else if (err instanceof TransportError) {
|
|
12430
|
-
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (
|
|
13307
|
+
console.error(`neat ${cmd}: ${err.message}. Is the daemon running? (endpoint=${baseUrl})`);
|
|
12431
13308
|
} else {
|
|
12432
13309
|
console.error(`neat ${cmd}: ${err.message}`);
|
|
12433
13310
|
}
|
|
@@ -12452,6 +13329,7 @@ if (/[\\/]cli\.(?:cjs|js)$/.test(entry) || entry.endsWith("/cli") || entry.endsW
|
|
|
12452
13329
|
parseArgs,
|
|
12453
13330
|
printBanner,
|
|
12454
13331
|
readPackageVersion,
|
|
13332
|
+
resolveDaemonUrl,
|
|
12455
13333
|
resolveProjectForVerb,
|
|
12456
13334
|
runInit,
|
|
12457
13335
|
runQueryVerb,
|