pumuki 6.3.311 → 6.3.313

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/AGENTS.md CHANGED
@@ -159,6 +159,10 @@ Antes de realizar cualquier accion:
159
159
  - crear commits atomicos separados o stashes con nombre explicito antes de continuar,
160
160
  - no usar un commit masivo para "limpiar" el estado.
161
161
  - Antes de cada commit:
162
+ - confirmar RED registrado o justificar que es cambio documental puro;
163
+ - confirmar GREEN con test focal ejecutado;
164
+ - confirmar REFACTOR acotado y gobernado por las skills aplicables;
165
+ - confirmar VERIFY con typecheck/compilacion/build aplicable;
162
166
  - revisar `git diff --cached --name-status`,
163
167
  - confirmar que todos los archivos staged pertenecen a la misma intencion,
164
168
  - dejar fuera del stage cualquier archivo no relacionado,
@@ -174,9 +178,28 @@ Antes de realizar cualquier accion:
174
178
  - preservar cambios en commit o stash con nombre explicito,
175
179
  - y reportar la evidencia.
176
180
 
181
+ ## Golden Flow TDD / Red-Green-Refactor (flujo principal no negociable)
182
+ - Pumuki es primero un orquestador de flujo seguro de ingenieria: las skills son reglas de calidad dentro de este flujo, no sustituyen el flujo.
183
+ - Todo cambio funcional, bugfix de gate/lifecycle, detector AST/nodal, notificacion, hook, policy o codigo de producto debe seguir obligatoriamente:
184
+ 1. `RED`: crear o actualizar primero un test/regresion que falle por el bug o comportamiento esperado.
185
+ 2. `GREEN`: tocar el minimo codigo de produccion necesario para hacer pasar ese test.
186
+ 3. `REFACTOR`: limpiar solo dentro del alcance de la slice, sin cambiar comportamiento.
187
+ 4. `VERIFY`: ejecutar test focal y compilacion/typecheck/build aplicable.
188
+ 5. `COMMIT ATOMICO`: cerrar una unica intencion funcional o tecnica.
189
+ - La fase `REFACTOR` debe aplicar las skills de plataforma correspondientes:
190
+ - iOS/Swift/SwiftUI: `ios-enterprise-rules`, `swift-concurrency`, `swiftui-expert-skill`; y `swift-testing-expert`, `core-data-expert` o skills Apple adicionales cuando aplique.
191
+ - Android: `android-enterprise-rules`.
192
+ - Frontend: `frontend-enterprise-rules`.
193
+ - Backend: `backend-enterprise-rules`.
194
+ - Esta prohibido commitear codigo funcional sin test focal ejecutado y verde.
195
+ - Esta prohibido publicar release o repinear consumers si el test focal, typecheck o build aplicable fallan.
196
+ - Si no es tecnicamente posible escribir RED antes de tocar produccion, declarar `STATUS: BLOCKED` o documentar una excepcion explicita y trazable antes de continuar.
197
+ - Cambios documentales puros pueden omitir RED, pero deben mantener diff acotado, trazabilidad y commit atomico.
198
+
177
199
  ## Gate operativo obligatorio (antes de editar codigo)
178
200
  - Declarar internamente las skills aplicables y tratarlas como activas durante TODO el turno.
179
201
  - Verificar cumplimiento minimo previo:
202
+ - Golden Flow TDD `RED -> GREEN -> REFACTOR -> VERIFY -> COMMIT ATOMICO` requerido para todo cambio funcional.
180
203
  - BDD/TDD requerido por la skill correspondiente.
181
204
  - Concurrencia y aislamiento segun `swift-concurrency` cuando haya codigo Swift.
182
205
  - Estado/arquitectura/UI segun `swiftui-expert-skill` e `ios-enterprise-rules` cuando aplique iOS/SwiftUI.
@@ -39,6 +39,10 @@ export type SddEvidenceScaffoldResult = {
39
39
  status: SddEvidenceScaffoldTestStatus;
40
40
  timestamp: string;
41
41
  };
42
+ verify: {
43
+ status: SddEvidenceScaffoldTestStatus;
44
+ timestamp: string;
45
+ };
42
46
  }>;
43
47
  metadata: {
44
48
  source: 'pumuki-sdd-evidence';
@@ -226,6 +230,10 @@ export const runSddEvidenceScaffold = (params?: {
226
230
  status: testStatus,
227
231
  timestamp: generatedAt,
228
232
  },
233
+ verify: {
234
+ status: testStatus,
235
+ timestamp: generatedAt,
236
+ },
229
237
  },
230
238
  ],
