pumuki 6.3.314 → 6.3.316
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/integrations/lifecycle/preWriteAutomation.ts +19 -1
- package/integrations/tdd/enforcement.test.ts +77 -0
- package/integrations/tdd/enforcement.ts +21 -8
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +39 -6
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +23 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +3 -1
|
@@ -98,11 +98,23 @@ const PRE_WRITE_FUNCTIONAL_EXTENSIONS = [
|
|
|
98
98
|
'.kts',
|
|
99
99
|
] as const;
|
|
100
100
|
|
|
101
|
+
const PRE_WRITE_RUNTIME_ARTIFACT_PATHS = new Set<string>([
|
|
102
|
+
'.ai_evidence.json',
|
|
103
|
+
'.AI_EVIDENCE.json',
|
|
104
|
+
'.pumuki/artifacts/mcp-ai-gate-receipt.json',
|
|
105
|
+
'.pumuki/prewrite-lease.json',
|
|
106
|
+
]);
|
|
107
|
+
|
|
101
108
|
const isFunctionalPath = (filePath: string): boolean => {
|
|
102
109
|
const normalized = filePath.trim().toLowerCase();
|
|
103
110
|
return PRE_WRITE_FUNCTIONAL_EXTENSIONS.some((extension) => normalized.endsWith(extension));
|
|
104
111
|
};
|
|
105
112
|
|
|
113
|
+
const isRuntimeArtifactPath = (filePath: string): boolean => {
|
|
114
|
+
const normalized = filePath.trim().replace(/\\/g, '/');
|
|
115
|
+
return PRE_WRITE_RUNTIME_ARTIFACT_PATHS.has(normalized);
|
|
116
|
+
};
|
|
117
|
+
|
|
106
118
|
const collectStagedPaths = (repoRoot: string): ReadonlyArray<string> | null => {
|
|
107
119
|
try {
|
|
108
120
|
return execFileSync('git', ['diff', '--cached', '--name-only'], {
|
|
@@ -127,7 +139,13 @@ const resolvePreWriteRefreshScope = (aiGate: ReturnType<typeof evaluateAiGate>):
|
|
|
127
139
|
if (stagedPaths === null || stagedPaths.some(isFunctionalPath)) {
|
|
128
140
|
return { kind: 'staged' };
|
|
129
141
|
}
|
|
130
|
-
|
|
142
|
+
if (stagedPaths.length === 0) {
|
|
143
|
+
return { kind: 'workingTree' };
|
|
144
|
+
}
|
|
145
|
+
if (stagedPaths.length > 0 && stagedPaths.every(isRuntimeArtifactPath)) {
|
|
146
|
+
return { kind: 'workingTree' };
|
|
147
|
+
}
|
|
148
|
+
return { kind: 'staged' };
|
|
131
149
|
};
|
|
132
150
|
|
|
133
151
|
export const buildPreWriteAutomationTrace = async (params: {
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import type { Fact } from '../../core/facts/Fact';
|
|
7
|
+
import { enforceTddBddPolicy } from './enforcement';
|
|
8
|
+
|
|
9
|
+
test('enforceTddBddPolicy explica cómo registrar XCTest verde cuando falta VERIFY', () => {
|
|
10
|
+
const repoRoot = mkdtempSync(join(tmpdir(), 'pumuki-tdd-verify-xctest-'));
|
|
11
|
+
try {
|
|
12
|
+
mkdirSync(join(repoRoot, 'features'), { recursive: true });
|
|
13
|
+
mkdirSync(join(repoRoot, '.pumuki', 'artifacts'), { recursive: true });
|
|
14
|
+
writeFileSync(join(repoRoot, 'features', 'rgo-1900.feature'), 'Feature: social auth\n', 'utf8');
|
|
15
|
+
writeFileSync(
|
|
16
|
+
join(repoRoot, '.pumuki', 'artifacts', 'pumuki-evidence-v1.json'),
|
|
17
|
+
JSON.stringify({
|
|
18
|
+
version: '1',
|
|
19
|
+
generated_at: '2026-05-20T08:00:00.000Z',
|
|
20
|
+
slices: [
|
|
21
|
+
{
|
|
22
|
+
id: 'rgo-1900',
|
|
23
|
+
scenario_ref: 'features/rgo-1900.feature',
|
|
24
|
+
red: {
|
|
25
|
+
status: 'failed',
|
|
26
|
+
timestamp: '2026-05-20T08:00:00.000Z',
|
|
27
|
+
test_ref: 'RuralGoMacTests/BuyerAppViewModelAuthTests',
|
|
28
|
+
},
|
|
29
|
+
green: {
|
|
30
|
+
status: 'passed',
|
|
31
|
+
timestamp: '2026-05-20T08:10:00.000Z',
|
|
32
|
+
test_ref: 'RuralGoMacTests/BuyerAppViewModelAuthTests',
|
|
33
|
+
},
|
|
34
|
+
refactor: {
|
|
35
|
+
status: 'passed',
|
|
36
|
+
timestamp: '2026-05-20T08:20:00.000Z',
|
|
37
|
+
test_ref: 'RuralGoMacTests/BuyerCommerceScreensSnapshotTests/test_checkoutSnapshot_matchesReference',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
}),
|
|
42
|
+
'utf8'
|
|
43
|
+
);
|
|
44
|
+
const facts: ReadonlyArray<Fact> = [
|
|
45
|
+
{
|
|
46
|
+
kind: 'FileChange',
|
|
47
|
+
path: 'apps/ios/Presentation/Features/BuyerCommerce/BuyerAuthScreen.swift',
|
|
48
|
+
changeType: 'modified',
|
|
49
|
+
source: 'git:staged',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
kind: 'FileContent',
|
|
53
|
+
path: 'apps/ios/Presentation/Features/BuyerCommerce/BuyerAuthScreen.swift',
|
|
54
|
+
content: 'public struct BuyerAuthScreen {}',
|
|
55
|
+
source: 'git:staged',
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
const result = enforceTddBddPolicy({
|
|
60
|
+
facts,
|
|
61
|
+
repoRoot,
|
|
62
|
+
branch: 'feature/rgo-1900-02-checkout-pixel-perfect',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const finding = result.findings.find(
|
|
66
|
+
(item) => item.code === 'GOLDEN_FLOW_VERIFY_EVIDENCE_MISSING'
|
|
67
|
+
);
|
|
68
|
+
assert.ok(finding);
|
|
69
|
+
assert.match(finding.message, /missing=\[verify\.status=passed\]/);
|
|
70
|
+
assert.match(finding.message, /BuyerCommerceScreensSnapshotTests/);
|
|
71
|
+
assert.match(finding.expected_fix ?? '', /pumuki sdd evidence/);
|
|
72
|
+
assert.match(finding.expected_fix ?? '', /--test-status=passed/);
|
|
73
|
+
assert.match(finding.expected_fix ?? '', /commit atómico/);
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(repoRoot, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
@@ -19,6 +19,7 @@ const buildFinding = (params: {
|
|
|
19
19
|
severity?: Finding['severity'];
|
|
20
20
|
filePath?: string;
|
|
21
21
|
source?: string;
|
|
22
|
+
expected_fix?: string;
|
|
22
23
|
}): Finding => {
|
|
23
24
|
return {
|
|
24
25
|
ruleId: params.ruleId,
|
|
@@ -28,6 +29,7 @@ const buildFinding = (params: {
|
|
|
28
29
|
filePath: params.filePath,
|
|
29
30
|
matchedBy: 'TddBddEnforcer',
|
|
30
31
|
source: params.source ?? 'tdd-bdd-contract',
|
|
32
|
+
expected_fix: params.expected_fix,
|
|
31
33
|
};
|
|
32
34
|
};
|
|
33
35
|
|
|
@@ -301,8 +303,14 @@ export const enforceTddBddPolicy = (params: {
|
|
|
301
303
|
buildFinding({
|
|
302
304
|
ruleId: 'generic_golden_flow_verify_required',
|
|
303
305
|
code: 'GOLDEN_FLOW_VERIFY_EVIDENCE_MISSING',
|
|
304
|
-
message:
|
|
306
|
+
message:
|
|
307
|
+
`Slice ${slice.id} is missing final test verification after REFACTOR. ` +
|
|
308
|
+
`missing=[verify.status=passed] test_ref=${slice.refactor.test_ref ?? slice.green.test_ref ?? 'unregistered'}`,
|
|
305
309
|
filePath: evidenceRead.path,
|
|
310
|
+
expected_fix:
|
|
311
|
+
`Si el test/build ya pasó, informa a Pumuki del resultado real con: ` +
|
|
312
|
+
`npx pumuki sdd evidence --scenario-id=${slice.id} --test-command="<comando XCTest/build ejecutado>" --test-status=passed. ` +
|
|
313
|
+
`Después haz un commit atómico del slice.`,
|
|
306
314
|
})
|
|
307
315
|
);
|
|
308
316
|
} else {
|
|
@@ -310,13 +318,18 @@ export const enforceTddBddPolicy = (params: {
|
|
|
310
318
|
if (slice.verify.status !== 'passed') {
|
|
311
319
|
sliceFindings.push(
|
|
312
320
|
buildFinding({
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
ruleId: 'generic_golden_flow_verify_required',
|
|
322
|
+
code: 'GOLDEN_FLOW_VERIFY_MUST_PASS',
|
|
323
|
+
message:
|
|
324
|
+
`Slice ${slice.id} has final test verification but it is not green. ` +
|
|
325
|
+
`missing=[verify.status=passed] test_ref=${slice.verify.test_ref ?? 'unregistered'}`,
|
|
326
|
+
filePath: evidenceRead.path,
|
|
327
|
+
expected_fix:
|
|
328
|
+
`Corrige el fallo del test/build final y vuelve a informar a Pumuki con ` +
|
|
329
|
+
`npx pumuki sdd evidence --scenario-id=${slice.id} --test-command="<comando XCTest/build ejecutado>" --test-status=passed.`,
|
|
330
|
+
})
|
|
331
|
+
);
|
|
332
|
+
}
|
|
320
333
|
}
|
|
321
334
|
|
|
322
335
|
if (
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.316",
|
|
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": {
|
|
@@ -60,6 +60,32 @@ const stripTechnicalFieldsFromMessage = (message: string): string => {
|
|
|
60
60
|
.trim();
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
const localizeDeveloperText = (value: string): string => {
|
|
64
|
+
const normalized = normalizeNotificationText(value);
|
|
65
|
+
const knownTexts: ReadonlyArray<[RegExp, string]> = [
|
|
66
|
+
[/Disallow empty catch blocks in backend runtime code\./iu, 'Catch vacío en código backend runtime.'],
|
|
67
|
+
[/Action handlers should reference methods, not contain inline logic/iu, 'El handler contiene lógica inline; debe llamar a un método o comando del view model.'],
|
|
68
|
+
[/Ensure constant number of views per ForEach element/iu, 'Cada elemento de ForEach debe devolver una estructura de vistas estable.'],
|
|
69
|
+
[/Ensure ForEach uses stable identity/iu, 'ForEach debe usar una identidad estable.'],
|
|
70
|
+
[/Prefer modifiers over conditional views for state changes/iu, 'Para cambios de estado, usa modificadores en vez de cambiar la identidad de la vista.'],
|
|
71
|
+
[/Prefer static member lookup \(\.blue vs Color\.blue\)/iu, 'Usa colores semánticos o static member lookup en SwiftUI.'],
|
|
72
|
+
[/LazyVStack\/LazyHStack - Para listas grandes/iu, 'Las listas grandes deben renderizarse con LazyVStack o LazyHStack.'],
|
|
73
|
+
[/navigationDestination \(for:\) - Destinos tipados/iu, 'La navegación debe usar navigationDestination(for:) con destinos tipados.'],
|
|
74
|
+
[/Extract the action body to a named method or view model command and reference that method from Button\./iu, 'Extrae la acción a un método con nombre o comando del view model y referencia ese método desde Button.'],
|
|
75
|
+
[/Move branching into a dedicated row view or prefer conditional modifiers\/values that keep a stable view structure per element\./iu, 'Mueve la ramificación a una fila dedicada o usa modificadores condicionales que mantengan estable la estructura de vistas por elemento.'],
|
|
76
|
+
[/Use Identifiable models or an explicit stable domain identifier such as id: \\.id\./iu, 'Usa modelos Identifiable o un identificador estable del dominio, por ejemplo id: \\.id.'],
|
|
77
|
+
[/Keep one view instance and move the condition into modifiers, parameters or extracted stable row state\./iu, 'Mantén una única instancia de vista y mueve la condición a modificadores, parámetros o estado estable extraído.'],
|
|
78
|
+
[/Replace Color\.blue\/Color\.primary\/etc\. with \.blue\/\.primary in SwiftUI style contexts, or use named asset colors such as Color\("BrandPrimary"\) when design tokens are required\./iu, 'Sustituye Color.blue/Color.primary por .blue/.primary en contextos SwiftUI, o usa colores semánticos del design system cuando aplique.'],
|
|
79
|
+
[/Replace VStack\/HStack under ScrollView with LazyVStack\/LazyHStack when rendering collection rows\./iu, 'Sustituye VStack/HStack dentro de ScrollView por LazyVStack/LazyHStack al renderizar colecciones.'],
|
|
80
|
+
];
|
|
81
|
+
for (const [pattern, replacement] of knownTexts) {
|
|
82
|
+
if (pattern.test(normalized)) {
|
|
83
|
+
return replacement;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return normalized;
|
|
87
|
+
};
|
|
88
|
+
|
|
63
89
|
const extractFieldFromCauseMessage = (message: string, field: string): string | null => {
|
|
64
90
|
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
65
91
|
const match = message.match(new RegExp(`\\b${escapedField}=([^\\n]+?)(?=\\s(?:severity|code|rule|file|lines?|message|remediation|snippet|node|primary_node|missing)=|$)`, 'i'));
|
|
@@ -104,15 +130,22 @@ const formatLocation = (cause: NonNullable<Extract<PumukiCriticalNotificationEve
|
|
|
104
130
|
|
|
105
131
|
const humanizeRuleId = (ruleId: string): string => {
|
|
106
132
|
const knownRules: Record<string, string> = {
|
|
133
|
+
'no-empty-catch': 'catch vacío',
|
|
107
134
|
'prefer-swift-testing': 'Swift Testing',
|
|
108
135
|
'no-xctassert': 'XCTest assertions',
|
|
109
136
|
'no-wait-for-expectations': 'waitForExpectations en XCTest',
|
|
110
137
|
'ios-test-quality': 'calidad de tests iOS',
|
|
138
|
+
'action-handlers-should-reference-methods-not-contain-inline-logic': 'handlers SwiftUI sin lógica inline',
|
|
139
|
+
'ensure-constant-number-of-views-per-foreach-element': 'estructura estable en ForEach',
|
|
140
|
+
'ensure-foreach-uses-stable-identity-see-references-list-patterns-md': 'identidad estable en ForEach',
|
|
141
|
+
'prefer-modifiers-over-conditional-views-for-state-changes': 'modificadores para cambios de estado',
|
|
111
142
|
'dynamic-type-font-scaling-automatico': 'Dynamic Type',
|
|
112
143
|
'dynamic-type-fuentes-escalables-y-layouts-adaptativos': 'fuentes escalables y layouts adaptativos',
|
|
113
144
|
'use-relative-layout-over-hard-coded-constants': 'layout relativo en vez de constantes fijas',
|
|
114
145
|
'magic-numbers-usar-constantes-con-nombres': 'números mágicos sin constantes con nombre',
|
|
115
146
|
'prefer-static-member-lookup-blue-vs-color-blue': 'colores semánticos en SwiftUI',
|
|
147
|
+
'use-lazyvstack-lazyhstack-for-large-lists': 'LazyVStack/LazyHStack para listas grandes',
|
|
148
|
+
'use-navigationdestination-for-type-safe-navigation': 'navigationDestination type-safe',
|
|
116
149
|
};
|
|
117
150
|
const normalized = ruleId
|
|
118
151
|
.replace(/^skills\./u, '')
|
|
@@ -194,7 +227,7 @@ const formatGoldenFlowMissing = (message: string): string => {
|
|
|
194
227
|
return missing.toLowerCase();
|
|
195
228
|
};
|
|
196
229
|
|
|
197
|
-
const formatGoldenFlowRemediation = (message: string): string => {
|
|
230
|
+
const formatGoldenFlowRemediation = (message: string, remediation?: string): string => {
|
|
198
231
|
const missing = normalizeGoldenFlowMissingToken(message);
|
|
199
232
|
if (missing.includes('RED')) {
|
|
200
233
|
return 'Implementa la fase RED antes de continuar. Las fases del ciclo TDD (RED, GREEN, REFACTOR) deben completarse para desbloquear.';
|
|
@@ -206,7 +239,7 @@ const formatGoldenFlowRemediation = (message: string): string => {
|
|
|
206
239
|
return 'Implementa la fase REFACTOR antes de continuar. Limpia la solución manteniendo los tests en verde.';
|
|
207
240
|
}
|
|
208
241
|
if (missing.includes('VERIFY')) {
|
|
209
|
-
return GOLDEN_FLOW_REMEDIATION;
|
|
242
|
+
return remediation ? localizeDeveloperText(remediation) : GOLDEN_FLOW_REMEDIATION;
|
|
210
243
|
}
|
|
211
244
|
return 'Completa el ciclo TDD en orden: RED, GREEN, REFACTOR y tests finales en verde antes del commit.';
|
|
212
245
|
};
|
|
@@ -218,13 +251,13 @@ const formatBlockingCauseForDialog = (
|
|
|
218
251
|
): readonly string[] => {
|
|
219
252
|
const rule = cause.ruleId ?? cause.code;
|
|
220
253
|
const visibleRule = formatVisibleRule(cause);
|
|
221
|
-
const problem = stripTechnicalFieldsFromMessage(cause.message) || cause.code;
|
|
254
|
+
const problem = localizeDeveloperText(stripTechnicalFieldsFromMessage(cause.message) || cause.code);
|
|
222
255
|
const remediation = isGoldenFlowCause(cause)
|
|
223
256
|
? GOLDEN_FLOW_REMEDIATION
|
|
224
257
|
: isWorktreeHygieneCause(cause)
|
|
225
258
|
? WORKTREE_HYGIENE_REMEDIATION
|
|
226
259
|
: cause.remediation
|
|
227
|
-
?
|
|
260
|
+
? localizeDeveloperText(cause.remediation)
|
|
228
261
|
: 'Corrige la violación indicada y vuelve a intentar el commit.';
|
|
229
262
|
if (isWorktreeHygieneCause(cause)) {
|
|
230
263
|
return [
|
|
@@ -246,7 +279,7 @@ const formatBlockingCauseForDialog = (
|
|
|
246
279
|
return [
|
|
247
280
|
`${index + 1}. ${causeLabel}: ${truncateNotificationText(visibleRule, 96)}`,
|
|
248
281
|
` Fichero: ${truncateNotificationText(formatLocation(cause), 120)}`,
|
|
249
|
-
`
|
|
282
|
+
` Falla: ${truncateNotificationText(problem, 160)}`,
|
|
250
283
|
buildEvidenceLine(cause),
|
|
251
284
|
` Solución: ${truncateNotificationText(remediation, 180)}`,
|
|
252
285
|
].filter((line): line is string => typeof line === 'string');
|
|
@@ -266,7 +299,7 @@ const resolveGoldenFlowDialogRemediation = (
|
|
|
266
299
|
causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
|
|
267
300
|
): string => {
|
|
268
301
|
const first = causes?.find(isGoldenFlowCause);
|
|
269
|
-
return first ? formatGoldenFlowRemediation(first.message) : GOLDEN_FLOW_REMEDIATION;
|
|
302
|
+
return first ? formatGoldenFlowRemediation(first.message, first.remediation) : GOLDEN_FLOW_REMEDIATION;
|
|
270
303
|
};
|
|
271
304
|
|
|
272
305
|
export const buildBlockedDialogPayload = (params: {
|
|
@@ -41,8 +41,30 @@ final class DialogAppDelegate: NSObject, NSApplicationDelegate {
|
|
|
41
41
|
|
|
42
42
|
let alert = NSAlert()
|
|
43
43
|
alert.messageText = config.title
|
|
44
|
-
alert.informativeText = "
|
|
44
|
+
alert.informativeText = "Revisa el detalle del bloqueo. Puedes seleccionar y copiar el texto."
|
|
45
45
|
alert.alertStyle = .critical
|
|
46
|
+
|
|
47
|
+
let details = "Causa:\n\(config.cause)\n\nSolución:\n\(config.remediation)"
|
|
48
|
+
let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 560, height: 420))
|
|
49
|
+
scrollView.hasVerticalScroller = true
|
|
50
|
+
scrollView.hasHorizontalScroller = false
|
|
51
|
+
scrollView.borderType = .bezelBorder
|
|
52
|
+
|
|
53
|
+
let textView = NSTextView(frame: scrollView.contentView.bounds)
|
|
54
|
+
textView.string = details
|
|
55
|
+
textView.isEditable = false
|
|
56
|
+
textView.isSelectable = true
|
|
57
|
+
textView.drawsBackground = false
|
|
58
|
+
textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
|
|
59
|
+
textView.textContainerInset = NSSize(width: 8, height: 8)
|
|
60
|
+
textView.textContainer?.widthTracksTextView = true
|
|
61
|
+
textView.textContainer?.containerSize = NSSize(width: scrollView.contentSize.width, height: CGFloat.greatestFiniteMagnitude)
|
|
62
|
+
textView.isVerticallyResizable = true
|
|
63
|
+
textView.isHorizontallyResizable = false
|
|
64
|
+
textView.autoresizingMask = [.width]
|
|
65
|
+
scrollView.documentView = textView
|
|
66
|
+
alert.accessoryView = scrollView
|
|
67
|
+
|
|
46
68
|
alert.addButton(withTitle: config.keepButton)
|
|
47
69
|
alert.addButton(withTitle: config.muteButton)
|
|
48
70
|
alert.addButton(withTitle: config.disableButton)
|
|
@@ -55,7 +55,9 @@ const formatGoldenFlowBannerRemediation = (cause: BlockedCause): string => {
|
|
|
55
55
|
return 'Implementa la fase REFACTOR antes de continuar. Limpia la solución manteniendo los tests en verde.';
|
|
56
56
|
}
|
|
57
57
|
if (missing.includes('VERIFY')) {
|
|
58
|
-
return
|
|
58
|
+
return cause.remediation
|
|
59
|
+
? normalizeNotificationText(cause.remediation)
|
|
60
|
+
: 'Ejecuta los tests de la implementación. Si están en verde, haz un commit atómico de los cambios.';
|
|
59
61
|
}
|
|
60
62
|
return 'Completa el ciclo TDD en orden: RED, GREEN, REFACTOR y tests finales en verde antes del commit.';
|
|
61
63
|
};
|