pumuki 6.3.273 → 6.3.274

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.274] - 2026-05-18
4
+
5
+ - Notifications: gate blocks now propagate every blocking cause as `blockingCauses[]`, emit one `[pumuki][blocked-cause]` line per violation with rule, file, reason and fix, and show rule/file/remediation in the macOS dialog without hiding AST skill violations behind tracking or governance.
6
+
3
7
  ## [6.3.273] - 2026-05-18
4
8
 
5
9
  - Fix `PUMUKI-INC-142` residual hook communication: when PRE_WRITE cannot issue a lease because real gate findings exist, PRE_COMMIT/PRE_PUSH keep failing closed but prioritize the real blocking finding before the lease symptom.
@@ -1461,6 +1461,22 @@ export const hasSwiftNavigationViewUsage = (source: string): boolean => {
1461
1461
  });
1462
1462
  };
1463
1463
 
1464
+ export const hasSwiftNSLayoutConstraintUsage = (source: string): boolean => {
1465
+ return scanCodeLikeSource(source, ({ source: swiftSource, index, current }) => {
1466
+ if (current !== 'N') {
1467
+ return false;
1468
+ }
1469
+
1470
+ return (
1471
+ hasIdentifierAt(swiftSource, index, 'NSLayoutConstraint') ||
1472
+ hasIdentifierAt(swiftSource, index, 'NSLayoutAnchor') ||
1473
+ hasIdentifierAt(swiftSource, index, 'NSLayoutXAxisAnchor') ||
1474
+ hasIdentifierAt(swiftSource, index, 'NSLayoutYAxisAnchor') ||
1475
+ hasIdentifierAt(swiftSource, index, 'NSLayoutDimension')
1476
+ );
1477
+ });
1478
+ };
1479
+
1464
1480
  export const hasSwiftUntypedNavigationLinkDestinationUsage = (source: string): boolean => {
1465
1481
  const swiftSource = sanitizeSwiftSourceForMultilineRegex(source);
1466
1482
  const destinationParameterPattern = /\bNavigationLink\s*\([^)]*\bdestination\s*:/;
@@ -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.274)
8
+
9
+ - Notifications now carry all blocking causes instead of a single collapsed cause. Hooks and audit output emit `[pumuki][blocked-cause]` for each violation with code, rule, file, reason and fix; macOS blocked dialogs show rule/file/solution details and no longer hide AST skill violations behind tracking/governance blockers.
10
+
7
11
  ### 2026-05-18 (v6.3.272)
8
12
 
9
13
  - `PUMUKI-INC-142`: `sdd validate --stage=PRE_WRITE` refreshes evidence with PRE_WRITE semantics before writing the validated-diff lease, avoiding the PRE_COMMIT/PRE_PUSH bootstrap loop that still produced `ENFORCEMENT_GAP_PRE_WRITE_LEASE_MISSING` in real RuralGo commits.
@@ -833,15 +833,53 @@ const shouldBlockFromFinding = (finding: Finding | undefined): boolean => {
833
833
  return finding.blocking !== false;
834
834
  };
835
835
 
836
+ const isAstRuleFinding = (finding: Finding): boolean => {
837
+ const ruleId = finding.ruleId.toLowerCase();
838
+ const source = finding.source?.toLowerCase() ?? '';
839
+ const matchedBy = finding.matchedBy?.toLowerCase() ?? '';
840
+ if (ruleId.startsWith('governance.')) {
841
+ return false;
842
+ }
843
+ return (
844
+ ruleId.startsWith('skills.') ||
845
+ ruleId.startsWith('heuristics.') ||
846
+ source.includes('heuristic') ||
847
+ matchedBy.includes('heuristic') ||
848
+ matchedBy.includes('ast')
849
+ );
850
+ };
851
+
836
852
  const resolvePrimaryBlockingFinding = (
837
853
  findings: ReadonlyArray<Finding>
838
854
  ): Finding | undefined => (
855
+ findings.find(
856
+ (finding) =>
857
+ shouldBlockFromFinding(finding) &&
858
+ isAstRuleFinding(finding) &&
859
+ finding.severity === 'CRITICAL'
860
+ ) ??
861
+ findings.find(
862
+ (finding) =>
863
+ shouldBlockFromFinding(finding) &&
864
+ isAstRuleFinding(finding) &&
865
+ finding.severity === 'ERROR'
866
+ ) ??
867
+ findings.find((finding) => shouldBlockFromFinding(finding) && isAstRuleFinding(finding)) ??
839
868
  findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'CRITICAL') ??
