pumuki 6.3.71 → 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.
Files changed (33) hide show
  1. package/AGENTS.md +269 -0
  2. package/CHANGELOG.md +666 -0
  3. package/README.md +32 -0
  4. package/docs/README.md +7 -2
  5. package/docs/operations/RELEASE_NOTES.md +7 -0
  6. package/docs/product/USAGE.md +15 -2
  7. package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +111 -0
  8. package/integrations/git/GitService.ts +25 -0
  9. package/integrations/git/runPlatformGateFacts.ts +7 -0
  10. package/integrations/mcp/preFlightCheck.ts +2 -1
  11. package/integrations/sdd/openSpecCli.ts +12 -3
  12. package/package.json +4 -1
  13. package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
  14. package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
  15. package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
  16. package/scripts/framework-menu-consumer-runtime-actions.ts +86 -6
  17. package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
  18. package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
  19. package/scripts/framework-menu-consumer-runtime-lib.ts +2 -0
  20. package/scripts/framework-menu-consumer-runtime-types.ts +3 -1
  21. package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
  22. package/scripts/framework-menu-evidence-summary-read.ts +57 -5
  23. package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
  24. package/scripts/framework-menu-evidence-summary-types.ts +7 -0
  25. package/scripts/framework-menu-gate-lib.ts +9 -0
  26. package/scripts/framework-menu-layout-data.ts +5 -0
  27. package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
  28. package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
  29. package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
  30. package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
  31. package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
  32. package/scripts/framework-menu-system-notifications-macos.ts +4 -0
  33. package/scripts/framework-menu.ts +3 -0
@@ -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
- renderConsumerRuntimeSummary({
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 o 2 para validar repo completo.\n'
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 2 para validar repo completo.\n'
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 { FrameworkMenuEvidenceSummary } from './framework-menu-evidence-summary-types';
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, 10)
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: 10 });
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, 5)
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
  };
@@ -9,6 +9,7 @@ export type MenuStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
9
9
 
10
10
  export type MenuScope =
11
11
  | { kind: 'staged' }
12
+ | { kind: 'unstaged' }
12
13
  | { kind: 'repo' }
13
14
  | { kind: 'repoAndStaged' }
14
15
  | { kind: 'workingTree' }
@@ -141,6 +142,14 @@ export const runWorkingTreePrePushGateSilent = async (): Promise<void> => {
141
142
  await runMenuAuditGate(gateParams);
142
143
  };
143
144
 
