codarch 0.1.0 → 0.1.1
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/README.md +13 -0
- package/index.js +392 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,19 @@ codarch diff base.cc.json head.cc.json -o report.json --config .codarch.yml --fa
|
|
|
42
42
|
Códigos de salida de `diff`: `0` sin bloqueo, `2` política incumplida, `1` error de lectura o
|
|
43
43
|
configuración.
|
|
44
44
|
|
|
45
|
+
## Lente de migración
|
|
46
|
+
|
|
47
|
+
`analyze` mide TS/JS, Rust y artefactos `.wasm`/`.node`. El snapshot incluye la lente `migration` y
|
|
48
|
+
las métricas `migrationDebt` (complejidad × consumidores) y `ffiCrossings`.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
codarch inspect .codarch/snapshot.cc.json --metric migrationDebt --top 10
|
|
52
|
+
codarch render .codarch/snapshot.cc.json --area rloc --color migrationDebt -o migration.html
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Rust se mide con un escáner léxico; las aristas napi no se inventan si el glue generado no está en el
|
|
56
|
+
fuente. Ver [`docs/migration.md`](https://github.com/jaminsmoke/codarch/blob/main/docs/migration.md).
|
|
57
|
+
|
|
45
58
|
## Reglas
|
|
46
59
|
|
|
47
60
|
`.codarch.yml` define capas, dependencias prohibidas, umbrales y ciclos. Solo bloquean las
|
package/index.js
CHANGED
|
@@ -1544,6 +1544,240 @@ function runGit(cwd, args, maxBuffer) {
|
|
|
1544
1544
|
return { ok: true, stdout: result.stdout ?? "", error: "" };
|
|
1545
1545
|
}
|
|
1546
1546
|
|
|
1547
|
+
// packages/parsers/src/migration.ts
|
|
1548
|
+
var EXTENSION_LANGUAGE = {
|
|
1549
|
+
".ts": "ts",
|
|
1550
|
+
".tsx": "ts",
|
|
1551
|
+
".mts": "ts",
|
|
1552
|
+
".cts": "ts",
|
|
1553
|
+
".js": "js",
|
|
1554
|
+
".jsx": "js",
|
|
1555
|
+
".mjs": "js",
|
|
1556
|
+
".cjs": "js",
|
|
1557
|
+
".rs": "rust",
|
|
1558
|
+
".wasm": "wasm",
|
|
1559
|
+
".node": "node"
|
|
1560
|
+
};
|
|
1561
|
+
var MIGRATION_EXTENSIONS = Object.keys(EXTENSION_LANGUAGE);
|
|
1562
|
+
var LANGUAGE_ORDER = ["ts", "js", "rust", "wasm", "node"];
|
|
1563
|
+
var RUST_BRIDGE = /#\[\s*(?:napi|wasm_bindgen)(?:\(|\])|\bnapi_derive\b|\bnapi::|\bwasm_bindgen(?:::|\b)|\bwasm-pack\b/;
|
|
1564
|
+
var JS_FFI = /\bWebAssembly\s*\.\s*(?:instantiate|compile|Module|Instance)\b|\bprocess\.dlopen\b/;
|
|
1565
|
+
function languageForPath(relativePath2) {
|
|
1566
|
+
const lower = relativePath2.toLowerCase();
|
|
1567
|
+
const dot = lower.lastIndexOf(".");
|
|
1568
|
+
if (dot === -1) return null;
|
|
1569
|
+
return EXTENSION_LANGUAGE[lower.slice(dot)] ?? null;
|
|
1570
|
+
}
|
|
1571
|
+
function isTsJsLanguage(language) {
|
|
1572
|
+
return language === "ts" || language === "js";
|
|
1573
|
+
}
|
|
1574
|
+
function isNativeLanguage(language) {
|
|
1575
|
+
return language === "wasm" || language === "node";
|
|
1576
|
+
}
|
|
1577
|
+
function measureRustSource(source) {
|
|
1578
|
+
if (source.length === 0) return { loc: 0, rloc: 0 };
|
|
1579
|
+
const loc = countPhysicalLines(source);
|
|
1580
|
+
const codeLines = /* @__PURE__ */ new Set();
|
|
1581
|
+
let line = 0;
|
|
1582
|
+
let lineHasCode = false;
|
|
1583
|
+
let blockDepth = 0;
|
|
1584
|
+
let inString = false;
|
|
1585
|
+
let inChar = false;
|
|
1586
|
+
let escaped = false;
|
|
1587
|
+
let rawHashes = -1;
|
|
1588
|
+
const mark = () => {
|
|
1589
|
+
lineHasCode = true;
|
|
1590
|
+
};
|
|
1591
|
+
const newline = () => {
|
|
1592
|
+
if (lineHasCode) codeLines.add(line);
|
|
1593
|
+
line++;
|
|
1594
|
+
lineHasCode = inString || rawHashes >= 0;
|
|
1595
|
+
};
|
|
1596
|
+
for (let index = 0; index < source.length; index++) {
|
|
1597
|
+
const current = source[index];
|
|
1598
|
+
const next = source[index + 1];
|
|
1599
|
+
if (current === "\r") {
|
|
1600
|
+
if (next === "\n") continue;
|
|
1601
|
+
newline();
|
|
1602
|
+
continue;
|
|
1603
|
+
}
|
|
1604
|
+
if (current === "\n") {
|
|
1605
|
+
newline();
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
if (blockDepth > 0) {
|
|
1609
|
+
if (current === "/" && next === "*") {
|
|
1610
|
+
blockDepth++;
|
|
1611
|
+
index++;
|
|
1612
|
+
} else if (current === "*" && next === "/") {
|
|
1613
|
+
blockDepth--;
|
|
1614
|
+
index++;
|
|
1615
|
+
}
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (rawHashes >= 0) {
|
|
1619
|
+
if (current === '"') {
|
|
1620
|
+
let hashes = 0;
|
|
1621
|
+
while (source[index + 1 + hashes] === "#") hashes++;
|
|
1622
|
+
if (hashes >= rawHashes) {
|
|
1623
|
+
index += rawHashes;
|
|
1624
|
+
rawHashes = -1;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (inString) {
|
|
1630
|
+
mark();
|
|
1631
|
+
if (escaped) escaped = false;
|
|
1632
|
+
else if (current === "\\") escaped = true;
|
|
1633
|
+
else if (current === '"') inString = false;
|
|
1634
|
+
continue;
|
|
1635
|
+
}
|
|
1636
|
+
if (inChar) {
|
|
1637
|
+
mark();
|
|
1638
|
+
if (escaped) escaped = false;
|
|
1639
|
+
else if (current === "\\") escaped = true;
|
|
1640
|
+
else if (current === "'") inChar = false;
|
|
1641
|
+
continue;
|
|
1642
|
+
}
|
|
1643
|
+
if (current === "/" && next === "/") {
|
|
1644
|
+
while (index + 1 < source.length && source[index + 1] !== "\n" && source[index + 1] !== "\r") {
|
|
1645
|
+
index++;
|
|
1646
|
+
}
|
|
1647
|
+
continue;
|
|
1648
|
+
}
|
|
1649
|
+
if (current === "/" && next === "*") {
|
|
1650
|
+
blockDepth = 1;
|
|
1651
|
+
index++;
|
|
1652
|
+
continue;
|
|
1653
|
+
}
|
|
1654
|
+
if (current === "r" || current === "b" && next === "r") {
|
|
1655
|
+
let cursor = current === "r" ? index + 1 : index + 2;
|
|
1656
|
+
let hashes = 0;
|
|
1657
|
+
while (source[cursor] === "#") {
|
|
1658
|
+
hashes++;
|
|
1659
|
+
cursor++;
|
|
1660
|
+
}
|
|
1661
|
+
if (source[cursor] === '"') {
|
|
1662
|
+
rawHashes = hashes;
|
|
1663
|
+
index = cursor;
|
|
1664
|
+
mark();
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
if (current === '"') {
|
|
1669
|
+
inString = true;
|
|
1670
|
+
mark();
|
|
1671
|
+
continue;
|
|
1672
|
+
}
|
|
1673
|
+
if (current === "'" && isRustCharLiteral(source, index)) {
|
|
1674
|
+
inChar = true;
|
|
1675
|
+
mark();
|
|
1676
|
+
continue;
|
|
1677
|
+
}
|
|
1678
|
+
if (current.trim() !== "") mark();
|
|
1679
|
+
}
|
|
1680
|
+
if (lineHasCode) codeLines.add(line);
|
|
1681
|
+
return { loc, rloc: codeLines.size };
|
|
1682
|
+
}
|
|
1683
|
+
function rustIsBridge(source) {
|
|
1684
|
+
return RUST_BRIDGE.test(source);
|
|
1685
|
+
}
|
|
1686
|
+
function jsIsBridge(source, specifiers) {
|
|
1687
|
+
return specifiers.some((specifier) => ffiKindForSpecifier(specifier) !== null) || JS_FFI.test(source);
|
|
1688
|
+
}
|
|
1689
|
+
function ffiKindForSpecifier(specifier) {
|
|
1690
|
+
const clean = specifier.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
|
|
1691
|
+
if (clean.endsWith(".wasm")) return "wasm";
|
|
1692
|
+
if (clean.endsWith(".node")) return "native";
|
|
1693
|
+
return null;
|
|
1694
|
+
}
|
|
1695
|
+
function buildMigration(files, links) {
|
|
1696
|
+
const sortedFiles = [...files].sort(
|
|
1697
|
+
(left, right) => compareStrings2(left.relativePath, right.relativePath)
|
|
1698
|
+
);
|
|
1699
|
+
const sortedLinks = [...links].sort(
|
|
1700
|
+
(left, right) => compareStrings2(left.from, right.from) || compareStrings2(left.to, right.to) || compareStrings2(left.kind, right.kind)
|
|
1701
|
+
);
|
|
1702
|
+
const crossings = /* @__PURE__ */ new Map();
|
|
1703
|
+
for (const link of sortedLinks) {
|
|
1704
|
+
crossings.set(link.from, (crossings.get(link.from) ?? 0) + 1);
|
|
1705
|
+
crossings.set(link.to, (crossings.get(link.to) ?? 0) + 1);
|
|
1706
|
+
}
|
|
1707
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1708
|
+
const status = {
|
|
1709
|
+
js: 0,
|
|
1710
|
+
rust: 0,
|
|
1711
|
+
bridge: 0,
|
|
1712
|
+
native: 0
|
|
1713
|
+
};
|
|
1714
|
+
const languageStats = /* @__PURE__ */ new Map();
|
|
1715
|
+
let migrationDebt = 0;
|
|
1716
|
+
let ffiCrossings = 0;
|
|
1717
|
+
for (const file of sortedFiles) {
|
|
1718
|
+
const fileStatus = isNativeLanguage(file.language) ? "native" : file.bridge ? "bridge" : file.language === "rust" ? "rust" : "js";
|
|
1719
|
+
const debt = isTsJsLanguage(file.language) ? (file.maxComplexity + 1) * (1 + file.fanIn) : void 0;
|
|
1720
|
+
const fileCrossings = crossings.get(file.relativePath) ?? 0;
|
|
1721
|
+
nodes.set(file.relativePath, {
|
|
1722
|
+
language: file.language,
|
|
1723
|
+
status: fileStatus,
|
|
1724
|
+
loc: file.loc,
|
|
1725
|
+
rloc: file.rloc,
|
|
1726
|
+
...debt !== void 0 ? { debt } : {},
|
|
1727
|
+
crossings: fileCrossings
|
|
1728
|
+
});
|
|
1729
|
+
status[fileStatus]++;
|
|
1730
|
+
migrationDebt += debt ?? 0;
|
|
1731
|
+
ffiCrossings += fileCrossings;
|
|
1732
|
+
const stats = languageStats.get(file.language) ?? { files: 0, loc: 0, rloc: 0 };
|
|
1733
|
+
stats.files++;
|
|
1734
|
+
stats.loc += file.loc;
|
|
1735
|
+
stats.rloc += file.rloc;
|
|
1736
|
+
languageStats.set(file.language, stats);
|
|
1737
|
+
}
|
|
1738
|
+
const languages = {};
|
|
1739
|
+
for (const language of LANGUAGE_ORDER) {
|
|
1740
|
+
const stats = languageStats.get(language);
|
|
1741
|
+
if (stats) languages[language] = stats;
|
|
1742
|
+
}
|
|
1743
|
+
return {
|
|
1744
|
+
nodes,
|
|
1745
|
+
edges: sortedLinks,
|
|
1746
|
+
summary: {
|
|
1747
|
+
files: sortedFiles.length,
|
|
1748
|
+
languages,
|
|
1749
|
+
status,
|
|
1750
|
+
bridgeFiles: status.bridge,
|
|
1751
|
+
ffiEdges: sortedLinks.length,
|
|
1752
|
+
ffiCrossings,
|
|
1753
|
+
migrationDebt
|
|
1754
|
+
}
|
|
1755
|
+
};
|
|
1756
|
+
}
|
|
1757
|
+
function isRustCharLiteral(source, index) {
|
|
1758
|
+
const next = source[index + 1];
|
|
1759
|
+
if (next === void 0 || next === "\n" || next === "\r") return false;
|
|
1760
|
+
if (next === "\\") return true;
|
|
1761
|
+
return source[index + 2] === "'";
|
|
1762
|
+
}
|
|
1763
|
+
function countPhysicalLines(source) {
|
|
1764
|
+
if (source.length === 0) return 0;
|
|
1765
|
+
let lines = 1;
|
|
1766
|
+
for (let index = 0; index < source.length; index++) {
|
|
1767
|
+
const code = source.charCodeAt(index);
|
|
1768
|
+
if (code === 10) {
|
|
1769
|
+
lines++;
|
|
1770
|
+
} else if (code === 13 && source.charCodeAt(index + 1) !== 10) {
|
|
1771
|
+
lines++;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
const last = source.charCodeAt(source.length - 1);
|
|
1775
|
+
return last === 10 || last === 13 ? lines - 1 : lines;
|
|
1776
|
+
}
|
|
1777
|
+
function compareStrings2(left, right) {
|
|
1778
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1547
1781
|
// packages/parsers/src/snapshot.ts
|
|
1548
1782
|
import { createHash as createHash2 } from "node:crypto";
|
|
1549
1783
|
var METRIC_DESCRIPTORS = {
|
|
@@ -1631,6 +1865,16 @@ var METRIC_DESCRIPTORS = {
|
|
|
1631
1865
|
title: "Nivel de dependencia",
|
|
1632
1866
|
description: "Profundidad en el DAG de dependencias",
|
|
1633
1867
|
direction: 0
|
|
1868
|
+
},
|
|
1869
|
+
migrationDebt: {
|
|
1870
|
+
title: "Deuda de migraci\xF3n",
|
|
1871
|
+
description: "(maxComplexity + 1) \xD7 (1 + fan-in); 0 si ya es Rust o nativo",
|
|
1872
|
+
direction: -1
|
|
1873
|
+
},
|
|
1874
|
+
ffiCrossings: {
|
|
1875
|
+
title: "Cruces FFI",
|
|
1876
|
+
description: "Aristas WASM/native que tocan el fichero",
|
|
1877
|
+
direction: -1
|
|
1634
1878
|
}
|
|
1635
1879
|
};
|
|
1636
1880
|
var METRIC_ORDER = Object.keys(METRIC_DESCRIPTORS);
|
|
@@ -1648,7 +1892,7 @@ var EDGE_DESCRIPTORS = {
|
|
|
1648
1892
|
};
|
|
1649
1893
|
function buildSnapshot(input) {
|
|
1650
1894
|
const files = [...input.files].sort(
|
|
1651
|
-
(left, right) =>
|
|
1895
|
+
(left, right) => compareStrings3(left.relativePath, right.relativePath)
|
|
1652
1896
|
);
|
|
1653
1897
|
const rootName = sanitizeNodeName(input.projectName);
|
|
1654
1898
|
const root = {
|
|
@@ -1713,6 +1957,7 @@ function buildSnapshot(input) {
|
|
|
1713
1957
|
for (const [path8, metrics] of input.graph.metrics) {
|
|
1714
1958
|
dependencyNodes[fileId(path8)] = { level: metrics.dependencyLevel };
|
|
1715
1959
|
}
|
|
1960
|
+
const migrationLens = input.migration ? migrationLensFrom(input.migration) : void 0;
|
|
1716
1961
|
const lenses = {
|
|
1717
1962
|
metrics: {
|
|
1718
1963
|
attributes,
|
|
@@ -1727,7 +1972,8 @@ function buildSnapshot(input) {
|
|
|
1727
1972
|
weight: "absolute",
|
|
1728
1973
|
typeOnly: "absolute"
|
|
1729
1974
|
}
|
|
1730
|
-
}
|
|
1975
|
+
},
|
|
1976
|
+
...migrationLens ? { migration: migrationLens } : {}
|
|
1731
1977
|
};
|
|
1732
1978
|
return {
|
|
1733
1979
|
meta: {
|
|
@@ -1739,6 +1985,24 @@ function buildSnapshot(input) {
|
|
|
1739
1985
|
lenses
|
|
1740
1986
|
};
|
|
1741
1987
|
}
|
|
1988
|
+
function migrationLensFrom(migration) {
|
|
1989
|
+
const nodes = {};
|
|
1990
|
+
for (const [relativePath2, node] of migration.nodes) {
|
|
1991
|
+
nodes[fileId(relativePath2)] = node;
|
|
1992
|
+
}
|
|
1993
|
+
const edges = migration.edges.map((edge) => ({
|
|
1994
|
+
fromId: fileId(edge.from),
|
|
1995
|
+
toId: fileId(edge.to),
|
|
1996
|
+
kind: edge.kind
|
|
1997
|
+
})).sort(
|
|
1998
|
+
(left, right) => compareStrings3(left.fromId, right.fromId) || compareStrings3(left.toId, right.toId) || compareStrings3(left.kind, right.kind)
|
|
1999
|
+
);
|
|
2000
|
+
return {
|
|
2001
|
+
nodes,
|
|
2002
|
+
edges,
|
|
2003
|
+
summary: migration.summary
|
|
2004
|
+
};
|
|
2005
|
+
}
|
|
1742
2006
|
function checksumFor(files, lenses) {
|
|
1743
2007
|
return createHash2("md5").update(stableStringify({ files, lenses }), "utf8").digest("hex");
|
|
1744
2008
|
}
|
|
@@ -1764,7 +2028,7 @@ function sortChildren(node) {
|
|
|
1764
2028
|
if (!node.children) return;
|
|
1765
2029
|
node.children.sort((left, right) => {
|
|
1766
2030
|
if (left.type !== right.type) return left.type === "Folder" ? -1 : 1;
|
|
1767
|
-
return
|
|
2031
|
+
return compareStrings3(left.name, right.name);
|
|
1768
2032
|
});
|
|
1769
2033
|
for (const child of node.children) sortChildren(child);
|
|
1770
2034
|
}
|
|
@@ -1772,7 +2036,7 @@ function sanitizeNodeName(name) {
|
|
|
1772
2036
|
const sanitized = name.replace(/[\\/]/g, "-").trim();
|
|
1773
2037
|
return sanitized && sanitized !== "." && sanitized !== ".." ? sanitized : "Proyecto";
|
|
1774
2038
|
}
|
|
1775
|
-
function
|
|
2039
|
+
function compareStrings3(left, right) {
|
|
1776
2040
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
1777
2041
|
}
|
|
1778
2042
|
|
|
@@ -1881,19 +2145,39 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
1881
2145
|
const projectName = options.projectName ?? readProjectName(root);
|
|
1882
2146
|
const walked = walkSourceFiles(root, {
|
|
1883
2147
|
ignore: options.ignore,
|
|
1884
|
-
extensions: options.extensions
|
|
2148
|
+
extensions: options.extensions ?? MIGRATION_EXTENSIONS
|
|
1885
2149
|
});
|
|
1886
2150
|
const warnings = [...walked.warnings];
|
|
1887
2151
|
const resolver = new ImportResolver(root, walked.files);
|
|
1888
2152
|
warnings.push(...resolver.warnings);
|
|
1889
2153
|
const parser = new TypeScriptParser();
|
|
1890
2154
|
const parsedFiles = [];
|
|
2155
|
+
const migrationFiles = [];
|
|
2156
|
+
const migrationLinks = [];
|
|
1891
2157
|
const links = [];
|
|
1892
2158
|
const unresolved = [];
|
|
1893
2159
|
let externalImports = 0;
|
|
1894
2160
|
let builtinImports = 0;
|
|
1895
2161
|
let typeOnlyImports = 0;
|
|
1896
2162
|
for (const file of walked.files) {
|
|
2163
|
+
const language = languageForPath(file.relativePath);
|
|
2164
|
+
if (!language) continue;
|
|
2165
|
+
if (isNativeLanguage(language)) {
|
|
2166
|
+
parsedFiles.push({
|
|
2167
|
+
relativePath: file.relativePath,
|
|
2168
|
+
metrics: { loc: 0, rloc: 0 }
|
|
2169
|
+
});
|
|
2170
|
+
migrationFiles.push({
|
|
2171
|
+
relativePath: file.relativePath,
|
|
2172
|
+
language,
|
|
2173
|
+
loc: 0,
|
|
2174
|
+
rloc: 0,
|
|
2175
|
+
maxComplexity: 0,
|
|
2176
|
+
fanIn: 0,
|
|
2177
|
+
bridge: false
|
|
2178
|
+
});
|
|
2179
|
+
continue;
|
|
2180
|
+
}
|
|
1897
2181
|
let source;
|
|
1898
2182
|
try {
|
|
1899
2183
|
source = fs4.readFileSync(file.absolutePath, "utf8");
|
|
@@ -1903,6 +2187,23 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
1903
2187
|
);
|
|
1904
2188
|
continue;
|
|
1905
2189
|
}
|
|
2190
|
+
if (language === "rust") {
|
|
2191
|
+
const measured = measureRustSource(source);
|
|
2192
|
+
parsedFiles.push({
|
|
2193
|
+
relativePath: file.relativePath,
|
|
2194
|
+
metrics: { loc: measured.loc, rloc: measured.rloc }
|
|
2195
|
+
});
|
|
2196
|
+
migrationFiles.push({
|
|
2197
|
+
relativePath: file.relativePath,
|
|
2198
|
+
language,
|
|
2199
|
+
loc: measured.loc,
|
|
2200
|
+
rloc: measured.rloc,
|
|
2201
|
+
maxComplexity: 0,
|
|
2202
|
+
fanIn: 0,
|
|
2203
|
+
bridge: rustIsBridge(source)
|
|
2204
|
+
});
|
|
2205
|
+
continue;
|
|
2206
|
+
}
|
|
1906
2207
|
let parsed;
|
|
1907
2208
|
try {
|
|
1908
2209
|
parsed = parser.parse(source, file.relativePath);
|
|
@@ -1920,11 +2221,20 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
1920
2221
|
if (imported.typeOnly) fileTypeOnlyImports++;
|
|
1921
2222
|
const result = resolver.resolve(file.absolutePath, imported.specifier);
|
|
1922
2223
|
if (result.kind === "internal") {
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
2224
|
+
const targetLanguage = languageForPath(result.file.relativePath);
|
|
2225
|
+
if (targetLanguage === "wasm" || targetLanguage === "node") {
|
|
2226
|
+
migrationLinks.push({
|
|
2227
|
+
from: file.relativePath,
|
|
2228
|
+
to: result.file.relativePath,
|
|
2229
|
+
kind: targetLanguage === "wasm" ? "wasm" : "native"
|
|
2230
|
+
});
|
|
2231
|
+
} else {
|
|
2232
|
+
links.push({
|
|
2233
|
+
from: file.relativePath,
|
|
2234
|
+
to: result.file.relativePath,
|
|
2235
|
+
typeOnly: imported.typeOnly
|
|
2236
|
+
});
|
|
2237
|
+
}
|
|
1928
2238
|
} else if (result.kind === "external") {
|
|
1929
2239
|
if (result.builtin) fileBuiltinImports++;
|
|
1930
2240
|
else fileExternalImports++;
|
|
@@ -1952,6 +2262,18 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
1952
2262
|
unresolvedImports: fileUnresolvedImports
|
|
1953
2263
|
}
|
|
1954
2264
|
});
|
|
2265
|
+
migrationFiles.push({
|
|
2266
|
+
relativePath: file.relativePath,
|
|
2267
|
+
language,
|
|
2268
|
+
loc: parsed.metrics.loc ?? 0,
|
|
2269
|
+
rloc: parsed.metrics.rloc ?? 0,
|
|
2270
|
+
maxComplexity: parsed.metrics.maxComplexity ?? 0,
|
|
2271
|
+
fanIn: 0,
|
|
2272
|
+
bridge: jsIsBridge(
|
|
2273
|
+
source,
|
|
2274
|
+
parsed.imports.map((imported) => imported.specifier)
|
|
2275
|
+
)
|
|
2276
|
+
});
|
|
1955
2277
|
}
|
|
1956
2278
|
const git = collectGitChurn(
|
|
1957
2279
|
root,
|
|
@@ -1981,6 +2303,16 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
1981
2303
|
file.metrics.dependencyLevel = metrics.dependencyLevel;
|
|
1982
2304
|
}
|
|
1983
2305
|
}
|
|
2306
|
+
for (const file of migrationFiles) {
|
|
2307
|
+
file.fanIn = graph.metrics.get(file.relativePath)?.fanIn ?? 0;
|
|
2308
|
+
}
|
|
2309
|
+
const migration = buildMigration(migrationFiles, migrationLinks);
|
|
2310
|
+
for (const file of parsedFiles) {
|
|
2311
|
+
const node = migration.nodes.get(file.relativePath);
|
|
2312
|
+
if (!node) continue;
|
|
2313
|
+
file.metrics.ffiCrossings = node.crossings;
|
|
2314
|
+
if (node.debt !== void 0) file.metrics.migrationDebt = node.debt;
|
|
2315
|
+
}
|
|
1984
2316
|
if (unresolved.length > 0) {
|
|
1985
2317
|
warnings.push(
|
|
1986
2318
|
`${unresolved.length} imports sin resolver; se excluyen del grafo`
|
|
@@ -1989,7 +2321,8 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
1989
2321
|
const snapshot = buildSnapshot({
|
|
1990
2322
|
projectName,
|
|
1991
2323
|
files: parsedFiles,
|
|
1992
|
-
graph
|
|
2324
|
+
graph,
|
|
2325
|
+
migration
|
|
1993
2326
|
});
|
|
1994
2327
|
let folders = 0;
|
|
1995
2328
|
walkTree(snapshot.files, (entry) => {
|
|
@@ -2019,6 +2352,7 @@ function analyzeRepository(rootInput, options = {}) {
|
|
|
2019
2352
|
authors: git.authors,
|
|
2020
2353
|
shallow: git.shallow
|
|
2021
2354
|
},
|
|
2355
|
+
migration: migration.summary,
|
|
2022
2356
|
durationMs: Date.now() - started
|
|
2023
2357
|
};
|
|
2024
2358
|
}
|
|
@@ -2369,9 +2703,9 @@ function cycleMessage(paths) {
|
|
|
2369
2703
|
return `Ciclo de ${paths.length} ficheros: ${visible.join(" \u2192 ")}${suffix}`;
|
|
2370
2704
|
}
|
|
2371
2705
|
function compareViolations(left, right) {
|
|
2372
|
-
return
|
|
2706
|
+
return compareStrings4(left.key, right.key);
|
|
2373
2707
|
}
|
|
2374
|
-
function
|
|
2708
|
+
function compareStrings4(left, right) {
|
|
2375
2709
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
2376
2710
|
}
|
|
2377
2711
|
|
|
@@ -2431,7 +2765,7 @@ function indexSnapshot(cc) {
|
|
|
2431
2765
|
});
|
|
2432
2766
|
}
|
|
2433
2767
|
const nodes = [...paths.keys()].sort(
|
|
2434
|
-
(left, right) =>
|
|
2768
|
+
(left, right) => compareStrings5(paths.get(left) ?? left, paths.get(right) ?? right)
|
|
2435
2769
|
);
|
|
2436
2770
|
const metrics = cc.lenses?.metrics?.attributes ?? {};
|
|
2437
2771
|
const edges = /* @__PURE__ */ new Map();
|
|
@@ -2494,7 +2828,7 @@ function changedFiles(base, head) {
|
|
|
2494
2828
|
const headValues = head.metrics[id] ?? {};
|
|
2495
2829
|
const keys = /* @__PURE__ */ new Set([...Object.keys(baseValues), ...Object.keys(headValues)]);
|
|
2496
2830
|
const changes = [];
|
|
2497
|
-
for (const metric of [...keys].sort(
|
|
2831
|
+
for (const metric of [...keys].sort(compareStrings5)) {
|
|
2498
2832
|
const baseValue = finiteNumber(baseValues[metric]);
|
|
2499
2833
|
const headValue = finiteNumber(headValues[metric]);
|
|
2500
2834
|
if (baseValue === null && headValue === null) continue;
|
|
@@ -2554,10 +2888,10 @@ function cycleGroups(index) {
|
|
|
2554
2888
|
reverse.get(edge.toId)?.push(edge.fromId);
|
|
2555
2889
|
}
|
|
2556
2890
|
for (const [node, neighbors] of adjacency) {
|
|
2557
|
-
adjacency.set(node, [...new Set(neighbors)].sort(
|
|
2891
|
+
adjacency.set(node, [...new Set(neighbors)].sort(compareStrings5));
|
|
2558
2892
|
}
|
|
2559
2893
|
for (const [node, neighbors] of reverse) {
|
|
2560
|
-
reverse.set(node, [...new Set(neighbors)].sort(
|
|
2894
|
+
reverse.set(node, [...new Set(neighbors)].sort(compareStrings5));
|
|
2561
2895
|
}
|
|
2562
2896
|
const visited = /* @__PURE__ */ new Set();
|
|
2563
2897
|
const order = [];
|
|
@@ -2611,9 +2945,9 @@ function cycleGroups(index) {
|
|
|
2611
2945
|
id,
|
|
2612
2946
|
path: index.paths.get(id) ?? id
|
|
2613
2947
|
})).sort((left, right) => {
|
|
2614
|
-
return
|
|
2948
|
+
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
2615
2949
|
});
|
|
2616
|
-
const key = [...component].sort(
|
|
2950
|
+
const key = [...component].sort(compareStrings5).join("\0");
|
|
2617
2951
|
groups.set(key, { key, component: { size: nodes.length, nodes } });
|
|
2618
2952
|
}
|
|
2619
2953
|
return groups;
|
|
@@ -2680,13 +3014,13 @@ function buildHotspots(base, head, added, changed, metric, top) {
|
|
|
2680
3014
|
});
|
|
2681
3015
|
}
|
|
2682
3016
|
return hotspots.sort(
|
|
2683
|
-
(left, right) => right.delta - left.delta || right.head - left.head ||
|
|
3017
|
+
(left, right) => right.delta - left.delta || right.head - left.head || compareStrings5(left.path, right.path)
|
|
2684
3018
|
).slice(0, top);
|
|
2685
3019
|
}
|
|
2686
3020
|
function sortedMetrics(values) {
|
|
2687
3021
|
const sorted = {};
|
|
2688
3022
|
if (!values) return sorted;
|
|
2689
|
-
for (const key of Object.keys(values).sort(
|
|
3023
|
+
for (const key of Object.keys(values).sort(compareStrings5)) {
|
|
2690
3024
|
const value = finiteNumber(values[key]);
|
|
2691
3025
|
if (value !== null) sorted[key] = value;
|
|
2692
3026
|
}
|
|
@@ -2699,21 +3033,21 @@ function edgeKey(fromId, toId) {
|
|
|
2699
3033
|
return `${fromId}\0${toId}`;
|
|
2700
3034
|
}
|
|
2701
3035
|
function compareFiles(left, right) {
|
|
2702
|
-
return
|
|
3036
|
+
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
2703
3037
|
}
|
|
2704
3038
|
function compareChangedFiles(left, right) {
|
|
2705
|
-
return
|
|
3039
|
+
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
2706
3040
|
}
|
|
2707
3041
|
function compareEdgeChanges(left, right) {
|
|
2708
|
-
return
|
|
3042
|
+
return compareStrings5(left.from, right.from) || compareStrings5(left.to, right.to) || compareStrings5(left.fromId, right.fromId) || compareStrings5(left.toId, right.toId);
|
|
2709
3043
|
}
|
|
2710
3044
|
function compareCycles(left, right) {
|
|
2711
|
-
return
|
|
3045
|
+
return compareStrings5(
|
|
2712
3046
|
left.nodes[0]?.path ?? "",
|
|
2713
3047
|
right.nodes[0]?.path ?? ""
|
|
2714
3048
|
) || left.size - right.size;
|
|
2715
3049
|
}
|
|
2716
|
-
function
|
|
3050
|
+
function compareStrings5(left, right) {
|
|
2717
3051
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
2718
3052
|
}
|
|
2719
3053
|
|
|
@@ -3882,6 +4216,17 @@ function runAnalyze(options) {
|
|
|
3882
4216
|
)} sin resolver`
|
|
3883
4217
|
)
|
|
3884
4218
|
);
|
|
4219
|
+
const migrationLanguages = Object.entries(result.migration.languages).map(([language, stats]) => `${language} ${num(stats?.rloc ?? 0)}`).join(" \xB7 ");
|
|
4220
|
+
lines.push(
|
|
4221
|
+
field(
|
|
4222
|
+
"migraci\xF3n",
|
|
4223
|
+
`${migrationLanguages || "\u2014"} \xB7 ${num(
|
|
4224
|
+
result.migration.bridgeFiles
|
|
4225
|
+
)} puentes \xB7 ${num(result.migration.ffiEdges)} FFI \xB7 deuda ${num(
|
|
4226
|
+
result.migration.migrationDebt
|
|
4227
|
+
)}`
|
|
4228
|
+
)
|
|
4229
|
+
);
|
|
3885
4230
|
lines.push(field("tiempo", `${num(result.durationMs)} ms`));
|
|
3886
4231
|
lines.push(
|
|
3887
4232
|
field(
|
|
@@ -4127,6 +4472,12 @@ function truncateLeft(value, max = 78) {
|
|
|
4127
4472
|
function shortChecksum(value) {
|
|
4128
4473
|
return value ? `${value.slice(0, 8)}\u2026` : "\u2014";
|
|
4129
4474
|
}
|
|
4475
|
+
function readMigrationLens(value) {
|
|
4476
|
+
if (typeof value !== "object" || value === null) return null;
|
|
4477
|
+
const lens = value;
|
|
4478
|
+
if (typeof lens.summary !== "object" || lens.summary === null) return null;
|
|
4479
|
+
return lens;
|
|
4480
|
+
}
|
|
4130
4481
|
function runInspect(options) {
|
|
4131
4482
|
if (!options.file) {
|
|
4132
4483
|
console.error("falta la ruta del fichero cc.json\n\n codarch inspect <fichero.cc.json[.gz]>");
|
|
@@ -4201,6 +4552,20 @@ function runInspect(options) {
|
|
|
4201
4552
|
lines.push(
|
|
4202
4553
|
field3("aristas", `${num3(stats.edges)} \xB7 leafEdges ${num3(stats.leafEdges)}`)
|
|
4203
4554
|
);
|
|
4555
|
+
const migration = readMigrationLens(cc.lenses?.migration);
|
|
4556
|
+
if (migration) {
|
|
4557
|
+
const languages = Object.entries(migration.summary.languages).map(([language, entry]) => `${language} ${num3(entry?.rloc ?? 0)}`).join(" \xB7 ");
|
|
4558
|
+
lines.push(
|
|
4559
|
+
field3(
|
|
4560
|
+
"migraci\xF3n",
|
|
4561
|
+
`${languages || "\u2014"} \xB7 ${num3(
|
|
4562
|
+
migration.summary.bridgeFiles
|
|
4563
|
+
)} puentes \xB7 ${num3(migration.summary.ffiEdges)} FFI \xB7 deuda ${num3(
|
|
4564
|
+
migration.summary.migrationDebt
|
|
4565
|
+
)}`
|
|
4566
|
+
)
|
|
4567
|
+
);
|
|
4568
|
+
}
|
|
4204
4569
|
if (stats.orphanMetricNodes > 0) {
|
|
4205
4570
|
lines.push(field3("hu\xE9rfanos", `${num3(stats.orphanMetricNodes)} nodos de m\xE9tricas sin fichero`));
|
|
4206
4571
|
}
|
|
@@ -4338,7 +4703,7 @@ function runRender(options) {
|
|
|
4338
4703
|
}
|
|
4339
4704
|
|
|
4340
4705
|
// apps/cli/src/index.ts
|
|
4341
|
-
var VERSION = true ? "0.1.
|
|
4706
|
+
var VERSION = true ? "0.1.1" : "0.0.0";
|
|
4342
4707
|
var USAGE = `
|
|
4343
4708
|
CodArch \xB7 mapa vivo de la arquitectura de tu software
|
|
4344
4709
|
|