@neat.is/core 0.4.6 → 0.4.7
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-RBWL4HRB.js → chunk-7FTK47JQ.js} +277 -103
- package/dist/chunk-7FTK47JQ.js.map +1 -0
- package/dist/{chunk-NON5AXOR.js → chunk-LS6NS72S.js} +2 -2
- package/dist/cli.cjs +402 -145
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +107 -16
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +280 -115
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +280 -115
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +263 -98
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-RBWL4HRB.js.map +0 -1
- /package/dist/{chunk-NON5AXOR.js.map → chunk-LS6NS72S.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -1649,6 +1649,86 @@ function hostFromUrl(u) {
|
|
|
1649
1649
|
function pickAddress(span) {
|
|
1650
1650
|
return pickAttr(span, "server.address", "net.peer.name", "net.host.name") ?? hostFromUrl(pickAttr(span, "url.full", "http.url"));
|
|
1651
1651
|
}
|
|
1652
|
+
var CODE_FILEPATH_ATTR = "code.filepath";
|
|
1653
|
+
var CODE_LINENO_ATTR = "code.lineno";
|
|
1654
|
+
var CODE_FUNCTION_ATTR = "code.function";
|
|
1655
|
+
function toPosix(p) {
|
|
1656
|
+
return p.split("\\").join("/");
|
|
1657
|
+
}
|
|
1658
|
+
function languageForExt(relPath) {
|
|
1659
|
+
const dot = relPath.lastIndexOf(".");
|
|
1660
|
+
if (dot === -1) return void 0;
|
|
1661
|
+
switch (relPath.slice(dot).toLowerCase()) {
|
|
1662
|
+
case ".py":
|
|
1663
|
+
return "python";
|
|
1664
|
+
case ".ts":
|
|
1665
|
+
case ".tsx":
|
|
1666
|
+
return "typescript";
|
|
1667
|
+
case ".js":
|
|
1668
|
+
case ".jsx":
|
|
1669
|
+
case ".mjs":
|
|
1670
|
+
case ".cjs":
|
|
1671
|
+
return "javascript";
|
|
1672
|
+
default:
|
|
1673
|
+
return void 0;
|
|
1674
|
+
}
|
|
1675
|
+
}
|
|
1676
|
+
function relPathForRuntimeFile(filepath, serviceNode) {
|
|
1677
|
+
let p = toPosix(filepath).replace(/^file:\/\//, "");
|
|
1678
|
+
const root = serviceNode?.repoPath;
|
|
1679
|
+
if (root && root !== "." && root.length > 0) {
|
|
1680
|
+
const rootPosix = toPosix(root);
|
|
1681
|
+
const anchor = `/${rootPosix}/`;
|
|
1682
|
+
const idx = p.lastIndexOf(anchor);
|
|
1683
|
+
if (idx !== -1) return p.slice(idx + anchor.length);
|
|
1684
|
+
const base = rootPosix.split("/").filter(Boolean).pop();
|
|
1685
|
+
if (base) {
|
|
1686
|
+
const baseAnchor = `/${base}/`;
|
|
1687
|
+
const bidx = p.lastIndexOf(baseAnchor);
|
|
1688
|
+
if (bidx !== -1) return p.slice(bidx + baseAnchor.length);
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
1691
|
+
p = p.replace(/^[A-Za-z]:/, "").replace(/^\/+/, "");
|
|
1692
|
+
return p.length > 0 ? p : null;
|
|
1693
|
+
}
|
|
1694
|
+
function callSiteFromSpan(span, serviceNode) {
|
|
1695
|
+
const filepath = span.attributes[CODE_FILEPATH_ATTR];
|
|
1696
|
+
if (typeof filepath !== "string" || filepath.length === 0) return null;
|
|
1697
|
+
const relPath = relPathForRuntimeFile(filepath, serviceNode);
|
|
1698
|
+
if (!relPath) return null;
|
|
1699
|
+
const linenoRaw = span.attributes[CODE_LINENO_ATTR];
|
|
1700
|
+
const line = typeof linenoRaw === "number" && Number.isFinite(linenoRaw) ? linenoRaw : void 0;
|
|
1701
|
+
const fnRaw = span.attributes[CODE_FUNCTION_ATTR];
|
|
1702
|
+
const fn = typeof fnRaw === "string" && fnRaw.length > 0 ? fnRaw : void 0;
|
|
1703
|
+
return { relPath, ...line !== void 0 ? { line } : {}, ...fn ? { fn } : {} };
|
|
1704
|
+
}
|
|
1705
|
+
function ensureObservedFileNode(graph, serviceName, serviceNodeId, callSite) {
|
|
1706
|
+
const fileNodeId = (0, import_types3.fileId)(serviceName, callSite.relPath);
|
|
1707
|
+
if (!graph.hasNode(fileNodeId)) {
|
|
1708
|
+
const language = languageForExt(callSite.relPath);
|
|
1709
|
+
const node = {
|
|
1710
|
+
id: fileNodeId,
|
|
1711
|
+
type: import_types3.NodeType.FileNode,
|
|
1712
|
+
service: serviceName,
|
|
1713
|
+
path: callSite.relPath,
|
|
1714
|
+
...language ? { language } : {},
|
|
1715
|
+
discoveredVia: "otel"
|
|
1716
|
+
};
|
|
1717
|
+
graph.addNode(fileNodeId, node);
|
|
1718
|
+
}
|
|
1719
|
+
const containsId = makeObservedEdgeId(import_types3.EdgeType.CONTAINS, serviceNodeId, fileNodeId);
|
|
1720
|
+
if (!graph.hasEdge(containsId)) {
|
|
1721
|
+
const edge = {
|
|
1722
|
+
id: containsId,
|
|
1723
|
+
source: serviceNodeId,
|
|
1724
|
+
target: fileNodeId,
|
|
1725
|
+
type: import_types3.EdgeType.CONTAINS,
|
|
1726
|
+
provenance: import_types3.Provenance.OBSERVED
|
|
1727
|
+
};
|
|
1728
|
+
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
1729
|
+
}
|
|
1730
|
+
return fileNodeId;
|
|
1731
|
+
}
|
|
1652
1732
|
function makeObservedEdgeId(type, source, target) {
|
|
1653
1733
|
return (0, import_types3.observedEdgeId)(source, target, type);
|
|
1654
1734
|
}
|
|
@@ -1891,6 +1971,9 @@ async function handleSpan(ctx, span) {
|
|
|
1891
1971
|
const sourceId = ensureServiceNode(ctx.graph, span.service, env);
|
|
1892
1972
|
const isError = span.statusCode === 2;
|
|
1893
1973
|
cacheSpanService(span, nowMs);
|
|
1974
|
+
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1975
|
+
const callSite = callSiteFromSpan(span, sourceServiceNode);
|
|
1976
|
+
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1894
1977
|
let affectedNode = sourceId;
|
|
1895
1978
|
if (span.dbSystem) {
|
|
1896
1979
|
const host = pickAddress(span);
|
|
@@ -1900,7 +1983,7 @@ async function handleSpan(ctx, span) {
|
|
|
1900
1983
|
const result = upsertObservedEdge(
|
|
1901
1984
|
ctx.graph,
|
|
1902
1985
|
import_types3.EdgeType.CONNECTS_TO,
|
|
1903
|
-
|
|
1986
|
+
observedSource(),
|
|
1904
1987
|
targetId,
|
|
1905
1988
|
ts,
|
|
1906
1989
|
isError
|
|
@@ -1916,7 +1999,7 @@ async function handleSpan(ctx, span) {
|
|
|
1916
1999
|
upsertObservedEdge(
|
|
1917
2000
|
ctx.graph,
|
|
1918
2001
|
import_types3.EdgeType.CALLS,
|
|
1919
|
-
|
|
2002
|
+
observedSource(),
|
|
1920
2003
|
targetId,
|
|
1921
2004
|
ts,
|
|
1922
2005
|
isError
|
|
@@ -1928,7 +2011,7 @@ async function handleSpan(ctx, span) {
|
|
|
1928
2011
|
upsertObservedEdge(
|
|
1929
2012
|
ctx.graph,
|
|
1930
2013
|
import_types3.EdgeType.CALLS,
|
|
1931
|
-
|
|
2014
|
+
observedSource(),
|
|
1932
2015
|
frontierNodeId,
|
|
1933
2016
|
ts,
|
|
1934
2017
|
isError
|
|
@@ -3571,7 +3654,7 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
3571
3654
|
|
|
3572
3655
|
// src/extract/calls/index.ts
|
|
3573
3656
|
init_cjs_shims();
|
|
3574
|
-
var
|
|
3657
|
+
var import_types15 = require("@neat.is/types");
|
|
3575
3658
|
|
|
3576
3659
|
// src/extract/calls/http.ts
|
|
3577
3660
|
init_cjs_shims();
|
|
@@ -3579,12 +3662,13 @@ var import_node_path20 = __toESM(require("path"), 1);
|
|
|
3579
3662
|
var import_tree_sitter = __toESM(require("tree-sitter"), 1);
|
|
3580
3663
|
var import_tree_sitter_javascript = __toESM(require("tree-sitter-javascript"), 1);
|
|
3581
3664
|
var import_tree_sitter_python = __toESM(require("tree-sitter-python"), 1);
|
|
3582
|
-
var
|
|
3665
|
+
var import_types10 = require("@neat.is/types");
|
|
3583
3666
|
|
|
3584
3667
|
// src/extract/calls/shared.ts
|
|
3585
3668
|
init_cjs_shims();
|
|
3586
3669
|
var import_node_fs13 = require("fs");
|
|
3587
3670
|
var import_node_path19 = __toESM(require("path"), 1);
|
|
3671
|
+
var import_types9 = require("@neat.is/types");
|
|
3588
3672
|
async function walkSourceFiles(dir) {
|
|
3589
3673
|
const out = [];
|
|
3590
3674
|
async function walk(current) {
|
|
@@ -3624,6 +3708,58 @@ function snippet(text, line) {
|
|
|
3624
3708
|
const lines = text.split("\n");
|
|
3625
3709
|
return (lines[line - 1] ?? "").trim();
|
|
3626
3710
|
}
|
|
3711
|
+
function toPosix2(p) {
|
|
3712
|
+
return p.split("\\").join("/");
|
|
3713
|
+
}
|
|
3714
|
+
function languageForPath(relPath) {
|
|
3715
|
+
switch (import_node_path19.default.extname(relPath).toLowerCase()) {
|
|
3716
|
+
case ".py":
|
|
3717
|
+
return "python";
|
|
3718
|
+
case ".ts":
|
|
3719
|
+
case ".tsx":
|
|
3720
|
+
return "typescript";
|
|
3721
|
+
case ".js":
|
|
3722
|
+
case ".jsx":
|
|
3723
|
+
case ".mjs":
|
|
3724
|
+
case ".cjs":
|
|
3725
|
+
return "javascript";
|
|
3726
|
+
default:
|
|
3727
|
+
return void 0;
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
3731
|
+
let nodesAdded = 0;
|
|
3732
|
+
let edgesAdded = 0;
|
|
3733
|
+
const fileNodeId = (0, import_types9.fileId)(serviceName, relPath);
|
|
3734
|
+
if (!graph.hasNode(fileNodeId)) {
|
|
3735
|
+
const language = languageForPath(relPath);
|
|
3736
|
+
const node = {
|
|
3737
|
+
id: fileNodeId,
|
|
3738
|
+
type: import_types9.NodeType.FileNode,
|
|
3739
|
+
service: serviceName,
|
|
3740
|
+
path: relPath,
|
|
3741
|
+
...language ? { language } : {},
|
|
3742
|
+
discoveredVia: "static"
|
|
3743
|
+
};
|
|
3744
|
+
graph.addNode(fileNodeId, node);
|
|
3745
|
+
nodesAdded++;
|
|
3746
|
+
}
|
|
3747
|
+
const containsId = (0, import_types9.extractedEdgeId)(serviceNodeId, fileNodeId, import_types9.EdgeType.CONTAINS);
|
|
3748
|
+
if (!graph.hasEdge(containsId)) {
|
|
3749
|
+
const edge = {
|
|
3750
|
+
id: containsId,
|
|
3751
|
+
source: serviceNodeId,
|
|
3752
|
+
target: fileNodeId,
|
|
3753
|
+
type: import_types9.EdgeType.CONTAINS,
|
|
3754
|
+
provenance: import_types9.Provenance.EXTRACTED,
|
|
3755
|
+
confidence: (0, import_types9.confidenceForExtracted)("structural"),
|
|
3756
|
+
evidence: { file: relPath }
|
|
3757
|
+
};
|
|
3758
|
+
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
3759
|
+
edgesAdded++;
|
|
3760
|
+
}
|
|
3761
|
+
return { fileNodeId, nodesAdded, edgesAdded };
|
|
3762
|
+
}
|
|
3627
3763
|
|
|
3628
3764
|
// src/extract/calls/http.ts
|
|
3629
3765
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
@@ -3657,16 +3793,16 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3657
3793
|
const tree = parser.parse(source);
|
|
3658
3794
|
const literals = [];
|
|
3659
3795
|
collectStringLiterals(tree.rootNode, literals);
|
|
3660
|
-
const
|
|
3796
|
+
const out = [];
|
|
3661
3797
|
for (const lit of literals) {
|
|
3662
3798
|
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
3663
3799
|
for (const host of knownHosts) {
|
|
3664
3800
|
if (urlMatchesHost(lit.text, host)) {
|
|
3665
|
-
|
|
3801
|
+
out.push({ host, line: lit.node.startPosition.row + 1 });
|
|
3666
3802
|
}
|
|
3667
3803
|
}
|
|
3668
3804
|
}
|
|
3669
|
-
return
|
|
3805
|
+
return out;
|
|
3670
3806
|
}
|
|
3671
3807
|
function makeJsParser() {
|
|
3672
3808
|
const p = new import_tree_sitter.default();
|
|
@@ -3689,71 +3825,78 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3689
3825
|
hostToNodeId.set(import_node_path20.default.basename(service.dir), service.node.id);
|
|
3690
3826
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3691
3827
|
}
|
|
3828
|
+
let nodesAdded = 0;
|
|
3692
3829
|
let edgesAdded = 0;
|
|
3693
3830
|
for (const service of services) {
|
|
3694
3831
|
const files = await loadSourceFiles(service.dir);
|
|
3695
|
-
const
|
|
3832
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3696
3833
|
for (const file of files) {
|
|
3697
3834
|
if (isTestPath(file.path)) continue;
|
|
3698
3835
|
const parser = import_node_path20.default.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3699
|
-
let
|
|
3836
|
+
let sites;
|
|
3700
3837
|
try {
|
|
3701
|
-
|
|
3838
|
+
sites = callsFromSource(file.content, parser, knownHosts);
|
|
3702
3839
|
} catch (err) {
|
|
3703
3840
|
recordExtractionError("http call extraction", file.path, err);
|
|
3704
3841
|
continue;
|
|
3705
3842
|
}
|
|
3706
|
-
|
|
3707
|
-
|
|
3843
|
+
if (sites.length === 0) continue;
|
|
3844
|
+
const relFile = toPosix2(import_node_path20.default.relative(service.dir, file.path));
|
|
3845
|
+
for (const site of sites) {
|
|
3846
|
+
const targetId = hostToNodeId.get(site.host);
|
|
3708
3847
|
if (!targetId || targetId === service.node.id) continue;
|
|
3709
|
-
|
|
3710
|
-
|
|
3848
|
+
const dedupKey = `${relFile}|${targetId}`;
|
|
3849
|
+
if (seen.has(dedupKey)) continue;
|
|
3850
|
+
seen.add(dedupKey);
|
|
3851
|
+
const confidence = (0, import_types10.confidenceForExtracted)("hostname-shape-match");
|
|
3852
|
+
const ev = {
|
|
3853
|
+
file: relFile,
|
|
3854
|
+
line: site.line,
|
|
3855
|
+
snippet: snippet(file.content, site.line)
|
|
3856
|
+
};
|
|
3857
|
+
if (!(0, import_types10.passesExtractedFloor)(confidence)) {
|
|
3858
|
+
noteExtractedDropped({
|
|
3859
|
+
source: service.node.id,
|
|
3860
|
+
target: targetId,
|
|
3861
|
+
type: import_types10.EdgeType.CALLS,
|
|
3862
|
+
confidence,
|
|
3863
|
+
confidenceKind: "hostname-shape-match",
|
|
3864
|
+
evidence: ev
|
|
3865
|
+
});
|
|
3866
|
+
continue;
|
|
3867
|
+
}
|
|
3868
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
3869
|
+
graph,
|
|
3870
|
+
service.pkg.name,
|
|
3871
|
+
service.node.id,
|
|
3872
|
+
relFile
|
|
3873
|
+
);
|
|
3874
|
+
nodesAdded += n;
|
|
3875
|
+
edgesAdded += e;
|
|
3876
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, targetId, import_types10.EdgeType.CALLS);
|
|
3877
|
+
if (!graph.hasEdge(edgeId)) {
|
|
3878
|
+
const edge = {
|
|
3879
|
+
id: edgeId,
|
|
3880
|
+
source: fileNodeId,
|
|
3881
|
+
target: targetId,
|
|
3882
|
+
type: import_types10.EdgeType.CALLS,
|
|
3883
|
+
provenance: import_types10.Provenance.EXTRACTED,
|
|
3884
|
+
confidence,
|
|
3885
|
+
evidence: ev
|
|
3886
|
+
};
|
|
3887
|
+
graph.addEdgeWithKey(edgeId, fileNodeId, targetId, edge);
|
|
3888
|
+
edgesAdded++;
|
|
3711
3889
|
}
|
|
3712
|
-
}
|
|
3713
|
-
}
|
|
3714
|
-
for (const [targetId, evidenceFile] of seenTargets) {
|
|
3715
|
-
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
3716
|
-
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3717
|
-
const confidence = (0, import_types9.confidenceForExtracted)("hostname-shape-match");
|
|
3718
|
-
const ev = {
|
|
3719
|
-
file: import_node_path20.default.relative(service.dir, evidenceFile.file),
|
|
3720
|
-
line,
|
|
3721
|
-
snippet: snippet(fileContent, line)
|
|
3722
|
-
};
|
|
3723
|
-
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, targetId, import_types9.EdgeType.CALLS);
|
|
3724
|
-
if (!(0, import_types9.passesExtractedFloor)(confidence)) {
|
|
3725
|
-
noteExtractedDropped({
|
|
3726
|
-
source: service.node.id,
|
|
3727
|
-
target: targetId,
|
|
3728
|
-
type: import_types9.EdgeType.CALLS,
|
|
3729
|
-
confidence,
|
|
3730
|
-
confidenceKind: "hostname-shape-match",
|
|
3731
|
-
evidence: ev
|
|
3732
|
-
});
|
|
3733
|
-
continue;
|
|
3734
|
-
}
|
|
3735
|
-
const edge = {
|
|
3736
|
-
id: edgeId,
|
|
3737
|
-
source: service.node.id,
|
|
3738
|
-
target: targetId,
|
|
3739
|
-
type: import_types9.EdgeType.CALLS,
|
|
3740
|
-
provenance: import_types9.Provenance.EXTRACTED,
|
|
3741
|
-
confidence,
|
|
3742
|
-
evidence: ev
|
|
3743
|
-
};
|
|
3744
|
-
if (!graph.hasEdge(edge.id)) {
|
|
3745
|
-
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
3746
|
-
edgesAdded++;
|
|
3747
3890
|
}
|
|
3748
3891
|
}
|
|
3749
3892
|
}
|
|
3750
|
-
return edgesAdded;
|
|
3893
|
+
return { nodesAdded, edgesAdded };
|
|
3751
3894
|
}
|
|
3752
3895
|
|
|
3753
3896
|
// src/extract/calls/kafka.ts
|
|
3754
3897
|
init_cjs_shims();
|
|
3755
3898
|
var import_node_path21 = __toESM(require("path"), 1);
|
|
3756
|
-
var
|
|
3899
|
+
var import_types11 = require("@neat.is/types");
|
|
3757
3900
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3758
3901
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
3759
3902
|
function findAll(re, text) {
|
|
@@ -3774,7 +3917,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3774
3917
|
seen.add(key);
|
|
3775
3918
|
const line = lineOf(file.content, topic);
|
|
3776
3919
|
out.push({
|
|
3777
|
-
infraId: (0,
|
|
3920
|
+
infraId: (0, import_types11.infraId)("kafka-topic", topic),
|
|
3778
3921
|
name: topic,
|
|
3779
3922
|
kind: "kafka-topic",
|
|
3780
3923
|
edgeType,
|
|
@@ -3797,7 +3940,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3797
3940
|
// src/extract/calls/redis.ts
|
|
3798
3941
|
init_cjs_shims();
|
|
3799
3942
|
var import_node_path22 = __toESM(require("path"), 1);
|
|
3800
|
-
var
|
|
3943
|
+
var import_types12 = require("@neat.is/types");
|
|
3801
3944
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3802
3945
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
3803
3946
|
const out = [];
|
|
@@ -3810,7 +3953,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3810
3953
|
seen.add(host);
|
|
3811
3954
|
const line = lineOf(file.content, host);
|
|
3812
3955
|
out.push({
|
|
3813
|
-
infraId: (0,
|
|
3956
|
+
infraId: (0, import_types12.infraId)("redis", host),
|
|
3814
3957
|
name: host,
|
|
3815
3958
|
kind: "redis",
|
|
3816
3959
|
edgeType: "CALLS",
|
|
@@ -3831,7 +3974,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3831
3974
|
// src/extract/calls/aws.ts
|
|
3832
3975
|
init_cjs_shims();
|
|
3833
3976
|
var import_node_path23 = __toESM(require("path"), 1);
|
|
3834
|
-
var
|
|
3977
|
+
var import_types13 = require("@neat.is/types");
|
|
3835
3978
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3836
3979
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3837
3980
|
function hasMarker(text, markers) {
|
|
@@ -3855,7 +3998,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3855
3998
|
seen.add(key);
|
|
3856
3999
|
const line = lineOf(file.content, name);
|
|
3857
4000
|
out.push({
|
|
3858
|
-
infraId: (0,
|
|
4001
|
+
infraId: (0, import_types13.infraId)(kind, name),
|
|
3859
4002
|
name,
|
|
3860
4003
|
kind,
|
|
3861
4004
|
edgeType: "CALLS",
|
|
@@ -3890,7 +4033,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3890
4033
|
// src/extract/calls/grpc.ts
|
|
3891
4034
|
init_cjs_shims();
|
|
3892
4035
|
var import_node_path24 = __toESM(require("path"), 1);
|
|
3893
|
-
var
|
|
4036
|
+
var import_types14 = require("@neat.is/types");
|
|
3894
4037
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3895
4038
|
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3896
4039
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
@@ -3938,7 +4081,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3938
4081
|
const { kind } = classified;
|
|
3939
4082
|
const line = lineOf(file.content, m[0]);
|
|
3940
4083
|
out.push({
|
|
3941
|
-
infraId: (0,
|
|
4084
|
+
infraId: (0, import_types14.infraId)(kind, name),
|
|
3942
4085
|
name,
|
|
3943
4086
|
kind,
|
|
3944
4087
|
edgeType: "CALLS",
|
|
@@ -3960,11 +4103,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3960
4103
|
function edgeTypeFromEndpoint(ep) {
|
|
3961
4104
|
switch (ep.edgeType) {
|
|
3962
4105
|
case "PUBLISHES_TO":
|
|
3963
|
-
return
|
|
4106
|
+
return import_types15.EdgeType.PUBLISHES_TO;
|
|
3964
4107
|
case "CONSUMES_FROM":
|
|
3965
|
-
return
|
|
4108
|
+
return import_types15.EdgeType.CONSUMES_FROM;
|
|
3966
4109
|
default:
|
|
3967
|
-
return
|
|
4110
|
+
return import_types15.EdgeType.CALLS;
|
|
3968
4111
|
}
|
|
3969
4112
|
}
|
|
3970
4113
|
function isAwsKind(kind) {
|
|
@@ -3991,7 +4134,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3991
4134
|
if (!graph.hasNode(ep.infraId)) {
|
|
3992
4135
|
const node = {
|
|
3993
4136
|
id: ep.infraId,
|
|
3994
|
-
type:
|
|
4137
|
+
type: import_types15.NodeType.InfraNode,
|
|
3995
4138
|
name: ep.name,
|
|
3996
4139
|
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3997
4140
|
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
@@ -4003,11 +4146,8 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4003
4146
|
nodesAdded++;
|
|
4004
4147
|
}
|
|
4005
4148
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
4006
|
-
const
|
|
4007
|
-
if (
|
|
4008
|
-
seenEdges.add(edgeId);
|
|
4009
|
-
const confidence = (0, import_types14.confidenceForExtracted)(ep.confidenceKind);
|
|
4010
|
-
if (!(0, import_types14.passesExtractedFloor)(confidence)) {
|
|
4149
|
+
const confidence = (0, import_types15.confidenceForExtracted)(ep.confidenceKind);
|
|
4150
|
+
if (!(0, import_types15.passesExtractedFloor)(confidence)) {
|
|
4011
4151
|
noteExtractedDropped({
|
|
4012
4152
|
source: service.node.id,
|
|
4013
4153
|
target: ep.infraId,
|
|
@@ -4018,13 +4158,25 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4018
4158
|
});
|
|
4019
4159
|
continue;
|
|
4020
4160
|
}
|
|
4161
|
+
const relFile = toPosix2(ep.evidence.file);
|
|
4162
|
+
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
4163
|
+
graph,
|
|
4164
|
+
service.pkg.name,
|
|
4165
|
+
service.node.id,
|
|
4166
|
+
relFile
|
|
4167
|
+
);
|
|
4168
|
+
nodesAdded += n;
|
|
4169
|
+
edgesAdded += e;
|
|
4170
|
+
const edgeId = (0, import_types4.extractedEdgeId)(fileNodeId, ep.infraId, edgeType);
|
|
4171
|
+
if (seenEdges.has(edgeId)) continue;
|
|
4172
|
+
seenEdges.add(edgeId);
|
|
4021
4173
|
if (!graph.hasEdge(edgeId)) {
|
|
4022
4174
|
const edge = {
|
|
4023
4175
|
id: edgeId,
|
|
4024
|
-
source:
|
|
4176
|
+
source: fileNodeId,
|
|
4025
4177
|
target: ep.infraId,
|
|
4026
4178
|
type: edgeType,
|
|
4027
|
-
provenance:
|
|
4179
|
+
provenance: import_types15.Provenance.EXTRACTED,
|
|
4028
4180
|
confidence,
|
|
4029
4181
|
evidence: ep.evidence
|
|
4030
4182
|
};
|
|
@@ -4036,11 +4188,11 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
4036
4188
|
return { nodesAdded, edgesAdded };
|
|
4037
4189
|
}
|
|
4038
4190
|
async function addCallEdges(graph, services) {
|
|
4039
|
-
const
|
|
4191
|
+
const http2 = await addHttpCallEdges(graph, services);
|
|
4040
4192
|
const ext = await addExternalEndpointEdges(graph, services);
|
|
4041
4193
|
return {
|
|
4042
|
-
nodesAdded: ext.nodesAdded,
|
|
4043
|
-
edgesAdded:
|
|
4194
|
+
nodesAdded: http2.nodesAdded + ext.nodesAdded,
|
|
4195
|
+
edgesAdded: http2.edgesAdded + ext.edgesAdded
|
|
4044
4196
|
};
|
|
4045
4197
|
}
|
|
4046
4198
|
|
|
@@ -4050,15 +4202,15 @@ init_cjs_shims();
|
|
|
4050
4202
|
// src/extract/infra/docker-compose.ts
|
|
4051
4203
|
init_cjs_shims();
|
|
4052
4204
|
var import_node_path25 = __toESM(require("path"), 1);
|
|
4053
|
-
var
|
|
4205
|
+
var import_types17 = require("@neat.is/types");
|
|
4054
4206
|
|
|
4055
4207
|
// src/extract/infra/shared.ts
|
|
4056
4208
|
init_cjs_shims();
|
|
4057
|
-
var
|
|
4209
|
+
var import_types16 = require("@neat.is/types");
|
|
4058
4210
|
function makeInfraNode(kind, name, provider = "self", extras) {
|
|
4059
4211
|
return {
|
|
4060
|
-
id: (0,
|
|
4061
|
-
type:
|
|
4212
|
+
id: (0, import_types16.infraId)(kind, name),
|
|
4213
|
+
type: import_types16.NodeType.InfraNode,
|
|
4062
4214
|
name,
|
|
4063
4215
|
provider,
|
|
4064
4216
|
kind,
|
|
@@ -4137,15 +4289,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4137
4289
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
4138
4290
|
const targetId = composeNameToNodeId.get(dep);
|
|
4139
4291
|
if (!targetId) continue;
|
|
4140
|
-
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId,
|
|
4292
|
+
const edgeId = (0, import_types4.extractedEdgeId)(sourceId, targetId, import_types17.EdgeType.DEPENDS_ON);
|
|
4141
4293
|
if (graph.hasEdge(edgeId)) continue;
|
|
4142
4294
|
const edge = {
|
|
4143
4295
|
id: edgeId,
|
|
4144
4296
|
source: sourceId,
|
|
4145
4297
|
target: targetId,
|
|
4146
|
-
type:
|
|
4147
|
-
provenance:
|
|
4148
|
-
confidence: (0,
|
|
4298
|
+
type: import_types17.EdgeType.DEPENDS_ON,
|
|
4299
|
+
provenance: import_types17.Provenance.EXTRACTED,
|
|
4300
|
+
confidence: (0, import_types17.confidenceForExtracted)("structural"),
|
|
4149
4301
|
evidence: { file: evidenceFile }
|
|
4150
4302
|
};
|
|
4151
4303
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -4159,7 +4311,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
4159
4311
|
init_cjs_shims();
|
|
4160
4312
|
var import_node_path26 = __toESM(require("path"), 1);
|
|
4161
4313
|
var import_node_fs14 = require("fs");
|
|
4162
|
-
var
|
|
4314
|
+
var import_types18 = require("@neat.is/types");
|
|
4163
4315
|
function runtimeImage(content) {
|
|
4164
4316
|
const lines = content.split("\n");
|
|
4165
4317
|
let last = null;
|
|
@@ -4198,15 +4350,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
4198
4350
|
graph.addNode(node.id, node);
|
|
4199
4351
|
nodesAdded++;
|
|
4200
4352
|
}
|
|
4201
|
-
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id,
|
|
4353
|
+
const edgeId = (0, import_types4.extractedEdgeId)(service.node.id, node.id, import_types18.EdgeType.RUNS_ON);
|
|
4202
4354
|
if (!graph.hasEdge(edgeId)) {
|
|
4203
4355
|
const edge = {
|
|
4204
4356
|
id: edgeId,
|
|
4205
4357
|
source: service.node.id,
|
|
4206
4358
|
target: node.id,
|
|
4207
|
-
type:
|
|
4208
|
-
provenance:
|
|
4209
|
-
confidence: (0,
|
|
4359
|
+
type: import_types18.EdgeType.RUNS_ON,
|
|
4360
|
+
provenance: import_types18.Provenance.EXTRACTED,
|
|
4361
|
+
confidence: (0, import_types18.confidenceForExtracted)("structural"),
|
|
4210
4362
|
evidence: {
|
|
4211
4363
|
file: import_node_path26.default.relative(scanPath, dockerfilePath).split(import_node_path26.default.sep).join("/")
|
|
4212
4364
|
}
|
|
@@ -4334,17 +4486,29 @@ var import_node_path30 = __toESM(require("path"), 1);
|
|
|
4334
4486
|
init_cjs_shims();
|
|
4335
4487
|
var import_node_fs17 = require("fs");
|
|
4336
4488
|
var import_node_path29 = __toESM(require("path"), 1);
|
|
4337
|
-
var
|
|
4489
|
+
var import_types19 = require("@neat.is/types");
|
|
4490
|
+
function dropOrphanedFileNodes(graph) {
|
|
4491
|
+
const orphans = [];
|
|
4492
|
+
graph.forEachNode((id, attrs) => {
|
|
4493
|
+
if (attrs.type !== import_types19.NodeType.FileNode) return;
|
|
4494
|
+
if (graph.inboundEdges(id).length === 0 && graph.outboundEdges(id).length === 0) {
|
|
4495
|
+
orphans.push(id);
|
|
4496
|
+
}
|
|
4497
|
+
});
|
|
4498
|
+
for (const id of orphans) graph.dropNode(id);
|
|
4499
|
+
return orphans.length;
|
|
4500
|
+
}
|
|
4338
4501
|
function retireEdgesByFile(graph, file) {
|
|
4339
4502
|
const normalized = file.split("\\").join("/");
|
|
4340
4503
|
const toDrop = [];
|
|
4341
4504
|
graph.forEachEdge((id, attrs) => {
|
|
4342
4505
|
const edge = attrs;
|
|
4343
|
-
if (edge.provenance !==
|
|
4506
|
+
if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
|
|
4344
4507
|
if (!edge.evidence?.file) return;
|
|
4345
4508
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4346
4509
|
});
|
|
4347
4510
|
for (const id of toDrop) graph.dropEdge(id);
|
|
4511
|
+
dropOrphanedFileNodes(graph);
|
|
4348
4512
|
return toDrop.length;
|
|
4349
4513
|
}
|
|
4350
4514
|
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
@@ -4352,7 +4516,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4352
4516
|
const bases = [scanPath, ...serviceDirs];
|
|
4353
4517
|
graph.forEachEdge((id, attrs) => {
|
|
4354
4518
|
const edge = attrs;
|
|
4355
|
-
if (edge.provenance !==
|
|
4519
|
+
if (edge.provenance !== import_types19.Provenance.EXTRACTED) return;
|
|
4356
4520
|
const evidenceFile = edge.evidence?.file;
|
|
4357
4521
|
if (!evidenceFile) return;
|
|
4358
4522
|
if (import_node_path29.default.isAbsolute(evidenceFile)) {
|
|
@@ -4363,6 +4527,7 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4363
4527
|
if (!found) toDrop.push(id);
|
|
4364
4528
|
});
|
|
4365
4529
|
for (const id of toDrop) graph.dropEdge(id);
|
|
4530
|
+
dropOrphanedFileNodes(graph);
|
|
4366
4531
|
return toDrop.length;
|
|
4367
4532
|
}
|
|
4368
4533
|
|
|
@@ -4430,7 +4595,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4430
4595
|
|
|
4431
4596
|
// src/divergences.ts
|
|
4432
4597
|
init_cjs_shims();
|
|
4433
|
-
var
|
|
4598
|
+
var import_types20 = require("@neat.is/types");
|
|
4434
4599
|
function bucketKey(source, target, type) {
|
|
4435
4600
|
return `${type}|${source}|${target}`;
|
|
4436
4601
|
}
|
|
@@ -4438,22 +4603,22 @@ function bucketEdges(graph) {
|
|
|
4438
4603
|
const buckets = /* @__PURE__ */ new Map();
|
|
4439
4604
|
graph.forEachEdge((id, attrs) => {
|
|
4440
4605
|
const e = attrs;
|
|
4441
|
-
const parsed = (0,
|
|
4606
|
+
const parsed = (0, import_types20.parseEdgeId)(id);
|
|
4442
4607
|
const provenance = parsed?.provenance ?? e.provenance;
|
|
4443
4608
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4444
4609
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4445
4610
|
switch (provenance) {
|
|
4446
|
-
case
|
|
4611
|
+
case import_types20.Provenance.EXTRACTED:
|
|
4447
4612
|
cur.extracted = e;
|
|
4448
4613
|
break;
|
|
4449
|
-
case
|
|
4614
|
+
case import_types20.Provenance.OBSERVED:
|
|
4450
4615
|
cur.observed = e;
|
|
4451
4616
|
break;
|
|
4452
|
-
case
|
|
4617
|
+
case import_types20.Provenance.INFERRED:
|
|
4453
4618
|
cur.inferred = e;
|
|
4454
4619
|
break;
|
|
4455
4620
|
default:
|
|
4456
|
-
if (e.provenance ===
|
|
4621
|
+
if (e.provenance === import_types20.Provenance.STALE) cur.stale = e;
|
|
4457
4622
|
}
|
|
4458
4623
|
buckets.set(key, cur);
|
|
4459
4624
|
});
|
|
@@ -4462,7 +4627,7 @@ function bucketEdges(graph) {
|
|
|
4462
4627
|
function nodeIsFrontier(graph, nodeId) {
|
|
4463
4628
|
if (!graph.hasNode(nodeId)) return false;
|
|
4464
4629
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
4465
|
-
return attrs.type ===
|
|
4630
|
+
return attrs.type === import_types20.NodeType.FrontierNode;
|
|
4466
4631
|
}
|
|
4467
4632
|
function clampConfidence(n) {
|
|
4468
4633
|
if (!Number.isFinite(n)) return 0;
|
|
@@ -4483,6 +4648,7 @@ function gradedConfidence(edge) {
|
|
|
4483
4648
|
}
|
|
4484
4649
|
function detectMissingDivergences(graph, bucket) {
|
|
4485
4650
|
const out = [];
|
|
4651
|
+
if (bucket.type === import_types20.EdgeType.CONTAINS) return out;
|
|
4486
4652
|
if (bucket.extracted && !bucket.observed) {
|
|
4487
4653
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4488
4654
|
out.push({
|
|
@@ -4523,7 +4689,7 @@ function declaredHostFor(svc) {
|
|
|
4523
4689
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4524
4690
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4525
4691
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4526
|
-
if (e.type ===
|
|
4692
|
+
if (e.type === import_types20.EdgeType.CONFIGURED_BY && e.provenance === import_types20.Provenance.EXTRACTED) {
|
|
4527
4693
|
return true;
|
|
4528
4694
|
}
|
|
4529
4695
|
}
|
|
@@ -4536,10 +4702,10 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4536
4702
|
const out = [];
|
|
4537
4703
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4538
4704
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4539
|
-
if (edge.type !==
|
|
4540
|
-
if (edge.provenance !==
|
|
4705
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4706
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4541
4707
|
const target = graph.getNodeAttributes(edge.target);
|
|
4542
|
-
if (target.type !==
|
|
4708
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4543
4709
|
const observedHost = target.host?.trim();
|
|
4544
4710
|
if (!observedHost) continue;
|
|
4545
4711
|
if (observedHost === declaredHost) continue;
|
|
@@ -4561,10 +4727,10 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4561
4727
|
const deps = svc.dependencies ?? {};
|
|
4562
4728
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4563
4729
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4564
|
-
if (edge.type !==
|
|
4565
|
-
if (edge.provenance !==
|
|
4730
|
+
if (edge.type !== import_types20.EdgeType.CONNECTS_TO) continue;
|
|
4731
|
+
if (edge.provenance !== import_types20.Provenance.OBSERVED) continue;
|
|
4566
4732
|
const target = graph.getNodeAttributes(edge.target);
|
|
4567
|
-
if (target.type !==
|
|
4733
|
+
if (target.type !== import_types20.NodeType.DatabaseNode) continue;
|
|
4568
4734
|
for (const pair of compatPairs()) {
|
|
4569
4735
|
if (pair.engine !== target.engine) continue;
|
|
4570
4736
|
const declared = deps[pair.driver];
|
|
@@ -4625,7 +4791,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4625
4791
|
}
|
|
4626
4792
|
graph.forEachNode((nodeId, attrs) => {
|
|
4627
4793
|
const n = attrs;
|
|
4628
|
-
if (n.type !==
|
|
4794
|
+
if (n.type !== import_types20.NodeType.ServiceNode) return;
|
|
4629
4795
|
const svc = n;
|
|
4630
4796
|
for (const d of detectHostMismatch(graph, nodeId, svc)) all.push(d);
|
|
4631
4797
|
for (const d of detectCompatDivergences(graph, nodeId, svc)) all.push(d);
|
|
@@ -4658,7 +4824,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4658
4824
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4659
4825
|
return a.target.localeCompare(b.target);
|
|
4660
4826
|
});
|
|
4661
|
-
return
|
|
4827
|
+
return import_types20.DivergenceResultSchema.parse({
|
|
4662
4828
|
divergences: filtered,
|
|
4663
4829
|
totalAffected: filtered.length,
|
|
4664
4830
|
computedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -4669,7 +4835,7 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4669
4835
|
init_cjs_shims();
|
|
4670
4836
|
var import_node_fs18 = require("fs");
|
|
4671
4837
|
var import_node_path31 = __toESM(require("path"), 1);
|
|
4672
|
-
var
|
|
4838
|
+
var import_types21 = require("@neat.is/types");
|
|
4673
4839
|
var SCHEMA_VERSION = 4;
|
|
4674
4840
|
function migrateV1ToV2(payload) {
|
|
4675
4841
|
const nodes = payload.graph.nodes;
|
|
@@ -4691,12 +4857,12 @@ function migrateV2ToV3(payload) {
|
|
|
4691
4857
|
for (const edge of edges) {
|
|
4692
4858
|
const attrs = edge.attributes;
|
|
4693
4859
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4694
|
-
attrs.provenance =
|
|
4860
|
+
attrs.provenance = import_types21.Provenance.OBSERVED;
|
|
4695
4861
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4696
4862
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4697
4863
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
4698
4864
|
if (type && source && target) {
|
|
4699
|
-
const newId = (0,
|
|
4865
|
+
const newId = (0, import_types21.observedEdgeId)(source, target, type);
|
|
4700
4866
|
attrs.id = newId;
|
|
4701
4867
|
if (edge.key) edge.key = newId;
|
|
4702
4868
|
}
|
|
@@ -4816,7 +4982,7 @@ ${NEAT_OUT_LINE}
|
|
|
4816
4982
|
|
|
4817
4983
|
// src/summary.ts
|
|
4818
4984
|
init_cjs_shims();
|
|
4819
|
-
var
|
|
4985
|
+
var import_types22 = require("@neat.is/types");
|
|
4820
4986
|
function renderOtelEnvBlock() {
|
|
4821
4987
|
return [
|
|
4822
4988
|
"for prod OTel routing, set these in your deploy platform's env:",
|
|
@@ -4826,19 +4992,19 @@ function renderOtelEnvBlock() {
|
|
|
4826
4992
|
}
|
|
4827
4993
|
function findIncompatServices(nodes) {
|
|
4828
4994
|
return nodes.filter(
|
|
4829
|
-
(n) => n.type ===
|
|
4995
|
+
(n) => n.type === import_types22.NodeType.ServiceNode && Array.isArray(n.incompatibilities) && (n.incompatibilities ?? []).length > 0
|
|
4830
4996
|
);
|
|
4831
4997
|
}
|
|
4832
4998
|
function servicesWithoutObserved(nodes, edges) {
|
|
4833
4999
|
const seen = /* @__PURE__ */ new Set();
|
|
4834
5000
|
for (const e of edges) {
|
|
4835
|
-
if (e.provenance ===
|
|
5001
|
+
if (e.provenance === import_types22.Provenance.OBSERVED) {
|
|
4836
5002
|
seen.add(e.source);
|
|
4837
5003
|
seen.add(e.target);
|
|
4838
5004
|
}
|
|
4839
5005
|
}
|
|
4840
5006
|
return nodes.filter(
|
|
4841
|
-
(n) => n.type ===
|
|
5007
|
+
(n) => n.type === import_types22.NodeType.ServiceNode && !seen.has(n.id)
|
|
4842
5008
|
);
|
|
4843
5009
|
}
|
|
4844
5010
|
function formatDivergence(d) {
|
|
@@ -4920,7 +5086,7 @@ var import_chokidar = __toESM(require("chokidar"), 1);
|
|
|
4920
5086
|
init_cjs_shims();
|
|
4921
5087
|
var import_fastify = __toESM(require("fastify"), 1);
|
|
4922
5088
|
var import_cors = __toESM(require("@fastify/cors"), 1);
|
|
4923
|
-
var
|
|
5089
|
+
var import_types24 = require("@neat.is/types");
|
|
4924
5090
|
|
|
4925
5091
|
// src/diff.ts
|
|
4926
5092
|
init_cjs_shims();
|
|
@@ -5056,7 +5222,7 @@ init_cjs_shims();
|
|
|
5056
5222
|
var import_node_fs21 = require("fs");
|
|
5057
5223
|
var import_node_os2 = __toESM(require("os"), 1);
|
|
5058
5224
|
var import_node_path34 = __toESM(require("path"), 1);
|
|
5059
|
-
var
|
|
5225
|
+
var import_types23 = require("@neat.is/types");
|
|
5060
5226
|
var LOCK_TIMEOUT_MS = 5e3;
|
|
5061
5227
|
var LOCK_RETRY_MS = 50;
|
|
5062
5228
|
function neatHome() {
|
|
@@ -5135,10 +5301,10 @@ async function readRegistry() {
|
|
|
5135
5301
|
throw err;
|
|
5136
5302
|
}
|
|
5137
5303
|
const parsed = JSON.parse(raw);
|
|
5138
|
-
return
|
|
5304
|
+
return import_types23.RegistryFileSchema.parse(parsed);
|
|
5139
5305
|
}
|
|
5140
5306
|
async function writeRegistry(reg) {
|
|
5141
|
-
const validated =
|
|
5307
|
+
const validated = import_types23.RegistryFileSchema.parse(reg);
|
|
5142
5308
|
await writeAtomically(registryPath(), JSON.stringify(validated, null, 2) + "\n");
|
|
5143
5309
|
}
|
|
5144
5310
|
var ProjectNameCollisionError = class extends Error {
|
|
@@ -5401,11 +5567,11 @@ function registerRoutes(scope, ctx) {
|
|
|
5401
5567
|
const candidates = req.query.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
5402
5568
|
const parsed = [];
|
|
5403
5569
|
for (const c of candidates) {
|
|
5404
|
-
const r =
|
|
5570
|
+
const r = import_types24.DivergenceTypeSchema.safeParse(c);
|
|
5405
5571
|
if (!r.success) {
|
|
5406
5572
|
return reply.code(400).send({
|
|
5407
5573
|
error: `unknown divergence type "${c}"`,
|
|
5408
|
-
allowed:
|
|
5574
|
+
allowed: import_types24.DivergenceTypeSchema.options
|
|
5409
5575
|
});
|
|
5410
5576
|
}
|
|
5411
5577
|
parsed.push(r.data);
|
|
@@ -5618,7 +5784,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5618
5784
|
const log = new PolicyViolationsLog(proj.paths.policyViolationsPath);
|
|
5619
5785
|
let violations = await log.readAll();
|
|
5620
5786
|
if (req.query.severity) {
|
|
5621
|
-
const sev =
|
|
5787
|
+
const sev = import_types24.PolicySeveritySchema.safeParse(req.query.severity);
|
|
5622
5788
|
if (!sev.success) {
|
|
5623
5789
|
return reply.code(400).send({
|
|
5624
5790
|
error: "invalid severity",
|
|
@@ -5635,7 +5801,7 @@ function registerRoutes(scope, ctx) {
|
|
|
5635
5801
|
scope.post("/policies/check", async (req, reply) => {
|
|
5636
5802
|
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5637
5803
|
if (!proj) return;
|
|
5638
|
-
const parsed =
|
|
5804
|
+
const parsed = import_types24.PoliciesCheckBodySchema.safeParse(req.body ?? {});
|
|
5639
5805
|
if (!parsed.success) {
|
|
5640
5806
|
return reply.code(400).send({
|
|
5641
5807
|
error: "invalid /policies/check body",
|
|
@@ -6569,38 +6735,119 @@ var import_node_path40 = __toESM(require("path"), 1);
|
|
|
6569
6735
|
// src/installers/templates.ts
|
|
6570
6736
|
init_cjs_shims();
|
|
6571
6737
|
var OTEL_INIT_HEADER = "// Generated by `neat init --apply` (ADR-069). OpenTelemetry auto-instrumentation hook.";
|
|
6738
|
+
var OTEL_INIT_STAMP = "// neat-template-version: 2 \u2014 file-first call-site capture (ADR-089).";
|
|
6739
|
+
function neatCallsiteProcessorSource(ts) {
|
|
6740
|
+
const stackT = ts ? ": string" : "";
|
|
6741
|
+
const spanT = ts ? ": any" : "";
|
|
6742
|
+
const strOpt = ts ? ": string | undefined" : "";
|
|
6743
|
+
return `function __neatPickUserFrame(stack${stackT}) {
|
|
6744
|
+
const lines = String(stack || '').split('\\n')
|
|
6745
|
+
for (let i = 0; i < lines.length; i++) {
|
|
6746
|
+
const raw = lines[i].trim()
|
|
6747
|
+
if (raw.indexOf('at ') !== 0) continue
|
|
6748
|
+
if (raw.indexOf('node_modules') !== -1) continue
|
|
6749
|
+
if (raw.indexOf('@opentelemetry') !== -1) continue
|
|
6750
|
+
if (raw.indexOf('node:') !== -1) continue
|
|
6751
|
+
if (raw.indexOf('NeatCallSiteSpanProcessor') !== -1) continue
|
|
6752
|
+
const bodyText = raw.slice(3)
|
|
6753
|
+
const loc = bodyText.match(/:(\\d+):(\\d+)\\)?$/)
|
|
6754
|
+
if (!loc) continue
|
|
6755
|
+
const paren = bodyText.lastIndexOf('(')
|
|
6756
|
+
let filepath${strOpt}
|
|
6757
|
+
let fn${strOpt}
|
|
6758
|
+
if (paren !== -1) {
|
|
6759
|
+
fn = bodyText.slice(0, paren).trim()
|
|
6760
|
+
if (fn.indexOf('async ') === 0) fn = fn.slice(6)
|
|
6761
|
+
if (fn.indexOf('new ') === 0) fn = fn.slice(4)
|
|
6762
|
+
filepath = bodyText.slice(paren + 1, bodyText.length - loc[0].length)
|
|
6763
|
+
} else {
|
|
6764
|
+
filepath = bodyText.slice(0, bodyText.length - loc[0].length)
|
|
6765
|
+
}
|
|
6766
|
+
if (filepath.indexOf('file://') === 0) filepath = filepath.slice(7)
|
|
6767
|
+
if (!filepath) continue
|
|
6768
|
+
return { filepath: filepath, lineno: Number(loc[1]), function: fn || undefined }
|
|
6769
|
+
}
|
|
6770
|
+
return null
|
|
6771
|
+
}
|
|
6772
|
+
|
|
6773
|
+
class NeatCallSiteSpanProcessor {
|
|
6774
|
+
onStart(span${spanT}) {
|
|
6775
|
+
if (!span || (span.kind !== 2 && span.kind !== 3)) return
|
|
6776
|
+
const frame = __neatPickUserFrame(new Error().stack)
|
|
6777
|
+
if (!frame) return
|
|
6778
|
+
span.setAttribute('code.filepath', frame.filepath)
|
|
6779
|
+
if (typeof frame.lineno === 'number') span.setAttribute('code.lineno', frame.lineno)
|
|
6780
|
+
if (frame.function) span.setAttribute('code.function', frame.function)
|
|
6781
|
+
}
|
|
6782
|
+
onEnd() {}
|
|
6783
|
+
forceFlush() { return Promise.resolve() }
|
|
6784
|
+
shutdown() { return Promise.resolve() }
|
|
6785
|
+
}`;
|
|
6786
|
+
}
|
|
6787
|
+
var CALLSITE_PROCESSOR_JS = neatCallsiteProcessorSource(false);
|
|
6788
|
+
var CALLSITE_PROCESSOR_TS = neatCallsiteProcessorSource(true);
|
|
6789
|
+
function neatRegisterCallsiteSource(ts) {
|
|
6790
|
+
const providerT = ts ? ": any" : "";
|
|
6791
|
+
return `try {
|
|
6792
|
+
const provider${providerT} = trace.getTracerProvider()
|
|
6793
|
+
const delegate = provider && typeof provider.getDelegate === 'function' ? provider.getDelegate() : provider
|
|
6794
|
+
if (delegate && typeof delegate.addSpanProcessor === 'function') {
|
|
6795
|
+
delegate.addSpanProcessor(new NeatCallSiteSpanProcessor())
|
|
6796
|
+
}
|
|
6797
|
+
} catch {
|
|
6798
|
+
// capture is best-effort; span export is unaffected
|
|
6799
|
+
}`;
|
|
6800
|
+
}
|
|
6572
6801
|
var OTEL_INIT_CJS = `${OTEL_INIT_HEADER}
|
|
6802
|
+
${OTEL_INIT_STAMP}
|
|
6573
6803
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
6574
6804
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
6575
6805
|
|
|
6576
6806
|
const { NodeSDK } = require('@opentelemetry/sdk-node')
|
|
6577
6807
|
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node')
|
|
6808
|
+
const { trace } = require('@opentelemetry/api')
|
|
6809
|
+
|
|
6810
|
+
${CALLSITE_PROCESSOR_JS}
|
|
6578
6811
|
|
|
6579
6812
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
6580
6813
|
__INSTRUMENTATION_BLOCK__
|
|
6581
|
-
new NodeSDK({ instrumentations })
|
|
6814
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
6815
|
+
sdk.start()
|
|
6816
|
+
${neatRegisterCallsiteSource(false)}
|
|
6582
6817
|
`;
|
|
6583
6818
|
var OTEL_INIT_ESM = `${OTEL_INIT_HEADER}
|
|
6819
|
+
${OTEL_INIT_STAMP}
|
|
6584
6820
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
6585
6821
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
6586
6822
|
|
|
6587
6823
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6588
6824
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6825
|
+
import { trace } from '@opentelemetry/api'
|
|
6826
|
+
|
|
6827
|
+
${CALLSITE_PROCESSOR_JS}
|
|
6589
6828
|
|
|
6590
6829
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
6591
6830
|
__INSTRUMENTATION_BLOCK__
|
|
6592
|
-
new NodeSDK({ instrumentations })
|
|
6831
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
6832
|
+
sdk.start()
|
|
6833
|
+
${neatRegisterCallsiteSource(false)}
|
|
6593
6834
|
`;
|
|
6594
6835
|
var OTEL_INIT_TS = `${OTEL_INIT_HEADER}
|
|
6836
|
+
${OTEL_INIT_STAMP}
|
|
6595
6837
|
process.env.OTEL_SERVICE_NAME ||= '__SERVICE_NAME__'
|
|
6596
6838
|
process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT ||= 'http://localhost:4318/projects/__PROJECT__/v1/traces'
|
|
6597
6839
|
|
|
6598
6840
|
import { NodeSDK } from '@opentelemetry/sdk-node'
|
|
6599
6841
|
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'
|
|
6842
|
+
import { trace } from '@opentelemetry/api'
|
|
6843
|
+
|
|
6844
|
+
${CALLSITE_PROCESSOR_TS}
|
|
6600
6845
|
|
|
6601
6846
|
const instrumentations = [getNodeAutoInstrumentations()]
|
|
6602
6847
|
__INSTRUMENTATION_BLOCK__
|
|
6603
|
-
new NodeSDK({ instrumentations })
|
|
6848
|
+
const sdk = new NodeSDK({ instrumentations })
|
|
6849
|
+
sdk.start()
|
|
6850
|
+
${neatRegisterCallsiteSource(true)}
|
|
6604
6851
|
`;
|
|
6605
6852
|
function renderNodeOtelInit(template, serviceName, projectName, registrations = []) {
|
|
6606
6853
|
const block = registrations.length === 0 ? "" : `
|
|
@@ -6824,6 +7071,23 @@ async function exists2(p) {
|
|
|
6824
7071
|
return false;
|
|
6825
7072
|
}
|
|
6826
7073
|
}
|
|
7074
|
+
async function readFileMaybe(p) {
|
|
7075
|
+
try {
|
|
7076
|
+
return await import_node_fs25.promises.readFile(p, "utf8");
|
|
7077
|
+
} catch {
|
|
7078
|
+
return null;
|
|
7079
|
+
}
|
|
7080
|
+
}
|
|
7081
|
+
async function planOtelInitGeneration(file, contents) {
|
|
7082
|
+
const existing = await readFileMaybe(file);
|
|
7083
|
+
if (existing === null) {
|
|
7084
|
+
return { file, contents, skipIfExists: true };
|
|
7085
|
+
}
|
|
7086
|
+
if (existing.includes(OTEL_INIT_HEADER) && !existing.includes(OTEL_INIT_STAMP)) {
|
|
7087
|
+
return { file, contents, skipIfExists: false };
|
|
7088
|
+
}
|
|
7089
|
+
return null;
|
|
7090
|
+
}
|
|
6827
7091
|
async function detect(serviceDir) {
|
|
6828
7092
|
const pkg = await readPackageJson(serviceDir);
|
|
6829
7093
|
return pkg !== null && typeof pkg.name === "string";
|
|
@@ -7554,18 +7818,11 @@ async function plan(serviceDir, opts) {
|
|
|
7554
7818
|
const projectName = projectToken(pkg, serviceDir, project);
|
|
7555
7819
|
const registrations = nonBundled.map((i) => i.registration);
|
|
7556
7820
|
const generatedFiles = [];
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7562
|
-
svcName,
|
|
7563
|
-
projectName,
|
|
7564
|
-
registrations
|
|
7565
|
-
),
|
|
7566
|
-
skipIfExists: true
|
|
7567
|
-
});
|
|
7568
|
-
}
|
|
7821
|
+
const otelInitGen = await planOtelInitGeneration(
|
|
7822
|
+
otelInitFile,
|
|
7823
|
+
renderNodeOtelInit(otelInitContents(flavor), svcName, projectName, registrations)
|
|
7824
|
+
);
|
|
7825
|
+
if (otelInitGen) generatedFiles.push(otelInitGen);
|
|
7569
7826
|
if (!await exists2(envNeatFile)) {
|
|
7570
7827
|
generatedFiles.push({
|
|
7571
7828
|
file: envNeatFile,
|
|
@@ -8608,7 +8865,7 @@ var import_node_path44 = __toESM(require("path"), 1);
|
|
|
8608
8865
|
|
|
8609
8866
|
// src/cli-client.ts
|
|
8610
8867
|
init_cjs_shims();
|
|
8611
|
-
var
|
|
8868
|
+
var import_types25 = require("@neat.is/types");
|
|
8612
8869
|
var HttpError = class extends Error {
|
|
8613
8870
|
constructor(status2, message, responseBody = "") {
|
|
8614
8871
|
super(message);
|
|
@@ -8746,7 +9003,7 @@ async function runBlastRadius(client, input) {
|
|
|
8746
9003
|
}
|
|
8747
9004
|
}
|
|
8748
9005
|
function formatBlastEntry(n) {
|
|
8749
|
-
const tag = n.edgeProvenance ===
|
|
9006
|
+
const tag = n.edgeProvenance === import_types25.Provenance.STALE ? " [STALE \u2014 last seen too long ago]" : "";
|
|
8750
9007
|
return ` \u2022 ${n.nodeId} (distance ${n.distance}, ${n.edgeProvenance})${tag}`;
|
|
8751
9008
|
}
|
|
8752
9009
|
async function runDependencies(client, input) {
|
|
@@ -8792,9 +9049,9 @@ async function runObservedDependencies(client, input) {
|
|
|
8792
9049
|
const edges = await client.get(
|
|
8793
9050
|
projectPath(input.project, `/graph/edges/${encodeURIComponent(input.nodeId)}`)
|
|
8794
9051
|
);
|
|
8795
|
-
const observed = edges.outbound.filter((e) => e.provenance ===
|
|
9052
|
+
const observed = edges.outbound.filter((e) => e.provenance === import_types25.Provenance.OBSERVED);
|
|
8796
9053
|
if (observed.length === 0) {
|
|
8797
|
-
const hasExtracted = edges.outbound.some((e) => e.provenance ===
|
|
9054
|
+
const hasExtracted = edges.outbound.some((e) => e.provenance === import_types25.Provenance.EXTRACTED);
|
|
8798
9055
|
const note = hasExtracted ? " Static (EXTRACTED) dependencies exist but no runtime traffic has been seen \u2014 is OTel running?" : "";
|
|
8799
9056
|
return { summary: `No OBSERVED dependencies for ${input.nodeId}.${note}` };
|
|
8800
9057
|
}
|
|
@@ -8802,7 +9059,7 @@ async function runObservedDependencies(client, input) {
|
|
|
8802
9059
|
return {
|
|
8803
9060
|
summary: `${input.nodeId} has ${observed.length} runtime dependenc${observed.length === 1 ? "y" : "ies"} confirmed by OTel.`,
|
|
8804
9061
|
block: blockLines.join("\n"),
|
|
8805
|
-
provenance:
|
|
9062
|
+
provenance: import_types25.Provenance.OBSERVED
|
|
8806
9063
|
};
|
|
8807
9064
|
} catch (err) {
|
|
8808
9065
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -8856,7 +9113,7 @@ async function runIncidents(client, input) {
|
|
|
8856
9113
|
return {
|
|
8857
9114
|
summary: `${target} has ${body.total} recorded incident${body.total === 1 ? "" : "s"}; showing the ${ordered.length} most recent.`,
|
|
8858
9115
|
block: blockLines.join("\n"),
|
|
8859
|
-
provenance:
|
|
9116
|
+
provenance: import_types25.Provenance.OBSERVED
|
|
8860
9117
|
};
|
|
8861
9118
|
} catch (err) {
|
|
8862
9119
|
if (err instanceof HttpError && err.status === 404) {
|
|
@@ -8965,7 +9222,7 @@ async function runStaleEdges(client, input) {
|
|
|
8965
9222
|
return {
|
|
8966
9223
|
summary: `${events.length} stale-edge transition${events.length === 1 ? "" : "s"} recorded${input.edgeType ? ` for ${input.edgeType}` : ""}.`,
|
|
8967
9224
|
block: blockLines.join("\n"),
|
|
8968
|
-
provenance:
|
|
9225
|
+
provenance: import_types25.Provenance.STALE
|
|
8969
9226
|
};
|
|
8970
9227
|
}
|
|
8971
9228
|
async function runPolicies(client, input) {
|
|
@@ -9279,7 +9536,7 @@ async function runSync(opts) {
|
|
|
9279
9536
|
}
|
|
9280
9537
|
|
|
9281
9538
|
// src/cli.ts
|
|
9282
|
-
var
|
|
9539
|
+
var import_types26 = require("@neat.is/types");
|
|
9283
9540
|
function usage() {
|
|
9284
9541
|
console.log("usage: neat <command> [args] [--project <name>]");
|
|
9285
9542
|
console.log("");
|
|
@@ -10112,10 +10369,10 @@ async function runQueryVerb(cmd, parsed) {
|
|
|
10112
10369
|
const parts = parsed.type.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
|
|
10113
10370
|
const out = [];
|
|
10114
10371
|
for (const p of parts) {
|
|
10115
|
-
const r =
|
|
10372
|
+
const r = import_types26.DivergenceTypeSchema.safeParse(p);
|
|
10116
10373
|
if (!r.success) {
|
|
10117
10374
|
console.error(
|
|
10118
|
-
`neat divergences: unknown --type "${p}". allowed: ${
|
|
10375
|
+
`neat divergences: unknown --type "${p}". allowed: ${import_types26.DivergenceTypeSchema.options.join(", ")}`
|
|
10119
10376
|
);
|
|
10120
10377
|
return 2;
|
|
10121
10378
|
}
|