@saulwade/swl-ses 2.2.3 → 2.3.0

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 (66) hide show
  1. package/CLAUDE.md +4 -4
  2. package/README.md +3 -3
  3. package/agentes/arquitecto-swl.md +1 -1
  4. package/agentes/auto-evolucion-swl.md +908 -908
  5. package/agentes/backend-csharp-swl.md +1 -1
  6. package/agentes/backend-go-swl.md +1 -1
  7. package/agentes/backend-java-swl.md +1 -1
  8. package/agentes/backend-rust-swl.md +1 -1
  9. package/agentes/evals/orquestador-swl.evals.json +1 -1
  10. package/agentes/implementador-swl.md +2 -2
  11. package/agentes/llm-apps-swl.md +1 -1
  12. package/agentes/orquestador-swl.md +7 -7
  13. package/agentes/pagos-swl.md +1 -1
  14. package/agentes/perfilador-usuario-swl.md +321 -321
  15. package/agentes/planificador-swl.md +1 -1
  16. package/agentes/producto-prd-swl.md +1 -1
  17. package/agentes/revisor-seguridad-swl.md +1 -1
  18. package/agentes/sre-swl.md +1 -1
  19. package/comandos/swl/aprobar-plan.md +146 -141
  20. package/comandos/swl/release.md +45 -8
  21. package/comandos/swl/status.md +343 -333
  22. package/comandos/swl/verificar.md +817 -817
  23. package/habilidades/compactacion-contexto/SKILL.md +3 -3
  24. package/habilidades/contenedores-docker/SKILL.md +3 -1
  25. package/habilidades/context-builder/SKILL.md +4 -4
  26. package/habilidades/control-profundidad/SKILL.md +5 -5
  27. package/habilidades/ejecutar-fase/SKILL.md +564 -541
  28. package/habilidades/extractor-de-aprendizajes/SKILL.md +3 -3
  29. package/habilidades/guardrail-semantico/SKILL.md +1 -1
  30. package/habilidades/harness-claude-code/SKILL.md +305 -305
  31. package/habilidades/meta-skills-estandar/recursos/skills-as-agents.md +1 -1
  32. package/habilidades/planear-fase/SKILL.md +15 -1
  33. package/habilidades/proceso-debate-adversarial/SKILL.md +191 -164
  34. package/habilidades/prompt-engineering/SKILL.md +5 -5
  35. package/habilidades/release-semver/SKILL.md +4 -1
  36. package/habilidades/tdd-workflow/SKILL.md +33 -1
  37. package/habilidades/workflow-claude-code/SKILL.md +6 -6
  38. package/hooks/captura-acciones-post.js +151 -0
  39. package/hooks/captura-acciones-session.js +155 -0
  40. package/hooks/lib/briefing.js +512 -474
  41. package/hooks/lib/captura-acciones.js +392 -0
  42. package/hooks/lib/context-builder.js +2 -2
  43. package/hooks/lib/model-router.js +1 -1
  44. package/hooks/lib/token-estimator.js +2 -0
  45. package/hooks/linea-estado.js +7 -5
  46. package/llms.txt +29 -29
  47. package/manifiestos/canonical-hashes.json +1964 -1310
  48. package/manifiestos/hooks-config.json +18 -0
  49. package/manifiestos/modulos.json +4 -0
  50. package/manifiestos/skills-lock.json +1282 -1282
  51. package/package.json +93 -93
  52. package/plugin.json +371 -371
  53. package/reglas/git-coauthor.md +100 -100
  54. package/reglas/memoria-consolidada.md +41 -0
  55. package/reglas/pruebas.md +15 -0
  56. package/reglas/seguridad-agentes.md +66 -0
  57. package/schemas/agent-frontmatter.schema.json +294 -294
  58. package/schemas/instinct.schema.json +161 -152
  59. package/scripts/bootstrap-instintos.js +25 -7
  60. package/scripts/lib/claude-sessions.js +1 -0
  61. package/scripts/lib/gitignore-manifest.js +44 -11
  62. package/scripts/lib/skill-metrics.js +41 -1
  63. package/scripts/lib/tool-cost-analyzer.js +1 -0
  64. package/scripts/publicar.js +34 -85
  65. package/scripts/verificar-trazabilidad.js +329 -298
  66. package/scripts/vendor/claude-usage/__pycache__/scanner.cpython-314.pyc +0 -0
