@imolko/ultra-agent 0.1.0-beta

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 (239) hide show
  1. package/README.md +93 -0
  2. package/bin/cli.js +2 -0
  3. package/dist-node/bin.d.ts +2 -0
  4. package/dist-node/bin.d.ts.map +1 -0
  5. package/dist-node/bin.js +5 -0
  6. package/dist-node/bin.js.map +1 -0
  7. package/dist-node/cli.d.ts +41 -0
  8. package/dist-node/cli.d.ts.map +1 -0
  9. package/dist-node/cli.js +170 -0
  10. package/dist-node/cli.js.map +1 -0
  11. package/dist-node/index.d.ts +14 -0
  12. package/dist-node/index.d.ts.map +1 -0
  13. package/dist-node/index.js +40 -0
  14. package/dist-node/index.js.map +1 -0
  15. package/dist-node/init-lint.d.ts +43 -0
  16. package/dist-node/init-lint.d.ts.map +1 -0
  17. package/dist-node/init-lint.js +84 -0
  18. package/dist-node/init-lint.js.map +1 -0
  19. package/dist-node/manifest.d.ts +33 -0
  20. package/dist-node/manifest.d.ts.map +1 -0
  21. package/dist-node/manifest.js +59 -0
  22. package/dist-node/manifest.js.map +1 -0
  23. package/dist-node/materializer.d.ts +52 -0
  24. package/dist-node/materializer.d.ts.map +1 -0
  25. package/dist-node/materializer.js +194 -0
  26. package/dist-node/materializer.js.map +1 -0
  27. package/dist-node/package-root.d.ts +5 -0
  28. package/dist-node/package-root.d.ts.map +1 -0
  29. package/dist-node/package-root.js +17 -0
  30. package/dist-node/package-root.js.map +1 -0
  31. package/dist-node/payload.d.ts +22 -0
  32. package/dist-node/payload.d.ts.map +1 -0
  33. package/dist-node/payload.js +49 -0
  34. package/dist-node/payload.js.map +1 -0
  35. package/dist-node/pointer-hub.d.ts +22 -0
  36. package/dist-node/pointer-hub.d.ts.map +1 -0
  37. package/dist-node/pointer-hub.js +50 -0
  38. package/dist-node/pointer-hub.js.map +1 -0
  39. package/dist-node/publication.cli.d.ts +9 -0
  40. package/dist-node/publication.cli.d.ts.map +1 -0
  41. package/dist-node/publication.cli.js +17 -0
  42. package/dist-node/publication.cli.js.map +1 -0
  43. package/dist-node/publication.d.ts +23 -0
  44. package/dist-node/publication.d.ts.map +1 -0
  45. package/dist-node/publication.js +47 -0
  46. package/dist-node/publication.js.map +1 -0
  47. package/dist-node/reconcile.cli.d.ts +11 -0
  48. package/dist-node/reconcile.cli.d.ts.map +1 -0
  49. package/dist-node/reconcile.cli.js +39 -0
  50. package/dist-node/reconcile.cli.js.map +1 -0
  51. package/dist-node/reconcile.d.ts +30 -0
  52. package/dist-node/reconcile.d.ts.map +1 -0
  53. package/dist-node/reconcile.js +51 -0
  54. package/dist-node/reconcile.js.map +1 -0
  55. package/dist-node/ship/prompts/fix.md +22 -0
  56. package/dist-node/ship/prompts/implement.md +34 -0
  57. package/dist-node/ship/prompts/review.md +28 -0
  58. package/dist-node/ship/ship.agent.d.ts +22 -0
  59. package/dist-node/ship/ship.agent.d.ts.map +1 -0
  60. package/dist-node/ship/ship.agent.js +142 -0
  61. package/dist-node/ship/ship.agent.js.map +1 -0
  62. package/dist-node/ship/ship.args.d.ts +15 -0
  63. package/dist-node/ship/ship.args.d.ts.map +1 -0
  64. package/dist-node/ship/ship.args.js +93 -0
  65. package/dist-node/ship/ship.args.js.map +1 -0
  66. package/dist-node/ship/ship.context.d.ts +12 -0
  67. package/dist-node/ship/ship.context.d.ts.map +1 -0
  68. package/dist-node/ship/ship.context.js +3 -0
  69. package/dist-node/ship/ship.context.js.map +1 -0
  70. package/dist-node/ship/ship.d.ts +4 -0
  71. package/dist-node/ship/ship.d.ts.map +1 -0
  72. package/dist-node/ship/ship.js +221 -0
  73. package/dist-node/ship/ship.js.map +1 -0
  74. package/dist-node/ship/ship.mr.d.ts +9 -0
  75. package/dist-node/ship/ship.mr.d.ts.map +1 -0
  76. package/dist-node/ship/ship.mr.js +171 -0
  77. package/dist-node/ship/ship.mr.js.map +1 -0
  78. package/dist-node/ship/ship.output.d.ts +9 -0
  79. package/dist-node/ship/ship.output.d.ts.map +1 -0
  80. package/dist-node/ship/ship.output.js +21 -0
  81. package/dist-node/ship/ship.output.js.map +1 -0
  82. package/dist-node/ship/ship.tracker.d.ts +37 -0
  83. package/dist-node/ship/ship.tracker.d.ts.map +1 -0
  84. package/dist-node/ship/ship.tracker.js +220 -0
  85. package/dist-node/ship/ship.tracker.js.map +1 -0
  86. package/dist-node/version.cli.d.ts +9 -0
  87. package/dist-node/version.cli.d.ts.map +1 -0
  88. package/dist-node/version.cli.js +30 -0
  89. package/dist-node/version.cli.js.map +1 -0
  90. package/dist-node/version.d.ts +24 -0
  91. package/dist-node/version.d.ts.map +1 -0
  92. package/dist-node/version.js +76 -0
  93. package/dist-node/version.js.map +1 -0
  94. package/docs/adr/ADR-0000-proceso-y-formato-de-adrs.md +115 -0
  95. package/docs/adr/foundational/A-001-clean-architecture-4-capas.md +88 -0
  96. package/docs/adr/foundational/A-002-cqrs-semantico.md +58 -0
  97. package/docs/adr/foundational/A-003-tdd.md +97 -0
  98. package/docs/adr/foundational/A-005-either-return-custom-methods.md +80 -0
  99. package/docs/adr/foundational/A-006-feature-completion-criteria.md +47 -0
  100. package/docs/adr/foundational/A-007-data-contract-fail-fast.md +109 -0
  101. package/docs/adr/foundational/A-008-bounded-context-decomposition.md +130 -0
  102. package/docs/adr/foundational/A-009-use-case-taxonomy.md +99 -0
  103. package/docs/adr/foundational/A-010-propiedad-emision-eventos-dominio.md +95 -0
  104. package/docs/adr/foundational/A-011-characterization-first-refactoring.md +90 -0
  105. package/docs/adr/foundational/A-012-helm-mecanismo-primario-despliegue.md +67 -0
  106. package/docs/adr/foundational/A-013-contrato-settings-unica-fuente-configuracion.md +74 -0
  107. package/docs/adr/foundational/A-014-generacion-artefactos-despliegue-desde-contrato-settings.md +75 -0
  108. package/docs/adr/foundational/A-015-cloudevents-estandar-eventos.md +85 -0
  109. package/docs/adr/foundational/A-016-cicd-gitlab-pipeline-raiz-pipelines-modulares.md +92 -0
  110. package/docs/adr/foundational/A-017-taxonomia-repositorios-librerias-sistemas.md +93 -0
  111. package/docs/adr/foundational/A-018-practicas-build-imagenes-docker.md +85 -0
  112. package/docs/adr/foundational/A-019-convenciones-autoria-helm-charts.md +97 -0
  113. package/docs/adr/foundational/A-020-entorno-desarrollo-estandarizado-devcontainer.md +58 -0
  114. package/docs/adr/foundational/A-022-formato-reporte-tests-ctrf.md +55 -0
  115. package/docs/adr/foundational/A-023-composicion-bloques-prompt-agentes.md +65 -0
  116. package/docs/adr/foundational/A-024-features-slices-verticales-portables.md +41 -0
  117. package/docs/adr/foundational/A-025-anatomia-mecanica-feature-puertos-adaptadores-drivers.md +104 -0
  118. package/docs/adr/foundational/A-026-infraestructura-compartida.md +44 -0
  119. package/docs/adr/foundational/A-027-patron-wiring-entry-point.md +67 -0
  120. package/docs/adr/foundational/A-029-verbos-intencion-metodos-lado-negocio.md +54 -0
  121. package/docs/adr/foundational/A-030-global-development-tooling.md +52 -0
  122. package/docs/adr/foundational/A-031-ubiquitous-language.md +64 -0
  123. package/docs/adr/foundational/A-032-domain-artifact-documentation-format.md +128 -0
  124. package/docs/adr/foundational/A-033-estandar-mensajes-commit.md +127 -0
  125. package/docs/adr/foundational/A-034-canonical-skill-set.md +64 -0
  126. package/docs/adr/foundational/A-035-captura-ideas-reanclaje-candies.md +51 -0
  127. package/docs/adr/foundational/A-036-adopcion-wayfinder-ortogonalidad-labels.md +37 -0
  128. package/docs/adr/foundational/A-037-formalizacion-taxonomia-labels.md +98 -0
  129. package/docs/adr/foundational/A-038-issues-referencian-skills-nombre.md +54 -0
  130. package/docs/adr/foundational/A-039-pipeline-ship-issues-hijos-spec.md +64 -0
  131. package/docs/adr/foundational/A-040-punteros-configuracion-repo-skills.md +44 -0
  132. package/docs/adr/foundational/A-041-estrategia-de-ramas.md +45 -0
  133. package/docs/adr/foundational/A-042-formato-documentos-issue-tracker.md +78 -0
  134. package/docs/adr/foundational/A-043-vocabulario-etiquetas-adrs.md +42 -0
  135. package/docs/adr/foundational/A-044-composicion-funcional-procesos-operaciones.md +44 -0
  136. package/docs/adr/foundational/A-045-guardrails-aplicables-no-directrices.md +48 -0
  137. package/docs/adr/foundational/A-046-metricas-de-forma-y-legibilidad-del-codigo.md +50 -0
  138. package/docs/adr/foundational/A-047-dependencias-explicitas-sin-globales-ambientales.md +40 -0
  139. package/docs/adr/foundational/A-048-cobertura-minima-codigo.md +39 -0
  140. package/docs/adr/foundational/A-049-materializacion-bucle-guardrails-extension-nativa-pi.md +49 -0
  141. package/docs/adr/foundational/A-050-el-caso-de-uso-pide-al-dominio-no-lee-su-representacion.md +50 -0
  142. package/docs/adr/foundational/A-051-orden-imports-por-distancia.md +47 -0
  143. package/docs/adr/foundational/A-052-distribucion-metodologia-paquete-versionado.md +42 -0
  144. package/docs/adr/foundational/A-053-errores-compartidos-entre-features-dos-hogares-por-capa.md +43 -0
  145. package/docs/adr/foundational/A-054-contrato-costura-driver-port-raw-data-canal-error.md +65 -0
  146. package/docs/adr/foundational/index.md +98 -0
  147. package/docs/adr/index.md +19 -0
  148. package/docs/adr/python/P-001-dependencias-por-feature-fastapi-depends.md +63 -0
  149. package/docs/adr/python/P-002-inyeccion-token-por-request-sin-persistirlo.md +35 -0
  150. package/docs/adr/python/P-003-sqlalchemy-async-connection-pooling.md +59 -0
  151. package/docs/adr/python/P-004-serializer-json-custom-modelos-pydantic.md +47 -0
  152. package/docs/adr/python/P-005-patrones-pitfalls-nicegui.md +148 -0
  153. package/docs/adr/python/P-006-convenciones-pytest.md +96 -0
  154. package/docs/adr/python/P-007-ci-python-alineacion-poetry-version-unica.md +55 -0
  155. package/docs/adr/python/P-008-empaquetado-infraestructura-compartida-por-tecnologia.md +38 -0
  156. package/docs/adr/python/index.md +14 -0
  157. package/docs/adr/typescript/T-001-ddd-artifact-validation-zod-primitives.md +136 -0
  158. package/docs/adr/typescript/T-002-test-documentation-jsdoc.md +133 -0
  159. package/docs/adr/typescript/T-003-readme-formatting-domain-artifacts.md +51 -0
  160. package/docs/adr/typescript/T-005-input-optionality-use-case-not-entity.md +130 -0
  161. package/docs/adr/typescript/T-006-small-enums-inlined-zod-regex.md +77 -0
  162. package/docs/adr/typescript/T-007-controller-input-zod-schema.md +79 -0
  163. package/docs/adr/typescript/T-008-expressive-use-case-pipe.md +207 -0
  164. package/docs/adr/typescript/T-009-mdx-brace-escaping.md +80 -0
  165. package/docs/adr/typescript/T-010-stack-tooling-typescript-dominio.md +53 -0
  166. package/docs/adr/typescript/T-011-ultra-reporter-herramienta-canonica-documentacion.md +52 -0
  167. package/docs/adr/typescript/T-012-npm-workspaces-estructura-monorepo-clean-architecture.md +54 -0
  168. package/docs/adr/typescript/T-013-definicion-eventos-versionados-ultra-ddd.md +45 -0
  169. package/docs/adr/typescript/T-014-publicacion-distribucion-librerias-npm-semver.md +53 -0
  170. package/docs/adr/typescript/T-015-method-placement-definition.md +35 -0
  171. package/docs/adr/typescript/T-016-contrato-settings-zod.md +34 -0
  172. package/docs/adr/typescript/T-017-characterization-tests-golden-files.md +39 -0
  173. package/docs/adr/typescript/T-018-procedencia-campos-frontmatter.md +55 -0
  174. package/docs/adr/typescript/T-019-ultra-generator-herramienta-canonica-generacion.md +35 -0
  175. package/docs/adr/typescript/T-020-ultra-settings-herramienta-canonica-exportacion-despliegue.md +36 -0
  176. package/docs/adr/typescript/T-021-pipe-flujo-datos-operadores-semantica-contexto-inmutable.md +105 -0
  177. package/docs/adr/typescript/T-022-construction-is-execution-single-boundary.md +112 -0
  178. package/docs/adr/typescript/T-023-registry-cli-declara-dependencias-reales.md +84 -0
  179. package/docs/adr/typescript/T-024-paquete-ultra-agent-manifiesto-pi-peers-cli.md +39 -0
  180. package/docs/adr/typescript/T-025-publicacion-npm-trusted-publishing-procedencia.md +59 -0
  181. package/docs/adr/typescript/index.md +30 -0
  182. package/eslint/index.mjs +721 -0
  183. package/package.json +94 -0
  184. package/pi/extensions/handoff/README.md +92 -0
  185. package/pi/extensions/handoff/handoff-prompt.txt +9 -0
  186. package/pi/extensions/handoff/index.ts +296 -0
  187. package/pi/extensions/handoff/package.json +12 -0
  188. package/pi/extensions/qa-guardrail/README.md +108 -0
  189. package/pi/extensions/qa-guardrail/adapters/cli.ts +100 -0
  190. package/pi/extensions/qa-guardrail/adapters/eslint.ts +90 -0
  191. package/pi/extensions/qa-guardrail/adapters/registry.ts +45 -0
  192. package/pi/extensions/qa-guardrail/baseline.ts +74 -0
  193. package/pi/extensions/qa-guardrail/cerco.ts +134 -0
  194. package/pi/extensions/qa-guardrail/check.ts +185 -0
  195. package/pi/extensions/qa-guardrail/config.ts +189 -0
  196. package/pi/extensions/qa-guardrail/delta.ts +52 -0
  197. package/pi/extensions/qa-guardrail/feedback.ts +26 -0
  198. package/pi/extensions/qa-guardrail/index.ts +311 -0
  199. package/pi/extensions/qa-guardrail/nstop.ts +82 -0
  200. package/pi/extensions/qa-guardrail/package.json +12 -0
  201. package/pi/extensions/qa-guardrail/selection.ts +98 -0
  202. package/pi/extensions/qa-guardrail/types.ts +64 -0
  203. package/scripts/copy-assets.sh +10 -0
  204. package/scripts/reconcile-tags.sh +94 -0
  205. package/scripts/release-beta.sh +64 -0
  206. package/scripts/release-build.sh +53 -0
  207. package/scripts/release.sh +51 -0
  208. package/scripts/verify-adr-consistency.py +217 -0
  209. package/skills/code-review/SKILL.md +66 -0
  210. package/skills/codebase-design/SKILL.md +46 -0
  211. package/skills/configure-repo/SKILL.md +33 -0
  212. package/skills/diagnose-bug/SKILL.md +63 -0
  213. package/skills/domain-modeling/SKILL.md +56 -0
  214. package/skills/grill-me/SKILL.md +34 -0
  215. package/skills/i-have-an-idea/SKILL.md +166 -0
  216. package/skills/implement/SKILL.md +22 -0
  217. package/skills/research/SKILL.md +17 -0
  218. package/skills/ship/SKILL.md +26 -0
  219. package/skills/spike/SKILL.md +27 -0
  220. package/skills/tdd/SKILL.md +68 -0
  221. package/skills/tdd/references/deep-modules.md +53 -0
  222. package/skills/tdd/references/interface-design.md +74 -0
  223. package/skills/tdd/references/mocking.md +80 -0
  224. package/skills/tdd/references/refactoring.md +49 -0
  225. package/skills/tdd/references/tests.md +184 -0
  226. package/skills/to-specs/SKILL.md +141 -0
  227. package/skills/to-tasks/SKILL.md +143 -0
  228. package/skills/triage/SKILL.md +73 -0
  229. package/skills/ultra-generator/SKILL.md +263 -0
  230. package/skills/ultra-generator/references/aggregate-root.md +149 -0
  231. package/skills/ultra-generator/references/domain-event.md +174 -0
  232. package/skills/ultra-generator/references/entity.md +133 -0
  233. package/skills/ultra-generator/references/service.md +92 -0
  234. package/skills/ultra-generator/references/value-object.md +135 -0
  235. package/skills/wayfinder/SKILL.md +201 -0
  236. package/skills/workflow/SKILL.md +31 -0
  237. package/skills/writing-for-agents/SKILL-MECHANICS.md +32 -0
  238. package/skills/writing-for-agents/SKILL.md +86 -0
  239. package/skills/writing-for-agents/references/adrs.md +19 -0
