pumuki 6.3.109 → 6.3.110

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 (96) hide show
  1. package/CHANGELOG.md +4 -51
  2. package/README.md +2 -4
  3. package/VERSION +1 -1
  4. package/core/facts/detectors/typescript/index.test.ts +229 -0
  5. package/core/facts/detectors/typescript/index.ts +278 -0
  6. package/core/facts/extractHeuristicFacts.ts +4 -0
  7. package/core/rules/presets/heuristics/typescript.test.ts +21 -1
  8. package/core/rules/presets/heuristics/typescript.ts +72 -0
  9. package/docs/README.md +9 -13
  10. package/docs/codex-skills/backend-enterprise-rules.md +3 -3
  11. package/docs/operations/RELEASE_NOTES.md +4 -40
  12. package/docs/product/HOW_IT_WORKS.md +0 -6
  13. package/docs/product/INSTALLATION.md +1 -1
  14. package/docs/product/USAGE.md +4 -41
  15. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +44 -100
  16. package/docs/validation/README.md +3 -6
  17. package/integrations/config/skillsDetectorRegistry.ts +24 -0
  18. package/integrations/config/skillsMarkdownRules.ts +57 -0
  19. package/integrations/evidence/buildEvidence.ts +24 -0
  20. package/integrations/evidence/repoState.ts +3 -0
  21. package/integrations/evidence/schema.ts +18 -0
  22. package/integrations/evidence/trackingContract.ts +17 -0
  23. package/integrations/evidence/writeEvidence.ts +24 -0
  24. package/integrations/gate/evaluateAiGate.ts +251 -8
  25. package/integrations/gate/governanceActionCatalog.ts +275 -0
  26. package/integrations/gate/remediationCatalog.ts +8 -0
  27. package/integrations/git/GitService.ts +5 -44
  28. package/integrations/git/aiGateRepoPolicyFindings.ts +17 -86
  29. package/integrations/git/runPlatformGate.ts +9 -1
  30. package/integrations/git/runPlatformGateFacts.ts +1 -19
  31. package/integrations/git/runPlatformGateOutput.ts +42 -41
  32. package/integrations/lifecycle/adapter.templates.json +0 -1
  33. package/integrations/lifecycle/adapter.ts +24 -0
  34. package/integrations/lifecycle/bootstrapManifest.ts +248 -0
  35. package/integrations/lifecycle/cli.ts +99 -120
  36. package/integrations/lifecycle/cliGovernanceConsole.ts +69 -0
  37. package/integrations/lifecycle/cliSdd.ts +26 -4
  38. package/integrations/lifecycle/doctor.ts +111 -17
  39. package/integrations/lifecycle/governanceNextAction.ts +171 -0
  40. package/integrations/lifecycle/governanceObservationSnapshot.ts +379 -0
  41. package/integrations/lifecycle/index.ts +0 -2
  42. package/integrations/lifecycle/install.ts +21 -0
  43. package/integrations/lifecycle/state.ts +8 -1
  44. package/integrations/lifecycle/status.ts +57 -2
  45. package/integrations/lifecycle/trackingState.ts +392 -0
  46. package/integrations/mcp/aiGateCheck.ts +194 -10
  47. package/integrations/mcp/alignedPlatformGate.ts +232 -0
  48. package/integrations/mcp/autoExecuteAiStart.ts +92 -116
  49. package/integrations/mcp/enterpriseServer.ts +23 -7
  50. package/integrations/mcp/enterpriseStdioServer.cli.ts +31 -4
  51. package/integrations/mcp/preFlightCheck.ts +67 -5
  52. package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
  53. package/integrations/platform/detectPlatforms.ts +0 -37
  54. package/integrations/sdd/policy.ts +20 -28
  55. package/package.json +1 -1
  56. package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
  57. package/scripts/check-tracking-single-active.sh +1 -1
  58. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +38 -13
  59. package/scripts/consumer-postinstall.cjs +21 -76
  60. package/scripts/framework-menu-advanced-view-lib.ts +49 -0
  61. package/scripts/framework-menu-consumer-actions-lib.ts +4 -28
  62. package/scripts/framework-menu-consumer-preflight-hints.ts +2 -5
  63. package/scripts/framework-menu-consumer-preflight-render.ts +10 -0
  64. package/scripts/framework-menu-consumer-preflight-run.ts +23 -0
  65. package/scripts/framework-menu-consumer-preflight-types.ts +12 -0
  66. package/scripts/framework-menu-consumer-runtime-actions.ts +17 -87
  67. package/scripts/framework-menu-consumer-runtime-audit.ts +2 -36
  68. package/scripts/framework-menu-consumer-runtime-lib.ts +38 -2
  69. package/scripts/framework-menu-consumer-runtime-menu.ts +31 -4
  70. package/scripts/framework-menu-consumer-runtime-types.ts +5 -3
  71. package/scripts/framework-menu-evidence-summary-lib.ts +0 -1
  72. package/scripts/framework-menu-evidence-summary-read.ts +5 -57
  73. package/scripts/framework-menu-evidence-summary-severity.ts +1 -3
  74. package/scripts/framework-menu-evidence-summary-types.ts +0 -7
  75. package/scripts/framework-menu-gate-lib.ts +0 -9
  76. package/scripts/framework-menu-layout-data.ts +0 -5
  77. package/scripts/framework-menu-matrix-baseline-lib.ts +14 -15
  78. package/scripts/framework-menu-matrix-canary-lib.ts +1 -22
  79. package/scripts/framework-menu-matrix-evidence-lib.ts +0 -1
  80. package/scripts/framework-menu-matrix-evidence-types.ts +1 -13
  81. package/scripts/framework-menu-matrix-runner-lib.ts +0 -35
  82. package/scripts/framework-menu-system-notifications-cause.ts +3 -0
  83. package/scripts/framework-menu-system-notifications-macos-swift-source.ts +204 -24
  84. package/scripts/framework-menu-system-notifications-macos.ts +0 -4
  85. package/scripts/framework-menu-system-notifications-payloads-blocked.ts +1 -1
  86. package/scripts/framework-menu-system-notifications-text.ts +7 -1
  87. package/scripts/framework-menu.ts +24 -3
  88. package/scripts/package-install-smoke-consumer-git-repo-lib.ts +10 -1
  89. package/scripts/package-install-smoke-consumer-npm-lib.ts +46 -9
  90. package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
  91. package/integrations/lifecycle/audit.ts +0 -101
  92. package/scripts/consumer-postinstall-resolve-args.cjs +0 -44
  93. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +0 -140
  94. package/scripts/pumuki-full-surface-smoke-lib.ts +0 -37
  95. package/scripts/pumuki-full-surface-smoke.ts +0 -346
  96. package/scripts/pumuki-smoke-installed-wrapper.cjs +0 -31
