@neat.is/core 0.4.26-dev.20260702 → 0.4.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-ZX7PCMGZ.js → chunk-A3322JYS.js} +57 -3
- package/dist/chunk-A3322JYS.js.map +1 -0
- package/dist/{chunk-XOOCA5T7.js → chunk-BIY46Q6U.js} +25 -1
- package/dist/chunk-BIY46Q6U.js.map +1 -0
- package/dist/{chunk-O25KZNZK.js → chunk-QM6BMPVJ.js} +1200 -280
- package/dist/chunk-QM6BMPVJ.js.map +1 -0
- package/dist/{chunk-GAFTW2OX.js → chunk-UV5WSM7M.js} +2 -2
- package/dist/{chunk-IABNGQT2.js → chunk-XV4D7A3Z.js} +117 -6
- package/dist/chunk-XV4D7A3Z.js.map +1 -0
- package/dist/cli.cjs +1565 -546
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +49 -6
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1382 -326
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -1
- package/dist/index.d.ts +102 -1
- package/dist/index.js +4 -4
- package/dist/neatd.cjs +1543 -395
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +3 -3
- package/dist/{otel-grpc-QYHUJ4OY.js → otel-grpc-ET5Z6KI6.js} +3 -3
- package/dist/server.cjs +1206 -235
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +4 -4
- package/package.json +2 -2
- package/dist/chunk-IABNGQT2.js.map +0 -1
- package/dist/chunk-O25KZNZK.js.map +0 -1
- package/dist/chunk-XOOCA5T7.js.map +0 -1
- package/dist/chunk-ZX7PCMGZ.js.map +0 -1
- /package/dist/{chunk-GAFTW2OX.js.map → chunk-UV5WSM7M.js.map} +0 -0
- /package/dist/{otel-grpc-QYHUJ4OY.js.map → otel-grpc-ET5Z6KI6.js.map} +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mountBearerAuth,
|
|
3
3
|
readAuthEnv
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-A3322JYS.js";
|
|
5
5
|
|
|
6
6
|
// src/graph.ts
|
|
7
7
|
import GraphDefault from "graphology";
|
|
@@ -456,19 +456,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
456
456
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
457
457
|
let best = { path: [start], edges: [] };
|
|
458
458
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
459
|
-
function step(node,
|
|
460
|
-
if (
|
|
461
|
-
best = { path: [...
|
|
459
|
+
function step(node, path42, edges) {
|
|
460
|
+
if (path42.length > best.path.length) {
|
|
461
|
+
best = { path: [...path42], edges: [...edges] };
|
|
462
462
|
}
|
|
463
|
-
if (
|
|
463
|
+
if (path42.length - 1 >= maxDepth) return;
|
|
464
464
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
465
465
|
for (const [srcId, edge] of incoming) {
|
|
466
466
|
if (visited.has(srcId)) continue;
|
|
467
467
|
visited.add(srcId);
|
|
468
|
-
|
|
468
|
+
path42.push(srcId);
|
|
469
469
|
edges.push(edge);
|
|
470
|
-
step(srcId,
|
|
471
|
-
|
|
470
|
+
step(srcId, path42, edges);
|
|
471
|
+
path42.pop();
|
|
472
472
|
edges.pop();
|
|
473
473
|
visited.delete(srcId);
|
|
474
474
|
}
|
|
@@ -476,14 +476,14 @@ function longestIncomingWalk(graph, start, maxDepth) {
|
|
|
476
476
|
step(start, [start], []);
|
|
477
477
|
return best;
|
|
478
478
|
}
|
|
479
|
-
function databaseRootCauseShape(graph, origin,
|
|
479
|
+
function databaseRootCauseShape(graph, origin, walk3) {
|
|
480
480
|
const targetDb = origin;
|
|
481
481
|
const candidatePairs = compatPairs().filter((p) => p.engine === targetDb.engine);
|
|
482
482
|
if (candidatePairs.length === 0) return null;
|
|
483
|
-
for (const id of
|
|
483
|
+
for (const id of walk3.path) {
|
|
484
484
|
const owner = resolveOwningService(graph, id);
|
|
485
485
|
if (!owner) continue;
|
|
486
|
-
const { id:
|
|
486
|
+
const { id: serviceId4, svc } = owner;
|
|
487
487
|
const deps = svc.dependencies ?? {};
|
|
488
488
|
for (const pair of candidatePairs) {
|
|
489
489
|
const declared = deps[pair.driver];
|
|
@@ -496,7 +496,7 @@ function databaseRootCauseShape(graph, origin, walk) {
|
|
|
496
496
|
);
|
|
497
497
|
if (!result.compatible) {
|
|
498
498
|
return {
|
|
499
|
-
rootCauseNode:
|
|
499
|
+
rootCauseNode: serviceId4,
|
|
500
500
|
rootCauseReason: result.reason ?? "incompatible driver",
|
|
501
501
|
...result.minDriverVersion ? {
|
|
502
502
|
fixRecommendation: `Upgrade ${svc.name} ${pair.driver} driver to >= ${result.minDriverVersion}`
|
|
@@ -507,11 +507,11 @@ function databaseRootCauseShape(graph, origin, walk) {
|
|
|
507
507
|
}
|
|
508
508
|
return null;
|
|
509
509
|
}
|
|
510
|
-
function serviceRootCauseShape(graph, _origin,
|
|
511
|
-
for (const id of
|
|
510
|
+
function serviceRootCauseShape(graph, _origin, walk3) {
|
|
511
|
+
for (const id of walk3.path) {
|
|
512
512
|
const owner = resolveOwningService(graph, id);
|
|
513
513
|
if (!owner) continue;
|
|
514
|
-
const { id:
|
|
514
|
+
const { id: serviceId4, svc } = owner;
|
|
515
515
|
const deps = svc.dependencies ?? {};
|
|
516
516
|
const serviceNodeEngine = svc.nodeEngine;
|
|
517
517
|
for (const constraint of nodeEngineConstraints()) {
|
|
@@ -520,7 +520,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
|
|
|
520
520
|
const result = checkNodeEngineConstraint(constraint, declared, serviceNodeEngine);
|
|
521
521
|
if (!result.compatible && result.reason) {
|
|
522
522
|
return {
|
|
523
|
-
rootCauseNode:
|
|
523
|
+
rootCauseNode: serviceId4,
|
|
524
524
|
rootCauseReason: result.reason,
|
|
525
525
|
...result.requiredNodeVersion ? {
|
|
526
526
|
fixRecommendation: `Bump ${svc.name}'s engines.node to >= ${result.requiredNodeVersion}`
|
|
@@ -535,7 +535,7 @@ function serviceRootCauseShape(graph, _origin, walk) {
|
|
|
535
535
|
const result = checkPackageConflict(conflict, declared, requiredDeclared);
|
|
536
536
|
if (!result.compatible && result.reason) {
|
|
537
537
|
return {
|
|
538
|
-
rootCauseNode:
|
|
538
|
+
rootCauseNode: serviceId4,
|
|
539
539
|
rootCauseReason: result.reason,
|
|
540
540
|
fixRecommendation: `Upgrade ${svc.name}'s ${conflict.requires.name} to >= ${conflict.requires.minVersion}`
|
|
541
541
|
};
|
|
@@ -544,10 +544,10 @@ function serviceRootCauseShape(graph, _origin, walk) {
|
|
|
544
544
|
}
|
|
545
545
|
return null;
|
|
546
546
|
}
|
|
547
|
-
function fileRootCauseShape(graph, origin,
|
|
547
|
+
function fileRootCauseShape(graph, origin, walk3) {
|
|
548
548
|
const owner = resolveOwningService(graph, origin.id);
|
|
549
549
|
if (!owner) return null;
|
|
550
|
-
return serviceRootCauseShape(graph, owner.svc,
|
|
550
|
+
return serviceRootCauseShape(graph, owner.svc, walk3);
|
|
551
551
|
}
|
|
552
552
|
var rootCauseShapes = {
|
|
553
553
|
[NodeType.DatabaseNode]: databaseRootCauseShape,
|
|
@@ -559,16 +559,16 @@ function getRootCause(graph, errorNodeId, errorEvent, incidents) {
|
|
|
559
559
|
const origin = graph.getNodeAttributes(errorNodeId);
|
|
560
560
|
const shape = rootCauseShapes[origin.type];
|
|
561
561
|
if (shape) {
|
|
562
|
-
const
|
|
563
|
-
const match = shape(graph, origin,
|
|
562
|
+
const walk3 = longestIncomingWalk(graph, errorNodeId, ROOT_CAUSE_MAX_DEPTH);
|
|
563
|
+
const match = shape(graph, origin, walk3);
|
|
564
564
|
if (match) {
|
|
565
565
|
const reason = errorEvent ? `${match.rootCauseReason} (observed error: ${errorEvent.errorMessage})` : match.rootCauseReason;
|
|
566
566
|
return RootCauseResultSchema.parse({
|
|
567
567
|
rootCauseNode: match.rootCauseNode,
|
|
568
568
|
rootCauseReason: reason,
|
|
569
|
-
traversalPath:
|
|
570
|
-
edgeProvenances:
|
|
571
|
-
confidence: confidenceFromMix(
|
|
569
|
+
traversalPath: walk3.path,
|
|
570
|
+
edgeProvenances: walk3.edges.map((e) => e.provenance),
|
|
571
|
+
confidence: confidenceFromMix(walk3.edges),
|
|
572
572
|
fixRecommendation: match.fixRecommendation
|
|
573
573
|
});
|
|
574
574
|
}
|
|
@@ -626,9 +626,9 @@ function rootCauseFromIncidents(nodeId, incidents, errorEvent) {
|
|
|
626
626
|
function isFailingCallEdge(e) {
|
|
627
627
|
return e.type === EdgeType.CALLS && (e.signal?.errorCount ?? 0) > 0;
|
|
628
628
|
}
|
|
629
|
-
function callSourcesForService(graph,
|
|
630
|
-
const ids = [
|
|
631
|
-
for (const edgeId of graph.outboundEdges(
|
|
629
|
+
function callSourcesForService(graph, serviceId4) {
|
|
630
|
+
const ids = [serviceId4];
|
|
631
|
+
for (const edgeId of graph.outboundEdges(serviceId4)) {
|
|
632
632
|
const e = graph.getEdgeAttributes(edgeId);
|
|
633
633
|
if (e.type !== EdgeType.CONTAINS) continue;
|
|
634
634
|
const tgt = graph.getNodeAttributes(e.target);
|
|
@@ -645,9 +645,9 @@ function failingCallDominates(e, id, curEdge, curId) {
|
|
|
645
645
|
}
|
|
646
646
|
return id < curId;
|
|
647
647
|
}
|
|
648
|
-
function dominantFailingCall(graph,
|
|
648
|
+
function dominantFailingCall(graph, serviceId4, visited) {
|
|
649
649
|
let best = null;
|
|
650
|
-
for (const src of callSourcesForService(graph,
|
|
650
|
+
for (const src of callSourcesForService(graph, serviceId4)) {
|
|
651
651
|
for (const edgeId of graph.outboundEdges(src)) {
|
|
652
652
|
const e = graph.getEdgeAttributes(edgeId);
|
|
653
653
|
if (!isFailingCallEdge(e)) continue;
|
|
@@ -662,26 +662,26 @@ function dominantFailingCall(graph, serviceId3, visited) {
|
|
|
662
662
|
return best;
|
|
663
663
|
}
|
|
664
664
|
function followFailingCallChain(graph, originServiceId, maxDepth) {
|
|
665
|
-
const
|
|
665
|
+
const path42 = [originServiceId];
|
|
666
666
|
const edges = [];
|
|
667
667
|
const visited = /* @__PURE__ */ new Set([originServiceId]);
|
|
668
668
|
let current = originServiceId;
|
|
669
669
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
670
670
|
const hop = dominantFailingCall(graph, current, visited);
|
|
671
671
|
if (!hop) break;
|
|
672
|
-
|
|
672
|
+
path42.push(hop.nextService);
|
|
673
673
|
edges.push(hop.edge);
|
|
674
674
|
visited.add(hop.nextService);
|
|
675
675
|
current = hop.nextService;
|
|
676
676
|
}
|
|
677
677
|
if (edges.length === 0) return null;
|
|
678
|
-
return { path:
|
|
678
|
+
return { path: path42, edges, culprit: current };
|
|
679
679
|
}
|
|
680
680
|
function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
681
681
|
const chain = followFailingCallChain(graph, originId, ROOT_CAUSE_MAX_DEPTH);
|
|
682
682
|
if (!chain) return null;
|
|
683
683
|
const culprit = chain.culprit;
|
|
684
|
-
const
|
|
684
|
+
const path42 = [...chain.path];
|
|
685
685
|
const edgeProvenances = chain.edges.map((e) => e.provenance);
|
|
686
686
|
const baseConfidence = confidenceFromMix(chain.edges);
|
|
687
687
|
const confidence = Math.max(0, Math.min(1, baseConfidence * INCIDENT_ROOT_CAUSE_CONFIDENCE));
|
|
@@ -689,14 +689,14 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
689
689
|
if (loc) {
|
|
690
690
|
let rootCauseNode = culprit;
|
|
691
691
|
if (loc.fileNode) {
|
|
692
|
-
|
|
692
|
+
path42.push(loc.fileNode);
|
|
693
693
|
edgeProvenances.push(Provenance.OBSERVED);
|
|
694
694
|
rootCauseNode = loc.fileNode;
|
|
695
695
|
}
|
|
696
696
|
return RootCauseResultSchema.parse({
|
|
697
697
|
rootCauseNode,
|
|
698
698
|
rootCauseReason: loc.rootCauseReason,
|
|
699
|
-
traversalPath:
|
|
699
|
+
traversalPath: path42,
|
|
700
700
|
edgeProvenances,
|
|
701
701
|
confidence,
|
|
702
702
|
...loc.fixRecommendation ? { fixRecommendation: loc.fixRecommendation } : {}
|
|
@@ -708,7 +708,7 @@ function crossServiceRootCause(graph, originId, incidents, errorEvent) {
|
|
|
708
708
|
return RootCauseResultSchema.parse({
|
|
709
709
|
rootCauseNode: culprit,
|
|
710
710
|
rootCauseReason: `${culpritName} is failing downstream calls (${errs} observed error${errs === 1 ? "" : "s"})`,
|
|
711
|
-
traversalPath:
|
|
711
|
+
traversalPath: path42,
|
|
712
712
|
edgeProvenances,
|
|
713
713
|
confidence,
|
|
714
714
|
fixRecommendation: `Inspect ${culpritName}'s failing handler`
|
|
@@ -1377,12 +1377,17 @@ import {
|
|
|
1377
1377
|
Provenance as Provenance2,
|
|
1378
1378
|
confidenceForObservedSignal,
|
|
1379
1379
|
databaseId,
|
|
1380
|
+
localDatabaseId,
|
|
1380
1381
|
extractedEdgeId,
|
|
1381
1382
|
fileId,
|
|
1382
1383
|
frontierId,
|
|
1384
|
+
graphqlOperationId,
|
|
1385
|
+
grpcMethodId,
|
|
1383
1386
|
inferredEdgeId,
|
|
1387
|
+
infraId,
|
|
1384
1388
|
observedEdgeId,
|
|
1385
|
-
serviceId
|
|
1389
|
+
serviceId,
|
|
1390
|
+
websocketChannelId
|
|
1386
1391
|
} from "@neat.is/types";
|
|
1387
1392
|
var HOUR_MS = 60 * 60 * 1e3;
|
|
1388
1393
|
var DAY_MS = 24 * HOUR_MS;
|
|
@@ -1719,10 +1724,82 @@ var STITCH_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
|
1719
1724
|
]);
|
|
1720
1725
|
var WIRE_SPAN_KIND_CLIENT = 3;
|
|
1721
1726
|
var WIRE_SPAN_KIND_PRODUCER = 4;
|
|
1727
|
+
var WIRE_SPAN_KIND_CONSUMER = 5;
|
|
1722
1728
|
function spanMintsObservedEdge(kind) {
|
|
1723
1729
|
if (kind === void 0 || kind === 0) return true;
|
|
1724
1730
|
return kind === WIRE_SPAN_KIND_CLIENT || kind === WIRE_SPAN_KIND_PRODUCER;
|
|
1725
1731
|
}
|
|
1732
|
+
function spanMintsMessagingEdge(kind) {
|
|
1733
|
+
return kind === WIRE_SPAN_KIND_PRODUCER || kind === WIRE_SPAN_KIND_CONSUMER;
|
|
1734
|
+
}
|
|
1735
|
+
function spanServesGraphqlOperation(kind) {
|
|
1736
|
+
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
1737
|
+
}
|
|
1738
|
+
function ensureGraphqlOperationNode(graph, serviceName, operationType, operationName) {
|
|
1739
|
+
const id = graphqlOperationId(serviceName, operationType, operationName);
|
|
1740
|
+
if (graph.hasNode(id)) return id;
|
|
1741
|
+
const node = {
|
|
1742
|
+
id,
|
|
1743
|
+
type: NodeType3.GraphQLOperationNode,
|
|
1744
|
+
name: operationName,
|
|
1745
|
+
service: serviceName,
|
|
1746
|
+
operationType: operationType.toLowerCase(),
|
|
1747
|
+
operationName,
|
|
1748
|
+
discoveredVia: "otel"
|
|
1749
|
+
};
|
|
1750
|
+
graph.addNode(id, node);
|
|
1751
|
+
return id;
|
|
1752
|
+
}
|
|
1753
|
+
function spanServesGrpcMethod(kind) {
|
|
1754
|
+
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
1755
|
+
}
|
|
1756
|
+
function ensureGrpcMethodNode(graph, rpcService, rpcMethod) {
|
|
1757
|
+
const id = grpcMethodId(rpcService, rpcMethod);
|
|
1758
|
+
if (graph.hasNode(id)) return id;
|
|
1759
|
+
const node = {
|
|
1760
|
+
id,
|
|
1761
|
+
type: NodeType3.GrpcMethodNode,
|
|
1762
|
+
name: `${rpcService}/${rpcMethod}`,
|
|
1763
|
+
rpcService,
|
|
1764
|
+
rpcMethod,
|
|
1765
|
+
discoveredVia: "otel"
|
|
1766
|
+
};
|
|
1767
|
+
graph.addNode(id, node);
|
|
1768
|
+
return id;
|
|
1769
|
+
}
|
|
1770
|
+
function spanServesWebsocketChannel(kind) {
|
|
1771
|
+
return kind !== WIRE_SPAN_KIND_CLIENT && kind !== WIRE_SPAN_KIND_PRODUCER && kind !== WIRE_SPAN_KIND_CONSUMER;
|
|
1772
|
+
}
|
|
1773
|
+
function ensureWebsocketChannelNode(graph, serviceName, channel) {
|
|
1774
|
+
const id = websocketChannelId(serviceName, channel);
|
|
1775
|
+
if (graph.hasNode(id)) return id;
|
|
1776
|
+
const node = {
|
|
1777
|
+
id,
|
|
1778
|
+
type: NodeType3.WebSocketChannelNode,
|
|
1779
|
+
name: channel,
|
|
1780
|
+
service: serviceName,
|
|
1781
|
+
channel,
|
|
1782
|
+
discoveredVia: "otel"
|
|
1783
|
+
};
|
|
1784
|
+
graph.addNode(id, node);
|
|
1785
|
+
return id;
|
|
1786
|
+
}
|
|
1787
|
+
function messagingDestinationKind(system) {
|
|
1788
|
+
return `${system}-topic`;
|
|
1789
|
+
}
|
|
1790
|
+
function ensureMessagingDestinationNode(graph, system, destination) {
|
|
1791
|
+
const id = infraId(messagingDestinationKind(system), destination);
|
|
1792
|
+
if (graph.hasNode(id)) return id;
|
|
1793
|
+
const node = {
|
|
1794
|
+
id,
|
|
1795
|
+
type: NodeType3.InfraNode,
|
|
1796
|
+
name: destination,
|
|
1797
|
+
provider: "self",
|
|
1798
|
+
kind: messagingDestinationKind(system)
|
|
1799
|
+
};
|
|
1800
|
+
graph.addNode(id, node);
|
|
1801
|
+
return id;
|
|
1802
|
+
}
|
|
1726
1803
|
var PARENT_SPAN_CACHE_SIZE = 1e4;
|
|
1727
1804
|
var PARENT_SPAN_CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
1728
1805
|
var parentSpanCache = /* @__PURE__ */ new Map();
|
|
@@ -1816,6 +1893,21 @@ function ensureDatabaseNode(graph, host, engine) {
|
|
|
1816
1893
|
graph.addNode(id, node);
|
|
1817
1894
|
return id;
|
|
1818
1895
|
}
|
|
1896
|
+
function ensureLocalDatabaseNode(graph, serviceName, name, engine) {
|
|
1897
|
+
const id = localDatabaseId(serviceName, name);
|
|
1898
|
+
if (graph.hasNode(id)) return id;
|
|
1899
|
+
const node = {
|
|
1900
|
+
id,
|
|
1901
|
+
type: NodeType3.DatabaseNode,
|
|
1902
|
+
name,
|
|
1903
|
+
engine,
|
|
1904
|
+
engineVersion: "unknown",
|
|
1905
|
+
compatibleDrivers: [],
|
|
1906
|
+
discoveredVia: "otel"
|
|
1907
|
+
};
|
|
1908
|
+
graph.addNode(id, node);
|
|
1909
|
+
return id;
|
|
1910
|
+
}
|
|
1819
1911
|
function ensureFrontierNode(graph, host, ts) {
|
|
1820
1912
|
const id = frontierIdFor(host);
|
|
1821
1913
|
if (graph.hasNode(id)) {
|
|
@@ -2067,10 +2159,21 @@ async function handleSpan(ctx, span) {
|
|
|
2067
2159
|
let affectedNode = sourceId;
|
|
2068
2160
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
2069
2161
|
if (span.dbSystem) {
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2162
|
+
if (mintsFromCallerSide) {
|
|
2163
|
+
const host = pickAddress(span);
|
|
2164
|
+
let targetId;
|
|
2165
|
+
if (host) {
|
|
2166
|
+
ensureDatabaseNode(ctx.graph, host, span.dbSystem);
|
|
2167
|
+
targetId = databaseId(host);
|
|
2168
|
+
} else {
|
|
2169
|
+
const localName = span.dbName ?? span.dbSystem;
|
|
2170
|
+
targetId = ensureLocalDatabaseNode(
|
|
2171
|
+
ctx.graph,
|
|
2172
|
+
span.service,
|
|
2173
|
+
localName,
|
|
2174
|
+
span.dbSystem
|
|
2175
|
+
);
|
|
2176
|
+
}
|
|
2074
2177
|
const result = upsertObservedEdge(
|
|
2075
2178
|
ctx.graph,
|
|
2076
2179
|
EdgeType3.CONNECTS_TO,
|
|
@@ -2082,6 +2185,68 @@ async function handleSpan(ctx, span) {
|
|
|
2082
2185
|
);
|
|
2083
2186
|
if (result) affectedNode = targetId;
|
|
2084
2187
|
}
|
|
2188
|
+
} else if (span.messagingSystem && span.messagingDestination && spanMintsMessagingEdge(span.kind)) {
|
|
2189
|
+
const targetId = ensureMessagingDestinationNode(
|
|
2190
|
+
ctx.graph,
|
|
2191
|
+
span.messagingSystem,
|
|
2192
|
+
span.messagingDestination
|
|
2193
|
+
);
|
|
2194
|
+
const edgeType = span.kind === WIRE_SPAN_KIND_CONSUMER ? EdgeType3.CONSUMES_FROM : EdgeType3.PUBLISHES_TO;
|
|
2195
|
+
const result = upsertObservedEdge(
|
|
2196
|
+
ctx.graph,
|
|
2197
|
+
edgeType,
|
|
2198
|
+
observedSource(),
|
|
2199
|
+
targetId,
|
|
2200
|
+
ts,
|
|
2201
|
+
isError,
|
|
2202
|
+
callSiteEvidence
|
|
2203
|
+
);
|
|
2204
|
+
if (result) affectedNode = targetId;
|
|
2205
|
+
} else if (span.graphqlOperationName && span.graphqlOperationType && spanServesGraphqlOperation(span.kind)) {
|
|
2206
|
+
const targetId = ensureGraphqlOperationNode(
|
|
2207
|
+
ctx.graph,
|
|
2208
|
+
span.service,
|
|
2209
|
+
span.graphqlOperationType,
|
|
2210
|
+
span.graphqlOperationName
|
|
2211
|
+
);
|
|
2212
|
+
const result = upsertObservedEdge(
|
|
2213
|
+
ctx.graph,
|
|
2214
|
+
EdgeType3.CONTAINS,
|
|
2215
|
+
observedSource(),
|
|
2216
|
+
targetId,
|
|
2217
|
+
ts,
|
|
2218
|
+
isError,
|
|
2219
|
+
callSiteEvidence
|
|
2220
|
+
);
|
|
2221
|
+
if (result) affectedNode = targetId;
|
|
2222
|
+
} else if (span.rpcSystem === "grpc" && span.rpcService && span.rpcMethod && spanServesGrpcMethod(span.kind)) {
|
|
2223
|
+
const targetId = ensureGrpcMethodNode(ctx.graph, span.rpcService, span.rpcMethod);
|
|
2224
|
+
const result = upsertObservedEdge(
|
|
2225
|
+
ctx.graph,
|
|
2226
|
+
EdgeType3.CONTAINS,
|
|
2227
|
+
observedSource(),
|
|
2228
|
+
targetId,
|
|
2229
|
+
ts,
|
|
2230
|
+
isError,
|
|
2231
|
+
callSiteEvidence
|
|
2232
|
+
);
|
|
2233
|
+
if (result) affectedNode = targetId;
|
|
2234
|
+
} else if (span.websocketChannel && spanServesWebsocketChannel(span.kind)) {
|
|
2235
|
+
const targetId = ensureWebsocketChannelNode(
|
|
2236
|
+
ctx.graph,
|
|
2237
|
+
span.service,
|
|
2238
|
+
span.websocketChannel
|
|
2239
|
+
);
|
|
2240
|
+
const result = upsertObservedEdge(
|
|
2241
|
+
ctx.graph,
|
|
2242
|
+
EdgeType3.CONNECTS_TO,
|
|
2243
|
+
observedSource(),
|
|
2244
|
+
targetId,
|
|
2245
|
+
ts,
|
|
2246
|
+
isError,
|
|
2247
|
+
callSiteEvidence
|
|
2248
|
+
);
|
|
2249
|
+
if (result) affectedNode = targetId;
|
|
2085
2250
|
} else {
|
|
2086
2251
|
const host = pickAddress(span);
|
|
2087
2252
|
let resolvedViaAddress = false;
|
|
@@ -2191,29 +2356,29 @@ function promoteFrontierNodes(graph, opts = {}) {
|
|
|
2191
2356
|
toPromote.push({ frontierId: id, serviceId: target });
|
|
2192
2357
|
});
|
|
2193
2358
|
let promoted = 0;
|
|
2194
|
-
for (const { frontierId: frontierId2, serviceId:
|
|
2359
|
+
for (const { frontierId: frontierId2, serviceId: serviceId4 } of toPromote) {
|
|
2195
2360
|
if (opts.policies && opts.policies.length > 0 && opts.policyCtx) {
|
|
2196
2361
|
const gate = canPromoteFrontier(graph, frontierId2, opts.policies, opts.policyCtx);
|
|
2197
2362
|
if (!gate.allowed) {
|
|
2198
2363
|
continue;
|
|
2199
2364
|
}
|
|
2200
2365
|
}
|
|
2201
|
-
rewireFrontierEdges(graph, frontierId2,
|
|
2366
|
+
rewireFrontierEdges(graph, frontierId2, serviceId4);
|
|
2202
2367
|
graph.dropNode(frontierId2);
|
|
2203
2368
|
promoted++;
|
|
2204
2369
|
}
|
|
2205
2370
|
return promoted;
|
|
2206
2371
|
}
|
|
2207
|
-
function rewireFrontierEdges(graph, frontierId2,
|
|
2372
|
+
function rewireFrontierEdges(graph, frontierId2, serviceId4) {
|
|
2208
2373
|
const inbound = [...graph.inboundEdges(frontierId2)];
|
|
2209
2374
|
const outbound = [...graph.outboundEdges(frontierId2)];
|
|
2210
2375
|
for (const edgeId of inbound) {
|
|
2211
2376
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2212
|
-
rebuildEdge(graph, edge, edge.source,
|
|
2377
|
+
rebuildEdge(graph, edge, edge.source, serviceId4, edgeId);
|
|
2213
2378
|
}
|
|
2214
2379
|
for (const edgeId of outbound) {
|
|
2215
2380
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
2216
|
-
rebuildEdge(graph, edge,
|
|
2381
|
+
rebuildEdge(graph, edge, serviceId4, edge.target, edgeId);
|
|
2217
2382
|
}
|
|
2218
2383
|
}
|
|
2219
2384
|
function rebuildEdge(graph, edge, newSource, newTarget, oldEdgeId) {
|
|
@@ -2981,9 +3146,9 @@ var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
|
2981
3146
|
"StatefulSet",
|
|
2982
3147
|
"DaemonSet"
|
|
2983
3148
|
]);
|
|
2984
|
-
function addAliases(graph,
|
|
2985
|
-
if (!graph.hasNode(
|
|
2986
|
-
const node = graph.getNodeAttributes(
|
|
3149
|
+
function addAliases(graph, serviceId4, candidates) {
|
|
3150
|
+
if (!graph.hasNode(serviceId4)) return;
|
|
3151
|
+
const node = graph.getNodeAttributes(serviceId4);
|
|
2987
3152
|
if (node.type !== NodeType5.ServiceNode) return;
|
|
2988
3153
|
const set = new Set(node.aliases ?? []);
|
|
2989
3154
|
for (const c of candidates) {
|
|
@@ -2993,7 +3158,7 @@ function addAliases(graph, serviceId3, candidates) {
|
|
|
2993
3158
|
}
|
|
2994
3159
|
if (set.size === 0) return;
|
|
2995
3160
|
const updated = { ...node, aliases: [...set].sort() };
|
|
2996
|
-
graph.replaceNodeAttributes(
|
|
3161
|
+
graph.replaceNodeAttributes(serviceId4, updated);
|
|
2997
3162
|
}
|
|
2998
3163
|
function indexServicesByName(services) {
|
|
2999
3164
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -3026,12 +3191,12 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
3026
3191
|
}
|
|
3027
3192
|
if (!compose?.services) return;
|
|
3028
3193
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3029
|
-
const
|
|
3030
|
-
if (!
|
|
3194
|
+
const serviceId4 = serviceIndex.get(composeName);
|
|
3195
|
+
if (!serviceId4) continue;
|
|
3031
3196
|
const aliases = /* @__PURE__ */ new Set([composeName]);
|
|
3032
3197
|
if (svc.container_name) aliases.add(svc.container_name);
|
|
3033
3198
|
if (svc.hostname) aliases.add(svc.hostname);
|
|
3034
|
-
addAliases(graph,
|
|
3199
|
+
addAliases(graph, serviceId4, aliases);
|
|
3035
3200
|
}
|
|
3036
3201
|
}
|
|
3037
3202
|
var LABEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -3147,16 +3312,28 @@ import {
|
|
|
3147
3312
|
extractedEdgeId as extractedEdgeId3,
|
|
3148
3313
|
fileId as fileId2
|
|
3149
3314
|
} from "@neat.is/types";
|
|
3315
|
+
function buildServiceHostIndex(services) {
|
|
3316
|
+
const knownHosts = /* @__PURE__ */ new Set();
|
|
3317
|
+
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3318
|
+
for (const service of services) {
|
|
3319
|
+
const base = path10.basename(service.dir);
|
|
3320
|
+
knownHosts.add(base);
|
|
3321
|
+
knownHosts.add(service.pkg.name);
|
|
3322
|
+
hostToNodeId.set(base, service.node.id);
|
|
3323
|
+
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3324
|
+
}
|
|
3325
|
+
return { knownHosts, hostToNodeId };
|
|
3326
|
+
}
|
|
3150
3327
|
async function walkSourceFiles(dir) {
|
|
3151
3328
|
const out = [];
|
|
3152
|
-
async function
|
|
3329
|
+
async function walk3(current) {
|
|
3153
3330
|
const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3154
3331
|
for (const entry of entries) {
|
|
3155
3332
|
const full = path10.join(current, entry.name);
|
|
3156
3333
|
if (entry.isDirectory()) {
|
|
3157
3334
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3158
3335
|
if (await isPythonVenvDir(full)) continue;
|
|
3159
|
-
await
|
|
3336
|
+
await walk3(full);
|
|
3160
3337
|
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name)) && // Skip NEAT's own generated `otel-init.*` bootstrap — extracting it
|
|
3161
3338
|
// would attribute our instrumentation imports to the user's service.
|
|
3162
3339
|
!isNeatAuthoredSourceFile(entry.name)) {
|
|
@@ -3164,7 +3341,7 @@ async function walkSourceFiles(dir) {
|
|
|
3164
3341
|
}
|
|
3165
3342
|
}
|
|
3166
3343
|
}
|
|
3167
|
-
await
|
|
3344
|
+
await walk3(dir);
|
|
3168
3345
|
return out;
|
|
3169
3346
|
}
|
|
3170
3347
|
async function loadSourceFiles(dir) {
|
|
@@ -4264,20 +4441,20 @@ import {
|
|
|
4264
4441
|
} from "@neat.is/types";
|
|
4265
4442
|
async function walkConfigFiles(dir) {
|
|
4266
4443
|
const out = [];
|
|
4267
|
-
async function
|
|
4444
|
+
async function walk3(current) {
|
|
4268
4445
|
const entries = await fs14.readdir(current, { withFileTypes: true });
|
|
4269
4446
|
for (const entry of entries) {
|
|
4270
4447
|
const full = path21.join(current, entry.name);
|
|
4271
4448
|
if (entry.isDirectory()) {
|
|
4272
4449
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
4273
4450
|
if (await isPythonVenvDir(full)) continue;
|
|
4274
|
-
await
|
|
4451
|
+
await walk3(full);
|
|
4275
4452
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
4276
4453
|
out.push(full);
|
|
4277
4454
|
}
|
|
4278
4455
|
}
|
|
4279
4456
|
}
|
|
4280
|
-
await
|
|
4457
|
+
await walk3(dir);
|
|
4281
4458
|
return out;
|
|
4282
4459
|
}
|
|
4283
4460
|
async function addConfigNodes(graph, services, scanPath) {
|
|
@@ -4325,24 +4502,487 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
4325
4502
|
return { nodesAdded, edgesAdded };
|
|
4326
4503
|
}
|
|
4327
4504
|
|
|
4328
|
-
// src/extract/
|
|
4505
|
+
// src/extract/routes.ts
|
|
4506
|
+
import path22 from "path";
|
|
4507
|
+
import Parser2 from "tree-sitter";
|
|
4508
|
+
import JavaScript2 from "tree-sitter-javascript";
|
|
4329
4509
|
import {
|
|
4330
|
-
EdgeType as
|
|
4510
|
+
EdgeType as EdgeType8,
|
|
4331
4511
|
NodeType as NodeType9,
|
|
4512
|
+
Provenance as Provenance7,
|
|
4513
|
+
confidenceForExtracted as confidenceForExtracted5,
|
|
4514
|
+
extractedEdgeId as extractedEdgeId5,
|
|
4515
|
+
routeId
|
|
4516
|
+
} from "@neat.is/types";
|
|
4517
|
+
var PARSE_CHUNK2 = 16384;
|
|
4518
|
+
function parseSource2(parser, source) {
|
|
4519
|
+
return parser.parse(
|
|
4520
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
|
|
4521
|
+
);
|
|
4522
|
+
}
|
|
4523
|
+
function makeJsParser2() {
|
|
4524
|
+
const p = new Parser2();
|
|
4525
|
+
p.setLanguage(JavaScript2);
|
|
4526
|
+
return p;
|
|
4527
|
+
}
|
|
4528
|
+
var ROUTER_METHODS = /* @__PURE__ */ new Set([
|
|
4529
|
+
"get",
|
|
4530
|
+
"post",
|
|
4531
|
+
"put",
|
|
4532
|
+
"patch",
|
|
4533
|
+
"delete",
|
|
4534
|
+
"options",
|
|
4535
|
+
"head",
|
|
4536
|
+
"all"
|
|
4537
|
+
]);
|
|
4538
|
+
var NEXT_APP_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
4539
|
+
var JS_ROUTE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
4540
|
+
function canonicalizeTemplate(raw) {
|
|
4541
|
+
let p = raw.split("?")[0].split("#")[0];
|
|
4542
|
+
if (!p.startsWith("/")) p = "/" + p;
|
|
4543
|
+
if (p.length > 1 && p.endsWith("/")) p = p.slice(0, -1);
|
|
4544
|
+
return p;
|
|
4545
|
+
}
|
|
4546
|
+
function isDynamicSegment(seg) {
|
|
4547
|
+
if (seg.length === 0) return false;
|
|
4548
|
+
if (seg.includes(":")) return true;
|
|
4549
|
+
if (seg.startsWith("{") || seg.startsWith("[")) return true;
|
|
4550
|
+
if (/^\d+$/.test(seg)) return true;
|
|
4551
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(seg)) return true;
|
|
4552
|
+
if (/^[0-9a-f]{24,}$/i.test(seg)) return true;
|
|
4553
|
+
return false;
|
|
4554
|
+
}
|
|
4555
|
+
function normalizePathTemplate(raw) {
|
|
4556
|
+
const canonical = canonicalizeTemplate(raw);
|
|
4557
|
+
const segments = canonical.split("/").filter((s) => s.length > 0);
|
|
4558
|
+
const normalised = segments.map((seg) => isDynamicSegment(seg) ? ":param" : seg.toLowerCase());
|
|
4559
|
+
return "/" + normalised.join("/");
|
|
4560
|
+
}
|
|
4561
|
+
function walk(node, visit) {
|
|
4562
|
+
visit(node);
|
|
4563
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4564
|
+
const child = node.namedChild(i);
|
|
4565
|
+
if (child) walk(child, visit);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
function staticStringText(node) {
|
|
4569
|
+
if (node.type === "string") {
|
|
4570
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4571
|
+
const child = node.namedChild(i);
|
|
4572
|
+
if (child?.type === "string_fragment") return child.text;
|
|
4573
|
+
}
|
|
4574
|
+
return "";
|
|
4575
|
+
}
|
|
4576
|
+
if (node.type === "template_string") {
|
|
4577
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
4578
|
+
if (node.namedChild(i)?.type === "template_substitution") return null;
|
|
4579
|
+
}
|
|
4580
|
+
const raw = node.text;
|
|
4581
|
+
return raw.length >= 2 ? raw.slice(1, -1) : "";
|
|
4582
|
+
}
|
|
4583
|
+
return null;
|
|
4584
|
+
}
|
|
4585
|
+
function objectStringProp(objNode, key) {
|
|
4586
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
4587
|
+
const pair = objNode.namedChild(i);
|
|
4588
|
+
if (!pair || pair.type !== "pair") continue;
|
|
4589
|
+
const k = pair.childForFieldName("key");
|
|
4590
|
+
if (!k) continue;
|
|
4591
|
+
const kText = k.type === "string" ? staticStringText(k) : k.text;
|
|
4592
|
+
if (kText !== key) continue;
|
|
4593
|
+
const v = pair.childForFieldName("value");
|
|
4594
|
+
if (v) return staticStringText(v);
|
|
4595
|
+
}
|
|
4596
|
+
return null;
|
|
4597
|
+
}
|
|
4598
|
+
function fastifyRouteMethods(objNode) {
|
|
4599
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
4600
|
+
const pair = objNode.namedChild(i);
|
|
4601
|
+
if (!pair || pair.type !== "pair") continue;
|
|
4602
|
+
const k = pair.childForFieldName("key");
|
|
4603
|
+
const kText = k ? k.type === "string" ? staticStringText(k) : k.text : null;
|
|
4604
|
+
if (kText !== "method") continue;
|
|
4605
|
+
const v = pair.childForFieldName("value");
|
|
4606
|
+
if (!v) return [];
|
|
4607
|
+
if (v.type === "string" || v.type === "template_string") {
|
|
4608
|
+
const s = staticStringText(v);
|
|
4609
|
+
return s ? [s.toUpperCase()] : [];
|
|
4610
|
+
}
|
|
4611
|
+
if (v.type === "array") {
|
|
4612
|
+
const out = [];
|
|
4613
|
+
for (let j = 0; j < v.namedChildCount; j++) {
|
|
4614
|
+
const el = v.namedChild(j);
|
|
4615
|
+
if (el && (el.type === "string" || el.type === "template_string")) {
|
|
4616
|
+
const s = staticStringText(el);
|
|
4617
|
+
if (s) out.push(s.toUpperCase());
|
|
4618
|
+
}
|
|
4619
|
+
}
|
|
4620
|
+
return out;
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
return [];
|
|
4624
|
+
}
|
|
4625
|
+
function serverRoutesFromSource(source, parser, hasExpress, hasFastify) {
|
|
4626
|
+
const tree = parseSource2(parser, source);
|
|
4627
|
+
const out = [];
|
|
4628
|
+
const framework = hasExpress ? "express" : "fastify";
|
|
4629
|
+
walk(tree.rootNode, (node) => {
|
|
4630
|
+
if (node.type !== "call_expression") return;
|
|
4631
|
+
const fn = node.childForFieldName("function");
|
|
4632
|
+
if (!fn || fn.type !== "member_expression") return;
|
|
4633
|
+
const prop = fn.childForFieldName("property");
|
|
4634
|
+
if (!prop) return;
|
|
4635
|
+
const method = prop.text.toLowerCase();
|
|
4636
|
+
const args = node.childForFieldName("arguments");
|
|
4637
|
+
const first = args?.namedChild(0);
|
|
4638
|
+
if (!first) return;
|
|
4639
|
+
const line = node.startPosition.row + 1;
|
|
4640
|
+
if (ROUTER_METHODS.has(method)) {
|
|
4641
|
+
const p = staticStringText(first);
|
|
4642
|
+
if (p && p.startsWith("/")) {
|
|
4643
|
+
out.push({
|
|
4644
|
+
method: method === "all" ? "ALL" : method.toUpperCase(),
|
|
4645
|
+
pathTemplate: canonicalizeTemplate(p),
|
|
4646
|
+
line,
|
|
4647
|
+
framework
|
|
4648
|
+
});
|
|
4649
|
+
}
|
|
4650
|
+
return;
|
|
4651
|
+
}
|
|
4652
|
+
if (method === "route" && hasFastify && first.type === "object") {
|
|
4653
|
+
const url = objectStringProp(first, "url");
|
|
4654
|
+
if (!url || !url.startsWith("/")) return;
|
|
4655
|
+
const methods = fastifyRouteMethods(first);
|
|
4656
|
+
const list = methods.length > 0 ? methods : ["ALL"];
|
|
4657
|
+
for (const m of list) {
|
|
4658
|
+
out.push({
|
|
4659
|
+
method: m === "ALL" ? "ALL" : m.toUpperCase(),
|
|
4660
|
+
pathTemplate: canonicalizeTemplate(url),
|
|
4661
|
+
line,
|
|
4662
|
+
framework: "fastify"
|
|
4663
|
+
});
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
});
|
|
4667
|
+
return out;
|
|
4668
|
+
}
|
|
4669
|
+
function segmentsOf(relFile) {
|
|
4670
|
+
return toPosix2(relFile).split("/").filter((s) => s.length > 0);
|
|
4671
|
+
}
|
|
4672
|
+
function isNextAppRouteFile(relFile) {
|
|
4673
|
+
const segs = segmentsOf(relFile);
|
|
4674
|
+
if (!segs.includes("app")) return false;
|
|
4675
|
+
const base = segs[segs.length - 1] ?? "";
|
|
4676
|
+
return /^route\.(?:js|jsx|mjs|cjs|ts|tsx)$/.test(base);
|
|
4677
|
+
}
|
|
4678
|
+
function isNextPagesApiFile(relFile) {
|
|
4679
|
+
const segs = segmentsOf(relFile);
|
|
4680
|
+
const pagesIdx = segs.indexOf("pages");
|
|
4681
|
+
if (pagesIdx === -1 || segs[pagesIdx + 1] !== "api") return false;
|
|
4682
|
+
const base = segs[segs.length - 1] ?? "";
|
|
4683
|
+
if (/^_(app|document|middleware)\./.test(base)) return false;
|
|
4684
|
+
return JS_ROUTE_EXTENSIONS.has(path22.extname(base));
|
|
4685
|
+
}
|
|
4686
|
+
function nextSegment(seg) {
|
|
4687
|
+
if (seg.startsWith("(") && seg.endsWith(")")) return null;
|
|
4688
|
+
const catchAll = seg.match(/^\[\[?\.\.\.(.+?)\]?\]$/);
|
|
4689
|
+
if (catchAll) return ":" + catchAll[1];
|
|
4690
|
+
const dynamic = seg.match(/^\[(.+?)\]$/);
|
|
4691
|
+
if (dynamic) return ":" + dynamic[1];
|
|
4692
|
+
return seg;
|
|
4693
|
+
}
|
|
4694
|
+
function nextAppPathTemplate(relFile) {
|
|
4695
|
+
const segs = segmentsOf(relFile);
|
|
4696
|
+
const appIdx = segs.lastIndexOf("app");
|
|
4697
|
+
const between = segs.slice(appIdx + 1, segs.length - 1);
|
|
4698
|
+
const parts = [];
|
|
4699
|
+
for (const seg of between) {
|
|
4700
|
+
const mapped = nextSegment(seg);
|
|
4701
|
+
if (mapped !== null) parts.push(mapped);
|
|
4702
|
+
}
|
|
4703
|
+
return "/" + parts.join("/");
|
|
4704
|
+
}
|
|
4705
|
+
function nextPagesApiPathTemplate(relFile) {
|
|
4706
|
+
const segs = segmentsOf(relFile);
|
|
4707
|
+
const pagesIdx = segs.indexOf("pages");
|
|
4708
|
+
const rest = segs.slice(pagesIdx + 1);
|
|
4709
|
+
const parts = [];
|
|
4710
|
+
for (let i = 0; i < rest.length; i++) {
|
|
4711
|
+
let seg = rest[i];
|
|
4712
|
+
if (i === rest.length - 1) {
|
|
4713
|
+
seg = seg.replace(/\.(?:js|jsx|mjs|cjs|ts|tsx)$/, "");
|
|
4714
|
+
if (seg === "index") continue;
|
|
4715
|
+
}
|
|
4716
|
+
const mapped = nextSegment(seg);
|
|
4717
|
+
if (mapped !== null) parts.push(mapped);
|
|
4718
|
+
}
|
|
4719
|
+
return "/" + parts.join("/");
|
|
4720
|
+
}
|
|
4721
|
+
function nextAppMethods(root) {
|
|
4722
|
+
const out = [];
|
|
4723
|
+
walk(root, (node) => {
|
|
4724
|
+
if (node.type !== "export_statement") return;
|
|
4725
|
+
const decl = node.childForFieldName("declaration");
|
|
4726
|
+
if (!decl) return;
|
|
4727
|
+
const line = node.startPosition.row + 1;
|
|
4728
|
+
if (decl.type === "function_declaration") {
|
|
4729
|
+
const name = decl.childForFieldName("name")?.text;
|
|
4730
|
+
if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
|
|
4731
|
+
return;
|
|
4732
|
+
}
|
|
4733
|
+
if (decl.type === "lexical_declaration" || decl.type === "variable_declaration") {
|
|
4734
|
+
for (let i = 0; i < decl.namedChildCount; i++) {
|
|
4735
|
+
const d = decl.namedChild(i);
|
|
4736
|
+
if (d?.type !== "variable_declarator") continue;
|
|
4737
|
+
const name = d.childForFieldName("name")?.text;
|
|
4738
|
+
if (name && NEXT_APP_METHODS.has(name)) out.push({ method: name, line });
|
|
4739
|
+
}
|
|
4740
|
+
}
|
|
4741
|
+
});
|
|
4742
|
+
return out;
|
|
4743
|
+
}
|
|
4744
|
+
function nextRoutesFromFile(source, relFile, parser) {
|
|
4745
|
+
if (isNextAppRouteFile(relFile)) {
|
|
4746
|
+
const tree = parseSource2(parser, source);
|
|
4747
|
+
const template = nextAppPathTemplate(relFile);
|
|
4748
|
+
return nextAppMethods(tree.rootNode).map(({ method, line }) => ({
|
|
4749
|
+
method,
|
|
4750
|
+
pathTemplate: canonicalizeTemplate(template),
|
|
4751
|
+
line,
|
|
4752
|
+
framework: "next"
|
|
4753
|
+
}));
|
|
4754
|
+
}
|
|
4755
|
+
if (isNextPagesApiFile(relFile)) {
|
|
4756
|
+
return [
|
|
4757
|
+
{
|
|
4758
|
+
method: "ALL",
|
|
4759
|
+
pathTemplate: canonicalizeTemplate(nextPagesApiPathTemplate(relFile)),
|
|
4760
|
+
line: 1,
|
|
4761
|
+
framework: "next"
|
|
4762
|
+
}
|
|
4763
|
+
];
|
|
4764
|
+
}
|
|
4765
|
+
return [];
|
|
4766
|
+
}
|
|
4767
|
+
async function addRoutes(graph, services) {
|
|
4768
|
+
const jsParser = makeJsParser2();
|
|
4769
|
+
let nodesAdded = 0;
|
|
4770
|
+
let edgesAdded = 0;
|
|
4771
|
+
for (const service of services) {
|
|
4772
|
+
const deps = {
|
|
4773
|
+
...service.pkg.dependencies ?? {},
|
|
4774
|
+
...service.pkg.devDependencies ?? {}
|
|
4775
|
+
};
|
|
4776
|
+
const hasExpress = deps["express"] !== void 0;
|
|
4777
|
+
const hasFastify = deps["fastify"] !== void 0;
|
|
4778
|
+
const hasNext = deps["next"] !== void 0;
|
|
4779
|
+
if (!hasExpress && !hasFastify && !hasNext) continue;
|
|
4780
|
+
const files = await loadSourceFiles(service.dir);
|
|
4781
|
+
for (const file of files) {
|
|
4782
|
+
if (isTestPath(file.path)) continue;
|
|
4783
|
+
if (!JS_ROUTE_EXTENSIONS.has(path22.extname(file.path))) continue;
|
|
4784
|
+
const relFile = toPosix2(path22.relative(service.dir, file.path));
|
|
4785
|
+
let routes;
|
|
4786
|
+
try {
|
|
4787
|
+
if (hasNext && (isNextAppRouteFile(relFile) || isNextPagesApiFile(relFile))) {
|
|
4788
|
+
routes = nextRoutesFromFile(file.content, relFile, jsParser);
|
|
4789
|
+
} else if (hasExpress || hasFastify) {
|
|
4790
|
+
routes = serverRoutesFromSource(file.content, jsParser, hasExpress, hasFastify);
|
|
4791
|
+
} else {
|
|
4792
|
+
routes = [];
|
|
4793
|
+
}
|
|
4794
|
+
} catch (err) {
|
|
4795
|
+
recordExtractionError("route extraction", file.path, err);
|
|
4796
|
+
continue;
|
|
4797
|
+
}
|
|
4798
|
+
if (routes.length === 0) continue;
|
|
4799
|
+
for (const route of routes) {
|
|
4800
|
+
const rid = routeId(service.pkg.name, route.method, route.pathTemplate);
|
|
4801
|
+
if (!graph.hasNode(rid)) {
|
|
4802
|
+
const node = {
|
|
4803
|
+
id: rid,
|
|
4804
|
+
type: NodeType9.RouteNode,
|
|
4805
|
+
name: `${route.method} ${route.pathTemplate}`,
|
|
4806
|
+
service: service.pkg.name,
|
|
4807
|
+
method: route.method,
|
|
4808
|
+
pathTemplate: route.pathTemplate,
|
|
4809
|
+
path: relFile,
|
|
4810
|
+
line: route.line,
|
|
4811
|
+
framework: route.framework,
|
|
4812
|
+
discoveredVia: "static"
|
|
4813
|
+
};
|
|
4814
|
+
graph.addNode(rid, node);
|
|
4815
|
+
nodesAdded++;
|
|
4816
|
+
}
|
|
4817
|
+
const containsId = extractedEdgeId5(service.node.id, rid, EdgeType8.CONTAINS);
|
|
4818
|
+
if (!graph.hasEdge(containsId)) {
|
|
4819
|
+
const edge = {
|
|
4820
|
+
id: containsId,
|
|
4821
|
+
source: service.node.id,
|
|
4822
|
+
target: rid,
|
|
4823
|
+
type: EdgeType8.CONTAINS,
|
|
4824
|
+
provenance: Provenance7.EXTRACTED,
|
|
4825
|
+
confidence: confidenceForExtracted5("structural"),
|
|
4826
|
+
evidence: {
|
|
4827
|
+
file: relFile,
|
|
4828
|
+
line: route.line,
|
|
4829
|
+
snippet: snippet(file.content, route.line)
|
|
4830
|
+
}
|
|
4831
|
+
};
|
|
4832
|
+
graph.addEdgeWithKey(containsId, service.node.id, rid, edge);
|
|
4833
|
+
edgesAdded++;
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
}
|
|
4837
|
+
}
|
|
4838
|
+
return { nodesAdded, edgesAdded };
|
|
4839
|
+
}
|
|
4840
|
+
|
|
4841
|
+
// src/extract/proto.ts
|
|
4842
|
+
import { promises as fs15 } from "fs";
|
|
4843
|
+
import path23 from "path";
|
|
4844
|
+
import {
|
|
4845
|
+
EdgeType as EdgeType9,
|
|
4846
|
+
NodeType as NodeType10,
|
|
4332
4847
|
Provenance as Provenance8,
|
|
4333
4848
|
confidenceForExtracted as confidenceForExtracted6,
|
|
4334
|
-
|
|
4849
|
+
extractedEdgeId as extractedEdgeId6,
|
|
4850
|
+
grpcMethodId as grpcMethodId2
|
|
4851
|
+
} from "@neat.is/types";
|
|
4852
|
+
var PROTO_EXTENSION = ".proto";
|
|
4853
|
+
function packageOf(content) {
|
|
4854
|
+
const m = content.match(/^\s*package\s+([A-Za-z_][A-Za-z0-9_.]*)\s*;/m);
|
|
4855
|
+
return m ? m[1] : null;
|
|
4856
|
+
}
|
|
4857
|
+
function lineAt(content, offset) {
|
|
4858
|
+
return content.slice(0, offset).split("\n").length;
|
|
4859
|
+
}
|
|
4860
|
+
function grpcMethodsFromProto(content, fqPackage) {
|
|
4861
|
+
const out = [];
|
|
4862
|
+
const serviceRe = /\bservice\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/g;
|
|
4863
|
+
let sm;
|
|
4864
|
+
while ((sm = serviceRe.exec(content)) !== null) {
|
|
4865
|
+
const serviceName = sm[1];
|
|
4866
|
+
const rpcService = fqPackage ? `${fqPackage}.${serviceName}` : serviceName;
|
|
4867
|
+
const bodyStart = serviceRe.lastIndex;
|
|
4868
|
+
let depth = 1;
|
|
4869
|
+
let i = bodyStart;
|
|
4870
|
+
for (; i < content.length && depth > 0; i++) {
|
|
4871
|
+
const ch = content[i];
|
|
4872
|
+
if (ch === "{") depth++;
|
|
4873
|
+
else if (ch === "}") depth--;
|
|
4874
|
+
}
|
|
4875
|
+
const body = content.slice(bodyStart, i - 1);
|
|
4876
|
+
const rpcRe = /\brpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/g;
|
|
4877
|
+
let rm;
|
|
4878
|
+
while ((rm = rpcRe.exec(body)) !== null) {
|
|
4879
|
+
const rpcMethod = rm[1];
|
|
4880
|
+
const line = lineAt(content, bodyStart + rm.index);
|
|
4881
|
+
out.push({ rpcService, rpcMethod, line });
|
|
4882
|
+
}
|
|
4883
|
+
serviceRe.lastIndex = i;
|
|
4884
|
+
}
|
|
4885
|
+
return out;
|
|
4886
|
+
}
|
|
4887
|
+
async function walkProtoFiles(dir) {
|
|
4888
|
+
const out = [];
|
|
4889
|
+
async function walk3(current) {
|
|
4890
|
+
const entries = await fs15.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
4891
|
+
for (const entry of entries) {
|
|
4892
|
+
const full = path23.join(current, entry.name);
|
|
4893
|
+
if (entry.isDirectory()) {
|
|
4894
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
4895
|
+
if (await isPythonVenvDir(full)) continue;
|
|
4896
|
+
await walk3(full);
|
|
4897
|
+
} else if (entry.isFile() && path23.extname(entry.name) === PROTO_EXTENSION) {
|
|
4898
|
+
out.push(full);
|
|
4899
|
+
}
|
|
4900
|
+
}
|
|
4901
|
+
}
|
|
4902
|
+
await walk3(dir);
|
|
4903
|
+
return out;
|
|
4904
|
+
}
|
|
4905
|
+
async function addGrpcMethods(graph, services) {
|
|
4906
|
+
let nodesAdded = 0;
|
|
4907
|
+
let edgesAdded = 0;
|
|
4908
|
+
for (const service of services) {
|
|
4909
|
+
const protoPaths = await walkProtoFiles(service.dir);
|
|
4910
|
+
for (const protoPath of protoPaths) {
|
|
4911
|
+
if (isTestPath(protoPath)) continue;
|
|
4912
|
+
const relFile = toPosix2(path23.relative(service.dir, protoPath));
|
|
4913
|
+
let content;
|
|
4914
|
+
try {
|
|
4915
|
+
content = await fs15.readFile(protoPath, "utf8");
|
|
4916
|
+
} catch (err) {
|
|
4917
|
+
recordExtractionError("proto extraction", protoPath, err);
|
|
4918
|
+
continue;
|
|
4919
|
+
}
|
|
4920
|
+
let methods;
|
|
4921
|
+
try {
|
|
4922
|
+
methods = grpcMethodsFromProto(content, packageOf(content));
|
|
4923
|
+
} catch (err) {
|
|
4924
|
+
recordExtractionError("proto extraction", protoPath, err);
|
|
4925
|
+
continue;
|
|
4926
|
+
}
|
|
4927
|
+
if (methods.length === 0) continue;
|
|
4928
|
+
for (const method of methods) {
|
|
4929
|
+
const mid = grpcMethodId2(method.rpcService, method.rpcMethod);
|
|
4930
|
+
if (!graph.hasNode(mid)) {
|
|
4931
|
+
const node = {
|
|
4932
|
+
id: mid,
|
|
4933
|
+
type: NodeType10.GrpcMethodNode,
|
|
4934
|
+
name: `${method.rpcService}/${method.rpcMethod}`,
|
|
4935
|
+
rpcService: method.rpcService,
|
|
4936
|
+
rpcMethod: method.rpcMethod,
|
|
4937
|
+
path: relFile,
|
|
4938
|
+
line: method.line,
|
|
4939
|
+
discoveredVia: "static"
|
|
4940
|
+
};
|
|
4941
|
+
graph.addNode(mid, node);
|
|
4942
|
+
nodesAdded++;
|
|
4943
|
+
}
|
|
4944
|
+
const containsId = extractedEdgeId6(service.node.id, mid, EdgeType9.CONTAINS);
|
|
4945
|
+
if (!graph.hasEdge(containsId)) {
|
|
4946
|
+
const edge = {
|
|
4947
|
+
id: containsId,
|
|
4948
|
+
source: service.node.id,
|
|
4949
|
+
target: mid,
|
|
4950
|
+
type: EdgeType9.CONTAINS,
|
|
4951
|
+
provenance: Provenance8.EXTRACTED,
|
|
4952
|
+
confidence: confidenceForExtracted6("structural"),
|
|
4953
|
+
evidence: {
|
|
4954
|
+
file: relFile,
|
|
4955
|
+
line: method.line,
|
|
4956
|
+
snippet: snippet(content, method.line)
|
|
4957
|
+
}
|
|
4958
|
+
};
|
|
4959
|
+
graph.addEdgeWithKey(containsId, service.node.id, mid, edge);
|
|
4960
|
+
edgesAdded++;
|
|
4961
|
+
}
|
|
4962
|
+
}
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
return { nodesAdded, edgesAdded };
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
// src/extract/calls/index.ts
|
|
4969
|
+
import {
|
|
4970
|
+
EdgeType as EdgeType12,
|
|
4971
|
+
NodeType as NodeType12,
|
|
4972
|
+
Provenance as Provenance11,
|
|
4973
|
+
confidenceForExtracted as confidenceForExtracted9,
|
|
4974
|
+
passesExtractedFloor as passesExtractedFloor3
|
|
4335
4975
|
} from "@neat.is/types";
|
|
4336
4976
|
|
|
4337
4977
|
// src/extract/calls/http.ts
|
|
4338
|
-
import
|
|
4339
|
-
import
|
|
4340
|
-
import
|
|
4978
|
+
import path24 from "path";
|
|
4979
|
+
import Parser3 from "tree-sitter";
|
|
4980
|
+
import JavaScript3 from "tree-sitter-javascript";
|
|
4341
4981
|
import Python2 from "tree-sitter-python";
|
|
4342
4982
|
import {
|
|
4343
|
-
EdgeType as
|
|
4344
|
-
Provenance as
|
|
4345
|
-
confidenceForExtracted as
|
|
4983
|
+
EdgeType as EdgeType10,
|
|
4984
|
+
Provenance as Provenance9,
|
|
4985
|
+
confidenceForExtracted as confidenceForExtracted7,
|
|
4346
4986
|
passesExtractedFloor
|
|
4347
4987
|
} from "@neat.is/types";
|
|
4348
4988
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
@@ -4372,14 +5012,14 @@ function collectStringLiterals(node, out) {
|
|
|
4372
5012
|
if (child) collectStringLiterals(child, out);
|
|
4373
5013
|
}
|
|
4374
5014
|
}
|
|
4375
|
-
var
|
|
4376
|
-
function
|
|
5015
|
+
var PARSE_CHUNK3 = 16384;
|
|
5016
|
+
function parseSource3(parser, source) {
|
|
4377
5017
|
return parser.parse(
|
|
4378
|
-
(index) => index >= source.length ? "" : source.slice(index, index +
|
|
5018
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK3)
|
|
4379
5019
|
);
|
|
4380
5020
|
}
|
|
4381
5021
|
function callsFromSource(source, parser, knownHosts) {
|
|
4382
|
-
const tree =
|
|
5022
|
+
const tree = parseSource3(parser, source);
|
|
4383
5023
|
const literals = [];
|
|
4384
5024
|
collectStringLiterals(tree.rootNode, literals);
|
|
4385
5025
|
const out = [];
|
|
@@ -4393,27 +5033,20 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
4393
5033
|
}
|
|
4394
5034
|
return out;
|
|
4395
5035
|
}
|
|
4396
|
-
function
|
|
4397
|
-
const p = new
|
|
4398
|
-
p.setLanguage(
|
|
5036
|
+
function makeJsParser3() {
|
|
5037
|
+
const p = new Parser3();
|
|
5038
|
+
p.setLanguage(JavaScript3);
|
|
4399
5039
|
return p;
|
|
4400
5040
|
}
|
|
4401
5041
|
function makePyParser2() {
|
|
4402
|
-
const p = new
|
|
5042
|
+
const p = new Parser3();
|
|
4403
5043
|
p.setLanguage(Python2);
|
|
4404
5044
|
return p;
|
|
4405
5045
|
}
|
|
4406
5046
|
async function addHttpCallEdges(graph, services) {
|
|
4407
|
-
const jsParser =
|
|
5047
|
+
const jsParser = makeJsParser3();
|
|
4408
5048
|
const pyParser = makePyParser2();
|
|
4409
|
-
const knownHosts
|
|
4410
|
-
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
4411
|
-
for (const service of services) {
|
|
4412
|
-
knownHosts.add(path22.basename(service.dir));
|
|
4413
|
-
knownHosts.add(service.pkg.name);
|
|
4414
|
-
hostToNodeId.set(path22.basename(service.dir), service.node.id);
|
|
4415
|
-
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
4416
|
-
}
|
|
5049
|
+
const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
|
|
4417
5050
|
let nodesAdded = 0;
|
|
4418
5051
|
let edgesAdded = 0;
|
|
4419
5052
|
for (const service of services) {
|
|
@@ -4421,7 +5054,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4421
5054
|
const seen = /* @__PURE__ */ new Set();
|
|
4422
5055
|
for (const file of files) {
|
|
4423
5056
|
if (isTestPath(file.path)) continue;
|
|
4424
|
-
const parser =
|
|
5057
|
+
const parser = path24.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
4425
5058
|
let sites;
|
|
4426
5059
|
try {
|
|
4427
5060
|
sites = callsFromSource(file.content, parser, knownHosts);
|
|
@@ -4430,14 +5063,14 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4430
5063
|
continue;
|
|
4431
5064
|
}
|
|
4432
5065
|
if (sites.length === 0) continue;
|
|
4433
|
-
const relFile = toPosix2(
|
|
5066
|
+
const relFile = toPosix2(path24.relative(service.dir, file.path));
|
|
4434
5067
|
for (const site of sites) {
|
|
4435
5068
|
const targetId = hostToNodeId.get(site.host);
|
|
4436
5069
|
if (!targetId || targetId === service.node.id) continue;
|
|
4437
5070
|
const dedupKey = `${relFile}|${targetId}`;
|
|
4438
5071
|
if (seen.has(dedupKey)) continue;
|
|
4439
5072
|
seen.add(dedupKey);
|
|
4440
|
-
const confidence =
|
|
5073
|
+
const confidence = confidenceForExtracted7("url-literal-service-target");
|
|
4441
5074
|
const ev = {
|
|
4442
5075
|
file: relFile,
|
|
4443
5076
|
line: site.line,
|
|
@@ -4455,21 +5088,21 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4455
5088
|
noteExtractedDropped({
|
|
4456
5089
|
source: fileNodeId,
|
|
4457
5090
|
target: targetId,
|
|
4458
|
-
type:
|
|
5091
|
+
type: EdgeType10.CALLS,
|
|
4459
5092
|
confidence,
|
|
4460
5093
|
confidenceKind: "url-literal-service-target",
|
|
4461
5094
|
evidence: ev
|
|
4462
5095
|
});
|
|
4463
5096
|
continue;
|
|
4464
5097
|
}
|
|
4465
|
-
const edgeId = extractedEdgeId2(fileNodeId, targetId,
|
|
5098
|
+
const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType10.CALLS);
|
|
4466
5099
|
if (!graph.hasEdge(edgeId)) {
|
|
4467
5100
|
const edge = {
|
|
4468
5101
|
id: edgeId,
|
|
4469
5102
|
source: fileNodeId,
|
|
4470
5103
|
target: targetId,
|
|
4471
|
-
type:
|
|
4472
|
-
provenance:
|
|
5104
|
+
type: EdgeType10.CALLS,
|
|
5105
|
+
provenance: Provenance9.EXTRACTED,
|
|
4473
5106
|
confidence,
|
|
4474
5107
|
evidence: ev
|
|
4475
5108
|
};
|
|
@@ -4482,9 +5115,284 @@ async function addHttpCallEdges(graph, services) {
|
|
|
4482
5115
|
return { nodesAdded, edgesAdded };
|
|
4483
5116
|
}
|
|
4484
5117
|
|
|
5118
|
+
// src/extract/calls/route-match.ts
|
|
5119
|
+
import path25 from "path";
|
|
5120
|
+
import Parser4 from "tree-sitter";
|
|
5121
|
+
import JavaScript4 from "tree-sitter-javascript";
|
|
5122
|
+
import {
|
|
5123
|
+
EdgeType as EdgeType11,
|
|
5124
|
+
NodeType as NodeType11,
|
|
5125
|
+
Provenance as Provenance10,
|
|
5126
|
+
confidenceForExtracted as confidenceForExtracted8,
|
|
5127
|
+
passesExtractedFloor as passesExtractedFloor2,
|
|
5128
|
+
serviceId as serviceId3
|
|
5129
|
+
} from "@neat.is/types";
|
|
5130
|
+
var PARSE_CHUNK4 = 16384;
|
|
5131
|
+
function parseSource4(parser, source) {
|
|
5132
|
+
return parser.parse(
|
|
5133
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK4)
|
|
5134
|
+
);
|
|
5135
|
+
}
|
|
5136
|
+
function makeJsParser4() {
|
|
5137
|
+
const p = new Parser4();
|
|
5138
|
+
p.setLanguage(JavaScript4);
|
|
5139
|
+
return p;
|
|
5140
|
+
}
|
|
5141
|
+
var JS_CLIENT_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"]);
|
|
5142
|
+
var AXIOS_METHODS = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "head", "options", "request"]);
|
|
5143
|
+
function walk2(node, visit) {
|
|
5144
|
+
visit(node);
|
|
5145
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5146
|
+
const child = node.namedChild(i);
|
|
5147
|
+
if (child) walk2(child, visit);
|
|
5148
|
+
}
|
|
5149
|
+
}
|
|
5150
|
+
function reconstructUrl(node) {
|
|
5151
|
+
if (node.type === "string") {
|
|
5152
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5153
|
+
const child = node.namedChild(i);
|
|
5154
|
+
if (child?.type === "string_fragment") return child.text;
|
|
5155
|
+
}
|
|
5156
|
+
return "";
|
|
5157
|
+
}
|
|
5158
|
+
if (node.type === "template_string") {
|
|
5159
|
+
let out = "";
|
|
5160
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5161
|
+
const child = node.namedChild(i);
|
|
5162
|
+
if (!child) continue;
|
|
5163
|
+
if (child.type === "string_fragment") out += child.text;
|
|
5164
|
+
else if (child.type === "template_substitution") out += ":param";
|
|
5165
|
+
}
|
|
5166
|
+
if (out.length === 0) {
|
|
5167
|
+
const raw = node.text;
|
|
5168
|
+
return raw.length >= 2 ? raw.slice(1, -1) : "";
|
|
5169
|
+
}
|
|
5170
|
+
return out;
|
|
5171
|
+
}
|
|
5172
|
+
return null;
|
|
5173
|
+
}
|
|
5174
|
+
function methodFromOptions(objNode) {
|
|
5175
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
5176
|
+
const pair = objNode.namedChild(i);
|
|
5177
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5178
|
+
const k = pair.childForFieldName("key");
|
|
5179
|
+
const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
|
|
5180
|
+
if (kText !== "method") continue;
|
|
5181
|
+
const v = pair.childForFieldName("value");
|
|
5182
|
+
if (v && (v.type === "string" || v.type === "template_string")) {
|
|
5183
|
+
const s = stringText(v);
|
|
5184
|
+
return s ? s.toUpperCase() : void 0;
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
return void 0;
|
|
5188
|
+
}
|
|
5189
|
+
function stringText(node) {
|
|
5190
|
+
if (node.type === "string") {
|
|
5191
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
5192
|
+
const child = node.namedChild(i);
|
|
5193
|
+
if (child?.type === "string_fragment") return child.text;
|
|
5194
|
+
}
|
|
5195
|
+
return "";
|
|
5196
|
+
}
|
|
5197
|
+
return null;
|
|
5198
|
+
}
|
|
5199
|
+
function urlNodeFromConfig(objNode) {
|
|
5200
|
+
for (let i = 0; i < objNode.namedChildCount; i++) {
|
|
5201
|
+
const pair = objNode.namedChild(i);
|
|
5202
|
+
if (!pair || pair.type !== "pair") continue;
|
|
5203
|
+
const k = pair.childForFieldName("key");
|
|
5204
|
+
const kText = k ? k.type === "string" ? stringText(k) : k.text : null;
|
|
5205
|
+
if (kText === "url") return pair.childForFieldName("value");
|
|
5206
|
+
}
|
|
5207
|
+
return null;
|
|
5208
|
+
}
|
|
5209
|
+
function pathOf(urlStr) {
|
|
5210
|
+
try {
|
|
5211
|
+
const candidate = urlStr.startsWith("//") ? `http:${urlStr}` : urlStr;
|
|
5212
|
+
const parsed = new URL(candidate);
|
|
5213
|
+
return parsed.pathname || "/";
|
|
5214
|
+
} catch {
|
|
5215
|
+
return null;
|
|
5216
|
+
}
|
|
5217
|
+
}
|
|
5218
|
+
function matchHost(urlStr, knownHosts) {
|
|
5219
|
+
for (const host of knownHosts) {
|
|
5220
|
+
if (urlMatchesHost(urlStr, host)) return host;
|
|
5221
|
+
}
|
|
5222
|
+
return null;
|
|
5223
|
+
}
|
|
5224
|
+
function clientCallSitesFromSource(source, parser, knownHosts) {
|
|
5225
|
+
const tree = parseSource4(parser, source);
|
|
5226
|
+
const out = [];
|
|
5227
|
+
const push = (urlNode, method, callNode) => {
|
|
5228
|
+
const urlStr = reconstructUrl(urlNode);
|
|
5229
|
+
if (!urlStr) return;
|
|
5230
|
+
const host = matchHost(urlStr, knownHosts);
|
|
5231
|
+
if (!host) return;
|
|
5232
|
+
const p = pathOf(urlStr);
|
|
5233
|
+
if (p === null) return;
|
|
5234
|
+
const line = callNode.startPosition.row + 1;
|
|
5235
|
+
out.push({
|
|
5236
|
+
host,
|
|
5237
|
+
method,
|
|
5238
|
+
pathTemplate: p,
|
|
5239
|
+
line,
|
|
5240
|
+
snippet: snippet(source, line)
|
|
5241
|
+
});
|
|
5242
|
+
};
|
|
5243
|
+
walk2(tree.rootNode, (node) => {
|
|
5244
|
+
if (node.type !== "call_expression") return;
|
|
5245
|
+
const fn = node.childForFieldName("function");
|
|
5246
|
+
if (!fn) return;
|
|
5247
|
+
const args = node.childForFieldName("arguments");
|
|
5248
|
+
const first = args?.namedChild(0);
|
|
5249
|
+
if (!first) return;
|
|
5250
|
+
const fnName = fn.type === "identifier" ? fn.text : fn.type === "member_expression" ? fn.childForFieldName("property")?.text ?? "" : "";
|
|
5251
|
+
if (fn.type === "identifier" && fnName === "fetch") {
|
|
5252
|
+
const opts = args?.namedChild(1);
|
|
5253
|
+
const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
|
|
5254
|
+
push(first, method, node);
|
|
5255
|
+
return;
|
|
5256
|
+
}
|
|
5257
|
+
if (fn.type === "identifier" && fnName === "axios") {
|
|
5258
|
+
if (first.type === "object") {
|
|
5259
|
+
const urlNode = urlNodeFromConfig(first);
|
|
5260
|
+
if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
|
|
5261
|
+
} else {
|
|
5262
|
+
const opts = args?.namedChild(1);
|
|
5263
|
+
const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? "GET" : "GET";
|
|
5264
|
+
push(first, method, node);
|
|
5265
|
+
}
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
if (fn.type === "member_expression") {
|
|
5269
|
+
const obj = fn.childForFieldName("object");
|
|
5270
|
+
const objName = obj?.text ?? "";
|
|
5271
|
+
if (objName === "axios" && AXIOS_METHODS.has(fnName)) {
|
|
5272
|
+
if (fnName === "request" && first.type === "object") {
|
|
5273
|
+
const urlNode = urlNodeFromConfig(first);
|
|
5274
|
+
if (urlNode) push(urlNode, methodFromOptions(first) ?? "GET", node);
|
|
5275
|
+
} else {
|
|
5276
|
+
push(first, fnName.toUpperCase(), node);
|
|
5277
|
+
}
|
|
5278
|
+
return;
|
|
5279
|
+
}
|
|
5280
|
+
if ((objName === "http" || objName === "https") && (fnName === "request" || fnName === "get")) {
|
|
5281
|
+
const opts = args?.namedChild(1);
|
|
5282
|
+
const method = opts && opts.type === "object" ? methodFromOptions(opts) ?? (fnName === "get" ? "GET" : "GET") : "GET";
|
|
5283
|
+
push(first, method, node);
|
|
5284
|
+
return;
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
});
|
|
5288
|
+
return out;
|
|
5289
|
+
}
|
|
5290
|
+
function buildRouteIndex(graph) {
|
|
5291
|
+
const index = /* @__PURE__ */ new Map();
|
|
5292
|
+
graph.forEachNode((_id, attrs) => {
|
|
5293
|
+
const node = attrs;
|
|
5294
|
+
if (node.type !== NodeType11.RouteNode) return;
|
|
5295
|
+
const route = attrs;
|
|
5296
|
+
const owner = serviceId3(route.service);
|
|
5297
|
+
const entry = {
|
|
5298
|
+
method: route.method.toUpperCase(),
|
|
5299
|
+
normalizedPath: normalizePathTemplate(route.pathTemplate),
|
|
5300
|
+
routeNodeId: route.id
|
|
5301
|
+
};
|
|
5302
|
+
const list = index.get(owner);
|
|
5303
|
+
if (list) list.push(entry);
|
|
5304
|
+
else index.set(owner, [entry]);
|
|
5305
|
+
});
|
|
5306
|
+
return index;
|
|
5307
|
+
}
|
|
5308
|
+
function findRoute(entries, method, normalizedPath) {
|
|
5309
|
+
return entries.find(
|
|
5310
|
+
(e) => e.normalizedPath === normalizedPath && (e.method === "ALL" || method === void 0 || e.method === method)
|
|
5311
|
+
);
|
|
5312
|
+
}
|
|
5313
|
+
async function addRouteCallEdges(graph, services) {
|
|
5314
|
+
const jsParser = makeJsParser4();
|
|
5315
|
+
const { knownHosts, hostToNodeId } = buildServiceHostIndex(services);
|
|
5316
|
+
const routeIndex = buildRouteIndex(graph);
|
|
5317
|
+
if (routeIndex.size === 0) return { nodesAdded: 0, edgesAdded: 0 };
|
|
5318
|
+
let nodesAdded = 0;
|
|
5319
|
+
let edgesAdded = 0;
|
|
5320
|
+
for (const service of services) {
|
|
5321
|
+
const files = await loadSourceFiles(service.dir);
|
|
5322
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5323
|
+
for (const file of files) {
|
|
5324
|
+
if (isTestPath(file.path)) continue;
|
|
5325
|
+
if (!JS_CLIENT_EXTENSIONS.has(path25.extname(file.path))) continue;
|
|
5326
|
+
let sites;
|
|
5327
|
+
try {
|
|
5328
|
+
sites = clientCallSitesFromSource(file.content, jsParser, knownHosts);
|
|
5329
|
+
} catch (err) {
|
|
5330
|
+
recordExtractionError("route-match call extraction", file.path, err);
|
|
5331
|
+
continue;
|
|
5332
|
+
}
|
|
5333
|
+
if (sites.length === 0) continue;
|
|
5334
|
+
const relFile = toPosix2(path25.relative(service.dir, file.path));
|
|
5335
|
+
for (const site of sites) {
|
|
5336
|
+
const serverServiceId = hostToNodeId.get(site.host);
|
|
5337
|
+
if (!serverServiceId || serverServiceId === service.node.id) continue;
|
|
5338
|
+
const entries = routeIndex.get(serverServiceId);
|
|
5339
|
+
if (!entries) continue;
|
|
5340
|
+
const normalizedPath = normalizePathTemplate(site.pathTemplate);
|
|
5341
|
+
const match = findRoute(entries, site.method, normalizedPath);
|
|
5342
|
+
if (!match) continue;
|
|
5343
|
+
const dedupKey = `${relFile}|${match.routeNodeId}`;
|
|
5344
|
+
if (seen.has(dedupKey)) continue;
|
|
5345
|
+
seen.add(dedupKey);
|
|
5346
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
5347
|
+
graph,
|
|
5348
|
+
service.pkg.name,
|
|
5349
|
+
service.node.id,
|
|
5350
|
+
relFile
|
|
5351
|
+
);
|
|
5352
|
+
nodesAdded += n;
|
|
5353
|
+
edgesAdded += e;
|
|
5354
|
+
const confidence = confidenceForExtracted8("verified-call-site");
|
|
5355
|
+
const ev = {
|
|
5356
|
+
file: relFile,
|
|
5357
|
+
line: site.line,
|
|
5358
|
+
snippet: site.snippet,
|
|
5359
|
+
method: site.method ?? match.method,
|
|
5360
|
+
pathTemplate: site.pathTemplate
|
|
5361
|
+
};
|
|
5362
|
+
if (!passesExtractedFloor2(confidence)) {
|
|
5363
|
+
noteExtractedDropped({
|
|
5364
|
+
source: fileNodeId,
|
|
5365
|
+
target: match.routeNodeId,
|
|
5366
|
+
type: EdgeType11.CALLS,
|
|
5367
|
+
confidence,
|
|
5368
|
+
confidenceKind: "verified-call-site",
|
|
5369
|
+
evidence: ev
|
|
5370
|
+
});
|
|
5371
|
+
continue;
|
|
5372
|
+
}
|
|
5373
|
+
const edgeId = extractedEdgeId2(fileNodeId, match.routeNodeId, EdgeType11.CALLS);
|
|
5374
|
+
if (!graph.hasEdge(edgeId)) {
|
|
5375
|
+
const edge = {
|
|
5376
|
+
id: edgeId,
|
|
5377
|
+
source: fileNodeId,
|
|
5378
|
+
target: match.routeNodeId,
|
|
5379
|
+
type: EdgeType11.CALLS,
|
|
5380
|
+
provenance: Provenance10.EXTRACTED,
|
|
5381
|
+
confidence,
|
|
5382
|
+
evidence: ev
|
|
5383
|
+
};
|
|
5384
|
+
graph.addEdgeWithKey(edgeId, fileNodeId, match.routeNodeId, edge);
|
|
5385
|
+
edgesAdded++;
|
|
5386
|
+
}
|
|
5387
|
+
}
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
return { nodesAdded, edgesAdded };
|
|
5391
|
+
}
|
|
5392
|
+
|
|
4485
5393
|
// src/extract/calls/kafka.ts
|
|
4486
|
-
import
|
|
4487
|
-
import { infraId } from "@neat.is/types";
|
|
5394
|
+
import path26 from "path";
|
|
5395
|
+
import { infraId as infraId2 } from "@neat.is/types";
|
|
4488
5396
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
4489
5397
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
4490
5398
|
function findAll(re, text) {
|
|
@@ -4505,7 +5413,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
4505
5413
|
seen.add(key);
|
|
4506
5414
|
const line = lineOf(file.content, topic);
|
|
4507
5415
|
out.push({
|
|
4508
|
-
infraId:
|
|
5416
|
+
infraId: infraId2("kafka-topic", topic),
|
|
4509
5417
|
name: topic,
|
|
4510
5418
|
kind: "kafka-topic",
|
|
4511
5419
|
edgeType,
|
|
@@ -4514,7 +5422,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
4514
5422
|
// tier (ADR-066).
|
|
4515
5423
|
confidenceKind: "verified-call-site",
|
|
4516
5424
|
evidence: {
|
|
4517
|
-
file:
|
|
5425
|
+
file: path26.relative(serviceDir, file.path),
|
|
4518
5426
|
line,
|
|
4519
5427
|
snippet: snippet(file.content, line)
|
|
4520
5428
|
}
|
|
@@ -4526,8 +5434,8 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
4526
5434
|
}
|
|
4527
5435
|
|
|
4528
5436
|
// src/extract/calls/redis.ts
|
|
4529
|
-
import
|
|
4530
|
-
import { infraId as
|
|
5437
|
+
import path27 from "path";
|
|
5438
|
+
import { infraId as infraId3 } from "@neat.is/types";
|
|
4531
5439
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
4532
5440
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
4533
5441
|
const out = [];
|
|
@@ -4540,7 +5448,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
4540
5448
|
seen.add(host);
|
|
4541
5449
|
const line = lineOf(file.content, host);
|
|
4542
5450
|
out.push({
|
|
4543
|
-
infraId:
|
|
5451
|
+
infraId: infraId3("redis", host),
|
|
4544
5452
|
name: host,
|
|
4545
5453
|
kind: "redis",
|
|
4546
5454
|
edgeType: "CALLS",
|
|
@@ -4549,7 +5457,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
4549
5457
|
// support tier (ADR-066).
|
|
4550
5458
|
confidenceKind: "url-with-structural-support",
|
|
4551
5459
|
evidence: {
|
|
4552
|
-
file:
|
|
5460
|
+
file: path27.relative(serviceDir, file.path),
|
|
4553
5461
|
line,
|
|
4554
5462
|
snippet: snippet(file.content, line)
|
|
4555
5463
|
}
|
|
@@ -4559,8 +5467,8 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
4559
5467
|
}
|
|
4560
5468
|
|
|
4561
5469
|
// src/extract/calls/aws.ts
|
|
4562
|
-
import
|
|
4563
|
-
import { infraId as
|
|
5470
|
+
import path28 from "path";
|
|
5471
|
+
import { infraId as infraId4 } from "@neat.is/types";
|
|
4564
5472
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
4565
5473
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
4566
5474
|
function hasMarker(text, markers) {
|
|
@@ -4584,7 +5492,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
4584
5492
|
seen.add(key);
|
|
4585
5493
|
const line = lineOf(file.content, name);
|
|
4586
5494
|
out.push({
|
|
4587
|
-
infraId:
|
|
5495
|
+
infraId: infraId4(kind, name),
|
|
4588
5496
|
name,
|
|
4589
5497
|
kind,
|
|
4590
5498
|
edgeType: "CALLS",
|
|
@@ -4593,7 +5501,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
4593
5501
|
// (ADR-066).
|
|
4594
5502
|
confidenceKind: "verified-call-site",
|
|
4595
5503
|
evidence: {
|
|
4596
|
-
file:
|
|
5504
|
+
file: path28.relative(serviceDir, file.path),
|
|
4597
5505
|
line,
|
|
4598
5506
|
snippet: snippet(file.content, line)
|
|
4599
5507
|
}
|
|
@@ -4617,8 +5525,8 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
4617
5525
|
}
|
|
4618
5526
|
|
|
4619
5527
|
// src/extract/calls/grpc.ts
|
|
4620
|
-
import
|
|
4621
|
-
import { infraId as
|
|
5528
|
+
import path29 from "path";
|
|
5529
|
+
import { infraId as infraId5 } from "@neat.is/types";
|
|
4622
5530
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
4623
5531
|
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
4624
5532
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
@@ -4667,7 +5575,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
4667
5575
|
const { kind } = classified;
|
|
4668
5576
|
const line = lineOf(file.content, m[0]);
|
|
4669
5577
|
out.push({
|
|
4670
|
-
infraId:
|
|
5578
|
+
infraId: infraId5(kind, name),
|
|
4671
5579
|
name,
|
|
4672
5580
|
kind,
|
|
4673
5581
|
edgeType: "CALLS",
|
|
@@ -4676,7 +5584,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
4676
5584
|
// tier (ADR-066).
|
|
4677
5585
|
confidenceKind: "verified-call-site",
|
|
4678
5586
|
evidence: {
|
|
4679
|
-
file:
|
|
5587
|
+
file: path29.relative(serviceDir, file.path),
|
|
4680
5588
|
line,
|
|
4681
5589
|
snippet: snippet(file.content, line)
|
|
4682
5590
|
}
|
|
@@ -4686,8 +5594,8 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
4686
5594
|
}
|
|
4687
5595
|
|
|
4688
5596
|
// src/extract/calls/supabase.ts
|
|
4689
|
-
import
|
|
4690
|
-
import { infraId as
|
|
5597
|
+
import path30 from "path";
|
|
5598
|
+
import { infraId as infraId6 } from "@neat.is/types";
|
|
4691
5599
|
var SUPABASE_JS_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/supabase-js['"`]/;
|
|
4692
5600
|
var SUPABASE_SSR_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@supabase\/ssr['"`]/;
|
|
4693
5601
|
var SUPABASE_CLIENT_RE = /\b(createClient|createServerClient|createBrowserClient)\s*\(\s*(?:['"`]([^'"`]*)['"`])?/g;
|
|
@@ -4725,7 +5633,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
4725
5633
|
seen.add(name);
|
|
4726
5634
|
const line = lineOf(file.content, m[0]);
|
|
4727
5635
|
out.push({
|
|
4728
|
-
infraId:
|
|
5636
|
+
infraId: infraId6("supabase", name),
|
|
4729
5637
|
name,
|
|
4730
5638
|
kind: "supabase",
|
|
4731
5639
|
edgeType: "CALLS",
|
|
@@ -4735,7 +5643,7 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
4735
5643
|
// tier (ADR-066), the same grade aws.ts / grpc.ts emit at.
|
|
4736
5644
|
confidenceKind: "verified-call-site",
|
|
4737
5645
|
evidence: {
|
|
4738
|
-
file:
|
|
5646
|
+
file: path30.relative(serviceDir, file.path),
|
|
4739
5647
|
line,
|
|
4740
5648
|
snippet: snippet(file.content, line)
|
|
4741
5649
|
}
|
|
@@ -4748,11 +5656,11 @@ function supabaseEndpointsFromFile(file, serviceDir) {
|
|
|
4748
5656
|
function edgeTypeFromEndpoint(ep) {
|
|
4749
5657
|
switch (ep.edgeType) {
|
|
4750
5658
|
case "PUBLISHES_TO":
|
|
4751
|
-
return
|
|
5659
|
+
return EdgeType12.PUBLISHES_TO;
|
|
4752
5660
|
case "CONSUMES_FROM":
|
|
4753
|
-
return
|
|
5661
|
+
return EdgeType12.CONSUMES_FROM;
|
|
4754
5662
|
default:
|
|
4755
|
-
return
|
|
5663
|
+
return EdgeType12.CALLS;
|
|
4756
5664
|
}
|
|
4757
5665
|
}
|
|
4758
5666
|
function isAwsKind(kind) {
|
|
@@ -4780,7 +5688,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4780
5688
|
if (!graph.hasNode(ep.infraId)) {
|
|
4781
5689
|
const node = {
|
|
4782
5690
|
id: ep.infraId,
|
|
4783
|
-
type:
|
|
5691
|
+
type: NodeType12.InfraNode,
|
|
4784
5692
|
name: ep.name,
|
|
4785
5693
|
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
4786
5694
|
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
@@ -4792,7 +5700,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4792
5700
|
nodesAdded++;
|
|
4793
5701
|
}
|
|
4794
5702
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
4795
|
-
const confidence =
|
|
5703
|
+
const confidence = confidenceForExtracted9(ep.confidenceKind);
|
|
4796
5704
|
const relFile = toPosix2(ep.evidence.file);
|
|
4797
5705
|
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
4798
5706
|
graph,
|
|
@@ -4802,7 +5710,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4802
5710
|
);
|
|
4803
5711
|
nodesAdded += n;
|
|
4804
5712
|
edgesAdded += e;
|
|
4805
|
-
if (!
|
|
5713
|
+
if (!passesExtractedFloor3(confidence)) {
|
|
4806
5714
|
noteExtractedDropped({
|
|
4807
5715
|
source: fileNodeId,
|
|
4808
5716
|
target: ep.infraId,
|
|
@@ -4822,7 +5730,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4822
5730
|
source: fileNodeId,
|
|
4823
5731
|
target: ep.infraId,
|
|
4824
5732
|
type: edgeType,
|
|
4825
|
-
provenance:
|
|
5733
|
+
provenance: Provenance11.EXTRACTED,
|
|
4826
5734
|
confidence,
|
|
4827
5735
|
evidence: ep.evidence
|
|
4828
5736
|
};
|
|
@@ -4836,22 +5744,23 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4836
5744
|
async function addCallEdges(graph, services) {
|
|
4837
5745
|
const http = await addHttpCallEdges(graph, services);
|
|
4838
5746
|
const ext = await addExternalEndpointEdges(graph, services);
|
|
5747
|
+
const routes = await addRouteCallEdges(graph, services);
|
|
4839
5748
|
return {
|
|
4840
|
-
nodesAdded: http.nodesAdded + ext.nodesAdded,
|
|
4841
|
-
edgesAdded: http.edgesAdded + ext.edgesAdded
|
|
5749
|
+
nodesAdded: http.nodesAdded + ext.nodesAdded + routes.nodesAdded,
|
|
5750
|
+
edgesAdded: http.edgesAdded + ext.edgesAdded + routes.edgesAdded
|
|
4842
5751
|
};
|
|
4843
5752
|
}
|
|
4844
5753
|
|
|
4845
5754
|
// src/extract/infra/docker-compose.ts
|
|
4846
|
-
import
|
|
4847
|
-
import { EdgeType as
|
|
5755
|
+
import path31 from "path";
|
|
5756
|
+
import { EdgeType as EdgeType13, Provenance as Provenance12, confidenceForExtracted as confidenceForExtracted10 } from "@neat.is/types";
|
|
4848
5757
|
|
|
4849
5758
|
// src/extract/infra/shared.ts
|
|
4850
|
-
import { NodeType as
|
|
5759
|
+
import { NodeType as NodeType13, infraId as infraId7 } from "@neat.is/types";
|
|
4851
5760
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
4852
5761
|
return {
|
|
4853
|
-
id:
|
|
4854
|
-
type:
|
|
5762
|
+
id: infraId7(kind, name),
|
|
5763
|
+
type: NodeType13.InfraNode,
|
|
4855
5764
|
name,
|
|
4856
5765
|
provider,
|
|
4857
5766
|
kind,
|
|
@@ -4880,7 +5789,7 @@ function dependsOnList(value) {
|
|
|
4880
5789
|
}
|
|
4881
5790
|
function serviceNameToServiceNode(name, services) {
|
|
4882
5791
|
for (const s of services) {
|
|
4883
|
-
if (s.node.name === name ||
|
|
5792
|
+
if (s.node.name === name || path31.basename(s.dir) === name) return s.node.id;
|
|
4884
5793
|
}
|
|
4885
5794
|
return null;
|
|
4886
5795
|
}
|
|
@@ -4889,7 +5798,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4889
5798
|
let edgesAdded = 0;
|
|
4890
5799
|
let composePath = null;
|
|
4891
5800
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
4892
|
-
const abs =
|
|
5801
|
+
const abs = path31.join(scanPath, name);
|
|
4893
5802
|
if (await exists(abs)) {
|
|
4894
5803
|
composePath = abs;
|
|
4895
5804
|
break;
|
|
@@ -4902,13 +5811,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4902
5811
|
} catch (err) {
|
|
4903
5812
|
recordExtractionError(
|
|
4904
5813
|
"infra docker-compose",
|
|
4905
|
-
|
|
5814
|
+
path31.relative(scanPath, composePath),
|
|
4906
5815
|
err
|
|
4907
5816
|
);
|
|
4908
5817
|
return { nodesAdded, edgesAdded };
|
|
4909
5818
|
}
|
|
4910
5819
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
4911
|
-
const evidenceFile =
|
|
5820
|
+
const evidenceFile = path31.relative(scanPath, composePath).split(path31.sep).join("/");
|
|
4912
5821
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
4913
5822
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
4914
5823
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -4930,15 +5839,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4930
5839
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
4931
5840
|
const targetId = composeNameToNodeId.get(dep);
|
|
4932
5841
|
if (!targetId) continue;
|
|
4933
|
-
const edgeId = extractedEdgeId2(sourceId, targetId,
|
|
5842
|
+
const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType13.DEPENDS_ON);
|
|
4934
5843
|
if (graph.hasEdge(edgeId)) continue;
|
|
4935
5844
|
const edge = {
|
|
4936
5845
|
id: edgeId,
|
|
4937
5846
|
source: sourceId,
|
|
4938
5847
|
target: targetId,
|
|
4939
|
-
type:
|
|
4940
|
-
provenance:
|
|
4941
|
-
confidence:
|
|
5848
|
+
type: EdgeType13.DEPENDS_ON,
|
|
5849
|
+
provenance: Provenance12.EXTRACTED,
|
|
5850
|
+
confidence: confidenceForExtracted10("structural"),
|
|
4942
5851
|
evidence: { file: evidenceFile }
|
|
4943
5852
|
};
|
|
4944
5853
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4949,9 +5858,9 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4949
5858
|
}
|
|
4950
5859
|
|
|
4951
5860
|
// src/extract/infra/dockerfile.ts
|
|
4952
|
-
import
|
|
4953
|
-
import { promises as
|
|
4954
|
-
import { EdgeType as
|
|
5861
|
+
import path32 from "path";
|
|
5862
|
+
import { promises as fs16 } from "fs";
|
|
5863
|
+
import { EdgeType as EdgeType14, Provenance as Provenance13, confidenceForExtracted as confidenceForExtracted11 } from "@neat.is/types";
|
|
4955
5864
|
function readDockerfile(content) {
|
|
4956
5865
|
let image = null;
|
|
4957
5866
|
const ports = [];
|
|
@@ -4980,15 +5889,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4980
5889
|
let nodesAdded = 0;
|
|
4981
5890
|
let edgesAdded = 0;
|
|
4982
5891
|
for (const service of services) {
|
|
4983
|
-
const dockerfilePath =
|
|
5892
|
+
const dockerfilePath = path32.join(service.dir, "Dockerfile");
|
|
4984
5893
|
if (!await exists(dockerfilePath)) continue;
|
|
4985
5894
|
let content;
|
|
4986
5895
|
try {
|
|
4987
|
-
content = await
|
|
5896
|
+
content = await fs16.readFile(dockerfilePath, "utf8");
|
|
4988
5897
|
} catch (err) {
|
|
4989
5898
|
recordExtractionError(
|
|
4990
5899
|
"infra dockerfile",
|
|
4991
|
-
|
|
5900
|
+
path32.relative(scanPath, dockerfilePath),
|
|
4992
5901
|
err
|
|
4993
5902
|
);
|
|
4994
5903
|
continue;
|
|
@@ -5000,8 +5909,8 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5000
5909
|
graph.addNode(node.id, node);
|
|
5001
5910
|
nodesAdded++;
|
|
5002
5911
|
}
|
|
5003
|
-
const relDockerfile = toPosix2(
|
|
5004
|
-
const evidenceFile = toPosix2(
|
|
5912
|
+
const relDockerfile = toPosix2(path32.relative(service.dir, dockerfilePath));
|
|
5913
|
+
const evidenceFile = toPosix2(path32.relative(scanPath, dockerfilePath));
|
|
5005
5914
|
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
5006
5915
|
graph,
|
|
5007
5916
|
service.pkg.name,
|
|
@@ -5010,15 +5919,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5010
5919
|
);
|
|
5011
5920
|
nodesAdded += fn;
|
|
5012
5921
|
edgesAdded += fe;
|
|
5013
|
-
const edgeId = extractedEdgeId2(fileNodeId, node.id,
|
|
5922
|
+
const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType14.RUNS_ON);
|
|
5014
5923
|
if (!graph.hasEdge(edgeId)) {
|
|
5015
5924
|
const edge = {
|
|
5016
5925
|
id: edgeId,
|
|
5017
5926
|
source: fileNodeId,
|
|
5018
5927
|
target: node.id,
|
|
5019
|
-
type:
|
|
5020
|
-
provenance:
|
|
5021
|
-
confidence:
|
|
5928
|
+
type: EdgeType14.RUNS_ON,
|
|
5929
|
+
provenance: Provenance13.EXTRACTED,
|
|
5930
|
+
confidence: confidenceForExtracted11("structural"),
|
|
5022
5931
|
evidence: {
|
|
5023
5932
|
file: evidenceFile,
|
|
5024
5933
|
...facts.entrypoint ? { snippet: facts.entrypoint.slice(0, 120) } : {}
|
|
@@ -5033,15 +5942,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5033
5942
|
graph.addNode(portNode.id, portNode);
|
|
5034
5943
|
nodesAdded++;
|
|
5035
5944
|
}
|
|
5036
|
-
const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id,
|
|
5945
|
+
const portEdgeId = extractedEdgeId2(fileNodeId, portNode.id, EdgeType14.CONNECTS_TO);
|
|
5037
5946
|
if (graph.hasEdge(portEdgeId)) continue;
|
|
5038
5947
|
const portEdge = {
|
|
5039
5948
|
id: portEdgeId,
|
|
5040
5949
|
source: fileNodeId,
|
|
5041
5950
|
target: portNode.id,
|
|
5042
|
-
type:
|
|
5043
|
-
provenance:
|
|
5044
|
-
confidence:
|
|
5951
|
+
type: EdgeType14.CONNECTS_TO,
|
|
5952
|
+
provenance: Provenance13.EXTRACTED,
|
|
5953
|
+
confidence: confidenceForExtracted11("structural"),
|
|
5045
5954
|
evidence: { file: evidenceFile, snippet: `EXPOSE ${port}` }
|
|
5046
5955
|
};
|
|
5047
5956
|
graph.addEdgeWithKey(portEdgeId, portEdge.source, portEdge.target, portEdge);
|
|
@@ -5052,23 +5961,23 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
5052
5961
|
}
|
|
5053
5962
|
|
|
5054
5963
|
// src/extract/infra/terraform.ts
|
|
5055
|
-
import { promises as
|
|
5056
|
-
import
|
|
5057
|
-
import { EdgeType as
|
|
5964
|
+
import { promises as fs17 } from "fs";
|
|
5965
|
+
import path33 from "path";
|
|
5966
|
+
import { EdgeType as EdgeType15, Provenance as Provenance14, confidenceForExtracted as confidenceForExtracted12 } from "@neat.is/types";
|
|
5058
5967
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
5059
5968
|
var REFERENCE_RE = /(?<![\w.])(aws_[A-Za-z0-9_]+)\.([A-Za-z0-9_-]+)/g;
|
|
5060
5969
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
5061
5970
|
if (depth > max) return [];
|
|
5062
5971
|
const out = [];
|
|
5063
|
-
const entries = await
|
|
5972
|
+
const entries = await fs17.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
5064
5973
|
for (const entry of entries) {
|
|
5065
5974
|
if (entry.isDirectory()) {
|
|
5066
5975
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
5067
|
-
const child =
|
|
5976
|
+
const child = path33.join(start, entry.name);
|
|
5068
5977
|
if (await isPythonVenvDir(child)) continue;
|
|
5069
5978
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
5070
5979
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
5071
|
-
out.push(
|
|
5980
|
+
out.push(path33.join(start, entry.name));
|
|
5072
5981
|
}
|
|
5073
5982
|
}
|
|
5074
5983
|
return out;
|
|
@@ -5087,7 +5996,7 @@ function blockBody(content, from) {
|
|
|
5087
5996
|
}
|
|
5088
5997
|
return null;
|
|
5089
5998
|
}
|
|
5090
|
-
function
|
|
5999
|
+
function lineAt2(content, index) {
|
|
5091
6000
|
let line = 1;
|
|
5092
6001
|
for (let i = 0; i < index && i < content.length; i++) {
|
|
5093
6002
|
if (content[i] === "\n") line++;
|
|
@@ -5099,8 +6008,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
5099
6008
|
let edgesAdded = 0;
|
|
5100
6009
|
const files = await walkTfFiles(scanPath);
|
|
5101
6010
|
for (const file of files) {
|
|
5102
|
-
const content = await
|
|
5103
|
-
const evidenceFile = toPosix2(
|
|
6011
|
+
const content = await fs17.readFile(file, "utf8");
|
|
6012
|
+
const evidenceFile = toPosix2(path33.relative(scanPath, file));
|
|
5104
6013
|
const resources = [];
|
|
5105
6014
|
const byKey = /* @__PURE__ */ new Map();
|
|
5106
6015
|
RESOURCE_RE.lastIndex = 0;
|
|
@@ -5135,16 +6044,16 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
5135
6044
|
if (!target) continue;
|
|
5136
6045
|
if (seen.has(target.nodeId)) continue;
|
|
5137
6046
|
seen.add(target.nodeId);
|
|
5138
|
-
const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId,
|
|
6047
|
+
const edgeId = extractedEdgeId2(resource.nodeId, target.nodeId, EdgeType15.DEPENDS_ON);
|
|
5139
6048
|
if (graph.hasEdge(edgeId)) continue;
|
|
5140
|
-
const line =
|
|
6049
|
+
const line = lineAt2(content, resource.bodyOffset + ref.index);
|
|
5141
6050
|
const edge = {
|
|
5142
6051
|
id: edgeId,
|
|
5143
6052
|
source: resource.nodeId,
|
|
5144
6053
|
target: target.nodeId,
|
|
5145
|
-
type:
|
|
5146
|
-
provenance:
|
|
5147
|
-
confidence:
|
|
6054
|
+
type: EdgeType15.DEPENDS_ON,
|
|
6055
|
+
provenance: Provenance14.EXTRACTED,
|
|
6056
|
+
confidence: confidenceForExtracted12("structural"),
|
|
5148
6057
|
evidence: { file: evidenceFile, line, snippet: key }
|
|
5149
6058
|
};
|
|
5150
6059
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -5156,8 +6065,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
5156
6065
|
}
|
|
5157
6066
|
|
|
5158
6067
|
// src/extract/infra/k8s.ts
|
|
5159
|
-
import { promises as
|
|
5160
|
-
import
|
|
6068
|
+
import { promises as fs18 } from "fs";
|
|
6069
|
+
import path34 from "path";
|
|
5161
6070
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
5162
6071
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
5163
6072
|
Service: "k8s-service",
|
|
@@ -5171,15 +6080,15 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
5171
6080
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
5172
6081
|
if (depth > max) return [];
|
|
5173
6082
|
const out = [];
|
|
5174
|
-
const entries = await
|
|
6083
|
+
const entries = await fs18.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
5175
6084
|
for (const entry of entries) {
|
|
5176
6085
|
if (entry.isDirectory()) {
|
|
5177
6086
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
5178
|
-
const child =
|
|
6087
|
+
const child = path34.join(start, entry.name);
|
|
5179
6088
|
if (await isPythonVenvDir(child)) continue;
|
|
5180
6089
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
5181
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
5182
|
-
out.push(
|
|
6090
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path34.extname(entry.name))) {
|
|
6091
|
+
out.push(path34.join(start, entry.name));
|
|
5183
6092
|
}
|
|
5184
6093
|
}
|
|
5185
6094
|
return out;
|
|
@@ -5188,7 +6097,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
5188
6097
|
let nodesAdded = 0;
|
|
5189
6098
|
const files = await walkYamlFiles2(scanPath);
|
|
5190
6099
|
for (const file of files) {
|
|
5191
|
-
const content = await
|
|
6100
|
+
const content = await fs18.readFile(file, "utf8");
|
|
5192
6101
|
let docs;
|
|
5193
6102
|
try {
|
|
5194
6103
|
docs = parseAllDocuments2(content).map((d) => d.toJSON());
|
|
@@ -5223,16 +6132,16 @@ async function addInfra(graph, scanPath, services) {
|
|
|
5223
6132
|
}
|
|
5224
6133
|
|
|
5225
6134
|
// src/extract/index.ts
|
|
5226
|
-
import
|
|
6135
|
+
import path36 from "path";
|
|
5227
6136
|
|
|
5228
6137
|
// src/extract/retire.ts
|
|
5229
6138
|
import { existsSync as existsSync2 } from "fs";
|
|
5230
|
-
import
|
|
5231
|
-
import { NodeType as
|
|
6139
|
+
import path35 from "path";
|
|
6140
|
+
import { NodeType as NodeType14, Provenance as Provenance15 } from "@neat.is/types";
|
|
5232
6141
|
function dropOrphanedFileNodes(graph) {
|
|
5233
6142
|
const orphans = [];
|
|
5234
6143
|
graph.forEachNode((id, attrs) => {
|
|
5235
|
-
if (attrs.type !==
|
|
6144
|
+
if (attrs.type !== NodeType14.FileNode) return;
|
|
5236
6145
|
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
5237
6146
|
orphans.push(id);
|
|
5238
6147
|
}
|
|
@@ -5245,7 +6154,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
5245
6154
|
const toDrop = [];
|
|
5246
6155
|
graph.forEachEdge((id, attrs) => {
|
|
5247
6156
|
const edge = attrs;
|
|
5248
|
-
if (edge.provenance !==
|
|
6157
|
+
if (edge.provenance !== Provenance15.EXTRACTED) return;
|
|
5249
6158
|
if (!edge.evidence?.file) return;
|
|
5250
6159
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
5251
6160
|
});
|
|
@@ -5258,14 +6167,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
5258
6167
|
const bases = [scanPath, ...serviceDirs];
|
|
5259
6168
|
graph.forEachEdge((id, attrs) => {
|
|
5260
6169
|
const edge = attrs;
|
|
5261
|
-
if (edge.provenance !==
|
|
6170
|
+
if (edge.provenance !== Provenance15.EXTRACTED) return;
|
|
5262
6171
|
const evidenceFile = edge.evidence?.file;
|
|
5263
6172
|
if (!evidenceFile) return;
|
|
5264
|
-
if (
|
|
6173
|
+
if (path35.isAbsolute(evidenceFile)) {
|
|
5265
6174
|
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
5266
6175
|
return;
|
|
5267
6176
|
}
|
|
5268
|
-
const found = bases.some((base) => existsSync2(
|
|
6177
|
+
const found = bases.some((base) => existsSync2(path35.join(base, evidenceFile)));
|
|
5269
6178
|
if (!found) toDrop.push(id);
|
|
5270
6179
|
});
|
|
5271
6180
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -5284,6 +6193,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5284
6193
|
const importGraph = await addImports(graph, services);
|
|
5285
6194
|
const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
|
|
5286
6195
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
6196
|
+
const routePhase = await addRoutes(graph, services);
|
|
6197
|
+
const grpcPhase = await addGrpcMethods(graph, services);
|
|
5287
6198
|
const phase4 = await addCallEdges(graph, services);
|
|
5288
6199
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
5289
6200
|
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
@@ -5305,7 +6216,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5305
6216
|
}
|
|
5306
6217
|
const droppedEntries = drainDroppedExtracted();
|
|
5307
6218
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
5308
|
-
const rejectedPath =
|
|
6219
|
+
const rejectedPath = path36.join(path36.dirname(opts.errorsPath), "rejected.ndjson");
|
|
5309
6220
|
try {
|
|
5310
6221
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
5311
6222
|
} catch (err) {
|
|
@@ -5315,8 +6226,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5315
6226
|
}
|
|
5316
6227
|
}
|
|
5317
6228
|
const result = {
|
|
5318
|
-
nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
5319
|
-
edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
6229
|
+
nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + routePhase.nodesAdded + grpcPhase.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
6230
|
+
edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + routePhase.edgesAdded + grpcPhase.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
5320
6231
|
frontiersPromoted,
|
|
5321
6232
|
extractionErrors: errorEntries.length,
|
|
5322
6233
|
errorEntries,
|
|
@@ -5341,10 +6252,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
5341
6252
|
import {
|
|
5342
6253
|
databaseId as databaseId3,
|
|
5343
6254
|
DivergenceResultSchema,
|
|
5344
|
-
EdgeType as
|
|
5345
|
-
NodeType as
|
|
6255
|
+
EdgeType as EdgeType16,
|
|
6256
|
+
NodeType as NodeType15,
|
|
5346
6257
|
parseEdgeId,
|
|
5347
|
-
Provenance as
|
|
6258
|
+
Provenance as Provenance16
|
|
5348
6259
|
} from "@neat.is/types";
|
|
5349
6260
|
function bucketKey(source, target, type) {
|
|
5350
6261
|
return `${type}|${source}|${target}`;
|
|
@@ -5358,17 +6269,17 @@ function bucketEdges(graph) {
|
|
|
5358
6269
|
const key = bucketKey(e.source, e.target, e.type);
|
|
5359
6270
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
5360
6271
|
switch (provenance) {
|
|
5361
|
-
case
|
|
6272
|
+
case Provenance16.EXTRACTED:
|
|
5362
6273
|
cur.extracted = e;
|
|
5363
6274
|
break;
|
|
5364
|
-
case
|
|
6275
|
+
case Provenance16.OBSERVED:
|
|
5365
6276
|
cur.observed = e;
|
|
5366
6277
|
break;
|
|
5367
|
-
case
|
|
6278
|
+
case Provenance16.INFERRED:
|
|
5368
6279
|
cur.inferred = e;
|
|
5369
6280
|
break;
|
|
5370
6281
|
default:
|
|
5371
|
-
if (e.provenance ===
|
|
6282
|
+
if (e.provenance === Provenance16.STALE) cur.stale = e;
|
|
5372
6283
|
}
|
|
5373
6284
|
buckets.set(key, cur);
|
|
5374
6285
|
});
|
|
@@ -5377,7 +6288,12 @@ function bucketEdges(graph) {
|
|
|
5377
6288
|
function nodeIsFrontier(graph, nodeId) {
|
|
5378
6289
|
if (!graph.hasNode(nodeId)) return false;
|
|
5379
6290
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
5380
|
-
return attrs.type ===
|
|
6291
|
+
return attrs.type === NodeType15.FrontierNode;
|
|
6292
|
+
}
|
|
6293
|
+
function nodeIsWebsocketChannel(graph, nodeId) {
|
|
6294
|
+
if (!graph.hasNode(nodeId)) return false;
|
|
6295
|
+
const attrs = graph.getNodeAttributes(nodeId);
|
|
6296
|
+
return attrs.type === NodeType15.WebSocketChannelNode;
|
|
5381
6297
|
}
|
|
5382
6298
|
function clampConfidence(n) {
|
|
5383
6299
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -5397,14 +6313,14 @@ function gradedConfidence(edge) {
|
|
|
5397
6313
|
return clampConfidence(confidenceForEdge(edge));
|
|
5398
6314
|
}
|
|
5399
6315
|
var OBSERVABLE_EDGE_TYPES = /* @__PURE__ */ new Set([
|
|
5400
|
-
|
|
5401
|
-
|
|
5402
|
-
|
|
5403
|
-
|
|
6316
|
+
EdgeType16.CALLS,
|
|
6317
|
+
EdgeType16.CONNECTS_TO,
|
|
6318
|
+
EdgeType16.PUBLISHES_TO,
|
|
6319
|
+
EdgeType16.CONSUMES_FROM
|
|
5404
6320
|
]);
|
|
5405
6321
|
function detectMissingDivergences(graph, bucket) {
|
|
5406
6322
|
const out = [];
|
|
5407
|
-
if (bucket.type ===
|
|
6323
|
+
if (bucket.type === EdgeType16.CONTAINS) return out;
|
|
5408
6324
|
if (bucket.extracted && !bucket.observed && OBSERVABLE_EDGE_TYPES.has(bucket.type)) {
|
|
5409
6325
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
5410
6326
|
out.push({
|
|
@@ -5419,7 +6335,7 @@ function detectMissingDivergences(graph, bucket) {
|
|
|
5419
6335
|
});
|
|
5420
6336
|
}
|
|
5421
6337
|
}
|
|
5422
|
-
if (bucket.observed && !bucket.extracted) {
|
|
6338
|
+
if (bucket.observed && !bucket.extracted && !nodeIsWebsocketChannel(graph, bucket.target)) {
|
|
5423
6339
|
out.push({
|
|
5424
6340
|
type: "missing-extracted",
|
|
5425
6341
|
source: bucket.source,
|
|
@@ -5445,7 +6361,7 @@ function declaredHostFor(svc) {
|
|
|
5445
6361
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
5446
6362
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5447
6363
|
const e = graph.getEdgeAttributes(edgeId);
|
|
5448
|
-
if (e.type ===
|
|
6364
|
+
if (e.type === EdgeType16.CONFIGURED_BY && e.provenance === Provenance16.EXTRACTED) {
|
|
5449
6365
|
return true;
|
|
5450
6366
|
}
|
|
5451
6367
|
}
|
|
@@ -5458,10 +6374,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
5458
6374
|
const out = [];
|
|
5459
6375
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5460
6376
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5461
|
-
if (edge.type !==
|
|
5462
|
-
if (edge.provenance !==
|
|
6377
|
+
if (edge.type !== EdgeType16.CONNECTS_TO) continue;
|
|
6378
|
+
if (edge.provenance !== Provenance16.OBSERVED) continue;
|
|
5463
6379
|
const target = graph.getNodeAttributes(edge.target);
|
|
5464
|
-
if (target.type !==
|
|
6380
|
+
if (target.type !== NodeType15.DatabaseNode) continue;
|
|
5465
6381
|
const observedHost = target.host?.trim();
|
|
5466
6382
|
if (!observedHost) continue;
|
|
5467
6383
|
if (observedHost === declaredHost) continue;
|
|
@@ -5483,10 +6399,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
5483
6399
|
const deps = svc.dependencies ?? {};
|
|
5484
6400
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
5485
6401
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
5486
|
-
if (edge.type !==
|
|
5487
|
-
if (edge.provenance !==
|
|
6402
|
+
if (edge.type !== EdgeType16.CONNECTS_TO) continue;
|
|
6403
|
+
if (edge.provenance !== Provenance16.OBSERVED) continue;
|
|
5488
6404
|
const target = graph.getNodeAttributes(edge.target);
|
|
5489
|
-
if (target.type !==
|
|
6405
|
+
if (target.type !== NodeType15.DatabaseNode) continue;
|
|
5490
6406
|
for (const pair of compatPairs()) {
|
|
5491
6407
|
if (pair.engine !== target.engine) continue;
|
|
5492
6408
|
const declared = deps[pair.driver];
|
|
@@ -5564,7 +6480,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5564
6480
|
}
|
|
5565
6481
|
graph.forEachNode((nodeId, attrs) => {
|
|
5566
6482
|
const n = attrs;
|
|
5567
|
-
if (n.type !==
|
|
6483
|
+
if (n.type !== NodeType15.ServiceNode) return;
|
|
5568
6484
|
const svc = n;
|
|
5569
6485
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
5570
6486
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -5606,9 +6522,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
5606
6522
|
}
|
|
5607
6523
|
|
|
5608
6524
|
// src/persist.ts
|
|
5609
|
-
import { promises as
|
|
5610
|
-
import
|
|
5611
|
-
import { Provenance as
|
|
6525
|
+
import { promises as fs19 } from "fs";
|
|
6526
|
+
import path37 from "path";
|
|
6527
|
+
import { Provenance as Provenance17, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
5612
6528
|
var SCHEMA_VERSION = 4;
|
|
5613
6529
|
function migrateV1ToV2(payload) {
|
|
5614
6530
|
const nodes = payload.graph.nodes;
|
|
@@ -5630,7 +6546,7 @@ function migrateV2ToV3(payload) {
|
|
|
5630
6546
|
for (const edge of edges) {
|
|
5631
6547
|
const attrs = edge.attributes;
|
|
5632
6548
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
5633
|
-
attrs.provenance =
|
|
6549
|
+
attrs.provenance = Provenance17.OBSERVED;
|
|
5634
6550
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
5635
6551
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
5636
6552
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
@@ -5644,7 +6560,7 @@ function migrateV2ToV3(payload) {
|
|
|
5644
6560
|
return { ...payload, schemaVersion: 3 };
|
|
5645
6561
|
}
|
|
5646
6562
|
async function ensureDir(filePath) {
|
|
5647
|
-
await
|
|
6563
|
+
await fs19.mkdir(path37.dirname(filePath), { recursive: true });
|
|
5648
6564
|
}
|
|
5649
6565
|
async function saveGraphToDisk(graph, outPath) {
|
|
5650
6566
|
await ensureDir(outPath);
|
|
@@ -5654,13 +6570,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
5654
6570
|
graph: graph.export()
|
|
5655
6571
|
};
|
|
5656
6572
|
const tmp = `${outPath}.tmp`;
|
|
5657
|
-
await
|
|
5658
|
-
await
|
|
6573
|
+
await fs19.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
6574
|
+
await fs19.rename(tmp, outPath);
|
|
5659
6575
|
}
|
|
5660
6576
|
async function loadGraphFromDisk(graph, outPath) {
|
|
5661
6577
|
let raw;
|
|
5662
6578
|
try {
|
|
5663
|
-
raw = await
|
|
6579
|
+
raw = await fs19.readFile(outPath, "utf8");
|
|
5664
6580
|
} catch (err) {
|
|
5665
6581
|
if (err.code === "ENOENT") return;
|
|
5666
6582
|
throw err;
|
|
@@ -5724,7 +6640,7 @@ function startPersistLoop(graph, outPath, opts = {}) {
|
|
|
5724
6640
|
}
|
|
5725
6641
|
|
|
5726
6642
|
// src/diff.ts
|
|
5727
|
-
import { promises as
|
|
6643
|
+
import { promises as fs20 } from "fs";
|
|
5728
6644
|
async function loadSnapshotForDiff(target) {
|
|
5729
6645
|
if (/^https?:\/\//i.test(target)) {
|
|
5730
6646
|
const res = await fetch(target);
|
|
@@ -5733,7 +6649,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
5733
6649
|
}
|
|
5734
6650
|
return await res.json();
|
|
5735
6651
|
}
|
|
5736
|
-
const raw = await
|
|
6652
|
+
const raw = await fs20.readFile(target, "utf8");
|
|
5737
6653
|
return JSON.parse(raw);
|
|
5738
6654
|
}
|
|
5739
6655
|
function indexEntries(entries) {
|
|
@@ -5800,23 +6716,23 @@ function canonicalJson(value) {
|
|
|
5800
6716
|
}
|
|
5801
6717
|
|
|
5802
6718
|
// src/projects.ts
|
|
5803
|
-
import
|
|
6719
|
+
import path38 from "path";
|
|
5804
6720
|
function pathsForProject(project, baseDir) {
|
|
5805
6721
|
if (project === DEFAULT_PROJECT) {
|
|
5806
6722
|
return {
|
|
5807
|
-
snapshotPath:
|
|
5808
|
-
errorsPath:
|
|
5809
|
-
staleEventsPath:
|
|
5810
|
-
embeddingsCachePath:
|
|
5811
|
-
policyViolationsPath:
|
|
6723
|
+
snapshotPath: path38.join(baseDir, "graph.json"),
|
|
6724
|
+
errorsPath: path38.join(baseDir, "errors.ndjson"),
|
|
6725
|
+
staleEventsPath: path38.join(baseDir, "stale-events.ndjson"),
|
|
6726
|
+
embeddingsCachePath: path38.join(baseDir, "embeddings.json"),
|
|
6727
|
+
policyViolationsPath: path38.join(baseDir, "policy-violations.ndjson")
|
|
5812
6728
|
};
|
|
5813
6729
|
}
|
|
5814
6730
|
return {
|
|
5815
|
-
snapshotPath:
|
|
5816
|
-
errorsPath:
|
|
5817
|
-
staleEventsPath:
|
|
5818
|
-
embeddingsCachePath:
|
|
5819
|
-
policyViolationsPath:
|
|
6731
|
+
snapshotPath: path38.join(baseDir, `${project}.json`),
|
|
6732
|
+
errorsPath: path38.join(baseDir, `errors.${project}.ndjson`),
|
|
6733
|
+
staleEventsPath: path38.join(baseDir, `stale-events.${project}.ndjson`),
|
|
6734
|
+
embeddingsCachePath: path38.join(baseDir, `embeddings.${project}.json`),
|
|
6735
|
+
policyViolationsPath: path38.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
5820
6736
|
};
|
|
5821
6737
|
}
|
|
5822
6738
|
var Projects = class {
|
|
@@ -5855,9 +6771,9 @@ function parseExtraProjects(raw) {
|
|
|
5855
6771
|
}
|
|
5856
6772
|
|
|
5857
6773
|
// src/registry.ts
|
|
5858
|
-
import { promises as
|
|
6774
|
+
import { promises as fs21 } from "fs";
|
|
5859
6775
|
import os2 from "os";
|
|
5860
|
-
import
|
|
6776
|
+
import path39 from "path";
|
|
5861
6777
|
import {
|
|
5862
6778
|
RegistryFileSchema
|
|
5863
6779
|
} from "@neat.is/types";
|
|
@@ -5865,20 +6781,20 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
5865
6781
|
var LOCK_RETRY_MS = 50;
|
|
5866
6782
|
function neatHome() {
|
|
5867
6783
|
const override = process.env.NEAT_HOME;
|
|
5868
|
-
if (override && override.length > 0) return
|
|
5869
|
-
return
|
|
6784
|
+
if (override && override.length > 0) return path39.resolve(override);
|
|
6785
|
+
return path39.join(os2.homedir(), ".neat");
|
|
5870
6786
|
}
|
|
5871
6787
|
function registryPath() {
|
|
5872
|
-
return
|
|
6788
|
+
return path39.join(neatHome(), "projects.json");
|
|
5873
6789
|
}
|
|
5874
6790
|
function registryLockPath() {
|
|
5875
|
-
return
|
|
6791
|
+
return path39.join(neatHome(), "projects.json.lock");
|
|
5876
6792
|
}
|
|
5877
6793
|
function daemonPidPath() {
|
|
5878
|
-
return
|
|
6794
|
+
return path39.join(neatHome(), "neatd.pid");
|
|
5879
6795
|
}
|
|
5880
6796
|
function daemonsDir() {
|
|
5881
|
-
return
|
|
6797
|
+
return path39.join(neatHome(), "daemons");
|
|
5882
6798
|
}
|
|
5883
6799
|
function isFiniteInt(v) {
|
|
5884
6800
|
return typeof v === "number" && Number.isFinite(v);
|
|
@@ -5911,7 +6827,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
5911
6827
|
const dir = daemonsDir();
|
|
5912
6828
|
let names;
|
|
5913
6829
|
try {
|
|
5914
|
-
names = await
|
|
6830
|
+
names = await fs21.readdir(dir);
|
|
5915
6831
|
} catch (err) {
|
|
5916
6832
|
if (err.code === "ENOENT") return [];
|
|
5917
6833
|
throw err;
|
|
@@ -5919,10 +6835,10 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
5919
6835
|
const out = [];
|
|
5920
6836
|
for (const name of names) {
|
|
5921
6837
|
if (!name.endsWith(".json")) continue;
|
|
5922
|
-
const file =
|
|
6838
|
+
const file = path39.join(dir, name);
|
|
5923
6839
|
let raw;
|
|
5924
6840
|
try {
|
|
5925
|
-
raw = await
|
|
6841
|
+
raw = await fs21.readFile(file, "utf8");
|
|
5926
6842
|
} catch {
|
|
5927
6843
|
continue;
|
|
5928
6844
|
}
|
|
@@ -5935,7 +6851,7 @@ async function discoverDaemons(probe = defaultDiscoveryProbe) {
|
|
|
5935
6851
|
return out;
|
|
5936
6852
|
}
|
|
5937
6853
|
async function removeDaemonRecord(source) {
|
|
5938
|
-
await
|
|
6854
|
+
await fs21.unlink(source).catch(() => {
|
|
5939
6855
|
});
|
|
5940
6856
|
}
|
|
5941
6857
|
async function listMachineProjects(probe = defaultDiscoveryProbe) {
|
|
@@ -5992,7 +6908,7 @@ function isPidAliveDefault(pid) {
|
|
|
5992
6908
|
}
|
|
5993
6909
|
async function readPidFile(file) {
|
|
5994
6910
|
try {
|
|
5995
|
-
const raw = await
|
|
6911
|
+
const raw = await fs21.readFile(file, "utf8");
|
|
5996
6912
|
const pid = Number.parseInt(raw.trim(), 10);
|
|
5997
6913
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
5998
6914
|
} catch {
|
|
@@ -6040,32 +6956,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
6040
6956
|
}
|
|
6041
6957
|
}
|
|
6042
6958
|
async function normalizeProjectPath(input) {
|
|
6043
|
-
const resolved =
|
|
6959
|
+
const resolved = path39.resolve(input);
|
|
6044
6960
|
try {
|
|
6045
|
-
return await
|
|
6961
|
+
return await fs21.realpath(resolved);
|
|
6046
6962
|
} catch {
|
|
6047
6963
|
return resolved;
|
|
6048
6964
|
}
|
|
6049
6965
|
}
|
|
6050
6966
|
async function writeAtomically(target, contents) {
|
|
6051
|
-
await
|
|
6967
|
+
await fs21.mkdir(path39.dirname(target), { recursive: true });
|
|
6052
6968
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
6053
|
-
const fd = await
|
|
6969
|
+
const fd = await fs21.open(tmp, "w");
|
|
6054
6970
|
try {
|
|
6055
6971
|
await fd.writeFile(contents, "utf8");
|
|
6056
6972
|
await fd.sync();
|
|
6057
6973
|
} finally {
|
|
6058
6974
|
await fd.close();
|
|
6059
6975
|
}
|
|
6060
|
-
await
|
|
6976
|
+
await fs21.rename(tmp, target);
|
|
6061
6977
|
}
|
|
6062
6978
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
6063
6979
|
const deadline = Date.now() + timeoutMs;
|
|
6064
|
-
await
|
|
6980
|
+
await fs21.mkdir(path39.dirname(lockPath), { recursive: true });
|
|
6065
6981
|
let probedHolder = false;
|
|
6066
6982
|
while (true) {
|
|
6067
6983
|
try {
|
|
6068
|
-
const fd = await
|
|
6984
|
+
const fd = await fs21.open(lockPath, "wx");
|
|
6069
6985
|
try {
|
|
6070
6986
|
await fd.writeFile(`${process.pid}
|
|
6071
6987
|
`, "utf8");
|
|
@@ -6090,7 +7006,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
|
|
|
6090
7006
|
}
|
|
6091
7007
|
}
|
|
6092
7008
|
async function releaseLock(lockPath) {
|
|
6093
|
-
await
|
|
7009
|
+
await fs21.unlink(lockPath).catch(() => {
|
|
6094
7010
|
});
|
|
6095
7011
|
}
|
|
6096
7012
|
async function withLock(fn) {
|
|
@@ -6106,7 +7022,7 @@ async function readRegistry() {
|
|
|
6106
7022
|
const file = registryPath();
|
|
6107
7023
|
let raw;
|
|
6108
7024
|
try {
|
|
6109
|
-
raw = await
|
|
7025
|
+
raw = await fs21.readFile(file, "utf8");
|
|
6110
7026
|
} catch (err) {
|
|
6111
7027
|
if (err.code === "ENOENT") {
|
|
6112
7028
|
return { version: 1, projects: [] };
|
|
@@ -6211,7 +7127,7 @@ function pruneTtlMs() {
|
|
|
6211
7127
|
}
|
|
6212
7128
|
async function statPathStatus(p) {
|
|
6213
7129
|
try {
|
|
6214
|
-
const stat = await
|
|
7130
|
+
const stat = await fs21.stat(p);
|
|
6215
7131
|
return stat.isDirectory() ? "present" : "unknown";
|
|
6216
7132
|
} catch (err) {
|
|
6217
7133
|
return err.code === "ENOENT" ? "gone" : "unknown";
|
|
@@ -6256,14 +7172,14 @@ import cors from "@fastify/cors";
|
|
|
6256
7172
|
import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
6257
7173
|
|
|
6258
7174
|
// src/extend/index.ts
|
|
6259
|
-
import { promises as
|
|
6260
|
-
import
|
|
7175
|
+
import { promises as fs23 } from "fs";
|
|
7176
|
+
import path41 from "path";
|
|
6261
7177
|
import os3 from "os";
|
|
6262
7178
|
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
6263
7179
|
|
|
6264
7180
|
// src/installers/package-manager.ts
|
|
6265
|
-
import { promises as
|
|
6266
|
-
import
|
|
7181
|
+
import { promises as fs22 } from "fs";
|
|
7182
|
+
import path40 from "path";
|
|
6267
7183
|
import { spawn } from "child_process";
|
|
6268
7184
|
var LOCKFILE_PRIORITY = [
|
|
6269
7185
|
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
@@ -6278,29 +7194,29 @@ var LOCKFILE_PRIORITY = [
|
|
|
6278
7194
|
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
6279
7195
|
async function exists2(p) {
|
|
6280
7196
|
try {
|
|
6281
|
-
await
|
|
7197
|
+
await fs22.access(p);
|
|
6282
7198
|
return true;
|
|
6283
7199
|
} catch {
|
|
6284
7200
|
return false;
|
|
6285
7201
|
}
|
|
6286
7202
|
}
|
|
6287
7203
|
async function detectPackageManager(serviceDir) {
|
|
6288
|
-
let dir =
|
|
7204
|
+
let dir = path40.resolve(serviceDir);
|
|
6289
7205
|
const stops = /* @__PURE__ */ new Set();
|
|
6290
7206
|
for (let i = 0; i < 64; i++) {
|
|
6291
7207
|
if (stops.has(dir)) break;
|
|
6292
7208
|
stops.add(dir);
|
|
6293
7209
|
for (const candidate of LOCKFILE_PRIORITY) {
|
|
6294
|
-
const lockPath =
|
|
7210
|
+
const lockPath = path40.join(dir, candidate.lockfile);
|
|
6295
7211
|
if (await exists2(lockPath)) {
|
|
6296
7212
|
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
6297
7213
|
}
|
|
6298
7214
|
}
|
|
6299
|
-
const parent =
|
|
7215
|
+
const parent = path40.dirname(dir);
|
|
6300
7216
|
if (parent === dir) break;
|
|
6301
7217
|
dir = parent;
|
|
6302
7218
|
}
|
|
6303
|
-
return { pm: "npm", cwd:
|
|
7219
|
+
return { pm: "npm", cwd: path40.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
6304
7220
|
}
|
|
6305
7221
|
async function runPackageManagerInstall(cmd) {
|
|
6306
7222
|
return new Promise((resolve) => {
|
|
@@ -6342,30 +7258,30 @@ ${err.message}`
|
|
|
6342
7258
|
// src/extend/index.ts
|
|
6343
7259
|
async function fileExists2(p) {
|
|
6344
7260
|
try {
|
|
6345
|
-
await
|
|
7261
|
+
await fs23.access(p);
|
|
6346
7262
|
return true;
|
|
6347
7263
|
} catch {
|
|
6348
7264
|
return false;
|
|
6349
7265
|
}
|
|
6350
7266
|
}
|
|
6351
7267
|
async function readPackageJson(scanPath) {
|
|
6352
|
-
const pkgPath =
|
|
6353
|
-
const raw = await
|
|
7268
|
+
const pkgPath = path41.join(scanPath, "package.json");
|
|
7269
|
+
const raw = await fs23.readFile(pkgPath, "utf8");
|
|
6354
7270
|
return JSON.parse(raw);
|
|
6355
7271
|
}
|
|
6356
7272
|
async function findHookFiles(scanPath) {
|
|
6357
|
-
const entries = await
|
|
7273
|
+
const entries = await fs23.readdir(scanPath);
|
|
6358
7274
|
return entries.filter(
|
|
6359
7275
|
(e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
|
|
6360
7276
|
).sort();
|
|
6361
7277
|
}
|
|
6362
7278
|
function extendLogPath() {
|
|
6363
|
-
return process.env.NEAT_EXTEND_LOG ??
|
|
7279
|
+
return process.env.NEAT_EXTEND_LOG ?? path41.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
6364
7280
|
}
|
|
6365
7281
|
async function appendExtendLog(entry) {
|
|
6366
7282
|
const logPath = extendLogPath();
|
|
6367
|
-
await
|
|
6368
|
-
await
|
|
7283
|
+
await fs23.mkdir(path41.dirname(logPath), { recursive: true });
|
|
7284
|
+
await fs23.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
6369
7285
|
}
|
|
6370
7286
|
function splicedContent(fileContent, snippet2) {
|
|
6371
7287
|
if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
|
|
@@ -6423,7 +7339,7 @@ function lookupInstrumentation(library, installedVersion) {
|
|
|
6423
7339
|
}
|
|
6424
7340
|
async function describeProjectInstrumentation(ctx) {
|
|
6425
7341
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
6426
|
-
const envNeat = await fileExists2(
|
|
7342
|
+
const envNeat = await fileExists2(path41.join(ctx.scanPath, ".env.neat"));
|
|
6427
7343
|
const registryInstrPackages = new Set(
|
|
6428
7344
|
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
6429
7345
|
);
|
|
@@ -6445,31 +7361,31 @@ async function applyExtension(ctx, args, options) {
|
|
|
6445
7361
|
);
|
|
6446
7362
|
}
|
|
6447
7363
|
for (const file of hookFiles) {
|
|
6448
|
-
const content = await
|
|
7364
|
+
const content = await fs23.readFile(path41.join(ctx.scanPath, file), "utf8");
|
|
6449
7365
|
if (content.includes(args.registration_snippet)) {
|
|
6450
7366
|
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
6451
7367
|
}
|
|
6452
7368
|
}
|
|
6453
7369
|
const primaryFile = hookFiles[0];
|
|
6454
|
-
const primaryPath =
|
|
7370
|
+
const primaryPath = path41.join(ctx.scanPath, primaryFile);
|
|
6455
7371
|
const filesTouched = [];
|
|
6456
7372
|
const depsAdded = [];
|
|
6457
|
-
const pkgPath =
|
|
7373
|
+
const pkgPath = path41.join(ctx.scanPath, "package.json");
|
|
6458
7374
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
6459
7375
|
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
6460
7376
|
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
6461
|
-
await
|
|
7377
|
+
await fs23.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6462
7378
|
filesTouched.push("package.json");
|
|
6463
7379
|
depsAdded.push(`${args.instrumentation_package}@${args.version}`);
|
|
6464
7380
|
}
|
|
6465
|
-
const hookContent = await
|
|
7381
|
+
const hookContent = await fs23.readFile(primaryPath, "utf8");
|
|
6466
7382
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
6467
7383
|
if (!patched) {
|
|
6468
7384
|
throw new Error(
|
|
6469
7385
|
`Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
|
|
6470
7386
|
);
|
|
6471
7387
|
}
|
|
6472
|
-
await
|
|
7388
|
+
await fs23.writeFile(primaryPath, patched, "utf8");
|
|
6473
7389
|
filesTouched.push(primaryFile);
|
|
6474
7390
|
const cmd = await detectPackageManager(ctx.scanPath);
|
|
6475
7391
|
const installer = options?.runInstall ?? runPackageManagerInstall;
|
|
@@ -6500,7 +7416,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
6500
7416
|
};
|
|
6501
7417
|
}
|
|
6502
7418
|
for (const file of hookFiles) {
|
|
6503
|
-
const content = await
|
|
7419
|
+
const content = await fs23.readFile(path41.join(ctx.scanPath, file), "utf8");
|
|
6504
7420
|
if (content.includes(args.registration_snippet)) {
|
|
6505
7421
|
return {
|
|
6506
7422
|
library: args.library,
|
|
@@ -6522,7 +7438,7 @@ async function dryRunExtension(ctx, args) {
|
|
|
6522
7438
|
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
6523
7439
|
filesTouched.push("package.json");
|
|
6524
7440
|
}
|
|
6525
|
-
const hookContent = await
|
|
7441
|
+
const hookContent = await fs23.readFile(path41.join(ctx.scanPath, primaryFile), "utf8");
|
|
6526
7442
|
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
6527
7443
|
if (patched) {
|
|
6528
7444
|
filesTouched.push(primaryFile);
|
|
@@ -6537,28 +7453,28 @@ async function rollbackExtension(ctx, args) {
|
|
|
6537
7453
|
if (!await fileExists2(logPath)) {
|
|
6538
7454
|
return { undone: false, message: "no apply found for library" };
|
|
6539
7455
|
}
|
|
6540
|
-
const raw = await
|
|
7456
|
+
const raw = await fs23.readFile(logPath, "utf8");
|
|
6541
7457
|
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
6542
7458
|
const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
|
|
6543
7459
|
if (!match) {
|
|
6544
7460
|
return { undone: false, message: "no apply found for library" };
|
|
6545
7461
|
}
|
|
6546
|
-
const pkgPath =
|
|
7462
|
+
const pkgPath = path41.join(ctx.scanPath, "package.json");
|
|
6547
7463
|
if (await fileExists2(pkgPath)) {
|
|
6548
7464
|
const pkg = await readPackageJson(ctx.scanPath);
|
|
6549
7465
|
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
6550
7466
|
const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
|
|
6551
7467
|
pkg.dependencies = rest;
|
|
6552
|
-
await
|
|
7468
|
+
await fs23.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
6553
7469
|
}
|
|
6554
7470
|
}
|
|
6555
7471
|
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
6556
7472
|
for (const file of hookFiles) {
|
|
6557
|
-
const filePath =
|
|
6558
|
-
const content = await
|
|
7473
|
+
const filePath = path41.join(ctx.scanPath, file);
|
|
7474
|
+
const content = await fs23.readFile(filePath, "utf8");
|
|
6559
7475
|
if (content.includes(match.registration_snippet)) {
|
|
6560
7476
|
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
6561
|
-
await
|
|
7477
|
+
await fs23.writeFile(filePath, filtered, "utf8");
|
|
6562
7478
|
break;
|
|
6563
7479
|
}
|
|
6564
7480
|
}
|
|
@@ -7308,6 +8224,10 @@ export {
|
|
|
7308
8224
|
loadPolicyFile,
|
|
7309
8225
|
PolicyViolationsLog,
|
|
7310
8226
|
thresholdForEdgeType,
|
|
8227
|
+
reconcileObservedRelPath,
|
|
8228
|
+
ensureObservedFileNode,
|
|
8229
|
+
ensureServiceNode,
|
|
8230
|
+
upsertObservedEdge,
|
|
7311
8231
|
stitchTrace,
|
|
7312
8232
|
makeErrorSpanWriter,
|
|
7313
8233
|
handleSpan,
|
|
@@ -7362,4 +8282,4 @@ export {
|
|
|
7362
8282
|
pruneRegistry,
|
|
7363
8283
|
buildApi
|
|
7364
8284
|
};
|
|
7365
|
-
//# sourceMappingURL=chunk-
|
|
8285
|
+
//# sourceMappingURL=chunk-QM6BMPVJ.js.map
|