pumuki 6.3.113 → 6.3.114

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/CHANGELOG.md +51 -5
  2. package/README.md +4 -2
  3. package/VERSION +1 -1
  4. package/core/facts/detectors/typescript/index.test.ts +0 -229
  5. package/core/facts/detectors/typescript/index.ts +0 -278
  6. package/core/facts/extractHeuristicFacts.ts +0 -4
  7. package/core/rules/presets/heuristics/typescript.test.ts +1 -21
  8. package/core/rules/presets/heuristics/typescript.ts +0 -72
  9. package/docs/README.md +13 -9
  10. package/docs/codex-skills/backend-enterprise-rules.md +3 -3
  11. package/docs/operations/RELEASE_NOTES.md +40 -4
  12. package/docs/product/API_REFERENCE.md +1 -1
  13. package/docs/product/HOW_IT_WORKS.md +6 -0
  14. package/docs/product/INSTALLATION.md +1 -1
  15. package/docs/product/USAGE.md +42 -5
  16. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +100 -44
  17. package/docs/validation/README.md +6 -3
  18. package/integrations/config/skillsDetectorRegistry.ts +0 -24
  19. package/integrations/config/skillsMarkdownRules.ts +0 -57
  20. package/integrations/evidence/buildEvidence.ts +0 -24
  21. package/integrations/evidence/repoState.ts +0 -3
  22. package/integrations/evidence/schema.ts +0 -18
  23. package/integrations/evidence/writeEvidence.ts +0 -24
  24. package/integrations/gate/evaluateAiGate.ts +8 -251
  25. package/integrations/gate/remediationCatalog.ts +0 -8
  26. package/integrations/git/GitService.ts +44 -5
  27. package/integrations/git/aiGateRepoPolicyFindings.ts +86 -17
  28. package/integrations/git/runPlatformGate.ts +1 -9
  29. package/integrations/git/runPlatformGateFacts.ts +19 -1
  30. package/integrations/git/runPlatformGateOutput.ts +41 -42
  31. package/integrations/lifecycle/adapter.templates.json +1 -0
  32. package/integrations/lifecycle/adapter.ts +0 -24
  33. package/integrations/lifecycle/audit.ts +101 -0
  34. package/integrations/lifecycle/cli.ts +120 -99
  35. package/integrations/lifecycle/cliSdd.ts +4 -26
  36. package/integrations/lifecycle/doctor.ts +40 -102
  37. package/integrations/lifecycle/index.ts +2 -0
  38. package/integrations/lifecycle/install.ts +0 -21
  39. package/integrations/lifecycle/packageInfo.ts +1 -118
  40. package/integrations/lifecycle/state.ts +1 -8
  41. package/integrations/lifecycle/status.ts +40 -59
  42. package/integrations/lifecycle/watch.ts +1 -1
  43. package/integrations/mcp/aiGateCheck.ts +10 -194
  44. package/integrations/mcp/autoExecuteAiStart.ts +116 -92
  45. package/integrations/mcp/enterpriseServer.ts +7 -23
  46. package/integrations/mcp/enterpriseStdioServer.cli.ts +4 -31
  47. package/integrations/mcp/preFlightCheck.ts +5 -67
  48. package/integrations/platform/detectPlatforms.ts +37 -0
  49. package/integrations/sdd/policy.ts +28 -20
  50. package/package.json +1 -1
  51. package/scripts/check-tracking-single-active.sh +1 -1
  52. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
  53. package/scripts/consumer-postinstall-resolve-args.cjs +44 -0
  54. package/scripts/consumer-postinstall.cjs +76 -21
  55. package/scripts/framework-menu-advanced-view-lib.ts +0 -49
  56. package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
  57. package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
  58. package/scripts/framework-menu-consumer-preflight-render.ts +0 -10
  59. package/scripts/framework-menu-consumer-preflight-run.ts +0 -23
  60. package/scripts/framework-menu-consumer-preflight-types.ts +0 -12
  61. package/scripts/framework-menu-consumer-runtime-actions.ts +87 -17
  62. package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
  63. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
  64. package/scripts/framework-menu-consumer-runtime-lib.ts +2 -38
  65. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -31
  66. package/scripts/framework-menu-consumer-runtime-types.ts +3 -5
  67. package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
  68. package/scripts/framework-menu-evidence-summary-read.ts +57 -5
  69. package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
  70. package/scripts/framework-menu-evidence-summary-types.ts +7 -0
  71. package/scripts/framework-menu-gate-lib.ts +9 -0
  72. package/scripts/framework-menu-layout-data.ts +5 -0
  73. package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
  74. package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
  75. package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
  76. package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
  77. package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
  78. package/scripts/framework-menu-system-notifications-cause.ts +0 -3
  79. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +24 -204
  80. package/scripts/framework-menu-system-notifications-macos.ts +4 -0
  81. package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
  82. package/scripts/framework-menu-system-notifications-text.ts +1 -7
  83. package/scripts/framework-menu.ts +3 -24
  84. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -10
  85. package/scripts/package-install-smoke-consumer-npm-lib.ts +9 -46
  86. package/scripts/pumuki-full-surface-smoke-lib.ts +37 -0
  87. package/scripts/pumuki-full-surface-smoke.ts +346 -0
  88. package/scripts/pumuki-smoke-installed-wrapper.cjs +31 -0
  89. package/integrations/evidence/trackingContract.ts +0 -17
  90. package/integrations/gate/governanceActionCatalog.ts +0 -275
  91. package/integrations/lifecycle/bootstrapManifest.ts +0 -248
  92. package/integrations/lifecycle/cliGovernanceConsole.ts +0 -69
  93. package/integrations/lifecycle/governanceNextAction.ts +0 -171
  94. package/integrations/lifecycle/governanceObservationSnapshot.ts +0 -369
  95. package/integrations/lifecycle/trackingState.ts +0 -403
  96. package/integrations/mcp/alignedPlatformGate.ts +0 -232
  97. package/integrations/mcp/readMcpPrePushStdin.ts +0 -7
  98. package/scripts/build-ruralgo-s1-evidence-pack.ts +0 -85
  99. package/scripts/ruralgo-s1-evidence-pack-lib.ts +0 -200