840
869
  findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'ERROR') ??
841
870
  findings.find((finding) => shouldBlockFromFinding(finding)) ??
842
871
  findings[0]
843
872
  );
844
873
 
874
+ const resolveBlockingNotificationFindings = (
875
+ findings: ReadonlyArray<Finding>
876
+ ): ReadonlyArray<Finding> => {
877
+ const blockingFindings = findings.filter((finding) => shouldBlockFromFinding(finding));
878
+ const astFindings = blockingFindings.filter(isAstRuleFinding);
879
+ const nonAstFindings = blockingFindings.filter((finding) => !isAstRuleFinding(finding));
880
+ return [...astFindings, ...nonAstFindings];
881
+ };
882
+
845
883
  const applySkillsFindingEnforcement = (
846
884
  finding: Finding | undefined
847
885
  ): Finding | undefined => {
@@ -1477,7 +1515,9 @@ export async function runPlatformGate(params: {
1477
1515
  });
1478
1516
 
1479
1517
  if (gateOutcome === 'BLOCK') {
1480
- const primaryBlockingFinding = resolvePrimaryBlockingFinding(findingsWithWaiver);
1518
+ const notificationFindings = resolveBlockingNotificationFindings(findingsWithWaiver);
1519
+ const primaryBlockingFinding =
1520
+ notificationFindings[0] ?? resolvePrimaryBlockingFinding(findingsWithWaiver);
1481
1521
  if (
1482
1522
  primaryBlockingFinding &&
1483
1523
  (
@@ -1495,11 +1535,27 @@ export async function runPlatformGate(params: {
1495
1535
  remediation:
1496
1536
  primaryBlockingFinding.expected_fix ??
1497
1537
  'Corrige la causa bloqueante y reejecuta el gate.',
1538
+ blockingCauses: notificationFindings.map((finding) => ({
1539
+ code: finding.code ?? finding.ruleId,
1540
+ message: finding.message,
1541
+ ruleId: finding.ruleId,
1542
+ file: finding.filePath,
1543
+ remediation: finding.expected_fix,
1544
+ })),
1498
1545
  });
1499
1546
  process.stderr.write(
1500
1547
  `[pumuki][blocked] code=${primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId} ` +
1501
1548
  `stage=${params.policy.stage} reason=${primaryBlockingFinding.message}\n`
1502
1549
  );
1550
+ for (const finding of notificationFindings) {
1551
+ process.stderr.write(
1552
+ `[pumuki][blocked-cause] code=${finding.code ?? finding.ruleId}` +
1553
+ ` rule=${finding.ruleId}` +
1554
+ ` file=${finding.filePath ?? 'n/a'}` +
1555
+ ` reason=${finding.message}` +
1556
+ ` fix=${finding.expected_fix ?? 'Corrige la causa bloqueante y reejecuta el gate.'}\n`
1557
+ );
1558
+ }
1503
1559
  process.stderr.write(
1504
1560
  `[pumuki][notification] delivered=${notificationResult.delivered ? 'yes' : 'no'} ` +
1505
1561
  `reason=${notificationResult.reason}\n`
@@ -124,6 +124,13 @@ type StageRunnerDependencies = {
124
124
  causeCode: string;
125
125
  causeMessage: string;
126
126
  remediation: string;
127
+ blockingCauses?: ReadonlyArray<{
128
+ code: string;
129
+ message: string;
130
+ ruleId?: string;
131
+ file?: string;
132
+ remediation?: string;
133
+ }>;
127
134
  }) => void;
128
135
  readEvidenceResult: (repoRoot: string) => EvidenceReadResult;
129
136
  readEvidence: typeof readEvidence;
@@ -262,6 +269,22 @@ const notifyAuditSummaryForStage = (
262
269
  const resolvePrimaryBlockedStageFinding = (
263
270
  findings: ReadonlyArray<SnapshotFinding>
264
271
  ): SnapshotFinding | undefined => (
272
+ findings.find((finding) => {
273
+ const ruleId = finding.ruleId.toLowerCase();
274
+ const source = finding.source?.toLowerCase() ?? '';
275
+ const matchedBy = finding.matchedBy?.toLowerCase() ?? '';
276
+ return (
277
+ isSeverityAtLeast(finding.severity, 'ERROR') &&
278
+ !ruleId.startsWith('governance.') &&
279
+ (
280
+ ruleId.startsWith('skills.') ||
281
+ ruleId.startsWith('heuristics.') ||
282
+ source.includes('heuristic') ||
283
+ matchedBy.includes('ast') ||
284
+ matchedBy.includes('heuristic')
285
+ )
286
+ );
287
+ }) ??
265
288
  findings.find((finding) => {
266
289
  return isSeverityAtLeast(finding.severity, 'ERROR');
267
290
  }) ?? findings[0]
@@ -288,6 +311,13 @@ const notifyGateBlockedForStage = (params: {
288
311
  BLOCKED_REMEDIATION_BY_CODE[causeCode]
289
312
  ?? params.fallbackRemediation
290
313
  ?? DEFAULT_BLOCKED_REMEDIATION;
314
+ const blockingCauses = stageFindings.map((finding) => ({
315
+ code: finding.code ?? finding.ruleId,
316
+ message: finding.message,
317
+ ruleId: finding.ruleId,
318
+ file: finding.file,
319
+ remediation: finding.expected_fix,
320
+ }));
291
321
  params.dependencies.notifyGateBlocked({
292
322
  repoRoot,
293
323
  stage: params.stage,
@@ -297,6 +327,7 @@ const notifyGateBlockedForStage = (params: {
297
327
  causeCode,
298
328
  causeMessage,
299
329
  remediation,
330
+ blockingCauses,
300
331
  });
301
332
  };
302
333
 
@@ -159,6 +159,13 @@ export const emitGateBlockedNotification = (
159
159
  causeCode: string;
160
160
  causeMessage: string;
161
161
  remediation: string;
162
+ blockingCauses?: ReadonlyArray<{
163
+ code: string;
164
+ message: string;
165
+ ruleId?: string;
166
+ file?: string;
167
+ remediation?: string;
168
+ }>;
162
169
  },
163
170
  dependencies: Partial<AuditSummaryNotificationDependencies> = {}
164
171
  ): SystemNotificationEmitResult => {
@@ -177,6 +184,7 @@ export const emitGateBlockedNotification = (
177
184
  causeMessage: params.causeMessage,
178
185
  }),
179
186
  remediation: params.remediation,
187
+ blockingCauses: params.blockingCauses,
180
188
  },
181
189
  repoRoot: params.repoRoot,
182
190
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.273",
3
+ "version": "6.3.274",
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": {
@@ -77,6 +77,51 @@ const resolvePriorityCauseFromMessage = (message?: string): string | null => {
77
77
  const isCauseCodePriorityOverTracking = (causeCode: string): boolean =>
78
78
  causeCode === 'CONTEXT_NOT_APPLIED' || causeCode === 'CONTEXT_INVALID';
79
79
 
80
+ const isAstBlockedCauseCode = (causeCode: string): boolean => {
81
+ const normalized = causeCode.toLowerCase();
82
+ return (
83
+ normalized.startsWith('skills_') ||
84
+ normalized.startsWith('skills.') ||
85
+ normalized.startsWith('heuristics_') ||
86
+ normalized.startsWith('heuristics.') ||
87
+ normalized.includes('_ast') ||
88
+ normalized.includes('solid') ||
89
+ normalized.includes('no_') ||
90
+ normalized.includes('avoid_')
91
+ );
92
+ };
93
+
94
+ const isAstBlockedCause = (
95
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
96
+ ): boolean => {
97
+ const ruleId = cause.ruleId?.toLowerCase() ?? '';
98
+ const code = cause.code.toLowerCase();
99
+ return (
100
+ ruleId.startsWith('skills.') ||
101
+ ruleId.startsWith('heuristics.') ||
102
+ isAstBlockedCauseCode(code)
103
+ );
104
+ };
105
+
106
+ const buildBlockedCausesSummary = (
107
+ event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>
108
+ ): string | null => {
109
+ const causes = event.blockingCauses ?? [];
110
+ if (causes.length === 0) {
111
+ return null;
112
+ }
113
+ const astCauses = causes.filter(isAstBlockedCause);
114
+ const selectedCauses = astCauses.length > 0 ? astCauses : causes;
115
+ const first = selectedCauses[0];
116
+ if (!first) {
117
+ return null;
118
+ }
119
+ const file = first.file ? ` en ${first.file}` : '';
120
+ const rule = first.ruleId ?? first.code;
121
+ const suffix = selectedCauses.length > 1 ? ` (+${selectedCauses.length - 1} más)` : '';
122
+ return `Violación ${rule}${file}${suffix}.`;
123
+ };
124
+
80
125
  const buildGenericSpanishBlockedCauseSummary = (
81
126
  event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>,
82
127
  causeCode: string
@@ -120,6 +165,10 @@ export const resolveBlockedCauseSummary = (
120
165
  event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>,
121
166
  causeCode: string
122
167
  ): string => {
168
+ const blockedCausesSummary = buildBlockedCausesSummary(event);
169
+ if (blockedCausesSummary) {
170
+ return truncateNotificationText(blockedCausesSummary, 72);
171
+ }
123
172
  const trackingContext = extractNotificationTrackingContext(event.causeMessage);
124
173
  const priorityCode = resolvePriorityCauseFromMessage(event.causeMessage);
125
174
  if (priorityCode) {
@@ -1,5 +1,13 @@
1
1
  export type PumukiNotificationStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI' | 'PRE_WRITE';
2
2
 
3
+ export type PumukiBlockedNotificationCause = {
4
+ code: string;
5
+ message: string;
6
+ ruleId?: string;
7
+ file?: string;
8
+ remediation?: string;
9
+ };
10
+
3
11
  export type PumukiCriticalNotificationEvent =
4
12
  | {
5
13
  kind: 'audit.summary';
@@ -14,6 +22,7 @@ export type PumukiCriticalNotificationEvent =
14
22
  causeCode?: string;
15
23
  causeMessage?: string;
16
24
  remediation?: string;
25
+ blockingCauses?: ReadonlyArray<PumukiBlockedNotificationCause>;
17
26
  }
18
27
  | {
19
28
  kind: 'evidence.stale';
@@ -11,13 +11,31 @@ export type BlockedDialogPayload = {
11
11
  remediation: string;
12
12
  };
13
13
 
14
+ const buildBlockingCausesDetails = (
15
+ causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
16
+ ): string | null => {
17
+ if (!causes || causes.length === 0) {
18
+ return null;
19
+ }
20
+ return causes
21
+ .slice(0, 6)
22
+ .map((cause, index) => {
23
+ const rule = cause.ruleId ?? cause.code;
24
+ const file = cause.file ? ` · ${cause.file}` : '';
25
+ const fix = cause.remediation ? ` · Solución: ${cause.remediation}` : '';
26
+ return `${index + 1}. ${rule}${file}. ${cause.message}${fix}`;
27
+ })
28
+ .join('\n');
29
+ };
30
+
14
31
  export const buildBlockedDialogPayload = (params: {
15
32
  event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>;
16
33
  repoRoot: string;
17
34
  env: NodeJS.ProcessEnv;
18
35
  }): BlockedDialogPayload => {
19
36
  const causeCode = params.event.causeCode ?? 'GATE_BLOCKED';
20
- const cause = resolveBlockedCauseSummary(params.event, causeCode);
37
+ const cause = buildBlockingCausesDetails(params.event.blockingCauses)
38
+ ?? resolveBlockedCauseSummary(params.event, causeCode);
21
39
  const remediation = resolveBlockedRemediation(params.event, causeCode);
22
40
  const projectLabel = resolveProjectLabel({
23
41
  repoRoot: params.repoRoot,
@@ -102,6 +102,15 @@ export const resolveBlockedRemediation = (
102
102
  ): string => {
103
103
  const variant = options?.variant ?? 'dialog';
104
104
  const maxLength = BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT[variant];
105
+ const firstCauseWithRemediation = event.blockingCauses?.find(
106
+ (cause) => cause.remediation && cause.remediation.trim().length > 0
107
+ );
108
+ if (firstCauseWithRemediation?.remediation) {
109
+ return truncateNotificationText(
110
+ normalizeBlockedRemediation(firstCauseWithRemediation.remediation),
111
+ maxLength
112
+ );
113
+ }
105
114
  const fromEvent = event.remediation
106
115
  ? normalizeBlockedRemediation(event.remediation)
107
116
  : '';