145
+ export const runUnstagedGateSilent = async (): Promise<void> => {
146
+ const gateParams = buildMenuGateParams({
147
+ stage: 'PRE_COMMIT',
148
+ scope: { kind: 'unstaged' },
149
+ });
150
+ await runMenuAuditGate(gateParams);
151
+ };
152
+
144
153
  const runMenuAuditGate = async (
145
154
  gateParams: ReturnType<typeof buildMenuGateParams>
146
155
  ): Promise<void> => {
@@ -6,6 +6,11 @@ export const CONSUMER_MENU_LAYOUT: ReadonlyArray<MenuLayoutGroup> = [
6
6
  title: 'Read-Only Gate Flows',
7
7
  itemIds: ['1', '2', '3', '4'],
8
8
  },
9
+ {
10
+ key: 'engine-working-tree-no-preflight',
11
+ title: 'Engine · working tree (no preflight)',
12
+ itemIds: ['11', '12', '13', '14'],
13
+ },
9
14
  {
10
15
  key: 'export',
11
16
  title: 'Legacy Read-Only Export',
@@ -1,10 +1,11 @@
1
1
  import {
2
2
  runConsumerMenuMatrix,
3
3
  } from './framework-menu-matrix-runner-lib';
4
- import type {
5
- ConsumerMenuMatrixReport,
6
- ConsumerMenuMatrixOptionReport,
7
- MatrixOptionId,
4
+ import {
5
+ MATRIX_MENU_OPTION_IDS,
6
+ type ConsumerMenuMatrixReport,
7
+ type ConsumerMenuMatrixOptionReport,
8
+ type MatrixOptionId,
8
9
  } from './framework-menu-matrix-evidence-lib';
9
10
 
10
11
  export type MatrixOptionDrift = {
@@ -22,7 +23,13 @@ export type ConsumerMenuMatrixBaselineReport = {
22
23
  analysis: ConsumerMenuMatrixBaselineAnalysis;
23
24
  };
24
25
 
25
- const OPTION_IDS: ReadonlyArray<MatrixOptionId> = ['1', '2', '3', '4', '9'];
26
+ const EMPTY_DRIFT: MatrixOptionDrift = { stable: true, driftFields: [] };
27
+
28
+ const seedMatrixOptionDrift = (): Record<MatrixOptionId, MatrixOptionDrift> => {
29
+ return Object.fromEntries(
30
+ MATRIX_MENU_OPTION_IDS.map((optionId) => [optionId, { ...EMPTY_DRIFT }])
31
+ ) as Record<MatrixOptionId, MatrixOptionDrift>;
32
+ };
26
33
 
27
34
  const OPTION_FIELDS: ReadonlyArray<keyof ConsumerMenuMatrixOptionReport> = [
28
35
  'stage',
@@ -57,18 +64,12 @@ const computeOptionDrift = (
57
64
  export const analyzeConsumerMenuMatrixBaseline = (
58
65
  rounds: ReadonlyArray<ConsumerMenuMatrixReport>
59
66
  ): ConsumerMenuMatrixBaselineAnalysis => {
60
- const byOption = OPTION_IDS.reduce<Record<MatrixOptionId, MatrixOptionDrift>>((acc, optionId) => {
67
+ const byOption = MATRIX_MENU_OPTION_IDS.reduce<Record<MatrixOptionId, MatrixOptionDrift>>((acc, optionId) => {
61
68
  acc[optionId] = computeOptionDrift(rounds, optionId);
62
69
  return acc;
63
- }, {
64
- '1': { stable: true, driftFields: [] },
65
- '2': { stable: true, driftFields: [] },
66
- '3': { stable: true, driftFields: [] },
67
- '4': { stable: true, driftFields: [] },
68
- '9': { stable: true, driftFields: [] },
69
- });
70
+ }, seedMatrixOptionDrift());
70
71
 
71
- const stable = OPTION_IDS.every((optionId) => byOption[optionId].stable);
72
+ const stable = MATRIX_MENU_OPTION_IDS.every((optionId) => byOption[optionId].stable);
72
73
 
73
74
  return {
74
75
  stable,
@@ -6,6 +6,8 @@ import {
6
6
  runRepoAndStagedPrePushGateSilent,
7
7
  runRepoGateSilent,
8
8
  runStagedGateSilent,
9
+ runUnstagedGateSilent,
10
+ runWorkingTreeGateSilent,
9
11
  runWorkingTreePrePushGateSilent,
10
12
  } from './framework-menu-gate-lib';
11
13
  import { readMatrixOptionReport, type MatrixOptionId } from './framework-menu-matrix-evidence-lib';
@@ -70,6 +72,25 @@ const runGateByOption = async (option: MatrixOptionId): Promise<void> => {
70
72
  }
71
73
  if (option === '4') {
72
74
  await runWorkingTreePrePushGateSilent();
75
+ return;
76
+ }
77
+ if (option === '9') {
78
+ return;
79
+ }
80
+ if (option === '11') {
81
+ await runStagedGateSilent();
82
+ return;
83
+ }
84
+ if (option === '12') {
85
+ await runUnstagedGateSilent();
86
+ return;
87
+ }
88
+ if (option === '13') {
89
+ await runWorkingTreeGateSilent();
90
+ return;
91
+ }
92
+ if (option === '14') {
93
+ await runRepoGateSilent();
73
94
  }
74
95
  };
75
96
 
@@ -104,7 +125,7 @@ export const runConsumerMenuCanary = async (params?: {
104
125
  scenario.canarySource,
105
126
  'utf8'
106
127
  );
107
- if (scenario.option === '1' || scenario.option === '2') {
128
+ if (scenario.option === '1' || scenario.option === '2' || scenario.option === '3' || scenario.option === '11') {
108
129
  stageCanaryPath(repoRoot, canaryRelativePath);
109
130
  }
110
131
 
@@ -8,6 +8,7 @@ export type {
8
8
  MatrixOptionDiagnosis,
9
9
  MatrixOptionId,
10
10
  } from './framework-menu-matrix-evidence-types';
11
+ export { MATRIX_MENU_OPTION_IDS } from './framework-menu-matrix-evidence-types';
11
12
  export { toMatrixOptionReport } from './framework-menu-matrix-evidence-diagnosis';
12
13
 
13
14
  export const readMatrixOptionReport = (
@@ -1,4 +1,16 @@
1
- export type MatrixOptionId = '1' | '2' | '3' | '4' | '9';
1
+ export const MATRIX_MENU_OPTION_IDS = [
2
+ '1',
3
+ '2',
4
+ '3',
5
+ '4',
6
+ '9',
7
+ '11',
8
+ '12',
9
+ '13',
10
+ '14',
11
+ ] as const;
12
+
13
+ export type MatrixOptionId = (typeof MATRIX_MENU_OPTION_IDS)[number];
2
14
  export type MatrixOptionDiagnosis =
3
15
  | 'scope-empty'
4
16
  | 'repo-clean'