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 +4 -0
- package/core/facts/detectors/text/ios.ts +16 -0
- package/docs/operations/RELEASE_NOTES.md +4 -0
- package/integrations/git/runPlatformGate.ts +57 -1
- package/integrations/git/stageRunners.ts +31 -0
- package/integrations/notifications/emitAuditSummaryNotification.ts +8 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-cause.ts +49 -0
- package/scripts/framework-menu-system-notifications-event-types.ts +9 -0
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +19 -1
- package/scripts/framework-menu-system-notifications-remediation.ts +9 -0
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
|
|
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.
|
|
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 =
|
|
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
|
: '';
|