package/CHANGELOG.md CHANGED
@@ -6,13 +6,59 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [6.3.113] - 2026-04-23
9
+ ## [6.3.114] - 2026-04-24
10
10
 
11
11
  ### Fixed
12
12
 
13
- - **Contrato bloqueado canónico en `status` / `doctor`:** cuando el runtime está bloqueado, la salida expone `issues` accionables en vez de dejar el diagnóstico implícito.
14
- - **`governanceNextAction.stage` alineado con la evidencia:** el stage visible deja de forzarse y respeta `evidence.snapshot_stage` cuando existe.
15
- - **Remediación reproducible respecto al runtime diagnosticado:** la guía de alineación deja de apuntar a `pumuki@latest` y pasa a resolver contra el runtime efectivo del consumer.
13
+ - **`status` y `doctor` exponen `issues` canónicos también en evidencia `WARN`:** la línea publicada deja de reservar la lista de findings a estados bloqueados y pasa a emitir una advertencia consumible por automatización cuando governance está en atención operativa real.
14
+ - **Hotfix mínimo sobre la superficie estable de `main`:** el contrato bloqueado existente se conserva, pero ahora la evidencia `WARN` produce `Governance requires attention (...)` como issue canónico sin arrastrar snapshots adicionales de `develop`.
15
+ - **Cobertura de regresión de `INC-084` en la línea publicada:** nuevas pruebas fijan el caso `WARN` tanto en `status` como en `doctor`, manteniendo la semántica previa para estados `BLOCK`.
16
+
17
+ ## [6.3.108] - 2026-04-22
18
+
19
+ ### Fixed
20
+
21
+ - **MCP enterprise visible por defecto en la línea publicada:** `mcp_enterprise` deja de nacer en `off`, así que el editor/agente puede ver `ai_gate_check`, `pre_flight_check` y `auto_execute_ai_start` sin opt-in adicional.
22
+ - **Enforcement temprano más perceptible para `PRE_WRITE`:** el catálogo enterprise visible por defecto reduce el gap entre `status`/`doctor` bloqueados y la ruta real de trabajo del agente/editor.
23
+ - **Cobertura de regresión de la baseline MCP:** nuevas pruebas fijan el catálogo del enterprise server y la proyección de baseline consumer asociada a `mcp_enterprise`.
24
+
25
+ ## [6.3.107] - 2026-04-22
26
+
27
+ ### Fixed
28
+
29
+ - **Semántica inequívoca para sesión SDD expirada:** una sesión vencida deja de proyectarse como `active=true` y pasa a exponerse como inactiva (`active=false`) manteniendo `valid=false` y `remainingSeconds=0`.
30
+ - **Refresh de sesión expiradas todavía permitido:** `refreshSddSession` ya no exige `active=true`; basta con conservar el `changeId` para poder renovar una sesión caducada sin reabrirla manualmente.
31
+ - **Policy SDD alineada con esa semántica:** `evaluateSddPolicy` trata la sesión como `missing` solo cuando falta `changeId`, y conserva `SDD_SESSION_INVALID` para sesiones expiradas con contexto recuperable.
32
+
33
+ ## [6.3.106] - 2026-04-22
34
+
35
+ ### Fixed
36
+
37
+ - **Activación advisory de SDD/PRE_WRITE fijada al runtime diagnosticado:** `sdd validate` deja de devolver `activation_command` con `pumuki@latest` cuando el namespace experimental está desactivado por defecto.
38
+ - **Session guidance reproducible en SDD:** las instrucciones de `session --refresh` y `session --open` también quedan fijadas a la versión efectiva del runtime en lugar de depender de `latest`.
39
+
40
+ ## [6.3.105] - 2026-04-22
41
+
42
+ ### Fixed
43
+
44
+ - **Remediaciones PRE_WRITE fijadas a la versión diagnosticada:** `sdd validate`, `auto_execute_ai_start` y la remediación por defecto dejan de recomendar `pumuki@latest` y pasan a devolver comandos con la versión efectiva del runtime (`pumuki@6.3.105` en esta línea).
45
+ - **Backport útil de `PUMUKI-INC-089` a la línea publicada:** `main` mantiene la ruta reproducible para `install`, `policy reconcile --strict --json` y la revalidación `PRE_WRITE` sin exigir al consumer adivinar la versión correcta.
46
+
47
+ ## [6.3.104] - 2026-04-22
48
+
49
+ ### Fixed
50
+
51
+ - **Tracking canónico de RuralGo reconocido por el parser de repo-policy:** `appendTrackingActionableContext` ya inspecciona `docs/RURALGO_SEGUIMIENTO.md`, que es la ruta canónica real del consumer.
52
+ - **Filas `| 🚧 | TASK |` tratadas como entradas activas válidas:** el diagnóstico accionable cubre el formato de tabla usado por el hub de seguimiento de RuralGo además del backlog tabular de incidencias.
53
+ - **Cobertura de regresión para el hub canónico:** nuevas pruebas fijan parsing y priorización de `docs/RURALGO_SEGUIMIENTO.md` antes de otros archivos de tracking del consumer.
54
+
55
+ ## [6.3.103] - 2026-04-22
56
+
57
+ ### Fixed
58
+
59
+ - **Diagnóstico accionable del tracking canónico en consumers:** `status`, `doctor` y el gate repo-policy ya incluyen `TRACKING_CANONICAL_IN_PROGRESS_INVALID` junto con referencias a entradas activas y al board canónico del repo consumidor cuando existe.
60
+ - **Separación explícita entre blocker y warning secundario:** la salida de `PRE_WRITE` conserva un `block-summary` primario y añade `warning-summary` para warnings de higiene (`EVIDENCE_PREWRITE_WORKTREE_WARN`) cuando conviven con un bloqueo duro de tracking.
61
+ - **Cobertura de regresión del hotfix:** nuevas pruebas fijan el parsing de boards tabulares `🚧 reported activo` y la impresión del resumen jerárquico con warning secundario.
16
62
 
