pumuki 6.3.274 → 6.3.276
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/docs/operations/RELEASE_NOTES.md +8 -0
- package/integrations/evidence/blockingCauses.ts +82 -0
- package/integrations/gate/evaluateAiGate.ts +66 -6
- package/integrations/git/runPlatformGate.ts +5 -56
- package/integrations/git/runPlatformGateEvaluation.ts +3 -0
- package/integrations/git/runPlatformGateOutput.ts +31 -3
- package/integrations/git/stageRunners.ts +7 -0
- package/integrations/lifecycle/cliSdd.ts +10 -0
- package/integrations/lifecycle/doctor.ts +15 -1
- package/integrations/lifecycle/status.ts +13 -1
- package/integrations/lifecycle/watch.ts +7 -0
- package/integrations/mcp/evidencePayloadStatus.ts +4 -0
- package/integrations/mcp/evidencePayloadSummary.ts +4 -0
- package/package.json +1 -1
- package/scripts/framework-menu-consumer-runtime-actions.ts +51 -1
- package/scripts/framework-menu-consumer-runtime-audit.ts +115 -38
- package/scripts/framework-menu-consumer-runtime-menu.ts +29 -9
- package/scripts/framework-menu-consumer-runtime-types.ts +2 -0
- package/scripts/framework-menu-gate-lib.ts +2 -0
- package/scripts/framework-menu-layout-data.ts +3 -3
- package/scripts/framework-menu-system-notifications-cause.ts +2 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +5 -1
- package/scripts/framework-menu-system-notifications-remediation.ts +10 -0
- package/scripts/framework-menu.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [6.3.276] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
- `PUMUKI-INC-146`: PRE_WRITE no longer requires `skills.ios.critical-test-quality` for iOS/SwiftUI production-only slices that do not touch test files. The rule remains hard-blocking when the PRE_WRITE scope includes Swift test paths (`/Tests/`, `/UITests/`, `*Test.swift`, `*Tests.swift`, `*.spec.swift`), preserving XCTest/Swift Testing quality enforcement without blocking unrelated visual/product commits.
|
|
6
|
+
|
|
7
|
+
## [6.3.275] - 2026-05-18
|
|
8
|
+
|
|
9
|
+
- PRE_WRITE skills coverage: `PRE_WRITE` now loads the full runtime skills contract for the detected platform, so Web/Frontend slices evaluate `skills.frontend.*` before commit instead of failing with `EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE` while no frontend rule was active.
|
|
10
|
+
|
|
3
11
|
## [6.3.274] - 2026-05-18
|
|
4
12
|
|
|
5
13
|
- Notifications: gate blocks now propagate every blocking cause as `blockingCauses[]`, emit one `[pumuki][blocked-cause]` line per violation with rule, file, reason and fix, and show rule/file/remediation in the macOS dialog without hiding AST skill violations behind tracking or governance.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.276
|
|
@@ -4,6 +4,14 @@ This file tracks the active deterministic framework line used in this repository
|
|
|
4
4
|
Canonical release chronology lives in `CHANGELOG.md`.
|
|
5
5
|
This file keeps only the operational highlights and rollout notes that matter while running the framework.
|
|
6
6
|
|
|
7
|
+
### 2026-05-18 (v6.3.276)
|
|
8
|
+
|
|
9
|
+
- `PUMUKI-INC-146`: PRE_WRITE keeps `skills.ios.critical-test-quality` fail-closed for real Swift test slices, but stops applying that test-quality critical rule to iOS/SwiftUI production-only slices. RuralGo wordmark/splash-style commits should still be blocked by real iOS AST/skills violations, SDD, context or evidence issues, but not by missing XCTest quality coverage when no test file is in scope.
|
|
10
|
+
|
|
11
|
+
### 2026-05-18 (v6.3.275)
|
|
12
|
+
|
|
13
|
+
- `PUMUKI-INC-143`: PRE_WRITE now loads the full runtime skills contract for detected platforms. Frontend slices now materialize `skills.frontend.*` in `active_rule_ids` and `evaluated_rule_ids` instead of failing on missing scope coverage before any frontend rule can run.
|
|
14
|
+
|
|
7
15
|
### 2026-05-18 (v6.3.274)
|
|
8
16
|
|
|
9
17
|
- Notifications now carry all blocking causes instead of a single collapsed cause. Hooks and audit output emit `[pumuki][blocked-cause]` for each violation with code, rule, file, reason and fix; macOS blocked dialogs show rule/file/solution details and no longer hide AST skill violations behind tracking/governance blockers.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { AiEvidenceV2_1, CompatibilityViolation, SnapshotFinding } from './schema';
|
|
2
|
+
|
|
3
|
+
export type EvidenceBlockingCause = {
|
|
4
|
+
ruleId: string;
|
|
5
|
+
code: string;
|
|
6
|
+
severity: string;
|
|
7
|
+
message: string;
|
|
8
|
+
file?: string;
|
|
9
|
+
lines?: SnapshotFinding['lines'];
|
|
10
|
+
remediation?: string;
|
|
11
|
+
source: 'snapshot.findings' | 'ai_gate.violations';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const isBlockingSeverity = (severity: string): boolean =>
|
|
15
|
+
severity.toUpperCase() === 'CRITICAL' || severity.toUpperCase() === 'ERROR';
|
|
16
|
+
|
|
17
|
+
const toFindingBlockingCause = (finding: SnapshotFinding): EvidenceBlockingCause => ({
|
|
18
|
+
ruleId: finding.ruleId,
|
|
19
|
+
code: finding.code,
|
|
20
|
+
severity: finding.severity,
|
|
21
|
+
message: finding.message,
|
|
22
|
+
file: finding.file,
|
|
23
|
+
lines: finding.lines,
|
|
24
|
+
remediation: finding.expected_fix,
|
|
25
|
+
source: 'snapshot.findings',
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const toViolationBlockingCause = (
|
|
29
|
+
violation: CompatibilityViolation
|
|
30
|
+
): EvidenceBlockingCause => ({
|
|
31
|
+
ruleId: violation.ruleId,
|
|
32
|
+
code: violation.code,
|
|
33
|
+
severity: violation.level,
|
|
34
|
+
message: violation.message,
|
|
35
|
+
file: violation.file,
|
|
36
|
+
lines: violation.lines,
|
|
37
|
+
remediation: violation.expected_fix,
|
|
38
|
+
source: 'ai_gate.violations',
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const causeKey = (cause: EvidenceBlockingCause): string =>
|
|
42
|
+
[cause.source, cause.ruleId, cause.code, cause.file ?? '', String(cause.lines ?? '')].join('|');
|
|
43
|
+
|
|
44
|
+
export const extractEvidenceBlockingCauses = (
|
|
45
|
+
evidence: AiEvidenceV2_1
|
|
46
|
+
): ReadonlyArray<EvidenceBlockingCause> => {
|
|
47
|
+
const causes: EvidenceBlockingCause[] = [];
|
|
48
|
+
const seen = new Set<string>();
|
|
49
|
+
|
|
50
|
+
for (const finding of evidence.snapshot.findings) {
|
|
51
|
+
if (finding.blocking === false || !isBlockingSeverity(finding.severity)) {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const cause = toFindingBlockingCause(finding);
|
|
55
|
+
const key = causeKey(cause);
|
|
56
|
+
if (!seen.has(key)) {
|
|
57
|
+
causes.push(cause);
|
|
58
|
+
seen.add(key);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const violation of evidence.ai_gate.violations) {
|
|
63
|
+
if (violation.blocking === false || !isBlockingSeverity(violation.level)) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const cause = toViolationBlockingCause(violation);
|
|
67
|
+
const key = causeKey(cause);
|
|
68
|
+
if (!seen.has(key)) {
|
|
69
|
+
causes.push(cause);
|
|
70
|
+
seen.add(key);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return causes;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const formatEvidenceBlockingCause = (cause: EvidenceBlockingCause): string => {
|
|
78
|
+
const location = cause.file ? ` file=${cause.file}` : '';
|
|
79
|
+
const lines = cause.lines ? ` lines=${String(cause.lines)}` : '';
|
|
80
|
+
const remediation = cause.remediation ? ` remediation=${cause.remediation}` : '';
|
|
81
|
+
return `${cause.ruleId} severity=${cause.severity} code=${cause.code}${location}${lines} message=${cause.message}${remediation}`;
|
|
82
|
+
};
|
|
@@ -401,6 +401,38 @@ const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> =>
|
|
|
401
401
|
}
|
|
402
402
|
};
|
|
403
403
|
|
|
404
|
+
const isIosTestQualityPath = (filePath: string): boolean => {
|
|
405
|
+
const normalized = normalizeChangedPath(filePath).toLowerCase();
|
|
406
|
+
if (!normalized.endsWith('.swift')) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
return normalized.includes('/tests/')
|
|
410
|
+
|| normalized.includes('/uitests/')
|
|
411
|
+
|| normalized.endsWith('test.swift')
|
|
412
|
+
|| normalized.endsWith('tests.swift')
|
|
413
|
+
|| normalized.endsWith('.spec.swift');
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const hasPreWriteIosTestQualityScope = (repoRoot: string): boolean =>
|
|
417
|
+
collectWorktreeChangedPaths(repoRoot).some((filePath) => isIosTestQualityPath(filePath));
|
|
418
|
+
|
|
419
|
+
const resolvePreWriteCriticalSkillsRules = (params: {
|
|
420
|
+
platform: PreWriteSkillsPlatform;
|
|
421
|
+
stage: AiGateStage;
|
|
422
|
+
repoRoot: string;
|
|
423
|
+
}): ReadonlyArray<string> => {
|
|
424
|
+
const ruleIds = PREWRITE_CRITICAL_SKILLS_RULES[params.platform];
|
|
425
|
+
if (
|
|
426
|
+
params.platform === 'ios'
|
|
427
|
+
&& params.stage === 'PRE_WRITE'
|
|
428
|
+
&& ruleIds.includes('skills.ios.critical-test-quality')
|
|
429
|
+
&& !hasPreWriteIosTestQualityScope(params.repoRoot)
|
|
430
|
+
) {
|
|
431
|
+
return ruleIds.filter((ruleId) => ruleId !== 'skills.ios.critical-test-quality');
|
|
432
|
+
}
|
|
433
|
+
return ruleIds;
|
|
434
|
+
};
|
|
435
|
+
|
|
404
436
|
const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
|
|
405
437
|
const normalized = normalizeChangedPath(filePath).toLowerCase();
|
|
406
438
|
if (platform === 'ios') {
|
|
@@ -552,6 +584,7 @@ const collectActiveRuleIdsCoverageViolations = (params: {
|
|
|
552
584
|
};
|
|
553
585
|
|
|
554
586
|
const collectPreWritePlatformSkillsViolations = (params: {
|
|
587
|
+
repoRoot: string;
|
|
555
588
|
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
556
589
|
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
557
590
|
skillsEnforcement: SkillsEnforcementResolution;
|
|
@@ -625,7 +658,11 @@ const collectPreWritePlatformSkillsViolations = (params: {
|
|
|
625
658
|
|
|
626
659
|
const missingCriticalRulesByPlatform: string[] = [];
|
|
627
660
|
for (const platform of detectedPlatforms) {
|
|
628
|
-
const requiredCriticalRuleIds =
|
|
661
|
+
const requiredCriticalRuleIds = resolvePreWriteCriticalSkillsRules({
|
|
662
|
+
platform,
|
|
663
|
+
stage: 'PRE_WRITE',
|
|
664
|
+
repoRoot: params.repoRoot,
|
|
665
|
+
});
|
|
629
666
|
if (requiredCriticalRuleIds.length === 0) {
|
|
630
667
|
continue;
|
|
631
668
|
}
|
|
@@ -716,14 +753,22 @@ const toSkillsContractAssessment = (params: {
|
|
|
716
753
|
platform,
|
|
717
754
|
required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
|
|
718
755
|
required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
719
|
-
required_critical_rule_ids:
|
|
756
|
+
required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
757
|
+
platform,
|
|
758
|
+
stage: params.stage,
|
|
759
|
+
repoRoot: params.repoRoot,
|
|
760
|
+
}),
|
|
720
761
|
required_any_transversal_critical_rule_ids: [
|
|
721
762
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
722
763
|
],
|
|
723
764
|
active_prefix_covered: false,
|
|
724
765
|
evaluated_prefix_covered: false,
|
|
725
766
|
missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
726
|
-
missing_critical_rule_ids:
|
|
767
|
+
missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
768
|
+
platform,
|
|
769
|
+
stage: params.stage,
|
|
770
|
+
repoRoot: params.repoRoot,
|
|
771
|
+
}),
|
|
727
772
|
transversal_critical_covered: false,
|
|
728
773
|
missing_any_transversal_critical_rule_ids: [
|
|
729
774
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
@@ -798,14 +843,22 @@ const toSkillsContractAssessment = (params: {
|
|
|
798
843
|
platform,
|
|
799
844
|
required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
|
|
800
845
|
required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
801
|
-
required_critical_rule_ids:
|
|
846
|
+
required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
847
|
+
platform,
|
|
848
|
+
stage: params.stage,
|
|
849
|
+
repoRoot: params.repoRoot,
|
|
850
|
+
}),
|
|
802
851
|
required_any_transversal_critical_rule_ids: [
|
|
803
852
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
804
853
|
],
|
|
805
854
|
active_prefix_covered: false,
|
|
806
855
|
evaluated_prefix_covered: false,
|
|
807
856
|
missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
808
|
-
missing_critical_rule_ids:
|
|
857
|
+
missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
858
|
+
platform,
|
|
859
|
+
stage: params.stage,
|
|
860
|
+
repoRoot: params.repoRoot,
|
|
861
|
+
}),
|
|
809
862
|
transversal_critical_covered: false,
|
|
810
863
|
missing_any_transversal_critical_rule_ids: [
|
|
811
864
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
@@ -859,7 +912,13 @@ const toSkillsContractAssessment = (params: {
|
|
|
859
912
|
for (const platform of assessmentPlatforms) {
|
|
860
913
|
const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
|
|
861
914
|
const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
|
|
862
|
-
const requiredCriticalRuleIds = [
|
|
915
|
+
const requiredCriticalRuleIds = [
|
|
916
|
+
...resolvePreWriteCriticalSkillsRules({
|
|
917
|
+
platform,
|
|
918
|
+
stage: params.stage,
|
|
919
|
+
repoRoot: params.repoRoot,
|
|
920
|
+
}),
|
|
921
|
+
];
|
|
863
922
|
const requiredAnyTransversalCriticalRuleIds = [
|
|
864
923
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
865
924
|
];
|
|
@@ -1057,6 +1116,7 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
1057
1116
|
|
|
1058
1117
|
violations.push(
|
|
1059
1118
|
...collectPreWritePlatformSkillsViolations({
|
|
1119
|
+
repoRoot: params.repoRoot,
|
|
1060
1120
|
evidence: params.evidence,
|
|
1061
1121
|
coverage,
|
|
1062
1122
|
skillsEnforcement,
|
|
@@ -32,7 +32,6 @@ import { createEmptyEvaluationMetrics } from '../evidence/evaluationMetrics';
|
|
|
32
32
|
import { createEmptySnapshotRulesCoverage } from '../evidence/rulesCoverage';
|
|
33
33
|
import { enforceTddBddPolicy } from '../tdd/enforcement';
|
|
34
34
|
import type { TddBddSnapshot } from '../tdd/types';
|
|
35
|
-
import { resolveSkillsEnforcement } from '../policy/skillsEnforcement';
|
|
36
35
|
import { applyTddBddEnforcement } from '../policy/tddBddEnforcement';
|
|
37
36
|
import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings';
|
|
38
37
|
import {
|
|
@@ -886,25 +885,7 @@ const applySkillsFindingEnforcement = (
|
|
|
886
885
|
if (!finding) {
|
|
887
886
|
return undefined;
|
|
888
887
|
}
|
|
889
|
-
|
|
890
|
-
if (skillsEnforcement.blocking) {
|
|
891
|
-
return finding;
|
|
892
|
-
}
|
|
893
|
-
return {
|
|
894
|
-
...finding,
|
|
895
|
-
severity: 'WARN',
|
|
896
|
-
blocking: false,
|
|
897
|
-
};
|
|
898
|
-
};
|
|
899
|
-
|
|
900
|
-
const toSoftPreCommitSkillsFinding = (params: {
|
|
901
|
-
finding: Finding | undefined;
|
|
902
|
-
enabled: boolean;
|
|
903
|
-
observedCodePaths: ReadonlyArray<string>;
|
|
904
|
-
}): Finding | undefined => {
|
|
905
|
-
void params.enabled;
|
|
906
|
-
void params.observedCodePaths;
|
|
907
|
-
return params.finding;
|
|
888
|
+
return finding;
|
|
908
889
|
};
|
|
909
890
|
|
|
910
891
|
export async function runPlatformGate(params: {
|
|
@@ -1281,42 +1262,10 @@ export async function runPlatformGate(params: {
|
|
|
1281
1262
|
const hasNativeBlockingFinding = findings.some(
|
|
1282
1263
|
(finding) => shouldBlockFromFinding(finding)
|
|
1283
1264
|
);
|
|
1284
|
-
const
|
|
1285
|
-
const
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
&& preCommitSoftSkillsEnabled
|
|
1289
|
-
&& lowRiskPreCommitWindow
|
|
1290
|
-
&& !sddBlockingFinding
|
|
1291
|
-
&& !degradedModeBlocks
|
|
1292
|
-
&& !shouldBlockFromFinding(policyAsCodeBlockingFinding)
|
|
1293
|
-
&& !shouldBlockFromFinding(coverageBlockingFinding)
|
|
1294
|
-
&& !shouldBlockFromFinding(declarativeRulesClassificationFinding)
|
|
1295
|
-
&& !shouldBlockFromFinding(activeRulesEmptyForCodeChangesFinding)
|
|
1296
|
-
&& !shouldBlockFromFinding(effectiveIosTestsQualityFinding)
|
|
1297
|
-
&& !shouldBlockFromFinding(astIntelligenceDualFinding)
|
|
1298
|
-
&& !hasTddBddBlockingFinding
|
|
1299
|
-
&& !hasNativeBlockingFinding;
|
|
1300
|
-
const effectiveUnsupportedSkillsMappingFinding = toSoftPreCommitSkillsFinding({
|
|
1301
|
-
finding: effectiveUnsupportedSkillsMappingInput,
|
|
1302
|
-
enabled: shouldSoftEnforceSkillsFindings,
|
|
1303
|
-
observedCodePaths,
|
|
1304
|
-
});
|
|
1305
|
-
const effectivePlatformSkillsCoverageFinding = toSoftPreCommitSkillsFinding({
|
|
1306
|
-
finding: effectivePlatformSkillsCoverageInput,
|
|
1307
|
-
enabled: shouldSoftEnforceSkillsFindings,
|
|
1308
|
-
observedCodePaths,
|
|
1309
|
-
});
|
|
1310
|
-
const effectiveCrossPlatformCriticalFinding = toSoftPreCommitSkillsFinding({
|
|
1311
|
-
finding: effectiveCrossPlatformCriticalInput,
|
|
1312
|
-
enabled: shouldSoftEnforceSkillsFindings,
|
|
1313
|
-
observedCodePaths,
|
|
1314
|
-
});
|
|
1315
|
-
const effectiveSkillsScopeComplianceFinding = toSoftPreCommitSkillsFinding({
|
|
1316
|
-
finding: effectiveSkillsScopeComplianceInput,
|
|
1317
|
-
enabled: shouldSoftEnforceSkillsFindings,
|
|
1318
|
-
observedCodePaths,
|
|
1319
|
-
});
|
|
1265
|
+
const effectiveUnsupportedSkillsMappingFinding = effectiveUnsupportedSkillsMappingInput;
|
|
1266
|
+
const effectivePlatformSkillsCoverageFinding = effectivePlatformSkillsCoverageInput;
|
|
1267
|
+
const effectiveCrossPlatformCriticalFinding = effectiveCrossPlatformCriticalInput;
|
|
1268
|
+
const effectiveSkillsScopeComplianceFinding = effectiveSkillsScopeComplianceInput;
|
|
1320
1269
|
const effectiveFindings = sddBlockingFinding
|
|
1321
1270
|
? [
|
|
1322
1271
|
...(contextBlockingFinding ? [contextBlockingFinding] : []),
|
|
@@ -84,6 +84,9 @@ const defaultDependencies: PlatformGateEvaluationDependencies = {
|
|
|
84
84
|
const normalizeStageForSkills = (
|
|
85
85
|
stage: GateStage
|
|
86
86
|
): Exclude<GateStage, 'STAGED'> => {
|
|
87
|
+
if (stage === 'PRE_WRITE') {
|
|
88
|
+
return 'CI';
|
|
89
|
+
}
|
|
87
90
|
return stage === 'STAGED' ? 'PRE_COMMIT' : stage;
|
|
88
91
|
};
|
|
89
92
|
|
|
@@ -21,6 +21,12 @@ const BLOCK_NEXT_ACTION_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
21
21
|
'Reconcilia policy/skills y reintenta PRE_COMMIT: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki-pre-commit',
|
|
22
22
|
SKILLS_SKILLS_FRONTEND_NO_SOLID_VIOLATIONS:
|
|
23
23
|
'Aplica refactor incremental: extrae 1 componente/hook por commit y vuelve a ejecutar PRE_COMMIT.',
|
|
24
|
+
SKILLS_SKILLS_BACKEND_NO_CONSOLE_LOG:
|
|
25
|
+
'Elimina console.log del backend o sustitúyelo por el logger aprobado para runtime; vuelve a ejecutar el gate.',
|
|
26
|
+
SKILLS_SKILLS_FRONTEND_NO_CONSOLE_LOG:
|
|
27
|
+
'Elimina console.log del frontend de producción o muévelo a instrumentación aprobada; vuelve a ejecutar el gate.',
|
|
28
|
+
HEURISTICS_CONSOLE_LOG_AST:
|
|
29
|
+
'Elimina console.log del código productivo o usa el logger aprobado del proyecto; vuelve a ejecutar el gate.',
|
|
24
30
|
};
|
|
25
31
|
|
|
26
32
|
const severityWeight = (severity: string): number => {
|
|
@@ -38,6 +44,23 @@ const severityWeight = (severity: string): number => {
|
|
|
38
44
|
}
|
|
39
45
|
};
|
|
40
46
|
|
|
47
|
+
const isAstSkillFinding = (finding: Finding): boolean => {
|
|
48
|
+
const ruleId = finding.ruleId.toLowerCase();
|
|
49
|
+
const code = finding.code.toLowerCase();
|
|
50
|
+
return (
|
|
51
|
+
ruleId.startsWith('skills.') ||
|
|
52
|
+
ruleId.startsWith('heuristics.') ||
|
|
53
|
+
code.startsWith('skills_') ||
|
|
54
|
+
code.startsWith('heuristics_') ||
|
|
55
|
+
code.includes('_ast')
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const primaryWeight = (finding: Finding): number => {
|
|
60
|
+
const astSkillWeight = isAstSkillFinding(finding) ? 100 : 0;
|
|
61
|
+
return astSkillWeight + severityWeight(finding.severity);
|
|
62
|
+
};
|
|
63
|
+
|
|
41
64
|
const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
|
|
42
65
|
let primary = findings[0];
|
|
43
66
|
for (const finding of findings.slice(1)) {
|
|
@@ -45,7 +68,7 @@ const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
|
|
|
45
68
|
primary = finding;
|
|
46
69
|
continue;
|
|
47
70
|
}
|
|
48
|
-
if (
|
|
71
|
+
if (primaryWeight(finding) > primaryWeight(primary)) {
|
|
49
72
|
primary = finding;
|
|
50
73
|
}
|
|
51
74
|
}
|
|
@@ -54,6 +77,10 @@ const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
|
|
|
54
77
|
|
|
55
78
|
const sortFindingsBySeverity = (findings: ReadonlyArray<Finding>): ReadonlyArray<Finding> =>
|
|
56
79
|
[...findings].sort((left, right) => {
|
|
80
|
+
const primaryDelta = primaryWeight(right) - primaryWeight(left);
|
|
81
|
+
if (primaryDelta !== 0) {
|
|
82
|
+
return primaryDelta;
|
|
83
|
+
}
|
|
57
84
|
const severityDelta = severityWeight(right.severity) - severityWeight(left.severity);
|
|
58
85
|
if (severityDelta !== 0) {
|
|
59
86
|
return severityDelta;
|
|
@@ -126,7 +153,8 @@ export const printGateFindings = (findings: ReadonlyArray<Finding>): void => {
|
|
|
126
153
|
`[pumuki][warning-summary] secondary=${finding.code} severity=${finding.severity.toUpperCase()} rule=${finding.ruleId}\n`
|
|
127
154
|
);
|
|
128
155
|
}
|
|
129
|
-
|
|
130
|
-
|
|
156
|
+
process.stdout.write(`[pumuki][findings] total=${orderedFindings.length}\n`);
|
|
157
|
+
for (const [index, finding] of orderedFindings.entries()) {
|
|
158
|
+
process.stdout.write(`\n${index + 1}. ${formatFinding(finding)}\n`);
|
|
131
159
|
}
|
|
132
160
|
};
|
|
@@ -823,6 +823,13 @@ const enforceGitAtomicityGate = (params: {
|
|
|
823
823
|
causeCode: firstViolation.code,
|
|
824
824
|
causeMessage: firstViolation.message,
|
|
825
825
|
remediation: firstViolation.remediation,
|
|
826
|
+
blockingCauses: atomicity.violations.map((violation) => ({
|
|
827
|
+
code: violation.code,
|
|
828
|
+
message: violation.message,
|
|
829
|
+
ruleId: 'git.atomicity.blocked',
|
|
830
|
+
file: '.pumuki/git-atomicity.json',
|
|
831
|
+
remediation: violation.remediation,
|
|
832
|
+
})),
|
|
826
833
|
});
|
|
827
834
|
notifyAuditSummaryForStage(params.dependencies, params.stage);
|
|
828
835
|
return true;
|
|
@@ -334,6 +334,16 @@ export const runSddCommand = async (parsed: ParsedArgs, activeDependencies: Life
|
|
|
334
334
|
causeCode,
|
|
335
335
|
nextAction,
|
|
336
336
|
}),
|
|
337
|
+
blockingCauses: aiGate.violations.map((violation) => ({
|
|
338
|
+
code: violation.code,
|
|
339
|
+
message: violation.message,
|
|
340
|
+
ruleId: violation.code,
|
|
341
|
+
file: resolveAiGateViolationLocation(violation.code),
|
|
342
|
+
remediation: resolvePreWriteBlockedRemediation({
|
|
343
|
+
causeCode: violation.code,
|
|
344
|
+
nextAction,
|
|
345
|
+
}),
|
|
346
|
+
})),
|
|
337
347
|
});
|
|
338
348
|
return 1;
|
|
339
349
|
}
|
|
@@ -25,12 +25,18 @@ import {
|
|
|
25
25
|
readLifecycleNotificationStatus,
|
|
26
26
|
type LifecycleNotificationStatus,
|
|
27
27
|
} from './notificationStatus';
|
|
28
|
+
import {
|
|
29
|
+
extractEvidenceBlockingCauses,
|
|
30
|
+
formatEvidenceBlockingCause,
|
|
31
|
+
type EvidenceBlockingCause,
|
|
32
|
+
} from '../evidence/blockingCauses';
|
|
28
33
|
|
|
29
34
|
export type DoctorIssueSeverity = 'warning' | 'error';
|
|
30
35
|
|
|
31
36
|
export type DoctorIssue = {
|
|
32
37
|
severity: DoctorIssueSeverity;
|
|
33
38
|
message: string;
|
|
39
|
+
blockingCauses?: ReadonlyArray<EvidenceBlockingCause>;
|
|
34
40
|
};
|
|
35
41
|
|
|
36
42
|
export type DoctorDeepCheckId =
|
|
@@ -193,12 +199,20 @@ const buildDoctorIssues = (params: {
|
|
|
193
199
|
evidence.severity_metrics.gate_status === 'BLOCKED';
|
|
194
200
|
if (blocked) {
|
|
195
201
|
const blockedStage = evidence?.snapshot?.stage ?? 'PRE_WRITE';
|
|
202
|
+
const blockingCauses = extractEvidenceBlockingCauses(evidence);
|
|
203
|
+
const causeDetails =
|
|
204
|
+
blockingCauses.length > 0
|
|
205
|
+
? ` Blocking causes: ${blockingCauses.length}. ${blockingCauses
|
|
206
|
+
.map(formatEvidenceBlockingCause)
|
|
207
|
+
.join(' | ')}`
|
|
208
|
+
: '';
|
|
196
209
|
issues.push({
|
|
197
210
|
severity: 'error',
|
|
198
211
|
message: appendTrackingActionableContext({
|
|
199
212
|
repoRoot: params.repoRoot,
|
|
200
|
-
message: `Governance is blocked (${blockedStage})
|
|
213
|
+
message: `Governance is blocked (${blockedStage}).${causeDetails}`,
|
|
201
214
|
}),
|
|
215
|
+
blockingCauses,
|
|
202
216
|
});
|
|
203
217
|
} else if (evidence.snapshot.outcome === 'WARN') {
|
|
204
218
|
const warnStage = evidence?.snapshot?.stage ?? 'PRE_WRITE';
|
|
@@ -22,6 +22,10 @@ import {
|
|
|
22
22
|
readLifecycleNotificationStatus,
|
|
23
23
|
type LifecycleNotificationStatus,
|
|
24
24
|
} from './notificationStatus';
|
|
25
|
+
import {
|
|
26
|
+
extractEvidenceBlockingCauses,
|
|
27
|
+
formatEvidenceBlockingCause,
|
|
28
|
+
} from '../evidence/blockingCauses';
|
|
25
29
|
|
|
26
30
|
export type LifecycleStatus = {
|
|
27
31
|
repoRoot: string;
|
|
@@ -101,12 +105,20 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
|
|
|
101
105
|
repoRoot,
|
|
102
106
|
message: `Governance is blocked (${blockedStage}).`,
|
|
103
107
|
});
|
|
108
|
+
const blockingCauses = extractEvidenceBlockingCauses(evidence);
|
|
109
|
+
const causeDetails =
|
|
110
|
+
blockingCauses.length > 0
|
|
111
|
+
? ` Blocking causes: ${blockingCauses.length}. ${blockingCauses
|
|
112
|
+
.map(formatEvidenceBlockingCause)
|
|
113
|
+
.join(' | ')}`
|
|
114
|
+
: '';
|
|
104
115
|
return [
|
|
105
116
|
...notificationIssues,
|
|
106
117
|
...leaseIssues,
|
|
107
118
|
{
|
|
108
119
|
severity: 'error',
|
|
109
|
-
message
|
|
120
|
+
message: `${message}${causeDetails}`,
|
|
121
|
+
blockingCauses,
|
|
110
122
|
},
|
|
111
123
|
];
|
|
112
124
|
};
|
|
@@ -679,6 +679,13 @@ export const runLifecycleWatch = async (
|
|
|
679
679
|
causeCode: cause.code,
|
|
680
680
|
causeMessage: cause.message,
|
|
681
681
|
remediation: cause.remediation,
|
|
682
|
+
blockingCauses: matchedFindings.map((finding) => ({
|
|
683
|
+
code: finding.code ?? finding.ruleId,
|
|
684
|
+
message: finding.message,
|
|
685
|
+
ruleId: finding.ruleId,
|
|
686
|
+
file: finding.file,
|
|
687
|
+
remediation: finding.expected_fix,
|
|
688
|
+
})),
|
|
682
689
|
});
|
|
683
690
|
} else {
|
|
684
691
|
notificationResult = activeDependencies.emitAuditSummaryNotificationFromEvidence({
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from './evidenceFacets';
|
|
22
22
|
import { CONTEXT_API_CAPABILITIES } from './evidencePayloadConfig';
|
|
23
23
|
import { toSuppressedSummaryFields } from './evidencePayloadSummarySuppressed';
|
|
24
|
+
import { extractEvidenceBlockingCauses } from '../evidence/blockingCauses';
|
|
24
25
|
|
|
25
26
|
export const toStatusPayload = (repoRoot: string): unknown => {
|
|
26
27
|
const readResult = readEvidenceResult(repoRoot);
|
|
@@ -61,6 +62,7 @@ export const toStatusPayload = (repoRoot: string): unknown => {
|
|
|
61
62
|
const detectedPlatformsCount = sortedPlatforms.filter((entry) => entry.detected).length;
|
|
62
63
|
const suppressedFindingsCount = evidence.consolidation?.suppressed?.length ?? 0;
|
|
63
64
|
const findingsWithLinesCount = toFindingsWithLinesCount(evidence.snapshot.findings);
|
|
65
|
+
const blockingCauses = extractEvidenceBlockingCauses(evidence);
|
|
64
66
|
return {
|
|
65
67
|
status: 'ok',
|
|
66
68
|
context_api: CONTEXT_API_CAPABILITIES,
|
|
@@ -83,6 +85,8 @@ export const toStatusPayload = (repoRoot: string): unknown => {
|
|
|
83
85
|
findings_by_platform: toFindingsByPlatform(evidence.snapshot.findings),
|
|
84
86
|
highest_severity: toHighestSeverity(evidence.snapshot.findings),
|
|
85
87
|
blocking_findings_count: toBlockingFindingsCount(evidence.snapshot.findings),
|
|
88
|
+
blocking_causes_count: blockingCauses.length,
|
|
89
|
+
blocking_causes: blockingCauses,
|
|
86
90
|
ledger_count: evidence.ledger.length,
|
|
87
91
|
ledger_files_count: toLedgerFilesCount(evidence.ledger),
|
|
88
92
|
ledger_rules_count: toLedgerRulesCount(evidence.ledger),
|
|
@@ -19,12 +19,14 @@ import {
|
|
|
19
19
|
toSeverityCounts,
|
|
20
20
|
} from './evidenceFacets';
|
|
21
21
|
import { toSuppressedSummaryFields } from './evidencePayloadSummarySuppressed';
|
|
22
|
+
import { extractEvidenceBlockingCauses } from '../evidence/blockingCauses';
|
|
22
23
|
|
|
23
24
|
export const toSummaryPayload = (evidence: AiEvidenceV2_1) => {
|
|
24
25
|
const sortedPlatforms = sortPlatforms(evidence.platforms);
|
|
25
26
|
const detectedPlatforms = sortedPlatforms.filter((entry) => entry.detected);
|
|
26
27
|
const suppressedFindingsCount = evidence.consolidation?.suppressed?.length ?? 0;
|
|
27
28
|
const findingsWithLinesCount = toFindingsWithLinesCount(evidence.snapshot.findings);
|
|
29
|
+
const blockingCauses = extractEvidenceBlockingCauses(evidence);
|
|
28
30
|
return {
|
|
29
31
|
version: evidence.version,
|
|
30
32
|
timestamp: evidence.timestamp,
|
|
@@ -41,6 +43,8 @@ export const toSummaryPayload = (evidence: AiEvidenceV2_1) => {
|
|
|
41
43
|
findings_by_platform: toFindingsByPlatform(evidence.snapshot.findings),
|
|
42
44
|
highest_severity: toHighestSeverity(evidence.snapshot.findings),
|
|
43
45
|
blocking_findings_count: toBlockingFindingsCount(evidence.snapshot.findings),
|
|
46
|
+
blocking_causes_count: blockingCauses.length,
|
|
47
|
+
blocking_causes: blockingCauses,
|
|
44
48
|
},
|
|
45
49
|
ledger_count: evidence.ledger.length,
|
|
46
50
|
ledger_files_count: toLedgerFilesCount(evidence.ledger),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.276",
|
|
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 { createConsumerLegacyMenuActions } from './framework-menu-consumer-actio
|
|
|
2
2
|
import { formatConsumerPreflight, runConsumerPreflight } from './framework-menu-consumer-preflight-lib';
|
|
3
3
|
import {
|
|
4
4
|
buildConsumerRuntimeBlockedSummary,
|
|
5
|
+
buildConsumerRuntimeBlockedCauseLines,
|
|
5
6
|
exportConsumerRuntimeMarkdown,
|
|
6
7
|
notifyConsumerRuntimeAuditSummary,
|
|
7
8
|
printConsumerRuntimeEmptyScopeHint,
|
|
@@ -83,6 +84,11 @@ const renderSummaryAfterGate = (
|
|
|
83
84
|
dependencies.setSummaryOverride(
|
|
84
85
|
buildConsumerRuntimeBlockedSummary(gateResult.blocked)
|
|
85
86
|
);
|
|
87
|
+
dependencies.write(
|
|
88
|
+
'\n' +
|
|
89
|
+
buildConsumerRuntimeBlockedCauseLines(gateResult.blocked).join('\n') +
|
|
90
|
+
'\n'
|
|
91
|
+
);
|
|
86
92
|
} else {
|
|
87
93
|
dependencies.clearSummaryOverride();
|
|
88
94
|
}
|
|
@@ -102,13 +108,57 @@ const renderSummaryAfterGate = (
|
|
|
102
108
|
return summary;
|
|
103
109
|
};
|
|
104
110
|
|
|
111
|
+
const buildFullAuditFinalSummaryLines = (
|
|
112
|
+
summary: import('./framework-menu-evidence-summary-lib').FrameworkMenuEvidenceSummary
|
|
113
|
+
): string[] => {
|
|
114
|
+
const platformRows = summary.platformAuditRows ?? [];
|
|
115
|
+
const topFindings = summary.topFindings.slice(0, 10);
|
|
116
|
+
return [
|
|
117
|
+
'✅ AST Intelligence completed',
|
|
118
|
+
'═══════════════════════════════════════════════════════════════',
|
|
119
|
+
'FINAL SUMMARY - VIOLATIONS BY SEVERITY',
|
|
120
|
+
`🔴 CRITICAL: ${summary.byEnterpriseSeverity?.CRITICAL ?? summary.bySeverity.CRITICAL}`,
|
|
121
|
+
`🟠 HIGH: ${summary.byEnterpriseSeverity?.HIGH ?? summary.bySeverity.ERROR}`,
|
|
122
|
+
`🟡 MEDIUM: ${summary.byEnterpriseSeverity?.MEDIUM ?? summary.bySeverity.WARN}`,
|
|
123
|
+
`🔵 LOW: ${summary.byEnterpriseSeverity?.LOW ?? summary.bySeverity.INFO}`,
|
|
124
|
+
`Total violations: ${summary.totalFindings}`,
|
|
125
|
+
'',
|
|
126
|
+
'PLATFORM-SPECIFIC ANALYSIS',
|
|
127
|
+
...(platformRows.length === 0
|
|
128
|
+
? [' No platform findings.']
|
|
129
|
+
: platformRows.map((row) => ` Platform: ${row.platform} -> ${row.violations} violations`)),
|
|
130
|
+
'',
|
|
131
|
+
'TOP VIOLATIONS & REMEDIATION',
|
|
132
|
+
...(topFindings.length === 0
|
|
133
|
+
? [' No violations detected.']
|
|
134
|
+
: topFindings.map(
|
|
135
|
+
(finding) =>
|
|
136
|
+
` [${finding.severity}] ${finding.ruleId} -> ${finding.file}:${finding.line}`
|
|
137
|
+
)),
|
|
138
|
+
];
|
|
139
|
+
};
|
|
140
|
+
|
|
105
141
|
export const createConsumerRuntimeActions = (
|
|
106
142
|
dependencies: ConsumerRuntimeActionDependencies
|
|
107
143
|
): ReadonlyArray<ConsumerAction> =>
|
|
108
144
|
createConsumerLegacyMenuActions({
|
|
109
145
|
runFullAudit: async () => {
|
|
146
|
+
dependencies.write(
|
|
147
|
+
'\n' +
|
|
148
|
+
[
|
|
149
|
+
'Collecting source files...',
|
|
150
|
+
'Running pattern checks...',
|
|
151
|
+
'Running ESLint audits...',
|
|
152
|
+
'⚙️ AST Intelligence',
|
|
153
|
+
'Running AST analysis...',
|
|
154
|
+
].join('\n') +
|
|
155
|
+
'\n'
|
|
156
|
+
);
|
|
110
157
|
await runConsumerRuntimePreflight(dependencies, 'PRE_COMMIT');
|
|
111
|
-
renderSummaryAfterGate(dependencies, await dependencies.runRepoGate());
|
|
158
|
+
const summary = renderSummaryAfterGate(dependencies, await dependencies.runRepoGate());
|
|
159
|
+
dependencies.write(
|
|
160
|
+
`\n${buildFullAuditFinalSummaryLines(summary).join('\n')}\n`
|
|
161
|
+
);
|
|
112
162
|
},
|
|
113
163
|
runStrictRepoAndStaged: async () => {
|
|
114
164
|
await runConsumerRuntimePreflight(dependencies, 'PRE_PUSH');
|
|
@@ -155,46 +155,123 @@ export const printPrePushTrackedEvidenceDiskHint = (params: {
|
|
|
155
155
|
|
|
156
156
|
export const buildConsumerRuntimeBlockedSummary = (
|
|
157
157
|
blocked: ConsumerRuntimeBlockedGate
|
|
158
|
-
): FrameworkMenuEvidenceSummary =>
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
158
|
+
): FrameworkMenuEvidenceSummary => {
|
|
159
|
+
const normalizedCause = blocked.causeCode.toLowerCase();
|
|
160
|
+
const inferredPlatform = normalizedCause.includes('backend')
|
|
161
|
+
? 'Backend'
|
|
162
|
+
: normalizedCause.includes('frontend') || normalizedCause.includes('web')
|
|
163
|
+
? 'Frontend'
|
|
164
|
+
: normalizedCause.includes('ios')
|
|
165
|
+
? 'iOS'
|
|
166
|
+
: normalizedCause.includes('android')
|
|
167
|
+
? 'Android'
|
|
168
|
+
: null;
|
|
169
|
+
const blockingCauses = blocked.blockingCauses ?? [];
|
|
170
|
+
const topFindings =
|
|
171
|
+
blockingCauses.length > 0
|
|
172
|
+
? blockingCauses.map((cause) => ({
|
|
173
|
+
severity: 'HIGH' as const,
|
|
174
|
+
ruleId: cause.ruleId ?? cause.code,
|
|
175
|
+
file: cause.file ?? 'PROJECT_ROOT',
|
|
176
|
+
line: 1,
|
|
177
|
+
}))
|
|
178
|
+
: [
|
|
179
|
+
{
|
|
180
|
+
severity: 'HIGH' as const,
|
|
181
|
+
ruleId: blocked.causeCode,
|
|
182
|
+
file: 'PROJECT_ROOT',
|
|
183
|
+
line: 1,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
const topFiles = new Map<string, number>();
|
|
187
|
+
for (const finding of topFindings) {
|
|
188
|
+
topFiles.set(finding.file, (topFiles.get(finding.file) ?? 0) + 1);
|
|
189
|
+
}
|
|
190
|
+
const platformCounts = new Map<string, number>();
|
|
191
|
+
const inferPlatform = (value: string): string | null => {
|
|
192
|
+
const normalized = value.toLowerCase();
|
|
193
|
+
if (normalized.includes('backend')) {
|
|
194
|
+
return 'Backend';
|
|
195
|
+
}
|
|
196
|
+
if (normalized.includes('frontend') || normalized.includes('/web/') || normalized.includes('web')) {
|
|
197
|
+
return 'Frontend';
|
|
198
|
+
}
|
|
199
|
+
if (normalized.includes('ios')) {
|
|
200
|
+
return 'iOS';
|
|
201
|
+
}
|
|
202
|
+
if (normalized.includes('android')) {
|
|
203
|
+
return 'Android';
|
|
204
|
+
}
|
|
205
|
+
return null;
|
|
206
|
+
};
|
|
207
|
+
if (blockingCauses.length > 0) {
|
|
208
|
+
for (const cause of blockingCauses) {
|
|
209
|
+
const platform = inferPlatform(`${cause.ruleId ?? cause.code} ${cause.file ?? ''}`);
|
|
210
|
+
if (platform) {
|
|
211
|
+
platformCounts.set(platform, (platformCounts.get(platform) ?? 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} else if (inferredPlatform) {
|
|
215
|
+
platformCounts.set(inferredPlatform, blocked.totalViolations);
|
|
216
|
+
}
|
|
217
|
+
return {
|
|
218
|
+
status: 'ok',
|
|
219
|
+
stage: blocked.stage,
|
|
220
|
+
outcome: 'BLOCK',
|
|
221
|
+
totalFindings: blocked.totalViolations,
|
|
222
|
+
filesScanned: 0,
|
|
223
|
+
filesAffected: 0,
|
|
224
|
+
bySeverity: {
|
|
225
|
+
CRITICAL: 0,
|
|
226
|
+
ERROR: 1,
|
|
227
|
+
WARN: 0,
|
|
228
|
+
INFO: 0,
|
|
187
229
|
},
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
file: 'PROJECT_ROOT',
|
|
194
|
-
line: 1,
|
|
230
|
+
byEnterpriseSeverity: {
|
|
231
|
+
CRITICAL: 0,
|
|
232
|
+
HIGH: 1,
|
|
233
|
+
MEDIUM: 0,
|
|
234
|
+
LOW: 0,
|
|
195
235
|
},
|
|
196
|
-
|
|
197
|
-
|
|
236
|
+
topFiles: [
|
|
237
|
+
...topFiles.entries(),
|
|
238
|
+
].map(([file, count]) => ({
|
|
239
|
+
file,
|
|
240
|
+
count,
|
|
241
|
+
})),
|
|
242
|
+
topFileLocations: topFindings.map((finding) => ({
|
|
243
|
+
file: finding.file,
|
|
244
|
+
line: finding.line,
|
|
245
|
+
})),
|
|
246
|
+
topFindings,
|
|
247
|
+
platformAuditRows: [...platformCounts.entries()].map(([platform, violations]) => ({
|
|
248
|
+
platform,
|
|
249
|
+
violations,
|
|
250
|
+
})),
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
export const buildConsumerRuntimeBlockedCauseLines = (
|
|
255
|
+
blocked: ConsumerRuntimeBlockedGate
|
|
256
|
+
): string[] => {
|
|
257
|
+
const causes = blocked.blockingCauses ?? [];
|
|
258
|
+
if (causes.length === 0) {
|
|
259
|
+
return [
|
|
260
|
+
`🔴 Gate blocked: ${blocked.causeCode}`,
|
|
261
|
+
`Cause: ${blocked.causeMessage}`,
|
|
262
|
+
`Solution: ${blocked.remediation}`,
|
|
263
|
+
];
|
|
264
|
+
}
|
|
265
|
+
return [
|
|
266
|
+
`🔴 Gate blocked: ${blocked.causeCode}`,
|
|
267
|
+
`Blocking causes: ${causes.length}`,
|
|
268
|
+
...causes.flatMap((cause, index) => [
|
|
269
|
+
`${index + 1}. ${cause.ruleId ?? cause.code}${cause.file ? ` @ ${cause.file}` : ''}`,
|
|
270
|
+
` Cause: ${cause.message}`,
|
|
271
|
+
` Solution: ${cause.remediation ?? blocked.remediation}`,
|
|
272
|
+
]),
|
|
273
|
+
];
|
|
274
|
+
};
|
|
198
275
|
|
|
199
276
|
export const printConsumerRuntimeEmptyScopeHint = (
|
|
200
277
|
dependencies: Pick<ConsumerRuntimeSummaryDependencies, 'write'>,
|
|
@@ -18,6 +18,16 @@ import type {
|
|
|
18
18
|
ConsumerRuntimeWrite,
|
|
19
19
|
} from './framework-menu-consumer-runtime-types';
|
|
20
20
|
|
|
21
|
+
const PUMUKI_LEGACY_BANNER = [
|
|
22
|
+
'██████╗ ██╗ ██╗███╗ ███╗██╗ ██╗██╗ ██╗██╗',
|
|
23
|
+
'██╔══██╗██║ ██║████╗ ████║██║ ██║██║ ██╔╝██║',
|
|
24
|
+
'██████╔╝██║ ██║██╔████╔██║██║ ██║█████╔╝ ██║',
|
|
25
|
+
'██╔═══╝ ██║ ██║██║╚██╔╝██║██║ ██║██╔═██╗ ██║',
|
|
26
|
+
'██║ ╚██████╔╝██║ ╚═╝ ██║╚██████╔╝██║ ██╗██║',
|
|
27
|
+
'╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝╚═╝',
|
|
28
|
+
' 🐈 En memoria de Pumuki 💚',
|
|
29
|
+
];
|
|
30
|
+
|
|
21
31
|
const buildConsumerRuntimeMenuStatus = (
|
|
22
32
|
menuSummary: FrameworkMenuEvidenceSummary
|
|
23
33
|
): { level: 'info' | 'block' | 'warn' | 'ok'; label: string } =>
|
|
@@ -35,17 +45,26 @@ export const renderConsumerRuntimeClassicMenu = (
|
|
|
35
45
|
actions: ReadonlyArray<ConsumerAction>,
|
|
36
46
|
useColor: () => boolean
|
|
37
47
|
): string => {
|
|
38
|
-
const
|
|
48
|
+
const actionById = new Map(actions.map((action) => [action.id, action]));
|
|
49
|
+
const label = (id: string): string => actionById.get(id)?.label ?? 'Unavailable';
|
|
39
50
|
const lines = [
|
|
40
|
-
|
|
51
|
+
...PUMUKI_LEGACY_BANNER,
|
|
52
|
+
'',
|
|
41
53
|
'Advanced Project Audit — AST Intelligence & Quality Gate',
|
|
42
|
-
'A. Switch to advanced menu',
|
|
43
54
|
'',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
`1) ${label('1')} 6) ${label('6')}`,
|
|
56
|
+
`2) ${label('2')} 7) ${label('7')}`,
|
|
57
|
+
`3) ${label('3')} 8) ${label('8')}`,
|
|
58
|
+
`4) ${label('4')} 9) ${label('9')}`,
|
|
59
|
+
`5) ${label('5')} 10) ${label('10')}`,
|
|
60
|
+
'',
|
|
61
|
+
'Additional engine flows',
|
|
62
|
+
`11) ${label('11')}`,
|
|
63
|
+
`12) ${label('12')}`,
|
|
64
|
+
`13) ${label('13')}`,
|
|
65
|
+
`14) ${label('14')}`,
|
|
66
|
+
'',
|
|
67
|
+
'A) Switch to advanced menu',
|
|
49
68
|
];
|
|
50
69
|
return renderLegacyPanel(lines, {
|
|
51
70
|
width: resolveLegacyPanelOuterWidth(),
|
|
@@ -68,7 +87,8 @@ export const renderConsumerRuntimeModernMenu = (
|
|
|
68
87
|
});
|
|
69
88
|
const groupedActions = resolveConsumerMenuLayout(params.actions);
|
|
70
89
|
const lines = [
|
|
71
|
-
|
|
90
|
+
...PUMUKI_LEGACY_BANNER,
|
|
91
|
+
'',
|
|
72
92
|
'Advanced Project Audit — AST Intelligence & Quality Gate',
|
|
73
93
|
`Status: ${renderBadge(menuStatus.label, menuStatus.level, tokens)}`,
|
|
74
94
|
'A. Switch to advanced menu',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { FrameworkMenuEvidenceSummary } from './framework-menu-evidence-summary-lib';
|
|
2
2
|
import type {
|
|
3
|
+
PumukiBlockedNotificationCause,
|
|
3
4
|
PumukiCriticalNotificationEvent,
|
|
4
5
|
SystemNotificationEmitResult,
|
|
5
6
|
} from './framework-menu-system-notifications-lib';
|
|
@@ -16,6 +17,7 @@ export type ConsumerRuntimeBlockedGate = {
|
|
|
16
17
|
causeCode: string;
|
|
17
18
|
causeMessage: string;
|
|
18
19
|
remediation: string;
|
|
20
|
+
blockingCauses?: ReadonlyArray<PumukiBlockedNotificationCause>;
|
|
19
21
|
};
|
|
20
22
|
|
|
21
23
|
export type ConsumerRuntimeGateResult = {
|
|
@@ -239,6 +239,7 @@ const runMenuAuditGateWithBlocked = async (
|
|
|
239
239
|
causeCode: params.causeCode,
|
|
240
240
|
causeMessage: params.causeMessage,
|
|
241
241
|
remediation: params.remediation,
|
|
242
|
+
blockingCauses: params.blockingCauses,
|
|
242
243
|
};
|
|
243
244
|
return {
|
|
244
245
|
delivered: false,
|
|
@@ -273,6 +274,7 @@ const runRepoAndStagedPrePushGateSilentWithDependencies = async (
|
|
|
273
274
|
causeCode: params.causeCode,
|
|
274
275
|
causeMessage: params.causeMessage,
|
|
275
276
|
remediation: params.remediation,
|
|
277
|
+
blockingCauses: params.blockingCauses,
|
|
276
278
|
};
|
|
277
279
|
},
|
|
278
280
|
writeHookGateSummary: () => {},
|
|
@@ -3,7 +3,7 @@ import type { MenuLayoutGroup } from './framework-menu-layout-types';
|
|
|
3
3
|
export const CONSUMER_MENU_LAYOUT: ReadonlyArray<MenuLayoutGroup> = [
|
|
4
4
|
{
|
|
5
5
|
key: 'read-only-gates',
|
|
6
|
-
title: '
|
|
6
|
+
title: 'Classic audit flows',
|
|
7
7
|
itemIds: ['1', '2', '3', '4'],
|
|
8
8
|
},
|
|
9
9
|
{
|
|
@@ -13,12 +13,12 @@ export const CONSUMER_MENU_LAYOUT: ReadonlyArray<MenuLayoutGroup> = [
|
|
|
13
13
|
},
|
|
14
14
|
{
|
|
15
15
|
key: 'export',
|
|
16
|
-
title: '
|
|
16
|
+
title: 'Classic export',
|
|
17
17
|
itemIds: ['8'],
|
|
18
18
|
},
|
|
19
19
|
{
|
|
20
20
|
key: 'legacy-read-only-diagnostics',
|
|
21
|
-
title: '
|
|
21
|
+
title: 'Classic diagnostics',
|
|
22
22
|
itemIds: ['5', '6', '7', '9'],
|
|
23
23
|
},
|
|
24
24
|
{
|
|
@@ -118,7 +118,8 @@ const buildBlockedCausesSummary = (
|
|
|
118
118
|
}
|
|
119
119
|
const file = first.file ? ` en ${first.file}` : '';
|
|
120
120
|
const rule = first.ruleId ?? first.code;
|
|
121
|
-
const
|
|
121
|
+
const remainingCauses = Math.max(0, causes.length - 1);
|
|
122
|
+
const suffix = remainingCauses > 0 ? ` (+${remainingCauses} más)` : '';
|
|
122
123
|
return `Violación ${rule}${file}${suffix}.`;
|
|
123
124
|
};
|
|
124
125
|
|
|
@@ -17,14 +17,18 @@ const buildBlockingCausesDetails = (
|
|
|
17
17
|
if (!causes || causes.length === 0) {
|
|
18
18
|
return null;
|
|
19
19
|
}
|
|
20
|
+
const total = causes.length;
|
|
21
|
+
const header = total === 1
|
|
22
|
+
? 'Causas bloqueantes: 1'
|
|
23
|
+
: `Causas bloqueantes: ${total}`;
|
|
20
24
|
return causes
|
|
21
|
-
.slice(0, 6)
|
|
22
25
|
.map((cause, index) => {
|
|
23
26
|
const rule = cause.ruleId ?? cause.code;
|
|
24
27
|
const file = cause.file ? ` · ${cause.file}` : '';
|
|
25
28
|
const fix = cause.remediation ? ` · Solución: ${cause.remediation}` : '';
|
|
26
29
|
return `${index + 1}. ${rule}${file}. ${cause.message}${fix}`;
|
|
27
30
|
})
|
|
31
|
+
.reduce((lines, line) => [...lines, line], [header])
|
|
28
32
|
.join('\n');
|
|
29
33
|
};
|
|
30
34
|
|
|
@@ -41,6 +41,9 @@ const BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT: Readonly<Record<BlockedRemediat
|
|
|
41
41
|
const GENERIC_BLOCKED_REMEDIATION =
|
|
42
42
|
'Corrige el bloqueo indicado y vuelve a ejecutar el comando.';
|
|
43
43
|
|
|
44
|
+
const buildMultipleCausesRemediation = (count: number): string =>
|
|
45
|
+
`Corrige las ${count} causas bloqueantes listadas arriba, empezando por violaciones AST/skills de codigo cuando existan, y vuelve a ejecutar el gate.`;
|
|
46
|
+
|
|
44
47
|
const normalizeBlockedRemediation = (value: string): string =>
|
|
45
48
|
normalizeNotificationText(value)
|
|
46
49
|
.replace(/^cómo solucionarlo:\s*/i, '')
|
|
@@ -102,6 +105,13 @@ export const resolveBlockedRemediation = (
|
|
|
102
105
|
): string => {
|
|
103
106
|
const variant = options?.variant ?? 'dialog';
|
|
104
107
|
const maxLength = BLOCKED_REMEDIATION_MAX_LENGTH_BY_VARIANT[variant];
|
|
108
|
+
const blockingCausesCount = event.blockingCauses?.length ?? 0;
|
|
109
|
+
if (blockingCausesCount > 1) {
|
|
110
|
+
return truncateNotificationText(
|
|
111
|
+
buildMultipleCausesRemediation(blockingCausesCount),
|
|
112
|
+
maxLength
|
|
113
|
+
);
|
|
114
|
+
}
|
|
105
115
|
const firstCauseWithRemediation = event.blockingCauses?.find(
|
|
106
116
|
(cause) => cause.remediation && cause.remediation.trim().length > 0
|
|
107
117
|
);
|
|
@@ -103,7 +103,8 @@ const menu = async (): Promise<void> => {
|
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
|
-
const
|
|
106
|
+
const prompt = mode === 'consumer' ? '\nChoose an option: ' : '\nSelect option: ';
|
|
107
|
+
const option = (await rl.question(prompt)).trim();
|
|
107
108
|
const normalized = option.toUpperCase();
|
|
108
109
|
|
|
109
110
|
if (mode === 'consumer' && normalized === 'A') {
|