codarch 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +943 -28
- package/package.json +2 -2
- package/web/assets/index-BRgqIG_S.js +9 -0
- package/web/assets/{index-Bnq20GR3.css → index-C_1au_Hz.css} +1 -1
- package/web/assets/{worker-BWIy7NG8.js → worker-tF5VFTCV.js} +1 -1
- package/web/favicon.png +0 -0
- package/web/icon.png +0 -0
- package/web/index.html +4 -2
- package/web/assets/index-BLE7eQgu.js +0 -9
package/index.js
CHANGED
|
@@ -2095,8 +2095,8 @@ var MIGRATION_EXTENSIONS = Object.keys(EXTENSION_LANGUAGE);
|
|
|
2095
2095
|
var LANGUAGE_ORDER = ["ts", "js", "rust", "wasm", "node"];
|
|
2096
2096
|
var RUST_BRIDGE = /#\[\s*(?:napi|wasm_bindgen)(?:\(|\])|\bnapi_derive\b|\bnapi::|\bwasm_bindgen(?:::|\b)|\bwasm-pack\b/;
|
|
2097
2097
|
var JS_FFI = /\bWebAssembly\s*\.\s*(?:instantiate|compile|Module|Instance)\b|\bprocess\.dlopen\b/;
|
|
2098
|
-
function languageForPath(
|
|
2099
|
-
const lower =
|
|
2098
|
+
function languageForPath(relativePath3) {
|
|
2099
|
+
const lower = relativePath3.toLowerCase();
|
|
2100
2100
|
const dot = lower.lastIndexOf(".");
|
|
2101
2101
|
if (dot === -1) return null;
|
|
2102
2102
|
return EXTENSION_LANGUAGE[lower.slice(dot)] ?? null;
|
|
@@ -2519,8 +2519,8 @@ function buildSnapshot(input) {
|
|
|
2519
2519
|
}
|
|
2520
2520
|
function migrationLensFrom(migration) {
|
|
2521
2521
|
const nodes = {};
|
|
2522
|
-
for (const [
|
|
2523
|
-
nodes[fileId(
|
|
2522
|
+
for (const [relativePath3, node] of migration.nodes) {
|
|
2523
|
+
nodes[fileId(relativePath3)] = node;
|
|
2524
2524
|
}
|
|
2525
2525
|
const edges = migration.edges.map((edge) => ({
|
|
2526
2526
|
fromId: fileId(edge.from),
|
|
@@ -2616,17 +2616,17 @@ function walkSourceFiles(fileSystem, options = {}) {
|
|
|
2616
2616
|
(left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0
|
|
2617
2617
|
);
|
|
2618
2618
|
for (const entry of entries) {
|
|
2619
|
-
const
|
|
2619
|
+
const relativePath3 = directory.relativePath ? `${directory.relativePath}/${entry.name}` : entry.name;
|
|
2620
2620
|
const absolutePath = directory.absolutePath === "/" ? `/${entry.name}` : `${directory.absolutePath}/${entry.name}`;
|
|
2621
2621
|
if (entry.isDirectory) {
|
|
2622
|
-
if (isIgnored(directory.layers,
|
|
2623
|
-
const nested = readGitignore(fileSystem, absolutePath,
|
|
2624
|
-
const layers = nested ? [...directory.layers, { base:
|
|
2625
|
-
stack.push({ absolutePath, relativePath:
|
|
2622
|
+
if (isIgnored(directory.layers, relativePath3, true)) continue;
|
|
2623
|
+
const nested = readGitignore(fileSystem, absolutePath, relativePath3, warnings);
|
|
2624
|
+
const layers = nested ? [...directory.layers, { base: relativePath3, matcher: ignore().add(nested) }] : directory.layers;
|
|
2625
|
+
stack.push({ absolutePath, relativePath: relativePath3, layers });
|
|
2626
2626
|
} else if (entry.isFile) {
|
|
2627
|
-
if (isIgnored(directory.layers,
|
|
2627
|
+
if (isIgnored(directory.layers, relativePath3, false)) continue;
|
|
2628
2628
|
if (!matchesExtension(entry.name, extensions)) continue;
|
|
2629
|
-
files.push({ absolutePath, relativePath:
|
|
2629
|
+
files.push({ absolutePath, relativePath: relativePath3 });
|
|
2630
2630
|
}
|
|
2631
2631
|
}
|
|
2632
2632
|
}
|
|
@@ -2647,10 +2647,10 @@ function readGitignore(fileSystem, absoluteDirectory, relativeBase, warnings) {
|
|
|
2647
2647
|
return null;
|
|
2648
2648
|
}
|
|
2649
2649
|
}
|
|
2650
|
-
function isIgnored(layers,
|
|
2650
|
+
function isIgnored(layers, relativePath3, isDirectory) {
|
|
2651
2651
|
let ignored = false;
|
|
2652
2652
|
for (const layer of layers) {
|
|
2653
|
-
const localPath = layer.base ?
|
|
2653
|
+
const localPath = layer.base ? relativePath3.slice(layer.base.length + 1) : relativePath3;
|
|
2654
2654
|
if (!localPath) continue;
|
|
2655
2655
|
const probe = isDirectory ? `${localPath}/` : localPath;
|
|
2656
2656
|
const result = layer.matcher.test(probe);
|
|
@@ -2679,14 +2679,21 @@ function analyzeFiles(options) {
|
|
|
2679
2679
|
warnings.push(...resolver.warnings);
|
|
2680
2680
|
const parser = options.parser;
|
|
2681
2681
|
const parsedFiles = [];
|
|
2682
|
-
const
|
|
2682
|
+
const migrationFiles2 = [];
|
|
2683
2683
|
const migrationLinks = [];
|
|
2684
2684
|
const links = [];
|
|
2685
2685
|
const unresolved = [];
|
|
2686
2686
|
let externalImports = 0;
|
|
2687
2687
|
let builtinImports = 0;
|
|
2688
2688
|
let typeOnlyImports = 0;
|
|
2689
|
+
let processed = 0;
|
|
2689
2690
|
for (const file of walked.files) {
|
|
2691
|
+
processed += 1;
|
|
2692
|
+
options.onProgress?.({
|
|
2693
|
+
done: processed,
|
|
2694
|
+
total: walked.files.length,
|
|
2695
|
+
current: file.relativePath
|
|
2696
|
+
});
|
|
2690
2697
|
const language = languageForPath(file.relativePath);
|
|
2691
2698
|
if (!language) continue;
|
|
2692
2699
|
if (isNativeLanguage(language)) {
|
|
@@ -2694,7 +2701,7 @@ function analyzeFiles(options) {
|
|
|
2694
2701
|
relativePath: file.relativePath,
|
|
2695
2702
|
metrics: { loc: 0, rloc: 0 }
|
|
2696
2703
|
});
|
|
2697
|
-
|
|
2704
|
+
migrationFiles2.push({
|
|
2698
2705
|
relativePath: file.relativePath,
|
|
2699
2706
|
language,
|
|
2700
2707
|
loc: 0,
|
|
@@ -2720,7 +2727,7 @@ function analyzeFiles(options) {
|
|
|
2720
2727
|
relativePath: file.relativePath,
|
|
2721
2728
|
metrics: { loc: measured.loc, rloc: measured.rloc }
|
|
2722
2729
|
});
|
|
2723
|
-
|
|
2730
|
+
migrationFiles2.push({
|
|
2724
2731
|
relativePath: file.relativePath,
|
|
2725
2732
|
language,
|
|
2726
2733
|
loc: measured.loc,
|
|
@@ -2790,7 +2797,7 @@ function analyzeFiles(options) {
|
|
|
2790
2797
|
unresolvedImports: fileUnresolvedImports
|
|
2791
2798
|
}
|
|
2792
2799
|
});
|
|
2793
|
-
|
|
2800
|
+
migrationFiles2.push({
|
|
2794
2801
|
relativePath: file.relativePath,
|
|
2795
2802
|
language,
|
|
2796
2803
|
loc: parsed.metrics.loc ?? 0,
|
|
@@ -2828,10 +2835,10 @@ function analyzeFiles(options) {
|
|
|
2828
2835
|
file.metrics.dependencyLevel = metrics.dependencyLevel;
|
|
2829
2836
|
}
|
|
2830
2837
|
}
|
|
2831
|
-
for (const file of
|
|
2838
|
+
for (const file of migrationFiles2) {
|
|
2832
2839
|
file.fanIn = graph.metrics.get(file.relativePath)?.fanIn ?? 0;
|
|
2833
2840
|
}
|
|
2834
|
-
const migration = buildMigration(
|
|
2841
|
+
const migration = buildMigration(migrationFiles2, migrationLinks);
|
|
2835
2842
|
for (const file of parsedFiles) {
|
|
2836
2843
|
const node = migration.nodes.get(file.relativePath);
|
|
2837
2844
|
if (!node) continue;
|
|
@@ -3180,6 +3187,318 @@ function clamp01(value) {
|
|
|
3180
3187
|
return Math.max(0, Math.min(1, value));
|
|
3181
3188
|
}
|
|
3182
3189
|
|
|
3190
|
+
// packages/metrics/src/profiles.ts
|
|
3191
|
+
var DOMAIN_PROFILES = [
|
|
3192
|
+
{
|
|
3193
|
+
id: "capas",
|
|
3194
|
+
title: "Capas por convenci\xF3n",
|
|
3195
|
+
description: "Tres capas: el dominio no depende de nada, la aplicaci\xF3n no depende de la infraestructura y la infraestructura no depende de la aplicaci\xF3n.",
|
|
3196
|
+
when: "Para repos con src/domain, src/application y src/infra.",
|
|
3197
|
+
layers: [
|
|
3198
|
+
{
|
|
3199
|
+
name: "domain",
|
|
3200
|
+
patterns: [
|
|
3201
|
+
"src/domain/**",
|
|
3202
|
+
"src/core/**",
|
|
3203
|
+
"src/dominio/**",
|
|
3204
|
+
"src/nucleo/**",
|
|
3205
|
+
"domain/**"
|
|
3206
|
+
]
|
|
3207
|
+
},
|
|
3208
|
+
{
|
|
3209
|
+
name: "application",
|
|
3210
|
+
patterns: [
|
|
3211
|
+
"src/application/**",
|
|
3212
|
+
"src/app/**",
|
|
3213
|
+
"src/use-cases/**",
|
|
3214
|
+
"src/usecases/**",
|
|
3215
|
+
"src/aplicacion/**",
|
|
3216
|
+
"src/casos-de-uso/**",
|
|
3217
|
+
"application/**"
|
|
3218
|
+
]
|
|
3219
|
+
},
|
|
3220
|
+
{
|
|
3221
|
+
name: "infrastructure",
|
|
3222
|
+
patterns: [
|
|
3223
|
+
"src/infrastructure/**",
|
|
3224
|
+
"src/infra/**",
|
|
3225
|
+
"src/adapters/**",
|
|
3226
|
+
"src/persistence/**",
|
|
3227
|
+
"src/infraestructura/**",
|
|
3228
|
+
"src/adaptadores/**",
|
|
3229
|
+
"infrastructure/**"
|
|
3230
|
+
]
|
|
3231
|
+
}
|
|
3232
|
+
],
|
|
3233
|
+
rules: [
|
|
3234
|
+
{
|
|
3235
|
+
type: "forbidden",
|
|
3236
|
+
name: "dominio-sin-capas-exteriores",
|
|
3237
|
+
from: "domain",
|
|
3238
|
+
to: "application",
|
|
3239
|
+
severity: "warn",
|
|
3240
|
+
message: "El dominio no debe depender de la aplicaci\xF3n"
|
|
3241
|
+
},
|
|
3242
|
+
{
|
|
3243
|
+
type: "forbidden",
|
|
3244
|
+
name: "dominio-sin-infraestructura",
|
|
3245
|
+
from: "domain",
|
|
3246
|
+
to: "infrastructure",
|
|
3247
|
+
severity: "warn",
|
|
3248
|
+
message: "El dominio no debe depender de la infraestructura"
|
|
3249
|
+
},
|
|
3250
|
+
{
|
|
3251
|
+
type: "forbidden",
|
|
3252
|
+
name: "aplicacion-sin-infraestructura",
|
|
3253
|
+
from: "application",
|
|
3254
|
+
to: "infrastructure",
|
|
3255
|
+
severity: "warn",
|
|
3256
|
+
message: "La aplicaci\xF3n no debe depender de la infraestructura"
|
|
3257
|
+
}
|
|
3258
|
+
]
|
|
3259
|
+
},
|
|
3260
|
+
{
|
|
3261
|
+
id: "hexagonal",
|
|
3262
|
+
title: "Ports & adapters",
|
|
3263
|
+
description: "El dominio en el centro, la aplicaci\xF3n con los puertos y los adaptadores por fuera: la aplicaci\xF3n y el dominio no dependen de los adaptadores.",
|
|
3264
|
+
when: "Para repos con puertos (interfaces) en el centro y adaptadores fuera.",
|
|
3265
|
+
layers: [
|
|
3266
|
+
{
|
|
3267
|
+
name: "domain",
|
|
3268
|
+
patterns: [
|
|
3269
|
+
"src/domain/**",
|
|
3270
|
+
"src/core/**",
|
|
3271
|
+
"src/model/**",
|
|
3272
|
+
"src/dominio/**",
|
|
3273
|
+
"src/modelo/**",
|
|
3274
|
+
"domain/**"
|
|
3275
|
+
]
|
|
3276
|
+
},
|
|
3277
|
+
{
|
|
3278
|
+
name: "application",
|
|
3279
|
+
patterns: [
|
|
3280
|
+
"src/application/**",
|
|
3281
|
+
"src/app/**",
|
|
3282
|
+
"src/use-cases/**",
|
|
3283
|
+
"src/usecases/**",
|
|
3284
|
+
"src/ports/**",
|
|
3285
|
+
"src/puertos/**",
|
|
3286
|
+
"application/**"
|
|
3287
|
+
]
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
name: "adapters",
|
|
3291
|
+
patterns: [
|
|
3292
|
+
"src/adapters/**",
|
|
3293
|
+
"src/infra/**",
|
|
3294
|
+
"src/infrastructure/**",
|
|
3295
|
+
"src/persistence/**",
|
|
3296
|
+
"src/adaptadores/**",
|
|
3297
|
+
"src/infraestructura/**",
|
|
3298
|
+
"adapters/**"
|
|
3299
|
+
]
|
|
3300
|
+
}
|
|
3301
|
+
],
|
|
3302
|
+
rules: [
|
|
3303
|
+
{
|
|
3304
|
+
type: "forbidden",
|
|
3305
|
+
name: "dominio-sin-adaptadores",
|
|
3306
|
+
from: "domain",
|
|
3307
|
+
to: "adapters",
|
|
3308
|
+
severity: "warn",
|
|
3309
|
+
message: "El dominio no debe depender de los adaptadores"
|
|
3310
|
+
},
|
|
3311
|
+
{
|
|
3312
|
+
type: "forbidden",
|
|
3313
|
+
name: "aplicacion-sin-adaptadores",
|
|
3314
|
+
from: "application",
|
|
3315
|
+
to: "adapters",
|
|
3316
|
+
severity: "warn",
|
|
3317
|
+
message: "La aplicaci\xF3n habla con los adaptadores por puertos, no directamente"
|
|
3318
|
+
}
|
|
3319
|
+
]
|
|
3320
|
+
},
|
|
3321
|
+
{
|
|
3322
|
+
id: "onion",
|
|
3323
|
+
title: "Clean Architecture (cebolla)",
|
|
3324
|
+
description: "Cuatro anillos: el dominio en el centro, luego la aplicaci\xF3n, la infraestructura y la presentaci\xF3n fuera. Las dependencias van hacia dentro y el anillo exterior no se importa desde dentro.",
|
|
3325
|
+
when: "Para repos con una capa de presentaci\xF3n o de interfaz propia.",
|
|
3326
|
+
layers: [
|
|
3327
|
+
{
|
|
3328
|
+
name: "domain",
|
|
3329
|
+
patterns: [
|
|
3330
|
+
"src/domain/**",
|
|
3331
|
+
"src/entities/**",
|
|
3332
|
+
"src/value-objects/**",
|
|
3333
|
+
"src/dominio/**",
|
|
3334
|
+
"src/entidades/**",
|
|
3335
|
+
"domain/**"
|
|
3336
|
+
]
|
|
3337
|
+
},
|
|
3338
|
+
{
|
|
3339
|
+
name: "application",
|
|
3340
|
+
patterns: [
|
|
3341
|
+
"src/application/**",
|
|
3342
|
+
"src/use-cases/**",
|
|
3343
|
+
"src/usecases/**",
|
|
3344
|
+
"src/services/**",
|
|
3345
|
+
"src/aplicacion/**",
|
|
3346
|
+
"src/casos-de-uso/**",
|
|
3347
|
+
"application/**"
|
|
3348
|
+
]
|
|
3349
|
+
},
|
|
3350
|
+
{
|
|
3351
|
+
name: "infrastructure",
|
|
3352
|
+
patterns: [
|
|
3353
|
+
"src/infrastructure/**",
|
|
3354
|
+
"src/infra/**",
|
|
3355
|
+
"src/persistence/**",
|
|
3356
|
+
"src/repositories/**",
|
|
3357
|
+
"src/infraestructura/**",
|
|
3358
|
+
"infrastructure/**"
|
|
3359
|
+
]
|
|
3360
|
+
},
|
|
3361
|
+
{
|
|
3362
|
+
name: "presentation",
|
|
3363
|
+
patterns: [
|
|
3364
|
+
"src/presentation/**",
|
|
3365
|
+
"src/ui/**",
|
|
3366
|
+
"src/controllers/**",
|
|
3367
|
+
"src/pages/**",
|
|
3368
|
+
"src/components/**",
|
|
3369
|
+
"src/presentacion/**",
|
|
3370
|
+
"src/interfaz/**",
|
|
3371
|
+
"presentation/**"
|
|
3372
|
+
]
|
|
3373
|
+
}
|
|
3374
|
+
],
|
|
3375
|
+
rules: [
|
|
3376
|
+
{
|
|
3377
|
+
type: "forbidden",
|
|
3378
|
+
name: "dominio-sin-capas-exteriores",
|
|
3379
|
+
from: "domain",
|
|
3380
|
+
to: "application",
|
|
3381
|
+
severity: "warn",
|
|
3382
|
+
message: "El dominio no debe depender de la aplicaci\xF3n"
|
|
3383
|
+
},
|
|
3384
|
+
{
|
|
3385
|
+
type: "forbidden",
|
|
3386
|
+
name: "dominio-sin-infraestructura",
|
|
3387
|
+
from: "domain",
|
|
3388
|
+
to: "infrastructure",
|
|
3389
|
+
severity: "warn",
|
|
3390
|
+
message: "El dominio no debe depender de la infraestructura"
|
|
3391
|
+
},
|
|
3392
|
+
{
|
|
3393
|
+
type: "forbidden",
|
|
3394
|
+
name: "dominio-sin-presentacion",
|
|
3395
|
+
from: "domain",
|
|
3396
|
+
to: "presentation",
|
|
3397
|
+
severity: "warn",
|
|
3398
|
+
message: "El dominio no debe depender de la presentaci\xF3n"
|
|
3399
|
+
},
|
|
3400
|
+
{
|
|
3401
|
+
type: "forbidden",
|
|
3402
|
+
name: "aplicacion-sin-infraestructura",
|
|
3403
|
+
from: "application",
|
|
3404
|
+
to: "infrastructure",
|
|
3405
|
+
severity: "warn",
|
|
3406
|
+
message: "La aplicaci\xF3n no debe depender de la infraestructura"
|
|
3407
|
+
},
|
|
3408
|
+
{
|
|
3409
|
+
type: "forbidden",
|
|
3410
|
+
name: "aplicacion-sin-presentacion",
|
|
3411
|
+
from: "application",
|
|
3412
|
+
to: "presentation",
|
|
3413
|
+
severity: "warn",
|
|
3414
|
+
message: "La aplicaci\xF3n no debe depender de la presentaci\xF3n"
|
|
3415
|
+
},
|
|
3416
|
+
{
|
|
3417
|
+
type: "forbidden",
|
|
3418
|
+
name: "infraestructura-sin-presentacion",
|
|
3419
|
+
from: "infrastructure",
|
|
3420
|
+
to: "presentation",
|
|
3421
|
+
severity: "warn",
|
|
3422
|
+
message: "La infraestructura y la presentaci\xF3n se hablan por los puertos, no entre ellas"
|
|
3423
|
+
}
|
|
3424
|
+
]
|
|
3425
|
+
},
|
|
3426
|
+
{
|
|
3427
|
+
id: "ddd",
|
|
3428
|
+
title: "DDD por carpetas",
|
|
3429
|
+
description: "Las carpetas del dominio driven design: el modelo dentro, los servicios de aplicaci\xF3n y los repositorios en la infraestructura.",
|
|
3430
|
+
when: "Para repos con modelo, servicios y repositorios como carpetas.",
|
|
3431
|
+
layers: [
|
|
3432
|
+
{
|
|
3433
|
+
name: "domain",
|
|
3434
|
+
patterns: [
|
|
3435
|
+
"src/domain/**",
|
|
3436
|
+
"src/model/**",
|
|
3437
|
+
"src/entities/**",
|
|
3438
|
+
"src/value-objects/**",
|
|
3439
|
+
"src/dominio/**",
|
|
3440
|
+
"src/modelo/**",
|
|
3441
|
+
"domain/**"
|
|
3442
|
+
]
|
|
3443
|
+
},
|
|
3444
|
+
{
|
|
3445
|
+
name: "application",
|
|
3446
|
+
patterns: [
|
|
3447
|
+
"src/application/**",
|
|
3448
|
+
"src/services/**",
|
|
3449
|
+
"src/use-cases/**",
|
|
3450
|
+
"src/aplicacion/**",
|
|
3451
|
+
"src/servicios/**",
|
|
3452
|
+
"application/**"
|
|
3453
|
+
]
|
|
3454
|
+
},
|
|
3455
|
+
{
|
|
3456
|
+
name: "infrastructure",
|
|
3457
|
+
patterns: [
|
|
3458
|
+
"src/infrastructure/**",
|
|
3459
|
+
"src/repositories/**",
|
|
3460
|
+
"src/persistence/**",
|
|
3461
|
+
"src/infraestructura/**",
|
|
3462
|
+
"src/repositorios/**",
|
|
3463
|
+
"infrastructure/**"
|
|
3464
|
+
]
|
|
3465
|
+
}
|
|
3466
|
+
],
|
|
3467
|
+
rules: [
|
|
3468
|
+
{
|
|
3469
|
+
type: "forbidden",
|
|
3470
|
+
name: "dominio-sin-capas-exteriores",
|
|
3471
|
+
from: "domain",
|
|
3472
|
+
to: "application",
|
|
3473
|
+
severity: "warn",
|
|
3474
|
+
message: "El modelo de dominio no debe depender de los servicios"
|
|
3475
|
+
},
|
|
3476
|
+
{
|
|
3477
|
+
type: "forbidden",
|
|
3478
|
+
name: "dominio-sin-infraestructura",
|
|
3479
|
+
from: "domain",
|
|
3480
|
+
to: "infrastructure",
|
|
3481
|
+
severity: "warn",
|
|
3482
|
+
message: "El modelo de dominio no debe depender de los repositorios"
|
|
3483
|
+
},
|
|
3484
|
+
{
|
|
3485
|
+
type: "forbidden",
|
|
3486
|
+
name: "aplicacion-sin-infraestructura",
|
|
3487
|
+
from: "application",
|
|
3488
|
+
to: "infrastructure",
|
|
3489
|
+
severity: "warn",
|
|
3490
|
+
message: "Los servicios hablan con los repositorios por puertos, no directamente"
|
|
3491
|
+
}
|
|
3492
|
+
]
|
|
3493
|
+
}
|
|
3494
|
+
];
|
|
3495
|
+
function findDomainProfile(id) {
|
|
3496
|
+
return DOMAIN_PROFILES.find((profile) => profile.id === id);
|
|
3497
|
+
}
|
|
3498
|
+
function domainProfileIds() {
|
|
3499
|
+
return DOMAIN_PROFILES.map((profile) => profile.id);
|
|
3500
|
+
}
|
|
3501
|
+
|
|
3183
3502
|
// packages/render/src/model.ts
|
|
3184
3503
|
var RenderError = class extends Error {
|
|
3185
3504
|
constructor(message) {
|
|
@@ -4342,10 +4661,103 @@ function parseRuleConfig(input) {
|
|
|
4342
4661
|
if (root.version !== 1) {
|
|
4343
4662
|
throw new ConfigError("config.version debe ser 1");
|
|
4344
4663
|
}
|
|
4345
|
-
const
|
|
4664
|
+
const own = parseInlineProfiles(root.profiles);
|
|
4665
|
+
const profile = parseProfile(root.profile, own);
|
|
4666
|
+
const profileSeverity = parseProfileSeverity(root.profile_severity);
|
|
4667
|
+
const layers = mergeLayers(profile, parseLayers(root.layers));
|
|
4346
4668
|
const layerNames = new Set(layers.map((layer) => layer.name));
|
|
4347
|
-
const rules =
|
|
4348
|
-
|
|
4669
|
+
const rules = mergeRules(
|
|
4670
|
+
profile,
|
|
4671
|
+
profileSeverity,
|
|
4672
|
+
parseRules(root.rules, layerNames),
|
|
4673
|
+
layerNames
|
|
4674
|
+
);
|
|
4675
|
+
return { version: 1, profile: profile?.id ?? null, layers, rules };
|
|
4676
|
+
}
|
|
4677
|
+
function parseInlineProfiles(value) {
|
|
4678
|
+
const profiles = /* @__PURE__ */ new Map();
|
|
4679
|
+
if (value === void 0 || value === null) return profiles;
|
|
4680
|
+
if (!Array.isArray(value)) {
|
|
4681
|
+
throw new ConfigError("config.profiles debe ser una lista");
|
|
4682
|
+
}
|
|
4683
|
+
value.forEach((raw, index) => {
|
|
4684
|
+
const entry = asObject(raw, `config.profiles[${index}]`);
|
|
4685
|
+
const id = asString(entry.id, `config.profiles[${index}].id`);
|
|
4686
|
+
const title = asString(entry.title, `config.profiles[${index}].title`);
|
|
4687
|
+
const description = entry.description === void 0 ? "" : asString(entry.description, `config.profiles[${index}].description`);
|
|
4688
|
+
const layers = parseLayers(entry.layers).map((layer) => ({
|
|
4689
|
+
name: layer.name,
|
|
4690
|
+
patterns: layer.patterns
|
|
4691
|
+
}));
|
|
4692
|
+
if (layers.length === 0) {
|
|
4693
|
+
throw new ConfigError(
|
|
4694
|
+
`config.profiles[${index}] necesita al menos una capa`
|
|
4695
|
+
);
|
|
4696
|
+
}
|
|
4697
|
+
const layerNames = new Set(layers.map((layer) => layer.name));
|
|
4698
|
+
const rules = parseRules(entry.rules, layerNames).filter(
|
|
4699
|
+
(rule) => rule.type === "forbidden"
|
|
4700
|
+
);
|
|
4701
|
+
profiles.set(id, {
|
|
4702
|
+
id,
|
|
4703
|
+
title,
|
|
4704
|
+
description,
|
|
4705
|
+
when: `Perfil propio del equipo, definido en ${"el archivo de reglas"}.`,
|
|
4706
|
+
layers,
|
|
4707
|
+
rules
|
|
4708
|
+
});
|
|
4709
|
+
});
|
|
4710
|
+
return profiles;
|
|
4711
|
+
}
|
|
4712
|
+
function parseProfile(value, own) {
|
|
4713
|
+
if (value === void 0 || value === null) return null;
|
|
4714
|
+
const id = asString(value, "config.profile");
|
|
4715
|
+
const profile = own.get(id) ?? findDomainProfile(id);
|
|
4716
|
+
if (!profile) {
|
|
4717
|
+
const available = [...own.keys(), ...domainProfileIds()].join(", ");
|
|
4718
|
+
throw new ConfigError(
|
|
4719
|
+
`config.profile desconocido: ${id}. Disponibles: ${available}`
|
|
4720
|
+
);
|
|
4721
|
+
}
|
|
4722
|
+
return profile;
|
|
4723
|
+
}
|
|
4724
|
+
function parseProfileSeverity(value) {
|
|
4725
|
+
if (value === void 0 || value === null) return "warn";
|
|
4726
|
+
if (typeof value !== "string" || !SEVERITIES.has(value)) {
|
|
4727
|
+
throw new ConfigError(
|
|
4728
|
+
"config.profile_severity debe ser info, warn o error"
|
|
4729
|
+
);
|
|
4730
|
+
}
|
|
4731
|
+
return value;
|
|
4732
|
+
}
|
|
4733
|
+
function mergeLayers(profile, own) {
|
|
4734
|
+
const layers = /* @__PURE__ */ new Map();
|
|
4735
|
+
for (const layer of profile?.layers ?? []) layers.set(layer.name, layer);
|
|
4736
|
+
for (const layer of own) layers.set(layer.name, layer);
|
|
4737
|
+
return [...layers.values()];
|
|
4738
|
+
}
|
|
4739
|
+
function mergeRules(profile, profileSeverity, own, layerNames) {
|
|
4740
|
+
const rules = /* @__PURE__ */ new Map();
|
|
4741
|
+
for (const rule of profile?.rules ?? []) {
|
|
4742
|
+
rules.set(rule.name, {
|
|
4743
|
+
...rule,
|
|
4744
|
+
severity: profileSeverity,
|
|
4745
|
+
source: "profile"
|
|
4746
|
+
});
|
|
4747
|
+
}
|
|
4748
|
+
for (const rule of own) {
|
|
4749
|
+
rules.set(rule.name, { ...rule, source: "config" });
|
|
4750
|
+
}
|
|
4751
|
+
for (const rule of rules.values()) {
|
|
4752
|
+
if (rule.type !== "forbidden") continue;
|
|
4753
|
+
if (layerNames.has(rule.from) && layerNames.has(rule.to)) continue;
|
|
4754
|
+
if (rule.source === "profile") {
|
|
4755
|
+
throw new ConfigError(
|
|
4756
|
+
`la regla "${rule.name}" del perfil apunta a una capa que no existe: ${rule.from} \u2192 ${rule.to}`
|
|
4757
|
+
);
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
return [...rules.values()];
|
|
4349
4761
|
}
|
|
4350
4762
|
function loadRuleConfig(file) {
|
|
4351
4763
|
let text;
|
|
@@ -4512,6 +4924,7 @@ import { minimatch } from "minimatch";
|
|
|
4512
4924
|
function emptyRuleReport() {
|
|
4513
4925
|
return {
|
|
4514
4926
|
configured: false,
|
|
4927
|
+
profile: null,
|
|
4515
4928
|
summary: {
|
|
4516
4929
|
layers: 0,
|
|
4517
4930
|
rules: 0,
|
|
@@ -4548,6 +4961,7 @@ function evaluateRules(base, head, config) {
|
|
|
4548
4961
|
resolved.sort(compareViolations);
|
|
4549
4962
|
return {
|
|
4550
4963
|
configured: true,
|
|
4964
|
+
profile: config.profile ?? null,
|
|
4551
4965
|
summary: {
|
|
4552
4966
|
layers: config.layers.length,
|
|
4553
4967
|
rules: config.rules.length,
|
|
@@ -4603,6 +5017,7 @@ function evaluateForbidden(rule, input, layers, violations) {
|
|
|
4603
5017
|
rule: rule.name,
|
|
4604
5018
|
type: "forbidden",
|
|
4605
5019
|
severity: rule.severity,
|
|
5020
|
+
source: rule.source ?? "config",
|
|
4606
5021
|
message: rule.message ?? `${edge.from} no puede depender de ${edge.to}`,
|
|
4607
5022
|
files: [edge.from, edge.to],
|
|
4608
5023
|
edge: {
|
|
@@ -4627,6 +5042,7 @@ function evaluateThreshold(rule, input, violations) {
|
|
|
4627
5042
|
rule: rule.name,
|
|
4628
5043
|
type: "threshold",
|
|
4629
5044
|
severity: rule.severity,
|
|
5045
|
+
source: rule.source ?? "config",
|
|
4630
5046
|
message: rule.message ?? `${file.path}: ${rule.metric} = ${value} (${limit})`,
|
|
4631
5047
|
files: [file.path],
|
|
4632
5048
|
metric: {
|
|
@@ -4646,6 +5062,7 @@ function evaluateCycles(rule, cycles, violations) {
|
|
|
4646
5062
|
rule: rule.name,
|
|
4647
5063
|
type: "cycles",
|
|
4648
5064
|
severity: rule.severity,
|
|
5065
|
+
source: rule.source ?? "config",
|
|
4649
5066
|
message: rule.message ?? cycleMessage(paths),
|
|
4650
5067
|
files: paths,
|
|
4651
5068
|
cycle: {
|
|
@@ -4692,7 +5109,7 @@ function diffSnapshots(base, head, options = {}) {
|
|
|
4692
5109
|
options.rules
|
|
4693
5110
|
) : emptyRuleReport();
|
|
4694
5111
|
return {
|
|
4695
|
-
schemaVersion: "1.
|
|
5112
|
+
schemaVersion: "1.2",
|
|
4696
5113
|
base: summarize(baseIndex),
|
|
4697
5114
|
head: summarize(headIndex),
|
|
4698
5115
|
summary: {
|
|
@@ -4712,9 +5129,94 @@ function diffSnapshots(base, head, options = {}) {
|
|
|
4712
5129
|
files: { added, removed, changed },
|
|
4713
5130
|
edges: edgeDiff,
|
|
4714
5131
|
cycles: { introduced: introducedCycles, fixed: fixedCycles },
|
|
4715
|
-
hotspots
|
|
5132
|
+
hotspots,
|
|
5133
|
+
migration: buildMigrationReport(baseIndex, headIndex, top)
|
|
4716
5134
|
};
|
|
4717
5135
|
}
|
|
5136
|
+
function buildMigrationReport(baseIndex, headIndex, top) {
|
|
5137
|
+
const baseLens = baseIndex.cc.lenses?.migration;
|
|
5138
|
+
const headLens = headIndex.cc.lenses?.migration;
|
|
5139
|
+
if (!baseLens && !headLens) return null;
|
|
5140
|
+
const baseNodes = baseLens?.nodes ?? {};
|
|
5141
|
+
const headNodes = headLens?.nodes ?? {};
|
|
5142
|
+
const changes = [];
|
|
5143
|
+
for (const id of /* @__PURE__ */ new Set([...Object.keys(baseNodes), ...Object.keys(headNodes)])) {
|
|
5144
|
+
const base = baseNodes[id];
|
|
5145
|
+
const head = headNodes[id];
|
|
5146
|
+
if (!base && !head) continue;
|
|
5147
|
+
const reference = head ?? base;
|
|
5148
|
+
const baseDebt = base ? base.debt ?? 0 : null;
|
|
5149
|
+
const headDebt = head ? head.debt ?? 0 : null;
|
|
5150
|
+
const delta = baseDebt === null || headDebt === null ? null : headDebt - baseDebt;
|
|
5151
|
+
if (delta === null || delta === 0) continue;
|
|
5152
|
+
changes.push({
|
|
5153
|
+
id,
|
|
5154
|
+
path: headIndex.paths.get(id) ?? baseIndex.paths.get(id) ?? id,
|
|
5155
|
+
language: reference.language,
|
|
5156
|
+
status: reference.status,
|
|
5157
|
+
base: baseDebt,
|
|
5158
|
+
head: headDebt,
|
|
5159
|
+
delta
|
|
5160
|
+
});
|
|
5161
|
+
}
|
|
5162
|
+
const increased = changes.filter((change) => (change.delta ?? 0) > 0).sort(
|
|
5163
|
+
(left, right) => (right.delta ?? 0) - (left.delta ?? 0) || compareByPath(left, right)
|
|
5164
|
+
);
|
|
5165
|
+
const decreased = changes.filter((change) => (change.delta ?? 0) < 0).sort(
|
|
5166
|
+
(left, right) => (left.delta ?? 0) - (right.delta ?? 0) || compareByPath(left, right)
|
|
5167
|
+
);
|
|
5168
|
+
const topDebt = [];
|
|
5169
|
+
for (const [id, node] of Object.entries(headNodes)) {
|
|
5170
|
+
if (!node.debt) continue;
|
|
5171
|
+
topDebt.push({
|
|
5172
|
+
id,
|
|
5173
|
+
path: headIndex.paths.get(id) ?? id,
|
|
5174
|
+
language: node.language,
|
|
5175
|
+
status: node.status,
|
|
5176
|
+
debt: node.debt,
|
|
5177
|
+
rloc: node.rloc,
|
|
5178
|
+
crossings: node.crossings
|
|
5179
|
+
});
|
|
5180
|
+
}
|
|
5181
|
+
topDebt.sort(
|
|
5182
|
+
(left, right) => right.debt - left.debt || compareByPath(left, right)
|
|
5183
|
+
);
|
|
5184
|
+
const baseSummary = baseLens?.summary;
|
|
5185
|
+
const headSummary = headLens?.summary;
|
|
5186
|
+
return {
|
|
5187
|
+
languages: {
|
|
5188
|
+
base: baseSummary?.languages ?? {},
|
|
5189
|
+
head: headSummary?.languages ?? {}
|
|
5190
|
+
},
|
|
5191
|
+
status: {
|
|
5192
|
+
base: baseSummary?.status ?? emptyStatus(),
|
|
5193
|
+
head: headSummary?.status ?? emptyStatus()
|
|
5194
|
+
},
|
|
5195
|
+
debt: {
|
|
5196
|
+
base: baseSummary?.migrationDebt ?? 0,
|
|
5197
|
+
head: headSummary?.migrationDebt ?? 0,
|
|
5198
|
+
delta: (headSummary?.migrationDebt ?? 0) - (baseSummary?.migrationDebt ?? 0)
|
|
5199
|
+
},
|
|
5200
|
+
crossings: {
|
|
5201
|
+
base: baseSummary?.ffiCrossings ?? 0,
|
|
5202
|
+
head: headSummary?.ffiCrossings ?? 0,
|
|
5203
|
+
delta: (headSummary?.ffiCrossings ?? 0) - (baseSummary?.ffiCrossings ?? 0)
|
|
5204
|
+
},
|
|
5205
|
+
bridgeFiles: {
|
|
5206
|
+
base: baseSummary?.bridgeFiles ?? 0,
|
|
5207
|
+
head: headSummary?.bridgeFiles ?? 0,
|
|
5208
|
+
delta: (headSummary?.bridgeFiles ?? 0) - (baseSummary?.bridgeFiles ?? 0)
|
|
5209
|
+
},
|
|
5210
|
+
files: {
|
|
5211
|
+
increased: increased.slice(0, top),
|
|
5212
|
+
decreased: decreased.slice(0, top)
|
|
5213
|
+
},
|
|
5214
|
+
topDebt: topDebt.slice(0, top)
|
|
5215
|
+
};
|
|
5216
|
+
}
|
|
5217
|
+
function emptyStatus() {
|
|
5218
|
+
return { js: 0, rust: 0, bridge: 0, native: 0 };
|
|
5219
|
+
}
|
|
4718
5220
|
function indexSnapshot(cc) {
|
|
4719
5221
|
const paths = /* @__PURE__ */ new Map();
|
|
4720
5222
|
for (const root of cc.files) {
|
|
@@ -4776,7 +5278,7 @@ function difference(present, absent) {
|
|
|
4776
5278
|
metrics: sortedMetrics(present.metrics[id])
|
|
4777
5279
|
});
|
|
4778
5280
|
}
|
|
4779
|
-
return files.sort(
|
|
5281
|
+
return files.sort(compareByPath);
|
|
4780
5282
|
}
|
|
4781
5283
|
function changedFiles(base, head) {
|
|
4782
5284
|
const changed = [];
|
|
@@ -4990,7 +5492,7 @@ function finiteNumber(value) {
|
|
|
4990
5492
|
function edgeKey(fromId, toId) {
|
|
4991
5493
|
return `${fromId}\0${toId}`;
|
|
4992
5494
|
}
|
|
4993
|
-
function
|
|
5495
|
+
function compareByPath(left, right) {
|
|
4994
5496
|
return compareStrings5(left.path, right.path) || compareStrings5(left.id, right.id);
|
|
4995
5497
|
}
|
|
4996
5498
|
function compareChangedFiles(left, right) {
|
|
@@ -5009,6 +5511,209 @@ function compareStrings5(left, right) {
|
|
|
5009
5511
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
5010
5512
|
}
|
|
5011
5513
|
|
|
5514
|
+
// packages/metrics/src/suggest.ts
|
|
5515
|
+
var ROOT_FOLDERS = /* @__PURE__ */ new Set([
|
|
5516
|
+
"src",
|
|
5517
|
+
"lib",
|
|
5518
|
+
"libs",
|
|
5519
|
+
"source",
|
|
5520
|
+
"app",
|
|
5521
|
+
"apps",
|
|
5522
|
+
"packages",
|
|
5523
|
+
"pkg"
|
|
5524
|
+
]);
|
|
5525
|
+
function suggestLayers(cc) {
|
|
5526
|
+
const paths = /* @__PURE__ */ new Map();
|
|
5527
|
+
for (const root of cc.files) {
|
|
5528
|
+
walkFiles([root], (entry) => {
|
|
5529
|
+
paths.set(entry.node.id, relativePath2(root, entry.path));
|
|
5530
|
+
});
|
|
5531
|
+
}
|
|
5532
|
+
const edges = cc.lenses?.dependency?.edges ?? [];
|
|
5533
|
+
const groups = /* @__PURE__ */ new Map();
|
|
5534
|
+
let ungrouped = 0;
|
|
5535
|
+
const groupOf = (path7) => {
|
|
5536
|
+
const parts = path7.split("/").filter(Boolean);
|
|
5537
|
+
if (parts.length === 0) return null;
|
|
5538
|
+
const directory = parts.slice(0, -1);
|
|
5539
|
+
if (directory.length === 0) {
|
|
5540
|
+
ungrouped += 1;
|
|
5541
|
+
return null;
|
|
5542
|
+
}
|
|
5543
|
+
if (directory.length === 1 && ROOT_FOLDERS.has(directory[0])) {
|
|
5544
|
+
ungrouped += 1;
|
|
5545
|
+
return null;
|
|
5546
|
+
}
|
|
5547
|
+
const name = directory.slice(0, 2).join("/");
|
|
5548
|
+
let group = groups.get(name);
|
|
5549
|
+
if (!group) {
|
|
5550
|
+
group = {
|
|
5551
|
+
name,
|
|
5552
|
+
pattern: `${name}/**`,
|
|
5553
|
+
files: 0,
|
|
5554
|
+
dependsOn: /* @__PURE__ */ new Set(),
|
|
5555
|
+
usedBy: /* @__PURE__ */ new Set()
|
|
5556
|
+
};
|
|
5557
|
+
groups.set(name, group);
|
|
5558
|
+
}
|
|
5559
|
+
group.files += 1;
|
|
5560
|
+
return group;
|
|
5561
|
+
};
|
|
5562
|
+
for (const path7 of paths.values()) groupOf(path7);
|
|
5563
|
+
const groupEdges = [];
|
|
5564
|
+
for (const edge of edges) {
|
|
5565
|
+
const fromPath = paths.get(edge.fromId);
|
|
5566
|
+
const toPath = paths.get(edge.toId);
|
|
5567
|
+
if (!fromPath || !toPath) continue;
|
|
5568
|
+
const from = groupOf(fromPath);
|
|
5569
|
+
const to = groupOf(toPath);
|
|
5570
|
+
if (!from || !to || from.name === to.name) continue;
|
|
5571
|
+
from.dependsOn.add(to.name);
|
|
5572
|
+
to.usedBy.add(from.name);
|
|
5573
|
+
groupEdges.push({ from, to });
|
|
5574
|
+
}
|
|
5575
|
+
const depthOf = (name, visiting = /* @__PURE__ */ new Set()) => {
|
|
5576
|
+
const group = groups.get(name);
|
|
5577
|
+
if (!group) return 0;
|
|
5578
|
+
if (visiting.has(name)) return 0;
|
|
5579
|
+
visiting.add(name);
|
|
5580
|
+
let depth = 0;
|
|
5581
|
+
for (const dependency of group.dependsOn) {
|
|
5582
|
+
depth = Math.max(depth, depthOf(dependency, visiting) + 1);
|
|
5583
|
+
}
|
|
5584
|
+
visiting.delete(name);
|
|
5585
|
+
return depth;
|
|
5586
|
+
};
|
|
5587
|
+
const layers = [...groups.values()].map((group) => ({
|
|
5588
|
+
name: group.name,
|
|
5589
|
+
patterns: [group.pattern],
|
|
5590
|
+
files: group.files,
|
|
5591
|
+
dependsOn: [...group.dependsOn].sort(),
|
|
5592
|
+
usedBy: [...group.usedBy].sort(),
|
|
5593
|
+
depth: depthOf(group.name)
|
|
5594
|
+
})).sort((left, right) => left.depth - right.depth || left.name.localeCompare(right.name));
|
|
5595
|
+
const rules = [];
|
|
5596
|
+
for (const layer of layers) {
|
|
5597
|
+
for (const outer of layer.usedBy) {
|
|
5598
|
+
rules.push({
|
|
5599
|
+
name: `${slug(layer.name)}-sin-${slug(outer)}`,
|
|
5600
|
+
type: "forbidden",
|
|
5601
|
+
from: layer.name,
|
|
5602
|
+
to: outer,
|
|
5603
|
+
severity: "warn",
|
|
5604
|
+
message: `${layer.name} no debe depender de ${outer}`
|
|
5605
|
+
});
|
|
5606
|
+
}
|
|
5607
|
+
}
|
|
5608
|
+
rules.sort((left, right) => left.name.localeCompare(right.name));
|
|
5609
|
+
const notes = [];
|
|
5610
|
+
if (ungrouped > 0) {
|
|
5611
|
+
notes.push(
|
|
5612
|
+
`${ungrouped} ficheros en la ra\xEDz o sin carpeta: no forman capa (suelen ser puntos de entrada).`
|
|
5613
|
+
);
|
|
5614
|
+
}
|
|
5615
|
+
if (layers.length === 0) {
|
|
5616
|
+
notes.push("No se ha encontrado ninguna carpeta con c\xF3digo.");
|
|
5617
|
+
}
|
|
5618
|
+
if (layers.length === 1) {
|
|
5619
|
+
notes.push(
|
|
5620
|
+
"Solo hay una capa: no se puede deducir una estructura. Prueba con un perfil del cat\xE1logo."
|
|
5621
|
+
);
|
|
5622
|
+
}
|
|
5623
|
+
const fits = matchProfiles(groups, groupEdges);
|
|
5624
|
+
const best = fits.find((fit) => fit.matched >= 2) ?? null;
|
|
5625
|
+
const profile = best ? findDomainProfile(best.id) ?? null : null;
|
|
5626
|
+
const proposedLayers = profile ? profile.layers.map((layer) => {
|
|
5627
|
+
const group = groupForLayer(layer, groups);
|
|
5628
|
+
return {
|
|
5629
|
+
name: layer.name,
|
|
5630
|
+
patterns: [...layer.patterns],
|
|
5631
|
+
files: group?.files ?? 0,
|
|
5632
|
+
dependsOn: [],
|
|
5633
|
+
usedBy: [],
|
|
5634
|
+
depth: 0
|
|
5635
|
+
};
|
|
5636
|
+
}) : layers;
|
|
5637
|
+
const proposedRules = profile ? profile.rules.map((rule) => ({
|
|
5638
|
+
name: rule.name,
|
|
5639
|
+
type: "forbidden",
|
|
5640
|
+
from: rule.from,
|
|
5641
|
+
to: rule.to,
|
|
5642
|
+
severity: "warn",
|
|
5643
|
+
message: rule.message ?? `${rule.from} no debe depender de ${rule.to}`
|
|
5644
|
+
})) : rules;
|
|
5645
|
+
return {
|
|
5646
|
+
detected: layers,
|
|
5647
|
+
layers: proposedLayers,
|
|
5648
|
+
rules: proposedRules,
|
|
5649
|
+
proposal: profile ? "profile" : "deduced",
|
|
5650
|
+
profile: profile?.id ?? null,
|
|
5651
|
+
ungrouped,
|
|
5652
|
+
notes,
|
|
5653
|
+
fits
|
|
5654
|
+
};
|
|
5655
|
+
}
|
|
5656
|
+
function groupForLayer(layer, groups) {
|
|
5657
|
+
for (const candidate of groups.values()) {
|
|
5658
|
+
if (layer.patterns.some((pattern) => covers(pattern, candidate.name))) {
|
|
5659
|
+
return candidate;
|
|
5660
|
+
}
|
|
5661
|
+
}
|
|
5662
|
+
return void 0;
|
|
5663
|
+
}
|
|
5664
|
+
function matchProfiles(groups, groupEdges) {
|
|
5665
|
+
const totalFiles = [...groups.values()].reduce(
|
|
5666
|
+
(sum, group) => sum + group.files,
|
|
5667
|
+
0
|
|
5668
|
+
);
|
|
5669
|
+
return DOMAIN_PROFILES.map((profile) => {
|
|
5670
|
+
const layerOf = /* @__PURE__ */ new Map();
|
|
5671
|
+
for (const layer of profile.layers) {
|
|
5672
|
+
const group = [...groups.values()].find(
|
|
5673
|
+
(candidate) => layer.patterns.some((pattern) => covers(pattern, candidate.name))
|
|
5674
|
+
);
|
|
5675
|
+
if (group) layerOf.set(layer.name, group.name);
|
|
5676
|
+
}
|
|
5677
|
+
const covered = [...layerOf.values()].reduce(
|
|
5678
|
+
(sum, name) => sum + (groups.get(name)?.files ?? 0),
|
|
5679
|
+
0
|
|
5680
|
+
);
|
|
5681
|
+
const broken = [];
|
|
5682
|
+
for (const rule of profile.rules) {
|
|
5683
|
+
const fromGroup = layerOf.get(rule.from);
|
|
5684
|
+
const toGroup = layerOf.get(rule.to);
|
|
5685
|
+
if (!fromGroup || !toGroup) continue;
|
|
5686
|
+
const violates = groupEdges.some(
|
|
5687
|
+
(edge) => edge.from.name === fromGroup && edge.to.name === toGroup
|
|
5688
|
+
);
|
|
5689
|
+
if (violates) broken.push(rule.message ?? `${rule.from} no puede depender de ${rule.to}`);
|
|
5690
|
+
}
|
|
5691
|
+
return {
|
|
5692
|
+
id: profile.id,
|
|
5693
|
+
title: profile.title,
|
|
5694
|
+
matched: layerOf.size,
|
|
5695
|
+
total: profile.layers.length,
|
|
5696
|
+
coverage: totalFiles === 0 ? 0 : Math.round(covered / totalFiles * 100),
|
|
5697
|
+
broken
|
|
5698
|
+
};
|
|
5699
|
+
}).sort(
|
|
5700
|
+
(left, right) => right.matched - left.matched || right.coverage - left.coverage || left.id.localeCompare(right.id)
|
|
5701
|
+
);
|
|
5702
|
+
}
|
|
5703
|
+
function covers(pattern, name) {
|
|
5704
|
+
const prefix = pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern;
|
|
5705
|
+
return name === prefix || name.startsWith(`${prefix}/`);
|
|
5706
|
+
}
|
|
5707
|
+
function slug(name) {
|
|
5708
|
+
return name.replaceAll("/", "-");
|
|
5709
|
+
}
|
|
5710
|
+
function relativePath2(root, fullPath) {
|
|
5711
|
+
if (fullPath === root.name) return "";
|
|
5712
|
+
const prefix = `${root.name}/`;
|
|
5713
|
+
const stripped = fullPath.startsWith(prefix) ? fullPath.slice(prefix.length) : fullPath;
|
|
5714
|
+
return stripped.replace(/^\/+/, "");
|
|
5715
|
+
}
|
|
5716
|
+
|
|
5012
5717
|
// apps/cli/src/commands/diff.ts
|
|
5013
5718
|
var numberFormat2 = new Intl.NumberFormat("es-ES");
|
|
5014
5719
|
function num2(value) {
|
|
@@ -5651,7 +6356,11 @@ function runServe(options) {
|
|
|
5651
6356
|
if (pathname === "/data/report.json") {
|
|
5652
6357
|
const report = readReport(state.root);
|
|
5653
6358
|
if (report === null) {
|
|
5654
|
-
|
|
6359
|
+
response.writeHead(204, {
|
|
6360
|
+
"Cache-Control": "no-store",
|
|
6361
|
+
"X-Content-Type-Options": "nosniff"
|
|
6362
|
+
});
|
|
6363
|
+
response.end();
|
|
5655
6364
|
return;
|
|
5656
6365
|
}
|
|
5657
6366
|
sendJson(response, report);
|
|
@@ -5881,8 +6590,170 @@ data: ${JSON.stringify(data)}
|
|
|
5881
6590
|
}
|
|
5882
6591
|
}
|
|
5883
6592
|
|
|
6593
|
+
// apps/cli/src/commands/profiles.ts
|
|
6594
|
+
function runProfiles(json) {
|
|
6595
|
+
if (json) {
|
|
6596
|
+
console.log(
|
|
6597
|
+
JSON.stringify(
|
|
6598
|
+
{
|
|
6599
|
+
profiles: DOMAIN_PROFILES.map((profile) => ({
|
|
6600
|
+
id: profile.id,
|
|
6601
|
+
title: profile.title,
|
|
6602
|
+
description: profile.description,
|
|
6603
|
+
when: profile.when,
|
|
6604
|
+
layers: profile.layers.map((layer) => ({
|
|
6605
|
+
name: layer.name,
|
|
6606
|
+
patterns: layer.patterns
|
|
6607
|
+
})),
|
|
6608
|
+
rules: profile.rules.map((rule) => ({
|
|
6609
|
+
name: rule.name,
|
|
6610
|
+
from: rule.from,
|
|
6611
|
+
to: rule.to,
|
|
6612
|
+
severity: rule.severity,
|
|
6613
|
+
message: rule.message ?? null
|
|
6614
|
+
}))
|
|
6615
|
+
}))
|
|
6616
|
+
},
|
|
6617
|
+
null,
|
|
6618
|
+
2
|
|
6619
|
+
)
|
|
6620
|
+
);
|
|
6621
|
+
return 0;
|
|
6622
|
+
}
|
|
6623
|
+
console.log("");
|
|
6624
|
+
console.log(" CodArch \xB7 perfiles de arquitectura");
|
|
6625
|
+
console.log("");
|
|
6626
|
+
for (const profile of DOMAIN_PROFILES) {
|
|
6627
|
+
console.log(` ${profile.id} \xB7 ${profile.title}`);
|
|
6628
|
+
console.log(` ${profile.description}`);
|
|
6629
|
+
console.log(` para: ${profile.when}`);
|
|
6630
|
+
console.log(
|
|
6631
|
+
` capas: ${profile.layers.map((layer) => layer.name).join(", ")}`
|
|
6632
|
+
);
|
|
6633
|
+
for (const rule of profile.rules) {
|
|
6634
|
+
console.log(
|
|
6635
|
+
` \xB7 ${rule.name}: ${rule.from} no puede depender de ${rule.to} (${rule.severity})`
|
|
6636
|
+
);
|
|
6637
|
+
}
|
|
6638
|
+
console.log("");
|
|
6639
|
+
}
|
|
6640
|
+
console.log(" Para usarlo, en .codarch.yml:");
|
|
6641
|
+
console.log("");
|
|
6642
|
+
console.log(" version: 1");
|
|
6643
|
+
console.log(" profile: capas");
|
|
6644
|
+
console.log("");
|
|
6645
|
+
console.log(' Las reglas del perfil entran como aviso: suben a "error" cuando el c\xF3digo est\xE9 listo.');
|
|
6646
|
+
console.log("");
|
|
6647
|
+
return 0;
|
|
6648
|
+
}
|
|
6649
|
+
|
|
6650
|
+
// apps/cli/src/commands/suggest.ts
|
|
6651
|
+
import { stringify as toYaml } from "yaml";
|
|
6652
|
+
function runSuggest(options) {
|
|
6653
|
+
if (!options.file) {
|
|
6654
|
+
console.error(
|
|
6655
|
+
"falta la ruta del fichero cc.json\n\n codarch suggest <fichero.cc.json[.gz]>"
|
|
6656
|
+
);
|
|
6657
|
+
return 1;
|
|
6658
|
+
}
|
|
6659
|
+
let loaded;
|
|
6660
|
+
try {
|
|
6661
|
+
loaded = readJsonFile2(options.file);
|
|
6662
|
+
} catch (error) {
|
|
6663
|
+
console.error(`no se pudo leer ${options.file}: ${error.message}`);
|
|
6664
|
+
return 1;
|
|
6665
|
+
}
|
|
6666
|
+
let suggestion;
|
|
6667
|
+
try {
|
|
6668
|
+
const cc = parseCcJson(loaded.json);
|
|
6669
|
+
suggestion = suggestLayers(cc);
|
|
6670
|
+
} catch (error) {
|
|
6671
|
+
console.error(`no se pudo leer ${options.file}: ${error.message}`);
|
|
6672
|
+
return 1;
|
|
6673
|
+
}
|
|
6674
|
+
if (options.json) {
|
|
6675
|
+
console.log(
|
|
6676
|
+
JSON.stringify(
|
|
6677
|
+
{
|
|
6678
|
+
detected: suggestion.detected,
|
|
6679
|
+
layers: suggestion.layers,
|
|
6680
|
+
rules: suggestion.rules,
|
|
6681
|
+
proposal: suggestion.proposal,
|
|
6682
|
+
profile: suggestion.profile,
|
|
6683
|
+
ungrouped: suggestion.ungrouped,
|
|
6684
|
+
notes: suggestion.notes,
|
|
6685
|
+
fits: suggestion.fits
|
|
6686
|
+
},
|
|
6687
|
+
null,
|
|
6688
|
+
2
|
|
6689
|
+
)
|
|
6690
|
+
);
|
|
6691
|
+
return 0;
|
|
6692
|
+
}
|
|
6693
|
+
console.log("");
|
|
6694
|
+
console.log(" CodArch \xB7 propuesta de capas");
|
|
6695
|
+
console.log("");
|
|
6696
|
+
if (suggestion.layers.length === 0) {
|
|
6697
|
+
console.log(" No se ha encontrado ninguna carpeta con c\xF3digo.");
|
|
6698
|
+
for (const note of suggestion.notes) console.log(` ${note}`);
|
|
6699
|
+
console.log("");
|
|
6700
|
+
return 0;
|
|
6701
|
+
}
|
|
6702
|
+
for (const layer of suggestion.detected) {
|
|
6703
|
+
console.log(
|
|
6704
|
+
` ${layer.name} \xB7 ${layer.files} ficheros \xB7 nivel ${layer.depth}` + (layer.usedBy.length > 0 ? ` \xB7 de aqu\xED dependen: ${layer.usedBy.join(", ")}` : "")
|
|
6705
|
+
);
|
|
6706
|
+
}
|
|
6707
|
+
console.log("");
|
|
6708
|
+
const yaml = toYaml(
|
|
6709
|
+
{
|
|
6710
|
+
version: 1,
|
|
6711
|
+
layers: suggestion.layers.map((layer) => ({
|
|
6712
|
+
name: layer.name,
|
|
6713
|
+
paths: layer.patterns
|
|
6714
|
+
})),
|
|
6715
|
+
rules: suggestion.rules
|
|
6716
|
+
},
|
|
6717
|
+
{ lineWidth: 0 }
|
|
6718
|
+
);
|
|
6719
|
+
console.log(" Propuesta para .codarch.yml:");
|
|
6720
|
+
console.log("");
|
|
6721
|
+
console.log(
|
|
6722
|
+
suggestion.proposal === "profile" && suggestion.profile ? ` # Propuesta a partir del perfil "${suggestion.profile}" del cat\xE1logo.` : " # Propuesta deducida del grafo de dependencias del repo."
|
|
6723
|
+
);
|
|
6724
|
+
console.log(
|
|
6725
|
+
yaml.split("\n").map((line) => ` ${line}`).join("\n").trimEnd()
|
|
6726
|
+
);
|
|
6727
|
+
console.log("");
|
|
6728
|
+
const best = suggestion.fits[0];
|
|
6729
|
+
if (best && best.matched > 0) {
|
|
6730
|
+
console.log(" Encaje con el cat\xE1logo de perfiles:");
|
|
6731
|
+
console.log("");
|
|
6732
|
+
for (const fit of suggestion.fits.filter((entry) => entry.matched > 0)) {
|
|
6733
|
+
console.log(
|
|
6734
|
+
` ${fit.id.padEnd(12)} ${fit.matched} de ${fit.total} capas \xB7 cubre el ${fit.coverage}% del c\xF3digo`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6737
|
+
console.log("");
|
|
6738
|
+
if (best.broken.length > 0) {
|
|
6739
|
+
console.log(
|
|
6740
|
+
` Con el perfil "${best.id}", el c\xF3digo incumple hoy ${best.broken.length} ${best.broken.length === 1 ? "regla" : "reglas"}:`
|
|
6741
|
+
);
|
|
6742
|
+
for (const message of best.broken) console.log(` \xB7 ${message}`);
|
|
6743
|
+
console.log("");
|
|
6744
|
+
}
|
|
6745
|
+
}
|
|
6746
|
+
for (const note of suggestion.notes) console.log(` ${note}`);
|
|
6747
|
+
console.log("");
|
|
6748
|
+
console.log(
|
|
6749
|
+
' Las reglas salen como aviso: consultan, no bloquean. Rev\xEDsalas y s\xFAbelas a "error" si encajan.'
|
|
6750
|
+
);
|
|
6751
|
+
console.log("");
|
|
6752
|
+
return 0;
|
|
6753
|
+
}
|
|
6754
|
+
|
|
5884
6755
|
// apps/cli/src/index.ts
|
|
5885
|
-
var VERSION = true ? "0.
|
|
6756
|
+
var VERSION = true ? "0.2.0" : "0.0.0";
|
|
5886
6757
|
var USAGE = `
|
|
5887
6758
|
CodArch \xB7 mapa vivo de la arquitectura de tu software
|
|
5888
6759
|
|
|
@@ -5892,6 +6763,8 @@ Uso:
|
|
|
5892
6763
|
codarch render <fichero.cc.json[.gz]> [opciones]
|
|
5893
6764
|
codarch diff <base.cc.json[.gz]> <head.cc.json[.gz]> [opciones]
|
|
5894
6765
|
codarch serve [ruta] [opciones]
|
|
6766
|
+
codarch profiles [--json]
|
|
6767
|
+
codarch suggest <fichero.cc.json[.gz]> [--json]
|
|
5895
6768
|
|
|
5896
6769
|
Opciones de analyze:
|
|
5897
6770
|
-o, --out <dir> directorio de salida (por defecto <ruta>/.codarch)
|
|
@@ -5933,6 +6806,14 @@ Opciones de serve:
|
|
|
5933
6806
|
--no-web modo API sin servir la app (para Vite u otro frontend)
|
|
5934
6807
|
-h, --help esta ayuda
|
|
5935
6808
|
|
|
6809
|
+
Opciones de profiles:
|
|
6810
|
+
--json salida machine-readable
|
|
6811
|
+
-h, --help esta ayuda
|
|
6812
|
+
|
|
6813
|
+
Opciones de suggest:
|
|
6814
|
+
--json salida machine-readable
|
|
6815
|
+
-h, --help esta ayuda
|
|
6816
|
+
|
|
5936
6817
|
Ejemplos:
|
|
5937
6818
|
codarch analyze .
|
|
5938
6819
|
codarch analyze . -o .codarch --title "Mi proyecto"
|
|
@@ -5942,6 +6823,8 @@ Ejemplos:
|
|
|
5942
6823
|
codarch diff base.cc.json head.cc.json -o report.json --fail-on cycles
|
|
5943
6824
|
codarch serve .
|
|
5944
6825
|
codarch serve . --no-watch
|
|
6826
|
+
codarch profiles
|
|
6827
|
+
codarch suggest .codarch/snapshot.cc.json
|
|
5945
6828
|
`;
|
|
5946
6829
|
function parseAnalyze(args) {
|
|
5947
6830
|
const options = { help: false, ignore: [] };
|
|
@@ -6006,6 +6889,30 @@ function parseAnalyze(args) {
|
|
|
6006
6889
|
options.root = positional[0] ?? ".";
|
|
6007
6890
|
return options;
|
|
6008
6891
|
}
|
|
6892
|
+
function parseSuggest(args) {
|
|
6893
|
+
const options = { json: false };
|
|
6894
|
+
const positional = [];
|
|
6895
|
+
for (let index = 0; index < args.length; index++) {
|
|
6896
|
+
const arg = args[index];
|
|
6897
|
+
if (arg === "--json") {
|
|
6898
|
+
options.json = true;
|
|
6899
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
6900
|
+
console.log(USAGE);
|
|
6901
|
+
return 0;
|
|
6902
|
+
} else if (arg.startsWith("-")) {
|
|
6903
|
+
console.error(`opci\xF3n desconocida: ${arg}`);
|
|
6904
|
+
return 1;
|
|
6905
|
+
} else {
|
|
6906
|
+
positional.push(arg);
|
|
6907
|
+
}
|
|
6908
|
+
}
|
|
6909
|
+
if (positional.length > 1) {
|
|
6910
|
+
console.error(`sobra un argumento: ${positional[1]}`);
|
|
6911
|
+
return 1;
|
|
6912
|
+
}
|
|
6913
|
+
options.file = positional[0];
|
|
6914
|
+
return options;
|
|
6915
|
+
}
|
|
6009
6916
|
function parseInspect(args) {
|
|
6010
6917
|
const options = { json: false, top: 5, help: false };
|
|
6011
6918
|
const positional = [];
|
|
@@ -6301,6 +7208,14 @@ function main(argv) {
|
|
|
6301
7208
|
}
|
|
6302
7209
|
return runServe(parsed);
|
|
6303
7210
|
}
|
|
7211
|
+
if (command === "profiles") {
|
|
7212
|
+
return runProfiles(rest.includes("--json"));
|
|
7213
|
+
}
|
|
7214
|
+
if (command === "suggest") {
|
|
7215
|
+
const parsed = parseSuggest(rest);
|
|
7216
|
+
if (typeof parsed === "number") return parsed;
|
|
7217
|
+
return runSuggest(parsed);
|
|
7218
|
+
}
|
|
6304
7219
|
console.error(`comando desconocido: ${command}
|
|
6305
7220
|
${USAGE}`);
|
|
6306
7221
|
return 1;
|