pumuki 6.3.275 → 6.3.277
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 +97 -6
- package/integrations/git/runPlatformGate.ts +5 -56
- 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.277] - 2026-05-18
|
|
4
|
+
|
|
5
|
+
- `PUMUKI-INC-146`: PRE_WRITE now resolves the effective iOS test-quality scope from staged paths first when a staged slice exists, falling back to the worktree only when there is no staged code. This prevents unstaged Swift test files from forcing `skills.ios.critical-test-quality` onto unrelated staged iOS/SwiftUI production commits, while preserving the hard block for staged Swift test slices.
|
|
6
|
+
|
|
7
|
+
## [6.3.276] - 2026-05-18
|
|
8
|
+
|
|
9
|
+
- `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.
|
|
10
|
+
|
|
3
11
|
## [6.3.275] - 2026-05-18
|
|
4
12
|
|
|
5
13
|
- 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.
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.277
|
|
@@ -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.277)
|
|
8
|
+
|
|
9
|
+
- `PUMUKI-INC-146`: PRE_WRITE uses staged paths as the effective slice for `skills.ios.critical-test-quality` when staged files exist. Unstaged Swift tests no longer contaminate a staged production-only iOS commit, but staged Swift tests still require the critical test-quality rule and remain fail-closed.
|
|
10
|
+
|
|
11
|
+
### 2026-05-18 (v6.3.276)
|
|
12
|
+
|
|
13
|
+
- `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.
|
|
14
|
+
|
|
7
15
|
### 2026-05-18 (v6.3.275)
|
|
8
16
|
|
|
9
17
|
- `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.
|
|
@@ -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,69 @@ const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> =>
|
|
|
401
401
|
}
|
|
402
402
|
};
|
|
403
403
|
|
|
404
|
+
const collectStagedChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
|
|
405
|
+
try {
|
|
406
|
+
const output = execFileSync(
|
|
407
|
+
'git',
|
|
408
|
+
['diff', '--cached', '--name-only'],
|
|
409
|
+
{
|
|
410
|
+
cwd: repoRoot,
|
|
411
|
+
encoding: 'utf8',
|
|
412
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
const files = output
|
|
416
|
+
.split('\n')
|
|
417
|
+
.map((line) => normalizeChangedPath(line))
|
|
418
|
+
.filter((line) => line.length > 0);
|
|
419
|
+
return [...new Set(files)];
|
|
420
|
+
} catch {
|
|
421
|
+
return [];
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const collectPreWriteEffectiveChangedPaths = (repoRoot: string): ReadonlyArray<string> => {
|
|
426
|
+
const stagedPaths = collectStagedChangedPaths(repoRoot);
|
|
427
|
+
if (stagedPaths.length > 0) {
|
|
428
|
+
return stagedPaths;
|
|
429
|
+
}
|
|
430
|
+
return collectWorktreeChangedPaths(repoRoot);
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const isIosTestQualityPath = (filePath: string): boolean => {
|
|
434
|
+
const normalized = normalizeChangedPath(filePath).toLowerCase();
|
|
435
|
+
if (!normalized.endsWith('.swift')) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
return normalized.includes('/tests/')
|
|
439
|
+
|| normalized.includes('/uitests/')
|
|
440
|
+
|| normalized.endsWith('test.swift')
|
|
441
|
+
|| normalized.endsWith('tests.swift')
|
|
442
|
+
|| normalized.endsWith('.spec.swift');
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const hasPreWriteIosTestQualityScope = (repoRoot: string): boolean =>
|
|
446
|
+
collectPreWriteEffectiveChangedPaths(repoRoot).some((filePath) =>
|
|
447
|
+
isIosTestQualityPath(filePath)
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
const resolvePreWriteCriticalSkillsRules = (params: {
|
|
451
|
+
platform: PreWriteSkillsPlatform;
|
|
452
|
+
stage: AiGateStage;
|
|
453
|
+
repoRoot: string;
|
|
454
|
+
}): ReadonlyArray<string> => {
|
|
455
|
+
const ruleIds = PREWRITE_CRITICAL_SKILLS_RULES[params.platform];
|
|
456
|
+
if (
|
|
457
|
+
params.platform === 'ios'
|
|
458
|
+
&& params.stage === 'PRE_WRITE'
|
|
459
|
+
&& ruleIds.includes('skills.ios.critical-test-quality')
|
|
460
|
+
&& !hasPreWriteIosTestQualityScope(params.repoRoot)
|
|
461
|
+
) {
|
|
462
|
+
return ruleIds.filter((ruleId) => ruleId !== 'skills.ios.critical-test-quality');
|
|
463
|
+
}
|
|
464
|
+
return ruleIds;
|
|
465
|
+
};
|
|
466
|
+
|
|
404
467
|
const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
|
|
405
468
|
const normalized = normalizeChangedPath(filePath).toLowerCase();
|
|
406
469
|
if (platform === 'ios') {
|
|
@@ -552,6 +615,7 @@ const collectActiveRuleIdsCoverageViolations = (params: {
|
|
|
552
615
|
};
|
|
553
616
|
|
|
554
617
|
const collectPreWritePlatformSkillsViolations = (params: {
|
|
618
|
+
repoRoot: string;
|
|
555
619
|
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
556
620
|
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
557
621
|
skillsEnforcement: SkillsEnforcementResolution;
|
|
@@ -625,7 +689,11 @@ const collectPreWritePlatformSkillsViolations = (params: {
|
|
|
625
689
|
|
|
626
690
|
const missingCriticalRulesByPlatform: string[] = [];
|
|
627
691
|
for (const platform of detectedPlatforms) {
|
|
628
|
-
const requiredCriticalRuleIds =
|
|
692
|
+
const requiredCriticalRuleIds = resolvePreWriteCriticalSkillsRules({
|
|
693
|
+
platform,
|
|
694
|
+
stage: 'PRE_WRITE',
|
|
695
|
+
repoRoot: params.repoRoot,
|
|
696
|
+
});
|
|
629
697
|
if (requiredCriticalRuleIds.length === 0) {
|
|
630
698
|
continue;
|
|
631
699
|
}
|
|
@@ -716,14 +784,22 @@ const toSkillsContractAssessment = (params: {
|
|
|
716
784
|
platform,
|
|
717
785
|
required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
|
|
718
786
|
required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
719
|
-
required_critical_rule_ids:
|
|
787
|
+
required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
788
|
+
platform,
|
|
789
|
+
stage: params.stage,
|
|
790
|
+
repoRoot: params.repoRoot,
|
|
791
|
+
}),
|
|
720
792
|
required_any_transversal_critical_rule_ids: [
|
|
721
793
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
722
794
|
],
|
|
723
795
|
active_prefix_covered: false,
|
|
724
796
|
evaluated_prefix_covered: false,
|
|
725
797
|
missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
726
|
-
missing_critical_rule_ids:
|
|
798
|
+
missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
799
|
+
platform,
|
|
800
|
+
stage: params.stage,
|
|
801
|
+
repoRoot: params.repoRoot,
|
|
802
|
+
}),
|
|
727
803
|
transversal_critical_covered: false,
|
|
728
804
|
missing_any_transversal_critical_rule_ids: [
|
|
729
805
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
@@ -798,14 +874,22 @@ const toSkillsContractAssessment = (params: {
|
|
|
798
874
|
platform,
|
|
799
875
|
required_rule_prefix: PLATFORM_SKILLS_RULE_PREFIXES[platform],
|
|
800
876
|
required_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
801
|
-
required_critical_rule_ids:
|
|
877
|
+
required_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
878
|
+
platform,
|
|
879
|
+
stage: params.stage,
|
|
880
|
+
repoRoot: params.repoRoot,
|
|
881
|
+
}),
|
|
802
882
|
required_any_transversal_critical_rule_ids: [
|
|
803
883
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
804
884
|
],
|
|
805
885
|
active_prefix_covered: false,
|
|
806
886
|
evaluated_prefix_covered: false,
|
|
807
887
|
missing_bundles: [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]],
|
|
808
|
-
missing_critical_rule_ids:
|
|
888
|
+
missing_critical_rule_ids: resolvePreWriteCriticalSkillsRules({
|
|
889
|
+
platform,
|
|
890
|
+
stage: params.stage,
|
|
891
|
+
repoRoot: params.repoRoot,
|
|
892
|
+
}),
|
|
809
893
|
transversal_critical_covered: false,
|
|
810
894
|
missing_any_transversal_critical_rule_ids: [
|
|
811
895
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
@@ -859,7 +943,13 @@ const toSkillsContractAssessment = (params: {
|
|
|
859
943
|
for (const platform of assessmentPlatforms) {
|
|
860
944
|
const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
|
|
861
945
|
const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
|
|
862
|
-
const requiredCriticalRuleIds = [
|
|
946
|
+
const requiredCriticalRuleIds = [
|
|
947
|
+
...resolvePreWriteCriticalSkillsRules({
|
|
948
|
+
platform,
|
|
949
|
+
stage: params.stage,
|
|
950
|
+
repoRoot: params.repoRoot,
|
|
951
|
+
}),
|
|
952
|
+
];
|
|
863
953
|
const requiredAnyTransversalCriticalRuleIds = [
|
|
864
954
|
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
865
955
|
];
|
|
@@ -1057,6 +1147,7 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
1057
1147
|
|
|
1058
1148
|
violations.push(
|
|
1059
1149
|
...collectPreWritePlatformSkillsViolations({
|
|
1150
|
+
repoRoot: params.repoRoot,
|
|
1060
1151
|
evidence: params.evidence,
|
|
1061
1152
|
coverage,
|
|
1062
1153
|
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] : []),
|
|
@@ -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.277",
|
|
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') {
|