pumuki 6.3.70 → 6.3.72
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 +269 -0
- package/CHANGELOG.md +666 -0
- package/README.md +32 -0
- package/docs/README.md +7 -2
- package/docs/operations/RELEASE_NOTES.md +15 -0
- package/docs/product/CONFIGURATION.md +12 -0
- package/docs/product/USAGE.md +24 -3
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +111 -0
- package/integrations/evidence/buildEvidence.ts +15 -0
- package/integrations/evidence/operationalHints.ts +110 -0
- package/integrations/evidence/schema.ts +16 -0
- package/integrations/evidence/writeEvidence.ts +3 -0
- package/integrations/gate/remediationCatalog.ts +40 -0
- package/integrations/git/GitService.ts +25 -0
- package/integrations/git/filterFactsByPathPrefixes.ts +61 -0
- package/integrations/git/runPlatformGate.ts +12 -4
- package/integrations/git/runPlatformGateFacts.ts +7 -0
- package/integrations/git/stageRunners.ts +82 -28
- package/integrations/lifecycle/cli.ts +32 -3
- package/integrations/lifecycle/doctor.ts +112 -0
- package/integrations/mcp/aiGateCheck.ts +2 -11
- package/integrations/mcp/preFlightCheck.ts +2 -1
- package/integrations/sdd/openSpecCli.ts +12 -3
- package/package.json +4 -1
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
- package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
- package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
- package/scripts/framework-menu-consumer-runtime-actions.ts +86 -6
- package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +2 -0
- package/scripts/framework-menu-consumer-runtime-types.ts +3 -1
- package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
- package/scripts/framework-menu-evidence-summary-read.ts +57 -5
- package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
- package/scripts/framework-menu-evidence-summary-types.ts +7 -0
- package/scripts/framework-menu-gate-lib.ts +9 -0
- package/scripts/framework-menu-layout-data.ts +5 -0
- package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
- package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
- package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
- package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
- package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
- package/scripts/framework-menu-system-notifications-macos.ts +4 -0
- package/scripts/framework-menu.ts +3 -0
|
@@ -4,10 +4,11 @@ import type {
|
|
|
4
4
|
ConsumerMenuMatrixReport,
|
|
5
5
|
MatrixOptionId,
|
|
6
6
|
} from './framework-menu-matrix-baseline-lib';
|
|
7
|
+
import { MATRIX_MENU_OPTION_IDS } from './framework-menu-matrix-evidence-lib';
|
|
7
8
|
import type { DoctorDeepCheckLayer, LifecycleDoctorReport } from '../integrations/lifecycle/doctor';
|
|
8
9
|
import type { LifecycleStatus } from '../integrations/lifecycle/status';
|
|
9
10
|
|
|
10
|
-
const OPTION_IDS: ReadonlyArray<MatrixOptionId> = [
|
|
11
|
+
const OPTION_IDS: ReadonlyArray<MatrixOptionId> = [...MATRIX_MENU_OPTION_IDS];
|
|
11
12
|
const DOCTOR_LAYERS: ReadonlyArray<DoctorDeepCheckLayer> = [
|
|
12
13
|
'core',
|
|
13
14
|
'operational',
|
|
@@ -51,45 +52,19 @@ export type ConsumerMenuMatrixBaselineSnapshot = {
|
|
|
51
52
|
};
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
const UNKNOWN_OPTION_REPORT = {
|
|
56
|
+
stage: 'UNKNOWN',
|
|
57
|
+
outcome: 'UNKNOWN',
|
|
58
|
+
filesScanned: 0,
|
|
59
|
+
totalViolations: 0,
|
|
60
|
+
diagnosis: 'unknown' as const,
|
|
61
|
+
};
|
|
62
|
+
|
|
54
63
|
const buildEmptyRound = (): ConsumerMenuMatrixReport => {
|
|
55
64
|
return {
|
|
56
|
-
byOption:
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
outcome: 'UNKNOWN',
|
|
60
|
-
filesScanned: 0,
|
|
61
|
-
totalViolations: 0,
|
|
62
|
-
diagnosis: 'unknown',
|
|
63
|
-
},
|
|
64
|
-
'2': {
|
|
65
|
-
stage: 'UNKNOWN',
|
|
66
|
-
outcome: 'UNKNOWN',
|
|
67
|
-
filesScanned: 0,
|
|
68
|
-
totalViolations: 0,
|
|
69
|
-
diagnosis: 'unknown',
|
|
70
|
-
},
|
|
71
|
-
'3': {
|
|
72
|
-
stage: 'UNKNOWN',
|
|
73
|
-
outcome: 'UNKNOWN',
|
|
74
|
-
filesScanned: 0,
|
|
75
|
-
totalViolations: 0,
|
|
76
|
-
diagnosis: 'unknown',
|
|
77
|
-
},
|
|
78
|
-
'4': {
|
|
79
|
-
stage: 'UNKNOWN',
|
|
80
|
-
outcome: 'UNKNOWN',
|
|
81
|
-
filesScanned: 0,
|
|
82
|
-
totalViolations: 0,
|
|
83
|
-
diagnosis: 'unknown',
|
|
84
|
-
},
|
|
85
|
-
'9': {
|
|
86
|
-
stage: 'UNKNOWN',
|
|
87
|
-
outcome: 'UNKNOWN',
|
|
88
|
-
filesScanned: 0,
|
|
89
|
-
totalViolations: 0,
|
|
90
|
-
diagnosis: 'unknown',
|
|
91
|
-
},
|
|
92
|
-
},
|
|
65
|
+
byOption: Object.fromEntries(
|
|
66
|
+
MATRIX_MENU_OPTION_IDS.map((id) => [id, { ...UNKNOWN_OPTION_REPORT }])
|
|
67
|
+
) as ConsumerMenuMatrixReport['byOption'],
|
|
93
68
|
};
|
|
94
69
|
};
|
|
95
70
|
|
|
@@ -9,6 +9,10 @@ export type ConsumerLegacyMenuContext = {
|
|
|
9
9
|
runStrictRepoAndStaged: () => Promise<void>;
|
|
10
10
|
runStrictStagedOnly: () => Promise<void>;
|
|
11
11
|
runStandardCriticalHigh: () => Promise<void>;
|
|
12
|
+
runEngineStagedNoPreflight: () => Promise<void>;
|
|
13
|
+
runEngineUnstagedNoPreflight: () => Promise<void>;
|
|
14
|
+
runEngineStagedAndUnstagedNoPreflight: () => Promise<void>;
|
|
15
|
+
runEngineFullRepoNoPreflight: () => Promise<void>;
|
|
12
16
|
runPatternChecks: () => Promise<void>;
|
|
13
17
|
runEslintAudit: () => Promise<void>;
|
|
14
18
|
runAstIntelligence: () => Promise<void>;
|
|
@@ -22,24 +26,44 @@ export const createConsumerLegacyMenuActions = (
|
|
|
22
26
|
return [
|
|
23
27
|
{
|
|
24
28
|
id: '1',
|
|
25
|
-
label: '
|
|
29
|
+
label: 'Consumer preflight + gate: ALL tracked files (PRE_COMMIT · writes evidence)',
|
|
26
30
|
execute: params.runFullAudit,
|
|
27
31
|
},
|
|
28
32
|
{
|
|
29
33
|
id: '2',
|
|
30
|
-
label: '
|
|
34
|
+
label: 'Consumer preflight + gate: REPO+index contract (PRE_PUSH · disk skip risk if evidence tracked)',
|
|
31
35
|
execute: params.runStrictRepoAndStaged,
|
|
32
36
|
},
|
|
33
37
|
{
|
|
34
38
|
id: '3',
|
|
35
|
-
label: '
|
|
39
|
+
label: 'Consumer preflight + gate: STAGED only (PRE_COMMIT)',
|
|
36
40
|
execute: params.runStrictStagedOnly,
|
|
37
41
|
},
|
|
38
42
|
{
|
|
39
43
|
id: '4',
|
|
40
|
-
label: '
|
|
44
|
+
label: 'Consumer preflight + gate: working tree (PRE_PUSH policy · disk skip risk if evidence tracked)',
|
|
41
45
|
execute: params.runStandardCriticalHigh,
|
|
42
46
|
},
|
|
47
|
+
{
|
|
48
|
+
id: '11',
|
|
49
|
+
label: 'Engine audit · STAGED only (no preflight · PRE_COMMIT)',
|
|
50
|
+
execute: params.runEngineStagedNoPreflight,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: '12',
|
|
54
|
+
label: 'Engine audit · UNSTAGED only (no preflight · PRE_COMMIT)',
|
|
55
|
+
execute: params.runEngineUnstagedNoPreflight,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: '13',
|
|
59
|
+
label: 'Engine audit · STAGED + UNSTAGED (no preflight · PRE_COMMIT)',
|
|
60
|
+
execute: params.runEngineStagedAndUnstagedNoPreflight,
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: '14',
|
|
64
|
+
label: 'Engine audit · FULL tracked repo (no preflight · PRE_COMMIT)',
|
|
65
|
+
execute: params.runEngineFullRepoNoPreflight,
|
|
66
|
+
},
|
|
43
67
|
{
|
|
44
68
|
id: '5',
|
|
45
69
|
label: 'Legacy read-only pattern checks snapshot',
|
|
@@ -7,7 +7,8 @@ import type {
|
|
|
7
7
|
} from './framework-menu-consumer-preflight-types';
|
|
8
8
|
|
|
9
9
|
export const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
|
|
10
|
-
EVIDENCE_MISSING:
|
|
10
|
+
EVIDENCE_MISSING:
|
|
11
|
+
'ejecuta una auditoría (1/2/3/4 o motor 11–14) para regenerar .ai_evidence.json.',
|
|
11
12
|
EVIDENCE_INVALID: 'regenera .ai_evidence.json desde una opción de auditoría.',
|
|
12
13
|
EVIDENCE_STALE: 'refresca evidencia antes de continuar con commit/push.',
|
|
13
14
|
EVIDENCE_TIMESTAMP_INVALID: 'regenera evidencia para obtener un timestamp válido.',
|
|
@@ -54,7 +55,9 @@ export const buildConsumerPreflightHints = (
|
|
|
54
55
|
const actionableHintsByCode = resolveActionableHintsByCode(dependencies);
|
|
55
56
|
|
|
56
57
|
if (hasViolationCode(violations, 'EVIDENCE_MISSING')) {
|
|
57
|
-
hints.push(
|
|
58
|
+
hints.push(
|
|
59
|
+
'Evidence missing: ejecuta una auditoría (1/2/3/4 o motor 11–14) para regenerar .ai_evidence.json.'
|
|
60
|
+
);
|
|
58
61
|
handledCodes.add('EVIDENCE_MISSING');
|
|
59
62
|
}
|
|
60
63
|
if (hasViolationCode(violations, 'EVIDENCE_INVALID')) {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
exportConsumerRuntimeMarkdown,
|
|
6
6
|
notifyConsumerRuntimeAuditSummary,
|
|
7
7
|
printConsumerRuntimeEmptyScopeHint,
|
|
8
|
+
printPrePushTrackedEvidenceDiskHint,
|
|
8
9
|
renderConsumerRuntimeAstBreakdown,
|
|
9
10
|
renderConsumerRuntimeEslintAudit,
|
|
10
11
|
renderConsumerRuntimeFileDiagnostics,
|
|
@@ -20,7 +21,9 @@ type ConsumerRuntimeActionDependencies = {
|
|
|
20
21
|
runRepoGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
21
22
|
runRepoAndStagedGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
22
23
|
runStagedGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
24
|
+
runUnstagedGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
23
25
|
runWorkingTreeGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
26
|
+
runWorkingTreePreCommitGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
24
27
|
runPreflight?: (
|
|
25
28
|
stage: 'PRE_COMMIT' | 'PRE_PUSH'
|
|
26
29
|
) => Promise<string | void> | string | void;
|
|
@@ -88,18 +91,25 @@ export const createConsumerRuntimeActions = (
|
|
|
88
91
|
} else {
|
|
89
92
|
dependencies.clearSummaryOverride();
|
|
90
93
|
}
|
|
94
|
+
const summary = renderConsumerRuntimeSummary({
|
|
95
|
+
repoRoot: dependencies.repoRoot,
|
|
96
|
+
write: dependencies.write,
|
|
97
|
+
useColor: dependencies.useColor,
|
|
98
|
+
summaryOverride: dependencies.getSummaryOverride(),
|
|
99
|
+
});
|
|
91
100
|
notifyConsumerRuntimeAuditSummary(
|
|
92
101
|
{
|
|
93
102
|
emitNotification: dependencies.emitNotification,
|
|
94
103
|
repoRoot: dependencies.repoRoot,
|
|
95
104
|
},
|
|
96
|
-
|
|
97
|
-
repoRoot: dependencies.repoRoot,
|
|
98
|
-
write: dependencies.write,
|
|
99
|
-
useColor: dependencies.useColor,
|
|
100
|
-
summaryOverride: dependencies.getSummaryOverride(),
|
|
101
|
-
})
|
|
105
|
+
summary
|
|
102
106
|
);
|
|
107
|
+
if (
|
|
108
|
+
!gateResult?.blocked
|
|
109
|
+
&& (summary.outcome === 'PASS' || summary.outcome === 'WARN')
|
|
110
|
+
) {
|
|
111
|
+
printPrePushTrackedEvidenceDiskHint({ write: dependencies.write });
|
|
112
|
+
}
|
|
103
113
|
},
|
|
104
114
|
runStrictStagedOnly: async () => {
|
|
105
115
|
await runConsumerRuntimePreflight(dependencies, 'PRE_COMMIT');
|
|
@@ -135,8 +145,78 @@ export const createConsumerRuntimeActions = (
|
|
|
135
145
|
},
|
|
136
146
|
summary
|
|
137
147
|
);
|
|
148
|
+
if (summary.outcome === 'PASS' || summary.outcome === 'WARN') {
|
|
149
|
+
printPrePushTrackedEvidenceDiskHint({ write: dependencies.write });
|
|
150
|
+
}
|
|
138
151
|
printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'workingTree');
|
|
139
152
|
},
|
|
153
|
+
runEngineStagedNoPreflight: async () => {
|
|
154
|
+
await dependencies.runStagedGate();
|
|
155
|
+
dependencies.clearSummaryOverride();
|
|
156
|
+
const summary = renderConsumerRuntimeSummary({
|
|
157
|
+
repoRoot: dependencies.repoRoot,
|
|
158
|
+
write: dependencies.write,
|
|
159
|
+
useColor: dependencies.useColor,
|
|
160
|
+
});
|
|
161
|
+
notifyConsumerRuntimeAuditSummary(
|
|
162
|
+
{
|
|
163
|
+
emitNotification: dependencies.emitNotification,
|
|
164
|
+
repoRoot: dependencies.repoRoot,
|
|
165
|
+
},
|
|
166
|
+
summary
|
|
167
|
+
);
|
|
168
|
+
printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'staged');
|
|
169
|
+
},
|
|
170
|
+
runEngineUnstagedNoPreflight: async () => {
|
|
171
|
+
await dependencies.runUnstagedGate();
|
|
172
|
+
dependencies.clearSummaryOverride();
|
|
173
|
+
const summary = renderConsumerRuntimeSummary({
|
|
174
|
+
repoRoot: dependencies.repoRoot,
|
|
175
|
+
write: dependencies.write,
|
|
176
|
+
useColor: dependencies.useColor,
|
|
177
|
+
});
|
|
178
|
+
notifyConsumerRuntimeAuditSummary(
|
|
179
|
+
{
|
|
180
|
+
emitNotification: dependencies.emitNotification,
|
|
181
|
+
repoRoot: dependencies.repoRoot,
|
|
182
|
+
},
|
|
183
|
+
summary
|
|
184
|
+
);
|
|
185
|
+
printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'unstaged');
|
|
186
|
+
},
|
|
187
|
+
runEngineStagedAndUnstagedNoPreflight: async () => {
|
|
188
|
+
await dependencies.runWorkingTreePreCommitGate();
|
|
189
|
+
dependencies.clearSummaryOverride();
|
|
190
|
+
const summary = renderConsumerRuntimeSummary({
|
|
191
|
+
repoRoot: dependencies.repoRoot,
|
|
192
|
+
write: dependencies.write,
|
|
193
|
+
useColor: dependencies.useColor,
|
|
194
|
+
});
|
|
195
|
+
notifyConsumerRuntimeAuditSummary(
|
|
196
|
+
{
|
|
197
|
+
emitNotification: dependencies.emitNotification,
|
|
198
|
+
repoRoot: dependencies.repoRoot,
|
|
199
|
+
},
|
|
200
|
+
summary
|
|
201
|
+
);
|
|
202
|
+
printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'workingTree');
|
|
203
|
+
},
|
|
204
|
+
runEngineFullRepoNoPreflight: async () => {
|
|
205
|
+
await dependencies.runRepoGate();
|
|
206
|
+
dependencies.clearSummaryOverride();
|
|
207
|
+
const summary = renderConsumerRuntimeSummary({
|
|
208
|
+
repoRoot: dependencies.repoRoot,
|
|
209
|
+
write: dependencies.write,
|
|
210
|
+
useColor: dependencies.useColor,
|
|
211
|
+
});
|
|
212
|
+
notifyConsumerRuntimeAuditSummary(
|
|
213
|
+
{
|
|
214
|
+
emitNotification: dependencies.emitNotification,
|
|
215
|
+
repoRoot: dependencies.repoRoot,
|
|
216
|
+
},
|
|
217
|
+
summary
|
|
218
|
+
);
|
|
219
|
+
},
|
|
140
220
|
runPatternChecks: async () => {
|
|
141
221
|
dependencies.write(`\n${renderConsumerRuntimePatternChecks(dependencies.repoRoot)}\n`);
|
|
142
222
|
},
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
readEvidenceSummaryForMenu,
|
|
15
15
|
type FrameworkMenuEvidenceSummary,
|
|
16
16
|
} from './framework-menu-evidence-summary-lib';
|
|
17
|
+
import { renderVintageEvidenceReport } from './framework-menu-consumer-runtime-evidence-classic';
|
|
17
18
|
import type {
|
|
18
19
|
ConsumerRuntimeBlockedGate,
|
|
19
20
|
ConsumerRuntimeNotificationDependencies,
|
|
@@ -56,9 +57,36 @@ export const renderConsumerRuntimeSummary = (
|
|
|
56
57
|
width: resolveLegacyPanelOuterWidth(),
|
|
57
58
|
color: dependencies.useColor(),
|
|
58
59
|
})}\n`);
|
|
60
|
+
|
|
61
|
+
const vintageSource =
|
|
62
|
+
summary.status === 'ok' && !dependencies.summaryOverride
|
|
63
|
+
? readEvidenceSummaryForMenu(dependencies.repoRoot, {
|
|
64
|
+
topFindingsLimit: 45,
|
|
65
|
+
topFileLocationsLimit: 15,
|
|
66
|
+
topFilesLimit: 10,
|
|
67
|
+
})
|
|
68
|
+
: summary;
|
|
69
|
+
renderVintageEvidenceReport({
|
|
70
|
+
write: dependencies.write,
|
|
71
|
+
summary: vintageSource,
|
|
72
|
+
useColor: dependencies.useColor,
|
|
73
|
+
});
|
|
59
74
|
return summary;
|
|
60
75
|
};
|
|
61
76
|
|
|
77
|
+
export const printPrePushTrackedEvidenceDiskHint = (params: {
|
|
78
|
+
write: ConsumerRuntimeSummaryDependencies['write'];
|
|
79
|
+
}): void => {
|
|
80
|
+
params.write(
|
|
81
|
+
'\n' +
|
|
82
|
+
[
|
|
83
|
+
'ℹ PRE_PUSH + `.ai_evidence.json` tracked: on PASS/WARN the gate may skip rewriting the file on disk',
|
|
84
|
+
' (avoids dirty tracked evidence). Local dev: PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE=1 forces disk write.',
|
|
85
|
+
].join('\n') +
|
|
86
|
+
'\n'
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
62
90
|
export const buildConsumerRuntimeBlockedSummary = (
|
|
63
91
|
blocked: ConsumerRuntimeBlockedGate
|
|
64
92
|
): FrameworkMenuEvidenceSummary => ({
|
|
@@ -116,12 +144,18 @@ export const printConsumerRuntimeEmptyScopeHint = (
|
|
|
116
144
|
}
|
|
117
145
|
if (scope === 'staged') {
|
|
118
146
|
dependencies.write(
|
|
119
|
-
'\nℹ Scope vacío (staged): no hay archivos soportados en staged para auditar. Resultado PASS por alcance vacío; usa 1
|
|
147
|
+
'\nℹ Scope vacío (staged): no hay archivos soportados en staged para auditar. Resultado PASS por alcance vacío; usa 1, 2 o 14 (repo completo) según necesidad.\n'
|
|
148
|
+
);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (scope === 'unstaged') {
|
|
152
|
+
dependencies.write(
|
|
153
|
+
'\nℹ Scope vacío (unstaged): no hay cambios sin stage ni untracked auditable. Resultado PASS por alcance vacío; usa 1, 14 u opciones 11–13 según el alcance que necesites.\n'
|
|
120
154
|
);
|
|
121
155
|
return;
|
|
122
156
|
}
|
|
123
157
|
dependencies.write(
|
|
124
|
-
'\nℹ Scope vacío (working tree): no hay archivos soportados en staged/unstaged para auditar. Resultado PASS por alcance vacío; usa 1 o
|
|
158
|
+
'\nℹ Scope vacío (working tree): no hay archivos soportados en staged/unstaged para auditar. Resultado PASS por alcance vacío; usa 1, 2, 13 o 14 según el alcance que necesites.\n'
|
|
125
159
|
);
|
|
126
160
|
};
|
|
127
161
|
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { EnterpriseEvidenceSeverity } from './framework-menu-evidence-summary-types';
|
|
2
|
+
import type { FrameworkMenuEvidenceSummary } from './framework-menu-evidence-summary-lib';
|
|
3
|
+
import { renderLegacyPanel, resolveLegacyPanelOuterWidth } from './framework-menu-legacy-audit-lib';
|
|
4
|
+
import { applyCliPalette, buildCliDesignTokens } from './framework-menu-ui-components-lib';
|
|
5
|
+
import type { CliPaletteRole } from './framework-menu-ui-components-types';
|
|
6
|
+
|
|
7
|
+
const enterpriseSeverityToRole = (severity: EnterpriseEvidenceSeverity): CliPaletteRole => {
|
|
8
|
+
if (severity === 'CRITICAL') {
|
|
9
|
+
return 'critical';
|
|
10
|
+
}
|
|
11
|
+
if (severity === 'HIGH') {
|
|
12
|
+
return 'high';
|
|
13
|
+
}
|
|
14
|
+
if (severity === 'MEDIUM') {
|
|
15
|
+
return 'medium';
|
|
16
|
+
}
|
|
17
|
+
return 'low';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const outcomeToRole = (outcome: string | null): CliPaletteRole => {
|
|
21
|
+
const normalized = (outcome ?? '').trim().toUpperCase();
|
|
22
|
+
if (normalized === 'BLOCK' || normalized === 'BLOCKED') {
|
|
23
|
+
return 'critical';
|
|
24
|
+
}
|
|
25
|
+
if (normalized === 'WARN') {
|
|
26
|
+
return 'statusWarning';
|
|
27
|
+
}
|
|
28
|
+
if (normalized === 'PASS' || normalized === 'ALLOW' || normalized === 'ALLOWED') {
|
|
29
|
+
return 'goal';
|
|
30
|
+
}
|
|
31
|
+
return 'sectionTitle';
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const formatVintageEvidenceReportLines = (
|
|
35
|
+
summary: FrameworkMenuEvidenceSummary,
|
|
36
|
+
tokens: ReturnType<typeof buildCliDesignTokens>
|
|
37
|
+
): string[] => {
|
|
38
|
+
if (summary.status !== 'ok') {
|
|
39
|
+
return [
|
|
40
|
+
applyCliPalette('PUMUKI — Classic evidence view', 'title', tokens),
|
|
41
|
+
applyCliPalette(
|
|
42
|
+
summary.status === 'missing'
|
|
43
|
+
? 'No .ai_evidence.json — run an engine audit (1, 11–14) first.'
|
|
44
|
+
: 'Invalid .ai_evidence.json — regenerate from a gate run.',
|
|
45
|
+
'muted',
|
|
46
|
+
tokens
|
|
47
|
+
),
|
|
48
|
+
];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ent = summary.byEnterpriseSeverity ?? {
|
|
52
|
+
CRITICAL: summary.bySeverity.CRITICAL,
|
|
53
|
+
HIGH: summary.bySeverity.ERROR,
|
|
54
|
+
MEDIUM: summary.bySeverity.WARN,
|
|
55
|
+
LOW: summary.bySeverity.INFO,
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const lines: string[] = [
|
|
59
|
+
applyCliPalette('PUMUKI — Classic evidence view (severities + platforms)', 'title', tokens),
|
|
60
|
+
'',
|
|
61
|
+
`${applyCliPalette('Stage', 'sectionTitle', tokens)}: ${summary.stage ?? 'unknown'} ${applyCliPalette('Outcome', 'sectionTitle', tokens)}: ${applyCliPalette(String(summary.outcome ?? 'unknown'), outcomeToRole(summary.outcome), tokens)}`,
|
|
62
|
+
`${applyCliPalette('Findings', 'sectionTitle', tokens)}: ${summary.totalFindings} ${applyCliPalette('Scanned', 'muted', tokens)}: ${summary.filesScanned} ${applyCliPalette('Affected', 'muted', tokens)}: ${summary.filesAffected}`,
|
|
63
|
+
'',
|
|
64
|
+
applyCliPalette('Enterprise severities', 'sectionTitle', tokens),
|
|
65
|
+
];
|
|
66
|
+
|
|
67
|
+
const order: EnterpriseEvidenceSeverity[] = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'];
|
|
68
|
+
for (const band of order) {
|
|
69
|
+
const count = ent[band] ?? 0;
|
|
70
|
+
if (count === 0) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
lines.push(
|
|
74
|
+
` ${applyCliPalette(`${band}:`, enterpriseSeverityToRole(band), tokens)} ${count}`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
lines.push('', applyCliPalette('Legacy severities', 'sectionTitle', tokens));
|
|
79
|
+
lines.push(
|
|
80
|
+
` ${applyCliPalette('CRITICAL', 'critical', tokens)} ${summary.bySeverity.CRITICAL} ` +
|
|
81
|
+
`${applyCliPalette('ERROR', 'high', tokens)} ${summary.bySeverity.ERROR} ` +
|
|
82
|
+
`${applyCliPalette('WARN', 'medium', tokens)} ${summary.bySeverity.WARN} ` +
|
|
83
|
+
`${applyCliPalette('INFO', 'low', tokens)} ${summary.bySeverity.INFO}`
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
if (summary.platformAuditRows && summary.platformAuditRows.length > 0) {
|
|
87
|
+
lines.push('', applyCliPalette('Platforms (from evidence snapshot)', 'sectionTitle', tokens));
|
|
88
|
+
for (const row of summary.platformAuditRows) {
|
|
89
|
+
lines.push(
|
|
90
|
+
` ${applyCliPalette(row.platform, 'rule', tokens)}: ${row.violations} violations`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
lines.push(
|
|
94
|
+
applyCliPalette(
|
|
95
|
+
'Note: platform buckets are heuristic (path + ruleId); "Other" is not absence of iOS/Android work.',
|
|
96
|
+
'muted',
|
|
97
|
+
tokens
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
lines.push('', applyCliPalette('Top violations (ranked)', 'sectionTitle', tokens));
|
|
103
|
+
if (summary.topFindings.length === 0) {
|
|
104
|
+
lines.push(applyCliPalette(' (none)', 'goal', tokens));
|
|
105
|
+
} else {
|
|
106
|
+
for (const finding of summary.topFindings) {
|
|
107
|
+
const role = enterpriseSeverityToRole(finding.severity);
|
|
108
|
+
lines.push(
|
|
109
|
+
` ${applyCliPalette(`[${finding.severity}]`, role, tokens)} ${finding.ruleId}`
|
|
110
|
+
);
|
|
111
|
+
lines.push(
|
|
112
|
+
` ${applyCliPalette(`${finding.file}:${finding.line}`, 'muted', tokens)}`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return lines;
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
export const renderVintageEvidenceReport = (params: {
|
|
121
|
+
write: (text: string) => void;
|
|
122
|
+
summary: FrameworkMenuEvidenceSummary;
|
|
123
|
+
useColor: () => boolean;
|
|
124
|
+
}): void => {
|
|
125
|
+
if (process.env.PUMUKI_MENU_VINTAGE_REPORT === '0') {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const tokens = buildCliDesignTokens({
|
|
129
|
+
width: resolveLegacyPanelOuterWidth(),
|
|
130
|
+
color: params.useColor(),
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const lines = formatVintageEvidenceReportLines(params.summary, tokens);
|
|
134
|
+
params.write(
|
|
135
|
+
`\n${renderLegacyPanel(lines, {
|
|
136
|
+
width: resolveLegacyPanelOuterWidth(),
|
|
137
|
+
color: params.useColor(),
|
|
138
|
+
})}\n`
|
|
139
|
+
);
|
|
140
|
+
};
|
|
@@ -35,7 +35,9 @@ export const createConsumerMenuRuntime = (
|
|
|
35
35
|
runRepoGate: params.runRepoGate,
|
|
36
36
|
runRepoAndStagedGate: params.runRepoAndStagedGate,
|
|
37
37
|
runStagedGate: params.runStagedGate,
|
|
38
|
+
runUnstagedGate: params.runUnstagedGate,
|
|
38
39
|
runWorkingTreeGate: params.runWorkingTreeGate,
|
|
40
|
+
runWorkingTreePreCommitGate: params.runWorkingTreePreCommitGate,
|
|
39
41
|
runPreflight: params.runPreflight,
|
|
40
42
|
emitNotification,
|
|
41
43
|
clearSummaryOverride: () => {
|
|
@@ -33,7 +33,9 @@ export type ConsumerMenuRuntimeParams = {
|
|
|
33
33
|
runRepoGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
34
34
|
runRepoAndStagedGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
35
35
|
runStagedGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
36
|
+
runUnstagedGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
36
37
|
runWorkingTreeGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
38
|
+
runWorkingTreePreCommitGate: () => Promise<ConsumerRuntimeGateResult | void>;
|
|
37
39
|
runPreflight?: (
|
|
38
40
|
stage: 'PRE_COMMIT' | 'PRE_PUSH'
|
|
39
41
|
) => Promise<string | void> | string | void;
|
|
@@ -47,7 +49,7 @@ export type ConsumerMenuRuntime = {
|
|
|
47
49
|
readCurrentSummary: () => FrameworkMenuEvidenceSummary | null;
|
|
48
50
|
};
|
|
49
51
|
|
|
50
|
-
export type ConsumerRuntimeScope = 'staged' | 'workingTree';
|
|
52
|
+
export type ConsumerRuntimeScope = 'staged' | 'unstaged' | 'workingTree';
|
|
51
53
|
|
|
52
54
|
export type ConsumerRuntimeSummaryDependencies = {
|
|
53
55
|
repoRoot: string;
|
|
@@ -3,5 +3,6 @@ export type {
|
|
|
3
3
|
EvidenceSeverity,
|
|
4
4
|
FrameworkMenuEvidenceSummary,
|
|
5
5
|
} from './framework-menu-evidence-summary-types';
|
|
6
|
+
export type { ReadEvidenceSummaryForMenuOptions } from './framework-menu-evidence-summary-read';
|
|
6
7
|
export { readEvidenceSummaryForMenu } from './framework-menu-evidence-summary-read';
|
|
7
8
|
export { formatEvidenceSummaryForMenu } from './framework-menu-evidence-summary-format';
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
EvidenceSnapshot,
|
|
3
|
+
FrameworkMenuEvidencePlatformRow,
|
|
4
|
+
FrameworkMenuEvidenceSummary,
|
|
5
|
+
} from './framework-menu-evidence-summary-types';
|
|
2
6
|
import { readEvidenceSummaryFile } from './framework-menu-evidence-summary-file';
|
|
3
7
|
import {
|
|
4
8
|
asFindings,
|
|
@@ -16,9 +20,55 @@ import {
|
|
|
16
20
|
toTopFiles,
|
|
17
21
|
} from './framework-menu-evidence-summary-severity';
|
|
18
22
|
|
|
23
|
+
export type ReadEvidenceSummaryForMenuOptions = {
|
|
24
|
+
topFindingsLimit?: number;
|
|
25
|
+
topFileLocationsLimit?: number;
|
|
26
|
+
topFilesLimit?: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const sumLegacySeverityBand = (row: { by_severity?: unknown }): number => {
|
|
30
|
+
const bands = row.by_severity;
|
|
31
|
+
if (!bands || typeof bands !== 'object') {
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
const b = bands as Record<string, unknown>;
|
|
35
|
+
const keys = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const;
|
|
36
|
+
let total = 0;
|
|
37
|
+
for (const key of keys) {
|
|
38
|
+
const value = b[key];
|
|
39
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
40
|
+
total += value;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return total;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const parsePlatformAuditRows = (
|
|
47
|
+
snapshot: EvidenceSnapshot
|
|
48
|
+
): ReadonlyArray<FrameworkMenuEvidencePlatformRow> => {
|
|
49
|
+
const raw = snapshot.platforms;
|
|
50
|
+
if (!Array.isArray(raw)) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const rows: FrameworkMenuEvidencePlatformRow[] = [];
|
|
54
|
+
for (const entry of raw) {
|
|
55
|
+
if (!entry || typeof entry !== 'object') {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const platform = toStringOrNull((entry as { platform?: unknown }).platform) ?? 'Other';
|
|
59
|
+
const violations = sumLegacySeverityBand(entry as { by_severity?: unknown });
|
|
60
|
+
rows.push({ platform, violations });
|
|
61
|
+
}
|
|
62
|
+
return rows;
|
|
63
|
+
};
|
|
64
|
+
|
|
19
65
|
export const readEvidenceSummaryForMenu = (
|
|
20
|
-
repoRoot: string = process.cwd()
|
|
66
|
+
repoRoot: string = process.cwd(),
|
|
67
|
+
options?: ReadEvidenceSummaryForMenuOptions
|
|
21
68
|
): FrameworkMenuEvidenceSummary => {
|
|
69
|
+
const topFindingsLimit = options?.topFindingsLimit ?? 10;
|
|
70
|
+
const topFileLocationsLimit = options?.topFileLocationsLimit ?? 10;
|
|
71
|
+
const topFilesLimit = options?.topFilesLimit ?? 5;
|
|
22
72
|
const evidence = readEvidenceSummaryFile(repoRoot);
|
|
23
73
|
|
|
24
74
|
if (evidence.status === 'missing') {
|
|
@@ -65,12 +115,13 @@ export const readEvidenceSummaryForMenu = (
|
|
|
65
115
|
evidence.snapshot.files_affected ?? evidence.snapshot.filesAffected
|
|
66
116
|
);
|
|
67
117
|
const topFileLocations = countFiles({ findings, repoRoot })
|
|
68
|
-
.slice(0,
|
|
118
|
+
.slice(0, topFileLocationsLimit)
|
|
69
119
|
.map((entry) => ({
|
|
70
120
|
file: entry.file,
|
|
71
121
|
line: entry.line,
|
|
72
122
|
}));
|
|
73
|
-
const topFindings = buildTopFindings({ findings, repoRoot, maxItems:
|
|
123
|
+
const topFindings = buildTopFindings({ findings, repoRoot, maxItems: topFindingsLimit });
|
|
124
|
+
const platformAuditRows = parsePlatformAuditRows(evidence.snapshot);
|
|
74
125
|
|
|
75
126
|
return {
|
|
76
127
|
status: 'ok',
|
|
@@ -81,8 +132,9 @@ export const readEvidenceSummaryForMenu = (
|
|
|
81
132
|
filesAffected,
|
|
82
133
|
bySeverity,
|
|
83
134
|
byEnterpriseSeverity,
|
|
84
|
-
topFiles: toTopFiles({ findings, repoRoot }),
|
|
135
|
+
topFiles: toTopFiles({ findings, repoRoot, maxItems: topFilesLimit }),
|
|
85
136
|
topFileLocations,
|
|
86
137
|
topFindings,
|
|
138
|
+
...(platformAuditRows.length > 0 ? { platformAuditRows } : {}),
|
|
87
139
|
};
|
|
88
140
|
};
|
|
@@ -50,7 +50,9 @@ export const countFindingsBySeverity = (
|
|
|
50
50
|
export const toTopFiles = (params: {
|
|
51
51
|
findings: ReadonlyArray<EvidenceFinding>;
|
|
52
52
|
repoRoot: string;
|
|
53
|
+
maxItems?: number;
|
|
53
54
|
}): ReadonlyArray<{ file: string; count: number }> => {
|
|
55
|
+
const maxItems = params.maxItems ?? 5;
|
|
54
56
|
const filesMap = new Map<string, number>();
|
|
55
57
|
for (const finding of params.findings) {
|
|
56
58
|
const rawFile = toStringOrNull(finding.file);
|
|
@@ -66,7 +68,7 @@ export const toTopFiles = (params: {
|
|
|
66
68
|
|
|
67
69
|
return [...filesMap.entries()]
|
|
68
70
|
.sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]))
|
|
69
|
-
.slice(0,
|
|
71
|
+
.slice(0, maxItems)
|
|
70
72
|
.map(([file, count]) => ({ file, count }));
|
|
71
73
|
};
|
|
72
74
|
|
|
@@ -18,12 +18,18 @@ export type EvidenceSnapshot = {
|
|
|
18
18
|
stage?: unknown;
|
|
19
19
|
outcome?: unknown;
|
|
20
20
|
findings?: unknown;
|
|
21
|
+
platforms?: unknown;
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
export type EvidenceSeverityMetrics = {
|
|
24
25
|
by_enterprise_severity?: unknown;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
export type FrameworkMenuEvidencePlatformRow = {
|
|
29
|
+
platform: string;
|
|
30
|
+
violations: number;
|
|
31
|
+
};
|
|
32
|
+
|
|
27
33
|
export type FrameworkMenuEvidenceSummary = {
|
|
28
34
|
status: 'ok' | 'missing' | 'invalid';
|
|
29
35
|
stage: string | null;
|
|
@@ -41,4 +47,5 @@ export type FrameworkMenuEvidenceSummary = {
|
|
|
41
47
|
file: string;
|
|
42
48
|
line: number;
|
|
43
49
|
}>;
|
|
50
|
+
platformAuditRows?: ReadonlyArray<FrameworkMenuEvidencePlatformRow>;
|
|
44
51
|
};
|