@saulwade/swl-ses 2.3.0 → 2.3.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 (303) hide show
  1. package/CLAUDE.md +47 -6
  2. package/README.md +1 -1
  3. package/agentes/accesibilidad-wcag-swl.md +690 -690
  4. package/agentes/arquitecto-swl.md +267 -267
  5. package/agentes/auto-evolucion-swl.md +908 -908
  6. package/agentes/backend-api-swl.md +472 -472
  7. package/agentes/backend-csharp-swl.md +420 -420
  8. package/agentes/backend-go-swl.md +390 -390
  9. package/agentes/backend-java-swl.md +281 -281
  10. package/agentes/backend-node-swl.md +479 -479
  11. package/agentes/backend-python-swl.md +605 -605
  12. package/agentes/backend-rust-swl.md +364 -364
  13. package/agentes/backend-workers-swl.md +482 -482
  14. package/agentes/cloud-infra-swl.md +509 -509
  15. package/agentes/consolidador-swl.md +541 -541
  16. package/agentes/datos-swl.md +605 -605
  17. package/agentes/depurador-swl.md +352 -352
  18. package/agentes/devops-ci-swl.md +400 -400
  19. package/agentes/disenador-ui-swl.md +569 -569
  20. package/agentes/documentador-swl.md +345 -345
  21. package/agentes/frontend-angular-swl.md +621 -621
  22. package/agentes/frontend-css-swl.md +716 -716
  23. package/agentes/frontend-react-swl.md +692 -692
  24. package/agentes/frontend-swl.md +496 -496
  25. package/agentes/frontend-tailwind-swl.md +826 -826
  26. package/agentes/gh-fix-ci-swl.md +2 -2
  27. package/agentes/implementador-swl.md +328 -328
  28. package/agentes/investigador-swl.md +432 -432
  29. package/agentes/investigador-ux-swl.md +505 -505
  30. package/agentes/llm-apps-swl.md +278 -278
  31. package/agentes/migrador-swl.md +442 -442
  32. package/agentes/mobile-android-swl.md +511 -511
  33. package/agentes/mobile-cross-swl.md +541 -541
  34. package/agentes/mobile-ios-swl.md +502 -502
  35. package/agentes/mobile-testing-swl.md +302 -302
  36. package/agentes/nemesis-auditor-swl.md +285 -285
  37. package/agentes/notificador-swl.md +918 -918
  38. package/agentes/observabilidad-swl.md +438 -438
  39. package/agentes/orquestador-swl.md +1026 -1026
  40. package/agentes/pagos-swl.md +310 -310
  41. package/agentes/perfilador-usuario-swl.md +321 -321
  42. package/agentes/planificador-swl.md +399 -399
  43. package/agentes/producto-prd-swl.md +589 -589
  44. package/agentes/red-team-swl.md +1 -1
  45. package/agentes/release-manager-swl.md +590 -590
  46. package/agentes/rendimiento-swl.md +713 -713
  47. package/agentes/resolutor-build-swl.md +245 -245
  48. package/agentes/revisor-angular-swl.md +278 -278
  49. package/agentes/revisor-codigo-swl.md +505 -505
  50. package/agentes/revisor-csharp-swl.md +264 -264
  51. package/agentes/revisor-go-swl.md +259 -259
  52. package/agentes/revisor-java-swl.md +257 -257
  53. package/agentes/revisor-kotlin-swl.md +273 -273
  54. package/agentes/revisor-nextjs-swl.md +281 -281
  55. package/agentes/revisor-php-swl.md +271 -271
  56. package/agentes/revisor-react-swl.md +278 -278
  57. package/agentes/revisor-rust-swl.md +346 -346
  58. package/agentes/revisor-seguridad-swl.md +399 -399
  59. package/agentes/revisor-swift-swl.md +268 -268
  60. package/agentes/revisor-typescript-swl.md +346 -346
  61. package/agentes/sre-swl.md +291 -291
  62. package/agentes/tdd-qa-swl.md +393 -393
  63. package/comandos/swl/briefing.md +119 -119
  64. package/comandos/swl/contribuir.md +233 -233
  65. package/comandos/swl/predecir.md +139 -139
  66. package/comandos/swl/sesiones.md +1 -1
  67. package/gateway/agent-executor.js +1 -1
  68. package/gateway/lib/event-channel.js +191 -191
  69. package/habilidades/agent-deep-links/SKILL.md +148 -148
  70. package/habilidades/agentes-como-servicio/SKILL.md +2 -2
  71. package/habilidades/angular-moderno/SKILL.md +20 -1
  72. package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
  73. package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
  74. package/habilidades/backend-error-design/SKILL.md +221 -221
  75. package/habilidades/backend-production-resilience/SKILL.md +288 -288
  76. package/habilidades/benchmark-memoria/SKILL.md +186 -186
  77. package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
  78. package/habilidades/calidad-contract-testing/SKILL.md +165 -165
  79. package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
  80. package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
  81. package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
  82. package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
  83. package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
  84. package/habilidades/drift-detection/SKILL.md +179 -179
  85. package/habilidades/ejecutar-task-iterativo/SKILL.md +278 -278
  86. package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
  87. package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
  88. package/habilidades/eval-framework/SKILL.md +212 -212
  89. package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
  90. package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +252 -252
  91. package/habilidades/legacy-code-rescue/SKILL.md +267 -267
  92. package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
  93. package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
  94. package/habilidades/perfil-usuario/SKILL.md +200 -200
  95. package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
  96. package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
  97. package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
  98. package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
  99. package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
  100. package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
  101. package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
  102. package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
  103. package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
  104. package/habilidades/proceso-modular-split/SKILL.md +256 -256
  105. package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
  106. package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
  107. package/habilidades/structured-outputs/SKILL.md +2 -2
  108. package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
  109. package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
  110. package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
  111. package/habilidades/swl-dashboard/SKILL.md +2 -2
  112. package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
  113. package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
  114. package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
  115. package/hooks/ciclo-evolucion-subagente.js +26 -26
  116. package/hooks/ciclo-evolucion.js +26 -26
  117. package/hooks/claudemd-bloat-detector.js +161 -161
  118. package/hooks/claudemd-duplicacion-detector.js +170 -170
  119. package/hooks/contexto-iteracion.js +144 -144
  120. package/hooks/guardrail-modelo.js +1 -1
  121. package/hooks/lib/agent-routing.js +107 -107
  122. package/hooks/lib/auto-consolidator.js +335 -335
  123. package/hooks/lib/autonomia.js +208 -208
  124. package/hooks/lib/ciclo-evolucion.js +47 -47
  125. package/hooks/lib/deep-links.js +185 -185
  126. package/hooks/lib/error-classifier.js +308 -308
  127. package/hooks/lib/etapa-auto-evolucion.js +701 -701
  128. package/hooks/lib/etapa-metricas.js +388 -388
  129. package/hooks/lib/etapa-perfil-usuario.js +376 -376
  130. package/hooks/lib/loop-telemetry.js +321 -321
  131. package/hooks/lib/merkle-audit.js +96 -96
  132. package/hooks/lib/model-router.js +2 -2
  133. package/hooks/lib/propose-step.js +358 -358
  134. package/hooks/lib/provenance-tracker.js +191 -191
  135. package/hooks/lib/rate-limit-tracker.js +253 -253
  136. package/hooks/lib/resource-quota.js +122 -122
  137. package/hooks/lib/retry-jitter.js +165 -165
  138. package/hooks/lib/security-net.js +201 -201
  139. package/hooks/lib/skill-auditor.js +588 -588
  140. package/hooks/lib/sync-status.js +228 -228
  141. package/hooks/lib/taint-tracker.js +107 -107
  142. package/hooks/lib/text-similarity.js +241 -241
  143. package/hooks/lib/token-estimator.js +1 -0
  144. package/hooks/lib/toon-compressor.js +245 -245
  145. package/hooks/lib/usage-model.js +1 -1
  146. package/hooks/linea-estado.js +2 -0
  147. package/hooks/registro-turnos.js +209 -209
  148. package/hooks/session-briefing.js +98 -98
  149. package/hooks/spec-gate.js +211 -211
  150. package/hooks/sugerir-regenerar-inventario.js +170 -170
  151. package/hooks/tdd-gate.js +241 -241
  152. package/hooks/telemetria-skill-routing.js +100 -100
  153. package/hooks/validar-formato-post-subagente.js +140 -140
  154. package/hooks/validar-intent-spec.js +242 -242
  155. package/hooks/validar-memoria-hook.js +218 -218
  156. package/hooks/validar-planning-paths.js +134 -134
  157. package/instintos/autonomia.yaml +27 -27
  158. package/instintos/prompt-appendices.yaml +57 -57
  159. package/llms.txt +29 -29
  160. package/manifiestos/agent-output-schemas.json +57 -57
  161. package/manifiestos/canonical-hashes.json +2291 -1964
  162. package/manifiestos/planning-paths.json +44 -44
  163. package/manifiestos/skills-lock.json +1282 -1282
  164. package/package.json +1 -1
  165. package/plantillas/auditor-veto-template.md +105 -105
  166. package/plantillas/github-workflows/README.md +47 -47
  167. package/plantillas/github-workflows/release-please.yml +44 -44
  168. package/plantillas/github-workflows/swl-ci.yml +107 -107
  169. package/plantillas/github-workflows/swl-security.yml +51 -51
  170. package/plugin.json +1 -1
  171. package/reglas/accesibilidad.md +10 -10
  172. package/reglas/analisis-previo-tareas-grandes.md +172 -172
  173. package/reglas/api-diseno.md +9 -9
  174. package/reglas/arreglar-al-detectar.md +240 -240
  175. package/reglas/auditorias-documentales-estructurales.md +7 -7
  176. package/reglas/cloud-infra.md +8 -8
  177. package/reglas/consultar-vault-primero.md +195 -195
  178. package/reglas/debatir-antes-de-aceptar.md +158 -158
  179. package/reglas/fragmentos-compartidos.md +183 -183
  180. package/reglas/hooks.md +6 -6
  181. package/reglas/intent-engineering.md +218 -218
  182. package/reglas/markitdown.md +8 -8
  183. package/reglas/monitor-ci.md +309 -309
  184. package/reglas/patrones.md +6 -6
  185. package/reglas/sesiones-paralelas.md +180 -180
  186. package/reglas/sin-duplicacion-reglas-globales.md +182 -182
  187. package/reglas/skills-estandar.md +6 -6
  188. package/reglas/testing.md +7 -7
  189. package/reglas/tests-cleanup.md +224 -224
  190. package/reglas/usar-code-review-graph.md +155 -155
  191. package/reglas/usar-context7.md +226 -226
  192. package/reglas/verificar-citas-normativas.md +548 -548
  193. package/schemas/agent-frontmatter.schema.json +3 -3
  194. package/schemas/agent-message.schema.json +73 -73
  195. package/schemas/agent-output-implementacion.schema.json +114 -114
  196. package/schemas/agent-output-planificacion.schema.json +150 -150
  197. package/schemas/agent-output-review.schema.json +98 -98
  198. package/schemas/diary-entry.schema.json +112 -112
  199. package/schemas/hook-profiles.schema.json +54 -54
  200. package/schemas/hooks-config.schema.json +89 -89
  201. package/schemas/modulos.schema.json +38 -38
  202. package/schemas/perfiles.schema.json +36 -36
  203. package/schemas/plugin.schema.json +77 -77
  204. package/schemas/skill-evals.schema.json +119 -119
  205. package/schemas/skill-frontmatter.schema.json +245 -245
  206. package/scripts/audit-tools/audit-history.js +330 -330
  207. package/scripts/audit-tools/bundle-tracker.js +290 -290
  208. package/scripts/audit-tools/canary-monitor.js +352 -352
  209. package/scripts/audit-tools/code-profiler.js +605 -605
  210. package/scripts/audit-tools/dep-doctor.js +320 -320
  211. package/scripts/audit-tools/env-validator.js +206 -206
  212. package/scripts/audit-tools/lib/fs-walk.js +48 -48
  213. package/scripts/audit-tools/lib/output.js +23 -23
  214. package/scripts/audit-tools/migration-checker.js +392 -392
  215. package/scripts/audit-tools/pentest-scanner.js +1436 -1436
  216. package/scripts/benchmark-memoria.js +167 -167
  217. package/scripts/cli/aprobar-plan.js +73 -73
  218. package/scripts/cli/briefing.js +23 -23
  219. package/scripts/cli/ciclo-evolucion.js +26 -26
  220. package/scripts/cli/configurar-ci.js +40 -40
  221. package/scripts/cli/derivar-feature-list.js +25 -25
  222. package/scripts/cli/detectar-host.js +27 -27
  223. package/scripts/cli/diary-entry.js +69 -69
  224. package/scripts/cli/execution-state.js +18 -18
  225. package/scripts/cli/gateway-notify.js +41 -41
  226. package/scripts/cli/liberar-fase.js +42 -42
  227. package/scripts/cli/loop-telemetry.js +125 -125
  228. package/scripts/cli/mark-evolved.js +56 -56
  229. package/scripts/cli/metricas-dora.js +26 -26
  230. package/scripts/cli/near-duplicate.js +55 -55
  231. package/scripts/cli/notificaciones.js +123 -123
  232. package/scripts/cli/propose-step.js +29 -29
  233. package/scripts/cli/schedule-parse.js +19 -19
  234. package/scripts/cli/sugerir-modelo.js +20 -20
  235. package/scripts/cli/verificar-plan.js +36 -36
  236. package/scripts/cli/verificar-trazabilidad.js +35 -35
  237. package/scripts/configurar-branch-protection.js +418 -418
  238. package/scripts/detectar-aprendizajes-duplicados.js +151 -151
  239. package/scripts/field-report.js +199 -199
  240. package/scripts/generar-checklists-consolidados.js +273 -273
  241. package/scripts/generar-matriz-lenguajes.js +271 -271
  242. package/scripts/lib/artefactos-python.js +43 -43
  243. package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
  244. package/scripts/lib/benchmark-metrics.js +160 -160
  245. package/scripts/lib/budget-enforcer.js +252 -252
  246. package/scripts/lib/ci-reader.js +193 -193
  247. package/scripts/lib/claude-sessions.js +1 -0
  248. package/scripts/lib/configurar-ci.js +380 -380
  249. package/scripts/lib/contadores-inventario.js +217 -217
  250. package/scripts/lib/detectar-host-swl.js +175 -175
  251. package/scripts/lib/detectar-stack-detallado.js +307 -307
  252. package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
  253. package/scripts/lib/detector-reglas-duplicadas.js +220 -220
  254. package/scripts/lib/diary-entry.js +234 -234
  255. package/scripts/lib/eval-metrics-store.js +218 -218
  256. package/scripts/lib/eval-quality.js +171 -171
  257. package/scripts/lib/eval-schemas.js +144 -144
  258. package/scripts/lib/eval-self-correct.js +106 -106
  259. package/scripts/lib/eval-validator.js +185 -185
  260. package/scripts/lib/evidencia-release.js +322 -322
  261. package/scripts/lib/expandir-targets.js +71 -71
  262. package/scripts/lib/gate-hooks-requires.js +249 -249
  263. package/scripts/lib/gate-licencias.js +212 -212
  264. package/scripts/lib/git-metricas.js +257 -257
  265. package/scripts/lib/jaccard-similarity.js +98 -98
  266. package/scripts/lib/longmemeval-runner.js +125 -125
  267. package/scripts/lib/metricas-dora.js +204 -204
  268. package/scripts/lib/npm-version.js +261 -261
  269. package/scripts/lib/paquetes-conocidos.js +50 -50
  270. package/scripts/lib/plan-lock.js +275 -275
  271. package/scripts/lib/pr-analyzer.js +399 -399
  272. package/scripts/lib/prompt-builder.js +264 -264
  273. package/scripts/lib/reglas-globales-conocidas.json +180 -180
  274. package/scripts/lib/resolver-plan-fase.js +37 -37
  275. package/scripts/lib/rrf-fusion.js +175 -175
  276. package/scripts/lib/schema-version.js +164 -164
  277. package/scripts/lib/scoring-instintos.js +277 -277
  278. package/scripts/lib/semantic-search.js +252 -252
  279. package/scripts/lib/toml-merge.js +204 -204
  280. package/scripts/lib/tool-cost-analyzer.js +1 -0
  281. package/scripts/limpiar-artefactos-python.js +131 -131
  282. package/scripts/mcp-server/auth.js +105 -105
  283. package/scripts/mcp-server/cache.js +106 -106
  284. package/scripts/migrar-csv-a-array.js +168 -168
  285. package/scripts/migrar-fase-dominio.js +200 -200
  286. package/scripts/publicar.js +497 -497
  287. package/scripts/run-eval.js +141 -141
  288. package/scripts/tui/componentes/selector-multi.js +189 -189
  289. package/scripts/tui/componentes/selector-unico.js +158 -158
  290. package/scripts/tui/ejecutores.js +375 -375
  291. package/scripts/tui/index.js +162 -162
  292. package/scripts/tui/lib/colores.js +129 -129
  293. package/scripts/tui/lib/render.js +264 -264
  294. package/scripts/tui/lib/teclas.js +113 -113
  295. package/scripts/tui/pantallas/inspect.js +173 -173
  296. package/scripts/tui/pantallas/install-wizard.js +334 -334
  297. package/scripts/tui/pantallas/menu-principal.js +52 -52
  298. package/scripts/tui/pantallas/progreso.js +274 -274
  299. package/scripts/tui/pantallas/resumen.js +132 -132
  300. package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
  301. package/scripts/tui/pantallas/update-wizard.js +232 -232
  302. package/scripts/tui/pantallas/welcome.js +187 -187
  303. package/scripts/validar-userland-vacio.js +110 -110