17
63
  ## [6.3.102] - 2026-04-22
18
64
 
@@ -41,7 +87,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
41
87
  ### Fixed
42
88
 
43
89
  - **PRE_WRITE visible y coherente en la línea de producción:** `policyValidationSnapshot` refleja `PRE_WRITE` como estricto cuando el enforcement efectivo está activado en `strict`, evitando contradicción entre policy y runtime.
44
- - **Arranque agentic sin éxito falso:** `auto_execute_ai_start` devuelve semántica de fallo real cuando el gate bloquea y fuerza remediación explícita antes de continuar.
90
+ - **Arranque agentic sin éxito falso:** `auto_execute_ai_start` devuelve `success=false` cuando el gate bloquea y fuerza remediación explícita antes de continuar.
45
91
  - **Contrato MCP actualizado:** la superficie HTTP del enterprise server hereda ese mismo contrato de bloqueo para `auto_execute_ai_start`.
46
92
  - **Cobertura de regresión del hotfix:** nuevas regresiones fijan la proyección de `PRE_WRITE` y el comportamiento bloqueante del arranque agentic sobre la línea `main`.
47
93
 
package/README.md CHANGED
@@ -32,7 +32,7 @@ Elige un perfil y profundiza en los enlaces; **no** repite aquí reglas largas (
32
32
 
33
33
  | Perfil | Qué instalar / arrancar | Stages habituales | Opcional típico |
34
34
  |--------|-------------------------|-------------------|-----------------|
35
- | **Mínimo** | `npm install --save-exact pumuki` (en repos Git el `postinstall` puede ejecutar `pumuki install` para hooks y lifecycle). | Hooks Git: **PRE_COMMIT**, **PRE_PUSH**; cadena **PRE_WRITE** cuando el hook lo encadena (según versión y config). | Evidencia [`.ai_evidence.json` v2.1](docs/mcp/ai-evidence-v2.1-contract.md); reglas core embebidas. |
35
+ | **Mínimo** | `npm install --save-exact pumuki` (en repos Git el `postinstall` ejecuta baseline `pumuki install`; `--with-mcp --agent=repo` es opt-in con `PUMUKI_POSTINSTALL_WITH_MCP=1` o `PUMUKI_POSTINSTALL_MCP_AGENT=repo`). | Hooks Git: **PRE_COMMIT**, **PRE_PUSH**; cadena **PRE_WRITE** cuando el hook lo encadena (según versión y config). | Evidencia [`.ai_evidence.json` v2.1](docs/mcp/ai-evidence-v2.1-contract.md); reglas core embebidas. |
36
36
  | **Estándar** | Lo anterior + flujo **OpenSpec/SDD** bajo `openspec/` según tu política. | Lo anterior + validación SDD por stage (`pumuki sdd validate --stage=…`). | Sesiones SDD, cambios versionados bajo `openspec/changes/`. |
37
37
  | **Enterprise completo** | `pumuki bootstrap --enterprise` (o equivalente documentado) + `skills.lock.json` / reglas custom / [policy-as-code](docs/product/CONFIGURATION.md) donde aplique. | Lo anterior + **CI** (`pumuki-ci`) y comprobaciones de alineación (`doctor`, parity). | [Skills / MCP](docs/mcp/mcp-servers-overview.md), `pumuki doctor --parity`, notificaciones, [hard mode](docs/product/CONFIGURATION.md). |
38
38
 
@@ -50,6 +50,8 @@ Cinco entradas que cubren el 80 % del día a día en un consumidor; el detalle e
50
50
  4. **Gates locales** — `npx pumuki-pre-write`, `npx pumuki-pre-commit` (y `pumuki-pre-push` cuando toque push). Detalle: [USAGE](docs/product/USAGE.md), [Troubleshooting (USAGE)](docs/product/USAGE.md#troubleshooting).
51
51
  5. **SDD por stage (enterprise)** — `npx pumuki sdd validate --stage=PRE_COMMIT` (u otro stage). Detalle: [USAGE](docs/product/USAGE.md), [INSTALLATION](docs/product/INSTALLATION.md#troubleshooting) si falla el bootstrap.
52
52
 
53
+ **Desarrollo en este repo (sin depender de GitHub Actions):** barra mínima antes de merge o publicar — `npm run -s validation:local-merge-bar` (`typecheck` + smoke de superficie CLI + `npm test`). Detalle del smoke: [docs/validation/README.md](docs/validation/README.md).
54
+
53
55
  Si algo bloquea o el mensaje no es claro: [Troubleshooting](#troubleshooting) (más abajo en este README), [USAGE § Troubleshooting](docs/product/USAGE.md#troubleshooting) y [GitHub Issues](https://github.com/SwiftEnProfundidad/ast-intelligence-hooks/issues).
54
56
 
55
57
  ## 5-Minute Quick Start (Consumer)
@@ -69,7 +71,7 @@ npx --yes pumuki status
69
71
  npx --yes pumuki doctor --json
70
72
  ```
71
73
 
72
- Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta **`pumuki install` solo** (hooks `pre-commit` / `pre-push`, lifecycle, evidencia cuando aplica). **Pumuki no depende de ningún IDE** para el baseline: no toca `.cursor/` ni otros ficheros de editor por defecto. Desde **6.3.68**, cada hook gestionado ejecuta **`pumuki-pre-write` antes** de `pumuki-pre-commit` / `pumuki-pre-push` (stage **PRE_WRITE** vía Git). Saltar solo PRE_WRITE en hooks: `PUMUKI_SKIP_CHAINED_PRE_WRITE=1`. Desde **6.3.69**, esos mismos hooks aplican también **git-flow en ramas protegidas** (`GITFLOW_PROTECTED_BRANCH`) e **higiene de worktree** (`PUMUKI_PREWRITE_WORKTREE_*` / códigos `EVIDENCE_PREWRITE_WORKTREE_*`) cuando la evidencia es válida; el **modal macOS** de bloqueo (Desactivar / Silenciar 30 min / Mantener activas) queda **activo por defecto** si las notificaciones están habilitadas (`"blockedDialogEnabled": false` o `PUMUKI_MACOS_BLOCKED_DIALOG=0` para apagarlo). Tras install, si no existía, aparece **`.pumuki/adapter.json`** con los comandos de hooks y **MCP stdio** para referencia o para clientes que los registren manualmente. Para generar también config de IDE: **`pumuki install --with-mcp --agent=cursor`** (u otro) o **`pumuki bootstrap --enterprise --agent=…`**. Desactivar el postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. En CI suele saltarse solo (`CI=true`). En **6.3.64+**, las notificaciones del sistema en plataformas sin banner nativo se reflejan en **stderr** por defecto (`PUMUKI_DISABLE_STDERR_NOTIFICATIONS=1` para silenciarlas). En **6.3.69+**, un `gate.blocked` en macOS también deja una copia en **stderr** por defecto (`PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1` para desactivar solo eso). Desde **6.3.70**, si **`.ai_evidence.json` está versionado** y **PRE_PUSH** no bloquea, ese archivo **no se reescribe** en el push (compatibilidad con **pre-commit** como hook de **pre-push**); para forzar escritura: `PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE=1`. Con modal de bloqueo activo, el panel interactivo prioriza foco/clics y se evita el banner duplicado.
74
+ Desde **6.3.63**, `npm install` en la raíz de un repo **Git** dispara un `postinstall` que ejecuta baseline **`pumuki install`** (hooks `pre-commit` / `pre-push`, lifecycle, merge de **`.pumuki/adapter.json`** con comandos MCP stdio, sin rutas de IDE salvo opt-in). El wiring MCP no va activado por defecto; para activarlo en postinstall usa `PUMUKI_POSTINSTALL_WITH_MCP=1` o `PUMUKI_POSTINSTALL_MCP_AGENT=repo/cursor/claude/codex`. **Pumuki no depende de ningún IDE** para el baseline. Opt-out del postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. Ficheros de IDE en postinstall: `PUMUKI_POSTINSTALL_MCP_AGENT=cursor|claude|codex` o comando manual / `bootstrap`. Desde **6.3.68**, cada hook gestionado ejecuta **`pumuki-pre-write` antes** de `pumuki-pre-commit` / `pumuki-pre-push` (stage **PRE_WRITE** vía Git). Saltar solo PRE_WRITE en hooks: `PUMUKI_SKIP_CHAINED_PRE_WRITE=1`. Desde **6.3.69**, esos mismos hooks aplican también **git-flow en ramas protegidas** (`GITFLOW_PROTECTED_BRANCH`) e **higiene de worktree** (`PUMUKI_PREWRITE_WORKTREE_*` / códigos `EVIDENCE_PREWRITE_WORKTREE_*`) cuando la evidencia es válida; el **modal macOS** de bloqueo (Desactivar / Silenciar 30 min / Mantener activas) queda **activo por defecto** si las notificaciones están habilitadas (`"blockedDialogEnabled": false` o `PUMUKI_MACOS_BLOCKED_DIALOG=0` para apagarlo). Desactivar el postinstall: `PUMUKI_SKIP_POSTINSTALL=1`. En CI suele saltarse solo (`CI=true`). En **6.3.64+**, las notificaciones del sistema en plataformas sin banner nativo se reflejan en **stderr** por defecto (`PUMUKI_DISABLE_STDERR_NOTIFICATIONS=1` para silenciarlas). En **6.3.69+**, un `gate.blocked` en macOS también deja una copia en **stderr** por defecto (`PUMUKI_DISABLE_GATE_BLOCKED_STDERR_MIRROR=1` para desactivar solo eso). Desde **6.3.70**, si **`.ai_evidence.json` está versionado** y **PRE_PUSH** no bloquea, ese archivo **no se reescribe** en el push (compatibilidad con **pre-commit** como hook de **pre-push**); para forzar escritura: `PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE=1`. Con modal de bloqueo activo, el panel interactivo prioriza foco/clics y se evita el banner duplicado.
73
75
 
74
76
  Fallback (equivalent in pasos separados):
75
77
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.113
1
+ v6.3.114
@@ -13,12 +13,6 @@ import {
13
13
  findUndefinedInBaseTypeUnionLines,
14
14
  findUnknownWithoutGuardLines,
15
15
  findUnknownTypeAssertionLines,
16
- findAnemicDomainModelLines,
17
- findControllerBusinessLogicLines,
18
- findMagicNumberLiteralLines,
19
- findProductionMockArtifactUsageLines,
20
- hasAnemicDomainModel,
21
- hasControllerBusinessLogic,
22
16
  hasAsyncPromiseExecutor,
23
17
  hasConcreteDependencyInstantiation,
24
18
  hasConsoleErrorCall,
@@ -30,8 +24,6 @@ import {
30
24
  hasExplicitAnyType,
31
25
  hasFrameworkDependencyImport,
32
26
  hasFunctionConstructorUsage,
33
- hasMagicNumberLiteral,
34
- hasProductionMockArtifactUsage,
35
27
  hasNetworkCallWithoutErrorHandling,
36
28
  hasMixedCommandQueryClass,
37
29
  hasMixedCommandQueryInterface,
@@ -892,227 +884,6 @@ test('hasLargeClassDeclaration detecta clases con 300 lineas o mas', () => {
892
884
  assert.equal(hasLargeClassDeclaration(compactClassAst), false);
893
885
  });
894
886
 
895
- test('hasMagicNumberLiteral detecta literales numericos repetidos en contexto ejecutable y descarta declarativos', () => {
896
- const magicAst = {
897
- type: 'Program',
898
- body: [
899
- {
900
- type: 'ExpressionStatement',
901
- expression: {
902
- type: 'CallExpression',
903
- callee: { type: 'Identifier', name: 'retry' },
904
- arguments: [{ type: 'NumericLiteral', value: 42, loc: { start: { line: 3 }, end: { line: 3 } } }],
905
- },
906
- },
907
- {
908
- type: 'VariableDeclaration',
909
- kind: 'const',
910
- declarations: [
911
- {
912
- type: 'VariableDeclarator',
913
- id: { type: 'Identifier', name: 'timeoutMs' },
914
- init: { type: 'NumericLiteral', value: 42, loc: { start: { line: 5 }, end: { line: 5 } } },
915
- },
916
- ],
917
- },
918
- {
919
- type: 'ExpressionStatement',
920
- expression: {
921
- type: 'BinaryExpression',
922
- operator: '>',
923
- left: { type: 'Identifier', name: 'elapsedMs' },
924
- right: { type: 'NumericLiteral', value: 42, loc: { start: { line: 7 }, end: { line: 7 } } },
925
- },
926
- },
927
- {
928
- type: 'ReturnStatement',
929
- argument: { type: 'NumericLiteral', value: 1, loc: { start: { line: 9 }, end: { line: 9 } } },
930
- },
931
- ],
932
- };
933
-
934
- const ignoredAst = {
935
- type: 'Program',
936
- body: [
937
- {
938
- type: 'VariableDeclaration',
939
- kind: 'const',
940
- declarations: [
941
- {
942
- type: 'VariableDeclarator',
943
- id: { type: 'Identifier', name: 'port' },
944
- init: { type: 'NumericLiteral', value: 3000, loc: { start: { line: 2 }, end: { line: 2 } } },
945
- },
946
- ],
947
- },
948
- {
949
- type: 'ReturnStatement',
950
- argument: { type: 'NumericLiteral', value: 1, loc: { start: { line: 4 }, end: { line: 4 } } },
951
- },
952
- ],
953
- };
954
-
955
- assert.equal(hasMagicNumberLiteral(magicAst), true);
956
- assert.deepEqual(findMagicNumberLiteralLines(magicAst), [3, 7]);
957
- assert.equal(hasMagicNumberLiteral(ignoredAst), false);
958
- assert.deepEqual(findMagicNumberLiteralLines(ignoredAst), []);
959
- });
960
-
961
- test('hasProductionMockArtifactUsage detecta imports/requires de doubles en runtime productivo', () => {
962
- const importAst = {
963
- type: 'ImportDeclaration',
964
- source: { type: 'StringLiteral', value: '../mocks/user-repository', loc: { start: { line: 3 }, end: { line: 3 } } },
965
- specifiers: [],
966
- loc: { start: { line: 3 }, end: { line: 3 } },
967
- };
968
- const requireAst = {
969
- type: 'CallExpression',
970
- callee: { type: 'Identifier', name: 'require' },
971
- arguments: [{ type: 'StringLiteral', value: 'sinon', loc: { start: { line: 7 }, end: { line: 7 } } }],
972
- loc: { start: { line: 7 }, end: { line: 7 } },
973
- };
974
- const cleanAst = {
975
- type: 'Program',
976
- body: [
977
- {
978
- type: 'ImportDeclaration',
979
- source: { type: 'StringLiteral', value: '../adapters/user-repository', loc: { start: { line: 1 }, end: { line: 1 } } },
980
- specifiers: [],
981
- },
982
- ],
983
- };
984
- const mixedAst = {
985
- type: 'Program',
986
- body: [importAst, { type: 'ExpressionStatement', expression: requireAst }],
987
- };
988
-
989
- assert.equal(hasProductionMockArtifactUsage(importAst), true);
990
- assert.equal(hasProductionMockArtifactUsage(requireAst), true);
991
- assert.equal(hasProductionMockArtifactUsage(cleanAst), false);
992
- assert.deepEqual(findProductionMockArtifactUsageLines(mixedAst), [3, 7]);
993
- });
994
-
995
- test('hasAnemicDomainModel detecta clases de dominio con solo accessors y sin comportamiento', () => {
996
- const anemicAst = {
997
- type: 'ClassDeclaration',
998
- id: { type: 'Identifier', name: 'OrderEntity' },
999
- body: {
1000
- type: 'ClassBody',
1001
- body: [
1002
- { type: 'ClassMethod', kind: 'constructor', key: { type: 'Identifier', name: 'constructor' }, loc: { start: { line: 3 }, end: { line: 3 } } },
1003
- { type: 'ClassMethod', key: { type: 'Identifier', name: 'getStatus' }, loc: { start: { line: 5 }, end: { line: 5 } } },
1004
- { type: 'ClassMethod', key: { type: 'Identifier', name: 'setStatus' }, loc: { start: { line: 7 }, end: { line: 7 } } },
1005
- ],
1006
- },
1007
- loc: { start: { line: 1 }, end: { line: 9 } },
1008
- };
1009
- const richAst = {
1010
- type: 'ClassDeclaration',
1011
- id: { type: 'Identifier', name: 'OrderEntity' },
1012
- body: {
1013
- type: 'ClassBody',
1014
- body: [
1015
- { type: 'ClassMethod', kind: 'constructor', key: { type: 'Identifier', name: 'constructor' }, loc: { start: { line: 3 }, end: { line: 3 } } },
1016
- { type: 'ClassMethod', key: { type: 'Identifier', name: 'getStatus' }, loc: { start: { line: 5 }, end: { line: 5 } } },
1017
- { type: 'ClassMethod', key: { type: 'Identifier', name: 'confirm' }, loc: { start: { line: 7 }, end: { line: 7 } } },
1018
- ],
1019
- },
1020
- loc: { start: { line: 1 }, end: { line: 9 } },
1021
- };
1022
- const serviceAst = {
1023
- type: 'ClassDeclaration',
1024
- id: { type: 'Identifier', name: 'OrderService' },
1025
- body: {
1026
- type: 'ClassBody',
1027
- body: [
1028
- { type: 'ClassMethod', kind: 'constructor', key: { type: 'Identifier', name: 'constructor' }, loc: { start: { line: 3 }, end: { line: 3 } } },
1029
- { type: 'ClassMethod', key: { type: 'Identifier', name: 'getStatus' }, loc: { start: { line: 5 }, end: { line: 5 } } },
1030
- { type: 'ClassMethod', key: { type: 'Identifier', name: 'setStatus' }, loc: { start: { line: 7 }, end: { line: 7 } } },
1031
- ],
1032
- },
1033
- loc: { start: { line: 1 }, end: { line: 9 } },
1034
- };
1035
-
1036
- assert.equal(hasAnemicDomainModel(anemicAst), true);
1037
- assert.deepEqual(findAnemicDomainModelLines(anemicAst), [1]);
1038
- assert.equal(hasAnemicDomainModel(richAst), false);
1039
- assert.equal(hasAnemicDomainModel(serviceAst), false);
1040
- });
1041
-
1042
- test('hasControllerBusinessLogic detecta flow control dentro de handlers en clases Controller', () => {
1043
- const controllerAst = {
1044
- type: 'ClassDeclaration',
1045
- id: { type: 'Identifier', name: 'OrdersController' },
1046
- body: {
1047
- type: 'ClassBody',
1048
- body: [
1049
- {
1050
- type: 'ClassMethod',
1051
- key: { type: 'Identifier', name: 'createOrder' },
1052
- decorators: [{ expression: { type: 'Identifier', name: 'Post' } }],
1053
- body: {
1054
- type: 'BlockStatement',
1055
- body: [
1056
- {
1057
- type: 'VariableDeclaration',
1058
- declarations: [{ type: 'VariableDeclarator', id: { type: 'Identifier', name: 'status' } }],
1059
- },
1060
- {
1061
- type: 'IfStatement',
1062
- test: { type: 'Identifier', name: 'isPriority' },
1063
- consequent: { type: 'BlockStatement', body: [] },
1064
- },
1065
- ],
1066
- },
1067
- loc: { start: { line: 4 }, end: { line: 10 } },
1068
- },
1069
- ],
1070
- },
1071
- loc: { start: { line: 1 }, end: { line: 12 } },
1072
- };
1073
- const delegatedControllerAst = {
1074
- type: 'ClassDeclaration',
1075
- id: { type: 'Identifier', name: 'OrdersController' },
1076
- body: {
1077
- type: 'ClassBody',
1078
- body: [
1079
- {
1080
- type: 'ClassMethod',
1081
- key: { type: 'Identifier', name: 'createOrder' },
1082
- decorators: [{ expression: { type: 'Identifier', name: 'Post' } }],
1083
- body: {
1084
- type: 'BlockStatement',
1085
- body: [
1086
- {
1087
- type: 'ReturnStatement',
1088
- argument: {
1089
- type: 'CallExpression',
1090
- callee: {
1091
- type: 'MemberExpression',
1092
- object: {
1093
- type: 'MemberExpression',
1094
- object: { type: 'ThisExpression' },
1095
- property: { type: 'Identifier', name: 'ordersService' },
1096
- },
1097
- property: { type: 'Identifier', name: 'create' },
1098
- },
1099
- arguments: [],
1100
- },
1101
- },
1102
- ],
1103
- },
1104
- loc: { start: { line: 4 }, end: { line: 8 } },
1105
- },
1106
- ],
1107
- },
1108
- loc: { start: { line: 1 }, end: { line: 10 } },
1109
- };
1110
-
1111
- assert.equal(hasControllerBusinessLogic(controllerAst), true);
1112
- assert.deepEqual(findControllerBusinessLogicLines(controllerAst), [1]);
1113
- assert.equal(hasControllerBusinessLogic(delegatedControllerAst), false);
1114
- });
1115
-
1116
887
  test('hasRecordStringUnknownType detecta Record<string, unknown>', () => {
1117
888
  const recordUnknownAst = {
1118
889
  type: 'TSTypeReference',
@@ -21,13 +21,6 @@ const concreteDependencyNames = new Set<string>([
21
21
  ]);
22
22
  const GOD_CLASS_MAX_LINES = 300;
23
23
  const networkCallCalleePattern = /^(fetch|axios|get|post|put|patch|delete|request)$/i;
24
- const ignoredMagicNumberValues = new Set<number>([0, 1]);
25
- const runtimeTestDoubleLibraryPattern = /^(sinon|testdouble|ts-mockito|jest-mock|vitest)$/i;
26
- const runtimeTestDoublePathPattern = /(^|\/)(__mocks__|mocks|fakes|spies|stubs)(\/|$)|\.(mock|fake|spy|stub)$/i;
27
- const anemicDomainClassNamePattern = /(Entity|Aggregate|Model)$/i;
28
- const controllerClassNamePattern = /Controller$/i;
29
- const controllerRoutingDecoratorPattern =
30
- /^(Get|Post|Put|Patch|Delete|All|Head|Options|MessagePattern|EventPattern)$/;
31
24
  type AstNode = Record<string, string | number | boolean | bigint | symbol | null | Date | object>;
32
25
  type TypeScriptSemanticNode = {
33
26
  kind: 'class' | 'property' | 'call' | 'member';
@@ -414,101 +407,6 @@ const collectClassMethodDescriptors = (classNode: AstNode): readonly ClassMethod
414
407
  return descriptors;
415
408
  };
416
409
 
417
- const isAccessorLikeMethodName = (name: string): boolean => {
418
- return name === 'constructor' || /^(get|set)[A-Z_]/.test(name);
419
- };
420
-
421
- const isAnemicDomainClassNode = (value: AstNode): boolean => {
422
- if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
423
- return false;
424
- }
425
-
426
- const className = classNameFromNode(value);
427
- if (!anemicDomainClassNamePattern.test(className)) {
428
- return false;
429
- }
430
-
431
- const descriptors = collectClassMethodDescriptors(value);
432
- if (descriptors.length < 2) {
433
- return false;
434
- }
435
-
436
- return descriptors.every((descriptor) => isAccessorLikeMethodName(descriptor.name));
437
- };
438
-
439
- const hasDecoratorNamed = (node: unknown, decoratorName: RegExp): boolean => {
440
- if (!isObject(node) || !Array.isArray(node.decorators)) {
441
- return false;
442
- }
443
-
444
- return node.decorators.some((decorator) => {
445
- if (!isObject(decorator)) {
446
- return false;
447
- }
448
- const expression = decorator.expression;
449
- if (!isObject(expression)) {
450
- return false;
451
- }
452
- const callee = expression.type === 'CallExpression' ? expression.callee : expression;
453
- const name = methodNameFromNode(callee) ?? memberExpressionPropertyName(callee);
454
- return typeof name === 'string' && decoratorName.test(name);
455
- });
456
- };
457
-
458
- const isControllerHandlerMethodNode = (node: AstNode): boolean => {
459
- return node.type === 'ClassMethod' && node.kind !== 'constructor';
460
- };
461
-
462
- const statementCountFromMethodNode = (node: AstNode): number => {
463
- if (!isObject(node.body) || !Array.isArray(node.body.body)) {
464
- return 0;
465
- }
466
- return node.body.body.filter((statement) => isObject(statement)).length;
467
- };
468
-
469
- const hasControllerBusinessFlow = (node: AstNode): boolean => {
470
- return hasNode(node, (value) => {
471
- return (
472
- value.type === 'IfStatement' ||
473
- value.type === 'SwitchStatement' ||
474
- value.type === 'ForStatement' ||
475
- value.type === 'ForInStatement' ||
476
- value.type === 'ForOfStatement' ||
477
- value.type === 'WhileStatement' ||
478
- value.type === 'DoWhileStatement' ||
479
- value.type === 'TryStatement' ||
480
- value.type === 'ConditionalExpression'
481
- );
482
- });
483
- };
484
-
485
- const isControllerBusinessLogicMethodNode = (node: AstNode): boolean => {
486
- if (!isControllerHandlerMethodNode(node)) {
487
- return false;
488
- }
489
- if (statementCountFromMethodNode(node) < 2) {
490
- return false;
491
- }
492
- return hasControllerBusinessFlow(node);
493
- };
494
-
495
- const isControllerClassNode = (value: AstNode): boolean => {
496
- if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {
497
- return false;
498
- }
499
- return controllerClassNamePattern.test(classNameFromNode(value));
500
- };
501
-
502
- const findControllerBusinessLogicMethod = (node: AstNode): AstNode | undefined => {
503
- const classBody = node.body;
504
- if (!isObject(classBody) || !Array.isArray(classBody.body)) {
505
- return undefined;
506
- }
507
- return classBody.body.find((member): member is AstNode => {
508
- return isObject(member) && isControllerBusinessLogicMethodNode(member);
509
- });
510
- };
511
-
512
410
  const interfaceNameFromNode = (node: AstNode): string => {
513
411
  const idNode = node.id;
514
412
  if (isObject(idNode) && idNode.type === 'Identifier' && typeof idNode.name === 'string') {
@@ -1648,96 +1546,6 @@ export const hasAsyncPromiseExecutor = (node: unknown): boolean => {
1648
1546
  });
1649
1547
  };
1650
1548
 
1651
- export const hasMagicNumberLiteral = (node: unknown): boolean => {
1652
- return [...collectMagicNumberOccurrences(node).values()].some(
1653
- (occurrence) => occurrence.count >= 2
1654
- );
1655
- };
1656
-
1657
- export const findMagicNumberLiteralLines = (node: unknown): readonly number[] => {
1658
- return collectRepeatedMagicNumberLines(node);
1659
- };
1660
-
1661
- const runtimeTestDoubleSpecifier = (node: unknown): string | undefined => {
1662
- if (!isObject(node) || node.type !== 'StringLiteral' || typeof node.value !== 'string') {
1663
- return undefined;
1664
- }
1665
- return node.value;
1666
- };
1667
-
1668
- const matchesRuntimeTestDoubleImport = (specifier: string): boolean => {
1669
- return (
1670
- runtimeTestDoubleLibraryPattern.test(specifier) ||
1671
- runtimeTestDoublePathPattern.test(specifier)
1672
- );
1673
- };
1674
-
1675
- const isRuntimeTestDoubleImportNode = (value: AstNode): boolean => {
1676
- if (value.type === 'ImportDeclaration') {
1677
- const specifier = runtimeTestDoubleSpecifier(value.source);
1678
- return typeof specifier === 'string' && matchesRuntimeTestDoubleImport(specifier);
1679
- }
1680
-
1681
- if (
1682
- value.type === 'CallExpression' &&
1683
- isObject(value.callee) &&
1684
- value.callee.type === 'Identifier' &&
1685
- value.callee.name === 'require' &&
1686
- Array.isArray(value.arguments)
1687
- ) {
1688
- return value.arguments.some((argument) => {
1689
- const specifier = runtimeTestDoubleSpecifier(argument);
1690
- return typeof specifier === 'string' && matchesRuntimeTestDoubleImport(specifier);
1691
- });
1692
- }
1693
-
1694
- return false;
1695
- };
1696
-
1697
- export const hasProductionMockArtifactUsage = (node: unknown): boolean => {
1698
- return hasNode(node, (value) => isRuntimeTestDoubleImportNode(value));
1699
- };
1700
-
1701
- export const findProductionMockArtifactUsageLines = (node: unknown): readonly number[] => {
1702
- return collectLineMatchesWithAncestors(node, (value) => isRuntimeTestDoubleImportNode(value), {
1703
- max: 8,
1704
- });
1705
- };
1706
-
1707
- export const hasAnemicDomainModel = (node: unknown): boolean => {
1708
- return hasNode(node, (value) => isAnemicDomainClassNode(value));
1709
- };
1710
-
1711
- export const findAnemicDomainModelLines = (node: unknown): readonly number[] => {
1712
- return collectLineMatchesWithAncestors(node, (value) => isAnemicDomainClassNode(value), {
1713
- max: 8,
1714
- });
1715
- };
1716
-
1717
- export const hasControllerBusinessLogic = (node: unknown): boolean => {
1718
- return hasNode(node, (value) => {
1719
- if (!isControllerClassNode(value)) {
1720
- return false;
1721
- }
1722
- return Boolean(findControllerBusinessLogicMethod(value));
1723
- });
1724
- };
1725
-
1726
- export const findControllerBusinessLogicLines = (node: unknown): readonly number[] => {
1727
- return collectLineMatchesWithAncestors(
1728
- node,
1729
- (value) => {
1730
- if (!isControllerClassNode(value)) {
1731
- return false;
1732
- }
1733
- return Boolean(findControllerBusinessLogicMethod(value));
1734
- },
1735
- {
1736
- max: 8,
1737
- }
1738
- );
1739
- };
1740
-
1741
1549
  export const hasWithStatement = (node: unknown): boolean => {
1742
1550
  return hasNode(node, (value) => value.type === 'WithStatement');
1743
1551
  };
@@ -1951,92 +1759,6 @@ const nodeLineSpan = (node: unknown): number => {
1951
1759
  return Math.max(0, end - start + 1);
1952
1760
  };
1953
1761
 
1954
- type MagicNumberOccurrence = {
1955
- count: number;
1956
- lines: number[];
1957
- };
1958
-
1959
- const magicNumberValueFromNode = (node: unknown): number | undefined => {
1960
- if (!isObject(node) || node.type !== 'NumericLiteral' || typeof node.value !== 'number') {
1961
- return undefined;
1962
- }
1963
- if (!Number.isFinite(node.value) || ignoredMagicNumberValues.has(node.value)) {
1964
- return undefined;
1965
- }
1966
- return node.value;
1967
- };
1968
-
1969
- const isExecutableMagicNumberContext = (
1970
- value: AstNode,
1971
- ancestors: ReadonlyArray<AstNode>
1972
- ): boolean => {
1973
- const parent = ancestors[ancestors.length - 1];
1974
- if (!isObject(parent)) {
1975
- return false;
1976
- }
1977
- if (parent.type === 'BinaryExpression') {
1978
- return parent.left === value || parent.right === value;
1979
- }
1980
- if (parent.type === 'CallExpression' || parent.type === 'NewExpression') {
1981
- return Array.isArray(parent.arguments) && parent.arguments.includes(value);
1982
- }
1983
- if (parent.type === 'AssignmentExpression') {
1984
- return parent.right === value;
1985
- }
1986
- if (parent.type === 'ReturnStatement') {
1987
- return parent.argument === value;
1988
- }
1989
- if (parent.type === 'SwitchCase') {
1990
- return parent.test === value;
1991
- }
1992
- return false;
1993
- };
1994
-
1995
- const collectMagicNumberOccurrences = (
1996
- node: unknown
1997
- ): ReadonlyMap<number, MagicNumberOccurrence> => {
1998
- const occurrences = new Map<number, MagicNumberOccurrence>();
1999
-
2000
- const walk = (value: unknown, ancestors: ReadonlyArray<AstNode>): void => {
2001
- if (!isObject(value)) {
2002
- return;
2003
- }
2004
-
2005
- const numericValue = magicNumberValueFromNode(value);
2006
- if (typeof numericValue === 'number' && isExecutableMagicNumberContext(value, ancestors)) {
2007
- const current = occurrences.get(numericValue) ?? { count: 0, lines: [] };
2008
- current.count += 1;
2009
- const line = toPositiveLine(value);
2010
- if (typeof line === 'number') {
2011
- current.lines.push(line);
2012
- }
2013
- occurrences.set(numericValue, current);
2014
- }
2015
-
2016
- const nextAncestors = [...ancestors, value];
2017
- for (const child of Object.values(value)) {
2018
- if (Array.isArray(child)) {
2019
- for (const entry of child) {
2020
- walk(entry, nextAncestors);
2021
- }
2022
- continue;
2023
- }
2024
- walk(child, nextAncestors);
2025
- }
2026
- };
2027
-
2028
- walk(node, []);
2029
- return occurrences;
2030
- };
2031
-
2032
- const collectRepeatedMagicNumberLines = (node: unknown): readonly number[] => {
2033
- return sortedUniqueLines(
2034
- [...collectMagicNumberOccurrences(node).values()]
2035
- .filter((occurrence) => occurrence.count >= 2)
2036
- .flatMap((occurrence) => occurrence.lines)
2037
- );
2038
- };
2039
-
2040
1762
  export const hasLargeClassDeclaration = (node: unknown): boolean => {
2041
1763
  return hasNode(node, (value) => {
2042
1764
  if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {