@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.
- package/CLAUDE.md +241 -199
- package/README.md +597 -597
- package/agentes/_intent-spec.md +73 -73
- package/agentes/_propose-step.md +90 -90
- package/agentes/accesibilidad-wcag-swl.md +2 -2
- package/agentes/arquitecto-swl.md +2 -2
- package/agentes/auto-evolucion-swl.md +2 -2
- package/agentes/backend-api-swl.md +2 -2
- package/agentes/backend-csharp-swl.md +2 -2
- package/agentes/backend-go-swl.md +2 -2
- package/agentes/backend-java-swl.md +2 -2
- package/agentes/backend-node-swl.md +2 -2
- package/agentes/backend-python-swl.md +2 -2
- package/agentes/backend-rust-swl.md +2 -2
- package/agentes/backend-workers-swl.md +2 -2
- package/agentes/cloud-infra-swl.md +2 -2
- package/agentes/consolidador-swl.md +2 -2
- package/agentes/datos-swl.md +2 -2
- package/agentes/depurador-swl.md +2 -2
- package/agentes/devops-ci-swl.md +2 -2
- package/agentes/disenador-ui-swl.md +2 -2
- package/agentes/documentador-swl.md +2 -2
- package/agentes/frontend-angular-swl.md +2 -2
- package/agentes/frontend-css-swl.md +2 -2
- package/agentes/frontend-react-swl.md +2 -2
- package/agentes/frontend-swl.md +2 -2
- package/agentes/frontend-tailwind-swl.md +2 -2
- package/agentes/gh-fix-ci-swl.md +275 -275
- package/agentes/implementador-swl.md +2 -2
- package/agentes/investigador-swl.md +2 -2
- package/agentes/investigador-ux-swl.md +2 -2
- package/agentes/llm-apps-swl.md +2 -2
- package/agentes/migrador-swl.md +2 -2
- package/agentes/mobile-android-swl.md +2 -2
- package/agentes/mobile-cross-swl.md +2 -2
- package/agentes/mobile-ios-swl.md +2 -2
- package/agentes/mobile-testing-swl.md +2 -2
- package/agentes/nemesis-auditor-swl.md +1 -1
- package/agentes/notificador-swl.md +2 -2
- package/agentes/observabilidad-swl.md +2 -2
- package/agentes/orquestador-swl.md +41 -14
- package/agentes/pagos-swl.md +2 -2
- package/agentes/perfilador-usuario-swl.md +2 -2
- package/agentes/planificador-swl.md +2 -2
- package/agentes/producto-prd-swl.md +2 -2
- package/agentes/red-team-swl.md +218 -218
- package/agentes/release-manager-swl.md +2 -2
- package/agentes/rendimiento-swl.md +2 -2
- package/agentes/resolutor-build-swl.md +2 -2
- package/agentes/revisor-angular-swl.md +2 -2
- package/agentes/revisor-codigo-swl.md +36 -3
- package/agentes/revisor-csharp-swl.md +2 -2
- package/agentes/revisor-go-swl.md +2 -2
- package/agentes/revisor-java-swl.md +2 -2
- package/agentes/revisor-kotlin-swl.md +2 -2
- package/agentes/revisor-nextjs-swl.md +2 -2
- package/agentes/revisor-php-swl.md +2 -2
- package/agentes/revisor-react-swl.md +2 -2
- package/agentes/revisor-rust-swl.md +2 -2
- package/agentes/revisor-seguridad-swl.md +2 -2
- package/agentes/revisor-swift-swl.md +2 -2
- package/agentes/revisor-typescript-swl.md +2 -2
- package/agentes/sre-swl.md +2 -2
- package/agentes/tdd-qa-swl.md +2 -2
- package/comandos/swl/briefing.md +119 -119
- package/comandos/swl/contexto.md +0 -2
- package/comandos/swl/contribuir.md +233 -233
- package/comandos/swl/deuda-codigo.md +97 -0
- package/comandos/swl/predecir.md +139 -139
- package/comandos/swl/sesiones.md +1 -1
- package/gateway/agent-executor.js +1 -1
- package/gateway/lib/event-channel.js +191 -191
- package/habilidades/agent-deep-links/SKILL.md +148 -148
- package/habilidades/agentes-como-servicio/SKILL.md +2 -2
- package/habilidades/angular-moderno/SKILL.md +24 -1
- package/habilidades/auto-evolucion-protocolo/SKILL.md +276 -276
- package/habilidades/backend-async-postgres-testing/SKILL.md +215 -215
- package/habilidades/backend-error-design/SKILL.md +221 -221
- package/habilidades/backend-production-resilience/SKILL.md +288 -288
- package/habilidades/benchmark-memoria/SKILL.md +186 -186
- package/habilidades/calidad-anti-patrones-universales/SKILL.md +368 -368
- package/habilidades/calidad-contract-testing/SKILL.md +165 -165
- package/habilidades/calidad-mutation-testing/SKILL.md +170 -170
- package/habilidades/changelog-generator/scripts/parse-commits.js +367 -367
- package/habilidades/checklist-seguridad/recursos/stride-cobertura.md +60 -60
- package/habilidades/contenedores-docker/SKILL.md +3 -1
- package/habilidades/diagrama-arquitectura/assets/template.html +276 -276
- package/habilidades/doubt-driven-review/recursos/EXAMPLES.md +130 -130
- package/habilidades/drift-detection/SKILL.md +179 -179
- package/habilidades/estructura-proyecto-claude/recursos/frontmatter-y-hooks-referencia.md +1 -1
- package/habilidades/estructura-proyecto-claude/recursos/mcp-json-template.json +57 -57
- package/habilidades/eval-framework/SKILL.md +212 -212
- package/habilidades/feynman-auditor-swl/recursos/preguntas-language-agnostic.md +108 -108
- package/habilidades/harness-claude-code/SKILL.md +4 -4
- package/habilidades/harness-claude-code/recursos/disciplina-harness-regla.md +253 -252
- package/habilidades/legacy-code-rescue/SKILL.md +268 -267
- package/habilidades/meta-skills-estandar/SKILL.md +1 -1
- package/habilidades/meta-skills-estandar/recursos/convencion-examples.md +93 -93
- package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +2 -2
- package/habilidades/patrones-python/recursos/patrones-avanzados.md +469 -469
- package/habilidades/perfil-usuario/SKILL.md +200 -200
- package/habilidades/postgresql-experto/SKILL.md +3 -1
- package/habilidades/prevencion-sobreingenieria/SKILL.md +70 -92
- package/habilidades/prevencion-sobreingenieria/recursos/EXAMPLES.md +580 -580
- package/habilidades/prevencion-sobreingenieria/recursos/soluciones-nativas.md +166 -0
- package/habilidades/prevencion-sobreingenieria/recursos/variables-residuales-post-refactor.md +85 -0
- package/habilidades/proceso-ddia-fundamentos/SKILL.md +255 -255
- package/habilidades/proceso-ddia-streaming/SKILL.md +231 -231
- package/habilidades/proceso-debate-adversarial/recursos/personas.md +105 -105
- package/habilidades/proceso-discovery-machote/SKILL.md +157 -157
- package/habilidades/proceso-dynamic-workflows/SKILL.md +138 -138
- package/habilidades/proceso-dynamic-workflows/recursos/template-adversarial-verify.js +65 -65
- package/habilidades/proceso-dynamic-workflows/recursos/template-triage.js +65 -65
- package/habilidades/proceso-intent-engineering/SKILL.md +269 -269
- package/habilidades/proceso-modular-split/SKILL.md +256 -256
- package/habilidades/prompt-engineering/recursos/patrones-avanzados.md +2 -2
- package/habilidades/state-inconsistency-auditor-swl/recursos/coupled-state-patterns.md +147 -147
- package/habilidades/structured-outputs/SKILL.md +2 -2
- package/habilidades/swl-claudemd/recursos/contrato-aprender.md +83 -83
- package/habilidades/swl-claudemd/recursos/duplicacion-reglas-globales.md +85 -85
- package/habilidades/swl-claudemd/recursos/plantillas-init.md +94 -94
- package/habilidades/swl-dashboard/SKILL.md +2 -2
- package/habilidades/tdd-workflow/recursos/gherkin-bdd.md +111 -111
- package/habilidades/validacion-ci-sistema/recursos/validadores-completos.md +1 -1
- package/habilidades/validacion-ci-sistema/scripts/validar-sistema.sh +1 -1
- package/hooks/audit-trail.js +4 -4
- package/hooks/calidad-pre-commit.js +15 -11
- package/hooks/captura-acciones-post.js +3 -2
- package/hooks/captura-acciones-session.js +3 -2
- package/hooks/ciclo-evolucion-subagente.js +26 -26
- package/hooks/ciclo-evolucion.js +26 -26
- package/hooks/claudemd-bloat-detector.js +161 -161
- package/hooks/claudemd-duplicacion-detector.js +170 -170
- package/hooks/contexto-iteracion.js +145 -144
- package/hooks/contexto-subagente.js +68 -0
- package/hooks/guardrail-modelo.js +14 -4
- package/hooks/inyeccion-contexto.js +12 -12
- package/hooks/lib/agent-routing.js +107 -107
- package/hooks/lib/auto-consolidator.js +335 -335
- package/hooks/lib/autonomia.js +208 -208
- package/hooks/lib/ciclo-evolucion.js +47 -47
- package/hooks/lib/deep-links.js +185 -185
- package/hooks/lib/error-classifier.js +308 -308
- package/hooks/lib/etapa-auto-evolucion.js +701 -701
- package/hooks/lib/etapa-metricas.js +388 -388
- package/hooks/lib/etapa-perfil-usuario.js +376 -376
- package/hooks/lib/loop-telemetry.js +321 -321
- package/hooks/lib/merkle-audit.js +96 -96
- package/hooks/lib/model-router.js +2 -2
- package/hooks/lib/propose-step.js +358 -358
- package/hooks/lib/provenance-tracker.js +191 -191
- package/hooks/lib/rate-limit-tracker.js +253 -253
- package/hooks/lib/resource-quota.js +122 -122
- package/hooks/lib/retry-jitter.js +165 -165
- package/hooks/lib/security-net.js +201 -201
- package/hooks/lib/skill-auditor.js +588 -588
- package/hooks/lib/sync-status.js +228 -228
- package/hooks/lib/taint-tracker.js +107 -107
- package/hooks/lib/text-similarity.js +241 -241
- package/hooks/lib/token-estimator.js +1 -0
- package/hooks/lib/toon-compressor.js +245 -245
- package/hooks/lib/usage-model.js +1 -1
- package/hooks/registro-turnos.js +210 -209
- package/hooks/session-briefing.js +98 -98
- package/hooks/spec-gate.js +212 -211
- package/hooks/sugerir-contribuir.js +2 -1
- package/hooks/sugerir-regenerar-inventario.js +171 -170
- package/hooks/tdd-gate.js +242 -241
- package/hooks/telemetria-skill-routing.js +100 -100
- package/hooks/validar-formato-post-subagente.js +140 -140
- package/hooks/validar-intent-spec.js +242 -242
- package/hooks/validar-memoria-hook.js +218 -218
- package/hooks/validar-planning-paths.js +134 -134
- package/instintos/autonomia.yaml +27 -27
- package/instintos/prompt-appendices.yaml +57 -57
- package/llms.txt +29 -29
- package/manifiestos/agent-output-schemas.json +57 -57
- package/manifiestos/canonical-hashes.json +2948 -1964
- package/manifiestos/hook-profiles.json +0 -2
- package/manifiestos/hooks-config.json +469 -477
- package/manifiestos/invariantes-criticos.json +30 -0
- package/manifiestos/modulos.json +1390 -1390
- package/manifiestos/planning-paths.json +44 -44
- package/manifiestos/skills-lock.json +1282 -1282
- package/package.json +93 -93
- package/plantillas/auditor-veto-template.md +105 -105
- package/plantillas/github-workflows/README.md +47 -47
- package/plantillas/github-workflows/release-please.yml +44 -44
- package/plantillas/github-workflows/swl-ci.yml +107 -107
- package/plantillas/github-workflows/swl-security.yml +51 -51
- package/plugin.json +369 -371
- package/reglas/accesibilidad.md +10 -10
- package/reglas/analisis-previo-tareas-grandes.md +172 -172
- package/reglas/api-diseno.md +9 -9
- package/reglas/arreglar-al-detectar.md +240 -240
- package/reglas/auditorias-documentales-estructurales.md +7 -7
- package/reglas/cloud-infra.md +8 -8
- package/reglas/consultar-vault-primero.md +195 -195
- package/reglas/debatir-antes-de-aceptar.md +158 -158
- package/reglas/fragmentos-compartidos.md +183 -183
- package/reglas/hooks.md +6 -6
- package/reglas/intent-engineering.md +218 -218
- package/reglas/markitdown.md +8 -8
- package/reglas/monitor-ci.md +309 -309
- package/reglas/patrones.md +6 -6
- package/reglas/sesiones-paralelas.md +180 -180
- package/reglas/sin-duplicacion-reglas-globales.md +182 -182
- package/reglas/skills-estandar.md +6 -6
- package/reglas/testing.md +7 -7
- package/reglas/tests-cleanup.md +224 -224
- package/reglas/usar-code-review-graph.md +155 -155
- package/reglas/usar-context7.md +226 -226
- package/reglas/verificar-citas-normativas.md +548 -548
- package/schemas/agent-frontmatter.schema.json +5 -3
- package/schemas/agent-message.schema.json +73 -73
- package/schemas/agent-output-implementacion.schema.json +114 -114
- package/schemas/agent-output-planificacion.schema.json +150 -150
- package/schemas/agent-output-review.schema.json +98 -98
- package/schemas/diary-entry.schema.json +112 -112
- package/schemas/hook-profiles.schema.json +54 -54
- package/schemas/hooks-config.schema.json +89 -89
- package/schemas/modulos.schema.json +38 -38
- package/schemas/perfiles.schema.json +36 -36
- package/schemas/plugin.schema.json +77 -77
- package/schemas/skill-evals.schema.json +119 -119
- package/schemas/skill-frontmatter.schema.json +245 -245
- package/scripts/audit-tools/audit-history.js +330 -330
- package/scripts/audit-tools/bundle-tracker.js +290 -290
- package/scripts/audit-tools/canary-monitor.js +352 -352
- package/scripts/audit-tools/code-profiler.js +605 -605
- package/scripts/audit-tools/dep-doctor.js +320 -320
- package/scripts/audit-tools/env-validator.js +206 -206
- package/scripts/audit-tools/lib/fs-walk.js +48 -48
- package/scripts/audit-tools/lib/output.js +23 -23
- package/scripts/audit-tools/migration-checker.js +392 -392
- package/scripts/audit-tools/pentest-scanner.js +1436 -1436
- package/scripts/benchmark-memoria.js +167 -167
- package/scripts/cli/aprobar-plan.js +73 -73
- package/scripts/cli/briefing.js +23 -23
- package/scripts/cli/ciclo-evolucion.js +26 -26
- package/scripts/cli/configurar-ci.js +40 -40
- package/scripts/cli/derivar-feature-list.js +25 -25
- package/scripts/cli/detectar-host.js +27 -27
- package/scripts/cli/diary-entry.js +69 -69
- package/scripts/cli/execution-state.js +18 -18
- package/scripts/cli/gateway-notify.js +41 -41
- package/scripts/cli/liberar-fase.js +42 -42
- package/scripts/cli/loop-telemetry.js +125 -125
- package/scripts/cli/mark-evolved.js +56 -56
- package/scripts/cli/metricas-dora.js +26 -26
- package/scripts/cli/near-duplicate.js +55 -55
- package/scripts/cli/notificaciones.js +123 -123
- package/scripts/cli/propose-step.js +29 -29
- package/scripts/cli/schedule-parse.js +19 -19
- package/scripts/cli/sugerir-modelo.js +20 -20
- package/scripts/cli/verificar-plan.js +36 -36
- package/scripts/cli/verificar-trazabilidad.js +35 -35
- package/scripts/configurar-branch-protection.js +418 -418
- package/scripts/detectar-aprendizajes-duplicados.js +151 -151
- package/scripts/field-report.js +199 -199
- package/scripts/generar-checklists-consolidados.js +273 -273
- package/scripts/generar-matriz-lenguajes.js +271 -271
- package/scripts/instalador.js +4 -1
- package/scripts/lib/artefactos-python.js +43 -43
- package/scripts/lib/auditar-invocaciones-comandos.js +104 -104
- package/scripts/lib/benchmark-metrics.js +160 -160
- package/scripts/lib/budget-enforcer.js +252 -252
- package/scripts/lib/ci-reader.js +193 -193
- package/scripts/lib/claude-sessions.js +1 -0
- package/scripts/lib/configurar-ci.js +380 -380
- package/scripts/lib/contadores-inventario.js +217 -217
- package/scripts/lib/detectar-host-swl.js +175 -175
- package/scripts/lib/detectar-stack-detallado.js +307 -307
- package/scripts/lib/detector-autoduplicacion-intra-archivo.js +234 -234
- package/scripts/lib/detector-reglas-duplicadas.js +220 -220
- package/scripts/lib/diary-entry.js +234 -234
- package/scripts/lib/eval-metrics-store.js +218 -218
- package/scripts/lib/eval-quality.js +171 -171
- package/scripts/lib/eval-schemas.js +144 -144
- package/scripts/lib/eval-self-correct.js +106 -106
- package/scripts/lib/eval-validator.js +185 -185
- package/scripts/lib/evidencia-release.js +322 -322
- package/scripts/lib/gate-hooks-requires.js +249 -249
- package/scripts/lib/gate-licencias.js +212 -212
- package/scripts/lib/git-metricas.js +257 -257
- package/scripts/lib/jaccard-similarity.js +98 -98
- package/scripts/lib/longmemeval-runner.js +125 -125
- package/scripts/lib/metricas-dora.js +204 -204
- package/scripts/lib/npm-version.js +261 -261
- package/scripts/lib/paquetes-conocidos.js +50 -50
- package/scripts/lib/plan-lock.js +275 -275
- package/scripts/lib/pr-analyzer.js +399 -399
- package/scripts/lib/preservar-usuario.js +39 -5
- package/scripts/lib/prompt-builder.js +264 -264
- package/scripts/lib/reglas-globales-conocidas.json +180 -180
- package/scripts/lib/resolver-plan-fase.js +37 -37
- package/scripts/lib/rrf-fusion.js +175 -175
- package/scripts/lib/schema-version.js +164 -164
- package/scripts/lib/scoring-instintos.js +277 -277
- package/scripts/lib/semantic-search.js +252 -252
- package/scripts/lib/tool-cost-analyzer.js +1 -0
- package/scripts/limpiar-artefactos-python.js +131 -131
- package/scripts/migrar-csv-a-array.js +168 -168
- package/scripts/migrar-fase-dominio.js +200 -200
- package/scripts/publicar.js +497 -497
- package/scripts/run-eval.js +141 -141
- package/scripts/tui/componentes/selector-multi.js +189 -189
- package/scripts/tui/componentes/selector-unico.js +158 -158
- package/scripts/tui/ejecutores.js +375 -375
- package/scripts/tui/index.js +162 -162
- package/scripts/tui/lib/colores.js +129 -129
- package/scripts/tui/lib/render.js +264 -264
- package/scripts/tui/lib/teclas.js +113 -113
- package/scripts/tui/pantallas/inspect.js +173 -173
- package/scripts/tui/pantallas/install-wizard.js +334 -334
- package/scripts/tui/pantallas/menu-principal.js +52 -52
- package/scripts/tui/pantallas/progreso.js +274 -274
- package/scripts/tui/pantallas/resumen.js +132 -132
- package/scripts/tui/pantallas/uninstall-wizard.js +208 -208
- package/scripts/tui/pantallas/update-wizard.js +232 -232
- package/scripts/tui/pantallas/welcome.js +187 -187
- package/scripts/validar-userland-vacio.js +110 -110
- package/scripts/validar.js +1 -1
- package/scripts/verificar-release.js +51 -0
- package/hooks/lib/context-compressor.js +0 -657
- package/hooks/linea-estado.js +0 -326
- package/hooks/monitor-contexto.js +0 -373
|
@@ -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
|
+
};
|