pumuki 6.3.145 → 6.3.147

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.
@@ -375,11 +375,34 @@ final class SyncTests: XCTestCase {
375
375
  }
376
376
  }
377
377
  }
378
+ `;
379
+ const brownfieldCompatibleUnitTest = `
380
+ import XCTest
381
+
382
+ final class LoginModelTests: XCTestCase {
383
+ func test_submit_validCredentials_storesSession() async throws {
384
+ let (sut, repository) = makeSUT()
385
+ try await sut.submit()
386
+ XCTAssertEqual(repository.receivedRequests.count, 1)
387
+ }
388
+
389
+ private func makeSUT(
390
+ file: StaticString = #filePath,
391
+ line: UInt = #line
392
+ ) -> (LoginModel, AuthRepositorySpy) {
393
+ let repository = AuthRepositorySpy()
394
+ let sut = LoginModel(repository: repository)
395
+ trackForMemoryLeaks(sut, testCase: self, file: file, line: line)
396
+ trackForMemoryLeaks(repository, testCase: self, file: file, line: line)
397
+ return (sut, repository)
398
+ }
399
+ }
378
400
  `;
379
401
 
380
402
  assert.equal(hasSwiftLegacyXCTestImportUsage(unitTest), true);
381
403
  assert.equal(hasSwiftLegacyXCTestImportUsage(uiTest), false);
382
404
  assert.equal(hasSwiftLegacyXCTestImportUsage(performanceTest), false);
405
+ assert.equal(hasSwiftLegacyXCTestImportUsage(brownfieldCompatibleUnitTest), false);
383
406
  });
384
407
 
385
408
  test('hasSwiftLegacySwiftUiObservableWrapperUsage detecta @StateObject/@ObservedObject legacy', () => {
@@ -476,11 +499,34 @@ final class LoginUITests: XCTestCase {
476
499
  app.launch()
477
500
  }
478
501
  }
502
+ `;
503
+ const brownfieldCompatibleSuite = `
504
+ import XCTest
505
+
506
+ final class LoginModelTests: XCTestCase {
507
+ func test_submit_validCredentials_storesSession() async throws {
508
+ let (sut, repository) = makeSUT()
509
+ try await sut.submit()
510
+ XCTAssertEqual(repository.receivedRequests.count, 1)
511
+ }
512
+
513
+ private func makeSUT(
514
+ file: StaticString = #filePath,
515
+ line: UInt = #line
516
+ ) -> (LoginModel, AuthRepositorySpy) {
517
+ let repository = AuthRepositorySpy()
518
+ let sut = LoginModel(repository: repository)
519
+ trackForMemoryLeaks(sut, testCase: self, file: file, line: line)
520
+ trackForMemoryLeaks(repository, testCase: self, file: file, line: line)
521
+ return (sut, repository)
522
+ }
523
+ }
479
524
  `;
480
525
 
481
526
  assert.equal(hasSwiftModernizableXCTestSuiteUsage(legacySuite), true);
482
527
  assert.equal(hasSwiftModernizableXCTestSuiteUsage(mixedSuite), false);
483
528
  assert.equal(hasSwiftModernizableXCTestSuiteUsage(uiSuite), false);
529
+ assert.equal(hasSwiftModernizableXCTestSuiteUsage(brownfieldCompatibleSuite), false);
484
530
  });
485
531
 
