pumuki 6.3.129 → 6.3.130
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/AGENTS.md +16 -1
- package/CHANGELOG.md +9 -0
- package/README.md +14 -10
- package/VERSION +1 -1
- package/core/facts/detectors/text/android.test.ts +2583 -24
- package/core/facts/detectors/text/android.ts +4633 -34
- package/core/facts/detectors/typescript/index.test.ts +3639 -73
- package/core/facts/detectors/typescript/index.ts +4819 -270
- package/core/facts/extractHeuristicFacts.ts +271 -6
- package/core/rules/presets/heuristics/android.test.ts +314 -1
- package/core/rules/presets/heuristics/android.ts +1220 -50
- package/core/rules/presets/heuristics/typescript.test.ts +158 -2
- package/core/rules/presets/heuristics/typescript.ts +508 -0
- package/docs/README.md +3 -3
- package/docs/operations/RELEASE_NOTES.md +7 -1
- package/docs/operations/framework-menu-consumer-walkthrough.md +18 -15
- package/docs/product/API_REFERENCE.md +1 -1
- package/docs/product/USAGE.md +1 -1
- package/docs/validation/README.md +3 -1
- package/integrations/config/skillsCompilerTemplates.test.ts +131 -0
- package/integrations/config/skillsCompilerTemplates.ts +953 -7
- package/integrations/config/skillsDetectorRegistry.ts +451 -8
- package/integrations/config/skillsMarkdownRules.ts +884 -2
- package/integrations/evidence/buildEvidence.ts +29 -1
- package/integrations/evidence/platformSummary.test.ts +73 -9
- package/integrations/evidence/platformSummary.ts +165 -7
- package/integrations/evidence/repoState.ts +3 -0
- package/integrations/evidence/schema.ts +18 -0
- package/integrations/evidence/trackingContract.ts +17 -0
- package/integrations/evidence/writeEvidence.test.ts +3 -0
- package/integrations/evidence/writeEvidence.ts +29 -1
- package/integrations/gate/evaluateAiGate.ts +251 -8
- package/integrations/gate/governanceActionCatalog.ts +275 -0
- package/integrations/gate/remediationCatalog.ts +8 -0
- package/integrations/git/runPlatformGate.ts +9 -1
- package/integrations/lifecycle/adapter.ts +24 -0
- package/integrations/lifecycle/bootstrapManifest.ts +248 -0
- package/integrations/lifecycle/cliGovernanceConsole.ts +69 -0
- package/integrations/lifecycle/governanceNextAction.ts +171 -0
- package/integrations/lifecycle/governanceObservationSnapshot.ts +369 -0
- package/integrations/lifecycle/packageInfo.ts +118 -1
- package/integrations/lifecycle/state.ts +8 -1
- package/integrations/lifecycle/trackingState.ts +403 -0
- package/integrations/lifecycle/watch.ts +1 -1
- package/integrations/mcp/aiGateCheck.ts +194 -10
- package/integrations/mcp/alignedPlatformGate.ts +232 -0
- package/integrations/mcp/enterpriseServer.ts +19 -3
- package/integrations/mcp/preFlightCheck.ts +66 -3
- package/integrations/mcp/readMcpPrePushStdin.ts +7 -0
- package/package.json +1 -1
- package/scripts/build-ruralgo-s1-evidence-pack.ts +85 -0
- package/scripts/check-tracking-single-active.sh +1 -1
- package/scripts/framework-menu-advanced-view-lib.ts +49 -0
- package/scripts/framework-menu-consumer-actions-lib.ts +32 -32
- package/scripts/framework-menu-consumer-preflight-render.ts +10 -0
- package/scripts/framework-menu-consumer-preflight-run.ts +23 -0
- package/scripts/framework-menu-consumer-preflight-types.ts +12 -0
- package/scripts/framework-menu-consumer-runtime-actions.ts +11 -5
- package/scripts/framework-menu-consumer-runtime-audit.ts +0 -28
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +118 -42
- package/scripts/framework-menu-consumer-runtime-lib.ts +38 -0
- package/scripts/framework-menu-consumer-runtime-menu.ts +55 -15
- package/scripts/framework-menu-consumer-runtime-types.ts +4 -0
- package/scripts/framework-menu-evidence-summary-read.ts +17 -1
- package/scripts/framework-menu-evidence-summary-types.ts +3 -0
- package/scripts/framework-menu-layout-data.ts +3 -23
- package/scripts/framework-menu-system-notifications-macos-applescript-dialog.ts +1 -1
- package/scripts/framework-menu-system-notifications-macos-dialog-payload.ts +14 -2
- package/scripts/framework-menu-system-notifications-macos-swift-source.ts +1 -1
- package/scripts/framework-menu-system-notifications-payloads-blocked.ts +128 -4
- package/scripts/framework-menu-system-notifications-payloads.ts +8 -1
- package/scripts/framework-menu-system-notifications-text.ts +7 -1
- package/scripts/framework-menu.ts +37 -2
- package/scripts/package-install-smoke-consumer-git-repo-lib.ts +10 -1
- package/scripts/package-install-smoke-consumer-npm-lib.ts +46 -9
- package/scripts/ruralgo-s1-evidence-pack-lib.ts +200 -0
- package/skills.lock.json +613 -698
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { EvidenceReadResult } from '../evidence/readEvidence';
|
|
2
2
|
import { readEvidenceResult } from '../evidence/readEvidence';
|
|
3
3
|
import { captureRepoState } from '../evidence/repoState';
|
|
4
|
-
import type { RepoState } from '../evidence/schema';
|
|
4
|
+
import type { RepoState, RepoTrackingState } from '../evidence/schema';
|
|
5
5
|
import { resolvePolicyForStage } from './stagePolicies';
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
7
|
import { existsSync, realpathSync } from 'node:fs';
|
|
8
8
|
import { resolve } from 'node:path';
|
|
9
|
+
import { isSeverityAtLeast } from '../../core/rules/Severity';
|
|
9
10
|
import type { SkillsLockV1, SkillsStage } from '../config/skillsLock';
|
|
10
11
|
import {
|
|
11
12
|
loadEffectiveSkillsLock,
|
|
@@ -132,6 +133,10 @@ const PREWRITE_WORKTREE_HYGIENE_WARN_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_W
|
|
|
132
133
|
const PREWRITE_WORKTREE_HYGIENE_BLOCK_THRESHOLD_ENV = 'PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD';
|
|
133
134
|
|
|
134
135
|
const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
|
|
136
|
+
const DEFAULT_GITFLOW_BRANCH_PATTERNS = [
|
|
137
|
+
/^(?:feature|bugfix|hotfix|chore|refactor|docs)\/[a-z0-9]+(?:-[a-z0-9]+)*$/,
|
|
138
|
+
/^release\/\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/,
|
|
139
|
+
] as const;
|
|
135
140
|
const PREWRITE_SKILLS_PLATFORMS = ['ios', 'android', 'backend', 'frontend'] as const;
|
|
136
141
|
type PreWriteSkillsPlatform = (typeof PREWRITE_SKILLS_PLATFORMS)[number];
|
|
137
142
|
const PLATFORM_SKILLS_RULE_PREFIXES: Readonly<Record<PreWriteSkillsPlatform, string>> = {
|
|
@@ -398,6 +403,45 @@ const collectWorktreeChangedPaths = (repoRoot: string): ReadonlyArray<string> =>
|
|
|
398
403
|
}
|
|
399
404
|
};
|
|
400
405
|
|
|
406
|
+
const collectGitChangedPaths = (
|
|
407
|
+
repoRoot: string,
|
|
408
|
+
args: ReadonlyArray<string>
|
|
409
|
+
): ReadonlyArray<string> => {
|
|
410
|
+
try {
|
|
411
|
+
const output = execFileSync(
|
|
412
|
+
'git',
|
|
413
|
+
[...args],
|
|
414
|
+
{
|
|
415
|
+
cwd: repoRoot,
|
|
416
|
+
encoding: 'utf8',
|
|
417
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
418
|
+
}
|
|
419
|
+
);
|
|
420
|
+
const files = output
|
|
421
|
+
.split('\n')
|
|
422
|
+
.map((line) => parseChangedPath(line))
|
|
423
|
+
.filter((line): line is string => typeof line === 'string' && line.length > 0);
|
|
424
|
+
return [...new Set(files)];
|
|
425
|
+
} catch {
|
|
426
|
+
return [];
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const collectStagedChangedPaths = (repoRoot: string): ReadonlyArray<string> =>
|
|
431
|
+
collectGitChangedPaths(repoRoot, ['diff', '--cached', '--name-status', '--find-renames']);
|
|
432
|
+
|
|
433
|
+
const collectRangeChangedPaths = (params: {
|
|
434
|
+
repoRoot: string;
|
|
435
|
+
fromRef: string;
|
|
436
|
+
toRef: string;
|
|
437
|
+
}): ReadonlyArray<string> =>
|
|
438
|
+
collectGitChangedPaths(params.repoRoot, [
|
|
439
|
+
'diff',
|
|
440
|
+
'--name-status',
|
|
441
|
+
'--find-renames',
|
|
442
|
+
`${params.fromRef}...${params.toRef}`,
|
|
443
|
+
]);
|
|
444
|
+
|
|
401
445
|
const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boolean => {
|
|
402
446
|
const normalized = normalizeChangedPath(filePath).toLowerCase();
|
|
403
447
|
if (platform === 'ios') {
|
|
@@ -444,6 +488,47 @@ const isPlatformPath = (platform: PreWriteSkillsPlatform, filePath: string): boo
|
|
|
444
488
|
|| /(^|\/)(frontend|web|client)(\/|$)/.test(normalized);
|
|
445
489
|
};
|
|
446
490
|
|
|
491
|
+
const toDetectedPlatformsFromPaths = (params: {
|
|
492
|
+
changedPaths: ReadonlyArray<string>;
|
|
493
|
+
requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
|
|
494
|
+
}): ReadonlyArray<PreWriteSkillsPlatform> => {
|
|
495
|
+
if (params.changedPaths.length === 0) {
|
|
496
|
+
return [];
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const detected = new Set<PreWriteSkillsPlatform>();
|
|
500
|
+
for (const filePath of params.changedPaths) {
|
|
501
|
+
for (const platform of params.requiredPlatforms) {
|
|
502
|
+
if (isPlatformPath(platform, filePath)) {
|
|
503
|
+
detected.add(platform);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return PREWRITE_SKILLS_PLATFORMS.filter((platform) => detected.has(platform));
|
|
509
|
+
};
|
|
510
|
+
|
|
511
|
+
const collectStageScopedChangedPaths = (params: {
|
|
512
|
+
stage: AiGateStage;
|
|
513
|
+
repoRoot: string;
|
|
514
|
+
upstream: string | null;
|
|
515
|
+
}): ReadonlyArray<string> => {
|
|
516
|
+
if (params.stage === 'PRE_WRITE') {
|
|
517
|
+
return collectWorktreeChangedPaths(params.repoRoot);
|
|
518
|
+
}
|
|
519
|
+
if (params.stage === 'PRE_COMMIT') {
|
|
520
|
+
return collectStagedChangedPaths(params.repoRoot);
|
|
521
|
+
}
|
|
522
|
+
if (params.stage === 'PRE_PUSH' && typeof params.upstream === 'string' && params.upstream.length > 0) {
|
|
523
|
+
return collectRangeChangedPaths({
|
|
524
|
+
repoRoot: params.repoRoot,
|
|
525
|
+
fromRef: params.upstream,
|
|
526
|
+
toRef: 'HEAD',
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return [];
|
|
530
|
+
};
|
|
531
|
+
|
|
447
532
|
const hasWorktreeCodePlatforms = (params: {
|
|
448
533
|
repoRoot: string;
|
|
449
534
|
requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
|
|
@@ -457,6 +542,22 @@ const hasWorktreeCodePlatforms = (params: {
|
|
|
457
542
|
);
|
|
458
543
|
};
|
|
459
544
|
|
|
545
|
+
const toStageScopedDetectedPlatforms = (params: {
|
|
546
|
+
stage: AiGateStage;
|
|
547
|
+
repoRoot: string;
|
|
548
|
+
upstream: string | null;
|
|
549
|
+
requiredPlatforms: ReadonlyArray<PreWriteSkillsPlatform>;
|
|
550
|
+
}): ReadonlyArray<PreWriteSkillsPlatform> => {
|
|
551
|
+
return toDetectedPlatformsFromPaths({
|
|
552
|
+
changedPaths: collectStageScopedChangedPaths({
|
|
553
|
+
stage: params.stage,
|
|
554
|
+
repoRoot: params.repoRoot,
|
|
555
|
+
upstream: params.upstream,
|
|
556
|
+
}),
|
|
557
|
+
requiredPlatforms: params.requiredPlatforms,
|
|
558
|
+
});
|
|
559
|
+
};
|
|
560
|
+
|
|
460
561
|
const toLockRequiredPlatforms = (
|
|
461
562
|
requiredLock: SkillsLockV1 | undefined
|
|
462
563
|
): ReadonlyArray<PreWriteSkillsPlatform> => {
|
|
@@ -748,8 +849,18 @@ const toSkillsContractAssessment = (params: {
|
|
|
748
849
|
const coverage = params.evidenceResult.evidence.snapshot.rules_coverage;
|
|
749
850
|
const explicitlyDetectedPlatforms = toDetectedSkillsPlatforms(params.evidenceResult.evidence.platforms);
|
|
750
851
|
const inferredPlatforms = toCoverageInferredPlatforms(coverage);
|
|
852
|
+
const stageScopedDetectedPlatforms =
|
|
853
|
+
params.stage !== 'CI' && requiredPlatforms.length > 0
|
|
854
|
+
? toStageScopedDetectedPlatforms({
|
|
855
|
+
stage: params.stage,
|
|
856
|
+
repoRoot: params.repoRoot,
|
|
857
|
+
upstream: params.repoState.git.upstream,
|
|
858
|
+
requiredPlatforms,
|
|
859
|
+
})
|
|
860
|
+
: [];
|
|
861
|
+
const worktreeDetectedPlatforms = stageScopedDetectedPlatforms;
|
|
751
862
|
const repoTreeDetectedPlatforms =
|
|
752
|
-
params.stage
|
|
863
|
+
params.stage === 'CI' && requiredPlatforms.length > 0
|
|
753
864
|
? toRepoTreeDetectedPlatforms({
|
|
754
865
|
repoRoot: params.repoRoot,
|
|
755
866
|
platforms: requiredPlatforms,
|
|
@@ -759,20 +870,24 @@ const toSkillsContractAssessment = (params: {
|
|
|
759
870
|
explicitlyDetectedPlatforms.length > 0
|
|
760
871
|
? toEffectiveSkillsPlatforms({
|
|
761
872
|
platforms: params.evidenceResult.evidence.platforms,
|
|
762
|
-
|
|
763
|
-
|
|
873
|
+
coverage,
|
|
874
|
+
})
|
|
764
875
|
: [];
|
|
765
876
|
const detectedPlatforms =
|
|
766
|
-
|
|
877
|
+
stageScopedDetectedPlatforms.length > 0
|
|
878
|
+
? stageScopedDetectedPlatforms
|
|
879
|
+
: explicitlyDetectedEffectivePlatforms.length > 0
|
|
767
880
|
? explicitlyDetectedEffectivePlatforms
|
|
768
881
|
: inferredPlatforms.length > 0
|
|
769
882
|
? inferredPlatforms
|
|
883
|
+
: worktreeDetectedPlatforms.length > 0
|
|
884
|
+
? worktreeDetectedPlatforms
|
|
770
885
|
: repoTreeDetectedPlatforms;
|
|
771
886
|
const pendingChanges = resolvePendingChanges(params.repoState);
|
|
772
887
|
const detectedPlatformSet = new Set(detectedPlatforms);
|
|
773
888
|
const assessmentPlatforms =
|
|
774
889
|
requiredPlatforms.length > 0
|
|
775
|
-
? params.stage
|
|
890
|
+
? params.stage !== 'CI' && detectedPlatforms.length > 0
|
|
776
891
|
? requiredPlatforms.filter((platform) => detectedPlatformSet.has(platform))
|
|
777
892
|
: requiredPlatforms
|
|
778
893
|
: detectedPlatforms;
|
|
@@ -1164,6 +1279,63 @@ const collectEvidenceViolations = (
|
|
|
1164
1279
|
return { violations, ageSeconds };
|
|
1165
1280
|
};
|
|
1166
1281
|
|
|
1282
|
+
const severityOrder: ReadonlyArray<'CRITICAL' | 'ERROR' | 'WARN' | 'INFO'> = [
|
|
1283
|
+
'CRITICAL',
|
|
1284
|
+
'ERROR',
|
|
1285
|
+
'WARN',
|
|
1286
|
+
'INFO',
|
|
1287
|
+
];
|
|
1288
|
+
|
|
1289
|
+
const toHighestTriggeredSeverity = (
|
|
1290
|
+
severityCounts: Readonly<Record<'INFO' | 'WARN' | 'ERROR' | 'CRITICAL', number>>,
|
|
1291
|
+
threshold: 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL'
|
|
1292
|
+
): 'INFO' | 'WARN' | 'ERROR' | 'CRITICAL' | null => {
|
|
1293
|
+
for (const severity of severityOrder) {
|
|
1294
|
+
if (severityCounts[severity] > 0 && isSeverityAtLeast(severity, threshold)) {
|
|
1295
|
+
return severity;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
return null;
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
const collectEvidencePolicyThresholdViolations = (params: {
|
|
1302
|
+
evidenceResult: EvidenceReadResult;
|
|
1303
|
+
policy: ReturnType<typeof resolvePolicyForStage>['policy'];
|
|
1304
|
+
}): AiGateViolation[] => {
|
|
1305
|
+
if (params.evidenceResult.kind !== 'valid') {
|
|
1306
|
+
return [];
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
const severityCounts = params.evidenceResult.evidence.severity_metrics.by_severity;
|
|
1310
|
+
const blockSeverity = toHighestTriggeredSeverity(
|
|
1311
|
+
severityCounts,
|
|
1312
|
+
params.policy.blockOnOrAbove
|
|
1313
|
+
);
|
|
1314
|
+
if (blockSeverity && params.evidenceResult.evidence.ai_gate.status !== 'BLOCKED') {
|
|
1315
|
+
return [
|
|
1316
|
+
toErrorViolation(
|
|
1317
|
+
'EVIDENCE_POLICY_THRESHOLD_BLOCK',
|
|
1318
|
+
`Evidence severities exceed block_on_or_above=${params.policy.blockOnOrAbove} (highest=${blockSeverity}).`
|
|
1319
|
+
),
|
|
1320
|
+
];
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
const warnSeverity = toHighestTriggeredSeverity(
|
|
1324
|
+
severityCounts,
|
|
1325
|
+
params.policy.warnOnOrAbove
|
|
1326
|
+
);
|
|
1327
|
+
if (warnSeverity && params.evidenceResult.evidence.ai_gate.status === 'ALLOWED') {
|
|
1328
|
+
return [
|
|
1329
|
+
toWarnViolation(
|
|
1330
|
+
'EVIDENCE_POLICY_THRESHOLD_WARN',
|
|
1331
|
+
`Evidence severities exceed warn_on_or_above=${params.policy.warnOnOrAbove} (highest=${warnSeverity}).`
|
|
1332
|
+
),
|
|
1333
|
+
];
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
return [];
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1167
1339
|
const toEvidenceSourceDescriptor = (
|
|
1168
1340
|
result: EvidenceReadResult,
|
|
1169
1341
|
repoRoot: string
|
|
@@ -1193,17 +1365,81 @@ const collectGitflowViolations = (
|
|
|
1193
1365
|
if (!repoState.git.available) {
|
|
1194
1366
|
return violations;
|
|
1195
1367
|
}
|
|
1196
|
-
|
|
1368
|
+
const branch = repoState.git.branch?.trim() ?? null;
|
|
1369
|
+
const normalizedBranch = branch?.toLowerCase() ?? null;
|
|
1370
|
+
if (branch && normalizedBranch && protectedBranches.has(normalizedBranch)) {
|
|
1197
1371
|
violations.push(
|
|
1198
1372
|
toErrorViolation(
|
|
1199
1373
|
'GITFLOW_PROTECTED_BRANCH',
|
|
1200
|
-
`Direct work on protected branch "${
|
|
1374
|
+
`Direct work on protected branch "${branch}" is not allowed.`
|
|
1375
|
+
)
|
|
1376
|
+
);
|
|
1377
|
+
return violations;
|
|
1378
|
+
}
|
|
1379
|
+
if (
|
|
1380
|
+
branch
|
|
1381
|
+
&& !DEFAULT_GITFLOW_BRANCH_PATTERNS.some((pattern) => pattern.test(branch))
|
|
1382
|
+
) {
|
|
1383
|
+
violations.push(
|
|
1384
|
+
toErrorViolation(
|
|
1385
|
+
'GITFLOW_BRANCH_NAMING_INVALID',
|
|
1386
|
+
`Branch "${branch}" does not comply with GitFlow naming. Use feature/*, bugfix/*, hotfix/*, release/*, chore/*, refactor/* or docs/*.`
|
|
1201
1387
|
)
|
|
1202
1388
|
);
|
|
1203
1389
|
}
|
|
1204
1390
|
return violations;
|
|
1205
1391
|
};
|
|
1206
1392
|
|
|
1393
|
+
const DEFAULT_TRACKING_STATE: RepoTrackingState = {
|
|
1394
|
+
enforced: false,
|
|
1395
|
+
canonical_path: null,
|
|
1396
|
+
canonical_present: false,
|
|
1397
|
+
source_file: null,
|
|
1398
|
+
in_progress_count: null,
|
|
1399
|
+
single_in_progress_valid: null,
|
|
1400
|
+
conflict: false,
|
|
1401
|
+
declarations: [],
|
|
1402
|
+
};
|
|
1403
|
+
|
|
1404
|
+
const collectTrackingViolations = (repoState: RepoState): AiGateViolation[] => {
|
|
1405
|
+
const tracking = repoState.lifecycle.tracking ?? DEFAULT_TRACKING_STATE;
|
|
1406
|
+
if (!tracking.enforced) {
|
|
1407
|
+
return [];
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
if (tracking.conflict) {
|
|
1411
|
+
const declaredPaths = tracking.declarations
|
|
1412
|
+
.map((entry) => `${entry.source_file}:${entry.resolved_path}`)
|
|
1413
|
+
.join(', ');
|
|
1414
|
+
return [
|
|
1415
|
+
toErrorViolation(
|
|
1416
|
+
'TRACKING_CANONICAL_SOURCE_CONFLICT',
|
|
1417
|
+
`Tracking canonical source conflict detected (${declaredPaths}).`
|
|
1418
|
+
),
|
|
1419
|
+
];
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (!tracking.canonical_path || !tracking.canonical_present) {
|
|
1423
|
+
return [
|
|
1424
|
+
toErrorViolation(
|
|
1425
|
+
'TRACKING_CANONICAL_FILE_MISSING',
|
|
1426
|
+
`Tracking canonical file is missing (${tracking.canonical_path ?? 'undeclared'}).`
|
|
1427
|
+
),
|
|
1428
|
+
];
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
if (tracking.single_in_progress_valid === false) {
|
|
1432
|
+
return [
|
|
1433
|
+
toErrorViolation(
|
|
1434
|
+
'TRACKING_CANONICAL_IN_PROGRESS_INVALID',
|
|
1435
|
+
`Tracking canonical file must contain exactly one in-progress task (count=${tracking.in_progress_count ?? 'n/a'}).`
|
|
1436
|
+
),
|
|
1437
|
+
];
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
return [];
|
|
1441
|
+
};
|
|
1442
|
+
|
|
1207
1443
|
const resolvePendingChanges = (repoState: RepoState): number | null => {
|
|
1208
1444
|
if (!repoState.git.available) {
|
|
1209
1445
|
return null;
|
|
@@ -1422,6 +1658,7 @@ export const evaluateAiGate = (
|
|
|
1422
1658
|
readMcpAiGateReceipt: activeDependencies.readMcpAiGateReceipt,
|
|
1423
1659
|
});
|
|
1424
1660
|
const gitflowViolations = collectGitflowViolations(repoState, protectedBranches);
|
|
1661
|
+
const trackingViolations = collectTrackingViolations(repoState);
|
|
1425
1662
|
const skillsContract = toSkillsContractAssessment({
|
|
1426
1663
|
stage: params.stage,
|
|
1427
1664
|
repoRoot: params.repoRoot,
|
|
@@ -1445,10 +1682,16 @@ export const evaluateAiGate = (
|
|
|
1445
1682
|
`Skills contract incomplete for ${params.stage}: ${skillsContract.violations.map((violation) => violation.code).join(', ')}.`
|
|
1446
1683
|
),
|
|
1447
1684
|
];
|
|
1685
|
+
const policyThresholdViolations = collectEvidencePolicyThresholdViolations({
|
|
1686
|
+
evidenceResult,
|
|
1687
|
+
policy: resolvedPolicy.policy,
|
|
1688
|
+
});
|
|
1448
1689
|
const violations = [
|
|
1449
1690
|
...evidenceAssessment.violations,
|
|
1691
|
+
...policyThresholdViolations,
|
|
1450
1692
|
...stageSkillsContractViolations,
|
|
1451
1693
|
...gitflowViolations,
|
|
1694
|
+
...trackingViolations,
|
|
1452
1695
|
...mcpReceiptAssessment.violations,
|
|
1453
1696
|
];
|
|
1454
1697
|
const blocked = violations.some((violation) => violation.severity === 'ERROR');
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { AiGateStage } from './evaluateAiGate';
|
|
2
|
+
|
|
3
|
+
export type GovernanceCatalogNextAction = {
|
|
4
|
+
kind: 'info' | 'run_command';
|
|
5
|
+
message: string;
|
|
6
|
+
command?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type GovernanceCatalogAction = {
|
|
10
|
+
reason_code: string;
|
|
11
|
+
instruction: string;
|
|
12
|
+
next_action: GovernanceCatalogNextAction;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const PRE_WRITE_VALIDATE_COMMAND =
|
|
16
|
+
'npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json';
|
|
17
|
+
|
|
18
|
+
export const buildGovernanceValidateCommand = (stage: AiGateStage): string =>
|
|
19
|
+
PRE_WRITE_VALIDATE_COMMAND.replace('PRE_WRITE', stage);
|
|
20
|
+
|
|
21
|
+
export const buildGovernancePolicyReconcileCommand = (stage: AiGateStage): string =>
|
|
22
|
+
`npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && ${buildGovernanceValidateCommand(stage)}`;
|
|
23
|
+
|
|
24
|
+
const buildFallbackAction = (code: string): GovernanceCatalogAction => ({
|
|
25
|
+
reason_code: code,
|
|
26
|
+
instruction: 'Corrige el bloqueante primario y vuelve a ejecutar la validación del stage actual.',
|
|
27
|
+
next_action: {
|
|
28
|
+
kind: 'info',
|
|
29
|
+
message: 'Corrige el bloqueante primario y vuelve a ejecutar el mismo comando.',
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
export const resolveGovernanceCatalogAction = (params: {
|
|
34
|
+
code: string;
|
|
35
|
+
stage?: AiGateStage;
|
|
36
|
+
fallback?: GovernanceCatalogAction;
|
|
37
|
+
}): GovernanceCatalogAction => {
|
|
38
|
+
const stage = params.stage ?? 'PRE_WRITE';
|
|
39
|
+
const validateCommand = buildGovernanceValidateCommand(stage);
|
|
40
|
+
switch (params.code) {
|
|
41
|
+
case 'READY':
|
|
42
|
+
return {
|
|
43
|
+
reason_code: 'READY',
|
|
44
|
+
instruction: 'Puedes continuar con la siguiente slice mínima y volver a validar al cerrar.',
|
|
45
|
+
next_action: {
|
|
46
|
+
kind: 'info',
|
|
47
|
+
message: 'Gate en verde. Continúa con la implementación.',
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
case 'EVIDENCE_INVALID_OR_CHAIN':
|
|
51
|
+
return {
|
|
52
|
+
reason_code: 'EVIDENCE_INVALID_OR_CHAIN',
|
|
53
|
+
instruction: 'Regenera la evidencia canónica antes de continuar.',
|
|
54
|
+
next_action: {
|
|
55
|
+
kind: 'run_command',
|
|
56
|
+
message: 'Regenera o corrige la evidencia canónica (.ai_evidence.json) y vuelve a validar governance.',
|
|
57
|
+
command: validateCommand,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
case 'AI_GATE_BLOCKED':
|
|
61
|
+
return {
|
|
62
|
+
reason_code: 'AI_GATE_BLOCKED',
|
|
63
|
+
instruction: 'Corrige primero el bloqueo principal del gate antes de continuar.',
|
|
64
|
+
next_action: {
|
|
65
|
+
kind: 'run_command',
|
|
66
|
+
message: 'Revalida el stage tras corregir la evidencia y el bloqueo principal.',
|
|
67
|
+
command: validateCommand,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
case 'SDD_SESSION_MISSING':
|
|
71
|
+
return {
|
|
72
|
+
reason_code: 'SDD_SESSION_MISSING',
|
|
73
|
+
instruction: 'Abre una sesión SDD válida antes de seguir trabajando.',
|
|
74
|
+
next_action: {
|
|
75
|
+
kind: 'run_command',
|
|
76
|
+
message: 'Abre la sesión SDD del cambio activo y vuelve a validar.',
|
|
77
|
+
command: 'npx --yes --package pumuki@latest pumuki sdd session --open --change=<id>',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
case 'SDD_SESSION_INVALID':
|
|
81
|
+
case 'SDD_SESSION_INVALID_OR_EXPIRED':
|
|
82
|
+
return {
|
|
83
|
+
reason_code: 'SDD_SESSION_INVALID_OR_EXPIRED',
|
|
84
|
+
instruction: 'Refresca o reabre la sesión SDD antes de seguir trabajando.',
|
|
85
|
+
next_action: {
|
|
86
|
+
kind: 'run_command',
|
|
87
|
+
message: 'Refresca la sesión SDD y vuelve a validar governance.',
|
|
88
|
+
command: 'npx --yes --package pumuki@latest pumuki sdd session --refresh --ttl-minutes=90',
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
case 'PRE_PUSH_UPSTREAM_MISSING':
|
|
92
|
+
return {
|
|
93
|
+
reason_code: 'PRE_PUSH_UPSTREAM_MISSING',
|
|
94
|
+
instruction: 'Configura el upstream remoto antes de continuar con PRE_PUSH.',
|
|
95
|
+
next_action: {
|
|
96
|
+
kind: 'run_command',
|
|
97
|
+
message: 'Configura tracking remoto para la rama actual.',
|
|
98
|
+
command: 'git push --set-upstream origin <branch>',
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
case 'PRE_PUSH_UPSTREAM_MISALIGNED':
|
|
102
|
+
return {
|
|
103
|
+
reason_code: 'PRE_PUSH_UPSTREAM_MISALIGNED',
|
|
104
|
+
instruction: 'Alinea el upstream remoto con la rama actual antes de continuar.',
|
|
105
|
+
next_action: {
|
|
106
|
+
kind: 'run_command',
|
|
107
|
+
message: 'Reconfigura el upstream remoto de la rama actual.',
|
|
108
|
+
command: 'git branch --unset-upstream && git push --set-upstream origin <branch>',
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
case 'GITFLOW_PROTECTED_BRANCH':
|
|
112
|
+
case 'GITFLOW_PROTECTED_BRANCH_CONTEXT':
|
|
113
|
+
return {
|
|
114
|
+
reason_code: 'GITFLOW_PROTECTED_BRANCH_CONTEXT',
|
|
115
|
+
instruction: 'Sal de la rama protegida y mueve el trabajo a feature/* o refactor/*.',
|
|
116
|
+
next_action: {
|
|
117
|
+
kind: 'run_command',
|
|
118
|
+
message: 'Crea una rama válida de trabajo antes de seguir.',
|
|
119
|
+
command: 'git checkout -b feature/<descripcion-kebab-case>',
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
case 'GITFLOW_BRANCH_NAMING_INVALID':
|
|
123
|
+
case 'GITFLOW_BRANCH_NAMING_INVALID_CONTEXT':
|
|
124
|
+
return {
|
|
125
|
+
reason_code: 'GITFLOW_BRANCH_NAMING_INVALID_CONTEXT',
|
|
126
|
+
instruction:
|
|
127
|
+
'Renombra o recrea la rama actual con un prefijo GitFlow válido antes de continuar.',
|
|
128
|
+
next_action: {
|
|
129
|
+
kind: 'run_command',
|
|
130
|
+
message: 'Crea una rama válida y mueve el trabajo a esa rama antes de seguir.',
|
|
131
|
+
command: 'git checkout -b feature/<descripcion-kebab-case>',
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
case 'TRACKING_CANONICAL_SOURCE_CONFLICT':
|
|
135
|
+
return {
|
|
136
|
+
reason_code: 'TRACKING_CANONICAL_SOURCE_CONFLICT',
|
|
137
|
+
instruction:
|
|
138
|
+
'Alinea AGENTS.md y los README canónicos para que todos apunten al mismo MD de seguimiento.',
|
|
139
|
+
next_action: {
|
|
140
|
+
kind: 'info',
|
|
141
|
+
message:
|
|
142
|
+
'Deja una única fuente canónica de tracking y elimina referencias legacy o contradictorias.',
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
case 'TRACKING_CANONICAL_FILE_MISSING':
|
|
146
|
+
return {
|
|
147
|
+
reason_code: 'TRACKING_CANONICAL_FILE_MISSING',
|
|
148
|
+
instruction:
|
|
149
|
+
'Crea o restaura el MD de seguimiento canónico declarado por el repo antes de continuar.',
|
|
150
|
+
next_action: {
|
|
151
|
+
kind: 'info',
|
|
152
|
+
message:
|
|
153
|
+
'Restaura el archivo canónico de tracking y vuelve a validar governance.',
|
|
154
|
+
},
|
|
155
|
+
};
|
|
156
|
+
case 'TRACKING_CANONICAL_IN_PROGRESS_INVALID':
|
|
157
|
+
return {
|
|
158
|
+
reason_code: 'TRACKING_CANONICAL_IN_PROGRESS_INVALID',
|
|
159
|
+
instruction:
|
|
160
|
+
'El tracking canónico debe tener exactamente una tarea o fase en construcción.',
|
|
161
|
+
next_action: {
|
|
162
|
+
kind: 'info',
|
|
163
|
+
message:
|
|
164
|
+
'Corrige el MD canónico para dejar exactamente una `🚧` antes de continuar.',
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
case 'POLICY_STAGE_NOT_STRICT':
|
|
168
|
+
return {
|
|
169
|
+
reason_code: 'POLICY_STAGE_NOT_STRICT',
|
|
170
|
+
instruction: 'Reconcilia policy/skills en modo estricto antes de seguir.',
|
|
171
|
+
next_action: {
|
|
172
|
+
kind: 'run_command',
|
|
173
|
+
message: 'Converge policy-as-code y revalida el stage actual.',
|
|
174
|
+
command: buildGovernancePolicyReconcileCommand(stage),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
case 'SKILLS_CONTRACT_SURFACE_INCOMPLETE':
|
|
178
|
+
case 'EVIDENCE_SKILLS_CONTRACT_INCOMPLETE':
|
|
179
|
+
return {
|
|
180
|
+
reason_code: 'SKILLS_CONTRACT_SURFACE_INCOMPLETE',
|
|
181
|
+
instruction: 'Materializa el lock/sources de skills antes de dar governance efectiva.',
|
|
182
|
+
next_action: {
|
|
183
|
+
kind: 'run_command',
|
|
184
|
+
message: 'Reconcilia policy/skills y vuelve a validar governance.',
|
|
185
|
+
command: buildGovernancePolicyReconcileCommand(stage),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
case 'ADAPTER_WIRING_MISSING':
|
|
189
|
+
return {
|
|
190
|
+
reason_code: 'ADAPTER_WIRING_MISSING',
|
|
191
|
+
instruction: 'Instala el adaptador canónico si quieres wiring explícito de IDE/MCP.',
|
|
192
|
+
next_action: {
|
|
193
|
+
kind: 'run_command',
|
|
194
|
+
message: 'Genera `.pumuki/adapter.json` desde la instalación canónica.',
|
|
195
|
+
command: 'npx --yes --package pumuki@latest pumuki install --with-mcp',
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
case 'EVIDENCE_STALE':
|
|
199
|
+
case 'EVIDENCE_BRANCH_MISMATCH':
|
|
200
|
+
case 'EVIDENCE_SNAPSHOT_WARN':
|
|
201
|
+
return {
|
|
202
|
+
reason_code: params.code === 'EVIDENCE_SNAPSHOT_WARN' ? 'EVIDENCE_SNAPSHOT_WARN' : 'EVIDENCE_STALE',
|
|
203
|
+
instruction: 'Refresca evidencia y revisa hallazgos WARN antes de continuar.',
|
|
204
|
+
next_action: {
|
|
205
|
+
kind: 'run_command',
|
|
206
|
+
message: 'Revalida el stage actual y revisa findings WARN.',
|
|
207
|
+
command: validateCommand,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
case 'ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH':
|
|
211
|
+
case 'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES':
|
|
212
|
+
return {
|
|
213
|
+
reason_code: 'EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES',
|
|
214
|
+
instruction: 'Reconcilia policy/skills en modo estricto y revalida el stage actual.',
|
|
215
|
+
next_action: {
|
|
216
|
+
kind: 'run_command',
|
|
217
|
+
message: 'Converge policy-as-code y vuelve a validar cobertura de active_rule_ids para reglas activas.',
|
|
218
|
+
command: buildGovernancePolicyReconcileCommand(stage),
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
case 'EVIDENCE_PLATFORM_SKILLS_SCOPE_INCOMPLETE':
|
|
222
|
+
case 'EVIDENCE_PLATFORM_SKILLS_BUNDLES_MISSING':
|
|
223
|
+
return {
|
|
224
|
+
reason_code: params.code,
|
|
225
|
+
instruction: 'Completa cobertura de skills por plataforma y vuelve a validar.',
|
|
226
|
+
next_action: {
|
|
227
|
+
kind: 'run_command',
|
|
228
|
+
message: 'Completa bundles/prefijos de skills requeridos y revalida el stage actual.',
|
|
229
|
+
command: validateCommand,
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
case 'EVIDENCE_PLATFORM_CRITICAL_SKILLS_RULES_MISSING':
|
|
233
|
+
case 'EVIDENCE_CROSS_PLATFORM_CRITICAL_ENFORCEMENT_INCOMPLETE':
|
|
234
|
+
return {
|
|
235
|
+
reason_code: params.code,
|
|
236
|
+
instruction: 'Reconcilia policy/skills en modo estricto para enforcement crítico y revalida.',
|
|
237
|
+
next_action: {
|
|
238
|
+
kind: 'run_command',
|
|
239
|
+
message: 'Materializa reglas críticas de plataforma y vuelve a validar el stage actual.',
|
|
240
|
+
command: buildGovernancePolicyReconcileCommand(stage),
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
case 'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT':
|
|
244
|
+
case 'EVIDENCE_PREWRITE_WORKTREE_WARN':
|
|
245
|
+
return {
|
|
246
|
+
reason_code: params.code,
|
|
247
|
+
instruction: 'Particiona el worktree en slices atómicos antes de continuar.',
|
|
248
|
+
next_action: {
|
|
249
|
+
kind: 'run_command',
|
|
250
|
+
message: 'Reduce el worktree pendiente y revalida el stage actual.',
|
|
251
|
+
command: `git status --short && git add -p && ${validateCommand}`,
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
case 'SKILLS_SKILLS_FRONTEND_NO_SOLID_VIOLATIONS':
|
|
255
|
+
return {
|
|
256
|
+
reason_code: 'SKILLS_SKILLS_FRONTEND_NO_SOLID_VIOLATIONS',
|
|
257
|
+
instruction: 'Aplica refactor incremental por componente/hook y vuelve a validar.',
|
|
258
|
+
next_action: {
|
|
259
|
+
kind: 'info',
|
|
260
|
+
message: 'Aplica refactor incremental: extrae 1 componente/hook por commit y vuelve a ejecutar el gate.',
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
case 'GOVERNANCE_ATTENTION':
|
|
264
|
+
return {
|
|
265
|
+
reason_code: 'GOVERNANCE_ATTENTION',
|
|
266
|
+
instruction: 'Revisa governance truth y corrige el primer hueco del contrato antes de continuar.',
|
|
267
|
+
next_action: {
|
|
268
|
+
kind: 'info',
|
|
269
|
+
message: 'Revisa governance truth, corrige el primer gap visible y vuelve a validar.',
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
default:
|
|
273
|
+
return params.fallback ?? buildFallbackAction(params.code);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
@@ -17,6 +17,14 @@ export const REMEDIATION_HINT_BY_CODE: Readonly<Record<string, string>> = {
|
|
|
17
17
|
EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
|
|
18
18
|
'Reconcilia policy/skills y revalida PRE_WRITE: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki sdd validate --stage=PRE_WRITE --json',
|
|
19
19
|
GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
|
|
20
|
+
GITFLOW_BRANCH_NAMING_INVALID:
|
|
21
|
+
'Renombra o recrea la rama con un prefijo GitFlow válido (feature/*, bugfix/*, hotfix/*, release/*, chore/*, refactor/* o docs/*).',
|
|
22
|
+
TRACKING_CANONICAL_SOURCE_CONFLICT:
|
|
23
|
+
'Alinea AGENTS.md y los README canónicos para que todos apunten al mismo MD de seguimiento.',
|
|
24
|
+
TRACKING_CANONICAL_FILE_MISSING:
|
|
25
|
+
'Crea o restaura el archivo canónico de tracking declarado por el repo.',
|
|
26
|
+
TRACKING_CANONICAL_IN_PROGRESS_INVALID:
|
|
27
|
+
'Deja exactamente una tarea o fase `🚧` en el MD canónico de seguimiento antes de continuar.',
|
|
20
28
|
EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
|
|
21
29
|
'Reduce archivos staged/unstaged por debajo del umbral (o ajusta PUMUKI_PREWRITE_WORKTREE_*); divide el trabajo en commits más pequeños.',
|
|
22
30
|
EVIDENCE_PREWRITE_WORKTREE_WARN:
|
|
@@ -1423,7 +1423,15 @@ export async function runPlatformGate(params: {
|
|
|
1423
1423
|
|
|
1424
1424
|
if (gateOutcome === 'BLOCK') {
|
|
1425
1425
|
if (params.silent !== true) {
|
|
1426
|
-
|
|
1426
|
+
const renderStage =
|
|
1427
|
+
params.policy.stage === 'PRE_PUSH'
|
|
1428
|
+
? 'PRE_PUSH'
|
|
1429
|
+
: params.policy.stage === 'CI'
|
|
1430
|
+
? 'CI'
|
|
1431
|
+
: 'PRE_COMMIT';
|
|
1432
|
+
dependencies.printGateFindings(findingsWithWaiver, {
|
|
1433
|
+
stage: renderStage,
|
|
1434
|
+
});
|
|
1427
1435
|
}
|
|
1428
1436
|
return 1;
|
|
1429
1437
|
}
|