pumuki 6.3.37 → 6.3.39
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/VERSION +1 -1
- package/docs/README.md +1 -1
- package/docs/RELEASE_NOTES.md +53 -0
- package/docs/registro-maestro-de-seguimiento.md +7 -6
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +129 -0
- package/docs/seguimiento-completo-validacion-ruralgo-03-03-2026.md +27 -3
- package/integrations/gate/evaluateAiGate.ts +20 -2
- package/integrations/git/GitService.ts +28 -1
- package/integrations/git/getCommitRangeFacts.ts +35 -5
- package/integrations/git/gitAtomicity.ts +274 -0
- package/integrations/git/runPlatformGate.ts +86 -0
- package/integrations/git/stageRunners.ts +193 -4
- package/integrations/lifecycle/adapter.templates.json +20 -20
- package/integrations/lifecycle/cli.ts +50 -1
- package/integrations/lifecycle/doctor.ts +17 -4
- package/integrations/lifecycle/hookBlock.ts +37 -11
- package/integrations/lifecycle/hookManager.ts +85 -1
- package/integrations/lifecycle/packageInfo.ts +27 -1
- package/integrations/lifecycle/status.ts +7 -2
- package/integrations/mcp/autoExecuteAiStart.ts +116 -0
- package/integrations/mcp/enterpriseServer.ts +56 -0
- package/integrations/mcp/preFlightCheck.ts +108 -0
- package/integrations/notifications/emitAuditSummaryNotification.ts +28 -0
- package/package.json +1 -1
- package/scripts/framework-menu-consumer-preflight-lib.ts +11 -0
- package/scripts/framework-menu-evidence-summary-lib.ts +1 -1
- package/scripts/framework-menu-system-notifications-lib.ts +281 -17
|
@@ -16,6 +16,16 @@ export type HookUninstallResult = {
|
|
|
16
16
|
changedHooks: ReadonlyArray<PumukiManagedHook>;
|
|
17
17
|
};
|
|
18
18
|
|
|
19
|
+
export type PumukiHooksDirectoryResolutionSource =
|
|
20
|
+
| 'git-rev-parse'
|
|
21
|
+
| 'git-config'
|
|
22
|
+
| 'default';
|
|
23
|
+
|
|
24
|
+
export type PumukiHooksDirectoryResolution = {
|
|
25
|
+
path: string;
|
|
26
|
+
source: PumukiHooksDirectoryResolutionSource;
|
|
27
|
+
};
|
|
28
|
+
|
|
19
29
|
const HOOK_FILE_MODE = 0o755;
|
|
20
30
|
|
|
21
31
|
const resolveGitPath = (repoRoot: string, gitPathTarget: string): string | null => {
|
|
@@ -34,8 +44,82 @@ const resolveGitPath = (repoRoot: string, gitPathTarget: string): string | null
|
|
|
34
44
|
}
|
|
35
45
|
};
|
|
36
46
|
|
|
47
|
+
const readCoreHooksPathFromGitConfig = (repoRoot: string): string | null => {
|
|
48
|
+
const configPath = join(repoRoot, '.git', 'config');
|
|
49
|
+
if (!existsSync(configPath)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let contents = '';
|
|
54
|
+
try {
|
|
55
|
+
contents = readFileSync(configPath, 'utf8');
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let inCoreSection = false;
|
|
61
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
62
|
+
const line = rawLine.trim();
|
|
63
|
+
if (line.length === 0 || line.startsWith(';') || line.startsWith('#')) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (line.startsWith('[') && line.endsWith(']')) {
|
|
68
|
+
inCoreSection = /^\[core\]$/i.test(line);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!inCoreSection) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const match = /^hookspath\s*=\s*(.+)$/i.exec(line);
|
|
77
|
+
if (!match) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let hooksPath = match[1]?.trim() ?? '';
|
|
82
|
+
if (hooksPath.startsWith('"') && hooksPath.endsWith('"')) {
|
|
83
|
+
hooksPath = hooksPath.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
if (hooksPath.length === 0) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
return hooksPath;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
export const resolvePumukiHooksDirectory = (
|
|
95
|
+
repoRoot: string
|
|
96
|
+
): PumukiHooksDirectoryResolution => {
|
|
97
|
+
const gitPathHooks = resolveGitPath(repoRoot, 'hooks');
|
|
98
|
+
if (gitPathHooks) {
|
|
99
|
+
return {
|
|
100
|
+
path: gitPathHooks,
|
|
101
|
+
source: 'git-rev-parse',
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const hooksPathFromConfig = readCoreHooksPathFromGitConfig(repoRoot);
|
|
106
|
+
if (hooksPathFromConfig) {
|
|
107
|
+
return {
|
|
108
|
+
path: isAbsolute(hooksPathFromConfig)
|
|
109
|
+
? hooksPathFromConfig
|
|
110
|
+
: resolve(repoRoot, hooksPathFromConfig),
|
|
111
|
+
source: 'git-config',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
path: join(repoRoot, '.git', 'hooks'),
|
|
117
|
+
source: 'default',
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
37
121
|
const resolveHooksDirectory = (repoRoot: string): string =>
|
|
38
|
-
|
|
122
|
+
resolvePumukiHooksDirectory(repoRoot).path;
|
|
39
123
|
|
|
40
124
|
const resolveHookPath = (repoRoot: string, hook: PumukiManagedHook): string =>
|
|
41
125
|
join(resolveHooksDirectory(repoRoot), hook);
|
|
@@ -1,5 +1,31 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
1
3
|
import packageJson from '../../package.json';
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
const readConsumerInstalledVersion = (repoRoot: string): string | null => {
|
|
6
|
+
const packagePath = join(repoRoot, 'node_modules', packageJson.name, 'package.json');
|
|
7
|
+
if (!existsSync(packagePath)) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const parsed = JSON.parse(readFileSync(packagePath, 'utf8')) as { version?: unknown };
|
|
12
|
+
return typeof parsed.version === 'string' && parsed.version.trim().length > 0
|
|
13
|
+
? parsed.version.trim()
|
|
14
|
+
: null;
|
|
15
|
+
} catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const getCurrentPumukiVersion = (params?: { repoRoot?: string }): string => {
|
|
21
|
+
const repoRoot = params?.repoRoot;
|
|
22
|
+
if (typeof repoRoot === 'string' && repoRoot.trim().length > 0) {
|
|
23
|
+
const installedVersion = readConsumerInstalledVersion(repoRoot.trim());
|
|
24
|
+
if (installedVersion) {
|
|
25
|
+
return installedVersion;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return packageJson.version;
|
|
29
|
+
};
|
|
4
30
|
|
|
5
31
|
export const getCurrentPumukiPackageName = (): string => packageJson.name;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getPumukiHooksStatus } from './hookManager';
|
|
1
|
+
import { getPumukiHooksStatus, resolvePumukiHooksDirectory } from './hookManager';
|
|
2
2
|
import { LifecycleGitService, type ILifecycleGitService } from './gitService';
|
|
3
3
|
import { getCurrentPumukiVersion } from './packageInfo';
|
|
4
4
|
import {
|
|
@@ -12,6 +12,8 @@ export type LifecycleStatus = {
|
|
|
12
12
|
packageVersion: string;
|
|
13
13
|
lifecycleState: LifecycleState;
|
|
14
14
|
hookStatus: ReturnType<typeof getPumukiHooksStatus>;
|
|
15
|
+
hooksDirectory: string;
|
|
16
|
+
hooksDirectoryResolution: 'git-rev-parse' | 'git-config' | 'default';
|
|
15
17
|
trackedNodeModulesCount: number;
|
|
16
18
|
policyValidation: LifecyclePolicyValidationSnapshot;
|
|
17
19
|
};
|
|
@@ -23,13 +25,16 @@ export const readLifecycleStatus = (params?: {
|
|
|
23
25
|
const git = params?.git ?? new LifecycleGitService();
|
|
24
26
|
const cwd = params?.cwd ?? process.cwd();
|
|
25
27
|
const repoRoot = git.resolveRepoRoot(cwd);
|
|
28
|
+
const hooksDirectory = resolvePumukiHooksDirectory(repoRoot);
|
|
26
29
|
const trackedNodeModulesCount = git.trackedNodeModulesPaths(repoRoot).length;
|
|
27
30
|
|
|
28
31
|
return {
|
|
29
32
|
repoRoot,
|
|
30
|
-
packageVersion: getCurrentPumukiVersion(),
|
|
33
|
+
packageVersion: getCurrentPumukiVersion({ repoRoot }),
|
|
31
34
|
lifecycleState: readLifecycleState(git, repoRoot),
|
|
32
35
|
hookStatus: getPumukiHooksStatus(repoRoot),
|
|
36
|
+
hooksDirectory: hooksDirectory.path,
|
|
37
|
+
hooksDirectoryResolution: hooksDirectory.source,
|
|
33
38
|
trackedNodeModulesCount,
|
|
34
39
|
policyValidation: readLifecyclePolicyValidationSnapshot(repoRoot),
|
|
35
40
|
};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
|
|
2
|
+
|
|
3
|
+
type AutoExecuteAction = 'proceed' | 'ask';
|
|
4
|
+
|
|
5
|
+
type AutoExecuteNextAction = {
|
|
6
|
+
kind: 'info' | 'run_command';
|
|
7
|
+
message: string;
|
|
8
|
+
command?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const isEvidenceCode = (code: string): boolean =>
|
|
12
|
+
code === 'EVIDENCE_MISSING' || code === 'EVIDENCE_INVALID' || code === 'EVIDENCE_STALE';
|
|
13
|
+
|
|
14
|
+
const confidenceFromViolation = (violationCode: string | null): number => {
|
|
15
|
+
if (!violationCode) {
|
|
16
|
+
return 90;
|
|
17
|
+
}
|
|
18
|
+
if (isEvidenceCode(violationCode)) {
|
|
19
|
+
return 65;
|
|
20
|
+
}
|
|
21
|
+
if (violationCode === 'GITFLOW_PROTECTED_BRANCH') {
|
|
22
|
+
return 40;
|
|
23
|
+
}
|
|
24
|
+
return 50;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const nextActionFromViolation = (violation: AiGateViolation | undefined): AutoExecuteNextAction => {
|
|
28
|
+
if (!violation) {
|
|
29
|
+
return {
|
|
30
|
+
kind: 'info',
|
|
31
|
+
message: 'Gate listo. Puedes continuar con implementación.',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
switch (violation.code) {
|
|
35
|
+
case 'EVIDENCE_MISSING':
|
|
36
|
+
case 'EVIDENCE_INVALID':
|
|
37
|
+
case 'EVIDENCE_STALE':
|
|
38
|
+
return {
|
|
39
|
+
kind: 'run_command',
|
|
40
|
+
message: 'Refresca evidencia y vuelve a evaluar PRE_WRITE.',
|
|
41
|
+
command: 'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
42
|
+
};
|
|
43
|
+
case 'GITFLOW_PROTECTED_BRANCH':
|
|
44
|
+
return {
|
|
45
|
+
kind: 'run_command',
|
|
46
|
+
message: 'Cambia a una rama feature/* antes de continuar.',
|
|
47
|
+
command: 'git checkout -b feature/<descripcion-kebab-case>',
|
|
48
|
+
};
|
|
49
|
+
default:
|
|
50
|
+
return {
|
|
51
|
+
kind: 'info',
|
|
52
|
+
message: 'Corrige la violación bloqueante y vuelve a ejecutar auto_execute_ai_start.',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type EnterpriseAutoExecuteAiStartResult = {
|
|
58
|
+
tool: 'auto_execute_ai_start';
|
|
59
|
+
dryRun: true;
|
|
60
|
+
executed: true;
|
|
61
|
+
success: boolean;
|
|
62
|
+
result: {
|
|
63
|
+
action: AutoExecuteAction;
|
|
64
|
+
confidence_pct: number;
|
|
65
|
+
reason_code: string;
|
|
66
|
+
next_action: AutoExecuteNextAction;
|
|
67
|
+
gate: {
|
|
68
|
+
stage: ReturnType<typeof evaluateAiGate>['stage'];
|
|
69
|
+
status: ReturnType<typeof evaluateAiGate>['status'];
|
|
70
|
+
allowed: ReturnType<typeof evaluateAiGate>['allowed'];
|
|
71
|
+
violations_count: number;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const runEnterpriseAutoExecuteAiStart = (params: {
|
|
77
|
+
repoRoot: string;
|
|
78
|
+
stage?: AiGateStage;
|
|
79
|
+
requireMcpReceipt?: boolean;
|
|
80
|
+
}): EnterpriseAutoExecuteAiStartResult => {
|
|
81
|
+
const stage = params.stage ?? 'PRE_WRITE';
|
|
82
|
+
const evaluation = evaluateAiGate({
|
|
83
|
+
repoRoot: params.repoRoot,
|
|
84
|
+
stage,
|
|
85
|
+
requireMcpReceipt: params.requireMcpReceipt ?? false,
|
|
86
|
+
});
|
|
87
|
+
const firstViolation = evaluation.violations[0];
|
|
88
|
+
const reasonCode = firstViolation?.code ?? 'READY';
|
|
89
|
+
const action: AutoExecuteAction = evaluation.allowed ? 'proceed' : 'ask';
|
|
90
|
+
const confidencePct = confidenceFromViolation(firstViolation?.code ?? null);
|
|
91
|
+
const nextAction = evaluation.allowed
|
|
92
|
+
? {
|
|
93
|
+
kind: 'info' as const,
|
|
94
|
+
message: 'Gate en verde. Continúa con la implementación.',
|
|
95
|
+
}
|
|
96
|
+
: nextActionFromViolation(firstViolation);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
tool: 'auto_execute_ai_start',
|
|
100
|
+
dryRun: true,
|
|
101
|
+
executed: true,
|
|
102
|
+
success: true,
|
|
103
|
+
result: {
|
|
104
|
+
action,
|
|
105
|
+
confidence_pct: confidencePct,
|
|
106
|
+
reason_code: reasonCode,
|
|
107
|
+
next_action: nextAction,
|
|
108
|
+
gate: {
|
|
109
|
+
stage: evaluation.stage,
|
|
110
|
+
status: evaluation.status,
|
|
111
|
+
allowed: evaluation.allowed,
|
|
112
|
+
violations_count: evaluation.violations.length,
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
};
|
|
@@ -9,6 +9,8 @@ import { evaluateSddPolicy, readSddStatus } from '../sdd';
|
|
|
9
9
|
import type { SddStage } from '../sdd';
|
|
10
10
|
import { toStatusPayload } from './evidencePayloads';
|
|
11
11
|
import { runEnterpriseAiGateCheck } from './aiGateCheck';
|
|
12
|
+
import { runEnterprisePreFlightCheck } from './preFlightCheck';
|
|
13
|
+
import { runEnterpriseAutoExecuteAiStart } from './autoExecuteAiStart';
|
|
12
14
|
import { writeMcpAiGateReceipt } from './aiGateReceipt';
|
|
13
15
|
|
|
14
16
|
export interface EnterpriseServerOptions {
|
|
@@ -73,6 +75,8 @@ const ENTERPRISE_RESOURCE_DESCRIPTORS: ReadonlyArray<{
|
|
|
73
75
|
|
|
74
76
|
const ENTERPRISE_TOOLS = [
|
|
75
77
|
'ai_gate_check',
|
|
78
|
+
'pre_flight_check',
|
|
79
|
+
'auto_execute_ai_start',
|
|
76
80
|
'check_sdd_status',
|
|
77
81
|
'validate_and_fix',
|
|
78
82
|
'sync_branches',
|
|
@@ -92,6 +96,18 @@ const ENTERPRISE_TOOL_DESCRIPTORS: ReadonlyArray<{
|
|
|
92
96
|
mutating: false,
|
|
93
97
|
safeByDefault: true,
|
|
94
98
|
},
|
|
99
|
+
{
|
|
100
|
+
name: 'pre_flight_check',
|
|
101
|
+
description: 'Runs pre-flight gate checks with actionable hints using the same AI gate evaluator.',
|
|
102
|
+
mutating: false,
|
|
103
|
+
safeByDefault: true,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
name: 'auto_execute_ai_start',
|
|
107
|
+
description: 'Returns actionable decision to continue or ask user with stable confidence and next_action.',
|
|
108
|
+
mutating: false,
|
|
109
|
+
safeByDefault: true,
|
|
110
|
+
},
|
|
95
111
|
{
|
|
96
112
|
name: 'check_sdd_status',
|
|
97
113
|
description: 'Evaluates SDD/OpenSpec policy for a stage without changing repository state.',
|
|
@@ -378,6 +394,46 @@ const executeEnterpriseTool = (
|
|
|
378
394
|
},
|
|
379
395
|
};
|
|
380
396
|
}
|
|
397
|
+
case 'pre_flight_check': {
|
|
398
|
+
const stage = toSddStage(args.stage, 'PRE_COMMIT');
|
|
399
|
+
const execution = runEnterprisePreFlightCheck({
|
|
400
|
+
repoRoot,
|
|
401
|
+
stage,
|
|
402
|
+
});
|
|
403
|
+
const receiptWrite = writeMcpAiGateReceipt({
|
|
404
|
+
repoRoot,
|
|
405
|
+
stage: execution.result.stage,
|
|
406
|
+
status: execution.result.status,
|
|
407
|
+
allowed: execution.result.allowed,
|
|
408
|
+
});
|
|
409
|
+
return {
|
|
410
|
+
name: toolName,
|
|
411
|
+
success: execution.success,
|
|
412
|
+
dryRun: execution.dryRun,
|
|
413
|
+
executed: execution.executed,
|
|
414
|
+
data: {
|
|
415
|
+
...execution.result,
|
|
416
|
+
receipt: {
|
|
417
|
+
path: receiptWrite.path,
|
|
418
|
+
issued_at: receiptWrite.receipt.issued_at,
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
case 'auto_execute_ai_start': {
|
|
424
|
+
const stage = toSddStage(args.stage, 'PRE_WRITE');
|
|
425
|
+
const execution = runEnterpriseAutoExecuteAiStart({
|
|
426
|
+
repoRoot,
|
|
427
|
+
stage,
|
|
428
|
+
});
|
|
429
|
+
return {
|
|
430
|
+
name: toolName,
|
|
431
|
+
success: execution.success,
|
|
432
|
+
dryRun: execution.dryRun,
|
|
433
|
+
executed: execution.executed,
|
|
434
|
+
data: execution.result,
|
|
435
|
+
};
|
|
436
|
+
}
|
|
381
437
|
case 'check_sdd_status': {
|
|
382
438
|
const stage = toSddStage(args.stage, 'PRE_COMMIT');
|
|
383
439
|
let evaluation: ReturnType<typeof evaluateSddPolicy>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { evaluateAiGate, type AiGateStage, type AiGateViolation } from '../gate/evaluateAiGate';
|
|
2
|
+
|
|
3
|
+
const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
|
|
4
|
+
EVIDENCE_MISSING: 'Ejecuta una auditoría (1/2/3/4) para regenerar .ai_evidence.json.',
|
|
5
|
+
EVIDENCE_INVALID: 'Regenera .ai_evidence.json desde una opción de auditoría.',
|
|
6
|
+
EVIDENCE_STALE: 'Refresca evidencia antes de continuar con commit/push.',
|
|
7
|
+
EVIDENCE_TIMESTAMP_INVALID: 'Regenera evidencia para obtener un timestamp válido.',
|
|
8
|
+
EVIDENCE_GATE_BLOCKED: 'Corrige primero las violaciones bloqueantes y vuelve a auditar.',
|
|
9
|
+
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este mismo repositorio.',
|
|
10
|
+
EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual antes de continuar.',
|
|
11
|
+
EVIDENCE_GATE_STATUS_INCOHERENT: 'Regenera evidencia para alinear ai_gate y severity_metrics.',
|
|
12
|
+
EVIDENCE_OUTCOME_INCOHERENT: 'Regenera evidencia para alinear ai_gate y snapshot.outcome.',
|
|
13
|
+
EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
|
|
14
|
+
EVIDENCE_RULES_COVERAGE_STAGE_MISMATCH: 'Reanuda auditoría en el stage correcto.',
|
|
15
|
+
EVIDENCE_RULES_COVERAGE_INCOMPLETE:
|
|
16
|
+
'Asegura unevaluated=0 y coverage_ratio=1 antes de continuar.',
|
|
17
|
+
EVIDENCE_UNSUPPORTED_AUTO_RULES:
|
|
18
|
+
'Mapea todas las reglas AUTO a detectores AST antes de continuar.',
|
|
19
|
+
EVIDENCE_TIMESTAMP_FUTURE: 'Corrige la hora del sistema y regenera evidencia.',
|
|
20
|
+
GITFLOW_PROTECTED_BRANCH: 'Evita trabajo directo en ramas protegidas (usa feature/*).',
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const buildPreFlightHints = (params: {
|
|
24
|
+
stage: AiGateStage;
|
|
25
|
+
status: ReturnType<typeof evaluateAiGate>['status'];
|
|
26
|
+
violations: ReadonlyArray<AiGateViolation>;
|
|
27
|
+
upstream: string | null;
|
|
28
|
+
}): ReadonlyArray<string> => {
|
|
29
|
+
const hints: string[] = [];
|
|
30
|
+
const emittedCodes = new Set<string>();
|
|
31
|
+
for (const violation of params.violations) {
|
|
32
|
+
if (emittedCodes.has(violation.code)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const action = ACTIONABLE_HINTS_BY_CODE[violation.code];
|
|
36
|
+
if (!action) {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
hints.push(`${violation.code}: ${action}`);
|
|
40
|
+
emittedCodes.add(violation.code);
|
|
41
|
+
}
|
|
42
|
+
if (params.stage === 'PRE_PUSH' && !params.upstream) {
|
|
43
|
+
hints.push('PRE_PUSH sin upstream: configura tracking (git push --set-upstream origin <branch>).');
|
|
44
|
+
}
|
|
45
|
+
if (hints.length === 0) {
|
|
46
|
+
if (params.status === 'ALLOWED') {
|
|
47
|
+
hints.push('Pre-flight operativo: sin bloqueos previos detectados.');
|
|
48
|
+
} else {
|
|
49
|
+
hints.push('Corrige la causa bloqueante y vuelve a ejecutar el pre-flight.');
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return hints;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export type EnterprisePreFlightCheckResult = {
|
|
56
|
+
tool: 'pre_flight_check';
|
|
57
|
+
dryRun: true;
|
|
58
|
+
executed: true;
|
|
59
|
+
success: boolean;
|
|
60
|
+
result: {
|
|
61
|
+
allowed: ReturnType<typeof evaluateAiGate>['allowed'];
|
|
62
|
+
status: ReturnType<typeof evaluateAiGate>['status'];
|
|
63
|
+
stage: ReturnType<typeof evaluateAiGate>['stage'];
|
|
64
|
+
policy: ReturnType<typeof evaluateAiGate>['policy'];
|
|
65
|
+
violations: ReturnType<typeof evaluateAiGate>['violations'];
|
|
66
|
+
evidence: ReturnType<typeof evaluateAiGate>['evidence'];
|
|
67
|
+
mcp_receipt: ReturnType<typeof evaluateAiGate>['mcp_receipt'];
|
|
68
|
+
repo_state: ReturnType<typeof evaluateAiGate>['repo_state'];
|
|
69
|
+
hints: ReadonlyArray<string>;
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export const runEnterprisePreFlightCheck = (params: {
|
|
74
|
+
repoRoot: string;
|
|
75
|
+
stage: AiGateStage;
|
|
76
|
+
requireMcpReceipt?: boolean;
|
|
77
|
+
}): EnterprisePreFlightCheckResult => {
|
|
78
|
+
const evaluation = evaluateAiGate({
|
|
79
|
+
repoRoot: params.repoRoot,
|
|
80
|
+
stage: params.stage,
|
|
81
|
+
requireMcpReceipt: params.requireMcpReceipt ?? false,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const hints = buildPreFlightHints({
|
|
85
|
+
stage: evaluation.stage,
|
|
86
|
+
status: evaluation.status,
|
|
87
|
+
violations: evaluation.violations,
|
|
88
|
+
upstream: evaluation.repo_state.git.upstream,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
tool: 'pre_flight_check',
|
|
93
|
+
dryRun: true,
|
|
94
|
+
executed: true,
|
|
95
|
+
success: evaluation.allowed,
|
|
96
|
+
result: {
|
|
97
|
+
allowed: evaluation.allowed,
|
|
98
|
+
status: evaluation.status,
|
|
99
|
+
stage: evaluation.stage,
|
|
100
|
+
policy: evaluation.policy,
|
|
101
|
+
violations: evaluation.violations,
|
|
102
|
+
evidence: evaluation.evidence,
|
|
103
|
+
mcp_receipt: evaluation.mcp_receipt,
|
|
104
|
+
repo_state: evaluation.repo_state,
|
|
105
|
+
hints,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -133,3 +133,31 @@ export const emitAuditSummaryNotificationFromAiGate = (
|
|
|
133
133
|
repoRoot: params.repoRoot,
|
|
134
134
|
});
|
|
135
135
|
};
|
|
136
|
+
|
|
137
|
+
export const emitGateBlockedNotification = (
|
|
138
|
+
params: {
|
|
139
|
+
repoRoot: string;
|
|
140
|
+
stage: AuditSummaryNotificationStage;
|
|
141
|
+
totalViolations: number;
|
|
142
|
+
causeCode: string;
|
|
143
|
+
causeMessage: string;
|
|
144
|
+
remediation: string;
|
|
145
|
+
},
|
|
146
|
+
dependencies: Partial<AuditSummaryNotificationDependencies> = {}
|
|
147
|
+
): SystemNotificationEmitResult => {
|
|
148
|
+
const activeDependencies: AuditSummaryNotificationDependencies = {
|
|
149
|
+
...defaultDependencies,
|
|
150
|
+
...dependencies,
|
|
151
|
+
};
|
|
152
|
+
return activeDependencies.emitSystemNotification({
|
|
153
|
+
event: {
|
|
154
|
+
kind: 'gate.blocked',
|
|
155
|
+
stage: params.stage,
|
|
156
|
+
totalViolations: params.totalViolations,
|
|
157
|
+
causeCode: params.causeCode,
|
|
158
|
+
causeMessage: params.causeMessage,
|
|
159
|
+
remediation: params.remediation,
|
|
160
|
+
},
|
|
161
|
+
repoRoot: params.repoRoot,
|
|
162
|
+
});
|
|
163
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.39",
|
|
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": {
|
|
@@ -154,10 +154,21 @@ const buildNotificationEvents = (result: AiGateCheckResult): ReadonlyArray<Pumuk
|
|
|
154
154
|
});
|
|
155
155
|
}
|
|
156
156
|
if (result.status === 'BLOCKED') {
|
|
157
|
+
const firstViolation = result.violations[0];
|
|
158
|
+
const causeCode = firstViolation?.code ?? 'GATE_BLOCKED';
|
|
159
|
+
const causeMessage =
|
|
160
|
+
firstViolation?.message
|
|
161
|
+
?? `Detected ${result.violations.length} blocking violations in stage ${result.stage}.`;
|
|
162
|
+
const remediation =
|
|
163
|
+
(firstViolation ? ACTIONABLE_HINTS_BY_CODE[firstViolation.code] : undefined)
|
|
164
|
+
?? 'Corrige la causa bloqueante y vuelve a ejecutar el gate.';
|
|
157
165
|
events.push({
|
|
158
166
|
kind: 'gate.blocked',
|
|
159
167
|
stage: result.stage,
|
|
160
168
|
totalViolations: result.violations.length,
|
|
169
|
+
causeCode,
|
|
170
|
+
causeMessage,
|
|
171
|
+
remediation,
|
|
161
172
|
});
|
|
162
173
|
}
|
|
163
174
|
return events;
|
|
@@ -234,7 +234,7 @@ export const formatEvidenceSummaryForMenu = (
|
|
|
234
234
|
if (summary.status === 'missing') {
|
|
235
235
|
return [
|
|
236
236
|
'Evidence: status=missing',
|
|
237
|
-
'Run
|
|
237
|
+
'Run `./node_modules/.bin/pumuki-pre-commit` to generate fresh evidence.',
|
|
238
238
|
].join('\n');
|
|
239
239
|
}
|
|
240
240
|
|