package/CHANGELOG.md CHANGED
@@ -6,59 +6,12 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [6.3.109] - 2026-04-22
9
+ ## [6.3.110] - 2026-04-23
10
10
 
11
11
  ### Fixed
12
12
 
13
- - **`install` materializa policy estricta cuando el repo ya puede reconciliarla:** tras una instalación limpia, Pumuki intenta persistir `.pumuki/policy-as-code.json` con `strict=true` por stage en lugar de dejar `status` y `doctor` en `computed-local`.
14
- - **Convergencia de `status`/`doctor` tras install en consumers reales:** el runtime deja de depender de `PUMUKI_POLICY_STRICT` para que `PRE_COMMIT`, `PRE_PUSH` y `CI` reflejen el mismo contrato estricto que `PRE_WRITE`.
15
- - **Cobertura de regresión del ciclo de install:** nuevas pruebas fijan que `runLifecycleInstall` materializa el contrato firmado cuando `AGENTS.md` y `skills.lock.json` exponen los insumos mínimos.
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.
13
+ - **`status` expone `issues` canónicos cuando el runtime está bloqueado:** la salida JSON ya no obliga a combinar `governanceEffective`, `attention_codes` y `human_summary_preview` para entender un bloqueo real.
14
+ - **Contrato alineado en `main` y `develop`:** la lista de issues consumible queda fijada en ambas líneas para los consumers que leen `status` como fuente principal de diagnóstico.
62
15
 