231
239
  metadata: {
@@ -17,6 +17,7 @@ const tddSliceSchema = z.object({
17
17
  red: tddEventSchema,
18
18
  green: tddEventSchema,
19
19
  refactor: tddEventSchema,
20
+ verify: tddEventSchema.optional(),
20
21
  });
21
22
 
22
23
  const tddIntegritySchema = z.object({
@@ -96,15 +96,21 @@ export const enforceTddBddPolicy = (params: {
96
96
  public_interface_files: scope.metrics.publicInterfaceFiles,
97
97
  },
98
98
  },
99
- evidence: {
100
- path: '',
101
- state: 'not_required',
102
- slices_total: 0,
103
- slices_valid: 0,
104
- slices_invalid: 0,
105
- integrity_ok: true,
106
- errors: [],
107
- },
99
+ evidence: {
100
+ path: '',
101
+ state: 'not_required',
102
+ slices_total: 0,
103
+ slices_valid: 0,
104
+ slices_invalid: 0,
105
+ integrity_ok: true,
106
+ errors: [],
107
+ phases: {
108
+ red: 'missing',
109
+ green: 'missing',
110
+ refactor: 'missing',
111
+ verify: 'missing',
112
+ },
113
+ },
108
114
  waiver: {
109
115
  applied: false,
110
116
  },
@@ -163,7 +169,7 @@ export const enforceTddBddPolicy = (params: {
163
169
  ruleId: 'generic_evidence_integrity_required',
164
170
  code: 'TDD_BDD_EVIDENCE_MISSING',
165
171
  message:
166
- 'TDD/BDD evidence contract is required for new/complex changes and was not found.',
172
+ 'Golden Flow evidence is required for functional code changes and was not found. Required phases: RED failed, GREEN passed, REFACTOR passed, VERIFY passed.',
167
173
  filePath: evidenceRead.path,
168
174
  });
169
175
  return {
@@ -207,6 +213,12 @@ export const enforceTddBddPolicy = (params: {
207
213
  const sliceFindings: Finding[] = [];
208
214
  const seenSliceIds = new Set<string>();
209
215
  let validSlices = 0;
216
+ const phaseState: NonNullable<TddBddSnapshot['evidence']['phases']> = {
217
+ red: 'missing',
218
+ green: 'missing',
219
+ refactor: 'missing',
220
+ verify: 'missing',
221
+ };
210
222
 
211
223
  if (evidenceRead.evidence.slices.length === 0) {
212
224
  sliceFindings.push(
@@ -258,6 +270,7 @@ export const enforceTddBddPolicy = (params: {
258
270
  }
259
271
 
260
272
  if (slice.red.status !== 'failed') {
273
+ phaseState.red = slice.red.status;
261
274
  sliceFindings.push(
262
275
  buildFinding({
263
276
  ruleId: 'generic_tdd_vertical_required',
@@ -266,8 +279,12 @@ export const enforceTddBddPolicy = (params: {
266
279
  filePath: evidenceRead.path,
267
280
  })
268
281
  );
282
+ } else {
283
+ phaseState.red = 'failed';
269
284
  }
270
285
 
286
+ phaseState.green = slice.green.status;
287
+ phaseState.refactor = slice.refactor.status;
271
288
  if (slice.green.status !== 'passed' || slice.refactor.status !== 'passed') {
272
289
  sliceFindings.push(
273
290
  buildFinding({
@@ -279,18 +296,42 @@ export const enforceTddBddPolicy = (params: {
279
296
  );
280
297
  }
281
298
 
299
+ if (!slice.verify) {
300
+ sliceFindings.push(
301
+ buildFinding({
302
+ ruleId: 'generic_golden_flow_verify_required',
303
+ code: 'GOLDEN_FLOW_VERIFY_EVIDENCE_MISSING',
304
+ message: `Slice ${slice.id} must include VERIFY passing evidence after REFACTOR.`,
305
+ filePath: evidenceRead.path,
306
+ })
307
+ );
308
+ } else {
309
+ phaseState.verify = slice.verify.status;
310
+ if (slice.verify.status !== 'passed') {
311
+ sliceFindings.push(
312
+ buildFinding({
313
+ ruleId: 'generic_golden_flow_verify_required',
314
+ code: 'GOLDEN_FLOW_VERIFY_MUST_PASS',
315
+ message: `Slice ${slice.id} must finish with VERIFY passing evidence.`,
316
+ filePath: evidenceRead.path,
317
+ })
318
+ );
319
+ }
320
+ }
321
+
282
322
  if (
283
323
  !isTimelineOrdered([
284
324
  slice.red.timestamp,
285
325
  slice.green.timestamp,
286
326
  slice.refactor.timestamp,
327
+ slice.verify?.timestamp,
287
328
  ])
288
329
  ) {
289
330
  sliceFindings.push(
290
331
  buildFinding({
291
332
  ruleId: 'generic_red_green_refactor_enforced',
292
333
  code: 'TDD_PHASE_TIMELINE_INVALID',
293
- message: `Slice ${slice.id} has invalid RED->GREEN->REFACTOR timestamp ordering.`,
334
+ message: `Slice ${slice.id} has invalid RED->GREEN->REFACTOR->VERIFY timestamp ordering.`,
294
335
  filePath: evidenceRead.path,
295
336
  })
296
337
  );
@@ -320,6 +361,7 @@ export const enforceTddBddPolicy = (params: {
320
361
  slices_invalid: invalidSlices,
321
362
  integrity_ok: evidenceRead.integrity.valid,
322
363
  errors: sliceFindings.map((finding) => finding.code),
364
+ phases: phaseState,
323
365
  },
324
366
  waiver: {
325
367
  applied: false,
@@ -154,6 +154,9 @@ export const classifyTddBddScope = (
154
154
  }
155
155
 
156
156
  const reasons: string[] = [];
157
+ if (changedImplementationPaths.size > 0) {
158
+ reasons.push('functional.implementation_changed');
159
+ }
157
160
  if (changedImplementationPaths.size > thresholds.maxChangedFiles) {
158
161
  reasons.push('complex.changed_files_threshold');
159
162
  }
@@ -168,9 +171,9 @@ export const classifyTddBddScope = (
168
171
  }
169
172
 
170
173
  const isNewFeature = addedImplementationPaths.size > 0;
171
- const isComplexChange = reasons.length > 0;
174
+ const isComplexChange = reasons.some((reason) => reason.startsWith('complex.'));
172
175
  return {
173
- inScope: isNewFeature || isComplexChange,
176
+ inScope: changedImplementationPaths.size > 0,
174
177
  isNewFeature,
175
178
  isComplexChange,
176
179
  reasons,
@@ -21,6 +21,12 @@ export type TddBddSnapshot = {
21
21
  slices_invalid: number;
22
22
  integrity_ok: boolean;
23
23
  errors: string[];
24
+ phases?: {
25
+ red: 'missing' | 'passed' | 'failed';
26
+ green: 'missing' | 'passed' | 'failed';
27
+ refactor: 'missing' | 'passed' | 'failed';
28
+ verify: 'missing' | 'passed' | 'failed';
29
+ };
24
30
  };
25
31
  waiver: {
26
32
  applied: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.311",
3
+ "version": "6.3.313",
4
4
  "description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -27,7 +27,10 @@ const buildBlockingCausesDetails = (
27
27
  ? 'Causas bloqueantes: 1'
28
28
  : `Causas bloqueantes: ${total}`;
29
29
  return resolvePrioritizedBlockingCauses(causes)
30
- .flatMap((cause, index) => formatBlockingCauseForDialog(cause, index))
30
+ .flatMap((cause, index) => {
31
+ const formatted = formatBlockingCauseForDialog(cause, index);
32
+ return index === 0 ? formatted : ['', ...formatted];
33
+ })
31
34
  .reduce((lines, line) => [...lines, line], [header])
32
35
  .join('\n');
33
36
  };
@@ -49,10 +52,48 @@ const stripTechnicalFieldsFromMessage = (message: string): string => {
49
52
  .replace(/\blines?=[0-9][0-9,\-\s]*/gi, '')
50
53
  .replace(/\bmessage=/gi, '')
51
54
  .replace(/\bremediation=/gi, '')
55
+ .replace(/\bmissing=\[[^\]]*\]/gi, '')
56
+ .replace(/\bsnippet=.+$/gi, '')
57
+ .replace(/\bnode=.+$/gi, '')
58
+ .replace(/\bprimary_node=.+$/gi, '')
52
59
  .replace(/\s+/g, ' ')
53
60
  .trim();
54
61
  };
55
62
 
63
+ const extractFieldFromCauseMessage = (message: string, field: string): string | null => {
64
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
65
+ const match = message.match(new RegExp(`\\b${escapedField}=([^\\n]+?)(?=\\s(?:severity|code|rule|file|lines?|message|remediation|snippet|node|primary_node|missing)=|$)`, 'i'));
66
+ const value = match?.[1]?.trim();
67
+ return value && value.length > 0 ? normalizeNotificationText(value) : null;
68
+ };
69
+
70
+ const extractSnippetFromCauseMessage = (message: string): string | null =>
71
+ extractFieldFromCauseMessage(message, 'snippet') ??
72
+ extractFieldFromCauseMessage(message, 'node') ??
73
+ extractFieldFromCauseMessage(message, 'primary_node');
74
+
75
+ const extractMissingContractFromCauseMessage = (message: string): string | null => {
76
+ const missing = extractFieldFromCauseMessage(message, 'missing')?.replace(/\]\]$/u, ']');
77
+ if (!missing) {
78
+ return null;
79
+ }
80
+ return `Falta: ${missing}`;
81
+ };
82
+
83
+ const buildEvidenceLine = (
84
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
85
+ ): string | null => {
86
+ const snippet = extractSnippetFromCauseMessage(cause.message);
87
+ if (snippet) {
88
+ return ` Evidencia: ${truncateNotificationText(snippet, 160)}`;
89
+ }
90
+ const missing = extractMissingContractFromCauseMessage(cause.message);
91
+ if (missing) {
92
+ return ` Evidencia: ${truncateNotificationText(missing, 160)}`;
93
+ }
94
+ return null;
95
+ };
96
+
56
97
  const formatLocation = (cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]): string => {
57
98
  if (!cause.file) {
58
99
  return 'sin fichero';
@@ -61,6 +102,47 @@ const formatLocation = (cause: NonNullable<Extract<PumukiCriticalNotificationEve
61
102
  return line ? `${cause.file}:${line}` : cause.file;
62
103
  };
63
104
 
105
+ const humanizeRuleId = (ruleId: string): string => {
106
+ const knownRules: Record<string, string> = {
107
+ 'prefer-swift-testing': 'Swift Testing',
108
+ 'no-xctassert': 'XCTest assertions',
109
+ 'no-wait-for-expectations': 'waitForExpectations en XCTest',
110
+ 'ios-test-quality': 'calidad de tests iOS',
111
+ 'dynamic-type-font-scaling-automatico': 'Dynamic Type',
112
+ 'dynamic-type-fuentes-escalables-y-layouts-adaptativos': 'fuentes escalables y layouts adaptativos',
113
+ 'use-relative-layout-over-hard-coded-constants': 'layout relativo en vez de constantes fijas',
114
+ 'magic-numbers-usar-constantes-con-nombres': 'números mágicos sin constantes con nombre',
115
+ 'prefer-static-member-lookup-blue-vs-color-blue': 'colores semánticos en SwiftUI',
116
+ };
117
+ const normalized = ruleId
118
+ .replace(/^skills\./u, '')
119
+ .replace(/^governance\./u, '')
120
+ .replace(/^ai_gate\./u, '')
121
+ .replace(/_/gu, '-');
122
+ const parts = normalized.split('.').filter(Boolean);
123
+ const leaf = parts.at(-1) ?? normalized;
124
+ if (knownRules[leaf]) {
125
+ return knownRules[leaf];
126
+ }
127
+ return leaf
128
+ .replace(/-/gu, ' ')
129
+ .replace(/\bios\b/giu, 'iOS')
130
+ .replace(/\bui\b/giu, 'UI')
131
+ .replace(/\bapi\b/giu, 'API')
132
+ .replace(/\btdd\b/giu, 'TDD')
133
+ .replace(/\bsdd\b/giu, 'SDD')
134
+ .replace(/\bguard\b/giu, 'guard')
135
+ .trim();
136
+ };
137
+
138
+ const formatVisibleRule = (
139
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
140
+ ): string => {
141
+ const ruleId = cause.ruleId ?? cause.code;
142
+ const readable = humanizeRuleId(ruleId);
143
+ return readable.length > 0 ? readable : ruleId;
144
+ };
145
+
64
146
  const isWorktreeHygieneCause = (
65
147
  cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
66
148
  ): boolean =>
@@ -68,16 +150,78 @@ const isWorktreeHygieneCause = (
68
150
  cause.code === 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT' ||
69
151
  (cause.ruleId ?? '').includes('EVIDENCE_PREWRITE_WORKTREE');
70
152
 
153
+ const isGoldenFlowCause = (
154
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
155
+ ): boolean =>
156
+ cause.code.startsWith('GOLDEN_FLOW_') ||
157
+ cause.code.startsWith('TDD_') ||
158
+ (cause.ruleId ?? '').includes('golden_flow') ||
159
+ (cause.ruleId ?? '').startsWith('generic_tdd_') ||
160
+ (cause.ruleId ?? '').startsWith('generic_red_green_refactor');
161
+
162
+ const isSkillCause = (
163
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
164
+ ): boolean =>
165
+ (cause.ruleId ?? '').startsWith('skills.') ||
166
+ cause.code.startsWith('SKILLS_');
167
+
71
168
  const WORKTREE_HYGIENE_REMEDIATION =
72
169
  'Reduce el worktree pendiente a un slice atómico: stagea solo la tarea activa o guarda el resto en stash nombrado, y reejecuta PRE_WRITE.';
73
170
 
171
+ const GOLDEN_FLOW_REMEDIATION =
172
+ 'Ejecuta los tests de la implementación. Si están en verde, haz un commit atómico de los cambios.';
173
+
174
+ const normalizeGoldenFlowMissingToken = (message: string): string => {
175
+ const missing = (extractMissingContractFromCauseMessage(message) ?? 'VERIFY passed')
176
+ .replace(/^Falta:\s*/u, '');
177
+ return missing.toUpperCase();
178
+ };
179
+
180
+ const formatGoldenFlowMissing = (message: string): string => {
181
+ const missing = normalizeGoldenFlowMissingToken(message);
182
+ if (missing.includes('RED')) {
183
+ return 'fase RED del ciclo TDD sin implementar';
184
+ }
185
+ if (missing.includes('GREEN')) {
186
+ return 'fase GREEN del ciclo TDD sin implementar';
187
+ }
188
+ if (missing.includes('REFACTOR')) {
189
+ return 'fase REFACTOR del ciclo TDD sin implementar';
190
+ }
191
+ if (missing.includes('VERIFY')) {
192
+ return 'tests finales en verde sin ejecutar antes del commit';
193
+ }
194
+ return missing.toLowerCase();
195
+ };
196
+
197
+ const formatGoldenFlowRemediation = (message: string): string => {
198
+ const missing = normalizeGoldenFlowMissingToken(message);
199
+ if (missing.includes('RED')) {
200
+ return 'Implementa la fase RED antes de continuar. Las fases del ciclo TDD (RED, GREEN, REFACTOR) deben completarse para desbloquear.';
201
+ }
202
+ if (missing.includes('GREEN')) {
203
+ return 'Implementa la fase GREEN antes de continuar. Haz el cambio mínimo para que el test pase y continúa el ciclo TDD.';
204
+ }
205
+ if (missing.includes('REFACTOR')) {
206
+ return 'Implementa la fase REFACTOR antes de continuar. Limpia la solución manteniendo los tests en verde.';
207
+ }
208
+ if (missing.includes('VERIFY')) {
209
+ return GOLDEN_FLOW_REMEDIATION;
210
+ }
211
+ return 'Completa el ciclo TDD en orden: RED, GREEN, REFACTOR y tests finales en verde antes del commit.';
212
+ };
213
+
214
+
74
215
  const formatBlockingCauseForDialog = (
75
216
  cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number],
76
217
  index: number
77
218
  ): readonly string[] => {
78
219
  const rule = cause.ruleId ?? cause.code;
220
+ const visibleRule = formatVisibleRule(cause);
79
221
  const problem = stripTechnicalFieldsFromMessage(cause.message) || cause.code;
80
- const remediation = isWorktreeHygieneCause(cause)
222
+ const remediation = isGoldenFlowCause(cause)
223
+ ? GOLDEN_FLOW_REMEDIATION
224
+ : isWorktreeHygieneCause(cause)
81
225
  ? WORKTREE_HYGIENE_REMEDIATION
82
226
  : cause.remediation
83
227
  ? normalizeNotificationText(cause.remediation)
@@ -90,13 +234,22 @@ const formatBlockingCauseForDialog = (
90
234
  ` Solución: ${truncateNotificationText(WORKTREE_HYGIENE_REMEDIATION, 180)}`,
91
235
  ];
92
236
  }
237
+ if (isGoldenFlowCause(cause)) {
238
+ return [
239
+ `${index + 1}. Causa bloqueante: Violación del ciclo TDD`,
240
+ ` Registro TDD: ${truncateNotificationText(formatLocation(cause), 120)}`,
241
+ ` Falta: ${truncateNotificationText(formatGoldenFlowMissing(cause.message), 120)}`,
242
+ ];
243
+ }
93
244
 
245
+ const causeLabel = isSkillCause(cause) ? 'Regla' : 'Causa bloqueante';
94
246
  return [
95
- `${index + 1}. Regla: ${truncateNotificationText(rule, 96)}`,
247
+ `${index + 1}. ${causeLabel}: ${truncateNotificationText(visibleRule, 96)}`,
96
248
  ` Fichero: ${truncateNotificationText(formatLocation(cause), 120)}`,
97
249
  ` Viola: ${truncateNotificationText(problem, 160)}`,
250
+ buildEvidenceLine(cause),
98
251
  ` Solución: ${truncateNotificationText(remediation, 180)}`,
99
- ];
252
+ ].filter((line): line is string => typeof line === 'string');
100
253
  };
101
254
 
102
255
  const hasOnlyWorktreeHygieneCauses = (
@@ -104,6 +257,18 @@ const hasOnlyWorktreeHygieneCauses = (
104
257
  ): boolean =>
105
258
  Boolean(causes?.length) && causes!.every(isWorktreeHygieneCause);
106
259
 
260
+ const hasOnlyGoldenFlowCauses = (
261
+ causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
262
+ ): boolean =>
263
+ Boolean(causes?.length) && causes!.every(isGoldenFlowCause);
264
+
265
+ const resolveGoldenFlowDialogRemediation = (
266
+ causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
267
+ ): string => {
268
+ const first = causes?.find(isGoldenFlowCause);
269
+ return first ? formatGoldenFlowRemediation(first.message) : GOLDEN_FLOW_REMEDIATION;
270
+ };
271
+
107
272
  export const buildBlockedDialogPayload = (params: {
108
273
  event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>;
109
274
  repoRoot: string;
@@ -114,6 +279,8 @@ export const buildBlockedDialogPayload = (params: {
114
279
  ?? resolveBlockedCauseSummary(params.event, causeCode);
115
280
  const remediation = hasOnlyWorktreeHygieneCauses(params.event.blockingCauses)
116
281
  ? WORKTREE_HYGIENE_REMEDIATION
282
+ : hasOnlyGoldenFlowCauses(params.event.blockingCauses)
283
+ ? resolveGoldenFlowDialogRemediation(params.event.blockingCauses)
117
284
  : params.event.blockingCauses && params.event.blockingCauses.length > 0
118
285
  ? 'Corrige las violaciones listadas y vuelve a intentar el commit.'
119
286
  : resolveBlockedRemediation(params.event, causeCode);
@@ -4,7 +4,10 @@ import type {
4
4
  } from './framework-menu-system-notifications-types';
5
5
  import { resolveBlockedCauseSummary } from './framework-menu-system-notifications-cause';
6
6
  import { resolveBlockedRemediation } from './framework-menu-system-notifications-remediation';
7
- import { truncateNotificationText } from './framework-menu-system-notifications-text';
7
+ import {
8
+ normalizeNotificationText,
9
+ truncateNotificationText,
10
+ } from './framework-menu-system-notifications-text';
8
11
 
9
12
  export {
10
13
  resolveBlockedCauseSummary,
@@ -23,6 +26,40 @@ const isSkillCause = (cause: BlockedCause): boolean => {
23
26
  return ruleId.startsWith('skills.') || code.startsWith('SKILLS_');
24
27
  };
25
28
 
29
+ const isGoldenFlowCause = (cause: BlockedCause): boolean => {
30
+ const ruleId = cause.ruleId ?? '';
31
+ const code = cause.code ?? '';
32
+ return (
33
+ code.startsWith('GOLDEN_FLOW_') ||
34
+ code.startsWith('TDD_') ||
35
+ ruleId.includes('golden_flow') ||
36
+ ruleId.startsWith('generic_tdd_') ||
37
+ ruleId.startsWith('generic_red_green_refactor')
38
+ );
39
+ };
40
+
41
+ const normalizeGoldenFlowMissingToken = (message: string): string => {
42
+ const match = message.match(/missing=\[([^\]]+)\]/i);
43
+ return (match?.[1] ?? '').toUpperCase();
44
+ };
45
+
46
+ const formatGoldenFlowBannerRemediation = (cause: BlockedCause): string => {
47
+ const missing = normalizeGoldenFlowMissingToken(cause.message);
48
+ if (missing.includes('RED')) {
49
+ return 'Implementa la fase RED antes de continuar. Las fases del ciclo TDD (RED, GREEN, REFACTOR) deben completarse para desbloquear.';
50
+ }
51
+ if (missing.includes('GREEN')) {
52
+ return 'Implementa la fase GREEN antes de continuar. Haz el cambio mínimo para que el test pase y continúa el ciclo TDD.';
53
+ }
54
+ if (missing.includes('REFACTOR')) {
55
+ return 'Implementa la fase REFACTOR antes de continuar. Limpia la solución manteniendo los tests en verde.';
56
+ }
57
+ if (missing.includes('VERIFY')) {
58
+ return 'Ejecuta los tests de la implementación. Si están en verde, haz un commit atómico de los cambios.';
59
+ }
60
+ return 'Completa el ciclo TDD en orden: RED, GREEN, REFACTOR y tests finales en verde antes del commit.';
61
+ };
62
+
26
63
  const formatCauseLocation = (cause: BlockedCause): string => {
27
64
  if (!cause.file) {
28
65
  return 'sin fichero';
@@ -37,6 +74,78 @@ const formatCauseLocation = (cause: BlockedCause): string => {
37
74
 
38
75
  const formatCauseRule = (cause: BlockedCause): string => cause.ruleId ?? cause.code;
39
76
 
77
+ const humanizeRuleId = (ruleId: string): string => {
78
+ const knownRules: Record<string, string> = {
79
+ 'prefer-swift-testing': 'Swift Testing',
80
+ 'no-xctassert': 'XCTest assertions',
81
+ 'no-wait-for-expectations': 'waitForExpectations en XCTest',
82
+ 'ios-test-quality': 'calidad de tests iOS',
83
+ 'dynamic-type-font-scaling-automatico': 'Dynamic Type',
84
+ 'dynamic-type-fuentes-escalables-y-layouts-adaptativos': 'fuentes escalables y layouts adaptativos',
85
+ 'use-relative-layout-over-hard-coded-constants': 'layout relativo en vez de constantes fijas',
86
+ 'magic-numbers-usar-constantes-con-nombres': 'números mágicos sin constantes con nombre',
87
+ 'prefer-static-member-lookup-blue-vs-color-blue': 'colores semánticos en SwiftUI',
88
+ };
89
+ const normalized = ruleId
90
+ .replace(/^skills\./u, '')
91
+ .replace(/^governance\./u, '')
92
+ .replace(/^ai_gate\./u, '')
93
+ .replace(/_/gu, '-');
94
+ const parts = normalized.split('.').filter(Boolean);
95
+ const leaf = parts.at(-1) ?? normalized;
96
+ if (knownRules[leaf]) {
97
+ return knownRules[leaf];
98
+ }
99
+ return leaf
100
+ .replace(/-/gu, ' ')
101
+ .replace(/\bios\b/giu, 'iOS')
102
+ .replace(/\bui\b/giu, 'UI')
103
+ .replace(/\bapi\b/giu, 'API')
104
+ .replace(/\btdd\b/giu, 'TDD')
105
+ .replace(/\bsdd\b/giu, 'SDD')
106
+ .replace(/\bguard\b/giu, 'guard')
107
+ .trim();
108
+ };
109
+
110
+ const formatVisibleRule = (cause: BlockedCause): string => {
111
+ const ruleId = formatCauseRule(cause);
112
+ const readable = humanizeRuleId(ruleId);
113
+ return readable.length > 0 ? readable : ruleId;
114
+ };
115
+
116
+ const extractMessageField = (message: string, field: string): string | null => {
117
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
118
+ const match = message.match(new RegExp(`\\b${escapedField}=([^\\n]+?)(?=\\s(?:severity|code|rule|file|lines?|message|remediation|snippet|node|primary_node|missing)=|$)`, 'i'));
119
+ const value = match?.[1]?.trim();
120
+ return value && value.length > 0 ? normalizeNotificationText(value) : null;
121
+ };
122
+
123
+ const stripTechnicalFieldsFromMessage = (message: string): string =>
124
+ normalizeNotificationText(message)
125
+ .replace(/\bseverity=[A-Z]+\b/gi, '')
126
+ .replace(/\bcode=[A-Z0-9_]+\b/gi, '')
127
+ .replace(/\brule=[^\s]+/gi, '')
128
+ .replace(/\bfile=[^\s]+/gi, '')
129
+ .replace(/\blines?=[0-9][0-9,\-\s]*/gi, '')
130
+ .replace(/\bmessage=/gi, '')
131
+ .replace(/\bremediation=/gi, '')
132
+ .replace(/\s+/g, ' ')
133
+ .trim();
134
+
135
+ const formatCauseProblem = (cause: BlockedCause): string => {
136
+ const messageField = extractMessageField(cause.message, 'message');
137
+ if (messageField) {
138
+ return messageField;
139
+ }
140
+ return stripTechnicalFieldsFromMessage(cause.message) || cause.code;
141
+ };
142
+
143
+ const formatCauseFix = (cause: BlockedCause): string =>
144
+ normalizeNotificationText(
145
+ cause.remediation ??
146
+ 'Corrige la violación indicada con regla, fichero y línea, y vuelve a ejecutar el gate.'
147
+ );
148
+
40
149
  export const resolvePrioritizedBlockingCauses = (
41
150
  causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
42
151
  ): ReadonlyArray<BlockedCause> => {
@@ -49,6 +158,11 @@ export const resolvePrioritizedBlockingCauses = (
49
158
  if (leftSkill !== rightSkill) {
50
159
  return leftSkill ? -1 : 1;
51
160
  }
161
+ const leftGoldenFlow = isGoldenFlowCause(left);
162
+ const rightGoldenFlow = isGoldenFlowCause(right);
163
+ if (leftGoldenFlow !== rightGoldenFlow) {
164
+ return leftGoldenFlow ? -1 : 1;
165
+ }
52
166
  return 0;
53
167
  });
54
168
  };
@@ -66,6 +180,12 @@ const buildBlockingCausesSummary = (
66
180
  }
67
181
  const prefix = isSkillCause(first) ? 'Skill violada' : 'Causa bloqueante';
68
182
  const overflow = prioritized.length > 1 ? ` (+${prioritized.length - 1} más)` : '';
183
+ if (isSkillCause(first)) {
184
+ return `${prefix}: ${formatCauseLocation(first)} · ${formatVisibleRule(first)}${overflow}`;
185
+ }
186
+ if (isGoldenFlowCause(first)) {
187
+ return `Violación del ciclo TDD${overflow}`;
188
+ }
69
189
  return `${prefix}: ${formatCauseRule(first)} · ${formatCauseLocation(first)}${overflow}`;
70
190
  };
71
191
 
@@ -80,10 +200,21 @@ const buildBlockingCausesRemediation = (
80
200
  if (!first) {
81
201
  return null;
82
202
  }
83
- const remediation =
84
- first.remediation ??
85
- 'Corrige la violación indicada con regla, fichero y línea, y vuelve a ejecutar el gate.';
86
- return `${remediation} Revisa el reporte completo para el resto de causas bloqueantes.`;
203
+ if (isSkillCause(first)) {
204
+ return [
205
+ `Regla: ${formatVisibleRule(first)}.`,
206
+ `Fichero: ${formatCauseLocation(first)}.`,
207
+ `Viola: ${formatCauseProblem(first)}.`,
208
+ `Solución: ${formatCauseFix(first)}.`,
209
+ prioritized.length > 1 ? `Quedan ${prioritized.length - 1} causa(s) más en el reporte completo.` : '',
210
+ ]
211
+ .filter((line) => line.length > 0)
212
+ .join(' ');
213
+ }
214
+ if (isGoldenFlowCause(first)) {
215
+ return formatGoldenFlowBannerRemediation(first);
216
+ }
217
+ return `${formatCauseFix(first)} Revisa el reporte completo para el resto de causas bloqueantes.`;
87
218
  };
88
219
 
89
220
  export const buildGateBlockedPayload = (
@@ -93,7 +224,7 @@ export const buildGateBlockedPayload = (
93
224
  const causeCode = event.causeCode ?? 'GATE_BLOCKED';
94
225
  const causeSummary = truncateNotificationText(
95
226
  buildBlockingCausesSummary(event.blockingCauses) ?? resolveBlockedCauseSummary(event, causeCode),
96
- 72
227
+ 96
97
228
  );
98
229
  const remediation =
99
230
  buildBlockingCausesRemediation(event.blockingCauses) ?? resolveBlockedRemediation(event, causeCode);