@neat.is/core 0.3.2 → 0.3.4
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-FIXKIYNF.js → chunk-33ZZ2ZID.js} +606 -240
- package/dist/chunk-33ZZ2ZID.js.map +1 -0
- package/dist/{chunk-66Z5IEFY.js → chunk-T3A2GK3X.js} +2 -2
- package/dist/cli.cjs +674 -337
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +26 -26
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +565 -249
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +2 -2
- package/dist/neatd.cjs +572 -256
- package/dist/neatd.cjs.map +1 -1
- package/dist/neatd.js +2 -2
- package/dist/server.cjs +535 -219
- package/dist/server.cjs.map +1 -1
- package/dist/server.js +1 -1
- package/package.json +2 -2
- package/dist/chunk-FIXKIYNF.js.map +0 -1
- /package/dist/{chunk-66Z5IEFY.js.map → chunk-T3A2GK3X.js.map} +0 -0
|
@@ -427,19 +427,19 @@ function confidenceFromMix(edges, now = Date.now()) {
|
|
|
427
427
|
function longestIncomingWalk(graph, start, maxDepth) {
|
|
428
428
|
let best = { path: [start], edges: [] };
|
|
429
429
|
const visited = /* @__PURE__ */ new Set([start]);
|
|
430
|
-
function step(node,
|
|
431
|
-
if (
|
|
432
|
-
best = { path: [...
|
|
430
|
+
function step(node, path34, edges) {
|
|
431
|
+
if (path34.length > best.path.length) {
|
|
432
|
+
best = { path: [...path34], edges: [...edges] };
|
|
433
433
|
}
|
|
434
|
-
if (
|
|
434
|
+
if (path34.length - 1 >= maxDepth) return;
|
|
435
435
|
const incoming = bestEdgeBySource(graph, graph.inboundEdges(node));
|
|
436
436
|
for (const [srcId, edge] of incoming) {
|
|
437
437
|
if (visited.has(srcId)) continue;
|
|
438
438
|
visited.add(srcId);
|
|
439
|
-
|
|
439
|
+
path34.push(srcId);
|
|
440
440
|
edges.push(edge);
|
|
441
|
-
step(srcId,
|
|
442
|
-
|
|
441
|
+
step(srcId, path34, edges);
|
|
442
|
+
path34.pop();
|
|
443
443
|
edges.pop();
|
|
444
444
|
visited.delete(srcId);
|
|
445
445
|
}
|
|
@@ -1018,6 +1018,7 @@ import {
|
|
|
1018
1018
|
EdgeType as EdgeType2,
|
|
1019
1019
|
NodeType as NodeType3,
|
|
1020
1020
|
Provenance as Provenance3,
|
|
1021
|
+
confidenceForObservedSignal,
|
|
1021
1022
|
databaseId,
|
|
1022
1023
|
extractedEdgeId,
|
|
1023
1024
|
frontierEdgeId,
|
|
@@ -1214,35 +1215,37 @@ function upsertObservedEdge(graph, type, source, target, ts, isError = false) {
|
|
|
1214
1215
|
const existing = graph.getEdgeAttributes(id);
|
|
1215
1216
|
const newSpanCount = (existing.signal?.spanCount ?? existing.callCount ?? 0) + 1;
|
|
1216
1217
|
const newErrorCount = (existing.signal?.errorCount ?? 0) + (isError ? 1 : 0);
|
|
1218
|
+
const newSignal = {
|
|
1219
|
+
spanCount: newSpanCount,
|
|
1220
|
+
errorCount: newErrorCount,
|
|
1221
|
+
lastObservedAgeMs: 0
|
|
1222
|
+
};
|
|
1217
1223
|
const updated = {
|
|
1218
1224
|
...existing,
|
|
1219
1225
|
provenance: Provenance3.OBSERVED,
|
|
1220
1226
|
lastObserved: ts,
|
|
1221
1227
|
callCount: newSpanCount,
|
|
1222
|
-
signal:
|
|
1223
|
-
|
|
1224
|
-
errorCount: newErrorCount,
|
|
1225
|
-
lastObservedAgeMs: 0
|
|
1226
|
-
},
|
|
1227
|
-
confidence: 1
|
|
1228
|
+
signal: newSignal,
|
|
1229
|
+
confidence: confidenceForObservedSignal(newSignal)
|
|
1228
1230
|
};
|
|
1229
1231
|
graph.replaceEdgeAttributes(id, updated);
|
|
1230
1232
|
return { edge: updated, created: false };
|
|
1231
1233
|
}
|
|
1234
|
+
const signal = {
|
|
1235
|
+
spanCount: 1,
|
|
1236
|
+
errorCount: isError ? 1 : 0,
|
|
1237
|
+
lastObservedAgeMs: 0
|
|
1238
|
+
};
|
|
1232
1239
|
const edge = {
|
|
1233
1240
|
id,
|
|
1234
1241
|
source,
|
|
1235
1242
|
target,
|
|
1236
1243
|
type,
|
|
1237
1244
|
provenance: Provenance3.OBSERVED,
|
|
1238
|
-
confidence:
|
|
1245
|
+
confidence: confidenceForObservedSignal(signal),
|
|
1239
1246
|
lastObserved: ts,
|
|
1240
1247
|
callCount: 1,
|
|
1241
|
-
signal
|
|
1242
|
-
spanCount: 1,
|
|
1243
|
-
errorCount: isError ? 1 : 0,
|
|
1244
|
-
lastObservedAgeMs: 0
|
|
1245
|
-
}
|
|
1248
|
+
signal
|
|
1246
1249
|
};
|
|
1247
1250
|
graph.addEdgeWithKey(id, source, target, edge);
|
|
1248
1251
|
return { edge, created: true };
|
|
@@ -1560,16 +1563,71 @@ async function readErrorEvents(errorsPath) {
|
|
|
1560
1563
|
}
|
|
1561
1564
|
}
|
|
1562
1565
|
|
|
1566
|
+
// src/extract/errors.ts
|
|
1567
|
+
import { promises as fs4 } from "fs";
|
|
1568
|
+
import path4 from "path";
|
|
1569
|
+
var sink = [];
|
|
1570
|
+
function recordExtractionError(producer, file, err) {
|
|
1571
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1572
|
+
sink.push({
|
|
1573
|
+
producer,
|
|
1574
|
+
file,
|
|
1575
|
+
error: e.message,
|
|
1576
|
+
stack: e.stack,
|
|
1577
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1578
|
+
source: "extract"
|
|
1579
|
+
});
|
|
1580
|
+
console.warn(`[neat] ${producer} skipped ${file}: ${e.message}`);
|
|
1581
|
+
}
|
|
1582
|
+
function drainExtractionErrors() {
|
|
1583
|
+
return sink.splice(0, sink.length);
|
|
1584
|
+
}
|
|
1585
|
+
async function writeExtractionErrors(errors, errorsPath) {
|
|
1586
|
+
if (errors.length === 0) return;
|
|
1587
|
+
await fs4.mkdir(path4.dirname(errorsPath), { recursive: true });
|
|
1588
|
+
const lines = errors.map((e) => JSON.stringify(e)).join("\n") + "\n";
|
|
1589
|
+
await fs4.appendFile(errorsPath, lines, "utf8");
|
|
1590
|
+
}
|
|
1591
|
+
function isStrictExtractionEnabled() {
|
|
1592
|
+
const raw = process.env.NEAT_STRICT_EXTRACTION;
|
|
1593
|
+
return raw === "1" || raw === "true";
|
|
1594
|
+
}
|
|
1595
|
+
function formatExtractionBanner(count) {
|
|
1596
|
+
if (count === 1) return `[neat] 1 file skipped due to parse errors`;
|
|
1597
|
+
return `[neat] ${count} files skipped due to parse errors`;
|
|
1598
|
+
}
|
|
1599
|
+
var droppedSink = [];
|
|
1600
|
+
function noteExtractedDropped(edge) {
|
|
1601
|
+
droppedSink.push(edge);
|
|
1602
|
+
}
|
|
1603
|
+
function drainDroppedExtracted() {
|
|
1604
|
+
return droppedSink.splice(0, droppedSink.length);
|
|
1605
|
+
}
|
|
1606
|
+
function isRejectedLogEnabled() {
|
|
1607
|
+
const raw = process.env.NEAT_EXTRACTED_REJECTED_LOG;
|
|
1608
|
+
return raw === "1" || raw === "true";
|
|
1609
|
+
}
|
|
1610
|
+
async function writeRejectedExtracted(drops, rejectedPath) {
|
|
1611
|
+
if (drops.length === 0) return;
|
|
1612
|
+
await fs4.mkdir(path4.dirname(rejectedPath), { recursive: true });
|
|
1613
|
+
const lines = drops.map((d) => JSON.stringify({ ...d, ts: (/* @__PURE__ */ new Date()).toISOString() })).join("\n") + "\n";
|
|
1614
|
+
await fs4.appendFile(rejectedPath, lines, "utf8");
|
|
1615
|
+
}
|
|
1616
|
+
function formatPrecisionFloorBanner(count) {
|
|
1617
|
+
if (count === 1) return `[neat] 1 extracted edge dropped below precision floor`;
|
|
1618
|
+
return `[neat] ${count} extracted edges dropped below precision floor`;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1563
1621
|
// src/extract/services.ts
|
|
1564
|
-
import { promises as
|
|
1565
|
-
import
|
|
1622
|
+
import { promises as fs8 } from "fs";
|
|
1623
|
+
import path8 from "path";
|
|
1566
1624
|
import ignore from "ignore";
|
|
1567
1625
|
import { minimatch as minimatch2 } from "minimatch";
|
|
1568
1626
|
import { NodeType as NodeType4, serviceId as serviceId2 } from "@neat.is/types";
|
|
1569
1627
|
|
|
1570
1628
|
// src/extract/shared.ts
|
|
1571
|
-
import { promises as
|
|
1572
|
-
import
|
|
1629
|
+
import { promises as fs5 } from "fs";
|
|
1630
|
+
import path5 from "path";
|
|
1573
1631
|
import { parse as parseYaml } from "yaml";
|
|
1574
1632
|
import { extractedEdgeId as extractedEdgeId2 } from "@neat.is/types";
|
|
1575
1633
|
var SERVICE_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs", ".cjs", ".ts", ".tsx", ".py"]);
|
|
@@ -1583,26 +1641,124 @@ var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
|
1583
1641
|
".next"
|
|
1584
1642
|
]);
|
|
1585
1643
|
function isConfigFile(name) {
|
|
1586
|
-
const ext =
|
|
1644
|
+
const ext = path5.extname(name);
|
|
1587
1645
|
if (CONFIG_FILE_EXTENSIONS.has(ext)) return { match: true, fileType: ext.slice(1) };
|
|
1588
|
-
if (name === ".env" || name.startsWith(".env."))
|
|
1646
|
+
if (name === ".env" || name.startsWith(".env.")) {
|
|
1647
|
+
if (isEnvTemplateFile(name)) return { match: false, fileType: "" };
|
|
1648
|
+
return { match: true, fileType: "env" };
|
|
1649
|
+
}
|
|
1589
1650
|
return { match: false, fileType: "" };
|
|
1590
1651
|
}
|
|
1652
|
+
function isTestPath(filePath) {
|
|
1653
|
+
const normalised = filePath.replace(/\\/g, "/");
|
|
1654
|
+
const segments = normalised.split("/");
|
|
1655
|
+
for (const seg of segments) {
|
|
1656
|
+
if (seg === "__tests__" || seg === "__fixtures__" || seg === "integration-tests") {
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
const base = segments[segments.length - 1] ?? "";
|
|
1661
|
+
return /\.(spec|test)\.(?:tsx?|jsx?|mjs|cjs|py)$/i.test(base);
|
|
1662
|
+
}
|
|
1663
|
+
function isEnvTemplateFile(name) {
|
|
1664
|
+
if (name === ".env.template" || name === ".env.example" || name === ".env.sample") {
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
return /^\.env\.[^.]+\.(?:template|example|sample)$/i.test(name);
|
|
1668
|
+
}
|
|
1669
|
+
function maskCommentsInSource(src) {
|
|
1670
|
+
const len = src.length;
|
|
1671
|
+
const out = new Array(len);
|
|
1672
|
+
let i = 0;
|
|
1673
|
+
let inString = 0;
|
|
1674
|
+
let escaped = false;
|
|
1675
|
+
while (i < len) {
|
|
1676
|
+
const c = src[i];
|
|
1677
|
+
if (inString !== 0) {
|
|
1678
|
+
out[i] = c;
|
|
1679
|
+
if (escaped) {
|
|
1680
|
+
escaped = false;
|
|
1681
|
+
} else if (c === "\\") {
|
|
1682
|
+
escaped = true;
|
|
1683
|
+
} else if (c === inString) {
|
|
1684
|
+
inString = 0;
|
|
1685
|
+
}
|
|
1686
|
+
i++;
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (c === "/" && i + 1 < len) {
|
|
1690
|
+
const next = src[i + 1];
|
|
1691
|
+
if (next === "/") {
|
|
1692
|
+
out[i] = " ";
|
|
1693
|
+
out[i + 1] = " ";
|
|
1694
|
+
let j = i + 2;
|
|
1695
|
+
while (j < len && src[j] !== "\n") {
|
|
1696
|
+
out[j] = " ";
|
|
1697
|
+
j++;
|
|
1698
|
+
}
|
|
1699
|
+
i = j;
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
if (next === "*") {
|
|
1703
|
+
out[i] = " ";
|
|
1704
|
+
out[i + 1] = " ";
|
|
1705
|
+
let j = i + 2;
|
|
1706
|
+
while (j < len) {
|
|
1707
|
+
if (src[j] === "\n") {
|
|
1708
|
+
out[j] = "\n";
|
|
1709
|
+
j++;
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
if (src[j] === "*" && j + 1 < len && src[j + 1] === "/") {
|
|
1713
|
+
out[j] = " ";
|
|
1714
|
+
out[j + 1] = " ";
|
|
1715
|
+
j += 2;
|
|
1716
|
+
break;
|
|
1717
|
+
}
|
|
1718
|
+
out[j] = " ";
|
|
1719
|
+
j++;
|
|
1720
|
+
}
|
|
1721
|
+
i = j;
|
|
1722
|
+
continue;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
out[i] = c;
|
|
1726
|
+
if (c === "'" || c === '"' || c === "`") inString = c;
|
|
1727
|
+
i++;
|
|
1728
|
+
}
|
|
1729
|
+
return out.join("");
|
|
1730
|
+
}
|
|
1731
|
+
var URL_LIKE = /^(?:[a-z][a-z0-9+.-]*:)?\/\//i;
|
|
1732
|
+
function urlMatchesHost(urlString, host) {
|
|
1733
|
+
if (typeof urlString !== "string" || urlString.length === 0) return false;
|
|
1734
|
+
if (!URL_LIKE.test(urlString)) return false;
|
|
1735
|
+
const [wantedHost, wantedPort] = host.split(":");
|
|
1736
|
+
let parsed;
|
|
1737
|
+
try {
|
|
1738
|
+
const candidate = urlString.startsWith("//") ? `http:${urlString}` : urlString;
|
|
1739
|
+
parsed = new URL(candidate);
|
|
1740
|
+
} catch {
|
|
1741
|
+
return false;
|
|
1742
|
+
}
|
|
1743
|
+
if (parsed.hostname.toLowerCase() !== (wantedHost ?? "").toLowerCase()) return false;
|
|
1744
|
+
if (wantedPort && parsed.port !== wantedPort) return false;
|
|
1745
|
+
return true;
|
|
1746
|
+
}
|
|
1591
1747
|
function cleanVersion(raw) {
|
|
1592
1748
|
if (!raw) return void 0;
|
|
1593
1749
|
return raw.replace(/^[\^~><=v\s]+/, "").trim() || void 0;
|
|
1594
1750
|
}
|
|
1595
1751
|
async function readJson(filePath) {
|
|
1596
|
-
const raw = await
|
|
1752
|
+
const raw = await fs5.readFile(filePath, "utf8");
|
|
1597
1753
|
return JSON.parse(raw);
|
|
1598
1754
|
}
|
|
1599
1755
|
async function readYaml(filePath) {
|
|
1600
|
-
const raw = await
|
|
1756
|
+
const raw = await fs5.readFile(filePath, "utf8");
|
|
1601
1757
|
return parseYaml(raw);
|
|
1602
1758
|
}
|
|
1603
1759
|
async function exists(p) {
|
|
1604
1760
|
try {
|
|
1605
|
-
await
|
|
1761
|
+
await fs5.access(p);
|
|
1606
1762
|
return true;
|
|
1607
1763
|
} catch {
|
|
1608
1764
|
return false;
|
|
@@ -1610,8 +1766,8 @@ async function exists(p) {
|
|
|
1610
1766
|
}
|
|
1611
1767
|
|
|
1612
1768
|
// src/extract/python.ts
|
|
1613
|
-
import { promises as
|
|
1614
|
-
import
|
|
1769
|
+
import { promises as fs6 } from "fs";
|
|
1770
|
+
import path6 from "path";
|
|
1615
1771
|
import { parse as parseToml } from "smol-toml";
|
|
1616
1772
|
var REQUIREMENT_LINE = /^\s*([A-Za-z0-9_.-]+)(?:\[[^\]]*\])?\s*(?:(==)\s*([A-Za-z0-9_.+-]+))?/;
|
|
1617
1773
|
function parseRequirementsTxt(content) {
|
|
@@ -1644,25 +1800,25 @@ function depsFromPyProject(pyproject) {
|
|
|
1644
1800
|
return out;
|
|
1645
1801
|
}
|
|
1646
1802
|
async function discoverPythonService(serviceDir) {
|
|
1647
|
-
const pyprojectPath =
|
|
1648
|
-
const requirementsPath =
|
|
1649
|
-
const setupPath =
|
|
1803
|
+
const pyprojectPath = path6.join(serviceDir, "pyproject.toml");
|
|
1804
|
+
const requirementsPath = path6.join(serviceDir, "requirements.txt");
|
|
1805
|
+
const setupPath = path6.join(serviceDir, "setup.py");
|
|
1650
1806
|
const hasPyproject = await exists(pyprojectPath);
|
|
1651
1807
|
const hasRequirements = await exists(requirementsPath);
|
|
1652
1808
|
const hasSetup = await exists(setupPath);
|
|
1653
1809
|
if (!hasPyproject && !hasRequirements && !hasSetup) return null;
|
|
1654
|
-
let name =
|
|
1810
|
+
let name = path6.basename(serviceDir);
|
|
1655
1811
|
let version;
|
|
1656
1812
|
const dependencies = {};
|
|
1657
1813
|
if (hasPyproject) {
|
|
1658
|
-
const raw = await
|
|
1814
|
+
const raw = await fs6.readFile(pyprojectPath, "utf8");
|
|
1659
1815
|
const pyproject = parseToml(raw);
|
|
1660
1816
|
name = pyproject.project?.name ?? pyproject.tool?.poetry?.name ?? name;
|
|
1661
1817
|
version = pyproject.project?.version ?? pyproject.tool?.poetry?.version ?? void 0;
|
|
1662
1818
|
Object.assign(dependencies, depsFromPyProject(pyproject));
|
|
1663
1819
|
}
|
|
1664
1820
|
if (hasRequirements) {
|
|
1665
|
-
const raw = await
|
|
1821
|
+
const raw = await fs6.readFile(requirementsPath, "utf8");
|
|
1666
1822
|
Object.assign(dependencies, parseRequirementsTxt(raw));
|
|
1667
1823
|
}
|
|
1668
1824
|
return { name, version, dependencies };
|
|
@@ -1676,17 +1832,17 @@ function pythonToPackage(service) {
|
|
|
1676
1832
|
}
|
|
1677
1833
|
|
|
1678
1834
|
// src/extract/owners.ts
|
|
1679
|
-
import { promises as
|
|
1680
|
-
import
|
|
1835
|
+
import { promises as fs7 } from "fs";
|
|
1836
|
+
import path7 from "path";
|
|
1681
1837
|
import { minimatch } from "minimatch";
|
|
1682
1838
|
async function loadCodeowners(scanPath) {
|
|
1683
1839
|
const candidates = [
|
|
1684
|
-
|
|
1685
|
-
|
|
1840
|
+
path7.join(scanPath, "CODEOWNERS"),
|
|
1841
|
+
path7.join(scanPath, ".github", "CODEOWNERS")
|
|
1686
1842
|
];
|
|
1687
1843
|
for (const file of candidates) {
|
|
1688
1844
|
if (await exists(file)) {
|
|
1689
|
-
const raw = await
|
|
1845
|
+
const raw = await fs7.readFile(file, "utf8");
|
|
1690
1846
|
return parseCodeowners(raw);
|
|
1691
1847
|
}
|
|
1692
1848
|
}
|
|
@@ -1704,7 +1860,7 @@ function parseCodeowners(raw) {
|
|
|
1704
1860
|
return { rules };
|
|
1705
1861
|
}
|
|
1706
1862
|
function matchOwner(file, repoPath) {
|
|
1707
|
-
const normalized = repoPath.split(
|
|
1863
|
+
const normalized = repoPath.split(path7.sep).join("/");
|
|
1708
1864
|
for (const rule of file.rules) {
|
|
1709
1865
|
if (matchesPattern(rule.pattern, normalized)) return rule.owners;
|
|
1710
1866
|
}
|
|
@@ -1720,7 +1876,7 @@ function matchesPattern(rawPattern, repoPath) {
|
|
|
1720
1876
|
return false;
|
|
1721
1877
|
}
|
|
1722
1878
|
async function readPackageJsonAuthor(serviceDir) {
|
|
1723
|
-
const pkgPath =
|
|
1879
|
+
const pkgPath = path7.join(serviceDir, "package.json");
|
|
1724
1880
|
if (!await exists(pkgPath)) return null;
|
|
1725
1881
|
try {
|
|
1726
1882
|
const pkg = await readJson(pkgPath);
|
|
@@ -1757,21 +1913,21 @@ function workspaceGlobs(pkg) {
|
|
|
1757
1913
|
return null;
|
|
1758
1914
|
}
|
|
1759
1915
|
async function loadGitignore(scanPath) {
|
|
1760
|
-
const gitignorePath =
|
|
1916
|
+
const gitignorePath = path8.join(scanPath, ".gitignore");
|
|
1761
1917
|
if (!await exists(gitignorePath)) return null;
|
|
1762
|
-
const raw = await
|
|
1918
|
+
const raw = await fs8.readFile(gitignorePath, "utf8");
|
|
1763
1919
|
return ignore().add(raw);
|
|
1764
1920
|
}
|
|
1765
1921
|
async function walkDirs(start, scanPath, options, visit) {
|
|
1766
1922
|
async function recurse(current, depth) {
|
|
1767
1923
|
if (depth > options.maxDepth) return;
|
|
1768
|
-
const entries = await
|
|
1924
|
+
const entries = await fs8.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
1769
1925
|
for (const entry of entries) {
|
|
1770
1926
|
if (!entry.isDirectory()) continue;
|
|
1771
1927
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
1772
|
-
const child =
|
|
1928
|
+
const child = path8.join(current, entry.name);
|
|
1773
1929
|
if (options.ig) {
|
|
1774
|
-
const rel =
|
|
1930
|
+
const rel = path8.relative(scanPath, child).split(path8.sep).join("/");
|
|
1775
1931
|
if (rel && options.ig.ignores(rel + "/")) continue;
|
|
1776
1932
|
}
|
|
1777
1933
|
await visit(child);
|
|
@@ -1786,8 +1942,8 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1786
1942
|
for (const raw of globs) {
|
|
1787
1943
|
const pattern = raw.replace(/^\.\//, "");
|
|
1788
1944
|
if (!pattern.includes("*")) {
|
|
1789
|
-
const candidate =
|
|
1790
|
-
if (await exists(
|
|
1945
|
+
const candidate = path8.join(scanPath, pattern);
|
|
1946
|
+
if (await exists(path8.join(candidate, "package.json"))) found.add(candidate);
|
|
1791
1947
|
continue;
|
|
1792
1948
|
}
|
|
1793
1949
|
const segments = pattern.split("/");
|
|
@@ -1796,13 +1952,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1796
1952
|
if (seg.includes("*")) break;
|
|
1797
1953
|
staticSegments.push(seg);
|
|
1798
1954
|
}
|
|
1799
|
-
const start =
|
|
1955
|
+
const start = path8.join(scanPath, ...staticSegments);
|
|
1800
1956
|
if (!await exists(start)) continue;
|
|
1801
1957
|
const hasDoubleStar = pattern.includes("**");
|
|
1802
1958
|
const walkDepth = hasDoubleStar ? scanDepth : Math.max(0, segments.length - staticSegments.length - 1);
|
|
1803
1959
|
await walkDirs(start, scanPath, { maxDepth: walkDepth, ig: null }, async (dir) => {
|
|
1804
|
-
const rel =
|
|
1805
|
-
if (minimatch2(rel, pattern) && await exists(
|
|
1960
|
+
const rel = path8.relative(scanPath, dir).split(path8.sep).join("/");
|
|
1961
|
+
if (minimatch2(rel, pattern) && await exists(path8.join(dir, "package.json"))) {
|
|
1806
1962
|
found.add(dir);
|
|
1807
1963
|
}
|
|
1808
1964
|
});
|
|
@@ -1810,15 +1966,13 @@ async function expandWorkspaceGlobs(scanPath, globs) {
|
|
|
1810
1966
|
return [...found];
|
|
1811
1967
|
}
|
|
1812
1968
|
async function discoverNodeService(scanPath, dir) {
|
|
1813
|
-
const pkgPath =
|
|
1969
|
+
const pkgPath = path8.join(dir, "package.json");
|
|
1814
1970
|
if (!await exists(pkgPath)) return null;
|
|
1815
1971
|
let pkg;
|
|
1816
1972
|
try {
|
|
1817
1973
|
pkg = await readJson(pkgPath);
|
|
1818
1974
|
} catch (err) {
|
|
1819
|
-
|
|
1820
|
-
`[neat] services skipped ${path7.relative(scanPath, pkgPath)}: ${err.message}`
|
|
1821
|
-
);
|
|
1975
|
+
recordExtractionError("services", path8.relative(scanPath, pkgPath), err);
|
|
1822
1976
|
return null;
|
|
1823
1977
|
}
|
|
1824
1978
|
if (!pkg.name) return null;
|
|
@@ -1829,7 +1983,7 @@ async function discoverNodeService(scanPath, dir) {
|
|
|
1829
1983
|
language: "javascript",
|
|
1830
1984
|
version: pkg.version,
|
|
1831
1985
|
dependencies: pkg.dependencies ?? {},
|
|
1832
|
-
repoPath:
|
|
1986
|
+
repoPath: path8.relative(scanPath, dir),
|
|
1833
1987
|
...pkg.engines?.node ? { nodeEngine: pkg.engines.node } : {}
|
|
1834
1988
|
};
|
|
1835
1989
|
return { pkg, dir, node };
|
|
@@ -1845,19 +1999,21 @@ async function discoverPyService(scanPath, dir) {
|
|
|
1845
1999
|
language: "python",
|
|
1846
2000
|
version: py.version,
|
|
1847
2001
|
dependencies: py.dependencies,
|
|
1848
|
-
repoPath:
|
|
2002
|
+
repoPath: path8.relative(scanPath, dir)
|
|
1849
2003
|
};
|
|
1850
2004
|
return { pkg, dir, node };
|
|
1851
2005
|
}
|
|
1852
2006
|
async function discoverServices(scanPath) {
|
|
1853
|
-
const rootPkgPath =
|
|
2007
|
+
const rootPkgPath = path8.join(scanPath, "package.json");
|
|
1854
2008
|
let rootPkg = null;
|
|
1855
2009
|
if (await exists(rootPkgPath)) {
|
|
1856
2010
|
try {
|
|
1857
2011
|
rootPkg = await readJson(rootPkgPath);
|
|
1858
2012
|
} catch (err) {
|
|
1859
|
-
|
|
1860
|
-
|
|
2013
|
+
recordExtractionError(
|
|
2014
|
+
"services workspaces",
|
|
2015
|
+
path8.relative(scanPath, rootPkgPath),
|
|
2016
|
+
err
|
|
1861
2017
|
);
|
|
1862
2018
|
}
|
|
1863
2019
|
}
|
|
@@ -1873,9 +2029,9 @@ async function discoverServices(scanPath) {
|
|
|
1873
2029
|
scanPath,
|
|
1874
2030
|
{ maxDepth: parseScanDepth(), ig },
|
|
1875
2031
|
async (dir) => {
|
|
1876
|
-
if (await exists(
|
|
2032
|
+
if (await exists(path8.join(dir, "package.json"))) {
|
|
1877
2033
|
candidateDirs.push(dir);
|
|
1878
|
-
} else if (await exists(
|
|
2034
|
+
} else if (await exists(path8.join(dir, "pyproject.toml")) || await exists(path8.join(dir, "requirements.txt")) || await exists(path8.join(dir, "setup.py"))) {
|
|
1879
2035
|
candidateDirs.push(dir);
|
|
1880
2036
|
}
|
|
1881
2037
|
}
|
|
@@ -1889,8 +2045,8 @@ async function discoverServices(scanPath) {
|
|
|
1889
2045
|
if (!service) continue;
|
|
1890
2046
|
const existingDir = seen.get(service.node.name);
|
|
1891
2047
|
if (existingDir !== void 0) {
|
|
1892
|
-
const a =
|
|
1893
|
-
const b =
|
|
2048
|
+
const a = path8.relative(scanPath, existingDir) || ".";
|
|
2049
|
+
const b = path8.relative(scanPath, dir) || ".";
|
|
1894
2050
|
console.warn(
|
|
1895
2051
|
`[neat] duplicate package name "${service.node.name}" \u2014 keeping ${a}, ignoring ${b}`
|
|
1896
2052
|
);
|
|
@@ -1926,8 +2082,8 @@ function addServiceNodes(graph, services) {
|
|
|
1926
2082
|
}
|
|
1927
2083
|
|
|
1928
2084
|
// src/extract/aliases.ts
|
|
1929
|
-
import
|
|
1930
|
-
import { promises as
|
|
2085
|
+
import path9 from "path";
|
|
2086
|
+
import { promises as fs9 } from "fs";
|
|
1931
2087
|
import { parseAllDocuments } from "yaml";
|
|
1932
2088
|
import { NodeType as NodeType5 } from "@neat.is/types";
|
|
1933
2089
|
var K8S_KINDS_WITH_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
@@ -1954,14 +2110,14 @@ function indexServicesByName(services) {
|
|
|
1954
2110
|
const map = /* @__PURE__ */ new Map();
|
|
1955
2111
|
for (const s of services) {
|
|
1956
2112
|
map.set(s.node.name, s.node.id);
|
|
1957
|
-
map.set(
|
|
2113
|
+
map.set(path9.basename(s.dir), s.node.id);
|
|
1958
2114
|
}
|
|
1959
2115
|
return map;
|
|
1960
2116
|
}
|
|
1961
2117
|
async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
1962
2118
|
let composePath = null;
|
|
1963
2119
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
1964
|
-
const abs =
|
|
2120
|
+
const abs = path9.join(scanPath, name);
|
|
1965
2121
|
if (await exists(abs)) {
|
|
1966
2122
|
composePath = abs;
|
|
1967
2123
|
break;
|
|
@@ -1972,8 +2128,10 @@ async function collectComposeAliases(graph, scanPath, serviceIndex) {
|
|
|
1972
2128
|
try {
|
|
1973
2129
|
compose = await readYaml(composePath);
|
|
1974
2130
|
} catch (err) {
|
|
1975
|
-
|
|
1976
|
-
|
|
2131
|
+
recordExtractionError(
|
|
2132
|
+
"aliases compose",
|
|
2133
|
+
path9.relative(scanPath, composePath),
|
|
2134
|
+
err
|
|
1977
2135
|
);
|
|
1978
2136
|
return;
|
|
1979
2137
|
}
|
|
@@ -2015,15 +2173,13 @@ function parseDockerfileLabels(content) {
|
|
|
2015
2173
|
}
|
|
2016
2174
|
async function collectDockerfileAliases(graph, services) {
|
|
2017
2175
|
for (const service of services) {
|
|
2018
|
-
const dockerfilePath =
|
|
2176
|
+
const dockerfilePath = path9.join(service.dir, "Dockerfile");
|
|
2019
2177
|
if (!await exists(dockerfilePath)) continue;
|
|
2020
2178
|
let content;
|
|
2021
2179
|
try {
|
|
2022
|
-
content = await
|
|
2180
|
+
content = await fs9.readFile(dockerfilePath, "utf8");
|
|
2023
2181
|
} catch (err) {
|
|
2024
|
-
|
|
2025
|
-
`[neat] aliases dockerfile skipped ${dockerfilePath}: ${err.message}`
|
|
2026
|
-
);
|
|
2182
|
+
recordExtractionError("aliases dockerfile", dockerfilePath, err);
|
|
2027
2183
|
continue;
|
|
2028
2184
|
}
|
|
2029
2185
|
const aliases = parseDockerfileLabels(content);
|
|
@@ -2033,13 +2189,13 @@ async function collectDockerfileAliases(graph, services) {
|
|
|
2033
2189
|
async function walkYamlFiles(start, depth = 0, max = 5) {
|
|
2034
2190
|
if (depth > max) return [];
|
|
2035
2191
|
const out = [];
|
|
2036
|
-
const entries = await
|
|
2192
|
+
const entries = await fs9.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
2037
2193
|
for (const entry of entries) {
|
|
2038
2194
|
if (entry.isDirectory()) {
|
|
2039
2195
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
2040
|
-
out.push(...await walkYamlFiles(
|
|
2041
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
2042
|
-
out.push(
|
|
2196
|
+
out.push(...await walkYamlFiles(path9.join(start, entry.name), depth + 1, max));
|
|
2197
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path9.extname(entry.name))) {
|
|
2198
|
+
out.push(path9.join(start, entry.name));
|
|
2043
2199
|
}
|
|
2044
2200
|
}
|
|
2045
2201
|
return out;
|
|
@@ -2066,7 +2222,7 @@ function k8sServiceTarget(doc, byName) {
|
|
|
2066
2222
|
async function collectK8sAliases(graph, scanPath, serviceIndex) {
|
|
2067
2223
|
const files = await walkYamlFiles(scanPath);
|
|
2068
2224
|
for (const file of files) {
|
|
2069
|
-
const content = await
|
|
2225
|
+
const content = await fs9.readFile(file, "utf8");
|
|
2070
2226
|
let docs;
|
|
2071
2227
|
try {
|
|
2072
2228
|
docs = parseAllDocuments(content).map((d) => d.toJSON());
|
|
@@ -2090,13 +2246,19 @@ async function addServiceAliases(graph, scanPath, services) {
|
|
|
2090
2246
|
}
|
|
2091
2247
|
|
|
2092
2248
|
// src/extract/databases/index.ts
|
|
2093
|
-
import
|
|
2094
|
-
import {
|
|
2249
|
+
import path17 from "path";
|
|
2250
|
+
import {
|
|
2251
|
+
EdgeType as EdgeType3,
|
|
2252
|
+
NodeType as NodeType6,
|
|
2253
|
+
Provenance as Provenance4,
|
|
2254
|
+
databaseId as databaseId2,
|
|
2255
|
+
confidenceForExtracted
|
|
2256
|
+
} from "@neat.is/types";
|
|
2095
2257
|
|
|
2096
2258
|
// src/extract/databases/db-config-yaml.ts
|
|
2097
|
-
import
|
|
2259
|
+
import path10 from "path";
|
|
2098
2260
|
async function parse(serviceDir) {
|
|
2099
|
-
const yamlPath =
|
|
2261
|
+
const yamlPath = path10.join(serviceDir, "db-config.yaml");
|
|
2100
2262
|
if (!await exists(yamlPath)) return [];
|
|
2101
2263
|
const raw = await readYaml(yamlPath);
|
|
2102
2264
|
return [
|
|
@@ -2113,12 +2275,12 @@ async function parse(serviceDir) {
|
|
|
2113
2275
|
var dbConfigYamlParser = { name: "db-config.yaml", parse };
|
|
2114
2276
|
|
|
2115
2277
|
// src/extract/databases/dotenv.ts
|
|
2116
|
-
import { promises as
|
|
2117
|
-
import
|
|
2278
|
+
import { promises as fs11 } from "fs";
|
|
2279
|
+
import path12 from "path";
|
|
2118
2280
|
|
|
2119
2281
|
// src/extract/databases/shared.ts
|
|
2120
|
-
import { promises as
|
|
2121
|
-
import
|
|
2282
|
+
import { promises as fs10 } from "fs";
|
|
2283
|
+
import path11 from "path";
|
|
2122
2284
|
function schemeToEngine(scheme) {
|
|
2123
2285
|
const s = scheme.toLowerCase().split("+")[0];
|
|
2124
2286
|
switch (s) {
|
|
@@ -2157,14 +2319,14 @@ function parseConnectionString(url) {
|
|
|
2157
2319
|
}
|
|
2158
2320
|
async function readIfExists(filePath) {
|
|
2159
2321
|
try {
|
|
2160
|
-
return await
|
|
2322
|
+
return await fs10.readFile(filePath, "utf8");
|
|
2161
2323
|
} catch {
|
|
2162
2324
|
return null;
|
|
2163
2325
|
}
|
|
2164
2326
|
}
|
|
2165
2327
|
async function findFirst(serviceDir, candidates) {
|
|
2166
2328
|
for (const rel of candidates) {
|
|
2167
|
-
const abs =
|
|
2329
|
+
const abs = path11.join(serviceDir, rel);
|
|
2168
2330
|
const content = await readIfExists(abs);
|
|
2169
2331
|
if (content !== null) return abs;
|
|
2170
2332
|
}
|
|
@@ -2215,15 +2377,15 @@ function parseDotenvLine(line) {
|
|
|
2215
2377
|
return { key, value };
|
|
2216
2378
|
}
|
|
2217
2379
|
async function parse2(serviceDir) {
|
|
2218
|
-
const entries = await
|
|
2380
|
+
const entries = await fs11.readdir(serviceDir, { withFileTypes: true }).catch(() => []);
|
|
2219
2381
|
const configs = [];
|
|
2220
2382
|
const seen = /* @__PURE__ */ new Set();
|
|
2221
2383
|
for (const entry of entries) {
|
|
2222
2384
|
if (!entry.isFile()) continue;
|
|
2223
2385
|
const match = isConfigFile(entry.name);
|
|
2224
2386
|
if (!match.match || match.fileType !== "env") continue;
|
|
2225
|
-
const filePath =
|
|
2226
|
-
const content = await
|
|
2387
|
+
const filePath = path12.join(serviceDir, entry.name);
|
|
2388
|
+
const content = await fs11.readFile(filePath, "utf8");
|
|
2227
2389
|
for (const line of content.split("\n")) {
|
|
2228
2390
|
const parsed = parseDotenvLine(line);
|
|
2229
2391
|
if (!parsed) continue;
|
|
@@ -2241,9 +2403,9 @@ async function parse2(serviceDir) {
|
|
|
2241
2403
|
var dotenvParser = { name: ".env", parse: parse2 };
|
|
2242
2404
|
|
|
2243
2405
|
// src/extract/databases/prisma.ts
|
|
2244
|
-
import
|
|
2406
|
+
import path13 from "path";
|
|
2245
2407
|
async function parse3(serviceDir) {
|
|
2246
|
-
const schemaPath =
|
|
2408
|
+
const schemaPath = path13.join(serviceDir, "prisma", "schema.prisma");
|
|
2247
2409
|
const content = await readIfExists(schemaPath);
|
|
2248
2410
|
if (!content) return [];
|
|
2249
2411
|
const block = content.match(/datasource\s+\w+\s*\{([^}]*)\}/s);
|
|
@@ -2372,10 +2534,10 @@ async function parse5(serviceDir) {
|
|
|
2372
2534
|
var knexParser = { name: "knex", parse: parse5 };
|
|
2373
2535
|
|
|
2374
2536
|
// src/extract/databases/ormconfig.ts
|
|
2375
|
-
import
|
|
2537
|
+
import path14 from "path";
|
|
2376
2538
|
async function parse6(serviceDir) {
|
|
2377
2539
|
for (const candidate of ["ormconfig.json", "ormconfig.yaml", "ormconfig.yml"]) {
|
|
2378
|
-
const abs =
|
|
2540
|
+
const abs = path14.join(serviceDir, candidate);
|
|
2379
2541
|
if (!await exists(abs)) continue;
|
|
2380
2542
|
const raw = candidate.endsWith(".json") ? await readJson(abs) : await readYaml(abs);
|
|
2381
2543
|
const entries = Array.isArray(raw) ? raw : [raw];
|
|
@@ -2433,9 +2595,9 @@ async function parse7(serviceDir) {
|
|
|
2433
2595
|
var typeormParser = { name: "typeorm", parse: parse7 };
|
|
2434
2596
|
|
|
2435
2597
|
// src/extract/databases/sequelize.ts
|
|
2436
|
-
import
|
|
2598
|
+
import path15 from "path";
|
|
2437
2599
|
async function parse8(serviceDir) {
|
|
2438
|
-
const configPath =
|
|
2600
|
+
const configPath = path15.join(serviceDir, "config", "config.json");
|
|
2439
2601
|
if (!await exists(configPath)) return [];
|
|
2440
2602
|
const raw = await readJson(configPath);
|
|
2441
2603
|
const out = [];
|
|
@@ -2461,7 +2623,7 @@ async function parse8(serviceDir) {
|
|
|
2461
2623
|
var sequelizeParser = { name: "sequelize", parse: parse8 };
|
|
2462
2624
|
|
|
2463
2625
|
// src/extract/databases/docker-compose.ts
|
|
2464
|
-
import
|
|
2626
|
+
import path16 from "path";
|
|
2465
2627
|
function portFromService(svc) {
|
|
2466
2628
|
for (const raw of svc.ports ?? []) {
|
|
2467
2629
|
const str = String(raw);
|
|
@@ -2488,7 +2650,7 @@ function databaseFromEnv(svc) {
|
|
|
2488
2650
|
}
|
|
2489
2651
|
async function parse9(serviceDir) {
|
|
2490
2652
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
2491
|
-
const abs =
|
|
2653
|
+
const abs = path16.join(serviceDir, name);
|
|
2492
2654
|
if (!await exists(abs)) continue;
|
|
2493
2655
|
const raw = await readYaml(abs);
|
|
2494
2656
|
if (!raw?.services) return [];
|
|
@@ -2666,11 +2828,13 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2666
2828
|
target: dbNode.id,
|
|
2667
2829
|
type: EdgeType3.CONNECTS_TO,
|
|
2668
2830
|
provenance: Provenance4.EXTRACTED,
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2831
|
+
confidence: confidenceForExtracted("structural"),
|
|
2832
|
+
// ADR-032 / #140 — every EXTRACTED edge carries evidence.file.
|
|
2833
|
+
// Ghost-edge cleanup keys retirement on this; the conditional
|
|
2834
|
+
// sourceFile spread that used to live here was a v0.1.x leftover.
|
|
2835
|
+
evidence: {
|
|
2836
|
+
file: path17.relative(scanPath, config.sourceFile).split(path17.sep).join("/")
|
|
2837
|
+
}
|
|
2674
2838
|
};
|
|
2675
2839
|
if (!graph.hasEdge(edge.id)) {
|
|
2676
2840
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2695,15 +2859,21 @@ async function addDatabasesAndCompat(graph, services, scanPath) {
|
|
|
2695
2859
|
}
|
|
2696
2860
|
|
|
2697
2861
|
// src/extract/configs.ts
|
|
2698
|
-
import { promises as
|
|
2699
|
-
import
|
|
2700
|
-
import {
|
|
2862
|
+
import { promises as fs12 } from "fs";
|
|
2863
|
+
import path18 from "path";
|
|
2864
|
+
import {
|
|
2865
|
+
EdgeType as EdgeType4,
|
|
2866
|
+
NodeType as NodeType7,
|
|
2867
|
+
Provenance as Provenance5,
|
|
2868
|
+
configId,
|
|
2869
|
+
confidenceForExtracted as confidenceForExtracted2
|
|
2870
|
+
} from "@neat.is/types";
|
|
2701
2871
|
async function walkConfigFiles(dir) {
|
|
2702
2872
|
const out = [];
|
|
2703
2873
|
async function walk(current) {
|
|
2704
|
-
const entries = await
|
|
2874
|
+
const entries = await fs12.readdir(current, { withFileTypes: true });
|
|
2705
2875
|
for (const entry of entries) {
|
|
2706
|
-
const full =
|
|
2876
|
+
const full = path18.join(current, entry.name);
|
|
2707
2877
|
if (entry.isDirectory()) {
|
|
2708
2878
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2709
2879
|
} else if (entry.isFile() && isConfigFile(entry.name).match) {
|
|
@@ -2720,13 +2890,13 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2720
2890
|
for (const service of services) {
|
|
2721
2891
|
const configFiles = await walkConfigFiles(service.dir);
|
|
2722
2892
|
for (const file of configFiles) {
|
|
2723
|
-
const relPath =
|
|
2893
|
+
const relPath = path18.relative(scanPath, file);
|
|
2724
2894
|
const node = {
|
|
2725
2895
|
id: configId(relPath),
|
|
2726
2896
|
type: NodeType7.ConfigNode,
|
|
2727
|
-
name:
|
|
2897
|
+
name: path18.basename(file),
|
|
2728
2898
|
path: relPath,
|
|
2729
|
-
fileType: isConfigFile(
|
|
2899
|
+
fileType: isConfigFile(path18.basename(file)).fileType
|
|
2730
2900
|
};
|
|
2731
2901
|
if (!graph.hasNode(node.id)) {
|
|
2732
2902
|
graph.addNode(node.id, node);
|
|
@@ -2738,7 +2908,8 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2738
2908
|
target: node.id,
|
|
2739
2909
|
type: EdgeType4.CONFIGURED_BY,
|
|
2740
2910
|
provenance: Provenance5.EXTRACTED,
|
|
2741
|
-
|
|
2911
|
+
confidence: confidenceForExtracted2("structural"),
|
|
2912
|
+
evidence: { file: relPath.split(path18.sep).join("/") }
|
|
2742
2913
|
};
|
|
2743
2914
|
if (!graph.hasEdge(edge.id)) {
|
|
2744
2915
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2750,27 +2921,38 @@ async function addConfigNodes(graph, services, scanPath) {
|
|
|
2750
2921
|
}
|
|
2751
2922
|
|
|
2752
2923
|
// src/extract/calls/index.ts
|
|
2753
|
-
import {
|
|
2924
|
+
import {
|
|
2925
|
+
EdgeType as EdgeType6,
|
|
2926
|
+
NodeType as NodeType8,
|
|
2927
|
+
Provenance as Provenance7,
|
|
2928
|
+
confidenceForExtracted as confidenceForExtracted4,
|
|
2929
|
+
passesExtractedFloor as passesExtractedFloor2
|
|
2930
|
+
} from "@neat.is/types";
|
|
2754
2931
|
|
|
2755
2932
|
// src/extract/calls/http.ts
|
|
2756
|
-
import
|
|
2933
|
+
import path20 from "path";
|
|
2757
2934
|
import Parser from "tree-sitter";
|
|
2758
2935
|
import JavaScript from "tree-sitter-javascript";
|
|
2759
2936
|
import Python from "tree-sitter-python";
|
|
2760
|
-
import {
|
|
2937
|
+
import {
|
|
2938
|
+
EdgeType as EdgeType5,
|
|
2939
|
+
Provenance as Provenance6,
|
|
2940
|
+
confidenceForExtracted as confidenceForExtracted3,
|
|
2941
|
+
passesExtractedFloor
|
|
2942
|
+
} from "@neat.is/types";
|
|
2761
2943
|
|
|
2762
2944
|
// src/extract/calls/shared.ts
|
|
2763
|
-
import { promises as
|
|
2764
|
-
import
|
|
2945
|
+
import { promises as fs13 } from "fs";
|
|
2946
|
+
import path19 from "path";
|
|
2765
2947
|
async function walkSourceFiles(dir) {
|
|
2766
2948
|
const out = [];
|
|
2767
2949
|
async function walk(current) {
|
|
2768
|
-
const entries = await
|
|
2950
|
+
const entries = await fs13.readdir(current, { withFileTypes: true }).catch(() => []);
|
|
2769
2951
|
for (const entry of entries) {
|
|
2770
|
-
const full =
|
|
2952
|
+
const full = path19.join(current, entry.name);
|
|
2771
2953
|
if (entry.isDirectory()) {
|
|
2772
2954
|
if (!IGNORED_DIRS.has(entry.name)) await walk(full);
|
|
2773
|
-
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(
|
|
2955
|
+
} else if (entry.isFile() && SERVICE_FILE_EXTENSIONS.has(path19.extname(entry.name))) {
|
|
2774
2956
|
out.push(full);
|
|
2775
2957
|
}
|
|
2776
2958
|
}
|
|
@@ -2783,7 +2965,7 @@ async function loadSourceFiles(dir) {
|
|
|
2783
2965
|
const out = [];
|
|
2784
2966
|
for (const p of paths) {
|
|
2785
2967
|
try {
|
|
2786
|
-
const content = await
|
|
2968
|
+
const content = await fs13.readFile(p, "utf8");
|
|
2787
2969
|
out.push({ path: p, content });
|
|
2788
2970
|
} catch {
|
|
2789
2971
|
}
|
|
@@ -2802,8 +2984,27 @@ function snippet(text, line) {
|
|
|
2802
2984
|
|
|
2803
2985
|
// src/extract/calls/http.ts
|
|
2804
2986
|
var STRING_LITERAL_NODE_TYPES = /* @__PURE__ */ new Set(["string_fragment", "string_content"]);
|
|
2987
|
+
var JSX_EXTERNAL_LINK_TAGS = /* @__PURE__ */ new Set(["a", "Link", "NavLink", "ExternalLink", "Anchor"]);
|
|
2988
|
+
function isInsideJsxExternalLink(node) {
|
|
2989
|
+
let cursor = node.parent;
|
|
2990
|
+
while (cursor) {
|
|
2991
|
+
if (cursor.type === "jsx_attribute") {
|
|
2992
|
+
let owner = cursor.parent;
|
|
2993
|
+
while (owner && owner.type !== "jsx_opening_element" && owner.type !== "jsx_self_closing_element") {
|
|
2994
|
+
owner = owner.parent;
|
|
2995
|
+
}
|
|
2996
|
+
if (!owner) return false;
|
|
2997
|
+
const tagNode = owner.namedChild(0);
|
|
2998
|
+
const tagName = tagNode?.text ?? "";
|
|
2999
|
+
const right = tagName.includes(".") ? tagName.split(".").pop() : tagName;
|
|
3000
|
+
return JSX_EXTERNAL_LINK_TAGS.has(right);
|
|
3001
|
+
}
|
|
3002
|
+
cursor = cursor.parent;
|
|
3003
|
+
}
|
|
3004
|
+
return false;
|
|
3005
|
+
}
|
|
2805
3006
|
function collectStringLiterals(node, out) {
|
|
2806
|
-
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push(node.text);
|
|
3007
|
+
if (STRING_LITERAL_NODE_TYPES.has(node.type)) out.push({ text: node.text, node });
|
|
2807
3008
|
for (let i = 0; i < node.namedChildCount; i++) {
|
|
2808
3009
|
const child = node.namedChild(i);
|
|
2809
3010
|
if (child) collectStringLiterals(child, out);
|
|
@@ -2815,8 +3016,9 @@ function callsFromSource(source, parser, knownHosts) {
|
|
|
2815
3016
|
collectStringLiterals(tree.rootNode, literals);
|
|
2816
3017
|
const targets = /* @__PURE__ */ new Set();
|
|
2817
3018
|
for (const lit of literals) {
|
|
3019
|
+
if (isInsideJsxExternalLink(lit.node)) continue;
|
|
2818
3020
|
for (const host of knownHosts) {
|
|
2819
|
-
if (lit.
|
|
3021
|
+
if (urlMatchesHost(lit.text, host)) {
|
|
2820
3022
|
targets.add(host);
|
|
2821
3023
|
}
|
|
2822
3024
|
}
|
|
@@ -2839,9 +3041,9 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2839
3041
|
const knownHosts = /* @__PURE__ */ new Set();
|
|
2840
3042
|
const hostToNodeId = /* @__PURE__ */ new Map();
|
|
2841
3043
|
for (const service of services) {
|
|
2842
|
-
knownHosts.add(
|
|
3044
|
+
knownHosts.add(path20.basename(service.dir));
|
|
2843
3045
|
knownHosts.add(service.pkg.name);
|
|
2844
|
-
hostToNodeId.set(
|
|
3046
|
+
hostToNodeId.set(path20.basename(service.dir), service.node.id);
|
|
2845
3047
|
hostToNodeId.set(service.pkg.name, service.node.id);
|
|
2846
3048
|
}
|
|
2847
3049
|
let edgesAdded = 0;
|
|
@@ -2849,14 +3051,13 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2849
3051
|
const files = await loadSourceFiles(service.dir);
|
|
2850
3052
|
const seenTargets = /* @__PURE__ */ new Map();
|
|
2851
3053
|
for (const file of files) {
|
|
2852
|
-
|
|
3054
|
+
if (isTestPath(file.path)) continue;
|
|
3055
|
+
const parser = path20.extname(file.path) === ".py" ? pyParser : jsParser;
|
|
2853
3056
|
let targets;
|
|
2854
3057
|
try {
|
|
2855
3058
|
targets = callsFromSource(file.content, parser, knownHosts);
|
|
2856
3059
|
} catch (err) {
|
|
2857
|
-
|
|
2858
|
-
`[neat] http call extraction skipped ${file.path}: ${err.message}`
|
|
2859
|
-
);
|
|
3060
|
+
recordExtractionError("http call extraction", file.path, err);
|
|
2860
3061
|
continue;
|
|
2861
3062
|
}
|
|
2862
3063
|
for (const t of targets) {
|
|
@@ -2870,17 +3071,32 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2870
3071
|
for (const [targetId, evidenceFile] of seenTargets) {
|
|
2871
3072
|
const fileContent = files.find((f) => f.path === evidenceFile.file)?.content ?? "";
|
|
2872
3073
|
const line = lineOf(fileContent, `//${evidenceFile.host}`);
|
|
3074
|
+
const confidence = confidenceForExtracted3("hostname-shape-match");
|
|
3075
|
+
const ev = {
|
|
3076
|
+
file: path20.relative(service.dir, evidenceFile.file),
|
|
3077
|
+
line,
|
|
3078
|
+
snippet: snippet(fileContent, line)
|
|
3079
|
+
};
|
|
3080
|
+
const edgeId = extractedEdgeId2(service.node.id, targetId, EdgeType5.CALLS);
|
|
3081
|
+
if (!passesExtractedFloor(confidence)) {
|
|
3082
|
+
noteExtractedDropped({
|
|
3083
|
+
source: service.node.id,
|
|
3084
|
+
target: targetId,
|
|
3085
|
+
type: EdgeType5.CALLS,
|
|
3086
|
+
confidence,
|
|
3087
|
+
confidenceKind: "hostname-shape-match",
|
|
3088
|
+
evidence: ev
|
|
3089
|
+
});
|
|
3090
|
+
continue;
|
|
3091
|
+
}
|
|
2873
3092
|
const edge = {
|
|
2874
|
-
id:
|
|
3093
|
+
id: edgeId,
|
|
2875
3094
|
source: service.node.id,
|
|
2876
3095
|
target: targetId,
|
|
2877
3096
|
type: EdgeType5.CALLS,
|
|
2878
3097
|
provenance: Provenance6.EXTRACTED,
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
line,
|
|
2882
|
-
snippet: snippet(fileContent, line)
|
|
2883
|
-
}
|
|
3098
|
+
confidence,
|
|
3099
|
+
evidence: ev
|
|
2884
3100
|
};
|
|
2885
3101
|
if (!graph.hasEdge(edge.id)) {
|
|
2886
3102
|
graph.addEdgeWithKey(edge.id, edge.source, edge.target, edge);
|
|
@@ -2892,7 +3108,7 @@ async function addHttpCallEdges(graph, services) {
|
|
|
2892
3108
|
}
|
|
2893
3109
|
|
|
2894
3110
|
// src/extract/calls/kafka.ts
|
|
2895
|
-
import
|
|
3111
|
+
import path21 from "path";
|
|
2896
3112
|
import { infraId } from "@neat.is/types";
|
|
2897
3113
|
var PRODUCER_TOPIC_RE = /(?:producer|kafkaProducer)[\s\S]{0,40}?\.send\s*\(\s*\{[\s\S]{0,200}?topic\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
2898
3114
|
var CONSUMER_TOPIC_RE = /(?:consumer|kafkaConsumer)[\s\S]{0,40}?\.(?:subscribe|run)\s*\(\s*\{[\s\S]{0,200}?topic[s]?\s*:\s*(?:\[\s*)?['"`]([^'"`]+)['"`]/g;
|
|
@@ -2918,8 +3134,12 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
2918
3134
|
name: topic,
|
|
2919
3135
|
kind: "kafka-topic",
|
|
2920
3136
|
edgeType,
|
|
3137
|
+
// `producer.send({topic: 'x'})` / `consumer.subscribe({topic: 'x'})` —
|
|
3138
|
+
// framework-aware (kafkajs / node-rdkafka shape). Verified-call-site
|
|
3139
|
+
// tier (ADR-066).
|
|
3140
|
+
confidenceKind: "verified-call-site",
|
|
2921
3141
|
evidence: {
|
|
2922
|
-
file:
|
|
3142
|
+
file: path21.relative(serviceDir, file.path),
|
|
2923
3143
|
line,
|
|
2924
3144
|
snippet: snippet(file.content, line)
|
|
2925
3145
|
}
|
|
@@ -2931,7 +3151,7 @@ function kafkaEndpointsFromFile(file, serviceDir) {
|
|
|
2931
3151
|
}
|
|
2932
3152
|
|
|
2933
3153
|
// src/extract/calls/redis.ts
|
|
2934
|
-
import
|
|
3154
|
+
import path22 from "path";
|
|
2935
3155
|
import { infraId as infraId2 } from "@neat.is/types";
|
|
2936
3156
|
var REDIS_URL_RE = /redis(?:s)?:\/\/(?:[^@'"`\s]+@)?([^:/'"`\s]+)(?::(\d+))?/g;
|
|
2937
3157
|
function redisEndpointsFromFile(file, serviceDir) {
|
|
@@ -2949,8 +3169,12 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
2949
3169
|
name: host,
|
|
2950
3170
|
kind: "redis",
|
|
2951
3171
|
edgeType: "CALLS",
|
|
3172
|
+
// `redis://host` URL literal — the scheme is structural support, but no
|
|
3173
|
+
// call expression is verified to wire it through. URL-with-structural-
|
|
3174
|
+
// support tier (ADR-066).
|
|
3175
|
+
confidenceKind: "url-with-structural-support",
|
|
2952
3176
|
evidence: {
|
|
2953
|
-
file:
|
|
3177
|
+
file: path22.relative(serviceDir, file.path),
|
|
2954
3178
|
line,
|
|
2955
3179
|
snippet: snippet(file.content, line)
|
|
2956
3180
|
}
|
|
@@ -2960,7 +3184,7 @@ function redisEndpointsFromFile(file, serviceDir) {
|
|
|
2960
3184
|
}
|
|
2961
3185
|
|
|
2962
3186
|
// src/extract/calls/aws.ts
|
|
2963
|
-
import
|
|
3187
|
+
import path23 from "path";
|
|
2964
3188
|
import { infraId as infraId3 } from "@neat.is/types";
|
|
2965
3189
|
var S3_BUCKET_RE = /Bucket\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
2966
3190
|
var DYNAMO_TABLE_RE = /TableName\s*:\s*['"`]([^'"`]+)['"`]/g;
|
|
@@ -2989,8 +3213,12 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
2989
3213
|
name,
|
|
2990
3214
|
kind,
|
|
2991
3215
|
edgeType: "CALLS",
|
|
3216
|
+
// SDK marker (S3Client, GetCommand, etc.) plus a Bucket/TableName
|
|
3217
|
+
// literal — framework-aware recognizer, verified-call-site tier
|
|
3218
|
+
// (ADR-066).
|
|
3219
|
+
confidenceKind: "verified-call-site",
|
|
2992
3220
|
evidence: {
|
|
2993
|
-
file:
|
|
3221
|
+
file: path23.relative(serviceDir, file.path),
|
|
2994
3222
|
line,
|
|
2995
3223
|
snippet: snippet(file.content, line)
|
|
2996
3224
|
}
|
|
@@ -3014,16 +3242,42 @@ function awsEndpointsFromFile(file, serviceDir) {
|
|
|
3014
3242
|
}
|
|
3015
3243
|
|
|
3016
3244
|
// src/extract/calls/grpc.ts
|
|
3017
|
-
import
|
|
3245
|
+
import path24 from "path";
|
|
3018
3246
|
import { infraId as infraId4 } from "@neat.is/types";
|
|
3019
3247
|
var GRPC_CLIENT_RE = /new\s+([A-Z][A-Za-z0-9_]*)Client\s*\(\s*['"`]?([^,'"`)]+)?/g;
|
|
3248
|
+
var AWS_SDK_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@aws-sdk\/client-([a-z0-9-]+)['"`]/g;
|
|
3249
|
+
var GRPC_IMPORT_RE = /(?:from\s+['"`]|require\(\s*['"`])@grpc\/grpc-js['"`]|_grpc_pb['"`]/;
|
|
3020
3250
|
function isLikelyAddress(value) {
|
|
3021
3251
|
if (!value) return false;
|
|
3022
3252
|
return /:\d{2,5}$/.test(value) || value.includes(".");
|
|
3023
3253
|
}
|
|
3254
|
+
function normaliseForMatch(s) {
|
|
3255
|
+
return s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3256
|
+
}
|
|
3257
|
+
function readImports(content) {
|
|
3258
|
+
const awsSdkSuffixes = /* @__PURE__ */ new Map();
|
|
3259
|
+
AWS_SDK_IMPORT_RE.lastIndex = 0;
|
|
3260
|
+
let m;
|
|
3261
|
+
while ((m = AWS_SDK_IMPORT_RE.exec(content)) !== null) {
|
|
3262
|
+
const raw = m[1];
|
|
3263
|
+
awsSdkSuffixes.set(normaliseForMatch(raw), raw);
|
|
3264
|
+
}
|
|
3265
|
+
return {
|
|
3266
|
+
awsSdkSuffixes,
|
|
3267
|
+
hasGrpcImport: GRPC_IMPORT_RE.test(content)
|
|
3268
|
+
};
|
|
3269
|
+
}
|
|
3270
|
+
function classifyClient(symbol, ctx) {
|
|
3271
|
+
const key = normaliseForMatch(symbol);
|
|
3272
|
+
const awsRaw = ctx.awsSdkSuffixes.get(key);
|
|
3273
|
+
if (awsRaw) return { kind: `aws-${awsRaw}` };
|
|
3274
|
+
if (ctx.hasGrpcImport) return { kind: "grpc-service" };
|
|
3275
|
+
return null;
|
|
3276
|
+
}
|
|
3024
3277
|
function grpcEndpointsFromFile(file, serviceDir) {
|
|
3025
3278
|
const out = [];
|
|
3026
3279
|
const seen = /* @__PURE__ */ new Set();
|
|
3280
|
+
const ctx = readImports(file.content);
|
|
3027
3281
|
GRPC_CLIENT_RE.lastIndex = 0;
|
|
3028
3282
|
let m;
|
|
3029
3283
|
while ((m = GRPC_CLIENT_RE.exec(file.content)) !== null) {
|
|
@@ -3031,15 +3285,22 @@ function grpcEndpointsFromFile(file, serviceDir) {
|
|
|
3031
3285
|
const addr = m[2]?.trim();
|
|
3032
3286
|
const name = isLikelyAddress(addr) ? addr : symbol;
|
|
3033
3287
|
if (seen.has(name)) continue;
|
|
3288
|
+
const classified = classifyClient(symbol, ctx);
|
|
3289
|
+
if (!classified) continue;
|
|
3034
3290
|
seen.add(name);
|
|
3291
|
+
const { kind } = classified;
|
|
3035
3292
|
const line = lineOf(file.content, m[0]);
|
|
3036
3293
|
out.push({
|
|
3037
|
-
infraId: infraId4(
|
|
3294
|
+
infraId: infraId4(kind, name),
|
|
3038
3295
|
name,
|
|
3039
|
-
kind
|
|
3296
|
+
kind,
|
|
3040
3297
|
edgeType: "CALLS",
|
|
3298
|
+
// `new <Name>Client(...)` with @aws-sdk/* or @grpc/grpc-js import
|
|
3299
|
+
// context — import-aware classification per #238. Verified-call-site
|
|
3300
|
+
// tier (ADR-066).
|
|
3301
|
+
confidenceKind: "verified-call-site",
|
|
3041
3302
|
evidence: {
|
|
3042
|
-
file:
|
|
3303
|
+
file: path24.relative(serviceDir, file.path),
|
|
3043
3304
|
line,
|
|
3044
3305
|
snippet: snippet(file.content, line)
|
|
3045
3306
|
}
|
|
@@ -3059,6 +3320,9 @@ function edgeTypeFromEndpoint(ep) {
|
|
|
3059
3320
|
return EdgeType6.CALLS;
|
|
3060
3321
|
}
|
|
3061
3322
|
}
|
|
3323
|
+
function isAwsKind(kind) {
|
|
3324
|
+
return kind.startsWith("aws-") || kind.startsWith("s3") || kind.startsWith("dynamodb");
|
|
3325
|
+
}
|
|
3062
3326
|
async function addExternalEndpointEdges(graph, services) {
|
|
3063
3327
|
let nodesAdded = 0;
|
|
3064
3328
|
let edgesAdded = 0;
|
|
@@ -3066,10 +3330,13 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3066
3330
|
const files = await loadSourceFiles(service.dir);
|
|
3067
3331
|
const endpoints = [];
|
|
3068
3332
|
for (const file of files) {
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
endpoints.push(...
|
|
3333
|
+
if (isTestPath(file.path)) continue;
|
|
3334
|
+
const masked = maskCommentsInSource(file.content);
|
|
3335
|
+
const maskedFile = { path: file.path, content: masked };
|
|
3336
|
+
endpoints.push(...kafkaEndpointsFromFile(maskedFile, service.dir));
|
|
3337
|
+
endpoints.push(...redisEndpointsFromFile(maskedFile, service.dir));
|
|
3338
|
+
endpoints.push(...awsEndpointsFromFile(maskedFile, service.dir));
|
|
3339
|
+
endpoints.push(...grpcEndpointsFromFile(maskedFile, service.dir));
|
|
3073
3340
|
}
|
|
3074
3341
|
if (endpoints.length === 0) continue;
|
|
3075
3342
|
const seenEdges = /* @__PURE__ */ new Set();
|
|
@@ -3079,7 +3346,10 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3079
3346
|
id: ep.infraId,
|
|
3080
3347
|
type: NodeType8.InfraNode,
|
|
3081
3348
|
name: ep.name,
|
|
3082
|
-
|
|
3349
|
+
// #238 — `aws-*` covers AWS-SDK client kinds (aws-s3, aws-dynamodb,
|
|
3350
|
+
// aws-cognito-identity-provider, …); `s3-` / `dynamodb-` cover the
|
|
3351
|
+
// bucket / table kinds from aws.ts.
|
|
3352
|
+
provider: isAwsKind(ep.kind) ? "aws" : "self",
|
|
3083
3353
|
kind: ep.kind
|
|
3084
3354
|
};
|
|
3085
3355
|
graph.addNode(node.id, node);
|
|
@@ -3089,6 +3359,18 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3089
3359
|
const edgeId = extractedEdgeId2(service.node.id, ep.infraId, edgeType);
|
|
3090
3360
|
if (seenEdges.has(edgeId)) continue;
|
|
3091
3361
|
seenEdges.add(edgeId);
|
|
3362
|
+
const confidence = confidenceForExtracted4(ep.confidenceKind);
|
|
3363
|
+
if (!passesExtractedFloor2(confidence)) {
|
|
3364
|
+
noteExtractedDropped({
|
|
3365
|
+
source: service.node.id,
|
|
3366
|
+
target: ep.infraId,
|
|
3367
|
+
type: edgeType,
|
|
3368
|
+
confidence,
|
|
3369
|
+
confidenceKind: ep.confidenceKind,
|
|
3370
|
+
evidence: ep.evidence
|
|
3371
|
+
});
|
|
3372
|
+
continue;
|
|
3373
|
+
}
|
|
3092
3374
|
if (!graph.hasEdge(edgeId)) {
|
|
3093
3375
|
const edge = {
|
|
3094
3376
|
id: edgeId,
|
|
@@ -3096,6 +3378,7 @@ async function addExternalEndpointEdges(graph, services) {
|
|
|
3096
3378
|
target: ep.infraId,
|
|
3097
3379
|
type: edgeType,
|
|
3098
3380
|
provenance: Provenance7.EXTRACTED,
|
|
3381
|
+
confidence,
|
|
3099
3382
|
evidence: ep.evidence
|
|
3100
3383
|
};
|
|
3101
3384
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3115,8 +3398,8 @@ async function addCallEdges(graph, services) {
|
|
|
3115
3398
|
}
|
|
3116
3399
|
|
|
3117
3400
|
// src/extract/infra/docker-compose.ts
|
|
3118
|
-
import
|
|
3119
|
-
import { EdgeType as EdgeType7, Provenance as Provenance8 } from "@neat.is/types";
|
|
3401
|
+
import path25 from "path";
|
|
3402
|
+
import { EdgeType as EdgeType7, Provenance as Provenance8, confidenceForExtracted as confidenceForExtracted5 } from "@neat.is/types";
|
|
3120
3403
|
|
|
3121
3404
|
// src/extract/infra/shared.ts
|
|
3122
3405
|
import { NodeType as NodeType9, infraId as infraId5 } from "@neat.is/types";
|
|
@@ -3152,7 +3435,7 @@ function dependsOnList(value) {
|
|
|
3152
3435
|
}
|
|
3153
3436
|
function serviceNameToServiceNode(name, services) {
|
|
3154
3437
|
for (const s of services) {
|
|
3155
|
-
if (s.node.name === name ||
|
|
3438
|
+
if (s.node.name === name || path25.basename(s.dir) === name) return s.node.id;
|
|
3156
3439
|
}
|
|
3157
3440
|
return null;
|
|
3158
3441
|
}
|
|
@@ -3161,7 +3444,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3161
3444
|
let edgesAdded = 0;
|
|
3162
3445
|
let composePath = null;
|
|
3163
3446
|
for (const name of ["docker-compose.yml", "docker-compose.yaml"]) {
|
|
3164
|
-
const abs =
|
|
3447
|
+
const abs = path25.join(scanPath, name);
|
|
3165
3448
|
if (await exists(abs)) {
|
|
3166
3449
|
composePath = abs;
|
|
3167
3450
|
break;
|
|
@@ -3172,13 +3455,15 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3172
3455
|
try {
|
|
3173
3456
|
compose = await readYaml(composePath);
|
|
3174
3457
|
} catch (err) {
|
|
3175
|
-
|
|
3176
|
-
|
|
3458
|
+
recordExtractionError(
|
|
3459
|
+
"infra docker-compose",
|
|
3460
|
+
path25.relative(scanPath, composePath),
|
|
3461
|
+
err
|
|
3177
3462
|
);
|
|
3178
3463
|
return { nodesAdded, edgesAdded };
|
|
3179
3464
|
}
|
|
3180
3465
|
if (!compose?.services) return { nodesAdded, edgesAdded };
|
|
3181
|
-
const evidenceFile =
|
|
3466
|
+
const evidenceFile = path25.relative(scanPath, composePath).split(path25.sep).join("/");
|
|
3182
3467
|
const composeNameToNodeId = /* @__PURE__ */ new Map();
|
|
3183
3468
|
for (const [composeName, svc] of Object.entries(compose.services)) {
|
|
3184
3469
|
const matchedServiceId = serviceNameToServiceNode(composeName, services);
|
|
@@ -3208,6 +3493,7 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3208
3493
|
target: targetId,
|
|
3209
3494
|
type: EdgeType7.DEPENDS_ON,
|
|
3210
3495
|
provenance: Provenance8.EXTRACTED,
|
|
3496
|
+
confidence: confidenceForExtracted5("structural"),
|
|
3211
3497
|
evidence: { file: evidenceFile }
|
|
3212
3498
|
};
|
|
3213
3499
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3218,9 +3504,9 @@ async function addComposeInfra(graph, scanPath, services) {
|
|
|
3218
3504
|
}
|
|
3219
3505
|
|
|
3220
3506
|
// src/extract/infra/dockerfile.ts
|
|
3221
|
-
import
|
|
3222
|
-
import { promises as
|
|
3223
|
-
import { EdgeType as EdgeType8, Provenance as Provenance9 } from "@neat.is/types";
|
|
3507
|
+
import path26 from "path";
|
|
3508
|
+
import { promises as fs14 } from "fs";
|
|
3509
|
+
import { EdgeType as EdgeType8, Provenance as Provenance9, confidenceForExtracted as confidenceForExtracted6 } from "@neat.is/types";
|
|
3224
3510
|
function runtimeImage(content) {
|
|
3225
3511
|
const lines = content.split("\n");
|
|
3226
3512
|
let last = null;
|
|
@@ -3239,14 +3525,16 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3239
3525
|
let nodesAdded = 0;
|
|
3240
3526
|
let edgesAdded = 0;
|
|
3241
3527
|
for (const service of services) {
|
|
3242
|
-
const dockerfilePath =
|
|
3528
|
+
const dockerfilePath = path26.join(service.dir, "Dockerfile");
|
|
3243
3529
|
if (!await exists(dockerfilePath)) continue;
|
|
3244
3530
|
let content;
|
|
3245
3531
|
try {
|
|
3246
|
-
content = await
|
|
3532
|
+
content = await fs14.readFile(dockerfilePath, "utf8");
|
|
3247
3533
|
} catch (err) {
|
|
3248
|
-
|
|
3249
|
-
|
|
3534
|
+
recordExtractionError(
|
|
3535
|
+
"infra dockerfile",
|
|
3536
|
+
path26.relative(scanPath, dockerfilePath),
|
|
3537
|
+
err
|
|
3250
3538
|
);
|
|
3251
3539
|
continue;
|
|
3252
3540
|
}
|
|
@@ -3265,8 +3553,9 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3265
3553
|
target: node.id,
|
|
3266
3554
|
type: EdgeType8.RUNS_ON,
|
|
3267
3555
|
provenance: Provenance9.EXTRACTED,
|
|
3556
|
+
confidence: confidenceForExtracted6("structural"),
|
|
3268
3557
|
evidence: {
|
|
3269
|
-
file:
|
|
3558
|
+
file: path26.relative(scanPath, dockerfilePath).split(path26.sep).join("/")
|
|
3270
3559
|
}
|
|
3271
3560
|
};
|
|
3272
3561
|
graph.addEdgeWithKey(edgeId, edge.source, edge.target, edge);
|
|
@@ -3277,19 +3566,19 @@ async function addDockerfileRuntimes(graph, services, scanPath) {
|
|
|
3277
3566
|
}
|
|
3278
3567
|
|
|
3279
3568
|
// src/extract/infra/terraform.ts
|
|
3280
|
-
import { promises as
|
|
3281
|
-
import
|
|
3569
|
+
import { promises as fs15 } from "fs";
|
|
3570
|
+
import path27 from "path";
|
|
3282
3571
|
var RESOURCE_RE = /resource\s+"(aws_[A-Za-z0-9_]+)"\s+"([A-Za-z0-9_-]+)"/g;
|
|
3283
3572
|
async function walkTfFiles(start, depth = 0, max = 5) {
|
|
3284
3573
|
if (depth > max) return [];
|
|
3285
3574
|
const out = [];
|
|
3286
|
-
const entries = await
|
|
3575
|
+
const entries = await fs15.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3287
3576
|
for (const entry of entries) {
|
|
3288
3577
|
if (entry.isDirectory()) {
|
|
3289
3578
|
if (IGNORED_DIRS.has(entry.name) || entry.name === ".terraform") continue;
|
|
3290
|
-
out.push(...await walkTfFiles(
|
|
3579
|
+
out.push(...await walkTfFiles(path27.join(start, entry.name), depth + 1, max));
|
|
3291
3580
|
} else if (entry.isFile() && entry.name.endsWith(".tf")) {
|
|
3292
|
-
out.push(
|
|
3581
|
+
out.push(path27.join(start, entry.name));
|
|
3293
3582
|
}
|
|
3294
3583
|
}
|
|
3295
3584
|
return out;
|
|
@@ -3298,7 +3587,7 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3298
3587
|
let nodesAdded = 0;
|
|
3299
3588
|
const files = await walkTfFiles(scanPath);
|
|
3300
3589
|
for (const file of files) {
|
|
3301
|
-
const content = await
|
|
3590
|
+
const content = await fs15.readFile(file, "utf8");
|
|
3302
3591
|
RESOURCE_RE.lastIndex = 0;
|
|
3303
3592
|
let m;
|
|
3304
3593
|
while ((m = RESOURCE_RE.exec(content)) !== null) {
|
|
@@ -3315,8 +3604,8 @@ async function addTerraformResources(graph, scanPath) {
|
|
|
3315
3604
|
}
|
|
3316
3605
|
|
|
3317
3606
|
// src/extract/infra/k8s.ts
|
|
3318
|
-
import { promises as
|
|
3319
|
-
import
|
|
3607
|
+
import { promises as fs16 } from "fs";
|
|
3608
|
+
import path28 from "path";
|
|
3320
3609
|
import { parseAllDocuments as parseAllDocuments2 } from "yaml";
|
|
3321
3610
|
var K8S_KIND_TO_INFRA_KIND = {
|
|
3322
3611
|
Service: "k8s-service",
|
|
@@ -3330,13 +3619,13 @@ var K8S_KIND_TO_INFRA_KIND = {
|
|
|
3330
3619
|
async function walkYamlFiles2(start, depth = 0, max = 5) {
|
|
3331
3620
|
if (depth > max) return [];
|
|
3332
3621
|
const out = [];
|
|
3333
|
-
const entries = await
|
|
3622
|
+
const entries = await fs16.readdir(start, { withFileTypes: true }).catch(() => []);
|
|
3334
3623
|
for (const entry of entries) {
|
|
3335
3624
|
if (entry.isDirectory()) {
|
|
3336
3625
|
if (IGNORED_DIRS.has(entry.name)) continue;
|
|
3337
|
-
out.push(...await walkYamlFiles2(
|
|
3338
|
-
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(
|
|
3339
|
-
out.push(
|
|
3626
|
+
out.push(...await walkYamlFiles2(path28.join(start, entry.name), depth + 1, max));
|
|
3627
|
+
} else if (entry.isFile() && CONFIG_FILE_EXTENSIONS.has(path28.extname(entry.name))) {
|
|
3628
|
+
out.push(path28.join(start, entry.name));
|
|
3340
3629
|
}
|
|
3341
3630
|
}
|
|
3342
3631
|
return out;
|
|
@@ -3345,7 +3634,7 @@ async function addK8sResources(graph, scanPath) {
|
|
|
3345
3634
|
let nodesAdded = 0;
|
|
3346
3635
|
const files = await walkYamlFiles2(scanPath);
|
|
3347
3636
|
for (const file of files) {
|
|
3348
|
-
const content = await
|
|
3637
|
+
const content = await fs16.readFile(file, "utf8");
|
|
3349
3638
|
let docs;
|
|
3350
3639
|
try {
|
|
3351
3640
|
docs = parseAllDocuments2(content).map((d) => d.toJSON());
|
|
@@ -3379,9 +3668,48 @@ async function addInfra(graph, scanPath, services) {
|
|
|
3379
3668
|
};
|
|
3380
3669
|
}
|
|
3381
3670
|
|
|
3671
|
+
// src/extract/index.ts
|
|
3672
|
+
import path30 from "path";
|
|
3673
|
+
|
|
3674
|
+
// src/extract/retire.ts
|
|
3675
|
+
import { existsSync } from "fs";
|
|
3676
|
+
import path29 from "path";
|
|
3677
|
+
import { Provenance as Provenance10 } from "@neat.is/types";
|
|
3678
|
+
function retireEdgesByFile(graph, file) {
|
|
3679
|
+
const normalized = file.split("\\").join("/");
|
|
3680
|
+
const toDrop = [];
|
|
3681
|
+
graph.forEachEdge((id, attrs) => {
|
|
3682
|
+
const edge = attrs;
|
|
3683
|
+
if (edge.provenance !== Provenance10.EXTRACTED) return;
|
|
3684
|
+
if (!edge.evidence?.file) return;
|
|
3685
|
+
if (edge.evidence.file === normalized) toDrop.push(id);
|
|
3686
|
+
});
|
|
3687
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
3688
|
+
return toDrop.length;
|
|
3689
|
+
}
|
|
3690
|
+
function retireExtractedEdgesByMissingFile(graph, scanPath, serviceDirs = []) {
|
|
3691
|
+
const toDrop = [];
|
|
3692
|
+
const bases = [scanPath, ...serviceDirs];
|
|
3693
|
+
graph.forEachEdge((id, attrs) => {
|
|
3694
|
+
const edge = attrs;
|
|
3695
|
+
if (edge.provenance !== Provenance10.EXTRACTED) return;
|
|
3696
|
+
const evidenceFile = edge.evidence?.file;
|
|
3697
|
+
if (!evidenceFile) return;
|
|
3698
|
+
if (path29.isAbsolute(evidenceFile)) {
|
|
3699
|
+
if (!existsSync(evidenceFile)) toDrop.push(id);
|
|
3700
|
+
return;
|
|
3701
|
+
}
|
|
3702
|
+
const found = bases.some((base) => existsSync(path29.join(base, evidenceFile)));
|
|
3703
|
+
if (!found) toDrop.push(id);
|
|
3704
|
+
});
|
|
3705
|
+
for (const id of toDrop) graph.dropEdge(id);
|
|
3706
|
+
return toDrop.length;
|
|
3707
|
+
}
|
|
3708
|
+
|
|
3382
3709
|
// src/extract/index.ts
|
|
3383
3710
|
async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
3384
3711
|
await ensureCompatLoaded();
|
|
3712
|
+
drainExtractionErrors();
|
|
3385
3713
|
const services = await discoverServices(scanPath);
|
|
3386
3714
|
const phase1Nodes = addServiceNodes(graph, services);
|
|
3387
3715
|
await addServiceAliases(graph, scanPath, services);
|
|
@@ -3389,12 +3717,43 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3389
3717
|
const phase3 = await addConfigNodes(graph, services, scanPath);
|
|
3390
3718
|
const phase4 = await addCallEdges(graph, services);
|
|
3391
3719
|
const phase5 = await addInfra(graph, scanPath, services);
|
|
3720
|
+
const ghostsRetired = retireExtractedEdgesByMissingFile(
|
|
3721
|
+
graph,
|
|
3722
|
+
scanPath,
|
|
3723
|
+
services.map((s) => s.dir)
|
|
3724
|
+
);
|
|
3392
3725
|
const frontiersPromoted = promoteFrontierNodes(graph);
|
|
3393
3726
|
if (opts.onPolicyTrigger) await opts.onPolicyTrigger(graph);
|
|
3727
|
+
const errorEntries = drainExtractionErrors();
|
|
3728
|
+
if (opts.errorsPath && errorEntries.length > 0) {
|
|
3729
|
+
try {
|
|
3730
|
+
await writeExtractionErrors(errorEntries, opts.errorsPath);
|
|
3731
|
+
} catch (err) {
|
|
3732
|
+
console.warn(
|
|
3733
|
+
`[neat] failed to write extraction errors to ${opts.errorsPath}: ${err.message}`
|
|
3734
|
+
);
|
|
3735
|
+
}
|
|
3736
|
+
}
|
|
3737
|
+
const droppedEntries = drainDroppedExtracted();
|
|
3738
|
+
if (isRejectedLogEnabled() && opts.errorsPath && droppedEntries.length > 0) {
|
|
3739
|
+
const rejectedPath = path30.join(path30.dirname(opts.errorsPath), "rejected.ndjson");
|
|
3740
|
+
try {
|
|
3741
|
+
await writeRejectedExtracted(droppedEntries, rejectedPath);
|
|
3742
|
+
} catch (err) {
|
|
3743
|
+
console.warn(
|
|
3744
|
+
`[neat] failed to write rejected extracted edges to ${rejectedPath}: ${err.message}`
|
|
3745
|
+
);
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3394
3748
|
const result = {
|
|
3395
3749
|
nodesAdded: phase1Nodes + phase2.nodesAdded + phase3.nodesAdded + phase4.nodesAdded + phase5.nodesAdded,
|
|
3396
3750
|
edgesAdded: phase2.edgesAdded + phase3.edgesAdded + phase4.edgesAdded + phase5.edgesAdded,
|
|
3397
|
-
frontiersPromoted
|
|
3751
|
+
frontiersPromoted,
|
|
3752
|
+
extractionErrors: errorEntries.length,
|
|
3753
|
+
errorEntries,
|
|
3754
|
+
ghostsRetired,
|
|
3755
|
+
extractedDropped: droppedEntries.length,
|
|
3756
|
+
droppedEntries
|
|
3398
3757
|
};
|
|
3399
3758
|
emitNeatEvent({
|
|
3400
3759
|
type: "extraction-complete",
|
|
@@ -3410,8 +3769,8 @@ async function extractFromDirectory(graph, scanPath, opts = {}) {
|
|
|
3410
3769
|
}
|
|
3411
3770
|
|
|
3412
3771
|
// src/persist.ts
|
|
3413
|
-
import { promises as
|
|
3414
|
-
import
|
|
3772
|
+
import { promises as fs17 } from "fs";
|
|
3773
|
+
import path31 from "path";
|
|
3415
3774
|
var SCHEMA_VERSION = 2;
|
|
3416
3775
|
function migrateV1ToV2(payload) {
|
|
3417
3776
|
const nodes = payload.graph.nodes;
|
|
@@ -3425,7 +3784,7 @@ function migrateV1ToV2(payload) {
|
|
|
3425
3784
|
return { ...payload, schemaVersion: 2 };
|
|
3426
3785
|
}
|
|
3427
3786
|
async function ensureDir(filePath) {
|
|
3428
|
-
await
|
|
3787
|
+
await fs17.mkdir(path31.dirname(filePath), { recursive: true });
|
|
3429
3788
|
}
|
|
3430
3789
|
async function saveGraphToDisk(graph, outPath) {
|
|
3431
3790
|
await ensureDir(outPath);
|
|
@@ -3435,13 +3794,13 @@ async function saveGraphToDisk(graph, outPath) {
|
|
|
3435
3794
|
graph: graph.export()
|
|
3436
3795
|
};
|
|
3437
3796
|
const tmp = `${outPath}.tmp`;
|
|
3438
|
-
await
|
|
3439
|
-
await
|
|
3797
|
+
await fs17.writeFile(tmp, JSON.stringify(payload), "utf8");
|
|
3798
|
+
await fs17.rename(tmp, outPath);
|
|
3440
3799
|
}
|
|
3441
3800
|
async function loadGraphFromDisk(graph, outPath) {
|
|
3442
3801
|
let raw;
|
|
3443
3802
|
try {
|
|
3444
|
-
raw = await
|
|
3803
|
+
raw = await fs17.readFile(outPath, "utf8");
|
|
3445
3804
|
} catch (err) {
|
|
3446
3805
|
if (err.code === "ENOENT") return;
|
|
3447
3806
|
throw err;
|
|
@@ -3493,7 +3852,7 @@ function startPersistLoop(graph, outPath, intervalMs = 6e4) {
|
|
|
3493
3852
|
}
|
|
3494
3853
|
|
|
3495
3854
|
// src/diff.ts
|
|
3496
|
-
import { promises as
|
|
3855
|
+
import { promises as fs18 } from "fs";
|
|
3497
3856
|
async function loadSnapshotForDiff(target) {
|
|
3498
3857
|
if (/^https?:\/\//i.test(target)) {
|
|
3499
3858
|
const res = await fetch(target);
|
|
@@ -3502,7 +3861,7 @@ async function loadSnapshotForDiff(target) {
|
|
|
3502
3861
|
}
|
|
3503
3862
|
return await res.json();
|
|
3504
3863
|
}
|
|
3505
|
-
const raw = await
|
|
3864
|
+
const raw = await fs18.readFile(target, "utf8");
|
|
3506
3865
|
return JSON.parse(raw);
|
|
3507
3866
|
}
|
|
3508
3867
|
function indexEntries(entries) {
|
|
@@ -3569,23 +3928,23 @@ function canonicalJson(value) {
|
|
|
3569
3928
|
}
|
|
3570
3929
|
|
|
3571
3930
|
// src/projects.ts
|
|
3572
|
-
import
|
|
3931
|
+
import path32 from "path";
|
|
3573
3932
|
function pathsForProject(project, baseDir) {
|
|
3574
3933
|
if (project === DEFAULT_PROJECT) {
|
|
3575
3934
|
return {
|
|
3576
|
-
snapshotPath:
|
|
3577
|
-
errorsPath:
|
|
3578
|
-
staleEventsPath:
|
|
3579
|
-
embeddingsCachePath:
|
|
3580
|
-
policyViolationsPath:
|
|
3935
|
+
snapshotPath: path32.join(baseDir, "graph.json"),
|
|
3936
|
+
errorsPath: path32.join(baseDir, "errors.ndjson"),
|
|
3937
|
+
staleEventsPath: path32.join(baseDir, "stale-events.ndjson"),
|
|
3938
|
+
embeddingsCachePath: path32.join(baseDir, "embeddings.json"),
|
|
3939
|
+
policyViolationsPath: path32.join(baseDir, "policy-violations.ndjson")
|
|
3581
3940
|
};
|
|
3582
3941
|
}
|
|
3583
3942
|
return {
|
|
3584
|
-
snapshotPath:
|
|
3585
|
-
errorsPath:
|
|
3586
|
-
staleEventsPath:
|
|
3587
|
-
embeddingsCachePath:
|
|
3588
|
-
policyViolationsPath:
|
|
3943
|
+
snapshotPath: path32.join(baseDir, `${project}.json`),
|
|
3944
|
+
errorsPath: path32.join(baseDir, `errors.${project}.ndjson`),
|
|
3945
|
+
staleEventsPath: path32.join(baseDir, `stale-events.${project}.ndjson`),
|
|
3946
|
+
embeddingsCachePath: path32.join(baseDir, `embeddings.${project}.json`),
|
|
3947
|
+
policyViolationsPath: path32.join(baseDir, `policy-violations.${project}.ndjson`)
|
|
3589
3948
|
};
|
|
3590
3949
|
}
|
|
3591
3950
|
var Projects = class {
|
|
@@ -3624,9 +3983,9 @@ function parseExtraProjects(raw) {
|
|
|
3624
3983
|
}
|
|
3625
3984
|
|
|
3626
3985
|
// src/registry.ts
|
|
3627
|
-
import { promises as
|
|
3986
|
+
import { promises as fs19 } from "fs";
|
|
3628
3987
|
import os2 from "os";
|
|
3629
|
-
import
|
|
3988
|
+
import path33 from "path";
|
|
3630
3989
|
import {
|
|
3631
3990
|
RegistryFileSchema
|
|
3632
3991
|
} from "@neat.is/types";
|
|
@@ -3634,41 +3993,41 @@ var LOCK_TIMEOUT_MS = 5e3;
|
|
|
3634
3993
|
var LOCK_RETRY_MS = 50;
|
|
3635
3994
|
function neatHome() {
|
|
3636
3995
|
const override = process.env.NEAT_HOME;
|
|
3637
|
-
if (override && override.length > 0) return
|
|
3638
|
-
return
|
|
3996
|
+
if (override && override.length > 0) return path33.resolve(override);
|
|
3997
|
+
return path33.join(os2.homedir(), ".neat");
|
|
3639
3998
|
}
|
|
3640
3999
|
function registryPath() {
|
|
3641
|
-
return
|
|
4000
|
+
return path33.join(neatHome(), "projects.json");
|
|
3642
4001
|
}
|
|
3643
4002
|
function registryLockPath() {
|
|
3644
|
-
return
|
|
4003
|
+
return path33.join(neatHome(), "projects.json.lock");
|
|
3645
4004
|
}
|
|
3646
4005
|
async function normalizeProjectPath(input) {
|
|
3647
|
-
const resolved =
|
|
4006
|
+
const resolved = path33.resolve(input);
|
|
3648
4007
|
try {
|
|
3649
|
-
return await
|
|
4008
|
+
return await fs19.realpath(resolved);
|
|
3650
4009
|
} catch {
|
|
3651
4010
|
return resolved;
|
|
3652
4011
|
}
|
|
3653
4012
|
}
|
|
3654
4013
|
async function writeAtomically(target, contents) {
|
|
3655
|
-
await
|
|
4014
|
+
await fs19.mkdir(path33.dirname(target), { recursive: true });
|
|
3656
4015
|
const tmp = `${target}.${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 8)}.tmp`;
|
|
3657
|
-
const fd = await
|
|
4016
|
+
const fd = await fs19.open(tmp, "w");
|
|
3658
4017
|
try {
|
|
3659
4018
|
await fd.writeFile(contents, "utf8");
|
|
3660
4019
|
await fd.sync();
|
|
3661
4020
|
} finally {
|
|
3662
4021
|
await fd.close();
|
|
3663
4022
|
}
|
|
3664
|
-
await
|
|
4023
|
+
await fs19.rename(tmp, target);
|
|
3665
4024
|
}
|
|
3666
4025
|
async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
3667
4026
|
const deadline = Date.now() + timeoutMs;
|
|
3668
|
-
await
|
|
4027
|
+
await fs19.mkdir(path33.dirname(lockPath), { recursive: true });
|
|
3669
4028
|
while (true) {
|
|
3670
4029
|
try {
|
|
3671
|
-
const fd = await
|
|
4030
|
+
const fd = await fs19.open(lockPath, "wx");
|
|
3672
4031
|
await fd.close();
|
|
3673
4032
|
return;
|
|
3674
4033
|
} catch (err) {
|
|
@@ -3684,7 +4043,7 @@ async function acquireLock(lockPath, timeoutMs = LOCK_TIMEOUT_MS) {
|
|
|
3684
4043
|
}
|
|
3685
4044
|
}
|
|
3686
4045
|
async function releaseLock(lockPath) {
|
|
3687
|
-
await
|
|
4046
|
+
await fs19.unlink(lockPath).catch(() => {
|
|
3688
4047
|
});
|
|
3689
4048
|
}
|
|
3690
4049
|
async function withLock(fn) {
|
|
@@ -3700,7 +4059,7 @@ async function readRegistry() {
|
|
|
3700
4059
|
const file = registryPath();
|
|
3701
4060
|
let raw;
|
|
3702
4061
|
try {
|
|
3703
|
-
raw = await
|
|
4062
|
+
raw = await fs19.readFile(file, "utf8");
|
|
3704
4063
|
} catch (err) {
|
|
3705
4064
|
if (err.code === "ENOENT") {
|
|
3706
4065
|
return { version: 1, projects: [] };
|
|
@@ -3803,7 +4162,7 @@ import {
|
|
|
3803
4162
|
EdgeType as EdgeType9,
|
|
3804
4163
|
NodeType as NodeType10,
|
|
3805
4164
|
parseEdgeId,
|
|
3806
|
-
Provenance as
|
|
4165
|
+
Provenance as Provenance11
|
|
3807
4166
|
} from "@neat.is/types";
|
|
3808
4167
|
function bucketKey(source, target, type) {
|
|
3809
4168
|
return `${type}|${source}|${target}`;
|
|
@@ -3817,20 +4176,20 @@ function bucketEdges(graph) {
|
|
|
3817
4176
|
const key = bucketKey(e.source, e.target, e.type);
|
|
3818
4177
|
const cur = buckets.get(key) ?? { source: e.source, target: e.target, type: e.type };
|
|
3819
4178
|
switch (provenance) {
|
|
3820
|
-
case
|
|
4179
|
+
case Provenance11.EXTRACTED:
|
|
3821
4180
|
cur.extracted = e;
|
|
3822
4181
|
break;
|
|
3823
|
-
case
|
|
4182
|
+
case Provenance11.OBSERVED:
|
|
3824
4183
|
cur.observed = e;
|
|
3825
4184
|
break;
|
|
3826
|
-
case
|
|
4185
|
+
case Provenance11.INFERRED:
|
|
3827
4186
|
cur.inferred = e;
|
|
3828
4187
|
break;
|
|
3829
|
-
case
|
|
4188
|
+
case Provenance11.FRONTIER:
|
|
3830
4189
|
cur.frontier = e;
|
|
3831
4190
|
break;
|
|
3832
4191
|
default:
|
|
3833
|
-
if (e.provenance ===
|
|
4192
|
+
if (e.provenance === Provenance11.STALE) cur.stale = e;
|
|
3834
4193
|
}
|
|
3835
4194
|
buckets.set(key, cur);
|
|
3836
4195
|
});
|
|
@@ -3841,14 +4200,6 @@ function nodeIsFrontier(graph, nodeId) {
|
|
|
3841
4200
|
const attrs = graph.getNodeAttributes(nodeId);
|
|
3842
4201
|
return attrs.type === NodeType10.FrontierNode;
|
|
3843
4202
|
}
|
|
3844
|
-
function hasAnyObservedFromSource(graph, sourceId) {
|
|
3845
|
-
if (!graph.hasNode(sourceId)) return false;
|
|
3846
|
-
for (const edgeId of graph.outboundEdges(sourceId)) {
|
|
3847
|
-
const e = graph.getEdgeAttributes(edgeId);
|
|
3848
|
-
if (e.provenance === Provenance10.OBSERVED) return true;
|
|
3849
|
-
}
|
|
3850
|
-
return false;
|
|
3851
|
-
}
|
|
3852
4203
|
function clampConfidence(n) {
|
|
3853
4204
|
if (!Number.isFinite(n)) return 0;
|
|
3854
4205
|
return Math.max(0, Math.min(1, n));
|
|
@@ -3862,32 +4213,34 @@ function reasonForMissingExtracted(source, target, type) {
|
|
|
3862
4213
|
var RECOMMENDATION_MISSING_OBSERVED = "Verify the code path is exercised in production; check feature flags or conditional branches that might gate the call.";
|
|
3863
4214
|
var RECOMMENDATION_MISSING_EXTRACTED = "Likely dynamic dispatch, reflection, or a coverage gap in tree-sitter extraction. Consider an `aliases` entry on the source service or file an extractor issue.";
|
|
3864
4215
|
var RECOMMENDATION_HOST_MISMATCH = "Check environment-specific config overrides \u2014 the runtime host differs from what static configuration declares.";
|
|
4216
|
+
function gradedConfidence(edge) {
|
|
4217
|
+
if (typeof edge.confidence === "number") return clampConfidence(edge.confidence);
|
|
4218
|
+
return clampConfidence(confidenceForEdge(edge));
|
|
4219
|
+
}
|
|
3865
4220
|
function detectMissingDivergences(graph, bucket) {
|
|
3866
4221
|
const out = [];
|
|
3867
4222
|
if (bucket.extracted && !bucket.observed) {
|
|
3868
4223
|
if (!nodeIsFrontier(graph, bucket.target)) {
|
|
3869
|
-
const sourceHasTraffic = hasAnyObservedFromSource(graph, bucket.source);
|
|
3870
4224
|
out.push({
|
|
3871
4225
|
type: "missing-observed",
|
|
3872
4226
|
source: bucket.source,
|
|
3873
4227
|
target: bucket.target,
|
|
3874
4228
|
edgeType: bucket.type,
|
|
3875
4229
|
extracted: bucket.extracted,
|
|
3876
|
-
confidence:
|
|
4230
|
+
confidence: gradedConfidence(bucket.extracted),
|
|
3877
4231
|
reason: reasonForMissingObserved(bucket.source, bucket.target, bucket.type),
|
|
3878
4232
|
recommendation: RECOMMENDATION_MISSING_OBSERVED
|
|
3879
4233
|
});
|
|
3880
4234
|
}
|
|
3881
4235
|
}
|
|
3882
4236
|
if (bucket.observed && !bucket.extracted) {
|
|
3883
|
-
const cascaded = clampConfidence(confidenceForEdge(bucket.observed));
|
|
3884
4237
|
out.push({
|
|
3885
4238
|
type: "missing-extracted",
|
|
3886
4239
|
source: bucket.source,
|
|
3887
4240
|
target: bucket.target,
|
|
3888
4241
|
edgeType: bucket.type,
|
|
3889
4242
|
observed: bucket.observed,
|
|
3890
|
-
confidence:
|
|
4243
|
+
confidence: gradedConfidence(bucket.observed),
|
|
3891
4244
|
reason: reasonForMissingExtracted(bucket.source, bucket.target, bucket.type),
|
|
3892
4245
|
recommendation: RECOMMENDATION_MISSING_EXTRACTED
|
|
3893
4246
|
});
|
|
@@ -3906,7 +4259,7 @@ function declaredHostFor(svc) {
|
|
|
3906
4259
|
function hasExtractedConfiguredBy(graph, svcId) {
|
|
3907
4260
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3908
4261
|
const e = graph.getEdgeAttributes(edgeId);
|
|
3909
|
-
if (e.type === EdgeType9.CONFIGURED_BY && e.provenance ===
|
|
4262
|
+
if (e.type === EdgeType9.CONFIGURED_BY && e.provenance === Provenance11.EXTRACTED) {
|
|
3910
4263
|
return true;
|
|
3911
4264
|
}
|
|
3912
4265
|
}
|
|
@@ -3920,7 +4273,7 @@ function detectHostMismatch(graph, svcId, svc) {
|
|
|
3920
4273
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3921
4274
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3922
4275
|
if (edge.type !== EdgeType9.CONNECTS_TO) continue;
|
|
3923
|
-
if (edge.provenance !==
|
|
4276
|
+
if (edge.provenance !== Provenance11.OBSERVED) continue;
|
|
3924
4277
|
const target = graph.getNodeAttributes(edge.target);
|
|
3925
4278
|
if (target.type !== NodeType10.DatabaseNode) continue;
|
|
3926
4279
|
const observedHost = target.host?.trim();
|
|
@@ -3945,7 +4298,7 @@ function detectCompatDivergences(graph, svcId, svc) {
|
|
|
3945
4298
|
for (const edgeId of graph.outboundEdges(svcId)) {
|
|
3946
4299
|
const edge = graph.getEdgeAttributes(edgeId);
|
|
3947
4300
|
if (edge.type !== EdgeType9.CONNECTS_TO) continue;
|
|
3948
|
-
if (edge.provenance !==
|
|
4301
|
+
if (edge.provenance !== Provenance11.OBSERVED) continue;
|
|
3949
4302
|
const target = graph.getNodeAttributes(edge.target);
|
|
3950
4303
|
if (target.type !== NodeType10.DatabaseNode) continue;
|
|
3951
4304
|
for (const pair of compatPairs()) {
|
|
@@ -4026,8 +4379,17 @@ function computeDivergences(graph, opts = {}) {
|
|
|
4026
4379
|
const target = opts.node;
|
|
4027
4380
|
filtered = filtered.filter((d) => involvesNode(d, target));
|
|
4028
4381
|
}
|
|
4382
|
+
const TYPE_LEADERSHIP = {
|
|
4383
|
+
"missing-extracted": 0,
|
|
4384
|
+
"missing-observed": 1,
|
|
4385
|
+
"version-mismatch": 2,
|
|
4386
|
+
"host-mismatch": 3,
|
|
4387
|
+
"compat-violation": 4
|
|
4388
|
+
};
|
|
4029
4389
|
filtered.sort((a, b) => {
|
|
4030
4390
|
if (b.confidence !== a.confidence) return b.confidence - a.confidence;
|
|
4391
|
+
const lead = TYPE_LEADERSHIP[a.type] - TYPE_LEADERSHIP[b.type];
|
|
4392
|
+
if (lead !== 0) return lead;
|
|
4031
4393
|
if (a.type !== b.type) return a.type.localeCompare(b.type);
|
|
4032
4394
|
if (a.source !== b.source) return a.source.localeCompare(b.source);
|
|
4033
4395
|
return a.target.localeCompare(b.target);
|
|
@@ -4545,6 +4907,9 @@ export {
|
|
|
4545
4907
|
readStaleEvents,
|
|
4546
4908
|
startStalenessLoop,
|
|
4547
4909
|
readErrorEvents,
|
|
4910
|
+
isStrictExtractionEnabled,
|
|
4911
|
+
formatExtractionBanner,
|
|
4912
|
+
formatPrecisionFloorBanner,
|
|
4548
4913
|
discoverServices,
|
|
4549
4914
|
addServiceNodes,
|
|
4550
4915
|
addServiceAliases,
|
|
@@ -4552,6 +4917,7 @@ export {
|
|
|
4552
4917
|
addConfigNodes,
|
|
4553
4918
|
addCallEdges,
|
|
4554
4919
|
addInfra,
|
|
4920
|
+
retireEdgesByFile,
|
|
4555
4921
|
extractFromDirectory,
|
|
4556
4922
|
saveGraphToDisk,
|
|
4557
4923
|
loadGraphFromDisk,
|
|
@@ -4575,4 +4941,4 @@ export {
|
|
|
4575
4941
|
removeProject,
|
|
4576
4942
|
buildApi
|
|
4577
4943
|
};
|
|
4578
|
-
//# sourceMappingURL=chunk-
|
|
4944
|
+
//# sourceMappingURL=chunk-33ZZ2ZID.js.map
|