pumuki 6.3.123 → 6.3.125
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 +8 -0
- package/VERSION +1 -1
- package/core/facts/detectors/astRuleThresholdAudit.test.ts +33 -0
- package/core/facts/detectors/security/index.test.ts +8 -2
- package/core/facts/detectors/security/securityCredentials.test.ts +10 -4
- package/core/facts/detectors/security/securityCredentials.ts +11 -4
- package/core/facts/detectors/text/android.ts +0 -6
- package/core/facts/detectors/text/ios.ts +0 -6
- package/integrations/git/aiGateRepoPolicyFindings.ts +1 -1
- package/integrations/lifecycle/cli.ts +9 -7
- package/integrations/mcp/enterpriseServer.ts +18 -1
- package/integrations/notifications/emitAuditSummaryNotification.ts +21 -1
- package/integrations/sdd/policy.ts +3 -1
- package/package.json +1 -1
- package/scripts/framework-menu-system-notifications-cause.ts +26 -0
- package/scripts/framework-menu-system-notifications-remediation.ts +26 -0
- package/scripts/framework-menu-system-notifications-tracking.ts +42 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [6.3.125] - 2026-04-28
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Mensajes coherentes para bloqueos `gate.blocked`:** las notificaciones y diálogos traducen `EVIDENCE_GATE_BLOCKED`, tracking canónico y atomicidad a causas humanas en vez de mostrar copy interno en inglés.
|
|
14
|
+
- **Tracking como causa accionable:** cuando existe `active_entries` / `tracking_source`, la remediación prioriza corregir el MD de tracking y deja de sugerir `policy reconcile && sdd validate` como solución principal.
|
|
15
|
+
- **Cierre de `PUMUKI-INC-118`:** el evento central de bloqueo enriquece la causa con contexto de tracking antes de construir banners, diálogos macOS y payloads de sistema.
|
|
16
|
+
|
|
9
17
|
## [6.3.123] - 2026-04-28
|
|
10
18
|
|
|
11
19
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.125
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
const detectorFiles = [
|
|
7
|
+
'core/facts/detectors/text/ios.ts',
|
|
8
|
+
'core/facts/detectors/text/android.ts',
|
|
9
|
+
'core/facts/detectors/typescript/index.ts',
|
|
10
|
+
'core/facts/detectors/security/securityCredentials.ts',
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const forbiddenDecisionThresholds = [
|
|
14
|
+
/typeDeclarations\.length\s*<\s*\d+/,
|
|
15
|
+
/conformingTypes\.length\s*<\s*\d+/,
|
|
16
|
+
/trim\(\)\.length\s*>=\s*\d+/,
|
|
17
|
+
/relatedNodes\.length\s*<\s*\d+/,
|
|
18
|
+
/typedCaseCount\s*>=\s*\d+/,
|
|
19
|
+
/caseNodes\.length\s*<\s*\d+/,
|
|
20
|
+
/branchNodes\.length\s*<\s*\d+/,
|
|
21
|
+
/slice\(0,\s*\d+\)/,
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
test('detectores AST estructurales no usan umbrales internos para decidir skills', () => {
|
|
25
|
+
const violations = detectorFiles.flatMap((filePath) => {
|
|
26
|
+
const content = readFileSync(join(process.cwd(), filePath), 'utf8');
|
|
27
|
+
return forbiddenDecisionThresholds
|
|
28
|
+
.filter((pattern) => pattern.test(content))
|
|
29
|
+
.map((pattern) => `${filePath}: ${pattern.source}`);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.deepEqual(violations, []);
|
|
33
|
+
});
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
hasWeakTokenGenerationWithCryptoRandomUuid,
|
|
16
16
|
} from './index';
|
|
17
17
|
|
|
18
|
-
test('hasHardcodedSecretTokenLiteral detecta literales
|
|
18
|
+
test('hasHardcodedSecretTokenLiteral detecta literales reales en identificadores sensibles', () => {
|
|
19
19
|
const ast = {
|
|
20
20
|
type: 'VariableDeclarator',
|
|
21
21
|
id: { type: 'Identifier', name: 'apiToken' },
|
|
@@ -24,11 +24,17 @@ test('hasHardcodedSecretTokenLiteral detecta literales fuertes en identificadore
|
|
|
24
24
|
const safeAst = {
|
|
25
25
|
type: 'VariableDeclarator',
|
|
26
26
|
id: { type: 'Identifier', name: 'apiToken' },
|
|
27
|
-
init: { type: 'StringLiteral', value: '
|
|
27
|
+
init: { type: 'StringLiteral', value: 'example' },
|
|
28
|
+
};
|
|
29
|
+
const shortRealSecretAst = {
|
|
30
|
+
type: 'VariableDeclarator',
|
|
31
|
+
id: { type: 'Identifier', name: 'apiToken' },
|
|
32
|
+
init: { type: 'StringLiteral', value: 'prod' },
|
|
28
33
|
};
|
|
29
34
|
|
|
30
35
|
assert.equal(hasHardcodedSecretTokenLiteral(ast), true);
|
|
31
36
|
assert.equal(hasHardcodedSecretTokenLiteral(safeAst), false);
|
|
37
|
+
assert.equal(hasHardcodedSecretTokenLiteral(shortRealSecretAst), true);
|
|
32
38
|
});
|
|
33
39
|
|
|
34
40
|
test('hasInsecureTokenGenerationWithMathRandom detecta Math.random en asignacion sensible', () => {
|
|
@@ -11,16 +11,21 @@ import {
|
|
|
11
11
|
hasWeakTokenGenerationWithCryptoRandomUuid,
|
|
12
12
|
} from './securityCredentials';
|
|
13
13
|
|
|
14
|
-
test('hasHardcodedSecretTokenLiteral detecta literal
|
|
14
|
+
test('hasHardcodedSecretTokenLiteral detecta literal real en identificador sensible', () => {
|
|
15
15
|
const hardcodedSecretAst = {
|
|
16
16
|
type: 'VariableDeclarator',
|
|
17
17
|
id: { type: 'Identifier', name: 'apiKey' },
|
|
18
18
|
init: { type: 'StringLiteral', value: 'super-secret-key-123' },
|
|
19
19
|
};
|
|
20
|
-
const
|
|
20
|
+
const placeholderAst = {
|
|
21
21
|
type: 'VariableDeclarator',
|
|
22
22
|
id: { type: 'Identifier', name: 'apiKey' },
|
|
23
|
-
init: { type: 'StringLiteral', value: '
|
|
23
|
+
init: { type: 'StringLiteral', value: 'replace-me' },
|
|
24
|
+
};
|
|
25
|
+
const shortRealSecretAst = {
|
|
26
|
+
type: 'VariableDeclarator',
|
|
27
|
+
id: { type: 'Identifier', name: 'apiKey' },
|
|
28
|
+
init: { type: 'StringLiteral', value: 'prod' },
|
|
24
29
|
};
|
|
25
30
|
const nonSensitiveIdentifierAst = {
|
|
26
31
|
type: 'VariableDeclarator',
|
|
@@ -29,7 +34,8 @@ test('hasHardcodedSecretTokenLiteral detecta literal largo en identificador sens
|
|
|
29
34
|
};
|
|
30
35
|
|
|
31
36
|
assert.equal(hasHardcodedSecretTokenLiteral(hardcodedSecretAst), true);
|
|
32
|
-
assert.equal(hasHardcodedSecretTokenLiteral(
|
|
37
|
+
assert.equal(hasHardcodedSecretTokenLiteral(placeholderAst), false);
|
|
38
|
+
assert.equal(hasHardcodedSecretTokenLiteral(shortRealSecretAst), true);
|
|
33
39
|
assert.equal(hasHardcodedSecretTokenLiteral(nonSensitiveIdentifierAst), false);
|
|
34
40
|
});
|
|
35
41
|
|
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { collectNodeLineMatches, hasNode, isObject } from '../utils/astHelpers';
|
|
2
2
|
|
|
3
3
|
const sensitiveIdentifierPattern = /(secret|token|password|api[_-]?key)/i;
|
|
4
|
+
const placeholderSecretLiteralPattern =
|
|
5
|
+
/^(?:changeme|change-me|change_me|replace-me|replace_me|todo|tbd|example|sample|dummy|test|testing|placeholder|your[_-]?(?:secret|token|password|api[_-]?key)|xxx+)$/i;
|
|
4
6
|
|
|
5
|
-
const
|
|
7
|
+
const hasCredentialLiteralValue = (value: unknown): boolean => {
|
|
6
8
|
if (!isObject(value)) {
|
|
7
9
|
return false;
|
|
8
10
|
}
|
|
9
11
|
if (value.type === 'StringLiteral') {
|
|
10
|
-
return typeof value.value === 'string' && value.value
|
|
12
|
+
return typeof value.value === 'string' && isCredentialLiteral(value.value);
|
|
11
13
|
}
|
|
12
14
|
if (
|
|
13
15
|
value.type === 'TemplateLiteral' &&
|
|
@@ -17,11 +19,16 @@ const hasStrongLiteralValue = (value: unknown): boolean => {
|
|
|
17
19
|
value.quasis.length === 1
|
|
18
20
|
) {
|
|
19
21
|
const cooked = value.quasis[0]?.value?.cooked;
|
|
20
|
-
return typeof cooked === 'string' && cooked
|
|
22
|
+
return typeof cooked === 'string' && isCredentialLiteral(cooked);
|
|
21
23
|
}
|
|
22
24
|
return false;
|
|
23
25
|
};
|
|
24
26
|
|
|
27
|
+
const isCredentialLiteral = (value: string): boolean => {
|
|
28
|
+
const normalized = value.trim();
|
|
29
|
+
return normalized.length > 0 && !placeholderSecretLiteralPattern.test(normalized);
|
|
30
|
+
};
|
|
31
|
+
|
|
25
32
|
const containsMathRandomCall = (candidate: unknown): boolean => {
|
|
26
33
|
return hasNode(candidate, (value) => {
|
|
27
34
|
if (value.type !== 'CallExpression') {
|
|
@@ -109,7 +116,7 @@ const isHardcodedSecretTokenLiteralNode = (value: Record<string, string | number
|
|
|
109
116
|
if (!sensitiveIdentifierPattern.test(idNode.name as string)) {
|
|
110
117
|
return false;
|
|
111
118
|
}
|
|
112
|
-
return
|
|
119
|
+
return hasCredentialLiteralValue(value.init);
|
|
113
120
|
};
|
|
114
121
|
|
|
115
122
|
const isInsecureTokenGenerationWithMathRandomNode = (value: Record<string, string | number | boolean | bigint | symbol | null | Date | object>): boolean => {
|
|
@@ -699,9 +699,6 @@ export const findKotlinLiskovSubstitutionMatch = (
|
|
|
699
699
|
}
|
|
700
700
|
|
|
701
701
|
const typeDeclarations = parseKotlinTypeDeclarations(source);
|
|
702
|
-
if (typeDeclarations.length < 2) {
|
|
703
|
-
return undefined;
|
|
704
|
-
}
|
|
705
702
|
|
|
706
703
|
const sourceLines = source.split(/\r?\n/);
|
|
707
704
|
|
|
@@ -714,9 +711,6 @@ export const findKotlinLiskovSubstitutionMatch = (
|
|
|
714
711
|
const conformingTypes = typeDeclarations.filter((typeDeclaration) =>
|
|
715
712
|
typeDeclaration.conformances.includes(interfaceDeclaration.name)
|
|
716
713
|
);
|
|
717
|
-
if (conformingTypes.length < 2) {
|
|
718
|
-
continue;
|
|
719
|
-
}
|
|
720
714
|
|
|
721
715
|
for (const memberName of memberNames) {
|
|
722
716
|
let safeType: KotlinTypeDeclaration | undefined;
|
|
@@ -1435,9 +1435,6 @@ export const findSwiftLiskovSubstitutionMatch = (
|
|
|
1435
1435
|
}
|
|
1436
1436
|
|
|
1437
1437
|
const typeDeclarations = parseSwiftTypeDeclarations(source);
|
|
1438
|
-
if (typeDeclarations.length < 2) {
|
|
1439
|
-
return undefined;
|
|
1440
|
-
}
|
|
1441
1438
|
|
|
1442
1439
|
const sourceLines = source.split(/\r?\n/);
|
|
1443
1440
|
|
|
@@ -1450,9 +1447,6 @@ export const findSwiftLiskovSubstitutionMatch = (
|
|
|
1450
1447
|
const conformingTypes = typeDeclarations.filter((typeDeclaration) =>
|
|
1451
1448
|
typeDeclaration.conformances.includes(protocolDeclaration.name)
|
|
1452
1449
|
);
|
|
1453
|
-
if (conformingTypes.length < 2) {
|
|
1454
|
-
continue;
|
|
1455
|
-
}
|
|
1456
1450
|
|
|
1457
1451
|
for (const memberName of memberNames) {
|
|
1458
1452
|
let safeType: SwiftTypeDeclaration | undefined;
|
|
@@ -54,7 +54,7 @@ export const collectTrackingActiveEntriesFromMarkdown = (
|
|
|
54
54
|
const bulletMatch = line.match(/^- 🚧 (`?P[0-9A-Za-z.-]+`?)/u);
|
|
55
55
|
if (bulletMatch) {
|
|
56
56
|
entries.push({
|
|
57
|
-
taskId: bulletMatch[1]!.
|
|
57
|
+
taskId: bulletMatch[1]!.replace(/`/gu, '').trim(),
|
|
58
58
|
lineNumber: index + 1,
|
|
59
59
|
});
|
|
60
60
|
continue;
|
|
@@ -1689,16 +1689,18 @@ export type PreWriteOpenSpecBootstrapTrace = {
|
|
|
1689
1689
|
export const buildPreWriteExperimentalEnableAdvisoryCommand = (
|
|
1690
1690
|
repoRoot: string = process.cwd()
|
|
1691
1691
|
): string =>
|
|
1692
|
-
`PUMUKI_EXPERIMENTAL_PRE_WRITE=advisory ${buildPinnedPumukiNpxCommand(
|
|
1693
|
-
|
|
1694
|
-
|
|
1692
|
+
`PUMUKI_EXPERIMENTAL_PRE_WRITE=advisory ${buildPinnedPumukiNpxCommand({
|
|
1693
|
+
repoRoot,
|
|
1694
|
+
executableAndArgs: 'pumuki sdd validate --stage=PRE_WRITE --json',
|
|
1695
|
+
})}`;
|
|
1695
1696
|
export const buildSddExperimentalEnableAdvisoryCommand = (
|
|
1696
1697
|
stage: SddStage,
|
|
1697
1698
|
repoRoot: string = process.cwd()
|
|
1698
1699
|
): string =>
|
|
1699
|
-
`PUMUKI_EXPERIMENTAL_SDD=advisory ${buildPinnedPumukiNpxCommand(
|
|
1700
|
-
|
|
1701
|
-
|
|
1700
|
+
`PUMUKI_EXPERIMENTAL_SDD=advisory ${buildPinnedPumukiNpxCommand({
|
|
1701
|
+
repoRoot,
|
|
1702
|
+
executableAndArgs: `pumuki sdd validate --stage=${stage} --json`,
|
|
1703
|
+
})}`;
|
|
1702
1704
|
const buildAnalyticsExperimentalEnableCommand = (action: AnalyticsHotspotsCommand): string =>
|
|
1703
1705
|
`PUMUKI_EXPERIMENTAL_ANALYTICS=advisory npx --yes --package pumuki@latest pumuki analytics hotspots ${action} --json`;
|
|
1704
1706
|
const SAAS_INGESTION_ENABLE_ADVISORY_COMMAND =
|
|
@@ -1804,7 +1806,7 @@ export const buildPreWriteExperimentalDisabledResult = (params: {
|
|
|
1804
1806
|
layer: 'experimental',
|
|
1805
1807
|
activation_env: 'PUMUKI_EXPERIMENTAL_PRE_WRITE',
|
|
1806
1808
|
legacy_activation_env: 'PUMUKI_PREWRITE_ENFORCEMENT',
|
|
1807
|
-
activation_command:
|
|
1809
|
+
activation_command: buildPreWriteExperimentalEnableAdvisoryCommand(),
|
|
1808
1810
|
},
|
|
1809
1811
|
},
|
|
1810
1812
|
});
|
|
@@ -5,6 +5,7 @@ import { execFileSync as runBinarySync } from 'node:child_process';
|
|
|
5
5
|
import { existsSync } from 'node:fs';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
7
|
import { readLifecycleStatus, runLifecycleAudit } from '../lifecycle';
|
|
8
|
+
import { GitService } from '../git/GitService';
|
|
8
9
|
import { resolveMcpEnterpriseExperimentalFeature } from '../policy/experimentalFeatures';
|
|
9
10
|
import { evaluateSddPolicy, readSddStatus } from '../sdd';
|
|
10
11
|
import type { SddStage } from '../sdd';
|
|
@@ -337,6 +338,20 @@ type EnterpriseToolExecution = {
|
|
|
337
338
|
warnings?: ReadonlyArray<string>;
|
|
338
339
|
};
|
|
339
340
|
|
|
341
|
+
class EnterpriseRepoGitService extends GitService {
|
|
342
|
+
constructor(private readonly repoRoot: string) {
|
|
343
|
+
super();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
override resolveRepoRoot(): string {
|
|
347
|
+
return this.repoRoot;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
override runGit(args: ReadonlyArray<string>, cwd: string = this.repoRoot): string {
|
|
351
|
+
return super.runGit(args, cwd);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
340
355
|
type CriticalToolGuardResult = {
|
|
341
356
|
allowed: boolean;
|
|
342
357
|
stage: SddStage;
|
|
@@ -398,9 +413,11 @@ const executeEnterpriseTool = async (
|
|
|
398
413
|
switch (toolName) {
|
|
399
414
|
case 'pre_write_guard': {
|
|
400
415
|
const audit = await runLifecycleAudit({
|
|
401
|
-
repoRoot,
|
|
402
416
|
stage: 'PRE_WRITE',
|
|
403
417
|
auditMode: 'gate',
|
|
418
|
+
dependencies: {
|
|
419
|
+
git: new EnterpriseRepoGitService(repoRoot),
|
|
420
|
+
},
|
|
404
421
|
});
|
|
405
422
|
return {
|
|
406
423
|
name: toolName,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AiEvidenceV2_1 } from '../evidence/schema';
|
|
2
2
|
import { readEvidence } from '../evidence/readEvidence';
|
|
3
3
|
import type { AiGateCheckResult } from '../gate/evaluateAiGate';
|
|
4
|
+
import { appendTrackingActionableContext } from '../git/aiGateRepoPolicyFindings';
|
|
4
5
|
import {
|
|
5
6
|
emitSystemNotification,
|
|
6
7
|
type PumukiCriticalNotificationEvent,
|
|
@@ -34,6 +35,22 @@ const isTruthyEnvValue = (value?: string): boolean => {
|
|
|
34
35
|
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
35
36
|
};
|
|
36
37
|
|
|
38
|
+
const withTrackingContext = (params: {
|
|
39
|
+
repoRoot: string;
|
|
40
|
+
causeMessage: string;
|
|
41
|
+
}): string => {
|
|
42
|
+
if (
|
|
43
|
+
params.causeMessage.includes('active_entries=') ||
|
|
44
|
+
params.causeMessage.includes('tracking_source=')
|
|
45
|
+
) {
|
|
46
|
+
return params.causeMessage;
|
|
47
|
+
}
|
|
48
|
+
return appendTrackingActionableContext({
|
|
49
|
+
repoRoot: params.repoRoot,
|
|
50
|
+
message: params.causeMessage,
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
37
54
|
export const shouldEmitAuditSummaryNotificationForStage = (
|
|
38
55
|
stage: AuditSummaryNotificationStage,
|
|
39
56
|
env: NodeJS.ProcessEnv = process.env
|
|
@@ -155,7 +172,10 @@ export const emitGateBlockedNotification = (
|
|
|
155
172
|
stage: params.stage,
|
|
156
173
|
totalViolations: params.totalViolations,
|
|
157
174
|
causeCode: params.causeCode,
|
|
158
|
-
causeMessage:
|
|
175
|
+
causeMessage: withTrackingContext({
|
|
176
|
+
repoRoot: params.repoRoot,
|
|
177
|
+
causeMessage: params.causeMessage,
|
|
178
|
+
}),
|
|
159
179
|
remediation: params.remediation,
|
|
160
180
|
},
|
|
161
181
|
repoRoot: params.repoRoot,
|
|
@@ -167,6 +167,7 @@ const evaluateActiveChangeCompleteness = (params: {
|
|
|
167
167
|
|
|
168
168
|
const evaluateSessionRequirements = (params: {
|
|
169
169
|
status: SddStatusPayload;
|
|
170
|
+
repoRoot: string;
|
|
170
171
|
autoRefreshEnabled: boolean;
|
|
171
172
|
autoRefreshAttempted: boolean;
|
|
172
173
|
autoRefreshError?: string;
|
|
@@ -335,7 +336,7 @@ export const evaluateSddPolicy = (params: {
|
|
|
335
336
|
experimentalSource: sddExperimentalFeature.source,
|
|
336
337
|
activation_env: sddExperimentalFeature.activationVariable,
|
|
337
338
|
legacy_activation_env: sddExperimentalFeature.legacyActivationVariable,
|
|
338
|
-
activation_command: buildSddExperimentalEnableCommand(params.stage,
|
|
339
|
+
activation_command: buildSddExperimentalEnableCommand(params.stage, repoRoot),
|
|
339
340
|
},
|
|
340
341
|
},
|
|
341
342
|
};
|
|
@@ -428,6 +429,7 @@ export const evaluateSddPolicy = (params: {
|
|
|
428
429
|
|
|
429
430
|
const sessionDecision = evaluateSessionRequirements({
|
|
430
431
|
status,
|
|
432
|
+
repoRoot,
|
|
431
433
|
autoRefreshEnabled,
|
|
432
434
|
autoRefreshAttempted,
|
|
433
435
|
autoRefreshError,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.125",
|
|
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": {
|
|
@@ -3,8 +3,13 @@ import {
|
|
|
3
3
|
normalizeNotificationText,
|
|
4
4
|
truncateNotificationText,
|
|
5
5
|
} from './framework-menu-system-notifications-text';
|
|
6
|
+
import {
|
|
7
|
+
buildNotificationTrackingCauseSummary,
|
|
8
|
+
extractNotificationTrackingContext,
|
|
9
|
+
} from './framework-menu-system-notifications-tracking';
|
|
6
10
|
|
|
7
11
|
const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
12
|
+
EVIDENCE_GATE_BLOCKED: 'El gate de evidencia/gobernanza está bloqueado.',
|
|
8
13
|
EVIDENCE_MISSING: 'Falta evidencia para validar este paso.',
|
|
9
14
|
EVIDENCE_INVALID: 'La evidencia actual es inválida.',
|
|
10
15
|
EVIDENCE_CHAIN_INVALID: 'La cadena de evidencia no es válida.',
|
|
@@ -18,6 +23,13 @@ const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
18
23
|
OPENSPEC_MISSING: 'OpenSpec no está instalado en este repositorio.',
|
|
19
24
|
MCP_ENTERPRISE_RECEIPT_MISSING: 'Falta el recibo enterprise de MCP.',
|
|
20
25
|
BACKEND_AVOID_EXPLICIT_ANY: 'Se detectó uso de "any" explícito en backend.',
|
|
26
|
+
GIT_ATOMICITY_TOO_MANY_SCOPES: 'El cambio toca demasiados scopes para un commit atómico.',
|
|
27
|
+
TRACKING_CANONICAL_IN_PROGRESS_INVALID:
|
|
28
|
+
'El tracking canónico tiene una tarea activa inválida.',
|
|
29
|
+
TRACKING_CANONICAL_SOURCE_CONFLICT:
|
|
30
|
+
'Hay conflicto entre fuentes de tracking canónico.',
|
|
31
|
+
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
32
|
+
'No hay reglas activas para cambios de código.',
|
|
21
33
|
};
|
|
22
34
|
|
|
23
35
|
const ENGLISH_CAUSE_HINTS = [
|
|
@@ -53,9 +65,19 @@ const buildGenericSpanishBlockedCauseSummary = (
|
|
|
53
65
|
|
|
54
66
|
const toKnownSpanishCauseFromMessage = (message: string): string | null => {
|
|
55
67
|
const normalized = message.toLowerCase();
|
|
68
|
+
const trackingContext = extractNotificationTrackingContext(message);
|
|
69
|
+
if (trackingContext) {
|
|
70
|
+
return buildNotificationTrackingCauseSummary(trackingContext);
|
|
71
|
+
}
|
|
56
72
|
if (normalized.includes('avoid explicit any')) {
|
|
57
73
|
return BLOCKED_CAUSE_SUMMARY_BY_CODE.BACKEND_AVOID_EXPLICIT_ANY;
|
|
58
74
|
}
|
|
75
|
+
if (normalized.includes('atomicity')) {
|
|
76
|
+
return BLOCKED_CAUSE_SUMMARY_BY_CODE.GIT_ATOMICITY_TOO_MANY_SCOPES;
|
|
77
|
+
}
|
|
78
|
+
if (normalized.includes('evidence ai gate status is blocked')) {
|
|
79
|
+
return BLOCKED_CAUSE_SUMMARY_BY_CODE.EVIDENCE_GATE_BLOCKED;
|
|
80
|
+
}
|
|
59
81
|
if (normalized.includes('evidence is stale')) {
|
|
60
82
|
return BLOCKED_CAUSE_SUMMARY_BY_CODE.EVIDENCE_STALE;
|
|
61
83
|
}
|
|
@@ -74,6 +96,10 @@ export const resolveBlockedCauseSummary = (
|
|
|
74
96
|
event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>,
|
|
75
97
|
causeCode: string
|
|
76
98
|
): string => {
|
|
99
|
+
const trackingContext = extractNotificationTrackingContext(event.causeMessage);
|
|
100
|
+
if (trackingContext) {
|
|
101
|
+
return buildNotificationTrackingCauseSummary(trackingContext);
|
|
102
|
+
}
|
|
77
103
|
const mapped = BLOCKED_CAUSE_SUMMARY_BY_CODE[causeCode];
|
|
78
104
|
if (mapped) {
|
|
79
105
|
return mapped;
|
|
@@ -3,8 +3,16 @@ import {
|
|
|
3
3
|
normalizeNotificationText,
|
|
4
4
|
truncateNotificationText,
|
|
5
5
|
} from './framework-menu-system-notifications-text';
|
|
6
|
+
import {
|
|
7
|
+
extractNotificationTrackingContext,
|
|
8
|
+
TRACKING_BLOCKED_REMEDIATION,
|
|
9
|
+
} from './framework-menu-system-notifications-tracking';
|
|
10
|
+
|
|
11
|
+
type BlockedRemediationVariant = 'banner' | 'dialog';
|
|
6
12
|
|
|
7
13
|
const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
14
|
+
EVIDENCE_GATE_BLOCKED:
|
|
15
|
+
'Revisa status/doctor para ver la causa exacta del gate, corrígela y revalida.',
|
|
8
16
|
EVIDENCE_MISSING: 'Genera la evidencia del slice actual y vuelve a validar esta fase.',
|
|
9
17
|
EVIDENCE_INVALID: 'Regenera la evidencia de esta iteración y repite la validación.',
|
|
10
18
|
EVIDENCE_CHAIN_INVALID: 'Regenera la evidencia para restaurar la cadena de integridad y vuelve a validar.',
|
|
@@ -19,6 +27,10 @@ const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
19
27
|
BACKEND_AVOID_EXPLICIT_ANY: 'Sustituye `any` por tipos concretos en backend y relanza el gate.',
|
|
20
28
|
GIT_ATOMICITY_TOO_MANY_SCOPES: 'Divide el cambio por scope o en commits más pequeños y vuelve a ejecutar el gate.',
|
|
21
29
|
SOLID_HEURISTIC: 'Corrige la violación detectada y vuelve a ejecutar el gate.',
|
|
30
|
+
TRACKING_CANONICAL_IN_PROGRESS_INVALID: TRACKING_BLOCKED_REMEDIATION,
|
|
31
|
+
TRACKING_CANONICAL_SOURCE_CONFLICT: TRACKING_BLOCKED_REMEDIATION,
|
|
32
|
+
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
33
|
+
'Ejecuta `pumuki policy reconcile --strict --json` y revalida antes de continuar.',
|
|
22
34
|
};
|
|
23
35
|
|
|
24
36
|
const BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT: Readonly<Record<BlockedRemediationVariant, number>> = {
|
|
@@ -39,6 +51,11 @@ const normalizeBlockedRemediation = (value: string): string =>
|
|
|
39
51
|
const resolveFallbackRemediation = (causeCode: string): string =>
|
|
40
52
|
BLOCKED_REMEDIATION_BY_CODE[causeCode] ?? GENERIC_BLOCKED_REMEDIATION;
|
|
41
53
|
|
|
54
|
+
const isGenericPolicyReconcileRemediation = (message: string): boolean => {
|
|
55
|
+
const normalized = message.toLowerCase();
|
|
56
|
+
return normalized.includes('policy reconcile') && normalized.includes('sdd validate');
|
|
57
|
+
};
|
|
58
|
+
|
|
42
59
|
const hasEnglishHints = (message: string): boolean => {
|
|
43
60
|
const normalized = message.toLowerCase();
|
|
44
61
|
return [
|
|
@@ -88,7 +105,16 @@ export const resolveBlockedRemediation = (
|
|
|
88
105
|
const fromEvent = event.remediation
|
|
89
106
|
? normalizeBlockedRemediation(event.remediation)
|
|
90
107
|
: '';
|
|
108
|
+
if (extractNotificationTrackingContext(event.causeMessage)) {
|
|
109
|
+
return truncateNotificationText(TRACKING_BLOCKED_REMEDIATION, maxLength);
|
|
110
|
+
}
|
|
91
111
|
if (fromEvent.length > 0) {
|
|
112
|
+
if (
|
|
113
|
+
causeCode === 'EVIDENCE_GATE_BLOCKED' &&
|
|
114
|
+
isGenericPolicyReconcileRemediation(fromEvent)
|
|
115
|
+
) {
|
|
116
|
+
return truncateNotificationText(resolveFallbackRemediation(causeCode), maxLength);
|
|
117
|
+
}
|
|
92
118
|
const translated = toKnownSpanishRemediationFromMessage(fromEvent, causeCode);
|
|
93
119
|
if (translated) {
|
|
94
120
|
return truncateNotificationText(translated, maxLength);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export type NotificationTrackingContext = {
|
|
2
|
+
activeEntry?: string;
|
|
3
|
+
trackingSource?: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
const TRACKING_CONTEXT_PATTERN = /\b(active_entries=|tracking_source=|TRACKING_CANONICAL_)/u;
|
|
7
|
+
|
|
8
|
+
export const extractNotificationTrackingContext = (
|
|
9
|
+
message?: string
|
|
10
|
+
): NotificationTrackingContext | null => {
|
|
11
|
+
if (!message || !TRACKING_CONTEXT_PATTERN.test(message)) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const activeEntry = message
|
|
15
|
+
.match(/\bactive_entries=([^,\s]+)/u)?.[1]
|
|
16
|
+
?.replace(/@L\d+$/u, '')
|
|
17
|
+
.trim();
|
|
18
|
+
const trackingSource = message.match(/\btracking_source=([^\s]+)/u)?.[1]?.trim();
|
|
19
|
+
return {
|
|
20
|
+
activeEntry: activeEntry && activeEntry.length > 0 ? activeEntry : undefined,
|
|
21
|
+
trackingSource: trackingSource && trackingSource.length > 0 ? trackingSource : undefined,
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const buildNotificationTrackingCauseSummary = (
|
|
26
|
+
context: NotificationTrackingContext
|
|
27
|
+
): string => {
|
|
28
|
+
if (context.activeEntry && context.trackingSource) {
|
|
29
|
+
return `Tracking bloqueado: ${context.activeEntry} en ${context.trackingSource}.`;
|
|
30
|
+
}
|
|
31
|
+
if (context.activeEntry) {
|
|
32
|
+
return `Tracking bloqueado: ${context.activeEntry}.`;
|
|
33
|
+
}
|
|
34
|
+
if (context.trackingSource) {
|
|
35
|
+
return `Tracking bloqueado en ${context.trackingSource}.`;
|
|
36
|
+
}
|
|
37
|
+
return 'El tracking canónico del repo bloquea la governance.';
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const TRACKING_BLOCKED_REMEDIATION =
|
|
41
|
+
'Corrige el MD de tracking: deja una única tarea activa válida y vuelve a ejecutar el gate.';
|
|
42
|
+
|