pumuki 6.3.39 → 6.3.40
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 +21 -12
- package/VERSION +1 -1
- package/core/gate/evaluateRules.test.ts +40 -0
- package/core/gate/evaluateRules.ts +7 -1
- package/core/rules/Consequence.ts +1 -0
- package/docs/CONFIGURATION.md +50 -0
- package/docs/INSTALLATION.md +38 -11
- package/docs/MCP_SERVERS.md +1 -1
- package/docs/README.md +1 -0
- package/docs/RELEASE_NOTES.md +44 -0
- package/docs/USAGE.md +191 -9
- package/docs/registro-maestro-de-seguimiento.md +2 -2
- package/docs/seguimiento-activo-pumuki-saas-supermercados.md +1592 -1
- package/docs/validation/README.md +2 -1
- package/docs/validation/ast-intelligence-roadmap.md +96 -0
- package/integrations/config/skillsCustomRules.ts +14 -0
- package/integrations/config/skillsDetectorRegistry.ts +11 -1
- package/integrations/config/skillsLock.ts +30 -0
- package/integrations/config/skillsMarkdownRules.ts +14 -3
- package/integrations/config/skillsRuleSet.ts +25 -3
- package/integrations/evidence/readEvidence.test.ts +3 -2
- package/integrations/evidence/readEvidence.ts +14 -4
- package/integrations/evidence/repoState.ts +10 -2
- package/integrations/evidence/schema.test.ts +3 -2
- package/integrations/evidence/schema.ts +3 -0
- package/integrations/evidence/writeEvidence.test.ts +3 -2
- package/integrations/gate/evaluateAiGate.ts +511 -2
- package/integrations/git/GitService.ts +5 -1
- package/integrations/git/astIntelligenceDualValidation.ts +275 -0
- package/integrations/git/gitAtomicity.ts +42 -9
- package/integrations/git/resolveGitRefs.ts +37 -0
- package/integrations/git/runPlatformGate.ts +228 -1
- package/integrations/git/runPlatformGateEvaluation.ts +4 -0
- package/integrations/git/stageRunners.ts +116 -2
- package/integrations/lifecycle/cli.ts +759 -22
- package/integrations/lifecycle/doctor.ts +62 -0
- package/integrations/lifecycle/index.ts +1 -0
- package/integrations/lifecycle/packageInfo.ts +25 -3
- package/integrations/lifecycle/policyReconcile.ts +304 -0
- package/integrations/lifecycle/preWriteAutomation.ts +42 -2
- package/integrations/lifecycle/watch.ts +365 -0
- package/integrations/mcp/aiGateCheck.ts +59 -2
- package/integrations/mcp/autoExecuteAiStart.ts +25 -1
- package/integrations/mcp/preFlightCheck.ts +13 -0
- package/integrations/sdd/evidenceScaffold.ts +223 -0
- package/integrations/sdd/index.ts +2 -0
- package/integrations/sdd/stateSync.ts +400 -0
- package/integrations/sdd/syncDocs.ts +97 -2
- package/package.json +4 -1
- package/scripts/backlog-action-reasons-lib.ts +38 -0
- package/scripts/backlog-id-issue-map-lib.ts +69 -0
- package/scripts/backlog-json-contract-lib.ts +3 -0
- package/scripts/framework-menu-consumer-preflight-lib.ts +6 -0
- package/scripts/package-install-smoke-command-resolution-lib.ts +64 -0
- package/scripts/package-install-smoke-consumer-npm-lib.ts +43 -0
- package/scripts/package-install-smoke-consumer-repo-setup-lib.ts +2 -0
- package/scripts/package-install-smoke-execution-steps-lib.ts +27 -9
- package/scripts/package-install-smoke-lifecycle-lib.ts +15 -4
- package/scripts/package-install-smoke-workspace-factory-lib.ts +4 -1
- package/scripts/reconcile-consumer-backlog-issues-lib.ts +651 -0
- package/scripts/reconcile-consumer-backlog-issues.ts +348 -0
- package/scripts/watch-consumer-backlog-lib.ts +465 -0
- package/scripts/watch-consumer-backlog.ts +326 -0
|
@@ -20,6 +20,35 @@ export type AiGateViolation = {
|
|
|
20
20
|
severity: 'ERROR' | 'WARN';
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
type PreWriteWorktreeHygienePolicy = {
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
warnThreshold: number;
|
|
26
|
+
blockThreshold: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type AiGateSkillsContractPlatformRequirement = {
|
|
30
|
+
platform: PreWriteSkillsPlatform;
|
|
31
|
+
required_rule_prefix: string;
|
|
32
|
+
required_bundles: ReadonlyArray<string>;
|
|
33
|
+
required_critical_rule_ids: ReadonlyArray<string>;
|
|
34
|
+
required_any_transversal_critical_rule_ids: ReadonlyArray<string>;
|
|
35
|
+
active_prefix_covered: boolean;
|
|
36
|
+
evaluated_prefix_covered: boolean;
|
|
37
|
+
missing_bundles: ReadonlyArray<string>;
|
|
38
|
+
missing_critical_rule_ids: ReadonlyArray<string>;
|
|
39
|
+
transversal_critical_covered: boolean;
|
|
40
|
+
missing_any_transversal_critical_rule_ids: ReadonlyArray<string>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export type AiGateSkillsContractAssessment = {
|
|
44
|
+
stage: AiGateStage;
|
|
45
|
+
enforced: boolean;
|
|
46
|
+
status: 'PASS' | 'FAIL' | 'NOT_APPLICABLE';
|
|
47
|
+
detected_platforms: ReadonlyArray<PreWriteSkillsPlatform>;
|
|
48
|
+
requirements: ReadonlyArray<AiGateSkillsContractPlatformRequirement>;
|
|
49
|
+
violations: ReadonlyArray<AiGateViolation>;
|
|
50
|
+
};
|
|
51
|
+
|
|
23
52
|
export type AiGateCheckResult = {
|
|
24
53
|
stage: AiGateStage;
|
|
25
54
|
status: 'ALLOWED' | 'BLOCKED';
|
|
@@ -53,6 +82,7 @@ export type AiGateCheckResult = {
|
|
|
53
82
|
max_age_seconds: number | null;
|
|
54
83
|
age_seconds: number | null;
|
|
55
84
|
};
|
|
85
|
+
skills_contract: AiGateSkillsContractAssessment;
|
|
56
86
|
repo_state: RepoState;
|
|
57
87
|
violations: AiGateViolation[];
|
|
58
88
|
};
|
|
@@ -79,8 +109,46 @@ const DEFAULT_MAX_AGE_SECONDS: Readonly<Record<AiGateStage, number>> = {
|
|
|
79
109
|
PRE_PUSH: 1800,
|
|
80
110
|
CI: 7200,
|
|
81
111
|
};
|
|
112
|
+
const DEFAULT_PREWRITE_WORKTREE_HYGIENE: PreWriteWorktreeHygienePolicy = {
|
|
113
|
+
enabled: true,
|
|
114
|
+
warnThreshold: 12,
|
|
115
|
+
blockThreshold: 24,
|
|
116
|
+
};
|
|
117
|
+
const PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV = 'PUMUKI_PREWRITE_WORKTREE_HYGIENE_ENABLED';
|
|
118
|
+
const PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_WARN_THRESHOLD';
|
|
119
|
+
const PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD';
|
|
82
120
|
|
|
83
121
|
const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
|
|
122
|
+
const PREWRITE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const;
|
|
123
|
+
type PreWriteSkillsPlatform = (typeof PREWRITE_SKILLS_PLATFORMS)[number];
|
|
124
|
+
const PLATFORM_SKILLS_RULE_PREFIXES: Readonly<Record<PreWriteSkillsPlatform, string>> = {
|
|
125
|
+
ios: 'skills.ios.',
|
|
126
|
+
android: 'skills.android.',
|
|
127
|
+
backend: 'skills.backend.',
|
|
128
|
+
frontend: 'skills.frontend.',
|
|
129
|
+
};
|
|
130
|
+
const PLATFORM_REQUIRED_SKILLS_BUNDLES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
|
|
131
|
+
ios: [
|
|
132
|
+
'ios-guidelines',
|
|
133
|
+
'ios-concurrency-guidelines',
|
|
134
|
+
'ios-swiftui-expert-guidelines',
|
|
135
|
+
],
|
|
136
|
+
android: ['android-guidelines'],
|
|
137
|
+
backend: ['backend-guidelines'],
|
|
138
|
+
frontend: ['frontend-guidelines'],
|
|
139
|
+
};
|
|
140
|
+
const PREWRITE_CRITICAL_SKILLS_RULES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
|
|
141
|
+
ios: ['skills.ios.critical-test-quality'],
|
|
142
|
+
android: [],
|
|
143
|
+
backend: [],
|
|
144
|
+
frontend: [],
|
|
145
|
+
};
|
|
146
|
+
const PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES: Readonly<Record<PreWriteSkillsPlatform, ReadonlyArray<string>>> = {
|
|
147
|
+
ios: [],
|
|
148
|
+
android: ['skills.android.no-runblocking', 'skills.android.no-thread-sleep'],
|
|
149
|
+
backend: ['skills.backend.no-empty-catch', 'skills.backend.avoid-explicit-any'],
|
|
150
|
+
frontend: ['skills.frontend.no-empty-catch', 'skills.frontend.avoid-explicit-any'],
|
|
151
|
+
};
|
|
84
152
|
const MCP_RECEIPT_STAGE_ORDER: Readonly<Record<AiGateStage, number>> = {
|
|
85
153
|
PRE_WRITE: 0,
|
|
86
154
|
PRE_COMMIT: 1,
|
|
@@ -94,6 +162,66 @@ const toErrorViolation = (code: string, message: string): AiGateViolation => ({
|
|
|
94
162
|
message,
|
|
95
163
|
});
|
|
96
164
|
|
|
165
|
+
const toWarnViolation = (code: string, message: string): AiGateViolation => ({
|
|
166
|
+
code,
|
|
167
|
+
severity: 'WARN',
|
|
168
|
+
message,
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const toPositiveInteger = (value: unknown, fallback: number): number => {
|
|
172
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
173
|
+
return fallback;
|
|
174
|
+
}
|
|
175
|
+
const normalized = Math.trunc(value);
|
|
176
|
+
return normalized > 0 ? normalized : fallback;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
const toBooleanFromEnv = (value: string | undefined, fallback: boolean): boolean => {
|
|
180
|
+
if (typeof value !== 'string') {
|
|
181
|
+
return fallback;
|
|
182
|
+
}
|
|
183
|
+
const normalized = value.trim().toLowerCase();
|
|
184
|
+
if (normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'on') {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
if (normalized === '0' || normalized === 'false' || normalized === 'no' || normalized === 'off') {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return fallback;
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const resolvePreWriteWorktreeHygienePolicy = (
|
|
194
|
+
input?: Partial<PreWriteWorktreeHygienePolicy>
|
|
195
|
+
): PreWriteWorktreeHygienePolicy => {
|
|
196
|
+
const enabled = input?.enabled
|
|
197
|
+
?? toBooleanFromEnv(
|
|
198
|
+
process.env[PREWRITE_WORKTREE_HYGIENE_ENABLED_ENV],
|
|
199
|
+
DEFAULT_PREWRITE_WORKTREE_HYGIENE.enabled
|
|
200
|
+
);
|
|
201
|
+
const warnThreshold = toPositiveInteger(
|
|
202
|
+
input?.warnThreshold
|
|
203
|
+
?? (process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV]
|
|
204
|
+
? Number(process.env[PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV])
|
|
205
|
+
: undefined),
|
|
206
|
+
DEFAULT_PREWRITE_WORKTREE_HYGIENE.warnThreshold
|
|
207
|
+
);
|
|
208
|
+
const requestedBlockThreshold = toPositiveInteger(
|
|
209
|
+
input?.blockThreshold
|
|
210
|
+
?? (process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV]
|
|
211
|
+
? Number(process.env[PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV])
|
|
212
|
+
: undefined),
|
|
213
|
+
DEFAULT_PREWRITE_WORKTREE_HYGIENE.blockThreshold
|
|
214
|
+
);
|
|
215
|
+
const blockThreshold =
|
|
216
|
+
requestedBlockThreshold >= warnThreshold ? requestedBlockThreshold : warnThreshold;
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
enabled,
|
|
220
|
+
warnThreshold,
|
|
221
|
+
blockThreshold,
|
|
222
|
+
};
|
|
223
|
+
};
|
|
224
|
+
|
|
97
225
|
const toTimestampAgeSeconds = (
|
|
98
226
|
timestamp: string,
|
|
99
227
|
nowMs: number
|
|
@@ -123,11 +251,330 @@ const toCanonicalPath = (value: string): string => {
|
|
|
123
251
|
}
|
|
124
252
|
};
|
|
125
253
|
|
|
254
|
+
const toNormalizedSkillsBundleName = (bundle: string): string => {
|
|
255
|
+
const separatorIndex = bundle.lastIndexOf('@');
|
|
256
|
+
if (separatorIndex <= 0) {
|
|
257
|
+
return bundle.trim();
|
|
258
|
+
}
|
|
259
|
+
return bundle.slice(0, separatorIndex).trim();
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const toDetectedSkillsPlatforms = (
|
|
263
|
+
platforms: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['platforms'] | undefined
|
|
264
|
+
): ReadonlyArray<PreWriteSkillsPlatform> => {
|
|
265
|
+
const platformsState = platforms ?? {};
|
|
266
|
+
const detected: PreWriteSkillsPlatform[] = [];
|
|
267
|
+
for (const platform of PREWRITE_SKILLS_PLATFORMS) {
|
|
268
|
+
if (platformsState[platform]?.detected === true) {
|
|
269
|
+
detected.push(platform);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return detected;
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const collectActiveRuleIdsCoverageViolations = (params: {
|
|
276
|
+
stage: AiGateStage;
|
|
277
|
+
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
278
|
+
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
279
|
+
}): AiGateViolation[] => {
|
|
280
|
+
if (params.coverage.active_rule_ids.length > 0) {
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
const detectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
|
|
284
|
+
if (detectedPlatforms.length === 0) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
return [
|
|
288
|
+
toErrorViolation(
|
|
289
|
+
'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES',
|
|
290
|
+
`Active rules coverage is empty at ${params.stage} with detected code platforms=[${detectedPlatforms.join(', ')}].`
|
|
291
|
+
),
|
|
292
|
+
];
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const collectPreWritePlatformSkillsViolations = (params: {
|
|
296
|
+
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
297
|
+
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
298
|
+
}): AiGateViolation[] => {
|
|
299
|
+
const detectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
|
|
300
|
+
if (detectedPlatforms.length === 0) {
|
|
301
|
+
return [];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const violations: AiGateViolation[] = [];
|
|
305
|
+
const missingScopeCoverage: string[] = [];
|
|
306
|
+
const missingBundlesByPlatform: string[] = [];
|
|
307
|
+
|
|
308
|
+
for (const platform of detectedPlatforms) {
|
|
309
|
+
const prefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
|
|
310
|
+
const hasActivePrefix = params.coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(prefix));
|
|
311
|
+
const hasEvaluatedPrefix = params.coverage.evaluated_rule_ids.some((ruleId) =>
|
|
312
|
+
ruleId.startsWith(prefix)
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
if (!hasActivePrefix || !hasEvaluatedPrefix) {
|
|
316
|
+
const reasons: string[] = [];
|
|
317
|
+
if (!hasActivePrefix) {
|
|
318
|
+
reasons.push(`active_rules_prefix=${prefix} missing`);
|
|
319
|
+
}
|
|
320
|
+
if (!hasEvaluatedPrefix) {
|
|
321
|
+
reasons.push(`evaluated_rules_prefix=${prefix} missing`);
|
|
322
|
+
}
|
|
323
|
+
missingScopeCoverage.push(`${platform}{${reasons.join('; ')}}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (missingScopeCoverage.length > 0) {
|
|
328
|
+
violations.push(
|
|
329
|
+
toErrorViolation(
|
|
330
|
+
'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
|
|
331
|
+
`Detected platforms missing skill-rule coverage in PRE_WRITE: ${missingScopeCoverage.join(' | ')}.`
|
|
332
|
+
)
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const activeSkillsBundles = new Set(
|
|
337
|
+
params.evidence.rulesets
|
|
338
|
+
.filter((ruleset) => ruleset.platform === 'skills')
|
|
339
|
+
.map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle))
|
|
340
|
+
.filter((bundle) => bundle.length > 0)
|
|
341
|
+
);
|
|
342
|
+
|
|
343
|
+
for (const platform of detectedPlatforms) {
|
|
344
|
+
const requiredBundles = PLATFORM_REQUIRED_SKILLS_BUNDLES[platform];
|
|
345
|
+
const missingBundles = requiredBundles.filter((bundleName) => !activeSkillsBundles.has(bundleName));
|
|
346
|
+
if (missingBundles.length === 0) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
missingBundlesByPlatform.push(`${platform}{missing_bundles=[${missingBundles.join(', ')}]}`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (missingBundlesByPlatform.length > 0) {
|
|
353
|
+
violations.push(
|
|
354
|
+
toErrorViolation(
|
|
355
|
+
'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
|
|
356
|
+
`Detected platforms missing required skill bundles in PRE_WRITE: ${missingBundlesByPlatform.join(' | ')}.`
|
|
357
|
+
)
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const missingCriticalRulesByPlatform: string[] = [];
|
|
362
|
+
for (const platform of detectedPlatforms) {
|
|
363
|
+
const requiredCriticalRuleIds = PREWRITE_CRITICAL_SKILLS_RULES[platform];
|
|
364
|
+
if (requiredCriticalRuleIds.length === 0) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
const missingCriticalRuleIds = requiredCriticalRuleIds.filter((ruleId) => {
|
|
368
|
+
const hasActive = params.coverage.active_rule_ids.includes(ruleId);
|
|
369
|
+
const hasEvaluated = params.coverage.evaluated_rule_ids.includes(ruleId);
|
|
370
|
+
return !hasActive || !hasEvaluated;
|
|
371
|
+
});
|
|
372
|
+
if (missingCriticalRuleIds.length === 0) {
|
|
373
|
+
continue;
|
|
374
|
+
}
|
|
375
|
+
missingCriticalRulesByPlatform.push(
|
|
376
|
+
`${platform}{missing_critical_rule_ids=[${missingCriticalRuleIds.join(', ')}]}`
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (missingCriticalRulesByPlatform.length > 0) {
|
|
381
|
+
violations.push(
|
|
382
|
+
toErrorViolation(
|
|
383
|
+
'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
|
|
384
|
+
`Detected platforms missing critical skill-rule enforcement in PRE_WRITE: ${missingCriticalRulesByPlatform.join(' | ')}.`
|
|
385
|
+
)
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return violations;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
const collectPreWriteCrossPlatformCriticalViolations = (params: {
|
|
393
|
+
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
394
|
+
coverage: NonNullable<Extract<EvidenceReadResult, { kind: 'valid' }>['evidence']['snapshot']['rules_coverage']>;
|
|
395
|
+
}): AiGateViolation[] => {
|
|
396
|
+
const detectedPlatforms = toDetectedSkillsPlatforms(params.evidence.platforms);
|
|
397
|
+
if (detectedPlatforms.length === 0) {
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const missingCriticalCoverage: string[] = [];
|
|
402
|
+
for (const platform of detectedPlatforms) {
|
|
403
|
+
const requiredRuleIds = PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform];
|
|
404
|
+
if (requiredRuleIds.length === 0) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const hasCoverage = requiredRuleIds.some((ruleId) =>
|
|
409
|
+
params.coverage.active_rule_ids.includes(ruleId) &&
|
|
410
|
+
params.coverage.evaluated_rule_ids.includes(ruleId)
|
|
411
|
+
);
|
|
412
|
+
|
|
413
|
+
if (hasCoverage) {
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
missingCriticalCoverage.push(
|
|
418
|
+
`${platform}{required_any=[${requiredRuleIds.join(', ')}]}`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (missingCriticalCoverage.length === 0) {
|
|
423
|
+
return [];
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return [
|
|
427
|
+
toErrorViolation(
|
|
428
|
+
'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
|
|
429
|
+
`Cross-platform critical enforcement incomplete in PRE_WRITE: ${missingCriticalCoverage.join(' | ')}.`
|
|
430
|
+
),
|
|
431
|
+
];
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const toSkillsContractAssessment = (params: {
|
|
435
|
+
stage: AiGateStage;
|
|
436
|
+
evidenceResult: EvidenceReadResult;
|
|
437
|
+
}): AiGateSkillsContractAssessment => {
|
|
438
|
+
if (params.evidenceResult.kind !== 'valid') {
|
|
439
|
+
return {
|
|
440
|
+
stage: params.stage,
|
|
441
|
+
enforced: false,
|
|
442
|
+
status: 'NOT_APPLICABLE',
|
|
443
|
+
detected_platforms: [],
|
|
444
|
+
requirements: [],
|
|
445
|
+
violations: [],
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const coverage = params.evidenceResult.evidence.snapshot.rules_coverage;
|
|
450
|
+
const detectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
|
|
451
|
+
if (detectedPlatforms.length === 0) {
|
|
452
|
+
return {
|
|
453
|
+
stage: params.stage,
|
|
454
|
+
enforced: false,
|
|
455
|
+
status: 'NOT_APPLICABLE',
|
|
456
|
+
detected_platforms: [],
|
|
457
|
+
requirements: [],
|
|
458
|
+
violations: [],
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const activeSkillsBundles = new Set(
|
|
463
|
+
params.evidenceResult.evidence.rulesets
|
|
464
|
+
.filter((ruleset) => ruleset.platform === 'skills')
|
|
465
|
+
.map((ruleset) => toNormalizedSkillsBundleName(ruleset.bundle))
|
|
466
|
+
.filter((bundle) => bundle.length > 0)
|
|
467
|
+
);
|
|
468
|
+
|
|
469
|
+
const requirements: AiGateSkillsContractPlatformRequirement[] = [];
|
|
470
|
+
const violations: AiGateViolation[] = [];
|
|
471
|
+
for (const platform of detectedPlatforms) {
|
|
472
|
+
const requiredRulePrefix = PLATFORM_SKILLS_RULE_PREFIXES[platform];
|
|
473
|
+
const requiredBundles = [...PLATFORM_REQUIRED_SKILLS_BUNDLES[platform]];
|
|
474
|
+
const requiredCriticalRuleIds = [...PREWRITE_CRITICAL_SKILLS_RULES[platform]];
|
|
475
|
+
const requiredAnyTransversalCriticalRuleIds = [
|
|
476
|
+
...PREWRITE_TRANSVERSAL_CRITICAL_SKILLS_RULES[platform],
|
|
477
|
+
];
|
|
478
|
+
const activePrefixCovered = coverage
|
|
479
|
+
? coverage.active_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
|
|
480
|
+
: false;
|
|
481
|
+
const evaluatedPrefixCovered = coverage
|
|
482
|
+
? coverage.evaluated_rule_ids.some((ruleId) => ruleId.startsWith(requiredRulePrefix))
|
|
483
|
+
: false;
|
|
484
|
+
const missingBundles = requiredBundles.filter(
|
|
485
|
+
(bundleName) => !activeSkillsBundles.has(bundleName)
|
|
486
|
+
);
|
|
487
|
+
const missingCriticalRuleIds = coverage
|
|
488
|
+
? requiredCriticalRuleIds.filter((ruleId) => {
|
|
489
|
+
const hasActive = coverage.active_rule_ids.includes(ruleId);
|
|
490
|
+
const hasEvaluated = coverage.evaluated_rule_ids.includes(ruleId);
|
|
491
|
+
return !hasActive || !hasEvaluated;
|
|
492
|
+
})
|
|
493
|
+
: [...requiredCriticalRuleIds];
|
|
494
|
+
const transversalCriticalCovered =
|
|
495
|
+
requiredAnyTransversalCriticalRuleIds.length === 0
|
|
496
|
+
? true
|
|
497
|
+
: coverage
|
|
498
|
+
? requiredAnyTransversalCriticalRuleIds.some((ruleId) =>
|
|
499
|
+
coverage.active_rule_ids.includes(ruleId)
|
|
500
|
+
&& coverage.evaluated_rule_ids.includes(ruleId)
|
|
501
|
+
)
|
|
502
|
+
: false;
|
|
503
|
+
const missingAnyTransversalCriticalRuleIds = transversalCriticalCovered
|
|
504
|
+
? []
|
|
505
|
+
: [...requiredAnyTransversalCriticalRuleIds];
|
|
506
|
+
|
|
507
|
+
requirements.push({
|
|
508
|
+
platform,
|
|
509
|
+
required_rule_prefix: requiredRulePrefix,
|
|
510
|
+
required_bundles: requiredBundles,
|
|
511
|
+
required_critical_rule_ids: requiredCriticalRuleIds,
|
|
512
|
+
required_any_transversal_critical_rule_ids: requiredAnyTransversalCriticalRuleIds,
|
|
513
|
+
active_prefix_covered: activePrefixCovered,
|
|
514
|
+
evaluated_prefix_covered: evaluatedPrefixCovered,
|
|
515
|
+
missing_bundles: missingBundles,
|
|
516
|
+
missing_critical_rule_ids: missingCriticalRuleIds,
|
|
517
|
+
transversal_critical_covered: transversalCriticalCovered,
|
|
518
|
+
missing_any_transversal_critical_rule_ids: missingAnyTransversalCriticalRuleIds,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
if (!activePrefixCovered || !evaluatedPrefixCovered) {
|
|
522
|
+
const missingParts: string[] = [];
|
|
523
|
+
if (!activePrefixCovered) {
|
|
524
|
+
missingParts.push('active_prefix');
|
|
525
|
+
}
|
|
526
|
+
if (!evaluatedPrefixCovered) {
|
|
527
|
+
missingParts.push('evaluated_prefix');
|
|
528
|
+
}
|
|
529
|
+
violations.push(
|
|
530
|
+
toErrorViolation(
|
|
531
|
+
'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE',
|
|
532
|
+
`Skills contract scope coverage missing for ${platform}: ${missingParts.join(', ')} (${requiredRulePrefix}).`
|
|
533
|
+
)
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (missingBundles.length > 0) {
|
|
537
|
+
violations.push(
|
|
538
|
+
toErrorViolation(
|
|
539
|
+
'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING',
|
|
540
|
+
`Skills contract missing bundles for ${platform}: [${missingBundles.join(', ')}].`
|
|
541
|
+
)
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
if (missingCriticalRuleIds.length > 0) {
|
|
545
|
+
violations.push(
|
|
546
|
+
toErrorViolation(
|
|
547
|
+
'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING',
|
|
548
|
+
`Skills contract missing critical rule coverage for ${platform}: [${missingCriticalRuleIds.join(', ')}].`
|
|
549
|
+
)
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
if (!transversalCriticalCovered && requiredAnyTransversalCriticalRuleIds.length > 0) {
|
|
553
|
+
violations.push(
|
|
554
|
+
toErrorViolation(
|
|
555
|
+
'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE',
|
|
556
|
+
`Skills contract missing transversal critical coverage for ${platform}: required_any=[${requiredAnyTransversalCriticalRuleIds.join(', ')}].`
|
|
557
|
+
)
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return {
|
|
563
|
+
stage: params.stage,
|
|
564
|
+
enforced: true,
|
|
565
|
+
status: violations.length === 0 ? 'PASS' : 'FAIL',
|
|
566
|
+
detected_platforms: detectedPlatforms,
|
|
567
|
+
requirements,
|
|
568
|
+
violations,
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
|
|
126
572
|
const collectPreWriteCoherenceViolations = (params: {
|
|
127
573
|
evidence: Extract<EvidenceReadResult, { kind: 'valid' }>['evidence'];
|
|
128
574
|
repoRoot: string;
|
|
129
575
|
repoState: RepoState;
|
|
130
576
|
nowMs: number;
|
|
577
|
+
preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy;
|
|
131
578
|
}): AiGateViolation[] => {
|
|
132
579
|
const violations: AiGateViolation[] = [];
|
|
133
580
|
const evidenceRepoRoot = params.evidence.repo_state?.repo_root;
|
|
@@ -216,6 +663,27 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
216
663
|
)
|
|
217
664
|
);
|
|
218
665
|
}
|
|
666
|
+
|
|
667
|
+
violations.push(
|
|
668
|
+
...collectActiveRuleIdsCoverageViolations({
|
|
669
|
+
stage: 'PRE_WRITE',
|
|
670
|
+
evidence: params.evidence,
|
|
671
|
+
coverage,
|
|
672
|
+
})
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
violations.push(
|
|
676
|
+
...collectPreWritePlatformSkillsViolations({
|
|
677
|
+
evidence: params.evidence,
|
|
678
|
+
coverage,
|
|
679
|
+
})
|
|
680
|
+
);
|
|
681
|
+
violations.push(
|
|
682
|
+
...collectPreWriteCrossPlatformCriticalViolations({
|
|
683
|
+
evidence: params.evidence,
|
|
684
|
+
coverage,
|
|
685
|
+
})
|
|
686
|
+
);
|
|
219
687
|
}
|
|
220
688
|
|
|
221
689
|
if (isTimestampFuture(params.evidence.timestamp, params.nowMs)) {
|
|
@@ -227,6 +695,25 @@ const collectPreWriteCoherenceViolations = (params: {
|
|
|
227
695
|
);
|
|
228
696
|
}
|
|
229
697
|
|
|
698
|
+
if (params.preWriteWorktreeHygiene.enabled && params.repoState.git.available) {
|
|
699
|
+
const pendingChanges = params.repoState.git.staged + params.repoState.git.unstaged;
|
|
700
|
+
if (pendingChanges >= params.preWriteWorktreeHygiene.blockThreshold) {
|
|
701
|
+
violations.push(
|
|
702
|
+
toErrorViolation(
|
|
703
|
+
'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
|
|
704
|
+
`PRE_WRITE hygiene exceeded: pending_changes=${pendingChanges} (block_threshold=${params.preWriteWorktreeHygiene.blockThreshold}). Split worktree into atomic slices.`
|
|
705
|
+
)
|
|
706
|
+
);
|
|
707
|
+
} else if (pendingChanges >= params.preWriteWorktreeHygiene.warnThreshold) {
|
|
708
|
+
violations.push(
|
|
709
|
+
toWarnViolation(
|
|
710
|
+
'EVIDENCE_PREWRITE_WORKTREE_WARN',
|
|
711
|
+
`PRE_WRITE hygiene warning: pending_changes=${pendingChanges} (warn_threshold=${params.preWriteWorktreeHygiene.warnThreshold}). Consider splitting worktree into smaller slices.`
|
|
712
|
+
)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
230
717
|
return violations;
|
|
231
718
|
};
|
|
232
719
|
|
|
@@ -236,7 +723,8 @@ const collectEvidenceViolations = (
|
|
|
236
723
|
repoState: RepoState,
|
|
237
724
|
stage: AiGateStage,
|
|
238
725
|
nowMs: number,
|
|
239
|
-
maxAgeSecondsByStage: Readonly<Record<AiGateStage, number
|
|
726
|
+
maxAgeSecondsByStage: Readonly<Record<AiGateStage, number>>,
|
|
727
|
+
preWriteWorktreeHygiene: PreWriteWorktreeHygienePolicy
|
|
240
728
|
): { violations: AiGateViolation[]; ageSeconds: number | null } => {
|
|
241
729
|
const violations: AiGateViolation[] = [];
|
|
242
730
|
const maxAgeSeconds = maxAgeSecondsByStage[stage];
|
|
@@ -287,6 +775,7 @@ const collectEvidenceViolations = (
|
|
|
287
775
|
repoRoot,
|
|
288
776
|
repoState,
|
|
289
777
|
nowMs,
|
|
778
|
+
preWriteWorktreeHygiene,
|
|
290
779
|
})
|
|
291
780
|
);
|
|
292
781
|
}
|
|
@@ -473,6 +962,7 @@ export const evaluateAiGate = (
|
|
|
473
962
|
maxAgeSecondsByStage?: Readonly<Record<AiGateStage, number>>;
|
|
474
963
|
protectedBranches?: ReadonlyArray<string>;
|
|
475
964
|
requireMcpReceipt?: boolean;
|
|
965
|
+
preWriteWorktreeHygiene?: Partial<PreWriteWorktreeHygienePolicy>;
|
|
476
966
|
},
|
|
477
967
|
dependencies: Partial<AiGateDependencies> = {}
|
|
478
968
|
): AiGateCheckResult => {
|
|
@@ -481,6 +971,9 @@ export const evaluateAiGate = (
|
|
|
481
971
|
...dependencies,
|
|
482
972
|
};
|
|
483
973
|
const maxAgeSecondsByStage = params.maxAgeSecondsByStage ?? DEFAULT_MAX_AGE_SECONDS;
|
|
974
|
+
const preWriteWorktreeHygiene = resolvePreWriteWorktreeHygienePolicy(
|
|
975
|
+
params.preWriteWorktreeHygiene
|
|
976
|
+
);
|
|
484
977
|
const protectedBranches = new Set(params.protectedBranches ?? Array.from(DEFAULT_PROTECTED_BRANCHES));
|
|
485
978
|
const nowMs = activeDependencies.now();
|
|
486
979
|
const evidenceResult = activeDependencies.readEvidenceResult(params.repoRoot);
|
|
@@ -496,7 +989,8 @@ export const evaluateAiGate = (
|
|
|
496
989
|
repoState,
|
|
497
990
|
params.stage,
|
|
498
991
|
nowMs,
|
|
499
|
-
maxAgeSecondsByStage
|
|
992
|
+
maxAgeSecondsByStage,
|
|
993
|
+
preWriteWorktreeHygiene
|
|
500
994
|
);
|
|
501
995
|
const mcpReceiptAssessment = collectMcpReceiptViolations({
|
|
502
996
|
required: params.requireMcpReceipt ?? false,
|
|
@@ -507,8 +1001,22 @@ export const evaluateAiGate = (
|
|
|
507
1001
|
readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
|
|
508
1002
|
});
|
|
509
1003
|
const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
|
|
1004
|
+
const skillsContract = toSkillsContractAssessment({
|
|
1005
|
+
stage: params.stage,
|
|
1006
|
+
evidenceResult,
|
|
1007
|
+
});
|
|
1008
|
+
const stageSkillsContractViolations =
|
|
1009
|
+
params.stage === 'PRE_WRITE' || skillsContract.status !== 'FAIL'
|
|
1010
|
+
? []
|
|
1011
|
+
: [
|
|
1012
|
+
toErrorViolation(
|
|
1013
|
+
'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE',
|
|
1014
|
+
`Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
|
|
1015
|
+
),
|
|
1016
|
+
];
|
|
510
1017
|
const violations = [
|
|
511
1018
|
...evidenceAssessment.violations,
|
|
1019
|
+
...stageSkillsContractViolations,
|
|
512
1020
|
...gitflowViolations,
|
|
513
1021
|
...mcpReceiptAssessment.violations,
|
|
514
1022
|
];
|
|
@@ -538,6 +1046,7 @@ export const evaluateAiGate = (
|
|
|
538
1046
|
max_age_seconds: mcpReceiptAssessment.maxAgeSeconds,
|
|
539
1047
|
age_seconds: mcpReceiptAssessment.ageSeconds,
|
|
540
1048
|
},
|
|
1049
|
+
skills_contract: skillsContract,
|
|
541
1050
|
repo_state: repoState,
|
|
542
1051
|
violations,
|
|
543
1052
|
};
|
|
@@ -26,7 +26,11 @@ const assertSafeGitArgs = (args: ReadonlyArray<string>): void => {
|
|
|
26
26
|
export class GitService implements IGitService {
|
|
27
27
|
runGit(args: ReadonlyArray<string>, cwd?: string): string {
|
|
28
28
|
assertSafeGitArgs(args);
|
|
29
|
-
return runBinarySync('git', [...args], {
|
|
29
|
+
return runBinarySync('git', [...args], {
|
|
30
|
+
cwd,
|
|
31
|
+
encoding: 'utf8',
|
|
32
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
33
|
+
});
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
getStagedFacts(extensions: ReadonlyArray<string>): ReadonlyArray<Fact> {
|