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.
Files changed (3) hide show
  1. package/README.md +13 -0
  2. package/index.js +392 -27
  3. 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) => compareStrings2(left.relativePath, right.relativePath)
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 compareStrings2(left.name, right.name);
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 compareStrings2(left, right) {
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
- links.push({
1924
- from: file.relativePath,
1925
- to: result.file.relativePath,
1926
- typeOnly: imported.typeOnly
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 compareStrings3(left.key, right.key);
2706
+ return compareStrings4(left.key, right.key);
2373
2707
  }
2374
- function compareStrings3(left, right) {
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) => compareStrings4(paths.get(left) ?? left, paths.get(right) ?? 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(compareStrings4)) {
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(compareStrings4));
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(compareStrings4));
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 compareStrings4(left.path, right.path) || compareStrings4(left.id, right.id);
2948
+ return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
2615
2949
  });
2616
- const key = [...component].sort(compareStrings4).join("\0");
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 || compareStrings4(left.path, right.path)
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(compareStrings4)) {
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 compareStrings4(left.path, right.path) || compareStrings4(left.id, right.id);
3036
+ return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
2703
3037
  }
2704
3038
  function compareChangedFiles(left, right) {
2705
- return compareStrings4(left.path, right.path) || compareStrings4(left.id, right.id);
3039
+ return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
2706
3040
  }
2707
3041
  function compareEdgeChanges(left, right) {
2708
- return compareStrings4(left.from, right.from) || compareStrings4(left.to, right.to) || compareStrings4(left.fromId, right.fromId) || compareStrings4(left.toId, right.toId);
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 compareStrings4(
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 compareStrings4(left, right) {
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.0" : "0.0.0";
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codarch",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Mapa de arquitectura para equipos y agentes de IA",
5
5
  "license": "MIT",
6
6
  "type": "module",