pumuki 6.3.270 → 6.3.271

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/CHANGELOG.md +4 -0
  2. package/VERSION +1 -1
  3. package/core/facts/detectors/text/android.test.ts +538 -0
  4. package/core/facts/detectors/text/android.ts +436 -0
  5. package/core/facts/detectors/text/ios.test.ts +328 -1
  6. package/core/facts/detectors/text/ios.ts +241 -0
  7. package/core/facts/detectors/typescript/index.test.ts +393 -0
  8. package/core/facts/detectors/typescript/index.ts +316 -0
  9. package/core/facts/extractHeuristicFacts.ts +70 -1
  10. package/core/rules/presets/heuristics/android.test.ts +91 -1
  11. package/core/rules/presets/heuristics/android.ts +360 -0
  12. package/core/rules/presets/heuristics/ios.test.ts +54 -1
  13. package/core/rules/presets/heuristics/ios.ts +243 -2
  14. package/core/rules/presets/heuristics/typescript.test.ts +50 -2
  15. package/core/rules/presets/heuristics/typescript.ts +162 -0
  16. package/docs/operations/RELEASE_NOTES.md +4 -0
  17. package/integrations/config/skillsDetectorRegistry.ts +501 -0
  18. package/integrations/config/skillsRuleClassification.ts +127 -3
  19. package/integrations/git/runPlatformGate.ts +4 -1
  20. package/integrations/lifecycle/preWriteAutomation.ts +1 -0
  21. package/integrations/lifecycle/preWriteLease.ts +41 -4
  22. package/package.json +1 -1
  23. package/scripts/classify-skills-rules.ts +2 -2
  24. package/scripts/framework-menu-consumer-actions-lib.ts +9 -9
  25. package/scripts/framework-menu-consumer-runtime-actions.ts +53 -117
  26. package/scripts/framework-menu-consumer-runtime-audit.ts +66 -0
  27. package/scripts/framework-menu-consumer-runtime-menu.ts +4 -4
  28. package/scripts/framework-menu-gate-lib.ts +86 -1
  29. package/scripts/framework-menu-layout-data.ts +3 -3
  30. package/scripts/framework-menu-legacy-audit-render-sections.ts +6 -0
  31. package/scripts/framework-menu.ts +10 -6
  32. package/scripts/package-install-smoke-consumer-npm-lib.ts +10 -4
  33. package/scripts/package-install-smoke-lifecycle-lib.ts +19 -0
@@ -3,7 +3,7 @@ import { resolveMappedHeuristicRuleIdsForCompiledRule } from './skillsDetectorRe
3
3
 
4
4
  export type SkillsRuleClassificationStatus =
5
5
  | 'AST_IMPLEMENTED'
6
- | 'IMPLEMENTABLE_AHORA'
6
+ | 'IMPLEMENTAR_DETECTOR'
7
7
  | 'REQUIERE_ESTUDIO'
8
8
  | 'NO_ES_REGLA_DE_CODIGO';
9
9
 
