pumuki 6.3.268 → 6.3.269
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/VERSION +1 -1
- package/docs/governance/CONTRIBUTING.md +1 -1
- package/integrations/config/coreSkillsLock.ts +2 -1
- package/integrations/config/skillsDetectorRegistry.ts +18 -0
- package/integrations/config/skillsRuleClassification.ts +147 -0
- package/integrations/git/runPlatformGate.ts +100 -30
- package/integrations/lifecycle/cli.ts +80 -0
- package/integrations/lifecycle/doctor.ts +19 -0
- package/integrations/lifecycle/install.ts +14 -0
- package/integrations/lifecycle/notificationStatus.ts +54 -0
- package/integrations/lifecycle/status.ts +22 -2
- package/integrations/lifecycle/watch.ts +2 -1
- package/integrations/mcp/evidenceStdioServer.cli.ts +2 -2
- package/integrations/policy/skillsEnforcement.ts +1 -12
- package/package.json +2 -1
- package/scripts/classify-skills-rules.ts +32 -0
- package/scripts/framework-menu-system-notifications-cause.ts +11 -1
- package/scripts/package-install-smoke-consumer-git-payload-lib.ts +2 -2
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.269
|
|
@@ -89,4 +89,4 @@ npm run test:heuristics
|
|
|
89
89
|
## Notes for automation contributors
|
|
90
90
|
|
|
91
91
|
This repository still contains legacy hooks/scripts for compatibility.
|
|
92
|
-
|
|
92
|
+
Framework refactor commits must run through the managed hooks; bypassing verification is not an accepted maintainer workflow.
|
|
@@ -427,6 +427,16 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
427
427
|
'skills.frontend.no-god-classes': heuristicDetector('typescript.god-class', [
|
|
428
428
|
'heuristics.ts.god-class-large-class.ast',
|
|
429
429
|
]),
|
|
430
|
+
'skills.ios.guideline.ios.verificar-que-no-viole-solid-srp-ocp-lsp-isp-dip': heuristicDetector(
|
|
431
|
+
'ios.solid',
|
|
432
|
+
[
|
|
433
|
+
'heuristics.ios.solid.srp.presentation-mixed-responsibilities.ast',
|
|
434
|
+
'heuristics.ios.solid.ocp.discriminator-switch.ast',
|
|
435
|
+
'heuristics.ios.solid.dip.concrete-framework-dependency.ast',
|
|
436
|
+
'heuristics.ios.solid.isp.fat-protocol-dependency.ast',
|
|
437
|
+
'heuristics.ios.solid.lsp.narrowed-precondition.ast',
|
|
438
|
+
]
|
|
439
|
+
),
|
|
430
440
|
'skills.android.no-thread-sleep': heuristicDetector('android.thread-sleep', [
|
|
431
441
|
'heuristics.android.thread-sleep.ast',
|
|
432
442
|
]),
|
|
@@ -505,6 +515,14 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
505
515
|
'heuristics.android.solid.isp.fat-interface-dependency.ast',
|
|
506
516
|
'heuristics.android.solid.lsp.narrowed-precondition.ast',
|
|
507
517
|
]),
|
|
518
|
+
'skills.android.guideline.android.verificar-que-no-viole-solid-srp-ocp-lsp-isp-dip':
|
|
519
|
+
heuristicDetector('android.solid', [
|
|
520
|
+
'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
|
|
521
|
+
'heuristics.android.solid.ocp.discriminator-branching.ast',
|
|
522
|
+
'heuristics.android.solid.dip.concrete-framework-dependency.ast',
|
|
523
|
+
'heuristics.android.solid.isp.fat-interface-dependency.ast',
|
|
524
|
+
'heuristics.android.solid.lsp.narrowed-precondition.ast',
|
|
525
|
+
]),
|
|
508
526
|
};
|
|
509
527
|
|
|
510
528
|
export const listSkillsDetectorBindings = (): ReadonlyArray<{
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import type { SkillsCompiledRule, SkillsLockV1 } from './skillsLock';
|
|
2
|
+
import { resolveMappedHeuristicRuleIdsForCompiledRule } from './skillsDetectorRegistry';
|
|
3
|
+
|
|
4
|
+
export type SkillsRuleClassificationStatus =
|
|
5
|
+
| 'AST_IMPLEMENTED'
|
|
6
|
+
| 'IMPLEMENTABLE_AHORA'
|
|
7
|
+
| 'REQUIERE_ESTUDIO'
|
|
8
|
+
| 'NO_ES_REGLA_DE_CODIGO';
|
|
9
|
+
|
|
10
|
+
export type ClassifiedSkillsRule = {
|
|
11
|
+
ruleId: string;
|
|
12
|
+
platform: SkillsCompiledRule['platform'];
|
|
13
|
+
sourceSkill: string;
|
|
14
|
+
sourcePath: string;
|
|
15
|
+
evaluationMode: 'AUTO' | 'DECLARATIVE';
|
|
16
|
+
severity: SkillsCompiledRule['severity'];
|
|
17
|
+
status: SkillsRuleClassificationStatus;
|
|
18
|
+
reason: string;
|
|
19
|
+
astNodeIds: ReadonlyArray<string>;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type SkillsRuleClassificationSummary = {
|
|
23
|
+
total: number;
|
|
24
|
+
byStatus: Record<SkillsRuleClassificationStatus, number>;
|
|
25
|
+
byPlatform: Record<string, number>;
|
|
26
|
+
rules: ReadonlyArray<ClassifiedSkillsRule>;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const emptyStatusCounts = (): Record<SkillsRuleClassificationStatus, number> => ({
|
|
30
|
+
AST_IMPLEMENTED: 0,
|
|
31
|
+
IMPLEMENTABLE_AHORA: 0,
|
|
32
|
+
REQUIERE_ESTUDIO: 0,
|
|
33
|
+
NO_ES_REGLA_DE_CODIGO: 0,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const normalizeText = (value: string): string =>
|
|
37
|
+
value
|
|
38
|
+
.normalize('NFD')
|
|
39
|
+
.replace(/\p{Diacritic}/gu, '')
|
|
40
|
+
.toLowerCase();
|
|
41
|
+
|
|
42
|
+
const NON_CODE_RULE_PATTERNS: ReadonlyArray<RegExp> = [
|
|
43
|
+
/\bactua como\b/,
|
|
44
|
+
/\bsiempre responder\b/,
|
|
45
|
+
/\bflujo bdd\b/,
|
|
46
|
+
/\bfeature files\b/,
|
|
47
|
+
/\bspecs\b.*\bimplementacion\b/,
|
|
48
|
+
/\bcomprobar que compile\b/,
|
|
49
|
+
/\banalizar estructura existente\b/,
|
|
50
|
+
/\bandroid profiler\b/,
|
|
51
|
+
/\bxcode instruments\b/,
|
|
52
|
+
/\bdocumentation\b/,
|
|
53
|
+
/\bdocumentacion\b/,
|
|
54
|
+
/\breadme\b/,
|
|
55
|
+
/\bci\/cd\b/,
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const isNonCodeRule = (rule: SkillsCompiledRule): boolean => {
|
|
59
|
+
const haystack = normalizeText(`${rule.id} ${rule.description} ${rule.sourceSkill}`);
|
|
60
|
+
return NON_CODE_RULE_PATTERNS.some((pattern) => pattern.test(haystack));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const classifyRule = (rule: SkillsCompiledRule): ClassifiedSkillsRule => {
|
|
64
|
+
const evaluationMode = rule.evaluationMode ?? 'AUTO';
|
|
65
|
+
const astNodeIds = resolveMappedHeuristicRuleIdsForCompiledRule(rule);
|
|
66
|
+
|
|
67
|
+
if (astNodeIds.length > 0) {
|
|
68
|
+
return {
|
|
69
|
+
ruleId: rule.id,
|
|
70
|
+
platform: rule.platform,
|
|
71
|
+
sourceSkill: rule.sourceSkill,
|
|
72
|
+
sourcePath: rule.sourcePath,
|
|
73
|
+
evaluationMode,
|
|
74
|
+
severity: rule.severity,
|
|
75
|
+
status: 'AST_IMPLEMENTED',
|
|
76
|
+
reason: `Mapped to AST/nodal detector(s): ${astNodeIds.join(', ')}`,
|
|
77
|
+
astNodeIds,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (evaluationMode === 'AUTO') {
|
|
82
|
+
return {
|
|
83
|
+
ruleId: rule.id,
|
|
84
|
+
platform: rule.platform,
|
|
85
|
+
sourceSkill: rule.sourceSkill,
|
|
86
|
+
sourcePath: rule.sourcePath,
|
|
87
|
+
evaluationMode,
|
|
88
|
+
severity: rule.severity,
|
|
89
|
+
status: 'IMPLEMENTABLE_AHORA',
|
|
90
|
+
reason: 'AUTO rule without AST/nodal detector binding; implement detector mapping.',
|
|
91
|
+
astNodeIds,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (isNonCodeRule(rule)) {
|
|
96
|
+
return {
|
|
97
|
+
ruleId: rule.id,
|
|
98
|
+
platform: rule.platform,
|
|
99
|
+
sourceSkill: rule.sourceSkill,
|
|
100
|
+
sourcePath: rule.sourcePath,
|
|
101
|
+
evaluationMode,
|
|
102
|
+
severity: rule.severity,
|
|
103
|
+
status: 'NO_ES_REGLA_DE_CODIGO',
|
|
104
|
+
reason: 'Process, documentation, validation workflow, or operating rule; keep outside code AST runtime.',
|
|
105
|
+
astNodeIds,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
ruleId: rule.id,
|
|
111
|
+
platform: rule.platform,
|
|
112
|
+
sourceSkill: rule.sourceSkill,
|
|
113
|
+
sourcePath: rule.sourcePath,
|
|
114
|
+
evaluationMode,
|
|
115
|
+
severity: rule.severity,
|
|
116
|
+
status: 'REQUIERE_ESTUDIO',
|
|
117
|
+
reason: 'Declarative skill rule without AST/nodal detector. Study exact code pattern, AST node, and closure criterion.',
|
|
118
|
+
astNodeIds,
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const classifySkillsRules = (
|
|
123
|
+
lock: SkillsLockV1
|
|
124
|
+
): SkillsRuleClassificationSummary => {
|
|
125
|
+
const rules = lock.bundles
|
|
126
|
+
.flatMap((bundle) => bundle.rules)
|
|
127
|
+
.filter((rule) => rule.id.startsWith('skills.'))
|
|
128
|
+
.map(classifyRule)
|
|
129
|
+
.sort((left, right) => left.ruleId.localeCompare(right.ruleId));
|
|
130
|
+
|
|
131
|
+
const byStatus = emptyStatusCounts();
|
|
132
|
+
const byPlatform: Record<string, number> = {};
|
|
133
|
+
|
|
134
|
+
for (const rule of rules) {
|
|
135
|
+
byStatus[rule.status] += 1;
|
|
136
|
+
byPlatform[rule.platform] = (byPlatform[rule.platform] ?? 0) + 1;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
total: rules.length,
|
|
141
|
+
byStatus,
|
|
142
|
+
byPlatform: Object.fromEntries(
|
|
143
|
+
Object.entries(byPlatform).sort(([left], [right]) => left.localeCompare(right))
|
|
144
|
+
),
|
|
145
|
+
rules,
|
|
146
|
+
};
|
|
147
|
+
};
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
readPreWriteLeaseStatus,
|
|
44
44
|
toPreWriteEnforcementGapFinding,
|
|
45
45
|
} from '../lifecycle/preWriteLease';
|
|
46
|
+
import { evaluateContextGate, type ContextGateResult } from '../context/contextGate';
|
|
47
|
+
import { emitGateBlockedNotification } from '../notifications/emitAuditSummaryNotification';
|
|
46
48
|
|
|
47
49
|
export type OperationalMemoryShadowRecommendation = {
|
|
48
50
|
recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK';
|
|
@@ -77,6 +79,8 @@ export type GateDependencies = {
|
|
|
77
79
|
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
78
80
|
branch: string | null;
|
|
79
81
|
}) => GateWaiverResult;
|
|
82
|
+
evaluateContextGate: typeof evaluateContextGate;
|
|
83
|
+
notifyGateBlocked: typeof emitGateBlockedNotification;
|
|
80
84
|
};
|
|
81
85
|
|
|
82
86
|
const defaultServices: GateServices = {
|
|
@@ -109,20 +113,8 @@ const normalizeScopedRuleEngineFindings = (params: {
|
|
|
109
113
|
findings: ReadonlyArray<Finding>;
|
|
110
114
|
scope: GateScope;
|
|
111
115
|
}): ReadonlyArray<Finding> => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
return params.findings.filter((finding) => {
|
|
117
|
-
if (
|
|
118
|
-
!finding.filePath ||
|
|
119
|
-
hasActionableFindingLocation(finding) ||
|
|
120
|
-
(!finding.ruleId.startsWith('skills.') && !finding.ruleId.startsWith('heuristics.'))
|
|
121
|
-
) {
|
|
122
|
-
return true;
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
});
|
|
116
|
+
void params.scope;
|
|
117
|
+
return params.findings;
|
|
126
118
|
};
|
|
127
119
|
|
|
128
120
|
const buildDefaultMemoryShadowRecommendation = (params: {
|
|
@@ -199,6 +191,8 @@ const defaultDependencies: GateDependencies = {
|
|
|
199
191
|
stage,
|
|
200
192
|
branch,
|
|
201
193
|
}),
|
|
194
|
+
evaluateContextGate,
|
|
195
|
+
notifyGateBlocked: emitGateBlockedNotification,
|
|
202
196
|
};
|
|
203
197
|
|
|
204
198
|
const readSymbolicBranchRef = (git: IGitService, repoRoot: string): string | null => {
|
|
@@ -287,6 +281,34 @@ const toSkillsUnsupportedAutoRulesBlockingFinding = (params: {
|
|
|
287
281
|
};
|
|
288
282
|
};
|
|
289
283
|
|
|
284
|
+
const toSkillsDeclarativeRulesClassificationFinding = (params: {
|
|
285
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
286
|
+
declarativeRuleIds: ReadonlyArray<string>;
|
|
287
|
+
facts: ReadonlyArray<Fact>;
|
|
288
|
+
}): Finding | undefined => {
|
|
289
|
+
if (params.declarativeRuleIds.length === 0) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
if (collectObservedCodePathsFromFacts(params.facts).length === 0) {
|
|
293
|
+
return undefined;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const sampleRuleIds = [...params.declarativeRuleIds].sort().slice(0, 12).join(', ');
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
ruleId: 'governance.skills.declarative-rules.classification-required',
|
|
300
|
+
severity: 'ERROR',
|
|
301
|
+
code: 'SKILLS_DECLARATIVE_RULES_CLASSIFICATION_REQUIRED',
|
|
302
|
+
message:
|
|
303
|
+
`Skills declarative rules require AST/nodal classification at ${params.stage}: ` +
|
|
304
|
+
`total=${params.declarativeRuleIds.length} sample_rule_ids=[${sampleRuleIds}]. ` +
|
|
305
|
+
'Classify every rule as IMPLEMENTABLE_AHORA, REQUIERE_ESTUDIO, or NO_ES_REGLA_DE_CODIGO; code skills cannot remain as hidden declarative/advisory rules.',
|
|
306
|
+
filePath: 'skills.lock.json',
|
|
307
|
+
matchedBy: 'SkillsDeclarativeRulesClassificationGuard',
|
|
308
|
+
source: 'skills-declarative-rules-classification',
|
|
309
|
+
};
|
|
310
|
+
};
|
|
311
|
+
|
|
290
312
|
const PLATFORM_SKILLS_RULE_PREFIXES: Record<
|
|
291
313
|
'ios' | 'android' | 'backend' | 'frontend',
|
|
292
314
|
string
|
|
@@ -808,6 +830,15 @@ const shouldBlockFromFinding = (finding: Finding | undefined): boolean => {
|
|
|
808
830
|
return finding.blocking !== false;
|
|
809
831
|
};
|
|
810
832
|
|
|
833
|
+
const resolvePrimaryBlockingFinding = (
|
|
834
|
+
findings: ReadonlyArray<Finding>
|
|
835
|
+
): Finding | undefined => (
|
|
836
|
+
findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'CRITICAL') ??
|
|
837
|
+
findings.find((finding) => shouldBlockFromFinding(finding) && finding.severity === 'ERROR') ??
|
|
838
|
+
findings.find((finding) => shouldBlockFromFinding(finding)) ??
|
|
839
|
+
findings[0]
|
|
840
|
+
);
|
|
841
|
+
|
|
811
842
|
const applySkillsFindingEnforcement = (
|
|
812
843
|
finding: Finding | undefined
|
|
813
844
|
): Finding | undefined => {
|
|
@@ -830,22 +861,9 @@ const toSoftPreCommitSkillsFinding = (params: {
|
|
|
830
861
|
enabled: boolean;
|
|
831
862
|
observedCodePaths: ReadonlyArray<string>;
|
|
832
863
|
}): Finding | undefined => {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
if (!params.enabled || !shouldBlockFromFinding(params.finding)) {
|
|
837
|
-
return params.finding;
|
|
838
|
-
}
|
|
839
|
-
return {
|
|
840
|
-
...params.finding,
|
|
841
|
-
severity: 'WARN',
|
|
842
|
-
blocking: false,
|
|
843
|
-
code: `${params.finding.code}_SOFT_PRECOMMIT`,
|
|
844
|
-
message:
|
|
845
|
-
`${params.finding.message} ` +
|
|
846
|
-
`Soft-enforced at PRE_COMMIT for low-risk scope (observed_code_paths=${params.observedCodePaths.length}). ` +
|
|
847
|
-
'Strict enforcement remains active at PRE_PUSH/CI.',
|
|
848
|
-
};
|
|
864
|
+
void params.enabled;
|
|
865
|
+
void params.observedCodePaths;
|
|
866
|
+
return params.finding;
|
|
849
867
|
};
|
|
850
868
|
|
|
851
869
|
export async function runPlatformGate(params: {
|
|
@@ -868,6 +886,11 @@ export async function runPlatformGate(params: {
|
|
|
868
886
|
const repoRoot = git.resolveRepoRoot();
|
|
869
887
|
const auditMode = params.auditMode ?? 'gate';
|
|
870
888
|
const shouldShortCircuitSdd = params.sddShortCircuit ?? false;
|
|
889
|
+
const contextGate: ContextGateResult = dependencies.evaluateContextGate({
|
|
890
|
+
repoRoot,
|
|
891
|
+
stage: params.policy.stage,
|
|
892
|
+
});
|
|
893
|
+
const contextBlockingFinding = contextGate.finding;
|
|
871
894
|
let sddDecision:
|
|
872
895
|
| Pick<SddDecision, 'allowed' | 'code' | 'message'>
|
|
873
896
|
| undefined;
|
|
@@ -1016,6 +1039,16 @@ export async function runPlatformGate(params: {
|
|
|
1016
1039
|
unsupportedAutoRuleIds: skillsRuleSet.unsupportedAutoRuleIds ?? [],
|
|
1017
1040
|
})
|
|
1018
1041
|
: undefined;
|
|
1042
|
+
const declarativeRulesClassificationFinding =
|
|
1043
|
+
params.policy.stage === 'PRE_COMMIT' ||
|
|
1044
|
+
params.policy.stage === 'PRE_PUSH' ||
|
|
1045
|
+
params.policy.stage === 'CI'
|
|
1046
|
+
? toSkillsDeclarativeRulesClassificationFinding({
|
|
1047
|
+
stage: params.policy.stage,
|
|
1048
|
+
declarativeRuleIds: skillsRuleSet.registryCoverage?.declarativeRuleIds ?? [],
|
|
1049
|
+
facts: factsForPlatformEvaluation,
|
|
1050
|
+
})
|
|
1051
|
+
: undefined;
|
|
1019
1052
|
const effectiveUnsupportedSkillsMappingInput = applySkillsFindingEnforcement(
|
|
1020
1053
|
unsupportedSkillsMappingFinding
|
|
1021
1054
|
);
|
|
@@ -1217,6 +1250,7 @@ export async function runPlatformGate(params: {
|
|
|
1217
1250
|
&& !degradedModeBlocks
|
|
1218
1251
|
&& !shouldBlockFromFinding(policyAsCodeBlockingFinding)
|
|
1219
1252
|
&& !shouldBlockFromFinding(coverageBlockingFinding)
|
|
1253
|
+
&& !shouldBlockFromFinding(declarativeRulesClassificationFinding)
|
|
1220
1254
|
&& !shouldBlockFromFinding(activeRulesEmptyForCodeChangesFinding)
|
|
1221
1255
|
&& !shouldBlockFromFinding(effectiveIosTestsQualityFinding)
|
|
1222
1256
|
&& !shouldBlockFromFinding(astIntelligenceDualFinding)
|
|
@@ -1244,6 +1278,7 @@ export async function runPlatformGate(params: {
|
|
|
1244
1278
|
});
|
|
1245
1279
|
const effectiveFindings = sddBlockingFinding
|
|
1246
1280
|
? [
|
|
1281
|
+
...(contextBlockingFinding ? [contextBlockingFinding] : []),
|
|
1247
1282
|
sddBlockingFinding,
|
|
1248
1283
|
...(degradedModeFinding ? [degradedModeFinding] : []),
|
|
1249
1284
|
...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []),
|
|
@@ -1256,6 +1291,7 @@ export async function runPlatformGate(params: {
|
|
|
1256
1291
|
...(effectiveIosTestsQualityFinding ? [effectiveIosTestsQualityFinding] : []),
|
|
1257
1292
|
...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []),
|
|
1258
1293
|
...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
|
|
1294
|
+
...(declarativeRulesClassificationFinding ? [declarativeRulesClassificationFinding] : []),
|
|
1259
1295
|
...brownfieldHotspotFindings,
|
|
1260
1296
|
...tddBddEvaluation.findings,
|
|
1261
1297
|
...findings,
|
|
@@ -1269,11 +1305,14 @@ export async function runPlatformGate(params: {
|
|
|
1269
1305
|
|| effectiveIosTestsQualityFinding
|
|
1270
1306
|
|| astIntelligenceDualFinding
|
|
1271
1307
|
|| coverageBlockingFinding
|
|
1308
|
+
|| declarativeRulesClassificationFinding
|
|
1272
1309
|
|| brownfieldHotspotFindings.length > 0
|
|
1273
1310
|
|| policyAsCodeBlockingFinding
|
|
1311
|
+
|| contextBlockingFinding
|
|
1274
1312
|
|| degradedModeFinding
|
|
1275
1313
|
|| tddBddEvaluation.findings.length > 0
|
|
1276
1314
|
? [
|
|
1315
|
+
...(contextBlockingFinding ? [contextBlockingFinding] : []),
|
|
1277
1316
|
...(degradedModeFinding ? [degradedModeFinding] : []),
|
|
1278
1317
|
...(preWriteLeaseFinding ? [preWriteLeaseFinding] : []),
|
|
1279
1318
|
...(policyAsCodeBlockingFinding ? [policyAsCodeBlockingFinding] : []),
|
|
@@ -1285,6 +1324,7 @@ export async function runPlatformGate(params: {
|
|
|
1285
1324
|
...(effectiveIosTestsQualityFinding ? [effectiveIosTestsQualityFinding] : []),
|
|
1286
1325
|
...(astIntelligenceDualFinding ? [astIntelligenceDualFinding] : []),
|
|
1287
1326
|
...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
|
|
1327
|
+
...(declarativeRulesClassificationFinding ? [declarativeRulesClassificationFinding] : []),
|
|
1288
1328
|
...brownfieldHotspotFindings,
|
|
1289
1329
|
...tddBddEvaluation.findings,
|
|
1290
1330
|
...findings,
|
|
@@ -1304,6 +1344,7 @@ export async function runPlatformGate(params: {
|
|
|
1304
1344
|
);
|
|
1305
1345
|
const baseGateOutcome =
|
|
1306
1346
|
sddBlockingFinding ||
|
|
1347
|
+
shouldBlockFromFinding(contextBlockingFinding) ||
|
|
1307
1348
|
degradedModeBlocks ||
|
|
1308
1349
|
shouldBlockFromFinding(preWriteLeaseFinding) ||
|
|
1309
1350
|
shouldBlockFromFinding(policyAsCodeBlockingFinding) ||
|
|
@@ -1315,6 +1356,7 @@ export async function runPlatformGate(params: {
|
|
|
1315
1356
|
shouldBlockFromFinding(effectiveIosTestsQualityFinding) ||
|
|
1316
1357
|
hasAstIntelligenceBlockingFinding ||
|
|
1317
1358
|
shouldBlockFromFinding(coverageBlockingFinding) ||
|
|
1359
|
+
shouldBlockFromFinding(declarativeRulesClassificationFinding) ||
|
|
1318
1360
|
brownfieldHotspotFindings.some((finding) => shouldBlockFromFinding(finding)) ||
|
|
1319
1361
|
hasTddBddBlockingFinding
|
|
1320
1362
|
? 'BLOCK'
|
|
@@ -1432,6 +1474,34 @@ export async function runPlatformGate(params: {
|
|
|
1432
1474
|
});
|
|
1433
1475
|
|
|
1434
1476
|
if (gateOutcome === 'BLOCK') {
|
|
1477
|
+
const primaryBlockingFinding = resolvePrimaryBlockingFinding(findingsWithWaiver);
|
|
1478
|
+
if (
|
|
1479
|
+
primaryBlockingFinding &&
|
|
1480
|
+
(
|
|
1481
|
+
params.policy.stage === 'PRE_COMMIT' ||
|
|
1482
|
+
params.policy.stage === 'PRE_PUSH' ||
|
|
1483
|
+
params.policy.stage === 'CI'
|
|
1484
|
+
)
|
|
1485
|
+
) {
|
|
1486
|
+
const notificationResult = dependencies.notifyGateBlocked({
|
|
1487
|
+
repoRoot,
|
|
1488
|
+
stage: params.policy.stage,
|
|
1489
|
+
totalViolations: findingsWithWaiver.length,
|
|
1490
|
+
causeCode: primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId,
|
|
1491
|
+
causeMessage: primaryBlockingFinding.message,
|
|
1492
|
+
remediation:
|
|
1493
|
+
primaryBlockingFinding.expected_fix ??
|
|
1494
|
+
'Corrige la causa bloqueante y reejecuta el gate.',
|
|
1495
|
+
});
|
|
1496
|
+
process.stderr.write(
|
|
1497
|
+
`[pumuki][blocked] code=${primaryBlockingFinding.code ?? primaryBlockingFinding.ruleId} ` +
|
|
1498
|
+
`stage=${params.policy.stage} reason=${primaryBlockingFinding.message}\n`
|
|
1499
|
+
);
|
|
1500
|
+
process.stderr.write(
|
|
1501
|
+
`[pumuki][notification] delivered=${notificationResult.delivered ? 'yes' : 'no'} ` +
|
|
1502
|
+
`reason=${notificationResult.reason}\n`
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1435
1505
|
if (params.silent !== true) {
|
|
1436
1506
|
dependencies.printGateFindings(findingsWithWaiver);
|
|
1437
1507
|
}
|
|
@@ -76,6 +76,11 @@ import {
|
|
|
76
76
|
import { runPolicyReconcile } from './policyReconcile';
|
|
77
77
|
import { runLifecycleAudit, type LifecycleAuditStage } from './audit';
|
|
78
78
|
import { resolvePreWriteEnforcement, type PreWriteEnforcementResolution } from '../policy/preWriteEnforcement';
|
|
79
|
+
import {
|
|
80
|
+
readLocalContextStatus,
|
|
81
|
+
repairLocalContext,
|
|
82
|
+
writeLocalContext,
|
|
83
|
+
} from '../context/contextGate';
|
|
79
84
|
|
|
80
85
|
type LifecycleCommand =
|
|
81
86
|
| 'bootstrap'
|
|
@@ -92,6 +97,7 @@ type LifecycleCommand =
|
|
|
92
97
|
| 'adapter'
|
|
93
98
|
| 'analytics'
|
|
94
99
|
| 'policy'
|
|
100
|
+
| 'context'
|
|
95
101
|
| 'audit';
|
|
96
102
|
|
|
97
103
|
type SddCommand =
|
|
@@ -107,6 +113,7 @@ type LoopCommand = 'run' | 'status' | 'stop' | 'resume' | 'list' | 'export';
|
|
|
107
113
|
type AnalyticsCommand = 'hotspots';
|
|
108
114
|
type AnalyticsHotspotsCommand = 'report' | 'diagnose';
|
|
109
115
|
type PolicyCommand = 'reconcile';
|
|
116
|
+
type ContextCommand = 'init' | 'status' | 'repair';
|
|
110
117
|
|
|
111
118
|
type SddSessionAction = 'open' | 'refresh' | 'close';
|
|
112
119
|
|
|
@@ -178,6 +185,7 @@ export type ParsedArgs = {
|
|
|
178
185
|
policyCommand?: PolicyCommand;
|
|
179
186
|
policyStrict?: boolean;
|
|
180
187
|
policyApply?: boolean;
|
|
188
|
+
contextCommand?: ContextCommand;
|
|
181
189
|
auditStage?: LifecycleAuditStage;
|
|
182
190
|
auditEngine?: boolean;
|
|
183
191
|
};
|
|
@@ -204,6 +212,9 @@ Pumuki lifecycle commands:
|
|
|
204
212
|
pumuki analytics hotspots report [--top=<n>] [--since-days=<n>] [--json] [--output-json=<path>] [--output-markdown=<path>]
|
|
205
213
|
pumuki analytics hotspots diagnose [--json]
|
|
206
214
|
pumuki policy reconcile [--strict] [--apply] [--json]
|
|
215
|
+
pumuki context init [--json]
|
|
216
|
+
pumuki context status [--json]
|
|
217
|
+
pumuki context repair [--json]
|
|
207
218
|
pumuki sdd status [--json]
|
|
208
219
|
pumuki sdd validate [--stage=PRE_WRITE|PRE_COMMIT|PRE_PUSH|CI] [--json]
|
|
209
220
|
pumuki sdd session --open --change=<change-id|auto> [--ttl-minutes=<n>] [--json]
|
|
@@ -239,6 +250,7 @@ const isLifecycleCommand = (value: string): value is LifecycleCommand =>
|
|
|
239
250
|
value === 'adapter' ||
|
|
240
251
|
value === 'analytics' ||
|
|
241
252
|
value === 'policy' ||
|
|
253
|
+
value === 'context' ||
|
|
242
254
|
value === 'audit';
|
|
243
255
|
|
|
244
256
|
const parseAdapterAgent = (value?: string): AdapterAgent => {
|
|
@@ -651,6 +663,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
651
663
|
let analyticsSinceDays: ParsedArgs['analyticsSinceDays'];
|
|
652
664
|
let analyticsJsonOutputPath: ParsedArgs['analyticsJsonOutputPath'];
|
|
653
665
|
let analyticsMarkdownOutputPath: ParsedArgs['analyticsMarkdownOutputPath'];
|
|
666
|
+
let contextCommand: ParsedArgs['contextCommand'];
|
|
654
667
|
let auditStage: LifecycleAuditStage | undefined;
|
|
655
668
|
let auditEngine = false;
|
|
656
669
|
|
|
@@ -863,6 +876,31 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
863
876
|
};
|
|
864
877
|
}
|
|
865
878
|
|
|
879
|
+
if (commandRaw === 'context') {
|
|
880
|
+
const subcommandRaw = argv[1] ?? 'status';
|
|
881
|
+
if (
|
|
882
|
+
subcommandRaw !== 'init' &&
|
|
883
|
+
subcommandRaw !== 'status' &&
|
|
884
|
+
subcommandRaw !== 'repair'
|
|
885
|
+
) {
|
|
886
|
+
throw new Error(`Unsupported context subcommand "${subcommandRaw}".\n\n${HELP_TEXT}`);
|
|
887
|
+
}
|
|
888
|
+
contextCommand = subcommandRaw;
|
|
889
|
+
for (const arg of argv.slice(2)) {
|
|
890
|
+
if (arg === '--json') {
|
|
891
|
+
json = true;
|
|
892
|
+
continue;
|
|
893
|
+
}
|
|
894
|
+
throw new Error(`Unsupported argument "${arg}".\n\n${HELP_TEXT}`);
|
|
895
|
+
}
|
|
896
|
+
return {
|
|
897
|
+
command: commandRaw,
|
|
898
|
+
purgeArtifacts: false,
|
|
899
|
+
json,
|
|
900
|
+
contextCommand,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
866
904
|
if (commandRaw === 'loop') {
|
|
867
905
|
const subcommandRaw = argv[1] ?? '';
|
|
868
906
|
if (
|
|
@@ -1582,6 +1620,16 @@ const printDoctorReport = (
|
|
|
1582
1620
|
`PRE_PUSH=${report.policyValidation.stages.PRE_PUSH.validationCode ?? 'n/a'} strict=${report.policyValidation.stages.PRE_PUSH.strict ? 'yes' : 'no'} ` +
|
|
1583
1621
|
`CI=${report.policyValidation.stages.CI.validationCode ?? 'n/a'} strict=${report.policyValidation.stages.CI.strict ? 'yes' : 'no'}`
|
|
1584
1622
|
);
|
|
1623
|
+
writeInfo(
|
|
1624
|
+
`[pumuki] notifications: enabled=${report.notifications.enabled ? 'yes' : 'no'} ` +
|
|
1625
|
+
`deliverable=${report.notifications.deliverable ? 'yes' : 'no'} ` +
|
|
1626
|
+
`channel=${report.notifications.channel} ` +
|
|
1627
|
+
`blocked_dialog=${report.notifications.blockedDialogEnabled ? 'yes' : 'no'} ` +
|
|
1628
|
+
`reason=${report.notifications.disabledReason ?? 'none'}`
|
|
1629
|
+
);
|
|
1630
|
+
if (report.notifications.remediation) {
|
|
1631
|
+
writeInfo(`[pumuki] notifications remediation: ${report.notifications.remediation}`);
|
|
1632
|
+
}
|
|
1585
1633
|
|
|
1586
1634
|
for (const issue of report.issues) {
|
|
1587
1635
|
writeInfo(`[pumuki] ${issue.severity.toUpperCase()}: ${issue.message}`);
|
|
@@ -2377,6 +2425,28 @@ export const runLifecycleCli = async (
|
|
|
2377
2425
|
}
|
|
2378
2426
|
return result.gate_exit_code;
|
|
2379
2427
|
}
|
|
2428
|
+
case 'context': {
|
|
2429
|
+
const repoRoot = process.cwd();
|
|
2430
|
+
const result =
|
|
2431
|
+
parsed.contextCommand === 'init'
|
|
2432
|
+
? writeLocalContext(repoRoot)
|
|
2433
|
+
: parsed.contextCommand === 'repair'
|
|
2434
|
+
? repairLocalContext(repoRoot)
|
|
2435
|
+
: readLocalContextStatus(repoRoot);
|
|
2436
|
+
if (parsed.json) {
|
|
2437
|
+
writeInfo(JSON.stringify(result, null, 2));
|
|
2438
|
+
} else {
|
|
2439
|
+
writeInfo(`[pumuki][context] status=${result.status}`);
|
|
2440
|
+
writeInfo(`[pumuki][context] path=${result.path}`);
|
|
2441
|
+
writeInfo(`[pumuki][context] ${result.message}`);
|
|
2442
|
+
if (result.document) {
|
|
2443
|
+
writeInfo(`[pumuki][context] repo=${result.document.repo_root}`);
|
|
2444
|
+
writeInfo(`[pumuki][context] agent=${result.document.contract.agent_instruction}`);
|
|
2445
|
+
writeInfo(`[pumuki][context] user=${result.document.contract.user_remediation}`);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
return result.status === 'ok' ? 0 : 1;
|
|
2449
|
+
}
|
|
2380
2450
|
case 'doctor': {
|
|
2381
2451
|
const report = runLifecycleDoctor({
|
|
2382
2452
|
deep: parsed.doctorDeep === true,
|
|
@@ -2467,6 +2537,16 @@ export const runLifecycleCli = async (
|
|
|
2467
2537
|
`PRE_PUSH=${status.policyValidation.stages.PRE_PUSH.validationCode ?? 'n/a'} strict=${status.policyValidation.stages.PRE_PUSH.strict ? 'yes' : 'no'} ` +
|
|
2468
2538
|
`CI=${status.policyValidation.stages.CI.validationCode ?? 'n/a'} strict=${status.policyValidation.stages.CI.strict ? 'yes' : 'no'}`
|
|
2469
2539
|
);
|
|
2540
|
+
writeInfo(
|
|
2541
|
+
`[pumuki] notifications: enabled=${status.notifications.enabled ? 'yes' : 'no'} ` +
|
|
2542
|
+
`deliverable=${status.notifications.deliverable ? 'yes' : 'no'} ` +
|
|
2543
|
+
`channel=${status.notifications.channel} ` +
|
|
2544
|
+
`blocked_dialog=${status.notifications.blockedDialogEnabled ? 'yes' : 'no'} ` +
|
|
2545
|
+
`reason=${status.notifications.disabledReason ?? 'none'}`
|
|
2546
|
+
);
|
|
2547
|
+
if (status.notifications.remediation) {
|
|
2548
|
+
writeInfo(`[pumuki] notifications remediation: ${status.notifications.remediation}`);
|
|
2549
|
+
}
|
|
2470
2550
|
writeInfo(
|
|
2471
2551
|
`[pumuki] experimental: ANALYTICS=${status.experimentalFeatures.features.analytics.mode} source=${status.experimentalFeatures.features.analytics.source} layer=${status.experimentalFeatures.features.analytics.layer} blocking=${status.experimentalFeatures.features.analytics.blocking ? 'yes' : 'no'} env=${status.experimentalFeatures.features.analytics.activationVariable}`
|
|
2472
2552
|
);
|
|
@@ -21,6 +21,10 @@ import {
|
|
|
21
21
|
type LifecycleDependencyInventory,
|
|
22
22
|
} from './dependencyInventory';
|
|
23
23
|
import { readPreWriteLeaseStatus } from './preWriteLease';
|
|
24
|
+
import {
|
|
25
|
+
readLifecycleNotificationStatus,
|
|
26
|
+
type LifecycleNotificationStatus,
|
|
27
|
+
} from './notificationStatus';
|
|
24
28
|
|
|
25
29
|
export type DoctorIssueSeverity = 'warning' | 'error';
|
|
26
30
|
|
|
@@ -102,6 +106,7 @@ export type LifecycleDoctorReport = {
|
|
|
102
106
|
hooksDirectory: string;
|
|
103
107
|
hooksDirectoryResolution: 'git-rev-parse' | 'git-config' | 'default';
|
|
104
108
|
policyValidation: LifecyclePolicyValidationSnapshot;
|
|
109
|
+
notifications: LifecycleNotificationStatus;
|
|
105
110
|
policy_signature_remediation?: string;
|
|
106
111
|
issues: ReadonlyArray<DoctorIssue>;
|
|
107
112
|
deep?: DoctorDeepReport;
|
|
@@ -117,6 +122,9 @@ const buildDoctorIssues = (params: {
|
|
|
117
122
|
lifecycleState: LifecycleState;
|
|
118
123
|
}): ReadonlyArray<DoctorIssue> => {
|
|
119
124
|
const issues: DoctorIssue[] = [];
|
|
125
|
+
const notificationStatus = readLifecycleNotificationStatus({
|
|
126
|
+
repoRoot: params.repoRoot,
|
|
127
|
+
});
|
|
120
128
|
const evidenceResult = readEvidenceResult(params.repoRoot);
|
|
121
129
|
const preWriteLeaseStatus = readPreWriteLeaseStatus({
|
|
122
130
|
repoRoot: params.repoRoot,
|
|
@@ -132,6 +140,15 @@ const buildDoctorIssues = (params: {
|
|
|
132
140
|
});
|
|
133
141
|
}
|
|
134
142
|
|
|
143
|
+
if (!notificationStatus.deliverable) {
|
|
144
|
+
issues.push({
|
|
145
|
+
severity: 'warning',
|
|
146
|
+
message:
|
|
147
|
+
`System notifications are not deliverable (${notificationStatus.disabledReason ?? 'unknown'}). ` +
|
|
148
|
+
`${notificationStatus.remediation ?? 'Review notification configuration.'}`,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
135
152
|
if (params.trackedNodeModulesPaths.length > 0) {
|
|
136
153
|
issues.push({
|
|
137
154
|
severity: 'error',
|
|
@@ -900,6 +917,7 @@ export const runLifecycleDoctor = (params?: {
|
|
|
900
917
|
|
|
901
918
|
const policyValidation = readLifecyclePolicyValidationSnapshot(repoRoot);
|
|
902
919
|
const policySignatureRemediation = buildPolicySignatureRemediation(policyValidation);
|
|
920
|
+
const notifications = readLifecycleNotificationStatus({ repoRoot });
|
|
903
921
|
|
|
904
922
|
return {
|
|
905
923
|
repoRoot,
|
|
@@ -912,6 +930,7 @@ export const runLifecycleDoctor = (params?: {
|
|
|
912
930
|
hooksDirectory: hooksDirectory.path,
|
|
913
931
|
hooksDirectoryResolution: hooksDirectory.source,
|
|
914
932
|
policyValidation,
|
|
933
|
+
notifications,
|
|
915
934
|
...(policySignatureRemediation
|
|
916
935
|
? { policy_signature_remediation: policySignatureRemediation }
|
|
917
936
|
: {}),
|
|
@@ -15,6 +15,11 @@ import { ensureRuntimeArtifactsIgnored } from './artifacts';
|
|
|
15
15
|
import { runLifecycleAdapterInstall } from './adapter';
|
|
16
16
|
import { runPolicyReconcile } from './policyReconcile';
|
|
17
17
|
import { emitGateBlockedNotification } from '../notifications/emitAuditSummaryNotification';
|
|
18
|
+
import { persistSystemNotificationsConfig } from '../../scripts/framework-menu-system-notifications-config';
|
|
19
|
+
import {
|
|
20
|
+
readLifecycleNotificationStatus,
|
|
21
|
+
type LifecycleNotificationStatus,
|
|
22
|
+
} from './notificationStatus';
|
|
18
23
|
|
|
19
24
|
export type LifecycleInstallResult = {
|
|
20
25
|
repoRoot: string;
|
|
@@ -22,6 +27,7 @@ export type LifecycleInstallResult = {
|
|
|
22
27
|
changedHooks: ReadonlyArray<string>;
|
|
23
28
|
openSpecBootstrap?: OpenSpecBootstrapResult;
|
|
24
29
|
degradedDoctorBypass?: boolean;
|
|
30
|
+
notifications: LifecycleNotificationStatus;
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
const shouldBootstrapEvidence = (repoRoot: string): boolean =>
|
|
@@ -102,6 +108,11 @@ const materializeStrictPolicyAsCode = (repoRoot: string): void => {
|
|
|
102
108
|
const isPreWriteEnforcementGapIssue = (message: string): boolean =>
|
|
103
109
|
message.startsWith('ENFORCEMENT_GAP: PRE_WRITE hard-stop lease is not valid');
|
|
104
110
|
|
|
111
|
+
const ensureInstallNotificationsEnabled = (repoRoot: string): LifecycleNotificationStatus => {
|
|
112
|
+
persistSystemNotificationsConfig(repoRoot, true);
|
|
113
|
+
return readLifecycleNotificationStatus({ repoRoot });
|
|
114
|
+
};
|
|
115
|
+
|
|
105
116
|
export const runLifecycleInstall = (params?: {
|
|
106
117
|
cwd?: string;
|
|
107
118
|
git?: ILifecycleGitService;
|
|
@@ -117,6 +128,7 @@ export const runLifecycleInstall = (params?: {
|
|
|
117
128
|
cwd: params?.cwd,
|
|
118
129
|
git,
|
|
119
130
|
});
|
|
131
|
+
const notifications = ensureInstallNotificationsEnabled(report.repoRoot);
|
|
120
132
|
const installBlockingIssues = report.issues.filter(
|
|
121
133
|
(issue) => issue.severity === 'error' && !isPreWriteEnforcementGapIssue(issue.message)
|
|
122
134
|
);
|
|
@@ -141,6 +153,7 @@ export const runLifecycleInstall = (params?: {
|
|
|
141
153
|
changedHooks,
|
|
142
154
|
openSpecBootstrap: undefined,
|
|
143
155
|
degradedDoctorBypass: true,
|
|
156
|
+
notifications,
|
|
144
157
|
};
|
|
145
158
|
}
|
|
146
159
|
const renderedIssues = installBlockingIssues.map((issue) => `- [${issue.severity}] ${issue.message}`).join('\n');
|
|
@@ -193,5 +206,6 @@ export const runLifecycleInstall = (params?: {
|
|
|
193
206
|
version,
|
|
194
207
|
changedHooks,
|
|
195
208
|
openSpecBootstrap,
|
|
209
|
+
notifications,
|
|
196
210
|
};
|
|
197
211
|
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { resolveEffectiveSystemNotificationsConfig } from '../../scripts/framework-menu-system-notifications-effective-config';
|
|
2
|
+
import { resolveSystemNotificationGate } from '../../scripts/framework-menu-system-notifications-gate';
|
|
3
|
+
|
|
4
|
+
export type LifecycleNotificationStatus = {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
blockedDialogEnabled: boolean;
|
|
7
|
+
channel: string;
|
|
8
|
+
muteUntil: string | null;
|
|
9
|
+
deliverable: boolean;
|
|
10
|
+
disabledReason: string | null;
|
|
11
|
+
remediation: string | null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const remediationForReason = (reason: string | null): string | null => {
|
|
15
|
+
if (reason === null) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
if (reason === 'disabled') {
|
|
19
|
+
return 'Activa notificaciones con la configuracion repo-local de Pumuki y elimina env vars PUMUKI_*NOTIFICATIONS que las deshabiliten.';
|
|
20
|
+
}
|
|
21
|
+
if (reason === 'muted') {
|
|
22
|
+
return 'Quita el silencio temporal desde la notificacion o elimina muteUntil en .pumuki/system-notifications.json.';
|
|
23
|
+
}
|
|
24
|
+
if (reason === 'unsupported-platform') {
|
|
25
|
+
return 'Usa el fallback stderr o configura un canal compatible para este sistema.';
|
|
26
|
+
}
|
|
27
|
+
return 'Revisa la configuracion de notificaciones y reejecuta pumuki status/doctor.';
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export const readLifecycleNotificationStatus = (params: {
|
|
31
|
+
repoRoot: string;
|
|
32
|
+
env?: NodeJS.ProcessEnv;
|
|
33
|
+
now?: () => number;
|
|
34
|
+
}): LifecycleNotificationStatus => {
|
|
35
|
+
const env = params.env ?? process.env;
|
|
36
|
+
const config = resolveEffectiveSystemNotificationsConfig({
|
|
37
|
+
repoRoot: params.repoRoot,
|
|
38
|
+
});
|
|
39
|
+
const gate = resolveSystemNotificationGate({
|
|
40
|
+
config,
|
|
41
|
+
nowMs: (params.now ?? Date.now)(),
|
|
42
|
+
env,
|
|
43
|
+
});
|
|
44
|
+
const disabledReason = gate?.reason ?? null;
|
|
45
|
+
return {
|
|
46
|
+
enabled: config.enabled,
|
|
47
|
+
blockedDialogEnabled: config.blockedDialogEnabled === true,
|
|
48
|
+
channel: config.channel,
|
|
49
|
+
muteUntil: config.muteUntil ?? null,
|
|
50
|
+
deliverable: gate === null,
|
|
51
|
+
disabledReason,
|
|
52
|
+
remediation: remediationForReason(disabledReason),
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
type LifecycleDependencyInventory,
|
|
19
19
|
} from './dependencyInventory';
|
|
20
20
|
import { readPreWriteLeaseStatus } from './preWriteLease';
|
|
21
|
+
import {
|
|
22
|
+
readLifecycleNotificationStatus,
|
|
23
|
+
type LifecycleNotificationStatus,
|
|
24
|
+
} from './notificationStatus';
|
|
21
25
|
|
|
22
26
|
export type LifecycleStatus = {
|
|
23
27
|
repoRoot: string;
|
|
@@ -31,15 +35,27 @@ export type LifecycleStatus = {
|
|
|
31
35
|
dependencyInventory: LifecycleDependencyInventory;
|
|
32
36
|
policyValidation: LifecyclePolicyValidationSnapshot;
|
|
33
37
|
experimentalFeatures: LifecycleExperimentalFeaturesSnapshot;
|
|
38
|
+
notifications: LifecycleNotificationStatus;
|
|
34
39
|
issues: ReadonlyArray<DoctorIssue>;
|
|
35
40
|
};
|
|
36
41
|
|
|
37
42
|
const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
|
|
43
|
+
const notificationStatus = readLifecycleNotificationStatus({ repoRoot });
|
|
38
44
|
const evidenceResult = readEvidenceResult(repoRoot);
|
|
39
45
|
const preWriteLeaseStatus = readPreWriteLeaseStatus({
|
|
40
46
|
repoRoot,
|
|
41
47
|
git: new LifecycleGitService(),
|
|
42
48
|
});
|
|
49
|
+
const notificationIssues: DoctorIssue[] = notificationStatus.deliverable
|
|
50
|
+
? []
|
|
51
|
+
: [
|
|
52
|
+
{
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
message:
|
|
55
|
+
`System notifications are not deliverable (${notificationStatus.disabledReason ?? 'unknown'}). ` +
|
|
56
|
+
`${notificationStatus.remediation ?? 'Review notification configuration.'}`,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
43
59
|
const leaseIssues: DoctorIssue[] =
|
|
44
60
|
!preWriteLeaseStatus.valid && preWriteLeaseStatus.changedCodePaths.length > 0
|
|
45
61
|
? [
|
|
@@ -52,7 +68,7 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
|
|
|
52
68
|
]
|
|
53
69
|
: [];
|
|
54
70
|
if (evidenceResult.kind !== 'valid') {
|
|
55
|
-
return leaseIssues;
|
|
71
|
+
return [...notificationIssues, ...leaseIssues];
|
|
56
72
|
}
|
|
57
73
|
|
|
58
74
|
const evidence = evidenceResult.evidence;
|
|
@@ -63,11 +79,12 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
|
|
|
63
79
|
|
|
64
80
|
if (!blocked) {
|
|
65
81
|
if (evidence.snapshot.outcome !== 'WARN') {
|
|
66
|
-
return leaseIssues;
|
|
82
|
+
return [...notificationIssues, ...leaseIssues];
|
|
67
83
|
}
|
|
68
84
|
|
|
69
85
|
const warnStage = evidence?.snapshot?.stage ?? 'PRE_WRITE';
|
|
70
86
|
return [
|
|
87
|
+
...notificationIssues,
|
|
71
88
|
...leaseIssues,
|
|
72
89
|
{
|
|
73
90
|
severity: 'warning',
|
|
@@ -85,6 +102,7 @@ const buildLifecycleIssues = (repoRoot: string): ReadonlyArray<DoctorIssue> => {
|
|
|
85
102
|
message: `Governance is blocked (${blockedStage}).`,
|
|
86
103
|
});
|
|
87
104
|
return [
|
|
105
|
+
...notificationIssues,
|
|
88
106
|
...leaseIssues,
|
|
89
107
|
{
|
|
90
108
|
severity: 'error',
|
|
@@ -110,6 +128,7 @@ export const readLifecycleStatus = (params?: {
|
|
|
110
128
|
});
|
|
111
129
|
const policyValidation = readLifecyclePolicyValidationSnapshot(repoRoot);
|
|
112
130
|
const experimentalFeatures = readLifecycleExperimentalFeaturesSnapshot();
|
|
131
|
+
const notifications = readLifecycleNotificationStatus({ repoRoot });
|
|
113
132
|
const issues = buildLifecycleIssues(repoRoot);
|
|
114
133
|
|
|
115
134
|
return {
|
|
@@ -124,6 +143,7 @@ export const readLifecycleStatus = (params?: {
|
|
|
124
143
|
dependencyInventory,
|
|
125
144
|
policyValidation,
|
|
126
145
|
experimentalFeatures,
|
|
146
|
+
notifications,
|
|
127
147
|
issues,
|
|
128
148
|
};
|
|
129
149
|
};
|
|
@@ -155,7 +155,8 @@ const defaultDependencies: LifecycleWatchDependencies = {
|
|
|
155
155
|
ensureRuntimeArtifactsIgnored: (repoRoot) => {
|
|
156
156
|
try {
|
|
157
157
|
ensureRuntimeArtifactsIgnored(repoRoot);
|
|
158
|
-
} catch {
|
|
158
|
+
} catch (error) {
|
|
159
|
+
void error;
|
|
159
160
|
}
|
|
160
161
|
},
|
|
161
162
|
emitAuditSummaryNotificationFromEvidence,
|
|
@@ -108,8 +108,8 @@ const startOrReuseEvidenceHttp = async (): Promise<{
|
|
|
108
108
|
if (health.status === 'ok') {
|
|
109
109
|
return { host, port: requestedPort, route };
|
|
110
110
|
}
|
|
111
|
-
} catch {
|
|
112
|
-
|
|
111
|
+
} catch (error) {
|
|
112
|
+
void error;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
const portInUse = await isPortInUse(host, requestedPort);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type SkillsEnforcementMode = '
|
|
1
|
+
export type SkillsEnforcementMode = 'strict';
|
|
2
2
|
|
|
3
3
|
export type SkillsEnforcementResolution = {
|
|
4
4
|
mode: SkillsEnforcementMode;
|
|
@@ -24,17 +24,6 @@ const toSkillsEnforcementMode = (
|
|
|
24
24
|
) {
|
|
25
25
|
return 'strict';
|
|
26
26
|
}
|
|
27
|
-
if (
|
|
28
|
-
normalized === 'advisory'
|
|
29
|
-
|| normalized === 'warn'
|
|
30
|
-
|| normalized === 'warning'
|
|
31
|
-
|| normalized === '0'
|
|
32
|
-
|| normalized === 'false'
|
|
33
|
-
|| normalized === 'no'
|
|
34
|
-
|| normalized === 'off'
|
|
35
|
-
) {
|
|
36
|
-
return 'advisory';
|
|
37
|
-
}
|
|
38
27
|
return null;
|
|
39
28
|
};
|
|
40
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.269",
|
|
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": {
|
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
"validation:consumer-startup-unblock-status": "npx --yes tsx@4.21.0 scripts/build-consumer-startup-unblock-status.ts",
|
|
110
110
|
"validation:consumer-startup-triage": "npx --yes tsx@4.21.0 scripts/build-consumer-startup-triage.ts",
|
|
111
111
|
"validation:mock-consumer-ab-report": "npx --yes tsx@4.21.0 scripts/build-mock-consumer-ab-report.ts",
|
|
112
|
+
"skills:classify": "npx --yes tsx@4.21.0 scripts/classify-skills-rules.ts",
|
|
112
113
|
"validation:adapter-readiness": "npx --yes tsx@4.21.0 scripts/build-adapter-readiness.ts",
|
|
113
114
|
"validation:adapter-real-session-report": "npx --yes tsx@4.21.0 scripts/build-adapter-real-session-report.ts",
|
|
114
115
|
"validation:adapter-session-status": "npx --yes tsx@4.21.0 scripts/build-adapter-session-status.ts",
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { loadSkillsLock } from '../integrations/config/skillsLock';
|
|
2
|
+
import { classifySkillsRules } from '../integrations/config/skillsRuleClassification';
|
|
3
|
+
|
|
4
|
+
const repoRoot = process.argv[2] ?? process.cwd();
|
|
5
|
+
const lock = loadSkillsLock(repoRoot);
|
|
6
|
+
|
|
7
|
+
if (!lock) {
|
|
8
|
+
process.stderr.write(`skills.lock.json not found or invalid at ${repoRoot}\n`);
|
|
9
|
+
process.exit(1);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const summary = classifySkillsRules(lock);
|
|
13
|
+
|
|
14
|
+
process.stdout.write(`${JSON.stringify({
|
|
15
|
+
total: summary.total,
|
|
16
|
+
byStatus: summary.byStatus,
|
|
17
|
+
byPlatform: summary.byPlatform,
|
|
18
|
+
samples: {
|
|
19
|
+
AST_IMPLEMENTED: summary.rules
|
|
20
|
+
.filter((rule) => rule.status === 'AST_IMPLEMENTED')
|
|
21
|
+
.slice(0, 12),
|
|
22
|
+
IMPLEMENTABLE_AHORA: summary.rules
|
|
23
|
+
.filter((rule) => rule.status === 'IMPLEMENTABLE_AHORA')
|
|
24
|
+
.slice(0, 12),
|
|
25
|
+
REQUIERE_ESTUDIO: summary.rules
|
|
26
|
+
.filter((rule) => rule.status === 'REQUIERE_ESTUDIO')
|
|
27
|
+
.slice(0, 12),
|
|
28
|
+
NO_ES_REGLA_DE_CODIGO: summary.rules
|
|
29
|
+
.filter((rule) => rule.status === 'NO_ES_REGLA_DE_CODIGO')
|
|
30
|
+
.slice(0, 12),
|
|
31
|
+
},
|
|
32
|
+
}, null, 2)}\n`);
|
|
@@ -34,6 +34,10 @@ const BLOCKED_CAUSE_SUMMARY_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
34
34
|
'Falta enforcement crítico de skills para la plataforma detectada.',
|
|
35
35
|
EVIDENCE_SKILLS_CONTRACT_INCOMPLETE:
|
|
36
36
|
'El contrato de skills está incompleto para este stage.',
|
|
37
|
+
CONTEXT_NOT_APPLIED:
|
|
38
|
+
'Contexto local obligatorio no aplicado.',
|
|
39
|
+
CONTEXT_INVALID:
|
|
40
|
+
'Contexto local obligatorio inválido.',
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
const ENGLISH_CAUSE_HINTS = [
|
|
@@ -70,6 +74,9 @@ const resolvePriorityCauseFromMessage = (message?: string): string | null => {
|
|
|
70
74
|
return PRIORITY_CODES_FROM_MESSAGE.find((code) => message.includes(code)) ?? null;
|
|
71
75
|
};
|
|
72
76
|
|
|
77
|
+
const isCauseCodePriorityOverTracking = (causeCode: string): boolean =>
|
|
78
|
+
causeCode === 'CONTEXT_NOT_APPLIED' || causeCode === 'CONTEXT_INVALID';
|
|
79
|
+
|
|
73
80
|
const buildGenericSpanishBlockedCauseSummary = (
|
|
74
81
|
event: Extract<PumukiCriticalNotificationEvent, { kind: 'gate.blocked' }>,
|
|
75
82
|
causeCode: string
|
|
@@ -118,10 +125,13 @@ export const resolveBlockedCauseSummary = (
|
|
|
118
125
|
if (priorityCode) {
|
|
119
126
|
return BLOCKED_CAUSE_SUMMARY_BY_CODE[priorityCode];
|
|
120
127
|
}
|
|
128
|
+
const mapped = BLOCKED_CAUSE_SUMMARY_BY_CODE[causeCode];
|
|
129
|
+
if (mapped && isCauseCodePriorityOverTracking(causeCode)) {
|
|
130
|
+
return mapped;
|
|
131
|
+
}
|
|
121
132
|
if (trackingContext) {
|
|
122
133
|
return buildNotificationTrackingCauseSummary(trackingContext);
|
|
123
134
|
}
|
|
124
|
-
const mapped = BLOCKED_CAUSE_SUMMARY_BY_CODE[causeCode];
|
|
125
135
|
if (mapped) {
|
|
126
136
|
return mapped;
|
|
127
137
|
}
|
|
@@ -15,7 +15,7 @@ export const commitBaseline = (
|
|
|
15
15
|
runGitStep(workspace, ['add', '.'], 'git add baseline');
|
|
16
16
|
runGitStep(
|
|
17
17
|
workspace,
|
|
18
|
-
['commit', '
|
|
18
|
+
['commit', '-m', 'chore: baseline'],
|
|
19
19
|
'git commit baseline'
|
|
20
20
|
);
|
|
21
21
|
};
|
|
@@ -32,7 +32,7 @@ export const writeAndCommitRangePayloadForBlockMode = (
|
|
|
32
32
|
runGitStep(workspace, ['add', '.'], 'git add range payload');
|
|
33
33
|
runGitStep(
|
|
34
34
|
workspace,
|
|
35
|
-
['commit', '
|
|
35
|
+
['commit', '-m', 'test: range payload for package smoke'],
|
|
36
36
|
'git commit range payload'
|
|
37
37
|
);
|
|
38
38
|
};
|
|
@@ -39,7 +39,7 @@ export const configureRemoteAndFeatureBranch = (
|
|
|
39
39
|
workspace.tmpRoot
|
|
40
40
|
);
|
|
41
41
|
runGitStep(workspace, ['remote', 'add', 'origin', workspace.bareRemote], 'git remote add origin');
|
|
42
|
-
runGitStep(workspace, ['push', '
|
|
42
|
+
runGitStep(workspace, ['push', '-u', 'origin', 'main'], 'git push origin main');
|
|
43
43
|
runGitStep(workspace, ['checkout', '-b', 'feature/package-smoke'], 'git checkout feature branch');
|
|
44
44
|
runGitStep(
|
|
45
45
|
workspace,
|