pumuki 6.3.316 → 6.3.318

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.316",
3
+ "version": "6.3.318",
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": {
@@ -192,6 +192,24 @@ const isGoldenFlowCause = (
192
192
  (cause.ruleId ?? '').startsWith('generic_tdd_') ||
193
193
  (cause.ruleId ?? '').startsWith('generic_red_green_refactor');
194
194
 
195
+ const isBddScenarioMissingCause = (
196
+ cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
197
+ ): boolean => {
198
+ const searchable = `${cause.code} ${cause.ruleId ?? ''} ${cause.message}`.toLowerCase();
199
+ return (
200
+ searchable.includes('tdd_bdd_scenario_file_missing') ||
201
+ searchable.includes('generic_bdd_feature_required') ||
202
+ searchable.includes('missing feature file')
203
+ );
204
+ };
205
+
206
+ const extractExpectedBddFeatureFile = (cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]): string => {
207
+ const directFile = cause.file?.endsWith('.feature') ? cause.file : null;
208
+ const fileField = extractFieldFromCauseMessage(cause.message, 'file');
209
+ const missingFeature = cause.message.match(/missing feature file\s+([^\s.]+\.feature|[^\s]+\.feature)/i)?.[1];
210
+ return directFile ?? fileField ?? missingFeature ?? 'fichero .feature de la slice';
211
+ };
212
+
195
213
  const isSkillCause = (
196
214
  cause: NonNullable<Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']>[number]
197
215
  ): boolean =>
@@ -274,6 +292,15 @@ const formatBlockingCauseForDialog = (
274
292
  ` Falta: ${truncateNotificationText(formatGoldenFlowMissing(cause.message), 120)}`,
275
293
  ];
276
294
  }
295
+ if (isBddScenarioMissingCause(cause)) {
296
+ const expectedFeatureFile = extractExpectedBddFeatureFile(cause);
297
+ return [
298
+ `${index + 1}. Causa bloqueante: escenario BDD sin fichero .feature`,
299
+ ` Fichero esperado: ${truncateNotificationText(expectedFeatureFile, 120)}`,
300
+ ' Falla: La slice referencia un escenario BDD, pero no existe su fichero .feature.',
301
+ ' Solución: Crea ese .feature con el escenario de aceptación de la slice, o corrige la referencia si el id de tarea no es correcto.',
302
+ ];
303
+ }
277
304
 
278
305
  const causeLabel = isSkillCause(cause) ? 'Regla' : 'Causa bloqueante';