486
532
  test('hasSwiftMixedTestingFrameworksUsage detecta mezcla XCTestCase y Testing/@Test', () => {
@@ -549,6 +595,46 @@ final class BuyerCommerceUISmokeTests: XCTestCase {
549
595
  assert.equal(hasSwiftXCTUnwrapUsage(`${uiSource}\nlet value = try XCTUnwrap(optional)`), false);
550
596
  });
551
597
 
598
+ test('hasSwiftXCTestAssertionUsage excluye XCTest brownfield compatible y bloquea suites sin contrato de calidad', () => {
599
+ const compatibleSource = `
600
+ import XCTest
601
+
602
+ final class LoginModelTests: XCTestCase {
603
+ func test_submit_validCredentials_storesSession() async throws {
604
+ let (sut, repository) = makeSUT()
605
+ try await sut.submit()
606
+ XCTAssertEqual(repository.receivedRequests.count, 1)
607
+ let session = try XCTUnwrap(repository.savedSession)
608
+ XCTAssertEqual(session.userId, "buyer-1")
609
+ }
610
+
611
+ private func makeSUT(
612
+ file: StaticString = #filePath,
613
+ line: UInt = #line
614
+ ) -> (LoginModel, AuthRepositorySpy) {
615
+ let repository = AuthRepositorySpy()
616
+ let sut = LoginModel(repository: repository)
617
+ trackForMemoryLeaks(sut, testCase: self, file: file, line: line)
618
+ trackForMemoryLeaks(repository, testCase: self, file: file, line: line)
619
+ return (sut, repository)
620
+ }
621
+ }
622
+ `;
623
+ const missingQualityContract = `
624
+ import XCTest
625
+
626
+ final class LoginModelTests: XCTestCase {
627
+ func test_submit_validCredentials_storesSession() async throws {
628
+ XCTAssertEqual(repository.receivedRequests.count, 1)
629
+ }
630
+ }
631
+ `;
632
+
633
+ assert.equal(hasSwiftXCTestAssertionUsage(compatibleSource), false);
634
+ assert.equal(hasSwiftXCTUnwrapUsage(compatibleSource), false);
635
+ assert.equal(hasSwiftXCTestAssertionUsage(missingQualityContract), true);
636
+ });
637
+
552
638
  test('hasSwiftXCTUnwrapUsage detecta XCTUnwrap real y evita strings', () => {
553
639
  const source = `
554
640
  let value = try XCTUnwrap(optionalValue)
@@ -730,6 +730,30 @@ const hasSwiftLegacyXCTestMethodUsage = (source: string): boolean => {
730
730
  .length > 0;
731
731
  };
732
732
 
733
+ const hasSwiftMakeSutUsage = (source: string): boolean => {
734
+ return hasSwiftSanitizedRegexMatch(source, /\bmakeSUT\s*\(/);
735
+ };
736
+
737
+ const hasSwiftMemoryLeakTrackingUsage = (source: string): boolean => {
738
+ return hasSwiftSanitizedRegexMatch(source, /\btrackForMemoryLeaks\s*\(/);
739
+ };
740
+
741
+ const hasSwiftBrownfieldCompatibleXCTestUsage = (source: string): boolean => {
742
+ if (!hasSwiftXCTestImportUsage(source) || !hasSwiftXCTestCaseSubclassUsage(source)) {
743
+ return false;
744
+ }
745
+
746
+ if (!hasSwiftLegacyXCTestMethodUsage(source)) {
747
+ return false;
748
+ }
749
+
750
+ if (hasSwiftTestingImportUsage(source) || hasSwiftTestingSuiteAttributeUsage(source)) {
751
+ return false;
752
+ }
753
+
754
+ return hasSwiftMakeSutUsage(source) && hasSwiftMemoryLeakTrackingUsage(source);
755
+ };
756
+
733
757
  export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
734
758
  if (!hasSwiftXCTestImportUsage(source)) {
735
759
  return false;
@@ -739,6 +763,10 @@ export const hasSwiftLegacyXCTestImportUsage = (source: string): boolean => {
739
763
  return false;
740
764
  }
741
765
 
766
+ if (hasSwiftBrownfieldCompatibleXCTestUsage(source)) {
767
+ return false;
768
+ }
769
+
742
770
  return true;
743
771
  };
744
772
 
@@ -771,6 +799,10 @@ export const hasSwiftXCTestAssertionUsage = (source: string): boolean => {
771
799
  return false;
772
800
  }
773
801
 
802
+ if (hasSwiftBrownfieldCompatibleXCTestUsage(source)) {
803
+ return false;
804
+ }
805
+
774
806
  return (
775
807
  collectSwiftRegexLines(source, /\bXCTAssert[A-Za-z0-9_]*\s*\(/).length > 0 ||
776
808
  collectSwiftRegexLines(source, /\bXCTFail\s*\(/).length > 0
@@ -782,6 +814,10 @@ export const hasSwiftXCTUnwrapUsage = (source: string): boolean => {
782
814
  return false;
783
815
  }
784
816
 
817
+ if (hasSwiftBrownfieldCompatibleXCTestUsage(source)) {
818
+ return false;
819
+ }
820
+
785
821
  return collectSwiftRegexLines(source, /\bXCTUnwrap\s*\(/).length > 0;
786
822
  };
787
823
 
@@ -0,0 +1,40 @@
1
+ type BlockingCauseCandidate = {
2
+ code?: string;
3
+ message?: string;
4
+ remediation?: string;
5
+ severity?: string;
6
+ };
7
+
8
+ const UMBRELLA_EVIDENCE_CODES = new Set([
9
+ 'EVIDENCE_GATE_BLOCKED',
10
+ 'AI_GATE_BLOCKED',
11
+ 'GATE_BLOCKED',
12
+ ]);
13
+
14
+ export const isTddBddBlockingCause = (candidate?: {
15
+ code?: string;
16
+ message?: string;
17
+ }): boolean => {
18
+ const code = candidate?.code ?? '';
19
+ const message = candidate?.message ?? '';
20
+ return code.startsWith('TDD_BDD_') || /\bTDD_BDD_[A-Z0-9_]+\b/u.test(message);
21
+ };
22
+
23
+ export const resolvePrimaryBlockingCause = <T extends BlockingCauseCandidate>(
24
+ candidates: ReadonlyArray<T>
25
+ ): T | undefined => {
26
+ const tddBddCause = candidates.find(isTddBddBlockingCause);
27
+ if (tddBddCause) {
28
+ return tddBddCause;
29
+ }
30
+
31
+ const specificCause = candidates.find((candidate) => {
32
+ const code = candidate.code ?? '';
33
+ return code.length > 0 && !UMBRELLA_EVIDENCE_CODES.has(code);
34
+ });
35
+ if (specificCause) {
36
+ return specificCause;
37
+ }
38
+
39
+ return candidates[0];
40
+ };
@@ -12,6 +12,14 @@ export const REMEDIATION_HINT_BY_CODE: Readonly<Record<string, string>> = {
12
12
  EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual y reintenta.',
13
13
  TDD_BDD_BASELINE_BLOCKED:
14
14
  'Corrige el baseline TDD/BDD roto y regenera la evidencia antes de continuar.',
15
+ TDD_BDD_EVIDENCE_INVALID:
16
+ 'Regenera la evidencia TDD/BDD válida del escenario afectado y vuelve a ejecutar el gate.',
17
+ TDD_BDD_SCENARIO_FILE_MISSING:
18
+ 'Crea o corrige el fichero .feature referenciado por la evidencia TDD/BDD y revalida.',
19
+ TDD_BDD_EVIDENCE_STALE:
20
+ 'Reejecuta los tests baseline del componente tocado, refresca la evidencia TDD/BDD y revalida.',
21
+ TDD_BDD_EVIDENCE_MISSING:
22
+ 'Genera evidencia TDD/BDD para el cambio actual antes de continuar.',
15
23
  EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
16
24
  EVIDENCE_RULES_COVERAGE_INCOMPLETE: 'Asegura coverage_ratio=1 y unevaluated=0.',
17
25
  ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
@@ -35,6 +35,7 @@ import {
35
35
  DEFAULT_GATE_REMEDIATION as DEFAULT_BLOCKED_REMEDIATION,
36
36
  REMEDIATION_HINT_BY_CODE as BLOCKED_REMEDIATION_BY_CODE,
37
37
  } from '../gate/remediationCatalog';
38
+ import { resolvePrimaryBlockingCause } from '../gate/blockingCause';
38
39
 
39
40
  const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
40
41
  'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
@@ -273,10 +274,16 @@ const notifyGateBlockedForStage = (params: {
273
274
  evidence?.snapshot.stage === params.stage
274
275
  ? evidence.snapshot.findings
275
276
  : [];
276
- const primaryStageFinding = resolvePrimaryBlockedStageFinding(stageFindings);
277
- const firstViolation = evidence?.ai_gate.violations[0];
278
- const causeCode = primaryStageFinding?.code ?? firstViolation?.code ?? params.fallbackCauseCode;
279
- const causeMessage = primaryStageFinding?.message ?? firstViolation?.message ?? params.fallbackCauseMessage;
277
+ const blockingStageFindings = stageFindings.filter((finding) =>
278
+ isSeverityAtLeast(finding.severity, 'ERROR')
279
+ );
280
+ const primaryCause = resolvePrimaryBlockingCause([
281
+ ...blockingStageFindings,
282
+ ...(evidence?.ai_gate.violations ?? []),
283
+ ]);
284
+ const primaryStageFinding = primaryCause ?? resolvePrimaryBlockedStageFinding(stageFindings);
285
+ const causeCode = primaryStageFinding?.code ?? params.fallbackCauseCode;
286
+ const causeMessage = primaryStageFinding?.message ?? params.fallbackCauseMessage;
280
287
  const remediation =
281
288
  BLOCKED_REMEDIATION_BY_CODE[causeCode]
282
289
  ?? params.fallbackRemediation
@@ -21,6 +21,7 @@ import {
21
21
  type PreWriteEnforcementResolution,
22
22
  } from '../policy/preWriteEnforcement';
23
23
  import { readLifecycleExperimentalFeaturesSnapshot } from './experimentalFeaturesSnapshot';
24
+ import { resolvePrimaryBlockingCause } from '../gate/blockingCause';
24
25
 
25
26
  import {
26
27
  type ParsedArgs,
@@ -321,9 +322,9 @@ export const runSddCommand = async (parsed: ParsedArgs, activeDependencies: Life
321
322
  return 1;
322
323
  }
323
324
  if (aiGate && !aiGate.allowed && preWriteEnforcement.blocking) {
324
- const firstViolation = aiGate.violations[0];
325
- const causeCode = firstViolation?.code ?? 'AI_GATE_BLOCKED';
326
- const causeMessage = firstViolation?.message ?? 'AI gate blocked PRE_WRITE stage.';
325
+ const primaryViolation = resolvePrimaryBlockingCause(aiGate.violations);
326
+ const causeCode = primaryViolation?.code ?? 'AI_GATE_BLOCKED';
327
+ const causeMessage = primaryViolation?.message ?? 'AI gate blocked PRE_WRITE stage.';
327
328
  activeDependencies.emitGateBlockedNotification({
328
329
  repoRoot: process.cwd(),
329
330
  stage: result.stage,
@@ -36,6 +36,7 @@ import {
36
36
  resolveGitAtomicityEnforcement,
37
37
  type GitAtomicityEnforcementResolution,
38
38
  } from '../policy/gitAtomicityEnforcement';
39
+ import { resolvePrimaryBlockingCause } from '../gate/blockingCause';
39
40
 
40
41
  export type LifecycleWatchStage = 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
41
42
  export type LifecycleWatchScope = 'workingTree' | 'staged' | 'repoAndStaged' | 'repo';
@@ -119,6 +120,11 @@ type LifecycleWatchDependencies = {
119
120
  };
120
121
 
121
122
  const defaultGitService = new GitService();
123
+ const GIT_STATUS_PORCELAIN_ARGS = [
124
+ 'status',
125
+ '--porcelain=v1',
126
+ '--untracked-files=all',
127
+ ] as const;
122
128
 
123
129
  class RepoScopedGitService extends GitService implements IGitService {
124
130
  constructor(private readonly repoRoot: string) {
@@ -137,7 +143,7 @@ class RepoScopedGitService extends GitService implements IGitService {
137
143
  const defaultDependencies: LifecycleWatchDependencies = {
138
144
  resolveRepoRoot: () => defaultGitService.resolveRepoRoot(),
139
145
  readChangeToken: (repoRoot) =>
140
- defaultGitService.runGit(['status', '--porcelain=v1', '--untracked-files=all'], repoRoot),
146
+ defaultGitService.runGit([...GIT_STATUS_PORCELAIN_ARGS], repoRoot),
141
147
  resolvePolicyForStage: (stage) => resolvePolicyForStage(stage),
142
148
  resolveUpstreamRef,
143
149
  resolvePrePushBootstrapBaseRef,
@@ -155,7 +161,10 @@ const defaultDependencies: LifecycleWatchDependencies = {
155
161
  ensureRuntimeArtifactsIgnored: (repoRoot) => {
156
162
  try {
157
163
  ensureRuntimeArtifactsIgnored(repoRoot);
158
- } catch {
164
+ } catch (error) {
165
+ process.stderr.write(
166
+ `[pumuki][watch] unable to ensure runtime artifacts are ignored: ${String(error)}\n`
167
+ );
159
168
  }
160
169
  },
161
170
  emitAuditSummaryNotificationFromEvidence,
@@ -180,6 +189,16 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
180
189
  SDD_SESSION_MISSING: 'Abre sesión SDD y vuelve a intentar.',
181
190
  SDD_SESSION_INVALID: 'Refresca la sesión SDD y vuelve a intentar.',
182
191
  OPENSPEC_MISSING: 'Instala OpenSpec y reintenta la validación.',
192
+ TDD_BDD_EVIDENCE_INVALID:
193
+ 'Regenera la evidencia TDD/BDD válida del escenario afectado y vuelve a ejecutar el gate.',
194
+ TDD_BDD_SCENARIO_FILE_MISSING:
195
+ 'Crea o corrige el fichero .feature referenciado por la evidencia TDD/BDD y revalida.',
196
+ TDD_BDD_EVIDENCE_STALE:
197
+ 'Reejecuta los tests baseline del componente tocado, refresca la evidencia TDD/BDD y revalida.',
198
+ TDD_BDD_EVIDENCE_MISSING:
199
+ 'Genera evidencia TDD/BDD para el cambio actual antes de continuar.',
200
+ TDD_BDD_BASELINE_BLOCKED:
201
+ 'Corrige el baseline TDD/BDD roto y regenera la evidencia antes de continuar.',
183
202
  MCP_ENTERPRISE_RECEIPT_MISSING: 'Genera el receipt enterprise de MCP antes de continuar.',
184
203
  MANIFEST_MUTATION_DETECTED:
185
204
  'Validación no debe mutar package.json/lockfiles. Revisa wiring y realiza upgrades solo con comando explícito.',
@@ -369,12 +388,13 @@ const toFirstCause = (params: {
369
388
  evidence: AiEvidenceV2_1 | undefined;
370
389
  matchedFindings: ReadonlyArray<SnapshotFinding>;
371
390
  }): { code: string; message: string; remediation: string } => {
372
- const firstFinding = params.matchedFindings[0];
373
- const firstViolation = params.evidence?.ai_gate.violations[0];
374
- const code = firstFinding?.code ?? firstViolation?.code ?? 'WATCH_GATE_BLOCKED';
391
+ const primaryCause = resolvePrimaryBlockingCause([
392
+ ...params.matchedFindings,
393
+ ...(params.evidence?.ai_gate.violations ?? []),
394
+ ]);
395
+ const code = primaryCause?.code ?? 'WATCH_GATE_BLOCKED';
375
396
  const message =
376
- firstFinding?.message ??
377
- firstViolation?.message ??
397
+ primaryCause?.message ??
378
398
  `Watch gate bloqueado (${code}).`;
379
399
  const remediation =
380
400
  BLOCKED_REMEDIATION_BY_CODE[code] ??
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.145",
3
+ "version": "6.3.147",
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": {
@@ -2,6 +2,7 @@ import {
2
2
  evaluateAiGate,
3
3
  type AiGateCheckResult,
4
4
  } from '../integrations/gate/evaluateAiGate';
5
+ import { resolvePrimaryBlockingCause } from '../integrations/gate/blockingCause';
5
6
  import { readLifecycleExperimentalFeaturesSnapshot } from '../integrations/lifecycle/experimentalFeaturesSnapshot';
6
7
  import { LifecycleGitService } from '../integrations/lifecycle/gitService';
7
8
  import { readGovernanceObservationSnapshot } from '../integrations/lifecycle/governanceObservationSnapshot';
@@ -36,6 +37,8 @@ const defaultDependencies: ConsumerPreflightDependencies = {
36
37
  readGovernanceNextAction,
37
38
  };
38
39
 
40
+ const SECONDS_PER_MINUTE = 60;
41
+
39
42
  const buildNotificationEvents = (
40
43
  result: AiGateCheckResult
41
44
  ): ReadonlyArray<PumukiCriticalNotificationEvent> => {
@@ -45,7 +48,7 @@ const buildNotificationEvents = (
45
48
  events.push({
46
49
  kind: 'evidence.stale',
47
50
  evidencePath: '.ai_evidence.json',
48
- ageMinutes: Math.max(1, Math.ceil(ageSeconds / 60)),
51
+ ageMinutes: Math.max(1, Math.ceil(ageSeconds / SECONDS_PER_MINUTE)),
49
52
  });
50
53
  }
51
54
  if (hasViolationCode(result.violations, 'GITFLOW_PROTECTED_BRANCH')) {
@@ -56,13 +59,13 @@ const buildNotificationEvents = (
56
59
  });
57
60
  }
58
61
  if (result.status === 'BLOCKED') {
59
- const firstViolation = result.violations[0];
60
- const causeCode = firstViolation?.code ?? 'GATE_BLOCKED';
62
+ const primaryViolation = resolvePrimaryBlockingCause(result.violations);
63
+ const causeCode = primaryViolation?.code ?? 'GATE_BLOCKED';
61
64
  const causeMessage =
62
- firstViolation?.message
65
+ primaryViolation?.message
63
66
  ?? `Detected ${result.violations.length} blocking violations in stage ${result.stage}.`;
64
67
  const remediation =
65
- (firstViolation ? ACTIONABLE_HINTS_BY_CODE[firstViolation.code] : undefined)
68
+ (primaryViolation ? ACTIONABLE_HINTS_BY_CODE[primaryViolation.code] : undefined)
66
69
  ?? 'Corrige la causa bloqueante y vuelve a ejecutar el gate.';
67
70
  events.push({
68
71
  kind: 'gate.blocked',
@@ -7,9 +7,16 @@ import {
7
7
  buildNotificationTrackingCauseSummary,
8
8
  extractNotificationTrackingContext,
9
9
  } from './framework-menu-system-notifications-tracking';
10
+ import { isTddBddBlockingCause } from '../integrations/gate/blockingCause';
10
11
 
11
12
  const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
12
13
  EVIDENCE_GATE_BLOCKED: 'El gate de evidencia/gobernanza está bloqueado.',
14
+ TDD_BDD_EVIDENCE_INVALID: 'La evidencia TDD/BDD actual es inválida.',
15
+ TDD_BDD_SCENARIO_FILE_MISSING:
16
+ 'Falta el fichero de escenario TDD/BDD referenciado por la evidencia.',
17
+ TDD_BDD_EVIDENCE_STALE: 'La evidencia TDD/BDD está caducada.',
18
+ TDD_BDD_EVIDENCE_MISSING: 'Falta evidencia TDD/BDD para el cambio actual.',
19
+ TDD_BDD_BASELINE_BLOCKED: 'La baseline TDD/BDD está bloqueada.',
13
20
  EVIDENCE_MISSING: 'Falta evidencia para validar este paso.',
14
21
  EVIDENCE_INVALID: 'La evidencia actual es inválida.',
15
22
  EVIDENCE_CHAIN_INVALID: 'La cadena de evidencia no es válida.',
@@ -32,6 +39,13 @@ const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
32
39
  'No hay reglas activas para cambios de código.',
33
40
  };
34
41
 
42
+ const TRACKING_COMPATIBLE_UMBRELLA_CODES = new Set([
43
+ 'EVIDENCE_GATE_BLOCKED',
44
+ 'AI_GATE_BLOCKED',
45
+ 'AI_GATE_BLOCK',
46
+ 'GATE_BLOCKED',
47
+ ]);
48
+
35
49
  const ENGLISH_CAUSE_HINTS = [
36
50
  'detected',
37
51
  'avoid explicit any',
@@ -97,13 +111,22 @@ export const resolveBlockedCauseSummary = (
97
111
  causeCode: string
98
112
  ): string => {
99
113
  const trackingContext = extractNotificationTrackingContext(event.causeMessage);
100
- if (trackingContext) {
114
+ if (isTddBddBlockingCause({ code: causeCode, message: event.causeMessage })) {
115
+ return (
116
+ BLOCKED_CAUSE_SUMMARY_BY_CODE[causeCode] ??
117
+ 'La evidencia TDD/BDD bloquea el gate; revisa el escenario y el artefacto de evidencia.'
118
+ );
119
+ }
120
+ if (trackingContext && TRACKING_COMPATIBLE_UMBRELLA_CODES.has(causeCode)) {
101
121
  return buildNotificationTrackingCauseSummary(trackingContext);
102
122
  }
103
123
  const mapped = BLOCKED_CAUSE_SUMMARY_BY_CODE[causeCode];
104
124
  if (mapped) {
105
125
  return mapped;
106
126
  }
127
+ if (trackingContext) {
128
+ return buildNotificationTrackingCauseSummary(trackingContext);
129
+ }
107
130
  if (event.causeMessage && event.causeMessage.trim().length > 0) {
108
131
  const rawMessage = normalizeNotificationText(event.causeMessage).replace(/^[A-Z0-9_]+:\s*/, '');
109
132
  const translated = toKnownSpanishCauseFromMessage(rawMessage);
@@ -7,12 +7,23 @@ import {
7
7
  extractNotificationTrackingContext,
8
8
  TRACKING_BLOCKED_REMEDIATION,
9
9
  } from './framework-menu-system-notifications-tracking';
10
+ import { isTddBddBlockingCause } from '../integrations/gate/blockingCause';
10
11
 
11
12
  type BlockedRemediationVariant = 'banner' | 'dialog';
12
13
 
13
14
  const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
14
15
  EVIDENCE_GATE_BLOCKED:
15
16
  'Revisa status/doctor para ver la causa exacta del gate, corrígela y revalida.',
17
+ TDD_BDD_EVIDENCE_INVALID:
18
+ 'Regenera la evidencia TDD/BDD válida del escenario afectado y vuelve a ejecutar el gate.',
19
+ TDD_BDD_SCENARIO_FILE_MISSING:
20
+ 'Crea o corrige el fichero .feature referenciado por la evidencia TDD/BDD y revalida.',
21
+ TDD_BDD_EVIDENCE_STALE:
22
+ 'Reejecuta los tests baseline del componente tocado, refresca la evidencia TDD/BDD y revalida.',
23
+ TDD_BDD_EVIDENCE_MISSING:
24
+ 'Genera evidencia TDD/BDD para el cambio actual antes de continuar.',
25
+ TDD_BDD_BASELINE_BLOCKED:
26
+ 'Corrige la baseline TDD/BDD rota, registra evidencia pasada y vuelve a ejecutar el gate.',
16
27
  EVIDENCE_MISSING: 'Genera la evidencia del slice actual y vuelve a validar esta fase.',
17
28
  EVIDENCE_INVALID: 'Regenera la evidencia de esta iteración y repite la validación.',
18
29
  EVIDENCE_CHAIN_INVALID: 'Regenera la evidencia para restaurar la cadena de integridad y vuelve a validar.',
@@ -105,6 +116,9 @@ export const resolveBlockedRemediation = (
105
116
  const fromEvent = event.remediation
106
117
  ? normalizeBlockedRemediation(event.remediation)
107
118
  : '';
119
+ if (isTddBddBlockingCause({ code: causeCode, message: event.causeMessage })) {
120
+ return truncateNotificationText(resolveFallbackRemediation(causeCode), maxLength);
121
+ }
108
122
  if (extractNotificationTrackingContext(event.causeMessage)) {
109
123
  return truncateNotificationText(TRACKING_BLOCKED_REMEDIATION, maxLength);
110
124
  }