pumuki 6.3.26 → 6.3.28
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/README.md +3 -1
- package/bin/pumuki-mcp-enterprise-stdio.js +5 -0
- package/bin/pumuki-mcp-evidence-stdio.js +5 -0
- package/core/gate/conditionMatches.ts +1 -21
- package/core/gate/evaluateGate.js +5 -0
- package/core/gate/evaluateRules.js +5 -0
- package/core/gate/evaluateRules.ts +1 -24
- package/core/gate/scopeMatcher.ts +84 -0
- package/docs/EXECUTION_BOARD.md +749 -376
- package/docs/MCP_SERVERS.md +41 -2
- package/docs/README.md +6 -2
- package/docs/REFRACTOR_PROGRESS.md +374 -6
- package/docs/validation/README.md +11 -1
- package/docs/validation/p9-ruralgo-bug-registry.md +607 -0
- package/docs/validation/p9-ruralgo-fork-validation-tracking.md +904 -0
- package/docs/validation/real-repo-manual-e2e-ruralgo-fork.md +372 -0
- package/integrations/config/skillsCompliance.ts +212 -0
- package/integrations/evidence/integrity.ts +352 -0
- package/integrations/evidence/rulesCoverage.ts +94 -0
- package/integrations/evidence/schema.test.ts +16 -0
- package/integrations/evidence/schema.ts +41 -0
- package/integrations/evidence/writeEvidence.test.ts +68 -0
- package/integrations/evidence/writeEvidence.ts +23 -2
- package/integrations/gate/evaluateAiGate.ts +382 -15
- package/integrations/gate/stagePolicies.ts +70 -15
- package/integrations/gate/waivers.ts +209 -0
- package/integrations/git/findingTraceability.ts +3 -23
- package/integrations/git/index.js +5 -0
- package/integrations/git/runCliCommand.ts +16 -0
- package/integrations/git/runPlatformGate.ts +53 -1
- package/integrations/git/runPlatformGateEvaluation.ts +13 -0
- package/integrations/git/stageRunners.ts +168 -5
- package/integrations/lifecycle/adapter.templates.json +72 -5
- package/integrations/lifecycle/adapter.ts +78 -4
- package/integrations/lifecycle/cli.ts +384 -14
- package/integrations/lifecycle/doctor.ts +534 -0
- package/integrations/lifecycle/hookBlock.ts +2 -1
- package/integrations/lifecycle/index.js +5 -0
- package/integrations/lifecycle/install.ts +115 -3
- package/integrations/lifecycle/openSpecBootstrap.ts +68 -8
- package/integrations/lifecycle/preWriteAutomation.ts +142 -0
- package/integrations/mcp/aiGateCheck.ts +6 -0
- package/integrations/mcp/aiGateReceipt.ts +188 -0
- package/integrations/mcp/enterpriseServer.ts +14 -1
- package/integrations/mcp/enterpriseStdioServer.cli.ts +315 -0
- package/integrations/mcp/evidenceStdioServer.cli.ts +342 -0
- package/integrations/mcp/index.js +5 -0
- package/integrations/sdd/index.js +5 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/policy.ts +191 -2
- package/integrations/sdd/sessionStore.ts +139 -19
- package/integrations/sdd/syncDocs.ts +180 -0
- package/integrations/sdd/types.ts +4 -1
- package/integrations/telemetry/structuredTelemetry.ts +197 -0
- package/package.json +27 -8
- package/scripts/build-p9-validation-manifests.ts +53 -0
- package/scripts/check-p9-ruralgo-baseline-clean.ts +200 -0
- package/scripts/check-p9-ruralgo-baseline-versioned.ts +198 -0
- package/scripts/check-p9-ruralgo-branch-ready.ts +215 -0
- package/scripts/check-p9-ruralgo-install-health.ts +288 -0
- package/scripts/check-p9-ruralgo-runtime-ready.ts +188 -0
- package/scripts/check-package-manifest.ts +49 -0
- package/scripts/check-tracking-single-active.sh +40 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +31 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +3 -3
- package/scripts/framework-menu-legacy-audit-lib.ts +35 -7
- package/scripts/framework-menu-matrix-evidence-lib.ts +6 -2
- package/scripts/manage-library.sh +1 -1
- package/scripts/p9-ruralgo-baseline-clean-lib.ts +117 -0
- package/scripts/p9-ruralgo-baseline-versioned-lib.ts +119 -0
- package/scripts/p9-ruralgo-branch-ready-lib.ts +128 -0
- package/scripts/p9-ruralgo-install-health-lib.ts +121 -0
- package/scripts/p9-ruralgo-runtime-ready-lib.ts +149 -0
- package/scripts/p9-validation-manifests-lib.ts +366 -0
- package/scripts/package-manifest-lib.ts +9 -0
- package/skills.lock.json +1 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { isAbsolute, resolve } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import type { AiGateStage, AiGateViolation } from './evaluateAiGate';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_AI_GATE_WAIVER_PATH = '.pumuki/waivers/ai-gate.json';
|
|
7
|
+
|
|
8
|
+
const stageSchema = z.enum(['PRE_WRITE', 'PRE_COMMIT', 'PRE_PUSH', 'CI']);
|
|
9
|
+
|
|
10
|
+
const waiverEntrySchema = z.object({
|
|
11
|
+
id: z.string().min(1),
|
|
12
|
+
code: z.string().min(1),
|
|
13
|
+
owner: z.string().min(1),
|
|
14
|
+
reason: z.string().min(1),
|
|
15
|
+
created_at: z.string().datetime({ offset: true }),
|
|
16
|
+
expires_at: z.string().datetime({ offset: true }),
|
|
17
|
+
stage: z.union([stageSchema, z.array(stageSchema).min(1)]).optional(),
|
|
18
|
+
branch: z.string().min(1).optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const waiverFileSchema = z.object({
|
|
22
|
+
version: z.literal('1'),
|
|
23
|
+
waivers: z.array(waiverEntrySchema),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
type AiGateWaiverEntry = z.infer<typeof waiverEntrySchema>;
|
|
27
|
+
|
|
28
|
+
export type AppliedAiGateWaiver = {
|
|
29
|
+
id: string;
|
|
30
|
+
code: string;
|
|
31
|
+
owner: string;
|
|
32
|
+
reason: string;
|
|
33
|
+
expires_at: string;
|
|
34
|
+
ttl_seconds: number;
|
|
35
|
+
branch?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type AiGateWaiverApplyResult = {
|
|
39
|
+
path: string;
|
|
40
|
+
status: 'none' | 'applied' | 'invalid';
|
|
41
|
+
invalid_reason: string | null;
|
|
42
|
+
violations: ReadonlyArray<AiGateViolation>;
|
|
43
|
+
applied: ReadonlyArray<AppliedAiGateWaiver>;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const parseDate = (value: string): Date | null => {
|
|
47
|
+
const parsed = Date.parse(value);
|
|
48
|
+
if (!Number.isFinite(parsed)) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return new Date(parsed);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const toTtlSeconds = (expiresAt: Date, now: Date): number =>
|
|
55
|
+
Math.max(0, Math.floor((expiresAt.getTime() - now.getTime()) / 1000));
|
|
56
|
+
|
|
57
|
+
const matchesStage = (
|
|
58
|
+
waiver: AiGateWaiverEntry,
|
|
59
|
+
stage: AiGateStage
|
|
60
|
+
): boolean => {
|
|
61
|
+
if (!waiver.stage) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
if (Array.isArray(waiver.stage)) {
|
|
65
|
+
return waiver.stage.includes(stage);
|
|
66
|
+
}
|
|
67
|
+
return waiver.stage === stage;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const matchesBranch = (
|
|
71
|
+
waiver: AiGateWaiverEntry,
|
|
72
|
+
branch: string | null
|
|
73
|
+
): boolean => {
|
|
74
|
+
if (!waiver.branch) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (!branch) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
return waiver.branch === branch;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const matchesViolationCode = (
|
|
84
|
+
waiver: AiGateWaiverEntry,
|
|
85
|
+
violationCode: string
|
|
86
|
+
): boolean =>
|
|
87
|
+
waiver.code === '*' || waiver.code === violationCode;
|
|
88
|
+
|
|
89
|
+
export const resolveAiGateWaiverPath = (repoRoot: string): string => {
|
|
90
|
+
const candidate = process.env.PUMUKI_AI_GATE_WAIVER_PATH?.trim();
|
|
91
|
+
if (!candidate) {
|
|
92
|
+
return resolve(repoRoot, DEFAULT_AI_GATE_WAIVER_PATH);
|
|
93
|
+
}
|
|
94
|
+
if (isAbsolute(candidate)) {
|
|
95
|
+
return candidate;
|
|
96
|
+
}
|
|
97
|
+
return resolve(repoRoot, candidate);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const toAppliedWaiver = (
|
|
101
|
+
waiver: AiGateWaiverEntry,
|
|
102
|
+
now: Date
|
|
103
|
+
): AppliedAiGateWaiver | null => {
|
|
104
|
+
const expiresAt = parseDate(waiver.expires_at);
|
|
105
|
+
if (!expiresAt) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const ttlSeconds = toTtlSeconds(expiresAt, now);
|
|
109
|
+
if (ttlSeconds <= 0) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: waiver.id,
|
|
114
|
+
code: waiver.code,
|
|
115
|
+
owner: waiver.owner,
|
|
116
|
+
reason: waiver.reason,
|
|
117
|
+
expires_at: waiver.expires_at,
|
|
118
|
+
ttl_seconds: ttlSeconds,
|
|
119
|
+
branch: waiver.branch,
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export const applyAiGateWaivers = (params: {
|
|
124
|
+
repoRoot: string;
|
|
125
|
+
stage: AiGateStage;
|
|
126
|
+
branch: string | null;
|
|
127
|
+
violations: ReadonlyArray<AiGateViolation>;
|
|
128
|
+
now?: Date;
|
|
129
|
+
}): AiGateWaiverApplyResult => {
|
|
130
|
+
const path = resolveAiGateWaiverPath(params.repoRoot);
|
|
131
|
+
if (!existsSync(path)) {
|
|
132
|
+
return {
|
|
133
|
+
path,
|
|
134
|
+
status: 'none',
|
|
135
|
+
invalid_reason: null,
|
|
136
|
+
violations: params.violations,
|
|
137
|
+
applied: [],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let parsed: unknown;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
144
|
+
} catch (error) {
|
|
145
|
+
return {
|
|
146
|
+
path,
|
|
147
|
+
status: 'invalid',
|
|
148
|
+
invalid_reason: error instanceof Error ? error.message : String(error),
|
|
149
|
+
violations: params.violations,
|
|
150
|
+
applied: [],
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const validation = waiverFileSchema.safeParse(parsed);
|
|
155
|
+
if (!validation.success) {
|
|
156
|
+
return {
|
|
157
|
+
path,
|
|
158
|
+
status: 'invalid',
|
|
159
|
+
invalid_reason: validation.error.issues[0]?.message ?? 'invalid_schema',
|
|
160
|
+
violations: params.violations,
|
|
161
|
+
applied: [],
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const now = params.now ?? new Date();
|
|
166
|
+
const activeWaivers = validation.data.waivers
|
|
167
|
+
.filter((waiver) => matchesStage(waiver, params.stage))
|
|
168
|
+
.filter((waiver) => matchesBranch(waiver, params.branch))
|
|
169
|
+
.map((waiver) => ({
|
|
170
|
+
raw: waiver,
|
|
171
|
+
normalized: toAppliedWaiver(waiver, now),
|
|
172
|
+
}))
|
|
173
|
+
.filter((entry): entry is { raw: AiGateWaiverEntry; normalized: AppliedAiGateWaiver } =>
|
|
174
|
+
entry.normalized !== null
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
if (activeWaivers.length === 0) {
|
|
178
|
+
return {
|
|
179
|
+
path,
|
|
180
|
+
status: 'none',
|
|
181
|
+
invalid_reason: null,
|
|
182
|
+
violations: params.violations,
|
|
183
|
+
applied: [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const remaining: AiGateViolation[] = [];
|
|
188
|
+
const applied = new Map<string, AppliedAiGateWaiver>();
|
|
189
|
+
for (const violation of params.violations) {
|
|
190
|
+
const match = activeWaivers.find((entry) =>
|
|
191
|
+
matchesViolationCode(entry.raw, violation.code)
|
|
192
|
+
);
|
|
193
|
+
if (!match) {
|
|
194
|
+
remaining.push(violation);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
applied.set(match.normalized.id, match.normalized);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
path,
|
|
202
|
+
status: applied.size > 0 ? 'applied' : 'none',
|
|
203
|
+
invalid_reason: null,
|
|
204
|
+
violations: remaining,
|
|
205
|
+
applied: [...applied.values()].sort((left, right) =>
|
|
206
|
+
left.id.localeCompare(right.id)
|
|
207
|
+
),
|
|
208
|
+
};
|
|
209
|
+
};
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import type { Fact } from '../../core/facts/Fact';
|
|
2
2
|
import type { Finding } from '../../core/gate/Finding';
|
|
3
|
+
import { matchesScope } from '../../core/gate/scopeMatcher';
|
|
3
4
|
import type { Condition } from '../../core/rules/Condition';
|
|
4
5
|
import type { RuleDefinition } from '../../core/rules/RuleDefinition';
|
|
5
6
|
import type { RuleSet } from '../../core/rules/RuleSet';
|
|
6
7
|
|
|
8
|
+
type RuleScope = RuleDefinition['scope'];
|
|
9
|
+
|
|
7
10
|
type Trace = {
|
|
8
11
|
matched: boolean;
|
|
9
12
|
filePath?: string;
|
|
@@ -12,29 +15,6 @@ type Trace = {
|
|
|
12
15
|
source?: string;
|
|
13
16
|
};
|
|
14
17
|
|
|
15
|
-
type RuleScope = RuleDefinition['scope'];
|
|
16
|
-
|
|
17
|
-
const extractPrefix = (pattern: string): string => {
|
|
18
|
-
const wildcardIndex = pattern.indexOf('*');
|
|
19
|
-
return wildcardIndex === -1 ? pattern : pattern.slice(0, wildcardIndex);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const matchesAnyPrefix = (path: string, patterns: ReadonlyArray<string>): boolean => {
|
|
23
|
-
return patterns.some((pattern) => path.startsWith(extractPrefix(pattern)));
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
const matchesScope = (path: string, scope?: RuleScope): boolean => {
|
|
27
|
-
const include = scope?.include;
|
|
28
|
-
const exclude = scope?.exclude;
|
|
29
|
-
if (exclude && exclude.length > 0 && matchesAnyPrefix(path, exclude)) {
|
|
30
|
-
return false;
|
|
31
|
-
}
|
|
32
|
-
if (!include || include.length === 0) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
return matchesAnyPrefix(path, include);
|
|
36
|
-
};
|
|
37
|
-
|
|
38
18
|
const normalizePath = (path: string): string => path.replace(/\\/g, '/');
|
|
39
19
|
|
|
40
20
|
const sortedUniqueLines = (lines: ReadonlyArray<number>): readonly number[] | undefined => {
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
export const runCliCommand = (runner: () => Promise<number>): void => {
|
|
2
|
+
const hasQuietFlag = process.argv.slice(2).includes('--quiet');
|
|
3
|
+
const previousQuietMode = process.env.PUMUKI_HOOK_SUMMARY_QUIET;
|
|
4
|
+
if (hasQuietFlag) {
|
|
5
|
+
process.env.PUMUKI_HOOK_SUMMARY_QUIET = '1';
|
|
6
|
+
}
|
|
7
|
+
|
|
2
8
|
void runner()
|
|
3
9
|
.then((code) => {
|
|
4
10
|
process.exitCode = code;
|
|
@@ -7,5 +13,15 @@ export const runCliCommand = (runner: () => Promise<number>): void => {
|
|
|
7
13
|
const message = error instanceof Error ? error.message : 'Unexpected CLI runner error.';
|
|
8
14
|
process.stderr.write(`${message}\n`);
|
|
9
15
|
process.exitCode = 1;
|
|
16
|
+
})
|
|
17
|
+
.finally(() => {
|
|
18
|
+
if (!hasQuietFlag) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
if (typeof previousQuietMode === 'undefined') {
|
|
22
|
+
delete process.env.PUMUKI_HOOK_SUMMARY_QUIET;
|
|
23
|
+
} else {
|
|
24
|
+
process.env.PUMUKI_HOOK_SUMMARY_QUIET = previousQuietMode;
|
|
25
|
+
}
|
|
10
26
|
});
|
|
11
27
|
};
|
|
@@ -188,6 +188,41 @@ const toSkillsUnsupportedAutoRulesBlockingFinding = (params: {
|
|
|
188
188
|
};
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
+
const toSkillsComplianceBlockingFinding = (params: {
|
|
192
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
193
|
+
skillsCompliance?: {
|
|
194
|
+
missing_rule_ids: string[];
|
|
195
|
+
by_file: Array<{
|
|
196
|
+
file_path: string;
|
|
197
|
+
missing_rule_ids: string[];
|
|
198
|
+
}>;
|
|
199
|
+
};
|
|
200
|
+
}): Finding | undefined => {
|
|
201
|
+
const missingRuleIds = params.skillsCompliance?.missing_rule_ids ?? [];
|
|
202
|
+
if (missingRuleIds.length === 0) {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const filesWithMissing = (params.skillsCompliance?.by_file ?? [])
|
|
207
|
+
.filter((entry) => entry.missing_rule_ids.length > 0)
|
|
208
|
+
.map((entry) => entry.file_path)
|
|
209
|
+
.sort((left, right) => left.localeCompare(right))
|
|
210
|
+
.join(', ');
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
ruleId: 'governance.skills.compliance.incomplete',
|
|
214
|
+
severity: 'ERROR',
|
|
215
|
+
code: 'SKILLS_COMPLIANCE_INCOMPLETE_HIGH',
|
|
216
|
+
message:
|
|
217
|
+
`Skills compliance incomplete at ${params.stage}: ` +
|
|
218
|
+
`missing_rule_ids=[${[...missingRuleIds].sort().join(', ')}]. ` +
|
|
219
|
+
`affected_files=[${filesWithMissing}].`,
|
|
220
|
+
filePath: '.ai_evidence.json',
|
|
221
|
+
matchedBy: 'SkillsComplianceGuard',
|
|
222
|
+
source: 'skills-compliance',
|
|
223
|
+
};
|
|
224
|
+
};
|
|
225
|
+
|
|
191
226
|
export async function runPlatformGate(params: {
|
|
192
227
|
policy: GatePolicy;
|
|
193
228
|
auditMode?: 'gate' | 'engine';
|
|
@@ -307,6 +342,15 @@ export async function runPlatformGate(params: {
|
|
|
307
342
|
unsupportedAutoRuleIds: skillsRuleSet.unsupportedAutoRuleIds ?? [],
|
|
308
343
|
})
|
|
309
344
|
: undefined;
|
|
345
|
+
const skillsComplianceBlockingFinding =
|
|
346
|
+
params.policy.stage === 'PRE_COMMIT' ||
|
|
347
|
+
params.policy.stage === 'PRE_PUSH' ||
|
|
348
|
+
params.policy.stage === 'CI'
|
|
349
|
+
? toSkillsComplianceBlockingFinding({
|
|
350
|
+
stage: params.policy.stage,
|
|
351
|
+
skillsCompliance: coverage?.skillsCompliance,
|
|
352
|
+
})
|
|
353
|
+
: undefined;
|
|
310
354
|
const rulesCoverage = coverage
|
|
311
355
|
? {
|
|
312
356
|
stage: params.policy.stage,
|
|
@@ -334,6 +378,11 @@ export async function runPlatformGate(params: {
|
|
|
334
378
|
coverage.activeRuleIds.length === 0
|
|
335
379
|
? 1
|
|
336
380
|
: Number((coverage.evaluatedRuleIds.length / coverage.activeRuleIds.length).toFixed(6)),
|
|
381
|
+
...(coverage.skillsCompliance
|
|
382
|
+
? {
|
|
383
|
+
skills_compliance: coverage.skillsCompliance,
|
|
384
|
+
}
|
|
385
|
+
: {}),
|
|
337
386
|
}
|
|
338
387
|
: createEmptySnapshotRulesCoverage(params.policy.stage);
|
|
339
388
|
const currentBranch = resolveCurrentBranch(git, repoRoot);
|
|
@@ -352,13 +401,15 @@ export async function runPlatformGate(params: {
|
|
|
352
401
|
? [
|
|
353
402
|
sddBlockingFinding,
|
|
354
403
|
...(unsupportedSkillsMappingFinding ? [unsupportedSkillsMappingFinding] : []),
|
|
404
|
+
...(skillsComplianceBlockingFinding ? [skillsComplianceBlockingFinding] : []),
|
|
355
405
|
...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
|
|
356
406
|
...tddBddEvaluation.findings,
|
|
357
407
|
...findings,
|
|
358
408
|
]
|
|
359
|
-
: unsupportedSkillsMappingFinding || coverageBlockingFinding || tddBddEvaluation.findings.length > 0
|
|
409
|
+
: unsupportedSkillsMappingFinding || skillsComplianceBlockingFinding || coverageBlockingFinding || tddBddEvaluation.findings.length > 0
|
|
360
410
|
? [
|
|
361
411
|
...(unsupportedSkillsMappingFinding ? [unsupportedSkillsMappingFinding] : []),
|
|
412
|
+
...(skillsComplianceBlockingFinding ? [skillsComplianceBlockingFinding] : []),
|
|
362
413
|
...(coverageBlockingFinding ? [coverageBlockingFinding] : []),
|
|
363
414
|
...tddBddEvaluation.findings,
|
|
364
415
|
...findings,
|
|
@@ -368,6 +419,7 @@ export async function runPlatformGate(params: {
|
|
|
368
419
|
const gateOutcome =
|
|
369
420
|
sddBlockingFinding ||
|
|
370
421
|
unsupportedSkillsMappingFinding ||
|
|
422
|
+
skillsComplianceBlockingFinding ||
|
|
371
423
|
coverageBlockingFinding ||
|
|
372
424
|
hasTddBddBlockingFinding
|
|
373
425
|
? 'BLOCK'
|
|
@@ -12,6 +12,10 @@ import {
|
|
|
12
12
|
loadSkillsRuleSetForStage,
|
|
13
13
|
type SkillsRuleSetLoadResult,
|
|
14
14
|
} from '../config/skillsRuleSet';
|
|
15
|
+
import {
|
|
16
|
+
evaluateSkillsCompliance,
|
|
17
|
+
type SkillsComplianceSnapshot,
|
|
18
|
+
} from '../config/skillsCompliance';
|
|
15
19
|
import { applyHeuristicSeverityForStage } from '../gate/stagePolicies';
|
|
16
20
|
import {
|
|
17
21
|
detectPlatformsFromFacts,
|
|
@@ -45,6 +49,7 @@ type PlatformGateEvaluationResult = {
|
|
|
45
49
|
matchedRuleIds: ReadonlyArray<string>;
|
|
46
50
|
unmatchedRuleIds: ReadonlyArray<string>;
|
|
47
51
|
unevaluatedRuleIds: ReadonlyArray<string>;
|
|
52
|
+
skillsCompliance?: SkillsComplianceSnapshot;
|
|
48
53
|
};
|
|
49
54
|
findings: ReadonlyArray<Finding>;
|
|
50
55
|
};
|
|
@@ -218,6 +223,13 @@ export const evaluatePlatformGateFindings = (
|
|
|
218
223
|
const unevaluatedRuleIds = activeRuleIds.filter(
|
|
219
224
|
(ruleId) => !evaluatedRuleIdsSet.has(ruleId)
|
|
220
225
|
);
|
|
226
|
+
const skillsCompliance = evaluateSkillsCompliance({
|
|
227
|
+
skillsRuleSet,
|
|
228
|
+
observedFilePaths,
|
|
229
|
+
activeRuleIds,
|
|
230
|
+
evaluatedRuleIds,
|
|
231
|
+
matchedRuleIds,
|
|
232
|
+
});
|
|
221
233
|
|
|
222
234
|
return {
|
|
223
235
|
detectedPlatforms,
|
|
@@ -243,6 +255,7 @@ export const evaluatePlatformGateFindings = (
|
|
|
243
255
|
matchedRuleIds,
|
|
244
256
|
unmatchedRuleIds,
|
|
245
257
|
unevaluatedRuleIds,
|
|
258
|
+
...(skillsCompliance ? { skillsCompliance } : {}),
|
|
246
259
|
},
|
|
247
260
|
findings,
|
|
248
261
|
};
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { resolvePolicyForStage } from '../gate/stagePolicies';
|
|
2
|
+
import type { ResolvedStagePolicy } from '../gate/stagePolicies';
|
|
2
3
|
import { resolveCiBaseRef, resolveUpstreamRef } from './resolveGitRefs';
|
|
3
4
|
import { runPlatformGate } from './runPlatformGate';
|
|
4
5
|
import { GitService } from './GitService';
|
|
5
6
|
import { emitAuditSummaryNotificationFromEvidence } from '../notifications/emitAuditSummaryNotification';
|
|
7
|
+
import { readEvidenceResult, type EvidenceReadResult } from '../evidence/readEvidence';
|
|
8
|
+
import { emitStructuredTelemetry } from '../telemetry/structuredTelemetry';
|
|
6
9
|
|
|
7
10
|
const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
|
|
8
11
|
'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
|
|
@@ -13,10 +16,14 @@ type StageRunnerDependencies = {
|
|
|
13
16
|
resolveCiBaseRef: typeof resolveCiBaseRef;
|
|
14
17
|
runPlatformGate: typeof runPlatformGate;
|
|
15
18
|
resolveRepoRoot: () => string;
|
|
19
|
+
readEvidenceResult: (repoRoot: string) => EvidenceReadResult;
|
|
20
|
+
env: NodeJS.ProcessEnv;
|
|
21
|
+
writeStdout: (message: string) => void;
|
|
16
22
|
notifyAuditSummaryFromEvidence: (params: {
|
|
17
23
|
repoRoot: string;
|
|
18
24
|
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
19
25
|
}) => void;
|
|
26
|
+
emitStructuredTelemetry: typeof emitStructuredTelemetry;
|
|
20
27
|
};
|
|
21
28
|
|
|
22
29
|
const defaultDependencies: StageRunnerDependencies = {
|
|
@@ -25,12 +32,18 @@ const defaultDependencies: StageRunnerDependencies = {
|
|
|
25
32
|
resolveCiBaseRef,
|
|
26
33
|
runPlatformGate,
|
|
27
34
|
resolveRepoRoot: () => new GitService().resolveRepoRoot(),
|
|
35
|
+
readEvidenceResult,
|
|
36
|
+
env: process.env,
|
|
37
|
+
writeStdout: (message: string) => {
|
|
38
|
+
process.stdout.write(`${message}\n`);
|
|
39
|
+
},
|
|
28
40
|
notifyAuditSummaryFromEvidence: ({ repoRoot, stage }) => {
|
|
29
41
|
emitAuditSummaryNotificationFromEvidence({
|
|
30
42
|
repoRoot,
|
|
31
43
|
stage,
|
|
32
44
|
});
|
|
33
45
|
},
|
|
46
|
+
emitStructuredTelemetry,
|
|
34
47
|
};
|
|
35
48
|
|
|
36
49
|
const getDependencies = (
|
|
@@ -42,18 +55,138 @@ const getDependencies = (
|
|
|
42
55
|
|
|
43
56
|
const notifyAuditSummaryForStage = (
|
|
44
57
|
dependencies: StageRunnerDependencies,
|
|
58
|
+
repoRoot: string,
|
|
45
59
|
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI'
|
|
46
60
|
): void => {
|
|
47
61
|
dependencies.notifyAuditSummaryFromEvidence({
|
|
48
|
-
repoRoot
|
|
62
|
+
repoRoot,
|
|
49
63
|
stage,
|
|
50
64
|
});
|
|
51
65
|
};
|
|
52
66
|
|
|
67
|
+
const isQuietHookSummaryEnabled = (env: NodeJS.ProcessEnv): boolean => {
|
|
68
|
+
const value = env.PUMUKI_HOOK_SUMMARY_QUIET?.trim().toLowerCase();
|
|
69
|
+
return value === '1' || value === 'true' || value === 'yes';
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const resolveEvidenceAgeSeconds = (timestamp: string): string => {
|
|
73
|
+
const generatedAtMillis = Date.parse(timestamp);
|
|
74
|
+
if (!Number.isFinite(generatedAtMillis)) {
|
|
75
|
+
return 'n/a';
|
|
76
|
+
}
|
|
77
|
+
return String(Math.max(0, Math.floor((Date.now() - generatedAtMillis) / 1000)));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const resolveEvidenceIntegrityStatus = (evidenceResult: EvidenceReadResult): string => {
|
|
81
|
+
if (evidenceResult.kind !== 'valid') {
|
|
82
|
+
return 'n/a';
|
|
83
|
+
}
|
|
84
|
+
return evidenceResult.evidence.integrity ? 'valid' : 'missing';
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const resolveEvidenceChainHash = (evidenceResult: EvidenceReadResult): string => {
|
|
88
|
+
if (evidenceResult.kind !== 'valid') {
|
|
89
|
+
return 'n/a';
|
|
90
|
+
}
|
|
91
|
+
return evidenceResult.evidence.integrity?.chain_hash ?? 'n/a';
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const resolvePolicyBundle = (trace?: ResolvedStagePolicy['trace']): string =>
|
|
95
|
+
trace?.bundle ?? 'n/a';
|
|
96
|
+
|
|
97
|
+
const resolvePolicyHash = (trace?: ResolvedStagePolicy['trace']): string =>
|
|
98
|
+
trace?.hash ?? 'n/a';
|
|
99
|
+
|
|
100
|
+
const resolvePolicyVersion = (trace?: ResolvedStagePolicy['trace']): string =>
|
|
101
|
+
trace?.version ?? 'n/a';
|
|
102
|
+
|
|
103
|
+
const resolvePolicySignature = (trace?: ResolvedStagePolicy['trace']): string =>
|
|
104
|
+
trace?.signature ?? 'n/a';
|
|
105
|
+
|
|
106
|
+
const emitHookGateSummary = (params: {
|
|
107
|
+
dependencies: StageRunnerDependencies;
|
|
108
|
+
repoRoot: string;
|
|
109
|
+
stage: 'PRE_COMMIT' | 'PRE_PUSH' | 'CI';
|
|
110
|
+
policyTrace?: ResolvedStagePolicy['trace'];
|
|
111
|
+
exitCode: number;
|
|
112
|
+
printSummary?: boolean;
|
|
113
|
+
}): void => {
|
|
114
|
+
const evidenceResult = params.dependencies.readEvidenceResult(params.repoRoot);
|
|
115
|
+
const decision = params.exitCode === 0 ? 'ALLOW' : 'BLOCK';
|
|
116
|
+
const status = params.exitCode === 0 ? 'ALLOWED' : 'BLOCKED';
|
|
117
|
+
const outcome =
|
|
118
|
+
evidenceResult.kind === 'valid' ? evidenceResult.evidence.snapshot.outcome : 'n/a';
|
|
119
|
+
const evidenceAgeSeconds =
|
|
120
|
+
evidenceResult.kind === 'valid'
|
|
121
|
+
? resolveEvidenceAgeSeconds(evidenceResult.evidence.timestamp)
|
|
122
|
+
: 'n/a';
|
|
123
|
+
const policyTrace = params.policyTrace;
|
|
124
|
+
params.dependencies.emitStructuredTelemetry({
|
|
125
|
+
repoRoot: params.repoRoot,
|
|
126
|
+
env: params.dependencies.env,
|
|
127
|
+
event: {
|
|
128
|
+
schema_version: '1',
|
|
129
|
+
timestamp: new Date().toISOString(),
|
|
130
|
+
source: 'pumuki',
|
|
131
|
+
channel: 'hook_gate',
|
|
132
|
+
event: 'hook_gate.evaluated',
|
|
133
|
+
stage: params.stage,
|
|
134
|
+
repo_root: params.repoRoot,
|
|
135
|
+
status,
|
|
136
|
+
decision,
|
|
137
|
+
policy: {
|
|
138
|
+
source: policyTrace?.source ?? 'n/a',
|
|
139
|
+
bundle: resolvePolicyBundle(policyTrace),
|
|
140
|
+
hash: resolvePolicyHash(policyTrace),
|
|
141
|
+
version: resolvePolicyVersion(policyTrace),
|
|
142
|
+
signature: resolvePolicySignature(policyTrace),
|
|
143
|
+
},
|
|
144
|
+
evidence: {
|
|
145
|
+
kind: evidenceResult.kind,
|
|
146
|
+
age_seconds:
|
|
147
|
+
evidenceResult.kind === 'valid'
|
|
148
|
+
? Number.parseInt(evidenceAgeSeconds, 10)
|
|
149
|
+
: null,
|
|
150
|
+
max_age_seconds: null,
|
|
151
|
+
source: 'local_file_ai_evidence',
|
|
152
|
+
path: `${params.repoRoot}/.ai_evidence.json`,
|
|
153
|
+
digest: null,
|
|
154
|
+
generated_at:
|
|
155
|
+
evidenceResult.kind === 'valid' ? evidenceResult.evidence.timestamp : null,
|
|
156
|
+
integrity_status: resolveEvidenceIntegrityStatus(evidenceResult),
|
|
157
|
+
chain_hash: resolveEvidenceChainHash(evidenceResult),
|
|
158
|
+
},
|
|
159
|
+
metrics: {
|
|
160
|
+
violations_total:
|
|
161
|
+
evidenceResult.kind === 'valid'
|
|
162
|
+
? evidenceResult.evidence.severity_metrics.total_violations
|
|
163
|
+
: 0,
|
|
164
|
+
violations_error:
|
|
165
|
+
evidenceResult.kind === 'valid'
|
|
166
|
+
? evidenceResult.evidence.severity_metrics.by_severity.ERROR
|
|
167
|
+
: 0,
|
|
168
|
+
violations_warn:
|
|
169
|
+
evidenceResult.kind === 'valid'
|
|
170
|
+
? evidenceResult.evidence.severity_metrics.by_severity.WARN
|
|
171
|
+
: 0,
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if ((params.printSummary ?? true) && isQuietHookSummaryEnabled(params.dependencies.env)) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
params.dependencies.writeStdout(
|
|
181
|
+
`[pumuki][hook-gate] stage=${params.stage} policy_bundle=${resolvePolicyBundle(policyTrace)} policy_hash=${resolvePolicyHash(policyTrace)} policy_version=${resolvePolicyVersion(policyTrace)} policy_signature=${resolvePolicySignature(policyTrace)} decision=${decision} outcome=${outcome} evidence_kind=${evidenceResult.kind} evidence_age_seconds=${evidenceAgeSeconds} evidence_integrity=${resolveEvidenceIntegrityStatus(evidenceResult)} evidence_chain_hash=${resolveEvidenceChainHash(evidenceResult)}`
|
|
182
|
+
);
|
|
183
|
+
};
|
|
184
|
+
|
|
53
185
|
export async function runPreCommitStage(
|
|
54
186
|
dependencies: Partial<StageRunnerDependencies> = {}
|
|
55
187
|
): Promise<number> {
|
|
56
188
|
const activeDependencies = getDependencies(dependencies);
|
|
189
|
+
const repoRoot = activeDependencies.resolveRepoRoot();
|
|
57
190
|
const resolved = activeDependencies.resolvePolicyForStage('PRE_COMMIT');
|
|
58
191
|
const exitCode = await activeDependencies.runPlatformGate({
|
|
59
192
|
policy: resolved.policy,
|
|
@@ -62,7 +195,14 @@ export async function runPreCommitStage(
|
|
|
62
195
|
kind: 'staged',
|
|
63
196
|
},
|
|
64
197
|
});
|
|
65
|
-
|
|
198
|
+
emitHookGateSummary({
|
|
199
|
+
dependencies: activeDependencies,
|
|
200
|
+
repoRoot,
|
|
201
|
+
stage: 'PRE_COMMIT',
|
|
202
|
+
policyTrace: resolved.trace,
|
|
203
|
+
exitCode,
|
|
204
|
+
});
|
|
205
|
+
notifyAuditSummaryForStage(activeDependencies, repoRoot, 'PRE_COMMIT');
|
|
66
206
|
return exitCode;
|
|
67
207
|
}
|
|
68
208
|
|
|
@@ -70,10 +210,17 @@ export async function runPrePushStage(
|
|
|
70
210
|
dependencies: Partial<StageRunnerDependencies> = {}
|
|
71
211
|
): Promise<number> {
|
|
72
212
|
const activeDependencies = getDependencies(dependencies);
|
|
213
|
+
const repoRoot = activeDependencies.resolveRepoRoot();
|
|
73
214
|
const upstreamRef = activeDependencies.resolveUpstreamRef();
|
|
74
215
|
if (!upstreamRef) {
|
|
75
216
|
process.stderr.write(`${PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE}\n`);
|
|
76
|
-
|
|
217
|
+
emitHookGateSummary({
|
|
218
|
+
dependencies: activeDependencies,
|
|
219
|
+
repoRoot,
|
|
220
|
+
stage: 'PRE_PUSH',
|
|
221
|
+
exitCode: 1,
|
|
222
|
+
});
|
|
223
|
+
notifyAuditSummaryForStage(activeDependencies, repoRoot, 'PRE_PUSH');
|
|
77
224
|
return 1;
|
|
78
225
|
}
|
|
79
226
|
|
|
@@ -87,7 +234,14 @@ export async function runPrePushStage(
|
|
|
87
234
|
toRef: 'HEAD',
|
|
88
235
|
},
|
|
89
236
|
});
|
|
90
|
-
|
|
237
|
+
emitHookGateSummary({
|
|
238
|
+
dependencies: activeDependencies,
|
|
239
|
+
repoRoot,
|
|
240
|
+
stage: 'PRE_PUSH',
|
|
241
|
+
policyTrace: resolved.trace,
|
|
242
|
+
exitCode,
|
|
243
|
+
});
|
|
244
|
+
notifyAuditSummaryForStage(activeDependencies, repoRoot, 'PRE_PUSH');
|
|
91
245
|
return exitCode;
|
|
92
246
|
}
|
|
93
247
|
|
|
@@ -95,6 +249,7 @@ export async function runCiStage(
|
|
|
95
249
|
dependencies: Partial<StageRunnerDependencies> = {}
|
|
96
250
|
): Promise<number> {
|
|
97
251
|
const activeDependencies = getDependencies(dependencies);
|
|
252
|
+
const repoRoot = activeDependencies.resolveRepoRoot();
|
|
98
253
|
const resolved = activeDependencies.resolvePolicyForStage('CI');
|
|
99
254
|
const exitCode = await activeDependencies.runPlatformGate({
|
|
100
255
|
policy: resolved.policy,
|
|
@@ -105,6 +260,14 @@ export async function runCiStage(
|
|
|
105
260
|
toRef: 'HEAD',
|
|
106
261
|
},
|
|
107
262
|
});
|
|
108
|
-
|
|
263
|
+
emitHookGateSummary({
|
|
264
|
+
dependencies: activeDependencies,
|
|
265
|
+
repoRoot,
|
|
266
|
+
stage: 'CI',
|
|
267
|
+
policyTrace: resolved.trace,
|
|
268
|
+
exitCode,
|
|
269
|
+
printSummary: false,
|
|
270
|
+
});
|
|
271
|
+
notifyAuditSummaryForStage(activeDependencies, repoRoot, 'CI');
|
|
109
272
|
return exitCode;
|
|
110
273
|
}
|