codarch 0.0.0-stage → 0.1.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 ADDED
@@ -0,0 +1,4687 @@
1
+ #!/usr/bin/env node
2
+
3
+ // apps/cli/src/commands/analyze.ts
4
+ import fs7 from "node:fs";
5
+ import path5 from "node:path";
6
+
7
+ // packages/parsers/src/ts/parser.ts
8
+ import Parser from "tree-sitter";
9
+ import TypeScript from "tree-sitter-typescript";
10
+ var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
11
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
12
+ "function_declaration",
13
+ "function_expression",
14
+ "arrow_function",
15
+ "method_definition",
16
+ "generator_function_declaration",
17
+ "generator_function"
18
+ ]);
19
+ var DECISION_TYPES = /* @__PURE__ */ new Set([
20
+ "if_statement",
21
+ "for_statement",
22
+ "for_in_statement",
23
+ "while_statement",
24
+ "do_statement",
25
+ "switch_case",
26
+ "catch_clause",
27
+ "ternary_expression",
28
+ "conditional_expression"
29
+ ]);
30
+ 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).rootNode;
43
+ return {
44
+ metrics: measureSource(source, root),
45
+ imports: collectImports(root)
46
+ };
47
+ }
48
+ };
49
+ function usesTsxGrammar(filePath) {
50
+ const lower = filePath.toLowerCase();
51
+ return lower.endsWith(".tsx") || lower.endsWith(".jsx") || lower.endsWith(".js") || lower.endsWith(".mjs") || lower.endsWith(".cjs");
52
+ }
53
+ function measureSource(source, root) {
54
+ const codeLines = /* @__PURE__ */ new Set();
55
+ const stack = [root];
56
+ let functions = 0;
57
+ let maxComplexity = 0;
58
+ let maxFunctionLength = 0;
59
+ while (stack.length > 0) {
60
+ const node = stack.pop();
61
+ if (node.type === "comment" || node.type === "hash_bang_line") continue;
62
+ if (node.childCount === 0) {
63
+ if (node.text.trim() !== "") {
64
+ for (let row = node.startPosition.row; row <= node.endPosition.row; row++) {
65
+ codeLines.add(row);
66
+ }
67
+ }
68
+ continue;
69
+ }
70
+ if (FUNCTION_TYPES.has(node.type)) {
71
+ functions++;
72
+ const complexity = functionComplexity(node);
73
+ const length = node.endPosition.row - node.startPosition.row + 1;
74
+ if (complexity > maxComplexity) maxComplexity = complexity;
75
+ if (length > maxFunctionLength) maxFunctionLength = length;
76
+ }
77
+ for (const child of node.children) stack.push(child);
78
+ }
79
+ return {
80
+ loc: countLines(source),
81
+ rloc: codeLines.size,
82
+ functions,
83
+ maxComplexity,
84
+ maxFunctionLength
85
+ };
86
+ }
87
+ function functionComplexity(fn) {
88
+ let complexity = 1;
89
+ const body = fn.childForFieldName("body") ?? fn;
90
+ const stack = [];
91
+ for (const child of body.children) stack.push(child);
92
+ while (stack.length > 0) {
93
+ const node = stack.pop();
94
+ if (FUNCTION_TYPES.has(node.type)) continue;
95
+ if (DECISION_TYPES.has(node.type)) complexity++;
96
+ if (node.type === "binary_expression" || node.type === "augmented_assignment_expression") {
97
+ const operator = node.childForFieldName("operator")?.text;
98
+ if (operator && LOGICAL_OPERATORS.has(operator)) complexity++;
99
+ }
100
+ for (const child of node.children) stack.push(child);
101
+ }
102
+ return complexity;
103
+ }
104
+ function countLines(source) {
105
+ if (source.length === 0) return 0;
106
+ let lines = 1;
107
+ for (let index = 0; index < source.length; index++) {
108
+ const code = source.charCodeAt(index);
109
+ if (code === 10) {
110
+ lines++;
111
+ } else if (code === 13 && source.charCodeAt(index + 1) !== 10) {
112
+ lines++;
113
+ }
114
+ }
115
+ const last = source.charCodeAt(source.length - 1);
116
+ return last === 10 || last === 13 ? lines - 1 : lines;
117
+ }
118
+ function collectImports(root) {
119
+ const imports = [];
120
+ const stack = [root];
121
+ while (stack.length > 0) {
122
+ const node = stack.pop();
123
+ if (node.type === "import_statement") {
124
+ const source = findImportSource(node);
125
+ const specifier = source ? stringLiteralValue(source) : null;
126
+ if (specifier !== null) {
127
+ const isRequire = node.namedChildren.some(
128
+ (child) => child.type === "import_require_clause"
129
+ );
130
+ imports.push({
131
+ specifier,
132
+ kind: isRequire ? "require" : "static",
133
+ typeOnly: node.children.some((child) => child.type === "type"),
134
+ line: node.startPosition.row + 1
135
+ });
136
+ }
137
+ } else if (node.type === "export_statement") {
138
+ const source = node.childForFieldName("source");
139
+ const specifier = source ? stringLiteralValue(source) : null;
140
+ if (specifier !== null) {
141
+ imports.push({
142
+ specifier,
143
+ kind: "export",
144
+ typeOnly: node.children.some((child) => child.type === "type"),
145
+ line: node.startPosition.row + 1
146
+ });
147
+ }
148
+ } else if (node.type === "call_expression") {
149
+ const kind = callImportKind(node);
150
+ if (kind) {
151
+ const argumentsNode = node.childForFieldName("arguments");
152
+ const first = argumentsNode?.namedChildren[0];
153
+ const specifier = first ? stringLiteralValue(first) : null;
154
+ if (specifier !== null) {
155
+ imports.push({
156
+ specifier,
157
+ kind,
158
+ typeOnly: false,
159
+ line: node.startPosition.row + 1
160
+ });
161
+ }
162
+ }
163
+ }
164
+ for (let index = node.children.length - 1; index >= 0; index--) {
165
+ stack.push(node.children[index]);
166
+ }
167
+ }
168
+ return imports;
169
+ }
170
+ function findImportSource(node) {
171
+ const direct = node.childForFieldName("source");
172
+ if (direct) return direct;
173
+ const clause = node.namedChildren.find(
174
+ (child) => child.type === "import_require_clause"
175
+ );
176
+ return clause?.childForFieldName("source") ?? null;
177
+ }
178
+ function callImportKind(node) {
179
+ const fn = node.childForFieldName("function");
180
+ if (!fn) return null;
181
+ if (fn.type === "import") return "dynamic";
182
+ if (fn.type === "identifier" && fn.text === "require") return "require";
183
+ return null;
184
+ }
185
+ function stringLiteralValue(node) {
186
+ if (node.type === "string") {
187
+ return decodeStringBody(node.text.slice(1, -1));
188
+ }
189
+ if (node.type === "template_string") {
190
+ const hasSubstitution = node.namedChildren.some(
191
+ (child) => child.type === "template_substitution"
192
+ );
193
+ if (!hasSubstitution) return decodeStringBody(node.text.slice(1, -1));
194
+ }
195
+ return null;
196
+ }
197
+ function decodeStringBody(body) {
198
+ return body.replace(
199
+ /\\(u\{([0-9a-fA-F]+)\}|u([0-9a-fA-F]{4})|x([0-9a-fA-F]{2})|([\s\S]))/g,
200
+ (_match, _escaped, braced, four, two, character) => {
201
+ if (braced) return String.fromCodePoint(Number.parseInt(braced, 16));
202
+ if (four) return String.fromCharCode(Number.parseInt(four, 16));
203
+ if (two) return String.fromCharCode(Number.parseInt(two, 16));
204
+ switch (character) {
205
+ case "n":
206
+ return "\n";
207
+ case "r":
208
+ return "\r";
209
+ case "t":
210
+ return " ";
211
+ case "b":
212
+ return "\b";
213
+ case "f":
214
+ return "\f";
215
+ case "v":
216
+ return "\v";
217
+ case "0":
218
+ return "\0";
219
+ default:
220
+ return character;
221
+ }
222
+ }
223
+ );
224
+ }
225
+
226
+ // packages/parsers/src/resolve.ts
227
+ import fs from "node:fs";
228
+ import path from "node:path";
229
+ import { builtinModules } from "node:module";
230
+ import ts from "typescript";
231
+ var SOURCE_EXTENSIONS = [
232
+ ".ts",
233
+ ".tsx",
234
+ ".mts",
235
+ ".cts",
236
+ ".d.ts",
237
+ ".js",
238
+ ".jsx",
239
+ ".mjs",
240
+ ".cjs"
241
+ ];
242
+ var PACKAGE_MAIN_FIELDS = ["types", "module", "main"];
243
+ var WORKSPACE_SKIP_DIRS = /* @__PURE__ */ new Set([
244
+ ".git",
245
+ "node_modules",
246
+ ".codarch",
247
+ "dist",
248
+ "build",
249
+ "coverage",
250
+ ".next",
251
+ "out",
252
+ "target"
253
+ ]);
254
+ var MAX_WORKSPACE_DEPTH = 8;
255
+ var MAX_RESOLVE_DEPTH = 8;
256
+ var BUILTINS = new Set(
257
+ builtinModules.flatMap((name) => [name, `node:${name}`])
258
+ );
259
+ var ImportResolver = class {
260
+ warnings = [];
261
+ root;
262
+ byPath = /* @__PURE__ */ new Map();
263
+ packageJsonCache = /* @__PURE__ */ new Map();
264
+ configCache = /* @__PURE__ */ new Map();
265
+ workspaces;
266
+ constructor(rootInput, files) {
267
+ this.root = path.resolve(rootInput);
268
+ for (const file of files) this.byPath.set(pathKey(file.absolutePath), file);
269
+ this.workspaces = discoverWorkspaces(this.root, this.warnings);
270
+ }
271
+ resolve(importerPath, specifier) {
272
+ if (BUILTINS.has(specifier) || specifier.startsWith("node:")) {
273
+ return { kind: "external", specifier, builtin: true };
274
+ }
275
+ if (specifier.startsWith(".") || specifier.startsWith("/")) {
276
+ const base = specifier.startsWith("/") ? path.resolve(this.root, `.${specifier}`) : path.resolve(path.dirname(path.resolve(importerPath)), specifier);
277
+ const file = this.resolveAsFileOrDirectory(base);
278
+ return file ? { kind: "internal", file } : { kind: "unresolved", specifier, reason: "no se encontr\xF3 el fichero" };
279
+ }
280
+ const config = this.configFor(path.dirname(path.resolve(importerPath)));
281
+ if (config) {
282
+ const alias = matchAlias(config.aliases, specifier);
283
+ if (alias) {
284
+ for (const target of alias.targets) {
285
+ const file = this.resolveAsFileOrDirectory(target);
286
+ if (file) return { kind: "internal", file };
287
+ }
288
+ return {
289
+ kind: "unresolved",
290
+ specifier,
291
+ reason: "alias de tsconfig sin fichero fuente"
292
+ };
293
+ }
294
+ }
295
+ const workspace = this.matchWorkspace(specifier);
296
+ if (workspace) {
297
+ const file = this.resolveWorkspace(workspace, specifier);
298
+ return file ? { kind: "internal", file } : {
299
+ kind: "unresolved",
300
+ specifier,
301
+ reason: "paquete del workspace sin fichero fuente"
302
+ };
303
+ }
304
+ return { kind: "external", specifier, builtin: false };
305
+ }
306
+ matchWorkspace(specifier) {
307
+ for (const workspace of this.workspaces) {
308
+ if (specifier === workspace.name || specifier.startsWith(`${workspace.name}/`)) {
309
+ return workspace;
310
+ }
311
+ }
312
+ return null;
313
+ }
314
+ resolveWorkspace(workspace, specifier) {
315
+ const subpath = specifier === workspace.name ? "." : `./${specifier.slice(workspace.name.length + 1)}`;
316
+ for (const target of packageExportTargets(workspace.json.exports, subpath)) {
317
+ const file = this.resolveAsFileOrDirectory(
318
+ path.resolve(workspace.dir, target)
319
+ );
320
+ if (file) return file;
321
+ }
322
+ if (subpath === ".") {
323
+ for (const field5 of PACKAGE_MAIN_FIELDS) {
324
+ const value = workspace.json[field5];
325
+ if (typeof value !== "string") continue;
326
+ const file = this.resolveAsFileOrDirectory(
327
+ path.resolve(workspace.dir, value)
328
+ );
329
+ if (file) return file;
330
+ }
331
+ for (const fallback of [
332
+ "src/index.ts",
333
+ "src/index.tsx",
334
+ "index.ts",
335
+ "index.tsx"
336
+ ]) {
337
+ const file = this.resolveAsFileOrDirectory(
338
+ path.resolve(workspace.dir, fallback)
339
+ );
340
+ if (file) return file;
341
+ }
342
+ } else {
343
+ const file = this.resolveAsFileOrDirectory(
344
+ path.resolve(workspace.dir, subpath.slice(2))
345
+ );
346
+ if (file) return file;
347
+ }
348
+ return null;
349
+ }
350
+ resolveAsFileOrDirectory(base, depth = 0) {
351
+ if (depth > MAX_RESOLVE_DEPTH) return null;
352
+ for (const candidate of fileCandidates(base)) {
353
+ const file = this.byPath.get(pathKey(candidate));
354
+ if (file) return file;
355
+ }
356
+ const pkg = this.readPackageJson(base);
357
+ if (pkg) {
358
+ for (const field5 of PACKAGE_MAIN_FIELDS) {
359
+ const value = pkg[field5];
360
+ if (typeof value !== "string") continue;
361
+ const file = this.resolveAsFileOrDirectory(
362
+ path.resolve(base, value),
363
+ depth + 1
364
+ );
365
+ if (file) return file;
366
+ }
367
+ for (const target of packageExportTargets(pkg.exports, ".")) {
368
+ const file = this.resolveAsFileOrDirectory(
369
+ path.resolve(base, target),
370
+ depth + 1
371
+ );
372
+ if (file) return file;
373
+ }
374
+ }
375
+ for (const candidate of indexCandidates(base)) {
376
+ const file = this.byPath.get(pathKey(candidate));
377
+ if (file) return file;
378
+ }
379
+ return null;
380
+ }
381
+ readPackageJson(directory) {
382
+ const key = pathKey(directory);
383
+ if (this.packageJsonCache.has(key)) {
384
+ return this.packageJsonCache.get(key) ?? null;
385
+ }
386
+ const json = readJsonFile(path.join(directory, "package.json"), this.warnings);
387
+ this.packageJsonCache.set(key, json);
388
+ return json;
389
+ }
390
+ configFor(startDirectory) {
391
+ const visited = [];
392
+ let directory = path.resolve(startDirectory);
393
+ while (true) {
394
+ const key = pathKey(directory);
395
+ const cached = this.configCache.get(key);
396
+ if (cached !== void 0) {
397
+ for (const visitedKey of visited) this.configCache.set(visitedKey, cached);
398
+ return cached;
399
+ }
400
+ visited.push(key);
401
+ const configPath = findTsConfig(directory);
402
+ if (configPath) {
403
+ const loaded = loadTsConfig(configPath, this.warnings);
404
+ for (const visitedKey of visited) this.configCache.set(visitedKey, loaded);
405
+ return loaded;
406
+ }
407
+ if (key === pathKey(this.root)) break;
408
+ const parent = path.dirname(directory);
409
+ if (parent === directory || !isWithin(this.root, parent)) break;
410
+ directory = parent;
411
+ }
412
+ for (const visitedKey of visited) this.configCache.set(visitedKey, null);
413
+ return null;
414
+ }
415
+ };
416
+ function pathKey(value) {
417
+ const resolved = path.resolve(value);
418
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
419
+ }
420
+ function isWithin(root, candidate) {
421
+ const relative = path.relative(root, candidate);
422
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
423
+ }
424
+ function fileCandidates(base) {
425
+ const lower = base.toLowerCase();
426
+ if (lower.endsWith(".js")) {
427
+ const stem = base.slice(0, -3);
428
+ return [`${stem}.ts`, `${stem}.tsx`, `${stem}.d.ts`, base];
429
+ }
430
+ if (lower.endsWith(".jsx")) {
431
+ const stem = base.slice(0, -4);
432
+ return [`${stem}.tsx`, `${stem}.d.ts`, base];
433
+ }
434
+ if (lower.endsWith(".mjs")) {
435
+ return [`${base.slice(0, -4)}.mts`, base];
436
+ }
437
+ if (lower.endsWith(".cjs")) {
438
+ return [`${base.slice(0, -4)}.cts`, base];
439
+ }
440
+ if (path.extname(base) !== "") return [base];
441
+ return [
442
+ base,
443
+ ...SOURCE_EXTENSIONS.map((extension) => `${base}${extension}`)
444
+ ];
445
+ }
446
+ function indexCandidates(directory) {
447
+ return SOURCE_EXTENSIONS.map(
448
+ (extension) => path.join(directory, `index${extension}`)
449
+ );
450
+ }
451
+ function findTsConfig(directory) {
452
+ for (const name of ["tsconfig.json", "jsconfig.json"]) {
453
+ const candidate = path.join(directory, name);
454
+ if (fs.existsSync(candidate)) return candidate;
455
+ }
456
+ return null;
457
+ }
458
+ function loadTsConfig(configPath, warnings) {
459
+ const read = ts.readConfigFile(configPath, ts.sys.readFile);
460
+ if (read.error || read.config === void 0) {
461
+ warnings.push(
462
+ `no se pudo leer ${configPath}: ${read.error?.messageText ?? "configuraci\xF3n vac\xEDa"}`
463
+ );
464
+ return null;
465
+ }
466
+ const parsed = ts.parseJsonConfigFileContent(
467
+ read.config,
468
+ ts.sys,
469
+ path.dirname(configPath),
470
+ void 0,
471
+ configPath
472
+ );
473
+ const configDirectory = path.dirname(configPath);
474
+ const baseUrl = path.resolve(configDirectory, parsed.options.baseUrl ?? ".");
475
+ const paths = parsed.options.paths ?? {};
476
+ const aliases = [];
477
+ for (const [pattern, targets] of Object.entries(paths)) {
478
+ if (!Array.isArray(targets)) continue;
479
+ const star = pattern.indexOf("*");
480
+ const prefix = star === -1 ? pattern : pattern.slice(0, star);
481
+ const suffix = star === -1 ? "" : pattern.slice(star + 1);
482
+ const resolvedTargets = targets.filter((target) => typeof target === "string").map((target) => path.resolve(baseUrl, target));
483
+ if (resolvedTargets.length > 0) {
484
+ aliases.push({
485
+ prefix,
486
+ suffix,
487
+ wildcard: star !== -1,
488
+ targets: resolvedTargets
489
+ });
490
+ }
491
+ }
492
+ aliases.sort((left, right) => {
493
+ if (left.wildcard !== right.wildcard) return left.wildcard ? 1 : -1;
494
+ if (right.prefix.length !== left.prefix.length) {
495
+ return right.prefix.length - left.prefix.length;
496
+ }
497
+ return right.suffix.length - left.suffix.length;
498
+ });
499
+ return { configPath, baseUrl, aliases };
500
+ }
501
+ function matchAlias(aliases, specifier) {
502
+ for (const alias of aliases) {
503
+ if (!alias.wildcard) {
504
+ if (specifier === alias.prefix) return { targets: alias.targets };
505
+ continue;
506
+ }
507
+ if (specifier.startsWith(alias.prefix) && specifier.endsWith(alias.suffix) && specifier.length >= alias.prefix.length + alias.suffix.length) {
508
+ const wildcard = specifier.slice(
509
+ alias.prefix.length,
510
+ specifier.length - alias.suffix.length
511
+ );
512
+ return {
513
+ targets: alias.targets.map(
514
+ (target) => target.includes("*") ? target.replace(/\*/g, wildcard) : target
515
+ )
516
+ };
517
+ }
518
+ }
519
+ return null;
520
+ }
521
+ function packageExportTargets(exportsField, subpath) {
522
+ if (typeof exportsField === "string") {
523
+ return subpath === "." ? [exportsField] : [];
524
+ }
525
+ if (Array.isArray(exportsField)) {
526
+ return unique(
527
+ exportsField.flatMap((value) => packageExportTargets(value, subpath))
528
+ );
529
+ }
530
+ if (!isRecord(exportsField)) return [];
531
+ const keys = Object.keys(exportsField);
532
+ if (keys.length === 0) return [];
533
+ const usesSubpaths = keys.some(
534
+ (key) => key === "." || key.startsWith("./")
535
+ );
536
+ if (!usesSubpaths) {
537
+ return subpath === "." ? conditionTargets(exportsField) : [];
538
+ }
539
+ if (exportsField[subpath] !== void 0) {
540
+ return conditionTargets(exportsField[subpath]);
541
+ }
542
+ const wildcardKeys = keys.filter((key) => key.includes("*")).sort((left, right) => right.length - left.length);
543
+ for (const key of wildcardKeys) {
544
+ const wildcard = matchWildcardKey(key, subpath);
545
+ if (wildcard === null) continue;
546
+ return conditionTargets(exportsField[key]).map(
547
+ (target) => target.replace(/\*/g, wildcard)
548
+ );
549
+ }
550
+ return [];
551
+ }
552
+ function conditionTargets(value) {
553
+ if (typeof value === "string") return [value];
554
+ if (Array.isArray(value)) {
555
+ return unique(value.flatMap((item) => conditionTargets(item)));
556
+ }
557
+ if (!isRecord(value)) return [];
558
+ const preferred = ["types", "import", "module", "default", "node", "require"];
559
+ const targets = [];
560
+ for (const key of preferred) {
561
+ if (key in value) targets.push(...conditionTargets(value[key]));
562
+ }
563
+ return unique(targets);
564
+ }
565
+ function matchWildcardKey(pattern, subpath) {
566
+ const star = pattern.indexOf("*");
567
+ if (star === -1) return null;
568
+ const prefix = pattern.slice(0, star);
569
+ const suffix = pattern.slice(star + 1);
570
+ if (!subpath.startsWith(prefix) || !subpath.endsWith(suffix) || subpath.length < prefix.length + suffix.length) {
571
+ return null;
572
+ }
573
+ return subpath.slice(prefix.length, subpath.length - suffix.length);
574
+ }
575
+ function discoverWorkspaces(root, warnings) {
576
+ const rootJson = readJsonFile(path.join(root, "package.json"), warnings);
577
+ if (!rootJson) return [];
578
+ const patterns = workspacePatterns(rootJson.workspaces);
579
+ if (patterns.length === 0) return [];
580
+ const byName = /* @__PURE__ */ new Map();
581
+ const stack = [
582
+ { directory: root, relative: "", depth: 0 }
583
+ ];
584
+ while (stack.length > 0) {
585
+ const current = stack.pop();
586
+ if (current.relative !== "" && patterns.some((pattern) => pattern.test(current.relative))) {
587
+ const json = readJsonFile(
588
+ path.join(current.directory, "package.json"),
589
+ warnings
590
+ );
591
+ if (json && typeof json.name === "string") {
592
+ const existing = byName.get(json.name);
593
+ if (existing) {
594
+ warnings.push(
595
+ `workspace duplicado "${json.name}": ${path.relative(root, existing.dir)} y ${path.relative(root, current.directory)}`
596
+ );
597
+ } else {
598
+ byName.set(json.name, {
599
+ name: json.name,
600
+ dir: current.directory,
601
+ json
602
+ });
603
+ }
604
+ }
605
+ }
606
+ if (current.depth >= MAX_WORKSPACE_DEPTH) continue;
607
+ let entries;
608
+ try {
609
+ entries = fs.readdirSync(current.directory, { withFileTypes: true });
610
+ } catch (error) {
611
+ warnings.push(
612
+ `no se pudo leer ${current.relative || "."}: ${error.message}`
613
+ );
614
+ continue;
615
+ }
616
+ for (const entry of entries) {
617
+ if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
618
+ if (WORKSPACE_SKIP_DIRS.has(entry.name)) continue;
619
+ stack.push({
620
+ directory: path.join(current.directory, entry.name),
621
+ relative: current.relative ? `${current.relative}/${entry.name}` : entry.name,
622
+ depth: current.depth + 1
623
+ });
624
+ }
625
+ }
626
+ return [...byName.values()].sort(
627
+ (left, right) => left.dir < right.dir ? -1 : left.dir > right.dir ? 1 : 0
628
+ );
629
+ }
630
+ function workspacePatterns(value) {
631
+ const raw = Array.isArray(value) ? value : isRecord(value) && Array.isArray(value.packages) ? value.packages : [];
632
+ return raw.filter((item) => typeof item === "string").map(
633
+ (pattern) => globToRegExp(
634
+ pattern.replace(/\\/g, "/").replace(/\/+$/, "").replace(/\/package\.json$/, "")
635
+ )
636
+ );
637
+ }
638
+ function globToRegExp(glob) {
639
+ let source = "^";
640
+ for (let index = 0; index < glob.length; index++) {
641
+ const character = glob[index];
642
+ if (character === "*") {
643
+ if (glob[index + 1] === "*") {
644
+ index++;
645
+ if (glob[index + 1] === "/") {
646
+ index++;
647
+ source += "(?:.*/)?";
648
+ } else {
649
+ source += ".*";
650
+ }
651
+ } else {
652
+ source += "[^/]*";
653
+ }
654
+ } else if (character === "?") {
655
+ source += "[^/]";
656
+ } else {
657
+ source += escapeRegExp(character);
658
+ }
659
+ }
660
+ source += "$";
661
+ return new RegExp(source);
662
+ }
663
+ function escapeRegExp(value) {
664
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
665
+ }
666
+ function readJsonFile(file, warnings) {
667
+ let text;
668
+ try {
669
+ text = fs.readFileSync(file, "utf8");
670
+ } catch (error) {
671
+ if (error.code !== "ENOENT") {
672
+ warnings.push(`no se pudo leer ${file}: ${error.message}`);
673
+ }
674
+ return null;
675
+ }
676
+ try {
677
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
678
+ return isRecord(parsed) ? parsed : null;
679
+ } catch (error) {
680
+ warnings.push(`JSON inv\xE1lido en ${file}: ${error.message}`);
681
+ return null;
682
+ }
683
+ }
684
+ function isRecord(value) {
685
+ return typeof value === "object" && value !== null && !Array.isArray(value);
686
+ }
687
+ function unique(values) {
688
+ return [...new Set(values)];
689
+ }
690
+
691
+ // packages/parsers/src/graph.ts
692
+ function buildGraph(files, links) {
693
+ const nodes = [...new Set(files)].sort(compareStrings);
694
+ const nodeSet = new Set(nodes);
695
+ const edgesByKey = /* @__PURE__ */ new Map();
696
+ for (const link of links) {
697
+ if (!nodeSet.has(link.from) || !nodeSet.has(link.to)) continue;
698
+ const key = `${link.from}\0${link.to}`;
699
+ const existing = edgesByKey.get(key);
700
+ if (existing) {
701
+ existing.weight++;
702
+ if (link.typeOnly) existing.typeOnly++;
703
+ } else {
704
+ edgesByKey.set(key, {
705
+ from: link.from,
706
+ to: link.to,
707
+ weight: 1,
708
+ typeOnly: link.typeOnly ? 1 : 0
709
+ });
710
+ }
711
+ }
712
+ const edges = [...edgesByKey.values()].sort(
713
+ (left, right) => compareStrings(left.from, right.from) || compareStrings(left.to, right.to)
714
+ ).map((edge) => ({ ...edge, isCyclic: false }));
715
+ const adjacency = /* @__PURE__ */ new Map();
716
+ const reverse = /* @__PURE__ */ new Map();
717
+ for (const node of nodes) {
718
+ adjacency.set(node, []);
719
+ reverse.set(node, []);
720
+ }
721
+ for (const edge of edges) {
722
+ adjacency.get(edge.from)?.push(edge.to);
723
+ reverse.get(edge.to)?.push(edge.from);
724
+ }
725
+ for (const neighbors of adjacency.values()) neighbors.sort(compareStrings);
726
+ for (const neighbors of reverse.values()) neighbors.sort(compareStrings);
727
+ const components = stronglyConnectedComponents(nodes, adjacency, reverse);
728
+ const componentOf = /* @__PURE__ */ new Map();
729
+ components.forEach((component, index) => {
730
+ for (const node of component) componentOf.set(node, index);
731
+ });
732
+ const selfLoops = new Set(
733
+ edges.filter((edge) => edge.from === edge.to).map((edge) => edge.from)
734
+ );
735
+ const cycleComponents = /* @__PURE__ */ new Set();
736
+ components.forEach((component, index) => {
737
+ if (component.length > 1 || component.length === 1 && selfLoops.has(component[0])) {
738
+ cycleComponents.add(index);
739
+ }
740
+ });
741
+ const levels = componentLevels(components, edges, componentOf);
742
+ for (const edge of edges) {
743
+ const fromComponent = componentOf.get(edge.from);
744
+ const toComponent = componentOf.get(edge.to);
745
+ edge.isCyclic = fromComponent !== void 0 && fromComponent === toComponent && cycleComponents.has(fromComponent);
746
+ }
747
+ const metrics = /* @__PURE__ */ new Map();
748
+ for (const node of nodes) {
749
+ const component = componentOf.get(node);
750
+ if (component === void 0) continue;
751
+ metrics.set(node, {
752
+ fanIn: reverse.get(node)?.length ?? 0,
753
+ fanOut: adjacency.get(node)?.length ?? 0,
754
+ inCycle: cycleComponents.has(component) ? 1 : 0,
755
+ cycleSize: cycleComponents.has(component) ? components[component]?.length ?? 0 : 0,
756
+ dependencyLevel: levels[component] ?? 0
757
+ });
758
+ }
759
+ return {
760
+ edges,
761
+ metrics,
762
+ cycles: components.filter((_, index) => cycleComponents.has(index))
763
+ };
764
+ }
765
+ function stronglyConnectedComponents(nodes, adjacency, reverse) {
766
+ const visited = /* @__PURE__ */ new Set();
767
+ const order = [];
768
+ for (const start of nodes) {
769
+ if (visited.has(start)) continue;
770
+ visited.add(start);
771
+ const stack = [{ node: start, neighbors: adjacency.get(start) ?? [], index: 0 }];
772
+ while (stack.length > 0) {
773
+ const frame = stack[stack.length - 1];
774
+ if (frame.index < frame.neighbors.length) {
775
+ const next = frame.neighbors[frame.index++];
776
+ if (!visited.has(next)) {
777
+ visited.add(next);
778
+ stack.push({
779
+ node: next,
780
+ neighbors: adjacency.get(next) ?? [],
781
+ index: 0
782
+ });
783
+ }
784
+ } else {
785
+ order.push(frame.node);
786
+ stack.pop();
787
+ }
788
+ }
789
+ }
790
+ const assigned = /* @__PURE__ */ new Set();
791
+ const components = [];
792
+ for (let index = order.length - 1; index >= 0; index--) {
793
+ const start = order[index];
794
+ if (assigned.has(start)) continue;
795
+ const component = [];
796
+ const stack = [start];
797
+ assigned.add(start);
798
+ while (stack.length > 0) {
799
+ const node = stack.pop();
800
+ component.push(node);
801
+ for (const previous of reverse.get(node) ?? []) {
802
+ if (!assigned.has(previous)) {
803
+ assigned.add(previous);
804
+ stack.push(previous);
805
+ }
806
+ }
807
+ }
808
+ component.sort(compareStrings);
809
+ components.push(component);
810
+ }
811
+ components.sort(
812
+ (left, right) => compareStrings(left[0], right[0])
813
+ );
814
+ return components;
815
+ }
816
+ function componentLevels(components, edges, componentOf) {
817
+ const condensation = /* @__PURE__ */ new Map();
818
+ const indegree = new Array(components.length).fill(0);
819
+ for (const edge of edges) {
820
+ const from = componentOf.get(edge.from);
821
+ const to = componentOf.get(edge.to);
822
+ if (from === void 0 || to === void 0 || from === to) continue;
823
+ const neighbors = condensation.get(from) ?? /* @__PURE__ */ new Set();
824
+ if (!neighbors.has(to)) {
825
+ neighbors.add(to);
826
+ condensation.set(from, neighbors);
827
+ indegree[to] = (indegree[to] ?? 0) + 1;
828
+ }
829
+ }
830
+ const levels = new Array(components.length).fill(0);
831
+ const queue = [];
832
+ for (let index = 0; index < components.length; index++) {
833
+ if (indegree[index] === 0) queue.push(index);
834
+ }
835
+ let cursor = 0;
836
+ while (cursor < queue.length) {
837
+ const current = queue[cursor++];
838
+ const neighbors = [...condensation.get(current) ?? []].sort(
839
+ (left, right) => left - right
840
+ );
841
+ for (const next of neighbors) {
842
+ levels[next] = Math.max(levels[next] ?? 0, (levels[current] ?? 0) + 1);
843
+ indegree[next] = (indegree[next] ?? 0) - 1;
844
+ if (indegree[next] === 0) queue.push(next);
845
+ }
846
+ }
847
+ return levels;
848
+ }
849
+ function compareStrings(left, right) {
850
+ return left < right ? -1 : left > right ? 1 : 0;
851
+ }
852
+
853
+ // packages/parsers/src/analyze.ts
854
+ import fs4 from "node:fs";
855
+ import path4 from "node:path";
856
+
857
+ // packages/schema/src/errors.ts
858
+ var CcJsonError = class extends Error {
859
+ path;
860
+ constructor(message, path8) {
861
+ super(`${message} (en ${path8})`);
862
+ this.name = "CcJsonError";
863
+ this.path = path8;
864
+ }
865
+ };
866
+ function isRecord2(value) {
867
+ return typeof value === "object" && value !== null && !Array.isArray(value);
868
+ }
869
+ function requireRecord(value, path8) {
870
+ if (!isRecord2(value)) throw new CcJsonError("se esperaba un objeto", path8);
871
+ return value;
872
+ }
873
+ function requireString(value, path8, field5) {
874
+ if (typeof value !== "string" || value.length === 0) {
875
+ throw new CcJsonError(`"${field5}" debe ser una cadena no vac\xEDa`, path8);
876
+ }
877
+ return value;
878
+ }
879
+
880
+ // packages/schema/src/identity.ts
881
+ import { createHash } from "node:crypto";
882
+ var ID_LENGTH = 16;
883
+ function normalizeName(name) {
884
+ return name.normalize("NFC");
885
+ }
886
+ function pathSegments(path8) {
887
+ const segments = [];
888
+ for (const raw of path8.replace(/\\/g, "/").split("/")) {
889
+ const segment = normalizeName(raw);
890
+ if (segment === "" || segment === ".") continue;
891
+ if (segment === "..") {
892
+ segments.pop();
893
+ continue;
894
+ }
895
+ segments.push(segment);
896
+ }
897
+ return segments;
898
+ }
899
+ function canonicalPath(segments) {
900
+ return `/${segments.map(normalizeName).join("/")}`;
901
+ }
902
+ function idFromCanonicalPath(path8, type = "File") {
903
+ const canonical = path8.startsWith("/") ? path8 : `/${path8}`;
904
+ return createHash("sha256").update(type + canonical, "utf8").digest("hex").slice(0, ID_LENGTH);
905
+ }
906
+ function nodeId(type, segments) {
907
+ return idFromCanonicalPath(canonicalPath(segments), type);
908
+ }
909
+ function fileId(path8) {
910
+ return nodeId("File", pathSegments(path8));
911
+ }
912
+ function folderId(path8) {
913
+ return nodeId("Folder", pathSegments(path8));
914
+ }
915
+
916
+ // packages/schema/src/legacy.ts
917
+ var MAX_DEPTH = 512;
918
+ function isLegacyCcJson(input) {
919
+ if (!isRecord2(input) || "files" in input) return false;
920
+ if ("data" in input) {
921
+ if (!isRecord2(input.data) || "files" in input.data) return false;
922
+ return "nodes" in input.data || typeof input.data.apiVersion === "string" && input.data.apiVersion.startsWith("1");
923
+ }
924
+ return "nodes" in input || typeof input.apiVersion === "string" && input.apiVersion.startsWith("1");
925
+ }
926
+ function normalizeLegacyCcJson(input) {
927
+ const outer = requireRecord(input, "$");
928
+ const wrapped = "data" in outer;
929
+ const payload = wrapped ? requireRecord(outer.data, "$.data") : outer;
930
+ const base = wrapped ? "$.data" : "$";
931
+ const version = payload.apiVersion;
932
+ if (version !== void 0) {
933
+ if (typeof version !== "string" || !/^1(?:\.|$)/.test(version)) {
934
+ throw new CcJsonError(`apiVersion "${String(version)}" no es 1.x`, `${base}.apiVersion`);
935
+ }
936
+ }
937
+ const nodesPath = `${base}.nodes`;
938
+ const nodesRaw = payload.nodes;
939
+ if (!Array.isArray(nodesRaw)) {
940
+ throw new CcJsonError('"nodes" debe ser un array', nodesPath);
941
+ }
942
+ if (nodesRaw.length === 0) {
943
+ throw new CcJsonError("el \xE1rbol est\xE1 vac\xEDo", nodesPath);
944
+ }
945
+ const roots = nodesRaw.map(
946
+ (node, index) => requireRecord(node, `${nodesPath}[${index}]`)
947
+ );
948
+ const rootNames = roots.map(
949
+ (root, index) => readNodeName(root.name, `${nodesPath}[${index}].name`)
950
+ );
951
+ const multipleRoots = roots.length > 1;
952
+ let syntheticName;
953
+ let treeRoot;
954
+ if (multipleRoots) {
955
+ syntheticName = rootNames.includes("root") ? "merged-root" : "root";
956
+ while (rootNames.includes(syntheticName)) syntheticName = `${syntheticName}_`;
957
+ treeRoot = {
958
+ name: syntheticName,
959
+ type: "Folder",
960
+ attributes: {},
961
+ children: roots
962
+ };
963
+ } else {
964
+ treeRoot = roots[0];
965
+ }
966
+ const metrics = {};
967
+ const built = [];
968
+ const ids = /* @__PURE__ */ new Map();
969
+ const buildNode = (value, parentPath, path8, depth) => {
970
+ if (depth > MAX_DEPTH) {
971
+ throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH} superada`, path8);
972
+ }
973
+ const raw = requireRecord(value, path8);
974
+ const name = readNodeName(raw.name, `${path8}.name`);
975
+ const type = raw.type;
976
+ if (type !== "File" && type !== "Folder") {
977
+ throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path8}.type`);
978
+ }
979
+ const fullPath = [...parentPath, name];
980
+ const id = nodeId(type, fullPath.slice(1));
981
+ const previous = ids.get(id);
982
+ if (previous !== void 0) {
983
+ throw new CcJsonError(
984
+ `id duplicado "${id}" para "${previous}" y "${fullPath.join("/")}"`,
985
+ `${path8}.name`
986
+ );
987
+ }
988
+ ids.set(id, fullPath.join("/"));
989
+ built.push({ id, name, fullPath: fullPath.join("/") });
990
+ const node = { id, name, type };
991
+ const attributes = readMetrics(raw.attributes, `${path8}.attributes`);
992
+ if (attributes) metrics[id] = attributes;
993
+ const contentHash = readOptionalString(raw.checksum) ?? readOptionalString(raw.contentHash);
994
+ if (contentHash) node.contentHash = contentHash;
995
+ const link = readOptionalString(raw.link);
996
+ if (link) node.link = link;
997
+ if (raw.children !== void 0) {
998
+ if (!Array.isArray(raw.children)) {
999
+ throw new CcJsonError('"children" debe ser un array', `${path8}.children`);
1000
+ }
1001
+ if (type === "File" && raw.children.length > 0) {
1002
+ throw new CcJsonError("un fichero no puede tener hijos", `${path8}.children`);
1003
+ }
1004
+ if (type === "Folder" && raw.children.length > 0) {
1005
+ node.children = raw.children.map(
1006
+ (child, index) => buildNode(child, fullPath, `${path8}.children[${index}]`, depth + 1)
1007
+ );
1008
+ }
1009
+ }
1010
+ return node;
1011
+ };
1012
+ const files = [buildNode(treeRoot, [], multipleRoots ? "$.roots" : `${nodesPath}[0]`, 0)];
1013
+ const pathIndex = buildPathIndex(built, multipleRoots);
1014
+ const stripNames = /* @__PURE__ */ new Set(["root", ...rootNames]);
1015
+ if (syntheticName) stripNames.add(syntheticName);
1016
+ const edges = [];
1017
+ const edgesRaw = payload.edges;
1018
+ if (edgesRaw !== void 0) {
1019
+ if (!Array.isArray(edgesRaw)) {
1020
+ throw new CcJsonError('"edges" debe ser un array', `${base}.edges`);
1021
+ }
1022
+ edgesRaw.forEach((edge, index) => {
1023
+ const path8 = `${base}.edges[${index}]`;
1024
+ const raw = requireRecord(edge, path8);
1025
+ const fromName = requireString(raw.fromNodeName, `${path8}.fromNodeName`, "fromNodeName");
1026
+ const toName = requireString(raw.toNodeName, `${path8}.toNodeName`, "toNodeName");
1027
+ const normalized = {
1028
+ fromId: resolveEndpoint(fromName, pathIndex, stripNames, `${path8}.fromNodeName`),
1029
+ toId: resolveEndpoint(toName, pathIndex, stripNames, `${path8}.toNodeName`)
1030
+ };
1031
+ const attributes = readMetrics(raw.attributes, `${path8}.attributes`);
1032
+ if (attributes) normalized.attributes = attributes;
1033
+ if (typeof raw.isCyclic === "boolean") normalized.isCyclic = raw.isCyclic;
1034
+ if (typeof raw.isPointingUpwards === "boolean") {
1035
+ normalized.isPointingUpwards = raw.isPointingUpwards;
1036
+ }
1037
+ edges.push(normalized);
1038
+ });
1039
+ }
1040
+ const types = readAttributeTypes(payload.attributeTypes, `${base}.attributeTypes`);
1041
+ const descriptorSections = readDescriptors(
1042
+ payload.attributeDescriptors,
1043
+ `${base}.attributeDescriptors`
1044
+ );
1045
+ const nodeMetricNames = new Set(Object.values(metrics).flatMap((row) => Object.keys(row)));
1046
+ const edgeMetricNames = new Set(
1047
+ edges.flatMap((edge) => Object.keys(edge.attributes ?? {}))
1048
+ );
1049
+ let nodeDescriptors = descriptorSections.nodes;
1050
+ let edgeDescriptors = descriptorSections.edges;
1051
+ if (descriptorSections.flat) {
1052
+ const nodes = {};
1053
+ const edgeMap = {};
1054
+ for (const [name, descriptor] of Object.entries(descriptorSections.flat)) {
1055
+ if (nodeMetricNames.has(name) || !edgeMetricNames.has(name)) nodes[name] = descriptor;
1056
+ if (edgeMetricNames.has(name)) edgeMap[name] = descriptor;
1057
+ }
1058
+ if (Object.keys(nodes).length > 0) nodeDescriptors = nodes;
1059
+ if (Object.keys(edgeMap).length > 0) edgeDescriptors = edgeMap;
1060
+ }
1061
+ const lenses = {};
1062
+ const metricsLens = { attributes: metrics };
1063
+ if (types.nodes && Object.keys(types.nodes).length > 0) {
1064
+ metricsLens.attributeTypes = types.nodes;
1065
+ }
1066
+ if (nodeDescriptors && Object.keys(nodeDescriptors).length > 0) {
1067
+ metricsLens.attributeDescriptors = nodeDescriptors;
1068
+ }
1069
+ if (Object.keys(metrics).length > 0 || metricsLens.attributeTypes !== void 0 || metricsLens.attributeDescriptors !== void 0) {
1070
+ lenses.metrics = metricsLens;
1071
+ }
1072
+ const dependency = {};
1073
+ if (edges.length > 0) dependency.edges = edges;
1074
+ if (types.edges && Object.keys(types.edges).length > 0) {
1075
+ dependency.attributeTypes = types.edges;
1076
+ }
1077
+ if (edgeDescriptors && Object.keys(edgeDescriptors).length > 0) {
1078
+ dependency.attributeDescriptors = edgeDescriptors;
1079
+ }
1080
+ if (Object.keys(dependency).length > 0) lenses.dependency = dependency;
1081
+ const meta = { apiVersion: "2.0" };
1082
+ const projectName = readOptionalString(payload.projectName);
1083
+ if (projectName) meta.projectName = projectName;
1084
+ const commitHash = readOptionalString(payload.commitHash);
1085
+ if (commitHash) meta.commitHash = commitHash;
1086
+ return {
1087
+ meta,
1088
+ files,
1089
+ ...Object.keys(lenses).length > 0 ? { lenses } : {}
1090
+ };
1091
+ }
1092
+ function buildPathIndex(built, multipleRoots) {
1093
+ const index = /* @__PURE__ */ new Map();
1094
+ const add = (key, id) => {
1095
+ if (key !== "" && !index.has(key)) index.set(key, id);
1096
+ };
1097
+ for (const node of built) add(node.fullPath, node.id);
1098
+ for (const node of built) {
1099
+ const segments = node.fullPath.split("/");
1100
+ if (segments.length > 1) add(segments.slice(1).join("/"), node.id);
1101
+ const relative = segments.slice(multipleRoots ? 2 : 1).join("/");
1102
+ if (relative !== "") {
1103
+ add(relative, node.id);
1104
+ add(`root/${relative}`, node.id);
1105
+ } else if (segments.length === 1) {
1106
+ add("root", node.id);
1107
+ }
1108
+ }
1109
+ const nameCounts = /* @__PURE__ */ new Map();
1110
+ for (const node of built) nameCounts.set(node.name, (nameCounts.get(node.name) ?? 0) + 1);
1111
+ for (const node of built) {
1112
+ if (nameCounts.get(node.name) === 1) add(node.name, node.id);
1113
+ }
1114
+ return index;
1115
+ }
1116
+ function resolveEndpoint(endpoint, index, stripNames, path8) {
1117
+ const segments = canonicalSegments(endpoint);
1118
+ if (segments.length === 0) {
1119
+ throw new CcJsonError("extremo de arista vac\xEDo", path8);
1120
+ }
1121
+ const candidates = [segments.join("/")];
1122
+ if (segments.length > 1 && stripNames.has(segments[0])) {
1123
+ candidates.push(segments.slice(1).join("/"));
1124
+ }
1125
+ for (const candidate of candidates) {
1126
+ const id = index.get(candidate);
1127
+ if (id) return id;
1128
+ }
1129
+ const bare = index.get(segments[segments.length - 1]);
1130
+ if (bare) return bare;
1131
+ throw new CcJsonError(`no se pudo resolver el nodo "${endpoint}"`, path8);
1132
+ }
1133
+ function canonicalSegments(value) {
1134
+ const result = [];
1135
+ for (const raw of value.replace(/\\/g, "/").split("/")) {
1136
+ const segment = raw.normalize("NFC");
1137
+ if (segment === "" || segment === ".") continue;
1138
+ if (segment === "..") {
1139
+ result.pop();
1140
+ continue;
1141
+ }
1142
+ result.push(segment);
1143
+ }
1144
+ return result;
1145
+ }
1146
+ function readNodeName(value, path8) {
1147
+ const name = normalizeName(requireString(value, path8, "name"));
1148
+ if (name === "." || name === "..") {
1149
+ throw new CcJsonError('el nombre no puede ser "." ni ".."', path8);
1150
+ }
1151
+ if (name.includes("/")) {
1152
+ throw new CcJsonError('el nombre no puede contener "/"', path8);
1153
+ }
1154
+ return name;
1155
+ }
1156
+ function readOptionalString(value) {
1157
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1158
+ }
1159
+ function readMetrics(value, path8) {
1160
+ if (value === void 0) return void 0;
1161
+ const raw = requireRecord(value, path8);
1162
+ const out = {};
1163
+ for (const [metric, metricValue] of Object.entries(raw)) {
1164
+ if (typeof metricValue !== "number" || !Number.isFinite(metricValue)) {
1165
+ throw new CcJsonError(
1166
+ `la m\xE9trica "${metric}" debe ser un n\xFAmero finito`,
1167
+ `${path8}["${metric}"]`
1168
+ );
1169
+ }
1170
+ out[metric] = metricValue;
1171
+ }
1172
+ return Object.keys(out).length > 0 ? out : void 0;
1173
+ }
1174
+ function readAttributeTypes(value, path8) {
1175
+ if (value === void 0) return {};
1176
+ const raw = requireRecord(value, path8);
1177
+ if ("nodes" in raw || "edges" in raw) {
1178
+ const nodes2 = raw.nodes === void 0 ? void 0 : readTypeMap(raw.nodes, `${path8}.nodes`);
1179
+ const edges = raw.edges === void 0 ? void 0 : readTypeMap(raw.edges, `${path8}.edges`);
1180
+ return {
1181
+ ...nodes2 && Object.keys(nodes2).length > 0 ? { nodes: nodes2 } : {},
1182
+ ...edges && Object.keys(edges).length > 0 ? { edges } : {}
1183
+ };
1184
+ }
1185
+ const nodes = readTypeMap(raw, path8);
1186
+ return Object.keys(nodes).length > 0 ? { nodes } : {};
1187
+ }
1188
+ function readTypeMap(value, path8) {
1189
+ const raw = requireRecord(value, path8);
1190
+ const out = {};
1191
+ for (const [metric, type] of Object.entries(raw)) {
1192
+ if (type !== "absolute" && type !== "relative") {
1193
+ throw new CcJsonError(
1194
+ `el tipo de "${metric}" debe ser "absolute" o "relative"`,
1195
+ `${path8}["${metric}"]`
1196
+ );
1197
+ }
1198
+ out[metric] = type;
1199
+ }
1200
+ return out;
1201
+ }
1202
+ function readDescriptors(value, path8) {
1203
+ if (value === void 0) return {};
1204
+ const raw = requireRecord(value, path8);
1205
+ if ("nodes" in raw || "edges" in raw) {
1206
+ const nodes = raw.nodes === void 0 ? void 0 : readDescriptorMap(raw.nodes, `${path8}.nodes`);
1207
+ const edges = raw.edges === void 0 ? void 0 : readDescriptorMap(raw.edges, `${path8}.edges`);
1208
+ return {
1209
+ ...nodes && Object.keys(nodes).length > 0 ? { nodes } : {},
1210
+ ...edges && Object.keys(edges).length > 0 ? { edges } : {}
1211
+ };
1212
+ }
1213
+ const flat = readDescriptorMap(raw, path8);
1214
+ return Object.keys(flat).length > 0 ? { flat } : {};
1215
+ }
1216
+ function readDescriptorMap(value, path8) {
1217
+ const raw = requireRecord(value, path8);
1218
+ const out = {};
1219
+ for (const [metric, descriptor] of Object.entries(raw)) {
1220
+ const record = requireRecord(descriptor, `${path8}["${metric}"]`);
1221
+ if (record.direction !== void 0 && typeof record.direction !== "number") {
1222
+ throw new CcJsonError(
1223
+ `"direction" de "${metric}" debe ser un n\xFAmero`,
1224
+ `${path8}["${metric}"].direction`
1225
+ );
1226
+ }
1227
+ out[metric] = record;
1228
+ }
1229
+ return out;
1230
+ }
1231
+
1232
+ // packages/schema/src/parse.ts
1233
+ var MAX_DEPTH2 = 512;
1234
+ function parseCcJson(input) {
1235
+ if (!isRecord2(input)) throw new CcJsonError("se esperaba un objeto", "$");
1236
+ if (isLegacyCcJson(input)) return normalizeLegacyCcJson(input);
1237
+ const metaRaw = requireRecord(input.meta, "$.meta");
1238
+ const apiVersion = requireString(metaRaw.apiVersion, "$.meta.apiVersion", "apiVersion");
1239
+ if (!apiVersion.startsWith("2.")) {
1240
+ throw new CcJsonError(
1241
+ `apiVersion "${apiVersion}" no soportada (se espera 2.x)`,
1242
+ "$.meta.apiVersion"
1243
+ );
1244
+ }
1245
+ const filesRaw = input.files;
1246
+ if (filesRaw === void 0) throw new CcJsonError('falta "files"', "$");
1247
+ const roots = Array.isArray(filesRaw) ? filesRaw : [filesRaw];
1248
+ if (roots.length === 0) throw new CcJsonError("el \xE1rbol est\xE1 vac\xEDo", "$.files");
1249
+ const ids = /* @__PURE__ */ new Set();
1250
+ const validateNode = (value, path8, depth) => {
1251
+ if (depth > MAX_DEPTH2) throw new CcJsonError(`profundidad m\xE1xima ${MAX_DEPTH2} superada`, path8);
1252
+ const raw = requireRecord(value, path8);
1253
+ const id = requireString(raw.id, `${path8}.id`, "id");
1254
+ if (ids.has(id)) throw new CcJsonError(`id duplicado "${id}"`, `${path8}.id`);
1255
+ ids.add(id);
1256
+ const name = requireString(raw.name, `${path8}.name`, "name");
1257
+ const type = raw.type;
1258
+ if (type !== "File" && type !== "Folder") {
1259
+ throw new CcJsonError('"type" debe ser "File" o "Folder"', `${path8}.type`);
1260
+ }
1261
+ const node = { id, name, type };
1262
+ if (typeof raw.contentHash === "string") node.contentHash = raw.contentHash;
1263
+ if (typeof raw.link === "string") node.link = raw.link;
1264
+ if (raw.children !== void 0) {
1265
+ if (!Array.isArray(raw.children)) {
1266
+ throw new CcJsonError('"children" debe ser un array', `${path8}.children`);
1267
+ }
1268
+ node.children = raw.children.map(
1269
+ (child, index) => validateNode(child, `${path8}.children[${index}]`, depth + 1)
1270
+ );
1271
+ }
1272
+ return node;
1273
+ };
1274
+ const files = roots.map((root, index) => validateNode(root, `$.files[${index}]`, 0));
1275
+ const lensesRaw = input.lenses;
1276
+ if (lensesRaw !== void 0) {
1277
+ const lenses = requireRecord(lensesRaw, "$.lenses");
1278
+ if (lenses.metrics !== void 0) {
1279
+ const metrics = requireRecord(lenses.metrics, "$.lenses.metrics");
1280
+ const attributes = requireRecord(metrics.attributes, "$.lenses.metrics.attributes");
1281
+ for (const [nodeId2, values] of Object.entries(attributes)) {
1282
+ const row = requireRecord(values, `$.lenses.metrics.attributes["${nodeId2}"]`);
1283
+ for (const [metric, value] of Object.entries(row)) {
1284
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1285
+ throw new CcJsonError(
1286
+ `la m\xE9trica "${metric}" debe ser un n\xFAmero finito`,
1287
+ `$.lenses.metrics.attributes["${nodeId2}"].${metric}`
1288
+ );
1289
+ }
1290
+ }
1291
+ }
1292
+ }
1293
+ if (lenses.dependency !== void 0) {
1294
+ const dependency = requireRecord(lenses.dependency, "$.lenses.dependency");
1295
+ if (dependency.edges !== void 0) {
1296
+ if (!Array.isArray(dependency.edges)) {
1297
+ throw new CcJsonError('"edges" debe ser un array', "$.lenses.dependency.edges");
1298
+ }
1299
+ dependency.edges.forEach((edge, index) => {
1300
+ const path8 = `$.lenses.dependency.edges[${index}]`;
1301
+ const raw = requireRecord(edge, path8);
1302
+ requireString(raw.fromId, `${path8}.fromId`, "fromId");
1303
+ requireString(raw.toId, `${path8}.toId`, "toId");
1304
+ });
1305
+ }
1306
+ }
1307
+ }
1308
+ const meta = { ...metaRaw, apiVersion };
1309
+ return {
1310
+ meta,
1311
+ files,
1312
+ ...lensesRaw !== void 0 ? { lenses: lensesRaw } : {}
1313
+ };
1314
+ }
1315
+
1316
+ // packages/schema/src/tree.ts
1317
+ function walkTree(roots, visit) {
1318
+ const stack = roots.map((node) => ({ node, path: node.name, depth: 0 })).reverse();
1319
+ while (stack.length > 0) {
1320
+ const entry = stack.pop();
1321
+ visit(entry);
1322
+ const children = entry.node.children;
1323
+ if (!children || children.length === 0) continue;
1324
+ for (let index = children.length - 1; index >= 0; index--) {
1325
+ const child = children[index];
1326
+ stack.push({
1327
+ node: child,
1328
+ path: `${entry.path}/${child.name}`,
1329
+ depth: entry.depth + 1
1330
+ });
1331
+ }
1332
+ }
1333
+ }
1334
+ function walkFiles(roots, visit) {
1335
+ walkTree(roots, (entry) => {
1336
+ if (entry.node.type === "File") visit(entry);
1337
+ });
1338
+ }
1339
+ function pathById(roots) {
1340
+ const map = /* @__PURE__ */ new Map();
1341
+ walkTree(roots, (entry) => map.set(entry.node.id, entry.path));
1342
+ return map;
1343
+ }
1344
+
1345
+ // packages/schema/src/stats.ts
1346
+ function getStats(cc) {
1347
+ let files = 0;
1348
+ let folders = 0;
1349
+ let maxDepth = 0;
1350
+ walkTree(cc.files, (entry) => {
1351
+ maxDepth = Math.max(maxDepth, entry.depth);
1352
+ if (entry.node.type === "File") files++;
1353
+ else folders++;
1354
+ });
1355
+ const coverage = /* @__PURE__ */ new Map();
1356
+ const attributes = cc.lenses?.metrics?.attributes ?? {};
1357
+ for (const values of Object.values(attributes)) {
1358
+ for (const metric of Object.keys(values)) {
1359
+ coverage.set(metric, (coverage.get(metric) ?? 0) + 1);
1360
+ }
1361
+ }
1362
+ const knownIds = new Set(pathById(cc.files).keys());
1363
+ let orphanMetricNodes = 0;
1364
+ for (const id of Object.keys(attributes)) {
1365
+ if (!knownIds.has(id)) orphanMetricNodes++;
1366
+ }
1367
+ const dependency = cc.lenses?.dependency;
1368
+ return {
1369
+ files,
1370
+ folders,
1371
+ maxDepth,
1372
+ lenses: Object.keys(cc.lenses ?? {}),
1373
+ metricNames: coverage.size,
1374
+ metricCoverage: [...coverage.entries()].map(([name, count]) => ({ name, files: count })).sort((a, b) => b.files - a.files || a.name.localeCompare(b.name)),
1375
+ edges: dependency?.edges?.length ?? 0,
1376
+ leafEdges: dependency?.leafEdges?.length ?? 0,
1377
+ orphanMetricNodes
1378
+ };
1379
+ }
1380
+ function topByMetric(cc, metric, limit = 10) {
1381
+ const attributes = cc.lenses?.metrics?.attributes ?? {};
1382
+ const paths = pathById(cc.files);
1383
+ const ranked = [];
1384
+ for (const [id, values] of Object.entries(attributes)) {
1385
+ const value = values[metric];
1386
+ const path8 = paths.get(id);
1387
+ if (value === void 0 || path8 === void 0) continue;
1388
+ ranked.push({ id, path: path8, value });
1389
+ }
1390
+ ranked.sort((a, b) => b.value - a.value || a.path.localeCompare(b.path));
1391
+ return ranked.slice(0, limit);
1392
+ }
1393
+ function defaultMetric(stats) {
1394
+ if (stats.metricCoverage.some((metric) => metric.name === "rloc")) return "rloc";
1395
+ return stats.metricCoverage[0]?.name;
1396
+ }
1397
+
1398
+ // packages/parsers/src/git.ts
1399
+ import { spawnSync } from "node:child_process";
1400
+ import fs2 from "node:fs";
1401
+ import path2 from "node:path";
1402
+ var COMMIT_PREFIX = "__CODARCH_COMMIT__";
1403
+ var DEFAULT_MAX_BUFFER = 128 * 1024 * 1024;
1404
+ var EMPTY = {
1405
+ metrics: /* @__PURE__ */ new Map(),
1406
+ commits: 0,
1407
+ authors: 0,
1408
+ shallow: false,
1409
+ enabled: false,
1410
+ warnings: []
1411
+ };
1412
+ function collectGitChurn(rootInput, files, options = {}) {
1413
+ if (options.enabled === false || files.length === 0) {
1414
+ return { ...EMPTY, metrics: /* @__PURE__ */ new Map() };
1415
+ }
1416
+ const root = realPath(rootInput);
1417
+ const maxBuffer = options.maxBuffer ?? DEFAULT_MAX_BUFFER;
1418
+ const warnings = [];
1419
+ const topLevel = runGit(root, ["rev-parse", "--show-toplevel"], maxBuffer);
1420
+ if (!topLevel.ok) {
1421
+ return {
1422
+ ...EMPTY,
1423
+ metrics: /* @__PURE__ */ new Map(),
1424
+ warnings: [`no hay historial git utilizable: ${topLevel.error}`]
1425
+ };
1426
+ }
1427
+ const repoRoot = realPath(topLevel.stdout.trim());
1428
+ const shallowResult = runGit(
1429
+ repoRoot,
1430
+ ["rev-parse", "--is-shallow-repository"],
1431
+ maxBuffer
1432
+ );
1433
+ const shallow = shallowResult.ok && shallowResult.stdout.trim().toLowerCase() === "true";
1434
+ if (shallow) {
1435
+ warnings.push(
1436
+ "el repositorio es shallow: el churn puede estar incompleto"
1437
+ );
1438
+ }
1439
+ const log = runGit(
1440
+ repoRoot,
1441
+ [
1442
+ "-c",
1443
+ "core.quotePath=false",
1444
+ "log",
1445
+ "--no-merges",
1446
+ "--numstat",
1447
+ `--format=${COMMIT_PREFIX}%H%x09%ae`
1448
+ ],
1449
+ maxBuffer
1450
+ );
1451
+ if (!log.ok) {
1452
+ return {
1453
+ ...EMPTY,
1454
+ shallow,
1455
+ warnings: [...warnings, `no se pudo leer git log: ${log.error}`]
1456
+ };
1457
+ }
1458
+ const wanted = new Set(files);
1459
+ const commitsByFile = /* @__PURE__ */ new Map();
1460
+ const authorsByFile = /* @__PURE__ */ new Map();
1461
+ const authors = /* @__PURE__ */ new Set();
1462
+ let commits = 0;
1463
+ let currentAuthor = "";
1464
+ let touched = /* @__PURE__ */ new Set();
1465
+ const flushCommit = () => {
1466
+ if (!currentAuthor || touched.size === 0) return;
1467
+ commits++;
1468
+ authors.add(currentAuthor);
1469
+ for (const file of touched) {
1470
+ commitsByFile.set(file, (commitsByFile.get(file) ?? 0) + 1);
1471
+ let fileAuthors = authorsByFile.get(file);
1472
+ if (!fileAuthors) {
1473
+ fileAuthors = /* @__PURE__ */ new Set();
1474
+ authorsByFile.set(file, fileAuthors);
1475
+ }
1476
+ fileAuthors.add(currentAuthor);
1477
+ }
1478
+ touched = /* @__PURE__ */ new Set();
1479
+ currentAuthor = "";
1480
+ };
1481
+ for (const rawLine of log.stdout.split(/\r?\n/)) {
1482
+ if (rawLine.startsWith(COMMIT_PREFIX)) {
1483
+ flushCommit();
1484
+ const fields2 = rawLine.slice(COMMIT_PREFIX.length).split(" ");
1485
+ const author = fields2[1]?.trim() ?? "";
1486
+ currentAuthor = author || "(sin autor)";
1487
+ continue;
1488
+ }
1489
+ if (!currentAuthor || rawLine === "") continue;
1490
+ const fields = rawLine.split(" ");
1491
+ if (fields.length < 3) continue;
1492
+ const gitPath = fields.slice(2).join(" ");
1493
+ if (!gitPath) continue;
1494
+ const relative = path2.relative(root, path2.resolve(repoRoot, gitPath));
1495
+ if (relative === "" || relative === ".." || relative.startsWith(`..${path2.sep}`) || path2.isAbsolute(relative)) {
1496
+ continue;
1497
+ }
1498
+ const normalized = relative.split(path2.sep).join("/");
1499
+ if (!wanted.has(normalized)) continue;
1500
+ touched.add(normalized);
1501
+ }
1502
+ flushCommit();
1503
+ const metrics = /* @__PURE__ */ new Map();
1504
+ for (const file of files) {
1505
+ metrics.set(file, {
1506
+ commits: commitsByFile.get(file) ?? 0,
1507
+ authors: authorsByFile.get(file)?.size ?? 0
1508
+ });
1509
+ }
1510
+ return {
1511
+ metrics,
1512
+ commits,
1513
+ authors: authors.size,
1514
+ shallow,
1515
+ enabled: true,
1516
+ warnings
1517
+ };
1518
+ }
1519
+ function realPath(input) {
1520
+ const resolved = path2.resolve(input);
1521
+ try {
1522
+ return fs2.realpathSync.native(resolved);
1523
+ } catch {
1524
+ return resolved;
1525
+ }
1526
+ }
1527
+ function runGit(cwd, args, maxBuffer) {
1528
+ const result = spawnSync("git", args, {
1529
+ cwd,
1530
+ encoding: "utf8",
1531
+ maxBuffer,
1532
+ windowsHide: true
1533
+ });
1534
+ if (result.error) {
1535
+ return { ok: false, stdout: "", error: result.error.message };
1536
+ }
1537
+ if (result.status !== 0) {
1538
+ return {
1539
+ ok: false,
1540
+ stdout: result.stdout ?? "",
1541
+ error: (result.stderr ?? "").trim() || `git exited with ${result.status}`
1542
+ };
1543
+ }
1544
+ return { ok: true, stdout: result.stdout ?? "", error: "" };
1545
+ }
1546
+
1547
+ // packages/parsers/src/snapshot.ts
1548
+ import { createHash as createHash2 } from "node:crypto";
1549
+ var METRIC_DESCRIPTORS = {
1550
+ loc: {
1551
+ title: "L\xEDneas",
1552
+ description: "L\xEDneas f\xEDsicas del fichero",
1553
+ direction: -1
1554
+ },
1555
+ rloc: {
1556
+ title: "L\xEDneas de c\xF3digo",
1557
+ description: "L\xEDneas con al menos un token de c\xF3digo",
1558
+ direction: -1
1559
+ },
1560
+ functions: {
1561
+ title: "Funciones",
1562
+ description: "Funciones, m\xE9todos y arrows implementados",
1563
+ direction: 0
1564
+ },
1565
+ maxComplexity: {
1566
+ title: "Complejidad m\xE1x.",
1567
+ description: "Complejidad ciclom\xE1tica de la funci\xF3n m\xE1s compleja",
1568
+ direction: -1
1569
+ },
1570
+ maxFunctionLength: {
1571
+ title: "Funci\xF3n m\xE1s larga",
1572
+ description: "L\xEDneas de la funci\xF3n m\xE1s larga",
1573
+ direction: -1
1574
+ },
1575
+ number_of_commits: {
1576
+ title: "Commits",
1577
+ description: "Commits que tocaron el fichero",
1578
+ direction: -1
1579
+ },
1580
+ number_of_authors: {
1581
+ title: "Autores",
1582
+ description: "Autores \xFAnicos que tocaron el fichero",
1583
+ direction: 0
1584
+ },
1585
+ imports: {
1586
+ title: "Imports",
1587
+ description: "Imports y reexports declarados",
1588
+ direction: 0
1589
+ },
1590
+ externalImports: {
1591
+ title: "Imports externos",
1592
+ description: "Paquetes fuera del proyecto, sin builtins de Node",
1593
+ direction: -1
1594
+ },
1595
+ builtinImports: {
1596
+ title: "Builtins",
1597
+ description: "M\xF3dulos nativos de Node",
1598
+ direction: 0
1599
+ },
1600
+ typeOnlyImports: {
1601
+ title: "Imports type-only",
1602
+ description: "Imports que solo existen en tipos",
1603
+ direction: 0
1604
+ },
1605
+ unresolvedImports: {
1606
+ title: "Imports sin resolver",
1607
+ description: "Specifiers internos que no apuntan a un fichero analizado",
1608
+ direction: -1
1609
+ },
1610
+ fanIn: {
1611
+ title: "Fan-in",
1612
+ description: "Ficheros que importan a este",
1613
+ direction: 1
1614
+ },
1615
+ fanOut: {
1616
+ title: "Fan-out",
1617
+ description: "Ficheros importados por este",
1618
+ direction: -1
1619
+ },
1620
+ inCycle: {
1621
+ title: "En ciclo",
1622
+ description: "Pertenece a un ciclo de dependencias",
1623
+ direction: -1
1624
+ },
1625
+ cycleSize: {
1626
+ title: "Tama\xF1o del ciclo",
1627
+ description: "Ficheros en su componente c\xEDclico",
1628
+ direction: -1
1629
+ },
1630
+ dependencyLevel: {
1631
+ title: "Nivel de dependencia",
1632
+ description: "Profundidad en el DAG de dependencias",
1633
+ direction: 0
1634
+ }
1635
+ };
1636
+ var METRIC_ORDER = Object.keys(METRIC_DESCRIPTORS);
1637
+ var EDGE_DESCRIPTORS = {
1638
+ weight: {
1639
+ title: "Peso",
1640
+ description: "N\xFAmero de imports entre los dos ficheros",
1641
+ direction: 0
1642
+ },
1643
+ typeOnly: {
1644
+ title: "Type-only",
1645
+ description: "Imports de solo tipos",
1646
+ direction: 0
1647
+ }
1648
+ };
1649
+ function buildSnapshot(input) {
1650
+ const files = [...input.files].sort(
1651
+ (left, right) => compareStrings2(left.relativePath, right.relativePath)
1652
+ );
1653
+ const rootName = sanitizeNodeName(input.projectName);
1654
+ const root = {
1655
+ id: folderId(""),
1656
+ name: rootName,
1657
+ type: "Folder",
1658
+ children: []
1659
+ };
1660
+ const folders = /* @__PURE__ */ new Map([["", root]]);
1661
+ const attributes = {};
1662
+ const metricNames = /* @__PURE__ */ new Set();
1663
+ for (const file of files) {
1664
+ const segments = file.relativePath.split("/");
1665
+ const fileName = segments.pop();
1666
+ if (!fileName) continue;
1667
+ let parentPath = "";
1668
+ let parent = root;
1669
+ for (const segment of segments) {
1670
+ const folderPath = parentPath ? `${parentPath}/${segment}` : segment;
1671
+ let folder = folders.get(folderPath);
1672
+ if (!folder) {
1673
+ folder = {
1674
+ id: folderId(folderPath),
1675
+ name: segment,
1676
+ type: "Folder",
1677
+ children: []
1678
+ };
1679
+ folders.set(folderPath, folder);
1680
+ parent.children?.push(folder);
1681
+ }
1682
+ parent = folder;
1683
+ parentPath = folderPath;
1684
+ }
1685
+ const node = {
1686
+ id: fileId(file.relativePath),
1687
+ name: fileName,
1688
+ type: "File"
1689
+ };
1690
+ parent.children?.push(node);
1691
+ attributes[node.id] = { ...file.metrics };
1692
+ for (const metric of Object.keys(file.metrics)) metricNames.add(metric);
1693
+ }
1694
+ sortChildren(root);
1695
+ const presentMetrics = METRIC_ORDER.filter((metric) => metricNames.has(metric));
1696
+ for (const metric of metricNames) {
1697
+ if (!presentMetrics.includes(metric)) presentMetrics.push(metric);
1698
+ }
1699
+ const descriptors = {};
1700
+ const attributeTypes = {};
1701
+ for (const metric of presentMetrics) {
1702
+ const descriptor = METRIC_DESCRIPTORS[metric];
1703
+ if (descriptor) descriptors[metric] = descriptor;
1704
+ attributeTypes[metric] = "absolute";
1705
+ }
1706
+ const edges = input.graph.edges.map((edge) => ({
1707
+ fromId: fileId(edge.from),
1708
+ toId: fileId(edge.to),
1709
+ attributes: { weight: edge.weight, typeOnly: edge.typeOnly },
1710
+ isCyclic: edge.isCyclic
1711
+ }));
1712
+ const dependencyNodes = {};
1713
+ for (const [path8, metrics] of input.graph.metrics) {
1714
+ dependencyNodes[fileId(path8)] = { level: metrics.dependencyLevel };
1715
+ }
1716
+ const lenses = {
1717
+ metrics: {
1718
+ attributes,
1719
+ attributeDescriptors: descriptors,
1720
+ attributeTypes
1721
+ },
1722
+ dependency: {
1723
+ edges,
1724
+ nodes: dependencyNodes,
1725
+ attributeDescriptors: EDGE_DESCRIPTORS,
1726
+ attributeTypes: {
1727
+ weight: "absolute",
1728
+ typeOnly: "absolute"
1729
+ }
1730
+ }
1731
+ };
1732
+ return {
1733
+ meta: {
1734
+ projectName: input.projectName,
1735
+ apiVersion: "2.0",
1736
+ checksum: checksumFor([root], lenses)
1737
+ },
1738
+ files: [root],
1739
+ lenses
1740
+ };
1741
+ }
1742
+ function checksumFor(files, lenses) {
1743
+ return createHash2("md5").update(stableStringify({ files, lenses }), "utf8").digest("hex");
1744
+ }
1745
+ function stableStringify(value) {
1746
+ if (value === null || value === void 0) return "null";
1747
+ if (typeof value === "number") {
1748
+ return Number.isFinite(value) ? JSON.stringify(value) : "null";
1749
+ }
1750
+ if (typeof value === "string" || typeof value === "boolean") {
1751
+ return JSON.stringify(value);
1752
+ }
1753
+ if (Array.isArray(value)) {
1754
+ return `[${value.map(stableStringify).join(",")}]`;
1755
+ }
1756
+ if (typeof value === "object") {
1757
+ const record = value;
1758
+ const keys = Object.keys(record).filter((key) => record[key] !== void 0).sort();
1759
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(",")}}`;
1760
+ }
1761
+ return "null";
1762
+ }
1763
+ function sortChildren(node) {
1764
+ if (!node.children) return;
1765
+ node.children.sort((left, right) => {
1766
+ if (left.type !== right.type) return left.type === "Folder" ? -1 : 1;
1767
+ return compareStrings2(left.name, right.name);
1768
+ });
1769
+ for (const child of node.children) sortChildren(child);
1770
+ }
1771
+ function sanitizeNodeName(name) {
1772
+ const sanitized = name.replace(/[\\/]/g, "-").trim();
1773
+ return sanitized && sanitized !== "." && sanitized !== ".." ? sanitized : "Proyecto";
1774
+ }
1775
+ function compareStrings2(left, right) {
1776
+ return left < right ? -1 : left > right ? 1 : 0;
1777
+ }
1778
+
1779
+ // packages/parsers/src/walk.ts
1780
+ import fs3 from "node:fs";
1781
+ import path3 from "node:path";
1782
+ import ignore from "ignore";
1783
+ var DEFAULT_EXTENSIONS = [
1784
+ ".ts",
1785
+ ".tsx",
1786
+ ".mts",
1787
+ ".cts",
1788
+ ".js",
1789
+ ".jsx",
1790
+ ".mjs",
1791
+ ".cjs"
1792
+ ];
1793
+ var ALWAYS_IGNORED = [".git", "node_modules", ".codarch"];
1794
+ function walkSourceFiles(rootInput, options = {}) {
1795
+ const root = path3.resolve(rootInput);
1796
+ const stat = fs3.statSync(root);
1797
+ if (!stat.isDirectory()) {
1798
+ throw new Error(`no es un directorio: ${rootInput}`);
1799
+ }
1800
+ const extensions = options.extensions ?? DEFAULT_EXTENSIONS;
1801
+ const warnings = [];
1802
+ const files = [];
1803
+ const rootMatcher = ignore().add(ALWAYS_IGNORED);
1804
+ for (const pattern of options.ignore ?? []) rootMatcher.add(pattern);
1805
+ const rootGitignore = readGitignore(root, "", warnings);
1806
+ if (rootGitignore) rootMatcher.add(rootGitignore);
1807
+ const initialLayers = [{ base: "", matcher: rootMatcher }];
1808
+ const stack = [
1809
+ { absolutePath: root, relativePath: "", layers: initialLayers }
1810
+ ];
1811
+ while (stack.length > 0) {
1812
+ const directory = stack.pop();
1813
+ let entries;
1814
+ try {
1815
+ entries = fs3.readdirSync(directory.absolutePath, { withFileTypes: true });
1816
+ } catch (error) {
1817
+ warnings.push(
1818
+ `no se pudo leer ${directory.relativePath || "."}: ${error.message}`
1819
+ );
1820
+ continue;
1821
+ }
1822
+ entries.sort(
1823
+ (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
1824
+ );
1825
+ for (const entry of entries) {
1826
+ if (entry.isSymbolicLink()) continue;
1827
+ const relativePath2 = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
1828
+ const absolutePath = path3.join(directory.absolutePath, entry.name);
1829
+ if (entry.isDirectory()) {
1830
+ if (isIgnored(directory.layers, relativePath2, true)) continue;
1831
+ const nested = readGitignore(absolutePath, relativePath2, warnings);
1832
+ const layers = nested ? [...directory.layers, { base: relativePath2, matcher: ignore().add(nested) }] : directory.layers;
1833
+ stack.push({ absolutePath, relativePath: relativePath2, layers });
1834
+ } else if (entry.isFile()) {
1835
+ if (isIgnored(directory.layers, relativePath2, false)) continue;
1836
+ if (!matchesExtension(entry.name, extensions)) continue;
1837
+ files.push({ absolutePath, relativePath: relativePath2 });
1838
+ }
1839
+ }
1840
+ }
1841
+ files.sort(
1842
+ (left, right) => left.relativePath < right.relativePath ? -1 : left.relativePath > right.relativePath ? 1 : 0
1843
+ );
1844
+ warnings.sort();
1845
+ return { files, warnings };
1846
+ }
1847
+ function readGitignore(directory, relativeBase, warnings) {
1848
+ const file = path3.join(directory, ".gitignore");
1849
+ try {
1850
+ return fs3.readFileSync(file, "utf8");
1851
+ } catch (error) {
1852
+ if (error.code !== "ENOENT") {
1853
+ warnings.push(
1854
+ `no se pudo leer ${relativeBase ? `${relativeBase}/` : ""}.gitignore: ${error.message}`
1855
+ );
1856
+ }
1857
+ return null;
1858
+ }
1859
+ }
1860
+ function isIgnored(layers, relativePath2, isDirectory) {
1861
+ let ignored = false;
1862
+ for (const layer of layers) {
1863
+ const localPath = layer.base ? relativePath2.slice(layer.base.length + 1) : relativePath2;
1864
+ if (!localPath) continue;
1865
+ const probe = isDirectory ? `${localPath}/` : localPath;
1866
+ const result = layer.matcher.test(probe);
1867
+ if (result.ignored) ignored = true;
1868
+ else if (result.unignored) ignored = false;
1869
+ }
1870
+ return ignored;
1871
+ }
1872
+ function matchesExtension(name, extensions) {
1873
+ const lower = name.toLowerCase();
1874
+ return extensions.some((extension) => lower.endsWith(extension));
1875
+ }
1876
+
1877
+ // packages/parsers/src/analyze.ts
1878
+ function analyzeRepository(rootInput, options = {}) {
1879
+ const started = Date.now();
1880
+ const root = path4.resolve(rootInput);
1881
+ const projectName = options.projectName ?? readProjectName(root);
1882
+ const walked = walkSourceFiles(root, {
1883
+ ignore: options.ignore,
1884
+ extensions: options.extensions
1885
+ });
1886
+ const warnings = [...walked.warnings];
1887
+ const resolver = new ImportResolver(root, walked.files);
1888
+ warnings.push(...resolver.warnings);
1889
+ const parser = new TypeScriptParser();
1890
+ const parsedFiles = [];
1891
+ const links = [];
1892
+ const unresolved = [];
1893
+ let externalImports = 0;
1894
+ let builtinImports = 0;
1895
+ let typeOnlyImports = 0;
1896
+ for (const file of walked.files) {
1897
+ let source;
1898
+ try {
1899
+ source = fs4.readFileSync(file.absolutePath, "utf8");
1900
+ } catch (error) {
1901
+ warnings.push(
1902
+ `no se pudo leer ${file.relativePath}: ${error.message}`
1903
+ );
1904
+ continue;
1905
+ }
1906
+ let parsed;
1907
+ try {
1908
+ parsed = parser.parse(source, file.relativePath);
1909
+ } catch (error) {
1910
+ warnings.push(
1911
+ `no se pudo analizar ${file.relativePath}: ${error.message}`
1912
+ );
1913
+ continue;
1914
+ }
1915
+ let fileExternalImports = 0;
1916
+ let fileBuiltinImports = 0;
1917
+ let fileTypeOnlyImports = 0;
1918
+ let fileUnresolvedImports = 0;
1919
+ for (const imported of parsed.imports) {
1920
+ if (imported.typeOnly) fileTypeOnlyImports++;
1921
+ const result = resolver.resolve(file.absolutePath, imported.specifier);
1922
+ if (result.kind === "internal") {
1923
+ links.push({
1924
+ from: file.relativePath,
1925
+ to: result.file.relativePath,
1926
+ typeOnly: imported.typeOnly
1927
+ });
1928
+ } else if (result.kind === "external") {
1929
+ if (result.builtin) fileBuiltinImports++;
1930
+ else fileExternalImports++;
1931
+ } else {
1932
+ fileUnresolvedImports++;
1933
+ unresolved.push({
1934
+ file: file.relativePath,
1935
+ line: imported.line,
1936
+ specifier: imported.specifier,
1937
+ reason: result.reason
1938
+ });
1939
+ }
1940
+ }
1941
+ externalImports += fileExternalImports;
1942
+ builtinImports += fileBuiltinImports;
1943
+ typeOnlyImports += fileTypeOnlyImports;
1944
+ parsedFiles.push({
1945
+ relativePath: file.relativePath,
1946
+ metrics: {
1947
+ ...parsed.metrics,
1948
+ imports: parsed.imports.length,
1949
+ externalImports: fileExternalImports,
1950
+ builtinImports: fileBuiltinImports,
1951
+ typeOnlyImports: fileTypeOnlyImports,
1952
+ unresolvedImports: fileUnresolvedImports
1953
+ }
1954
+ });
1955
+ }
1956
+ const git = collectGitChurn(
1957
+ root,
1958
+ parsedFiles.map((file) => file.relativePath),
1959
+ { enabled: options.git !== false }
1960
+ );
1961
+ warnings.push(...git.warnings);
1962
+ if (git.enabled) {
1963
+ for (const file of parsedFiles) {
1964
+ const churn = git.metrics.get(file.relativePath);
1965
+ if (!churn) continue;
1966
+ file.metrics.number_of_commits = churn.commits;
1967
+ file.metrics.number_of_authors = churn.authors;
1968
+ }
1969
+ }
1970
+ const graph = buildGraph(
1971
+ parsedFiles.map((file) => file.relativePath),
1972
+ links
1973
+ );
1974
+ for (const file of parsedFiles) {
1975
+ const metrics = graph.metrics.get(file.relativePath);
1976
+ if (metrics) {
1977
+ file.metrics.fanIn = metrics.fanIn;
1978
+ file.metrics.fanOut = metrics.fanOut;
1979
+ file.metrics.inCycle = metrics.inCycle;
1980
+ file.metrics.cycleSize = metrics.cycleSize;
1981
+ file.metrics.dependencyLevel = metrics.dependencyLevel;
1982
+ }
1983
+ }
1984
+ if (unresolved.length > 0) {
1985
+ warnings.push(
1986
+ `${unresolved.length} imports sin resolver; se excluyen del grafo`
1987
+ );
1988
+ }
1989
+ const snapshot = buildSnapshot({
1990
+ projectName,
1991
+ files: parsedFiles,
1992
+ graph
1993
+ });
1994
+ let folders = 0;
1995
+ walkTree(snapshot.files, (entry) => {
1996
+ if (entry.node.type === "Folder" && entry.depth > 0) folders++;
1997
+ });
1998
+ const metricNames = /* @__PURE__ */ new Set();
1999
+ for (const file of parsedFiles) {
2000
+ for (const metric of Object.keys(file.metrics)) metricNames.add(metric);
2001
+ }
2002
+ warnings.sort();
2003
+ return {
2004
+ snapshot,
2005
+ files: parsedFiles.length,
2006
+ folders,
2007
+ edges: graph.edges.length,
2008
+ cycles: graph.cycles,
2009
+ metricNames: [...metricNames].sort(),
2010
+ warnings,
2011
+ unresolved,
2012
+ externalImports,
2013
+ builtinImports,
2014
+ typeOnlyImports,
2015
+ unresolvedImports: unresolved.length,
2016
+ git: {
2017
+ enabled: git.enabled,
2018
+ commits: git.commits,
2019
+ authors: git.authors,
2020
+ shallow: git.shallow
2021
+ },
2022
+ durationMs: Date.now() - started
2023
+ };
2024
+ }
2025
+ function readProjectName(root) {
2026
+ try {
2027
+ const text = fs4.readFileSync(path4.join(root, "package.json"), "utf8");
2028
+ const parsed = JSON.parse(text.replace(/^\uFEFF/, ""));
2029
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) && typeof parsed.name === "string") {
2030
+ const name = parsed.name.trim();
2031
+ if (name) return name;
2032
+ }
2033
+ } catch {
2034
+ }
2035
+ return path4.basename(root) || "Proyecto";
2036
+ }
2037
+
2038
+ // packages/metrics/src/config.ts
2039
+ import fs5 from "node:fs";
2040
+ import { parse as parseYaml } from "yaml";
2041
+ var ConfigError = class extends Error {
2042
+ constructor(message) {
2043
+ super(message);
2044
+ this.name = "ConfigError";
2045
+ }
2046
+ };
2047
+ var SEVERITIES = /* @__PURE__ */ new Set(["info", "warn", "error"]);
2048
+ function parseRuleConfig(input) {
2049
+ const root = asObject(input, "config");
2050
+ if (root.version !== 1) {
2051
+ throw new ConfigError("config.version debe ser 1");
2052
+ }
2053
+ const layers = parseLayers(root.layers);
2054
+ const layerNames = new Set(layers.map((layer) => layer.name));
2055
+ const rules = parseRules(root.rules, layerNames);
2056
+ return { version: 1, layers, rules };
2057
+ }
2058
+ function loadRuleConfig(file) {
2059
+ let text;
2060
+ try {
2061
+ text = fs5.readFileSync(file, "utf8");
2062
+ } catch (error) {
2063
+ throw new ConfigError(
2064
+ `no se pudo leer ${file}: ${error.message}`
2065
+ );
2066
+ }
2067
+ let value;
2068
+ try {
2069
+ value = parseYaml(text);
2070
+ } catch (error) {
2071
+ throw new ConfigError(
2072
+ `YAML inv\xE1lido en ${file}: ${error.message}`
2073
+ );
2074
+ }
2075
+ try {
2076
+ return parseRuleConfig(value);
2077
+ } catch (error) {
2078
+ if (error instanceof ConfigError) {
2079
+ throw new ConfigError(`${file}: ${error.message}`);
2080
+ }
2081
+ throw error;
2082
+ }
2083
+ }
2084
+ function parseLayers(value) {
2085
+ if (value === void 0) return [];
2086
+ if (!Array.isArray(value)) {
2087
+ throw new ConfigError("config.layers debe ser una lista");
2088
+ }
2089
+ const layers = [];
2090
+ const names = /* @__PURE__ */ new Set();
2091
+ value.forEach((raw, index) => {
2092
+ const layer = asObject(raw, `config.layers[${index}]`);
2093
+ const name = asString(layer.name, `config.layers[${index}].name`);
2094
+ if (names.has(name)) {
2095
+ throw new ConfigError(`capa duplicada: ${name}`);
2096
+ }
2097
+ names.add(name);
2098
+ if (layer.patterns !== void 0 && layer.paths !== void 0) {
2099
+ throw new ConfigError(
2100
+ `config.layers[${index}] no puede tener patterns y paths a la vez`
2101
+ );
2102
+ }
2103
+ const rawPatterns = layer.patterns ?? layer.paths;
2104
+ if (!Array.isArray(rawPatterns) || rawPatterns.length === 0) {
2105
+ throw new ConfigError(
2106
+ `config.layers[${index}].patterns debe ser una lista no vac\xEDa`
2107
+ );
2108
+ }
2109
+ const patterns = rawPatterns.map((rawPattern, patternIndex) => {
2110
+ const pattern = asString(
2111
+ rawPattern,
2112
+ `config.layers[${index}].patterns[${patternIndex}]`
2113
+ ).replaceAll("\\", "/").replace(/^\.\//, "");
2114
+ if (pattern.startsWith("/") || /^[A-Za-z]:\//.test(pattern)) {
2115
+ throw new ConfigError(
2116
+ `el patr\xF3n "${pattern}" debe ser relativo al root del proyecto`
2117
+ );
2118
+ }
2119
+ return pattern;
2120
+ });
2121
+ layers.push({ name, patterns });
2122
+ });
2123
+ return layers;
2124
+ }
2125
+ function parseRules(value, layerNames) {
2126
+ if (value === void 0) return [];
2127
+ if (!Array.isArray(value)) {
2128
+ throw new ConfigError("config.rules debe ser una lista");
2129
+ }
2130
+ const rules = [];
2131
+ const names = /* @__PURE__ */ new Set();
2132
+ value.forEach((raw, index) => {
2133
+ const rule = asObject(raw, `config.rules[${index}]`);
2134
+ const name = asString(rule.name, `config.rules[${index}].name`);
2135
+ if (names.has(name)) {
2136
+ throw new ConfigError(`regla duplicada: ${name}`);
2137
+ }
2138
+ names.add(name);
2139
+ const type = asString(rule.type, `config.rules[${index}].type`);
2140
+ const severity = parseSeverity(
2141
+ rule.severity,
2142
+ `config.rules[${index}].severity`
2143
+ );
2144
+ const message = rule.message === void 0 ? void 0 : asString(rule.message, `config.rules[${index}].message`);
2145
+ if (type === "forbidden") {
2146
+ const from = asString(rule.from, `config.rules[${index}].from`);
2147
+ const to = asString(rule.to, `config.rules[${index}].to`);
2148
+ if (!layerNames.has(from)) {
2149
+ throw new ConfigError(
2150
+ `config.rules[${index}].from apunta a una capa inexistente: ${from}`
2151
+ );
2152
+ }
2153
+ if (!layerNames.has(to)) {
2154
+ throw new ConfigError(
2155
+ `config.rules[${index}].to apunta a una capa inexistente: ${to}`
2156
+ );
2157
+ }
2158
+ rules.push({ type, name, from, to, severity, message });
2159
+ return;
2160
+ }
2161
+ if (type === "threshold") {
2162
+ const metric = asString(
2163
+ rule.metric,
2164
+ `config.rules[${index}].metric`
2165
+ );
2166
+ const min = optionalNumber(rule.min, `config.rules[${index}].min`);
2167
+ const max = optionalNumber(rule.max, `config.rules[${index}].max`);
2168
+ if (min === void 0 && max === void 0) {
2169
+ throw new ConfigError(
2170
+ `config.rules[${index}] necesita min o max`
2171
+ );
2172
+ }
2173
+ if (min !== void 0 && max !== void 0 && min > max) {
2174
+ throw new ConfigError(
2175
+ `config.rules[${index}]: min no puede ser mayor que max`
2176
+ );
2177
+ }
2178
+ rules.push({ type, name, metric, min, max, severity, message });
2179
+ return;
2180
+ }
2181
+ if (type === "cycles") {
2182
+ rules.push({ type, name, severity, message });
2183
+ return;
2184
+ }
2185
+ throw new ConfigError(
2186
+ `config.rules[${index}].type desconocido: ${type}`
2187
+ );
2188
+ });
2189
+ return rules;
2190
+ }
2191
+ function parseSeverity(value, path8) {
2192
+ if (value === void 0) return "error";
2193
+ if (typeof value !== "string" || !SEVERITIES.has(value)) {
2194
+ throw new ConfigError(`${path8} debe ser info, warn o error`);
2195
+ }
2196
+ return value;
2197
+ }
2198
+ function optionalNumber(value, path8) {
2199
+ if (value === void 0) return void 0;
2200
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2201
+ throw new ConfigError(`${path8} debe ser un n\xFAmero`);
2202
+ }
2203
+ return value;
2204
+ }
2205
+ function asObject(value, path8) {
2206
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
2207
+ throw new ConfigError(`${path8} debe ser un objeto`);
2208
+ }
2209
+ return value;
2210
+ }
2211
+ function asString(value, path8) {
2212
+ if (typeof value !== "string" || value.trim() === "") {
2213
+ throw new ConfigError(`${path8} debe ser un texto no vac\xEDo`);
2214
+ }
2215
+ return value.trim();
2216
+ }
2217
+
2218
+ // packages/metrics/src/rules.ts
2219
+ import { minimatch } from "minimatch";
2220
+ function emptyRuleReport() {
2221
+ return {
2222
+ configured: false,
2223
+ summary: {
2224
+ layers: 0,
2225
+ rules: 0,
2226
+ introduced: 0,
2227
+ existing: 0,
2228
+ resolved: 0,
2229
+ introducedErrors: 0,
2230
+ introducedWarnings: 0
2231
+ },
2232
+ violations: { introduced: [], existing: [], resolved: [] }
2233
+ };
2234
+ }
2235
+ function evaluateRules(base, head, config) {
2236
+ const baseViolations = evaluateInput(base, config);
2237
+ const headViolations = evaluateInput(head, config);
2238
+ const baseByKey = new Map(
2239
+ baseViolations.map((violation) => [violation.key, violation])
2240
+ );
2241
+ const headByKey = new Map(
2242
+ headViolations.map((violation) => [violation.key, violation])
2243
+ );
2244
+ const introduced = [];
2245
+ const existing = [];
2246
+ const resolved = [];
2247
+ for (const [key, violation] of headByKey) {
2248
+ if (baseByKey.has(key)) existing.push(violation);
2249
+ else introduced.push(violation);
2250
+ }
2251
+ for (const [key, violation] of baseByKey) {
2252
+ if (!headByKey.has(key)) resolved.push(violation);
2253
+ }
2254
+ introduced.sort(compareViolations);
2255
+ existing.sort(compareViolations);
2256
+ resolved.sort(compareViolations);
2257
+ return {
2258
+ configured: true,
2259
+ summary: {
2260
+ layers: config.layers.length,
2261
+ rules: config.rules.length,
2262
+ introduced: introduced.length,
2263
+ existing: existing.length,
2264
+ resolved: resolved.length,
2265
+ introducedErrors: introduced.filter(
2266
+ (violation) => violation.severity === "error"
2267
+ ).length,
2268
+ introducedWarnings: introduced.filter(
2269
+ (violation) => violation.severity === "warn"
2270
+ ).length
2271
+ },
2272
+ violations: { introduced, existing, resolved }
2273
+ };
2274
+ }
2275
+ function evaluateInput(input, config) {
2276
+ const layers = layerIndex(input.files, config.layers);
2277
+ const violations = [];
2278
+ for (const rule of config.rules) {
2279
+ if (rule.type === "forbidden") {
2280
+ evaluateForbidden(rule, input, layers, violations);
2281
+ } else if (rule.type === "threshold") {
2282
+ evaluateThreshold(rule, input, violations);
2283
+ } else {
2284
+ evaluateCycles(rule, input.cycles, violations);
2285
+ }
2286
+ }
2287
+ const byKey = /* @__PURE__ */ new Map();
2288
+ for (const violation of violations) byKey.set(violation.key, violation);
2289
+ return [...byKey.values()].sort(compareViolations);
2290
+ }
2291
+ function layerIndex(files, layers) {
2292
+ const index = /* @__PURE__ */ new Map();
2293
+ for (const file of files) {
2294
+ for (const layer of layers) {
2295
+ if (layer.patterns.some(
2296
+ (pattern) => minimatch(file.path, pattern, { dot: true })
2297
+ )) {
2298
+ index.set(file.id, layer.name);
2299
+ break;
2300
+ }
2301
+ }
2302
+ }
2303
+ return index;
2304
+ }
2305
+ function evaluateForbidden(rule, input, layers, violations) {
2306
+ for (const edge of input.edges) {
2307
+ if (layers.get(edge.fromId) !== rule.from) continue;
2308
+ if (layers.get(edge.toId) !== rule.to) continue;
2309
+ violations.push({
2310
+ key: `forbidden:${rule.name}:${edge.fromId}->${edge.toId}`,
2311
+ rule: rule.name,
2312
+ type: "forbidden",
2313
+ severity: rule.severity,
2314
+ message: rule.message ?? `${edge.from} no puede depender de ${edge.to}`,
2315
+ files: [edge.from, edge.to],
2316
+ edge: {
2317
+ fromId: edge.fromId,
2318
+ toId: edge.toId,
2319
+ from: edge.from,
2320
+ to: edge.to
2321
+ }
2322
+ });
2323
+ }
2324
+ }
2325
+ function evaluateThreshold(rule, input, violations) {
2326
+ for (const file of input.files) {
2327
+ const value = file.metrics[rule.metric];
2328
+ if (typeof value !== "number" || !Number.isFinite(value)) continue;
2329
+ const tooHigh = rule.max !== void 0 && value > rule.max;
2330
+ const tooLow = rule.min !== void 0 && value < rule.min;
2331
+ if (!tooHigh && !tooLow) continue;
2332
+ const limit = tooHigh && rule.max !== void 0 ? `m\xE1ximo ${rule.max}` : `m\xEDnimo ${rule.min}`;
2333
+ violations.push({
2334
+ key: `threshold:${rule.name}:${file.id}`,
2335
+ rule: rule.name,
2336
+ type: "threshold",
2337
+ severity: rule.severity,
2338
+ message: rule.message ?? `${file.path}: ${rule.metric} = ${value} (${limit})`,
2339
+ files: [file.path],
2340
+ metric: {
2341
+ name: rule.metric,
2342
+ value,
2343
+ ...rule.min !== void 0 ? { min: rule.min } : {},
2344
+ ...rule.max !== void 0 ? { max: rule.max } : {}
2345
+ }
2346
+ });
2347
+ }
2348
+ }
2349
+ function evaluateCycles(rule, cycles, violations) {
2350
+ for (const cycle of cycles) {
2351
+ const paths = cycle.nodes.map((node) => node.path);
2352
+ violations.push({
2353
+ key: `cycles:${rule.name}:${cycle.nodes.map((node) => node.id).sort().join("|")}`,
2354
+ rule: rule.name,
2355
+ type: "cycles",
2356
+ severity: rule.severity,
2357
+ message: rule.message ?? cycleMessage(paths),
2358
+ files: paths,
2359
+ cycle: {
2360
+ size: cycle.size,
2361
+ nodes: cycle.nodes.map((node) => ({ ...node }))
2362
+ }
2363
+ });
2364
+ }
2365
+ }
2366
+ function cycleMessage(paths) {
2367
+ const visible = paths.slice(0, 6);
2368
+ const suffix = paths.length > visible.length ? " \u2192 \u2026" : "";
2369
+ return `Ciclo de ${paths.length} ficheros: ${visible.join(" \u2192 ")}${suffix}`;
2370
+ }
2371
+ function compareViolations(left, right) {
2372
+ return compareStrings3(left.key, right.key);
2373
+ }
2374
+ function compareStrings3(left, right) {
2375
+ return left < right ? -1 : left > right ? 1 : 0;
2376
+ }
2377
+
2378
+ // packages/metrics/src/diff.ts
2379
+ function diffSnapshots(base, head, options = {}) {
2380
+ const top = Math.max(1, Math.floor(options.top ?? 10));
2381
+ const baseIndex = indexSnapshot(base);
2382
+ const headIndex = indexSnapshot(head);
2383
+ const added = difference(headIndex, baseIndex);
2384
+ const removed = difference(baseIndex, headIndex);
2385
+ const changed = changedFiles(baseIndex, headIndex);
2386
+ const edgeDiff = diffEdges(baseIndex, headIndex);
2387
+ const baseCycles = cycleGroups(baseIndex);
2388
+ const headCycles = cycleGroups(headIndex);
2389
+ const introducedCycles = cycleDifference(headCycles, baseCycles);
2390
+ const fixedCycles = cycleDifference(baseCycles, headCycles);
2391
+ const hotspotMetric = chooseHotspotMetric(
2392
+ baseIndex,
2393
+ headIndex,
2394
+ options.hotspotMetric
2395
+ );
2396
+ const hotspots = hotspotMetric ? buildHotspots(baseIndex, headIndex, added, changed, hotspotMetric, top) : [];
2397
+ const rules = options.rules ? evaluateRules(
2398
+ ruleInput(baseIndex, [...baseCycles.values()].map((group) => group.component)),
2399
+ ruleInput(headIndex, [...headCycles.values()].map((group) => group.component)),
2400
+ options.rules
2401
+ ) : emptyRuleReport();
2402
+ return {
2403
+ schemaVersion: "1.0",
2404
+ base: summarize(baseIndex),
2405
+ head: summarize(headIndex),
2406
+ summary: {
2407
+ addedFiles: added.length,
2408
+ removedFiles: removed.length,
2409
+ changedFiles: changed.length,
2410
+ addedEdges: edgeDiff.added.length,
2411
+ removedEdges: edgeDiff.removed.length,
2412
+ cyclicEdgesAdded: edgeDiff.cyclicAdded.length,
2413
+ cyclicEdgesRemoved: edgeDiff.cyclicRemoved.length,
2414
+ introducedCycles: introducedCycles.length,
2415
+ fixedCycles: fixedCycles.length,
2416
+ hotspots: hotspots.length
2417
+ },
2418
+ hotspotMetric,
2419
+ rules,
2420
+ files: { added, removed, changed },
2421
+ edges: edgeDiff,
2422
+ cycles: { introduced: introducedCycles, fixed: fixedCycles },
2423
+ hotspots
2424
+ };
2425
+ }
2426
+ function indexSnapshot(cc) {
2427
+ const paths = /* @__PURE__ */ new Map();
2428
+ for (const root of cc.files) {
2429
+ walkFiles([root], (entry) => {
2430
+ paths.set(entry.node.id, relativePath(root, entry.path, entry.node));
2431
+ });
2432
+ }
2433
+ const nodes = [...paths.keys()].sort(
2434
+ (left, right) => compareStrings4(paths.get(left) ?? left, paths.get(right) ?? right)
2435
+ );
2436
+ const metrics = cc.lenses?.metrics?.attributes ?? {};
2437
+ const edges = /* @__PURE__ */ new Map();
2438
+ for (const edge of cc.lenses?.dependency?.edges ?? []) {
2439
+ const key = edgeKey(edge.fromId, edge.toId);
2440
+ const weight = finiteNumber(edge.attributes?.weight) ?? 1;
2441
+ const typeOnly = finiteNumber(edge.attributes?.typeOnly) ?? 0;
2442
+ const existing = edges.get(key);
2443
+ if (existing) {
2444
+ existing.weight += weight;
2445
+ existing.typeOnly += typeOnly;
2446
+ existing.isCyclic = existing.isCyclic || edge.isCyclic === true;
2447
+ } else {
2448
+ edges.set(key, {
2449
+ fromId: edge.fromId,
2450
+ toId: edge.toId,
2451
+ weight,
2452
+ typeOnly,
2453
+ isCyclic: edge.isCyclic === true
2454
+ });
2455
+ }
2456
+ }
2457
+ return { cc, paths, nodes, metrics, edges };
2458
+ }
2459
+ function relativePath(root, fullPath, leaf) {
2460
+ if (fullPath === root.name) return leaf.name;
2461
+ const prefix = `${root.name}/`;
2462
+ return fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
2463
+ }
2464
+ function summarize(index) {
2465
+ let folders = 0;
2466
+ walkTree(index.cc.files, (entry) => {
2467
+ if (entry.node.type === "Folder" && entry.depth > 0) folders++;
2468
+ });
2469
+ return {
2470
+ projectName: index.cc.meta.projectName ?? "",
2471
+ checksum: index.cc.meta.checksum ?? "",
2472
+ files: index.paths.size,
2473
+ folders,
2474
+ edges: index.edges.size
2475
+ };
2476
+ }
2477
+ function difference(present, absent) {
2478
+ const files = [];
2479
+ for (const [id, path8] of present.paths) {
2480
+ if (absent.paths.has(id)) continue;
2481
+ files.push({
2482
+ id,
2483
+ path: path8,
2484
+ metrics: sortedMetrics(present.metrics[id])
2485
+ });
2486
+ }
2487
+ return files.sort(compareFiles);
2488
+ }
2489
+ function changedFiles(base, head) {
2490
+ const changed = [];
2491
+ for (const [id, path8] of head.paths) {
2492
+ if (!base.paths.has(id)) continue;
2493
+ const baseValues = base.metrics[id] ?? {};
2494
+ const headValues = head.metrics[id] ?? {};
2495
+ const keys = /* @__PURE__ */ new Set([...Object.keys(baseValues), ...Object.keys(headValues)]);
2496
+ const changes = [];
2497
+ for (const metric of [...keys].sort(compareStrings4)) {
2498
+ const baseValue = finiteNumber(baseValues[metric]);
2499
+ const headValue = finiteNumber(headValues[metric]);
2500
+ if (baseValue === null && headValue === null) continue;
2501
+ if (baseValue !== null && headValue !== null && baseValue === headValue) {
2502
+ continue;
2503
+ }
2504
+ changes.push({
2505
+ metric,
2506
+ base: baseValue,
2507
+ head: headValue,
2508
+ delta: baseValue !== null && headValue !== null ? headValue - baseValue : null
2509
+ });
2510
+ }
2511
+ if (changes.length > 0) changed.push({ id, path: path8, changes });
2512
+ }
2513
+ return changed.sort(compareChangedFiles);
2514
+ }
2515
+ function diffEdges(base, head) {
2516
+ const added = [];
2517
+ const removed = [];
2518
+ for (const [key, edge] of head.edges) {
2519
+ if (!base.edges.has(key)) added.push(edgeChange(head, edge));
2520
+ }
2521
+ for (const [key, edge] of base.edges) {
2522
+ if (!head.edges.has(key)) removed.push(edgeChange(base, edge));
2523
+ }
2524
+ added.sort(compareEdgeChanges);
2525
+ removed.sort(compareEdgeChanges);
2526
+ return {
2527
+ added,
2528
+ removed,
2529
+ cyclicAdded: added.filter((edge) => edge.isCyclic),
2530
+ cyclicRemoved: removed.filter((edge) => edge.isCyclic)
2531
+ };
2532
+ }
2533
+ function edgeChange(index, edge) {
2534
+ return {
2535
+ fromId: edge.fromId,
2536
+ toId: edge.toId,
2537
+ from: index.paths.get(edge.fromId) ?? edge.fromId,
2538
+ to: index.paths.get(edge.toId) ?? edge.toId,
2539
+ weight: edge.weight,
2540
+ typeOnly: edge.typeOnly,
2541
+ isCyclic: edge.isCyclic
2542
+ };
2543
+ }
2544
+ function cycleGroups(index) {
2545
+ const adjacency = /* @__PURE__ */ new Map();
2546
+ const reverse = /* @__PURE__ */ new Map();
2547
+ for (const node of index.nodes) {
2548
+ adjacency.set(node, []);
2549
+ reverse.set(node, []);
2550
+ }
2551
+ for (const edge of index.edges.values()) {
2552
+ if (!index.paths.has(edge.fromId) || !index.paths.has(edge.toId)) continue;
2553
+ adjacency.get(edge.fromId)?.push(edge.toId);
2554
+ reverse.get(edge.toId)?.push(edge.fromId);
2555
+ }
2556
+ for (const [node, neighbors] of adjacency) {
2557
+ adjacency.set(node, [...new Set(neighbors)].sort(compareStrings4));
2558
+ }
2559
+ for (const [node, neighbors] of reverse) {
2560
+ reverse.set(node, [...new Set(neighbors)].sort(compareStrings4));
2561
+ }
2562
+ const visited = /* @__PURE__ */ new Set();
2563
+ const order = [];
2564
+ for (const start of index.nodes) {
2565
+ if (visited.has(start)) continue;
2566
+ visited.add(start);
2567
+ const stack = [
2568
+ { node: start, neighbors: adjacency.get(start) ?? [], next: 0 }
2569
+ ];
2570
+ while (stack.length > 0) {
2571
+ const frame = stack[stack.length - 1];
2572
+ if (frame.next < frame.neighbors.length) {
2573
+ const next = frame.neighbors[frame.next];
2574
+ frame.next++;
2575
+ if (!visited.has(next)) {
2576
+ visited.add(next);
2577
+ stack.push({
2578
+ node: next,
2579
+ neighbors: adjacency.get(next) ?? [],
2580
+ next: 0
2581
+ });
2582
+ }
2583
+ } else {
2584
+ order.push(frame.node);
2585
+ stack.pop();
2586
+ }
2587
+ }
2588
+ }
2589
+ const assigned = /* @__PURE__ */ new Set();
2590
+ const groups = /* @__PURE__ */ new Map();
2591
+ for (let position = order.length - 1; position >= 0; position--) {
2592
+ const start = order[position];
2593
+ if (assigned.has(start)) continue;
2594
+ assigned.add(start);
2595
+ const component = [];
2596
+ const stack = [start];
2597
+ while (stack.length > 0) {
2598
+ const node = stack.pop();
2599
+ component.push(node);
2600
+ for (const next of reverse.get(node) ?? []) {
2601
+ if (assigned.has(next)) continue;
2602
+ assigned.add(next);
2603
+ stack.push(next);
2604
+ }
2605
+ }
2606
+ const hasSelfLoop = component.some(
2607
+ (node) => (adjacency.get(node) ?? []).includes(node)
2608
+ );
2609
+ if (component.length < 2 && !hasSelfLoop) continue;
2610
+ const nodes = component.map((id) => ({
2611
+ id,
2612
+ path: index.paths.get(id) ?? id
2613
+ })).sort((left, right) => {
2614
+ return compareStrings4(left.path, right.path) || compareStrings4(left.id, right.id);
2615
+ });
2616
+ const key = [...component].sort(compareStrings4).join("\0");
2617
+ groups.set(key, { key, component: { size: nodes.length, nodes } });
2618
+ }
2619
+ return groups;
2620
+ }
2621
+ function cycleDifference(present, absent) {
2622
+ const cycles = [];
2623
+ for (const [key, group] of present) {
2624
+ if (!absent.has(key)) cycles.push(group.component);
2625
+ }
2626
+ return cycles.sort(compareCycles);
2627
+ }
2628
+ function ruleInput(index, cycles) {
2629
+ const files = index.nodes.map((id) => ({
2630
+ id,
2631
+ path: index.paths.get(id) ?? id,
2632
+ metrics: sortedMetrics(index.metrics[id])
2633
+ }));
2634
+ const edges = [];
2635
+ for (const edge of index.edges.values()) {
2636
+ if (!index.paths.has(edge.fromId) || !index.paths.has(edge.toId)) {
2637
+ continue;
2638
+ }
2639
+ edges.push({
2640
+ fromId: edge.fromId,
2641
+ toId: edge.toId,
2642
+ from: index.paths.get(edge.fromId) ?? edge.fromId,
2643
+ to: index.paths.get(edge.toId) ?? edge.toId
2644
+ });
2645
+ }
2646
+ return { files, edges, cycles };
2647
+ }
2648
+ function chooseHotspotMetric(base, head, requested) {
2649
+ if (requested) return requested;
2650
+ for (const metric of ["maxComplexity", "rloc"]) {
2651
+ if (hasMetric(base, metric) || hasMetric(head, metric)) return metric;
2652
+ }
2653
+ return null;
2654
+ }
2655
+ function hasMetric(index, metric) {
2656
+ for (const values of Object.values(index.metrics)) {
2657
+ if (finiteNumber(values[metric]) !== null) return true;
2658
+ }
2659
+ return false;
2660
+ }
2661
+ function buildHotspots(base, head, added, changed, metric, top) {
2662
+ const addedIds = new Set(added.map((file) => file.id));
2663
+ const candidates = [...added, ...changed];
2664
+ const hotspots = [];
2665
+ for (const file of candidates) {
2666
+ const baseValue = finiteNumber(base.metrics[file.id]?.[metric]);
2667
+ const headValue = finiteNumber(head.metrics[file.id]?.[metric]);
2668
+ if (headValue === null) continue;
2669
+ const before = baseValue ?? 0;
2670
+ const delta = headValue - before;
2671
+ if (delta <= 0) continue;
2672
+ hotspots.push({
2673
+ id: file.id,
2674
+ path: file.path,
2675
+ metric,
2676
+ base: before,
2677
+ head: headValue,
2678
+ delta,
2679
+ change: addedIds.has(file.id) ? "added" : "changed"
2680
+ });
2681
+ }
2682
+ return hotspots.sort(
2683
+ (left, right) => right.delta - left.delta || right.head - left.head || compareStrings4(left.path, right.path)
2684
+ ).slice(0, top);
2685
+ }
2686
+ function sortedMetrics(values) {
2687
+ const sorted = {};
2688
+ if (!values) return sorted;
2689
+ for (const key of Object.keys(values).sort(compareStrings4)) {
2690
+ const value = finiteNumber(values[key]);
2691
+ if (value !== null) sorted[key] = value;
2692
+ }
2693
+ return sorted;
2694
+ }
2695
+ function finiteNumber(value) {
2696
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
2697
+ }
2698
+ function edgeKey(fromId, toId) {
2699
+ return `${fromId}\0${toId}`;
2700
+ }
2701
+ function compareFiles(left, right) {
2702
+ return compareStrings4(left.path, right.path) || compareStrings4(left.id, right.id);
2703
+ }
2704
+ function compareChangedFiles(left, right) {
2705
+ return compareStrings4(left.path, right.path) || compareStrings4(left.id, right.id);
2706
+ }
2707
+ function compareEdgeChanges(left, right) {
2708
+ return compareStrings4(left.from, right.from) || compareStrings4(left.to, right.to) || compareStrings4(left.fromId, right.fromId) || compareStrings4(left.toId, right.toId);
2709
+ }
2710
+ function compareCycles(left, right) {
2711
+ return compareStrings4(
2712
+ left.nodes[0]?.path ?? "",
2713
+ right.nodes[0]?.path ?? ""
2714
+ ) || left.size - right.size;
2715
+ }
2716
+ function compareStrings4(left, right) {
2717
+ return left < right ? -1 : left > right ? 1 : 0;
2718
+ }
2719
+
2720
+ // packages/metrics/src/risk.ts
2721
+ var COMPLEXITY_METRIC_CANDIDATES = [
2722
+ "maxComplexity",
2723
+ "max_complexity_per_function",
2724
+ "complexity",
2725
+ "logic_complexity",
2726
+ "sonar_cognitive_complexity"
2727
+ ];
2728
+ var CHURN_METRIC_CANDIDATES = [
2729
+ "number_of_commits",
2730
+ "number_of_authors",
2731
+ "number_of_renames"
2732
+ ];
2733
+ var RISK_WEIGHTS = {
2734
+ complexity: 0.5,
2735
+ churn: 0.32,
2736
+ size: 0.18
2737
+ };
2738
+ var RISK_GAMMA = 1.7;
2739
+ function riskScores(files) {
2740
+ const complexityP95 = percentile(
2741
+ files.map((file) => file.complexity).filter((value) => value > 0),
2742
+ 0.95
2743
+ );
2744
+ const churnP95 = percentile(
2745
+ files.map((file) => file.churn).filter((value) => value > 0),
2746
+ 0.95
2747
+ );
2748
+ const sizeP95 = percentile(
2749
+ files.map((file) => file.size).filter((value) => value > 0),
2750
+ 0.95
2751
+ );
2752
+ const complexityWeight = complexityP95 > 0 ? RISK_WEIGHTS.complexity : 0;
2753
+ const churnWeight = churnP95 > 0 ? RISK_WEIGHTS.churn : 0;
2754
+ const sizeWeight = sizeP95 > 0 ? RISK_WEIGHTS.size : 0;
2755
+ const totalWeight = complexityWeight + churnWeight + sizeWeight;
2756
+ if (totalWeight === 0) return files.map(() => 0);
2757
+ return files.map((file) => {
2758
+ const raw = (complexityWeight * norm(file.complexity, complexityP95) + churnWeight * norm(file.churn, churnP95) + sizeWeight * norm(file.size, sizeP95)) / totalWeight;
2759
+ return Math.pow(clamp01(raw), RISK_GAMMA);
2760
+ });
2761
+ }
2762
+ function firstMetric(available, candidates) {
2763
+ const set = available instanceof Set ? available : new Set(available);
2764
+ for (const candidate of candidates) {
2765
+ if (set.has(candidate)) return candidate;
2766
+ }
2767
+ return null;
2768
+ }
2769
+ function norm(value, p95) {
2770
+ return p95 > 0 ? clamp01(value / p95) : 0;
2771
+ }
2772
+ function percentile(values, p) {
2773
+ if (values.length === 0) return 0;
2774
+ const sorted = [...values].sort((a, b) => a - b);
2775
+ const index = Math.min(
2776
+ sorted.length - 1,
2777
+ Math.max(0, Math.floor((sorted.length - 1) * p))
2778
+ );
2779
+ return sorted[index] ?? 0;
2780
+ }
2781
+ function clamp01(value) {
2782
+ if (!Number.isFinite(value)) return 0;
2783
+ return Math.max(0, Math.min(1, value));
2784
+ }
2785
+
2786
+ // packages/render/src/model.ts
2787
+ var RenderError = class extends Error {
2788
+ constructor(message) {
2789
+ super(message);
2790
+ this.name = "RenderError";
2791
+ }
2792
+ };
2793
+ var PALETTE = [
2794
+ "#b9c4d0",
2795
+ "#c9b8a8",
2796
+ "#a9bfae",
2797
+ "#c4b6c9",
2798
+ "#c9c2a6",
2799
+ "#d5d2ca",
2800
+ "#c2c8d0",
2801
+ "#d6c6bd",
2802
+ "#b7c6c2",
2803
+ "#cbbfb0",
2804
+ "#bcc3b9",
2805
+ "#cfc6c9"
2806
+ ];
2807
+ var CURATED_METRICS = [
2808
+ "rloc",
2809
+ "maxComplexity",
2810
+ "max_complexity_per_function",
2811
+ "complexity",
2812
+ "functions",
2813
+ "number_of_functions",
2814
+ "maxFunctionLength",
2815
+ "loc",
2816
+ "imports",
2817
+ "externalImports",
2818
+ "builtinImports",
2819
+ "typeOnlyImports",
2820
+ "unresolvedImports",
2821
+ "fanIn",
2822
+ "fanOut",
2823
+ "inCycle",
2824
+ "cycleSize",
2825
+ "dependencyLevel",
2826
+ "number_of_commits",
2827
+ "number_of_authors",
2828
+ "incoming_dependencies",
2829
+ "outgoing_dependencies"
2830
+ ];
2831
+ function isRecord3(value) {
2832
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2833
+ }
2834
+ function valueOf(values, key) {
2835
+ if (!key || !values) return void 0;
2836
+ const value = values[key];
2837
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2838
+ }
2839
+ function percentile2(values, p) {
2840
+ if (values.length === 0) return 0;
2841
+ const sorted = [...values].sort((a, b) => a - b);
2842
+ const index = Math.min(
2843
+ sorted.length - 1,
2844
+ Math.max(0, Math.floor((sorted.length - 1) * p))
2845
+ );
2846
+ return sorted[index] ?? 0;
2847
+ }
2848
+ function hashString(value) {
2849
+ let hash = 2166136261;
2850
+ for (let index = 0; index < value.length; index++) {
2851
+ hash ^= value.charCodeAt(index);
2852
+ hash = Math.imul(hash, 16777619);
2853
+ }
2854
+ return hash >>> 0;
2855
+ }
2856
+ function sectionForPath(path8) {
2857
+ const folders = path8.split("/").filter(Boolean).slice(0, -1);
2858
+ if (folders.length === 0) return "(ra\xEDz)";
2859
+ if (folders.length === 1) return folders[0];
2860
+ return folders.slice(0, 3).join("/");
2861
+ }
2862
+ function asEdges(value) {
2863
+ if (!Array.isArray(value)) return [];
2864
+ const edges = [];
2865
+ for (const item of value) {
2866
+ if (!isRecord3(item)) continue;
2867
+ if (typeof item.fromId !== "string" || typeof item.toId !== "string") continue;
2868
+ edges.push({
2869
+ fromId: item.fromId,
2870
+ toId: item.toId,
2871
+ ...item.isCyclic === true ? { isCyclic: true } : {}
2872
+ });
2873
+ }
2874
+ return edges;
2875
+ }
2876
+ function metricTitle(attributes, key) {
2877
+ const descriptor = attributes?.[key];
2878
+ if (isRecord3(descriptor) && typeof descriptor.title === "string" && descriptor.title) {
2879
+ return descriptor.title;
2880
+ }
2881
+ return key;
2882
+ }
2883
+ function metricDirection(attributes, key) {
2884
+ const descriptor = attributes?.[key];
2885
+ if (isRecord3(descriptor) && typeof descriptor.direction === "number") {
2886
+ return descriptor.direction;
2887
+ }
2888
+ return 0;
2889
+ }
2890
+ function buildRenderModel(cc, options = {}) {
2891
+ const stats = getStats(cc);
2892
+ const coverage = new Set(stats.metricCoverage.map((entry) => entry.name));
2893
+ const attributes = cc.lenses?.metrics?.attributes ?? {};
2894
+ const descriptors = cc.lenses?.metrics?.attributeDescriptors;
2895
+ let areaMetric;
2896
+ if (options.areaMetric) {
2897
+ if (!coverage.has(options.areaMetric)) {
2898
+ throw new RenderError(
2899
+ `la m\xE9trica de \xE1rea "${options.areaMetric}" no existe en este cc.json`
2900
+ );
2901
+ }
2902
+ areaMetric = options.areaMetric;
2903
+ } else {
2904
+ areaMetric = defaultMetric(stats) ?? null;
2905
+ }
2906
+ const complexityKey = firstMetric(coverage, COMPLEXITY_METRIC_CANDIDATES);
2907
+ const churnKey = firstMetric(coverage, CHURN_METRIC_CANDIDATES);
2908
+ const metricKeys = [];
2909
+ const addMetric = (key) => {
2910
+ if (key && coverage.has(key) && !metricKeys.includes(key)) metricKeys.push(key);
2911
+ };
2912
+ for (const key of CURATED_METRICS) addMetric(key);
2913
+ addMetric(areaMetric);
2914
+ addMetric(complexityKey);
2915
+ addMetric(churnKey);
2916
+ if (options.color && options.color !== "risk" && options.color !== "structure") {
2917
+ addMetric(options.color);
2918
+ }
2919
+ const singleRoot = cc.files.length === 1 && cc.files[0]?.type === "Folder" ? cc.files[0] : null;
2920
+ const prefix = singleRoot ? `${singleRoot.name}/` : "";
2921
+ const rootName = singleRoot?.name ?? "";
2922
+ const warnings = [];
2923
+ const files = [];
2924
+ const fileIndexById = /* @__PURE__ */ new Map();
2925
+ walkFiles(cc.files, (entry) => {
2926
+ const fullPath = entry.path;
2927
+ const path8 = prefix && fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
2928
+ const values = attributes[entry.node.id];
2929
+ const rawArea = valueOf(values, areaMetric);
2930
+ const index = files.length;
2931
+ fileIndexById.set(entry.node.id, index);
2932
+ files.push({
2933
+ index,
2934
+ nodeId: entry.node.id,
2935
+ path: path8,
2936
+ name: entry.node.name,
2937
+ section: -1,
2938
+ area: Math.max(1, rawArea ?? 0),
2939
+ actualArea: rawArea ?? 0,
2940
+ risk: 0,
2941
+ complexity: valueOf(values, complexityKey) ?? 0,
2942
+ churn: valueOf(values, churnKey) ?? 0,
2943
+ values: []
2944
+ });
2945
+ });
2946
+ if (areaMetric && files.length > 0) {
2947
+ const missing = files.filter((file) => file.actualArea === 0).length;
2948
+ if (missing > 0) {
2949
+ warnings.push(
2950
+ `${missing} ficheros sin "${areaMetric}"; se dibujan con \xE1rea m\xEDnima para no desaparecer`
2951
+ );
2952
+ }
2953
+ if (!options.areaMetric && areaMetric !== "rloc") {
2954
+ warnings.push(`"rloc" no est\xE1 presente; se usa "${areaMetric}" como \xE1rea`);
2955
+ }
2956
+ }
2957
+ const risks = riskScores(
2958
+ files.map((file) => ({
2959
+ complexity: file.complexity,
2960
+ churn: file.churn,
2961
+ size: file.actualArea
2962
+ }))
2963
+ );
2964
+ for (const [index, file] of files.entries()) {
2965
+ file.risk = risks[index] ?? 0;
2966
+ }
2967
+ const sectionAccumulator = /* @__PURE__ */ new Map();
2968
+ for (const file of files) {
2969
+ const key = sectionForPath(file.path);
2970
+ const current = sectionAccumulator.get(key) ?? { files: 0, area: 0 };
2971
+ current.files++;
2972
+ current.area += file.actualArea;
2973
+ sectionAccumulator.set(key, current);
2974
+ }
2975
+ const sections = [...sectionAccumulator.entries()].map(([key, value]) => ({
2976
+ key,
2977
+ label: key,
2978
+ files: value.files,
2979
+ area: value.area,
2980
+ color: PALETTE[hashString(key) % PALETTE.length]
2981
+ })).sort((a, b) => b.area - a.area || a.key.localeCompare(b.key));
2982
+ const sectionIndex = new Map(sections.map((section, index) => [section.key, index]));
2983
+ for (const file of files) {
2984
+ file.section = sectionIndex.get(sectionForPath(file.path)) ?? 0;
2985
+ }
2986
+ const metrics = metricKeys.map((key) => {
2987
+ const present = files.map((file) => valueOf(attributes[file.nodeId], key)).filter((value) => value !== void 0);
2988
+ const min = present.length > 0 ? Math.min(...present) : 0;
2989
+ const actualMax = present.length > 0 ? Math.max(...present) : 0;
2990
+ const p95 = percentile2(present, 0.95);
2991
+ const skewed = min >= 0 && p95 > 0 && actualMax > p95 * 4;
2992
+ const max = skewed ? actualMax : p95 > 0 ? p95 : actualMax;
2993
+ return {
2994
+ key,
2995
+ title: metricTitle(descriptors, key),
2996
+ direction: metricDirection(descriptors, key),
2997
+ min,
2998
+ max,
2999
+ actualMax,
3000
+ p95,
3001
+ mode: skewed ? "log" : "linear",
3002
+ coverage: present.length
3003
+ };
3004
+ });
3005
+ for (const file of files) {
3006
+ const values = attributes[file.nodeId];
3007
+ file.values = metrics.map((metric) => valueOf(values, metric.key) ?? null);
3008
+ }
3009
+ const roots = buildRoots(cc, singleRoot, fileIndexById, files);
3010
+ const folderCount = countFolders(roots);
3011
+ const dependency = cc.lenses?.dependency;
3012
+ const leafEdges = asEdges(dependency?.leafEdges);
3013
+ const rawEdges = leafEdges.length > 0 ? leafEdges : asEdges(dependency?.edges);
3014
+ const edgeSet = /* @__PURE__ */ new Set();
3015
+ const edges = [];
3016
+ const n = files.length;
3017
+ for (const edge of rawEdges) {
3018
+ const from = fileIndexById.get(edge.fromId);
3019
+ const to = fileIndexById.get(edge.toId);
3020
+ if (from === void 0 || to === void 0 || from === to) continue;
3021
+ const key = from * n + to;
3022
+ if (edgeSet.has(key)) continue;
3023
+ edgeSet.add(key);
3024
+ edges.push({ from, to });
3025
+ if (edges.length >= 2e4) {
3026
+ warnings.push("se han limitado las dependencias a 20.000 aristas");
3027
+ break;
3028
+ }
3029
+ }
3030
+ edges.sort((a, b) => a.from - b.from || a.to - b.to);
3031
+ const flaggedEdges = /* @__PURE__ */ new Set();
3032
+ for (const edge of rawEdges) {
3033
+ if (edge.isCyclic !== true) continue;
3034
+ const from = fileIndexById.get(edge.fromId);
3035
+ const to = fileIndexById.get(edge.toId);
3036
+ if (from === void 0 || to === void 0) continue;
3037
+ flaggedEdges.add(from * n + to);
3038
+ }
3039
+ const cycleSet = /* @__PURE__ */ new Set();
3040
+ const cycles = [];
3041
+ for (const edge of edges) {
3042
+ const key = edge.from * n + edge.to;
3043
+ const backward = edgeSet.has(edge.to * n + edge.from);
3044
+ if (backward || flaggedEdges.has(key)) {
3045
+ if (!cycleSet.has(key)) {
3046
+ cycleSet.add(key);
3047
+ cycles.push(edge);
3048
+ }
3049
+ }
3050
+ }
3051
+ cycles.sort((a, b) => a.from - b.from || a.to - b.to);
3052
+ if (rawEdges.length === 0) {
3053
+ warnings.push("este cc.json no trae una lente de dependencias utilizable");
3054
+ }
3055
+ const totalArea = files.reduce((sum, file) => sum + file.actualArea, 0);
3056
+ const hotspotCount = files.filter((file) => file.risk >= 0.66).length;
3057
+ let initialLens = "risk";
3058
+ let initialMetric = null;
3059
+ if (options.color === "structure") {
3060
+ initialLens = "structure";
3061
+ } else if (options.color && options.color !== "risk") {
3062
+ if (!coverage.has(options.color)) {
3063
+ throw new RenderError(`la m\xE9trica de color "${options.color}" no existe en este cc.json`);
3064
+ }
3065
+ initialLens = "metric";
3066
+ initialMetric = options.color;
3067
+ }
3068
+ const defaultSelection = pickDefaultSelection(files, initialLens, initialMetric, metrics);
3069
+ const project = cc.meta.projectName || options.title || "Proyecto sin nombre";
3070
+ return {
3071
+ meta: {
3072
+ project,
3073
+ title: options.title || project,
3074
+ commit: cc.meta.commitHash || "",
3075
+ checksum: cc.meta.checksum || "",
3076
+ apiVersion: cc.meta.apiVersion,
3077
+ rootName
3078
+ },
3079
+ files,
3080
+ sections,
3081
+ metrics,
3082
+ edges,
3083
+ cycles,
3084
+ roots,
3085
+ areaMetric,
3086
+ areaTitle: areaMetric ? metricTitle(descriptors, areaMetric) : "\xC1rea",
3087
+ totalArea,
3088
+ fileCount: files.length,
3089
+ folderCount,
3090
+ hotspotCount,
3091
+ defaultSelection,
3092
+ initialLens,
3093
+ initialMetric,
3094
+ warnings
3095
+ };
3096
+ }
3097
+ function buildRoots(cc, singleRoot, fileIndexById, files) {
3098
+ const build = (node, path8) => {
3099
+ if (node.type === "File") {
3100
+ const index = fileIndexById.get(node.id);
3101
+ const file = index === void 0 ? void 0 : files[index];
3102
+ return {
3103
+ kind: "file",
3104
+ key: `f:${index ?? -1}`,
3105
+ name: node.name,
3106
+ path: path8,
3107
+ value: file?.area ?? 1,
3108
+ fileCount: 1,
3109
+ section: file?.section ?? 0,
3110
+ fileIndex: index
3111
+ };
3112
+ }
3113
+ const children = (node.children ?? []).map(
3114
+ (child) => build(child, path8 ? `${path8}/${child.name}` : child.name)
3115
+ );
3116
+ const folder = {
3117
+ kind: "folder",
3118
+ key: `d:${path8}`,
3119
+ name: node.name,
3120
+ path: path8,
3121
+ value: children.reduce((sum, child) => sum + child.value, 0) || 1,
3122
+ fileCount: children.reduce((sum, child) => sum + child.fileCount, 0),
3123
+ section: -1,
3124
+ children
3125
+ };
3126
+ while (folder.children?.length === 1 && folder.children[0]?.kind === "folder") {
3127
+ const only = folder.children[0];
3128
+ folder.name = `${folder.name}/${only.name}`;
3129
+ folder.path = only.path;
3130
+ folder.key = `d:${only.path}`;
3131
+ folder.value = only.value;
3132
+ folder.fileCount = only.fileCount;
3133
+ folder.children = only.children;
3134
+ }
3135
+ return folder;
3136
+ };
3137
+ if (singleRoot) {
3138
+ return (singleRoot.children ?? []).map((child) => build(child, child.name));
3139
+ }
3140
+ return cc.files.map((root) => build(root, root.name));
3141
+ }
3142
+ function countFolders(nodes) {
3143
+ let count = 0;
3144
+ const stack = [...nodes];
3145
+ while (stack.length > 0) {
3146
+ const node = stack.pop();
3147
+ if (node.kind === "folder") count++;
3148
+ if (node.children) stack.push(...node.children);
3149
+ }
3150
+ return count;
3151
+ }
3152
+ function pickDefaultSelection(files, lens, metric, metrics) {
3153
+ if (files.length === 0) return null;
3154
+ if (lens === "metric" && metric) {
3155
+ const index = metrics.findIndex((entry) => entry.key === metric);
3156
+ if (index >= 0) {
3157
+ let best = null;
3158
+ for (const file of files) {
3159
+ const value = file.values[index];
3160
+ if (value === null || value === void 0) continue;
3161
+ if (!best || value > (best.values[index] ?? -Infinity)) best = file;
3162
+ }
3163
+ if (best) return best.index;
3164
+ }
3165
+ }
3166
+ if (lens === "structure") {
3167
+ return files.reduce(
3168
+ (best, file) => file.actualArea > best.actualArea ? file : best
3169
+ ).index;
3170
+ }
3171
+ return files.reduce((best, file) => file.risk > best.risk ? file : best).index;
3172
+ }
3173
+
3174
+ // packages/render/src/html.ts
3175
+ import { readFileSync } from "node:fs";
3176
+
3177
+ // packages/render/src/color.ts
3178
+ var RISK_STOPS = [
3179
+ [0, "#ebe7df"],
3180
+ [0.35, "#e9cd98"],
3181
+ [0.65, "#d68a54"],
3182
+ [1, "#a42e2c"]
3183
+ ];
3184
+ var METRIC_STOPS = [
3185
+ [0, "#ebe7df"],
3186
+ [0.5, "#b9c9cf"],
3187
+ [1, "#355b6b"]
3188
+ ];
3189
+ var RISK_GRADIENT = "linear-gradient(90deg, #ebe7df 0%, #e9cd98 35%, #d68a54 65%, #a42e2c 100%)";
3190
+ var METRIC_GRADIENT = "linear-gradient(90deg, #ebe7df 0%, #b9c9cf 50%, #355b6b 100%)";
3191
+ function clamp012(value) {
3192
+ if (!Number.isFinite(value)) return 0;
3193
+ return Math.max(0, Math.min(1, value));
3194
+ }
3195
+ function hexToRgb(hex) {
3196
+ const value = hex.replace("#", "");
3197
+ return [
3198
+ Number.parseInt(value.slice(0, 2), 16),
3199
+ Number.parseInt(value.slice(2, 4), 16),
3200
+ Number.parseInt(value.slice(4, 6), 16)
3201
+ ];
3202
+ }
3203
+ function rgbToHex(rgb) {
3204
+ return `#${rgb.map((channel) => channel.toString(16).padStart(2, "0")).join("")}`;
3205
+ }
3206
+ function mix(a, b, t) {
3207
+ const from = hexToRgb(a);
3208
+ const to = hexToRgb(b);
3209
+ return rgbToHex([
3210
+ Math.round(from[0] + (to[0] - from[0]) * t),
3211
+ Math.round(from[1] + (to[1] - from[1]) * t),
3212
+ Math.round(from[2] + (to[2] - from[2]) * t)
3213
+ ]);
3214
+ }
3215
+ function ramp(stops, t) {
3216
+ const value = clamp012(t);
3217
+ for (let index = 0; index < stops.length - 1; index++) {
3218
+ const from = stops[index];
3219
+ const to = stops[index + 1];
3220
+ if (value <= to[0]) {
3221
+ const span2 = to[0] - from[0] || 1;
3222
+ return mix(from[1], to[1], (value - from[0]) / span2);
3223
+ }
3224
+ }
3225
+ return stops[stops.length - 1][1];
3226
+ }
3227
+ function riskColor(risk) {
3228
+ return ramp(RISK_STOPS, risk);
3229
+ }
3230
+ function metricColor(value, metric) {
3231
+ if (value === null) return METRIC_STOPS[0][1];
3232
+ let t;
3233
+ if (metric.mode === "log") {
3234
+ const span2 = Math.max(1e-9, metric.max - metric.min);
3235
+ t = Math.log1p(Math.max(0, value - metric.min)) / Math.log1p(span2);
3236
+ } else {
3237
+ const span2 = metric.max - metric.min;
3238
+ t = span2 > 0 ? (value - metric.min) / span2 : 0;
3239
+ }
3240
+ return ramp(METRIC_STOPS, t);
3241
+ }
3242
+ function inkFor(color) {
3243
+ const [r, g, b] = hexToRgb(color);
3244
+ const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255;
3245
+ return luminance > 0.52 ? "#1a1a1a" : "#f7f5f1";
3246
+ }
3247
+ function colorForFile(lens, risk, section, value, metric) {
3248
+ if (lens === "structure") return section?.color ?? "#ddd8cf";
3249
+ if (lens === "risk" || !metric) return riskColor(risk);
3250
+ return metricColor(value, metric);
3251
+ }
3252
+
3253
+ // packages/layouts/src/squarify.ts
3254
+ function squarify(items, rect) {
3255
+ if (rect.w <= 0 || rect.h <= 0) return [];
3256
+ const usable = items.filter(
3257
+ (item) => Number.isFinite(item.value) && item.value > 0
3258
+ );
3259
+ const total = usable.reduce((sum, item) => sum + item.value, 0);
3260
+ if (usable.length === 0 || total <= 0) return [];
3261
+ const sorted = usable.map((item, index2) => ({ item, index: index2 })).sort((a, b) => b.item.value - a.item.value || a.index - b.index).map(({ item }) => item);
3262
+ const scale = rect.w * rect.h / total;
3263
+ const placed = [];
3264
+ let x = rect.x;
3265
+ let y = rect.y;
3266
+ let w = rect.w;
3267
+ let h = rect.h;
3268
+ let row = [];
3269
+ const area = (item) => item.value * scale;
3270
+ function worst(candidate, side) {
3271
+ let sum = 0;
3272
+ let max = -Infinity;
3273
+ let min = Infinity;
3274
+ for (const item of candidate) {
3275
+ const value = area(item);
3276
+ sum += value;
3277
+ if (value > max) max = value;
3278
+ if (value < min) min = value;
3279
+ }
3280
+ if (sum <= 0 || side <= 0) return Infinity;
3281
+ return Math.max(
3282
+ side * side * max / (sum * sum),
3283
+ sum * sum / (side * side * min)
3284
+ );
3285
+ }
3286
+ function place(current, horizontal) {
3287
+ if (current.length === 0) return;
3288
+ const sum = current.reduce((acc, item) => acc + area(item), 0);
3289
+ if (sum <= 0) return;
3290
+ if (horizontal) {
3291
+ if (w <= 0) return;
3292
+ const thickness = sum / w;
3293
+ if (!Number.isFinite(thickness) || thickness <= 0) return;
3294
+ let position = x;
3295
+ for (const item of current) {
3296
+ const length = area(item) / thickness;
3297
+ placed.push({ item, x: position, y, w: length, h: thickness });
3298
+ position += length;
3299
+ }
3300
+ y += thickness;
3301
+ h -= thickness;
3302
+ } else {
3303
+ if (h <= 0) return;
3304
+ const thickness = sum / h;
3305
+ if (!Number.isFinite(thickness) || thickness <= 0) return;
3306
+ let position = y;
3307
+ for (const item of current) {
3308
+ const length = area(item) / thickness;
3309
+ placed.push({ item, x, y: position, w: thickness, h: length });
3310
+ position += length;
3311
+ }
3312
+ x += thickness;
3313
+ w -= thickness;
3314
+ }
3315
+ }
3316
+ let index = 0;
3317
+ while (index < sorted.length) {
3318
+ const horizontal = w >= h;
3319
+ const side = horizontal ? w : h;
3320
+ const next = sorted[index];
3321
+ if (row.length === 0) {
3322
+ row.push(next);
3323
+ index++;
3324
+ continue;
3325
+ }
3326
+ if (worst([...row, next], side) <= worst(row, side)) {
3327
+ row.push(next);
3328
+ index++;
3329
+ } else {
3330
+ place(row, horizontal);
3331
+ row = [];
3332
+ }
3333
+ }
3334
+ if (row.length > 0) place(row, w >= h);
3335
+ return placed;
3336
+ }
3337
+
3338
+ // packages/render/src/layout.ts
3339
+ var VIEW_WIDTH = 1400;
3340
+ var VIEW_HEIGHT = 900;
3341
+ var VIEW_PADDING = 8;
3342
+ var MAX_DEPTH3 = 12;
3343
+ function layoutMap(roots) {
3344
+ const rect = {
3345
+ x: VIEW_PADDING,
3346
+ y: VIEW_PADDING,
3347
+ w: VIEW_WIDTH - VIEW_PADDING * 2,
3348
+ h: VIEW_HEIGHT - VIEW_PADDING * 2
3349
+ };
3350
+ return layoutChildren(roots, rect, 0);
3351
+ }
3352
+ function layoutChildren(nodes, rect, depth) {
3353
+ if (nodes.length === 0 || rect.w < 2 || rect.h < 2) return [];
3354
+ const items = nodes.map((node) => ({
3355
+ key: node.key,
3356
+ value: Math.max(1, node.value),
3357
+ node
3358
+ }));
3359
+ return squarify(items, rect).map(
3360
+ (placed) => layoutNode(placed.item.node, {
3361
+ x: placed.x,
3362
+ y: placed.y,
3363
+ w: placed.w,
3364
+ h: placed.h
3365
+ }, depth)
3366
+ );
3367
+ }
3368
+ function layoutNode(node, rect, depth) {
3369
+ if (node.kind === "file" || !node.children || node.children.length === 0) {
3370
+ return { node, rect, headerHeight: 0, children: [] };
3371
+ }
3372
+ let headerHeight = 0;
3373
+ if (rect.w >= 30 && rect.h >= 32) {
3374
+ headerHeight = rect.h >= 62 ? 17 : 13;
3375
+ }
3376
+ const inner = {
3377
+ x: rect.x + 1,
3378
+ y: rect.y + headerHeight + 1,
3379
+ w: Math.max(0, rect.w - 2),
3380
+ h: Math.max(0, rect.h - headerHeight - 2)
3381
+ };
3382
+ const children = depth < MAX_DEPTH3 && inner.w >= 1 && inner.h >= 1 ? layoutChildren(node.children, inner, depth + 1) : [];
3383
+ return { node, rect, headerHeight, children };
3384
+ }
3385
+
3386
+ // packages/render/src/svg.ts
3387
+ function escapeHtml(value) {
3388
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3389
+ }
3390
+ function escapeAttr(value) {
3391
+ return escapeHtml(value);
3392
+ }
3393
+ function formatInt(value) {
3394
+ if (!Number.isFinite(value)) return "\u2014";
3395
+ return Math.round(value).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
3396
+ }
3397
+ function formatCompact(value) {
3398
+ if (!Number.isFinite(value)) return "\u2014";
3399
+ const absolute = Math.abs(value);
3400
+ if (absolute >= 1e6) {
3401
+ return `${(value / 1e6).toFixed(1).replace(".0", "").replace(".", ",")}M`;
3402
+ }
3403
+ if (absolute >= 1e3) {
3404
+ return `${(value / 1e3).toFixed(1).replace(".0", "").replace(".", ",")}k`;
3405
+ }
3406
+ return formatInt(value);
3407
+ }
3408
+ function coord(value) {
3409
+ const rounded = Math.round(value);
3410
+ return Object.is(rounded, -0) ? "0" : String(rounded);
3411
+ }
3412
+ function span(value) {
3413
+ return String(Math.max(1, Math.round(value)));
3414
+ }
3415
+ function truncate(value, maxWidth, fontSize) {
3416
+ const maxChars = Math.floor(maxWidth / (fontSize * 0.56));
3417
+ if (maxChars <= 0) return "";
3418
+ if (value.length <= maxChars) return value;
3419
+ if (maxChars === 1) return "\u2026";
3420
+ return `${value.slice(0, maxChars - 1)}\u2026`;
3421
+ }
3422
+ function labelFor(nodeName, rect, out, ink) {
3423
+ if (rect.w < 44 || rect.h < 13) return;
3424
+ const available = rect.w - 8;
3425
+ const dot = nodeName.lastIndexOf(".");
3426
+ const twoLines = rect.h >= 27 && rect.w >= 64 && dot > 0 && dot < nodeName.length - 1;
3427
+ if (twoLines) {
3428
+ const base = truncate(nodeName.slice(0, dot), available, 12.5);
3429
+ const extension = truncate(nodeName.slice(dot), available, 10);
3430
+ if (base) {
3431
+ out.push(
3432
+ `<text class="a" x="${coord(rect.x + 4)}" y="${coord(rect.y + 12)}" fill="${ink}">${escapeHtml(base)}</text>`
3433
+ );
3434
+ }
3435
+ if (extension) {
3436
+ out.push(
3437
+ `<text class="b" x="${coord(rect.x + 4)}" y="${coord(rect.y + 22.5)}" fill="${ink}">${escapeHtml(extension)}</text>`
3438
+ );
3439
+ }
3440
+ return;
3441
+ }
3442
+ const label = truncate(nodeName, available, 12.5);
3443
+ if (!label) return;
3444
+ out.push(
3445
+ `<text class="a" x="${coord(rect.x + 4)}" y="${coord(rect.y + 13)}" fill="${ink}">${escapeHtml(label)}</text>`
3446
+ );
3447
+ }
3448
+ function renderPlaced(placed, out, model, metricIndex, metric) {
3449
+ const node = placed.node;
3450
+ const rect = placed.rect;
3451
+ if (node.kind === "file") {
3452
+ const index = node.fileIndex;
3453
+ if (index === void 0) return;
3454
+ const file = model.files[index];
3455
+ if (!file) return;
3456
+ const value = metricIndex >= 0 ? file.values[metricIndex] ?? null : null;
3457
+ const fill = colorForFile(
3458
+ model.initialLens,
3459
+ file.risk,
3460
+ model.sections[file.section],
3461
+ value,
3462
+ metric
3463
+ );
3464
+ const ink = inkFor(fill);
3465
+ out.push(
3466
+ `<rect data-i="${index}" x="${coord(rect.x)}" y="${coord(rect.y)}" width="${span(rect.w)}" height="${span(rect.h)}" fill="${fill}"/>`
3467
+ );
3468
+ labelFor(file.name, rect, out, ink);
3469
+ return;
3470
+ }
3471
+ const folder = node;
3472
+ out.push(
3473
+ `<g class="folder" data-nm="${escapeAttr(folder.name)}" data-n="${folder.fileCount}" data-a="${coord(folder.value)}">`
3474
+ );
3475
+ out.push(
3476
+ `<rect class="fr" x="${coord(rect.x)}" y="${coord(rect.y)}" width="${span(rect.w)}" height="${span(rect.h)}"/>`
3477
+ );
3478
+ if (placed.headerHeight >= 13 && rect.w >= 30) {
3479
+ const headerHeight = placed.headerHeight;
3480
+ out.push(
3481
+ `<rect class="fh" x="${coord(rect.x + 1)}" y="${coord(rect.y + 1)}" width="${span(rect.w - 2)}" height="${span(headerHeight - 1)}"/>`
3482
+ );
3483
+ const count = formatCompact(folder.value);
3484
+ const showCount = rect.w >= 86;
3485
+ const countWidth = showCount ? count.length * 5.8 + 12 : 0;
3486
+ const name = truncate(folder.name, Math.max(8, rect.w - 12 - countWidth), 11.5);
3487
+ if (name) {
3488
+ out.push(
3489
+ `<text class="ft" x="${coord(rect.x + 6)}" y="${coord(rect.y + headerHeight / 2 + 4)}">${escapeHtml(name)}</text>`
3490
+ );
3491
+ }
3492
+ if (showCount) {
3493
+ out.push(
3494
+ `<text class="fn" x="${coord(rect.x + rect.w - 6)}" y="${coord(rect.y + headerHeight / 2 + 3.6)}">${escapeHtml(count)}</text>`
3495
+ );
3496
+ }
3497
+ }
3498
+ for (const child of placed.children) {
3499
+ renderPlaced(child, out, model, metricIndex, metric);
3500
+ }
3501
+ out.push("</g>");
3502
+ }
3503
+ function renderSvg(model) {
3504
+ const metricIndex = model.initialMetric ? model.metrics.findIndex((metric2) => metric2.key === model.initialMetric) : -1;
3505
+ const metric = metricIndex >= 0 ? model.metrics[metricIndex] : void 0;
3506
+ const out = [];
3507
+ out.push(
3508
+ `<svg id="map" viewBox="0 0 ${VIEW_WIDTH} ${VIEW_HEIGHT}" preserveAspectRatio="xMidYMid meet" role="img" aria-label="Mapa de arquitectura de ${escapeAttr(model.meta.project)}">`
3509
+ );
3510
+ out.push(
3511
+ `<rect class="canvas" x="0" y="0" width="${VIEW_WIDTH}" height="${VIEW_HEIGHT}"/>`
3512
+ );
3513
+ out.push(`<g id="cells">`);
3514
+ for (const placed of layoutMap(model.roots)) {
3515
+ renderPlaced(placed, out, model, metricIndex, metric);
3516
+ }
3517
+ out.push(`</g><g id="edges"></g></svg>`);
3518
+ return out.join("");
3519
+ }
3520
+
3521
+ // packages/render/src/html.ts
3522
+ var STYLES = readFileSync(new URL("./styles.css", import.meta.url), "utf8");
3523
+ var CLIENT = readFileSync(new URL("./client.js", import.meta.url), "utf8").replace(
3524
+ /<\/script/gi,
3525
+ "<\\/script"
3526
+ );
3527
+ function round(value, digits = 2) {
3528
+ const factor = 10 ** digits;
3529
+ return Math.round(value * factor) / factor;
3530
+ }
3531
+ function jsonForScript(value) {
3532
+ return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
3533
+ }
3534
+ function serialise(model) {
3535
+ const directoryIndex = /* @__PURE__ */ new Map();
3536
+ const directories = [];
3537
+ const fileRows = model.files.map((file) => {
3538
+ const slash = file.path.lastIndexOf("/");
3539
+ const directory = slash >= 0 ? file.path.slice(0, slash) : "";
3540
+ let dirIndex = -1;
3541
+ if (directory) {
3542
+ const known = directoryIndex.get(directory);
3543
+ if (known === void 0) {
3544
+ dirIndex = directories.length;
3545
+ directoryIndex.set(directory, dirIndex);
3546
+ directories.push(directory);
3547
+ } else {
3548
+ dirIndex = known;
3549
+ }
3550
+ }
3551
+ const name = slash >= 0 ? file.path.slice(slash + 1) : file.path;
3552
+ return [
3553
+ dirIndex,
3554
+ name,
3555
+ file.section,
3556
+ round(file.area, 1),
3557
+ round(file.actualArea, 1),
3558
+ round(file.risk, 4),
3559
+ round(file.complexity, 1),
3560
+ round(file.churn, 1),
3561
+ ...file.values.map((value) => value === null ? null : round(value, 2))
3562
+ ];
3563
+ });
3564
+ return {
3565
+ v: 1,
3566
+ meta: {
3567
+ project: model.meta.project,
3568
+ commit: model.meta.commit,
3569
+ checksum: model.meta.checksum,
3570
+ api: model.meta.apiVersion,
3571
+ root: model.meta.rootName
3572
+ },
3573
+ area: {
3574
+ key: model.areaMetric,
3575
+ title: model.areaTitle,
3576
+ total: round(model.totalArea, 1)
3577
+ },
3578
+ lens: model.initialLens,
3579
+ metric: model.initialMetric,
3580
+ metrics: model.metrics.map((metric) => ({
3581
+ key: metric.key,
3582
+ title: metric.title,
3583
+ min: round(metric.min, 2),
3584
+ max: round(metric.max, 2),
3585
+ mode: metric.mode,
3586
+ dir: metric.direction,
3587
+ coverage: metric.coverage
3588
+ })),
3589
+ sections: model.sections.map((section) => ({
3590
+ key: section.key,
3591
+ color: section.color,
3592
+ files: section.files,
3593
+ area: round(section.area, 1)
3594
+ })),
3595
+ dirs: directories,
3596
+ files: fileRows,
3597
+ edges: model.edges.flatMap((edge) => [edge.from, edge.to]),
3598
+ cycles: model.cycles.flatMap((edge) => [edge.from, edge.to]),
3599
+ hot: model.hotspotCount,
3600
+ default: model.defaultSelection,
3601
+ fileCount: model.fileCount,
3602
+ folderCount: model.folderCount,
3603
+ totalArea: round(model.totalArea, 1)
3604
+ };
3605
+ }
3606
+ function renderLensButtons(model) {
3607
+ const lenses = [
3608
+ { id: "structure", label: "Estructura" },
3609
+ { id: "risk", label: "Riesgo" }
3610
+ ];
3611
+ if (model.metrics.length > 0) lenses.push({ id: "metric", label: "M\xE9trica" });
3612
+ return lenses.map(
3613
+ (lens) => `<button type="button" class="lens" role="tab" data-lens="${lens.id}" aria-selected="${String(
3614
+ model.initialLens === lens.id
3615
+ )}">${lens.label}</button>`
3616
+ ).join("");
3617
+ }
3618
+ function renderFindings(model) {
3619
+ const chips = [];
3620
+ if (model.hotspotCount > 0) {
3621
+ chips.push(
3622
+ `<button type="button" class="finding" data-focus="hotspot"><i class="dot info"></i><b>${formatInt(
3623
+ model.hotspotCount
3624
+ )}</b> hotspots</button>`
3625
+ );
3626
+ }
3627
+ if (model.cycles.length > 0) {
3628
+ chips.push(
3629
+ `<button type="button" class="finding" data-focus="cycle"><i class="dot warn"></i><b>${formatInt(
3630
+ model.cycles.length
3631
+ )}</b> aristas c\xEDclicas</button>`
3632
+ );
3633
+ }
3634
+ chips.push(
3635
+ `<span class="finding static"><i class="dot neutral"></i><b>${formatInt(
3636
+ model.fileCount
3637
+ )}</b> ficheros</span>`
3638
+ );
3639
+ chips.push(
3640
+ `<span class="finding static"><i class="dot neutral"></i><b>${formatInt(
3641
+ model.edges.length
3642
+ )}</b> dependencias</span>`
3643
+ );
3644
+ return `${chips.join("")}<span class="spacer"></span><span id="search-status" class="search-status" aria-live="polite"></span><span class="run-meta">${escapeHtml(
3645
+ model.areaTitle
3646
+ )} \xB7 ${formatInt(model.metrics.length)} m\xE9tricas \xB7 cc.json ${escapeHtml(
3647
+ model.meta.apiVersion
3648
+ )}</span>`;
3649
+ }
3650
+ function metricByKey(model, key) {
3651
+ return key ? model.metrics.find((metric) => metric.key === key) : void 0;
3652
+ }
3653
+ function renderLegend(model) {
3654
+ if (model.initialLens === "structure") {
3655
+ const items = model.sections.slice(0, 6).map(
3656
+ (section) => `<span><i style="background:${section.color}"></i>${escapeHtml(section.key)}</span>`
3657
+ ).join("");
3658
+ return `<span>Carpeta</span><span class="sws">${items}</span>`;
3659
+ }
3660
+ if (model.initialLens === "metric" && model.initialMetric) {
3661
+ const metric = metricByKey(model, model.initialMetric);
3662
+ if (metric) {
3663
+ const direction = metric.direction === -1 ? " \xB7 menos es mejor" : metric.direction === 1 ? " \xB7 m\xE1s es mejor" : "";
3664
+ return `<span>${escapeHtml(metric.title)}${direction}</span><span class="mono">${escapeHtml(
3665
+ formatCompact(metric.min)
3666
+ )}</span><span class="bar" style="background:${METRIC_GRADIENT}"></span><span class="mono">${escapeHtml(
3667
+ formatCompact(metric.max)
3668
+ )}</span>`;
3669
+ }
3670
+ }
3671
+ return `<span>Riesgo (0\u2013100)</span><span class="mono">0</span><span class="bar" style="background:${RISK_GRADIENT}"></span><span class="mono">100</span>`;
3672
+ }
3673
+ function renderMetricOptions(model) {
3674
+ return model.metrics.map(
3675
+ (metric) => `<option value="${escapeAttr(metric.key)}"${metric.key === model.initialMetric ? " selected" : ""}>${escapeHtml(metric.title)}</option>`
3676
+ ).join("");
3677
+ }
3678
+ function renderInspectorSummary(model) {
3679
+ const hotspots = [...model.files].sort((a, b) => b.risk - a.risk || a.path.localeCompare(b.path)).slice(0, 5);
3680
+ return `<div class="summary">
3681
+ <div class="sub">Proyecto</div>
3682
+ <div class="big">${formatInt(model.fileCount)}</div>
3683
+ <div class="sub">ficheros \xB7 ${formatInt(model.folderCount)} carpetas</div>
3684
+ <p>${formatInt(model.totalArea)} de ${escapeHtml(
3685
+ model.areaTitle.toLowerCase()
3686
+ )} \xB7 ${formatInt(model.metrics.length)} m\xE9tricas \xB7 ${formatInt(
3687
+ model.hotspotCount
3688
+ )} hotspots.</p>
3689
+ <h4>Mayor riesgo</h4>
3690
+ <ul class="deps">${hotspots.map(
3691
+ (file) => `<li data-file="${file.index}"><i class="sw" style="background:${riskColor(
3692
+ file.risk
3693
+ )}"></i><span>${escapeHtml(file.path)}</span><em>${Math.round(
3694
+ file.risk * 100
3695
+ )}</em></li>`
3696
+ ).join("")}</ul>
3697
+ <p>Selecciona un bloque para ver sus m\xE9tricas y sus dependencias. Clic en la cabecera de una carpeta para enfocarla.</p>
3698
+ </div>`;
3699
+ }
3700
+ function renderHtml(model) {
3701
+ const project = escapeHtml(model.meta.project);
3702
+ const title = escapeHtml(model.meta.title);
3703
+ const commit = model.meta.commit ? model.meta.commit.slice(0, 7) : "";
3704
+ const checksum = model.meta.checksum ? model.meta.checksum.slice(0, 8) : "";
3705
+ const badge = commit || checksum || `cc.json ${model.meta.apiVersion}`;
3706
+ const payload = jsonForScript(serialise(model));
3707
+ const metricPicker = model.metrics.length > 0 ? `<label class="metric-pick" id="metric-pick"${model.initialLens === "metric" ? "" : " hidden"}><span>M\xE9trica</span><select id="metric-select">${renderMetricOptions(
3708
+ model
3709
+ )}</select></label>` : "";
3710
+ const crumbs = model.meta.rootName ? `${project} <span class="sep">/</span> ${escapeHtml(model.meta.rootName)}` : project;
3711
+ return `<!doctype html>
3712
+ <html lang="es">
3713
+ <head>
3714
+ <meta charset="utf-8">
3715
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3716
+ <meta name="color-scheme" content="light">
3717
+ <meta name="generator" content="CodArch">
3718
+ <title>CodArch \xB7 ${title}</title>
3719
+ <style>${STYLES}</style>
3720
+ </head>
3721
+ <body>
3722
+ <div class="app">
3723
+ <header>
3724
+ <div class="brand">CodArch</div>
3725
+ <div class="repo">
3726
+ <b>${title}</b>
3727
+ ${model.meta.rootName && model.meta.rootName !== model.meta.project ? `<span>${escapeHtml(model.meta.rootName)}</span>` : ""}
3728
+ <span class="commit">${escapeHtml(badge)}</span>
3729
+ </div>
3730
+ <div class="spacer"></div>
3731
+ <nav class="lenses" role="tablist" aria-label="Lentes">${renderLensButtons(model)}</nav>
3732
+ ${metricPicker}
3733
+ <label class="search">
3734
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="none" aria-hidden="true">
3735
+ <circle cx="7" cy="7" r="4.5" stroke="#8d877d" stroke-width="1.4"/>
3736
+ <path d="M10.5 10.5 14 14" stroke="#8d877d" stroke-width="1.4"/>
3737
+ </svg>
3738
+ <input id="q" type="search" placeholder="Buscar fichero\u2026" autocomplete="off" aria-label="Buscar fichero">
3739
+ </label>
3740
+ </header>
3741
+
3742
+ <div class="findings">${renderFindings(model)}</div>
3743
+
3744
+ <main class="map-wrap">
3745
+ <div class="map" id="map">${renderSvg(model)}</div>
3746
+ <div class="tooltip" id="tooltip"></div>
3747
+ </main>
3748
+
3749
+ <aside id="inspector">${renderInspectorSummary(model)}</aside>
3750
+
3751
+ <footer>
3752
+ <span class="crumbs" id="crumbs">${crumbs}</span>
3753
+ <span class="hint">clic para fijar \xB7 esc para limpiar</span>
3754
+ <span class="legend" id="legend">${renderLegend(model)}</span>
3755
+ </footer>
3756
+ </div>
3757
+ <script id="codarch-data" type="application/json">${payload}</script>
3758
+ <script>${CLIENT}</script>
3759
+ </body>
3760
+ </html>`;
3761
+ }
3762
+
3763
+ // apps/cli/src/io.ts
3764
+ import fs6 from "node:fs";
3765
+ import zlib from "node:zlib";
3766
+ function readJsonFile2(file) {
3767
+ const buffer = fs6.readFileSync(file);
3768
+ const isGzip = buffer.length > 2 && buffer[0] === 31 && buffer[1] === 139;
3769
+ const raw = isGzip ? zlib.gunzipSync(buffer) : buffer;
3770
+ const text = raw.toString("utf8");
3771
+ try {
3772
+ return { json: JSON.parse(text), bytes: buffer.length };
3773
+ } catch (error) {
3774
+ const message = error instanceof Error ? error.message : String(error);
3775
+ throw new Error(`no es JSON v\xE1lido: ${message}`);
3776
+ }
3777
+ }
3778
+ function formatBytes(bytes) {
3779
+ if (bytes < 1024) return `${bytes} B`;
3780
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
3781
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
3782
+ }
3783
+
3784
+ // apps/cli/src/commands/analyze.ts
3785
+ var numberFormat = new Intl.NumberFormat("es-ES");
3786
+ function num(value) {
3787
+ return numberFormat.format(value);
3788
+ }
3789
+ function field(label, value) {
3790
+ return ` ${label.padEnd(12)} ${value}`;
3791
+ }
3792
+ function runAnalyze(options) {
3793
+ const root = options.root ?? ".";
3794
+ let result;
3795
+ try {
3796
+ result = analyzeRepository(root, {
3797
+ ignore: options.ignore,
3798
+ projectName: options.projectName,
3799
+ git: options.git
3800
+ });
3801
+ } catch (error) {
3802
+ console.error(`no se pudo analizar ${root}: ${error.message}`);
3803
+ return 1;
3804
+ }
3805
+ let cc;
3806
+ try {
3807
+ cc = parseCcJson(result.snapshot);
3808
+ } catch (error) {
3809
+ if (error instanceof CcJsonError) {
3810
+ console.error(`el snapshot generado no es cc.json v\xE1lido: ${error.message}`);
3811
+ return 1;
3812
+ }
3813
+ throw error;
3814
+ }
3815
+ const outDirectory = path5.resolve(
3816
+ options.out ?? path5.join(path5.resolve(root), ".codarch")
3817
+ );
3818
+ const snapshotPath = path5.join(outDirectory, "snapshot.cc.json");
3819
+ const mapPath = path5.join(outDirectory, "map.html");
3820
+ try {
3821
+ fs7.mkdirSync(outDirectory, { recursive: true });
3822
+ fs7.writeFileSync(
3823
+ snapshotPath,
3824
+ `${JSON.stringify(cc, null, 2)}
3825
+ `,
3826
+ "utf8"
3827
+ );
3828
+ } catch (error) {
3829
+ console.error(
3830
+ `no se pudo escribir ${snapshotPath}: ${error.message}`
3831
+ );
3832
+ return 1;
3833
+ }
3834
+ let mapBytes = 0;
3835
+ let mapError = null;
3836
+ if (result.files > 0) {
3837
+ try {
3838
+ const model = buildRenderModel(cc, {
3839
+ areaMetric: "rloc",
3840
+ color: "risk",
3841
+ title: options.title
3842
+ });
3843
+ const html = renderHtml(model);
3844
+ fs7.writeFileSync(mapPath, html, "utf8");
3845
+ mapBytes = Buffer.byteLength(html, "utf8");
3846
+ } catch (error) {
3847
+ mapError = error instanceof RenderError ? error.message : error.message;
3848
+ }
3849
+ } else {
3850
+ mapError = "no hay ficheros TypeScript/JavaScript que dibujar";
3851
+ }
3852
+ const snapshotBytes = Buffer.byteLength(
3853
+ fs7.readFileSync(snapshotPath),
3854
+ "utf8"
3855
+ );
3856
+ const lines = [];
3857
+ lines.push("");
3858
+ lines.push(" CodArch \xB7 an\xE1lisis");
3859
+ lines.push("");
3860
+ lines.push(field("ra\xEDz", path5.resolve(root)));
3861
+ lines.push(field("snapshot", `${snapshotPath} (${formatBytes(snapshotBytes)})`));
3862
+ if (mapError) {
3863
+ lines.push(field("mapa", `omitido (${mapError})`));
3864
+ } else {
3865
+ lines.push(field("mapa", `${mapPath} (${formatBytes(mapBytes)})`));
3866
+ }
3867
+ lines.push(
3868
+ field(
3869
+ "estructura",
3870
+ `${num(result.files)} ficheros \xB7 ${num(result.folders)} carpetas \xB7 ${num(
3871
+ result.edges
3872
+ )} aristas \xB7 ${num(result.cycles.length)} ciclos`
3873
+ )
3874
+ );
3875
+ lines.push(
3876
+ field(
3877
+ "imports",
3878
+ `${num(result.externalImports)} externos \xB7 ${num(
3879
+ result.builtinImports
3880
+ )} builtins \xB7 ${num(result.typeOnlyImports)} type-only \xB7 ${num(
3881
+ result.unresolvedImports
3882
+ )} sin resolver`
3883
+ )
3884
+ );
3885
+ lines.push(field("tiempo", `${num(result.durationMs)} ms`));
3886
+ lines.push(
3887
+ field(
3888
+ "historial",
3889
+ result.git.enabled ? `${num(result.git.commits)} commits \xB7 ${num(
3890
+ result.git.authors
3891
+ )} autores${result.git.shallow ? " \xB7 shallow" : ""}` : "sin git; se omite churn"
3892
+ )
3893
+ );
3894
+ for (const warning of result.warnings.slice(0, 10)) {
3895
+ lines.push(field("aviso", warning));
3896
+ }
3897
+ if (result.warnings.length > 10) {
3898
+ lines.push(field("aviso", `y ${num(result.warnings.length - 10)} m\xE1s`));
3899
+ }
3900
+ for (const issue of result.unresolved.slice(0, 5)) {
3901
+ lines.push(
3902
+ field(
3903
+ "sin resolver",
3904
+ `${issue.file}:${issue.line} ${issue.specifier} (${issue.reason})`
3905
+ )
3906
+ );
3907
+ }
3908
+ if (result.unresolved.length > 5) {
3909
+ lines.push(
3910
+ field("sin resolver", `y ${num(result.unresolved.length - 5)} m\xE1s`)
3911
+ );
3912
+ }
3913
+ lines.push("");
3914
+ console.log(lines.join("\n"));
3915
+ return 0;
3916
+ }
3917
+
3918
+ // apps/cli/src/commands/diff.ts
3919
+ import fs8 from "node:fs";
3920
+ import path6 from "node:path";
3921
+ var numberFormat2 = new Intl.NumberFormat("es-ES");
3922
+ function num2(value) {
3923
+ return numberFormat2.format(value);
3924
+ }
3925
+ function field2(label, value) {
3926
+ return ` ${label.padEnd(12)} ${value}`;
3927
+ }
3928
+ function signed(value) {
3929
+ if (value === 0) return "0";
3930
+ return value > 0 ? `+${num2(value)}` : num2(value);
3931
+ }
3932
+ function runDiff(options) {
3933
+ if (!options.base || !options.head) {
3934
+ console.error(
3935
+ "faltan los dos snapshots\n\n codarch diff <base.cc.json> <head.cc.json> [-o report.json]"
3936
+ );
3937
+ return 1;
3938
+ }
3939
+ const base = loadSnapshot(options.base);
3940
+ if (!base) return 1;
3941
+ const head = loadSnapshot(options.head);
3942
+ if (!head) return 1;
3943
+ let rules;
3944
+ let configPath;
3945
+ try {
3946
+ if (!options.noConfig) {
3947
+ configPath = options.config ?? defaultConfigPath();
3948
+ if (configPath) rules = loadRuleConfig(configPath);
3949
+ }
3950
+ } catch (error) {
3951
+ if (error instanceof ConfigError) {
3952
+ console.error(`configuraci\xF3n inv\xE1lida: ${error.message}`);
3953
+ return 1;
3954
+ }
3955
+ throw error;
3956
+ }
3957
+ let report;
3958
+ try {
3959
+ report = diffSnapshots(base, head, {
3960
+ top: options.top,
3961
+ hotspotMetric: options.hotspotMetric,
3962
+ rules
3963
+ });
3964
+ } catch (error) {
3965
+ console.error(`no se pudo comparar: ${error.message}`);
3966
+ return 1;
3967
+ }
3968
+ const outPath = path6.resolve(options.out ?? "report.json");
3969
+ let reportBytes = 0;
3970
+ try {
3971
+ fs8.mkdirSync(path6.dirname(outPath), { recursive: true });
3972
+ const text = `${JSON.stringify(report, null, 2)}
3973
+ `;
3974
+ fs8.writeFileSync(outPath, text, "utf8");
3975
+ reportBytes = Buffer.byteLength(text, "utf8");
3976
+ } catch (error) {
3977
+ console.error(
3978
+ `no se pudo escribir ${outPath}: ${error.message}`
3979
+ );
3980
+ return 1;
3981
+ }
3982
+ const lines = [];
3983
+ lines.push("");
3984
+ lines.push(" CodArch \xB7 diff");
3985
+ lines.push("");
3986
+ lines.push(field2("base", `${options.base} (${report.base.checksum || "sin checksum"})`));
3987
+ lines.push(field2("head", `${options.head} (${report.head.checksum || "sin checksum"})`));
3988
+ lines.push(
3989
+ field2(
3990
+ "ficheros",
3991
+ `${signed(report.summary.addedFiles)} \xB7 ${signed(
3992
+ -report.summary.removedFiles
3993
+ )} \xB7 ~${num2(report.summary.changedFiles)}`
3994
+ )
3995
+ );
3996
+ lines.push(
3997
+ field2(
3998
+ "aristas",
3999
+ `${signed(report.summary.addedEdges)} \xB7 ${signed(
4000
+ -report.summary.removedEdges
4001
+ )} \xB7 ${num2(report.summary.cyclicEdgesAdded)} c\xEDclicas nuevas`
4002
+ )
4003
+ );
4004
+ lines.push(
4005
+ field2(
4006
+ "ciclos",
4007
+ `${signed(report.summary.introducedCycles)} nuevos \xB7 ${signed(
4008
+ -report.summary.fixedCycles
4009
+ )} resueltos`
4010
+ )
4011
+ );
4012
+ lines.push(
4013
+ field2(
4014
+ "hotspots",
4015
+ report.hotspotMetric ? `${num2(report.summary.hotspots)} (${report.hotspotMetric})` : "sin m\xE9trica de complejidad ni tama\xF1o"
4016
+ )
4017
+ );
4018
+ lines.push(
4019
+ field2(
4020
+ "reglas",
4021
+ report.rules.configured ? `${num2(report.rules.summary.layers)} capas \xB7 ${num2(
4022
+ report.rules.summary.rules
4023
+ )} reglas \xB7 ${num2(report.rules.summary.introduced)} nuevas \xB7 ${num2(
4024
+ report.rules.summary.existing
4025
+ )} existentes${configPath ? ` (${configPath})` : ""}` : "sin configuraci\xF3n"
4026
+ )
4027
+ );
4028
+ lines.push(field2("reporte", `${outPath} (${formatBytes(reportBytes)})`));
4029
+ lines.push("");
4030
+ if (report.cycles.introduced.length === 0) {
4031
+ lines.push(" Sin ciclos nuevos.");
4032
+ } else {
4033
+ lines.push(" Ciclos nuevos:");
4034
+ for (const cycle of report.cycles.introduced.slice(0, 5)) {
4035
+ lines.push(
4036
+ ` \u2717 ${cycle.nodes.map((node) => node.path).join(" \u2192 ")}`
4037
+ );
4038
+ }
4039
+ if (report.cycles.introduced.length > 5) {
4040
+ lines.push(
4041
+ ` \u2026 y ${num2(report.cycles.introduced.length - 5)} m\xE1s`
4042
+ );
4043
+ }
4044
+ }
4045
+ if (report.hotspots.length > 0) {
4046
+ lines.push("");
4047
+ lines.push(` Hotspots (${report.hotspotMetric}):`);
4048
+ for (const hotspot of report.hotspots.slice(0, 5)) {
4049
+ lines.push(
4050
+ ` \u25CF ${hotspot.path} ${num2(hotspot.base)} \u2192 ${num2(
4051
+ hotspot.head
4052
+ )} (${signed(hotspot.delta)})`
4053
+ );
4054
+ }
4055
+ if (report.hotspots.length > 5) {
4056
+ lines.push(` \u2026 y ${num2(report.hotspots.length - 5)} m\xE1s`);
4057
+ }
4058
+ }
4059
+ if (report.rules.violations.introduced.length > 0) {
4060
+ lines.push("");
4061
+ lines.push(" Violaciones nuevas:");
4062
+ for (const violation of report.rules.violations.introduced.slice(0, 5)) {
4063
+ const icon = violation.severity === "error" ? "\u2717" : violation.severity === "warn" ? "!" : "\xB7";
4064
+ lines.push(` ${icon} [${violation.rule}] ${violation.message}`);
4065
+ }
4066
+ if (report.rules.violations.introduced.length > 5) {
4067
+ lines.push(
4068
+ ` \u2026 y ${num2(report.rules.violations.introduced.length - 5)} m\xE1s`
4069
+ );
4070
+ }
4071
+ }
4072
+ lines.push("");
4073
+ console.log(lines.join("\n"));
4074
+ const failOn = options.failOn ?? "none";
4075
+ const cyclesFailed = (failOn === "cycles" || failOn === "any") && report.summary.introducedCycles > 0;
4076
+ const hotspotsFailed = (failOn === "hotspots" || failOn === "any") && report.summary.hotspots > 0;
4077
+ const violationsFailed = (failOn === "violations" || failOn === "any") && report.rules.summary.introducedErrors > 0;
4078
+ if (cyclesFailed || hotspotsFailed || violationsFailed) {
4079
+ console.error(
4080
+ ` gate fallido: ${[
4081
+ cyclesFailed ? "ciclos nuevos" : "",
4082
+ hotspotsFailed ? "hotspots" : "",
4083
+ violationsFailed ? "violaciones nuevas" : ""
4084
+ ].filter(Boolean).join(" + ")}`
4085
+ );
4086
+ return 2;
4087
+ }
4088
+ return 0;
4089
+ }
4090
+ function loadSnapshot(file) {
4091
+ let loaded;
4092
+ try {
4093
+ loaded = readJsonFile2(file);
4094
+ } catch (error) {
4095
+ console.error(`no se pudo leer ${file}: ${error.message}`);
4096
+ return null;
4097
+ }
4098
+ try {
4099
+ return parseCcJson(loaded.json);
4100
+ } catch (error) {
4101
+ if (error instanceof CcJsonError) {
4102
+ console.error(`cc.json inv\xE1lido en ${file}: ${error.message}`);
4103
+ return null;
4104
+ }
4105
+ throw error;
4106
+ }
4107
+ }
4108
+ function defaultConfigPath() {
4109
+ for (const file of [".codarch.yml", ".codarch.yaml"]) {
4110
+ if (fs8.existsSync(file)) return file;
4111
+ }
4112
+ return void 0;
4113
+ }
4114
+
4115
+ // apps/cli/src/commands/inspect.ts
4116
+ var numberFormat3 = new Intl.NumberFormat("es-ES");
4117
+ function num3(value) {
4118
+ return numberFormat3.format(value);
4119
+ }
4120
+ function field3(label, value) {
4121
+ return ` ${label.padEnd(12)} ${value}`;
4122
+ }
4123
+ function truncateLeft(value, max = 78) {
4124
+ if (value.length <= max) return value;
4125
+ return `\u2026${value.slice(value.length - max + 1)}`;
4126
+ }
4127
+ function shortChecksum(value) {
4128
+ return value ? `${value.slice(0, 8)}\u2026` : "\u2014";
4129
+ }
4130
+ function runInspect(options) {
4131
+ if (!options.file) {
4132
+ console.error("falta la ruta del fichero cc.json\n\n codarch inspect <fichero.cc.json[.gz]>");
4133
+ return 1;
4134
+ }
4135
+ let loaded;
4136
+ try {
4137
+ loaded = readJsonFile2(options.file);
4138
+ } catch (error) {
4139
+ console.error(`no se pudo leer ${options.file}: ${error.message}`);
4140
+ return 1;
4141
+ }
4142
+ let cc;
4143
+ try {
4144
+ cc = parseCcJson(loaded.json);
4145
+ } catch (error) {
4146
+ if (error instanceof CcJsonError) {
4147
+ console.error(`cc.json inv\xE1lido: ${error.message}`);
4148
+ return 1;
4149
+ }
4150
+ throw error;
4151
+ }
4152
+ const stats = getStats(cc);
4153
+ const metric = options.metric ?? defaultMetric(stats);
4154
+ const top = metric ? topByMetric(cc, metric, options.top) : [];
4155
+ const rootPrefix = cc.files.length === 1 ? `${cc.files[0]?.name}/` : "";
4156
+ const displayPath = (path8) => rootPrefix && path8.startsWith(rootPrefix) ? path8.slice(rootPrefix.length) : path8;
4157
+ if (options.metric && !stats.metricCoverage.some((entry) => entry.name === options.metric)) {
4158
+ console.error(`la m\xE9trica "${options.metric}" no existe en este fichero`);
4159
+ return 1;
4160
+ }
4161
+ if (options.json) {
4162
+ console.log(
4163
+ JSON.stringify(
4164
+ {
4165
+ file: options.file,
4166
+ meta: cc.meta,
4167
+ stats,
4168
+ metric: metric ?? null,
4169
+ top
4170
+ },
4171
+ null,
4172
+ 2
4173
+ )
4174
+ );
4175
+ return 0;
4176
+ }
4177
+ const lines = [];
4178
+ lines.push("");
4179
+ lines.push(" CodArch \xB7 inspecci\xF3n");
4180
+ lines.push("");
4181
+ lines.push(field3("fichero", `${options.file} (${formatBytes(loaded.bytes)})`));
4182
+ lines.push(field3("formato", `cc.json ${cc.meta.apiVersion}`));
4183
+ lines.push(field3("proyecto", cc.meta.projectName || "(sin nombre)"));
4184
+ lines.push(field3("checksum", shortChecksum(cc.meta.checksum)));
4185
+ lines.push(field3("commit", cc.meta.commitHash || "\u2014"));
4186
+ lines.push("");
4187
+ lines.push(
4188
+ field3(
4189
+ "estructura",
4190
+ `${num3(stats.files)} ficheros \xB7 ${num3(stats.folders)} carpetas \xB7 profundidad ${stats.maxDepth}`
4191
+ )
4192
+ );
4193
+ lines.push(field3("lentes", stats.lenses.length ? stats.lenses.join(" \xB7 ") : "ninguna"));
4194
+ const coverage = metric ? stats.metricCoverage.find((entry) => entry.name === metric) : void 0;
4195
+ lines.push(
4196
+ field3(
4197
+ "m\xE9tricas",
4198
+ `${num3(stats.metricNames)} distintas` + (coverage ? ` \xB7 ${metric} en ${num3(coverage.files)} ficheros` : "")
4199
+ )
4200
+ );
4201
+ lines.push(
4202
+ field3("aristas", `${num3(stats.edges)} \xB7 leafEdges ${num3(stats.leafEdges)}`)
4203
+ );
4204
+ if (stats.orphanMetricNodes > 0) {
4205
+ lines.push(field3("hu\xE9rfanos", `${num3(stats.orphanMetricNodes)} nodos de m\xE9tricas sin fichero`));
4206
+ }
4207
+ if (metric && top.length > 0) {
4208
+ lines.push("");
4209
+ lines.push(` Top ${top.length} por ${metric}`);
4210
+ const width = Math.max(...top.map((entry) => num3(entry.value).length));
4211
+ for (const entry of top) {
4212
+ lines.push(` ${num3(entry.value).padStart(width)} ${truncateLeft(displayPath(entry.path))}`);
4213
+ }
4214
+ }
4215
+ lines.push("");
4216
+ console.log(lines.join("\n"));
4217
+ return 0;
4218
+ }
4219
+
4220
+ // apps/cli/src/commands/render.ts
4221
+ import fs9 from "node:fs";
4222
+ import path7 from "node:path";
4223
+ function defaultOutputPath(input) {
4224
+ const lower = input.toLowerCase();
4225
+ const suffixes = [".cc.json.gz", ".cc.json", ".json.gz", ".json", ".gz"];
4226
+ for (const suffix of suffixes) {
4227
+ if (lower.endsWith(suffix)) return `${input.slice(0, -suffix.length)}.html`;
4228
+ }
4229
+ return `${input}.html`;
4230
+ }
4231
+ function defaultProjectName(input) {
4232
+ let name = path7.basename(input.replace(/\\/g, "/"));
4233
+ const lower = name.toLowerCase();
4234
+ const suffixes = [".cc.json.gz", ".cc.json", ".json.gz", ".json", ".gz"];
4235
+ for (const suffix of suffixes) {
4236
+ if (lower.endsWith(suffix)) {
4237
+ name = name.slice(0, -suffix.length);
4238
+ break;
4239
+ }
4240
+ }
4241
+ return name || "Proyecto sin nombre";
4242
+ }
4243
+ var numberFormat4 = new Intl.NumberFormat("es-ES");
4244
+ function num4(value) {
4245
+ return numberFormat4.format(value);
4246
+ }
4247
+ function field4(label, value) {
4248
+ return ` ${label.padEnd(12)} ${value}`;
4249
+ }
4250
+ function runRender(options) {
4251
+ if (!options.file) {
4252
+ console.error(
4253
+ "falta la ruta del fichero cc.json\n\n codarch render <fichero.cc.json[.gz]> [-o mapa.html]"
4254
+ );
4255
+ return 1;
4256
+ }
4257
+ let loaded;
4258
+ try {
4259
+ loaded = readJsonFile2(options.file);
4260
+ } catch (error) {
4261
+ console.error(`no se pudo leer ${options.file}: ${error.message}`);
4262
+ return 1;
4263
+ }
4264
+ let cc;
4265
+ try {
4266
+ cc = parseCcJson(loaded.json);
4267
+ } catch (error) {
4268
+ if (error instanceof CcJsonError) {
4269
+ console.error(`cc.json inv\xE1lido: ${error.message}`);
4270
+ return 1;
4271
+ }
4272
+ throw error;
4273
+ }
4274
+ const started = Date.now();
4275
+ let model;
4276
+ try {
4277
+ model = buildRenderModel(cc, {
4278
+ areaMetric: options.area,
4279
+ color: options.color,
4280
+ title: options.title ?? (cc.meta.projectName ? void 0 : defaultProjectName(options.file))
4281
+ });
4282
+ } catch (error) {
4283
+ if (error instanceof RenderError) {
4284
+ console.error(`no se pudo construir el mapa: ${error.message}`);
4285
+ return 1;
4286
+ }
4287
+ throw error;
4288
+ }
4289
+ let html;
4290
+ try {
4291
+ html = renderHtml(model);
4292
+ } catch (error) {
4293
+ console.error(`no se pudo generar el HTML: ${error.message}`);
4294
+ return 1;
4295
+ }
4296
+ const elapsed = Date.now() - started;
4297
+ const out = options.out ?? defaultOutputPath(options.file);
4298
+ try {
4299
+ fs9.mkdirSync(path7.dirname(path7.resolve(out)), { recursive: true });
4300
+ fs9.writeFileSync(out, html, "utf8");
4301
+ } catch (error) {
4302
+ console.error(`no se pudo escribir ${out}: ${error.message}`);
4303
+ return 1;
4304
+ }
4305
+ const bytes = Buffer.byteLength(html, "utf8");
4306
+ const lens = model.initialLens === "structure" ? "estructura" : model.initialLens === "risk" ? "riesgo" : model.initialMetric ?? "m\xE9trica";
4307
+ const lines = [];
4308
+ lines.push("");
4309
+ lines.push(" CodArch \xB7 mapa");
4310
+ lines.push("");
4311
+ lines.push(field4("fichero", `${options.file} (${formatBytes(loaded.bytes)})`));
4312
+ lines.push(field4("salida", `${out} (${formatBytes(bytes)})`));
4313
+ lines.push(
4314
+ field4(
4315
+ "estructura",
4316
+ `${num4(model.fileCount)} ficheros \xB7 ${num4(model.folderCount)} carpetas \xB7 ${num4(
4317
+ model.metrics.length
4318
+ )} m\xE9tricas`
4319
+ )
4320
+ );
4321
+ lines.push(
4322
+ field4("\xE1rea", `${model.areaMetric ?? "(sin m\xE9trica)"} \xB7 ${num4(model.totalArea)} en total`)
4323
+ );
4324
+ lines.push(field4("color", `${lens} \xB7 ${num4(model.hotspotCount)} hotspots`));
4325
+ lines.push(
4326
+ field4(
4327
+ "dependencias",
4328
+ `${num4(model.edges.length)} aristas \xB7 ${num4(model.cycles.length)} c\xEDclicas`
4329
+ )
4330
+ );
4331
+ lines.push(field4("tiempo", `${num4(elapsed)} ms`));
4332
+ for (const warning of model.warnings) {
4333
+ lines.push(field4("aviso", warning));
4334
+ }
4335
+ lines.push("");
4336
+ console.log(lines.join("\n"));
4337
+ return 0;
4338
+ }
4339
+
4340
+ // apps/cli/src/index.ts
4341
+ var VERSION = true ? "0.1.0" : "0.0.0";
4342
+ var USAGE = `
4343
+ CodArch \xB7 mapa vivo de la arquitectura de tu software
4344
+
4345
+ Uso:
4346
+ codarch analyze [ruta] [opciones]
4347
+ codarch inspect <fichero.cc.json[.gz]> [opciones]
4348
+ codarch render <fichero.cc.json[.gz]> [opciones]
4349
+ codarch diff <base.cc.json[.gz]> <head.cc.json[.gz]> [opciones]
4350
+
4351
+ Opciones de analyze:
4352
+ -o, --out <dir> directorio de salida (por defecto <ruta>/.codarch)
4353
+ --ignore <patr\xF3n> excluir rutas (repetible)
4354
+ --project <nombre> nombre del proyecto en el snapshot
4355
+ --title <texto> t\xEDtulo del mapa
4356
+ --no-git no leer churn del historial git
4357
+ -h, --help esta ayuda
4358
+ -v, --version versi\xF3n
4359
+
4360
+ Opciones de inspect:
4361
+ --json salida machine-readable
4362
+ --top <n> n\xFAmero de ficheros en el ranking (por defecto 5)
4363
+ --metric <nombre> m\xE9trica del ranking (por defecto rloc)
4364
+ -h, --help esta ayuda
4365
+ -v, --version versi\xF3n
4366
+
4367
+ Opciones de render:
4368
+ -o, --out <html> fichero de salida (por defecto, junto al cc.json)
4369
+ --area <m\xE9trica> m\xE9trica que decide el \xE1rea (por defecto rloc)
4370
+ --color <modo> risk | structure | <m\xE9trica> (por defecto risk)
4371
+ --title <texto> t\xEDtulo del proyecto en el mapa
4372
+ -h, --help esta ayuda
4373
+
4374
+ Opciones de diff:
4375
+ -o, --out <file> reporte JSON (por defecto report.json)
4376
+ --top <n> hotspots en el reporte (por defecto 10)
4377
+ --hotspot-metric <m\xE9trica> por defecto maxComplexity, luego rloc
4378
+ --config <file> reglas YAML (por defecto .codarch.yml si existe)
4379
+ --no-config no cargar reglas
4380
+ --fail-on <modo> none | cycles | violations | hotspots | any (por defecto none)
4381
+ -h, --help esta ayuda
4382
+
4383
+ Ejemplos:
4384
+ codarch analyze .
4385
+ codarch analyze . -o .codarch --title "Mi proyecto"
4386
+ codarch inspect samples/codecharta_visualization.cc.json.gz
4387
+ codarch render samples/codecharta_visualization.cc.json.gz
4388
+ codarch render mapa.cc.json -o mapa.html --area rloc --color maxComplexity
4389
+ codarch diff base.cc.json head.cc.json -o report.json --fail-on cycles
4390
+ `;
4391
+ function parseAnalyze(args) {
4392
+ const options = { help: false, ignore: [] };
4393
+ const positional = [];
4394
+ for (let index = 0; index < args.length; index++) {
4395
+ const arg = args[index];
4396
+ if (arg === "--out" || arg === "-o") {
4397
+ const value = args[++index];
4398
+ if (!value) {
4399
+ console.error("--out necesita una ruta");
4400
+ return 1;
4401
+ }
4402
+ options.out = value;
4403
+ } else if (arg.startsWith("--out=")) {
4404
+ options.out = arg.slice("--out=".length);
4405
+ } else if (arg === "--ignore") {
4406
+ const value = args[++index];
4407
+ if (!value) {
4408
+ console.error("--ignore necesita un patr\xF3n");
4409
+ return 1;
4410
+ }
4411
+ options.ignore?.push(value);
4412
+ } else if (arg.startsWith("--ignore=")) {
4413
+ options.ignore?.push(arg.slice("--ignore=".length));
4414
+ } else if (arg === "--project") {
4415
+ const value = args[++index];
4416
+ if (!value) {
4417
+ console.error("--project necesita un nombre");
4418
+ return 1;
4419
+ }
4420
+ options.projectName = value;
4421
+ } else if (arg.startsWith("--project=")) {
4422
+ options.projectName = arg.slice("--project=".length);
4423
+ } else if (arg === "--title") {
4424
+ const value = args[++index];
4425
+ if (!value) {
4426
+ console.error("--title necesita un texto");
4427
+ return 1;
4428
+ }
4429
+ options.title = value;
4430
+ } else if (arg.startsWith("--title=")) {
4431
+ options.title = arg.slice("--title=".length);
4432
+ } else if (arg === "--no-git") {
4433
+ options.git = false;
4434
+ } else if (arg === "--help" || arg === "-h") {
4435
+ console.log(USAGE);
4436
+ return 0;
4437
+ } else if (arg === "--version" || arg === "-v") {
4438
+ console.log(VERSION);
4439
+ return 0;
4440
+ } else if (arg.startsWith("-")) {
4441
+ console.error(`opci\xF3n desconocida: ${arg}`);
4442
+ return 1;
4443
+ } else {
4444
+ positional.push(arg);
4445
+ }
4446
+ }
4447
+ if (positional.length > 1) {
4448
+ console.error(`sobra un argumento: ${positional[1]}`);
4449
+ return 1;
4450
+ }
4451
+ options.root = positional[0] ?? ".";
4452
+ return options;
4453
+ }
4454
+ function parseInspect(args) {
4455
+ const options = { json: false, top: 5, help: false };
4456
+ const positional = [];
4457
+ for (let index = 0; index < args.length; index++) {
4458
+ const arg = args[index];
4459
+ if (arg === "--json") {
4460
+ options.json = true;
4461
+ } else if (arg === "--top") {
4462
+ const value = args[++index];
4463
+ const parsed = Number(value);
4464
+ if (!value || !Number.isInteger(parsed) || parsed < 1) {
4465
+ console.error("--top necesita un entero positivo");
4466
+ return 1;
4467
+ }
4468
+ options.top = parsed;
4469
+ } else if (arg.startsWith("--top=")) {
4470
+ const parsed = Number(arg.slice("--top=".length));
4471
+ if (!Number.isInteger(parsed) || parsed < 1) {
4472
+ console.error("--top necesita un entero positivo");
4473
+ return 1;
4474
+ }
4475
+ options.top = parsed;
4476
+ } else if (arg === "--metric") {
4477
+ const value = args[++index];
4478
+ if (!value) {
4479
+ console.error("--metric necesita un nombre");
4480
+ return 1;
4481
+ }
4482
+ options.metric = value;
4483
+ } else if (arg.startsWith("--metric=")) {
4484
+ options.metric = arg.slice("--metric=".length);
4485
+ } else if (arg === "--help" || arg === "-h") {
4486
+ console.log(USAGE);
4487
+ return 0;
4488
+ } else if (arg === "--version" || arg === "-v") {
4489
+ console.log(VERSION);
4490
+ return 0;
4491
+ } else if (arg.startsWith("-")) {
4492
+ console.error(`opci\xF3n desconocida: ${arg}`);
4493
+ return 1;
4494
+ } else {
4495
+ positional.push(arg);
4496
+ }
4497
+ }
4498
+ options.file = positional[0];
4499
+ return options;
4500
+ }
4501
+ function parseRender(args) {
4502
+ const options = { help: false };
4503
+ const positional = [];
4504
+ for (let index = 0; index < args.length; index++) {
4505
+ const arg = args[index];
4506
+ if (arg === "--out" || arg === "-o") {
4507
+ const value = args[++index];
4508
+ if (!value) {
4509
+ console.error("--out necesita una ruta");
4510
+ return 1;
4511
+ }
4512
+ options.out = value;
4513
+ } else if (arg.startsWith("--out=")) {
4514
+ options.out = arg.slice("--out=".length);
4515
+ } else if (arg === "--area") {
4516
+ const value = args[++index];
4517
+ if (!value) {
4518
+ console.error("--area necesita un nombre de m\xE9trica");
4519
+ return 1;
4520
+ }
4521
+ options.area = value;
4522
+ } else if (arg.startsWith("--area=")) {
4523
+ options.area = arg.slice("--area=".length);
4524
+ } else if (arg === "--color") {
4525
+ const value = args[++index];
4526
+ if (!value) {
4527
+ console.error("--color necesita risk, structure o una m\xE9trica");
4528
+ return 1;
4529
+ }
4530
+ options.color = value;
4531
+ } else if (arg.startsWith("--color=")) {
4532
+ options.color = arg.slice("--color=".length);
4533
+ } else if (arg === "--title") {
4534
+ const value = args[++index];
4535
+ if (!value) {
4536
+ console.error("--title necesita un texto");
4537
+ return 1;
4538
+ }
4539
+ options.title = value;
4540
+ } else if (arg.startsWith("--title=")) {
4541
+ options.title = arg.slice("--title=".length);
4542
+ } else if (arg === "--help" || arg === "-h") {
4543
+ console.log(USAGE);
4544
+ return 0;
4545
+ } else if (arg === "--version" || arg === "-v") {
4546
+ console.log(VERSION);
4547
+ return 0;
4548
+ } else if (arg.startsWith("-")) {
4549
+ console.error(`opci\xF3n desconocida: ${arg}`);
4550
+ return 1;
4551
+ } else {
4552
+ positional.push(arg);
4553
+ }
4554
+ }
4555
+ if (positional.length > 1) {
4556
+ console.error(`sobra un argumento: ${positional[1]}`);
4557
+ return 1;
4558
+ }
4559
+ options.file = positional[0];
4560
+ return options;
4561
+ }
4562
+ function parseDiff(args) {
4563
+ const options = { help: false, top: 10, failOn: "none" };
4564
+ const positional = [];
4565
+ for (let index = 0; index < args.length; index++) {
4566
+ const arg = args[index];
4567
+ if (arg === "--out" || arg === "-o") {
4568
+ const value = args[++index];
4569
+ if (!value) {
4570
+ console.error("--out necesita una ruta");
4571
+ return 1;
4572
+ }
4573
+ options.out = value;
4574
+ } else if (arg.startsWith("--out=")) {
4575
+ options.out = arg.slice("--out=".length);
4576
+ } else if (arg === "--top") {
4577
+ const value = args[++index];
4578
+ const parsed = Number(value);
4579
+ if (!value || !Number.isInteger(parsed) || parsed < 1) {
4580
+ console.error("--top necesita un entero positivo");
4581
+ return 1;
4582
+ }
4583
+ options.top = parsed;
4584
+ } else if (arg.startsWith("--top=")) {
4585
+ const parsed = Number(arg.slice("--top=".length));
4586
+ if (!Number.isInteger(parsed) || parsed < 1) {
4587
+ console.error("--top necesita un entero positivo");
4588
+ return 1;
4589
+ }
4590
+ options.top = parsed;
4591
+ } else if (arg === "--hotspot-metric") {
4592
+ const value = args[++index];
4593
+ if (!value) {
4594
+ console.error("--hotspot-metric necesita un nombre");
4595
+ return 1;
4596
+ }
4597
+ options.hotspotMetric = value;
4598
+ } else if (arg.startsWith("--hotspot-metric=")) {
4599
+ options.hotspotMetric = arg.slice("--hotspot-metric=".length);
4600
+ } else if (arg === "--config") {
4601
+ const value = args[++index];
4602
+ if (!value) {
4603
+ console.error("--config necesita una ruta");
4604
+ return 1;
4605
+ }
4606
+ options.config = value;
4607
+ } else if (arg.startsWith("--config=")) {
4608
+ options.config = arg.slice("--config=".length);
4609
+ } else if (arg === "--no-config") {
4610
+ options.noConfig = true;
4611
+ } else if (arg === "--fail-on") {
4612
+ const value = args[++index];
4613
+ if (!isFailOn(value)) {
4614
+ console.error(
4615
+ "--fail-on necesita none, cycles, violations, hotspots o any"
4616
+ );
4617
+ return 1;
4618
+ }
4619
+ options.failOn = value;
4620
+ } else if (arg.startsWith("--fail-on=")) {
4621
+ const value = arg.slice("--fail-on=".length);
4622
+ if (!isFailOn(value)) {
4623
+ console.error(
4624
+ "--fail-on necesita none, cycles, violations, hotspots o any"
4625
+ );
4626
+ return 1;
4627
+ }
4628
+ options.failOn = value;
4629
+ } else if (arg === "--help" || arg === "-h") {
4630
+ console.log(USAGE);
4631
+ return 0;
4632
+ } else if (arg === "--version" || arg === "-v") {
4633
+ console.log(VERSION);
4634
+ return 0;
4635
+ } else if (arg.startsWith("-")) {
4636
+ console.error(`opci\xF3n desconocida: ${arg}`);
4637
+ return 1;
4638
+ } else {
4639
+ positional.push(arg);
4640
+ }
4641
+ }
4642
+ if (positional.length !== 2) {
4643
+ console.error("diff necesita exactamente dos snapshots: base y head");
4644
+ return 1;
4645
+ }
4646
+ options.base = positional[0];
4647
+ options.head = positional[1];
4648
+ return options;
4649
+ }
4650
+ function isFailOn(value) {
4651
+ return value === "none" || value === "cycles" || value === "violations" || value === "hotspots" || value === "any";
4652
+ }
4653
+ function main(argv) {
4654
+ const [command, ...rest] = argv;
4655
+ if (!command || command === "--help" || command === "-h") {
4656
+ console.log(USAGE);
4657
+ return 0;
4658
+ }
4659
+ if (command === "--version" || command === "-v") {
4660
+ console.log(VERSION);
4661
+ return 0;
4662
+ }
4663
+ if (command === "analyze") {
4664
+ const parsed = parseAnalyze(rest);
4665
+ if (typeof parsed === "number") return parsed;
4666
+ return runAnalyze(parsed);
4667
+ }
4668
+ if (command === "inspect") {
4669
+ const parsed = parseInspect(rest);
4670
+ if (typeof parsed === "number") return parsed;
4671
+ return runInspect(parsed);
4672
+ }
4673
+ if (command === "render") {
4674
+ const parsed = parseRender(rest);
4675
+ if (typeof parsed === "number") return parsed;
4676
+ return runRender(parsed);
4677
+ }
4678
+ if (command === "diff") {
4679
+ const parsed = parseDiff(rest);
4680
+ if (typeof parsed === "number") return parsed;
4681
+ return runDiff(parsed);
4682
+ }
4683
+ console.error(`comando desconocido: ${command}
4684
+ ${USAGE}`);
4685
+ return 1;
4686
+ }
4687
+ process.exitCode = main(process.argv.slice(2));