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 +23 -0
- package/integrations/sdd/evidenceScaffold.ts +8 -0
- package/integrations/tdd/contract.ts +1 -0
- package/integrations/tdd/enforcement.ts +53 -11
- package/integrations/tdd/scope.ts +5 -2
- package/integrations/tdd/types.ts +6 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +171 -4
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +137 -6
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: {
|
|
@@ -96,15 +96,21 @@ export const enforceTddBddPolicy = (params: {
|
|
|
96
96
|
public_interface_files: scope.metrics.publicInterfaceFiles,
|
|
97
97
|
},
|
|
98
98
|
},
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
'
|
|
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.
|
|
174
|
+
const isComplexChange = reasons.some((reason) => reason.startsWith('complex.'));
|
|
172
175
|
return {
|
|
173
|
-
inScope:
|
|
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.
|
|
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) =>
|
|
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 =
|
|
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}.
|
|
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 {
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
227
|
+
96
|
|
97
228
|
);
|
|
98
229
|
const remediation =
|
|
99
230
|
buildBlockingCausesRemediation(event.blockingCauses) ?? resolveBlockedRemediation(event, causeCode);
|