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.
@@ -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.313",
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
- ? normalizeNotificationText(cause.remediation)
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
- ` Viola: ${truncateNotificationText(problem, 160)}`,
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 = "Causa: \(config.cause)\n\nSolución: \(config.remediation)"
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: 'AST Audit Blocked',
12
- message: `🔴 ${event.criticalViolations} CRITICAL, ${event.highViolations} HIGH violations`,
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: 'AST Audit Blocked',
18
- message: `🟡 ${event.highViolations} HIGH violations found`,
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: 'AST Audit Blocked',
24
- message: `🔴 ${event.totalViolations} violations block the gate`,
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: 'AST Audit Complete',
29
- message: 'No violations found',
28
+ title: 'Pumuki audit: OK',
29
+ message: 'No se han detectado violaciones.',
30
30
  };
31
31
  };