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.
@@ -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
- return { kind: 'workingTree' };
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: `Slice ${slice.id} must include VERIFY passing evidence after REFACTOR.`,
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
- 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
- }
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.314",
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
- ? normalizeNotificationText(cause.remediation)
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
- ` Viola: ${truncateNotificationText(problem, 160)}`,
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 = "Causa: \(config.cause)\n\nSolución: \(config.remediation)"
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 'Ejecuta los tests de la implementación. Si están en verde, haz un commit atómico de los cambios.';
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
  };