pumuki 6.3.275 → 6.3.276

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/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.3.276] - 2026-05-18
4
+
5
+ - `PUMUKI-INC-146`: PRE_WRITE no longer requires `skills.ios.critical-test-quality` for iOS/SwiftUI production-only slices that do not touch test files. The rule remains hard-blocking when the PRE_WRITE scope includes Swift test paths (`/Tests/`, `/UITests/`, `*Test.swift`, `*Tests.swift`, `*.spec.swift`), preserving XCTest/Swift Testing quality enforcement without blocking unrelated visual/product commits.
6
+
3
7
  ## [6.3.275] - 2026-05-18
4
8
 
5
9
  - PRE_WRITE skills coverage: `PRE_WRITE` now loads the full runtime skills contract for the detected platform, so Web/Frontend slices evaluate `skills.frontend.*` before commit instead of failing with `EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE` while no frontend rule was active.
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.272
1
+ v6.3.276
@@ -4,6 +4,10 @@ This file tracks the active deterministic framework line used in this repository
4
4
  Canonical release chronology lives in `CHANGELOG.md`.
5
5
  This file keeps only the operational highlights and rollout notes that matter while running the framework.
6
6
 
7
+ ### 2026-05-18 (v6.3.276)
8
+
9
+ - `PUMUKI-INC-146`: PRE_WRITE keeps `skills.ios.critical-test-quality` fail-closed for real Swift test slices, but stops applying that test-quality critical rule to iOS/SwiftUI production-only slices. RuralGo wordmark/splash-style commits should still be blocked by real iOS AST/skills violations, SDD, context or evidence issues, but not by missing XCTest quality coverage when no test file is in scope.
10
+
7
11
  ### 2026-05-18 (v6.3.275)
8
12
 
9
13
  - `PUMUKI-INC-143`: PRE_WRITE now loads the full runtime skills contract for detected platforms. Frontend slices now materialize `skills.frontend.*` in `active_rule_ids` and `evaluated_rule_ids` instead of failing on missing scope coverage before any frontend rule can run.
@@ -0,0 +1,82 @@
1
+ import type { AiEvidenceV2_1, CompatibilityViolation, SnapshotFinding } from './schema';
2
+
3
+ export type EvidenceBlockingCause = {
4
+ ruleId: string;
5
+ code: string;
6
+ severity: string;
7
+ message: string;
8
+ file?: string;
9
+ lines?: SnapshotFinding['lines'];
10
+ remediation?: string;
11
+ source: 'snapshot.findings' | 'ai_gate.violations';
12
+ };
13
+
14
+ const isBlockingSeverity = (severity: string): boolean =>
15
+ severity.toUpperCase() === 'CRITICAL' || severity.toUpperCase() === 'ERROR';
16
+
17
+ const toFindingBlockingCause = (finding: SnapshotFinding): EvidenceBlockingCause => ({
18
+ ruleId: finding.ruleId,
19
+ code: finding.code,
20
+ severity: finding.severity,
21
+ message: finding.message,
22
+ file: finding.file,
23
+ lines: finding.lines,
24
+ remediation: finding.expected_fix,
25
+ source: 'snapshot.findings',
26
+ });
27
+
28
+ const toViolationBlockingCause = (
29
+ violation: CompatibilityViolation
30
+ ): EvidenceBlockingCause => ({
31
+ ruleId: violation.ruleId,
32
+ code: violation.code,
33
+ severity: violation.level,
34
+ message: violation.message,
35
+ file: violation.file,
36
+ lines: violation.lines,
37
+ remediation: violation.expected_fix,
38
+ source: 'ai_gate.violations',
39
+ });
40
+
41
+ const causeKey = (cause: EvidenceBlockingCause): string =>
42
+ [cause.source, cause.ruleId, cause.code, cause.file ?? '', String(cause.lines ?? '')].join('|');
43
+
44
+ export const extractEvidenceBlockingCauses = (
45
+ evidence: AiEvidenceV2_1
46
+ ): ReadonlyArray<EvidenceBlockingCause> => {
47
+ const causes: EvidenceBlockingCause[] = [];
48
+ const seen = new Set<string>();
49
+
50
+ for (const finding of evidence.snapshot.findings) {
51
+ if (finding.blocking === false || !isBlockingSeverity(finding.severity)) {
52
+ continue;
53
+ }
54
+ const cause = toFindingBlockingCause(finding);
55
+ const key = causeKey(cause);
56
+ if (!seen.has(key)) {
57
+ causes.push(cause);
58
+ seen.add(key);
59
+ }
60
+ }
61
+
62
+ for (const violation of evidence.ai_gate.violations) {
63
+ if (violation.blocking === false || !isBlockingSeverity(violation.level)) {
64
+ continue;
65
+ }
66
+ const cause = toViolationBlockingCause(violation);
67
+ const key = causeKey(cause);
68
+ if (!seen.has(key)) {
69
+ causes.push(cause);
70
+ seen.add(key);
71
+ }
72
+ }
73
+
74
+ return causes;
75
+ };
76
+
77
+ export const formatEvidenceBlockingCause = (cause: EvidenceBlockingCause): string => {
78
+ const location = cause.file ? ` file=${cause.file}` : '';
79
+ const lines = cause.lines ? ` lines=${String(cause.lines)}` : '';
80
+ const remediation = cause.remediation ? ` remediation=${cause.remediation}` : '';
81
+ return `${cause.ruleId} severity=${cause.severity} code=${cause.code}${location}${lines} message=${cause.message}${remediation}`;
82
+ };
@@ -401,6 +401,38 @@ const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> =>
401
401
  }
402
402
  };
403
403
 