@@ -1,358 +1,358 @@
1
- 'use strict';
2
-
3
- /**
4
- * hooks/lib/propose-step.js — Fase 13 (ADR-0037): propose-step de adyacencias.
5
- *
6
- * Separa PROPONER de ACTUAR: al cerrar una tarea/fase, evalúa el diff contra una
7
- * checklist mecanizable de adyacencias de riesgo y emite un anexo PROPOSITIVO.
8
- * Nunca bloquea, nunca ejecuta. El anexo es texto; el usuario decide si actúa.
9
- *
10
- * Taxonomía v1 (D-13-01): 2 señales de alta precisión.
11
- * - auth-pii-pagos: el cambio toca autenticación, PII o pagos.
12
- * - migracion-schema: el cambio introduce o modifica el esquema de datos.
13
- *
14
- * Telemetría de aceptación en archivo separado .planning/user-profile/
15
- * propose-telemetria.json, reusando la lógica pura de hooks/lib/briefing.js
16
- * (D-13-07). Opt-out con SWL_PROPOSE=0.
17
- *
18
- * Zero-deps (Node stdlib). Require dual ./lib/X → ./X para funcionar tanto en el
19
- * repo madre como en el destino aplanado por el instalador (patrón D-17 de F12).
20
- */
21
-
22
- const fs = require('fs');
23
- const path = require('path');
24
- const { execFileSync } = require('child_process');
25
-
26
- // ─── require dual (repo madre: ./lib/X ; destino aplanado: ./X) ──────────────
27
-
28
- function requireDual(nombre) {
29
- try {
30
- return require(`./${nombre}`); // repo madre (mismo dir) o destino aplanado
31
- } catch (e1) {
32
- if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
33
- return require(`./lib/${nombre}`);
34
- }
35
- }
36
-
37
- let atomicWriteJSON;
38
- try {
39
- ({ atomicWriteJSON } = requireDual('atomic-write'));
40
- } catch (_) {
41
- // Fallback no-atómico: funciona, pierde la garantía de write atómico.
42
- atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
43
- }
44
-
45
- let briefing;
46
- try {
47
- briefing = requireDual('briefing');
48
- } catch (_) {
49
- briefing = null; // sin telemetría compartida; la detección sigue funcionando
50
- }
51
-
52
- // ─── detectores de señales ───────────────────────────────────────────────────
53
-
54
- // Patrones de auth/PII/pagos. Acotados a alta precisión: word boundaries donde
55
- // el término es ambiguo (rfc, card, cvv, pago) para no disparar con prosa.
56
- const RE_AUTH_PII_DIFF = new RegExp(
57
- [
58
- 'password', 'passwd', 'contraseña',
59
- '\\btoken\\b', 'secret', 'api[_-]?key', '\\bjwt\\b', 'oauth',
60
- 'authorization', '\\bbearer\\b', '\\bcredential', '\\bsession\\b',
61
- 'stripe', 'payment', '\\bpago\\b', '\\bpagos\\b', 'tarjeta',
62
- '\\bcvv\\b', '\\bcard\\b', '\\bcurp\\b', '\\brfc\\b',
63
- ].join('|'),
64
- 'i',
65
- );
66
-
67
- const RE_AUTH_PII_PATH = new RegExp(
68
- '(^|/)(auth|login|logout|oauth|jwt|session|credential|credentials|password|payment|pagos?|stripe|checkout)([/._-]|$)',
69
- 'i',
70
- );
71
-
72
- // Patrones de migración / esquema de datos.
73
- const RE_SCHEMA_PATH = new RegExp(
74
- '(^|/)(migrations?|alembic|models?|schema|prisma)([/._-]|$)|\\.sql$|schema\\.prisma$',
75
- 'i',
76
- );
77
-
78
- const RE_SCHEMA_DIFF = new RegExp(
79
- [
80
- '\\bALTER\\s+TABLE\\b', '\\bCREATE\\s+TABLE\\b', '\\bDROP\\s+TABLE\\b',
81
- '\\bADD\\s+COLUMN\\b', '\\bDROP\\s+COLUMN\\b', '\\bRENAME\\s+(TABLE|COLUMN)\\b',
82
- 'op\\.(create_table|add_column|drop_column|alter_column)',
83
- 'createTable|addColumn|dropColumn', // ORMs JS (knex, sequelize)
84
- ].join('|'),
85
- 'i',
86
- );
87
-
88
- function _primerPathQueMatchea(paths, re) {
89
- if (!Array.isArray(paths)) return null;
90
- for (const p of paths) {
91
- if (typeof p === 'string' && re.test(p)) return p;
92
- }
93
- return null;
94
- }
95
-
96
- // Cota de tamaño del diff antes de aplicar regex: acota ReDoS y memoria. El
97
- // detector solo necesita el primer match, así que 2 MiB son más que suficientes.
98
- const MAX_DIFF_SCAN = 2 * 1024 * 1024;
99
-
100
- // Devuelve SOLO si el diff matchea (boolean), nunca el fragmento matcheado: la
101
- // evidencia del anexo NO debe contener slices del diff (podrían arrastrar el
102
- // secreto adyacente al keyword). Hardening por construcción.
103
- function _diffMatchea(diff, re) {
104
- if (typeof diff !== 'string') return false;
105
- return re.test(diff.length > MAX_DIFF_SCAN ? diff.slice(0, MAX_DIFF_SCAN) : diff);
106
- }
107
-
108
- /**
109
- * Detecta si el cambio toca autenticación, PII o pagos.
110
- * @param {string[]} paths - rutas de archivos del diff.
111
- * @param {string} diff - contenido del diff.
112
- * @returns {null|{categoria,titulo,evidencia,accion}}
113
- */
114
- function detectarAuthPiiPagos(paths, diff) {
115
- const porPath = _primerPathQueMatchea(paths, RE_AUTH_PII_PATH);
116
- const porDiff = porPath ? false : _diffMatchea(diff, RE_AUTH_PII_DIFF);
117
- if (!porPath && !porDiff) return null;
118
- return {
119
- categoria: 'auth-pii-pagos',
120
- titulo: 'El cambio toca autenticación, PII o pagos',
121
- evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
122
- accion:
123
- 'Confirma revisión de seguridad (revisor-seguridad-swl) y tests de ' +
124
- 'autorización/validación antes de cerrar; no expongas secretos en logs.',
125
- };
126
- }
127
-
128
- /**
129
- * Detecta si el cambio introduce o modifica el esquema de datos.
130
- * @param {string[]} paths
131
- * @param {string} diff
132
- * @returns {null|{categoria,titulo,evidencia,accion}}
133
- */
134
- function detectarMigracionSchema(paths, diff) {
135
- const porPath = _primerPathQueMatchea(paths, RE_SCHEMA_PATH);
136
- const porDiff = porPath ? false : _diffMatchea(diff, RE_SCHEMA_DIFF);
137
- if (!porPath && !porDiff) return null;
138
- return {
139
- categoria: 'migracion-schema',
140
- titulo: 'El cambio introduce o modifica el esquema de datos',
141
- evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
142
- accion:
143
- 'Confirma plan de rollback / expand-contract y reversibilidad de la ' +
144
- 'migración (migrador-swl); verifica que no rompe datos existentes.',
145
- };
146
- }
147
-
148
- const DETECTORES = [detectarAuthPiiPagos, detectarMigracionSchema];
149
-
150
- /**
151
- * Evalúa todas las señales sobre un diff. Función pura: solo datos, sin side
152
- * effects (REQ-13-04).
153
- * @param {string[]} paths
154
- * @param {string} diff
155
- * @returns {{señales: Array<{categoria,titulo,evidencia,accion}>}}
156
- */
157
- function evaluarSenales(paths, diff) {
158
- const señales = [];
159
- for (const detectar of DETECTORES) {
160
- const s = detectar(paths, diff);
161
- if (s) señales.push(s);
162
- }
163
- return { señales };
164
- }
165
-
166
- // ─── anexo propositivo ───────────────────────────────────────────────────────
167
-
168
- /**
169
- * Formatea el anexo propositivo. Devuelve null si no hay señales activas (silencio
170
- * total, D-13-03) o si todas las categorías están silenciadas.
171
- * @param {Array} señales
172
- * @param {Set<string>} silenciadas - categorías que la telemetría silenció.
173
- * @returns {string|null}
174
- */
175
- function formatearAnexo(señales, silenciadas) {
176
- const sil = silenciadas instanceof Set ? silenciadas : new Set();
177
- const activas = Array.isArray(señales) ? señales.filter((s) => s && !sil.has(s.categoria)) : [];
178
- if (activas.length === 0) return null;
179
- const lineas = [
180
- '## Anexo propositivo — adyacencias de riesgo',
181
- '',
182
- 'Sugerencias, **no acciones**: nada se ejecuta ni se bloquea automáticamente. ' +
183
- 'Revisa si aplican.',
184
- '',
185
- ];
186
- for (const s of activas) {
187
- lineas.push(`- [${s.categoria}] ${s.titulo} (${s.evidencia}) → ${s.accion}`);
188
- }
189
- return lineas.join('\n');
190
- }
191
-
192
- // ─── telemetría de aceptación (archivo separado, REQ-13-08/09) ───────────────
193
- //
194
- // Modelo a nivel de CATEGORÍA (no por hash). Las señales del propose tienen
195
- // título fijo por categoría → el conteo por-hash de briefing.actualizarTelemetria
196
- // nunca alcanzaría MIN_MUESTRAS_SILENCIO. Se reusan los UMBRALES de briefing.js
197
- // (D-13-07) y categoriasSilenciadas, pero el conteo es por exposición:
198
- // - registrarPropose: mostrado += 1 por categoría mostrada (automático).
199
- // - registrarFeedback: actuado/ignorado += 1 (canal de aceptación explícito).
200
- // - silenciada se recomputa con los mismos umbrales que el briefing.
201
-
202
- const TELE_PATH = ['.planning', 'user-profile', 'propose-telemetria.json'];
203
-
204
- // Umbrales: reusar los de briefing.js; fallback a los mismos valores si la lib
205
- // no está disponible en el destino.
206
- const UMBRAL = {
207
- RATIO_SILENCIO: (briefing && briefing.RATIO_SILENCIO) || 0.8,
208
- MIN_MUESTRAS_SILENCIO: (briefing && briefing.MIN_MUESTRAS_SILENCIO) || 5,
209
- };
210
-
211
- function telemetriaPath(baseDir) {
212
- return path.join(baseDir || process.cwd(), ...TELE_PATH);
213
- }
214
-
215
- function _catVacia() {
216
- return { mostrado: 0, actuado: 0, ignorado: 0, silenciada: false, ultima_ts: null };
217
- }
218
-
219
- /** Lee la telemetría de propose; fallback a estructura vacía. */
220
- function leerTelemetriaPropose(baseDir) {
221
- try {
222
- const raw = fs.readFileSync(telemetriaPath(baseDir), 'utf8');
223
- const obj = JSON.parse(raw);
224
- return { categorias: obj.categorias && typeof obj.categorias === 'object' ? obj.categorias : {} };
225
- } catch (_) {
226
- return { categorias: {} };
227
- }
228
- }
229
-
230
- /** Recalcula silenciada con los umbrales del briefing. Muta la categoría dada. */
231
- function _recomputarSilenciada(c) {
232
- c.silenciada =
233
- c.mostrado >= UMBRAL.MIN_MUESTRAS_SILENCIO &&
234
- c.ignorado / c.mostrado >= UMBRAL.RATIO_SILENCIO;
235
- return c;
236
- }
237
-
238
- function _persistir(baseDir, tele) {
239
- try {
240
- const dir = path.dirname(telemetriaPath(baseDir));
241
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
242
- atomicWriteJSON(telemetriaPath(baseDir), tele);
243
- } catch (_) {
244
- // persistir es best-effort; no romper el cierre de la tarea.
245
- }
246
- }
247
-
248
- /**
249
- * Registra la exposición de un anexo: mostrado += 1 por cada categoría presente.
250
- * @param {string} baseDir
251
- * @param {Array} señales
252
- * @param {string} hoyISO - timestamp inyectable para tests deterministas.
253
- * @returns {{categorias}}
254
- */
255
- function registrarPropose(baseDir, señales, hoyISO) {
256
- const tele = leerTelemetriaPropose(baseDir);
257
- const hoy = hoyISO || new Date().toISOString();
258
- for (const s of Array.isArray(señales) ? señales : []) {
259
- if (!s || !s.categoria) continue;
260
- const c = tele.categorias[s.categoria] || _catVacia();
261
- c.mostrado += 1;
262
- c.ultima_ts = hoy;
263
- _recomputarSilenciada(c);
264
- tele.categorias[s.categoria] = c;
265
- }
266
- _persistir(baseDir, tele);
267
- return tele;
268
- }
269
-
270
- /**
271
- * Registra feedback de aceptación de una categoría (canal explícito).
272
- * @param {string} baseDir
273
- * @param {string} categoria
274
- * @param {'actuado'|'ignorado'} tipo
275
- * @param {string} hoyISO
276
- * @returns {{categorias}}
277
- */
278
- function registrarFeedback(baseDir, categoria, tipo, hoyISO) {
279
- if (tipo !== 'actuado' && tipo !== 'ignorado') {
280
- throw new Error(`registrarFeedback: tipo inválido "${tipo}" (esperado actuado|ignorado)`);
281
- }
282
- const tele = leerTelemetriaPropose(baseDir);
283
- const c = tele.categorias[categoria] || _catVacia();
284
- c[tipo] += 1;
285
- c.ultima_ts = hoyISO || new Date().toISOString();
286
- _recomputarSilenciada(c);
287
- tele.categorias[categoria] = c;
288
- _persistir(baseDir, tele);
289
- return tele;
290
- }
291
-
292
- /** Set de categorías silenciadas según la telemetría de propose. */
293
- function categoriasSilenciadasPropose(baseDir) {
294
- const tele = leerTelemetriaPropose(baseDir);
295
- if (briefing && briefing.categoriasSilenciadas) {
296
- return briefing.categoriasSilenciadas(tele);
297
- }
298
- const set = new Set();
299
- for (const [cat, c] of Object.entries(tele.categorias || {})) {
300
- if (c && c.silenciada) set.add(cat);
301
- }
302
- return set;
303
- }
304
-
305
- // ─── CLI ──────────────────────────────────────────────────────────────────────
306
-
307
- const MAX_DIFF_BUFFER = 32 * 1024 * 1024; // 32 MiB — tope de salida de git diff
308
- // Rango git válido: refs/SHAs y rangos `a..b` / `a...b`. Rechaza flags (`--output`,
309
- // `-G`) que git interpretaría aunque execFileSync evita el shell.
310
- const RE_RANGO_VALIDO = /^[A-Za-z0-9_./@~^-]+(\.\.\.?[A-Za-z0-9_./@~^-]+)?$/;
311
-
312
- function _gitDiff(rango) {
313
- // execFileSync con array de args: no pasa por shell. Además se valida el rango
314
- // y se usa el separador `--` para forzar que git lo trate como ref, no flag.
315
- const r = rango || 'HEAD~1..HEAD';
316
- if (!RE_RANGO_VALIDO.test(r)) return { paths: [], diff: '' };
317
- try {
318
- const paths = execFileSync('git', ['diff', '--name-only', r, '--'], { encoding: 'utf8' })
319
- .split('\n').map((s) => s.trim()).filter(Boolean);
320
- const diff = execFileSync('git', ['diff', r, '--'], { encoding: 'utf8', maxBuffer: MAX_DIFF_BUFFER });
321
- return { paths, diff };
322
- } catch (_) {
323
- return { paths: [], diff: '' };
324
- }
325
- }
326
-
327
- function main(argv) {
328
- // Opt-out: SWL_PROPOSE=0 → silencio inmediato.
329
- if (process.env.SWL_PROPOSE === '0') return 0;
330
- const args = argv.slice(2);
331
- let rango = 'HEAD~1..HEAD';
332
- for (const a of args) {
333
- if (a.startsWith('--rango=')) rango = a.slice('--rango='.length);
334
- }
335
- const baseDir = process.cwd();
336
- const { paths, diff } = _gitDiff(rango);
337
- const { señales } = evaluarSenales(paths, diff);
338
- registrarPropose(baseDir, señales);
339
- const anexo = formatearAnexo(señales, categoriasSilenciadasPropose(baseDir));
340
- if (anexo) process.stdout.write(anexo + '\n');
341
- return 0;
342
- }
343
-
344
- if (require.main === module) {
345
- process.exit(main(process.argv));
346
- }
347
-
348
- module.exports = {
349
- detectarAuthPiiPagos,
350
- detectarMigracionSchema,
351
- evaluarSenales,
352
- formatearAnexo,
353
- leerTelemetriaPropose,
354
- registrarPropose,
355
- registrarFeedback,
356
- categoriasSilenciadasPropose,
357
- main,
358
- };
1
+ 'use strict';
2
+
3
+ /**
4
+ * hooks/lib/propose-step.js — Fase 13 (ADR-0037): propose-step de adyacencias.
5
+ *
6
+ * Separa PROPONER de ACTUAR: al cerrar una tarea/fase, evalúa el diff contra una
7
+ * checklist mecanizable de adyacencias de riesgo y emite un anexo PROPOSITIVO.
8
+ * Nunca bloquea, nunca ejecuta. El anexo es texto; el usuario decide si actúa.
9
+ *
10
+ * Taxonomía v1 (D-13-01): 2 señales de alta precisión.
11
+ * - auth-pii-pagos: el cambio toca autenticación, PII o pagos.
12
+ * - migracion-schema: el cambio introduce o modifica el esquema de datos.
13
+ *
14
+ * Telemetría de aceptación en archivo separado .planning/user-profile/
15
+ * propose-telemetria.json, reusando la lógica pura de hooks/lib/briefing.js
16
+ * (D-13-07). Opt-out con SWL_PROPOSE=0.
17
+ *
18
+ * Zero-deps (Node stdlib). Require dual ./lib/X → ./X para funcionar tanto en el
19
+ * repo madre como en el destino aplanado por el instalador (patrón D-17 de F12).
20
+ */
21
+
22
+ const fs = require('fs');
23
+ const path = require('path');
24
+ const { execFileSync } = require('child_process');
25
+
26
+ // ─── require dual (repo madre: ./lib/X ; destino aplanado: ./X) ──────────────
27
+
28
+ function requireDual(nombre) {
29
+ try {
30
+ return require(`./${nombre}`); // repo madre (mismo dir) o destino aplanado
31
+ } catch (e1) {
32
+ if (e1 && e1.code !== 'MODULE_NOT_FOUND') throw e1;
33
+ return require(`./lib/${nombre}`);
34
+ }
35
+ }
36
+
37
+ let atomicWriteJSON;
38
+ try {
39
+ ({ atomicWriteJSON } = requireDual('atomic-write'));
40
+ } catch (_) {
41
+ // Fallback no-atómico: funciona, pierde la garantía de write atómico.
42
+ atomicWriteJSON = (p, o) => fs.writeFileSync(p, JSON.stringify(o, null, 2), 'utf8');
43
+ }
44
+
45
+ let briefing;
46
+ try {
47
+ briefing = requireDual('briefing');
48
+ } catch (_) {
49
+ briefing = null; // sin telemetría compartida; la detección sigue funcionando
50
+ }
51
+
52
+ // ─── detectores de señales ───────────────────────────────────────────────────
53
+
54
+ // Patrones de auth/PII/pagos. Acotados a alta precisión: word boundaries donde
55
+ // el término es ambiguo (rfc, card, cvv, pago) para no disparar con prosa.
56
+ const RE_AUTH_PII_DIFF = new RegExp(
57
+ [
58
+ 'password', 'passwd', 'contraseña',
59
+ '\\btoken\\b', 'secret', 'api[_-]?key', '\\bjwt\\b', 'oauth',
60
+ 'authorization', '\\bbearer\\b', '\\bcredential', '\\bsession\\b',
61
+ 'stripe', 'payment', '\\bpago\\b', '\\bpagos\\b', 'tarjeta',
62
+ '\\bcvv\\b', '\\bcard\\b', '\\bcurp\\b', '\\brfc\\b',
63
+ ].join('|'),
64
+ 'i',
65
+ );
66
+
67
+ const RE_AUTH_PII_PATH = new RegExp(
68
+ '(^|/)(auth|login|logout|oauth|jwt|session|credential|credentials|password|payment|pagos?|stripe|checkout)([/._-]|$)',
69
+ 'i',
70
+ );
71
+
72
+ // Patrones de migración / esquema de datos.
73
+ const RE_SCHEMA_PATH = new RegExp(
74
+ '(^|/)(migrations?|alembic|models?|schema|prisma)([/._-]|$)|\\.sql$|schema\\.prisma$',
75
+ 'i',
76
+ );
77
+
78
+ const RE_SCHEMA_DIFF = new RegExp(
79
+ [
80
+ '\\bALTER\\s+TABLE\\b', '\\bCREATE\\s+TABLE\\b', '\\bDROP\\s+TABLE\\b',
81
+ '\\bADD\\s+COLUMN\\b', '\\bDROP\\s+COLUMN\\b', '\\bRENAME\\s+(TABLE|COLUMN)\\b',
82
+ 'op\\.(create_table|add_column|drop_column|alter_column)',
83
+ 'createTable|addColumn|dropColumn', // ORMs JS (knex, sequelize)
84
+ ].join('|'),
85
+ 'i',
86
+ );
87
+
88
+ function _primerPathQueMatchea(paths, re) {
89
+ if (!Array.isArray(paths)) return null;
90
+ for (const p of paths) {
91
+ if (typeof p === 'string' && re.test(p)) return p;
92
+ }
93
+ return null;
94
+ }
95
+
96
+ // Cota de tamaño del diff antes de aplicar regex: acota ReDoS y memoria. El
97
+ // detector solo necesita el primer match, así que 2 MiB son más que suficientes.
98
+ const MAX_DIFF_SCAN = 2 * 1024 * 1024;
99
+
100
+ // Devuelve SOLO si el diff matchea (boolean), nunca el fragmento matcheado: la
101
+ // evidencia del anexo NO debe contener slices del diff (podrían arrastrar el
102
+ // secreto adyacente al keyword). Hardening por construcción.
103
+ function _diffMatchea(diff, re) {
104
+ if (typeof diff !== 'string') return false;
105
+ return re.test(diff.length > MAX_DIFF_SCAN ? diff.slice(0, MAX_DIFF_SCAN) : diff);
106
+ }
107
+
108
+ /**
109
+ * Detecta si el cambio toca autenticación, PII o pagos.
110
+ * @param {string[]} paths - rutas de archivos del diff.
111
+ * @param {string} diff - contenido del diff.
112
+ * @returns {null|{categoria,titulo,evidencia,accion}}
113
+ */
114
+ function detectarAuthPiiPagos(paths, diff) {
115
+ const porPath = _primerPathQueMatchea(paths, RE_AUTH_PII_PATH);
116
+ const porDiff = porPath ? false : _diffMatchea(diff, RE_AUTH_PII_DIFF);
117
+ if (!porPath && !porDiff) return null;
118
+ return {
119
+ categoria: 'auth-pii-pagos',
120
+ titulo: 'El cambio toca autenticación, PII o pagos',
121
+ evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
122
+ accion:
123
+ 'Confirma revisión de seguridad (revisor-seguridad-swl) y tests de ' +
124
+ 'autorización/validación antes de cerrar; no expongas secretos en logs.',
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Detecta si el cambio introduce o modifica el esquema de datos.
130
+ * @param {string[]} paths
131
+ * @param {string} diff
132
+ * @returns {null|{categoria,titulo,evidencia,accion}}
133
+ */
134
+ function detectarMigracionSchema(paths, diff) {
135
+ const porPath = _primerPathQueMatchea(paths, RE_SCHEMA_PATH);
136
+ const porDiff = porPath ? false : _diffMatchea(diff, RE_SCHEMA_DIFF);
137
+ if (!porPath && !porDiff) return null;
138
+ return {
139
+ categoria: 'migracion-schema',
140
+ titulo: 'El cambio introduce o modifica el esquema de datos',
141
+ evidencia: porPath ? `path: ${porPath}` : 'patrón detectado en el diff',
142
+ accion:
143
+ 'Confirma plan de rollback / expand-contract y reversibilidad de la ' +
144
+ 'migración (migrador-swl); verifica que no rompe datos existentes.',
145
+ };
146
+ }
147
+
148
+ const DETECTORES = [detectarAuthPiiPagos, detectarMigracionSchema];
149
+
150
+ /**
151
+ * Evalúa todas las señales sobre un diff. Función pura: solo datos, sin side
152
+ * effects (REQ-13-04).
153
+ * @param {string[]} paths
154
+ * @param {string} diff
155
+ * @returns {{señales: Array<{categoria,titulo,evidencia,accion}>}}
156
+ */
157
+ function evaluarSenales(paths, diff) {
158
+ const señales = [];
159
+ for (const detectar of DETECTORES) {
160
+ const s = detectar(paths, diff);
161
+ if (s) señales.push(s);
162
+ }
163
+ return { señales };
164
+ }
165
+
166
+ // ─── anexo propositivo ───────────────────────────────────────────────────────
167
+
168
+ /**
169
+ * Formatea el anexo propositivo. Devuelve null si no hay señales activas (silencio
170
+ * total, D-13-03) o si todas las categorías están silenciadas.
171
+ * @param {Array} señales
172
+ * @param {Set<string>} silenciadas - categorías que la telemetría silenció.
173
+ * @returns {string|null}
174
+ */
175
+ function formatearAnexo(señales, silenciadas) {
176
+ const sil = silenciadas instanceof Set ? silenciadas : new Set();
177
+ const activas = Array.isArray(señales) ? señales.filter((s) => s && !sil.has(s.categoria)) : [];
178
+ if (activas.length === 0) return null;
179
+ const lineas = [
180
+ '## Anexo propositivo — adyacencias de riesgo',
181
+ '',
182
+ 'Sugerencias, **no acciones**: nada se ejecuta ni se bloquea automáticamente. ' +
183
+ 'Revisa si aplican.',
184
+ '',
185
+ ];
186
+ for (const s of activas) {
187
+ lineas.push(`- [${s.categoria}] ${s.titulo} (${s.evidencia}) → ${s.accion}`);
188
+ }
189
+ return lineas.join('\n');
190
+ }
191
+
192
+ // ─── telemetría de aceptación (archivo separado, REQ-13-08/09) ───────────────
193
+ //
194
+ // Modelo a nivel de CATEGORÍA (no por hash). Las señales del propose tienen
195
+ // título fijo por categoría → el conteo por-hash de briefing.actualizarTelemetria
196
+ // nunca alcanzaría MIN_MUESTRAS_SILENCIO. Se reusan los UMBRALES de briefing.js
197
+ // (D-13-07) y categoriasSilenciadas, pero el conteo es por exposición:
198
+ // - registrarPropose: mostrado += 1 por categoría mostrada (automático).
199
+ // - registrarFeedback: actuado/ignorado += 1 (canal de aceptación explícito).
200
+ // - silenciada se recomputa con los mismos umbrales que el briefing.
201
+
202
+ const TELE_PATH = ['.planning', 'user-profile', 'propose-telemetria.json'];
203
+
204
+ // Umbrales: reusar los de briefing.js; fallback a los mismos valores si la lib
205
+ // no está disponible en el destino.
206
+ const UMBRAL = {
207
+ RATIO_SILENCIO: (briefing && briefing.RATIO_SILENCIO) || 0.8,
208
+ MIN_MUESTRAS_SILENCIO: (briefing && briefing.MIN_MUESTRAS_SILENCIO) || 5,
209
+ };
210
+
211
+ function telemetriaPath(baseDir) {
212
+ return path.join(baseDir || process.cwd(), ...TELE_PATH);
213
+ }
214
+
215
+ function _catVacia() {
216
+ return { mostrado: 0, actuado: 0, ignorado: 0, silenciada: false, ultima_ts: null };
217
+ }
218
+
219
+ /** Lee la telemetría de propose; fallback a estructura vacía. */
220
+ function leerTelemetriaPropose(baseDir) {
221
+ try {
222
+ const raw = fs.readFileSync(telemetriaPath(baseDir), 'utf8');
223
+ const obj = JSON.parse(raw);
224
+ return { categorias: obj.categorias && typeof obj.categorias === 'object' ? obj.categorias : {} };
225
+ } catch (_) {
226
+ return { categorias: {} };
227
+ }
228
+ }
229
+
230
+ /** Recalcula silenciada con los umbrales del briefing. Muta la categoría dada. */
231
+ function _recomputarSilenciada(c) {
232
+ c.silenciada =
233
+ c.mostrado >= UMBRAL.MIN_MUESTRAS_SILENCIO &&
234
+ c.ignorado / c.mostrado >= UMBRAL.RATIO_SILENCIO;
235
+ return c;
236
+ }
237
+
238
+ function _persistir(baseDir, tele) {
239
+ try {
240
+ const dir = path.dirname(telemetriaPath(baseDir));
241
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
242
+ atomicWriteJSON(telemetriaPath(baseDir), tele);
243
+ } catch (_) {
244
+ // persistir es best-effort; no romper el cierre de la tarea.
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Registra la exposición de un anexo: mostrado += 1 por cada categoría presente.
250
+ * @param {string} baseDir
251
+ * @param {Array} señales
252
+ * @param {string} hoyISO - timestamp inyectable para tests deterministas.
253
+ * @returns {{categorias}}
254
+ */
255
+ function registrarPropose(baseDir, señales, hoyISO) {
256
+ const tele = leerTelemetriaPropose(baseDir);
257
+ const hoy = hoyISO || new Date().toISOString();
258
+ for (const s of Array.isArray(señales) ? señales : []) {
259
+ if (!s || !s.categoria) continue;
260
+ const c = tele.categorias[s.categoria] || _catVacia();
261
+ c.mostrado += 1;
262
+ c.ultima_ts = hoy;
263
+ _recomputarSilenciada(c);
264
+ tele.categorias[s.categoria] = c;
265
+ }
266
+ _persistir(baseDir, tele);
267
+ return tele;
268
+ }
269
+
270
+ /**
271
+ * Registra feedback de aceptación de una categoría (canal explícito).
272
+ * @param {string} baseDir
273
+ * @param {string} categoria
274
+ * @param {'actuado'|'ignorado'} tipo
275
+ * @param {string} hoyISO
276
+ * @returns {{categorias}}
277
+ */
278
+ function registrarFeedback(baseDir, categoria, tipo, hoyISO) {
279
+ if (tipo !== 'actuado' && tipo !== 'ignorado') {
280
+ throw new Error(`registrarFeedback: tipo inválido "${tipo}" (esperado actuado|ignorado)`);
281
+ }
282
+ const tele = leerTelemetriaPropose(baseDir);
283
+ const c = tele.categorias[categoria] || _catVacia();
284
+ c[tipo] += 1;
285
+ c.ultima_ts = hoyISO || new Date().toISOString();
286
+ _recomputarSilenciada(c);
287
+ tele.categorias[categoria] = c;
288
+ _persistir(baseDir, tele);
289
+ return tele;
290
+ }
291
+
292
+ /** Set de categorías silenciadas según la telemetría de propose. */
293
+ function categoriasSilenciadasPropose(baseDir) {
294
+ const tele = leerTelemetriaPropose(baseDir);
295
+ if (briefing && briefing.categoriasSilenciadas) {
296
+ return briefing.categoriasSilenciadas(tele);
297
+ }
298
+ const set = new Set();
299
+ for (const [cat, c] of Object.entries(tele.categorias || {})) {
300
+ if (c && c.silenciada) set.add(cat);
301
+ }
302
+ return set;
303
+ }
304
+
305
+ // ─── CLI ──────────────────────────────────────────────────────────────────────
306
+
307
+ const MAX_DIFF_BUFFER = 32 * 1024 * 1024; // 32 MiB — tope de salida de git diff
308
+ // Rango git válido: refs/SHAs y rangos `a..b` / `a...b`. Rechaza flags (`--output`,
309
+ // `-G`) que git interpretaría aunque execFileSync evita el shell.
310
+ const RE_RANGO_VALIDO = /^[A-Za-z0-9_./@~^-]+(\.\.\.?[A-Za-z0-9_./@~^-]+)?$/;
311
+
312
+ function _gitDiff(rango) {
313
+ // execFileSync con array de args: no pasa por shell. Además se valida el rango
314
+ // y se usa el separador `--` para forzar que git lo trate como ref, no flag.
315
+ const r = rango || 'HEAD~1..HEAD';
316
+ if (!RE_RANGO_VALIDO.test(r)) return { paths: [], diff: '' };
317
+ try {
318
+ const paths = execFileSync('git', ['diff', '--name-only', r, '--'], { encoding: 'utf8' })
319
+ .split('\n').map((s) => s.trim()).filter(Boolean);
320
+ const diff = execFileSync('git', ['diff', r, '--'], { encoding: 'utf8', maxBuffer: MAX_DIFF_BUFFER });
321
+ return { paths, diff };
322
+ } catch (_) {
323
+ return { paths: [], diff: '' };
324
+ }
325
+ }
326
+
327
+ function main(argv) {
328
+ // Opt-out: SWL_PROPOSE=0 → silencio inmediato.
329
+ if (process.env.SWL_PROPOSE === '0') return 0;
330
+ const args = argv.slice(2);
331
+ let rango = 'HEAD~1..HEAD';
332
+ for (const a of args) {
333
+ if (a.startsWith('--rango=')) rango = a.slice('--rango='.length);
334
+ }
335
+ const baseDir = process.cwd();
336
+ const { paths, diff } = _gitDiff(rango);
337
+ const { señales } = evaluarSenales(paths, diff);
338
+ registrarPropose(baseDir, señales);
339
+ const anexo = formatearAnexo(señales, categoriasSilenciadasPropose(baseDir));
340
+ if (anexo) process.stdout.write(anexo + '\n');
341
+ return 0;
342
+ }
343
+
344
+ if (require.main === module) {
345
+ process.exit(main(process.argv));
346
+ }
347
+
348
+ module.exports = {
349
+ detectarAuthPiiPagos,
350
+ detectarMigracionSchema,
351
+ evaluarSenales,
352
+ formatearAnexo,
353
+ leerTelemetriaPropose,
354
+ registrarPropose,
355
+ registrarFeedback,
356
+ categoriasSilenciadasPropose,
357
+ main,
358
+ };