@@ -28,7 +28,7 @@ export type SkillsRuleClassificationSummary = {
28
28
 
29
29
  const emptyStatusCounts = (): Record<SkillsRuleClassificationStatus, number> => ({
30
30
  AST_IMPLEMENTED: 0,
31
- IMPLEMENTABLE_AHORA: 0,
31
+ IMPLEMENTAR_DETECTOR: 0,
32
32
  REQUIERE_ESTUDIO: 0,
33
33
  NO_ES_REGLA_DE_CODIGO: 0,
34
34
  });
@@ -55,11 +55,106 @@ const NON_CODE_RULE_PATTERNS: ReadonlyArray<RegExp> = [
55
55
  /\bci\/cd\b/,
56
56
  ];
57
57
 
58
+ const CODE_RULE_WITH_DIRECT_DETECTOR_PATTERNS: ReadonlyArray<RegExp> = [
59
+ /\baccessibility\b/,
60
+ /\bcontentdescription\b/,
61
+ /\bsemantic\b/,
62
+ /\btest tag\b/,
63
+ /\basynctask\b/,
64
+ /\bcallback\b/,
65
+ /\bglobal ?scope\b/,
66
+ /\brunblocking\b/,
67
+ /\bthread\.?sleep\b/,
68
+ /\bdispatch(queue|group|semaphore)\b/,
69
+ /\bforce\b.*\b(unwrap|try|cast)\b/,
70
+ /\btry!\b/,
71
+ /\bas!\b/,
72
+ /\bany\b/,
73
+ /\bconsole\b/,
74
+ /\blog\b/,
75
+ /\bprint\b/,
76
+ /\bsecret\b/,
77
+ /\btoken\b/,
78
+ /\bpassword\b/,
79
+ /\bapi key\b/,
80
+ /\bhardcoded\b/,
81
+ /\bmagic number\b/,
82
+ /\bcolor\b/,
83
+ /\bdimension\b/,
84
+ /\bgod\b/,
85
+ /\blarge\b.*\b(class|component|view|service|function|file)\b/,
86
+ /\bsolid\b/,
87
+ /\bsrp\b/,
88
+ /\bdependency\b.*\binjection\b/,
89
+ /\bconstructor\b.*\b(param|dependency)/,
90
+ /\bsharedpreferences\b/,
91
+ /\buserdefaults\b/,
92
+ /\bappstorage\b/,
93
+ /\bkeychain\b/,
94
+ /\braw sql\b/,
95
+ /\bsql\b.*\b(template|injection)\b/,
96
+ /\bempty catch\b/,
97
+ /\bcatch\b.*\b(empty|silenc)/,
98
+ /\bmock\b/,
99
+ /\bspy\b/,
100
+ /\bjunit4\b/,
101
+ /\bxctassert\b/,
102
+ /\bxctunwrap\b/,
103
+ /\bxctest\b/,
104
+ /\bquick\b/,
105
+ /\bnimble\b/,
106
+ /\bwaitforexpectations\b/,
107
+ /\bexpectation\(description\b/,
108
+ /\bnavigationview\b/,
109
+ /\bgeometryreader\b/,
110
+ /\banyview\b/,
111
+ /\bforegroundcolor\b/,
112
+ /\bcornerradius\b/,
113
+ /\bsheet\b.*\bispresented\b/,
114
+ /\bscrollview\b.*\bshowsindicators\b/,
115
+ /\bforeach\b.*\b(indices|index)\b/,
116
+ /\bonchange\b/,
117
+ /\bontapgesture\b/,
118
+ /\buiscreen\.main\.bounds\b/,
119
+ /\bstring\(format\b/,
120
+ /\bobservableobject\b/,
121
+ /\bstateobject\b/,
122
+ /\bobservable\b/,
123
+ /\blivedata\b/,
124
+ /\bstateflow\b/,
125
+ /\bsharedflow\b/,
126
+ /\bremember\b/,
127
+ /\blaunched(effect)?\b/,
128
+ /\blazy(column|row|vstack|hstack)\b/,
129
+ /\bwindow ?size ?class\b/,
130
+ /\bpadding\b/,
131
+ /\bframe\b/,
132
+ /\bfont\b/,
133
+ /\broute\b/,
134
+ /\bnavigation\b/,
135
+ ];
136
+
137
+ const STUDY_BEFORE_DETECTOR_RULE_IDS = new Set<string>([
138
+ 'skills.android.guideline.android.adaptive-layouts-responsive-design-windowsizeclass',
139
+ 'skills.android.guideline.android.color-contrast-wcag-aa-mi-nimo',
140
+ 'skills.android.guideline.android.play-console-production-deployment',
141
+ 'skills.backend.guideline.backend.dependency-injection-injectable-inject-providers-array',
142
+ 'skills.backend.guideline.backend.event-store-log-de-eventos-para-auditori-a',
143
+ ]);
144
+
145
+ const requiresStudyBeforeDetector = (rule: SkillsCompiledRule): boolean =>
146
+ STUDY_BEFORE_DETECTOR_RULE_IDS.has(rule.id);
147
+
58
148
  const isNonCodeRule = (rule: SkillsCompiledRule): boolean => {
59
149
  const haystack = normalizeText(`${rule.id} ${rule.description} ${rule.sourceSkill}`);
60
150
  return NON_CODE_RULE_PATTERNS.some((pattern) => pattern.test(haystack));
61
151
  };
62
152
 
153
+ const hasDirectCodeDetectorCandidate = (rule: SkillsCompiledRule): boolean => {
154
+ const haystack = normalizeText(`${rule.id} ${rule.description} ${rule.sourceSkill}`);
155
+ return CODE_RULE_WITH_DIRECT_DETECTOR_PATTERNS.some((pattern) => pattern.test(haystack));
156
+ };
157
+
63
158
  const classifyRule = (rule: SkillsCompiledRule): ClassifiedSkillsRule => {
64
159
  const evaluationMode = rule.evaluationMode ?? 'AUTO';
65
160
  const astNodeIds = resolveMappedHeuristicRuleIdsForCompiledRule(rule);
@@ -86,12 +181,27 @@ const classifyRule = (rule: SkillsCompiledRule): ClassifiedSkillsRule => {
86
181
  sourcePath: rule.sourcePath,
87
182
  evaluationMode,
88
183
  severity: rule.severity,
89
- status: 'IMPLEMENTABLE_AHORA',
184
+ status: 'IMPLEMENTAR_DETECTOR',
90
185
  reason: 'AUTO rule without AST/nodal detector binding; implement detector mapping.',
91
186
  astNodeIds,
92
187
  };
93
188
  }
94
189
 
190
+ if (requiresStudyBeforeDetector(rule)) {
191
+ return {
192
+ ruleId: rule.id,
193
+ platform: rule.platform,
194
+ sourceSkill: rule.sourceSkill,
195
+ sourcePath: rule.sourcePath,
196
+ evaluationMode,
197
+ severity: rule.severity,
198
+ status: 'REQUIERE_ESTUDIO',
199
+ reason:
200
+ 'Rule has code relevance, but the AST/nodal signal needs explicit design before implementation to avoid poor umbrella detectors.',
201
+ astNodeIds,
202
+ };
203
+ }
204
+
95
205
  if (isNonCodeRule(rule)) {
96
206
  return {
97
207
  ruleId: rule.id,
@@ -106,6 +216,20 @@ const classifyRule = (rule: SkillsCompiledRule): ClassifiedSkillsRule => {
106
216
  };
107
217
  }
108
218
 
219
+ if (hasDirectCodeDetectorCandidate(rule)) {
220
+ return {
221
+ ruleId: rule.id,
222
+ platform: rule.platform,
223
+ sourceSkill: rule.sourceSkill,
224
+ sourcePath: rule.sourcePath,
225
+ evaluationMode,
226
+ severity: rule.severity,
227
+ status: 'IMPLEMENTAR_DETECTOR',
228
+ reason: 'Code rule has a direct AST/nodal detection surface; implement detector and blocking evidence.',
229
+ astNodeIds,
230
+ };
231
+ }
232
+
109
233
  return {
110
234
  ruleId: rule.id,
111
235
  platform: rule.platform,
@@ -286,6 +286,9 @@ const toSkillsDeclarativeRulesClassificationFinding = (params: {
286
286
  declarativeRuleIds: ReadonlyArray<string>;
287
287
  facts: ReadonlyArray<Fact>;
288
288
  }): Finding | undefined => {
289
+ if (process.env.PUMUKI_ENFORCE_DECLARATIVE_RULE_CLASSIFICATION !== '1') {
290
+ return undefined;
291
+ }
289
292
  if (params.declarativeRuleIds.length === 0) {
290
293
  return undefined;
291
294
  }
@@ -302,7 +305,7 @@ const toSkillsDeclarativeRulesClassificationFinding = (params: {
302
305
  message:
303
306
  `Skills declarative rules require AST/nodal classification at ${params.stage}: ` +
304
307
  `total=${params.declarativeRuleIds.length} sample_rule_ids=[${sampleRuleIds}]. ` +
305
- 'Classify every rule as IMPLEMENTABLE_AHORA, REQUIERE_ESTUDIO, or NO_ES_REGLA_DE_CODIGO; code skills cannot remain as hidden declarative/advisory rules.',
308
+ 'Classify every rule as IMPLEMENTAR_DETECTOR, REQUIERE_ESTUDIO, or NO_ES_REGLA_DE_CODIGO; code skills cannot remain as hidden declarative/advisory rules.',
306
309
  filePath: 'skills.lock.json',
307
310
  matchedBy: 'SkillsDeclarativeRulesClassificationGuard',
308
311
  source: 'skills-declarative-rules-classification',
@@ -250,6 +250,7 @@ export const buildPreWriteAutomationTrace = async (params: {
250
250
  const leaseResult = activeDependencies.writePreWriteLease({
251
251
  repoRoot: params.repoRoot,
252
252
  git,
253
+ allowExistingCodeChanges: true,
253
254
  });
254
255
  trace.actions.push({
255
256
  action: 'write_prewrite_lease',
@@ -8,6 +8,7 @@ import { DEFAULT_FACT_FILE_EXTENSIONS } from '../git/runPlatformGateFacts';
8
8
  export type PreWriteLease = {
9
9
  version: '1';
10
10
  kind: 'pumuki-pre-write-lease';
11
+ validation_mode?: 'clean-prewrite' | 'validated-diff';
11
12
  repo_root: string;
12
13
  head: string;
13
14
  branch: string | null;
@@ -119,6 +120,10 @@ const parseLease = (raw: string): PreWriteLease | undefined => {
119
120
  return {
120
121
  version: '1',
121
122
  kind: 'pumuki-pre-write-lease',
123
+ validation_mode:
124
+ value.validation_mode === 'validated-diff' || value.validation_mode === 'clean-prewrite'
125
+ ? value.validation_mode
126
+ : undefined,
122
127
  repo_root: value.repo_root,
123
128
  head: value.head,
124
129
  branch: typeof value.branch === 'string' ? value.branch : null,
@@ -186,6 +191,33 @@ export const readPreWriteLeaseStatus = (params: {
186
191
  };
187
192
  }
188
193
 
194
+ if (lease.validation_mode === 'validated-diff') {
195
+ const expectedPaths = [...lease.pre_change_code_paths].sort((left, right) => left.localeCompare(right));
196
+ const currentPaths = [...changedCodePaths].sort((left, right) => left.localeCompare(right));
197
+ if (
198
+ lease.pre_change_code_changes_count !== expectedPaths.length ||
199
+ expectedPaths.length !== currentPaths.length ||
200
+ expectedPaths.some((path, index) => path !== currentPaths[index])
201
+ ) {
202
+ return {
203
+ valid: false,
204
+ code: 'PRE_WRITE_LEASE_DIRTY_AT_ISSUE',
205
+ path,
206
+ lease,
207
+ changedCodePaths,
208
+ message: 'PRE_WRITE lease was issued for a different validated code diff.',
209
+ };
210
+ }
211
+
212
+ return {
213
+ valid: true,
214
+ code: 'PRE_WRITE_LEASE_VALID',
215
+ path,
216
+ lease,
217
+ changedCodePaths,
218
+ };
219
+ }
220
+
189
221
  if (lease.pre_change_code_changes_count !== 0 || lease.pre_change_code_paths.length !== 0) {
190
222
  return {
191
223
  valid: false,
@@ -210,10 +242,11 @@ export const writePreWriteLease = (params: {
210
242
  repoRoot: string;
211
243
  git: Pick<IGitService, 'runGit'>;
212
244
  now?: Date;
245
+ allowExistingCodeChanges?: boolean;
213
246
  }): PreWriteLeaseWriteResult => {
214
247
  const path = resolvePreWriteLeasePath(params.repoRoot);
215
248
  const changedCodePaths = collectPreWriteCodeChangePaths(params);
216
- if (changedCodePaths.length > 0) {
249
+ if (changedCodePaths.length > 0 && !params.allowExistingCodeChanges) {
217
250
  return {
218
251
  path,
219
252
  written: false,
@@ -229,13 +262,14 @@ export const writePreWriteLease = (params: {
229
262
  const lease: PreWriteLease = {
230
263
  version: '1',
231
264
  kind: 'pumuki-pre-write-lease',
265
+ validation_mode: changedCodePaths.length > 0 ? 'validated-diff' : 'clean-prewrite',
232
266
  repo_root: params.repoRoot,
233
267
  head: resolveHead(params),
234
268
  branch: resolveBranch(params),
235
269
  issued_at: issuedAt.toISOString(),
236
270
  expires_at: new Date(issuedAt.getTime() + PRE_WRITE_LEASE_TTL_MS).toISOString(),
237
- pre_change_code_changes_count: 0,
238
- pre_change_code_paths: [],
271
+ pre_change_code_changes_count: changedCodePaths.length,
272
+ pre_change_code_paths: changedCodePaths,
239
273
  };
240
274
  mkdirSync(dirname(path), { recursive: true });
241
275
  writeFileSync(path, `${JSON.stringify(lease, null, 2)}\n`, 'utf8');
@@ -245,7 +279,10 @@ export const writePreWriteLease = (params: {
245
279
  valid: true,
246
280
  code: 'PRE_WRITE_LEASE_WRITTEN',
247
281
  changedCodePaths,
248
- message: 'PRE_WRITE lease written for clean code state.',
282
+ message:
283
+ changedCodePaths.length > 0
284
+ ? 'PRE_WRITE lease written for validated code diff.'
285
+ : 'PRE_WRITE lease written for clean code state.',
249
286
  lease,
250
287
  };
251
288
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.270",
3
+ "version": "6.3.271",
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": {
@@ -19,8 +19,8 @@ process.stdout.write(`${JSON.stringify({
19
19
  AST_IMPLEMENTED: summary.rules
20
20
  .filter((rule) => rule.status === 'AST_IMPLEMENTED')
21
21
  .slice(0, 12),
22
- IMPLEMENTABLE_AHORA: summary.rules
23
- .filter((rule) => rule.status === 'IMPLEMENTABLE_AHORA')
22
+ IMPLEMENTAR_DETECTOR: summary.rules
23
+ .filter((rule) => rule.status === 'IMPLEMENTAR_DETECTOR')
24
24
  .slice(0, 12),
25
25
  REQUIERE_ESTUDIO: summary.rules
26
26
  .filter((rule) => rule.status === 'REQUIERE_ESTUDIO')
@@ -26,22 +26,22 @@ export const createConsumerLegacyMenuActions = (
26
26
  return [
27
27
  {
28
28
  id: '1',
29
- label: 'Consumer preflight + gate: ALL tracked files (PRE_COMMIT · writes evidence)',
29
+ label: 'Full audit (repo analysis)',
30
30
  execute: params.runFullAudit,
31
31
  },
32
32
  {
33
33
  id: '2',
34
- label: 'Consumer preflight + gate: REPO+index contract (PRE_PUSH · disk skip risk if evidence tracked)',
34
+ label: 'Strict REPO+STAGING (CI/CD)',
35
35
  execute: params.runStrictRepoAndStaged,
36
36
  },
37
37
  {
38
38
  id: '3',
39
- label: 'Consumer preflight + gate: STAGED only (PRE_COMMIT)',
39
+ label: 'Strict STAGING only (dev)',
40
40
  execute: params.runStrictStagedOnly,
41
41
  },
42
42
  {
43
43
  id: '4',
44
- label: 'Consumer preflight + gate: working tree (PRE_PUSH policy · disk skip risk if evidence tracked)',
44
+ label: 'Standard CRITICAL/HIGH',
45
45
  execute: params.runStandardCriticalHigh,
46
46
  },
47
47
  {
@@ -66,27 +66,27 @@ export const createConsumerLegacyMenuActions = (
66
66
  },
67
67
  {
68
68
  id: '5',
69
- label: 'Legacy read-only pattern checks snapshot',
69
+ label: 'Pattern checks',
70
70
  execute: params.runPatternChecks,
71
71
  },
72
72
  {
73
73
  id: '6',
74
- label: 'Legacy read-only ESLint evidence snapshot',
74
+ label: 'ESLint Admin+Web',
75
75
  execute: params.runEslintAudit,
76
76
  },
77
77
  {
78
78
  id: '7',
79
- label: 'Legacy read-only AST snapshot',
79
+ label: 'AST Intelligence',
80
80
  execute: params.runAstIntelligence,
81
81
  },
82
82
  {
83
83
  id: '8',
84
- label: 'Export legacy read-only evidence snapshot',
84
+ label: 'Export Markdown',
85
85
  execute: params.runExportMarkdown,
86
86
  },
87
87
  {
88
88
  id: '9',
89
- label: 'Legacy read-only file diagnostics snapshot',
89
+ label: 'File diagnostics',
90
90
  execute: params.runFileDiagnostics,
91
91
  },
92
92
  {
@@ -12,7 +12,12 @@ import {
12
12
  renderConsumerRuntimePatternChecks,
13
13
  renderConsumerRuntimeSummary,
14
14
  } from './framework-menu-consumer-runtime-audit';
15
- import type { ConsumerAction, ConsumerRuntimeEmitNotification, ConsumerRuntimeWrite } from './framework-menu-consumer-runtime-types';
15
+ import type {
16
+ ConsumerAction,
17
+ ConsumerRuntimeEmitNotification,
18
+ ConsumerRuntimeGateResult,
19
+ ConsumerRuntimeWrite,
20
+ } from './framework-menu-consumer-runtime-types';
16
21
 
17
22
  type ConsumerRuntimeActionDependencies = {
18
23
  repoRoot: string;
@@ -61,49 +66,54 @@ const runConsumerRuntimePreflight = async (
61
66
  );
62
67
  };
63
68
 
69
+ const renderSummaryAfterGate = (
70
+ dependencies: Pick<
71
+ ConsumerRuntimeActionDependencies,
72
+ | 'clearSummaryOverride'
73
+ | 'emitNotification'
74
+ | 'getSummaryOverride'
75
+ | 'repoRoot'
76
+ | 'setSummaryOverride'
77
+ | 'useColor'
78
+ | 'write'
79
+ >,
80
+ gateResult: ConsumerRuntimeGateResult | void
81
+ ) => {
82
+ if (gateResult?.blocked) {
83
+ dependencies.setSummaryOverride(
84
+ buildConsumerRuntimeBlockedSummary(gateResult.blocked)
85
+ );
86
+ } else {
87
+ dependencies.clearSummaryOverride();
88
+ }
89
+ const summary = renderConsumerRuntimeSummary({
90
+ repoRoot: dependencies.repoRoot,
91
+ write: dependencies.write,
92
+ useColor: dependencies.useColor,
93
+ summaryOverride: dependencies.getSummaryOverride(),
94
+ });
95
+ notifyConsumerRuntimeAuditSummary(
96
+ {
97
+ emitNotification: dependencies.emitNotification,
98
+ repoRoot: dependencies.repoRoot,
99
+ },
100
+ summary
101
+ );
102
+ return summary;
103
+ };
104
+
64
105
  export const createConsumerRuntimeActions = (
65
106
  dependencies: ConsumerRuntimeActionDependencies
66
107
  ): ReadonlyArray<ConsumerAction> =>
67
108
  createConsumerLegacyMenuActions({
68
109
  runFullAudit: async () => {
69
110
  await runConsumerRuntimePreflight(dependencies, 'PRE_COMMIT');
70
- await dependencies.runRepoGate();
71
- dependencies.clearSummaryOverride();
72
- notifyConsumerRuntimeAuditSummary(
73
- {
74
- emitNotification: dependencies.emitNotification,
75
- repoRoot: dependencies.repoRoot,
76
- },
77
- renderConsumerRuntimeSummary({
78
- repoRoot: dependencies.repoRoot,
79
- write: dependencies.write,
80
- useColor: dependencies.useColor,
81
- })
82
- );
111
+ renderSummaryAfterGate(dependencies, await dependencies.runRepoGate());
83
112
  },
84
113
  runStrictRepoAndStaged: async () => {
85
114
  await runConsumerRuntimePreflight(dependencies, 'PRE_PUSH');
86
115
  const gateResult = await dependencies.runRepoAndStagedGate();
87
- if (gateResult?.blocked) {
88
- dependencies.setSummaryOverride(
89
- buildConsumerRuntimeBlockedSummary(gateResult.blocked)
90
- );
91
- } else {
92
- dependencies.clearSummaryOverride();
93
- }
94
- const summary = renderConsumerRuntimeSummary({
95
- repoRoot: dependencies.repoRoot,
96
- write: dependencies.write,
97
- useColor: dependencies.useColor,
98
- summaryOverride: dependencies.getSummaryOverride(),
99
- });
100
- notifyConsumerRuntimeAuditSummary(
101
- {
102
- emitNotification: dependencies.emitNotification,
103
- repoRoot: dependencies.repoRoot,
104
- },
105
- summary
106
- );
116
+ const summary = renderSummaryAfterGate(dependencies, gateResult);
107
117
  if (
108
118
  !gateResult?.blocked
109
119
  && (summary.outcome === 'PASS' || summary.outcome === 'WARN')
@@ -113,109 +123,35 @@ export const createConsumerRuntimeActions = (
113
123
  },
114
124
  runStrictStagedOnly: async () => {
115
125
  await runConsumerRuntimePreflight(dependencies, 'PRE_COMMIT');
116
- await dependencies.runStagedGate();
117
- dependencies.clearSummaryOverride();
118
- const summary = renderConsumerRuntimeSummary({
119
- repoRoot: dependencies.repoRoot,
120
- write: dependencies.write,
121
- useColor: dependencies.useColor,
122
- });
123
- notifyConsumerRuntimeAuditSummary(
124
- {
125
- emitNotification: dependencies.emitNotification,
126
- repoRoot: dependencies.repoRoot,
127
- },
128
- summary
129
- );
126
+ const summary = renderSummaryAfterGate(dependencies, await dependencies.runStagedGate());
130
127
  printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'staged');
131
128
  },
132
129
  runStandardCriticalHigh: async () => {
133
130
  await runConsumerRuntimePreflight(dependencies, 'PRE_PUSH');
134
- await dependencies.runWorkingTreeGate();
135
- dependencies.clearSummaryOverride();
136
- const summary = renderConsumerRuntimeSummary({
137
- repoRoot: dependencies.repoRoot,
138
- write: dependencies.write,
139
- useColor: dependencies.useColor,
140
- });
141
- notifyConsumerRuntimeAuditSummary(
142
- {
143
- emitNotification: dependencies.emitNotification,
144
- repoRoot: dependencies.repoRoot,
145
- },
146
- summary
147
- );
131
+ const gateResult = await dependencies.runWorkingTreeGate();
132
+ const summary = renderSummaryAfterGate(dependencies, gateResult);
148
133
  if (summary.outcome === 'PASS' || summary.outcome === 'WARN') {
149
134
  printPrePushTrackedEvidenceDiskHint({ write: dependencies.write });
150
135
  }
151
136
  printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'workingTree');
152
137
  },
153
138
  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
- );
139
+ const summary = renderSummaryAfterGate(dependencies, await dependencies.runStagedGate());
168
140
  printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'staged');
169
141
  },
170
142
  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
- );
143
+ const summary = renderSummaryAfterGate(dependencies, await dependencies.runUnstagedGate());
185
144
  printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'unstaged');
186
145
  },
187
146
  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
147
+ const summary = renderSummaryAfterGate(
148
+ dependencies,
149
+ await dependencies.runWorkingTreePreCommitGate()
201
150
  );
202
151
  printConsumerRuntimeEmptyScopeHint({ write: dependencies.write }, summary, 'workingTree');
203
152
  },
204
153
  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
- );
154
+ renderSummaryAfterGate(dependencies, await dependencies.runRepoGate());
219
155
  },
220
156
  runPatternChecks: async () => {
221
157
  dependencies.write(`\n${renderConsumerRuntimePatternChecks(dependencies.repoRoot)}\n`);
@@ -33,6 +33,9 @@ export const renderConsumerRuntimeSummary = (
33
33
  dependencies: ConsumerRuntimeSummaryDependencies
34
34
  ): FrameworkMenuEvidenceSummary => {
35
35
  const summary = dependencies.summaryOverride ?? readEvidenceSummaryForMenu(dependencies.repoRoot);
36
+ const canonicalSummary = dependencies.summaryOverride
37
+ ? readEvidenceSummaryForMenu(dependencies.repoRoot)
38
+ : null;
36
39
  const lines = [
37
40
  formatEvidenceSummaryForMenu(summary),
38
41
  '',
@@ -41,6 +44,13 @@ export const renderConsumerRuntimeSummary = (
41
44
  `Files affected: ${summary.filesAffected}`,
42
45
  ];
43
46
 
47
+ const driftLines = canonicalSummary
48
+ ? buildConsumerRuntimeEvidenceDriftLines(summary, canonicalSummary)
49
+ : [];
50
+ if (driftLines.length > 0) {
51
+ lines.push('', ...driftLines);
52
+ }
53
+
44
54
  if (summary.status === 'ok' && summary.topFindings.length > 0) {
45
55
  const primaryFinding = summary.topFindings[0];
46
56
  lines.push('', `Primary block: ${primaryFinding.ruleId}`);
@@ -74,6 +84,62 @@ export const renderConsumerRuntimeSummary = (
74
84
  return summary;
75
85
  };
76
86
 
87
+ const toPlatformMap = (
88
+ summary: FrameworkMenuEvidenceSummary
89
+ ): Map<string, number> =>
90
+ new Map((summary.platformAuditRows ?? []).map((row) => [row.platform, row.violations]));
91
+
92
+ const buildConsumerRuntimeEvidenceDriftLines = (
93
+ gateSummary: FrameworkMenuEvidenceSummary,
94
+ canonicalSummary: FrameworkMenuEvidenceSummary
95
+ ): string[] => {
96
+ const drifts: string[] = [];
97
+ if (canonicalSummary.status !== 'ok') {
98
+ return [
99
+ `⚠ Menu/gate evidence drift: canonical_evidence=${canonicalSummary.status}; using gate blocked summary`,
100
+ ];
101
+ }
102
+ if (gateSummary.stage !== canonicalSummary.stage) {
103
+ drifts.push(`stage gate=${gateSummary.stage ?? 'null'} evidence=${canonicalSummary.stage ?? 'null'}`);
104
+ }
105
+ if (gateSummary.outcome !== canonicalSummary.outcome) {
106
+ drifts.push(`outcome gate=${gateSummary.outcome ?? 'null'} evidence=${canonicalSummary.outcome ?? 'null'}`);
107
+ }
108
+ if (gateSummary.totalFindings !== canonicalSummary.totalFindings) {
109
+ drifts.push(`totalFindings gate=${gateSummary.totalFindings} evidence=${canonicalSummary.totalFindings}`);
110
+ }
111
+
112
+ const severityKeys = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW'] as const;
113
+ for (const key of severityKeys) {
114
+ const gateValue = gateSummary.byEnterpriseSeverity?.[key] ?? 0;
115
+ const evidenceValue = canonicalSummary.byEnterpriseSeverity?.[key] ?? 0;
116
+ if (gateValue !== evidenceValue) {
117
+ drifts.push(`severity.${key} gate=${gateValue} evidence=${evidenceValue}`);
118
+ }
119
+ }
120
+
121
+ const gatePlatforms = toPlatformMap(gateSummary);
122
+ const evidencePlatforms = toPlatformMap(canonicalSummary);
123
+ const platformNames = [...new Set([...gatePlatforms.keys(), ...evidencePlatforms.keys()])].sort();
124
+ for (const platform of platformNames) {
125
+ const gateValue = gatePlatforms.get(platform) ?? 0;
126
+ const evidenceValue = evidencePlatforms.get(platform) ?? 0;
127
+ if (gateValue !== evidenceValue) {
128
+ drifts.push(`platform.${platform} gate=${gateValue} evidence=${evidenceValue}`);
129
+ }
130
+ }
131
+
132
+ if (drifts.length === 0) {
133
+ return [];
134
+ }
135
+ return [
136
+ '⚠ Menu/gate evidence drift detected',
137
+ 'Diagnostic code: MENU_GATE_EVIDENCE_DRIFT',
138
+ 'Diagnostic severity: HIGH',
139
+ ...drifts.map((drift) => `- ${drift}`),
140
+ ];
141
+ };
142
+
77
143
  export const printPrePushTrackedEvidenceDiskHint = (params: {
78
144
  write: ConsumerRuntimeSummaryDependencies['write'];
79
145
  }): void => {