63
16
  ## [6.3.102] - 2026-04-22
64
17
 
@@ -87,7 +40,7 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
87
40
  ### Fixed
88
41
 
89
42
  - **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.
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.
43
+ - **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.
91
44
  - **Contrato MCP actualizado:** la superficie HTTP del enterprise server hereda ese mismo contrato de bloqueo para `auto_execute_ai_start`.
92
45
  - **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`.
93
46
 
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` 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. |
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. |
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,8 +50,6 @@ 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
-
55
53
  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).
56
54
 
57
55
  ## 5-Minute Quick Start (Consumer)
@@ -71,7 +69,7 @@ npx --yes pumuki status
71
69
  npx --yes pumuki doctor --json
72
70
  ```
73
71
 
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.
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.
75
73
 
76
74
  Fallback (equivalent in pasos separados):
77
75
 
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.109
1
+ v6.3.102
@@ -13,6 +13,12 @@ import {
13
13
  findUndefinedInBaseTypeUnionLines,
14
14
  findUnknownWithoutGuardLines,
15
15
  findUnknownTypeAssertionLines,
16
+ findAnemicDomainModelLines,
17
+ findControllerBusinessLogicLines,
18
+ findMagicNumberLiteralLines,
19
+ findProductionMockArtifactUsageLines,
20
+ hasAnemicDomainModel,
21
+ hasControllerBusinessLogic,
16
22
  hasAsyncPromiseExecutor,
17
23
  hasConcreteDependencyInstantiation,
18
24
  hasConsoleErrorCall,
@@ -24,6 +30,8 @@ import {
24
30
  hasExplicitAnyType,
25
31
  hasFrameworkDependencyImport,
26
32
  hasFunctionConstructorUsage,
33
+ hasMagicNumberLiteral,
34
+ hasProductionMockArtifactUsage,
27
35
  hasNetworkCallWithoutErrorHandling,
28
36
  hasMixedCommandQueryClass,
29
37
  hasMixedCommandQueryInterface,
@@ -884,6 +892,227 @@ test('hasLargeClassDeclaration detecta clases con 300 lineas o mas', () => {
884
892
  assert.equal(hasLargeClassDeclaration(compactClassAst), false);
885
893
  });
886
894
 
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
+
887
1116
  test('hasRecordStringUnknownType detecta Record<string, unknown>', () => {
888
1117
  const recordUnknownAst = {
889
1118
  type: 'TSTypeReference',
@@ -21,6 +21,13 @@ 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)$/;
24
31
  type AstNode = Record<string, string | number | boolean | bigint | symbol | null | Date | object>;
25
32
  type TypeScriptSemanticNode = {
26
33
  kind: 'class' | 'property' | 'call' | 'member';
@@ -407,6 +414,101 @@ const collectClassMethodDescriptors = (classNode: AstNode): readonly ClassMethod
407
414
  return descriptors;
408
415
  };
409
416
 
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
+
410
512
  const interfaceNameFromNode = (node: AstNode): string => {
411
513
  const idNode = node.id;
412
514
  if (isObject(idNode) && idNode.type === 'Identifier' && typeof idNode.name === 'string') {
@@ -1546,6 +1648,96 @@ export const hasAsyncPromiseExecutor = (node: unknown): boolean => {
1546
1648
  });
1547
1649
  };
1548
1650
 
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
+
1549
1741
  export const hasWithStatement = (node: unknown): boolean => {
1550
1742
  return hasNode(node, (value) => value.type === 'WithStatement');
1551
1743
  };
@@ -1759,6 +1951,92 @@ const nodeLineSpan = (node: unknown): number => {
1759
1951
  return Math.max(0, end - start + 1);
1760
1952
  };
1761
1953
 
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
+
1762
2040
  export const hasLargeClassDeclaration = (node: unknown): boolean => {
1763
2041
  return hasNode(node, (value) => {
1764
2042
  if (value.type !== 'ClassDeclaration' && value.type !== 'ClassExpression') {