404
+ const isIosTestQualityPath = (filePath: string): boolean => {
405
+ const normalized = normalizeChangedPath(filePath).toLowerCase();
406
+ if (!normalized.endsWith('.swift')) {
407
+ return false;
408
+ }
409
+ return normalized.includes('/tests/')
410
+ || normalized.includes('/uitests/')
411
+ || normalized.endsWith('test.swift')
412
+ || normalized.endsWith('tests.swift')
413
+ || normalized.endsWith('.spec.swift');
414
+ };
415
+
416
+ const hasPreWriteIosTestQualityScope = (repoRoot: string): boolean =>
417
+ collectWorktreeChangedPaths(repoRoot).some((filePath) => isIosTestQualityPath(filePath));
418
+
419
+ const resolvePreWriteCriticalSkillsRules = (params: {
420
+ platform: PreWriteSkillsPlatform;
421
+ stage: AiGateStage;
422
+ repoRoot: string;
423
+ }): ReadonlyArray<string> => {
424
+ const ruleIds = PREWRITE_CRITICAL_SKILLS_RULES[params.platform];
425
+ if (
426
+ params.platform === 'ios'
427
+ && params.stage === 'PRE_WRITE'
428
+ && ruleIds.includes('skills.ios.critical-test-quality')
429
+ && !hasPreWriteIosTestQualityScope(params.repoRoot)
430
+ ) {
431
+ return ruleIds.filter((ruleId) => ruleId !== 'skills.ios.critical-test-quality');
432
+ }
433
+ return ruleIds;
434
+ };
435
+
404
436
  const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
405
437
  const normalized = normalizeChangedPath(filePath).toLowerCase();
