pumuki 6.3.315 → 6.3.317
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 +3 -3
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +5 -3
- package/scripts/framework-menu-system-notifications-remediation.ts +3 -3
|
@@ -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.317",
|
|
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": {
|
|
@@ -227,7 +227,7 @@ const formatGoldenFlowMissing = (message: string): string => {
|
|
|
227
227
|
return missing.toLowerCase();
|
|
228
228
|
};
|
|
229
229
|
|
|
230
|
-
const formatGoldenFlowRemediation = (message: string): string => {
|
|
230
|
+
const formatGoldenFlowRemediation = (message: string, remediation?: string): string => {
|
|
231
231
|
const missing = normalizeGoldenFlowMissingToken(message);
|
|
232
232
|
if (missing.includes('RED')) {
|
|
233
233
|
return 'Implementa la fase RED antes de continuar. Las fases del ciclo TDD (RED, GREEN, REFACTOR) deben completarse para desbloquear.';
|
|
@@ -239,7 +239,7 @@ const formatGoldenFlowRemediation = (message: string): string => {
|
|
|
239
239
|
return 'Implementa la fase REFACTOR antes de continuar. Limpia la solución manteniendo los tests en verde.';
|
|
240
240
|
}
|
|
241
241
|
if (missing.includes('VERIFY')) {
|
|
242
|
-
return GOLDEN_FLOW_REMEDIATION;
|
|
242
|
+
return remediation ? localizeDeveloperText(remediation) : GOLDEN_FLOW_REMEDIATION;
|
|
243
243
|
}
|
|
244
244
|
return 'Completa el ciclo TDD en orden: RED, GREEN, REFACTOR y tests finales en verde antes del commit.';
|
|
245
245
|
};
|
|
@@ -299,7 +299,7 @@ const resolveGoldenFlowDialogRemediation = (
|
|
|
299
299
|
causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
|
|
300
300
|
): string => {
|
|
301
301
|
const first = causes?.find(isGoldenFlowCause);
|
|
302
|
-
return first ? formatGoldenFlowRemediation(first.message) : GOLDEN_FLOW_REMEDIATION;
|
|
302
|
+
return first ? formatGoldenFlowRemediation(first.message, first.remediation) : GOLDEN_FLOW_REMEDIATION;
|
|
303
303
|
};
|
|
304
304
|
|
|
305
305
|
export const buildBlockedDialogPayload = (params: {
|
|
@@ -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
|
};
|
|
@@ -143,7 +145,7 @@ const formatCauseProblem = (cause: BlockedCause): string => {
|
|
|
143
145
|
const formatCauseFix = (cause: BlockedCause): string =>
|
|
144
146
|
normalizeNotificationText(
|
|
145
147
|
cause.remediation ??
|
|
146
|
-
'Corrige la violación indicada con regla, fichero y línea, y vuelve a
|
|
148
|
+
'Corrige la violación indicada con regla, fichero y línea, y vuelve a intentar el commit.'
|
|
147
149
|
);
|
|
148
150
|
|
|
149
151
|
export const resolvePrioritizedBlockingCauses = (
|
|
@@ -204,7 +206,7 @@ const buildBlockingCausesRemediation = (
|
|
|
204
206
|
return [
|
|
205
207
|
`Regla: ${formatVisibleRule(first)}.`,
|
|
206
208
|
`Fichero: ${formatCauseLocation(first)}.`,
|
|
207
|
-
`
|
|
209
|
+
`Falla: ${formatCauseProblem(first)}.`,
|
|
208
210
|
`Solución: ${formatCauseFix(first)}.`,
|
|
209
211
|
prioritized.length > 1 ? `Quedan ${prioritized.length - 1} causa(s) más en el reporte completo.` : '',
|
|
210
212
|
]
|
|
@@ -21,9 +21,9 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
21
21
|
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera la evidencia desde este repositorio y vuelve a validar.',
|
|
22
22
|
PRE_PUSH_UPSTREAM_MISSING: 'Configura upstream con `git push --set-upstream origin <branch>` y repite PRE_PUSH.',
|
|
23
23
|
SDD_SESSION_MISSING:
|
|
24
|
-
'Abre
|
|
24
|
+
'Abre la sesión SDD del cambio activo: `pumuki sdd session --open --change=<id>`. Sustituye <id> por la carpeta en openspec/changes. Después vuelve a intentar el commit; Pumuki ejecutará PRE_WRITE automáticamente.',
|
|
25
25
|
SDD_SESSION_INVALID:
|
|
26
|
-
'Refresca la sesión SDD activa
|
|
26
|
+
'Refresca la sesión SDD activa: `pumuki sdd session --refresh --ttl-minutes=90`. Después vuelve a intentar el commit; Pumuki ejecutará PRE_WRITE automáticamente.',
|
|
27
27
|
OPENSPEC_MISSING: 'Instala OpenSpec en este repositorio y vuelve a validar el gate.',
|
|
28
28
|
MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP y vuelve a validar.',
|
|
29
29
|
BACKEND_AVOID_EXPLICIT_ANY: 'Sustituye `any` por tipos concretos en backend y relanza el gate.',
|
|
@@ -34,7 +34,7 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
34
34
|
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
35
35
|
'Ejecuta `pumuki policy reconcile --strict --json` y revalida antes de continuar.',
|
|
36
36
|
EVIDENCE_PREWRITE_WORKTREE_WARN:
|
|
37
|
-
'Reduce el worktree pendiente a un slice atómico antes del commit: stagea solo la tarea activa o guarda el resto en stash nombrado, y
|
|
37
|
+
'Reduce el worktree pendiente a un slice atómico antes del commit: stagea solo la tarea activa o guarda el resto en stash nombrado, y vuelve a intentar el commit.',
|
|
38
38
|
EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
|
|
39
39
|
'El worktree supera el límite permitido: divide cambios por scope en commits atómicos o stashes nombrados antes de continuar.',
|
|
40
40
|
};
|