pumuki 6.3.275 → 6.3.277

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,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.3.277] - 2026-05-18
4
+
5
+ - `PUMUKI-INC-146`: PRE_WRITE now resolves the effective iOS test-quality scope from staged paths first when a staged slice exists, falling back to the worktree only when there is no staged code. This prevents unstaged Swift test files from forcing `skills.ios.critical-test-quality` onto unrelated staged iOS/SwiftUI production commits, while preserving the hard block for staged Swift test slices.
6
+
7
+ ## [6.3.276] - 2026-05-18
8
+
9
+ - `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.
10
+
3
11
  ## [6.3.275] - 2026-05-18
4
12
 
5
13
  - 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.277
@@ -4,6 +4,14 @@ 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.277)
8
+
9
+ - `PUMUKI-INC-146`: PRE_WRITE uses staged paths as the effective slice for `skills.ios.critical-test-quality` when staged files exist. Unstaged Swift tests no longer contaminate a staged production-only iOS commit, but staged Swift tests still require the critical test-quality rule and remain fail-closed.
10
+
11
+ ### 2026-05-18 (v6.3.276)
12
+
13
+ - `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.
14
+
7
15
  ### 2026-05-18 (v6.3.275)
8
16
 
9
17
  - `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,69 @@ const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> =>
401
401
  }
402
402
  };
403
403
 
404
+ const collectStagedChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
405
+ try {
406
+ const output = execFileSync(
407
+ 'git',
408
+ ['diff', '--cached', '--name-only'],
409
+ {
410
+ cwd: repoRoot,
411
+ encoding: 'utf8',
412
+ stdio: ['ignore', 'pipe', 'ignore'],
413
+ }
414
+ );
415
+ const files = output
416
+ .split('\n')
417
+ .map((line) => normalizeChangedPath(line))
418
+ .filter((line) => line.length > 0);
419
+ return [...new Set(files)];
420
+ } catch {
421
+ return [];
422
+ }
423
+ };
424
+
425
+ const collectPreWriteEffectiveChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
426
+ const stagedPaths = collectStagedChangedPaths(repoRoot);
427
+ if (stagedPaths.length > 0) {
428
+ return stagedPaths;
429
+ }
430
+ return collectWorktreeChangedPaths(repoRoot);
431
+ };
432
+
433
+ const isIosTestQualityPath = (filePath: string): boolean => {
434
+ const normalized = normalizeChangedPath(filePath).toLowerCase();
435
+ if (!normalized.endsWith('.swift')) {
436
+ return false;
437
+ }
438
+ return normalized.includes('/tests/')
439
+ || normalized.includes('/uitests/')
440
+ || normalized.endsWith('test.swift')
441
+ || normalized.endsWith('tests.swift')
442
+ || normalized.endsWith('.spec.swift');
443
+ };
444
+
445
+ const hasPreWriteIosTestQualityScope = (repoRoot: string): boolean =>
446
+ collectPreWriteEffectiveChangedPaths(repoRoot).some((filePath) =>
447
+ isIosTestQualityPath(filePath)
448
+ );
449
+
450
+ const resolvePreWriteCriticalSkillsRules = (params: {
451
+ platform: PreWriteSkillsPlatform;
452
+ stage: AiGateStage;
453
+ repoRoot: string;
454
+ }): ReadonlyArray<string> => {
455
+ const ruleIds = PREWRITE_CRITICAL_SKILLS_RULES[params.platform];
456
+ if (
457
+ params.platform === 'ios'
458
+ && params.stage === 'PRE_WRITE'
459
+ && ruleIds.includes('skills.ios.critical-test-quality')
460
+ && !hasPreWriteIosTestQualityScope(params.repoRoot)
461
+ ) {
462
+ return ruleIds.filter((ruleId) => ruleId !== 'skills.ios.critical-test-quality');
463
+ }
464
+ return ruleIds;
465
+ };
466
+
404
467
  const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
405
468
  const normalized = normalizeChangedPath(filePath).toLowerCase();
406
469
  if (platform === 'ios') {
@@ -552,6 +615,7 @@ const collectActiveRuleIdsCoverageViolations = (params: {
552
615
  };
553
616
 
554
617
  const collectPreWritePlatformSkillsViolations = (params: {
618
+ repoRoot: string;
555
619
  evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
556
620
  coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
557
621
  skillsEnforcement: SkillsEnforcementResolution;
@@ -625,7 +689,11 @@ const collectPreWritePlatformSkillsViolations = (params: {
625
689
 
626
690
  const missingCriticalRulesByPlatform: string[] = [];
627
691
  for (const platform of detectedPlatforms) {
628
- const requiredCriticalRuleIds = PREWRITE_CRITICAL_SKILLS_RULES[platform];
692
+ const requiredCriticalRuleIds = resolvePreWriteCriticalSkillsRules({
693
+ platform,
694
+ stage: 'PRE_WRITE',
695
+ repoRoot: params.repoRoot,
696
+ });
629
697
  if (requiredCriticalRuleIds.length === 0) {
630
698
  continue;
631
699
  }
@@ -716,14 +784,22 @@ const toSkillsContractAssessment = (params: {
716
784
  platform,
717
785
  required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
718
786
  required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
719
- required_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
787
+ required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
788
+ platform,
789
+ stage: params.stage,
790
+ repoRoot: params.repoRoot,
791
+ }),
720
792
  required_any_transversal_critical_rule_ids: [
721
793
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
722
794
  ],
723
795
  active_prefix_covered: false,
724
796
  evaluated_prefix_covered: false,
725
797
  missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
726
- missing_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
798
+ missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
799
+ platform,
800
+ stage: params.stage,
801
+ repoRoot: params.repoRoot,
802
+ }),
727
803
  transversal_critical_covered: false,
728
804
  missing_any_transversal_critical_rule_ids: [
729
805
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
@@ -798,14 +874,22 @@ const toSkillsContractAssessment = (params: {
798
874
  platform,
799
875
  required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
800
876
  required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
801
- required_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
877
+ required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
878
+ platform,
879
+ stage: params.stage,
880
+ repoRoot: params.repoRoot,
881
+ }),
802
882
  required_any_transversal_critical_rule_ids: [
803
883
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
804
884
  ],
805
885
  active_prefix_covered: false,
806
886
  evaluated_prefix_covered: false,
807
887
  missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
808
- missing_critical_rule_ids: [...PREWRITE_CRITICAL_SKILLS_RULES[platform]],
888
+ missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
889
+ platform,
890
+ stage: params.stage,
891
+ repoRoot: params.repoRoot,
892
+ }),
809
893
  transversal_critical_covered: false,
810
894
  missing_any_transversal_critical_rule_ids: [
811
895
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
@@ -859,7 +943,13 @@ const toSkillsContractAssessment = (params: {
859
943
  for (const platform of assessmentPlatforms) {
860
944
  const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
861
945
  const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
862
- const requiredCriticalRuleIds = [...PREWRITE_CRITICAL_SKILLS_RULES[platform]];
946
+ const requiredCriticalRuleIds = [
947
+ ...resolvePreWriteCriticalSkillsRules({
948
+ platform,
949
+ stage: params.stage,
950
+ repoRoot: params.repoRoot,
951
+ }),
952
+ ];
863
953
  const requiredAnyTransversalCriticalRuleIds = [
864
954
  ...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
865
955
  ];
@@ -1057,6 +1147,7 @@ const collectPreWriteCoherenceViolations = (params: {
1057
1147
 
1058
1148
  violations.push(
1059
1149
  ...collectPreWritePlatformSkillsViolations({
1150
+ repoRoot: params.repoRoot,
1060
1151
  evidence: params.evidence,
1061
1152
  coverage,
1062
1153
  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.277",
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') {