406
438
  if (platform === 'ios') {
@@ -552,6 +584,7 @@ const collectActiveRuleIdsCoverageViolations = (params: {
552
584
  };
553
585
 
554
586
  const collectPreWritePlatformSkillsViolations = (params: {
587
+ repoRoot: string;
555
588
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
556
589
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
557
590
  skillsEnforcement: SkillsEnforcementResolution;
@@ -625,7 +658,11 @@ const collectPreWritePlatformSkillsViolations = (params: {
625
658
 
626
659
  const missingCriticalRulesByPlatform: string[] = [];
627
660
  for (const platform of detectedPlatforms) {
628
- const requiredCriticalRuleIds = PREWRITE_CRITICAL_SKILLS_RULES[platform];
661
+ const requiredCriticalRuleIds = resolvePreWriteCriticalSkillsRules({
662
+ platform,
663
+ stage: 'PRE_WRITE',
664
+ repoRoot: params.repoRoot,
665
+ });
629
666
  if (requiredCriticalRuleIds.length === 0) {
630
667
  continue;
631
668
  }
@@ -716,14 +753,22 @@ const toSkillsContractAssessment = (params: {
716
753
  platform,
717
754
  required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
718
755
  required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
719
- required_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
756
+ required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
757
+ platform,
758
+ stage: params.stage,
759
+ repoRoot: params.repoRoot,
760
+ }),
720
761
  required_any_transversal_critical_rule_ids: [
721
762
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
722
763
  ],
723
764
  active_prefix_covered: false,
724
765
  evaluated_prefix_covered: false,
725
766
  missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
726
- missing_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
767
+ missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
768
+ platform,
769
+ stage: params.stage,
770
+ repoRoot: params.repoRoot,
771
+ }),
727
772
  transversal_critical_covered: false,
728
773
  missing_any_transversal_critical_rule_ids: [
729
774
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
@@ -798,14 +843,22 @@ const toSkillsContractAssessment = (params: {
798
843
  platform,
799
844
  required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
800
845
  required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
801
- required_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
846
+ required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
847
+ platform,
848
+ stage: params.stage,
849
+ repoRoot: params.repoRoot,
850
+ }),
802
851
  required_any_transversal_critical_rule_ids: [
803
852
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
804
853
  ],
805
854
  active_prefix_covered: false,
806
855
  evaluated_prefix_covered: false,
807
856
  missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
808
- missing_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
857
+ missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
858
+ platform,
859
+ stage: params.stage,
860
+ repoRoot: params.repoRoot,
861
+ }),
809
862
  transversal_critical_covered: false,
810
863
  missing_any_transversal_critical_rule_ids: [
811
864
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
@@ -859,7 +912,13 @@ const toSkillsContractAssessment = (params: {
859
912
  for (const platform of assessmentPlatforms) {
860
913
  const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
861
914
  const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
862
- const requiredCriticalRuleIds = [...PREWRITE_CRITICAL_SKILLS_RULES[platform]];
915
+ const requiredCriticalRuleIds = [
916
+ ...resolvePreWriteCriticalSkillsRules({
917
+ platform,
918
+ stage: params.stage,
919
+ repoRoot: params.repoRoot,
920
+ }),
921
+ ];
863
922
  const requiredAnyTransversalCriticalRuleIds = [
864
923
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
865
924
  ];
@@ -1057,6 +1116,7 @@ const collectPreWriteCoherenceViolations = (params: {
1057
1116
 
1058
1117
  violations.push(
1059
1118
  ...collectPreWritePlatformSkillsViolations({
1119
+ repoRoot: params.repoRoot,
1060
1120
  evidence: params.evidence,
1061
1121
  coverage,
1062
1122
  skillsEnforcement,
@@ -32,7 +32,6 @@ import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
32
32
  import { createEmptySnapshotRulesCoverage } from '../evidence/rulesCoverage';
33
33
  import { enforceTddBddPolicy } from '../tdd/enforcement';
34
34
  import type { TddBddSnapshot } from '../tdd/types';
35
- import { resolveSkillsEnforcement } from '../policy/skillsEnforcement';
36
35
  import { applyTddBddEnforcement } from '../policy/tddBddEnforcement';
37
36
  import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings';
38
37
  import {
@@ -886,25 +885,7 @@ const applySkillsFindingEnforcement = (
886
885
  if (!finding) {
887
886
  return undefined;
888
887
  }
889
- const skillsEnforcement = resolveSkillsEnforcement();
890
- if (skillsEnforcement.blocking) {
891
- return finding;
892
- }
893
- return {
894
- ...finding,
895
- severity: 'WARN',
896
- blocking: false,
897
- };
898
- };
899
-
900
- const toSoftPreCommitSkillsFinding = (params: {
901
- finding: Finding | undefined;
902
- enabled: boolean;
903
- observedCodePaths: ReadonlyArray<string>;
904
- }): Finding | undefined => {
905
- void params.enabled;
906
- void params.observedCodePaths;
907
- return params.finding;
888
+ return finding;
908
889
  };
909
890
 
910
891
  export async function runPlatformGate(params: {
@@ -1281,42 +1262,10 @@ export async function runPlatformGate(params: {
1281
1262
  const hasNativeBlockingFinding = findings.some(
1282
1263
  (finding) => shouldBlockFromFinding(finding)
1283
1264
  );
1284
- const preCommitSoftSkillsEnabled = process.env.PUMUKI_PRE_COMMIT_SOFT_SKILLS === '1';
1285
- const lowRiskPreCommitWindow = observedCodePaths.length > 0 && observedCodePaths.length <= 3;
1286
- const shouldSoftEnforceSkillsFindings =
1287
- params.policy.stage === 'PRE_COMMIT'
1288
- && preCommitSoftSkillsEnabled
1289
- && lowRiskPreCommitWindow
1290
- && !sddBlockingFinding
1291
- && !degradedModeBlocks
1292
- && !shouldBlockFromFinding(policyAsCodeBlockingFinding)
1293
- && !shouldBlockFromFinding(coverageBlockingFinding)
1294
- && !shouldBlockFromFinding(declarativeRulesClassificationFinding)
1295
- && !shouldBlockFromFinding(activeRulesEmptyForCodeChangesFinding)
1296
- && !shouldBlockFromFinding(effectiveIosTestsQualityFinding)
1297
- && !shouldBlockFromFinding(astIntelligenceDualFinding)
1298
- && !hasTddBddBlockingFinding
1299
- && !hasNativeBlockingFinding;
1300
- const effectiveUnsupportedSkillsMappingFinding = toSoftPreCommitSkillsFinding({
1301
- finding: effectiveUnsupportedSkillsMappingInput,
1302
- enabled: shouldSoftEnforceSkillsFindings,
1303
- observedCodePaths,
1304
- });
1305
- const effectivePlatformSkillsCoverageFinding = toSoftPreCommitSkillsFinding({
1306
- finding: effectivePlatformSkillsCoverageInput,
1307
- enabled: shouldSoftEnforceSkillsFindings,
1308
- observedCodePaths,
1309
- });
1310
- const effectiveCrossPlatformCriticalFinding = toSoftPreCommitSkillsFinding({
1311
- finding: effectiveCrossPlatformCriticalInput,
1312
- enabled: shouldSoftEnforceSkillsFindings,
1313
- observedCodePaths,
1314
- });
1315
- const effectiveSkillsScopeComplianceFinding = toSoftPreCommitSkillsFinding({
1316
- finding: effectiveSkillsScopeComplianceInput,
1317
- enabled: shouldSoftEnforceSkillsFindings,
1318
- observedCodePaths,
1319
- });
1265
+ const effectiveUnsupportedSkillsMappingFinding = effectiveUnsupportedSkillsMappingInput;
1266
+ const effectivePlatformSkillsCoverageFinding = effectivePlatformSkillsCoverageInput;
1267
+ const effectiveCrossPlatformCriticalFinding = effectiveCrossPlatformCriticalInput;
1268
+ const effectiveSkillsScopeComplianceFinding = effectiveSkillsScopeComplianceInput;
1320
1269
  const effectiveFindings = sddBlockingFinding
1321
1270
  ? [
1322
1271
  ...(contextBlockingFinding ? [contextBlockingFinding] : []),
@@ -21,6 +21,12 @@ const BLOCK_NEXT_ACTION_BY_CODE: Readonly<Record<string, string>> = {
21
21
  'Reconcilia policy/skills y reintenta PRE_COMMIT: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki-pre-commit',
22
22
  SKILLS_SKILLS_FRONTEND_NO_SOLID_VIOLATIONS:
23
23
  'Aplica refactor incremental: extrae 1 componente/hook por commit y vuelve a ejecutar PRE_COMMIT.',
24
+ SKILLS_SKILLS_BACKEND_NO_CONSOLE_LOG:
25
+ 'Elimina console.log del backend o sustitúyelo por el logger aprobado para runtime; vuelve a ejecutar el gate.',
26
+ SKILLS_SKILLS_FRONTEND_NO_CONSOLE_LOG:
27
+ 'Elimina console.log del frontend de producción o muévelo a instrumentación aprobada; vuelve a ejecutar el gate.',
28
+ HEURISTICS_CONSOLE_LOG_AST:
29
+ 'Elimina console.log del código productivo o usa el logger aprobado del proyecto; vuelve a ejecutar el gate.',
24
30
  };
25
31
 
26
32
  const severityWeight = (severity: string): number => {
@@ -38,6 +44,23 @@ const severityWeight = (severity: string): number => {
38
44
  }
39
45
  };
40
46
 
47
+ const isAstSkillFinding = (finding: Finding): boolean => {
48
+ const ruleId = finding.ruleId.toLowerCase();
49
+ const code = finding.code.toLowerCase();
50
+ return (
51
+ ruleId.startsWith('skills.') ||
52
+ ruleId.startsWith('heuristics.') ||
53
+ code.startsWith('skills_') ||
54
+ code.startsWith('heuristics_') ||
55
+ code.includes('_ast')
56
+ );
57
+ };
58
+
59
+ const primaryWeight = (finding: Finding): number => {
60
+ const astSkillWeight = isAstSkillFinding(finding) ? 100 : 0;
61
+ return astSkillWeight + severityWeight(finding.severity);
62
+ };
63
+
41
64
  const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
42
65
  let primary = findings[0];
43
66
  for (const finding of findings.slice(1)) {
@@ -45,7 +68,7 @@ const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
45
68
  primary = finding;
46
69
  continue;
47
70
  }
48
- if (severityWeight(finding.severity) > severityWeight(primary.severity)) {
71
+ if (primaryWeight(finding) > primaryWeight(primary)) {
49
72
  primary = finding;
50
73
  }
51
74
  }
@@ -54,6 +77,10 @@ const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
54
77
 
55
78
  const sortFindingsBySeverity = (findings: ReadonlyArray<Finding>): ReadonlyArray<Finding> =>
56
79
  [...findings].sort((left, right) => {
80
+ const primaryDelta = primaryWeight(right) - primaryWeight(left);
81
+ if (primaryDelta !== 0) {
82
+ return primaryDelta;
83
+ }
57
84
  const severityDelta = severityWeight(right.severity) - severityWeight(left.severity);
58
85
  if (severityDelta !== 0) {
59
86
  return severityDelta;
@@ -126,7 +153,8 @@ export const printGateFindings = (findings: ReadonlyArray<Finding>): void => {
126
153
  `[pumuki][warning-summary] secondary=${finding.code} severity=${finding.severity.toUpperCase()} rule=${finding.ruleId}\n`
127
154
  );
128
155
  }
129
- for (const finding of orderedFindings) {
130
- process.stdout.write(`${formatFinding(finding)}\n`);
156
+ process.stdout.write(`[pumuki][findings] total=${orderedFindings.length}\n`);
157
+ for (const [index, finding] of orderedFindings.entries()) {
158
+ process.stdout.write(`\n${index + 1}. ${formatFinding(finding)}\n`);
131
159
  }
132
160
  };
@@ -823,6 +823,13 @@ const enforceGitAtomicityGate = (params: {
823
823
  causeCode: firstViolation.code,
824
824
  causeMessage: firstViolation.message,
825
825
  remediation: firstViolation.remediation,
826
+ blockingCauses: atomicity.violations.map((violation) => ({
827
+ code: violation.code,
828
+ message: violation.message,
829
+ ruleId: 'git.atomicity.blocked',
830
+ file: '.pumuki/git-atomicity.json',
831
+ remediation: violation.remediation,
832
+ })),
826
833
  });
827
834
  notifyAuditSummaryForStage(params.dependencies, params.stage);
828
835
  return true;
@@ -334,6 +334,16 @@ export const runSddCommand = async (parsed: ParsedArgs, activeDependencies: Life
334
334
  causeCode,
335
335
  nextAction,
336
336
  }),
337
+ blockingCauses: aiGate.violations.map((violation) => ({
338
+ code: violation.code,
339
+ message: violation.message,
340
+ ruleId: violation.code,
341
+ file: resolveAiGateViolationLocation(violation.code),
342
+ remediation: resolvePreWriteBlockedRemediation({
343
+ causeCode: violation.code,
344
+ nextAction,
345
+ }),
346
+ })),
337
347
  });
338
348
  return 1;
339
349
  }
@@ -25,12 +25,18 @@ import {
25
25
  readLifecycleNotificationStatus,
26
26
  type LifecycleNotificationStatus,
27
27
  } from './notificationStatus';
28
+ import {
29
+ extractEvidenceBlockingCauses,
30
+ formatEvidenceBlockingCause,
31
+ type EvidenceBlockingCause,
32
+ } from '../evidence/blockingCauses';
28
33
 
29
34
  export type DoctorIssueSeverity = 'warning' | 'error';
30
35
 
31
36
  export type DoctorIssue = {
32
37
  severity: DoctorIssueSeverity;
33
38
  message: string;
39
+ blockingCauses?: ReadonlyArray<EvidenceBlockingCause>;
34
40
  };
35
41
 
36
42
  export type DoctorDeepCheckId =
@@ -193,12 +199,20 @@ const buildDoctorIssues = (params: {
193
199
  evidence.severity_metrics.gate_status === 'BLOCKED';
194
200
  if (blocked) {
195
201
  const blockedStage = evidence?.snapshot?.stage ?? 'PRE_WRITE';
202
+ const blockingCauses = extractEvidenceBlockingCauses(evidence);
203
+ const causeDetails =
204
+ blockingCauses.length > 0
205
+ ? ` Blocking causes: ${blockingCauses.length}. ${blockingCauses
206
+ .map(formatEvidenceBlockingCause)
207
+ .join(' | ')}`
208
+ : '';
196
209
  issues.push({
197
210
  severity: 'error',
198
211
  message: appendTrackingActionableContext({
199
212
  repoRoot: params.repoRoot,
200
- message: `Governance is blocked (${blockedStage}).`,
213
+ message: `Governance is blocked (${blockedStage}).${causeDetails}`,
201
214
  }),
215
+ blockingCauses,
202
216
  });
203
217
  } else if (evidence.snapshot.outcome === 'WARN') {
204
218
  const warnStage = evidence?.snapshot?.stage ?? 'PRE_WRITE';
@@ -22,6 +22,10 @@ import {
22
22
  readLifecycleNotificationStatus,
23
23
  type LifecycleNotificationStatus,
24
24
  } from './notificationStatus';
25
+ import {
26
+ extractEvidenceBlockingCauses,
27
+ formatEvidenceBlockingCause,
28
+ } from '../evidence/blockingCauses';
25
29
 
26
30
  export type LifecycleStatus = {
27
31
  repoRoot: string;
@@ -101,12 +105,20 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
101
105
  repoRoot,
102
106
  message: `Governance is blocked (${blockedStage}).`,
103
107
  });
108
+ const blockingCauses = extractEvidenceBlockingCauses(evidence);
109
+ const causeDetails =
110
+ blockingCauses.length > 0
111
+ ? ` Blocking causes: ${blockingCauses.length}. ${blockingCauses
112
+ .map(formatEvidenceBlockingCause)
113
+ .join(' | ')}`
114
+ : '';
104
115
  return [
105
116
  ...notificationIssues,
106
117
  ...leaseIssues,
107
118
  {
108
119
  severity: 'error',
109
- message,
120
+ message: `${message}${causeDetails}`,
121
+ blockingCauses,
110
122
  },
111
123
  ];
112
124
  };
@@ -679,6 +679,13 @@ export const runLifecycleWatch = async (
679
679
  causeCode: cause.code,
680
680
  causeMessage: cause.message,
681
681
  remediation: cause.remediation,
682
+ blockingCauses: matchedFindings.map((finding) => ({
683
+ code: finding.code ?? finding.ruleId,
684
+ message: finding.message,
685
+ ruleId: finding.ruleId,
686
+ file: finding.file,
687
+ remediation: finding.expected_fix,
688
+ })),
682
689
  });
683
690
  } else {
684
691
  notificationResult = activeDependencies.emitAuditSummaryNotificationFromEvidence({
@@ -21,6 +21,7 @@ import {
21
21
  } from './evidenceFacets';
22
22
  import { CONTEXT_API_CAPABILITIES } from './evidencePayloadConfig';
23
23
  import { toSuppressedSummaryFields } from './evidencePayloadSummarySuppressed';
24
+ import { extractEvidenceBlockingCauses } from '../evidence/blockingCauses';
24
25
 
25
26
  export const toStatusPayload = (repoRoot: string): unknown => {
26
27
  const readResult = readEvidenceResult(repoRoot);
@@ -61,6 +62,7 @@ export const toStatusPayload = (repoRoot: string): unknown => {
61
62
  const detectedPlatformsCount = sortedPlatforms.filter((entry) => entry.detected).length;
62
63
  const suppressedFindingsCount = evidence.consolidation?.suppressed?.length ?? 0;
63
64
  const findingsWithLinesCount = toFindingsWithLinesCount(evidence.snapshot.findings);
65
+ const blockingCauses = extractEvidenceBlockingCauses(evidence);
64
66
  return {
65
67
  status: 'ok',
66
68
  context_api: CONTEXT_API_CAPABILITIES,
@@ -83,6 +85,8 @@ export const toStatusPayload = (repoRoot: string): unknown => {
83
85
  findings_by_platform: toFindingsByPlatform(evidence.snapshot.findings),
84
86
  highest_severity: toHighestSeverity(evidence.snapshot.findings),
85
87
  blocking_findings_count: toBlockingFindingsCount(evidence.snapshot.findings),
88
+ blocking_causes_count: blockingCauses.length,
89
+ blocking_causes: blockingCauses,
86
90
  ledger_count: evidence.ledger.length,
87
91
  ledger_files_count: toLedgerFilesCount(evidence.ledger),
88
92
  ledger_rules_count: toLedgerRulesCount(evidence.ledger),
@@ -19,12 +19,14 @@ import {
19
19
  toSeverityCounts,
20
20
  } from './evidenceFacets';
21
21
  import { toSuppressedSummaryFields } from './evidencePayloadSummarySuppressed';
22
+ import { extractEvidenceBlockingCauses } from '../evidence/blockingCauses';
22
23
 
23
24
  export const toSummaryPayload = (evidence: AiEvidenceV2_1) => {
24
25
  const sortedPlatforms = sortPlatforms(evidence.platforms);
25
26
  const detectedPlatforms = sortedPlatforms.filter((entry) => entry.detected);
26
27
  const suppressedFindingsCount = evidence.consolidation?.suppressed?.length ?? 0;
27
28
  const findingsWithLinesCount = toFindingsWithLinesCount(evidence.snapshot.findings);
29
+ const blockingCauses = extractEvidenceBlockingCauses(evidence);
28
30
  return {
29
31
  version: evidence.version,
30
32
  timestamp: evidence.timestamp,
@@ -41,6 +43,8 @@ export const toSummaryPayload = (evidence: AiEvidenceV2_1) => {
41
43
  findings_by_platform: toFindingsByPlatform(evidence.snapshot.findings),
42
44
  highest_severity: toHighestSeverity(evidence.snapshot.findings),
43
45
  blocking_findings_count: toBlockingFindingsCount(evidence.snapshot.findings),
46
+ blocking_causes_count: blockingCauses.length,
47
+ blocking_causes: blockingCauses,
44
48
  },
45
49
  ledger_count: evidence.ledger.length,
46
50
  ledger_files_count: toLedgerFilesCount(evidence.ledger),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.275",
3
+ "version": "6.3.276",
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": {
@@ -2,6 +2,7 @@ import { createConsumerLegacyMenuActions } from './framework-menu-consumer-actio
2
2
  import { formatConsumerPreflight, runConsumerPreflight } from './framework-menu-consumer-preflight-lib';
3
3
  import {
4
4
  buildConsumerRuntimeBlockedSummary,
5
+ buildConsumerRuntimeBlockedCauseLines,
5
6
  exportConsumerRuntimeMarkdown,
6
7
  notifyConsumerRuntimeAuditSummary,
7
8
  printConsumerRuntimeEmptyScopeHint,
@@ -83,6 +84,11 @@ const renderSummaryAfterGate = (
83
84
  dependencies.setSummaryOverride(
84
85
  buildConsumerRuntimeBlockedSummary(gateResult.blocked)
85
86
  );
87
+ dependencies.write(
88
+ '\n' +
89
+ buildConsumerRuntimeBlockedCauseLines(gateResult.blocked).join('\n') +
90
+ '\n'
91
+ );
86
92
  } else {
87
93
  dependencies.clearSummaryOverride();
88
94
  }
@@ -102,13 +108,57 @@ const renderSummaryAfterGate = (
102
108
  return summary;
103
109
  };
104
110
 
111
+ const buildFullAuditFinalSummaryLines = (
112
+ summary: import('./framework-menu-evidence-summary-lib').FrameworkMenuEvidenceSummary
113
+ ): string[] => {
114
+ const platformRows = summary.platformAuditRows ?? [];
115
+ const topFindings = summary.topFindings.slice(0, 10);
116
+ return [
117
+ '✅ AST Intelligence completed',
118
+ '═══════════════════════════════════════════════════════════════',
119
+ 'FINAL SUMMARY - VIOLATIONS BY SEVERITY',
120
+ `🔴 CRITICAL: ${summary.byEnterpriseSeverity?.CRITICAL ?? summary.bySeverity.CRITICAL}`,
121
+ `🟠 HIGH: ${summary.byEnterpriseSeverity?.HIGH ?? summary.bySeverity.ERROR}`,
122
+ `🟡 MEDIUM: ${summary.byEnterpriseSeverity?.MEDIUM ?? summary.bySeverity.WARN}`,
123
+ `🔵 LOW: ${summary.byEnterpriseSeverity?.LOW ?? summary.bySeverity.INFO}`,
124
+ `Total violations: ${summary.totalFindings}`,
125
+ '',
126
+ 'PLATFORM-SPECIFIC ANALYSIS',
127
+ ...(platformRows.length === 0
128
+ ? [' No platform findings.']
129
+ : platformRows.map((row) => ` Platform: ${row.platform} -> ${row.violations} violations`)),
130
+ '',
131
+ 'TOP VIOLATIONS & REMEDIATION',
132
+ ...(topFindings.length === 0
133
+ ? [' No violations detected.']
134
+ : topFindings.map(
135
+ (finding) =>
136
+ ` [${finding.severity}] ${finding.ruleId} -> ${finding.file}:${finding.line}`
137
+ )),
138
+ ];
139
+ };
140
+
105
141
  export const createConsumerRuntimeActions = (
106
142
  dependencies: ConsumerRuntimeActionDependencies
107
143
  ): ReadonlyArray<ConsumerAction> =>
108
144
  createConsumerLegacyMenuActions({
109
145
  runFullAudit: async () => {
146
+ dependencies.write(
147
+ '\n' +
148
+ [
149
+ 'Collecting source files...',
150
+ 'Running pattern checks...',
151
+ 'Running ESLint audits...',
152
+ '⚙️ AST Intelligence',
153
+ 'Running AST analysis...',
154
+ ].join('\n') +
155
+ '\n'
156
+ );
110
157
  await runConsumerRuntimePreflight(dependencies, 'PRE_COMMIT');
111
- renderSummaryAfterGate(dependencies, await dependencies.runRepoGate());
158
+ const summary = renderSummaryAfterGate(dependencies, await dependencies.runRepoGate());
159
+ dependencies.write(
160
+ `\n${buildFullAuditFinalSummaryLines(summary).join('\n')}\n`
161
+ );
112
162
  },
113
163
  runStrictRepoAndStaged: async () => {
114
164
  await runConsumerRuntimePreflight(dependencies, 'PRE_PUSH');
@@ -155,46 +155,123 @@ export const printPrePushTrackedEvidenceDiskHint = (params: {
155
155
 
156
156
  export const buildConsumerRuntimeBlockedSummary = (
157
157
  blocked: ConsumerRuntimeBlockedGate
158
- ): FrameworkMenuEvidenceSummary => ({
159
- status: 'ok',
160
- stage: blocked.stage,
161
- outcome: 'BLOCK',
162
- totalFindings: blocked.totalViolations,
163
- filesScanned: 0,
164
- filesAffected: 0,
165
- bySeverity: {
166
- CRITICAL: 0,
167
- ERROR: 1,
168
- WARN: 0,
169
- INFO: 0,
170
- },
171
- byEnterpriseSeverity: {
172
- CRITICAL: 0,
173
- HIGH: 1,
174
- MEDIUM: 0,
175
- LOW: 0,
176
- },
177
- topFiles: [
178
- {
179
- file: 'PROJECT_ROOT',
180
- count: 1,
181
- },
182
- ],
183
- topFileLocations: [
184
- {
185
- file: 'PROJECT_ROOT',
186
- line: 1,
158
+ ): FrameworkMenuEvidenceSummary => {
159
+ const normalizedCause = blocked.causeCode.toLowerCase();
160
+ const inferredPlatform = normalizedCause.includes('backend')
161
+ ? 'Backend'
162
+ : normalizedCause.includes('frontend') || normalizedCause.includes('web')
163
+ ? 'Frontend'
164
+ : normalizedCause.includes('ios')
165
+ ? 'iOS'
166
+ : normalizedCause.includes('android')
167
+ ? 'Android'
168
+ : null;
169
+ const blockingCauses = blocked.blockingCauses ?? [];
170
+ const topFindings =
171
+ blockingCauses.length > 0
172
+ ? blockingCauses.map((cause) => ({
173
+ severity: 'HIGH' as const,
174
+ ruleId: cause.ruleId ?? cause.code,
175
+ file: cause.file ?? 'PROJECT_ROOT',
176
+ line: 1,
177
+ }))
178
+ : [
179
+ {
180
+ severity: 'HIGH' as const,
181
+ ruleId: blocked.causeCode,
182
+ file: 'PROJECT_ROOT',
183
+ line: 1,
184
+ },
185
+ ];
186
+ const topFiles = new Map<string, number>();
187
+ for (const finding of topFindings) {
188
+ topFiles.set(finding.file, (topFiles.get(finding.file) ?? 0) + 1);
189
+ }
190
+ const platformCounts = new Map<string, number>();
191
+ const inferPlatform = (value: string): string | null => {
192
+ const normalized = value.toLowerCase();
193
+ if (normalized.includes('backend')) {
194
+ return 'Backend';
195
+ }
196
+ if (normalized.includes('frontend') || normalized.includes('/web/') || normalized.includes('web')) {
197
+ return 'Frontend';
198
+ }
199
+ if (normalized.includes('ios')) {
200
+ return 'iOS';
201
+ }
202
+ if (normalized.includes('android')) {
203
+ return 'Android';
204
+ }
205
+ return null;
206
+ };
207
+ if (blockingCauses.length > 0) {
208
+ for (const cause of blockingCauses) {
209
+ const platform = inferPlatform(`${cause.ruleId ?? cause.code} ${cause.file ?? ''}`);
210
+ if (platform) {
211
+ platformCounts.set(platform, (platformCounts.get(platform) ?? 0) + 1);
212
+ }
213
+ }
214
+ } else if (inferredPlatform) {
215
+ platformCounts.set(inferredPlatform, blocked.totalViolations);
216
+ }
217
+ return {
218
+ status: 'ok',
219
+ stage: blocked.stage,
220
+ outcome: 'BLOCK',
221
+ totalFindings: blocked.totalViolations,
222
+ filesScanned: 0,
223
+ filesAffected: 0,
224
+ bySeverity: {
225
+ CRITICAL: 0,
226
+ ERROR: 1,
227
+ WARN: 0,
228
+ INFO: 0,
187
229
  },
188
- ],
189
- topFindings: [
190
- {
191
- severity: 'HIGH',
192
- ruleId: blocked.causeCode,
193
- file: 'PROJECT_ROOT',
194
- line: 1,
230
+ byEnterpriseSeverity: {
231
+ CRITICAL: 0,
232
+ HIGH: 1,
233
+ MEDIUM: 0,
234
+ LOW: 0,
195
235
  },
196
- ],
197
- });
236
+ topFiles: [
237
+ ...topFiles.entries(),
238
+ ].map(([file, count]) => ({
239
+ file,
240
+ count,
241
+ })),
242
+ topFileLocations: topFindings.map((finding) => ({
243
+ file: finding.file,
244
+ line: finding.line,
245
+ })),
246
+ topFindings,
247
+ platformAuditRows: [...platformCounts.entries()].map(([platform, violations]) => ({
248
+ platform,
249
+ violations,
250
+ })),
251
+ };
252
+ };
253
+
254
+ export const buildConsumerRuntimeBlockedCauseLines = (
255
+ blocked: ConsumerRuntimeBlockedGate
256
+ ): string[] => {
257
+ const causes = blocked.blockingCauses ?? [];
258
+ if (causes.length === 0) {
259
+ return [
260
+ `🔴 Gate blocked: ${blocked.causeCode}`,
261
+ `Cause: ${blocked.causeMessage}`,
262
+ `Solution: ${blocked.remediation}`,
263
+ ];
264
+ }
265
+ return [
266
+ `🔴 Gate blocked: ${blocked.causeCode}`,
267
+ `Blocking causes: ${causes.length}`,
268
+ ...causes.flatMap((cause, index) => [
269
+ `${index + 1}. ${cause.ruleId ?? cause.code}${cause.file ? ` @ ${cause.file}` : ''}`,
270
+ ` Cause: ${cause.message}`,
271
+ ` Solution: ${cause.remediation ?? blocked.remediation}`,
272
+ ]),
273
+ ];
274
+ };
198
275
 
199
276
  export const printConsumerRuntimeEmptyScopeHint = (
200
277
  dependencies: Pick<ConsumerRuntimeSummaryDependencies, 'write'>,
@@ -18,6 +18,16 @@ import type {
18
18
  ConsumerRuntimeWrite,
19
19
  } from './framework-menu-consumer-runtime-types';
20
20
 
21
+ const PUMUKI_LEGACY_BANNER = [
22
+ '██████╗ ██╗ ██╗███╗ ███╗██╗ ██╗██╗ ██╗██╗',
23
+ '██╔══██╗██║ ██║████╗ ████║██║ ██║██║ ██╔╝██║',
24
+ '██████╔╝██║ ██║██╔████╔██║██║ ██║█████╔╝ ██║',
25
+ '██╔═══╝ ██║ ██║██║╚██╔╝██║██║ ██║██╔═██╗ ██║',
26
+ '██║ ╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║ ██╗██║',
27
+ '╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝',
28
+ ' 🐈 En memoria de Pumuki 💚',
29
+ ];
30
+
21
31
  const buildConsumerRuntimeMenuStatus = (
22
32
  menuSummary: FrameworkMenuEvidenceSummary
23
33
  ): { level: 'info' | 'block' | 'warn' | 'ok'; label: string } =>
@@ -35,17 +45,26 @@ export const renderConsumerRuntimeClassicMenu = (
35
45
  actions: ReadonlyArray<ConsumerAction>,
36
46
  useColor: () => boolean
37
47
  ): string => {
38
- const groupedActions = resolveConsumerMenuLayout(actions);
48
+ const actionById = new Map(actions.map((action) => [action.id, action]));
49
+ const label = (id: string): string => actionById.get(id)?.label ?? 'Unavailable';
39
50
  const lines = [
40
- 'PUMUKI',
51
+ ...PUMUKI_LEGACY_BANNER,
52
+ '',
41
53
  'Advanced Project Audit — AST Intelligence & Quality Gate',
42
- 'A. Switch to advanced menu',
43
54
  '',
44
- ...groupedActions.flatMap((group) => [
45
- group.title,
46
- ...group.items.map((item) => `${item.id}) ${item.action.label}`),
47
- '',
48
- ]),
55
+ `1) ${label('1')} 6) ${label('6')}`,
56
+ `2) ${label('2')} 7) ${label('7')}`,
57
+ `3) ${label('3')} 8) ${label('8')}`,
58
+ `4) ${label('4')} 9) ${label('9')}`,
59
+ `5) ${label('5')} 10) ${label('10')}`,
60
+ '',
61
+ 'Additional engine flows',
62
+ `11) ${label('11')}`,
63
+ `12) ${label('12')}`,
64
+ `13) ${label('13')}`,
65
+ `14) ${label('14')}`,
66
+ '',
67
+ 'A) Switch to advanced menu',
49
68
  ];
50
69
  return renderLegacyPanel(lines, {
51
70
  width: resolveLegacyPanelOuterWidth(),
@@ -68,7 +87,8 @@ export const renderConsumerRuntimeModernMenu = (
68
87
  });
69
88
  const groupedActions = resolveConsumerMenuLayout(params.actions);
70
89
  const lines = [
71
- 'PUMUKI',
90
+ ...PUMUKI_LEGACY_BANNER,
91
+ '',
72
92
  'Advanced Project Audit — AST Intelligence & Quality Gate',
73
93
  `Status: ${renderBadge(menuStatus.label, menuStatus.level, tokens)}`,
74
94
  'A. Switch to advanced menu',
@@ -1,5 +1,6 @@
1
1
  import type { FrameworkMenuEvidenceSummary } from './framework-menu-evidence-summary-lib';
2
2
  import type {
3
+ PumukiBlockedNotificationCause,
3
4
  PumukiCriticalNotificationEvent,
4
5
  SystemNotificationEmitResult,
5
6
  } from './framework-menu-system-notifications-lib';
@@ -16,6 +17,7 @@ export type ConsumerRuntimeBlockedGate = {
16
17
  causeCode: string;
17
18
  causeMessage: string;
18
19
  remediation: string;
20
+ blockingCauses?: ReadonlyArray<PumukiBlockedNotificationCause>;
19
21
  };
20
22
 
21
23
  export type ConsumerRuntimeGateResult = {
@@ -239,6 +239,7 @@ const runMenuAuditGateWithBlocked = async (
239
239
  causeCode: params.causeCode,
240
240
  causeMessage: params.causeMessage,
241
241
  remediation: params.remediation,
242
+ blockingCauses: params.blockingCauses,
242
243
  };
243
244
  return {
244
245
  delivered: false,
@@ -273,6 +274,7 @@ const runRepoAndStagedPrePushGateSilentWithDependencies = async (
273
274
  causeCode: params.causeCode,
274
275
  causeMessage: params.causeMessage,
275
276
  remediation: params.remediation,
277
+ blockingCauses: params.blockingCauses,
276
278
  };
277
279
  },
278
280
  writeHookGateSummary: () => {},
@@ -3,7 +3,7 @@ import type { MenuLayoutGroup } from './framework-menu-layout-types';
3
3
  export const CONSUMER_MENU_LAYOUT: ReadonlyArray<MenuLayoutGroup> = [
4
4
  {
5
5
  key: 'read-only-gates',
6
- title: 'Legacy Audit Flows',
6
+ title: 'Classic audit flows',
7
7
  itemIds: ['1', '2', '3', '4'],
8
8
  },
9
9
  {
@@ -13,12 +13,12 @@ export const CONSUMER_MENU_LAYOUT: ReadonlyArray<MenuLayoutGroup> = [
13
13
  },
14
14
  {
15
15
  key: 'export',
16
- title: 'Legacy Export',
16
+ title: 'Classic export',
17
17
  itemIds: ['8'],
18
18
  },
19
19
  {
20
20
  key: 'legacy-read-only-diagnostics',
21
- title: 'Legacy Diagnostics',
21
+ title: 'Classic diagnostics',
22
22
  itemIds: ['5', '6', '7', '9'],
23
23
  },
24
24
  {
@@ -118,7 +118,8 @@ const buildBlockedCausesSummary = (
118
118
  }
119
119
  const file = first.file ? ` en ${first.file}` : '';
120
120
  const rule = first.ruleId ?? first.code;
121
- const suffix = selectedCauses.length > 1 ? ` (+${selectedCauses.length - 1} más)` : '';
121
+ const remainingCauses = Math.max(0, causes.length - 1);
122
+ const suffix = remainingCauses > 0 ? ` (+${remainingCauses} más)` : '';
122
123
  return `Violación ${rule}${file}${suffix}.`;
123
124
  };
124
125
 
@@ -17,14 +17,18 @@ const buildBlockingCausesDetails = (
17
17
  if (!causes || causes.length === 0) {
18
18
  return null;
19
19
  }
20
+ const total = causes.length;
21
+ const header = total === 1
22
+ ? 'Causas bloqueantes: 1'
23
+ : `Causas bloqueantes: ${total}`;
20
24
  return causes
21
- .slice(0, 6)
22
25
  .map((cause, index) => {
23
26
  const rule = cause.ruleId ?? cause.code;
24
27
  const file = cause.file ? ` · ${cause.file}` : '';
25
28
  const fix = cause.remediation ? ` · Solución: ${cause.remediation}` : '';
26
29
  return `${index + 1}. ${rule}${file}. ${cause.message}${fix}`;
27
30
  })
31
+ .reduce((lines, line) => [...lines, line], [header])
28
32
  .join('\n');
29
33
  };
30
34
 
@@ -41,6 +41,9 @@ const BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT: Readonly<Record<BlockedRemediat
41
41
  const GENERIC_BLOCKED_REMEDIATION =
42
42
  'Corrige el bloqueo indicado y vuelve a ejecutar el comando.';
43
43
 
44
+ const buildMultipleCausesRemediation = (count: number): string =>
45
+ `Corrige las ${count} causas bloqueantes listadas arriba, empezando por violaciones AST/skills de codigo cuando existan, y vuelve a ejecutar el gate.`;
46
+
44
47
  const normalizeBlockedRemediation = (value: string): string =>
45
48
  normalizeNotificationText(value)
46
49
  .replace(/^cómo solucionarlo:\s*/i, '')
@@ -102,6 +105,13 @@ export const resolveBlockedRemediation = (
102
105
  ): string => {
103
106
  const variant = options?.variant ?? 'dialog';
104
107
  const maxLength = BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT[variant];
108
+ const blockingCausesCount = event.blockingCauses?.length ?? 0;
109
+ if (blockingCausesCount > 1) {
110
+ return truncateNotificationText(
111
+ buildMultipleCausesRemediation(blockingCausesCount),
112
+ maxLength
113
+ );
114
+ }
105
115
  const firstCauseWithRemediation = event.blockingCauses?.find(
106
116
  (cause) => cause.remediation && cause.remediation.trim().length > 0
107
117
  );
@@ -103,7 +103,8 @@ const menu = async (): Promise<void> => {
103
103
  }
104
104
  }
105
105
  }
106
- const option = (await rl.question('\nSelect option: ')).trim();
106
+ const prompt = mode === 'consumer' ? '\nChoose an option: ' : '\nSelect option: ';
107
+ const option = (await rl.question(prompt)).trim();
107
108
  const normalized = option.toUpperCase();
108
109
 
109
110
  if (mode === 'consumer' && normalized === 'A') {