@@ -1,298 +1,329 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * verificar-trazabilidad.js — Valida la cadena REQ→T→commit→test de una fase (G4).
6
- *
7
- * Uso:
8
- * node scripts/verificar-trazabilidad.js --fase=10 [--solo-plan] [--json]
9
- *
10
- * Fuentes:
11
- * - REQ-NN: `.planning/fases/NN-CONTEXTO.md` § Criterios de aceptación
12
- * (definiciones `**REQ-NN**:` o `REQ-NN:`; los marcados RETIRADO
13
- * se excluyen del universo exigible)
14
- * - T-NN: `.planning/fases/NN-PLAN.md` — campo `**Verifica REQ**: ...`
15
- * por tarea `### T-NN:`
16
- * - Commits: `git log` — footer `Refs: REQ-NN[, REQ-MM]`
17
- * - Tests: archivos de test versionados (git ls-files + clasificador de
18
- * calidad-pre-commit) con marker `verifica: REQ-NN`
19
- *
20
- * Exit codes:
21
- * 0 — cadena completa, o CONTEXTO legacy sin REQ-IDs (gracia, con nota)
22
- * 1 — huérfanos detectados (REQ sin tarea / sin commit / sin test)
23
- * 2 — error de invocación o parseo (fase inexistente, args inválidos)
24
- *
25
- * `--solo-plan`: valida únicamente la matriz REQ×T (lo usa /swl:aprobar-plan,
26
- * donde commits y tests aún no existen).
27
- *
28
- * Zero-deps. Espejo estructural de verificar-release.js (gates + exit codes).
29
- */
30
-
31
- const fs = require('fs');
32
- const path = require('path');
33
- const { execFileSync } = require('child_process');
34
-
35
- const LIMITE_COMMITS = 300;
36
-
37
- // ─── Parseo de fuentes ──────────────────────────────────────────────────────
38
-
39
- /**
40
- * Extrae definiciones REQ-NN del CONTEXTO.
41
- * Método de verificación por REQ (patrón requirements engineering):
42
- * - default `test` — exige test con marker `verifica: REQ-NN`
43
- * - `(verificación: inspección)` en el criterio — satisfecho por prosa/docs/grep;
44
- * exige tarea y commit, pero NO test automatizado.
45
- * @returns {{activos: string[], retirados: string[], metodo: Object<string,string>}}
46
- */
47
- function extraerReqs(contextoTexto) {
48
- const activos = new Set();
49
- const retirados = new Set();
50
- const metodo = {};
51
- // Definición: línea con `**REQ-NN**:` o `REQ-NN:` (con bullet opcional).
52
- // El criterio puede continuar en líneas siguientes indentadas — se capturan
53
- // hasta la próxima definición para detectar la anotación de método.
54
- // REQ-NN (legacy) o REQ-NN-MM (namespaceado por fase, DT-IDS-NAMESPACE).
55
- // El grupo `(?:-\d{1,3})?` es greedy: en `REQ-12-01` captura el ID completo,
56
- // nunca el prefijo `REQ-12` truncado.
57
- const re = /^\s*(?:[-*]\s*)?\*{0,2}(REQ-\d{1,3}(?:-\d{1,3})?)\*{0,2}\s*:([\s\S]*?)(?=^\s*(?:[-*]\s*)?\*{0,2}REQ-\d|^#|\n\n|$(?![\s\S]))/gm;
58
- let m;
59
- while ((m = re.exec(contextoTexto)) !== null) {
60
- const cuerpo = m[2];
61
- if (/RETIRADO/i.test(cuerpo)) {
62
- retirados.add(m[1]);
63
- continue;
64
- }
65
- activos.add(m[1]);
66
- metodo[m[1]] = /verificaci[oó]n:\s*inspecci[oó]n/i.test(cuerpo) ? 'inspeccion' : 'test';
67
- }
68
- return { activos: [...activos], retirados: [...retirados], metodo };
69
- }
70
-
71
- /** Extrae mapa tarea→REQs del PLAN. @returns {Map<string, string[]>} */
72
- function extraerMatrizPlan(planTexto) {
73
- const tareas = new Map();
74
- let tareaActual = null;
75
- for (const linea of planTexto.split('\n')) {
76
- const t = linea.match(/^#{2,4}\s*(T-\d{1,3})\s*:/);
77
- if (t) {
78
- tareaActual = t[1];
79
- if (!tareas.has(tareaActual)) tareas.set(tareaActual, []);
80
- continue;
81
- }
82
- // Solo la línea-campo cuenta (`- **Verifica REQ**: ...` al inicio, bullet
83
- // opcional). Una descripción que cite `**Verifica REQ**:` en prosa NO debe
84
- // contaminar la matriz ni ocultar huérfanos (ancla `^` obligatoria).
85
- if (tareaActual && /^\s*(?:[-*]\s*)?\*\*Verifica REQ\*\*\s*:/i.test(linea)) {
86
- const reqs = linea.match(/REQ-\d{1,3}(?:-\d{1,3})?/g) || [];
87
- tareas.get(tareaActual).push(...reqs);
88
- }
89
- }
90
- return tareas;
91
- }
92
-
93
- /** Commits con footer `Refs: REQ-NN`. @returns {Map<string, string[]>} REQ → hashes cortos */
94
- function extraerCommitsConRefs(cwd) {
95
- const porReq = new Map();
96
- let salida;
97
- try {
98
- salida = execFileSync(
99
- 'git',
100
- ['log', `-${LIMITE_COMMITS}`, '--format=%h%x00%B%x01'],
101
- { cwd, encoding: 'utf8', timeout: 10000 }
102
- );
103
- } catch (_) {
104
- return porReq; // sin repo git o sin commits — el reporte lo refleja como gap
105
- }
106
- for (const bloque of salida.split('\x01')) {
107
- const [hash, cuerpo] = bloque.split('\x00');
108
- if (!hash || !cuerpo) continue;
109
- const refs = cuerpo.match(/^Refs:\s*(.+)$/m);
110
- if (!refs) continue;
111
- for (const req of refs[1].match(/REQ-\d{1,3}(?:-\d{1,3})?/g) || []) {
112
- if (!porReq.has(req)) porReq.set(req, []);
113
- porReq.get(req).push(hash.trim());
114
- }
115
- }
116
- return porReq;
117
- }
118
-
119
- /** Tests con marker `verifica: REQ-NN`. @returns {Map<string, string[]>} REQ → archivos */
120
- function extraerTestsConMarker(cwd) {
121
- const porReq = new Map();
122
- let esArchivoTest;
123
- try {
124
- ({ esArchivoTest } = require(path.join(__dirname, '..', 'hooks', 'calidad-pre-commit.js')));
125
- } catch (_) {
126
- esArchivoTest = (r) => /\.(test|spec)\.[jt]sx?$/.test(r) || /test_.*\.py$/.test(r);
127
- }
128
-
129
- let archivos = [];
130
- try {
131
- archivos = execFileSync('git', ['ls-files'], { cwd, encoding: 'utf8', timeout: 10000 })
132
- .split('\n')
133
- .map((l) => l.trim())
134
- .filter(Boolean)
135
- .filter(esArchivoTest);
136
- } catch (_) {
137
- return porReq;
138
- }
139
-
140
- for (const archivo of archivos) {
141
- let contenido;
142
- try {
143
- contenido = fs.readFileSync(path.join(cwd, archivo), 'utf-8');
144
- } catch (_) {
145
- continue;
146
- }
147
- for (const m of contenido.matchAll(/verifica:\s*(REQ-\d{1,3}(?:-\d{1,3})?)/g)) {
148
- const req = m[1];
149
- if (!porReq.has(req)) porReq.set(req, []);
150
- if (!porReq.get(req).includes(archivo)) porReq.get(req).push(archivo);
151
- }
152
- }
153
- return porReq;
154
- }
155
-
156
- // ─── Validación ─────────────────────────────────────────────────────────────
157
-
158
- function validarFase({ cwd, fase, soloPlan }) {
159
- const nn = String(Number(fase)).padStart(2, '0');
160
- const contextoPath = path.join(cwd, '.planning', 'fases', `${nn}-CONTEXTO.md`);
161
- if (!fs.existsSync(contextoPath)) {
162
- return { error: `No existe ${path.relative(cwd, contextoPath)}`, exit: 2 };
163
- }
164
-
165
- const { activos, retirados, metodo } = extraerReqs(fs.readFileSync(contextoPath, 'utf-8'));
166
- if (activos.length === 0) {
167
- return {
168
- fase: nn,
169
- legacy: true,
170
- nota: 'CONTEXTO sin REQ-IDs — trazabilidad no exigible (gracia legacy, fases pre-10)',
171
- exit: 0,
172
- };
173
- }
174
-
175
- const planPath = path.join(cwd, '.planning', 'fases', `${nn}-PLAN.md`);
176
- const matrizTareas = fs.existsSync(planPath)
177
- ? extraerMatrizPlan(fs.readFileSync(planPath, 'utf-8'))
178
- : new Map();
179
-
180
- const reqATareas = new Map(activos.map((r) => [r, []]));
181
- const tareasSinReq = [];
182
- for (const [tarea, reqs] of matrizTareas) {
183
- if (reqs.length === 0) tareasSinReq.push(tarea);
184
- for (const req of reqs) {
185
- if (reqATareas.has(req)) reqATareas.get(req).push(tarea);
186
- }
187
- }
188
-
189
- const commits = soloPlan ? new Map() : extraerCommitsConRefs(cwd);
190
- const tests = soloPlan ? new Map() : extraerTestsConMarker(cwd);
191
-
192
- const validaciones = {
193
- req_sin_tarea: activos.filter((r) => (reqATareas.get(r) || []).length === 0),
194
- req_sin_commit: soloPlan ? [] : activos.filter((r) => !(commits.get(r) || []).length),
195
- req_sin_test: soloPlan
196
- ? []
197
- : activos.filter((r) => metodo[r] === 'test' && !(tests.get(r) || []).length),
198
- tarea_sin_req: tareasSinReq, // informativo, no rompe
199
- };
200
-
201
- const trazabilidad = {};
202
- for (const req of activos) {
203
- trazabilidad[req] = {
204
- metodo: metodo[req],
205
- tareas: reqATareas.get(req) || [],
206
- commits: commits.get(req) || [],
207
- tests: tests.get(req) || [],
208
- };
209
- }
210
-
211
- const huerfanos =
212
- validaciones.req_sin_tarea.length +
213
- validaciones.req_sin_commit.length +
214
- validaciones.req_sin_test.length;
215
-
216
- return {
217
- fase: nn,
218
- legacy: false,
219
- soloPlan: Boolean(soloPlan),
220
- reqs: activos,
221
- retirados,
222
- trazabilidad,
223
- validaciones,
224
- exit: huerfanos > 0 ? 1 : 0,
225
- };
226
- }
227
-
228
- // ─── Reporte ────────────────────────────────────────────────────────────────
229
-
230
- function imprimirReporte(r) {
231
- if (r.error) {
232
- console.error(`ERROR: ${r.error}`);
233
- return;
234
- }
235
- if (r.legacy) {
236
- console.log(`Fase ${r.fase}: ${r.nota}. Exit 0.`);
237
- return;
238
- }
239
- console.log(`Trazabilidad REQ→T→commit→test — Fase ${r.fase}${r.soloPlan ? ' (--solo-plan: matriz REQ×T)' : ''}`);
240
- console.log('');
241
- for (const req of r.reqs) {
242
- const t = r.trazabilidad[req];
243
- const partes = [`tareas: ${t.tareas.join(', ') || '—'}`];
244
- if (!r.soloPlan) {
245
- partes.push(`commits: ${t.commits.join(', ') || '—'}`);
246
- partes.push(t.metodo === 'inspeccion' ? 'verificación: inspección' : `tests: ${t.tests.join(', ') || '—'}`);
247
- }
248
- console.log(` ${req} ${partes.join(' | ')}`);
249
- }
250
- if (r.retirados.length) console.log(` Retirados (no exigibles): ${r.retirados.join(', ')}`);
251
- console.log('');
252
-
253
- const v = r.validaciones;
254
- const gaps = [];
255
- if (v.req_sin_tarea.length) gaps.push(`REQ sin tarea: ${v.req_sin_tarea.join(', ')}`);
256
- if (v.req_sin_commit.length) gaps.push(`REQ sin commit (footer Refs:): ${v.req_sin_commit.join(', ')}`);
257
- if (v.req_sin_test.length) gaps.push(`REQ sin test (marker verifica:): ${v.req_sin_test.join(', ')}`);
258
-
259
- if (gaps.length) {
260
- console.log('HUÉRFANOS DETECTADOS:');
261
- for (const g of gaps) console.log(` ✗ ${g}`);
262
- } else {
263
- console.log('✓ Cadena de trazabilidad completa: 0 huérfanos.');
264
- }
265
- if (v.tarea_sin_req.length) {
266
- console.log(` (info) Tareas sin REQ declarado: ${v.tarea_sin_req.join(', ')} — válido para infraestructura/refactor`);
267
- }
268
- }
269
-
270
- // ─── CLI ────────────────────────────────────────────────────────────────────
271
-
272
- function main() {
273
- const args = process.argv.slice(2);
274
- const faseArg = args.find((a) => a.startsWith('--fase='));
275
- if (!faseArg || !/^\d+$/.test(faseArg.split('=')[1] || '')) {
276
- console.error('Uso: node scripts/verificar-trazabilidad.js --fase=N [--solo-plan] [--json]');
277
- process.exit(2);
278
- }
279
-
280
- const resultado = validarFase({
281
- cwd: process.cwd(),
282
- fase: faseArg.split('=')[1],
283
- soloPlan: args.includes('--solo-plan'),
284
- });
285
-
286
- if (args.includes('--json')) {
287
- console.log(JSON.stringify(resultado, null, 2));
288
- } else {
289
- imprimirReporte(resultado);
290
- }
291
- process.exit(resultado.exit);
292
- }
293
-
294
- if (require.main === module) {
295
- main();
296
- }
297
-
298
- module.exports = { extraerReqs, extraerMatrizPlan, extraerCommitsConRefs, extraerTestsConMarker, validarFase, main };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * verificar-trazabilidad.js — Valida la cadena REQ→T→commit→test de una fase (G4).
6
+ *
7
+ * Uso:
8
+ * node scripts/verificar-trazabilidad.js --fase=10 [--solo-plan] [--json]
9
+ *
10
+ * Fuentes:
11
+ * - REQ-NN: `.planning/fases/NN-CONTEXTO.md` § Criterios de aceptación
12
+ * (definiciones `**REQ-NN**:` o `REQ-NN:`; los marcados RETIRADO
13
+ * se excluyen del universo exigible)
14
+ * - T-NN: `.planning/fases/NN-PLAN.md` — campo `**Verifica REQ**: ...`
15
+ * por tarea `### T-NN:`
16
+ * - Commits: `git log` — footer `Refs: REQ-NN[, REQ-MM]`
17
+ * - Tests: archivos de test versionados (git ls-files + clasificador de
18
+ * calidad-pre-commit) con marker `verifica: REQ-NN`
19
+ *
20
+ * Exit codes:
21
+ * 0 — cadena completa, o CONTEXTO legacy sin REQ-IDs (gracia, con nota)
22
+ * 1 — huérfanos detectados (REQ sin tarea / sin commit / sin test)
23
+ * 2 — error de invocación o parseo (fase inexistente, args inválidos)
24
+ *
25
+ * `--solo-plan`: valida únicamente la matriz REQ×T (lo usa /swl:aprobar-plan,
26
+ * donde commits y tests aún no existen).
27
+ *
28
+ * Zero-deps. Espejo estructural de verificar-release.js (gates + exit codes).
29
+ */
30
+
31
+ const fs = require('fs');
32
+ const path = require('path');
33
+ const { execFileSync } = require('child_process');
34
+
35
+ const LIMITE_COMMITS = 300;
36
+
37
+ // ─── Parseo de fuentes ──────────────────────────────────────────────────────
38
+
39
+ /**
40
+ * Extrae definiciones REQ-NN del CONTEXTO.
41
+ * Método de verificación por REQ (patrón requirements engineering):
42
+ * - default `test` — exige test con marker `verifica: REQ-NN`
43
+ * - `(verificación: inspección)` en el criterio — satisfecho por prosa/docs/grep;
44
+ * exige tarea y commit, pero NO test automatizado.
45
+ * @returns {{activos: string[], retirados: string[], metodo: Object<string,string>}}
46
+ */
47
+ function extraerReqs(contextoTexto) {
48
+ const activos = new Set();
49
+ const retirados = new Set();
50
+ const metodo = {};
51
+ // Definición: línea con `**REQ-NN**:` o `REQ-NN:` (con bullet opcional).
52
+ // El criterio puede continuar en líneas siguientes indentadas — se capturan
53
+ // hasta la próxima definición para detectar la anotación de método.
54
+ // REQ-NN (legacy) o REQ-NN-MM (namespaceado por fase, DT-IDS-NAMESPACE).
55
+ // El grupo `(?:-\d{1,3})?` es greedy: en `REQ-12-01` captura el ID completo,
56
+ // nunca el prefijo `REQ-12` truncado.
57
+ const re = /^\s*(?:[-*]\s*)?\*{0,2}(REQ-\d{1,3}(?:-\d{1,3})?)\*{0,2}\s*:([\s\S]*?)(?=^\s*(?:[-*]\s*)?\*{0,2}REQ-\d|^#|\n\n|$(?![\s\S]))/gm;
58
+ let m;
59
+ while ((m = re.exec(contextoTexto)) !== null) {
60
+ const cuerpo = m[2];
61
+ if (/RETIRADO/i.test(cuerpo)) {
62
+ retirados.add(m[1]);
63
+ continue;
64
+ }
65
+ activos.add(m[1]);
66
+ metodo[m[1]] = /verificaci[oó]n:\s*inspecci[oó]n/i.test(cuerpo) ? 'inspeccion' : 'test';
67
+ }
68
+ return { activos: [...activos], retirados: [...retirados], metodo };
69
+ }
70
+
71
+ /** Extrae mapa tarea→REQs del PLAN. @returns {Map<string, string[]>} */
72
+ function extraerMatrizPlan(planTexto) {
73
+ const tareas = new Map();
74
+ let tareaActual = null;
75
+ for (const linea of planTexto.split('\n')) {
76
+ const t = linea.match(/^#{2,4}\s*(T-\d{1,3})\s*:/);
77
+ if (t) {
78
+ tareaActual = t[1];
79
+ if (!tareas.has(tareaActual)) tareas.set(tareaActual, []);
80
+ continue;
81
+ }
82
+ // Solo la línea-campo cuenta (`- **Verifica REQ**: ...` al inicio, bullet
83
+ // opcional). Una descripción que cite `**Verifica REQ**:` en prosa NO debe
84
+ // contaminar la matriz ni ocultar huérfanos (ancla `^` obligatoria).
85
+ if (tareaActual && /^\s*(?:[-*]\s*)?\*\*Verifica REQ\*\*\s*:/i.test(linea)) {
86
+ const reqs = linea.match(/REQ-\d{1,3}(?:-\d{1,3})?/g) || [];
87
+ tareas.get(tareaActual).push(...reqs);
88
+ }
89
+ }
90
+ return tareas;
91
+ }
92
+
93
+ /**
94
+ * Detecta headers de tarea malformados que el parser de la matriz NO reconoce.
95
+ * El parser (`extraerMatrizPlan`) exige `### T-NN:` (2-4 `#`, dos puntos tras
96
+ * el ID). Un header como `#### T-01 — Foo` (em-dash) o `# T-01:` (un solo `#`)
97
+ * no matchea: la tarea no se registra y sus REQ salen huérfanos sin causa
98
+ * visible. Esta detección es warn-only — explica el porqué del huérfano, no
99
+ * cambia el exit code (patrón de introducción de gate warn-only, ADR-0034/0035).
100
+ * @returns {{linea:number, texto:string}[]}
101
+ */
102
+ function detectarHeadersMalformados(planTexto) {
103
+ const malformados = [];
104
+ const canonico = /^#{2,4}\s*T-\d{1,3}\s*:/;
105
+ // "Parece header de tarea": cualquier nivel de heading que arranque con T-NN.
106
+ const pareceTarea = /^#{1,6}\s*T-\d{1,3}\b/;
107
+ const lineas = planTexto.split('\n');
108
+ for (let i = 0; i < lineas.length; i++) {
109
+ if (pareceTarea.test(lineas[i]) && !canonico.test(lineas[i])) {
110
+ malformados.push({ linea: i + 1, texto: lineas[i].trim() });
111
+ }
112
+ }
113
+ return malformados;
114
+ }
115
+
116
+ /** Commits con footer `Refs: REQ-NN`. @returns {Map<string, string[]>} REQ → hashes cortos */
117
+ function extraerCommitsConRefs(cwd) {
118
+ const porReq = new Map();
119
+ let salida;
120
+ try {
121
+ salida = execFileSync(
122
+ 'git',
123
+ ['log', `-${LIMITE_COMMITS}`, '--format=%h%x00%B%x01'],
124
+ { cwd, encoding: 'utf8', timeout: 10000 }
125
+ );
126
+ } catch (_) {
127
+ return porReq; // sin repo git o sin commits — el reporte lo refleja como gap
128
+ }
129
+ for (const bloque of salida.split('\x01')) {
130
+ const [hash, cuerpo] = bloque.split('\x00');
131
+ if (!hash || !cuerpo) continue;
132
+ const refs = cuerpo.match(/^Refs:\s*(.+)$/m);
133
+ if (!refs) continue;
134
+ for (const req of refs[1].match(/REQ-\d{1,3}(?:-\d{1,3})?/g) || []) {
135
+ if (!porReq.has(req)) porReq.set(req, []);
136
+ porReq.get(req).push(hash.trim());
137
+ }
138
+ }
139
+ return porReq;
140
+ }
141
+
142
+ /** Tests con marker `verifica: REQ-NN`. @returns {Map<string, string[]>} REQ → archivos */
143
+ function extraerTestsConMarker(cwd) {
144
+ const porReq = new Map();
145
+ let esArchivoTest;
146
+ try {
147
+ ({ esArchivoTest } = require(path.join(__dirname, '..', 'hooks', 'calidad-pre-commit.js')));
148
+ } catch (_) {
149
+ esArchivoTest = (r) => /\.(test|spec)\.[jt]sx?$/.test(r) || /test_.*\.py$/.test(r);
150
+ }
151
+
152
+ let archivos = [];
153
+ try {
154
+ archivos = execFileSync('git', ['ls-files'], { cwd, encoding: 'utf8', timeout: 10000 })
155
+ .split('\n')
156
+ .map((l) => l.trim())
157
+ .filter(Boolean)
158
+ .filter(esArchivoTest);
159
+ } catch (_) {
160
+ return porReq;
161
+ }
162
+
163
+ for (const archivo of archivos) {
164
+ let contenido;
165
+ try {
166
+ contenido = fs.readFileSync(path.join(cwd, archivo), 'utf-8');
167
+ } catch (_) {
168
+ continue;
169
+ }
170
+ for (const m of contenido.matchAll(/verifica:\s*(REQ-\d{1,3}(?:-\d{1,3})?)/g)) {
171
+ const req = m[1];
172
+ if (!porReq.has(req)) porReq.set(req, []);
173
+ if (!porReq.get(req).includes(archivo)) porReq.get(req).push(archivo);
174
+ }
175
+ }
176
+ return porReq;
177
+ }
178
+
179
+ // ─── Validación ─────────────────────────────────────────────────────────────
180
+
181
+ function validarFase({ cwd, fase, soloPlan }) {
182
+ const nn = String(Number(fase)).padStart(2, '0');
183
+ const contextoPath = path.join(cwd, '.planning', 'fases', `${nn}-CONTEXTO.md`);
184
+ if (!fs.existsSync(contextoPath)) {
185
+ return { error: `No existe ${path.relative(cwd, contextoPath)}`, exit: 2 };
186
+ }
187
+
188
+ const { activos, retirados, metodo } = extraerReqs(fs.readFileSync(contextoPath, 'utf-8'));
189
+ if (activos.length === 0) {
190
+ return {
191
+ fase: nn,
192
+ legacy: true,
193
+ nota: 'CONTEXTO sin REQ-IDs — trazabilidad no exigible (gracia legacy, fases pre-10)',
194
+ exit: 0,
195
+ };
196
+ }
197
+
198
+ const planPath = path.join(cwd, '.planning', 'fases', `${nn}-PLAN.md`);
199
+ const planTexto = fs.existsSync(planPath) ? fs.readFileSync(planPath, 'utf-8') : '';
200
+ const matrizTareas = extraerMatrizPlan(planTexto);
201
+ const headersMalformados = detectarHeadersMalformados(planTexto);
202
+
203
+ const reqATareas = new Map(activos.map((r) => [r, []]));
204
+ const tareasSinReq = [];
205
+ for (const [tarea, reqs] of matrizTareas) {
206
+ if (reqs.length === 0) tareasSinReq.push(tarea);
207
+ for (const req of reqs) {
208
+ if (reqATareas.has(req)) reqATareas.get(req).push(tarea);
209
+ }
210
+ }
211
+
212
+ const commits = soloPlan ? new Map() : extraerCommitsConRefs(cwd);
213
+ const tests = soloPlan ? new Map() : extraerTestsConMarker(cwd);
214
+
215
+ const validaciones = {
216
+ req_sin_tarea: activos.filter((r) => (reqATareas.get(r) || []).length === 0),
217
+ req_sin_commit: soloPlan ? [] : activos.filter((r) => !(commits.get(r) || []).length),
218
+ req_sin_test: soloPlan
219
+ ? []
220
+ : activos.filter((r) => metodo[r] === 'test' && !(tests.get(r) || []).length),
221
+ tarea_sin_req: tareasSinReq, // informativo, no rompe
222
+ headers_malformados: headersMalformados, // warn-only: no cambia exit (ADR-0034/0035)
223
+ };
224
+
225
+ const trazabilidad = {};
226
+ for (const req of activos) {
227
+ trazabilidad[req] = {
228
+ metodo: metodo[req],
229
+ tareas: reqATareas.get(req) || [],
230
+ commits: commits.get(req) || [],
231
+ tests: tests.get(req) || [],
232
+ };
233
+ }
234
+
235
+ const huerfanos =
236
+ validaciones.req_sin_tarea.length +
237
+ validaciones.req_sin_commit.length +
238
+ validaciones.req_sin_test.length;
239
+
240
+ return {
241
+ fase: nn,
242
+ legacy: false,
243
+ soloPlan: Boolean(soloPlan),
244
+ reqs: activos,
245
+ retirados,
246
+ trazabilidad,
247
+ validaciones,
248
+ exit: huerfanos > 0 ? 1 : 0,
249
+ };
250
+ }
251
+
252
+ // ─── Reporte ────────────────────────────────────────────────────────────────
253
+
254
+ function imprimirReporte(r) {
255
+ if (r.error) {
256
+ console.error(`ERROR: ${r.error}`);
257
+ return;
258
+ }
259
+ if (r.legacy) {
260
+ console.log(`Fase ${r.fase}: ${r.nota}. Exit 0.`);
261
+ return;
262
+ }
263
+ console.log(`Trazabilidad REQ→T→commit→test Fase ${r.fase}${r.soloPlan ? ' (--solo-plan: matriz REQ×T)' : ''}`);
264
+ console.log('');
265
+ for (const req of r.reqs) {
266
+ const t = r.trazabilidad[req];
267
+ const partes = [`tareas: ${t.tareas.join(', ') || '—'}`];
268
+ if (!r.soloPlan) {
269
+ partes.push(`commits: ${t.commits.join(', ') || '—'}`);
270
+ partes.push(t.metodo === 'inspeccion' ? 'verificación: inspección' : `tests: ${t.tests.join(', ') || '—'}`);
271
+ }
272
+ console.log(` ${req} → ${partes.join(' | ')}`);
273
+ }
274
+ if (r.retirados.length) console.log(` Retirados (no exigibles): ${r.retirados.join(', ')}`);
275
+ console.log('');
276
+
277
+ const v = r.validaciones;
278
+ const gaps = [];
279
+ if (v.req_sin_tarea.length) gaps.push(`REQ sin tarea: ${v.req_sin_tarea.join(', ')}`);
280
+ if (v.req_sin_commit.length) gaps.push(`REQ sin commit (footer Refs:): ${v.req_sin_commit.join(', ')}`);
281
+ if (v.req_sin_test.length) gaps.push(`REQ sin test (marker verifica:): ${v.req_sin_test.join(', ')}`);
282
+
283
+ if (gaps.length) {
284
+ console.log('HUÉRFANOS DETECTADOS:');
285
+ for (const g of gaps) console.log(` ✗ ${g}`);
286
+ } else {
287
+ console.log('✓ Cadena de trazabilidad completa: 0 huérfanos.');
288
+ }
289
+ if (v.tarea_sin_req.length) {
290
+ console.log(` (info) Tareas sin REQ declarado: ${v.tarea_sin_req.join(', ')} — válido para infraestructura/refactor`);
291
+ }
292
+ if (v.headers_malformados && v.headers_malformados.length) {
293
+ console.log('');
294
+ console.log('⚠ WARN — headers de tarea malformados (la matriz REQ×T NO los parsea):');
295
+ for (const h of v.headers_malformados) console.log(` línea ${h.linea}: ${h.texto}`);
296
+ console.log(' Formato esperado: `### T-NN:` (2-4 `#`, dos puntos tras el ID; no em-dash).');
297
+ console.log(' Si hay REQ huérfanos arriba, esta suele ser la causa. (warn-only, no bloquea)');
298
+ }
299
+ }
300
+
301
+ // ─── CLI ────────────────────────────────────────────────────────────────────
302
+
303
+ function main() {
304
+ const args = process.argv.slice(2);
305
+ const faseArg = args.find((a) => a.startsWith('--fase='));
306
+ if (!faseArg || !/^\d+$/.test(faseArg.split('=')[1] || '')) {
307
+ console.error('Uso: node scripts/verificar-trazabilidad.js --fase=N [--solo-plan] [--json]');
308
+ process.exit(2);
309
+ }
310
+
311
+ const resultado = validarFase({
312
+ cwd: process.cwd(),
313
+ fase: faseArg.split('=')[1],
314
+ soloPlan: args.includes('--solo-plan'),
315
+ });
316
+
317
+ if (args.includes('--json')) {
318
+ console.log(JSON.stringify(resultado, null, 2));
319
+ } else {
320
+ imprimirReporte(resultado);
321
+ }
322
+ process.exit(resultado.exit);
323
+ }
324
+
325
+ if (require.main === module) {
326
+ main();
327
+ }
328
+
329
+ module.exports = { extraerReqs, extraerMatrizPlan, detectarHeadersMalformados, extraerCommitsConRefs, extraerTestsConMarker, validarFase, main };