@@ -0,0 +1,59 @@
1
+ # P-003: SQLAlchemy async con connection pooling
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-08-13
6
+
7
+ **originally**: cortex-018
8
+
9
+ **Enriquecido desde**: Cortex ADR 018 — SQLAlchemy async session with connection pooling
10
+
11
+ ## Contexto
12
+
13
+ Una API en Python necesita acceso asíncrono a base de datos para varias funciones: estado de sesión, seguimiento de procesos, búsqueda vectorial y CRUD. Sin una capa compartida de sesión y pool, cada feature crearía su propio engine y agotaría las conexiones.
14
+
15
+ ## Decisión
16
+
17
+ Centralizar el acceso a base de datos en un **servicio de datos asíncrono** único, creado una sola vez en el arranque:
18
+
19
+ ```python
20
+ class AsyncSqlAlchemyService:
21
+ def __init__(self, database_url: str):
22
+ self._engine = create_async_engine(
23
+ database_url,
24
+ pool_pre_ping=True,
25
+ pool_size=20,
26
+ max_overflow=10,
27
+ )
28
+ self._session_factory = async_sessionmaker(
29
+ bind=self._engine,
30
+ expire_on_commit=False,
31
+ )
32
+
33
+ def session(self) -> AsyncSession:
34
+ return self._session_factory()
35
+ ```
36
+
37
+ ### Decisiones de diseño
38
+
39
+ - **Driver `asyncpg`**: acceso asíncrono a PostgreSQL (`postgresql+asyncpg://`).
40
+ - **Connection pooling**: `pool_size` y `max_overflow` acotan el máximo de conexiones concurrentes.
41
+ - **`pool_pre_ping=True`**: valida la conexión antes de usarla, para evitar conexiones obsoletas tras un reinicio de la base.
42
+ - **`expire_on_commit=False`**: los objetos siguen siendo utilizables tras el commit, evitando errores de instancia desacoplada.
43
+ - **Inyección por constructor**: la URL de base de datos se construye desde el contrato de settings en el arranque.
44
+ - **Creación de tablas con `create_all`**: temporal para el MVP; producción usará migraciones Alembic.
45
+
46
+ ## Consecuencias
47
+
48
+ ### Positivas
49
+
50
+ - **Pool único** para todo el acceso a datos: uso eficiente de conexiones.
51
+ - **Sesiones acotadas por operación** mediante la factory `session()`.
52
+ - **Creación automática de tablas** que simplifica el desarrollo del MVP.
53
+
54
+ ### Negativas
55
+
56
+ - **`create_all` no es seguro en producción**: arranques concurrentes pueden competir en la creación de tablas.
57
+ - **Sin historial de migraciones** hasta adoptar Alembic: los cambios de esquema requieren conciliación manual.
58
+ - **Tamaño de pool fijo**: sin escalado dinámico según la carga.
59
+ - **`expire_on_commit=False`**: se cambia riesgo de datos obsoletos por conveniencia de desarrollo.
@@ -0,0 +1,47 @@
1
+ # P-004: Serializer JSON custom para modelos Pydantic
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-08-13
6
+
7
+ **originally**: cortex-019
8
+
9
+ **Enriquecido desde**: Cortex ADR 019 — MemorySaver with custom JSON serializer for Pydantic models
10
+
11
+ ## Contexto
12
+
13
+ Un checkpointer (estado de sesión) serializa el estado a JSON. Ese estado contiene modelos Pydantic (esquemas de salida, entidades de dominio) que no se serializan de forma nativa con `json.dumps`. Sin un serializer custom, el checkpointing falla al serializar modelos.
14
+
15
+ ## Decisión
16
+
17
+ Usar un **serializer JSON custom consciente de Pydantic**, registrado en el checkpointer:
18
+
19
+ ```python
20
+ def memory_factory() -> BaseCheckpointSaver[str]:
21
+ serde = JsonSerializerFromCustomModels(
22
+ valid_namespaces=["app"]
23
+ )
24
+ return MemorySaver(serde=serde)
25
+ ```
26
+
27
+ ### Decisiones de diseño
28
+
29
+ - **Ámbito por namespace**: `valid_namespaces` limita la serialización custom a las clases del namespace de la aplicación; el resto usa la serialización JSON por defecto.
30
+ - **Función factory**: `memory_factory()` crea una instancia nueva en cada llamada, evitando estado compartido entre tests.
31
+ - **Consciente de Pydantic**: el serializer usa `.model_dump()` / `.model_validate()` para los modelos del namespace declarado.
32
+
33
+ El serializer se inyecta en la cadena de proveedores de dependencias ([P-001](./P-001-dependencias-por-feature-fastapi-depends.md)) y queda disponible para todos los grafos de la aplicación.
34
+
35
+ ## Consecuencias
36
+
37
+ ### Positivas
38
+
39
+ - **Modelos Pydantic arbitrarios** en el estado, sin serialización manual.
40
+ - **Checkpointing transparente**: los agentes no saben que existe la serialización.
41
+ - **Ámbito por namespace** que evita serializar de forma custom objetos que no corresponden.
42
+
43
+ ### Negativas
44
+
45
+ - **Estado en memoria**: el checkpointer en memoria pierde todo el estado al reiniciar el proceso.
46
+ - **Sincronización**: el serializer debe mantenerse al día con los cambios de los modelos de la aplicación.
47
+ - **Namespace hardcodeado**: agregar un namespace nuevo de modelos obliga a actualizar el serializer.
@@ -0,0 +1,148 @@
1
+ # P-005: Patrones y pitfalls de NiceGUI
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-08-13
6
+
7
+ **originally**: cortex-026
8
+
9
+ **Enriquecido desde**: Cortex ADR 026 — NiceGUI UI element patterns and pitfalls
10
+
11
+ ## Contexto
12
+
13
+ Una UI en Python construida con NiceGUI necesita renderizar tablas de datos densas y manejar eventos de interacción (clic en una fila, apertura de un detalle). Durante el desarrollo de vistas con muchas filas surgieron problemas recurrentes que no son evidentes en la documentación de NiceGUI: tablas que se renderizan en blanco, handlers de eventos que no se invocan y gráficos que colapsan a altura cero.
14
+
15
+ ## Decisión
16
+
17
+ Fijar patrones explícitos para el render de tablas, el manejo de eventos y el ciclo de vida de diálogos.
18
+
19
+ ### 1. Render de tablas: ancho fijo, nunca `flex`
20
+
21
+ En las definiciones de columnas de tablas basadas en aggrid, **no usar la propiedad `flex`**: provoca que el cuerpo de la tabla se renderice en blanco (las filas existen en el DOM pero son invisibles). Usar siempre un `width` explícito por columna; el contenedor se encarga del scroll horizontal.
22
+
23
+ ```python
24
+ # INCORRECTO — la tabla se renderiza en blanco
25
+ ui.aggrid({
26
+ "columnDefs": [
27
+ {"field": "nombre", "headerName": "Nombre", "width": 200},
28
+ {"field": "descripcion", "headerName": "Descripción", "flex": 2}, # ← rompe la tabla
29
+ ],
30
+ "rowData": rows,
31
+ })
32
+
33
+ # CORRECTO
34
+ ui.aggrid({
35
+ "columnDefs": [
36
+ {"field": "nombre", "headerName": "Nombre", "width": 200},
37
+ {"field": "descripcion", "headerName": "Descripción", "width": 300},
38
+ ],
39
+ "rowData": rows,
40
+ "defaultColDef": {"sortable": True, "resizable": True},
41
+ }).classes("w-full")
42
+ ```
43
+
44
+ ### 2. Manejo de eventos: no apoyarse en los handlers del grid
45
+
46
+ Los handlers Python de eventos del grid (`.on("rowClicked", ...)`, `.on("cellClicked", ...)`, `.on("selectionChanged", ...)`) no reciben de forma confiable los eventos JavaScript del grid: el bridging entre Vue y los handlers Python es inconsistente y los handlers pueden no invocarse nunca.
47
+
48
+ Para interacción (clic para ver detalle, expandir, seleccionar) usar uno de estos patrones confiables:
49
+
50
+ #### Patrón A: grid de tarjetas con botones explícitos (preferido para ≤50 items)
51
+
52
+ ```python
53
+ with ui.row().classes("gap-3 flex-wrap w-full"):
54
+ for item in items:
55
+ with ui.card().classes("p-3 w-64"):
56
+ ui.label(item["titulo"]).classes("text-sm font-bold")
57
+ ui.button(
58
+ "Ver detalle",
59
+ on_click=lambda item=item: _mostrar_detalle(item),
60
+ ).props("flat dense size=sm")
61
+ ```
62
+
63
+ Cada tarjeta tiene su propio `on_click` con una referencia capturada al item. Sin bridging de eventos, sin polling, sin condiciones de carrera.
64
+
65
+ #### Patrón B: grid solo de lectura, acciones fuera
66
+
67
+ Usar el grid exclusivamente como tabla ordenable y filtrable de solo lectura; mover las acciones interactivas a controles externos.
68
+
69
+ ```python
70
+ grid = ui.aggrid({
71
+ "columnDefs": [...],
72
+ "rowData": items,
73
+ "defaultColDef": {"sortable": True, "resizable": True},
74
+ }).classes("w-full")
75
+
76
+ ui.button("Exportar CSV", on_click=exportar)
77
+ ```
78
+
79
+ #### Patrón C: tabla HTML simple para datasets pequeños
80
+
81
+ ```python
82
+ columns = [
83
+ {"name": "titulo", "label": "Título", "field": "titulo"},
84
+ {"name": "puntaje", "label": "Puntaje", "field": "puntaje"},
85
+ ]
86
+ ui.table(columns=columns, rows=items, row_key="id").classes("w-full")
87
+ ```
88
+
89
+ ### 3. Dimensiones explícitas en gráficos
90
+
91
+ Los gráficos (ECharts vía `ui.echart()`) pueden colapsar a altura cero si el contenedor no tiene dimensiones explícitas. Fijar siempre `.classes("w-full")` y una altura en `.style("height: Npx")`:
92
+
93
+ ```python
94
+ ui.echart({...}).classes("w-full mb-3").style("height: 240px")
95
+ ```
96
+
97
+ ### 4. Contenedores intermedios para contenido dinámico
98
+
99
+ NiceGUI puede perder elementos hijos al re-renderizar cuando se agregan directamente a una card o diálogo sin un contenedor intermedio. Envolver siempre el contenido dinámico en `ui.column()` o `ui.row()`:
100
+
101
+ ```python
102
+ with ui.dialog() as dialog, ui.card():
103
+ with ui.column().classes("gap-3"):
104
+ ui.label("Título")
105
+ # ... contenido dinámico
106
+ ```
107
+
108
+ ### 5. Ciclo de vida de diálogos: capturar la referencia de inmediato
109
+
110
+ Un diálogo creado dentro de un handler puede no ser accesible para cerrarlo programáticamente si se pierde la referencia. Capturar el diálogo en una variable del ámbito que lo encierra y abrirlo después de agregar todo el contenido:
111
+
112
+ ```python
113
+ def _mostrar_modal(data: dict) -> None:
114
+ with ui.dialog() as dialog, ui.card():
115
+ ui.label(data["titulo"])
116
+ ui.button("Cerrar", on_click=dialog.close)
117
+ dialog.open()
118
+ ```
119
+
120
+ ### 6. Callbacks de timer: se soportan funciones async
121
+
122
+ `ui.timer(intervalo, callback)` soporta callbacks `async def`: NiceGUI los agenda como tarea asyncio y `await` funciona dentro del callback:
123
+
124
+ ```python
125
+ async def _consultar_estado() -> None:
126
+ resultado = await ui.run_javascript("...")
127
+ if resultado:
128
+ actualizar_ui(resultado)
129
+
130
+ ui.timer(1.0, _consultar_estado)
131
+ ```
132
+
133
+ `ui.run_javascript()` retorna una respuesta awaitable que debe consumirse de inmediato; no almacenarla ni consumirla fuera del callback.
134
+
135
+ ## Consecuencias
136
+
137
+ ### Positivas
138
+
139
+ - **Tablas predecibles**: ancho fijo en todas las columnas evita el render en blanco.
140
+ - **Interacción confiable**: las vistas de detalle usan el patrón de tarjetas con botón, sin depender de eventos del grid.
141
+ - **Gráficos estables**: dimensiones explícitas evitan el colapso a altura cero.
142
+ - **Onboarding más rápido**: los problemas conocidos quedan documentados en lugar de depender de ensayo y error con los internals del framework.
143
+
144
+ ### Negativas
145
+
146
+ - **Grid limitado a solo lectura**: la interacción por fila no usa los handlers nativos del grid.
147
+ - **Convenciones extra**: hay que mantener la disciplina de contenedores intermedios y dimensiones explícitas.
148
+ - **Acoplamiento al framework**: estos patrones dependen del comportamiento actual de NiceGUI y pueden quedar obsoletos si el framework cambia.
@@ -0,0 +1,96 @@
1
+ # P-006: Convenciones de pytest
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-08-13
6
+
7
+ **originally**: cortex-testing-with-python
8
+
9
+ **Enriquecido desde**: Cortex doc "testing-with-python" — convenciones de pytest
10
+
11
+ ## Contexto
12
+
13
+ Los tests en Python usan pytest. Sin convenciones compartidas, cada feature decide de forma distinta cómo marcar tests costosos, cómo listar tests sin ejecutarlos, cómo ver la salida de consola y cómo re-ejecutar tests al cambiar archivos.
14
+
15
+ ## Decisión
16
+
17
+ Fijar las convenciones de pytest.
18
+
19
+ ### 1. Markers para tests que dependen de servicios externos
20
+
21
+ Los tests que se conectan a un proveedor externo (por ejemplo, la evaluación de un LLM) consumen tiempo y dinero. Se marcan con un marker propio para poder incluirlos o excluirlos a discreción:
22
+
23
+ ```python
24
+ @pytest.mark.llm_evaluation
25
+ def test_evalua_respuesta():
26
+ ...
27
+ ```
28
+
29
+ ```bash
30
+ # Solo los marcados
31
+ poetry run pytest -m llm_evaluation
32
+
33
+ # Todos menos los marcados
34
+ poetry run pytest -m "not llm_evaluation"
35
+ ```
36
+
37
+ En CI no se ejecutan los tests marcados: no evalúan el funcionamiento de la aplicación y su coste no está justificado en cada pipeline.
38
+
39
+ Los markers custom se declaran en el `pyproject.toml` de cada app:
40
+
41
+ ```toml
42
+ [tool.pytest.ini_options]
43
+ markers = [
44
+ "only: marca un test para que sea de los únicos ejecutados",
45
+ "llm_evaluation: indica un test que evalúa el desempeño de un LLM",
46
+ ]
47
+ ```
48
+
49
+ ### 2. Listar tests sin ejecutarlos
50
+
51
+ Para saber qué tests están definidos sin ejecutarlos:
52
+
53
+ ```bash
54
+ poetry run pytest --collect-only
55
+ ```
56
+
57
+ ### 3. Ver salida de consola con `--capture=no`
58
+
59
+ Por defecto pytest no muestra las salidas de consola. Para ver lo que se imprime durante la ejecución:
60
+
61
+ ```bash
62
+ poetry run pytest --capture=no
63
+ ```
64
+
65
+ ### 4. Re-ejecución automática con pytest-watcher
66
+
67
+ pytest no observa carpetas ni archivos por cambios. Para monitorear archivos y re-ejecutar tests al guardarlos se usa pytest-watcher:
68
+
69
+ ```bash
70
+ poetry run pytest-watcher <path_a_observar> <path_de_tests>
71
+ ```
72
+
73
+ Se puede combinar con markers, `--capture=no` e `--ignore`:
74
+
75
+ ```bash
76
+ poetry run pytest-watcher ./src/ ./src/ -- --capture=no -m="not llm_evaluation" --ignore=./src/rutas/excluidas
77
+ ```
78
+
79
+ ### 5. Ejecutar un archivo o directorio concreto
80
+
81
+ ```bash
82
+ poetry run pytest ./ruta/al/test --capture=no
83
+ ```
84
+
85
+ ## Consecuencias
86
+
87
+ ### Positivas
88
+
89
+ - **Coste controlado**: los tests que consumen recursos externos se ejecutan solo cuando corresponde.
90
+ - **Feedback rápido**: `--collect-only` y pytest-watcher aceleran la inspección y el ciclo de desarrollo.
91
+ - **Convenciones uniformes**: markers, captura de salida y re-ejecución se resuelven igual en todas las apps.
92
+
93
+ ### Negativas
94
+
95
+ - **Markers por app**: cada app declara sus markers en su `pyproject.toml`; hay que mantenerlos sincronizados si se comparten entre apps.
96
+ - **Herramienta adicional**: pytest-watcher es una dependencia de desarrollo más en el flujo.
@@ -0,0 +1,55 @@
1
+ # P-007: CI Python: alineación Poetry↔lockfile y fuente única de la versión de Python
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-08-13
6
+
7
+ **originally**: cortex-gitlab-ci-for-python
8
+
9
+ **Enriquecido desde**: Cortex doc "gitlab-ci-for-python" — trucos para apps Python en GitLab CI
10
+
11
+ ## Contexto
12
+
13
+ El pipeline de CI para aplicaciones Python construye imágenes y ejecuta tests. Dos valores deben coincidir entre el entorno local y el pipeline, y cuando no coinciden producen fallos oscuros:
14
+
15
+ 1. **La versión de Python** que usa el entorno local (devcontainer) frente a la que usa la imagen en CI.
16
+ 2. **La versión de Poetry** que genera el `poetry.lock` frente a la que se instala en la imagen que ejecuta `poetry install`.
17
+
18
+ ## Decisión
19
+
20
+ ### 1. Fuente única de la versión de Python
21
+
22
+ La versión de Python se define en **un único lugar**, y tanto el devcontainer como el pipeline de CI la consumen desde esa fuente.
23
+
24
+ En el entorno local, el devcontainer recibe la versión por una variable de entorno definida en el Dockerfile del devcontainer. En CI, la imagen usa la versión definida para la app. Si cada lado define la versión por su cuenta, aparecen incompatibilidades difíciles de notar porque el devcontainer se reconstruye con menos frecuencia que el pipeline (que se ejecuta en cada merge request).
25
+
26
+ La regla es: **un solo valor canónico** para la versión de Python por app, del que derivan el devcontainer y el CI. Cualquier actualización se hace en esa fuente y se propaga a ambos.
27
+
28
+ ### 2. Alineación de la versión de Poetry con el lockfile
29
+
30
+ Un error oscuro y difícil de diagnosticar ocurre cuando la versión de Poetry instalada en la imagen (`Dockerfile.poetry`) no coincide con la versión que generó `poetry.lock`.
31
+
32
+ **Síntoma**: el pipeline falla en `poetry install` con un mensaje del estilo "pyproject.toml changed significantly since poetry.lock was last generated", aunque el `poetry.lock` se acabe de regenerar y publicar.
33
+
34
+ **Causa raíz**: cada versión de Poetry usa su propio algoritmo para calcular el hash de contenido de `pyproject.toml` y lo guarda dentro del `poetry.lock`. Si la versión del Dockerfile es distinta de la que generó el lock, la versión más antigua recalcula el hash, obtiene otro valor y rechaza el archivo.
35
+
36
+ **Detección**:
37
+
38
+ - La primera línea de `poetry.lock` declara la versión que lo generó (por ejemplo `# ... @generated by Poetry 2.3.2 ...`).
39
+ - El `Dockerfile.poetry` declara `POETRY_VERSION`.
40
+ - Si no coinciden, esa es la causa.
41
+
42
+ **Solución**: alinear `POETRY_VERSION` del `Dockerfile.poetry` con la versión que generó el `poetry.lock`. Siempre que se actualice Poetry en el entorno local, verificar que el `Dockerfile.poetry` quede alineado; idealmente, extraer el valor a una variable compartida.
43
+
44
+ ## Consecuencias
45
+
46
+ ### Positivas
47
+
48
+ - **Menos fallos oscuros**: la versión de Python y la de Poetry no divergen entre local y CI.
49
+ - **Diagnóstico rápido**: el error de Poetry↔lockfile queda documentado con su causa raíz y su verificación.
50
+ - **Pipeline reproducible**: la imagen usa exactamente la versión de Python y Poetry que el proyecto declara.
51
+
52
+ ### Negativas
53
+
54
+ - **Disciplina de sincronización**: actualizar Poetry o Python exige actualizar la fuente única y los consumidores.
55
+ - **Acoplamiento al lockfile**: el `Dockerfile.poetry` debe mantenerse al día con la versión que genera el lock.
@@ -0,0 +1,38 @@
1
+ # P-008: Empaquetado de la infraestructura compartida por tecnología en Python
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-08-26
6
+
7
+ **originally**: imolko
8
+
9
+ ## Contexto
10
+
11
+ [A-026](../foundational/A-026-infraestructura-compartida.md) fija la infraestructura compartida como un paquete por tecnología (`shared-infra/mongo/`, `shared-infra/postgres/`, …): cada paquete declara solo las dependencias de su tecnología y contiene los drivers que implementan los driver ports de las features. En Python, un paquete es una distribución (con su `pyproject.toml`) con un nombre de importación distinto; PEP 503 normaliza `-` y `_` en los nombres de distribución, así que la dualidad distribución↔importación es estándar (`scikit-learn` → `sklearn`).
12
+
13
+ Si la infraestructura se empaqueta como una única distribución, instalar cualquier parte arrastra las dependencias de todas las tecnologías — pymongo y psycopg a la vez — el mismo problema de cierre que [A-026](../foundational/A-026-infraestructura-compartida.md) resuelve a nivel de paquete. Falta decidir cómo se distribuye e importa cada paquete por tecnología en Python.
14
+
15
+ ## Decisión
16
+
17
+ Cada tecnología de la infraestructura compartida es una distribución independiente con un paquete de importación independiente:
18
+
19
+ - Distribución `shared-infra-<tech>` (kebab) → import `shared_infra_<tech>` (snake): `shared-infra-mongo` → `shared_infra_mongo`, `shared-infra-postgres` → `shared_infra_postgres`, etc.
20
+ - Cada distribución declara solo las dependencias de su tecnología (`pymongo`, `psycopg`, …) en su `pyproject.toml`.
21
+
22
+ La feature define el driver port como un `typing.Protocol` estrecho ([A-025](../foundational/A-025-anatomia-mecanica-feature-puertos-adaptadores-drivers.md)): DTOs tecnológicos tipados, nunca agregados de dominio, nunca `Any`. La implementación concreta vive en `shared_infra_<tech>` y satisface el protocolo por tipado estructural — la feature nunca importa la distribución.
23
+
24
+ La app importa e instancia solo el driver de la tecnología que usa y lo inyecta en el wiring ([A-027](../foundational/A-027-patron-wiring-entry-point.md)). Solo esa distribución entra en su cierre de dependencias.
25
+
26
+ ## Consecuencias
27
+
28
+ ### Positivas
29
+
30
+ - **Cierre tecnológico ligero**: instalar la feature arrastra solo la distribución de la tecnología elegida; pymongo y psycopg no conviven salvo que la app use ambos.
31
+ - **Inversión de dependencia nativa**: el `Protocol` estructural expresa la costura sin importar la implementación, y el adaptador delega en él igual que en TypeScript.
32
+ - **Feature sin dependencias tecnológicas**: el paquete de la feature solo depende del dominio y de la stdlib.
33
+
34
+ ### Negativas
35
+
36
+ - **Una distribución por tecnología que mantener**: cada `shared_infra_<tech>` lleva su propio `pyproject.toml`, versionado y publicación.
37
+ - **Disciplina de tipado estructural**: el `Protocol` solo protege si se declara sin `Any` ni `unknown`; aflojarlo reintroduce el acoplamiento.
38
+ - **Nombre dual**: recordar la pareja distribución `shared-infra-<tech>` ↔ import `shared_infra_<tech>`.
@@ -0,0 +1,14 @@
1
+ # ADRs de Python (P-XXX)
2
+
3
+ Decisiones de arquitectura específicas del ecosistema Python (FastAPI, SQLAlchemy, Pydantic). Definen cómo implementar conceptos fundacionales en Python.
4
+
5
+ | # | Título | Estado |
6
+ |---|--------|--------|
7
+ | P-001 | [Dependencias por feature con FastAPI Depends](./P-001-dependencias-por-feature-fastapi-depends.md) | Aceptado |
8
+ | P-002 | [Inyección de token por request sin persistirlo](./P-002-inyeccion-token-por-request-sin-persistirlo.md) | Aceptado |
9
+ | P-003 | [SQLAlchemy async con connection pooling](./P-003-sqlalchemy-async-connection-pooling.md) | Aceptado |
10
+ | P-004 | [Serializer JSON custom para modelos Pydantic](./P-004-serializer-json-custom-modelos-pydantic.md) | Aceptado |
11
+ | P-005 | [Patrones y pitfalls de NiceGUI](./P-005-patrones-pitfalls-nicegui.md) | Aceptado |
12
+ | P-006 | [Convenciones de pytest](./P-006-convenciones-pytest.md) | Aceptado |
13
+ | P-007 | [CI Python: alineación Poetry↔lockfile y fuente única de la versión de Python](./P-007-ci-python-alineacion-poetry-version-unica.md) | Aceptado |
14
+ | P-008 | [Empaquetado de la infraestructura compartida por tecnología en Python](./P-008-empaquetado-infraestructura-compartida-por-tecnologia.md) | Aceptado |
@@ -0,0 +1,136 @@
1
+ # T-001: Validación de artefactos DDD con primitivas Zod y errores built-in
2
+
3
+ **Estado**: Aceptado
4
+
5
+ **Fecha**: 2026-05-15
6
+
7
+ **originally**: imolko
8
+
9
+ ## Contexto
10
+
11
+ La validación de artefactos DDD (value-objects, entidades y agregados) tiende a resolverse con un **validador por cada error de dominio** y un `superRefine` que evalúa manualmente cada condición. Este anti-patrón aparece tanto en código escrito a mano como en código generado; el scaffold de `ultra-generator` es solo el ejemplo más visible:
12
+
13
+ - Un validador por cada error de dominio (`validate-product-id-empty-error.ts`, `validate-product-id-too-short-error.ts`, etc.)
14
+ - Una clase `V8nCustomParseError` por cada error (`ProductIdEmptyErrorError`, `ProductIdTooShortErrorError`, etc.)
15
+ - Un `superRefine` en `conditions/index.ts` que evalúa manualmente cada condición
16
+
17
+ Ese enfoque genera **8 archivos adicionales** (~120 líneas de código) y usa `superRefine` donde las primitivas nativas de Zod (`.min()`, `.max()`, `.regex()`) son más legibles.
18
+
19
+ Además, las clases custom heredan nombres redundantes (`ProductIdEmptyErrorError` con doble `Error`) y duplican comportamiento que `TooSmallError`, `TooBigError` e `InvalidStringError` de ultra-v8n ya proveen.
20
+
21
+ ## Decisión
22
+
23
+ Para todos los artefactos DDD (value-objects, entidades, agregados), las reglas de validación estándar usan **primitivas nativas de Zod** y **errores built-in de ultra-v8n**. `superRefine` con errores custom se reserva exclusivamente para reglas cross-field que no pueden expresarse con primitivas.
24
+
25
+ ### Reglas de decisión
26
+
27
+ | La validación es… | Usar |
28
+ |-------------------|------|
29
+ | Longitud mínima (incluye vacío) | `z.string().min(N, 'mensaje')` → `TooSmallError` |
30
+ | Longitud máxima | `z.string().max(N, 'mensaje')` → `TooBigError` |
31
+ | Patrón regex | `z.string().regex(/.../, 'mensaje')` → `InvalidStringError` |
32
+ | Tipo | `z.string()` → `InvalidTypeError` |
33
+ | Valor positivo | `z.number().positive('mensaje')` → `TooSmallError` |
34
+ | Valor mínimo | `z.number().min(N, 'mensaje')` → `TooSmallError` |
35
+ | Referencia a otro artefacto | `Artefacto.rules()` — delega en el schema Zod del artefacto referenciado |
36
+ | Regla de dominio cross-field (ej: adjustmentReason obligatorio solo si type=ADJUSTMENT) | `superRefine` + `V8nCustomParseError` |
37
+
38
+ ### Value-objects
39
+
40
+ ```typescript
41
+ // conditions.ts — schema directo
42
+ export const conditions = z.string()
43
+ .min(3, 'Product ID must be at least 3 characters')
44
+ .max(50, 'Product ID must be at most 50 characters')
45
+ .regex(/^[A-Za-z0-9_-]+$/, 'Product ID can only contain letters, numbers, hyphens, and underscores');
46
+ ```
47
+
48
+ ### Entidades
49
+
50
+ ```typescript
51
+ // conditions/index.ts — z.object() componiendo .rules() de otros artefactos
52
+ export const conditions = z.object({
53
+ id: Uuid.rules(),
54
+ batchId: BatchId.rules(),
55
+ expirationDate: ExpirationDate.rules(),
56
+ });
57
+ ```
58
+
59
+ Las entidades componen sus condiciones desde los value-objects que las forman mediante `Artefacto.rules()`. No repiten validaciones — cada value-object es responsable de las suyas.
60
+
61
+ ### Agregados
62
+
63
+ ```typescript
64
+ // conditions/index.ts — { build, rebuild } con transformaciones
65
+ export const conditions = {
66
+ build: z.object({...}).transform(setVersionOne),
67
+ rebuild: z.object({...}).extend({ version: z.number().min(1) }),
68
+ };
69
+ ```
70
+
71
+ Los agregados exportan `{ build, rebuild }`:
72
+ - `build`: schema de creación, incluye `transform(setVersionOne)` para inicializar `version = 1`
73
+ - `rebuild`: schema de reconstrucción desde snapshot, extiende `build` con campos requeridos en el snapshot (`version`, `state`, `createdAt`, etc.)
74
+
75
+ Ambos usan `Artefacto.rules()` para las propiedades y `.superRefine` solo para validaciones cross-field.
76
+
77
+ ### Ejemplo
78
+
79
+ ```typescript
80
+ // ✅ Preferido — 7 líneas, sin archivos extra
81
+ export const conditions = z.object({
82
+ value: z
83
+ .string()
84
+ .min(3, 'Product ID must be at least 3 characters')
85
+ .max(50, 'Product ID must be at most 50 characters')
86
+ .regex(/^[A-Za-z0-9_-]+$/, 'Product ID can only contain letters, numbers, hyphens, and underscores'),
87
+ });
88
+
89
+ // ❌ Evitar — ~80 líneas + 8 archivos extra
90
+ export const conditions = z.object({
91
+ value: z.string().superRefine((value, ctx) => {
92
+ if (value.length === 0) { ctx.addIssue({ ... ProductIdEmptyErrorError.v8n(...) }); }
93
+ if (value.length > 0 && value.length < 3) { ctx.addIssue({ ... }); }
94
+ if (value.length > 50) { ctx.addIssue({ ... }); }
95
+ if (!/^[A-Za-z0-9_-]+$/.test(value)) { ctx.addIssue({ ... }); }
96
+ }),
97
+ });
98
+ ```
99
+
100
+ ### Mensajes de error
101
+
102
+ Los mensajes personalizados pasados a `.min()`, `.max()` y `.regex()` son convertidos por `convertZodIssueToV8nError` (ultra-v8n) en el `detail` del error built-in. El `title` y `type` provienen de la clase built-in:
103
+
104
+ ```typescript
105
+ // Producido por .min(3, 'Product ID must be at least 3 characters'):
106
+ {
107
+ type: 'http://journal/v8n/too_small',
108
+ title: 'Too Small',
109
+ detail: 'Product ID must be at least 3 characters'
110
+ }
111
+ ```
112
+
113
+ ### Cuándo usar `superRefine`
114
+
115
+ Reservar exclusivamente para **reglas cross-field** que no pueden expresarse con primitivas Zod:
116
+
117
+ - Relaciones entre campos del mismo artefacto (ej: `adjustmentReason` obligatorio solo si `type === ADJUSTMENT`)
118
+ - Invariantes de negocio que involucran múltiples propiedades (ej: "el monto no puede superar el límite diario del almacén")
119
+
120
+ Nunca usar `superRefine` para validaciones que ya cubren las primitivas (`.min()`, `.max()`, `.regex()`, `.positive()`). Usar uno u otro, no ambos.
121
+
122
+ ## Consecuencias
123
+
124
+ ### Positivas
125
+
126
+ - **Menos código**: 1 archivo de condiciones (~10 LOC) vs N+1 archivos (~80-120 LOC)
127
+ - **Más legible**: `.min(3)` expresa la intención mejor que un `if (value.length < 3)` dentro de `superRefine`
128
+ - **Estándar de Zod**: cualquier desarrollador que conozca Zod entiende el código inmediatamente
129
+ - **Sin nombres redundantes**: adiós a las clases `XxxErrorError`
130
+ - **Errores con detalle contextual**: el `detail` se personaliza con mensajes descriptivos
131
+
132
+ ### Negativas
133
+
134
+ - **Menos granularidad**: no se puede distinguir entre "vacío" y "muy corto" con `.min()` solo (ambos producen `TooSmallError`). Si el negocio requiere distinguirlos, usar `superRefine`. En la práctica, la distinción rara vez aporta valor
135
+ - **Dependencia de mensajes**: si se cambia el idioma del mensaje de error, los tests que asertan sobre `detail` deben actualizarse
136
+ - **`.rules()` requiere value-objects completos**: componer una entidad con `Artefacto.rules()` implica que el value-object ya existe con su propio `conditions.ts`. No se puede validar una entidad antes de que existan sus value-objects