pumuki 6.3.313 → 6.3.315
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/integrations/notifications/emitAuditSummaryNotification.ts +49 -0
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +36 -3
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +23 -1
- package/scripts/framework-menu-system-notifications-payloads-audit.ts +8 -8
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { AiEvidenceV2_1 } from '../evidence/schema';
|
|
2
2
|
import { readEvidence } from '../evidence/readEvidence';
|
|
3
|
+
import {
|
|
4
|
+
extractEvidenceBlockingCauses,
|
|
5
|
+
formatEvidenceBlockingCause,
|
|
6
|
+
} from '../evidence/blockingCauses';
|
|
3
7
|
import type { AiGateCheckResult } from '../gate/evaluateAiGate';
|
|
4
8
|
import { appendTrackingActionableContext } from '../git/aiGateRepoPolicyFindings';
|
|
5
9
|
import {
|
|
@@ -51,6 +55,13 @@ const withTrackingContext = (params: {
|
|
|
51
55
|
});
|
|
52
56
|
};
|
|
53
57
|
|
|
58
|
+
const normalizeNotificationStage = (stage: string): PumukiNotificationStage => {
|
|
59
|
+
if (stage === 'PRE_WRITE' || stage === 'PRE_COMMIT' || stage === 'PRE_PUSH' || stage === 'CI') {
|
|
60
|
+
return stage;
|
|
61
|
+
}
|
|
62
|
+
return 'PRE_COMMIT';
|
|
63
|
+
};
|
|
64
|
+
|
|
54
65
|
export const shouldEmitAuditSummaryNotificationForStage = (
|
|
55
66
|
stage: AuditSummaryNotificationStage,
|
|
56
67
|
env: NodeJS.ProcessEnv = process.env
|
|
@@ -66,6 +77,25 @@ export const toAuditSummaryEventFromEvidence = (
|
|
|
66
77
|
): PumukiCriticalNotificationEvent => {
|
|
67
78
|
const enterpriseSeverity = evidence.severity_metrics.by_enterprise_severity;
|
|
68
79
|
const severity = evidence.severity_metrics.by_severity;
|
|
80
|
+
const blockingCauses = extractEvidenceBlockingCauses(evidence);
|
|
81
|
+
if (blockingCauses.length > 0) {
|
|
82
|
+
const primaryCause = blockingCauses[0]!;
|
|
83
|
+
return {
|
|
84
|
+
kind: 'gate.blocked',
|
|
85
|
+
stage: normalizeNotificationStage(evidence.snapshot.stage),
|
|
86
|
+
totalViolations: evidence.severity_metrics.total_violations,
|
|
87
|
+
causeCode: primaryCause.code,
|
|
88
|
+
causeMessage: formatEvidenceBlockingCause(primaryCause),
|
|
89
|
+
remediation: primaryCause.remediation ?? 'Corrige la violación indicada y vuelve a intentar el commit.',
|
|
90
|
+
blockingCauses: blockingCauses.map((cause) => ({
|
|
91
|
+
code: cause.code,
|
|
92
|
+
ruleId: cause.ruleId,
|
|
93
|
+
file: cause.file,
|
|
94
|
+
message: formatEvidenceBlockingCause(cause),
|
|
95
|
+
remediation: cause.remediation,
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
69
99
|
return {
|
|
70
100
|
kind: 'audit.summary',
|
|
71
101
|
totalViolations: evidence.severity_metrics.total_violations,
|
|
@@ -76,6 +106,7 @@ export const toAuditSummaryEventFromEvidence = (
|
|
|
76
106
|
|
|
77
107
|
export const toAuditSummaryEventFromAiGate = (params: {
|
|
78
108
|
aiGateResult: Pick<AiGateCheckResult, 'violations'>;
|
|
109
|
+
stage?: PumukiNotificationStage;
|
|
79
110
|
}): PumukiCriticalNotificationEvent => {
|
|
80
111
|
const criticalViolations = params.aiGateResult.violations.reduce(
|
|
81
112
|
(total, violation) => (violation.severity === 'ERROR' ? total + 1 : total),
|
|
@@ -85,6 +116,23 @@ export const toAuditSummaryEventFromAiGate = (params: {
|
|
|
85
116
|
(total, violation) => (violation.severity === 'WARN' ? total + 1 : total),
|
|
86
117
|
0
|
|
87
118
|
);
|
|
119
|
+
if (params.aiGateResult.violations.length > 0) {
|
|
120
|
+
const primaryViolation = params.aiGateResult.violations[0]!;
|
|
121
|
+
return {
|
|
122
|
+
kind: 'gate.blocked',
|
|
123
|
+
stage: params.stage ?? 'PRE_WRITE',
|
|
124
|
+
totalViolations: params.aiGateResult.violations.length,
|
|
125
|
+
causeCode: primaryViolation.code,
|
|
126
|
+
causeMessage: primaryViolation.message,
|
|
127
|
+
remediation: 'Corrige la violación indicada y vuelve a intentar el commit.',
|
|
128
|
+
blockingCauses: params.aiGateResult.violations.map((violation) => ({
|
|
129
|
+
code: violation.code,
|
|
130
|
+
ruleId: violation.code,
|
|
131
|
+
message: violation.message,
|
|
132
|
+
remediation: 'Corrige la violación indicada y vuelve a intentar el commit.',
|
|
133
|
+
})),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
88
136
|
return {
|
|
89
137
|
kind: 'audit.summary',
|
|
90
138
|
totalViolations: params.aiGateResult.violations.length,
|
|
@@ -144,6 +192,7 @@ export const emitAuditSummaryNotificationFromAiGate = (
|
|
|
144
192
|
}
|
|
145
193
|
const event = toAuditSummaryEventFromAiGate({
|
|
146
194
|
aiGateResult: params.aiGateResult,
|
|
195
|
+
stage: params.stage,
|
|
147
196
|
});
|
|
148
197
|
return activeDependencies.emitSystemNotification({
|
|
149
198
|
event,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.315",
|
|
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": {
|
|
@@ -60,6 +60,32 @@ const stripTechnicalFieldsFromMessage = (message: string): string => {
|
|
|
60
60
|
.trim();
|
|
61
61
|
};
|
|
62
62
|
|
|
63
|
+
const localizeDeveloperText = (value: string): string => {
|
|
64
|
+
const normalized = normalizeNotificationText(value);
|
|
65
|
+
const knownTexts: ReadonlyArray<[RegExp, string]> = [
|
|
66
|
+
[/Disallow empty catch blocks in backend runtime code\./iu, 'Catch vacío en código backend runtime.'],
|
|
67
|
+
[/Action handlers should reference methods, not contain inline logic/iu, 'El handler contiene lógica inline; debe llamar a un método o comando del view model.'],
|
|
68
|
+
[/Ensure constant number of views per ForEach element/iu, 'Cada elemento de ForEach debe devolver una estructura de vistas estable.'],
|
|
69
|
+
[/Ensure ForEach uses stable identity/iu, 'ForEach debe usar una identidad estable.'],
|
|
70
|
+
[/Prefer modifiers over conditional views for state changes/iu, 'Para cambios de estado, usa modificadores en vez de cambiar la identidad de la vista.'],
|
|
71
|
+
[/Prefer static member lookup \(\.blue vs Color\.blue\)/iu, 'Usa colores semánticos o static member lookup en SwiftUI.'],
|
|
72
|
+
[/LazyVStack\/LazyHStack - Para listas grandes/iu, 'Las listas grandes deben renderizarse con LazyVStack o LazyHStack.'],
|
|
73
|
+
[/navigationDestination \(for:\) - Destinos tipados/iu, 'La navegación debe usar navigationDestination(for:) con destinos tipados.'],
|
|
74
|
+
[/Extract the action body to a named method or view model command and reference that method from Button\./iu, 'Extrae la acción a un método con nombre o comando del view model y referencia ese método desde Button.'],
|
|
75
|
+
[/Move branching into a dedicated row view or prefer conditional modifiers\/values that keep a stable view structure per element\./iu, 'Mueve la ramificación a una fila dedicada o usa modificadores condicionales que mantengan estable la estructura de vistas por elemento.'],
|
|
76
|
+
[/Use Identifiable models or an explicit stable domain identifier such as id: \\.id\./iu, 'Usa modelos Identifiable o un identificador estable del dominio, por ejemplo id: \\.id.'],
|
|
77
|
+
[/Keep one view instance and move the condition into modifiers, parameters or extracted stable row state\./iu, 'Mantén una única instancia de vista y mueve la condición a modificadores, parámetros o estado estable extraído.'],
|
|
78
|
+
[/Replace Color\.blue\/Color\.primary\/etc\. with \.blue\/\.primary in SwiftUI style contexts, or use named asset colors such as Color\("BrandPrimary"\) when design tokens are required\./iu, 'Sustituye Color.blue/Color.primary por .blue/.primary en contextos SwiftUI, o usa colores semánticos del design system cuando aplique.'],
|
|
79
|
+
[/Replace VStack\/HStack under ScrollView with LazyVStack\/LazyHStack when rendering collection rows\./iu, 'Sustituye VStack/HStack dentro de ScrollView por LazyVStack/LazyHStack al renderizar colecciones.'],
|
|
80
|
+
];
|
|
81
|
+
for (const [pattern, replacement] of knownTexts) {
|
|
82
|
+
if (pattern.test(normalized)) {
|
|
83
|
+
return replacement;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return normalized;
|
|
87
|
+
};
|
|
88
|
+
|
|
63
89
|
const extractFieldFromCauseMessage = (message: string, field: string): string | null => {
|
|
64
90
|
const escapedField = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
65
91
|
const match = message.match(new RegExp(`\\b${escapedField}=([^\\n]+?)(?=\\s(?:severity|code|rule|file|lines?|message|remediation|snippet|node|primary_node|missing)=|$)`, 'i'));
|
|
@@ -104,15 +130,22 @@ const formatLocation = (cause: NonNullable<Extract<PumukiCriticalNotificationEve
|
|
|
104
130
|
|
|
105
131
|
const humanizeRuleId = (ruleId: string): string => {
|
|
106
132
|
const knownRules: Record<string, string> = {
|
|
133
|
+
'no-empty-catch': 'catch vacío',
|
|
107
134
|
'prefer-swift-testing': 'Swift Testing',
|
|
108
135
|
'no-xctassert': 'XCTest assertions',
|
|
109
136
|
'no-wait-for-expectations': 'waitForExpectations en XCTest',
|
|
110
137
|
'ios-test-quality': 'calidad de tests iOS',
|
|
138
|
+
'action-handlers-should-reference-methods-not-contain-inline-logic': 'handlers SwiftUI sin lógica inline',
|
|
139
|
+
'ensure-constant-number-of-views-per-foreach-element': 'estructura estable en ForEach',
|
|
140
|
+
'ensure-foreach-uses-stable-identity-see-references-list-patterns-md': 'identidad estable en ForEach',
|
|
141
|
+
'prefer-modifiers-over-conditional-views-for-state-changes': 'modificadores para cambios de estado',
|
|
111
142
|
'dynamic-type-font-scaling-automatico': 'Dynamic Type',
|
|
112
143
|
'dynamic-type-fuentes-escalables-y-layouts-adaptativos': 'fuentes escalables y layouts adaptativos',
|
|
113
144
|
'use-relative-layout-over-hard-coded-constants': 'layout relativo en vez de constantes fijas',
|
|
114
145
|
'magic-numbers-usar-constantes-con-nombres': 'números mágicos sin constantes con nombre',
|
|
115
146
|
'prefer-static-member-lookup-blue-vs-color-blue': 'colores semánticos en SwiftUI',
|
|
147
|
+
'use-lazyvstack-lazyhstack-for-large-lists': 'LazyVStack/LazyHStack para listas grandes',
|
|
148
|
+
'use-navigationdestination-for-type-safe-navigation': 'navigationDestination type-safe',
|
|
116
149
|
};
|
|
117
150
|
const normalized = ruleId
|
|
118
151
|
.replace(/^skills\./u, '')
|
|
@@ -218,13 +251,13 @@ const formatBlockingCauseForDialog = (
|
|
|
218
251
|
): readonly string[] => {
|
|
219
252
|
const rule = cause.ruleId ?? cause.code;
|
|
220
253
|
const visibleRule = formatVisibleRule(cause);
|
|
221
|
-
const problem = stripTechnicalFieldsFromMessage(cause.message) || cause.code;
|
|
254
|
+
const problem = localizeDeveloperText(stripTechnicalFieldsFromMessage(cause.message) || cause.code);
|
|
222
255
|
const remediation = isGoldenFlowCause(cause)
|
|
223
256
|
? GOLDEN_FLOW_REMEDIATION
|
|
224
257
|
: isWorktreeHygieneCause(cause)
|
|
225
258
|
? WORKTREE_HYGIENE_REMEDIATION
|
|
226
259
|
: cause.remediation
|
|
227
|
-
?
|
|
260
|
+
? localizeDeveloperText(cause.remediation)
|
|
228
261
|
: 'Corrige la violación indicada y vuelve a intentar el commit.';
|
|
229
262
|
if (isWorktreeHygieneCause(cause)) {
|
|
230
263
|
return [
|
|
@@ -246,7 +279,7 @@ const formatBlockingCauseForDialog = (
|
|
|
246
279
|
return [
|
|
247
280
|
`${index + 1}. ${causeLabel}: ${truncateNotificationText(visibleRule, 96)}`,
|
|
248
281
|
` Fichero: ${truncateNotificationText(formatLocation(cause), 120)}`,
|
|
249
|
-
`
|
|
282
|
+
` Falla: ${truncateNotificationText(problem, 160)}`,
|
|
250
283
|
buildEvidenceLine(cause),
|
|
251
284
|
` Solución: ${truncateNotificationText(remediation, 180)}`,
|
|
252
285
|
].filter((line): line is string => typeof line === 'string');
|
|
@@ -41,8 +41,30 @@ final class DialogAppDelegate: NSObject, NSApplicationDelegate {
|
|
|
41
41
|
|
|
42
42
|
let alert = NSAlert()
|
|
43
43
|
alert.messageText = config.title
|
|
44
|
-
alert.informativeText = "
|
|
44
|
+
alert.informativeText = "Revisa el detalle del bloqueo. Puedes seleccionar y copiar el texto."
|
|
45
45
|
alert.alertStyle = .critical
|
|
46
|
+
|
|
47
|
+
let details = "Causa:\n\(config.cause)\n\nSolución:\n\(config.remediation)"
|
|
48
|
+
let scrollView = NSScrollView(frame: NSRect(x: 0, y: 0, width: 560, height: 420))
|
|
49
|
+
scrollView.hasVerticalScroller = true
|
|
50
|
+
scrollView.hasHorizontalScroller = false
|
|
51
|
+
scrollView.borderType = .bezelBorder
|
|
52
|
+
|
|
53
|
+
let textView = NSTextView(frame: scrollView.contentView.bounds)
|
|
54
|
+
textView.string = details
|
|
55
|
+
textView.isEditable = false
|
|
56
|
+
textView.isSelectable = true
|
|
57
|
+
textView.drawsBackground = false
|
|
58
|
+
textView.font = NSFont.monospacedSystemFont(ofSize: 12, weight: .regular)
|
|
59
|
+
textView.textContainerInset = NSSize(width: 8, height: 8)
|
|
60
|
+
textView.textContainer?.widthTracksTextView = true
|
|
61
|
+
textView.textContainer?.containerSize = NSSize(width: scrollView.contentSize.width, height: CGFloat.greatestFiniteMagnitude)
|
|
62
|
+
textView.isVerticallyResizable = true
|
|
63
|
+
textView.isHorizontallyResizable = false
|
|
64
|
+
textView.autoresizingMask = [.width]
|
|
65
|
+
scrollView.documentView = textView
|
|
66
|
+
alert.accessoryView = scrollView
|
|
67
|
+
|
|
46
68
|
alert.addButton(withTitle: config.keepButton)
|
|
47
69
|
alert.addButton(withTitle: config.muteButton)
|
|
48
70
|
alert.addButton(withTitle: config.disableButton)
|
|
@@ -8,24 +8,24 @@ export const buildAuditSummaryPayload = (
|
|
|
8
8
|
): SystemNotificationPayload => {
|
|
9
9
|
if (event.criticalViolations > 0) {
|
|
10
10
|
return {
|
|
11
|
-
title: '
|
|
12
|
-
message:
|
|
11
|
+
title: 'Pumuki audit: resumen',
|
|
12
|
+
message: `Hay ${event.criticalViolations} críticas y ${event.highViolations} high. Abre el bloqueo detallado para ver regla, fichero y solución.`,
|
|
13
13
|
};
|
|
14
14
|
}
|
|
15
15
|
if (event.highViolations > 0) {
|
|
16
16
|
return {
|
|
17
|
-
title: '
|
|
18
|
-
message:
|
|
17
|
+
title: 'Pumuki audit: resumen',
|
|
18
|
+
message: `Hay ${event.highViolations} high. Abre el bloqueo detallado para ver regla, fichero y solución.`,
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
if (event.totalViolations > 0) {
|
|
22
22
|
return {
|
|
23
|
-
title: '
|
|
24
|
-
message:
|
|
23
|
+
title: 'Pumuki audit: resumen',
|
|
24
|
+
message: `Hay ${event.totalViolations} violaciones. Abre el bloqueo detallado para ver regla, fichero y solución.`,
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
27
|
return {
|
|
28
|
-
title: '
|
|
29
|
-
message: '
|
|
28
|
+
title: 'Pumuki audit: OK',
|
|
29
|
+
message: 'No se han detectado violaciones.',
|
|
30
30
|
};
|
|
31
31
|
};
|