pumuki 6.3.311 → 6.3.312

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.312",
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
  };
@@ -53,6 +56,40 @@ const stripTechnicalFieldsFromMessage = (message: string): string => {
53
56
  .trim();
54
57
  };
55
58
 
59
+ const extractFieldFromCauseMessage = (message: string, field: string): string | null => {
60
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
61
+ const match = message.match(new RegExp(`\\b${escapedField}=([^\\n]+?)(?=\\s(?:severity|code|rule|file|lines?|message|remediation|snippet|node|primary_node|missing)=|$)`, 'i'));
62
+ const value = match?.[1]?.trim();
63
+ return value && value.length > 0 ? normalizeNotificationText(value) : null;
64
+ };
65
+
66
+ const extractSnippetFromCauseMessage = (message: string): string | null =>
67
+ extractFieldFromCauseMessage(message, 'snippet') ??
68
+ extractFieldFromCauseMessage(message, 'node') ??
69
+ extractFieldFromCauseMessage(message, 'primary_node');
70
+
71
+ const extractMissingContractFromCauseMessage = (message: string): string | null => {
72
+ const missing = extractFieldFromCauseMessage(message, 'missing')?.replace(/\]\]$/u, ']');
73
+ if (!missing) {
74
+ return null;
75
+ }
76
+ return `Falta: ${missing}`;
77
+ };
78
+
79
+ const buildEvidenceLine = (
80
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
81
+ ): string | null => {
82
+ const snippet = extractSnippetFromCauseMessage(cause.message);
83
+ if (snippet) {
84
+ return ` Evidencia: ${truncateNotificationText(snippet, 160)}`;
85
+ }
86
+ const missing = extractMissingContractFromCauseMessage(cause.message);
87
+ if (missing) {
88
+ return ` Evidencia: ${truncateNotificationText(missing, 160)}`;
89
+ }
90
+ return null;
91
+ };
92
+
56
93
  const formatLocation = (cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]): string => {
57
94
  if (!cause.file) {
58
95
  return 'sin fichero';
@@ -95,8 +132,9 @@ const formatBlockingCauseForDialog = (
95
132
  `${index + 1}. Regla: ${truncateNotificationText(rule, 96)}`,
96
133
  ` Fichero: ${truncateNotificationText(formatLocation(cause), 120)}`,
97
134
  ` Viola: ${truncateNotificationText(problem, 160)}`,
135
+ buildEvidenceLine(cause),
98
136
  ` Solución: ${truncateNotificationText(remediation, 180)}`,
99
- ];
137
+ ].filter((line): line is string => typeof line === 'string');
100
138
  };
101
139
 
102
140
  const hasOnlyWorktreeHygieneCauses = (
@@ -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,
@@ -37,6 +40,39 @@ const formatCauseLocation = (cause: BlockedCause): string => {
37
40
 
38
41
  const formatCauseRule = (cause: BlockedCause): string => cause.ruleId ?? cause.code;
39
42
 
43
+ const extractMessageField = (message: string, field: string): string | null => {
44
+ const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
+ const match = message.match(new RegExp(`\\b${escapedField}=([^\\n]+?)(?=\\s(?:severity|code|rule|file|lines?|message|remediation|snippet|node|primary_node|missing)=|$)`, 'i'));
46
+ const value = match?.[1]?.trim();
47
+ return value && value.length > 0 ? normalizeNotificationText(value) : null;
48
+ };
49
+
50
+ const stripTechnicalFieldsFromMessage = (message: string): string =>
51
+ normalizeNotificationText(message)
52
+ .replace(/\bseverity=[A-Z]+\b/gi, '')
53
+ .replace(/\bcode=[A-Z0-9_]+\b/gi, '')
54
+ .replace(/\brule=[^\s]+/gi, '')
55
+ .replace(/\bfile=[^\s]+/gi, '')
56
+ .replace(/\blines?=[0-9][0-9,\-\s]*/gi, '')
57
+ .replace(/\bmessage=/gi, '')
58
+ .replace(/\bremediation=/gi, '')
59
+ .replace(/\s+/g, ' ')
60
+ .trim();
61
+
62
+ const formatCauseProblem = (cause: BlockedCause): string => {
63
+ const messageField = extractMessageField(cause.message, 'message');
64
+ if (messageField) {
65
+ return messageField;
66
+ }
67
+ return stripTechnicalFieldsFromMessage(cause.message) || cause.code;
68
+ };
69
+
70
+ const formatCauseFix = (cause: BlockedCause): string =>
71
+ normalizeNotificationText(
72
+ cause.remediation ??
73
+ 'Corrige la violación indicada con regla, fichero y línea, y vuelve a ejecutar el gate.'
74
+ );
75
+
40
76
  export const resolvePrioritizedBlockingCauses = (
41
77
  causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
42
78
  ): ReadonlyArray<BlockedCause> => {
@@ -66,6 +102,9 @@ const buildBlockingCausesSummary = (
66
102
  }
67
103
  const prefix = isSkillCause(first) ? 'Skill violada' : 'Causa bloqueante';
68
104
  const overflow = prioritized.length > 1 ? ` (+${prioritized.length - 1} más)` : '';
105
+ if (isSkillCause(first)) {
106
+ return `${prefix}: ${formatCauseLocation(first)} · ${formatCauseRule(first)}${overflow}`;
107
+ }
69
108
  return `${prefix}: ${formatCauseRule(first)} · ${formatCauseLocation(first)}${overflow}`;
70
109
  };
71
110
 
@@ -80,10 +119,18 @@ const buildBlockingCausesRemediation = (
80
119
  if (!first) {
81
120
  return null;
82
121
  }
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.`;
122
+ if (isSkillCause(first)) {
123
+ return [
124
+ `Regla: ${formatCauseRule(first)}.`,
125
+ `Fichero: ${formatCauseLocation(first)}.`,
126
+ `Viola: ${formatCauseProblem(first)}.`,
127
+ `Solución: ${formatCauseFix(first)}.`,
128
+ prioritized.length > 1 ? `Quedan ${prioritized.length - 1} causa(s) más en el reporte completo.` : '',
129
+ ]
130
+ .filter((line) => line.length > 0)
131
+ .join(' ');
132
+ }
133
+ return `${formatCauseFix(first)} Revisa el reporte completo para el resto de causas bloqueantes.`;
87
134
  };
88
135
 
89
136
  export const buildGateBlockedPayload = (