codarch 0.1.3 → 0.2.0

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/index.js CHANGED
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // apps/cli/src/commands/analyze.ts
4
- import fs6 from "node:fs";
5
- import path5 from "node:path";
4
+ import fs4 from "node:fs";
5
+ import path3 from "node:path";
6
6
 
7
7
  // packages/parsers/src/ts/parser.ts
8
8
  import Parser from "tree-sitter";
9
9
  import TypeScript from "tree-sitter-typescript";
10
- var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
10
+
11
+ // packages/parsers/src/ts/measure.ts
11
12
  var FUNCTION_TYPES = /* @__PURE__ */ new Set([
12
13
  "function_declaration",
13
14
  "function_expression",
@@ -28,30 +29,13 @@ var DECISION_TYPES = /* @__PURE__ */ new Set([
28
29
  "conditional_expression"
29
30
  ]);
30
31
  var LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["&&", "||", "??"]);
31
- var TypeScriptParser = class {
32
- name = "typescript";
33
- extensions = EXTENSIONS;
34
- tsParser = new Parser();
35
- tsxParser = new Parser();
36
- constructor() {
37
- this.tsParser.setLanguage(TypeScript.typescript);
38
- this.tsxParser.setLanguage(TypeScript.tsx);
39
- }
40
- parse(source, filePath) {
41
- const parser = usesTsxGrammar(filePath) ? this.tsxParser : this.tsParser;
42
- const root = parser.parse(source, void 0, {
43
- bufferSize: Math.max(64 * 1024, Buffer.byteLength(source, "utf8") + 1)
44
- }).rootNode;
45
- return {
46
- metrics: measureSource(source, root),
47
- imports: collectImports(root)
48
- };
49
- }
50
- };
51
32
  function usesTsxGrammar(filePath) {
52
33
  const lower = filePath.toLowerCase();
53
34
  return lower.endsWith(".tsx") || lower.endsWith(".jsx") || lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs");
54
35
  }
36
+ function isComment(text) {
37
+ return text.startsWith("//") || text.startsWith("/*") || text.startsWith("#!");
38
+ }
55
39
  function measureSource(source, root) {
56
40
  const codeLines = /* @__PURE__ */ new Set();
57
41
  const stack = [root];
@@ -60,8 +44,8 @@ function measureSource(source, root) {
60
44
  let maxFunctionLength = 0;
61
45
  while (stack.length > 0) {
62
46
  const node = stack.pop();
63
- if (node.type === "comment" || node.type === "hash_bang_line") continue;
64
47
  if (node.childCount === 0) {
48
+ if (isComment(node.text)) continue;
65
49
  if (node.text.trim() !== "") {
66
50
  for (let row = node.startPosition.row; row <= node.endPosition.row; row++) {
67
51
  codeLines.add(row);
@@ -225,11 +209,175 @@ function decodeStringBody(body) {
225
209
  );
226
210
  }
227
211
 
212
+ // packages/parsers/src/ts/parser.ts
213
+ var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
214
+ var TypeScriptParser = class {
215
+ name = "typescript";
216
+ extensions = EXTENSIONS;
217
+ tsParser = new Parser();
218
+ tsxParser = new Parser();
219
+ constructor() {
220
+ this.tsParser.setLanguage(TypeScript.typescript);
221
+ this.tsxParser.setLanguage(TypeScript.tsx);
222
+ }
223
+ parse(source, filePath) {
224
+ const parser = usesTsxGrammar(filePath) ? this.tsxParser : this.tsParser;
225
+ const root = parser.parse(source, void 0, {
226
+ bufferSize: Math.max(64 * 1024, Buffer.byteLength(source, "utf8") + 1)
227
+ }).rootNode;
228
+ const node = root;
229
+ return {
230
+ metrics: measureSource(source, node),
231
+ imports: collectImports(node)
232
+ };
233
+ }
234
+ };
235
+
228
236
  // packages/parsers/src/resolve.ts
229
- import fs from "node:fs";
230
- import path from "node:path";
231
- import { builtinModules } from "node:module";
232
237
  import ts from "typescript";
238
+
239
+ // packages/parsers/src/builtins.ts
240
+ var NODE_BUILTINS = [
241
+ "_http_agent",
242
+ "_http_client",
243
+ "_http_common",
244
+ "_http_incoming",
245
+ "_http_outgoing",
246
+ "_http_server",
247
+ "_stream_duplex",
248
+ "_stream_passthrough",
249
+ "_stream_readable",
250
+ "_stream_transform",
251
+ "_stream_wrap",
252
+ "_stream_writable",
253
+ "_tls_common",
254
+ "_tls_wrap",
255
+ "assert",
256
+ "assert/strict",
257
+ "async_hooks",
258
+ "buffer",
259
+ "child_process",
260
+ "cluster",
261
+ "console",
262
+ "constants",
263
+ "crypto",
264
+ "dgram",
265
+ "diagnostics_channel",
266
+ "dns",
267
+ "dns/promises",
268
+ "domain",
269
+ "events",
270
+ "fs",
271
+ "fs/promises",
272
+ "http",
273
+ "http2",
274
+ "https",
275
+ "inspector",
276
+ "inspector/promises",
277
+ "module",
278
+ "net",
279
+ "os",
280
+ "path",
281
+ "path/posix",
282
+ "path/win32",
283
+ "perf_hooks",
284
+ "process",
285
+ "punycode",
286
+ "querystring",
287
+ "readline",
288
+ "readline/promises",
289
+ "repl",
290
+ "stream",
291
+ "stream/consumers",
292
+ "stream/promises",
293
+ "stream/web",
294
+ "string_decoder",
295
+ "sys",
296
+ "timers",
297
+ "timers/promises",
298
+ "tls",
299
+ "trace_events",
300
+ "tty",
301
+ "url",
302
+ "util",
303
+ "util/types",
304
+ "v8",
305
+ "vm",
306
+ "wasi",
307
+ "worker_threads",
308
+ "zlib",
309
+ "node:sea",
310
+ "node:sqlite",
311
+ "node:test",
312
+ "node:test/reporters"
313
+ ];
314
+ function isNodeBuiltin(specifier) {
315
+ if (specifier.startsWith("node:")) return true;
316
+ return NODE_BUILTINS.includes(specifier);
317
+ }
318
+
319
+ // packages/parsers/src/path.ts
320
+ function join(...segments) {
321
+ const parts = [];
322
+ for (const segment of segments) {
323
+ if (!segment) continue;
324
+ for (const piece of segment.split("/")) {
325
+ if (piece === "" || piece === ".") continue;
326
+ if (piece === "..") {
327
+ parts.pop();
328
+ continue;
329
+ }
330
+ parts.push(piece);
331
+ }
332
+ }
333
+ return `/${parts.join("/")}`;
334
+ }
335
+ function resolve(...segments) {
336
+ let resolved = "";
337
+ let absolute = false;
338
+ for (let index = segments.length - 1; index >= 0 && !absolute; index -= 1) {
339
+ const segment = segments[index];
340
+ if (!segment) continue;
341
+ resolved = resolved === "" ? segment : `${segment}/${resolved}`;
342
+ absolute = segment.startsWith("/");
343
+ }
344
+ if (!absolute) resolved = `/${resolved}`;
345
+ return join(resolved);
346
+ }
347
+ function dirname(target) {
348
+ const normalized = target.replace(/\/+$/, "");
349
+ const index = normalized.lastIndexOf("/");
350
+ if (index <= 0) return "/";
351
+ return normalized.slice(0, index);
352
+ }
353
+ function basename(target) {
354
+ const normalized = target.replace(/\/+$/, "");
355
+ const index = normalized.lastIndexOf("/");
356
+ return index === -1 ? normalized : normalized.slice(index + 1);
357
+ }
358
+ function extname(target) {
359
+ const name = basename(target);
360
+ const index = name.lastIndexOf(".");
361
+ if (index <= 0) return "";
362
+ return name.slice(index);
363
+ }
364
+ function relative(from, target) {
365
+ const fromParts = from.split("/").filter(Boolean);
366
+ const targetParts = target.split("/").filter(Boolean);
367
+ let common = 0;
368
+ while (common < fromParts.length && common < targetParts.length && fromParts[common] === targetParts[common]) {
369
+ common += 1;
370
+ }
371
+ const up = fromParts.length - common;
372
+ const down = targetParts.slice(common);
373
+ const parts = [...Array.from({ length: up }, () => ".."), ...down];
374
+ return parts.join("/");
375
+ }
376
+ function isAbsolute(target) {
377
+ return target.startsWith("/");
378
+ }
379
+
380
+ // packages/parsers/src/resolve.ts
233
381
  var SOURCE_EXTENSIONS = [
234
382
  ".ts",
235
383
  ".tsx",
@@ -255,9 +403,6 @@ var WORKSPACE_SKIP_DIRS = /* @__PURE__ */ new Set([
255
403
  ]);
256
404
  var MAX_WORKSPACE_DEPTH = 8;
257
405
  var MAX_RESOLVE_DEPTH = 8;
258
- var BUILTINS = new Set(
259
- builtinModules.flatMap((name) => [name, `node:${name}`])
260
- );
261
406
  var ASSET_EXTENSIONS = [
262
407
  ".css",
263
408
  ".scss",
@@ -283,31 +428,33 @@ var ASSET_EXTENSIONS = [
283
428
  ".pdf"
284
429
  ];
285
430
  function isAssetSpecifier(specifier) {
286
- const clean = specifier.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
287
- return ASSET_EXTENSIONS.some((extension) => clean.endsWith(extension));
431
+ const clean2 = specifier.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
432
+ return ASSET_EXTENSIONS.some((extension) => clean2.endsWith(extension));
288
433
  }
289
434
  var ImportResolver = class {
290
435
  warnings = [];
291
436
  root;
437
+ fileSystem;
292
438
  byPath = /* @__PURE__ */ new Map();
293
439
  packageJsonCache = /* @__PURE__ */ new Map();
294
440
  configCache = /* @__PURE__ */ new Map();
295
441
  workspaces;
296
- constructor(rootInput, files) {
297
- this.root = path.resolve(rootInput);
298
- for (const file of files) this.byPath.set(pathKey(file.absolutePath), file);
299
- this.workspaces = discoverWorkspaces(this.root, this.warnings);
442
+ constructor(rootInput, files, fileSystem) {
443
+ this.root = resolve(rootInput);
444
+ this.fileSystem = fileSystem;
445
+ for (const file of files) this.byPath.set(this.pathKey(file.absolutePath), file);
446
+ this.workspaces = discoverWorkspaces(this.root, this.fileSystem, this.warnings);
300
447
  }
301
448
  resolve(importerPath, specifier) {
302
- if (BUILTINS.has(specifier) || specifier.startsWith("node:")) {
449
+ if (isNodeBuiltin(specifier)) {
303
450
  return { kind: "external", specifier, builtin: true };
304
451
  }
305
452
  if (specifier.startsWith(".") || specifier.startsWith("/")) {
306
- const base = specifier.startsWith("/") ? path.resolve(this.root, `.${specifier}`) : path.resolve(path.dirname(path.resolve(importerPath)), specifier);
453
+ const base = specifier.startsWith("/") ? resolve(this.root, `.${specifier}`) : resolve(dirname(resolve(importerPath)), specifier);
307
454
  const file = this.resolveAsFileOrDirectory(base);
308
455
  return file ? { kind: "internal", file } : { kind: "unresolved", specifier, reason: "no se encontr\xF3 el fichero" };
309
456
  }
310
- const config = this.configFor(path.dirname(path.resolve(importerPath)));
457
+ const config = this.configFor(dirname(resolve(importerPath)));
311
458
  if (config) {
312
459
  const alias = matchAlias(config.aliases, specifier);
313
460
  if (alias) {
@@ -345,7 +492,7 @@ var ImportResolver = class {
345
492
  const subpath = specifier === workspace.name ? "." : `./${specifier.slice(workspace.name.length + 1)}`;
346
493
  for (const target of packageExportTargets(workspace.json.exports, subpath)) {
347
494
  const file = this.resolveAsFileOrDirectory(
348
- path.resolve(workspace.dir, target)
495
+ resolve(workspace.dir, target)
349
496
  );
350
497
  if (file) return file;
351
498
  }
@@ -354,7 +501,7 @@ var ImportResolver = class {
354
501
  const value = workspace.json[field5];
355
502
  if (typeof value !== "string") continue;
356
503
  const file = this.resolveAsFileOrDirectory(
357
- path.resolve(workspace.dir, value)
504
+ resolve(workspace.dir, value)
358
505
  );
359
506
  if (file) return file;
360
507
  }
@@ -365,13 +512,13 @@ var ImportResolver = class {
365
512
  "index.tsx"
366
513
  ]) {
367
514
  const file = this.resolveAsFileOrDirectory(
368
- path.resolve(workspace.dir, fallback)
515
+ resolve(workspace.dir, fallback)
369
516
  );
370
517
  if (file) return file;
371
518
  }
372
519
  } else {
373
520
  const file = this.resolveAsFileOrDirectory(
374
- path.resolve(workspace.dir, subpath.slice(2))
521
+ resolve(workspace.dir, subpath.slice(2))
375
522
  );
376
523
  if (file) return file;
377
524
  }
@@ -380,7 +527,7 @@ var ImportResolver = class {
380
527
  resolveAsFileOrDirectory(base, depth = 0) {
381
528
  if (depth > MAX_RESOLVE_DEPTH) return null;
382
529
  for (const candidate of fileCandidates(base)) {
383
- const file = this.byPath.get(pathKey(candidate));
530
+ const file = this.byPath.get(this.pathKey(candidate));
384
531
  if (file) return file;
385
532
  }
386
533
  const pkg = this.readPackageJson(base);
@@ -389,67 +536,77 @@ var ImportResolver = class {
389
536
  const value = pkg[field5];
390
537
  if (typeof value !== "string") continue;
391
538
  const file = this.resolveAsFileOrDirectory(
392
- path.resolve(base, value),
539
+ resolve(base, value),
393
540
  depth + 1
394
541
  );
395
542
  if (file) return file;
396
543
  }
397
544
  for (const target of packageExportTargets(pkg.exports, ".")) {
398
545
  const file = this.resolveAsFileOrDirectory(
399
- path.resolve(base, target),
546
+ resolve(base, target),
400
547
  depth + 1
401
548
  );
402
549
  if (file) return file;
403
550
  }
404
551
  }
405
552
  for (const candidate of indexCandidates(base)) {
406
- const file = this.byPath.get(pathKey(candidate));
553
+ const file = this.byPath.get(this.pathKey(candidate));
407
554
  if (file) return file;
408
555
  }
409
556
  return null;
410
557
  }
411
558
  readPackageJson(directory) {
412
- const key = pathKey(directory);
559
+ const key = this.pathKey(directory);
413
560
  if (this.packageJsonCache.has(key)) {
414
561
  return this.packageJsonCache.get(key) ?? null;
415
562
  }
416
- const json = readJsonFile(path.join(directory, "package.json"), this.warnings);
563
+ const json = readJsonFile(
564
+ this.fileSystem,
565
+ join(directory, "package.json"),
566
+ this.warnings
567
+ );
417
568
  this.packageJsonCache.set(key, json);
418
569
  return json;
419
570
  }
420
571
  configFor(startDirectory) {
421
572
  const visited = [];
422
- let directory = path.resolve(startDirectory);
573
+ let directory = resolve(startDirectory);
423
574
  while (true) {
424
- const key = pathKey(directory);
575
+ const key = this.pathKey(directory);
425
576
  const cached = this.configCache.get(key);
426
577
  if (cached !== void 0) {
427
578
  for (const visitedKey of visited) this.configCache.set(visitedKey, cached);
428
579
  return cached;
429
580
  }
430
581
  visited.push(key);
431
- const configPath = findTsConfig(directory);
582
+ const configPath = findTsConfig(this.fileSystem, directory);
432
583
  if (configPath) {
433
- const loaded = loadTsConfig(configPath, this.warnings);
584
+ const loaded = loadTsConfig(this.fileSystem, configPath, this.warnings);
434
585
  for (const visitedKey of visited) this.configCache.set(visitedKey, loaded);
435
586
  return loaded;
436
587
  }
437
- if (key === pathKey(this.root)) break;
438
- const parent = path.dirname(directory);
588
+ if (key === this.pathKey(this.root)) break;
589
+ const parent = dirname(directory);
439
590
  if (parent === directory || !isWithin(this.root, parent)) break;
440
591
  directory = parent;
441
592
  }
442
593
  for (const visitedKey of visited) this.configCache.set(visitedKey, null);
443
594
  return null;
444
595
  }
596
+ /**
597
+ * Clave de una ruta para las tablas internas.
598
+ *
599
+ * En sistemas que no distinguen mayúsculas, la clave se normaliza para que
600
+ * `./Foo` encuentre `foo.ts`.
601
+ */
602
+ pathKey(value) {
603
+ const resolved = resolve(value);
604
+ return this.fileSystem.caseSensitive ? resolved : resolved.toLowerCase();
605
+ }
445
606
  };
446
- function pathKey(value) {
447
- const resolved = path.resolve(value);
448
- return process.platform === "win32" ? resolved.toLowerCase() : resolved;
449
- }
450
607
  function isWithin(root, candidate) {
451
- const relative = path.relative(root, candidate);
452
- return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
608
+ const relative2 = relative(root, candidate);
609
+ return relative2 === "" || !relative2.startsWith("..") && !isAbsolute(relative2);
453
610
  }
454
611
  function fileCandidates(base) {
455
612
  const lower = base.toLowerCase();
@@ -467,7 +624,7 @@ function fileCandidates(base) {
467
624
  if (lower.endsWith(".cjs")) {
468
625
  return [`${base.slice(0, -4)}.cts`, base];
469
626
  }
470
- if (path.extname(base) !== "") return [base];
627
+ if (extname(base) !== "") return [base];
471
628
  return [
472
629
  base,
473
630
  ...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`)
@@ -475,18 +632,29 @@ function fileCandidates(base) {
475
632
  }
476
633
  function indexCandidates(directory) {
477
634
  return SOURCE_EXTENSIONS.map(
478
- (extension) => path.join(directory, `index${extension}`)
635
+ (extension) => join(directory, `index${extension}`)
479
636
  );
480
637
  }
481
- function findTsConfig(directory) {
638
+ function findTsConfig(fileSystem, directory) {
482
639
  for (const name of ["tsconfig.json", "jsconfig.json"]) {
483
- const candidate = path.join(directory, name);
484
- if (fs.existsSync(candidate)) return candidate;
640
+ const candidate = join(directory, name);
641
+ if (fileSystem.exists(candidate)) return candidate;
485
642
  }
486
643
  return null;
487
644
  }
488
- function loadTsConfig(configPath, warnings) {
489
- const read = ts.readConfigFile(configPath, ts.sys.readFile);
645
+ function loadTsConfig(fileSystem, configPath, warnings) {
646
+ const host = {
647
+ useCaseSensitiveFileNames: fileSystem.caseSensitive,
648
+ readDirectory: () => [],
649
+ fileExists: (candidate) => fileSystem.exists(candidate),
650
+ readFile: (candidate) => fileSystem.tryReadFile(candidate) ?? void 0,
651
+ getCurrentDirectory: () => "/",
652
+ onUnRecoverableConfigFileDiagnostic: () => void 0
653
+ };
654
+ const read = ts.readConfigFile(
655
+ configPath,
656
+ (candidate) => fileSystem.tryReadFile(candidate) ?? void 0
657
+ );
490
658
  if (read.error || read.config === void 0) {
491
659
  warnings.push(
492
660
  `no se pudo leer ${configPath}: ${read.error?.messageText ?? "configuraci\xF3n vac\xEDa"}`
@@ -495,13 +663,13 @@ function loadTsConfig(configPath, warnings) {
495
663
  }
496
664
  const parsed = ts.parseJsonConfigFileContent(
497
665
  read.config,
498
- ts.sys,
499
- path.dirname(configPath),
666
+ host,
667
+ dirname(configPath),
500
668
  void 0,
501
669
  configPath
502
670
  );
503
- const configDirectory = path.dirname(configPath);
504
- const baseUrl = path.resolve(configDirectory, parsed.options.baseUrl ?? ".");
671
+ const configDirectory = dirname(configPath);
672
+ const baseUrl = resolve(configDirectory, parsed.options.baseUrl ?? ".");
505
673
  const paths = parsed.options.paths ?? {};
506
674
  const aliases = [];
507
675
  for (const [pattern, targets] of Object.entries(paths)) {
@@ -509,7 +677,7 @@ function loadTsConfig(configPath, warnings) {
509
677
  const star = pattern.indexOf("*");
510
678
  const prefix = star === -1 ? pattern : pattern.slice(0, star);
511
679
  const suffix = star === -1 ? "" : pattern.slice(star + 1);
512
- const resolvedTargets = targets.filter((target) => typeof target === "string").map((target) => path.resolve(baseUrl, target));
680
+ const resolvedTargets = targets.filter((target) => typeof target === "string").map((target) => resolve(baseUrl, target));
513
681
  if (resolvedTargets.length > 0) {
514
682
  aliases.push({
515
683
  prefix,
@@ -602,8 +770,12 @@ function matchWildcardKey(pattern, subpath) {
602
770
  }
603
771
  return subpath.slice(prefix.length, subpath.length - suffix.length);
604
772
  }
605
- function discoverWorkspaces(root, warnings) {
606
- const rootJson = readJsonFile(path.join(root, "package.json"), warnings);
773
+ function discoverWorkspaces(root, fileSystem, warnings) {
774
+ const rootJson = readJsonFile(
775
+ fileSystem,
776
+ join(root, "package.json"),
777
+ warnings
778
+ );
607
779
  if (!rootJson) return [];
608
780
  const patterns = workspacePatterns(rootJson.workspaces);
609
781
  if (patterns.length === 0) return [];
@@ -615,14 +787,15 @@ function discoverWorkspaces(root, warnings) {
615
787
  const current = stack.pop();
616
788
  if (current.relative !== "" && patterns.some((pattern) => pattern.test(current.relative))) {
617
789
  const json = readJsonFile(
618
- path.join(current.directory, "package.json"),
790
+ fileSystem,
791
+ join(current.directory, "package.json"),
619
792
  warnings
620
793
  );
621
794
  if (json && typeof json.name === "string") {
622
795
  const existing = byName.get(json.name);
623
796
  if (existing) {
624
797
  warnings.push(
625
- `workspace duplicado "${json.name}": ${path.relative(root, existing.dir)} y ${path.relative(root, current.directory)}`
798
+ `workspace duplicado "${json.name}": ${relative(root, existing.dir)} y ${relative(root, current.directory)}`
626
799
  );
627
800
  } else {
628
801
  byName.set(json.name, {
@@ -636,7 +809,7 @@ function discoverWorkspaces(root, warnings) {
636
809
  if (current.depth >= MAX_WORKSPACE_DEPTH) continue;
637
810
  let entries;
638
811
  try {
639
- entries = fs.readdirSync(current.directory, { withFileTypes: true });
812
+ entries = fileSystem.readDirectory(current.directory);
640
813
  } catch (error) {
641
814
  warnings.push(
642
815
  `no se pudo leer ${current.relative || "."}: ${error.message}`
@@ -644,10 +817,10 @@ function discoverWorkspaces(root, warnings) {
644
817
  continue;
645
818
  }
646
819
  for (const entry of entries) {
647
- if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
820
+ if (!entry.isDirectory) continue;
648
821
  if (WORKSPACE_SKIP_DIRS.has(entry.name)) continue;
649
822
  stack.push({
650
- directory: path.join(current.directory, entry.name),
823
+ directory: join(current.directory, entry.name),
651
824
  relative: current.relative ? `${current.relative}/${entry.name}` : entry.name,
652
825
  depth: current.depth + 1
653
826
  });
@@ -693,16 +866,15 @@ function globToRegExp(glob) {
693
866
  function escapeRegExp(value) {
694
867
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
695
868
  }
696
- function readJsonFile(file, warnings) {
869
+ function readJsonFile(fileSystem, file, warnings) {
697
870
  let text;
698
871
  try {
699
- text = fs.readFileSync(file, "utf8");
872
+ text = fileSystem.tryReadFile(file);
700
873
  } catch (error) {
701
- if (error.code !== "ENOENT") {
702
- warnings.push(`no se pudo leer ${file}: ${error.message}`);
703
- }
874
+ warnings.push(`no se pudo leer ${file}: ${error.message}`);
704
875
  return null;
705
876
  }
877
+ if (text === null) return null;
706
878
  try {
707
879
  const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
708
880
  return isRecord(parsed) ? parsed : null;
@@ -881,41 +1053,516 @@ function compareStrings(left, right) {
881
1053
  }
882
1054
 
883
1055
  // packages/parsers/src/analyze.ts
884
- import fs4 from "node:fs";
885
- import path4 from "node:path";
1056
+ import { basename as basename2 } from "node:path";
886
1057
 
887
1058
  // packages/schema/src/errors.ts
888
1059
  var CcJsonError = class extends Error {
889
1060
  path;
890
- constructor(message, path9) {
891
- super(`${message} (en ${path9})`);
1061
+ constructor(message, path7) {
1062
+ super(`${message} (en ${path7})`);
892
1063
  this.name = "CcJsonError";
893
- this.path = path9;
1064
+ this.path = path7;
894
1065
  }
895
1066
  };
896
1067
  function isRecord2(value) {
897
1068
  return typeof value === "object" && value !== null && !Array.isArray(value);
898
1069
  }
899
- function requireRecord(value, path9) {
900
- if (!isRecord2(value)) throw new CcJsonError("se esperaba un objeto", path9);
1070
+ function requireRecord(value, path7) {
1071
+ if (!isRecord2(value)) throw new CcJsonError("se esperaba un objeto", path7);
901
1072
  return value;
902
1073
  }
903
- function requireString(value, path9, field5) {
1074
+ function requireString(value, path7, field5) {
904
1075
  if (typeof value !== "string" || value.length === 0) {
905
- throw new CcJsonError(`"${field5}" debe ser una cadena no vac\xEDa`, path9);
1076
+ throw new CcJsonError(`"${field5}" debe ser una cadena no vac\xEDa`, path7);
906
1077
  }
907
1078
  return value;
908
1079
  }
909
1080
 
1081
+ // node_modules/@noble/hashes/_u64.js
1082
+ var fromNumH = (n) => n / 2 ** 32 | 0;
1083
+ var fromNumL = (n) => n >>> 0;
1084
+ function setU64FromNum(view, byteOffset, n, isLE) {
1085
+ const h = fromNumH(n);
1086
+ const l = fromNumL(n);
1087
+ view.setUint32(byteOffset, isLE ? l : h, isLE);
1088
+ view.setUint32(byteOffset + 4, isLE ? h : l, isLE);
1089
+ }
1090
+
1091
+ // node_modules/@noble/hashes/utils.js
1092
+ function isBytes(a) {
1093
+ return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
1094
+ }
1095
+ var atitle = (title) => title ? `"${title}" ` : "";
1096
+ function anumber(n, title = "") {
1097
+ if (typeof n !== "number")
1098
+ throw new TypeError(atitle(title) + "expected number, got " + typeof n);
1099
+ if (!Number.isSafeInteger(n) || n < 0)
1100
+ throw new RangeError(atitle(title) + "expected integer >= 0, got " + n);
1101
+ return n;
1102
+ }
1103
+ function abytes(value, length, title = "") {
1104
+ if (isBytes(value) && (length === void 0 || value.length === length))
1105
+ return value;
1106
+ if (length !== void 0)
1107
+ anumber(length, "length");
1108
+ const bytes = isBytes(value);
1109
+ const ofLen = length !== void 0 ? ` of length ${length}` : "";
1110
+ const got = bytes ? `length=${value.length}` : `type=${typeof value}`;
1111
+ const message = atitle(title) + "expected Uint8Array" + ofLen + ", got " + got;
1112
+ if (!bytes)
1113
+ throw new TypeError(message);
1114
+ throw new RangeError(message);
1115
+ }
1116
+ var aobject = (value, label) => {
1117
+ if (value === null || typeof value !== "object" || Array.isArray(value))
1118
+ throw new TypeError((label === "object" ? "" : `"${label}" `) + "expected object, got type=" + typeof value);
1119
+ };
1120
+ var aopts = (value, label) => {
1121
+ aobject(value, label);
1122
+ const proto = Object.getPrototypeOf(value);
1123
+ if (proto !== Object.prototype && proto !== null)
1124
+ throw new TypeError(`"${label}" expected plain object`);
1125
+ if (Object.hasOwn(value, "__proto__"))
1126
+ throw new TypeError(`"${label}.__proto__" is not allowed`);
1127
+ };
1128
+ function aexists(instance, checkFinished = true) {
1129
+ if (instance.destroyed)
1130
+ throw new Error("hash was destroyed");
1131
+ if (checkFinished && instance.finished)
1132
+ throw new Error("digest() was already called");
1133
+ }
1134
+ function aoutput(out, instance) {
1135
+ abytes(out, void 0, "output");
1136
+ const min = instance.outputLen;
1137
+ if (!(out.length >= min)) {
1138
+ throw new RangeError('"output" expected length >= ' + min);
1139
+ }
1140
+ }
1141
+ function clean(...arrays) {
1142
+ for (let i = 0; i < arrays.length; i++) {
1143
+ arrays[i].fill(0);
1144
+ }
1145
+ }
1146
+ function createView(arr) {
1147
+ return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
1148
+ }
1149
+ function rotr(word, shift) {
1150
+ return word << 32 - shift | word >>> shift;
1151
+ }
1152
+ function rotl(word, shift) {
1153
+ return word << shift | word >>> 32 - shift >>> 0;
1154
+ }
1155
+ function checkOpts(defaults, opts, title = "opts") {
1156
+ aopts(defaults, "defaults");
1157
+ if (opts !== void 0)
1158
+ aopts(opts, title);
1159
+ const merged = Object.assign(/* @__PURE__ */ Object.create(null), defaults, opts);
1160
+ return merged;
1161
+ }
1162
+ function createHasher(hashCons, info = {}) {
1163
+ if (typeof hashCons !== "function")
1164
+ throw new TypeError('"hashCons" expected function, got type=' + typeof hashCons);
1165
+ info = checkOpts({}, info, "info");
1166
+ const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
1167
+ const tmp = hashCons(void 0);
1168
+ hashC.outputLen = tmp.outputLen;
1169
+ hashC.blockLen = tmp.blockLen;
1170
+ hashC.canXOF = tmp.canXOF;
1171
+ hashC.create = (opts) => hashCons(opts);
1172
+ Object.assign(hashC, info);
1173
+ return Object.freeze(hashC);
1174
+ }
1175
+ var oidNist = (suffix) => ({
1176
+ // Current NIST hashAlgs suffixes used here fit in one DER subidentifier octet.
1177
+ // Larger suffix values would need base-128 OID encoding and a different length byte.
1178
+ oid: Uint8Array.from([6, 9, 96, 134, 72, 1, 101, 3, 4, 2, suffix])
1179
+ });
1180
+
1181
+ // node_modules/@noble/hashes/_md.js
1182
+ function Chi(a, b, c) {
1183
+ return a & b ^ ~a & c;
1184
+ }
1185
+ function Maj(a, b, c) {
1186
+ return a & b ^ a & c ^ b & c;
1187
+ }
1188
+ var HashMD = class {
1189
+ blockLen;
1190
+ outputLen;
1191
+ canXOF = false;
1192
+ padOffset;
1193
+ isLE;
1194
+ // For partial updates less than block size
1195
+ buffer;
1196
+ view;
1197
+ finished = false;
1198
+ length = 0;
1199
+ pos = 0;
1200
+ destroyed = false;
1201
+ constructor(blockLen, outputLen, padOffset, isLE) {
1202
+ this.blockLen = blockLen;
1203
+ this.outputLen = outputLen;
1204
+ this.padOffset = padOffset;
1205
+ this.isLE = isLE;
1206
+ this.buffer = new Uint8Array(blockLen);
1207
+ this.view = createView(this.buffer);
1208
+ }
1209
+ update(data) {
1210
+ aexists(this);
1211
+ abytes(data);
1212
+ const { view, buffer, blockLen } = this;
1213
+ const len = data.length;
1214
+ let processed = false;
1215
+ for (let pos = 0; pos < len; ) {
1216
+ const take = Math.min(blockLen - this.pos, len - pos);
1217
+ if (take === blockLen) {
1218
+ const dataView = createView(data);
1219
+ for (; blockLen <= len - pos; pos += blockLen)
1220
+ this.process(dataView, pos);
1221
+ processed = true;
1222
+ continue;
1223
+ }
1224
+ buffer.set(pos === 0 && take === len ? data : data.subarray(pos, pos + take), this.pos);
1225
+ this.pos += take;
1226
+ pos += take;
1227
+ if (this.pos === blockLen) {
1228
+ this.process(view, 0);
1229
+ this.pos = 0;
1230
+ processed = true;
1231
+ }
1232
+ }
1233
+ this.length += data.length;
1234
+ if (processed)
1235
+ this.roundClean();
1236
+ return this;
1237
+ }
1238
+ digestInto(out) {
1239
+ aexists(this);
1240
+ aoutput(out, this);
1241
+ this.finished = true;
1242
+ const { buffer, view, blockLen, isLE } = this;
1243
+ let { pos } = this;
1244
+ buffer[pos++] = 128;
1245
+ buffer.fill(0, pos);
1246
+ if (this.padOffset > blockLen - pos) {
1247
+ this.process(view, 0);
1248
+ buffer.fill(0);
1249
+ }
1250
+ setU64FromNum(view, blockLen - 8, this.length * 8, isLE);
1251
+ this.process(view, 0);
1252
+ this.roundClean();
1253
+ const oview = out === buffer ? view : createView(out);
1254
+ const len = this.outputLen;
1255
+ const outLen = len / 4;
1256
+ const state = this.get();
1257
+ if (len % 4 || outLen > state.length)
1258
+ throw new Error("invalid outputLen");
1259
+ for (let i = 0; i < outLen; i++)
1260
+ oview.setUint32(4 * i, state[i], isLE);
1261
+ }
1262
+ digest() {
1263
+ const { buffer, outputLen } = this;
1264
+ this.digestInto(buffer);
1265
+ const res = buffer.slice(0, outputLen);
1266
+ this.destroy();
1267
+ return res;
1268
+ }
1269
+ _cloneIntoMeta(to) {
1270
+ const { buffer, length, finished, destroyed, pos } = this;
1271
+ to.destroyed = destroyed;
1272
+ to.finished = finished;
1273
+ to.length = length;
1274
+ to.pos = pos;
1275
+ if (pos)
1276
+ to.buffer.set(buffer);
1277
+ return to;
1278
+ }
1279
+ clone() {
1280
+ return this._cloneInto();
1281
+ }
1282
+ };
1283
+ var SHA256_IV = /* @__PURE__ */ Uint32Array.from([
1284
+ 1779033703,
1285
+ 3144134277,
1286
+ 1013904242,
1287
+ 2773480762,
1288
+ 1359893119,
1289
+ 2600822924,
1290
+ 528734635,
1291
+ 1541459225
1292
+ ]);
1293
+
1294
+ // node_modules/@noble/hashes/legacy.js
1295
+ var SHA1_IV = /* @__PURE__ */ Uint32Array.from([
1296
+ 1732584193,
1297
+ 4023233417,
1298
+ 2562383102,
1299
+ 271733878,
1300
+ 3285377520
1301
+ ]);
1302
+ var p32 = /* @__PURE__ */ Math.pow(2, 32);
1303
+ var K = /* @__PURE__ */ Array.from({ length: 64 }, (_, i) => Math.floor(p32 * Math.abs(Math.sin(i + 1))));
1304
+ var MD5_IV = /* @__PURE__ */ SHA1_IV.slice(0, 4);
1305
+ var MD5_W = /* @__PURE__ */ new Uint32Array(16);
1306
+ var MD5_SHIFTS = /* @__PURE__ */ (() => {
1307
+ const S = [
1308
+ [7, 12, 17, 22],
1309
+ [5, 9, 14, 20],
1310
+ [4, 11, 16, 23],
1311
+ [6, 10, 15, 21]
1312
+ ];
1313
+ return Uint8Array.from({ length: 64 }, (_, i) => S[Math.floor(i / 16)][i % 4]);
1314
+ })();
1315
+ var _MD5 = class extends HashMD {
1316
+ A = MD5_IV[0] | 0;
1317
+ B = MD5_IV[1] | 0;
1318
+ C = MD5_IV[2] | 0;
1319
+ D = MD5_IV[3] | 0;
1320
+ constructor() {
1321
+ super(64, 16, 8, true);
1322
+ }
1323
+ get() {
1324
+ const { A, B, C, D } = this;
1325
+ return [A, B, C, D];
1326
+ }
1327
+ set(A, B, C, D) {
1328
+ this.A = A | 0;
1329
+ this.B = B | 0;
1330
+ this.C = C | 0;
1331
+ this.D = D | 0;
1332
+ }
1333
+ _cloneInto(to) {
1334
+ (to ||= new this.constructor()).set(...this.get());
1335
+ return this._cloneIntoMeta(to);
1336
+ }
1337
+ process(view, offset) {
1338
+ for (let i = 0; i < 16; i++, offset += 4)
1339
+ MD5_W[i] = view.getUint32(offset, true);
1340
+ let { A, B, C, D } = this;
1341
+ for (let i = 0; i < 64; i++) {
1342
+ let F, g;
1343
+ if (i < 16) {
1344
+ F = Chi(B, C, D);
1345
+ g = i;
1346
+ } else if (i < 32) {
1347
+ F = Chi(D, B, C);
1348
+ g = (5 * i + 1) % 16;
1349
+ } else if (i < 48) {
1350
+ F = B ^ C ^ D;
1351
+ g = (3 * i + 5) % 16;
1352
+ } else {
1353
+ F = C ^ (B | ~D);
1354
+ g = 7 * i % 16;
1355
+ }
1356
+ F = F + A + K[i] + MD5_W[g];
1357
+ A = D;
1358
+ D = C;
1359
+ C = B;
1360
+ B = B + rotl(F, MD5_SHIFTS[i]);
1361
+ }
1362
+ A = A + this.A | 0;
1363
+ B = B + this.B | 0;
1364
+ C = C + this.C | 0;
1365
+ D = D + this.D | 0;
1366
+ this.set(A, B, C, D);
1367
+ }
1368
+ roundClean() {
1369
+ clean(MD5_W);
1370
+ }
1371
+ destroy() {
1372
+ this.destroyed = true;
1373
+ this.set(0, 0, 0, 0);
1374
+ clean(this.buffer);
1375
+ }
1376
+ };
1377
+ var md5 = /* @__PURE__ */ createHasher(() => new _MD5());
1378
+
1379
+ // node_modules/@noble/hashes/sha2.js
1380
+ var SHA256_K = /* @__PURE__ */ Uint32Array.from([
1381
+ 1116352408,
1382
+ 1899447441,
1383
+ 3049323471,
1384
+ 3921009573,
1385
+ 961987163,
1386
+ 1508970993,
1387
+ 2453635748,
1388
+ 2870763221,
1389
+ 3624381080,
1390
+ 310598401,
1391
+ 607225278,
1392
+ 1426881987,
1393
+ 1925078388,
1394
+ 2162078206,
1395
+ 2614888103,
1396
+ 3248222580,
1397
+ 3835390401,
1398
+ 4022224774,
1399
+ 264347078,
1400
+ 604807628,
1401
+ 770255983,
1402
+ 1249150122,
1403
+ 1555081692,
1404
+ 1996064986,
1405
+ 2554220882,
1406
+ 2821834349,
1407
+ 2952996808,
1408
+ 3210313671,
1409
+ 3336571891,
1410
+ 3584528711,
1411
+ 113926993,
1412
+ 338241895,
1413
+ 666307205,
1414
+ 773529912,
1415
+ 1294757372,
1416
+ 1396182291,
1417
+ 1695183700,
1418
+ 1986661051,
1419
+ 2177026350,
1420
+ 2456956037,
1421
+ 2730485921,
1422
+ 2820302411,
1423
+ 3259730800,
1424
+ 3345764771,
1425
+ 3516065817,
1426
+ 3600352804,
1427
+ 4094571909,
1428
+ 275423344,
1429
+ 430227734,
1430
+ 506948616,
1431
+ 659060556,
1432
+ 883997877,
1433
+ 958139571,
1434
+ 1322822218,
1435
+ 1537002063,
1436
+ 1747873779,
1437
+ 1955562222,
1438
+ 2024104815,
1439
+ 2227730452,
1440
+ 2361852424,
1441
+ 2428436474,
1442
+ 2756734187,
1443
+ 3204031479,
1444
+ 3329325298
1445
+ ]);
1446
+ var SHA256_W = /* @__PURE__ */ new Uint32Array(64);
1447
+ var SHA2_32B = class extends HashMD {
1448
+ // We cannot use array here since array allows indexing by variable
1449
+ // which means optimizer/compiler cannot use registers.
1450
+ // Numeric initializers matter: starting the fields as `undefined` changes
1451
+ // V8's field representation and makes sha256 3x slower (measured).
1452
+ A = 0;
1453
+ B = 0;
1454
+ C = 0;
1455
+ D = 0;
1456
+ E = 0;
1457
+ F = 0;
1458
+ G = 0;
1459
+ H = 0;
1460
+ constructor(outputLen, IV) {
1461
+ super(64, outputLen, 8, false);
1462
+ this.A = IV[0] | 0;
1463
+ this.B = IV[1] | 0;
1464
+ this.C = IV[2] | 0;
1465
+ this.D = IV[3] | 0;
1466
+ this.E = IV[4] | 0;
1467
+ this.F = IV[5] | 0;
1468
+ this.G = IV[6] | 0;
1469
+ this.H = IV[7] | 0;
1470
+ }
1471
+ get() {
1472
+ const { A, B, C, D, E, F, G, H } = this;
1473
+ return [A, B, C, D, E, F, G, H];
1474
+ }
1475
+ // prettier-ignore
1476
+ set(A, B, C, D, E, F, G, H) {
1477
+ this.A = A | 0;
1478
+ this.B = B | 0;
1479
+ this.C = C | 0;
1480
+ this.D = D | 0;
1481
+ this.E = E | 0;
1482
+ this.F = F | 0;
1483
+ this.G = G | 0;
1484
+ this.H = H | 0;
1485
+ }
1486
+ _cloneInto(to) {
1487
+ (to ||= new this.constructor()).set(...this.get());
1488
+ return this._cloneIntoMeta(to);
1489
+ }
1490
+ process(view, offset) {
1491
+ for (let i = 0; i < 16; i++, offset += 4)
1492
+ SHA256_W[i] = view.getUint32(offset, false);
1493
+ for (let i = 16; i < 64; i++) {
1494
+ const W15 = SHA256_W[i - 15];
1495
+ const W2 = SHA256_W[i - 2];
1496
+ const s0 = rotr(W15, 7) ^ rotr(W15, 18) ^ W15 >>> 3;
1497
+ const s1 = rotr(W2, 17) ^ rotr(W2, 19) ^ W2 >>> 10;
1498
+ SHA256_W[i] = s1 + SHA256_W[i - 7] + s0 + SHA256_W[i - 16] | 0;
1499
+ }
1500
+ let { A, B, C, D, E, F, G, H } = this;
1501
+ for (let i = 0; i < 64; i++) {
1502
+ const sigma1 = rotr(E, 6) ^ rotr(E, 11) ^ rotr(E, 25);
1503
+ const T1 = H + sigma1 + Chi(E, F, G) + SHA256_K[i] + SHA256_W[i] | 0;
1504
+ const sigma0 = rotr(A, 2) ^ rotr(A, 13) ^ rotr(A, 22);
1505
+ const T2 = sigma0 + Maj(A, B, C) | 0;
1506
+ H = G;
1507
+ G = F;
1508
+ F = E;
1509
+ E = D + T1 | 0;
1510
+ D = C;
1511
+ C = B;
1512
+ B = A;
1513
+ A = T1 + T2 | 0;
1514
+ }
1515
+ A = A + this.A | 0;
1516
+ B = B + this.B | 0;
1517
+ C = C + this.C | 0;
1518
+ D = D + this.D | 0;
1519
+ E = E + this.E | 0;
1520
+ F = F + this.F | 0;
1521
+ G = G + this.G | 0;
1522
+ H = H + this.H | 0;
1523
+ this.set(A, B, C, D, E, F, G, H);
1524
+ }
1525
+ roundClean() {
1526
+ clean(SHA256_W);
1527
+ }
1528
+ destroy() {
1529
+ this.destroyed = true;
1530
+ this.set(0, 0, 0, 0, 0, 0, 0, 0);
1531
+ clean(this.buffer);
1532
+ }
1533
+ };
1534
+ var _SHA256 = class extends SHA2_32B {
1535
+ constructor() {
1536
+ super(32, SHA256_IV);
1537
+ }
1538
+ };
1539
+ var sha256 = /* @__PURE__ */ createHasher(
1540
+ () => new _SHA256(),
1541
+ /* @__PURE__ */ oidNist(1)
1542
+ );
1543
+
1544
+ // packages/schema/src/hash.ts
1545
+ var encoder = new TextEncoder();
1546
+ function sha256Hex(input) {
1547
+ return toHex(sha256(encoder.encode(input)));
1548
+ }
1549
+ function md5Hex(input) {
1550
+ return toHex(md5(encoder.encode(input)));
1551
+ }
1552
+ function toHex(bytes) {
1553
+ let hex = "";
1554
+ for (const byte of bytes) hex += byte.toString(16).padStart(2, "0");
1555
+ return hex;
1556
+ }
1557
+
910
1558
  // packages/schema/src/identity.ts
911
- import { createHash } from "node:crypto";
912
1559
  var ID_LENGTH = 16;
913
1560
  function normalizeName(name) {
914
1561
  return name.normalize("NFC");
915
1562
  }
916
- function pathSegments(path9) {
1563
+ function pathSegments(path7) {
917
1564
  const segments = [];
918
- for (const raw of path9.replace(/\\/g, "/").split("/")) {
1565
+ for (const raw of path7.replace(/\\/g, "/").split("/")) {
919
1566
  const segment = normalizeName(raw);
920
1567
  if (segment === "" || segment === ".") continue;
921
1568
  if (segment === "..") {
@@ -929,18 +1576,18 @@ function pathSegments(path9) {
929
1576
  function canonicalPath(segments) {
930
1577
  return `/${segments.map(normalizeName).join("/")}`;
931
1578
  }
932
- function idFromCanonicalPath(path9, type = "File") {
933
- const canonical = path9.startsWith("/") ? path9 : `/${path9}`;
934
- return createHash("sha256").update(type + canonical, "utf8").digest("hex").slice(0, ID_LENGTH);
1579
+ function idFromCanonicalPath(path7, type = "File") {
1580
+ const canonical = path7.startsWith("/") ? path7 : `/${path7}`;
1581
+ return sha256Hex(type + canonical).slice(0, ID_LENGTH);
935
1582
  }
936
1583
  function nodeId(type, segments) {
937
1584
  return idFromCanonicalPath(canonicalPath(segments), type);
938
1585
  }
939
- function fileId(path9) {
940
- return nodeId("File", pathSegments(path9));
1586
+ function fileId(path7) {
1587
+ return nodeId("File", pathSegments(path7));
941
1588
  }
942
- function folderId(path9) {
943
- return nodeId("Folder", pathSegments(path9));
1589
+ function folderId(path7) {
1590
+ return nodeId("Folder", pathSegments(path7));
944
1591
  }
945
1592
 
946
1593
  // packages/schema/src/legacy.ts
@@ -996,15 +1643,15 @@ function normalizeLegacyCcJson(input) {
996
1643
  const metrics = {};
997
1644
  const built = [];
998
1645
  const ids = /* @__PURE__ */ new Map();
999
- const buildNode = (value, parentPath, path9, depth) => {
1646
+ const buildNode = (value, parentPath, path7, depth) => {
1000
1647
  if (depth > MAX_DEPTH) {
1001
- throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH} superada`, path9);
1648
+ throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH} superada`, path7);
1002
1649
  }
1003
- const raw = requireRecord(value, path9);
1004
- const name = readNodeName(raw.name, `${path9}.name`);
1650
+ const raw = requireRecord(value, path7);
1651
+ const name = readNodeName(raw.name, `${path7}.name`);
1005
1652
  const type = raw.type;
1006
1653
  if (type !== "File" && type !== "Folder") {
1007
- throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path9}.type`);
1654
+ throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path7}.type`);
1008
1655
  }
1009
1656
  const fullPath = [...parentPath, name];
1010
1657
  const id = nodeId(type, fullPath.slice(1));
@@ -1012,13 +1659,13 @@ function normalizeLegacyCcJson(input) {
1012
1659
  if (previous !== void 0) {
1013
1660
  throw new CcJsonError(
1014
1661
  `id duplicado "${id}" para "${previous}" y "${fullPath.join("/")}"`,
1015
- `${path9}.name`
1662
+ `${path7}.name`
1016
1663
  );
1017
1664
  }
1018
1665
  ids.set(id, fullPath.join("/"));
1019
1666
  built.push({ id, name, fullPath: fullPath.join("/") });
1020
1667
  const node = { id, name, type };
1021
- const attributes = readMetrics(raw.attributes, `${path9}.attributes`);
1668
+ const attributes = readMetrics(raw.attributes, `${path7}.attributes`);
1022
1669
  if (attributes) metrics[id] = attributes;
1023
1670
  const contentHash = readOptionalString(raw.checksum) ?? readOptionalString(raw.contentHash);
1024
1671
  if (contentHash) node.contentHash = contentHash;
@@ -1026,14 +1673,14 @@ function normalizeLegacyCcJson(input) {
1026
1673
  if (link) node.link = link;
1027
1674
  if (raw.children !== void 0) {
1028
1675
  if (!Array.isArray(raw.children)) {
1029
- throw new CcJsonError('"children" debe ser un array', `${path9}.children`);
1676
+ throw new CcJsonError('"children" debe ser un array', `${path7}.children`);
1030
1677
  }
1031
1678
  if (type === "File" && raw.children.length > 0) {
1032
- throw new CcJsonError("un fichero no puede tener hijos", `${path9}.children`);
1679
+ throw new CcJsonError("un fichero no puede tener hijos", `${path7}.children`);
1033
1680
  }
1034
1681
  if (type === "Folder" && raw.children.length > 0) {
1035
1682
  node.children = raw.children.map(
1036
- (child, index) => buildNode(child, fullPath, `${path9}.children[${index}]`, depth + 1)
1683
+ (child, index) => buildNode(child, fullPath, `${path7}.children[${index}]`, depth + 1)
1037
1684
  );
1038
1685
  }
1039
1686
  }
@@ -1050,15 +1697,15 @@ function normalizeLegacyCcJson(input) {
1050
1697
  throw new CcJsonError('"edges" debe ser un array', `${base}.edges`);
1051
1698
  }
1052
1699
  edgesRaw.forEach((edge, index) => {
1053
- const path9 = `${base}.edges[${index}]`;
1054
- const raw = requireRecord(edge, path9);
1055
- const fromName = requireString(raw.fromNodeName, `${path9}.fromNodeName`, "fromNodeName");
1056
- const toName = requireString(raw.toNodeName, `${path9}.toNodeName`, "toNodeName");
1700
+ const path7 = `${base}.edges[${index}]`;
1701
+ const raw = requireRecord(edge, path7);
1702
+ const fromName = requireString(raw.fromNodeName, `${path7}.fromNodeName`, "fromNodeName");
1703
+ const toName = requireString(raw.toNodeName, `${path7}.toNodeName`, "toNodeName");
1057
1704
  const normalized = {
1058
- fromId: resolveEndpoint(fromName, pathIndex, stripNames, `${path9}.fromNodeName`),
1059
- toId: resolveEndpoint(toName, pathIndex, stripNames, `${path9}.toNodeName`)
1705
+ fromId: resolveEndpoint(fromName, pathIndex, stripNames, `${path7}.fromNodeName`),
1706
+ toId: resolveEndpoint(toName, pathIndex, stripNames, `${path7}.toNodeName`)
1060
1707
  };
1061
- const attributes = readMetrics(raw.attributes, `${path9}.attributes`);
1708
+ const attributes = readMetrics(raw.attributes, `${path7}.attributes`);
1062
1709
  if (attributes) normalized.attributes = attributes;
1063
1710
  if (typeof raw.isCyclic === "boolean") normalized.isCyclic = raw.isCyclic;
1064
1711
  if (typeof raw.isPointingUpwards === "boolean") {
@@ -1128,10 +1775,10 @@ function buildPathIndex(built, multipleRoots) {
1128
1775
  for (const node of built) {
1129
1776
  const segments = node.fullPath.split("/");
1130
1777
  if (segments.length > 1) add(segments.slice(1).join("/"), node.id);
1131
- const relative = segments.slice(multipleRoots ? 2 : 1).join("/");
1132
- if (relative !== "") {
1133
- add(relative, node.id);
1134
- add(`root/${relative}`, node.id);
1778
+ const relative2 = segments.slice(multipleRoots ? 2 : 1).join("/");
1779
+ if (relative2 !== "") {
1780
+ add(relative2, node.id);
1781
+ add(`root/${relative2}`, node.id);
1135
1782
  } else if (segments.length === 1) {
1136
1783
  add("root", node.id);
1137
1784
  }
@@ -1143,10 +1790,10 @@ function buildPathIndex(built, multipleRoots) {
1143
1790
  }
1144
1791
  return index;
1145
1792
  }
1146
- function resolveEndpoint(endpoint, index, stripNames, path9) {
1793
+ function resolveEndpoint(endpoint, index, stripNames, path7) {
1147
1794
  const segments = canonicalSegments(endpoint);
1148
1795
  if (segments.length === 0) {
1149
- throw new CcJsonError("extremo de arista vac\xEDo", path9);
1796
+ throw new CcJsonError("extremo de arista vac\xEDo", path7);
1150
1797
  }
1151
1798
  const candidates = [segments.join("/")];
1152
1799
  if (segments.length > 1 && stripNames.has(segments[0])) {
@@ -1158,7 +1805,7 @@ function resolveEndpoint(endpoint, index, stripNames, path9) {
1158
1805
  }
1159
1806
  const bare = index.get(segments[segments.length - 1]);
1160
1807
  if (bare) return bare;
1161
- throw new CcJsonError(`no se pudo resolver el nodo "${endpoint}"`, path9);
1808
+ throw new CcJsonError(`no se pudo resolver el nodo "${endpoint}"`, path7);
1162
1809
  }
1163
1810
  function canonicalSegments(value) {
1164
1811
  const result = [];
@@ -1173,85 +1820,85 @@ function canonicalSegments(value) {
1173
1820
  }
1174
1821
  return result;
1175
1822
  }
1176
- function readNodeName(value, path9) {
1177
- const name = normalizeName(requireString(value, path9, "name"));
1823
+ function readNodeName(value, path7) {
1824
+ const name = normalizeName(requireString(value, path7, "name"));
1178
1825
  if (name === "." || name === "..") {
1179
- throw new CcJsonError('el nombre no puede ser "." ni ".."', path9);
1826
+ throw new CcJsonError('el nombre no puede ser "." ni ".."', path7);
1180
1827
  }
1181
1828
  if (name.includes("/")) {
1182
- throw new CcJsonError('el nombre no puede contener "/"', path9);
1829
+ throw new CcJsonError('el nombre no puede contener "/"', path7);
1183
1830
  }
1184
1831
  return name;
1185
1832
  }
1186
1833
  function readOptionalString(value) {
1187
1834
  return typeof value === "string" && value.length > 0 ? value : void 0;
1188
1835
  }
1189
- function readMetrics(value, path9) {
1836
+ function readMetrics(value, path7) {
1190
1837
  if (value === void 0) return void 0;
1191
- const raw = requireRecord(value, path9);
1838
+ const raw = requireRecord(value, path7);
1192
1839
  const out = {};
1193
1840
  for (const [metric, metricValue] of Object.entries(raw)) {
1194
1841
  if (typeof metricValue !== "number" || !Number.isFinite(metricValue)) {
1195
1842
  throw new CcJsonError(
1196
1843
  `la m\xE9trica "${metric}" debe ser un n\xFAmero finito`,
1197
- `${path9}["${metric}"]`
1844
+ `${path7}["${metric}"]`
1198
1845
  );
1199
1846
  }
1200
1847
  out[metric] = metricValue;
1201
1848
  }
1202
1849
  return Object.keys(out).length > 0 ? out : void 0;
1203
1850
  }
1204
- function readAttributeTypes(value, path9) {
1851
+ function readAttributeTypes(value, path7) {
1205
1852
  if (value === void 0) return {};
1206
- const raw = requireRecord(value, path9);
1853
+ const raw = requireRecord(value, path7);
1207
1854
  if ("nodes" in raw || "edges" in raw) {
1208
- const nodes2 = raw.nodes === void 0 ? void 0 : readTypeMap(raw.nodes, `${path9}.nodes`);
1209
- const edges = raw.edges === void 0 ? void 0 : readTypeMap(raw.edges, `${path9}.edges`);
1855
+ const nodes2 = raw.nodes === void 0 ? void 0 : readTypeMap(raw.nodes, `${path7}.nodes`);
1856
+ const edges = raw.edges === void 0 ? void 0 : readTypeMap(raw.edges, `${path7}.edges`);
1210
1857
  return {
1211
1858
  ...nodes2 && Object.keys(nodes2).length > 0 ? { nodes: nodes2 } : {},
1212
1859
  ...edges && Object.keys(edges).length > 0 ? { edges } : {}
1213
1860
  };
1214
1861
  }
1215
- const nodes = readTypeMap(raw, path9);
1862
+ const nodes = readTypeMap(raw, path7);
1216
1863
  return Object.keys(nodes).length > 0 ? { nodes } : {};
1217
1864
  }
1218
- function readTypeMap(value, path9) {
1219
- const raw = requireRecord(value, path9);
1865
+ function readTypeMap(value, path7) {
1866
+ const raw = requireRecord(value, path7);
1220
1867
  const out = {};
1221
1868
  for (const [metric, type] of Object.entries(raw)) {
1222
1869
  if (type !== "absolute" && type !== "relative") {
1223
1870
  throw new CcJsonError(
1224
1871
  `el tipo de "${metric}" debe ser "absolute" o "relative"`,
1225
- `${path9}["${metric}"]`
1872
+ `${path7}["${metric}"]`
1226
1873
  );
1227
1874
  }
1228
1875
  out[metric] = type;
1229
1876
  }
1230
1877
  return out;
1231
1878
  }
1232
- function readDescriptors(value, path9) {
1879
+ function readDescriptors(value, path7) {
1233
1880
  if (value === void 0) return {};
1234
- const raw = requireRecord(value, path9);
1881
+ const raw = requireRecord(value, path7);
1235
1882
  if ("nodes" in raw || "edges" in raw) {
1236
- const nodes = raw.nodes === void 0 ? void 0 : readDescriptorMap(raw.nodes, `${path9}.nodes`);
1237
- const edges = raw.edges === void 0 ? void 0 : readDescriptorMap(raw.edges, `${path9}.edges`);
1883
+ const nodes = raw.nodes === void 0 ? void 0 : readDescriptorMap(raw.nodes, `${path7}.nodes`);
1884
+ const edges = raw.edges === void 0 ? void 0 : readDescriptorMap(raw.edges, `${path7}.edges`);
1238
1885
  return {
1239
1886
  ...nodes && Object.keys(nodes).length > 0 ? { nodes } : {},
1240
1887
  ...edges && Object.keys(edges).length > 0 ? { edges } : {}
1241
1888
  };
1242
1889
  }
1243
- const flat = readDescriptorMap(raw, path9);
1890
+ const flat = readDescriptorMap(raw, path7);
1244
1891
  return Object.keys(flat).length > 0 ? { flat } : {};
1245
1892
  }
1246
- function readDescriptorMap(value, path9) {
1247
- const raw = requireRecord(value, path9);
1893
+ function readDescriptorMap(value, path7) {
1894
+ const raw = requireRecord(value, path7);
1248
1895
  const out = {};
1249
1896
  for (const [metric, descriptor] of Object.entries(raw)) {
1250
- const record = requireRecord(descriptor, `${path9}["${metric}"]`);
1897
+ const record = requireRecord(descriptor, `${path7}["${metric}"]`);
1251
1898
  if (record.direction !== void 0 && typeof record.direction !== "number") {
1252
1899
  throw new CcJsonError(
1253
1900
  `"direction" de "${metric}" debe ser un n\xFAmero`,
1254
- `${path9}["${metric}"].direction`
1901
+ `${path7}["${metric}"].direction`
1255
1902
  );
1256
1903
  }
1257
1904
  out[metric] = record;
@@ -1276,26 +1923,26 @@ function parseCcJson2(input) {
1276
1923
  const roots = Array.isArray(filesRaw) ? filesRaw : [filesRaw];
1277
1924
  if (roots.length === 0) throw new CcJsonError("el \xE1rbol est\xE1 vac\xEDo", "$.files");
1278
1925
  const ids = /* @__PURE__ */ new Set();
1279
- const validateNode = (value, path9, depth) => {
1280
- if (depth > MAX_DEPTH2) throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH2} superada`, path9);
1281
- const raw = requireRecord(value, path9);
1282
- const id = requireString(raw.id, `${path9}.id`, "id");
1283
- if (ids.has(id)) throw new CcJsonError(`id duplicado "${id}"`, `${path9}.id`);
1926
+ const validateNode = (value, path7, depth) => {
1927
+ if (depth > MAX_DEPTH2) throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH2} superada`, path7);
1928
+ const raw = requireRecord(value, path7);
1929
+ const id = requireString(raw.id, `${path7}.id`, "id");
1930
+ if (ids.has(id)) throw new CcJsonError(`id duplicado "${id}"`, `${path7}.id`);
1284
1931
  ids.add(id);
1285
- const name = requireString(raw.name, `${path9}.name`, "name");
1932
+ const name = requireString(raw.name, `${path7}.name`, "name");
1286
1933
  const type = raw.type;
1287
1934
  if (type !== "File" && type !== "Folder") {
1288
- throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path9}.type`);
1935
+ throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path7}.type`);
1289
1936
  }
1290
1937
  const node = { id, name, type };
1291
1938
  if (typeof raw.contentHash === "string") node.contentHash = raw.contentHash;
1292
1939
  if (typeof raw.link === "string") node.link = raw.link;
1293
1940
  if (raw.children !== void 0) {
1294
1941
  if (!Array.isArray(raw.children)) {
1295
- throw new CcJsonError('"children" debe ser un array', `${path9}.children`);
1942
+ throw new CcJsonError('"children" debe ser un array', `${path7}.children`);
1296
1943
  }
1297
1944
  node.children = raw.children.map(
1298
- (child, index) => validateNode(child, `${path9}.children[${index}]`, depth + 1)
1945
+ (child, index) => validateNode(child, `${path7}.children[${index}]`, depth + 1)
1299
1946
  );
1300
1947
  }
1301
1948
  return node;
@@ -1326,10 +1973,10 @@ function parseCcJson2(input) {
1326
1973
  throw new CcJsonError('"edges" debe ser un array', "$.lenses.dependency.edges");
1327
1974
  }
1328
1975
  dependency.edges.forEach((edge, index) => {
1329
- const path9 = `$.lenses.dependency.edges[${index}]`;
1330
- const raw = requireRecord(edge, path9);
1331
- requireString(raw.fromId, `${path9}.fromId`, "fromId");
1332
- requireString(raw.toId, `${path9}.toId`, "toId");
1976
+ const path7 = `$.lenses.dependency.edges[${index}]`;
1977
+ const raw = requireRecord(edge, path7);
1978
+ requireString(raw.fromId, `${path7}.fromId`, "fromId");
1979
+ requireString(raw.toId, `${path7}.toId`, "toId");
1333
1980
  });
1334
1981
  }
1335
1982
  }
@@ -1418,9 +2065,9 @@ function topByMetric(cc, metric, limit = 10) {
1418
2065
  const ranked = [];
1419
2066
  for (const [id, values] of Object.entries(attributes)) {
1420
2067
  const value = values[metric];
1421
- const path9 = paths.get(id);
1422
- if (value === void 0 || path9 === void 0) continue;
1423
- ranked.push({ id, path: path9, value });
2068
+ const path7 = paths.get(id);
2069
+ if (value === void 0 || path7 === void 0) continue;
2070
+ ranked.push({ id, path: path7, value });
1424
2071
  }
1425
2072
  ranked.sort((a, b) => b.value - a.value || a.path.localeCompare(b.path));
1426
2073
  return ranked.slice(0, limit);
@@ -1430,155 +2077,6 @@ function defaultMetric(stats) {
1430
2077
  return stats.metricCoverage[0]?.name;
1431
2078
  }
1432
2079
 
1433
- // packages/parsers/src/git.ts
1434
- import { spawnSync } from "node:child_process";
1435
- import fs2 from "node:fs";
1436
- import path2 from "node:path";
1437
- var COMMIT_PREFIX = "__CODARCH_COMMIT__";
1438
- var DEFAULT_MAX_BUFFER = 128 * 1024 * 1024;
1439
- var EMPTY = {
1440
- metrics: /* @__PURE__ */ new Map(),
1441
- commits: 0,
1442
- authors: 0,
1443
- shallow: false,
1444
- enabled: false,
1445
- warnings: []
1446
- };
1447
- function collectGitChurn(rootInput, files, options = {}) {
1448
- if (options.enabled === false || files.length === 0) {
1449
- return { ...EMPTY, metrics: /* @__PURE__ */ new Map() };
1450
- }
1451
- const root = realPath(rootInput);
1452
- const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
1453
- const warnings = [];
1454
- const topLevel = runGit(root, ["rev-parse", "--show-toplevel"], maxBuffer);
1455
- if (!topLevel.ok) {
1456
- return {
1457
- ...EMPTY,
1458
- metrics: /* @__PURE__ */ new Map(),
1459
- warnings: [`no hay historial git utilizable: ${topLevel.error}`]
1460
- };
1461
- }
1462
- const repoRoot = realPath(topLevel.stdout.trim());
1463
- const shallowResult = runGit(
1464
- repoRoot,
1465
- ["rev-parse", "--is-shallow-repository"],
1466
- maxBuffer
1467
- );
1468
- const shallow = shallowResult.ok && shallowResult.stdout.trim().toLowerCase() === "true";
1469
- if (shallow) {
1470
- warnings.push(
1471
- "el repositorio es shallow: el churn puede estar incompleto"
1472
- );
1473
- }
1474
- const log = runGit(
1475
- repoRoot,
1476
- [
1477
- "-c",
1478
- "core.quotePath=false",
1479
- "log",
1480
- "--no-merges",
1481
- "--numstat",
1482
- `--format=${COMMIT_PREFIX}%H%x09%ae`
1483
- ],
1484
- maxBuffer
1485
- );
1486
- if (!log.ok) {
1487
- return {
1488
- ...EMPTY,
1489
- shallow,
1490
- warnings: [...warnings, `no se pudo leer git log: ${log.error}`]
1491
- };
1492
- }
1493
- const wanted = new Set(files);
1494
- const commitsByFile = /* @__PURE__ */ new Map();
1495
- const authorsByFile = /* @__PURE__ */ new Map();
1496
- const authors = /* @__PURE__ */ new Set();
1497
- let commits = 0;
1498
- let currentAuthor = "";
1499
- let touched = /* @__PURE__ */ new Set();
1500
- const flushCommit = () => {
1501
- if (!currentAuthor || touched.size === 0) return;
1502
- commits++;
1503
- authors.add(currentAuthor);
1504
- for (const file of touched) {
1505
- commitsByFile.set(file, (commitsByFile.get(file) ?? 0) + 1);
1506
- let fileAuthors = authorsByFile.get(file);
1507
- if (!fileAuthors) {
1508
- fileAuthors = /* @__PURE__ */ new Set();
1509
- authorsByFile.set(file, fileAuthors);
1510
- }
1511
- fileAuthors.add(currentAuthor);
1512
- }
1513
- touched = /* @__PURE__ */ new Set();
1514
- currentAuthor = "";
1515
- };
1516
- for (const rawLine of log.stdout.split(/\r?\n/)) {
1517
- if (rawLine.startsWith(COMMIT_PREFIX)) {
1518
- flushCommit();
1519
- const fields2 = rawLine.slice(COMMIT_PREFIX.length).split(" ");
1520
- const author = fields2[1]?.trim() ?? "";
1521
- currentAuthor = author || "(sin autor)";
1522
- continue;
1523
- }
1524
- if (!currentAuthor || rawLine === "") continue;
1525
- const fields = rawLine.split(" ");
1526
- if (fields.length < 3) continue;
1527
- const gitPath = fields.slice(2).join(" ");
1528
- if (!gitPath) continue;
1529
- const relative = path2.relative(root, path2.resolve(repoRoot, gitPath));
1530
- if (relative === "" || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
1531
- continue;
1532
- }
1533
- const normalized = relative.split(path2.sep).join("/");
1534
- if (!wanted.has(normalized)) continue;
1535
- touched.add(normalized);
1536
- }
1537
- flushCommit();
1538
- const metrics = /* @__PURE__ */ new Map();
1539
- for (const file of files) {
1540
- metrics.set(file, {
1541
- commits: commitsByFile.get(file) ?? 0,
1542
- authors: authorsByFile.get(file)?.size ?? 0
1543
- });
1544
- }
1545
- return {
1546
- metrics,
1547
- commits,
1548
- authors: authors.size,
1549
- shallow,
1550
- enabled: true,
1551
- warnings
1552
- };
1553
- }
1554
- function realPath(input) {
1555
- const resolved = path2.resolve(input);
1556
- try {
1557
- return fs2.realpathSync.native(resolved);
1558
- } catch {
1559
- return resolved;
1560
- }
1561
- }
1562
- function runGit(cwd, args, maxBuffer) {
1563
- const result = spawnSync("git", args, {
1564
- cwd,
1565
- encoding: "utf8",
1566
- maxBuffer,
1567
- windowsHide: true
1568
- });
1569
- if (result.error) {
1570
- return { ok: false, stdout: "", error: result.error.message };
1571
- }
1572
- if (result.status !== 0) {
1573
- return {
1574
- ok: false,
1575
- stdout: result.stdout ?? "",
1576
- error: (result.stderr ?? "").trim() || `git exited with ${result.status}`
1577
- };
1578
- }
1579
- return { ok: true, stdout: result.stdout ?? "", error: "" };
1580
- }
1581
-
1582
2080
  // packages/parsers/src/migration.ts
1583
2081
  var EXTENSION_LANGUAGE = {
1584
2082
  ".ts": "ts",
@@ -1597,8 +2095,8 @@ var MIGRATION_EXTENSIONS = Object.keys(EXTENSION_LANGUAGE);
1597
2095
  var LANGUAGE_ORDER = ["ts", "js", "rust", "wasm", "node"];
1598
2096
  var RUST_BRIDGE = /#\[\s*(?:napi|wasm_bindgen)(?:\(|\])|\bnapi_derive\b|\bnapi::|\bwasm_bindgen(?:::|\b)|\bwasm-pack\b/;
1599
2097
  var JS_FFI = /\bWebAssembly\s*\.\s*(?:instantiate|compile|Module|Instance)\b|\bprocess\.dlopen\b/;
1600
- function languageForPath(relativePath2) {
1601
- const lower = relativePath2.toLowerCase();
2098
+ function languageForPath(relativePath3) {
2099
+ const lower = relativePath3.toLowerCase();
1602
2100
  const dot = lower.lastIndexOf(".");
1603
2101
  if (dot === -1) return null;
1604
2102
  return EXTENSION_LANGUAGE[lower.slice(dot)] ?? null;
@@ -1722,9 +2220,9 @@ function jsIsBridge(source, specifiers) {
1722
2220
  return specifiers.some((specifier) => ffiKindForSpecifier(specifier) !== null) || JS_FFI.test(source);
1723
2221
  }
1724
2222
  function ffiKindForSpecifier(specifier) {
1725
- const clean = specifier.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
1726
- if (clean.endsWith(".wasm")) return "wasm";
1727
- if (clean.endsWith(".node")) return "native";
2223
+ const clean2 = specifier.split(/[?#]/, 1)[0]?.toLowerCase() ?? "";
2224
+ if (clean2.endsWith(".wasm")) return "wasm";
2225
+ if (clean2.endsWith(".node")) return "native";
1728
2226
  return null;
1729
2227
  }
1730
2228
  function buildMigration(files, links) {
@@ -1814,7 +2312,6 @@ function compareStrings2(left, right) {
1814
2312
  }
1815
2313
 
1816
2314
  // packages/parsers/src/snapshot.ts
1817
- import { createHash as createHash2 } from "node:crypto";
1818
2315
  var METRIC_DESCRIPTORS = {
1819
2316
  loc: {
1820
2317
  title: "L\xEDneas",
@@ -1989,8 +2486,8 @@ function buildSnapshot(input) {
1989
2486
  isCyclic: edge.isCyclic
1990
2487
  }));
1991
2488
  const dependencyNodes = {};
1992
- for (const [path9, metrics] of input.graph.metrics) {
1993
- dependencyNodes[fileId(path9)] = { level: metrics.dependencyLevel };
2489
+ for (const [path7, metrics] of input.graph.metrics) {
2490
+ dependencyNodes[fileId(path7)] = { level: metrics.dependencyLevel };
1994
2491
  }
1995
2492
  const migrationLens = input.migration ? migrationLensFrom(input.migration) : void 0;
1996
2493
  const lenses = {
@@ -2022,8 +2519,8 @@ function buildSnapshot(input) {
2022
2519
  }
2023
2520
  function migrationLensFrom(migration) {
2024
2521
  const nodes = {};
2025
- for (const [relativePath2, node] of migration.nodes) {
2026
- nodes[fileId(relativePath2)] = node;
2522
+ for (const [relativePath3, node] of migration.nodes) {
2523
+ nodes[fileId(relativePath3)] = node;
2027
2524
  }
2028
2525
  const edges = migration.edges.map((edge) => ({
2029
2526
  fromId: fileId(edge.from),
@@ -2039,7 +2536,7 @@ function migrationLensFrom(migration) {
2039
2536
  };
2040
2537
  }
2041
2538
  function checksumFor(files, lenses) {
2042
- return createHash2("md5").update(stableStringify({ files, lenses }), "utf8").digest("hex");
2539
+ return md5Hex(stableStringify({ files, lenses }));
2043
2540
  }
2044
2541
  function stableStringify(value) {
2045
2542
  if (value === null || value === void 0) return "null";
@@ -2076,9 +2573,9 @@ function compareStrings3(left, right) {
2076
2573
  }
2077
2574
 
2078
2575
  // packages/parsers/src/walk.ts
2079
- import fs3 from "node:fs";
2080
- import path3 from "node:path";
2081
2576
  import ignore from "ignore";
2577
+
2578
+ // packages/parsers/src/extensions.ts
2082
2579
  var DEFAULT_EXTENSIONS = [
2083
2580
  ".ts",
2084
2581
  ".tsx",
@@ -2089,29 +2586,26 @@ var DEFAULT_EXTENSIONS = [
2089
2586
  ".mjs",
2090
2587
  ".cjs"
2091
2588
  ];
2589
+
2590
+ // packages/parsers/src/walk.ts
2092
2591
  var ALWAYS_IGNORED = [".git", "node_modules", ".codarch"];
2093
- function walkSourceFiles(rootInput, options = {}) {
2094
- const root = path3.resolve(rootInput);
2095
- const stat = fs3.statSync(root);
2096
- if (!stat.isDirectory()) {
2097
- throw new Error(`no es un directorio: ${rootInput}`);
2098
- }
2592
+ function walkSourceFiles(fileSystem, options = {}) {
2099
2593
  const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
2100
2594
  const warnings = [];
2101
2595
  const files = [];
2102
2596
  const rootMatcher = ignore().add(ALWAYS_IGNORED);
2103
2597
  for (const pattern of options.ignore ?? []) rootMatcher.add(pattern);
2104
- const rootGitignore = readGitignore(root, "", warnings);
2598
+ const rootGitignore = readGitignore(fileSystem, "/", "", warnings);
2105
2599
  if (rootGitignore) rootMatcher.add(rootGitignore);
2106
2600
  const initialLayers = [{ base: "", matcher: rootMatcher }];
2107
2601
  const stack = [
2108
- { absolutePath: root, relativePath: "", layers: initialLayers }
2602
+ { absolutePath: "/", relativePath: "", layers: initialLayers }
2109
2603
  ];
2110
2604
  while (stack.length > 0) {
2111
2605
  const directory = stack.pop();
2112
2606
  let entries;
2113
2607
  try {
2114
- entries = fs3.readdirSync(directory.absolutePath, { withFileTypes: true });
2608
+ entries = fileSystem.readDirectory(directory.absolutePath);
2115
2609
  } catch (error) {
2116
2610
  warnings.push(
2117
2611
  `no se pudo leer ${directory.relativePath || "."}: ${error.message}`
@@ -2122,18 +2616,17 @@ function walkSourceFiles(rootInput, options = {}) {
2122
2616
  (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
2123
2617
  );
2124
2618
  for (const entry of entries) {
2125
- if (entry.isSymbolicLink()) continue;
2126
- const relativePath2 = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
2127
- const absolutePath = path3.join(directory.absolutePath, entry.name);
2128
- if (entry.isDirectory()) {
2129
- if (isIgnored(directory.layers, relativePath2, true)) continue;
2130
- const nested = readGitignore(absolutePath, relativePath2, warnings);
2131
- const layers = nested ? [...directory.layers, { base: relativePath2, matcher: ignore().add(nested) }] : directory.layers;
2132
- stack.push({ absolutePath, relativePath: relativePath2, layers });
2133
- } else if (entry.isFile()) {
2134
- if (isIgnored(directory.layers, relativePath2, false)) continue;
2619
+ const relativePath3 = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
2620
+ const absolutePath = directory.absolutePath === "/" ? `/${entry.name}` : `${directory.absolutePath}/${entry.name}`;
2621
+ if (entry.isDirectory) {
2622
+ if (isIgnored(directory.layers, relativePath3, true)) continue;
2623
+ const nested = readGitignore(fileSystem, absolutePath, relativePath3, warnings);
2624
+ const layers = nested ? [...directory.layers, { base: relativePath3, matcher: ignore().add(nested) }] : directory.layers;
2625
+ stack.push({ absolutePath, relativePath: relativePath3, layers });
2626
+ } else if (entry.isFile) {
2627
+ if (isIgnored(directory.layers, relativePath3, false)) continue;
2135
2628
  if (!matchesExtension(entry.name, extensions)) continue;
2136
- files.push({ absolutePath, relativePath: relativePath2 });
2629
+ files.push({ absolutePath, relativePath: relativePath3 });
2137
2630
  }
2138
2631
  }
2139
2632
  }
@@ -2143,23 +2636,21 @@ function walkSourceFiles(rootInput, options = {}) {
2143
2636
  warnings.sort();
2144
2637
  return { files, warnings };
2145
2638
  }
2146
- function readGitignore(directory, relativeBase, warnings) {
2147
- const file = path3.join(directory, ".gitignore");
2639
+ function readGitignore(fileSystem, absoluteDirectory, relativeBase, warnings) {
2640
+ const file = absoluteDirectory === "/" ? "/.gitignore" : `${absoluteDirectory}/.gitignore`;
2148
2641
  try {
2149
- return fs3.readFileSync(file, "utf8");
2642
+ return fileSystem.tryReadFile(file);
2150
2643
  } catch (error) {
2151
- if (error.code !== "ENOENT") {
2152
- warnings.push(
2153
- `no se pudo leer ${relativeBase ? `${relativeBase}/` : ""}.gitignore: ${error.message}`
2154
- );
2155
- }
2644
+ warnings.push(
2645
+ `no se pudo leer ${relativeBase ? `${relativeBase}/` : ""}.gitignore: ${error.message}`
2646
+ );
2156
2647
  return null;
2157
2648
  }
2158
2649
  }
2159
- function isIgnored(layers, relativePath2, isDirectory) {
2650
+ function isIgnored(layers, relativePath3, isDirectory) {
2160
2651
  let ignored = false;
2161
2652
  for (const layer of layers) {
2162
- const localPath = layer.base ? relativePath2.slice(layer.base.length + 1) : relativePath2;
2653
+ const localPath = layer.base ? relativePath3.slice(layer.base.length + 1) : relativePath3;
2163
2654
  if (!localPath) continue;
2164
2655
  const probe = isDirectory ? `${localPath}/` : localPath;
2165
2656
  const result = layer.matcher.test(probe);
@@ -2173,28 +2664,36 @@ function matchesExtension(name, extensions) {
2173
2664
  return extensions.some((extension) => lower.endsWith(extension));
2174
2665
  }
2175
2666
 
2176
- // packages/parsers/src/analyze.ts
2177
- function analyzeRepository(rootInput, options = {}) {
2667
+ // packages/parsers/src/analyze-core.ts
2668
+ function analyzeFiles(options) {
2178
2669
  const started = Date.now();
2179
- const root = path4.resolve(rootInput);
2180
- const projectName = options.projectName ?? readProjectName(root);
2181
- const walked = walkSourceFiles(root, {
2670
+ const fileSystem = options.fileSystem;
2671
+ const root = options.root ?? "/";
2672
+ const projectName = options.projectName ?? readProjectName(fileSystem, root, options.fallbackName);
2673
+ const walked = walkSourceFiles(fileSystem, {
2182
2674
  ignore: options.ignore,
2183
2675
  extensions: options.extensions ?? MIGRATION_EXTENSIONS
2184
2676
  });
2185
2677
  const warnings = [...walked.warnings];
2186
- const resolver = new ImportResolver(root, walked.files);
2678
+ const resolver = new ImportResolver(root, walked.files, fileSystem);
2187
2679
  warnings.push(...resolver.warnings);
2188
- const parser = new TypeScriptParser();
2680
+ const parser = options.parser;
2189
2681
  const parsedFiles = [];
2190
- const migrationFiles = [];
2682
+ const migrationFiles2 = [];
2191
2683
  const migrationLinks = [];
2192
2684
  const links = [];
2193
2685
  const unresolved = [];
2194
2686
  let externalImports = 0;
2195
2687
  let builtinImports = 0;
2196
2688
  let typeOnlyImports = 0;
2689
+ let processed = 0;
2197
2690
  for (const file of walked.files) {
2691
+ processed += 1;
2692
+ options.onProgress?.({
2693
+ done: processed,
2694
+ total: walked.files.length,
2695
+ current: file.relativePath
2696
+ });
2198
2697
  const language = languageForPath(file.relativePath);
2199
2698
  if (!language) continue;
2200
2699
  if (isNativeLanguage(language)) {
@@ -2202,7 +2701,7 @@ function analyzeRepository(rootInput, options = {}) {
2202
2701
  relativePath: file.relativePath,
2203
2702
  metrics: { loc: 0, rloc: 0 }
2204
2703
  });
2205
- migrationFiles.push({
2704
+ migrationFiles2.push({
2206
2705
  relativePath: file.relativePath,
2207
2706
  language,
2208
2707
  loc: 0,
@@ -2215,7 +2714,7 @@ function analyzeRepository(rootInput, options = {}) {
2215
2714
  }
2216
2715
  let source;
2217
2716
  try {
2218
- source = fs4.readFileSync(file.absolutePath, "utf8");
2717
+ source = fileSystem.readFile(file.absolutePath);
2219
2718
  } catch (error) {
2220
2719
  warnings.push(
2221
2720
  `no se pudo leer ${file.relativePath}: ${error.message}`
@@ -2228,7 +2727,7 @@ function analyzeRepository(rootInput, options = {}) {
2228
2727
  relativePath: file.relativePath,
2229
2728
  metrics: { loc: measured.loc, rloc: measured.rloc }
2230
2729
  });
2231
- migrationFiles.push({
2730
+ migrationFiles2.push({
2232
2731
  relativePath: file.relativePath,
2233
2732
  language,
2234
2733
  loc: measured.loc,
@@ -2298,7 +2797,7 @@ function analyzeRepository(rootInput, options = {}) {
2298
2797
  unresolvedImports: fileUnresolvedImports
2299
2798
  }
2300
2799
  });
2301
- migrationFiles.push({
2800
+ migrationFiles2.push({
2302
2801
  relativePath: file.relativePath,
2303
2802
  language,
2304
2803
  loc: parsed.metrics.loc ?? 0,
@@ -2311,18 +2810,15 @@ function analyzeRepository(rootInput, options = {}) {
2311
2810
  )
2312
2811
  });
2313
2812
  }
2314
- const git = collectGitChurn(
2315
- root,
2316
- parsedFiles.map((file) => file.relativePath),
2317
- { enabled: options.git !== false }
2318
- );
2319
- warnings.push(...git.warnings);
2320
- if (git.enabled) {
2813
+ let churnResult = null;
2814
+ if (options.churn) {
2815
+ churnResult = options.churn(parsedFiles.map((file) => file.relativePath));
2816
+ warnings.push(...churnResult.warnings);
2321
2817
  for (const file of parsedFiles) {
2322
- const churn = git.metrics.get(file.relativePath);
2323
- if (!churn) continue;
2324
- file.metrics.number_of_commits = churn.commits;
2325
- file.metrics.number_of_authors = churn.authors;
2818
+ const metrics = churnResult.metrics.get(file.relativePath);
2819
+ if (!metrics) continue;
2820
+ file.metrics.number_of_commits = metrics.commits;
2821
+ file.metrics.number_of_authors = metrics.authors;
2326
2822
  }
2327
2823
  }
2328
2824
  const graph = buildGraph(
@@ -2339,10 +2835,10 @@ function analyzeRepository(rootInput, options = {}) {
2339
2835
  file.metrics.dependencyLevel = metrics.dependencyLevel;
2340
2836
  }
2341
2837
  }
2342
- for (const file of migrationFiles) {
2838
+ for (const file of migrationFiles2) {
2343
2839
  file.fanIn = graph.metrics.get(file.relativePath)?.fanIn ?? 0;
2344
2840
  }
2345
- const migration = buildMigration(migrationFiles, migrationLinks);
2841
+ const migration = buildMigration(migrationFiles2, migrationLinks);
2346
2842
  for (const file of parsedFiles) {
2347
2843
  const node = migration.nodes.get(file.relativePath);
2348
2844
  if (!node) continue;
@@ -2370,39 +2866,259 @@ function analyzeRepository(rootInput, options = {}) {
2370
2866
  }
2371
2867
  warnings.sort();
2372
2868
  return {
2373
- snapshot,
2374
- files: parsedFiles.length,
2375
- folders,
2376
- edges: graph.edges.length,
2377
- cycles: graph.cycles,
2378
- metricNames: [...metricNames].sort(),
2379
- warnings,
2380
- unresolved,
2381
- externalImports,
2382
- builtinImports,
2383
- typeOnlyImports,
2384
- unresolvedImports: unresolved.length,
2869
+ snapshot,
2870
+ files: parsedFiles.length,
2871
+ folders,
2872
+ edges: graph.edges.length,
2873
+ cycles: graph.cycles,
2874
+ metricNames: [...metricNames].sort(),
2875
+ warnings,
2876
+ unresolved,
2877
+ externalImports,
2878
+ builtinImports,
2879
+ typeOnlyImports,
2880
+ unresolvedImports: unresolved.length,
2881
+ migration: migration.summary,
2882
+ churn: churnResult,
2883
+ durationMs: Date.now() - started
2884
+ };
2885
+ }
2886
+ function readProjectName(fileSystem, root, fallbackName) {
2887
+ try {
2888
+ const text = fileSystem.readFile(join(root, "package.json"));
2889
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
2890
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.name === "string") {
2891
+ const name = parsed.name.trim();
2892
+ if (name) return name;
2893
+ }
2894
+ } catch {
2895
+ }
2896
+ return fallbackName ?? basename(root) ?? "Proyecto";
2897
+ }
2898
+
2899
+ // packages/parsers/src/git.ts
2900
+ import { spawnSync } from "node:child_process";
2901
+ import fs from "node:fs";
2902
+ import path from "node:path";
2903
+ var COMMIT_PREFIX = "__CODARCH_COMMIT__";
2904
+ var DEFAULT_MAX_BUFFER = 128 * 1024 * 1024;
2905
+ var EMPTY = {
2906
+ metrics: /* @__PURE__ */ new Map(),
2907
+ commits: 0,
2908
+ authors: 0,
2909
+ shallow: false,
2910
+ enabled: false,
2911
+ warnings: []
2912
+ };
2913
+ function collectGitChurn(rootInput, files, options = {}) {
2914
+ if (options.enabled === false || files.length === 0) {
2915
+ return { ...EMPTY, metrics: /* @__PURE__ */ new Map() };
2916
+ }
2917
+ const root = realPath(rootInput);
2918
+ const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
2919
+ const warnings = [];
2920
+ const topLevel = runGit(root, ["rev-parse", "--show-toplevel"], maxBuffer);
2921
+ if (!topLevel.ok) {
2922
+ return {
2923
+ ...EMPTY,
2924
+ metrics: /* @__PURE__ */ new Map(),
2925
+ warnings: [`no hay historial git utilizable: ${topLevel.error}`]
2926
+ };
2927
+ }
2928
+ const repoRoot = realPath(topLevel.stdout.trim());
2929
+ const shallowResult = runGit(
2930
+ repoRoot,
2931
+ ["rev-parse", "--is-shallow-repository"],
2932
+ maxBuffer
2933
+ );
2934
+ const shallow = shallowResult.ok && shallowResult.stdout.trim().toLowerCase() === "true";
2935
+ if (shallow) {
2936
+ warnings.push(
2937
+ "el repositorio es shallow: el churn puede estar incompleto"
2938
+ );
2939
+ }
2940
+ const log = runGit(
2941
+ repoRoot,
2942
+ [
2943
+ "-c",
2944
+ "core.quotePath=false",
2945
+ "log",
2946
+ "--no-merges",
2947
+ "--numstat",
2948
+ `--format=${COMMIT_PREFIX}%H%x09%ae`
2949
+ ],
2950
+ maxBuffer
2951
+ );
2952
+ if (!log.ok) {
2953
+ return {
2954
+ ...EMPTY,
2955
+ shallow,
2956
+ warnings: [...warnings, `no se pudo leer git log: ${log.error}`]
2957
+ };
2958
+ }
2959
+ const wanted = new Set(files);
2960
+ const commitsByFile = /* @__PURE__ */ new Map();
2961
+ const authorsByFile = /* @__PURE__ */ new Map();
2962
+ const authors = /* @__PURE__ */ new Set();
2963
+ let commits = 0;
2964
+ let currentAuthor = "";
2965
+ let touched = /* @__PURE__ */ new Set();
2966
+ const flushCommit = () => {
2967
+ if (!currentAuthor || touched.size === 0) return;
2968
+ commits++;
2969
+ authors.add(currentAuthor);
2970
+ for (const file of touched) {
2971
+ commitsByFile.set(file, (commitsByFile.get(file) ?? 0) + 1);
2972
+ let fileAuthors = authorsByFile.get(file);
2973
+ if (!fileAuthors) {
2974
+ fileAuthors = /* @__PURE__ */ new Set();
2975
+ authorsByFile.set(file, fileAuthors);
2976
+ }
2977
+ fileAuthors.add(currentAuthor);
2978
+ }
2979
+ touched = /* @__PURE__ */ new Set();
2980
+ currentAuthor = "";
2981
+ };
2982
+ for (const rawLine of log.stdout.split(/\r?\n/)) {
2983
+ if (rawLine.startsWith(COMMIT_PREFIX)) {
2984
+ flushCommit();
2985
+ const fields2 = rawLine.slice(COMMIT_PREFIX.length).split(" ");
2986
+ const author = fields2[1]?.trim() ?? "";
2987
+ currentAuthor = author || "(sin autor)";
2988
+ continue;
2989
+ }
2990
+ if (!currentAuthor || rawLine === "") continue;
2991
+ const fields = rawLine.split(" ");
2992
+ if (fields.length < 3) continue;
2993
+ const gitPath = fields.slice(2).join(" ");
2994
+ if (!gitPath) continue;
2995
+ const relative2 = path.relative(root, path.resolve(repoRoot, gitPath));
2996
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path.sep}`) || path.isAbsolute(relative2)) {
2997
+ continue;
2998
+ }
2999
+ const normalized = relative2.split(path.sep).join("/");
3000
+ if (!wanted.has(normalized)) continue;
3001
+ touched.add(normalized);
3002
+ }
3003
+ flushCommit();
3004
+ const metrics = /* @__PURE__ */ new Map();
3005
+ for (const file of files) {
3006
+ metrics.set(file, {
3007
+ commits: commitsByFile.get(file) ?? 0,
3008
+ authors: authorsByFile.get(file)?.size ?? 0
3009
+ });
3010
+ }
3011
+ return {
3012
+ metrics,
3013
+ commits,
3014
+ authors: authors.size,
3015
+ shallow,
3016
+ enabled: true,
3017
+ warnings
3018
+ };
3019
+ }
3020
+ function realPath(input) {
3021
+ const resolved = path.resolve(input);
3022
+ try {
3023
+ return fs.realpathSync.native(resolved);
3024
+ } catch {
3025
+ return resolved;
3026
+ }
3027
+ }
3028
+ function runGit(cwd, args, maxBuffer) {
3029
+ const result = spawnSync("git", args, {
3030
+ cwd,
3031
+ encoding: "utf8",
3032
+ maxBuffer,
3033
+ windowsHide: true
3034
+ });
3035
+ if (result.error) {
3036
+ return { ok: false, stdout: "", error: result.error.message };
3037
+ }
3038
+ if (result.status !== 0) {
3039
+ return {
3040
+ ok: false,
3041
+ stdout: result.stdout ?? "",
3042
+ error: (result.stderr ?? "").trim() || `git exited with ${result.status}`
3043
+ };
3044
+ }
3045
+ return { ok: true, stdout: result.stdout ?? "", error: "" };
3046
+ }
3047
+
3048
+ // packages/parsers/src/node-fs.ts
3049
+ import fs2 from "node:fs";
3050
+ import path2 from "node:path";
3051
+ var NodeFileSystem = class {
3052
+ caseSensitive = process.platform !== "win32";
3053
+ root;
3054
+ constructor(root) {
3055
+ this.root = path2.resolve(root);
3056
+ }
3057
+ readFile(target) {
3058
+ return fs2.readFileSync(this.native(target), "utf8");
3059
+ }
3060
+ tryReadFile(target) {
3061
+ try {
3062
+ return fs2.readFileSync(this.native(target), "utf8");
3063
+ } catch (error) {
3064
+ if (error.code !== "ENOENT") {
3065
+ throw error;
3066
+ }
3067
+ return null;
3068
+ }
3069
+ }
3070
+ readDirectory(target) {
3071
+ const entries = fs2.readdirSync(this.native(target), { withFileTypes: true });
3072
+ return entries.map((entry) => ({
3073
+ name: entry.name,
3074
+ isDirectory: entry.isDirectory(),
3075
+ isFile: entry.isFile()
3076
+ }));
3077
+ }
3078
+ exists(target) {
3079
+ return fs2.existsSync(this.native(target));
3080
+ }
3081
+ /** Ruta real en disco de una ruta virtual. */
3082
+ native(target) {
3083
+ const relative2 = target.replace(/^\/+/, "");
3084
+ return relative2 === "" ? this.root : path2.join(this.root, relative2);
3085
+ }
3086
+ };
3087
+
3088
+ // packages/parsers/src/analyze.ts
3089
+ function analyzeRepository(rootInput, options = {}) {
3090
+ const fileSystem = options.fileSystem ?? new NodeFileSystem(rootInput);
3091
+ const useGit = options.git !== false;
3092
+ const result = analyzeFiles({
3093
+ fileSystem,
3094
+ root: options.root ?? "/",
3095
+ projectName: options.projectName,
3096
+ fallbackName: basename2(rootInput),
3097
+ ignore: options.ignore,
3098
+ extensions: options.extensions,
3099
+ parser: new TypeScriptParser(),
3100
+ churn: (relativePaths) => {
3101
+ const git = collectGitChurn(rootInput, relativePaths, { enabled: useGit });
3102
+ return {
3103
+ metrics: git.metrics,
3104
+ warnings: git.warnings,
3105
+ enabled: git.enabled,
3106
+ commits: git.commits,
3107
+ authors: git.authors,
3108
+ shallow: git.shallow
3109
+ };
3110
+ }
3111
+ });
3112
+ const churn = result.churn;
3113
+ return {
3114
+ ...result,
2385
3115
  git: {
2386
- enabled: git.enabled,
2387
- commits: git.commits,
2388
- authors: git.authors,
2389
- shallow: git.shallow
2390
- },
2391
- migration: migration.summary,
2392
- durationMs: Date.now() - started
2393
- };
2394
- }
2395
- function readProjectName(root) {
2396
- try {
2397
- const text = fs4.readFileSync(path4.join(root, "package.json"), "utf8");
2398
- const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
2399
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.name === "string") {
2400
- const name = parsed.name.trim();
2401
- if (name) return name;
3116
+ enabled: churn?.enabled ?? false,
3117
+ commits: churn?.commits ?? 0,
3118
+ authors: churn?.authors ?? 0,
3119
+ shallow: churn?.shallow ?? false
2402
3120
  }
2403
- } catch {
2404
- }
2405
- return path4.basename(root) || "Proyecto";
3121
+ };
2406
3122
  }
2407
3123
 
2408
3124
  // packages/metrics/src/risk.ts
@@ -2471,6 +3187,318 @@ function clamp01(value) {
2471
3187
  return Math.max(0, Math.min(1, value));
2472
3188
  }
2473
3189
 
3190
+ // packages/metrics/src/profiles.ts
3191
+ var DOMAIN_PROFILES = [
3192
+ {
3193
+ id: "capas",
3194
+ title: "Capas por convenci\xF3n",
3195
+ description: "Tres capas: el dominio no depende de nada, la aplicaci\xF3n no depende de la infraestructura y la infraestructura no depende de la aplicaci\xF3n.",
3196
+ when: "Para repos con src/domain, src/application y src/infra.",
3197
+ layers: [
3198
+ {
3199
+ name: "domain",
3200
+ patterns: [
3201
+ "src/domain/**",
3202
+ "src/core/**",
3203
+ "src/dominio/**",
3204
+ "src/nucleo/**",
3205
+ "domain/**"
3206
+ ]
3207
+ },
3208
+ {
3209
+ name: "application",
3210
+ patterns: [
3211
+ "src/application/**",
3212
+ "src/app/**",
3213
+ "src/use-cases/**",
3214
+ "src/usecases/**",
3215
+ "src/aplicacion/**",
3216
+ "src/casos-de-uso/**",
3217
+ "application/**"
3218
+ ]
3219
+ },
3220
+ {
3221
+ name: "infrastructure",
3222
+ patterns: [
3223
+ "src/infrastructure/**",
3224
+ "src/infra/**",
3225
+ "src/adapters/**",
3226
+ "src/persistence/**",
3227
+ "src/infraestructura/**",
3228
+ "src/adaptadores/**",
3229
+ "infrastructure/**"
3230
+ ]
3231
+ }
3232
+ ],
3233
+ rules: [
3234
+ {
3235
+ type: "forbidden",
3236
+ name: "dominio-sin-capas-exteriores",
3237
+ from: "domain",
3238
+ to: "application",
3239
+ severity: "warn",
3240
+ message: "El dominio no debe depender de la aplicaci\xF3n"
3241
+ },
3242
+ {
3243
+ type: "forbidden",
3244
+ name: "dominio-sin-infraestructura",
3245
+ from: "domain",
3246
+ to: "infrastructure",
3247
+ severity: "warn",
3248
+ message: "El dominio no debe depender de la infraestructura"
3249
+ },
3250
+ {
3251
+ type: "forbidden",
3252
+ name: "aplicacion-sin-infraestructura",
3253
+ from: "application",
3254
+ to: "infrastructure",
3255
+ severity: "warn",
3256
+ message: "La aplicaci\xF3n no debe depender de la infraestructura"
3257
+ }
3258
+ ]
3259
+ },
3260
+ {
3261
+ id: "hexagonal",
3262
+ title: "Ports & adapters",
3263
+ description: "El dominio en el centro, la aplicaci\xF3n con los puertos y los adaptadores por fuera: la aplicaci\xF3n y el dominio no dependen de los adaptadores.",
3264
+ when: "Para repos con puertos (interfaces) en el centro y adaptadores fuera.",
3265
+ layers: [
3266
+ {
3267
+ name: "domain",
3268
+ patterns: [
3269
+ "src/domain/**",
3270
+ "src/core/**",
3271
+ "src/model/**",
3272
+ "src/dominio/**",
3273
+ "src/modelo/**",
3274
+ "domain/**"
3275
+ ]
3276
+ },
3277
+ {
3278
+ name: "application",
3279
+ patterns: [
3280
+ "src/application/**",
3281
+ "src/app/**",
3282
+ "src/use-cases/**",
3283
+ "src/usecases/**",
3284
+ "src/ports/**",
3285
+ "src/puertos/**",
3286
+ "application/**"
3287
+ ]
3288
+ },
3289
+ {
3290
+ name: "adapters",
3291
+ patterns: [
3292
+ "src/adapters/**",
3293
+ "src/infra/**",
3294
+ "src/infrastructure/**",
3295
+ "src/persistence/**",
3296
+ "src/adaptadores/**",
3297
+ "src/infraestructura/**",
3298
+ "adapters/**"
3299
+ ]
3300
+ }
3301
+ ],
3302
+ rules: [
3303
+ {
3304
+ type: "forbidden",
3305
+ name: "dominio-sin-adaptadores",
3306
+ from: "domain",
3307
+ to: "adapters",
3308
+ severity: "warn",
3309
+ message: "El dominio no debe depender de los adaptadores"
3310
+ },
3311
+ {
3312
+ type: "forbidden",
3313
+ name: "aplicacion-sin-adaptadores",
3314
+ from: "application",
3315
+ to: "adapters",
3316
+ severity: "warn",
3317
+ message: "La aplicaci\xF3n habla con los adaptadores por puertos, no directamente"
3318
+ }
3319
+ ]
3320
+ },
3321
+ {
3322
+ id: "onion",
3323
+ title: "Clean Architecture (cebolla)",
3324
+ description: "Cuatro anillos: el dominio en el centro, luego la aplicaci\xF3n, la infraestructura y la presentaci\xF3n fuera. Las dependencias van hacia dentro y el anillo exterior no se importa desde dentro.",
3325
+ when: "Para repos con una capa de presentaci\xF3n o de interfaz propia.",
3326
+ layers: [
3327
+ {
3328
+ name: "domain",
3329
+ patterns: [
3330
+ "src/domain/**",
3331
+ "src/entities/**",
3332
+ "src/value-objects/**",
3333
+ "src/dominio/**",
3334
+ "src/entidades/**",
3335
+ "domain/**"
3336
+ ]
3337
+ },
3338
+ {
3339
+ name: "application",
3340
+ patterns: [
3341
+ "src/application/**",
3342
+ "src/use-cases/**",
3343
+ "src/usecases/**",
3344
+ "src/services/**",
3345
+ "src/aplicacion/**",
3346
+ "src/casos-de-uso/**",
3347
+ "application/**"
3348
+ ]
3349
+ },
3350
+ {
3351
+ name: "infrastructure",
3352
+ patterns: [
3353
+ "src/infrastructure/**",
3354
+ "src/infra/**",
3355
+ "src/persistence/**",
3356
+ "src/repositories/**",
3357
+ "src/infraestructura/**",
3358
+ "infrastructure/**"
3359
+ ]
3360
+ },
3361
+ {
3362
+ name: "presentation",
3363
+ patterns: [
3364
+ "src/presentation/**",
3365
+ "src/ui/**",
3366
+ "src/controllers/**",
3367
+ "src/pages/**",
3368
+ "src/components/**",
3369
+ "src/presentacion/**",
3370
+ "src/interfaz/**",
3371
+ "presentation/**"
3372
+ ]
3373
+ }
3374
+ ],
3375
+ rules: [
3376
+ {
3377
+ type: "forbidden",
3378
+ name: "dominio-sin-capas-exteriores",
3379
+ from: "domain",
3380
+ to: "application",
3381
+ severity: "warn",
3382
+ message: "El dominio no debe depender de la aplicaci\xF3n"
3383
+ },
3384
+ {
3385
+ type: "forbidden",
3386
+ name: "dominio-sin-infraestructura",
3387
+ from: "domain",
3388
+ to: "infrastructure",
3389
+ severity: "warn",
3390
+ message: "El dominio no debe depender de la infraestructura"
3391
+ },
3392
+ {
3393
+ type: "forbidden",
3394
+ name: "dominio-sin-presentacion",
3395
+ from: "domain",
3396
+ to: "presentation",
3397
+ severity: "warn",
3398
+ message: "El dominio no debe depender de la presentaci\xF3n"
3399
+ },
3400
+ {
3401
+ type: "forbidden",
3402
+ name: "aplicacion-sin-infraestructura",
3403
+ from: "application",
3404
+ to: "infrastructure",
3405
+ severity: "warn",
3406
+ message: "La aplicaci\xF3n no debe depender de la infraestructura"
3407
+ },
3408
+ {
3409
+ type: "forbidden",
3410
+ name: "aplicacion-sin-presentacion",
3411
+ from: "application",
3412
+ to: "presentation",
3413
+ severity: "warn",
3414
+ message: "La aplicaci\xF3n no debe depender de la presentaci\xF3n"
3415
+ },
3416
+ {
3417
+ type: "forbidden",
3418
+ name: "infraestructura-sin-presentacion",
3419
+ from: "infrastructure",
3420
+ to: "presentation",
3421
+ severity: "warn",
3422
+ message: "La infraestructura y la presentaci\xF3n se hablan por los puertos, no entre ellas"
3423
+ }
3424
+ ]
3425
+ },
3426
+ {
3427
+ id: "ddd",
3428
+ title: "DDD por carpetas",
3429
+ description: "Las carpetas del dominio driven design: el modelo dentro, los servicios de aplicaci\xF3n y los repositorios en la infraestructura.",
3430
+ when: "Para repos con modelo, servicios y repositorios como carpetas.",
3431
+ layers: [
3432
+ {
3433
+ name: "domain",
3434
+ patterns: [
3435
+ "src/domain/**",
3436
+ "src/model/**",
3437
+ "src/entities/**",
3438
+ "src/value-objects/**",
3439
+ "src/dominio/**",
3440
+ "src/modelo/**",
3441
+ "domain/**"
3442
+ ]
3443
+ },
3444
+ {
3445
+ name: "application",
3446
+ patterns: [
3447
+ "src/application/**",
3448
+ "src/services/**",
3449
+ "src/use-cases/**",
3450
+ "src/aplicacion/**",
3451
+ "src/servicios/**",
3452
+ "application/**"
3453
+ ]
3454
+ },
3455
+ {
3456
+ name: "infrastructure",
3457
+ patterns: [
3458
+ "src/infrastructure/**",
3459
+ "src/repositories/**",
3460
+ "src/persistence/**",
3461
+ "src/infraestructura/**",
3462
+ "src/repositorios/**",
3463
+ "infrastructure/**"
3464
+ ]
3465
+ }
3466
+ ],
3467
+ rules: [
3468
+ {
3469
+ type: "forbidden",
3470
+ name: "dominio-sin-capas-exteriores",
3471
+ from: "domain",
3472
+ to: "application",
3473
+ severity: "warn",
3474
+ message: "El modelo de dominio no debe depender de los servicios"
3475
+ },
3476
+ {
3477
+ type: "forbidden",
3478
+ name: "dominio-sin-infraestructura",
3479
+ from: "domain",
3480
+ to: "infrastructure",
3481
+ severity: "warn",
3482
+ message: "El modelo de dominio no debe depender de los repositorios"
3483
+ },
3484
+ {
3485
+ type: "forbidden",
3486
+ name: "aplicacion-sin-infraestructura",
3487
+ from: "application",
3488
+ to: "infrastructure",
3489
+ severity: "warn",
3490
+ message: "Los servicios hablan con los repositorios por puertos, no directamente"
3491
+ }
3492
+ ]
3493
+ }
3494
+ ];
3495
+ function findDomainProfile(id) {
3496
+ return DOMAIN_PROFILES.find((profile) => profile.id === id);
3497
+ }
3498
+ function domainProfileIds() {
3499
+ return DOMAIN_PROFILES.map((profile) => profile.id);
3500
+ }
3501
+
2474
3502
  // packages/render/src/model.ts
2475
3503
  var RenderError = class extends Error {
2476
3504
  constructor(message) {
@@ -2541,8 +3569,8 @@ function hashString(value) {
2541
3569
  }
2542
3570
  return hash >>> 0;
2543
3571
  }
2544
- function sectionForPath(path9) {
2545
- const folders = path9.split("/").filter(Boolean).slice(0, -1);
3572
+ function sectionForPath(path7) {
3573
+ const folders = path7.split("/").filter(Boolean).slice(0, -1);
2546
3574
  if (folders.length === 0) return "(ra\xEDz)";
2547
3575
  if (folders.length === 1) return folders[0];
2548
3576
  return folders.slice(0, 3).join("/");
@@ -2612,7 +3640,7 @@ function buildRenderModel(cc, options = {}) {
2612
3640
  const fileIndexById = /* @__PURE__ */ new Map();
2613
3641
  walkFiles(cc.files, (entry) => {
2614
3642
  const fullPath = entry.path;
2615
- const path9 = prefix && fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
3643
+ const path7 = prefix && fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
2616
3644
  const values = attributes[entry.node.id];
2617
3645
  const rawArea = valueOf(values, areaMetric);
2618
3646
  const index = files.length;
@@ -2620,7 +3648,7 @@ function buildRenderModel(cc, options = {}) {
2620
3648
  files.push({
2621
3649
  index,
2622
3650
  nodeId: entry.node.id,
2623
- path: path9,
3651
+ path: path7,
2624
3652
  name: entry.node.name,
2625
3653
  section: -1,
2626
3654
  area: Math.max(1, rawArea ?? 0),
@@ -2783,7 +3811,7 @@ function buildRenderModel(cc, options = {}) {
2783
3811
  };
2784
3812
  }
2785
3813
  function buildRoots(cc, singleRoot, fileIndexById, files) {
2786
- const build = (node, path9) => {
3814
+ const build = (node, path7) => {
2787
3815
  if (node.type === "File") {
2788
3816
  const index = fileIndexById.get(node.id);
2789
3817
  const file = index === void 0 ? void 0 : files[index];
@@ -2791,7 +3819,7 @@ function buildRoots(cc, singleRoot, fileIndexById, files) {
2791
3819
  kind: "file",
2792
3820
  key: `f:${index ?? -1}`,
2793
3821
  name: node.name,
2794
- path: path9,
3822
+ path: path7,
2795
3823
  value: file?.area ?? 1,
2796
3824
  fileCount: 1,
2797
3825
  section: file?.section ?? 0,
@@ -2799,13 +3827,13 @@ function buildRoots(cc, singleRoot, fileIndexById, files) {
2799
3827
  };
2800
3828
  }
2801
3829
  const children = (node.children ?? []).map(
2802
- (child) => build(child, path9 ? `${path9}/${child.name}` : child.name)
3830
+ (child) => build(child, path7 ? `${path7}/${child.name}` : child.name)
2803
3831
  );
2804
3832
  const folder = {
2805
3833
  kind: "folder",
2806
- key: `d:${path9}`,
3834
+ key: `d:${path7}`,
2807
3835
  name: node.name,
2808
- path: path9,
3836
+ path: path7,
2809
3837
  value: children.reduce((sum, child) => sum + child.value, 0) || 1,
2810
3838
  fileCount: children.reduce((sum, child) => sum + child.fileCount, 0),
2811
3839
  section: -1,
@@ -3449,10 +4477,10 @@ function renderHtml(model) {
3449
4477
  }
3450
4478
 
3451
4479
  // apps/cli/src/io.ts
3452
- import fs5 from "node:fs";
4480
+ import fs3 from "node:fs";
3453
4481
  import zlib from "node:zlib";
3454
4482
  function readJsonFile2(file) {
3455
- const buffer = fs5.readFileSync(file);
4483
+ const buffer = fs3.readFileSync(file);
3456
4484
  const isGzip = buffer.length > 2 && buffer[0] === 31 && buffer[1] === 139;
3457
4485
  const raw = isGzip ? zlib.gunzipSync(buffer) : buffer;
3458
4486
  const text = raw.toString("utf8");
@@ -3500,14 +4528,14 @@ function runAnalyze(options) {
3500
4528
  }
3501
4529
  throw error;
3502
4530
  }
3503
- const outDirectory = path5.resolve(
3504
- options.out ?? path5.join(path5.resolve(root), ".codarch")
4531
+ const outDirectory = path3.resolve(
4532
+ options.out ?? path3.join(path3.resolve(root), ".codarch")
3505
4533
  );
3506
- const snapshotPath = path5.join(outDirectory, "snapshot.cc.json");
3507
- const mapPath = path5.join(outDirectory, "map.html");
4534
+ const snapshotPath = path3.join(outDirectory, "snapshot.cc.json");
4535
+ const mapPath = path3.join(outDirectory, "map.html");
3508
4536
  try {
3509
- fs6.mkdirSync(outDirectory, { recursive: true });
3510
- fs6.writeFileSync(
4537
+ fs4.mkdirSync(outDirectory, { recursive: true });
4538
+ fs4.writeFileSync(
3511
4539
  snapshotPath,
3512
4540
  `${JSON.stringify(cc, null, 2)}
3513
4541
  `,
@@ -3529,7 +4557,7 @@ function runAnalyze(options) {
3529
4557
  title: options.title
3530
4558
  });
3531
4559
  const html = renderHtml(model);
3532
- fs6.writeFileSync(mapPath, html, "utf8");
4560
+ fs4.writeFileSync(mapPath, html, "utf8");
3533
4561
  mapBytes = Buffer.byteLength(html, "utf8");
3534
4562
  } catch (error) {
3535
4563
  mapError = error instanceof RenderError ? error.message : error.message;
@@ -3538,14 +4566,14 @@ function runAnalyze(options) {
3538
4566
  mapError = "no hay ficheros TypeScript/JavaScript que dibujar";
3539
4567
  }
3540
4568
  const snapshotBytes = Buffer.byteLength(
3541
- fs6.readFileSync(snapshotPath),
4569
+ fs4.readFileSync(snapshotPath),
3542
4570
  "utf8"
3543
4571
  );
3544
4572
  const lines = [];
3545
4573
  lines.push("");
3546
4574
  lines.push(" CodArch \xB7 an\xE1lisis");
3547
4575
  lines.push("");
3548
- lines.push(field("ra\xEDz", path5.resolve(root)));
4576
+ lines.push(field("ra\xEDz", path3.resolve(root)));
3549
4577
  lines.push(field("snapshot", `${snapshotPath} (${formatBytes(snapshotBytes)})`));
3550
4578
  if (mapError) {
3551
4579
  lines.push(field("mapa", `omitido (${mapError})`));
@@ -3615,11 +4643,11 @@ function runAnalyze(options) {
3615
4643
  }
3616
4644
 
3617
4645
  // apps/cli/src/commands/diff.ts
3618
- import fs8 from "node:fs";
3619
- import path6 from "node:path";
4646
+ import fs6 from "node:fs";
4647
+ import path4 from "node:path";
3620
4648
 
3621
4649
  // packages/metrics/src/config.ts
3622
- import fs7 from "node:fs";
4650
+ import fs5 from "node:fs";
3623
4651
  import { parse as parseYaml } from "yaml";
3624
4652
  var ConfigError = class extends Error {
3625
4653
  constructor(message) {
@@ -3633,15 +4661,108 @@ function parseRuleConfig(input) {
3633
4661
  if (root.version !== 1) {
3634
4662
  throw new ConfigError("config.version debe ser 1");
3635
4663
  }
3636
- const layers = parseLayers(root.layers);
4664
+ const own = parseInlineProfiles(root.profiles);
4665
+ const profile = parseProfile(root.profile, own);
4666
+ const profileSeverity = parseProfileSeverity(root.profile_severity);
4667
+ const layers = mergeLayers(profile, parseLayers(root.layers));
3637
4668
  const layerNames = new Set(layers.map((layer) => layer.name));
3638
- const rules = parseRules(root.rules, layerNames);
3639
- return { version: 1, layers, rules };
4669
+ const rules = mergeRules(
4670
+ profile,
4671
+ profileSeverity,
4672
+ parseRules(root.rules, layerNames),
4673
+ layerNames
4674
+ );
4675
+ return { version: 1, profile: profile?.id ?? null, layers, rules };
4676
+ }
4677
+ function parseInlineProfiles(value) {
4678
+ const profiles = /* @__PURE__ */ new Map();
4679
+ if (value === void 0 || value === null) return profiles;
4680
+ if (!Array.isArray(value)) {
4681
+ throw new ConfigError("config.profiles debe ser una lista");
4682
+ }
4683
+ value.forEach((raw, index) => {
4684
+ const entry = asObject(raw, `config.profiles[${index}]`);
4685
+ const id = asString(entry.id, `config.profiles[${index}].id`);
4686
+ const title = asString(entry.title, `config.profiles[${index}].title`);
4687
+ const description = entry.description === void 0 ? "" : asString(entry.description, `config.profiles[${index}].description`);
4688
+ const layers = parseLayers(entry.layers).map((layer) => ({
4689
+ name: layer.name,
4690
+ patterns: layer.patterns
4691
+ }));
4692
+ if (layers.length === 0) {
4693
+ throw new ConfigError(
4694
+ `config.profiles[${index}] necesita al menos una capa`
4695
+ );
4696
+ }
4697
+ const layerNames = new Set(layers.map((layer) => layer.name));
4698
+ const rules = parseRules(entry.rules, layerNames).filter(
4699
+ (rule) => rule.type === "forbidden"
4700
+ );
4701
+ profiles.set(id, {
4702
+ id,
4703
+ title,
4704
+ description,
4705
+ when: `Perfil propio del equipo, definido en ${"el archivo de reglas"}.`,
4706
+ layers,
4707
+ rules
4708
+ });
4709
+ });
4710
+ return profiles;
4711
+ }
4712
+ function parseProfile(value, own) {
4713
+ if (value === void 0 || value === null) return null;
4714
+ const id = asString(value, "config.profile");
4715
+ const profile = own.get(id) ?? findDomainProfile(id);
4716
+ if (!profile) {
4717
+ const available = [...own.keys(), ...domainProfileIds()].join(", ");
4718
+ throw new ConfigError(
4719
+ `config.profile desconocido: ${id}. Disponibles: ${available}`
4720
+ );
4721
+ }
4722
+ return profile;
4723
+ }
4724
+ function parseProfileSeverity(value) {
4725
+ if (value === void 0 || value === null) return "warn";
4726
+ if (typeof value !== "string" || !SEVERITIES.has(value)) {
4727
+ throw new ConfigError(
4728
+ "config.profile_severity debe ser info, warn o error"
4729
+ );
4730
+ }
4731
+ return value;
4732
+ }
4733
+ function mergeLayers(profile, own) {
4734
+ const layers = /* @__PURE__ */ new Map();
4735
+ for (const layer of profile?.layers ?? []) layers.set(layer.name, layer);
4736
+ for (const layer of own) layers.set(layer.name, layer);
4737
+ return [...layers.values()];
4738
+ }
4739
+ function mergeRules(profile, profileSeverity, own, layerNames) {
4740
+ const rules = /* @__PURE__ */ new Map();
4741
+ for (const rule of profile?.rules ?? []) {
4742
+ rules.set(rule.name, {
4743
+ ...rule,
4744
+ severity: profileSeverity,
4745
+ source: "profile"
4746
+ });
4747
+ }
4748
+ for (const rule of own) {
4749
+ rules.set(rule.name, { ...rule, source: "config" });
4750
+ }
4751
+ for (const rule of rules.values()) {
4752
+ if (rule.type !== "forbidden") continue;
4753
+ if (layerNames.has(rule.from) && layerNames.has(rule.to)) continue;
4754
+ if (rule.source === "profile") {
4755
+ throw new ConfigError(
4756
+ `la regla "${rule.name}" del perfil apunta a una capa que no existe: ${rule.from} \u2192 ${rule.to}`
4757
+ );
4758
+ }
4759
+ }
4760
+ return [...rules.values()];
3640
4761
  }
3641
4762
  function loadRuleConfig(file) {
3642
4763
  let text;
3643
4764
  try {
3644
- text = fs7.readFileSync(file, "utf8");
4765
+ text = fs5.readFileSync(file, "utf8");
3645
4766
  } catch (error) {
3646
4767
  throw new ConfigError(
3647
4768
  `no se pudo leer ${file}: ${error.message}`
@@ -3771,29 +4892,29 @@ function parseRules(value, layerNames) {
3771
4892
  });
3772
4893
  return rules;
3773
4894
  }
3774
- function parseSeverity(value, path9) {
4895
+ function parseSeverity(value, path7) {
3775
4896
  if (value === void 0) return "error";
3776
4897
  if (typeof value !== "string" || !SEVERITIES.has(value)) {
3777
- throw new ConfigError(`${path9} debe ser info, warn o error`);
4898
+ throw new ConfigError(`${path7} debe ser info, warn o error`);
3778
4899
  }
3779
4900
  return value;
3780
4901
  }
3781
- function optionalNumber(value, path9) {
4902
+ function optionalNumber(value, path7) {
3782
4903
  if (value === void 0) return void 0;
3783
4904
  if (typeof value !== "number" || !Number.isFinite(value)) {
3784
- throw new ConfigError(`${path9} debe ser un n\xFAmero`);
4905
+ throw new ConfigError(`${path7} debe ser un n\xFAmero`);
3785
4906
  }
3786
4907
  return value;
3787
4908
  }
3788
- function asObject(value, path9) {
4909
+ function asObject(value, path7) {
3789
4910
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
3790
- throw new ConfigError(`${path9} debe ser un objeto`);
4911
+ throw new ConfigError(`${path7} debe ser un objeto`);
3791
4912
  }
3792
4913
  return value;
3793
4914
  }
3794
- function asString(value, path9) {
4915
+ function asString(value, path7) {
3795
4916
  if (typeof value !== "string" || value.trim() === "") {
3796
- throw new ConfigError(`${path9} debe ser un texto no vac\xEDo`);
4917
+ throw new ConfigError(`${path7} debe ser un texto no vac\xEDo`);
3797
4918
  }
3798
4919
  return value.trim();
3799
4920
  }
@@ -3803,6 +4924,7 @@ import { minimatch } from "minimatch";
3803
4924
  function emptyRuleReport() {
3804
4925
  return {
3805
4926
  configured: false,
4927
+ profile: null,
3806
4928
  summary: {
3807
4929
  layers: 0,
3808
4930
  rules: 0,
@@ -3839,6 +4961,7 @@ function evaluateRules(base, head, config) {
3839
4961
  resolved.sort(compareViolations);
3840
4962
  return {
3841
4963
  configured: true,
4964
+ profile: config.profile ?? null,
3842
4965
  summary: {
3843
4966
  layers: config.layers.length,
3844
4967
  rules: config.rules.length,
@@ -3894,6 +5017,7 @@ function evaluateForbidden(rule, input, layers, violations) {
3894
5017
  rule: rule.name,
3895
5018
  type: "forbidden",
3896
5019
  severity: rule.severity,
5020
+ source: rule.source ?? "config",
3897
5021
  message: rule.message ?? `${edge.from} no puede depender de ${edge.to}`,
3898
5022
  files: [edge.from, edge.to],
3899
5023
  edge: {
@@ -3918,6 +5042,7 @@ function evaluateThreshold(rule, input, violations) {
3918
5042
  rule: rule.name,
3919
5043
  type: "threshold",
3920
5044
  severity: rule.severity,
5045
+ source: rule.source ?? "config",
3921
5046
  message: rule.message ?? `${file.path}: ${rule.metric} = ${value} (${limit})`,
3922
5047
  files: [file.path],
3923
5048
  metric: {
@@ -3937,6 +5062,7 @@ function evaluateCycles(rule, cycles, violations) {
3937
5062
  rule: rule.name,
3938
5063
  type: "cycles",
3939
5064
  severity: rule.severity,
5065
+ source: rule.source ?? "config",
3940
5066
  message: rule.message ?? cycleMessage(paths),
3941
5067
  files: paths,
3942
5068
  cycle: {
@@ -3983,7 +5109,7 @@ function diffSnapshots(base, head, options = {}) {
3983
5109
  options.rules
3984
5110
  ) : emptyRuleReport();
3985
5111
  return {
3986
- schemaVersion: "1.0",
5112
+ schemaVersion: "1.2",
3987
5113
  base: summarize(baseIndex),
3988
5114
  head: summarize(headIndex),
3989
5115
  summary: {
@@ -4003,9 +5129,94 @@ function diffSnapshots(base, head, options = {}) {
4003
5129
  files: { added, removed, changed },
4004
5130
  edges: edgeDiff,
4005
5131
  cycles: { introduced: introducedCycles, fixed: fixedCycles },
4006
- hotspots
5132
+ hotspots,
5133
+ migration: buildMigrationReport(baseIndex, headIndex, top)
5134
+ };
5135
+ }
5136
+ function buildMigrationReport(baseIndex, headIndex, top) {
5137
+ const baseLens = baseIndex.cc.lenses?.migration;
5138
+ const headLens = headIndex.cc.lenses?.migration;
5139
+ if (!baseLens && !headLens) return null;
5140
+ const baseNodes = baseLens?.nodes ?? {};
5141
+ const headNodes = headLens?.nodes ?? {};
5142
+ const changes = [];
5143
+ for (const id of /* @__PURE__ */ new Set([...Object.keys(baseNodes), ...Object.keys(headNodes)])) {
5144
+ const base = baseNodes[id];
5145
+ const head = headNodes[id];
5146
+ if (!base && !head) continue;
5147
+ const reference = head ?? base;
5148
+ const baseDebt = base ? base.debt ?? 0 : null;
5149
+ const headDebt = head ? head.debt ?? 0 : null;
5150
+ const delta = baseDebt === null || headDebt === null ? null : headDebt - baseDebt;
5151
+ if (delta === null || delta === 0) continue;
5152
+ changes.push({
5153
+ id,
5154
+ path: headIndex.paths.get(id) ?? baseIndex.paths.get(id) ?? id,
5155
+ language: reference.language,
5156
+ status: reference.status,
5157
+ base: baseDebt,
5158
+ head: headDebt,
5159
+ delta
5160
+ });
5161
+ }
5162
+ const increased = changes.filter((change) => (change.delta ?? 0) > 0).sort(
5163
+ (left, right) => (right.delta ?? 0) - (left.delta ?? 0) || compareByPath(left, right)
5164
+ );
5165
+ const decreased = changes.filter((change) => (change.delta ?? 0) < 0).sort(
5166
+ (left, right) => (left.delta ?? 0) - (right.delta ?? 0) || compareByPath(left, right)
5167
+ );
5168
+ const topDebt = [];
5169
+ for (const [id, node] of Object.entries(headNodes)) {
5170
+ if (!node.debt) continue;
5171
+ topDebt.push({
5172
+ id,
5173
+ path: headIndex.paths.get(id) ?? id,
5174
+ language: node.language,
5175
+ status: node.status,
5176
+ debt: node.debt,
5177
+ rloc: node.rloc,
5178
+ crossings: node.crossings
5179
+ });
5180
+ }
5181
+ topDebt.sort(
5182
+ (left, right) => right.debt - left.debt || compareByPath(left, right)
5183
+ );
5184
+ const baseSummary = baseLens?.summary;
5185
+ const headSummary = headLens?.summary;
5186
+ return {
5187
+ languages: {
5188
+ base: baseSummary?.languages ?? {},
5189
+ head: headSummary?.languages ?? {}
5190
+ },
5191
+ status: {
5192
+ base: baseSummary?.status ?? emptyStatus(),
5193
+ head: headSummary?.status ?? emptyStatus()
5194
+ },
5195
+ debt: {
5196
+ base: baseSummary?.migrationDebt ?? 0,
5197
+ head: headSummary?.migrationDebt ?? 0,
5198
+ delta: (headSummary?.migrationDebt ?? 0) - (baseSummary?.migrationDebt ?? 0)
5199
+ },
5200
+ crossings: {
5201
+ base: baseSummary?.ffiCrossings ?? 0,
5202
+ head: headSummary?.ffiCrossings ?? 0,
5203
+ delta: (headSummary?.ffiCrossings ?? 0) - (baseSummary?.ffiCrossings ?? 0)
5204
+ },
5205
+ bridgeFiles: {
5206
+ base: baseSummary?.bridgeFiles ?? 0,
5207
+ head: headSummary?.bridgeFiles ?? 0,
5208
+ delta: (headSummary?.bridgeFiles ?? 0) - (baseSummary?.bridgeFiles ?? 0)
5209
+ },
5210
+ files: {
5211
+ increased: increased.slice(0, top),
5212
+ decreased: decreased.slice(0, top)
5213
+ },
5214
+ topDebt: topDebt.slice(0, top)
4007
5215
  };
4008
5216
  }
5217
+ function emptyStatus() {
5218
+ return { js: 0, rust: 0, bridge: 0, native: 0 };
5219
+ }
4009
5220
  function indexSnapshot(cc) {
4010
5221
  const paths = /* @__PURE__ */ new Map();
4011
5222
  for (const root of cc.files) {
@@ -4059,19 +5270,19 @@ function summarize(index) {
4059
5270
  }
4060
5271
  function difference(present, absent) {
4061
5272
  const files = [];
4062
- for (const [id, path9] of present.paths) {
5273
+ for (const [id, path7] of present.paths) {
4063
5274
  if (absent.paths.has(id)) continue;
4064
5275
  files.push({
4065
5276
  id,
4066
- path: path9,
5277
+ path: path7,
4067
5278
  metrics: sortedMetrics(present.metrics[id])
4068
5279
  });
4069
5280
  }
4070
- return files.sort(compareFiles);
5281
+ return files.sort(compareByPath);
4071
5282
  }
4072
5283
  function changedFiles(base, head) {
4073
5284
  const changed = [];
4074
- for (const [id, path9] of head.paths) {
5285
+ for (const [id, path7] of head.paths) {
4075
5286
  if (!base.paths.has(id)) continue;
4076
5287
  const baseValues = base.metrics[id] ?? {};
4077
5288
  const headValues = head.metrics[id] ?? {};
@@ -4091,7 +5302,7 @@ function changedFiles(base, head) {
4091
5302
  delta: baseValue !== null && headValue !== null ? headValue - baseValue : null
4092
5303
  });
4093
5304
  }
4094
- if (changes.length > 0) changed.push({ id, path: path9, changes });
5305
+ if (changes.length > 0) changed.push({ id, path: path7, changes });
4095
5306
  }
4096
5307
  return changed.sort(compareChangedFiles);
4097
5308
  }
@@ -4281,7 +5492,7 @@ function finiteNumber(value) {
4281
5492
  function edgeKey(fromId, toId) {
4282
5493
  return `${fromId}\0${toId}`;
4283
5494
  }
4284
- function compareFiles(left, right) {
5495
+ function compareByPath(left, right) {
4285
5496
  return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
4286
5497
  }
4287
5498
  function compareChangedFiles(left, right) {
@@ -4300,6 +5511,209 @@ function compareStrings5(left, right) {
4300
5511
  return left < right ? -1 : left > right ? 1 : 0;
4301
5512
  }
4302
5513
 
5514
+ // packages/metrics/src/suggest.ts
5515
+ var ROOT_FOLDERS = /* @__PURE__ */ new Set([
5516
+ "src",
5517
+ "lib",
5518
+ "libs",
5519
+ "source",
5520
+ "app",
5521
+ "apps",
5522
+ "packages",
5523
+ "pkg"
5524
+ ]);
5525
+ function suggestLayers(cc) {
5526
+ const paths = /* @__PURE__ */ new Map();
5527
+ for (const root of cc.files) {
5528
+ walkFiles([root], (entry) => {
5529
+ paths.set(entry.node.id, relativePath2(root, entry.path));
5530
+ });
5531
+ }
5532
+ const edges = cc.lenses?.dependency?.edges ?? [];
5533
+ const groups = /* @__PURE__ */ new Map();
5534
+ let ungrouped = 0;
5535
+ const groupOf = (path7) => {
5536
+ const parts = path7.split("/").filter(Boolean);
5537
+ if (parts.length === 0) return null;
5538
+ const directory = parts.slice(0, -1);
5539
+ if (directory.length === 0) {
5540
+ ungrouped += 1;
5541
+ return null;
5542
+ }
5543
+ if (directory.length === 1 && ROOT_FOLDERS.has(directory[0])) {
5544
+ ungrouped += 1;
5545
+ return null;
5546
+ }
5547
+ const name = directory.slice(0, 2).join("/");
5548
+ let group = groups.get(name);
5549
+ if (!group) {
5550
+ group = {
5551
+ name,
5552
+ pattern: `${name}/**`,
5553
+ files: 0,
5554
+ dependsOn: /* @__PURE__ */ new Set(),
5555
+ usedBy: /* @__PURE__ */ new Set()
5556
+ };
5557
+ groups.set(name, group);
5558
+ }
5559
+ group.files += 1;
5560
+ return group;
5561
+ };
5562
+ for (const path7 of paths.values()) groupOf(path7);
5563
+ const groupEdges = [];
5564
+ for (const edge of edges) {
5565
+ const fromPath = paths.get(edge.fromId);
5566
+ const toPath = paths.get(edge.toId);
5567
+ if (!fromPath || !toPath) continue;
5568
+ const from = groupOf(fromPath);
5569
+ const to = groupOf(toPath);
5570
+ if (!from || !to || from.name === to.name) continue;
5571
+ from.dependsOn.add(to.name);
5572
+ to.usedBy.add(from.name);
5573
+ groupEdges.push({ from, to });
5574
+ }
5575
+ const depthOf = (name, visiting = /* @__PURE__ */ new Set()) => {
5576
+ const group = groups.get(name);
5577
+ if (!group) return 0;
5578
+ if (visiting.has(name)) return 0;
5579
+ visiting.add(name);
5580
+ let depth = 0;
5581
+ for (const dependency of group.dependsOn) {
5582
+ depth = Math.max(depth, depthOf(dependency, visiting) + 1);
5583
+ }
5584
+ visiting.delete(name);
5585
+ return depth;
5586
+ };
5587
+ const layers = [...groups.values()].map((group) => ({
5588
+ name: group.name,
5589
+ patterns: [group.pattern],
5590
+ files: group.files,
5591
+ dependsOn: [...group.dependsOn].sort(),
5592
+ usedBy: [...group.usedBy].sort(),
5593
+ depth: depthOf(group.name)
5594
+ })).sort((left, right) => left.depth - right.depth || left.name.localeCompare(right.name));
5595
+ const rules = [];
5596
+ for (const layer of layers) {
5597
+ for (const outer of layer.usedBy) {
5598
+ rules.push({
5599
+ name: `${slug(layer.name)}-sin-${slug(outer)}`,
5600
+ type: "forbidden",
5601
+ from: layer.name,
5602
+ to: outer,
5603
+ severity: "warn",
5604
+ message: `${layer.name} no debe depender de ${outer}`
5605
+ });
5606
+ }
5607
+ }
5608
+ rules.sort((left, right) => left.name.localeCompare(right.name));
5609
+ const notes = [];
5610
+ if (ungrouped > 0) {
5611
+ notes.push(
5612
+ `${ungrouped} ficheros en la ra\xEDz o sin carpeta: no forman capa (suelen ser puntos de entrada).`
5613
+ );
5614
+ }
5615
+ if (layers.length === 0) {
5616
+ notes.push("No se ha encontrado ninguna carpeta con c\xF3digo.");
5617
+ }
5618
+ if (layers.length === 1) {
5619
+ notes.push(
5620
+ "Solo hay una capa: no se puede deducir una estructura. Prueba con un perfil del cat\xE1logo."
5621
+ );
5622
+ }
5623
+ const fits = matchProfiles(groups, groupEdges);
5624
+ const best = fits.find((fit) => fit.matched >= 2) ?? null;
5625
+ const profile = best ? findDomainProfile(best.id) ?? null : null;
5626
+ const proposedLayers = profile ? profile.layers.map((layer) => {
5627
+ const group = groupForLayer(layer, groups);
5628
+ return {
5629
+ name: layer.name,
5630
+ patterns: [...layer.patterns],
5631
+ files: group?.files ?? 0,
5632
+ dependsOn: [],
5633
+ usedBy: [],
5634
+ depth: 0
5635
+ };
5636
+ }) : layers;
5637
+ const proposedRules = profile ? profile.rules.map((rule) => ({
5638
+ name: rule.name,
5639
+ type: "forbidden",
5640
+ from: rule.from,
5641
+ to: rule.to,
5642
+ severity: "warn",
5643
+ message: rule.message ?? `${rule.from} no debe depender de ${rule.to}`
5644
+ })) : rules;
5645
+ return {
5646
+ detected: layers,
5647
+ layers: proposedLayers,
5648
+ rules: proposedRules,
5649
+ proposal: profile ? "profile" : "deduced",
5650
+ profile: profile?.id ?? null,
5651
+ ungrouped,
5652
+ notes,
5653
+ fits
5654
+ };
5655
+ }
5656
+ function groupForLayer(layer, groups) {
5657
+ for (const candidate of groups.values()) {
5658
+ if (layer.patterns.some((pattern) => covers(pattern, candidate.name))) {
5659
+ return candidate;
5660
+ }
5661
+ }
5662
+ return void 0;
5663
+ }
5664
+ function matchProfiles(groups, groupEdges) {
5665
+ const totalFiles = [...groups.values()].reduce(
5666
+ (sum, group) => sum + group.files,
5667
+ 0
5668
+ );
5669
+ return DOMAIN_PROFILES.map((profile) => {
5670
+ const layerOf = /* @__PURE__ */ new Map();
5671
+ for (const layer of profile.layers) {
5672
+ const group = [...groups.values()].find(
5673
+ (candidate) => layer.patterns.some((pattern) => covers(pattern, candidate.name))
5674
+ );
5675
+ if (group) layerOf.set(layer.name, group.name);
5676
+ }
5677
+ const covered = [...layerOf.values()].reduce(
5678
+ (sum, name) => sum + (groups.get(name)?.files ?? 0),
5679
+ 0
5680
+ );
5681
+ const broken = [];
5682
+ for (const rule of profile.rules) {
5683
+ const fromGroup = layerOf.get(rule.from);
5684
+ const toGroup = layerOf.get(rule.to);
5685
+ if (!fromGroup || !toGroup) continue;
5686
+ const violates = groupEdges.some(
5687
+ (edge) => edge.from.name === fromGroup && edge.to.name === toGroup
5688
+ );
5689
+ if (violates) broken.push(rule.message ?? `${rule.from} no puede depender de ${rule.to}`);
5690
+ }
5691
+ return {
5692
+ id: profile.id,
5693
+ title: profile.title,
5694
+ matched: layerOf.size,
5695
+ total: profile.layers.length,
5696
+ coverage: totalFiles === 0 ? 0 : Math.round(covered / totalFiles * 100),
5697
+ broken
5698
+ };
5699
+ }).sort(
5700
+ (left, right) => right.matched - left.matched || right.coverage - left.coverage || left.id.localeCompare(right.id)
5701
+ );
5702
+ }
5703
+ function covers(pattern, name) {
5704
+ const prefix = pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
5705
+ return name === prefix || name.startsWith(`${prefix}/`);
5706
+ }
5707
+ function slug(name) {
5708
+ return name.replaceAll("/", "-");
5709
+ }
5710
+ function relativePath2(root, fullPath) {
5711
+ if (fullPath === root.name) return "";
5712
+ const prefix = `${root.name}/`;
5713
+ const stripped = fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
5714
+ return stripped.replace(/^\/+/, "");
5715
+ }
5716
+
4303
5717
  // apps/cli/src/commands/diff.ts
4304
5718
  var numberFormat2 = new Intl.NumberFormat("es-ES");
4305
5719
  function num2(value) {
@@ -4348,13 +5762,13 @@ function runDiff(options) {
4348
5762
  console.error(`no se pudo comparar: ${error.message}`);
4349
5763
  return 1;
4350
5764
  }
4351
- const outPath = path6.resolve(options.out ?? "report.json");
5765
+ const outPath = path4.resolve(options.out ?? "report.json");
4352
5766
  let reportBytes = 0;
4353
5767
  try {
4354
- fs8.mkdirSync(path6.dirname(outPath), { recursive: true });
5768
+ fs6.mkdirSync(path4.dirname(outPath), { recursive: true });
4355
5769
  const text = `${JSON.stringify(report, null, 2)}
4356
5770
  `;
4357
- fs8.writeFileSync(outPath, text, "utf8");
5771
+ fs6.writeFileSync(outPath, text, "utf8");
4358
5772
  reportBytes = Buffer.byteLength(text, "utf8");
4359
5773
  } catch (error) {
4360
5774
  console.error(
@@ -4490,7 +5904,7 @@ function loadSnapshot(file) {
4490
5904
  }
4491
5905
  function defaultConfigPath() {
4492
5906
  for (const file of [".codarch.yml", ".codarch.yaml"]) {
4493
- if (fs8.existsSync(file)) return file;
5907
+ if (fs6.existsSync(file)) return file;
4494
5908
  }
4495
5909
  return void 0;
4496
5910
  }
@@ -4542,7 +5956,7 @@ function runInspect(options) {
4542
5956
  const metric = options.metric ?? defaultMetric(stats);
4543
5957
  const top = metric ? topByMetric(cc, metric, options.top) : [];
4544
5958
  const rootPrefix = cc.files.length === 1 ? `${cc.files[0]?.name}/` : "";
4545
- const displayPath = (path9) => rootPrefix && path9.startsWith(rootPrefix) ? path9.slice(rootPrefix.length) : path9;
5959
+ const displayPath = (path7) => rootPrefix && path7.startsWith(rootPrefix) ? path7.slice(rootPrefix.length) : path7;
4546
5960
  if (options.metric && !stats.metricCoverage.some((entry) => entry.name === options.metric)) {
4547
5961
  console.error(`la m\xE9trica "${options.metric}" no existe en este fichero`);
4548
5962
  return 1;
@@ -4621,8 +6035,8 @@ function runInspect(options) {
4621
6035
  }
4622
6036
 
4623
6037
  // apps/cli/src/commands/render.ts
4624
- import fs9 from "node:fs";
4625
- import path7 from "node:path";
6038
+ import fs7 from "node:fs";
6039
+ import path5 from "node:path";
4626
6040
  function defaultOutputPath(input) {
4627
6041
  const lower = input.toLowerCase();
4628
6042
  const suffixes = [".cc.json.gz", ".cc.json", ".json.gz", ".json", ".gz"];
@@ -4632,7 +6046,7 @@ function defaultOutputPath(input) {
4632
6046
  return `${input}.html`;
4633
6047
  }
4634
6048
  function defaultProjectName(input) {
4635
- let name = path7.basename(input.replace(/\\/g, "/"));
6049
+ let name = path5.basename(input.replace(/\\/g, "/"));
4636
6050
  const lower = name.toLowerCase();
4637
6051
  const suffixes = [".cc.json.gz", ".cc.json", ".json.gz", ".json", ".gz"];
4638
6052
  for (const suffix of suffixes) {
@@ -4699,8 +6113,8 @@ function runRender(options) {
4699
6113
  const elapsed = Date.now() - started;
4700
6114
  const out = options.out ?? defaultOutputPath(options.file);
4701
6115
  try {
4702
- fs9.mkdirSync(path7.dirname(path7.resolve(out)), { recursive: true });
4703
- fs9.writeFileSync(out, html, "utf8");
6116
+ fs7.mkdirSync(path5.dirname(path5.resolve(out)), { recursive: true });
6117
+ fs7.writeFileSync(out, html, "utf8");
4704
6118
  } catch (error) {
4705
6119
  console.error(`no se pudo escribir ${out}: ${error.message}`);
4706
6120
  return 1;
@@ -4741,9 +6155,9 @@ function runRender(options) {
4741
6155
  }
4742
6156
 
4743
6157
  // apps/cli/src/commands/serve.ts
4744
- import fs10 from "node:fs";
6158
+ import fs8 from "node:fs";
4745
6159
  import http from "node:http";
4746
- import path8 from "node:path";
6160
+ import path6 from "node:path";
4747
6161
  import { fileURLToPath } from "node:url";
4748
6162
  var MIME = {
4749
6163
  ".html": "text/html; charset=utf-8",
@@ -4769,7 +6183,7 @@ var IGNORED_BROWSE = /* @__PURE__ */ new Set([
4769
6183
  "target"
4770
6184
  ]);
4771
6185
  function runServe(options) {
4772
- const root = path8.resolve(options.root ?? ".");
6186
+ const root = path6.resolve(options.root ?? ".");
4773
6187
  const webRoot = options.web === false ? null : findWebRoot();
4774
6188
  if (options.web !== false && !webRoot) {
4775
6189
  console.error(
@@ -4784,10 +6198,10 @@ function runServe(options) {
4784
6198
  const startWatcher = () => {
4785
6199
  if (watcher) return null;
4786
6200
  try {
4787
- watcher = fs10.watch(state.root, { recursive: true }, (_event, filename) => {
6201
+ watcher = fs8.watch(state.root, { recursive: true }, (_event, filename) => {
4788
6202
  if (!filename) return;
4789
- const relative = filename.toString().replace(/\\/g, "/");
4790
- if (IGNORED_WATCH.test(relative)) return;
6203
+ const relative2 = filename.toString().replace(/\\/g, "/");
6204
+ if (IGNORED_WATCH.test(relative2)) return;
4791
6205
  scheduleRefresh();
4792
6206
  });
4793
6207
  return null;
@@ -4847,8 +6261,8 @@ function runServe(options) {
4847
6261
  broadcast(clients, "watch", { watch: state.watch });
4848
6262
  };
4849
6263
  const switchRoot = (nextRoot) => {
4850
- const resolved = path8.resolve(nextRoot);
4851
- const stat = fs10.statSync(resolved);
6264
+ const resolved = path6.resolve(nextRoot);
6265
+ const stat = fs8.statSync(resolved);
4852
6266
  if (!stat.isDirectory()) throw new Error("la ruta no es un directorio");
4853
6267
  const next = analyzeState(resolved, state.watch);
4854
6268
  stopWatcher();
@@ -4870,7 +6284,7 @@ function runServe(options) {
4870
6284
  return state;
4871
6285
  };
4872
6286
  const meta = () => ({
4873
- root: path8.basename(state.root),
6287
+ root: path6.basename(state.root),
4874
6288
  rootPath: state.root,
4875
6289
  watch: state.watch,
4876
6290
  files: state.files,
@@ -4942,7 +6356,11 @@ function runServe(options) {
4942
6356
  if (pathname === "/data/report.json") {
4943
6357
  const report = readReport(state.root);
4944
6358
  if (report === null) {
4945
- sendError(response, 404, "no hay .codarch/report.json");
6359
+ response.writeHead(204, {
6360
+ "Cache-Control": "no-store",
6361
+ "X-Content-Type-Options": "nosniff"
6362
+ });
6363
+ response.end();
4946
6364
  return;
4947
6365
  }
4948
6366
  sendJson(response, report);
@@ -5034,15 +6452,15 @@ function analyzeState(root, watch) {
5034
6452
  };
5035
6453
  }
5036
6454
  function browseDirectories(root, requested) {
5037
- const target = requested?.trim() ? path8.resolve(requested) : root;
5038
- const stat = fs10.statSync(target);
6455
+ const target = requested?.trim() ? path6.resolve(requested) : root;
6456
+ const stat = fs8.statSync(target);
5039
6457
  if (!stat.isDirectory()) throw new Error("la ruta no es un directorio");
5040
- const entries = fs10.readdirSync(target, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !IGNORED_BROWSE.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name)).slice(0, 500).map((entry) => ({
6458
+ const entries = fs8.readdirSync(target, { withFileTypes: true }).filter((entry) => entry.isDirectory() && !IGNORED_BROWSE.has(entry.name)).sort((left, right) => left.name.localeCompare(right.name)).slice(0, 500).map((entry) => ({
5041
6459
  name: entry.name,
5042
- path: path8.join(target, entry.name),
6460
+ path: path6.join(target, entry.name),
5043
6461
  kind: "directory"
5044
6462
  }));
5045
- const parent = path8.dirname(target);
6463
+ const parent = path6.dirname(target);
5046
6464
  return {
5047
6465
  path: target,
5048
6466
  parent: parent === target ? null : parent,
@@ -5078,9 +6496,9 @@ function originAllowed(request, options) {
5078
6496
  function findWebRoot() {
5079
6497
  const candidates = [
5080
6498
  fileURLToPath(new URL("./web", import.meta.url)),
5081
- path8.resolve(process.cwd(), "apps", "web", "dist"),
5082
- path8.resolve(
5083
- path8.dirname(fileURLToPath(import.meta.url)),
6499
+ path6.resolve(process.cwd(), "apps", "web", "dist"),
6500
+ path6.resolve(
6501
+ path6.dirname(fileURLToPath(import.meta.url)),
5084
6502
  "..",
5085
6503
  "..",
5086
6504
  "..",
@@ -5089,14 +6507,14 @@ function findWebRoot() {
5089
6507
  )
5090
6508
  ];
5091
6509
  for (const candidate of candidates) {
5092
- if (fs10.existsSync(path8.join(candidate, "index.html"))) return candidate;
6510
+ if (fs8.existsSync(path6.join(candidate, "index.html"))) return candidate;
5093
6511
  }
5094
6512
  return null;
5095
6513
  }
5096
6514
  function readReport(root) {
5097
- const file = path8.join(root, ".codarch", "report.json");
6515
+ const file = path6.join(root, ".codarch", "report.json");
5098
6516
  try {
5099
- return JSON.parse(fs10.readFileSync(file, "utf8"));
6517
+ return JSON.parse(fs8.readFileSync(file, "utf8"));
5100
6518
  } catch (error) {
5101
6519
  if (error.code !== "ENOENT") {
5102
6520
  console.error(`no se pudo leer ${file}: ${error.message}`);
@@ -5113,17 +6531,17 @@ function serveStatic(webRoot, pathname, headOnly, response) {
5113
6531
  response.end();
5114
6532
  return;
5115
6533
  }
5116
- const relative = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
5117
- let file = path8.resolve(webRoot, relative);
5118
- if (!isWithin2(webRoot, file) || !fs10.existsSync(file) || !fs10.statSync(file).isFile()) {
5119
- file = path8.join(webRoot, "index.html");
6534
+ const relative2 = decoded === "/" ? "index.html" : decoded.replace(/^\/+/, "");
6535
+ let file = path6.resolve(webRoot, relative2);
6536
+ if (!isWithin2(webRoot, file) || !fs8.existsSync(file) || !fs8.statSync(file).isFile()) {
6537
+ file = path6.join(webRoot, "index.html");
5120
6538
  }
5121
6539
  try {
5122
- const body = fs10.readFileSync(file);
6540
+ const body = fs8.readFileSync(file);
5123
6541
  response.writeHead(200, {
5124
- "Content-Type": MIME[path8.extname(file).toLowerCase()] ?? "application/octet-stream",
6542
+ "Content-Type": MIME[path6.extname(file).toLowerCase()] ?? "application/octet-stream",
5125
6543
  "Content-Length": body.length,
5126
- "Cache-Control": file.includes(`${path8.sep}assets${path8.sep}`) ? "public, max-age=31536000, immutable" : "no-store",
6544
+ "Cache-Control": file.includes(`${path6.sep}assets${path6.sep}`) ? "public, max-age=31536000, immutable" : "no-store",
5127
6545
  "X-Content-Type-Options": "nosniff"
5128
6546
  });
5129
6547
  if (headOnly) response.end();
@@ -5134,8 +6552,8 @@ function serveStatic(webRoot, pathname, headOnly, response) {
5134
6552
  }
5135
6553
  }
5136
6554
  function isWithin2(root, candidate) {
5137
- const relative = path8.relative(root, candidate);
5138
- return relative === "" || !relative.startsWith("..") && !path8.isAbsolute(relative);
6555
+ const relative2 = path6.relative(root, candidate);
6556
+ return relative2 === "" || !relative2.startsWith("..") && !path6.isAbsolute(relative2);
5139
6557
  }
5140
6558
  function sendJson(response, value) {
5141
6559
  const body = `${JSON.stringify(value)}
@@ -5172,8 +6590,170 @@ data: ${JSON.stringify(data)}
5172
6590
  }
5173
6591
  }
5174
6592
 
6593
+ // apps/cli/src/commands/profiles.ts
6594
+ function runProfiles(json) {
6595
+ if (json) {
6596
+ console.log(
6597
+ JSON.stringify(
6598
+ {
6599
+ profiles: DOMAIN_PROFILES.map((profile) => ({
6600
+ id: profile.id,
6601
+ title: profile.title,
6602
+ description: profile.description,
6603
+ when: profile.when,
6604
+ layers: profile.layers.map((layer) => ({
6605
+ name: layer.name,
6606
+ patterns: layer.patterns
6607
+ })),
6608
+ rules: profile.rules.map((rule) => ({
6609
+ name: rule.name,
6610
+ from: rule.from,
6611
+ to: rule.to,
6612
+ severity: rule.severity,
6613
+ message: rule.message ?? null
6614
+ }))
6615
+ }))
6616
+ },
6617
+ null,
6618
+ 2
6619
+ )
6620
+ );
6621
+ return 0;
6622
+ }
6623
+ console.log("");
6624
+ console.log(" CodArch \xB7 perfiles de arquitectura");
6625
+ console.log("");
6626
+ for (const profile of DOMAIN_PROFILES) {
6627
+ console.log(` ${profile.id} \xB7 ${profile.title}`);
6628
+ console.log(` ${profile.description}`);
6629
+ console.log(` para: ${profile.when}`);
6630
+ console.log(
6631
+ ` capas: ${profile.layers.map((layer) => layer.name).join(", ")}`
6632
+ );
6633
+ for (const rule of profile.rules) {
6634
+ console.log(
6635
+ ` \xB7 ${rule.name}: ${rule.from} no puede depender de ${rule.to} (${rule.severity})`
6636
+ );
6637
+ }
6638
+ console.log("");
6639
+ }
6640
+ console.log(" Para usarlo, en .codarch.yml:");
6641
+ console.log("");
6642
+ console.log(" version: 1");
6643
+ console.log(" profile: capas");
6644
+ console.log("");
6645
+ console.log(' Las reglas del perfil entran como aviso: suben a "error" cuando el c\xF3digo est\xE9 listo.');
6646
+ console.log("");
6647
+ return 0;
6648
+ }
6649
+
6650
+ // apps/cli/src/commands/suggest.ts
6651
+ import { stringify as toYaml } from "yaml";
6652
+ function runSuggest(options) {
6653
+ if (!options.file) {
6654
+ console.error(
6655
+ "falta la ruta del fichero cc.json\n\n codarch suggest <fichero.cc.json[.gz]>"
6656
+ );
6657
+ return 1;
6658
+ }
6659
+ let loaded;
6660
+ try {
6661
+ loaded = readJsonFile2(options.file);
6662
+ } catch (error) {
6663
+ console.error(`no se pudo leer ${options.file}: ${error.message}`);
6664
+ return 1;
6665
+ }
6666
+ let suggestion;
6667
+ try {
6668
+ const cc = parseCcJson(loaded.json);
6669
+ suggestion = suggestLayers(cc);
6670
+ } catch (error) {
6671
+ console.error(`no se pudo leer ${options.file}: ${error.message}`);
6672
+ return 1;
6673
+ }
6674
+ if (options.json) {
6675
+ console.log(
6676
+ JSON.stringify(
6677
+ {
6678
+ detected: suggestion.detected,
6679
+ layers: suggestion.layers,
6680
+ rules: suggestion.rules,
6681
+ proposal: suggestion.proposal,
6682
+ profile: suggestion.profile,
6683
+ ungrouped: suggestion.ungrouped,
6684
+ notes: suggestion.notes,
6685
+ fits: suggestion.fits
6686
+ },
6687
+ null,
6688
+ 2
6689
+ )
6690
+ );
6691
+ return 0;
6692
+ }
6693
+ console.log("");
6694
+ console.log(" CodArch \xB7 propuesta de capas");
6695
+ console.log("");
6696
+ if (suggestion.layers.length === 0) {
6697
+ console.log(" No se ha encontrado ninguna carpeta con c\xF3digo.");
6698
+ for (const note of suggestion.notes) console.log(` ${note}`);
6699
+ console.log("");
6700
+ return 0;
6701
+ }
6702
+ for (const layer of suggestion.detected) {
6703
+ console.log(
6704
+ ` ${layer.name} \xB7 ${layer.files} ficheros \xB7 nivel ${layer.depth}` + (layer.usedBy.length > 0 ? ` \xB7 de aqu\xED dependen: ${layer.usedBy.join(", ")}` : "")
6705
+ );
6706
+ }
6707
+ console.log("");
6708
+ const yaml = toYaml(
6709
+ {
6710
+ version: 1,
6711
+ layers: suggestion.layers.map((layer) => ({
6712
+ name: layer.name,
6713
+ paths: layer.patterns
6714
+ })),
6715
+ rules: suggestion.rules
6716
+ },
6717
+ { lineWidth: 0 }
6718
+ );
6719
+ console.log(" Propuesta para .codarch.yml:");
6720
+ console.log("");
6721
+ console.log(
6722
+ suggestion.proposal === "profile" && suggestion.profile ? ` # Propuesta a partir del perfil "${suggestion.profile}" del cat\xE1logo.` : " # Propuesta deducida del grafo de dependencias del repo."
6723
+ );
6724
+ console.log(
6725
+ yaml.split("\n").map((line) => ` ${line}`).join("\n").trimEnd()
6726
+ );
6727
+ console.log("");
6728
+ const best = suggestion.fits[0];
6729
+ if (best && best.matched > 0) {
6730
+ console.log(" Encaje con el cat\xE1logo de perfiles:");
6731
+ console.log("");
6732
+ for (const fit of suggestion.fits.filter((entry) => entry.matched > 0)) {
6733
+ console.log(
6734
+ ` ${fit.id.padEnd(12)} ${fit.matched} de ${fit.total} capas \xB7 cubre el ${fit.coverage}% del c\xF3digo`
6735
+ );
6736
+ }
6737
+ console.log("");
6738
+ if (best.broken.length > 0) {
6739
+ console.log(
6740
+ ` Con el perfil "${best.id}", el c\xF3digo incumple hoy ${best.broken.length} ${best.broken.length === 1 ? "regla" : "reglas"}:`
6741
+ );
6742
+ for (const message of best.broken) console.log(` \xB7 ${message}`);
6743
+ console.log("");
6744
+ }
6745
+ }
6746
+ for (const note of suggestion.notes) console.log(` ${note}`);
6747
+ console.log("");
6748
+ console.log(
6749
+ ' Las reglas salen como aviso: consultan, no bloquean. Rev\xEDsalas y s\xFAbelas a "error" si encajan.'
6750
+ );
6751
+ console.log("");
6752
+ return 0;
6753
+ }
6754
+
5175
6755
  // apps/cli/src/index.ts
5176
- var VERSION = true ? "0.1.3" : "0.0.0";
6756
+ var VERSION = true ? "0.2.0" : "0.0.0";
5177
6757
  var USAGE = `
5178
6758
  CodArch \xB7 mapa vivo de la arquitectura de tu software
5179
6759
 
@@ -5183,6 +6763,8 @@ Uso:
5183
6763
  codarch render <fichero.cc.json[.gz]> [opciones]
5184
6764
  codarch diff <base.cc.json[.gz]> <head.cc.json[.gz]> [opciones]
5185
6765
  codarch serve [ruta] [opciones]
6766
+ codarch profiles [--json]
6767
+ codarch suggest <fichero.cc.json[.gz]> [--json]
5186
6768
 
5187
6769
  Opciones de analyze:
5188
6770
  -o, --out <dir> directorio de salida (por defecto <ruta>/.codarch)
@@ -5224,6 +6806,14 @@ Opciones de serve:
5224
6806
  --no-web modo API sin servir la app (para Vite u otro frontend)
5225
6807
  -h, --help esta ayuda
5226
6808
 
6809
+ Opciones de profiles:
6810
+ --json salida machine-readable
6811
+ -h, --help esta ayuda
6812
+
6813
+ Opciones de suggest:
6814
+ --json salida machine-readable
6815
+ -h, --help esta ayuda
6816
+
5227
6817
  Ejemplos:
5228
6818
  codarch analyze .
5229
6819
  codarch analyze . -o .codarch --title "Mi proyecto"
@@ -5233,6 +6823,8 @@ Ejemplos:
5233
6823
  codarch diff base.cc.json head.cc.json -o report.json --fail-on cycles
5234
6824
  codarch serve .
5235
6825
  codarch serve . --no-watch
6826
+ codarch profiles
6827
+ codarch suggest .codarch/snapshot.cc.json
5236
6828
  `;
5237
6829
  function parseAnalyze(args) {
5238
6830
  const options = { help: false, ignore: [] };
@@ -5297,6 +6889,30 @@ function parseAnalyze(args) {
5297
6889
  options.root = positional[0] ?? ".";
5298
6890
  return options;
5299
6891
  }
6892
+ function parseSuggest(args) {
6893
+ const options = { json: false };
6894
+ const positional = [];
6895
+ for (let index = 0; index < args.length; index++) {
6896
+ const arg = args[index];
6897
+ if (arg === "--json") {
6898
+ options.json = true;
6899
+ } else if (arg === "--help" || arg === "-h") {
6900
+ console.log(USAGE);
6901
+ return 0;
6902
+ } else if (arg.startsWith("-")) {
6903
+ console.error(`opci\xF3n desconocida: ${arg}`);
6904
+ return 1;
6905
+ } else {
6906
+ positional.push(arg);
6907
+ }
6908
+ }
6909
+ if (positional.length > 1) {
6910
+ console.error(`sobra un argumento: ${positional[1]}`);
6911
+ return 1;
6912
+ }
6913
+ options.file = positional[0];
6914
+ return options;
6915
+ }
5300
6916
  function parseInspect(args) {
5301
6917
  const options = { json: false, top: 5, help: false };
5302
6918
  const positional = [];
@@ -5592,6 +7208,14 @@ function main(argv) {
5592
7208
  }
5593
7209
  return runServe(parsed);
5594
7210
  }
7211
+ if (command === "profiles") {
7212
+ return runProfiles(rest.includes("--json"));
7213
+ }
7214
+ if (command === "suggest") {
7215
+ const parsed = parseSuggest(rest);
7216
+ if (typeof parsed === "number") return parsed;
7217
+ return runSuggest(parsed);
7218
+ }
5595
7219
  console.error(`comando desconocido: ${command}
5596
7220
  ${USAGE}`);
5597
7221
  return 1;