@neat.is/core 0.4.11 → 0.4.13
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-OJHI33LD.js → chunk-5W7H35JJ.js} +1111 -343
- package/dist/chunk-5W7H35JJ.js.map +1 -0
- package/dist/{chunk-W4RNPPB5.js → chunk-TWNJX26R.js} +2 -2
- package/dist/cli.cjs +1471 -682
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +197 -169
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1145 -380
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +1133 -368
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +1267 -502
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +4 -2
- package/dist/chunk-OJHI33LD.js.map +0 -1
- /package/dist/{chunk-W4RNPPB5.js.map → chunk-TWNJX26R.js.map} +0 -0
|
@@ -454,19 +454,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
454
454
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
455
455
|
let best = { path: [start], edges: [] };
|
|
456
456
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
457
|
-
function step(node,
|
|
458
|
-
if (
|
|
459
|
-
best = { path: [...
|
|
457
|
+
function step(node, path38, edges) {
|
|
458
|
+
if (path38.length > best.path.length) {
|
|
459
|
+
best = { path: [...path38], edges: [...edges] };
|
|
460
460
|
}
|
|
461
|
-
if (
|
|
461
|
+
if (path38.length - 1 >= maxDepth) return;
|
|
462
462
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
463
463
|
for (const [srcId, edge] of incoming) {
|
|
464
464
|
if (visited.has(srcId)) continue;
|
|
465
465
|
visited.add(srcId);
|
|
466
|
-
|
|
466
|
+
path38.push(srcId);
|
|
467
467
|
edges.push(edge);
|
|
468
|
-
step(srcId,
|
|
469
|
-
|
|
468
|
+
step(srcId, path38, edges);
|
|
469
|
+
path38.pop();
|
|
470
470
|
edges.pop();
|
|
471
471
|
visited.delete(srcId);
|
|
472
472
|
}
|
|
@@ -1385,7 +1385,7 @@ function ensureFrontierNode(graph, host, ts) {
|
|
|
1385
1385
|
graph.addNode(id, node);
|
|
1386
1386
|
return id;
|
|
1387
1387
|
}
|
|
1388
|
-
function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
1388
|
+
function upsertObservedEdge(graph, type, source, target, ts, isError = false, evidence) {
|
|
1389
1389
|
if (!graph.hasNode(source) || !graph.hasNode(target)) return null;
|
|
1390
1390
|
const id = makeObservedEdgeId(type, source, target);
|
|
1391
1391
|
if (graph.hasEdge(id)) {
|
|
@@ -1422,7 +1422,10 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1422
1422
|
confidence: confidenceForObservedSignal(signal),
|
|
1423
1423
|
lastObserved: ts,
|
|
1424
1424
|
callCount: 1,
|
|
1425
|
-
signal
|
|
1425
|
+
signal,
|
|
1426
|
+
// Call-site evidence from span code.* semconv (file-awareness.md §4 + §6).
|
|
1427
|
+
// Only set when code.filepath was present on the span — never fabricated.
|
|
1428
|
+
...evidence ? { evidence } : {}
|
|
1426
1429
|
};
|
|
1427
1430
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1428
1431
|
return { edge, created: true };
|
|
@@ -1516,6 +1519,7 @@ async function handleSpan(ctx, span) {
|
|
|
1516
1519
|
const sourceServiceNode = ctx.graph.getNodeAttributes(sourceId);
|
|
1517
1520
|
const callSite = callSiteFromSpan(span, sourceServiceNode, ctx.scanPath);
|
|
1518
1521
|
const observedSource = () => callSite ? ensureObservedFileNode(ctx.graph, span.service, sourceId, callSite) : sourceId;
|
|
1522
|
+
const callSiteEvidence = callSite ? { file: callSite.relPath, ...callSite.line !== void 0 ? { line: callSite.line } : {} } : void 0;
|
|
1519
1523
|
let affectedNode = sourceId;
|
|
1520
1524
|
const mintsFromCallerSide = spanMintsObservedEdge(span.kind);
|
|
1521
1525
|
if (span.dbSystem) {
|
|
@@ -1529,7 +1533,8 @@ async function handleSpan(ctx, span) {
|
|
|
1529
1533
|
observedSource(),
|
|
1530
1534
|
targetId,
|
|
1531
1535
|
ts,
|
|
1532
|
-
isError
|
|
1536
|
+
isError,
|
|
1537
|
+
callSiteEvidence
|
|
1533
1538
|
);
|
|
1534
1539
|
if (result) affectedNode = targetId;
|
|
1535
1540
|
}
|
|
@@ -1545,7 +1550,8 @@ async function handleSpan(ctx, span) {
|
|
|
1545
1550
|
observedSource(),
|
|
1546
1551
|
targetId,
|
|
1547
1552
|
ts,
|
|
1548
|
-
isError
|
|
1553
|
+
isError,
|
|
1554
|
+
callSiteEvidence
|
|
1549
1555
|
);
|
|
1550
1556
|
affectedNode = targetId;
|
|
1551
1557
|
resolvedViaAddress = true;
|
|
@@ -1557,7 +1563,8 @@ async function handleSpan(ctx, span) {
|
|
|
1557
1563
|
observedSource(),
|
|
1558
1564
|
frontierNodeId,
|
|
1559
1565
|
ts,
|
|
1560
|
-
isError
|
|
1566
|
+
isError,
|
|
1567
|
+
callSiteEvidence
|
|
1561
1568
|
);
|
|
1562
1569
|
affectedNode = frontierNodeId;
|
|
1563
1570
|
resolvedViaAddress = true;
|
|
@@ -2512,20 +2519,445 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2512
2519
|
await collectK8sAliases(graph, scanPath, byName);
|
|
2513
2520
|
}
|
|
2514
2521
|
|
|
2515
|
-
// src/extract/
|
|
2516
|
-
import
|
|
2522
|
+
// src/extract/calls/shared.ts
|
|
2523
|
+
import { promises as fs10 } from "fs";
|
|
2524
|
+
import path10 from "path";
|
|
2517
2525
|
import {
|
|
2518
2526
|
EdgeType as EdgeType4,
|
|
2519
2527
|
NodeType as NodeType6,
|
|
2520
2528
|
Provenance as Provenance2,
|
|
2529
|
+
confidenceForExtracted,
|
|
2530
|
+
extractedEdgeId as extractedEdgeId3,
|
|
2531
|
+
fileId as fileId2
|
|
2532
|
+
} from "@neat.is/types";
|
|
2533
|
+
async function walkSourceFiles(dir) {
|
|
2534
|
+
const out = [];
|
|
2535
|
+
async function walk(current) {
|
|
2536
|
+
const entries = await fs10.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2537
|
+
for (const entry of entries) {
|
|
2538
|
+
const full = path10.join(current, entry.name);
|
|
2539
|
+
if (entry.isDirectory()) {
|
|
2540
|
+
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2541
|
+
if (await isPythonVenvDir(full)) continue;
|
|
2542
|
+
await walk(full);
|
|
2543
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path10.extname(entry.name))) {
|
|
2544
|
+
out.push(full);
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
await walk(dir);
|
|
2549
|
+
return out;
|
|
2550
|
+
}
|
|
2551
|
+
async function loadSourceFiles(dir) {
|
|
2552
|
+
const paths = await walkSourceFiles(dir);
|
|
2553
|
+
const out = [];
|
|
2554
|
+
for (const p of paths) {
|
|
2555
|
+
try {
|
|
2556
|
+
const content = await fs10.readFile(p, "utf8");
|
|
2557
|
+
out.push({ path: p, content });
|
|
2558
|
+
} catch {
|
|
2559
|
+
}
|
|
2560
|
+
}
|
|
2561
|
+
return out;
|
|
2562
|
+
}
|
|
2563
|
+
function lineOf(text, needle) {
|
|
2564
|
+
const idx = text.indexOf(needle);
|
|
2565
|
+
if (idx < 0) return 1;
|
|
2566
|
+
return text.slice(0, idx).split("\n").length;
|
|
2567
|
+
}
|
|
2568
|
+
function snippet(text, line) {
|
|
2569
|
+
const lines = text.split("\n");
|
|
2570
|
+
return (lines[line - 1] ?? "").trim();
|
|
2571
|
+
}
|
|
2572
|
+
function toPosix2(p) {
|
|
2573
|
+
return p.split("\\").join("/");
|
|
2574
|
+
}
|
|
2575
|
+
function languageForPath(relPath) {
|
|
2576
|
+
switch (path10.extname(relPath).toLowerCase()) {
|
|
2577
|
+
case ".py":
|
|
2578
|
+
return "python";
|
|
2579
|
+
case ".ts":
|
|
2580
|
+
case ".tsx":
|
|
2581
|
+
return "typescript";
|
|
2582
|
+
case ".js":
|
|
2583
|
+
case ".jsx":
|
|
2584
|
+
case ".mjs":
|
|
2585
|
+
case ".cjs":
|
|
2586
|
+
return "javascript";
|
|
2587
|
+
default:
|
|
2588
|
+
return void 0;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
function ensureFileNode(graph, serviceName, serviceNodeId, relPath) {
|
|
2592
|
+
let nodesAdded = 0;
|
|
2593
|
+
let edgesAdded = 0;
|
|
2594
|
+
const fileNodeId = fileId2(serviceName, relPath);
|
|
2595
|
+
if (!graph.hasNode(fileNodeId)) {
|
|
2596
|
+
const language = languageForPath(relPath);
|
|
2597
|
+
const node = {
|
|
2598
|
+
id: fileNodeId,
|
|
2599
|
+
type: NodeType6.FileNode,
|
|
2600
|
+
service: serviceName,
|
|
2601
|
+
path: relPath,
|
|
2602
|
+
...language ? { language } : {},
|
|
2603
|
+
discoveredVia: "static"
|
|
2604
|
+
};
|
|
2605
|
+
graph.addNode(fileNodeId, node);
|
|
2606
|
+
nodesAdded++;
|
|
2607
|
+
}
|
|
2608
|
+
const containsId = extractedEdgeId3(serviceNodeId, fileNodeId, EdgeType4.CONTAINS);
|
|
2609
|
+
if (!graph.hasEdge(containsId)) {
|
|
2610
|
+
const edge = {
|
|
2611
|
+
id: containsId,
|
|
2612
|
+
source: serviceNodeId,
|
|
2613
|
+
target: fileNodeId,
|
|
2614
|
+
type: EdgeType4.CONTAINS,
|
|
2615
|
+
provenance: Provenance2.EXTRACTED,
|
|
2616
|
+
confidence: confidenceForExtracted("structural"),
|
|
2617
|
+
evidence: { file: relPath }
|
|
2618
|
+
};
|
|
2619
|
+
graph.addEdgeWithKey(containsId, serviceNodeId, fileNodeId, edge);
|
|
2620
|
+
edgesAdded++;
|
|
2621
|
+
}
|
|
2622
|
+
return { fileNodeId, nodesAdded, edgesAdded };
|
|
2623
|
+
}
|
|
2624
|
+
|
|
2625
|
+
// src/extract/files.ts
|
|
2626
|
+
import path11 from "path";
|
|
2627
|
+
async function addFiles(graph, services) {
|
|
2628
|
+
let nodesAdded = 0;
|
|
2629
|
+
let edgesAdded = 0;
|
|
2630
|
+
for (const service of services) {
|
|
2631
|
+
const filePaths = await walkSourceFiles(service.dir);
|
|
2632
|
+
for (const filePath of filePaths) {
|
|
2633
|
+
const relPath = toPosix2(path11.relative(service.dir, filePath));
|
|
2634
|
+
const { nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
2635
|
+
graph,
|
|
2636
|
+
service.pkg.name,
|
|
2637
|
+
service.node.id,
|
|
2638
|
+
relPath
|
|
2639
|
+
);
|
|
2640
|
+
nodesAdded += n;
|
|
2641
|
+
edgesAdded += e;
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
return { nodesAdded, edgesAdded };
|
|
2645
|
+
}
|
|
2646
|
+
|
|
2647
|
+
// src/extract/imports.ts
|
|
2648
|
+
import path12 from "path";
|
|
2649
|
+
import { promises as fs11 } from "fs";
|
|
2650
|
+
import Parser from "tree-sitter";
|
|
2651
|
+
import JavaScript from "tree-sitter-javascript";
|
|
2652
|
+
import Python from "tree-sitter-python";
|
|
2653
|
+
import {
|
|
2654
|
+
EdgeType as EdgeType5,
|
|
2655
|
+
Provenance as Provenance3,
|
|
2656
|
+
confidenceForExtracted as confidenceForExtracted2,
|
|
2657
|
+
extractedEdgeId as extractedEdgeId4,
|
|
2658
|
+
fileId as fileId3
|
|
2659
|
+
} from "@neat.is/types";
|
|
2660
|
+
var PARSE_CHUNK = 16384;
|
|
2661
|
+
function parseSource(parser, source) {
|
|
2662
|
+
return parser.parse(
|
|
2663
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK)
|
|
2664
|
+
);
|
|
2665
|
+
}
|
|
2666
|
+
function makeJsParser() {
|
|
2667
|
+
const p = new Parser();
|
|
2668
|
+
p.setLanguage(JavaScript);
|
|
2669
|
+
return p;
|
|
2670
|
+
}
|
|
2671
|
+
function makePyParser() {
|
|
2672
|
+
const p = new Parser();
|
|
2673
|
+
p.setLanguage(Python);
|
|
2674
|
+
return p;
|
|
2675
|
+
}
|
|
2676
|
+
function stringLiteralText(node) {
|
|
2677
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2678
|
+
const child = node.child(i);
|
|
2679
|
+
if (child?.type === "string_fragment") return child.text;
|
|
2680
|
+
}
|
|
2681
|
+
const raw = node.text;
|
|
2682
|
+
if (raw.length >= 2) return raw.slice(1, -1);
|
|
2683
|
+
return raw.length === 0 ? null : "";
|
|
2684
|
+
}
|
|
2685
|
+
function clipSnippet(text) {
|
|
2686
|
+
const oneLine = text.split("\n")[0] ?? text;
|
|
2687
|
+
return oneLine.length > 120 ? oneLine.slice(0, 120) : oneLine;
|
|
2688
|
+
}
|
|
2689
|
+
function collectJsImports(node, out) {
|
|
2690
|
+
if (node.type === "import_statement") {
|
|
2691
|
+
const source = node.childForFieldName("source");
|
|
2692
|
+
if (source) {
|
|
2693
|
+
const specifier = stringLiteralText(source);
|
|
2694
|
+
if (specifier) {
|
|
2695
|
+
out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
return;
|
|
2699
|
+
}
|
|
2700
|
+
if (node.type === "call_expression") {
|
|
2701
|
+
const fn = node.childForFieldName("function");
|
|
2702
|
+
if (fn?.type === "identifier" && fn.text === "require") {
|
|
2703
|
+
const args = node.childForFieldName("arguments");
|
|
2704
|
+
const firstArg = args?.namedChild(0);
|
|
2705
|
+
if (firstArg?.type === "string") {
|
|
2706
|
+
const specifier = stringLiteralText(firstArg);
|
|
2707
|
+
if (specifier) {
|
|
2708
|
+
out.push({ specifier, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2714
|
+
const child = node.namedChild(i);
|
|
2715
|
+
if (child) collectJsImports(child, out);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
function collectPyImports(node, out) {
|
|
2719
|
+
if (node.type === "import_from_statement") {
|
|
2720
|
+
let level = 0;
|
|
2721
|
+
let modulePath = "";
|
|
2722
|
+
let pastFrom = false;
|
|
2723
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
2724
|
+
const child = node.child(i);
|
|
2725
|
+
if (!child) continue;
|
|
2726
|
+
if (!pastFrom) {
|
|
2727
|
+
if (child.type === "from") pastFrom = true;
|
|
2728
|
+
continue;
|
|
2729
|
+
}
|
|
2730
|
+
if (child.type === "import") break;
|
|
2731
|
+
if (child.type === "relative_import") {
|
|
2732
|
+
for (let j = 0; j < child.childCount; j++) {
|
|
2733
|
+
const rc = child.child(j);
|
|
2734
|
+
if (!rc) continue;
|
|
2735
|
+
if (rc.type === "import_prefix") {
|
|
2736
|
+
for (let k = 0; k < rc.childCount; k++) {
|
|
2737
|
+
if (rc.child(k)?.type === ".") level++;
|
|
2738
|
+
}
|
|
2739
|
+
} else if (rc.type === "dotted_name") modulePath = rc.text;
|
|
2740
|
+
}
|
|
2741
|
+
break;
|
|
2742
|
+
}
|
|
2743
|
+
if (child.type === "dotted_name") {
|
|
2744
|
+
modulePath = child.text;
|
|
2745
|
+
break;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
if (level > 0 || modulePath) {
|
|
2749
|
+
out.push({ modulePath, level, line: node.startPosition.row + 1, snippet: clipSnippet(node.text) });
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2753
|
+
const child = node.namedChild(i);
|
|
2754
|
+
if (child) collectPyImports(child, out);
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
async function fileExists(p) {
|
|
2758
|
+
try {
|
|
2759
|
+
await fs11.access(p);
|
|
2760
|
+
return true;
|
|
2761
|
+
} catch {
|
|
2762
|
+
return false;
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
function isWithinServiceDir(candidate, serviceDir) {
|
|
2766
|
+
const rel = path12.relative(serviceDir, candidate);
|
|
2767
|
+
return rel !== "" && !rel.startsWith("..") && !path12.isAbsolute(rel);
|
|
2768
|
+
}
|
|
2769
|
+
var JS_EXTENSIONS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
|
|
2770
|
+
var JS_INDEX_FILES = JS_EXTENSIONS.map((ext) => `index${ext}`);
|
|
2771
|
+
async function firstExistingCandidate(base, serviceDir) {
|
|
2772
|
+
for (const ext of JS_EXTENSIONS) {
|
|
2773
|
+
const candidate = base + ext;
|
|
2774
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
2775
|
+
return toPosix2(path12.relative(serviceDir, candidate));
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
for (const indexFile of JS_INDEX_FILES) {
|
|
2779
|
+
const candidate = path12.join(base, indexFile);
|
|
2780
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
2781
|
+
return toPosix2(path12.relative(serviceDir, candidate));
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return null;
|
|
2785
|
+
}
|
|
2786
|
+
async function loadTsPathConfig(serviceDir) {
|
|
2787
|
+
const tsconfigPath = path12.join(serviceDir, "tsconfig.json");
|
|
2788
|
+
let raw;
|
|
2789
|
+
try {
|
|
2790
|
+
raw = await fs11.readFile(tsconfigPath, "utf8");
|
|
2791
|
+
} catch {
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2794
|
+
try {
|
|
2795
|
+
const parsed = JSON.parse(raw);
|
|
2796
|
+
const paths = parsed.compilerOptions?.paths;
|
|
2797
|
+
if (!paths || Object.keys(paths).length === 0) return null;
|
|
2798
|
+
const baseUrl = parsed.compilerOptions?.baseUrl;
|
|
2799
|
+
return { paths, baseDir: baseUrl ? path12.resolve(serviceDir, baseUrl) : serviceDir };
|
|
2800
|
+
} catch (err) {
|
|
2801
|
+
recordExtractionError("import alias resolution", tsconfigPath, err);
|
|
2802
|
+
return null;
|
|
2803
|
+
}
|
|
2804
|
+
}
|
|
2805
|
+
async function resolveTsAlias(specifier, config, serviceDir) {
|
|
2806
|
+
for (const [pattern, targets] of Object.entries(config.paths)) {
|
|
2807
|
+
let suffix = null;
|
|
2808
|
+
if (pattern === specifier) {
|
|
2809
|
+
suffix = "";
|
|
2810
|
+
} else if (pattern.endsWith("/*")) {
|
|
2811
|
+
const prefix = pattern.slice(0, -1);
|
|
2812
|
+
if (specifier.startsWith(prefix)) suffix = specifier.slice(prefix.length);
|
|
2813
|
+
}
|
|
2814
|
+
if (suffix === null) continue;
|
|
2815
|
+
for (const target of targets) {
|
|
2816
|
+
const targetBase = target.endsWith("/*") ? target.slice(0, -2) : target.replace(/\*$/, "");
|
|
2817
|
+
const resolvedBase = path12.resolve(config.baseDir, targetBase, suffix);
|
|
2818
|
+
const hit = await firstExistingCandidate(resolvedBase, serviceDir);
|
|
2819
|
+
if (hit) return hit;
|
|
2820
|
+
if (isWithinServiceDir(resolvedBase, serviceDir) && await fileExists(resolvedBase)) {
|
|
2821
|
+
return toPosix2(path12.relative(serviceDir, resolvedBase));
|
|
2822
|
+
}
|
|
2823
|
+
}
|
|
2824
|
+
}
|
|
2825
|
+
return null;
|
|
2826
|
+
}
|
|
2827
|
+
async function resolveJsImport(specifier, importerDir, serviceDir, tsPaths) {
|
|
2828
|
+
if (!specifier) return null;
|
|
2829
|
+
if (specifier.startsWith("./") || specifier.startsWith("../")) {
|
|
2830
|
+
const base = path12.resolve(importerDir, specifier);
|
|
2831
|
+
const ext = path12.extname(specifier);
|
|
2832
|
+
if (ext) {
|
|
2833
|
+
if (ext === ".js" || ext === ".jsx") {
|
|
2834
|
+
const tsExt = ext === ".jsx" ? ".tsx" : ".ts";
|
|
2835
|
+
const tsSibling = base.slice(0, -ext.length) + tsExt;
|
|
2836
|
+
if (isWithinServiceDir(tsSibling, serviceDir) && await fileExists(tsSibling)) {
|
|
2837
|
+
return toPosix2(path12.relative(serviceDir, tsSibling));
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
if (isWithinServiceDir(base, serviceDir) && await fileExists(base)) {
|
|
2841
|
+
return toPosix2(path12.relative(serviceDir, base));
|
|
2842
|
+
}
|
|
2843
|
+
return null;
|
|
2844
|
+
}
|
|
2845
|
+
return firstExistingCandidate(base, serviceDir);
|
|
2846
|
+
}
|
|
2847
|
+
if (tsPaths) return resolveTsAlias(specifier, tsPaths, serviceDir);
|
|
2848
|
+
return null;
|
|
2849
|
+
}
|
|
2850
|
+
async function resolvePyImport(imp, importerPath, serviceDir) {
|
|
2851
|
+
if (!imp.modulePath) return null;
|
|
2852
|
+
const relPath = imp.modulePath.split(".").join("/");
|
|
2853
|
+
let baseDir;
|
|
2854
|
+
if (imp.level > 0) {
|
|
2855
|
+
baseDir = path12.dirname(importerPath);
|
|
2856
|
+
for (let i = 1; i < imp.level; i++) baseDir = path12.dirname(baseDir);
|
|
2857
|
+
} else {
|
|
2858
|
+
baseDir = serviceDir;
|
|
2859
|
+
}
|
|
2860
|
+
const candidates = [path12.join(baseDir, `${relPath}.py`), path12.join(baseDir, relPath, "__init__.py")];
|
|
2861
|
+
for (const candidate of candidates) {
|
|
2862
|
+
if (isWithinServiceDir(candidate, serviceDir) && await fileExists(candidate)) {
|
|
2863
|
+
return toPosix2(path12.relative(serviceDir, candidate));
|
|
2864
|
+
}
|
|
2865
|
+
}
|
|
2866
|
+
return null;
|
|
2867
|
+
}
|
|
2868
|
+
function emitImportEdge(graph, serviceName, importerFileId, importerRelPath, importeeRelPath, line, snippet2) {
|
|
2869
|
+
const importeeFileId = fileId3(serviceName, importeeRelPath);
|
|
2870
|
+
if (!graph.hasNode(importeeFileId)) return 0;
|
|
2871
|
+
const edgeId = extractedEdgeId4(importerFileId, importeeFileId, EdgeType5.IMPORTS);
|
|
2872
|
+
if (graph.hasEdge(edgeId)) return 0;
|
|
2873
|
+
const edge = {
|
|
2874
|
+
id: edgeId,
|
|
2875
|
+
source: importerFileId,
|
|
2876
|
+
target: importeeFileId,
|
|
2877
|
+
type: EdgeType5.IMPORTS,
|
|
2878
|
+
provenance: Provenance3.EXTRACTED,
|
|
2879
|
+
confidence: confidenceForExtracted2("structural"),
|
|
2880
|
+
evidence: { file: importerRelPath, line, snippet: snippet2 }
|
|
2881
|
+
};
|
|
2882
|
+
graph.addEdgeWithKey(edgeId, importerFileId, importeeFileId, edge);
|
|
2883
|
+
return 1;
|
|
2884
|
+
}
|
|
2885
|
+
async function addImports(graph, services) {
|
|
2886
|
+
const jsParser = makeJsParser();
|
|
2887
|
+
const pyParser = makePyParser();
|
|
2888
|
+
let edgesAdded = 0;
|
|
2889
|
+
for (const service of services) {
|
|
2890
|
+
const tsPaths = await loadTsPathConfig(service.dir);
|
|
2891
|
+
const files = await loadSourceFiles(service.dir);
|
|
2892
|
+
for (const file of files) {
|
|
2893
|
+
if (isTestPath(file.path)) continue;
|
|
2894
|
+
const relFile = toPosix2(path12.relative(service.dir, file.path));
|
|
2895
|
+
const importerFileId = fileId3(service.pkg.name, relFile);
|
|
2896
|
+
const isPython = path12.extname(file.path) === ".py";
|
|
2897
|
+
if (isPython) {
|
|
2898
|
+
let pyImports = [];
|
|
2899
|
+
try {
|
|
2900
|
+
const tree = parseSource(pyParser, file.content);
|
|
2901
|
+
collectPyImports(tree.rootNode, pyImports);
|
|
2902
|
+
} catch (err) {
|
|
2903
|
+
recordExtractionError("import extraction", file.path, err);
|
|
2904
|
+
continue;
|
|
2905
|
+
}
|
|
2906
|
+
for (const imp of pyImports) {
|
|
2907
|
+
const resolved = await resolvePyImport(imp, file.path, service.dir);
|
|
2908
|
+
if (!resolved) continue;
|
|
2909
|
+
edgesAdded += emitImportEdge(
|
|
2910
|
+
graph,
|
|
2911
|
+
service.pkg.name,
|
|
2912
|
+
importerFileId,
|
|
2913
|
+
relFile,
|
|
2914
|
+
resolved,
|
|
2915
|
+
imp.line,
|
|
2916
|
+
imp.snippet
|
|
2917
|
+
);
|
|
2918
|
+
}
|
|
2919
|
+
continue;
|
|
2920
|
+
}
|
|
2921
|
+
let jsImports = [];
|
|
2922
|
+
try {
|
|
2923
|
+
const tree = parseSource(jsParser, file.content);
|
|
2924
|
+
collectJsImports(tree.rootNode, jsImports);
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
recordExtractionError("import extraction", file.path, err);
|
|
2927
|
+
continue;
|
|
2928
|
+
}
|
|
2929
|
+
for (const imp of jsImports) {
|
|
2930
|
+
const resolved = await resolveJsImport(imp.specifier, path12.dirname(file.path), service.dir, tsPaths);
|
|
2931
|
+
if (!resolved) continue;
|
|
2932
|
+
edgesAdded += emitImportEdge(
|
|
2933
|
+
graph,
|
|
2934
|
+
service.pkg.name,
|
|
2935
|
+
importerFileId,
|
|
2936
|
+
relFile,
|
|
2937
|
+
resolved,
|
|
2938
|
+
imp.line,
|
|
2939
|
+
imp.snippet
|
|
2940
|
+
);
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
return { nodesAdded: 0, edgesAdded };
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// src/extract/databases/index.ts
|
|
2948
|
+
import path20 from "path";
|
|
2949
|
+
import {
|
|
2950
|
+
EdgeType as EdgeType6,
|
|
2951
|
+
NodeType as NodeType7,
|
|
2952
|
+
Provenance as Provenance4,
|
|
2521
2953
|
databaseId as databaseId2,
|
|
2522
|
-
confidenceForExtracted
|
|
2954
|
+
confidenceForExtracted as confidenceForExtracted3
|
|
2523
2955
|
} from "@neat.is/types";
|
|
2524
2956
|
|
|
2525
2957
|
// src/extract/databases/db-config-yaml.ts
|
|
2526
|
-
import
|
|
2958
|
+
import path13 from "path";
|
|
2527
2959
|
async function parse(serviceDir) {
|
|
2528
|
-
const yamlPath =
|
|
2960
|
+
const yamlPath = path13.join(serviceDir, "db-config.yaml");
|
|
2529
2961
|
if (!await exists(yamlPath)) return [];
|
|
2530
2962
|
const raw = await readYaml(yamlPath);
|
|
2531
2963
|
return [
|
|
@@ -2542,12 +2974,12 @@ async function parse(serviceDir) {
|
|
|
2542
2974
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
2543
2975
|
|
|
2544
2976
|
// src/extract/databases/dotenv.ts
|
|
2545
|
-
import { promises as
|
|
2546
|
-
import
|
|
2977
|
+
import { promises as fs13 } from "fs";
|
|
2978
|
+
import path15 from "path";
|
|
2547
2979
|
|
|
2548
2980
|
// src/extract/databases/shared.ts
|
|
2549
|
-
import { promises as
|
|
2550
|
-
import
|
|
2981
|
+
import { promises as fs12 } from "fs";
|
|
2982
|
+
import path14 from "path";
|
|
2551
2983
|
function schemeToEngine(scheme) {
|
|
2552
2984
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2553
2985
|
switch (s) {
|
|
@@ -2586,14 +3018,14 @@ function parseConnectionString(url) {
|
|
|
2586
3018
|
}
|
|
2587
3019
|
async function readIfExists(filePath) {
|
|
2588
3020
|
try {
|
|
2589
|
-
return await
|
|
3021
|
+
return await fs12.readFile(filePath, "utf8");
|
|
2590
3022
|
} catch {
|
|
2591
3023
|
return null;
|
|
2592
3024
|
}
|
|
2593
3025
|
}
|
|
2594
3026
|
async function findFirst(serviceDir, candidates) {
|
|
2595
3027
|
for (const rel of candidates) {
|
|
2596
|
-
const abs =
|
|
3028
|
+
const abs = path14.join(serviceDir, rel);
|
|
2597
3029
|
const content = await readIfExists(abs);
|
|
2598
3030
|
if (content !== null) return abs;
|
|
2599
3031
|
}
|
|
@@ -2644,15 +3076,15 @@ function parseDotenvLine(line) {
|
|
|
2644
3076
|
return { key, value };
|
|
2645
3077
|
}
|
|
2646
3078
|
async function parse2(serviceDir) {
|
|
2647
|
-
const entries = await
|
|
3079
|
+
const entries = await fs13.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2648
3080
|
const configs = [];
|
|
2649
3081
|
const seen = /* @__PURE__ */ new Set();
|
|
2650
3082
|
for (const entry of entries) {
|
|
2651
3083
|
if (!entry.isFile()) continue;
|
|
2652
3084
|
const match = isConfigFile(entry.name);
|
|
2653
3085
|
if (!match.match || match.fileType !== "env") continue;
|
|
2654
|
-
const filePath =
|
|
2655
|
-
const content = await
|
|
3086
|
+
const filePath = path15.join(serviceDir, entry.name);
|
|
3087
|
+
const content = await fs13.readFile(filePath, "utf8");
|
|
2656
3088
|
for (const line of content.split("\n")) {
|
|
2657
3089
|
const parsed = parseDotenvLine(line);
|
|
2658
3090
|
if (!parsed) continue;
|
|
@@ -2670,9 +3102,9 @@ async function parse2(serviceDir) {
|
|
|
2670
3102
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
2671
3103
|
|
|
2672
3104
|
// src/extract/databases/prisma.ts
|
|
2673
|
-
import
|
|
3105
|
+
import path16 from "path";
|
|
2674
3106
|
async function parse3(serviceDir) {
|
|
2675
|
-
const schemaPath =
|
|
3107
|
+
const schemaPath = path16.join(serviceDir, "prisma", "schema.prisma");
|
|
2676
3108
|
const content = await readIfExists(schemaPath);
|
|
2677
3109
|
if (!content) return [];
|
|
2678
3110
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2801,10 +3233,10 @@ async function parse5(serviceDir) {
|
|
|
2801
3233
|
var knexParser = { name: "knex", parse: parse5 };
|
|
2802
3234
|
|
|
2803
3235
|
// src/extract/databases/ormconfig.ts
|
|
2804
|
-
import
|
|
3236
|
+
import path17 from "path";
|
|
2805
3237
|
async function parse6(serviceDir) {
|
|
2806
3238
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2807
|
-
const abs =
|
|
3239
|
+
const abs = path17.join(serviceDir, candidate);
|
|
2808
3240
|
if (!await exists(abs)) continue;
|
|
2809
3241
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2810
3242
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2862,9 +3294,9 @@ async function parse7(serviceDir) {
|
|
|
2862
3294
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
2863
3295
|
|
|
2864
3296
|
// src/extract/databases/sequelize.ts
|
|
2865
|
-
import
|
|
3297
|
+
import path18 from "path";
|
|
2866
3298
|
async function parse8(serviceDir) {
|
|
2867
|
-
const configPath =
|
|
3299
|
+
const configPath = path18.join(serviceDir, "config", "config.json");
|
|
2868
3300
|
if (!await exists(configPath)) return [];
|
|
2869
3301
|
const raw = await readJson(configPath);
|
|
2870
3302
|
const out = [];
|
|
@@ -2890,7 +3322,7 @@ async function parse8(serviceDir) {
|
|
|
2890
3322
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
2891
3323
|
|
|
2892
3324
|
// src/extract/databases/docker-compose.ts
|
|
2893
|
-
import
|
|
3325
|
+
import path19 from "path";
|
|
2894
3326
|
function portFromService(svc) {
|
|
2895
3327
|
for (const raw of svc.ports ?? []) {
|
|
2896
3328
|
const str = String(raw);
|
|
@@ -2917,7 +3349,7 @@ function databaseFromEnv(svc) {
|
|
|
2917
3349
|
}
|
|
2918
3350
|
async function parse9(serviceDir) {
|
|
2919
3351
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2920
|
-
const abs =
|
|
3352
|
+
const abs = path19.join(serviceDir, name);
|
|
2921
3353
|
if (!await exists(abs)) continue;
|
|
2922
3354
|
const raw = await readYaml(abs);
|
|
2923
3355
|
if (!raw?.services) return [];
|
|
@@ -2959,7 +3391,7 @@ function compatibleDriversFor(engine) {
|
|
|
2959
3391
|
function toDatabaseNode(config) {
|
|
2960
3392
|
return {
|
|
2961
3393
|
id: databaseId2(config.host),
|
|
2962
|
-
type:
|
|
3394
|
+
type: NodeType7.DatabaseNode,
|
|
2963
3395
|
name: config.database || config.host,
|
|
2964
3396
|
engine: config.engine,
|
|
2965
3397
|
engineVersion: config.engineVersion,
|
|
@@ -3089,19 +3521,24 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3089
3521
|
discoveredVia: mergedDiscoveredVia
|
|
3090
3522
|
});
|
|
3091
3523
|
}
|
|
3524
|
+
const relConfigFile = toPosix2(path20.relative(service.dir, config.sourceFile));
|
|
3525
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
3526
|
+
graph,
|
|
3527
|
+
service.pkg.name,
|
|
3528
|
+
service.node.id,
|
|
3529
|
+
relConfigFile
|
|
3530
|
+
);
|
|
3531
|
+
nodesAdded += fn;
|
|
3532
|
+
edgesAdded += fe;
|
|
3533
|
+
const evidenceFile = toPosix2(path20.relative(scanPath, config.sourceFile));
|
|
3092
3534
|
const edge = {
|
|
3093
|
-
id: extractedEdgeId2(
|
|
3094
|
-
source:
|
|
3535
|
+
id: extractedEdgeId2(fileNodeId, dbNode.id, EdgeType6.CONNECTS_TO),
|
|
3536
|
+
source: fileNodeId,
|
|
3095
3537
|
target: dbNode.id,
|
|
3096
|
-
type:
|
|
3097
|
-
provenance:
|
|
3098
|
-
confidence:
|
|
3099
|
-
|
|
3100
|
-
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
3101
|
-
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
3102
|
-
evidence: {
|
|
3103
|
-
file: path17.relative(scanPath, config.sourceFile).split(path17.sep).join("/")
|
|
3104
|
-
}
|
|
3538
|
+
type: EdgeType6.CONNECTS_TO,
|
|
3539
|
+
provenance: Provenance4.EXTRACTED,
|
|
3540
|
+
confidence: confidenceForExtracted3("structural"),
|
|
3541
|
+
evidence: { file: evidenceFile }
|
|
3105
3542
|
};
|
|
3106
3543
|
if (!graph.hasEdge(edge.id)) {
|
|
3107
3544
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -3126,194 +3563,98 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
3126
3563
|
}
|
|
3127
3564
|
|
|
3128
3565
|
// src/extract/configs.ts
|
|
3129
|
-
import { promises as
|
|
3130
|
-
import
|
|
3566
|
+
import { promises as fs14 } from "fs";
|
|
3567
|
+
import path21 from "path";
|
|
3131
3568
|
import {
|
|
3132
|
-
EdgeType as
|
|
3133
|
-
NodeType as
|
|
3134
|
-
Provenance as
|
|
3569
|
+
EdgeType as EdgeType7,
|
|
3570
|
+
NodeType as NodeType8,
|
|
3571
|
+
Provenance as Provenance5,
|
|
3135
3572
|
configId,
|
|
3136
|
-
confidenceForExtracted as
|
|
3573
|
+
confidenceForExtracted as confidenceForExtracted4
|
|
3137
3574
|
} from "@neat.is/types";
|
|
3138
3575
|
async function walkConfigFiles(dir) {
|
|
3139
3576
|
const out = [];
|
|
3140
3577
|
async function walk(current) {
|
|
3141
|
-
const entries = await
|
|
3578
|
+
const entries = await fs14.readdir(current, { withFileTypes: true });
|
|
3142
3579
|
for (const entry of entries) {
|
|
3143
|
-
const full =
|
|
3580
|
+
const full = path21.join(current, entry.name);
|
|
3144
3581
|
if (entry.isDirectory()) {
|
|
3145
3582
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3146
3583
|
if (await isPythonVenvDir(full)) continue;
|
|
3147
3584
|
await walk(full);
|
|
3148
3585
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
3149
3586
|
out.push(full);
|
|
3150
|
-
}
|
|
3151
|
-
}
|
|
3152
|
-
}
|
|
3153
|
-
await walk(dir);
|
|
3154
|
-
return out;
|
|
3155
|
-
}
|
|
3156
|
-
async function addConfigNodes(graph, services, scanPath) {
|
|
3157
|
-
let nodesAdded = 0;
|
|
3158
|
-
let edgesAdded = 0;
|
|
3159
|
-
for (const service of services) {
|
|
3160
|
-
const configFiles = await walkConfigFiles(service.dir);
|
|
3161
|
-
for (const file of configFiles) {
|
|
3162
|
-
const relPath = path18.relative(scanPath, file);
|
|
3163
|
-
const node = {
|
|
3164
|
-
id: configId(relPath),
|
|
3165
|
-
type: NodeType7.ConfigNode,
|
|
3166
|
-
name: path18.basename(file),
|
|
3167
|
-
path: relPath,
|
|
3168
|
-
fileType: isConfigFile(path18.basename(file)).fileType
|
|
3169
|
-
};
|
|
3170
|
-
if (!graph.hasNode(node.id)) {
|
|
3171
|
-
graph.addNode(node.id, node);
|
|
3172
|
-
nodesAdded++;
|
|
3173
|
-
}
|
|
3174
|
-
const edge = {
|
|
3175
|
-
id: extractedEdgeId2(service.node.id, node.id, EdgeType5.CONFIGURED_BY),
|
|
3176
|
-
source: service.node.id,
|
|
3177
|
-
target: node.id,
|
|
3178
|
-
type: EdgeType5.CONFIGURED_BY,
|
|
3179
|
-
provenance: Provenance3.EXTRACTED,
|
|
3180
|
-
confidence: confidenceForExtracted2("structural"),
|
|
3181
|
-
evidence: { file: relPath.split(path18.sep).join("/") }
|
|
3182
|
-
};
|
|
3183
|
-
if (!graph.hasEdge(edge.id)) {
|
|
3184
|
-
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
3185
|
-
edgesAdded++;
|
|
3186
|
-
}
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
return { nodesAdded, edgesAdded };
|
|
3190
|
-
}
|
|
3191
|
-
|
|
3192
|
-
// src/extract/calls/index.ts
|
|
3193
|
-
import {
|
|
3194
|
-
EdgeType as EdgeType8,
|
|
3195
|
-
NodeType as NodeType9,
|
|
3196
|
-
Provenance as Provenance6,
|
|
3197
|
-
confidenceForExtracted as confidenceForExtracted5,
|
|
3198
|
-
passesExtractedFloor as passesExtractedFloor2
|
|
3199
|
-
} from "@neat.is/types";
|
|
3200
|
-
|
|
3201
|
-
// src/extract/calls/http.ts
|
|
3202
|
-
import path20 from "path";
|
|
3203
|
-
import Parser from "tree-sitter";
|
|
3204
|
-
import JavaScript from "tree-sitter-javascript";
|
|
3205
|
-
import Python from "tree-sitter-python";
|
|
3206
|
-
import {
|
|
3207
|
-
EdgeType as EdgeType7,
|
|
3208
|
-
Provenance as Provenance5,
|
|
3209
|
-
confidenceForExtracted as confidenceForExtracted4,
|
|
3210
|
-
passesExtractedFloor
|
|
3211
|
-
} from "@neat.is/types";
|
|
3212
|
-
|
|
3213
|
-
// src/extract/calls/shared.ts
|
|
3214
|
-
import { promises as fs13 } from "fs";
|
|
3215
|
-
import path19 from "path";
|
|
3216
|
-
import {
|
|
3217
|
-
EdgeType as EdgeType6,
|
|
3218
|
-
NodeType as NodeType8,
|
|
3219
|
-
Provenance as Provenance4,
|
|
3220
|
-
confidenceForExtracted as confidenceForExtracted3,
|
|
3221
|
-
extractedEdgeId as extractedEdgeId3,
|
|
3222
|
-
fileId as fileId2
|
|
3223
|
-
} from "@neat.is/types";
|
|
3224
|
-
async function walkSourceFiles(dir) {
|
|
3225
|
-
const out = [];
|
|
3226
|
-
async function walk(current) {
|
|
3227
|
-
const entries = await fs13.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
3228
|
-
for (const entry of entries) {
|
|
3229
|
-
const full = path19.join(current, entry.name);
|
|
3230
|
-
if (entry.isDirectory()) {
|
|
3231
|
-
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3232
|
-
if (await isPythonVenvDir(full)) continue;
|
|
3233
|
-
await walk(full);
|
|
3234
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
|
|
3235
|
-
out.push(full);
|
|
3236
|
-
}
|
|
3237
|
-
}
|
|
3238
|
-
}
|
|
3239
|
-
await walk(dir);
|
|
3240
|
-
return out;
|
|
3241
|
-
}
|
|
3242
|
-
async function loadSourceFiles(dir) {
|
|
3243
|
-
const paths = await walkSourceFiles(dir);
|
|
3244
|
-
const out = [];
|
|
3245
|
-
for (const p of paths) {
|
|
3246
|
-
try {
|
|
3247
|
-
const content = await fs13.readFile(p, "utf8");
|
|
3248
|
-
out.push({ path: p, content });
|
|
3249
|
-
} catch {
|
|
3250
|
-
}
|
|
3251
|
-
}
|
|
3252
|
-
return out;
|
|
3253
|
-
}
|
|
3254
|
-
function lineOf(text, needle) {
|
|
3255
|
-
const idx = text.indexOf(needle);
|
|
3256
|
-
if (idx < 0) return 1;
|
|
3257
|
-
return text.slice(0, idx).split("\n").length;
|
|
3258
|
-
}
|
|
3259
|
-
function snippet(text, line) {
|
|
3260
|
-
const lines = text.split("\n");
|
|
3261
|
-
return (lines[line - 1] ?? "").trim();
|
|
3262
|
-
}
|
|
3263
|
-
function toPosix2(p) {
|
|
3264
|
-
return p.split("\\").join("/");
|
|
3265
|
-
}
|
|
3266
|
-
function languageForPath(relPath) {
|
|
3267
|
-
switch (path19.extname(relPath).toLowerCase()) {
|
|
3268
|
-
case ".py":
|
|
3269
|
-
return "python";
|
|
3270
|
-
case ".ts":
|
|
3271
|
-
case ".tsx":
|
|
3272
|
-
return "typescript";
|
|
3273
|
-
case ".js":
|
|
3274
|
-
case ".jsx":
|
|
3275
|
-
case ".mjs":
|
|
3276
|
-
case ".cjs":
|
|
3277
|
-
return "javascript";
|
|
3278
|
-
default:
|
|
3279
|
-
return void 0;
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3280
3589
|
}
|
|
3590
|
+
await walk(dir);
|
|
3591
|
+
return out;
|
|
3281
3592
|
}
|
|
3282
|
-
function
|
|
3593
|
+
async function addConfigNodes(graph, services, scanPath) {
|
|
3283
3594
|
let nodesAdded = 0;
|
|
3284
3595
|
let edgesAdded = 0;
|
|
3285
|
-
const
|
|
3286
|
-
|
|
3287
|
-
const
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
|
|
3308
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3596
|
+
for (const service of services) {
|
|
3597
|
+
const configFiles = await walkConfigFiles(service.dir);
|
|
3598
|
+
for (const file of configFiles) {
|
|
3599
|
+
const relPath = path21.relative(scanPath, file);
|
|
3600
|
+
const node = {
|
|
3601
|
+
id: configId(relPath),
|
|
3602
|
+
type: NodeType8.ConfigNode,
|
|
3603
|
+
name: path21.basename(file),
|
|
3604
|
+
path: relPath,
|
|
3605
|
+
fileType: isConfigFile(path21.basename(file)).fileType
|
|
3606
|
+
};
|
|
3607
|
+
if (!graph.hasNode(node.id)) {
|
|
3608
|
+
graph.addNode(node.id, node);
|
|
3609
|
+
nodesAdded++;
|
|
3610
|
+
}
|
|
3611
|
+
const relToService = toPosix2(path21.relative(service.dir, file));
|
|
3612
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
3613
|
+
graph,
|
|
3614
|
+
service.pkg.name,
|
|
3615
|
+
service.node.id,
|
|
3616
|
+
relToService
|
|
3617
|
+
);
|
|
3618
|
+
nodesAdded += fn;
|
|
3619
|
+
edgesAdded += fe;
|
|
3620
|
+
const edge = {
|
|
3621
|
+
id: extractedEdgeId2(fileNodeId, node.id, EdgeType7.CONFIGURED_BY),
|
|
3622
|
+
source: fileNodeId,
|
|
3623
|
+
target: node.id,
|
|
3624
|
+
type: EdgeType7.CONFIGURED_BY,
|
|
3625
|
+
provenance: Provenance5.EXTRACTED,
|
|
3626
|
+
confidence: confidenceForExtracted4("structural"),
|
|
3627
|
+
evidence: { file: relPath.split(path21.sep).join("/") }
|
|
3628
|
+
};
|
|
3629
|
+
if (!graph.hasEdge(edge.id)) {
|
|
3630
|
+
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
3631
|
+
edgesAdded++;
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3312
3634
|
}
|
|
3313
|
-
return {
|
|
3635
|
+
return { nodesAdded, edgesAdded };
|
|
3314
3636
|
}
|
|
3315
3637
|
|
|
3638
|
+
// src/extract/calls/index.ts
|
|
3639
|
+
import {
|
|
3640
|
+
EdgeType as EdgeType9,
|
|
3641
|
+
NodeType as NodeType9,
|
|
3642
|
+
Provenance as Provenance7,
|
|
3643
|
+
confidenceForExtracted as confidenceForExtracted6,
|
|
3644
|
+
passesExtractedFloor as passesExtractedFloor2
|
|
3645
|
+
} from "@neat.is/types";
|
|
3646
|
+
|
|
3316
3647
|
// src/extract/calls/http.ts
|
|
3648
|
+
import path22 from "path";
|
|
3649
|
+
import Parser2 from "tree-sitter";
|
|
3650
|
+
import JavaScript2 from "tree-sitter-javascript";
|
|
3651
|
+
import Python2 from "tree-sitter-python";
|
|
3652
|
+
import {
|
|
3653
|
+
EdgeType as EdgeType8,
|
|
3654
|
+
Provenance as Provenance6,
|
|
3655
|
+
confidenceForExtracted as confidenceForExtracted5,
|
|
3656
|
+
passesExtractedFloor
|
|
3657
|
+
} from "@neat.is/types";
|
|
3317
3658
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
3318
3659
|
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
3319
3660
|
function isInsideJsxExternalLink(node) {
|
|
@@ -3341,14 +3682,14 @@ function collectStringLiterals(node, out) {
|
|
|
3341
3682
|
if (child) collectStringLiterals(child, out);
|
|
3342
3683
|
}
|
|
3343
3684
|
}
|
|
3344
|
-
var
|
|
3345
|
-
function
|
|
3685
|
+
var PARSE_CHUNK2 = 16384;
|
|
3686
|
+
function parseSource2(parser, source) {
|
|
3346
3687
|
return parser.parse(
|
|
3347
|
-
(index) => index >= source.length ? "" : source.slice(index, index +
|
|
3688
|
+
(index) => index >= source.length ? "" : source.slice(index, index + PARSE_CHUNK2)
|
|
3348
3689
|
);
|
|
3349
3690
|
}
|
|
3350
3691
|
function callsFromSource(source, parser, knownHosts) {
|
|
3351
|
-
const tree =
|
|
3692
|
+
const tree = parseSource2(parser, source);
|
|
3352
3693
|
const literals = [];
|
|
3353
3694
|
collectStringLiterals(tree.rootNode, literals);
|
|
3354
3695
|
const out = [];
|
|
@@ -3362,25 +3703,25 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
3362
3703
|
}
|
|
3363
3704
|
return out;
|
|
3364
3705
|
}
|
|
3365
|
-
function
|
|
3366
|
-
const p = new
|
|
3367
|
-
p.setLanguage(
|
|
3706
|
+
function makeJsParser2() {
|
|
3707
|
+
const p = new Parser2();
|
|
3708
|
+
p.setLanguage(JavaScript2);
|
|
3368
3709
|
return p;
|
|
3369
3710
|
}
|
|
3370
|
-
function
|
|
3371
|
-
const p = new
|
|
3372
|
-
p.setLanguage(
|
|
3711
|
+
function makePyParser2() {
|
|
3712
|
+
const p = new Parser2();
|
|
3713
|
+
p.setLanguage(Python2);
|
|
3373
3714
|
return p;
|
|
3374
3715
|
}
|
|
3375
3716
|
async function addHttpCallEdges(graph, services) {
|
|
3376
|
-
const jsParser =
|
|
3377
|
-
const pyParser =
|
|
3717
|
+
const jsParser = makeJsParser2();
|
|
3718
|
+
const pyParser = makePyParser2();
|
|
3378
3719
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
3379
3720
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
3380
3721
|
for (const service of services) {
|
|
3381
|
-
knownHosts.add(
|
|
3722
|
+
knownHosts.add(path22.basename(service.dir));
|
|
3382
3723
|
knownHosts.add(service.pkg.name);
|
|
3383
|
-
hostToNodeId.set(
|
|
3724
|
+
hostToNodeId.set(path22.basename(service.dir), service.node.id);
|
|
3384
3725
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
3385
3726
|
}
|
|
3386
3727
|
let nodesAdded = 0;
|
|
@@ -3390,7 +3731,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3390
3731
|
const seen = /* @__PURE__ */ new Set();
|
|
3391
3732
|
for (const file of files) {
|
|
3392
3733
|
if (isTestPath(file.path)) continue;
|
|
3393
|
-
const parser =
|
|
3734
|
+
const parser = path22.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
3394
3735
|
let sites;
|
|
3395
3736
|
try {
|
|
3396
3737
|
sites = callsFromSource(file.content, parser, knownHosts);
|
|
@@ -3399,14 +3740,14 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3399
3740
|
continue;
|
|
3400
3741
|
}
|
|
3401
3742
|
if (sites.length === 0) continue;
|
|
3402
|
-
const relFile = toPosix2(
|
|
3743
|
+
const relFile = toPosix2(path22.relative(service.dir, file.path));
|
|
3403
3744
|
for (const site of sites) {
|
|
3404
3745
|
const targetId = hostToNodeId.get(site.host);
|
|
3405
3746
|
if (!targetId || targetId === service.node.id) continue;
|
|
3406
3747
|
const dedupKey = `${relFile}|${targetId}`;
|
|
3407
3748
|
if (seen.has(dedupKey)) continue;
|
|
3408
3749
|
seen.add(dedupKey);
|
|
3409
|
-
const confidence =
|
|
3750
|
+
const confidence = confidenceForExtracted5("hostname-shape-match");
|
|
3410
3751
|
const ev = {
|
|
3411
3752
|
file: relFile,
|
|
3412
3753
|
line: site.line,
|
|
@@ -3424,21 +3765,21 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3424
3765
|
noteExtractedDropped({
|
|
3425
3766
|
source: fileNodeId,
|
|
3426
3767
|
target: targetId,
|
|
3427
|
-
type:
|
|
3768
|
+
type: EdgeType8.CALLS,
|
|
3428
3769
|
confidence,
|
|
3429
3770
|
confidenceKind: "hostname-shape-match",
|
|
3430
3771
|
evidence: ev
|
|
3431
3772
|
});
|
|
3432
3773
|
continue;
|
|
3433
3774
|
}
|
|
3434
|
-
const edgeId = extractedEdgeId2(fileNodeId, targetId,
|
|
3775
|
+
const edgeId = extractedEdgeId2(fileNodeId, targetId, EdgeType8.CALLS);
|
|
3435
3776
|
if (!graph.hasEdge(edgeId)) {
|
|
3436
3777
|
const edge = {
|
|
3437
3778
|
id: edgeId,
|
|
3438
3779
|
source: fileNodeId,
|
|
3439
3780
|
target: targetId,
|
|
3440
|
-
type:
|
|
3441
|
-
provenance:
|
|
3781
|
+
type: EdgeType8.CALLS,
|
|
3782
|
+
provenance: Provenance6.EXTRACTED,
|
|
3442
3783
|
confidence,
|
|
3443
3784
|
evidence: ev
|
|
3444
3785
|
};
|
|
@@ -3452,7 +3793,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
3452
3793
|
}
|
|
3453
3794
|
|
|
3454
3795
|
// src/extract/calls/kafka.ts
|
|
3455
|
-
import
|
|
3796
|
+
import path23 from "path";
|
|
3456
3797
|
import { infraId } from "@neat.is/types";
|
|
3457
3798
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3458
3799
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -3483,7 +3824,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3483
3824
|
// tier (ADR-066).
|
|
3484
3825
|
confidenceKind: "verified-call-site",
|
|
3485
3826
|
evidence: {
|
|
3486
|
-
file:
|
|
3827
|
+
file: path23.relative(serviceDir, file.path),
|
|
3487
3828
|
line,
|
|
3488
3829
|
snippet: snippet(file.content, line)
|
|
3489
3830
|
}
|
|
@@ -3495,7 +3836,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
3495
3836
|
}
|
|
3496
3837
|
|
|
3497
3838
|
// src/extract/calls/redis.ts
|
|
3498
|
-
import
|
|
3839
|
+
import path24 from "path";
|
|
3499
3840
|
import { infraId as infraId2 } from "@neat.is/types";
|
|
3500
3841
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
3501
3842
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -3518,7 +3859,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3518
3859
|
// support tier (ADR-066).
|
|
3519
3860
|
confidenceKind: "url-with-structural-support",
|
|
3520
3861
|
evidence: {
|
|
3521
|
-
file:
|
|
3862
|
+
file: path24.relative(serviceDir, file.path),
|
|
3522
3863
|
line,
|
|
3523
3864
|
snippet: snippet(file.content, line)
|
|
3524
3865
|
}
|
|
@@ -3528,7 +3869,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
3528
3869
|
}
|
|
3529
3870
|
|
|
3530
3871
|
// src/extract/calls/aws.ts
|
|
3531
|
-
import
|
|
3872
|
+
import path25 from "path";
|
|
3532
3873
|
import { infraId as infraId3 } from "@neat.is/types";
|
|
3533
3874
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
3534
3875
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -3562,7 +3903,7 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3562
3903
|
// (ADR-066).
|
|
3563
3904
|
confidenceKind: "verified-call-site",
|
|
3564
3905
|
evidence: {
|
|
3565
|
-
file:
|
|
3906
|
+
file: path25.relative(serviceDir, file.path),
|
|
3566
3907
|
line,
|
|
3567
3908
|
snippet: snippet(file.content, line)
|
|
3568
3909
|
}
|
|
@@ -3586,13 +3927,14 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3586
3927
|
}
|
|
3587
3928
|
|
|
3588
3929
|
// src/extract/calls/grpc.ts
|
|
3589
|
-
import
|
|
3930
|
+
import path26 from "path";
|
|
3590
3931
|
import { infraId as infraId4 } from "@neat.is/types";
|
|
3591
3932
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3592
3933
|
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3593
3934
|
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3594
3935
|
function isLikelyAddress(value) {
|
|
3595
3936
|
if (!value) return false;
|
|
3937
|
+
if (/\s/.test(value) || value.startsWith("{")) return false;
|
|
3596
3938
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3597
3939
|
}
|
|
3598
3940
|
function normaliseForMatch(s) {
|
|
@@ -3644,7 +3986,7 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3644
3986
|
// tier (ADR-066).
|
|
3645
3987
|
confidenceKind: "verified-call-site",
|
|
3646
3988
|
evidence: {
|
|
3647
|
-
file:
|
|
3989
|
+
file: path26.relative(serviceDir, file.path),
|
|
3648
3990
|
line,
|
|
3649
3991
|
snippet: snippet(file.content, line)
|
|
3650
3992
|
}
|
|
@@ -3657,11 +3999,11 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3657
3999
|
function edgeTypeFromEndpoint(ep) {
|
|
3658
4000
|
switch (ep.edgeType) {
|
|
3659
4001
|
case "PUBLISHES_TO":
|
|
3660
|
-
return
|
|
4002
|
+
return EdgeType9.PUBLISHES_TO;
|
|
3661
4003
|
case "CONSUMES_FROM":
|
|
3662
|
-
return
|
|
4004
|
+
return EdgeType9.CONSUMES_FROM;
|
|
3663
4005
|
default:
|
|
3664
|
-
return
|
|
4006
|
+
return EdgeType9.CALLS;
|
|
3665
4007
|
}
|
|
3666
4008
|
}
|
|
3667
4009
|
function isAwsKind(kind) {
|
|
@@ -3700,7 +4042,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3700
4042
|
nodesAdded++;
|
|
3701
4043
|
}
|
|
3702
4044
|
const edgeType = edgeTypeFromEndpoint(ep);
|
|
3703
|
-
const confidence =
|
|
4045
|
+
const confidence = confidenceForExtracted6(ep.confidenceKind);
|
|
3704
4046
|
const relFile = toPosix2(ep.evidence.file);
|
|
3705
4047
|
const { fileNodeId, nodesAdded: n, edgesAdded: e } = ensureFileNode(
|
|
3706
4048
|
graph,
|
|
@@ -3730,7 +4072,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3730
4072
|
source: fileNodeId,
|
|
3731
4073
|
target: ep.infraId,
|
|
3732
4074
|
type: edgeType,
|
|
3733
|
-
provenance:
|
|
4075
|
+
provenance: Provenance7.EXTRACTED,
|
|
3734
4076
|
confidence,
|
|
3735
4077
|
evidence: ep.evidence
|
|
3736
4078
|
};
|
|
@@ -3751,8 +4093,8 @@ async function addCallEdges(graph, services) {
|
|
|
3751
4093
|
}
|
|
3752
4094
|
|
|
3753
4095
|
// src/extract/infra/docker-compose.ts
|
|
3754
|
-
import
|
|
3755
|
-
import { EdgeType as
|
|
4096
|
+
import path27 from "path";
|
|
4097
|
+
import { EdgeType as EdgeType10, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted7 } from "@neat.is/types";
|
|
3756
4098
|
|
|
3757
4099
|
// src/extract/infra/shared.ts
|
|
3758
4100
|
import { NodeType as NodeType10, infraId as infraId5 } from "@neat.is/types";
|
|
@@ -3788,7 +4130,7 @@ function dependsOnList(value) {
|
|
|
3788
4130
|
}
|
|
3789
4131
|
function serviceNameToServiceNode(name, services) {
|
|
3790
4132
|
for (const s of services) {
|
|
3791
|
-
if (s.node.name === name ||
|
|
4133
|
+
if (s.node.name === name || path27.basename(s.dir) === name) return s.node.id;
|
|
3792
4134
|
}
|
|
3793
4135
|
return null;
|
|
3794
4136
|
}
|
|
@@ -3797,7 +4139,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3797
4139
|
let edgesAdded = 0;
|
|
3798
4140
|
let composePath = null;
|
|
3799
4141
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3800
|
-
const abs =
|
|
4142
|
+
const abs = path27.join(scanPath, name);
|
|
3801
4143
|
if (await exists(abs)) {
|
|
3802
4144
|
composePath = abs;
|
|
3803
4145
|
break;
|
|
@@ -3810,13 +4152,13 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3810
4152
|
} catch (err) {
|
|
3811
4153
|
recordExtractionError(
|
|
3812
4154
|
"infra docker-compose",
|
|
3813
|
-
|
|
4155
|
+
path27.relative(scanPath, composePath),
|
|
3814
4156
|
err
|
|
3815
4157
|
);
|
|
3816
4158
|
return { nodesAdded, edgesAdded };
|
|
3817
4159
|
}
|
|
3818
4160
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3819
|
-
const evidenceFile =
|
|
4161
|
+
const evidenceFile = path27.relative(scanPath, composePath).split(path27.sep).join("/");
|
|
3820
4162
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3821
4163
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3822
4164
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3838,15 +4180,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3838
4180
|
for (const dep of dependsOnList(svc.depends_on)) {
|
|
3839
4181
|
const targetId = composeNameToNodeId.get(dep);
|
|
3840
4182
|
if (!targetId) continue;
|
|
3841
|
-
const edgeId = extractedEdgeId2(sourceId, targetId,
|
|
4183
|
+
const edgeId = extractedEdgeId2(sourceId, targetId, EdgeType10.DEPENDS_ON);
|
|
3842
4184
|
if (graph.hasEdge(edgeId)) continue;
|
|
3843
4185
|
const edge = {
|
|
3844
4186
|
id: edgeId,
|
|
3845
4187
|
source: sourceId,
|
|
3846
4188
|
target: targetId,
|
|
3847
|
-
type:
|
|
3848
|
-
provenance:
|
|
3849
|
-
confidence:
|
|
4189
|
+
type: EdgeType10.DEPENDS_ON,
|
|
4190
|
+
provenance: Provenance8.EXTRACTED,
|
|
4191
|
+
confidence: confidenceForExtracted7("structural"),
|
|
3850
4192
|
evidence: { file: evidenceFile }
|
|
3851
4193
|
};
|
|
3852
4194
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3857,9 +4199,9 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3857
4199
|
}
|
|
3858
4200
|
|
|
3859
4201
|
// src/extract/infra/dockerfile.ts
|
|
3860
|
-
import
|
|
3861
|
-
import { promises as
|
|
3862
|
-
import { EdgeType as
|
|
4202
|
+
import path28 from "path";
|
|
4203
|
+
import { promises as fs15 } from "fs";
|
|
4204
|
+
import { EdgeType as EdgeType11, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted8 } from "@neat.is/types";
|
|
3863
4205
|
function runtimeImage(content) {
|
|
3864
4206
|
const lines = content.split("\n");
|
|
3865
4207
|
let last = null;
|
|
@@ -3878,15 +4220,15 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3878
4220
|
let nodesAdded = 0;
|
|
3879
4221
|
let edgesAdded = 0;
|
|
3880
4222
|
for (const service of services) {
|
|
3881
|
-
const dockerfilePath =
|
|
4223
|
+
const dockerfilePath = path28.join(service.dir, "Dockerfile");
|
|
3882
4224
|
if (!await exists(dockerfilePath)) continue;
|
|
3883
4225
|
let content;
|
|
3884
4226
|
try {
|
|
3885
|
-
content = await
|
|
4227
|
+
content = await fs15.readFile(dockerfilePath, "utf8");
|
|
3886
4228
|
} catch (err) {
|
|
3887
4229
|
recordExtractionError(
|
|
3888
4230
|
"infra dockerfile",
|
|
3889
|
-
|
|
4231
|
+
path28.relative(scanPath, dockerfilePath),
|
|
3890
4232
|
err
|
|
3891
4233
|
);
|
|
3892
4234
|
continue;
|
|
@@ -3898,17 +4240,26 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3898
4240
|
graph.addNode(node.id, node);
|
|
3899
4241
|
nodesAdded++;
|
|
3900
4242
|
}
|
|
3901
|
-
const
|
|
4243
|
+
const relDockerfile = toPosix2(path28.relative(service.dir, dockerfilePath));
|
|
4244
|
+
const { fileNodeId, nodesAdded: fn, edgesAdded: fe } = ensureFileNode(
|
|
4245
|
+
graph,
|
|
4246
|
+
service.pkg.name,
|
|
4247
|
+
service.node.id,
|
|
4248
|
+
relDockerfile
|
|
4249
|
+
);
|
|
4250
|
+
nodesAdded += fn;
|
|
4251
|
+
edgesAdded += fe;
|
|
4252
|
+
const edgeId = extractedEdgeId2(fileNodeId, node.id, EdgeType11.RUNS_ON);
|
|
3902
4253
|
if (!graph.hasEdge(edgeId)) {
|
|
3903
4254
|
const edge = {
|
|
3904
4255
|
id: edgeId,
|
|
3905
|
-
source:
|
|
4256
|
+
source: fileNodeId,
|
|
3906
4257
|
target: node.id,
|
|
3907
|
-
type:
|
|
3908
|
-
provenance:
|
|
3909
|
-
confidence:
|
|
4258
|
+
type: EdgeType11.RUNS_ON,
|
|
4259
|
+
provenance: Provenance9.EXTRACTED,
|
|
4260
|
+
confidence: confidenceForExtracted8("structural"),
|
|
3910
4261
|
evidence: {
|
|
3911
|
-
file:
|
|
4262
|
+
file: toPosix2(path28.relative(scanPath, dockerfilePath))
|
|
3912
4263
|
}
|
|
3913
4264
|
};
|
|
3914
4265
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3919,21 +4270,21 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3919
4270
|
}
|
|
3920
4271
|
|
|
3921
4272
|
// src/extract/infra/terraform.ts
|
|
3922
|
-
import { promises as
|
|
3923
|
-
import
|
|
4273
|
+
import { promises as fs16 } from "fs";
|
|
4274
|
+
import path29 from "path";
|
|
3924
4275
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3925
4276
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3926
4277
|
if (depth > max) return [];
|
|
3927
4278
|
const out = [];
|
|
3928
|
-
const entries = await
|
|
4279
|
+
const entries = await fs16.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3929
4280
|
for (const entry of entries) {
|
|
3930
4281
|
if (entry.isDirectory()) {
|
|
3931
4282
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3932
|
-
const child =
|
|
4283
|
+
const child = path29.join(start, entry.name);
|
|
3933
4284
|
if (await isPythonVenvDir(child)) continue;
|
|
3934
4285
|
out.push(...await walkTfFiles(child, depth + 1, max));
|
|
3935
4286
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3936
|
-
out.push(
|
|
4287
|
+
out.push(path29.join(start, entry.name));
|
|
3937
4288
|
}
|
|
3938
4289
|
}
|
|
3939
4290
|
return out;
|
|
@@ -3942,7 +4293,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3942
4293
|
let nodesAdded = 0;
|
|
3943
4294
|
const files = await walkTfFiles(scanPath);
|
|
3944
4295
|
for (const file of files) {
|
|
3945
|
-
const content = await
|
|
4296
|
+
const content = await fs16.readFile(file, "utf8");
|
|
3946
4297
|
RESOURCE_RE.lastIndex = 0;
|
|
3947
4298
|
let m;
|
|
3948
4299
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3959,8 +4310,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3959
4310
|
}
|
|
3960
4311
|
|
|
3961
4312
|
// src/extract/infra/k8s.ts
|
|
3962
|
-
import { promises as
|
|
3963
|
-
import
|
|
4313
|
+
import { promises as fs17 } from "fs";
|
|
4314
|
+
import path30 from "path";
|
|
3964
4315
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
3965
4316
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3966
4317
|
Service: "k8s-service",
|
|
@@ -3974,15 +4325,15 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3974
4325
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3975
4326
|
if (depth > max) return [];
|
|
3976
4327
|
const out = [];
|
|
3977
|
-
const entries = await
|
|
4328
|
+
const entries = await fs17.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3978
4329
|
for (const entry of entries) {
|
|
3979
4330
|
if (entry.isDirectory()) {
|
|
3980
4331
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3981
|
-
const child =
|
|
4332
|
+
const child = path30.join(start, entry.name);
|
|
3982
4333
|
if (await isPythonVenvDir(child)) continue;
|
|
3983
4334
|
out.push(...await walkYamlFiles2(child, depth + 1, max));
|
|
3984
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3985
|
-
out.push(
|
|
4335
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path30.extname(entry.name))) {
|
|
4336
|
+
out.push(path30.join(start, entry.name));
|
|
3986
4337
|
}
|
|
3987
4338
|
}
|
|
3988
4339
|
return out;
|
|
@@ -3991,7 +4342,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3991
4342
|
let nodesAdded = 0;
|
|
3992
4343
|
const files = await walkYamlFiles2(scanPath);
|
|
3993
4344
|
for (const file of files) {
|
|
3994
|
-
const content = await
|
|
4345
|
+
const content = await fs17.readFile(file, "utf8");
|
|
3995
4346
|
let docs;
|
|
3996
4347
|
try {
|
|
3997
4348
|
docs = parseAllDocuments2(content).map((d) => d.toJSON());
|
|
@@ -4026,12 +4377,12 @@ async function addInfra(graph, scanPath, services) {
|
|
|
4026
4377
|
}
|
|
4027
4378
|
|
|
4028
4379
|
// src/extract/index.ts
|
|
4029
|
-
import
|
|
4380
|
+
import path32 from "path";
|
|
4030
4381
|
|
|
4031
4382
|
// src/extract/retire.ts
|
|
4032
4383
|
import { existsSync as existsSync2 } from "fs";
|
|
4033
|
-
import
|
|
4034
|
-
import { NodeType as NodeType11, Provenance as
|
|
4384
|
+
import path31 from "path";
|
|
4385
|
+
import { NodeType as NodeType11, Provenance as Provenance10 } from "@neat.is/types";
|
|
4035
4386
|
function dropOrphanedFileNodes(graph) {
|
|
4036
4387
|
const orphans = [];
|
|
4037
4388
|
graph.forEachNode((id, attrs) => {
|
|
@@ -4048,7 +4399,7 @@ function retireEdgesByFile(graph, file) {
|
|
|
4048
4399
|
const toDrop = [];
|
|
4049
4400
|
graph.forEachEdge((id, attrs) => {
|
|
4050
4401
|
const edge = attrs;
|
|
4051
|
-
if (edge.provenance !==
|
|
4402
|
+
if (edge.provenance !== Provenance10.EXTRACTED) return;
|
|
4052
4403
|
if (!edge.evidence?.file) return;
|
|
4053
4404
|
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
4054
4405
|
});
|
|
@@ -4061,14 +4412,14 @@ function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
|
4061
4412
|
const bases = [scanPath, ...serviceDirs];
|
|
4062
4413
|
graph.forEachEdge((id, attrs) => {
|
|
4063
4414
|
const edge = attrs;
|
|
4064
|
-
if (edge.provenance !==
|
|
4415
|
+
if (edge.provenance !== Provenance10.EXTRACTED) return;
|
|
4065
4416
|
const evidenceFile = edge.evidence?.file;
|
|
4066
4417
|
if (!evidenceFile) return;
|
|
4067
|
-
if (
|
|
4418
|
+
if (path31.isAbsolute(evidenceFile)) {
|
|
4068
4419
|
if (!existsSync2(evidenceFile)) toDrop.push(id);
|
|
4069
4420
|
return;
|
|
4070
4421
|
}
|
|
4071
|
-
const found = bases.some((base) => existsSync2(
|
|
4422
|
+
const found = bases.some((base) => existsSync2(path31.join(base, evidenceFile)));
|
|
4072
4423
|
if (!found) toDrop.push(id);
|
|
4073
4424
|
});
|
|
4074
4425
|
for (const id of toDrop) graph.dropEdge(id);
|
|
@@ -4083,6 +4434,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4083
4434
|
const services = await discoverServices(scanPath);
|
|
4084
4435
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
4085
4436
|
await addServiceAliases(graph, scanPath, services);
|
|
4437
|
+
const fileEnum = await addFiles(graph, services);
|
|
4438
|
+
const importGraph = await addImports(graph, services);
|
|
4086
4439
|
const phase2 = await addDatabasesAndCompat(graph, services, scanPath);
|
|
4087
4440
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
4088
4441
|
const phase4 = await addCallEdges(graph, services);
|
|
@@ -4106,7 +4459,7 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4106
4459
|
}
|
|
4107
4460
|
const droppedEntries = drainDroppedExtracted();
|
|
4108
4461
|
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
4109
|
-
const rejectedPath =
|
|
4462
|
+
const rejectedPath = path32.join(path32.dirname(opts.errorsPath), "rejected.ndjson");
|
|
4110
4463
|
try {
|
|
4111
4464
|
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
4112
4465
|
} catch (err) {
|
|
@@ -4116,8 +4469,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4116
4469
|
}
|
|
4117
4470
|
}
|
|
4118
4471
|
const result = {
|
|
4119
|
-
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
4120
|
-
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
4472
|
+
nodesAdded: phase1Nodes + fileEnum.nodesAdded + importGraph.nodesAdded + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
4473
|
+
edgesAdded: fileEnum.edgesAdded + importGraph.edgesAdded + phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
4121
4474
|
frontiersPromoted,
|
|
4122
4475
|
extractionErrors: errorEntries.length,
|
|
4123
4476
|
errorEntries,
|
|
@@ -4141,10 +4494,10 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
4141
4494
|
// src/divergences.ts
|
|
4142
4495
|
import {
|
|
4143
4496
|
DivergenceResultSchema,
|
|
4144
|
-
EdgeType as
|
|
4497
|
+
EdgeType as EdgeType12,
|
|
4145
4498
|
NodeType as NodeType12,
|
|
4146
4499
|
parseEdgeId,
|
|
4147
|
-
Provenance as
|
|
4500
|
+
Provenance as Provenance11
|
|
4148
4501
|
} from "@neat.is/types";
|
|
4149
4502
|
function bucketKey(source, target, type) {
|
|
4150
4503
|
return `${type}|${source}|${target}`;
|
|
@@ -4158,17 +4511,17 @@ function bucketEdges(graph) {
|
|
|
4158
4511
|
const key = bucketKey(e.source, e.target, e.type);
|
|
4159
4512
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
4160
4513
|
switch (provenance) {
|
|
4161
|
-
case
|
|
4514
|
+
case Provenance11.EXTRACTED:
|
|
4162
4515
|
cur.extracted = e;
|
|
4163
4516
|
break;
|
|
4164
|
-
case
|
|
4517
|
+
case Provenance11.OBSERVED:
|
|
4165
4518
|
cur.observed = e;
|
|
4166
4519
|
break;
|
|
4167
|
-
case
|
|
4520
|
+
case Provenance11.INFERRED:
|
|
4168
4521
|
cur.inferred = e;
|
|
4169
4522
|
break;
|
|
4170
4523
|
default:
|
|
4171
|
-
if (e.provenance ===
|
|
4524
|
+
if (e.provenance === Provenance11.STALE) cur.stale = e;
|
|
4172
4525
|
}
|
|
4173
4526
|
buckets.set(key, cur);
|
|
4174
4527
|
});
|
|
@@ -4198,7 +4551,7 @@ function gradedConfidence(edge) {
|
|
|
4198
4551
|
}
|
|
4199
4552
|
function detectMissingDivergences(graph, bucket) {
|
|
4200
4553
|
const out = [];
|
|
4201
|
-
if (bucket.type ===
|
|
4554
|
+
if (bucket.type === EdgeType12.CONTAINS) return out;
|
|
4202
4555
|
if (bucket.extracted && !bucket.observed) {
|
|
4203
4556
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
4204
4557
|
out.push({
|
|
@@ -4239,7 +4592,7 @@ function declaredHostFor(svc) {
|
|
|
4239
4592
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
4240
4593
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4241
4594
|
const e = graph.getEdgeAttributes(edgeId);
|
|
4242
|
-
if (e.type ===
|
|
4595
|
+
if (e.type === EdgeType12.CONFIGURED_BY && e.provenance === Provenance11.EXTRACTED) {
|
|
4243
4596
|
return true;
|
|
4244
4597
|
}
|
|
4245
4598
|
}
|
|
@@ -4252,8 +4605,8 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
4252
4605
|
const out = [];
|
|
4253
4606
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4254
4607
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4255
|
-
if (edge.type !==
|
|
4256
|
-
if (edge.provenance !==
|
|
4608
|
+
if (edge.type !== EdgeType12.CONNECTS_TO) continue;
|
|
4609
|
+
if (edge.provenance !== Provenance11.OBSERVED) continue;
|
|
4257
4610
|
const target = graph.getNodeAttributes(edge.target);
|
|
4258
4611
|
if (target.type !== NodeType12.DatabaseNode) continue;
|
|
4259
4612
|
const observedHost = target.host?.trim();
|
|
@@ -4277,8 +4630,8 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
4277
4630
|
const deps = svc.dependencies ?? {};
|
|
4278
4631
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
4279
4632
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
4280
|
-
if (edge.type !==
|
|
4281
|
-
if (edge.provenance !==
|
|
4633
|
+
if (edge.type !== EdgeType12.CONNECTS_TO) continue;
|
|
4634
|
+
if (edge.provenance !== Provenance11.OBSERVED) continue;
|
|
4282
4635
|
const target = graph.getNodeAttributes(edge.target);
|
|
4283
4636
|
if (target.type !== NodeType12.DatabaseNode) continue;
|
|
4284
4637
|
for (const pair of compatPairs()) {
|
|
@@ -4382,9 +4735,9 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4382
4735
|
}
|
|
4383
4736
|
|
|
4384
4737
|
// src/persist.ts
|
|
4385
|
-
import { promises as
|
|
4386
|
-
import
|
|
4387
|
-
import { Provenance as
|
|
4738
|
+
import { promises as fs18 } from "fs";
|
|
4739
|
+
import path33 from "path";
|
|
4740
|
+
import { Provenance as Provenance12, observedEdgeId as observedEdgeId2 } from "@neat.is/types";
|
|
4388
4741
|
var SCHEMA_VERSION = 4;
|
|
4389
4742
|
function migrateV1ToV2(payload) {
|
|
4390
4743
|
const nodes = payload.graph.nodes;
|
|
@@ -4406,7 +4759,7 @@ function migrateV2ToV3(payload) {
|
|
|
4406
4759
|
for (const edge of edges) {
|
|
4407
4760
|
const attrs = edge.attributes;
|
|
4408
4761
|
if (!attrs || attrs.provenance !== "FRONTIER") continue;
|
|
4409
|
-
attrs.provenance =
|
|
4762
|
+
attrs.provenance = Provenance12.OBSERVED;
|
|
4410
4763
|
const type = typeof attrs.type === "string" ? attrs.type : void 0;
|
|
4411
4764
|
const source = typeof attrs.source === "string" ? attrs.source : void 0;
|
|
4412
4765
|
const target = typeof attrs.target === "string" ? attrs.target : void 0;
|
|
@@ -4420,7 +4773,7 @@ function migrateV2ToV3(payload) {
|
|
|
4420
4773
|
return { ...payload, schemaVersion: 3 };
|
|
4421
4774
|
}
|
|
4422
4775
|
async function ensureDir(filePath) {
|
|
4423
|
-
await
|
|
4776
|
+
await fs18.mkdir(path33.dirname(filePath), { recursive: true });
|
|
4424
4777
|
}
|
|
4425
4778
|
async function saveGraphToDisk(graph, outPath) {
|
|
4426
4779
|
await ensureDir(outPath);
|
|
@@ -4430,13 +4783,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
4430
4783
|
graph: graph.export()
|
|
4431
4784
|
};
|
|
4432
4785
|
const tmp = `${outPath}.tmp`;
|
|
4433
|
-
await
|
|
4434
|
-
await
|
|
4786
|
+
await fs18.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
4787
|
+
await fs18.rename(tmp, outPath);
|
|
4435
4788
|
}
|
|
4436
4789
|
async function loadGraphFromDisk(graph, outPath) {
|
|
4437
4790
|
let raw;
|
|
4438
4791
|
try {
|
|
4439
|
-
raw = await
|
|
4792
|
+
raw = await fs18.readFile(outPath, "utf8");
|
|
4440
4793
|
} catch (err) {
|
|
4441
4794
|
if (err.code === "ENOENT") return;
|
|
4442
4795
|
throw err;
|
|
@@ -4494,7 +4847,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
4494
4847
|
}
|
|
4495
4848
|
|
|
4496
4849
|
// src/diff.ts
|
|
4497
|
-
import { promises as
|
|
4850
|
+
import { promises as fs19 } from "fs";
|
|
4498
4851
|
async function loadSnapshotForDiff(target) {
|
|
4499
4852
|
if (/^https?:\/\//i.test(target)) {
|
|
4500
4853
|
const res = await fetch(target);
|
|
@@ -4503,7 +4856,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
4503
4856
|
}
|
|
4504
4857
|
return await res.json();
|
|
4505
4858
|
}
|
|
4506
|
-
const raw = await
|
|
4859
|
+
const raw = await fs19.readFile(target, "utf8");
|
|
4507
4860
|
return JSON.parse(raw);
|
|
4508
4861
|
}
|
|
4509
4862
|
function indexEntries(entries) {
|
|
@@ -4570,23 +4923,23 @@ function canonicalJson(value) {
|
|
|
4570
4923
|
}
|
|
4571
4924
|
|
|
4572
4925
|
// src/projects.ts
|
|
4573
|
-
import
|
|
4926
|
+
import path34 from "path";
|
|
4574
4927
|
function pathsForProject(project, baseDir) {
|
|
4575
4928
|
if (project === DEFAULT_PROJECT) {
|
|
4576
4929
|
return {
|
|
4577
|
-
snapshotPath:
|
|
4578
|
-
errorsPath:
|
|
4579
|
-
staleEventsPath:
|
|
4580
|
-
embeddingsCachePath:
|
|
4581
|
-
policyViolationsPath:
|
|
4930
|
+
snapshotPath: path34.join(baseDir, "graph.json"),
|
|
4931
|
+
errorsPath: path34.join(baseDir, "errors.ndjson"),
|
|
4932
|
+
staleEventsPath: path34.join(baseDir, "stale-events.ndjson"),
|
|
4933
|
+
embeddingsCachePath: path34.join(baseDir, "embeddings.json"),
|
|
4934
|
+
policyViolationsPath: path34.join(baseDir, "policy-violations.ndjson")
|
|
4582
4935
|
};
|
|
4583
4936
|
}
|
|
4584
4937
|
return {
|
|
4585
|
-
snapshotPath:
|
|
4586
|
-
errorsPath:
|
|
4587
|
-
staleEventsPath:
|
|
4588
|
-
embeddingsCachePath:
|
|
4589
|
-
policyViolationsPath:
|
|
4938
|
+
snapshotPath: path34.join(baseDir, `${project}.json`),
|
|
4939
|
+
errorsPath: path34.join(baseDir, `errors.${project}.ndjson`),
|
|
4940
|
+
staleEventsPath: path34.join(baseDir, `stale-events.${project}.ndjson`),
|
|
4941
|
+
embeddingsCachePath: path34.join(baseDir, `embeddings.${project}.json`),
|
|
4942
|
+
policyViolationsPath: path34.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
4590
4943
|
};
|
|
4591
4944
|
}
|
|
4592
4945
|
var Projects = class {
|
|
@@ -4625,9 +4978,9 @@ function parseExtraProjects(raw) {
|
|
|
4625
4978
|
}
|
|
4626
4979
|
|
|
4627
4980
|
// src/registry.ts
|
|
4628
|
-
import { promises as
|
|
4981
|
+
import { promises as fs20 } from "fs";
|
|
4629
4982
|
import os2 from "os";
|
|
4630
|
-
import
|
|
4983
|
+
import path35 from "path";
|
|
4631
4984
|
import {
|
|
4632
4985
|
RegistryFileSchema
|
|
4633
4986
|
} from "@neat.is/types";
|
|
@@ -4635,17 +4988,17 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
4635
4988
|
var LOCK_RETRY_MS = 50;
|
|
4636
4989
|
function neatHome() {
|
|
4637
4990
|
const override = process.env.NEAT_HOME;
|
|
4638
|
-
if (override && override.length > 0) return
|
|
4639
|
-
return
|
|
4991
|
+
if (override && override.length > 0) return path35.resolve(override);
|
|
4992
|
+
return path35.join(os2.homedir(), ".neat");
|
|
4640
4993
|
}
|
|
4641
4994
|
function registryPath() {
|
|
4642
|
-
return
|
|
4995
|
+
return path35.join(neatHome(), "projects.json");
|
|
4643
4996
|
}
|
|
4644
4997
|
function registryLockPath() {
|
|
4645
|
-
return
|
|
4998
|
+
return path35.join(neatHome(), "projects.json.lock");
|
|
4646
4999
|
}
|
|
4647
5000
|
function daemonPidPath() {
|
|
4648
|
-
return
|
|
5001
|
+
return path35.join(neatHome(), "neatd.pid");
|
|
4649
5002
|
}
|
|
4650
5003
|
function isPidAliveDefault(pid) {
|
|
4651
5004
|
try {
|
|
@@ -4657,7 +5010,7 @@ function isPidAliveDefault(pid) {
|
|
|
4657
5010
|
}
|
|
4658
5011
|
async function readPidFile(file) {
|
|
4659
5012
|
try {
|
|
4660
|
-
const raw = await
|
|
5013
|
+
const raw = await fs20.readFile(file, "utf8");
|
|
4661
5014
|
const pid = Number.parseInt(raw.trim(), 10);
|
|
4662
5015
|
return Number.isInteger(pid) && pid > 0 ? pid : void 0;
|
|
4663
5016
|
} catch {
|
|
@@ -4705,32 +5058,32 @@ function lockHolderMessage(holder, lockPath, timeoutMs) {
|
|
|
4705
5058
|
}
|
|
4706
5059
|
}
|
|
4707
5060
|
async function normalizeProjectPath(input) {
|
|
4708
|
-
const resolved =
|
|
5061
|
+
const resolved = path35.resolve(input);
|
|
4709
5062
|
try {
|
|
4710
|
-
return await
|
|
5063
|
+
return await fs20.realpath(resolved);
|
|
4711
5064
|
} catch {
|
|
4712
5065
|
return resolved;
|
|
4713
5066
|
}
|
|
4714
5067
|
}
|
|
4715
5068
|
async function writeAtomically(target, contents) {
|
|
4716
|
-
await
|
|
5069
|
+
await fs20.mkdir(path35.dirname(target), { recursive: true });
|
|
4717
5070
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
4718
|
-
const fd = await
|
|
5071
|
+
const fd = await fs20.open(tmp, "w");
|
|
4719
5072
|
try {
|
|
4720
5073
|
await fd.writeFile(contents, "utf8");
|
|
4721
5074
|
await fd.sync();
|
|
4722
5075
|
} finally {
|
|
4723
5076
|
await fd.close();
|
|
4724
5077
|
}
|
|
4725
|
-
await
|
|
5078
|
+
await fs20.rename(tmp, target);
|
|
4726
5079
|
}
|
|
4727
5080
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaultLockHolderProbe) {
|
|
4728
5081
|
const deadline = Date.now() + timeoutMs;
|
|
4729
|
-
await
|
|
5082
|
+
await fs20.mkdir(path35.dirname(lockPath), { recursive: true });
|
|
4730
5083
|
let probedHolder = false;
|
|
4731
5084
|
while (true) {
|
|
4732
5085
|
try {
|
|
4733
|
-
const fd = await
|
|
5086
|
+
const fd = await fs20.open(lockPath, "wx");
|
|
4734
5087
|
try {
|
|
4735
5088
|
await fd.writeFile(`${process.pid}
|
|
4736
5089
|
`, "utf8");
|
|
@@ -4755,7 +5108,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS, probe = defaul
|
|
|
4755
5108
|
}
|
|
4756
5109
|
}
|
|
4757
5110
|
async function releaseLock(lockPath) {
|
|
4758
|
-
await
|
|
5111
|
+
await fs20.unlink(lockPath).catch(() => {
|
|
4759
5112
|
});
|
|
4760
5113
|
}
|
|
4761
5114
|
async function withLock(fn) {
|
|
@@ -4771,7 +5124,7 @@ async function readRegistry() {
|
|
|
4771
5124
|
const file = registryPath();
|
|
4772
5125
|
let raw;
|
|
4773
5126
|
try {
|
|
4774
|
-
raw = await
|
|
5127
|
+
raw = await fs20.readFile(file, "utf8");
|
|
4775
5128
|
} catch (err) {
|
|
4776
5129
|
if (err.code === "ENOENT") {
|
|
4777
5130
|
return { version: 1, projects: [] };
|
|
@@ -4868,6 +5221,319 @@ import Fastify from "fastify";
|
|
|
4868
5221
|
import cors from "@fastify/cors";
|
|
4869
5222
|
import { DivergenceTypeSchema, PoliciesCheckBodySchema, PolicySeveritySchema } from "@neat.is/types";
|
|
4870
5223
|
|
|
5224
|
+
// src/extend/index.ts
|
|
5225
|
+
import { promises as fs22 } from "fs";
|
|
5226
|
+
import path37 from "path";
|
|
5227
|
+
import os3 from "os";
|
|
5228
|
+
import { resolve as registryResolve, list as registryList } from "@neat.is/instrumentation-registry";
|
|
5229
|
+
|
|
5230
|
+
// src/installers/package-manager.ts
|
|
5231
|
+
import { promises as fs21 } from "fs";
|
|
5232
|
+
import path36 from "path";
|
|
5233
|
+
import { spawn } from "child_process";
|
|
5234
|
+
var LOCKFILE_PRIORITY = [
|
|
5235
|
+
{ lockfile: "bun.lockb", pm: "bun", args: ["install", "--no-summary"] },
|
|
5236
|
+
{ lockfile: "pnpm-lock.yaml", pm: "pnpm", args: ["install", "--no-summary"] },
|
|
5237
|
+
{ lockfile: "yarn.lock", pm: "yarn", args: ["install", "--silent"] },
|
|
5238
|
+
{
|
|
5239
|
+
lockfile: "package-lock.json",
|
|
5240
|
+
pm: "npm",
|
|
5241
|
+
args: ["install", "--no-audit", "--no-fund", "--prefer-offline"]
|
|
5242
|
+
}
|
|
5243
|
+
];
|
|
5244
|
+
var NPM_FALLBACK_ARGS = ["install", "--no-audit", "--no-fund", "--prefer-offline"];
|
|
5245
|
+
async function exists2(p) {
|
|
5246
|
+
try {
|
|
5247
|
+
await fs21.access(p);
|
|
5248
|
+
return true;
|
|
5249
|
+
} catch {
|
|
5250
|
+
return false;
|
|
5251
|
+
}
|
|
5252
|
+
}
|
|
5253
|
+
async function detectPackageManager(serviceDir) {
|
|
5254
|
+
let dir = path36.resolve(serviceDir);
|
|
5255
|
+
const stops = /* @__PURE__ */ new Set();
|
|
5256
|
+
for (let i = 0; i < 64; i++) {
|
|
5257
|
+
if (stops.has(dir)) break;
|
|
5258
|
+
stops.add(dir);
|
|
5259
|
+
for (const candidate of LOCKFILE_PRIORITY) {
|
|
5260
|
+
const lockPath = path36.join(dir, candidate.lockfile);
|
|
5261
|
+
if (await exists2(lockPath)) {
|
|
5262
|
+
return { pm: candidate.pm, cwd: dir, args: [...candidate.args] };
|
|
5263
|
+
}
|
|
5264
|
+
}
|
|
5265
|
+
const parent = path36.dirname(dir);
|
|
5266
|
+
if (parent === dir) break;
|
|
5267
|
+
dir = parent;
|
|
5268
|
+
}
|
|
5269
|
+
return { pm: "npm", cwd: path36.resolve(serviceDir), args: [...NPM_FALLBACK_ARGS] };
|
|
5270
|
+
}
|
|
5271
|
+
async function runPackageManagerInstall(cmd) {
|
|
5272
|
+
return new Promise((resolve) => {
|
|
5273
|
+
const child = spawn(cmd.pm, cmd.args, {
|
|
5274
|
+
cwd: cmd.cwd,
|
|
5275
|
+
// Inherit PATH + HOME so the user's installed managers resolve.
|
|
5276
|
+
env: process.env,
|
|
5277
|
+
// `false` keeps the parent in control of cleanup if the orchestrator
|
|
5278
|
+
// exits before install finishes. Cross-platform-safe.
|
|
5279
|
+
shell: false,
|
|
5280
|
+
stdio: ["ignore", "ignore", "pipe"]
|
|
5281
|
+
});
|
|
5282
|
+
let stderr = "";
|
|
5283
|
+
child.stderr?.on("data", (chunk) => {
|
|
5284
|
+
stderr += chunk.toString("utf8");
|
|
5285
|
+
});
|
|
5286
|
+
child.on("error", (err) => {
|
|
5287
|
+
resolve({
|
|
5288
|
+
pm: cmd.pm,
|
|
5289
|
+
cwd: cmd.cwd,
|
|
5290
|
+
args: cmd.args,
|
|
5291
|
+
exitCode: 127,
|
|
5292
|
+
stderr: stderr + `
|
|
5293
|
+
${err.message}`
|
|
5294
|
+
});
|
|
5295
|
+
});
|
|
5296
|
+
child.on("close", (code) => {
|
|
5297
|
+
resolve({
|
|
5298
|
+
pm: cmd.pm,
|
|
5299
|
+
cwd: cmd.cwd,
|
|
5300
|
+
args: cmd.args,
|
|
5301
|
+
exitCode: code ?? 1,
|
|
5302
|
+
stderr: stderr.trim()
|
|
5303
|
+
});
|
|
5304
|
+
});
|
|
5305
|
+
});
|
|
5306
|
+
}
|
|
5307
|
+
|
|
5308
|
+
// src/extend/index.ts
|
|
5309
|
+
async function fileExists2(p) {
|
|
5310
|
+
try {
|
|
5311
|
+
await fs22.access(p);
|
|
5312
|
+
return true;
|
|
5313
|
+
} catch {
|
|
5314
|
+
return false;
|
|
5315
|
+
}
|
|
5316
|
+
}
|
|
5317
|
+
async function readPackageJson(scanPath) {
|
|
5318
|
+
const pkgPath = path37.join(scanPath, "package.json");
|
|
5319
|
+
const raw = await fs22.readFile(pkgPath, "utf8");
|
|
5320
|
+
return JSON.parse(raw);
|
|
5321
|
+
}
|
|
5322
|
+
async function findHookFiles(scanPath) {
|
|
5323
|
+
const entries = await fs22.readdir(scanPath);
|
|
5324
|
+
return entries.filter(
|
|
5325
|
+
(e) => (e.startsWith("instrumentation") || e.startsWith("otel-init")) && /\.(ts|js)$/.test(e)
|
|
5326
|
+
).sort();
|
|
5327
|
+
}
|
|
5328
|
+
function extendLogPath() {
|
|
5329
|
+
return process.env.NEAT_EXTEND_LOG ?? path37.join(os3.homedir(), ".neat", "extend-log.ndjson");
|
|
5330
|
+
}
|
|
5331
|
+
async function appendExtendLog(entry) {
|
|
5332
|
+
const logPath = extendLogPath();
|
|
5333
|
+
await fs22.mkdir(path37.dirname(logPath), { recursive: true });
|
|
5334
|
+
await fs22.appendFile(logPath, JSON.stringify(entry) + "\n", "utf8");
|
|
5335
|
+
}
|
|
5336
|
+
function splicedContent(fileContent, snippet2) {
|
|
5337
|
+
if (fileContent.includes("__INSTRUMENTATION_BLOCK__")) {
|
|
5338
|
+
return fileContent.replace("__INSTRUMENTATION_BLOCK__", `${snippet2}
|
|
5339
|
+
__INSTRUMENTATION_BLOCK__`);
|
|
5340
|
+
}
|
|
5341
|
+
const lines = fileContent.split("\n");
|
|
5342
|
+
let lastPushIdx = -1;
|
|
5343
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5344
|
+
if (lines[i].includes("instrumentations.push(")) lastPushIdx = i;
|
|
5345
|
+
}
|
|
5346
|
+
if (lastPushIdx >= 0) {
|
|
5347
|
+
lines.splice(lastPushIdx + 1, 0, snippet2);
|
|
5348
|
+
return lines.join("\n");
|
|
5349
|
+
}
|
|
5350
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5351
|
+
if (lines[i].includes("new NodeSDK(")) {
|
|
5352
|
+
lines.splice(i, 0, snippet2);
|
|
5353
|
+
return lines.join("\n");
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
return null;
|
|
5357
|
+
}
|
|
5358
|
+
async function listUninstrumented(ctx) {
|
|
5359
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5360
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
5361
|
+
const results = [];
|
|
5362
|
+
for (const [library, installedVersion] of Object.entries(allDeps)) {
|
|
5363
|
+
const entry = registryResolve(library, installedVersion);
|
|
5364
|
+
if (!entry) continue;
|
|
5365
|
+
if (entry.coverage === "bundled" || entry.coverage === "http-only") continue;
|
|
5366
|
+
results.push({
|
|
5367
|
+
library,
|
|
5368
|
+
coverage: entry.coverage,
|
|
5369
|
+
installedVersion,
|
|
5370
|
+
instrumentation_package: entry.instrumentation_package,
|
|
5371
|
+
package_version: entry.package_version,
|
|
5372
|
+
registration: entry.registration,
|
|
5373
|
+
notes: entry.notes
|
|
5374
|
+
});
|
|
5375
|
+
}
|
|
5376
|
+
return results;
|
|
5377
|
+
}
|
|
5378
|
+
function lookupInstrumentation(library, installedVersion) {
|
|
5379
|
+
const entry = registryResolve(library, installedVersion);
|
|
5380
|
+
if (!entry) return null;
|
|
5381
|
+
return {
|
|
5382
|
+
library: entry.library,
|
|
5383
|
+
coverage: entry.coverage,
|
|
5384
|
+
instrumentation_package: entry.instrumentation_package,
|
|
5385
|
+
package_version: entry.package_version,
|
|
5386
|
+
registration: entry.registration,
|
|
5387
|
+
notes: entry.notes
|
|
5388
|
+
};
|
|
5389
|
+
}
|
|
5390
|
+
async function describeProjectInstrumentation(ctx) {
|
|
5391
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5392
|
+
const envNeat = await fileExists2(path37.join(ctx.scanPath, ".env.neat"));
|
|
5393
|
+
const registryInstrPackages = new Set(
|
|
5394
|
+
registryList().map((e) => e.instrumentation_package).filter((p) => !!p)
|
|
5395
|
+
);
|
|
5396
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5397
|
+
const allDeps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
5398
|
+
const installedDeps = {};
|
|
5399
|
+
for (const [key, version] of Object.entries(allDeps)) {
|
|
5400
|
+
if (key.startsWith("@opentelemetry/") || registryInstrPackages.has(key)) {
|
|
5401
|
+
installedDeps[key] = version;
|
|
5402
|
+
}
|
|
5403
|
+
}
|
|
5404
|
+
return { hookFiles, envNeat, installedDeps };
|
|
5405
|
+
}
|
|
5406
|
+
async function applyExtension(ctx, args, options) {
|
|
5407
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5408
|
+
if (hookFiles.length === 0) {
|
|
5409
|
+
throw new Error(
|
|
5410
|
+
`No instrumentation hook files found in ${ctx.scanPath}. Run \`neat init\` first.`
|
|
5411
|
+
);
|
|
5412
|
+
}
|
|
5413
|
+
for (const file of hookFiles) {
|
|
5414
|
+
const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
|
|
5415
|
+
if (content.includes(args.registration_snippet)) {
|
|
5416
|
+
return { library: args.library, filesTouched: [], depsAdded: [], installOutput: "", alreadyApplied: true };
|
|
5417
|
+
}
|
|
5418
|
+
}
|
|
5419
|
+
const primaryFile = hookFiles[0];
|
|
5420
|
+
const primaryPath = path37.join(ctx.scanPath, primaryFile);
|
|
5421
|
+
const filesTouched = [];
|
|
5422
|
+
const depsAdded = [];
|
|
5423
|
+
const pkgPath = path37.join(ctx.scanPath, "package.json");
|
|
5424
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5425
|
+
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
5426
|
+
pkg.dependencies = { ...pkg.dependencies ?? {}, [args.instrumentation_package]: args.version };
|
|
5427
|
+
await fs22.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
5428
|
+
filesTouched.push("package.json");
|
|
5429
|
+
depsAdded.push(`${args.instrumentation_package}@${args.version}`);
|
|
5430
|
+
}
|
|
5431
|
+
const hookContent = await fs22.readFile(primaryPath, "utf8");
|
|
5432
|
+
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
5433
|
+
if (!patched) {
|
|
5434
|
+
throw new Error(
|
|
5435
|
+
`Could not find instrumentation insertion point in ${primaryFile}. Expected __INSTRUMENTATION_BLOCK__, instrumentations.push(, or new NodeSDK(.`
|
|
5436
|
+
);
|
|
5437
|
+
}
|
|
5438
|
+
await fs22.writeFile(primaryPath, patched, "utf8");
|
|
5439
|
+
filesTouched.push(primaryFile);
|
|
5440
|
+
const cmd = await detectPackageManager(ctx.scanPath);
|
|
5441
|
+
const installer = options?.runInstall ?? runPackageManagerInstall;
|
|
5442
|
+
const install = await installer(cmd);
|
|
5443
|
+
const installOutput = install.exitCode === 0 ? `${cmd.pm} install succeeded` : install.stderr || `${cmd.pm} install failed (exit ${install.exitCode})`;
|
|
5444
|
+
await appendExtendLog({
|
|
5445
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
5446
|
+
project: ctx.project,
|
|
5447
|
+
library: args.library,
|
|
5448
|
+
instrumentation_package: args.instrumentation_package,
|
|
5449
|
+
version: args.version,
|
|
5450
|
+
registration_snippet: args.registration_snippet,
|
|
5451
|
+
filesTouched,
|
|
5452
|
+
depsAdded,
|
|
5453
|
+
installOutput
|
|
5454
|
+
});
|
|
5455
|
+
return { library: args.library, filesTouched, depsAdded, installOutput, alreadyApplied: false };
|
|
5456
|
+
}
|
|
5457
|
+
async function dryRunExtension(ctx, args) {
|
|
5458
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5459
|
+
if (hookFiles.length === 0) {
|
|
5460
|
+
return {
|
|
5461
|
+
library: args.library,
|
|
5462
|
+
filesTouched: [],
|
|
5463
|
+
depsToAdd: [],
|
|
5464
|
+
packageJsonPatch: {},
|
|
5465
|
+
templatePatch: "No hook files found. Run 'neat init' first."
|
|
5466
|
+
};
|
|
5467
|
+
}
|
|
5468
|
+
for (const file of hookFiles) {
|
|
5469
|
+
const content = await fs22.readFile(path37.join(ctx.scanPath, file), "utf8");
|
|
5470
|
+
if (content.includes(args.registration_snippet)) {
|
|
5471
|
+
return {
|
|
5472
|
+
library: args.library,
|
|
5473
|
+
filesTouched: [],
|
|
5474
|
+
depsToAdd: [],
|
|
5475
|
+
packageJsonPatch: {},
|
|
5476
|
+
templatePatch: "Already applied \u2014 no changes would be made."
|
|
5477
|
+
};
|
|
5478
|
+
}
|
|
5479
|
+
}
|
|
5480
|
+
const primaryFile = hookFiles[0];
|
|
5481
|
+
const filesTouched = [];
|
|
5482
|
+
const depsToAdd = [];
|
|
5483
|
+
let packageJsonPatch = {};
|
|
5484
|
+
let templatePatch = "";
|
|
5485
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5486
|
+
if (!(pkg.dependencies ?? {})[args.instrumentation_package]) {
|
|
5487
|
+
packageJsonPatch = { dependencies: { [args.instrumentation_package]: args.version } };
|
|
5488
|
+
depsToAdd.push(`${args.instrumentation_package}@${args.version}`);
|
|
5489
|
+
filesTouched.push("package.json");
|
|
5490
|
+
}
|
|
5491
|
+
const hookContent = await fs22.readFile(path37.join(ctx.scanPath, primaryFile), "utf8");
|
|
5492
|
+
const patched = splicedContent(hookContent, args.registration_snippet);
|
|
5493
|
+
if (patched) {
|
|
5494
|
+
filesTouched.push(primaryFile);
|
|
5495
|
+
templatePatch = `+ ${args.registration_snippet}`;
|
|
5496
|
+
} else {
|
|
5497
|
+
templatePatch = "Could not find insertion point in hook file.";
|
|
5498
|
+
}
|
|
5499
|
+
return { library: args.library, filesTouched, depsToAdd, packageJsonPatch, templatePatch };
|
|
5500
|
+
}
|
|
5501
|
+
async function rollbackExtension(ctx, args) {
|
|
5502
|
+
const logPath = extendLogPath();
|
|
5503
|
+
if (!await fileExists2(logPath)) {
|
|
5504
|
+
return { undone: false, message: "no apply found for library" };
|
|
5505
|
+
}
|
|
5506
|
+
const raw = await fs22.readFile(logPath, "utf8");
|
|
5507
|
+
const entries = raw.trim().split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
5508
|
+
const match = [...entries].reverse().find((e) => e.project === ctx.project && e.library === args.library);
|
|
5509
|
+
if (!match) {
|
|
5510
|
+
return { undone: false, message: "no apply found for library" };
|
|
5511
|
+
}
|
|
5512
|
+
const pkgPath = path37.join(ctx.scanPath, "package.json");
|
|
5513
|
+
if (await fileExists2(pkgPath)) {
|
|
5514
|
+
const pkg = await readPackageJson(ctx.scanPath);
|
|
5515
|
+
if (pkg.dependencies?.[match.instrumentation_package]) {
|
|
5516
|
+
const { [match.instrumentation_package]: _removed, ...rest } = pkg.dependencies;
|
|
5517
|
+
pkg.dependencies = rest;
|
|
5518
|
+
await fs22.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
5519
|
+
}
|
|
5520
|
+
}
|
|
5521
|
+
const hookFiles = await findHookFiles(ctx.scanPath);
|
|
5522
|
+
for (const file of hookFiles) {
|
|
5523
|
+
const filePath = path37.join(ctx.scanPath, file);
|
|
5524
|
+
const content = await fs22.readFile(filePath, "utf8");
|
|
5525
|
+
if (content.includes(match.registration_snippet)) {
|
|
5526
|
+
const filtered = content.split("\n").filter((line) => !line.includes(match.registration_snippet)).join("\n");
|
|
5527
|
+
await fs22.writeFile(filePath, filtered, "utf8");
|
|
5528
|
+
break;
|
|
5529
|
+
}
|
|
5530
|
+
}
|
|
5531
|
+
return {
|
|
5532
|
+
undone: true,
|
|
5533
|
+
message: `rolled back ${match.library} (${match.instrumentation_package})`
|
|
5534
|
+
};
|
|
5535
|
+
}
|
|
5536
|
+
|
|
4871
5537
|
// src/streaming.ts
|
|
4872
5538
|
var SSE_HEARTBEAT_MS = 3e4;
|
|
4873
5539
|
var SSE_BACKPRESSURE_CAP = 1e3;
|
|
@@ -5324,6 +5990,105 @@ function registerRoutes(scope, ctx) {
|
|
|
5324
5990
|
violations
|
|
5325
5991
|
};
|
|
5326
5992
|
});
|
|
5993
|
+
scope.get("/extend/list-uninstrumented", async (req, reply) => {
|
|
5994
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
5995
|
+
if (!proj) return;
|
|
5996
|
+
if (!proj.scanPath) {
|
|
5997
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
5998
|
+
}
|
|
5999
|
+
try {
|
|
6000
|
+
const results = await listUninstrumented({ project: proj.name, scanPath: proj.scanPath });
|
|
6001
|
+
return { libraries: results };
|
|
6002
|
+
} catch (err) {
|
|
6003
|
+
return reply.code(500).send({ error: err.message });
|
|
6004
|
+
}
|
|
6005
|
+
});
|
|
6006
|
+
scope.get("/extend/lookup", async (req, reply) => {
|
|
6007
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6008
|
+
if (!proj) return;
|
|
6009
|
+
const { library, version } = req.query;
|
|
6010
|
+
if (!library) {
|
|
6011
|
+
return reply.code(400).send({ error: "query parameter `library` is required" });
|
|
6012
|
+
}
|
|
6013
|
+
const result = lookupInstrumentation(library, version);
|
|
6014
|
+
if (!result) {
|
|
6015
|
+
return reply.code(404).send({ error: "library not found in registry", library });
|
|
6016
|
+
}
|
|
6017
|
+
return result;
|
|
6018
|
+
});
|
|
6019
|
+
scope.get("/extend/describe", async (req, reply) => {
|
|
6020
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6021
|
+
if (!proj) return;
|
|
6022
|
+
if (!proj.scanPath) {
|
|
6023
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
6024
|
+
}
|
|
6025
|
+
try {
|
|
6026
|
+
const state = await describeProjectInstrumentation({ project: proj.name, scanPath: proj.scanPath });
|
|
6027
|
+
return state;
|
|
6028
|
+
} catch (err) {
|
|
6029
|
+
return reply.code(500).send({ error: err.message });
|
|
6030
|
+
}
|
|
6031
|
+
});
|
|
6032
|
+
scope.post("/extend/apply", async (req, reply) => {
|
|
6033
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6034
|
+
if (!proj) return;
|
|
6035
|
+
if (!proj.scanPath) {
|
|
6036
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
6037
|
+
}
|
|
6038
|
+
const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
|
|
6039
|
+
if (!library || !instrumentation_package || !version || !registration_snippet) {
|
|
6040
|
+
return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
|
|
6041
|
+
}
|
|
6042
|
+
try {
|
|
6043
|
+
const result = await applyExtension(
|
|
6044
|
+
{ project: proj.name, scanPath: proj.scanPath },
|
|
6045
|
+
{ library, instrumentation_package, version, registration_snippet }
|
|
6046
|
+
);
|
|
6047
|
+
return result;
|
|
6048
|
+
} catch (err) {
|
|
6049
|
+
return reply.code(500).send({ error: err.message });
|
|
6050
|
+
}
|
|
6051
|
+
});
|
|
6052
|
+
scope.post("/extend/dry-run", async (req, reply) => {
|
|
6053
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6054
|
+
if (!proj) return;
|
|
6055
|
+
if (!proj.scanPath) {
|
|
6056
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
6057
|
+
}
|
|
6058
|
+
const { library, instrumentation_package, version, registration_snippet } = req.body ?? {};
|
|
6059
|
+
if (!library || !instrumentation_package || !version || !registration_snippet) {
|
|
6060
|
+
return reply.code(400).send({ error: "body must include library, instrumentation_package, version, registration_snippet" });
|
|
6061
|
+
}
|
|
6062
|
+
try {
|
|
6063
|
+
const result = await dryRunExtension(
|
|
6064
|
+
{ project: proj.name, scanPath: proj.scanPath },
|
|
6065
|
+
{ library, instrumentation_package, version, registration_snippet }
|
|
6066
|
+
);
|
|
6067
|
+
return result;
|
|
6068
|
+
} catch (err) {
|
|
6069
|
+
return reply.code(500).send({ error: err.message });
|
|
6070
|
+
}
|
|
6071
|
+
});
|
|
6072
|
+
scope.post("/extend/rollback", async (req, reply) => {
|
|
6073
|
+
const proj = resolveProject(registry, req, reply, ctx.bootstrap);
|
|
6074
|
+
if (!proj) return;
|
|
6075
|
+
if (!proj.scanPath) {
|
|
6076
|
+
return reply.code(409).send({ error: "scan path not configured for this project", project: proj.name });
|
|
6077
|
+
}
|
|
6078
|
+
const { library } = req.body ?? {};
|
|
6079
|
+
if (!library) {
|
|
6080
|
+
return reply.code(400).send({ error: "body must include library" });
|
|
6081
|
+
}
|
|
6082
|
+
try {
|
|
6083
|
+
const result = await rollbackExtension(
|
|
6084
|
+
{ project: proj.name, scanPath: proj.scanPath },
|
|
6085
|
+
{ library }
|
|
6086
|
+
);
|
|
6087
|
+
return result;
|
|
6088
|
+
} catch (err) {
|
|
6089
|
+
return reply.code(500).send({ error: err.message });
|
|
6090
|
+
}
|
|
6091
|
+
});
|
|
5327
6092
|
}
|
|
5328
6093
|
async function buildApi(opts) {
|
|
5329
6094
|
const app = Fastify({ logger: false });
|
|
@@ -5459,6 +6224,7 @@ export {
|
|
|
5459
6224
|
discoverServices,
|
|
5460
6225
|
addServiceNodes,
|
|
5461
6226
|
addServiceAliases,
|
|
6227
|
+
addImports,
|
|
5462
6228
|
addDatabasesAndCompat,
|
|
5463
6229
|
addConfigNodes,
|
|
5464
6230
|
addCallEdges,
|
|
@@ -5469,6 +6235,8 @@ export {
|
|
|
5469
6235
|
saveGraphToDisk,
|
|
5470
6236
|
loadGraphFromDisk,
|
|
5471
6237
|
startPersistLoop,
|
|
6238
|
+
detectPackageManager,
|
|
6239
|
+
runPackageManagerInstall,
|
|
5472
6240
|
loadSnapshotForDiff,
|
|
5473
6241
|
computeGraphDiff,
|
|
5474
6242
|
pathsForProject,
|
|
@@ -5488,4 +6256,4 @@ export {
|
|
|
5488
6256
|
removeProject,
|
|
5489
6257
|
buildApi
|
|
5490
6258
|
};
|
|
5491
|
-
//# sourceMappingURL=chunk-
|
|
6259
|
+
//# sourceMappingURL=chunk-5W7H35JJ.js.map
|