@saulwade/swl-ses 2.3.0 → 2.4.1

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 (327) hide show
  1. package/CLAUDE.md +241 -199
  2. package/README.md +597 -597
  3. package/agentes/_intent-spec.md +73 -73
  4. package/agentes/_propose-step.md +90 -90
  5. package/agentes/accesibilidad-wcag-swl.md +2 -2
  6. package/agentes/arquitecto-swl.md +2 -2
  7. package/agentes/auto-evolucion-swl.md +2 -2
  8. package/agentes/backend-api-swl.md +2 -2
  9. package/agentes/backend-csharp-swl.md +2 -2
  10. package/agentes/backend-go-swl.md +2 -2
  11. package/agentes/backend-java-swl.md +2 -2
  12. package/agentes/backend-node-swl.md +2 -2
  13. package/agentes/backend-python-swl.md +2 -2
  14. package/agentes/backend-rust-swl.md +2 -2
  15. package/agentes/backend-workers-swl.md +2 -2
  16. package/agentes/cloud-infra-swl.md +2 -2
  17. package/agentes/consolidador-swl.md +2 -2
  18. package/agentes/datos-swl.md +2 -2
  19. package/agentes/depurador-swl.md +2 -2
  20. package/agentes/devops-ci-swl.md +2 -2
  21. package/agentes/disenador-ui-swl.md +2 -2
  22. package/agentes/documentador-swl.md +2 -2
  23. package/agentes/frontend-angular-swl.md +2 -2
  24. package/agentes/frontend-css-swl.md +2 -2
  25. package/agentes/frontend-react-swl.md +2 -2
  26. package/agentes/frontend-swl.md +2 -2
  27. package/agentes/frontend-tailwind-swl.md +2 -2
  28. package/agentes/gh-fix-ci-swl.md +275 -275
  29. package/agentes/implementador-swl.md +2 -2
  30. package/agentes/investigador-swl.md +2 -2
  31. package/agentes/investigador-ux-swl.md +2 -2
  32. package/agentes/llm-apps-swl.md +2 -2
  33. package/agentes/migrador-swl.md +2 -2
  34. package/agentes/mobile-android-swl.md +2 -2
  35. package/agentes/mobile-cross-swl.md +2 -2
  36. package/agentes/mobile-ios-swl.md +2 -2
  37. package/agentes/mobile-testing-swl.md +2 -2
  38. package/agentes/nemesis-auditor-swl.md +1 -1
  39. package/agentes/notificador-swl.md +2 -2
  40. package/agentes/observabilidad-swl.md +2 -2
  41. package/agentes/orquestador-swl.md +41 -14
  42. package/agentes/pagos-swl.md +2 -2
  43. package/agentes/perfilador-usuario-swl.md +2 -2
  44. package/agentes/planificador-swl.md +2 -2
  45. package/agentes/producto-prd-swl.md +2 -2
  46. package/agentes/red-team-swl.md +218 -218
  47. package/agentes/release-manager-swl.md +2 -2
  48. package/agentes/rendimiento-swl.md +2 -2
  49. package/agentes/resolutor-build-swl.md +2 -2
  50. package/agentes/revisor-angular-swl.md +2 -2
  51. package/agentes/revisor-codigo-swl.md +36 -3
  52. package/agentes/revisor-csharp-swl.md +2 -2
  53. package/agentes/revisor-go-swl.md +2 -2
  54. package/agentes/revisor-java-swl.md +2 -2
  55. package/agentes/revisor-kotlin-swl.md +2 -2
  56. package/agentes/revisor-nextjs-swl.md +2 -2
  57. package/agentes/revisor-php-swl.md +2 -2
  58. package/agentes/revisor-react-swl.md +2 -2
  59. package/agentes/revisor-rust-swl.md +2 -2
  60. package/agentes/revisor-seguridad-swl.md +2 -2
  61. package/agentes/revisor-swift-swl.md +2 -2
  62. package/agentes/revisor-typescript-swl.md +2 -2
  63. package/agentes/sre-swl.md +2 -2
  64. package/agentes/tdd-qa-swl.md +2 -2
  65. package/comandos/swl/briefing.md +119 -119
  66. package/comandos/swl/contexto.md +0 -2
  67. package/comandos/swl/contribuir.md +233 -233
  68. package/comandos/swl/deuda-codigo.md +97 -0
  69. package/comandos/swl/predecir.md +139 -139
  70. package/comandos/swl/sesiones.md +1 -1
  71. package/gateway/agent-executor.js +1 -1
  72. package/gateway/lib/event-channel.js +191 -191
  73. package/habilidades/agent-deep-links/SKILL.md +148 -148
  74. package/habilidades/agentes-como-servicio/SKILL.md +2 -2
  75. package/habilidades/angular-moderno/SKILL.md +24 -1
  76. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  77. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
  78. package/habilidades/backend-error-design/SKILL.md +221 -221
  79. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  80. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  81. package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
  82. package/habilidades/calidad-contract-testing/SKILL.md +165 -165
  83. package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
  84. package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
  85. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
  86. package/habilidades/contenedores-docker/SKILL.md +3 -1
  87. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  88. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  89. package/habilidades/drift-detection/SKILL.md +179 -179
  90. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
  91. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
  92. package/habilidades/eval-framework/SKILL.md +212 -212
  93. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  94. package/habilidades/harness-claude-code/SKILL.md +4 -4
  95. package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +253 -252
  96. package/habilidades/legacy-code-rescue/SKILL.md +268 -267
  97. package/habilidades/meta-skills-estandar/SKILL.md +1 -1
  98. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  99. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
  100. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  101. package/habilidades/perfil-usuario/SKILL.md +200 -200
  102. package/habilidades/postgresql-experto/SKILL.md +3 -1
  103. package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
  104. package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
  105. package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
  106. package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
  107. package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
  108. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
  109. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
  110. package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
  111. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
  112. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
  113. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
  114. package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
  115. package/habilidades/proceso-modular-split/SKILL.md +256 -256
  116. package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
  117. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  118. package/habilidades/structured-outputs/SKILL.md +2 -2
  119. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
  120. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
  121. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
  122. package/habilidades/swl-dashboard/SKILL.md +2 -2
  123. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
  124. package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
  125. package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
  126. package/hooks/audit-trail.js +4 -4
  127. package/hooks/calidad-pre-commit.js +15 -11
  128. package/hooks/captura-acciones-post.js +3 -2
  129. package/hooks/captura-acciones-session.js +3 -2
  130. package/hooks/ciclo-evolucion-subagente.js +26 -26
  131. package/hooks/ciclo-evolucion.js +26 -26
  132. package/hooks/claudemd-bloat-detector.js +161 -161
  133. package/hooks/claudemd-duplicacion-detector.js +170 -170
  134. package/hooks/contexto-iteracion.js +145 -144
  135. package/hooks/contexto-subagente.js +68 -0
  136. package/hooks/guardrail-modelo.js +14 -4
  137. package/hooks/inyeccion-contexto.js +12 -12
  138. package/hooks/lib/agent-routing.js +107 -107
  139. package/hooks/lib/auto-consolidator.js +335 -335
  140. package/hooks/lib/autonomia.js +208 -208
  141. package/hooks/lib/ciclo-evolucion.js +47 -47
  142. package/hooks/lib/deep-links.js +185 -185
  143. package/hooks/lib/error-classifier.js +308 -308
  144. package/hooks/lib/etapa-auto-evolucion.js +701 -701
  145. package/hooks/lib/etapa-metricas.js +388 -388
  146. package/hooks/lib/etapa-perfil-usuario.js +376 -376
  147. package/hooks/lib/loop-telemetry.js +321 -321
  148. package/hooks/lib/merkle-audit.js +96 -96
  149. package/hooks/lib/model-router.js +2 -2
  150. package/hooks/lib/propose-step.js +358 -358
  151. package/hooks/lib/provenance-tracker.js +191 -191
  152. package/hooks/lib/rate-limit-tracker.js +253 -253
  153. package/hooks/lib/resource-quota.js +122 -122
  154. package/hooks/lib/retry-jitter.js +165 -165
  155. package/hooks/lib/security-net.js +201 -201
  156. package/hooks/lib/skill-auditor.js +588 -588
  157. package/hooks/lib/sync-status.js +228 -228
  158. package/hooks/lib/taint-tracker.js +107 -107
  159. package/hooks/lib/text-similarity.js +241 -241
  160. package/hooks/lib/token-estimator.js +1 -0
  161. package/hooks/lib/toon-compressor.js +245 -245
  162. package/hooks/lib/usage-model.js +1 -1
  163. package/hooks/registro-turnos.js +210 -209
  164. package/hooks/session-briefing.js +98 -98
  165. package/hooks/spec-gate.js +212 -211
  166. package/hooks/sugerir-contribuir.js +2 -1
  167. package/hooks/sugerir-regenerar-inventario.js +171 -170
  168. package/hooks/tdd-gate.js +242 -241
  169. package/hooks/telemetria-skill-routing.js +100 -100
  170. package/hooks/validar-formato-post-subagente.js +140 -140
  171. package/hooks/validar-intent-spec.js +242 -242
  172. package/hooks/validar-memoria-hook.js +218 -218
  173. package/hooks/validar-planning-paths.js +134 -134
  174. package/instintos/autonomia.yaml +27 -27
  175. package/instintos/prompt-appendices.yaml +57 -57
  176. package/llms.txt +29 -29
  177. package/manifiestos/agent-output-schemas.json +57 -57
  178. package/manifiestos/canonical-hashes.json +2948 -1964
  179. package/manifiestos/hook-profiles.json +0 -2
  180. package/manifiestos/hooks-config.json +469 -477
  181. package/manifiestos/invariantes-criticos.json +30 -0
  182. package/manifiestos/modulos.json +1390 -1390
  183. package/manifiestos/planning-paths.json +44 -44
  184. package/manifiestos/skills-lock.json +1282 -1282
  185. package/package.json +93 -93
  186. package/plantillas/auditor-veto-template.md +105 -105
  187. package/plantillas/github-workflows/README.md +47 -47
  188. package/plantillas/github-workflows/release-please.yml +44 -44
  189. package/plantillas/github-workflows/swl-ci.yml +107 -107
  190. package/plantillas/github-workflows/swl-security.yml +51 -51
  191. package/plugin.json +369 -371
  192. package/reglas/accesibilidad.md +10 -10
  193. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  194. package/reglas/api-diseno.md +9 -9
  195. package/reglas/arreglar-al-detectar.md +240 -240
  196. package/reglas/auditorias-documentales-estructurales.md +7 -7
  197. package/reglas/cloud-infra.md +8 -8
  198. package/reglas/consultar-vault-primero.md +195 -195
  199. package/reglas/debatir-antes-de-aceptar.md +158 -158
  200. package/reglas/fragmentos-compartidos.md +183 -183
  201. package/reglas/hooks.md +6 -6
  202. package/reglas/intent-engineering.md +218 -218
  203. package/reglas/markitdown.md +8 -8
  204. package/reglas/monitor-ci.md +309 -309
  205. package/reglas/patrones.md +6 -6
  206. package/reglas/sesiones-paralelas.md +180 -180
  207. package/reglas/sin-duplicacion-reglas-globales.md +182 -182
  208. package/reglas/skills-estandar.md +6 -6
  209. package/reglas/testing.md +7 -7
  210. package/reglas/tests-cleanup.md +224 -224
  211. package/reglas/usar-code-review-graph.md +155 -155
  212. package/reglas/usar-context7.md +226 -226
  213. package/reglas/verificar-citas-normativas.md +548 -548
  214. package/schemas/agent-frontmatter.schema.json +5 -3
  215. package/schemas/agent-message.schema.json +73 -73
  216. package/schemas/agent-output-implementacion.schema.json +114 -114
  217. package/schemas/agent-output-planificacion.schema.json +150 -150
  218. package/schemas/agent-output-review.schema.json +98 -98
  219. package/schemas/diary-entry.schema.json +112 -112
  220. package/schemas/hook-profiles.schema.json +54 -54
  221. package/schemas/hooks-config.schema.json +89 -89
  222. package/schemas/modulos.schema.json +38 -38
  223. package/schemas/perfiles.schema.json +36 -36
  224. package/schemas/plugin.schema.json +77 -77
  225. package/schemas/skill-evals.schema.json +119 -119
  226. package/schemas/skill-frontmatter.schema.json +245 -245
  227. package/scripts/audit-tools/audit-history.js +330 -330
  228. package/scripts/audit-tools/bundle-tracker.js +290 -290
  229. package/scripts/audit-tools/canary-monitor.js +352 -352
  230. package/scripts/audit-tools/code-profiler.js +605 -605
  231. package/scripts/audit-tools/dep-doctor.js +320 -320
  232. package/scripts/audit-tools/env-validator.js +206 -206
  233. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  234. package/scripts/audit-tools/lib/output.js +23 -23
  235. package/scripts/audit-tools/migration-checker.js +392 -392
  236. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  237. package/scripts/benchmark-memoria.js +167 -167
  238. package/scripts/cli/aprobar-plan.js +73 -73
  239. package/scripts/cli/briefing.js +23 -23
  240. package/scripts/cli/ciclo-evolucion.js +26 -26
  241. package/scripts/cli/configurar-ci.js +40 -40
  242. package/scripts/cli/derivar-feature-list.js +25 -25
  243. package/scripts/cli/detectar-host.js +27 -27
  244. package/scripts/cli/diary-entry.js +69 -69
  245. package/scripts/cli/execution-state.js +18 -18
  246. package/scripts/cli/gateway-notify.js +41 -41
  247. package/scripts/cli/liberar-fase.js +42 -42
  248. package/scripts/cli/loop-telemetry.js +125 -125
  249. package/scripts/cli/mark-evolved.js +56 -56
  250. package/scripts/cli/metricas-dora.js +26 -26
  251. package/scripts/cli/near-duplicate.js +55 -55
  252. package/scripts/cli/notificaciones.js +123 -123
  253. package/scripts/cli/propose-step.js +29 -29
  254. package/scripts/cli/schedule-parse.js +19 -19
  255. package/scripts/cli/sugerir-modelo.js +20 -20
  256. package/scripts/cli/verificar-plan.js +36 -36
  257. package/scripts/cli/verificar-trazabilidad.js +35 -35
  258. package/scripts/configurar-branch-protection.js +418 -418
  259. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  260. package/scripts/field-report.js +199 -199
  261. package/scripts/generar-checklists-consolidados.js +273 -273
  262. package/scripts/generar-matriz-lenguajes.js +271 -271
  263. package/scripts/instalador.js +4 -1
  264. package/scripts/lib/artefactos-python.js +43 -43
  265. package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
  266. package/scripts/lib/benchmark-metrics.js +160 -160
  267. package/scripts/lib/budget-enforcer.js +252 -252
  268. package/scripts/lib/ci-reader.js +193 -193
  269. package/scripts/lib/claude-sessions.js +1 -0
  270. package/scripts/lib/configurar-ci.js +380 -380
  271. package/scripts/lib/contadores-inventario.js +217 -217
  272. package/scripts/lib/detectar-host-swl.js +175 -175
  273. package/scripts/lib/detectar-stack-detallado.js +307 -307
  274. package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
  275. package/scripts/lib/detector-reglas-duplicadas.js +220 -220
  276. package/scripts/lib/diary-entry.js +234 -234
  277. package/scripts/lib/eval-metrics-store.js +218 -218
  278. package/scripts/lib/eval-quality.js +171 -171
  279. package/scripts/lib/eval-schemas.js +144 -144
  280. package/scripts/lib/eval-self-correct.js +106 -106
  281. package/scripts/lib/eval-validator.js +185 -185
  282. package/scripts/lib/evidencia-release.js +322 -322
  283. package/scripts/lib/gate-hooks-requires.js +249 -249
  284. package/scripts/lib/gate-licencias.js +212 -212
  285. package/scripts/lib/git-metricas.js +257 -257
  286. package/scripts/lib/jaccard-similarity.js +98 -98
  287. package/scripts/lib/longmemeval-runner.js +125 -125
  288. package/scripts/lib/metricas-dora.js +204 -204
  289. package/scripts/lib/npm-version.js +261 -261
  290. package/scripts/lib/paquetes-conocidos.js +50 -50
  291. package/scripts/lib/plan-lock.js +275 -275
  292. package/scripts/lib/pr-analyzer.js +399 -399
  293. package/scripts/lib/preservar-usuario.js +39 -5
  294. package/scripts/lib/prompt-builder.js +264 -264
  295. package/scripts/lib/reglas-globales-conocidas.json +180 -180
  296. package/scripts/lib/resolver-plan-fase.js +37 -37
  297. package/scripts/lib/rrf-fusion.js +175 -175
  298. package/scripts/lib/schema-version.js +164 -164
  299. package/scripts/lib/scoring-instintos.js +277 -277
  300. package/scripts/lib/semantic-search.js +252 -252
  301. package/scripts/lib/tool-cost-analyzer.js +1 -0
  302. package/scripts/limpiar-artefactos-python.js +131 -131
  303. package/scripts/migrar-csv-a-array.js +168 -168
  304. package/scripts/migrar-fase-dominio.js +200 -200
  305. package/scripts/publicar.js +497 -497
  306. package/scripts/run-eval.js +141 -141
  307. package/scripts/tui/componentes/selector-multi.js +189 -189
  308. package/scripts/tui/componentes/selector-unico.js +158 -158
  309. package/scripts/tui/ejecutores.js +375 -375
  310. package/scripts/tui/index.js +162 -162
  311. package/scripts/tui/lib/colores.js +129 -129
  312. package/scripts/tui/lib/render.js +264 -264
  313. package/scripts/tui/lib/teclas.js +113 -113
  314. package/scripts/tui/pantallas/inspect.js +173 -173
  315. package/scripts/tui/pantallas/install-wizard.js +334 -334
  316. package/scripts/tui/pantallas/menu-principal.js +52 -52
  317. package/scripts/tui/pantallas/progreso.js +274 -274
  318. package/scripts/tui/pantallas/resumen.js +132 -132
  319. package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
  320. package/scripts/tui/pantallas/update-wizard.js +232 -232
  321. package/scripts/tui/pantallas/welcome.js +187 -187
  322. package/scripts/validar-userland-vacio.js +110 -110
  323. package/scripts/validar.js +1 -1
  324. package/scripts/verificar-release.js +51 -0
  325. package/hooks/lib/context-compressor.js +0 -657
  326. package/hooks/linea-estado.js +0 -326
  327. package/hooks/monitor-contexto.js +0 -373