279
306
  return [
@@ -295,6 +322,11 @@ const hasOnlyGoldenFlowCauses = (
295
322
  ): boolean =>
296
323
  Boolean(causes?.length) && causes!.every(isGoldenFlowCause);
297
324
 
325
+ const hasOnlyBddScenarioMissingCauses = (
326
+ causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
327
+ ): boolean =>
328
+ Boolean(causes?.length) && causes!.every(isBddScenarioMissingCause);
329
+
298
330
  const resolveGoldenFlowDialogRemediation = (
299
331
  causes: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>['blockingCauses']
300
332
  ): string => {
@@ -314,6 +346,8 @@ export const buildBlockedDialogPayload = (params: {
314
346
  ? WORKTREE_HYGIENE_REMEDIATION
315
347
  : hasOnlyGoldenFlowCauses(params.event.blockingCauses)
316
348
  ? resolveGoldenFlowDialogRemediation(params.event.blockingCauses)
349
+ : hasOnlyBddScenarioMissingCauses(params.event.blockingCauses)
350
+ ? 'Crea el fichero .feature esperado para la slice activa o corrige la referencia de tarea. Después vuelve a intentar el commit.'
317
351
  : params.event.blockingCauses && params.event.blockingCauses.length > 0
318
352
  ? 'Corrige las violaciones listadas y vuelve a intentar el commit.'
319
353
  : resolveBlockedRemediation(params.event, causeCode);
@@ -38,6 +38,23 @@ const isGoldenFlowCause = (cause: BlockedCause): boolean => {
38
38
  );
39
39
  };
40
40
 
41
+ const isBddScenarioMissingCause = (cause: BlockedCause): boolean => {
42
+ const searchable = `${cause.code} ${cause.ruleId ?? ''} ${cause.message}`.toLowerCase();
43
+ return (
44
+ searchable.includes('tdd_bdd_scenario_file_missing') ||
45
+ searchable.includes('generic_bdd_feature_required') ||
46
+ searchable.includes('missing feature file')
47
+ );
48
+ };
49
+
50
+ const extractExpectedBddFeatureFile = (cause: BlockedCause): string => {
51
+ const directFile = cause.file?.endsWith('.feature') ? cause.file : null;
52
+ const fieldMatch = cause.message.match(/\bfile=([^\s]+\.feature)\b/i)?.[1];
53
+ const missingFeature = cause.message.match(/missing feature file\s+([^\s.]+\.feature|[^\s]+\.feature)/i)?.[1];
54
+ const raw = directFile ?? fieldMatch ?? missingFeature ?? 'fichero .feature de la slice';
55
+ return raw.split('/').filter(Boolean).pop() ?? raw;
56
+ };
57
+
41
58
  const normalizeGoldenFlowMissingToken = (message: string): string => {
42
59
  const match = message.match(/missing=\[([^\]]+)\]/i);
43
60
  return (match?.[1] ?? '').toUpperCase();
@@ -145,7 +162,7 @@ const formatCauseProblem = (cause: BlockedCause): string => {
145
162
  const formatCauseFix = (cause: BlockedCause): string =>
146
163
  normalizeNotificationText(
147
164
  cause.remediation ??
148
- 'Corrige la violación indicada con regla, fichero y línea, y vuelve a ejecutar el gate.'
165
+ 'Corrige la violación indicada con regla, fichero y línea, y vuelve a intentar el commit.'
149
166
  );
150
167
 
151
168
  export const resolvePrioritizedBlockingCauses = (
@@ -165,6 +182,11 @@ export const resolvePrioritizedBlockingCauses = (
165
182
  if (leftGoldenFlow !== rightGoldenFlow) {
166
183
  return leftGoldenFlow ? -1 : 1;
167
184
  }
185
+ const leftBddScenarioMissing = isBddScenarioMissingCause(left);
186
+ const rightBddScenarioMissing = isBddScenarioMissingCause(right);
187
+ if (leftBddScenarioMissing !== rightBddScenarioMissing) {
188
+ return leftBddScenarioMissing ? -1 : 1;
189
+ }
168
190
  return 0;
169
191
  });
170
192
  };
@@ -188,6 +210,9 @@ const buildBlockingCausesSummary = (
188
210
  if (isGoldenFlowCause(first)) {
189
211
  return `Violación del ciclo TDD${overflow}`;
190
212
  }
213
+ if (isBddScenarioMissingCause(first)) {
214
+ return `BDD sin fichero .feature: ${extractExpectedBddFeatureFile(first)}${overflow}`;
215
+ }
191
216
  return `${prefix}: ${formatCauseRule(first)} · ${formatCauseLocation(first)}${overflow}`;
192
217
  };
193
218
 
@@ -206,7 +231,7 @@ const buildBlockingCausesRemediation = (
206
231
  return [
207
232
  `Regla: ${formatVisibleRule(first)}.`,
208
233
  `Fichero: ${formatCauseLocation(first)}.`,
209
- `Viola: ${formatCauseProblem(first)}.`,
234
+ `Falla: ${formatCauseProblem(first)}.`,
210
235
  `Solución: ${formatCauseFix(first)}.`,
211
236
  prioritized.length > 1 ? `Quedan ${prioritized.length - 1} causa(s) más en el reporte completo.` : '',
212
237
  ]
@@ -216,6 +241,9 @@ const buildBlockingCausesRemediation = (
216
241
  if (isGoldenFlowCause(first)) {
217
242
  return formatGoldenFlowBannerRemediation(first);
218
243
  }
244
+ if (isBddScenarioMissingCause(first)) {
245
+ return `Falta el fichero ${extractExpectedBddFeatureFile(first)}. Crea ese .feature con el escenario de aceptación de la slice, o corrige la referencia de tarea si no corresponde.`;
246
+ }
219
247
  return `${formatCauseFix(first)} Revisa el reporte completo para el resto de causas bloqueantes.`;
220
248
  };
221
249
 
@@ -4,6 +4,7 @@ import {
4
4
  truncateNotificationText,
5
5
  } from './framework-menu-system-notifications-text';
6
6
  import {
7
+ buildNotificationTrackingRemediation,
7
8
  extractNotificationTrackingContext,
8
9
  TRACKING_BLOCKED_REMEDIATION,
9
10
  } from './framework-menu-system-notifications-tracking';
@@ -21,9 +22,9 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
21
22
  EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera la evidencia desde este repositorio y vuelve a validar.',
22
23
  PRE_PUSH_UPSTREAM_MISSING: 'Configura upstream con `git push --set-upstream origin <branch>` y repite PRE_PUSH.',
23
24
  SDD_SESSION_MISSING:
24
- 'Abre una sesión SDD válida para el change activo (`pumuki sdd session --open --change=<id>`) y reejecuta PRE_WRITE. Agente IA: STOP hasta que exista esa sesión.',
25
+ 'Abre la sesión SDD del cambio activo: `pumuki sdd session --open --change=<id>`. Sustituye <id> por la carpeta en openspec/changes. Después vuelve a intentar el commit; Pumuki ejecutará PRE_WRITE automáticamente.',
25
26
  SDD_SESSION_INVALID:
26
- 'Refresca la sesión SDD activa (`pumuki sdd session --refresh --ttl-minutes=90`) y reejecuta PRE_WRITE. Agente IA: STOP hasta que sea válida.',
27
+ 'Refresca la sesión SDD activa: `pumuki sdd session --refresh --ttl-minutes=90`. Después vuelve a intentar el commit; Pumuki ejecutará PRE_WRITE automáticamente.',
27
28
  OPENSPEC_MISSING: 'Instala OpenSpec en este repositorio y vuelve a validar el gate.',
28
29
  MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP y vuelve a validar.',
29
30
  BACKEND_AVOID_EXPLICIT_ANY: 'Sustituye `any` por tipos concretos en backend y relanza el gate.',
@@ -34,7 +35,7 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
34
35
  ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
35
36
  'Ejecuta `pumuki policy reconcile --strict --json` y revalida antes de continuar.',
36
37
  EVIDENCE_PREWRITE_WORKTREE_WARN:
37
- 'Reduce el worktree pendiente a un slice atómico antes del commit: stagea solo la tarea activa o guarda el resto en stash nombrado, y reejecuta PRE_WRITE.',
38
+ 'Reduce el worktree pendiente a un slice atómico antes del commit: stagea solo la tarea activa o guarda el resto en stash nombrado, y vuelve a intentar el commit.',
38
39
  EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
39
40
  'El worktree supera el límite permitido: divide cambios por scope en commits atómicos o stashes nombrados antes de continuar.',
40
41
  };
@@ -219,8 +220,9 @@ export const resolveBlockedRemediation = (
219
220
  const fromEvent = event.remediation
220
221
  ? normalizeBlockedRemediation(event.remediation)
221
222
  : '';
222
- if (extractNotificationTrackingContext(event.causeMessage)) {
223
- return truncateNotificationText(TRACKING_BLOCKED_REMEDIATION, maxLength);
223
+ const trackingContext = extractNotificationTrackingContext(event.causeMessage);
224
+ if (trackingContext) {
225
+ return truncateNotificationText(buildNotificationTrackingRemediation(trackingContext), maxLength);
224
226
  }
225
227
  if (fromEvent.length > 0) {
226
228
  if (
@@ -1,6 +1,7 @@
1
1
  export type NotificationTrackingContext = {
2
2
  activeEntry?: string;
3
3
  trackingSource?: string;
4
+ line?: string;
4
5
  };
5
6
 
6
7
  const TRACKING_CONTEXT_PATTERN = /\b(active_entries=|tracking_source=|TRACKING_CANONICAL_)/u;
@@ -15,28 +16,46 @@ export const extractNotificationTrackingContext = (
15
16
  .match(/\bactive_entries=([^,\s]+)/u)?.[1]
16
17
  ?.replace(/@L\d+$/u, '')
17
18
  .trim();
19
+ const line = message.match(/\bline[_=]([0-9]+)\b/u)?.[1]?.trim();
18
20
  const trackingSource = message.match(/\btracking_source=([^\s]+)/u)?.[1]?.trim();
19
21
  return {
20
- activeEntry: activeEntry && activeEntry.length > 0 ? activeEntry : undefined,
22
+ activeEntry: activeEntry && activeEntry.length > 0 && !/^line_[0-9]+$/u.test(activeEntry)
23
+ ? activeEntry
24
+ : undefined,
21
25
  trackingSource: trackingSource && trackingSource.length > 0 ? trackingSource : undefined,
26
+ line: line && line.length > 0 ? line : undefined,
22
27
  };
23
28
  };
24
29
 
25
30
  export const buildNotificationTrackingCauseSummary = (
26
31
  context: NotificationTrackingContext
27
32
  ): string => {
33
+ const lineSuffix = context.line ? `, línea ${context.line}` : '';
28
34
  if (context.activeEntry && context.trackingSource) {
29
- return `Tracking bloqueado: ${context.activeEntry} en ${context.trackingSource}.`;
35
+ return `Tracking bloqueado: ${context.activeEntry} en ${context.trackingSource}${lineSuffix}.`;
30
36
  }
31
37
  if (context.activeEntry) {
32
- return `Tracking bloqueado: ${context.activeEntry}.`;
38
+ return `Tracking bloqueado: ${context.activeEntry}${lineSuffix}.`;
33
39
  }
34
40
  if (context.trackingSource) {
35
- return `Tracking bloqueado en ${context.trackingSource}.`;
41
+ return `Tracking bloqueado en ${context.trackingSource}${lineSuffix}.`;
42
+ }
43
+ if (context.line) {
44
+ return `Tracking bloqueado en la línea ${context.line}.`;
36
45
  }
37
46
  return 'El tracking canónico del repo bloquea la governance.';
38
47
  };
39
48
 
40
49
  export const TRACKING_BLOCKED_REMEDIATION =
41
- 'Corrige el MD de tracking: deja una única tarea activa válida y vuelve a ejecutar el gate.';
50
+ 'Corrige el MD de tracking: deja una única tarea activa válida, elimina estados activos duplicados o inválidos y vuelve a intentar el commit.';
42
51
 
52
+ export const buildNotificationTrackingRemediation = (
53
+ context: NotificationTrackingContext
54
+ ): string => {
55
+ const target = context.trackingSource
56
+ ? `${context.trackingSource}${context.line ? `, línea ${context.line}` : ''}`
57
+ : context.line
58
+ ? `la línea ${context.line} del MD de tracking`
59
+ : 'el MD de tracking';
60
+ return `Abre el MD de tracking ${target}. Deja una única tarea activa válida, elimina estados activos duplicados o inválidos y vuelve a intentar el commit.`;
61
+ };