@@ -1,469 +1,469 @@
1
- # Patrones avanzados — Python
2
-
3
- Recurso de profundidad cargado bajo demanda desde `SKILL.md`. Contiene 7 patrones
4
- que aparecen al integrar Python con pipelines reales: conversores de documentos,
5
- clientes heterogéneos, cachés determinísticos, soft imports, detectores de
6
- texto y duplicación deliberada de lógica.
7
-
8
- ## Índice
9
-
10
- 1. [Normalizadores: colapsar al formato canónico del proyecto](#normalizadores)
11
- 2. [kwargs opcionales entre clientes hermanos: try/except TypeError](#kwargs-opcionales)
12
- 3. [Caché content-addressable por SHA256](#cache-sha256)
13
- 4. [F401 en archivos con soft imports es intencional](#f401-soft-imports)
14
- 5. [Detectores regex multi-pattern: extender scope sin refinar](#regex-multi-pattern)
15
- 6. [Tracer/replicador paralelo del motor: marca SYNC obligatoria](#tracer-sync)
16
- 7. [Fixtures con datos pre-procesados NO ejercen el path crudo](#fixtures-crudos)
17
-
18
- ---
19
-
20
- <a id="normalizadores"></a>
21
- ## 1. Normalizadores: colapsar al formato canónico del PROYECTO, no al estándar genérico
22
-
23
- ### SIEMPRE: el normalizador debe conocer la convención del proyecto
24
-
25
- **Cuándo aplicar**: cuando un módulo externo (MarkItDown, Pandoc, mammoth, pdfminer) produce artefactos de conversión y hay que limpiarlos antes del pipeline interno.
26
-
27
- **Problema**: es tentador colapsar los artefactos al formato "más estándar" según la spec de CommonMark / JSON / el estándar de turno. Pero si el proyecto ya tiene una convención distinta (ej. puntuación FUERA del bold en vez de dentro), el normalizador debe colapsar a ESA convención, no al genérico.
28
-
29
- **Regla**: antes de escribir un normalizador, verificar la convención real del proyecto en docs internos o en un fixture "canónico" existente. Si la convención del proyecto contradice la intuición genérica, registrar en el docstring del normalizador POR QUÉ se eligió esa dirección.
30
-
31
- ```python
32
- # MAL — colapsa al estándar genérico sin verificar la convención del proyecto
33
- def _normalizar(texto: str) -> str:
34
- # "Mover puntuación al interior del bold" (parece "más correcto")
35
- return re.sub(r"\*\*([^*]+?)\*\*([:;.,])", r"**\1\2**", texto)
36
- # Resultado: `**ACTIVIDAD**:` → `**ACTIVIDAD:**` ← pero el proyecto usa el primero
37
-
38
- # BIEN — conoce la convención: "puntuación terminal FUERA del bold"
39
- def _normalizar(texto: str) -> str:
40
- """Normaliza a la convención del proyecto: puntuación fuera del bold.
41
-
42
- CommonMark genérico prefiere mover la puntuación al interior del bold,
43
- pero algunos proyectos adoptan la convención opuesta para preservar
44
- la semántica visual del documento de origen (DOCX, PDF). Cubrir el
45
- caso con un test de ground truth antes de modificar el regex.
46
- """
47
- # Artefacto `**ACTIVIDAD****:**` → `**ACTIVIDAD**:` (puntuación fuera)
48
- return re.sub(
49
- r"\*\*([^\n*]+?)\*{2,}([:;,.!?\-—)])\*{2,}",
50
- r"**\1**\2",
51
- texto,
52
- )
53
- ```
54
-
55
- **Verificación previa obligatoria**: en una PR que introduzca un normalizador, agregar un test con la convención canónica del proyecto como ground truth. Si no existe fixture, preguntarle al autor del proyecto antes de inventar la dirección.
56
-
57
- ---
58
-
59
- <a id="kwargs-opcionales"></a>
60
- ## 2. kwargs opcionales entre clientes hermanos: try/except TypeError
61
-
62
- ### Patrón: propagar kwargs a implementaciones heterogéneas con degradación silenciosa
63
-
64
- **Cuándo aplicar**: cuando dos o más clientes implementan la misma interfaz de alto nivel (ej. `OllamaClient.generate()` y `NIMClient.generate()`) pero uno acepta un kwarg nuevo y el otro aún no. Migración incremental sin versionar la interfaz.
65
-
66
- **Problema**: si el wrapper de alto nivel pasa siempre el kwarg, falla con `TypeError` en el cliente que aún no lo acepta. Si el wrapper nunca lo pasa, los clientes que sí lo aceptan pierden la funcionalidad.
67
-
68
- **Solución idiomática**: crear la coroutine (o call sync) dentro de `try`; capturar `TypeError` solo cuando ocurre al CONSTRUIR la llamada (signature mismatch); en el `except` re-llamar sin el kwarg.
69
-
70
- ```python
71
- # BIEN — degradación silenciosa compatible con clientes heterogéneos
72
- async def llamar_con_timeout_opcional(cliente, modelo, prompt, timeout_s):
73
- try:
74
- # Cliente moderno (acepta `timeout` kwarg)
75
- call = cliente.generate(model=modelo, prompt=prompt, timeout=timeout_s)
76
- except TypeError:
77
- # Cliente legacy (no acepta `timeout`); fallback silencioso
78
- call = cliente.generate(model=modelo, prompt=prompt)
79
- return await asyncio.wait_for(call, timeout=timeout_s)
80
- ```
81
-
82
- **Detalle importante para async**: en `async def f(**kwargs)` los kwargs se aceptan siempre. El `TypeError` solo ocurre con signature fija (`async def f(model, prompt, ...)`) y se lanza en la creación de la coroutine, ANTES del `await`. Probar con mocks de signature fija (no `MagicMock(**kwargs)`).
83
-
84
- **Aplicabilidad**: migraciones de librerías incrementales, plugins con versiones heterogéneas, strategy pattern donde las implementaciones evolucionan a distinto ritmo.
85
-
86
- ---
87
-
88
- <a id="cache-sha256"></a>
89
- ## 3. Caché content-addressable por SHA256 para llamadas determinísticas costosas
90
-
91
- ### Patrón
92
-
93
- Cuando una función pura (mismo input → mismo output) es costosa (>1 s, llamada
94
- externa, IO pesado) y se invoca repetidamente con la misma entrada (pipelines
95
- de ingesta, suites E2E, recompilación incremental), cachear el resultado por
96
- el **SHA256 del contenido** del input convierte re-ejecuciones en operaciones
97
- prácticamente gratuitas y hace el pipeline resumible.
98
-
99
- ### Por qué SHA256 del contenido y no `(path, mtime, size)`
100
-
101
- - `mtime` se rompe al copiar archivos entre sistemas (rsync, git checkout).
102
- - `size` colisiona trivialmente con archivos distintos del mismo tamaño.
103
- - `path` cambia al reorganizar el dataset.
104
-
105
- El SHA256 del **contenido** identifica la entrada de forma reproducible aunque
106
- se mueva, renombre o clone.
107
-
108
- ### Implementación canónica
109
-
110
- ```python
111
- import hashlib
112
- import os
113
- from pathlib import Path
114
-
115
-
116
- def _flag_off(env_var: str) -> bool:
117
- """True si la env var está en {1, true, yes, on} (case-insensitive)."""
118
- return os.environ.get(env_var, "").lower() in {"1", "true", "yes", "on"}
119
-
120
-
121
- def sha256_bytes(contenido: bytes) -> str:
122
- return hashlib.sha256(contenido).hexdigest()
123
-
124
-
125
- def sha256_archivo(ruta: Path) -> str:
126
- """SHA256 del contenido completo en bloques de 64KB."""
127
- h = hashlib.sha256()
128
- with ruta.open("rb") as f:
129
- for chunk in iter(lambda: f.read(65536), b""):
130
- h.update(chunk)
131
- return h.hexdigest()
132
-
133
-
134
- def procesar_con_cache(
135
- contenido: bytes,
136
- dir_cache: Path,
137
- procesar,
138
- *,
139
- bypass_env: str = "CACHE_OFF",
140
- dimension: str | None = None,
141
- ) -> str:
142
- """Ejecuta `procesar(contenido)` solo si el resultado no está en caché.
143
-
144
- Layout en disco con sharding de 2 chars: `dir_cache/[dim/]ab/abcdef...txt`.
145
- El sharding evita carpetas con 10k+ archivos en NTFS/ext4.
146
- """
147
- sha = sha256_bytes(contenido)
148
- base = dir_cache if dimension is None else dir_cache / dimension
149
- ruta_cache = base / sha[:2] / f"{sha}.txt"
150
-
151
- if ruta_cache.exists() and ruta_cache.stat().st_size > 0 and not _flag_off(bypass_env):
152
- return ruta_cache.read_text(encoding="utf-8")
153
-
154
- # Recalcular (operación costosa: OCR, LLM, transcripción, build, etc.)
155
- resultado = procesar(contenido)
156
-
157
- # Escritura atómica: temp + rename para evitar entradas parciales por kill
158
- ruta_cache.parent.mkdir(parents=True, exist_ok=True)
159
- tmp = ruta_cache.with_suffix(".tmp")
160
- tmp.write_text(resultado, encoding="utf-8")
161
- tmp.rename(ruta_cache)
162
-
163
- return resultado
164
- ```
165
-
166
- ### Reglas de diseño
167
-
168
- - **Sharding de 2 chars** (`{sha[:2]}/{sha}.txt`) evita degradación del filesystem
169
- cuando el caché crece a decenas de miles de entradas (NTFS, ext4, APFS).
170
- - **Un directorio por tipo de operación**: `cache/ocr/`, `cache/vision/`,
171
- `cache/transcripcion/`. Mezclar tipos hace imposible invalidar uno solo.
172
- - **Escritura atómica obligatoria**: `tmp → rename` previene que un kill del proceso
173
- deje entradas parciales que se interpretan como éxito en la próxima corrida.
174
- - **Verificar tamaño > 0 al leer**: archivos de 0 bytes son ejecuciones abortadas,
175
- no "resultado vacío válido".
176
- - **Bypass por variable de entorno** (`CACHE_OFF=1` aceptando `1/true/yes/on`):
177
- crítico para tests que validan el procesador real, no el caché. Sin bypass,
178
- imposible reproducir bugs del extractor real durante pruebas.
179
- - **Key multi-dimensión cuando aplica**: si la función es determinística sobre
180
- `(input, modelo)` —caso típico de síntesis LLM con distinto modelo— usar el
181
- parámetro `dimension` para particionar el caché:
182
- `cache/sintesis/{modelo_safe}/{sha[:2]}/{sha}.txt`. Sin esto, cambiar de modelo
183
- devuelve resultados del modelo viejo con semántica nueva.
184
- - **Versionar el caché cuando cambia el procesamiento**: si actualizas el modelo de
185
- OCR o la versión del prompt LLM, bumpear `dimension` (`v1` → `v2`) o invalidar
186
- el directorio completo.
187
- - **No cachear errores**: si `procesar()` lanza excepción, el caché queda vacío
188
- y la siguiente corrida reintenta. Cachear el error convierte errores transitorios
189
- en permanentes.
190
- - **Idempotencia natural**: la escritura siempre puede sobreescribir porque el SHA
191
- garantiza mismo contenido por construcción. No requiere lock entre procesos.
192
-
193
- ### Tests obligatorios
194
-
195
- Toda implementación de caché content-addressable debe cubrir:
196
-
197
- 1. **Roundtrip write→read**: invocar dos veces con mismo input verifica que
198
- la segunda lectura no llama al procesador (mock con counter).
199
- 2. **Bypass por flag**: con `CACHE_OFF=1` siempre llama al procesador aunque exista entrada.
200
- 3. **Keys distintas para inputs distintos**: dos inputs con SHA distinto no se pisan.
201
- 4. **Fixture aislado**: `monkeypatch.setenv("CACHE_DIR", str(tmp_path))` evita
202
- contaminación entre tests.
203
- 5. **Atomicidad**: simular kill durante escritura (interrupción del `tmp.write_text`)
204
- y verificar que la siguiente corrida no lee un archivo parcial.
205
-
206
- ### Aplicabilidad
207
-
208
- - Pipelines de ingesta de documentos (PDF → texto, imagen → OCR, audio → transcripción).
209
- - Extracción con LLM costoso (vision, clasificación, extracción estructurada).
210
- - Suites E2E donde el dataset es fijo y se ejecutan repetidamente —típicamente
211
- reduce el tiempo de la corrida en 70-90% si la función cacheada domina.
212
- - Compilaciones incrementales (aunque los build systems ya tienen este patrón).
213
- - Deduplicación en walkers resumables (ver `testing-python`: "tests de
214
- idempotencia requieren 2 ejecuciones + diff").
215
-
216
- ---
217
-
218
- <a id="f401-soft-imports"></a>
219
- ## 4. F401 en archivos con soft imports es intencional, no ruido
220
-
221
- ### Contexto
222
-
223
- `ruff --select=F401` (y `flake8 --select=F401`) reportan "imported but unused"
224
- cuando un módulo se importa pero no se usa. En archivos con **soft imports**
225
- (intentar importar una dependencia opcional dentro de `try/except ImportError`)
226
- el import **debe quedarse** aunque lint lo marque como no usado — es parte del
227
- patrón de detección de disponibilidad.
228
-
229
- ### Patrón de soft import canónico
230
-
231
- ```python
232
- # El módulo puede funcionar con o sin la dependencia opcional.
233
- # La presencia del símbolo habilita un code path; su ausencia cambia al fallback.
234
-
235
- try:
236
- from markitdown import MarkItDown # noqa: F401 — soft import
237
- _MARKITDOWN_DISPONIBLE = True
238
- except ImportError:
239
- _MARKITDOWN_DISPONIBLE = False
240
-
241
-
242
- def extraer_texto(ruta: Path) -> str:
243
- if _MARKITDOWN_DISPONIBLE:
244
- from markitdown import MarkItDown # re-import local, ya sabemos que existe
245
- return MarkItDown().convert(str(ruta)).text_content
246
- # Fallback: usar parser básico
247
- return _extraer_con_parser_basico(ruta)
248
- ```
249
-
250
- ### Regla
251
-
252
- - Agregar `# noqa: F401` en la línea del import top-level dentro del `try`.
253
- - Alternativa: configurar `ruff.toml` / `pyproject.toml` con `per-file-ignores` para los archivos de soft import si son muchos:
254
- ```toml
255
- [tool.ruff.per-file-ignores]
256
- "core/adapters/*.py" = ["F401"] # todos los adapters usan soft imports
257
- ```
258
- - NO importar dentro del `try` sin usar: usar el símbolo (`MarkItDown`) como sonda de disponibilidad es el patrón correcto; el warning F401 es el ruido.
259
- - NO reemplazar con `importlib.util.find_spec("markitdown")` salvo que la librería tenga side effects al importar — `find_spec` no valida que la versión instalada exponga los símbolos esperados.
260
-
261
- ### Anti-patrón
262
-
263
- ```python
264
- # MAL — el F401 se "soluciona" pero la detección se rompe
265
- try:
266
- import markitdown # noqa — "no se usa, pero quiero saber si está"
267
- _DISPONIBLE = True
268
- except ImportError:
269
- _DISPONIBLE = False
270
- ```
271
-
272
- El problema: si `markitdown` se instala pero la API cambió (el símbolo
273
- `MarkItDown` ya no existe), el import sigue pasando y `_DISPONIBLE = True`
274
- pero el code path fallará más tarde al usar el símbolo ausente. Importar
275
- el símbolo específico que vas a usar es más robusto que importar el módulo
276
- y esperar que todo lo demás funcione.
277
-
278
- ---
279
-
280
- <a id="regex-multi-pattern"></a>
281
- ## 5. Detectores regex multi-pattern: extender scope sin refinar genera falsos positivos
282
-
283
- ### NUNCA: agregar un nuevo input a un detector multi-pattern sin re-evaluar la sensibilidad de cada pattern individual
284
-
285
- **Problema**: tienes una función `detectar(texto)` que aplica una lista de regex
286
- con OR (`any(p.search(texto) for p in PATTERNS)`). Originalmente operaba sobre
287
- texto curado (título, etiquetas, campos cortos). Ahora extiendes el scope a un
288
- texto más ruidoso (prosa larga, descripciones libres, contenido de usuario).
289
- Patterns genéricos que funcionaban en el texto curado empiezan a generar falsos
290
- positivos en el ruidoso.
291
-
292
- ```python
293
- # Caso típico: detector de "enumeración múltiple de items relacionados"
294
- PATTERNS = [
295
- re.compile(r"(?:,\s+\w[\w\s]{3,45}){3,}"), # P1 generico: 3+ comas
296
- re.compile(r"(?:^|\n)\s*[•\-]\s+.{10,}", re.M), # P2 generico: bullets
297
- re.compile(r"(?:anexos|evidencias|listas){2,}", re.I), # P3 especifico
298
- ]
299
-
300
- # MAL — extender scope sin discriminar patterns
301
- def enumera_multiples(registro):
302
- texto = " ".join([
303
- registro.titulo, registro.resumen, # texto curado
304
- registro.descripcion_libre, # texto ruidoso (prosa larga)
305
- ])
306
- return any(p.search(texto) for p in PATTERNS)
307
- # → P1 (3+ comas) matchea series de citas o referencias en prosa larga
308
- # ("art. 5, art. 10, art. 23, art. 138 y art. 141") → falso positivo
309
- # regresión típica observada: baja precisión sobre el dataset golden.
310
-
311
- # BIEN — patterns por scope, refinados según ruido tolerado
312
- def enumera_multiples(registro):
313
- texto_curado = " ".join([registro.titulo, registro.resumen])
314
- if any(p.search(texto_curado) for p in PATTERNS):
315
- return True
316
- # Sobre texto ruidoso, solo el pattern especifico (P3)
317
- texto_ruidoso = " ".join([registro.descripcion_libre, registro.notas_libres])
318
- return PATTERNS[2].search(texto_ruidoso) is not None
319
- ```
320
-
321
- ### Regla operativa
322
-
323
- Cuando se extiende un detector regex a un nuevo scope textual, agregar un test de
324
- regresión específico que verifique que NO se introducen falsos positivos sobre
325
- muestras del nuevo scope que NO deberían matchear. Si los patterns genéricos
326
- hacen FP sobre el scope nuevo, **discriminarlos por scope** (no aplicarlos al
327
- scope ruidoso) en lugar de intentar refinarlos en una sola lista global.
328
-
329
- **Aplicabilidad**: clasificadores de texto, detectores de PII, filtros de spam,
330
- sistemas de moderación, extracción de entidades cuando el scope crece de campos
331
- estructurados a contenido libre.
332
-
333
- ---
334
-
335
- <a id="tracer-sync"></a>
336
- ## 6. Tracer/replicador paralelo del motor: marca SYNC obligatoria en cada cambio
337
-
338
- ### Patrón: cuando duplicas lógica del original, marca el SYNC y agrega test de paridad
339
-
340
- **Problema**: en sistemas con motores complejos (cascadas de reglas, clasificadores
341
- con prioridad, motores de decisión), suele crearse un "tracer" que replica la
342
- lógica paso a paso para producir telemetría, explicación o shadow runs. El tracer
343
- es **duplicación** del motor — si cambias el motor sin tocar el tracer, los
344
- outputs divergen silenciosamente y los tests del tracer pasan con datos viejos
345
- mientras el motor real ya cambió.
346
-
347
- ```python
348
- # Motor real (motor_decision.py)
349
- def resolver_resultado(self, registro):
350
- resultado = self._resolver_core(registro)
351
- # Ajuste 1: suavización por condición A
352
- if cond_a(registro): return "ACEPTADO"
353
- # Ajustes 2-4: endurecimientos
354
- if cond_b(registro): return "RECHAZADO"
355
- # Ajuste 5 (NUEVO): endurecimiento por condición compuesta
356
- if resultado == "ACEPTADO" and registro._compuesta:
357
- return "RECHAZADO"
358
- return resultado
359
-
360
- # Tracer paralelo (motor_decision_tracer.py)
361
- def replicar_resultado(...):
362
- # SYNC con: motor_decision.py::resolver_resultado
363
- # Si agregas un Ajuste, AGREGARLO TAMBIÉN aquí en el orden correcto.
364
- if cond_a(registro): ...
365
- if cond_b(registro): ...
366
- # Olvidé sincronizar Ajuste 5 aquí → tracer reporta ACEPTADO
367
- # pero motor real reporta RECHAZADO → tests ground truth fallan
368
- # silenciosamente porque el tracer es lo que se compara.
369
- ```
370
-
371
- ### Reglas obligatorias para código duplicado deliberado
372
-
373
- 1. **Marca de SYNC**: comentario `# SYNC con: <archivo>:<función>` visible en la
374
- cabecera del replicador. La búsqueda `grep -rn "SYNC con:"` lista todos los
375
- puntos de duplicación del repositorio en una corrida.
376
- 2. **Test de paridad**: para casos representativos del dataset golden, comparar
377
- `motor.resolver(x) == tracer.resolver(x)` y fallar si difieren. Es el único
378
- gate que detecta la divergencia sin esperar a producción.
379
- 3. **Convención de naming**: el archivo que duplica la lógica se nombra explícitamente
380
- como derivado (`_tracer.py`, `_replicador.py`, `_explainer.py`, `_shadow.py`).
381
- NUNCA esconderlo bajo nombre genérico — el nombre es la primera línea de defensa
382
- contra que alguien lo edite sin saber que es duplicación.
383
- 4. **Owner único**: el motor y su tracer cambian en el mismo PR. Code review rechaza
384
- PRs que tocan el motor sin tocar el tracer (o que justifiquen explícitamente
385
- por qué la divergencia es intencional, ej. "el tracer no necesita el ajuste de
386
- performance").
387
-
388
- **Aplicabilidad**: sistemas con explainability, dual-track production+shadow,
389
- motores de reglas con logging detallado, A/B testing de algoritmos, migración
390
- incremental de un motor legacy a uno nuevo donde ambos corren en paralelo.
391
-
392
- ---
393
-
394
- <a id="fixtures-crudos"></a>
395
- ## 7. Fixtures de test con datos pre-procesados NO ejercen el path de datos crudos
396
-
397
- ### Anti-patrón: fixtures construidos a mano que reflejan la salida del extractor, no su entrada
398
-
399
- **Problema**: el sistema en producción procesa input crudo (PDFs, HTMLs, JSON
400
- externos, mensajes raw) que pasa por extractores que producen un dict condensado.
401
- Los fixtures de test se construyen "a mano" copiando lo que el extractor produce
402
- (o aproximándolo). Resultado: cualquier path del motor que solo se activa con
403
- campos del input crudo (presentes solo cuando hay extractor real arriba) NO se
404
- ejercita por los tests, y los bugs aparecen únicamente en producción.
405
-
406
- ```python
407
- # Fixture típico hecho a mano (apariencia inocente)
408
- {
409
- "id": "REG-001",
410
- "registro": {
411
- "titulo": "Falta de evidencia documental",
412
- "descripcion_corta": "El operador manifiesta que el oficio acredita...", # condensada
413
- # NOTAR: no hay `descripcion_original`
414
- }
415
- }
416
-
417
- # Motor con fix nuevo
418
- def _clasificar_postura(registro):
419
- # Fix: prefiere texto crudo cuando está disponible
420
- texto = (
421
- registro.get("descripcion_original")
422
- or registro.get("descripcion_corta")
423
- )
424
- return any(p in texto.lower() for p in patrones_subsanadores)
425
-
426
- # Test del fixture pasa "por casualidad" (descripcion_corta condensada
427
- # casualmente contiene "se anexa") → falsa confianza.
428
- # En producción, descripcion_original sería 5000 chars con verbos
429
- # canónicos y descripcion_corta sería paráfrasis sin esos verbos.
430
- # Los tests no ejercitan el path real.
431
- ```
432
-
433
- ### Defensa
434
-
435
- Cuando se introduce un nuevo campo opcional que el motor prefiere leer
436
- (`descripcion_original`, `metadata_completa`, `raw_input`, `body_unparsed`,
437
- etc.), agregar **al menos un fixture** que tenga ese campo poblado con una
438
- muestra representativa del input crudo real — idealmente extraída del caché de
439
- un pipeline E2E ejecutado, no inventada a mano.
440
-
441
- ```python
442
- # Mejorado: fixture con ambos campos, con datos representativos del scope crudo
443
- {
444
- "registro": {
445
- "descripcion_corta": "El operador manifiesta...", # condensada (lo que el extractor produce)
446
- "descripcion_original": (
447
- "OFICIO 12345/2026 — DEPARTAMENTO DE OPERACIONES ... "
448
- "se anexa el oficio mediante correo institucional ... "
449
- "[texto crudo extraído del PDF, 5000 chars con jerga canónica]"
450
- ),
451
- }
452
- }
453
- ```
454
-
455
- ### Regla operativa
456
-
457
- - **Dos niveles de fixtures**: condensados (rápidos, para lógica de negocio) y
458
- crudos (lentos, para paths que dependen del input real). Marcar cada fixture
459
- con su nivel: `nombre.condensado.json` vs `nombre.crudo.json`.
460
- - **Snapshot del extractor real**: si el extractor es determinístico, ejecutarlo
461
- una vez sobre datos reales y persistir su output como fixture crudo. No inventarlo.
462
- - **Test de "campo prioritario nuevo"**: cada vez que el motor agrega un campo
463
- con prioridad sobre uno existente, agregar un test que verifique el comportamiento
464
- con AMBOS campos poblados con valores **divergentes** (que producen resultados
465
- distintos). Si el test pasa con valores iguales, no prueba la priorización.
466
-
467
- **Aplicabilidad**: pipelines de ingesta con extractores upstream, sistemas con
468
- adaptadores que normalizan inputs heterogéneos, motores que prefieren campos
469
- crudos sobre derivados, tests de regresión de migraciones de schema.
1
+ # Patrones avanzados — Python
2
+
3
+ Recurso de profundidad cargado bajo demanda desde `SKILL.md`. Contiene 7 patrones
4
+ que aparecen al integrar Python con pipelines reales: conversores de documentos,
5
+ clientes heterogéneos, cachés determinísticos, soft imports, detectores de
6
+ texto y duplicación deliberada de lógica.
7
+
8
+ ## Índice
9
+
10
+ 1. [Normalizadores: colapsar al formato canónico del proyecto](#normalizadores)
11
+ 2. [kwargs opcionales entre clientes hermanos: try/except TypeError](#kwargs-opcionales)
12
+ 3. [Caché content-addressable por SHA256](#cache-sha256)
13
+ 4. [F401 en archivos con soft imports es intencional](#f401-soft-imports)
14
+ 5. [Detectores regex multi-pattern: extender scope sin refinar](#regex-multi-pattern)
15
+ 6. [Tracer/replicador paralelo del motor: marca SYNC obligatoria](#tracer-sync)
16
+ 7. [Fixtures con datos pre-procesados NO ejercen el path crudo](#fixtures-crudos)
17
+
18
+ ---
19
+
20
+ <a id="normalizadores"></a>
21
+ ## 1. Normalizadores: colapsar al formato canónico del PROYECTO, no al estándar genérico
22
+
23
+ ### SIEMPRE: el normalizador debe conocer la convención del proyecto
24
+
25
+ **Cuándo aplicar**: cuando un módulo externo (MarkItDown, Pandoc, mammoth, pdfminer) produce artefactos de conversión y hay que limpiarlos antes del pipeline interno.
26
+
27
+ **Problema**: es tentador colapsar los artefactos al formato "más estándar" según la spec de CommonMark / JSON / el estándar de turno. Pero si el proyecto ya tiene una convención distinta (ej. puntuación FUERA del bold en vez de dentro), el normalizador debe colapsar a ESA convención, no al genérico.
28
+
29
+ **Regla**: antes de escribir un normalizador, verificar la convención real del proyecto en docs internos o en un fixture "canónico" existente. Si la convención del proyecto contradice la intuición genérica, registrar en el docstring del normalizador POR QUÉ se eligió esa dirección.
30
+
31
+ ```python
32
+ # MAL — colapsa al estándar genérico sin verificar la convención del proyecto
33
+ def _normalizar(texto: str) -> str:
34
+ # "Mover puntuación al interior del bold" (parece "más correcto")
35
+ return re.sub(r"\*\*([^*]+?)\*\*([:;.,])", r"**\1\2**", texto)
36
+ # Resultado: `**ACTIVIDAD**:` → `**ACTIVIDAD:**` ← pero el proyecto usa el primero
37
+
38
+ # BIEN — conoce la convención: "puntuación terminal FUERA del bold"
39
+ def _normalizar(texto: str) -> str:
40
+ """Normaliza a la convención del proyecto: puntuación fuera del bold.
41
+
42
+ CommonMark genérico prefiere mover la puntuación al interior del bold,
43
+ pero algunos proyectos adoptan la convención opuesta para preservar
44
+ la semántica visual del documento de origen (DOCX, PDF). Cubrir el
45
+ caso con un test de ground truth antes de modificar el regex.
46
+ """
47
+ # Artefacto `**ACTIVIDAD****:**` → `**ACTIVIDAD**:` (puntuación fuera)
48
+ return re.sub(
49
+ r"\*\*([^\n*]+?)\*{2,}([:;,.!?\-—)])\*{2,}",
50
+ r"**\1**\2",
51
+ texto,
52
+ )
53
+ ```
54
+
55
+ **Verificación previa obligatoria**: en una PR que introduzca un normalizador, agregar un test con la convención canónica del proyecto como ground truth. Si no existe fixture, preguntarle al autor del proyecto antes de inventar la dirección.
56
+
57
+ ---
58
+
59
+ <a id="kwargs-opcionales"></a>
60
+ ## 2. kwargs opcionales entre clientes hermanos: try/except TypeError
61
+
62
+ ### Patrón: propagar kwargs a implementaciones heterogéneas con degradación silenciosa
63
+
64
+ **Cuándo aplicar**: cuando dos o más clientes implementan la misma interfaz de alto nivel (ej. `OllamaClient.generate()` y `NIMClient.generate()`) pero uno acepta un kwarg nuevo y el otro aún no. Migración incremental sin versionar la interfaz.
65
+
66
+ **Problema**: si el wrapper de alto nivel pasa siempre el kwarg, falla con `TypeError` en el cliente que aún no lo acepta. Si el wrapper nunca lo pasa, los clientes que sí lo aceptan pierden la funcionalidad.
67
+
68
+ **Solución idiomática**: crear la coroutine (o call sync) dentro de `try`; capturar `TypeError` solo cuando ocurre al CONSTRUIR la llamada (signature mismatch); en el `except` re-llamar sin el kwarg.
69
+
70
+ ```python
71
+ # BIEN — degradación silenciosa compatible con clientes heterogéneos
72
+ async def llamar_con_timeout_opcional(cliente, modelo, prompt, timeout_s):
73
+ try:
74
+ # Cliente moderno (acepta `timeout` kwarg)
75
+ call = cliente.generate(model=modelo, prompt=prompt, timeout=timeout_s)
76
+ except TypeError:
77
+ # Cliente legacy (no acepta `timeout`); fallback silencioso
78
+ call = cliente.generate(model=modelo, prompt=prompt)
79
+ return await asyncio.wait_for(call, timeout=timeout_s)
80
+ ```
81
+
82
+ **Detalle importante para async**: en `async def f(**kwargs)` los kwargs se aceptan siempre. El `TypeError` solo ocurre con signature fija (`async def f(model, prompt, ...)`) y se lanza en la creación de la coroutine, ANTES del `await`. Probar con mocks de signature fija (no `MagicMock(**kwargs)`).
83
+
84
+ **Aplicabilidad**: migraciones de librerías incrementales, plugins con versiones heterogéneas, strategy pattern donde las implementaciones evolucionan a distinto ritmo.
85
+
86
+ ---
87
+
88
+ <a id="cache-sha256"></a>
89
+ ## 3. Caché content-addressable por SHA256 para llamadas determinísticas costosas
90
+
91
+ ### Patrón
92
+
93
+ Cuando una función pura (mismo input → mismo output) es costosa (>1 s, llamada
94
+ externa, IO pesado) y se invoca repetidamente con la misma entrada (pipelines
95
+ de ingesta, suites E2E, recompilación incremental), cachear el resultado por
96
+ el **SHA256 del contenido** del input convierte re-ejecuciones en operaciones
97
+ prácticamente gratuitas y hace el pipeline resumible.
98
+
99
+ ### Por qué SHA256 del contenido y no `(path, mtime, size)`
100
+
101
+ - `mtime` se rompe al copiar archivos entre sistemas (rsync, git checkout).
102
+ - `size` colisiona trivialmente con archivos distintos del mismo tamaño.
103
+ - `path` cambia al reorganizar el dataset.
104
+
105
+ El SHA256 del **contenido** identifica la entrada de forma reproducible aunque
106
+ se mueva, renombre o clone.
107
+
108
+ ### Implementación canónica
109
+
110
+ ```python
111
+ import hashlib
112
+ import os
113
+ from pathlib import Path
114
+
115
+
116
+ def _flag_off(env_var: str) -> bool:
117
+ """True si la env var está en {1, true, yes, on} (case-insensitive)."""
118
+ return os.environ.get(env_var, "").lower() in {"1", "true", "yes", "on"}
119
+
120
+
121
+ def sha256_bytes(contenido: bytes) -> str:
122
+ return hashlib.sha256(contenido).hexdigest()
123
+
124
+
125
+ def sha256_archivo(ruta: Path) -> str:
126
+ """SHA256 del contenido completo en bloques de 64KB."""
127
+ h = hashlib.sha256()
128
+ with ruta.open("rb") as f:
129
+ for chunk in iter(lambda: f.read(65536), b""):
130
+ h.update(chunk)
131
+ return h.hexdigest()
132
+
133
+
134
+ def procesar_con_cache(
135
+ contenido: bytes,
136
+ dir_cache: Path,
137
+ procesar,
138
+ *,
139
+ bypass_env: str = "CACHE_OFF",
140
+ dimension: str | None = None,
141
+ ) -> str:
142
+ """Ejecuta `procesar(contenido)` solo si el resultado no está en caché.
143
+
144
+ Layout en disco con sharding de 2 chars: `dir_cache/[dim/]ab/abcdef...txt`.
145
+ El sharding evita carpetas con 10k+ archivos en NTFS/ext4.
146
+ """
147
+ sha = sha256_bytes(contenido)
148
+ base = dir_cache if dimension is None else dir_cache / dimension
149
+ ruta_cache = base / sha[:2] / f"{sha}.txt"
150
+
151
+ if ruta_cache.exists() and ruta_cache.stat().st_size > 0 and not _flag_off(bypass_env):
152
+ return ruta_cache.read_text(encoding="utf-8")
153
+
154
+ # Recalcular (operación costosa: OCR, LLM, transcripción, build, etc.)
155
+ resultado = procesar(contenido)
156
+
157
+ # Escritura atómica: temp + rename para evitar entradas parciales por kill
158
+ ruta_cache.parent.mkdir(parents=True, exist_ok=True)
159
+ tmp = ruta_cache.with_suffix(".tmp")
160
+ tmp.write_text(resultado, encoding="utf-8")
161
+ tmp.rename(ruta_cache)
162
+
163
+ return resultado
164
+ ```
165
+
166
+ ### Reglas de diseño
167
+
168
+ - **Sharding de 2 chars** (`{sha[:2]}/{sha}.txt`) evita degradación del filesystem
169
+ cuando el caché crece a decenas de miles de entradas (NTFS, ext4, APFS).
170
+ - **Un directorio por tipo de operación**: `cache/ocr/`, `cache/vision/`,
171
+ `cache/transcripcion/`. Mezclar tipos hace imposible invalidar uno solo.
172
+ - **Escritura atómica obligatoria**: `tmp → rename` previene que un kill del proceso
173
+ deje entradas parciales que se interpretan como éxito en la próxima corrida.
174
+ - **Verificar tamaño > 0 al leer**: archivos de 0 bytes son ejecuciones abortadas,
175
+ no "resultado vacío válido".
176
+ - **Bypass por variable de entorno** (`CACHE_OFF=1` aceptando `1/true/yes/on`):
177
+ crítico para tests que validan el procesador real, no el caché. Sin bypass,
178
+ imposible reproducir bugs del extractor real durante pruebas.
179
+ - **Key multi-dimensión cuando aplica**: si la función es determinística sobre
180
+ `(input, modelo)` —caso típico de síntesis LLM con distinto modelo— usar el
181
+ parámetro `dimension` para particionar el caché:
182
+ `cache/sintesis/{modelo_safe}/{sha[:2]}/{sha}.txt`. Sin esto, cambiar de modelo
183
+ devuelve resultados del modelo viejo con semántica nueva.
184
+ - **Versionar el caché cuando cambia el procesamiento**: si actualizas el modelo de
185
+ OCR o la versión del prompt LLM, bumpear `dimension` (`v1` → `v2`) o invalidar
186
+ el directorio completo.
187
+ - **No cachear errores**: si `procesar()` lanza excepción, el caché queda vacío
188
+ y la siguiente corrida reintenta. Cachear el error convierte errores transitorios
189
+ en permanentes.
190
+ - **Idempotencia natural**: la escritura siempre puede sobreescribir porque el SHA
191
+ garantiza mismo contenido por construcción. No requiere lock entre procesos.
192
+
193
+ ### Tests obligatorios
194
+
195
+ Toda implementación de caché content-addressable debe cubrir:
196
+
197
+ 1. **Roundtrip write→read**: invocar dos veces con mismo input verifica que
198
+ la segunda lectura no llama al procesador (mock con counter).
199
+ 2. **Bypass por flag**: con `CACHE_OFF=1` siempre llama al procesador aunque exista entrada.
200
+ 3. **Keys distintas para inputs distintos**: dos inputs con SHA distinto no se pisan.
201
+ 4. **Fixture aislado**: `monkeypatch.setenv("CACHE_DIR", str(tmp_path))` evita
202
+ contaminación entre tests.
203
+ 5. **Atomicidad**: simular kill durante escritura (interrupción del `tmp.write_text`)
204
+ y verificar que la siguiente corrida no lee un archivo parcial.
205
+
206
+ ### Aplicabilidad
207
+
208
+ - Pipelines de ingesta de documentos (PDF → texto, imagen → OCR, audio → transcripción).
209
+ - Extracción con LLM costoso (vision, clasificación, extracción estructurada).
210
+ - Suites E2E donde el dataset es fijo y se ejecutan repetidamente —típicamente
211
+ reduce el tiempo de la corrida en 70-90% si la función cacheada domina.
212
+ - Compilaciones incrementales (aunque los build systems ya tienen este patrón).
213
+ - Deduplicación en walkers resumables (ver `testing-python`: "tests de
214
+ idempotencia requieren 2 ejecuciones + diff").
215
+
216
+ ---
217
+
218
+ <a id="f401-soft-imports"></a>
219
+ ## 4. F401 en archivos con soft imports es intencional, no ruido
220
+
221
+ ### Contexto
222
+
223
+ `ruff --select=F401` (y `flake8 --select=F401`) reportan "imported but unused"
224
+ cuando un módulo se importa pero no se usa. En archivos con **soft imports**
225
+ (intentar importar una dependencia opcional dentro de `try/except ImportError`)
226
+ el import **debe quedarse** aunque lint lo marque como no usado — es parte del
227
+ patrón de detección de disponibilidad.
228
+
229
+ ### Patrón de soft import canónico
230
+
231
+ ```python
232
+ # El módulo puede funcionar con o sin la dependencia opcional.
233
+ # La presencia del símbolo habilita un code path; su ausencia cambia al fallback.
234
+
235
+ try:
236
+ from markitdown import MarkItDown # noqa: F401 — soft import
237
+ _MARKITDOWN_DISPONIBLE = True
238
+ except ImportError:
239
+ _MARKITDOWN_DISPONIBLE = False
240
+
241
+
242
+ def extraer_texto(ruta: Path) -> str:
243
+ if _MARKITDOWN_DISPONIBLE:
244
+ from markitdown import MarkItDown # re-import local, ya sabemos que existe
245
+ return MarkItDown().convert(str(ruta)).text_content
246
+ # Fallback: usar parser básico
247
+ return _extraer_con_parser_basico(ruta)
248
+ ```
249
+
250
+ ### Regla
251
+
252
+ - Agregar `# noqa: F401` en la línea del import top-level dentro del `try`.
253
+ - Alternativa: configurar `ruff.toml` / `pyproject.toml` con `per-file-ignores` para los archivos de soft import si son muchos:
254
+ ```toml
255
+ [tool.ruff.per-file-ignores]
256
+ "core/adapters/*.py" = ["F401"] # todos los adapters usan soft imports
257
+ ```
258
+ - NO importar dentro del `try` sin usar: usar el símbolo (`MarkItDown`) como sonda de disponibilidad es el patrón correcto; el warning F401 es el ruido.
259
+ - NO reemplazar con `importlib.util.find_spec("markitdown")` salvo que la librería tenga side effects al importar — `find_spec` no valida que la versión instalada exponga los símbolos esperados.
260
+
261
+ ### Anti-patrón
262
+
263
+ ```python
264
+ # MAL — el F401 se "soluciona" pero la detección se rompe
265
+ try:
266
+ import markitdown # noqa — "no se usa, pero quiero saber si está"
267
+ _DISPONIBLE = True
268
+ except ImportError:
269
+ _DISPONIBLE = False
270
+ ```
271
+
272
+ El problema: si `markitdown` se instala pero la API cambió (el símbolo
273
+ `MarkItDown` ya no existe), el import sigue pasando y `_DISPONIBLE = True`
274
+ pero el code path fallará más tarde al usar el símbolo ausente. Importar
275
+ el símbolo específico que vas a usar es más robusto que importar el módulo
276
+ y esperar que todo lo demás funcione.
277
+
278
+ ---
279
+
280
+ <a id="regex-multi-pattern"></a>
281
+ ## 5. Detectores regex multi-pattern: extender scope sin refinar genera falsos positivos
282
+
283
+ ### NUNCA: agregar un nuevo input a un detector multi-pattern sin re-evaluar la sensibilidad de cada pattern individual
284
+
285
+ **Problema**: tienes una función `detectar(texto)` que aplica una lista de regex
286
+ con OR (`any(p.search(texto) for p in PATTERNS)`). Originalmente operaba sobre
287
+ texto curado (título, etiquetas, campos cortos). Ahora extiendes el scope a un
288
+ texto más ruidoso (prosa larga, descripciones libres, contenido de usuario).
289
+ Patterns genéricos que funcionaban en el texto curado empiezan a generar falsos
290
+ positivos en el ruidoso.
291
+
292
+ ```python
293
+ # Caso típico: detector de "enumeración múltiple de items relacionados"
294
+ PATTERNS = [
295
+ re.compile(r"(?:,\s+\w[\w\s]{3,45}){3,}"), # P1 generico: 3+ comas
296
+ re.compile(r"(?:^|\n)\s*[•\-]\s+.{10,}", re.M), # P2 generico: bullets
297
+ re.compile(r"(?:anexos|evidencias|listas){2,}", re.I), # P3 especifico
298
+ ]
299
+
300
+ # MAL — extender scope sin discriminar patterns
301
+ def enumera_multiples(registro):
302
+ texto = " ".join([
303
+ registro.titulo, registro.resumen, # texto curado
304
+ registro.descripcion_libre, # texto ruidoso (prosa larga)
305
+ ])
306
+ return any(p.search(texto) for p in PATTERNS)
307
+ # → P1 (3+ comas) matchea series de citas o referencias en prosa larga
308
+ # ("art. 5, art. 10, art. 23, art. 138 y art. 141") → falso positivo
309
+ # regresión típica observada: baja precisión sobre el dataset golden.
310
+
311
+ # BIEN — patterns por scope, refinados según ruido tolerado
312
+ def enumera_multiples(registro):
313
+ texto_curado = " ".join([registro.titulo, registro.resumen])
314
+ if any(p.search(texto_curado) for p in PATTERNS):
315
+ return True
316
+ # Sobre texto ruidoso, solo el pattern especifico (P3)
317
+ texto_ruidoso = " ".join([registro.descripcion_libre, registro.notas_libres])
318
+ return PATTERNS[2].search(texto_ruidoso) is not None
319
+ ```
320
+
321
+ ### Regla operativa
322
+
323
+ Cuando se extiende un detector regex a un nuevo scope textual, agregar un test de
324
+ regresión específico que verifique que NO se introducen falsos positivos sobre
325
+ muestras del nuevo scope que NO deberían matchear. Si los patterns genéricos
326
+ hacen FP sobre el scope nuevo, **discriminarlos por scope** (no aplicarlos al
327
+ scope ruidoso) en lugar de intentar refinarlos en una sola lista global.
328
+
329
+ **Aplicabilidad**: clasificadores de texto, detectores de PII, filtros de spam,
330
+ sistemas de moderación, extracción de entidades cuando el scope crece de campos
331
+ estructurados a contenido libre.
332
+
333
+ ---
334
+
335
+ <a id="tracer-sync"></a>
336
+ ## 6. Tracer/replicador paralelo del motor: marca SYNC obligatoria en cada cambio
337
+
338
+ ### Patrón: cuando duplicas lógica del original, marca el SYNC y agrega test de paridad
339
+
340
+ **Problema**: en sistemas con motores complejos (cascadas de reglas, clasificadores
341
+ con prioridad, motores de decisión), suele crearse un "tracer" que replica la
342
+ lógica paso a paso para producir telemetría, explicación o shadow runs. El tracer
343
+ es **duplicación** del motor — si cambias el motor sin tocar el tracer, los
344
+ outputs divergen silenciosamente y los tests del tracer pasan con datos viejos
345
+ mientras el motor real ya cambió.
346
+
347
+ ```python
348
+ # Motor real (motor_decision.py)
349
+ def resolver_resultado(self, registro):
350
+ resultado = self._resolver_core(registro)
351
+ # Ajuste 1: suavización por condición A
352
+ if cond_a(registro): return "ACEPTADO"
353
+ # Ajustes 2-4: endurecimientos
354
+ if cond_b(registro): return "RECHAZADO"
355
+ # Ajuste 5 (NUEVO): endurecimiento por condición compuesta
356
+ if resultado == "ACEPTADO" and registro._compuesta:
357
+ return "RECHAZADO"
358
+ return resultado
359
+
360
+ # Tracer paralelo (motor_decision_tracer.py)
361
+ def replicar_resultado(...):
362
+ # SYNC con: motor_decision.py::resolver_resultado
363
+ # Si agregas un Ajuste, AGREGARLO TAMBIÉN aquí en el orden correcto.
364
+ if cond_a(registro): ...
365
+ if cond_b(registro): ...
366
+ # Olvidé sincronizar Ajuste 5 aquí → tracer reporta ACEPTADO
367
+ # pero motor real reporta RECHAZADO → tests ground truth fallan
368
+ # silenciosamente porque el tracer es lo que se compara.
369
+ ```
370
+
371
+ ### Reglas obligatorias para código duplicado deliberado
372
+
373
+ 1. **Marca de SYNC**: comentario `# SYNC con: <archivo>:<función>` visible en la
374
+ cabecera del replicador. La búsqueda `grep -rn "SYNC con:"` lista todos los
375
+ puntos de duplicación del repositorio en una corrida.
376
+ 2. **Test de paridad**: para casos representativos del dataset golden, comparar
377
+ `motor.resolver(x) == tracer.resolver(x)` y fallar si difieren. Es el único
378
+ gate que detecta la divergencia sin esperar a producción.
379
+ 3. **Convención de naming**: el archivo que duplica la lógica se nombra explícitamente
380
+ como derivado (`_tracer.py`, `_replicador.py`, `_explainer.py`, `_shadow.py`).
381
+ NUNCA esconderlo bajo nombre genérico — el nombre es la primera línea de defensa
382
+ contra que alguien lo edite sin saber que es duplicación.
383
+ 4. **Owner único**: el motor y su tracer cambian en el mismo PR. Code review rechaza
384
+ PRs que tocan el motor sin tocar el tracer (o que justifiquen explícitamente
385
+ por qué la divergencia es intencional, ej. "el tracer no necesita el ajuste de
386
+ performance").
387
+
388
+ **Aplicabilidad**: sistemas con explainability, dual-track production+shadow,
389
+ motores de reglas con logging detallado, A/B testing de algoritmos, migración
390
+ incremental de un motor legacy a uno nuevo donde ambos corren en paralelo.
391
+
392
+ ---
393
+
394
+ <a id="fixtures-crudos"></a>
395
+ ## 7. Fixtures de test con datos pre-procesados NO ejercen el path de datos crudos
396
+
397
+ ### Anti-patrón: fixtures construidos a mano que reflejan la salida del extractor, no su entrada
398
+
399
+ **Problema**: el sistema en producción procesa input crudo (PDFs, HTMLs, JSON
400
+ externos, mensajes raw) que pasa por extractores que producen un dict condensado.
401
+ Los fixtures de test se construyen "a mano" copiando lo que el extractor produce
402
+ (o aproximándolo). Resultado: cualquier path del motor que solo se activa con
403
+ campos del input crudo (presentes solo cuando hay extractor real arriba) NO se
404
+ ejercita por los tests, y los bugs aparecen únicamente en producción.
405
+
406
+ ```python
407
+ # Fixture típico hecho a mano (apariencia inocente)
408
+ {
409
+ "id": "REG-001",
410
+ "registro": {
411
+ "titulo": "Falta de evidencia documental",
412
+ "descripcion_corta": "El operador manifiesta que el oficio acredita...", # condensada
413
+ # NOTAR: no hay `descripcion_original`
414
+ }
415
+ }
416
+
417
+ # Motor con fix nuevo
418
+ def _clasificar_postura(registro):
419
+ # Fix: prefiere texto crudo cuando está disponible
420
+ texto = (
421
+ registro.get("descripcion_original")
422
+ or registro.get("descripcion_corta")
423
+ )
424
+ return any(p in texto.lower() for p in patrones_subsanadores)
425
+
426
+ # Test del fixture pasa "por casualidad" (descripcion_corta condensada
427
+ # casualmente contiene "se anexa") → falsa confianza.
428
+ # En producción, descripcion_original sería 5000 chars con verbos
429
+ # canónicos y descripcion_corta sería paráfrasis sin esos verbos.
430
+ # Los tests no ejercitan el path real.
431
+ ```
432
+
433
+ ### Defensa
434
+
435
+ Cuando se introduce un nuevo campo opcional que el motor prefiere leer
436
+ (`descripcion_original`, `metadata_completa`, `raw_input`, `body_unparsed`,
437
+ etc.), agregar **al menos un fixture** que tenga ese campo poblado con una
438
+ muestra representativa del input crudo real — idealmente extraída del caché de
439
+ un pipeline E2E ejecutado, no inventada a mano.
440
+
441
+ ```python
442
+ # Mejorado: fixture con ambos campos, con datos representativos del scope crudo
443
+ {
444
+ "registro": {
445
+ "descripcion_corta": "El operador manifiesta...", # condensada (lo que el extractor produce)
446
+ "descripcion_original": (
447
+ "OFICIO 12345/2026 — DEPARTAMENTO DE OPERACIONES ... "
448
+ "se anexa el oficio mediante correo institucional ... "
449
+ "[texto crudo extraído del PDF, 5000 chars con jerga canónica]"
450
+ ),
451
+ }
452
+ }
453
+ ```
454
+
455
+ ### Regla operativa
456
+
457
+ - **Dos niveles de fixtures**: condensados (rápidos, para lógica de negocio) y
458
+ crudos (lentos, para paths que dependen del input real). Marcar cada fixture
459
+ con su nivel: `nombre.condensado.json` vs `nombre.crudo.json`.
460
+ - **Snapshot del extractor real**: si el extractor es determinístico, ejecutarlo
461
+ una vez sobre datos reales y persistir su output como fixture crudo. No inventarlo.
462
+ - **Test de "campo prioritario nuevo"**: cada vez que el motor agrega un campo
463
+ con prioridad sobre uno existente, agregar un test que verifique el comportamiento
464
+ con AMBOS campos poblados con valores **divergentes** (que producen resultados
465
+ distintos). Si el test pasa con valores iguales, no prueba la priorización.
466
+
467
+ **Aplicabilidad**: pipelines de ingesta con extractores upstream, sistemas con
468
+ adaptadores que normalizan inputs heterogéneos, motores que prefieren campos
469
+ crudos sobre derivados, tests de regresión de migraciones de schema.