pumuki 6.3.70 → 6.3.72
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 +269 -0
- package/CHANGELOG.md +666 -0
- package/README.md +32 -0
- package/docs/README.md +7 -2
- package/docs/operations/RELEASE_NOTES.md +15 -0
- package/docs/product/CONFIGURATION.md +12 -0
- package/docs/product/USAGE.md +24 -3
- package/docs/tracking/plan-curso-pumuki-stack-my-architecture.md +111 -0
- package/integrations/evidence/buildEvidence.ts +15 -0
- package/integrations/evidence/operationalHints.ts +110 -0
- package/integrations/evidence/schema.ts +16 -0
- package/integrations/evidence/writeEvidence.ts +3 -0
- package/integrations/gate/remediationCatalog.ts +40 -0
- package/integrations/git/GitService.ts +25 -0
- package/integrations/git/filterFactsByPathPrefixes.ts +61 -0
- package/integrations/git/runPlatformGate.ts +12 -4
- package/integrations/git/runPlatformGateFacts.ts +7 -0
- package/integrations/git/stageRunners.ts +82 -28
- package/integrations/lifecycle/cli.ts +32 -3
- package/integrations/lifecycle/doctor.ts +112 -0
- package/integrations/mcp/aiGateCheck.ts +2 -11
- package/integrations/mcp/preFlightCheck.ts +2 -1
- package/integrations/sdd/openSpecCli.ts +12 -3
- package/package.json +4 -1
- package/scripts/consumer-menu-matrix-baseline-report-lib.ts +13 -38
- package/scripts/framework-menu-consumer-actions-lib.ts +28 -4
- package/scripts/framework-menu-consumer-preflight-hints.ts +5 -2
- package/scripts/framework-menu-consumer-runtime-actions.ts +86 -6
- package/scripts/framework-menu-consumer-runtime-audit.ts +36 -2
- package/scripts/framework-menu-consumer-runtime-evidence-classic.ts +140 -0
- package/scripts/framework-menu-consumer-runtime-lib.ts +2 -0
- package/scripts/framework-menu-consumer-runtime-types.ts +3 -1
- package/scripts/framework-menu-evidence-summary-lib.ts +1 -0
- package/scripts/framework-menu-evidence-summary-read.ts +57 -5
- package/scripts/framework-menu-evidence-summary-severity.ts +3 -1
- package/scripts/framework-menu-evidence-summary-types.ts +7 -0
- package/scripts/framework-menu-gate-lib.ts +9 -0
- package/scripts/framework-menu-layout-data.ts +5 -0
- package/scripts/framework-menu-matrix-baseline-lib.ts +15 -14
- package/scripts/framework-menu-matrix-canary-lib.ts +22 -1
- package/scripts/framework-menu-matrix-evidence-lib.ts +1 -0
- package/scripts/framework-menu-matrix-evidence-types.ts +13 -1
- package/scripts/framework-menu-matrix-runner-lib.ts +35 -0
- package/scripts/framework-menu-system-notifications-macos.ts +4 -0
- package/scripts/framework-menu.ts +3 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Fact } from '../../core/facts/Fact';
|
|
2
|
+
|
|
3
|
+
const normalizePath = (value: string): string => value.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
4
|
+
|
|
5
|
+
export const resolveGateScopePathPrefixesFromEnv = (): string[] => {
|
|
6
|
+
const raw = process.env.PUMUKI_GATE_SCOPE_PATH_PREFIXES?.trim();
|
|
7
|
+
if (!raw) {
|
|
8
|
+
return [];
|
|
9
|
+
}
|
|
10
|
+
return Array.from(
|
|
11
|
+
new Set(
|
|
12
|
+
raw
|
|
13
|
+
.split(/[,;]/)
|
|
14
|
+
.map((segment) => normalizePath(segment.trim()))
|
|
15
|
+
.filter((segment) => segment.length > 0)
|
|
16
|
+
)
|
|
17
|
+
).sort((a, b) => a.localeCompare(b));
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const primaryPathForFact = (fact: Fact): string | null => {
|
|
21
|
+
if (fact.kind === 'FileContent' || fact.kind === 'FileChange') {
|
|
22
|
+
return fact.path;
|
|
23
|
+
}
|
|
24
|
+
if (fact.kind === 'Heuristic') {
|
|
25
|
+
return fact.filePath ?? null;
|
|
26
|
+
}
|
|
27
|
+
if (fact.kind === 'Dependency') {
|
|
28
|
+
return fact.from;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const pathMatchesAnyPrefix = (path: string, prefixes: ReadonlyArray<string>): boolean => {
|
|
34
|
+
const normalized = normalizePath(path);
|
|
35
|
+
for (const prefix of prefixes) {
|
|
36
|
+
if (normalized === prefix) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
const withSlash = prefix.endsWith('/') ? prefix : `${prefix}/`;
|
|
40
|
+
if (normalized.startsWith(withSlash)) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const filterFactsByPathPrefixes = (
|
|
48
|
+
facts: ReadonlyArray<Fact>,
|
|
49
|
+
prefixes: ReadonlyArray<string>
|
|
50
|
+
): Fact[] => {
|
|
51
|
+
if (prefixes.length === 0) {
|
|
52
|
+
return [...facts];
|
|
53
|
+
}
|
|
54
|
+
return facts.filter((fact) => {
|
|
55
|
+
const primary = primaryPathForFact(fact);
|
|
56
|
+
if (primary === null) {
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
return pathMatchesAnyPrefix(primary, prefixes);
|
|
60
|
+
});
|
|
61
|
+
};
|
|
@@ -35,6 +35,10 @@ import type { TddBddSnapshot } from '../tdd/types';
|
|
|
35
35
|
import { resolveSkillsEnforcement } from '../policy/skillsEnforcement';
|
|
36
36
|
import { applyTddBddEnforcement } from '../policy/tddBddEnforcement';
|
|
37
37
|
import { collectAiGateRepoPolicyFindings } from './aiGateRepoPolicyFindings';
|
|
38
|
+
import {
|
|
39
|
+
filterFactsByPathPrefixes,
|
|
40
|
+
resolveGateScopePathPrefixesFromEnv,
|
|
41
|
+
} from './filterFactsByPathPrefixes';
|
|
38
42
|
|
|
39
43
|
export type OperationalMemoryShadowRecommendation = {
|
|
40
44
|
recommendedOutcome: 'ALLOW' | 'WARN' | 'BLOCK';
|
|
@@ -911,10 +915,14 @@ export async function runPlatformGate(params: {
|
|
|
911
915
|
}
|
|
912
916
|
}
|
|
913
917
|
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
+
const gateScopePathPrefixes = resolveGateScopePathPrefixesFromEnv();
|
|
919
|
+
const facts = filterFactsByPathPrefixes(
|
|
920
|
+
await dependencies.resolveFactsForGateScope({
|
|
921
|
+
scope: params.scope,
|
|
922
|
+
git,
|
|
923
|
+
}),
|
|
924
|
+
gateScopePathPrefixes
|
|
925
|
+
);
|
|
918
926
|
const stagedPaths = collectStagedPaths(git, repoRoot);
|
|
919
927
|
const factsForPlatformEvaluation = shouldAugmentStagedSkillsContractFactsWithRepoFacts({
|
|
920
928
|
scope: params.scope,
|
|
@@ -19,6 +19,10 @@ export type GateScope =
|
|
|
19
19
|
kind: 'workingTree';
|
|
20
20
|
extensions?: string[];
|
|
21
21
|
}
|
|
22
|
+
| {
|
|
23
|
+
kind: 'unstaged';
|
|
24
|
+
extensions?: string[];
|
|
25
|
+
}
|
|
22
26
|
| {
|
|
23
27
|
kind: 'range';
|
|
24
28
|
fromRef: string;
|
|
@@ -66,6 +70,9 @@ export const resolveFactsForGateScope = async (params: {
|
|
|
66
70
|
if (params.scope.kind === 'workingTree') {
|
|
67
71
|
return params.git.getStagedAndUnstagedFacts(extensions);
|
|
68
72
|
}
|
|
73
|
+
if (params.scope.kind === 'unstaged') {
|
|
74
|
+
return params.git.getUnstagedFacts(extensions);
|
|
75
|
+
}
|
|
69
76
|
|
|
70
77
|
return getFactsForCommitRange({
|
|
71
78
|
fromRef: params.scope.fromRef,
|
|
@@ -20,7 +20,9 @@ import {
|
|
|
20
20
|
} from '../notifications/emitAuditSummaryNotification';
|
|
21
21
|
import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { join } from 'node:path';
|
|
23
|
+
import { buildEvidenceOperationalHints } from '../evidence/operationalHints';
|
|
23
24
|
import { readEvidence, readEvidenceResult } from '../evidence/readEvidence';
|
|
25
|
+
import { writeEvidence } from '../evidence/writeEvidence';
|
|
24
26
|
import type { EvidenceReadResult } from '../evidence/readEvidence';
|
|
25
27
|
import type { SnapshotFinding } from '../evidence/schema';
|
|
26
28
|
import { ensureRuntimeArtifactsIgnored } from '../lifecycle/artifacts';
|
|
@@ -31,6 +33,10 @@ import {
|
|
|
31
33
|
resolveGitAtomicityEnforcement,
|
|
32
34
|
type GitAtomicityEnforcementResolution,
|
|
33
35
|
} from '../policy/gitAtomicityEnforcement';
|
|
36
|
+
import {
|
|
37
|
+
DEFAULT_GATE_REMEDIATION as DEFAULT_BLOCKED_REMEDIATION,
|
|
38
|
+
REMEDIATION_HINT_BY_CODE as BLOCKED_REMEDIATION_BY_CODE,
|
|
39
|
+
} from '../gate/remediationCatalog';
|
|
34
40
|
|
|
35
41
|
const PRE_PUSH_UPSTREAM_REQUIRED_MESSAGE =
|
|
36
42
|
'pumuki pre-push blocked: branch has no upstream tracking reference. Configure upstream first (for example: git push --set-upstream origin <branch>) and retry.';
|
|
@@ -40,39 +46,43 @@ const PRE_PUSH_MANUAL_FALLBACK_MESSAGE =
|
|
|
40
46
|
'[pumuki][pre-push] branch has no upstream and stdin is empty; using working-tree fallback scope.';
|
|
41
47
|
const PRE_PUSH_UPSTREAM_MISALIGNED_AHEAD_THRESHOLD = 5;
|
|
42
48
|
|
|
49
|
+
const isTruthyEnvFlag = (value?: string): boolean => {
|
|
50
|
+
if (!value) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
const normalized = value.trim().toLowerCase();
|
|
54
|
+
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isDocumentationOnlyStagedPath = (relativePath: string): boolean => {
|
|
58
|
+
const normalized = relativePath.replace(/\\/g, '/').trim();
|
|
59
|
+
if (normalized.length === 0) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return /\.(md|mdx)$/i.test(normalized);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const shouldSkipRestagingTrackedEvidenceForDocumentationOnlyScope = (params: {
|
|
66
|
+
listStagedIndexPaths: (repoRoot: string) => ReadonlyArray<string>;
|
|
67
|
+
repoRoot: string;
|
|
68
|
+
}): boolean => {
|
|
69
|
+
if (isTruthyEnvFlag(process.env.PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
const paths = params.listStagedIndexPaths(params.repoRoot).filter(
|
|
73
|
+
(p) => p !== '.ai_evidence.json' && p !== '.AI_EVIDENCE.json'
|
|
74
|
+
);
|
|
75
|
+
if (paths.length === 0) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
return paths.every(isDocumentationOnlyStagedPath);
|
|
79
|
+
};
|
|
80
|
+
|
|
43
81
|
const PRE_COMMIT_EVIDENCE_MAX_AGE_SECONDS = 900;
|
|
44
82
|
const PRE_PUSH_EVIDENCE_MAX_AGE_SECONDS = 1800;
|
|
45
83
|
const HOOK_GATE_PROGRESS_REMINDER_MS = 2000;
|
|
46
|
-
const DEFAULT_BLOCKED_REMEDIATION = 'Corrige la causa del bloqueo y vuelve a ejecutar el gate.';
|
|
47
84
|
const EVIDENCE_FILE_PATH = '.ai_evidence.json';
|
|
48
85
|
|
|
49
|
-
const BLOCKED_REMEDIATION_BY_CODE: Readonly<Record<string, string>> = {
|
|
50
|
-
EVIDENCE_MISSING: 'Regenera .ai_evidence.json ejecutando una auditoría.',
|
|
51
|
-
EVIDENCE_INVALID: 'Corrige/regenera .ai_evidence.json y vuelve a ejecutar el gate.',
|
|
52
|
-
EVIDENCE_CHAIN_INVALID: 'Regenera evidencia para restaurar la cadena criptográfica.',
|
|
53
|
-
EVIDENCE_STAGE_SYNC_FAILED:
|
|
54
|
-
'Sincroniza la evidencia trackeada y reintenta: git add -- .ai_evidence.json && git commit --amend --no-edit',
|
|
55
|
-
EVIDENCE_STALE: 'Refresca evidencia antes de continuar.',
|
|
56
|
-
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este mismo repositorio.',
|
|
57
|
-
EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual y reintenta.',
|
|
58
|
-
EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
|
|
59
|
-
EVIDENCE_RULES_COVERAGE_INCOMPLETE: 'Asegura coverage_ratio=1 y unevaluated=0.',
|
|
60
|
-
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
61
|
-
'Reconcilia policy/skills y reintenta PRE_COMMIT: npx --yes --package pumuki@latest pumuki policy reconcile --strict --json && npx --yes --package pumuki@latest pumuki-pre-commit',
|
|
62
|
-
EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
|
|
63
|
-
'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',
|
|
64
|
-
GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
|
|
65
|
-
EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
|
|
66
|
-
'Reduce archivos staged/unstaged por debajo del umbral (o ajusta PUMUKI_PREWRITE_WORKTREE_*); divide el trabajo en commits más pequeños.',
|
|
67
|
-
EVIDENCE_PREWRITE_WORKTREE_WARN:
|
|
68
|
-
'El worktree supera el umbral de aviso; reduce alcance antes del siguiente commit/push.',
|
|
69
|
-
PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta: git push --set-upstream origin <branch>',
|
|
70
|
-
PRE_PUSH_UPSTREAM_MISALIGNED:
|
|
71
|
-
'Alinea upstream con la rama actual: git branch --unset-upstream && git push --set-upstream origin <branch>',
|
|
72
|
-
MANIFEST_MUTATION_DETECTED:
|
|
73
|
-
'Los hooks/gates no deben modificar manifests. Revisa wiring y ejecuta upgrade explícito solo cuando aplique (por ejemplo: pumuki update --latest).',
|
|
74
|
-
};
|
|
75
|
-
|
|
76
86
|
const HOOK_POLICY_RECONCILE_CODES = new Set<string>([
|
|
77
87
|
'SKILLS_PLATFORM_COVERAGE_INCOMPLETE_HIGH',
|
|
78
88
|
'SKILLS_SCOPE_COMPLIANCE_INCOMPLETE_HIGH',
|
|
@@ -123,6 +133,7 @@ type StageRunnerDependencies = {
|
|
|
123
133
|
ensureRuntimeArtifactsIgnored: (repoRoot: string) => void;
|
|
124
134
|
runPolicyReconcile: typeof runPolicyReconcile;
|
|
125
135
|
isPathTracked: (repoRoot: string, relativePath: string) => boolean;
|
|
136
|
+
listStagedIndexPaths: (repoRoot: string) => ReadonlyArray<string>;
|
|
126
137
|
stagePath: (repoRoot: string, relativePath: string) => void;
|
|
127
138
|
resolveHeadOid: (repoRoot: string) => string | null;
|
|
128
139
|
resolveGitAtomicityEnforcement: () => GitAtomicityEnforcementResolution;
|
|
@@ -202,6 +213,13 @@ const defaultDependencies: StageRunnerDependencies = {
|
|
|
202
213
|
return false;
|
|
203
214
|
}
|
|
204
215
|
},
|
|
216
|
+
listStagedIndexPaths: (repoRoot) => {
|
|
217
|
+
const raw = new GitService().runGit(['diff', '--cached', '--name-only'], repoRoot);
|
|
218
|
+
return raw
|
|
219
|
+
.split('\n')
|
|
220
|
+
.map((line) => line.trim())
|
|
221
|
+
.filter((line) => line.length > 0);
|
|
222
|
+
},
|
|
205
223
|
stagePath: (repoRoot, relativePath) => {
|
|
206
224
|
new GitService().runGit(['add', '--', relativePath], repoRoot);
|
|
207
225
|
},
|
|
@@ -489,6 +507,26 @@ const runHookGateWithPolicyRetry = async (params: {
|
|
|
489
507
|
}
|
|
490
508
|
};
|
|
491
509
|
|
|
510
|
+
const patchOperationalHintsAfterDocumentationOnlyEvidenceSync = (repoRoot: string): void => {
|
|
511
|
+
const evidenceRead = readEvidenceResult(repoRoot);
|
|
512
|
+
if (evidenceRead.kind !== 'valid') {
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
const evidence = evidenceRead.evidence;
|
|
516
|
+
const hints = buildEvidenceOperationalHints({
|
|
517
|
+
stage: evidence.snapshot.stage,
|
|
518
|
+
outcome: evidence.snapshot.outcome,
|
|
519
|
+
findings: evidence.snapshot.findings,
|
|
520
|
+
rulesCoverage: evidence.snapshot.rules_coverage,
|
|
521
|
+
evaluationMetrics: evidence.snapshot.evaluation_metrics,
|
|
522
|
+
extra: {
|
|
523
|
+
requires_second_pass: true,
|
|
524
|
+
second_pass_reason: 'tracked_evidence_refreshed_on_disk_not_staged_documentation_only_commit',
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
writeEvidence({ ...evidence, operational_hints: hints }, { repoRoot });
|
|
528
|
+
};
|
|
529
|
+
|
|
492
530
|
const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
|
|
493
531
|
dependencies: StageRunnerDependencies;
|
|
494
532
|
repoRoot: string;
|
|
@@ -500,6 +538,22 @@ const syncTrackedEvidenceAfterSuccessfulPreCommit = (params: {
|
|
|
500
538
|
if (!params.dependencies.isPathTracked(params.repoRoot, EVIDENCE_FILE_PATH)) {
|
|
501
539
|
return false;
|
|
502
540
|
}
|
|
541
|
+
if (
|
|
542
|
+
shouldSkipRestagingTrackedEvidenceForDocumentationOnlyScope({
|
|
543
|
+
repoRoot: params.repoRoot,
|
|
544
|
+
listStagedIndexPaths: params.dependencies.listStagedIndexPaths,
|
|
545
|
+
})
|
|
546
|
+
) {
|
|
547
|
+
if (!params.dependencies.isQuietMode()) {
|
|
548
|
+
process.stderr.write(
|
|
549
|
+
`[pumuki][evidence-sync] tracked ${EVIDENCE_FILE_PATH} updated on disk but not auto-staged (documentation-only staged paths: *.md / *.mdx). ` +
|
|
550
|
+
`Include in this commit if needed: git add -- ${EVIDENCE_FILE_PATH}. ` +
|
|
551
|
+
`Force previous behavior: PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1\n`
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
patchOperationalHintsAfterDocumentationOnlyEvidenceSync(params.repoRoot);
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
503
557
|
try {
|
|
504
558
|
params.dependencies.stagePath(params.repoRoot, EVIDENCE_FILE_PATH);
|
|
505
559
|
return false;
|
|
@@ -5,6 +5,7 @@ import { runPlatformGate } from '../git/runPlatformGate';
|
|
|
5
5
|
import { collectWorktreeAtomicSlices } from '../git/worktreeAtomicSlices';
|
|
6
6
|
import {
|
|
7
7
|
doctorHasBlockingIssues,
|
|
8
|
+
doctorHasParityMismatch,
|
|
8
9
|
runLifecycleDoctor,
|
|
9
10
|
type LifecycleDoctorReport,
|
|
10
11
|
} from './doctor';
|
|
@@ -115,6 +116,7 @@ export type ParsedArgs = {
|
|
|
115
116
|
installMcpAgent?: AdapterAgent;
|
|
116
117
|
remoteChecks?: boolean;
|
|
117
118
|
doctorDeep?: boolean;
|
|
119
|
+
doctorParity?: boolean;
|
|
118
120
|
sddCommand?: SddCommand;
|
|
119
121
|
loopCommand?: LoopCommand;
|
|
120
122
|
loopSessionId?: string;
|
|
@@ -180,7 +182,7 @@ Pumuki lifecycle commands:
|
|
|
180
182
|
pumuki uninstall [--purge-artifacts]
|
|
181
183
|
pumuki remove
|
|
182
184
|
pumuki update [--latest|--spec=<package-spec>]
|
|
183
|
-
pumuki doctor [--remote-checks] [--deep] [--json]
|
|
185
|
+
pumuki doctor [--remote-checks] [--deep] [--parity] [--json]
|
|
184
186
|
pumuki status [--json] [--remote-checks]
|
|
185
187
|
pumuki watch [--stage=PRE_COMMIT|PRE_PUSH|CI] [--scope=workingTree|staged|repoAndStaged|repo] [--severity=critical|high|medium|low] [--interval-ms=<n>] [--notify-cooldown-ms=<n>] [--no-notify] [--once|--iterations=<n>] [--json]
|
|
186
188
|
pumuki loop run --objective=<text> [--max-attempts=<n>] [--json]
|
|
@@ -567,6 +569,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
567
569
|
let installMcpAgent: ParsedArgs['installMcpAgent'];
|
|
568
570
|
let remoteChecks = false;
|
|
569
571
|
let doctorDeep = false;
|
|
572
|
+
let doctorParity = false;
|
|
570
573
|
let watchStage: ParsedArgs['watchStage'];
|
|
571
574
|
let watchScope: ParsedArgs['watchScope'];
|
|
572
575
|
let watchIntervalMs: ParsedArgs['watchIntervalMs'];
|
|
@@ -1395,6 +1398,10 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
1395
1398
|
doctorDeep = true;
|
|
1396
1399
|
continue;
|
|
1397
1400
|
}
|
|
1401
|
+
if (arg === '--parity') {
|
|
1402
|
+
doctorParity = true;
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1398
1405
|
if (arg === '--purge-artifacts') {
|
|
1399
1406
|
purgeArtifacts = true;
|
|
1400
1407
|
continue;
|
|
@@ -1417,6 +1424,9 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
1417
1424
|
if (doctorDeep && commandRaw !== 'doctor') {
|
|
1418
1425
|
throw new Error(`--deep is only supported with "pumuki doctor".\n\n${HELP_TEXT}`);
|
|
1419
1426
|
}
|
|
1427
|
+
if (doctorParity && commandRaw !== 'doctor') {
|
|
1428
|
+
throw new Error(`--parity is only supported with "pumuki doctor".\n\n${HELP_TEXT}`);
|
|
1429
|
+
}
|
|
1420
1430
|
if (commandRaw !== 'bootstrap' && bootstrapEnterprise) {
|
|
1421
1431
|
throw new Error(`--enterprise is only supported with "pumuki bootstrap".\n\n${HELP_TEXT}`);
|
|
1422
1432
|
}
|
|
@@ -1444,6 +1454,7 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
|
|
|
1444
1454
|
...(installMcpAgent ? { installMcpAgent } : {}),
|
|
1445
1455
|
...(remoteChecks ? { remoteChecks: true } : {}),
|
|
1446
1456
|
...(doctorDeep ? { doctorDeep: true } : {}),
|
|
1457
|
+
...(doctorParity ? { doctorParity: true } : {}),
|
|
1447
1458
|
};
|
|
1448
1459
|
};
|
|
1449
1460
|
|
|
@@ -1516,6 +1527,22 @@ const printDoctorReport = (
|
|
|
1516
1527
|
writeInfo(`[pumuki] ${issue.severity.toUpperCase()}: ${issue.message}`);
|
|
1517
1528
|
}
|
|
1518
1529
|
|
|
1530
|
+
if (report.parity_profile) {
|
|
1531
|
+
writeInfo(
|
|
1532
|
+
`[pumuki][doctor][parity] pumuki=${report.parity_profile.pumuki_package_version} bundle=${report.parity_profile.pre_commit_policy_bundle} hash=${report.parity_profile.pre_commit_policy_hash}`
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
if (report.parity_comparison) {
|
|
1536
|
+
writeInfo(
|
|
1537
|
+
`[pumuki][doctor][parity] expected_file=${report.parity_comparison.expected_path} matches=${report.parity_comparison.matches ? 'yes' : 'no'}`
|
|
1538
|
+
);
|
|
1539
|
+
for (const mismatch of report.parity_comparison.mismatches) {
|
|
1540
|
+
writeInfo(
|
|
1541
|
+
`[pumuki][doctor][parity] mismatch ${mismatch.field}: expected=${mismatch.expected} actual=${mismatch.actual}`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1519
1546
|
if (report.deep?.enabled) {
|
|
1520
1547
|
for (const check of report.deep.checks) {
|
|
1521
1548
|
writeInfo(
|
|
@@ -1527,7 +1554,8 @@ const printDoctorReport = (
|
|
|
1527
1554
|
}
|
|
1528
1555
|
}
|
|
1529
1556
|
|
|
1530
|
-
const hasBlocking =
|
|
1557
|
+
const hasBlocking =
|
|
1558
|
+
doctorHasBlockingIssues(report) || doctorHasParityMismatch(report);
|
|
1531
1559
|
const hasWarnings =
|
|
1532
1560
|
report.issues.length > 0 ||
|
|
1533
1561
|
report.deep?.checks.some((check) => check.status !== 'pass') === true;
|
|
@@ -2225,6 +2253,7 @@ export const runLifecycleCli = async (
|
|
|
2225
2253
|
case 'doctor': {
|
|
2226
2254
|
const report = runLifecycleDoctor({
|
|
2227
2255
|
deep: parsed.doctorDeep === true,
|
|
2256
|
+
parity: parsed.doctorParity === true,
|
|
2228
2257
|
});
|
|
2229
2258
|
const remoteCiDiagnostics = parsed.remoteChecks
|
|
2230
2259
|
? activeDependencies.collectRemoteCiDiagnostics({
|
|
@@ -2247,7 +2276,7 @@ export const runLifecycleCli = async (
|
|
|
2247
2276
|
} else {
|
|
2248
2277
|
printDoctorReport(report, remoteCiDiagnostics);
|
|
2249
2278
|
}
|
|
2250
|
-
return doctorHasBlockingIssues(report) ? 1 : 0;
|
|
2279
|
+
return doctorHasBlockingIssues(report) || doctorHasParityMismatch(report) ? 1 : 0;
|
|
2251
2280
|
}
|
|
2252
2281
|
case 'status': {
|
|
2253
2282
|
const status = readLifecycleStatus();
|
|
@@ -69,6 +69,22 @@ export type DoctorCompatibilityContract = {
|
|
|
69
69
|
};
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
export type DoctorParityProfile = {
|
|
73
|
+
schema_version: '1';
|
|
74
|
+
pumuki_package_version: string;
|
|
75
|
+
pre_commit_policy_bundle: string;
|
|
76
|
+
pre_commit_policy_hash: string;
|
|
77
|
+
pre_commit_policy_signature: string | null;
|
|
78
|
+
pre_commit_policy_version: string | null;
|
|
79
|
+
skills_policy_present: boolean;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
export type DoctorParityComparison = {
|
|
83
|
+
expected_path: string;
|
|
84
|
+
matches: boolean;
|
|
85
|
+
mismatches: ReadonlyArray<{ field: string; expected: string; actual: string }>;
|
|
86
|
+
};
|
|
87
|
+
|
|
72
88
|
export type LifecycleDoctorReport = {
|
|
73
89
|
repoRoot: string;
|
|
74
90
|
packageVersion: string;
|
|
@@ -81,6 +97,8 @@ export type LifecycleDoctorReport = {
|
|
|
81
97
|
policyValidation: LifecyclePolicyValidationSnapshot;
|
|
82
98
|
issues: ReadonlyArray<DoctorIssue>;
|
|
83
99
|
deep?: DoctorDeepReport;
|
|
100
|
+
parity_profile?: DoctorParityProfile;
|
|
101
|
+
parity_comparison?: DoctorParityComparison;
|
|
84
102
|
};
|
|
85
103
|
|
|
86
104
|
const buildDoctorIssues = (params: {
|
|
@@ -698,10 +716,87 @@ const buildDoctorDeepReport = (params: {
|
|
|
698
716
|
};
|
|
699
717
|
};
|
|
700
718
|
|
|
719
|
+
const buildDoctorParityProfile = (params: {
|
|
720
|
+
repoRoot: string;
|
|
721
|
+
packageVersion: string;
|
|
722
|
+
}): DoctorParityProfile => {
|
|
723
|
+
const policy = resolvePolicyForStage('PRE_COMMIT', params.repoRoot);
|
|
724
|
+
const skillsPolicyPath = join(params.repoRoot, 'skills.policy.json');
|
|
725
|
+
return {
|
|
726
|
+
schema_version: '1',
|
|
727
|
+
pumuki_package_version: params.packageVersion,
|
|
728
|
+
pre_commit_policy_bundle: policy.trace.bundle,
|
|
729
|
+
pre_commit_policy_hash: policy.trace.hash,
|
|
730
|
+
pre_commit_policy_signature: policy.trace.signature ?? null,
|
|
731
|
+
pre_commit_policy_version: policy.trace.version ?? null,
|
|
732
|
+
skills_policy_present: existsSync(skillsPolicyPath),
|
|
733
|
+
};
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
const compareDoctorParityProfile = (params: {
|
|
737
|
+
repoRoot: string;
|
|
738
|
+
actual: DoctorParityProfile;
|
|
739
|
+
}): DoctorParityComparison | undefined => {
|
|
740
|
+
const expectedPath = join(params.repoRoot, '.pumuki', 'ci-parity-expected.json');
|
|
741
|
+
if (!existsSync(expectedPath)) {
|
|
742
|
+
return undefined;
|
|
743
|
+
}
|
|
744
|
+
let raw: unknown;
|
|
745
|
+
try {
|
|
746
|
+
raw = JSON.parse(readFileSync(expectedPath, 'utf8')) as unknown;
|
|
747
|
+
} catch {
|
|
748
|
+
return {
|
|
749
|
+
expected_path: expectedPath,
|
|
750
|
+
matches: false,
|
|
751
|
+
mismatches: [
|
|
752
|
+
{
|
|
753
|
+
field: 'ci-parity-expected.json',
|
|
754
|
+
expected: 'valid-json',
|
|
755
|
+
actual: 'parse-error',
|
|
756
|
+
},
|
|
757
|
+
],
|
|
758
|
+
};
|
|
759
|
+
}
|
|
760
|
+
if (!isRecord(raw)) {
|
|
761
|
+
return {
|
|
762
|
+
expected_path: expectedPath,
|
|
763
|
+
matches: false,
|
|
764
|
+
mismatches: [{ field: 'root', expected: 'object', actual: 'non-object' }],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const mismatches: Array<{ field: string; expected: string; actual: string }> = [];
|
|
768
|
+
const expectField = (field: string, expected: unknown, actual: string) => {
|
|
769
|
+
if (typeof expected === 'string' && expected.trim().length > 0 && expected !== actual) {
|
|
770
|
+
mismatches.push({ field, expected, actual });
|
|
771
|
+
}
|
|
772
|
+
};
|
|
773
|
+
expectField(
|
|
774
|
+
'pumuki_package_version',
|
|
775
|
+
raw.pumuki_package_version,
|
|
776
|
+
params.actual.pumuki_package_version
|
|
777
|
+
);
|
|
778
|
+
expectField(
|
|
779
|
+
'pre_commit_policy_hash',
|
|
780
|
+
raw.pre_commit_policy_hash,
|
|
781
|
+
params.actual.pre_commit_policy_hash
|
|
782
|
+
);
|
|
783
|
+
expectField(
|
|
784
|
+
'pre_commit_policy_bundle',
|
|
785
|
+
raw.pre_commit_policy_bundle,
|
|
786
|
+
params.actual.pre_commit_policy_bundle
|
|
787
|
+
);
|
|
788
|
+
return {
|
|
789
|
+
expected_path: expectedPath,
|
|
790
|
+
matches: mismatches.length === 0,
|
|
791
|
+
mismatches,
|
|
792
|
+
};
|
|
793
|
+
};
|
|
794
|
+
|
|
701
795
|
export const runLifecycleDoctor = (params?: {
|
|
702
796
|
cwd?: string;
|
|
703
797
|
git?: ILifecycleGitService;
|
|
704
798
|
deep?: boolean;
|
|
799
|
+
parity?: boolean;
|
|
705
800
|
}): LifecycleDoctorReport => {
|
|
706
801
|
const git = params?.git ?? new LifecycleGitService();
|
|
707
802
|
const cwd = params?.cwd ?? process.cwd();
|
|
@@ -730,6 +825,18 @@ export const runLifecycleDoctor = (params?: {
|
|
|
730
825
|
lifecycleVersion: lifecycleState.version,
|
|
731
826
|
});
|
|
732
827
|
|
|
828
|
+
const parity_profile =
|
|
829
|
+
params?.parity === true
|
|
830
|
+
? buildDoctorParityProfile({
|
|
831
|
+
repoRoot,
|
|
832
|
+
packageVersion: version.effective,
|
|
833
|
+
})
|
|
834
|
+
: undefined;
|
|
835
|
+
const parity_comparison =
|
|
836
|
+
typeof parity_profile !== 'undefined'
|
|
837
|
+
? compareDoctorParityProfile({ repoRoot, actual: parity_profile })
|
|
838
|
+
: undefined;
|
|
839
|
+
|
|
733
840
|
return {
|
|
734
841
|
repoRoot,
|
|
735
842
|
packageVersion: version.effective,
|
|
@@ -742,8 +849,13 @@ export const runLifecycleDoctor = (params?: {
|
|
|
742
849
|
policyValidation: readLifecyclePolicyValidationSnapshot(repoRoot),
|
|
743
850
|
issues,
|
|
744
851
|
deep,
|
|
852
|
+
parity_profile,
|
|
853
|
+
parity_comparison,
|
|
745
854
|
};
|
|
746
855
|
};
|
|
747
856
|
|
|
748
857
|
export const doctorHasBlockingIssues = (report: LifecycleDoctorReport): boolean =>
|
|
749
858
|
report.issues.some((issue) => issue.severity === 'error') || report.deep?.blocking === true;
|
|
859
|
+
|
|
860
|
+
export const doctorHasParityMismatch = (report: LifecycleDoctorReport): boolean =>
|
|
861
|
+
typeof report.parity_comparison !== 'undefined' && report.parity_comparison.matches === false;
|
|
@@ -1,17 +1,8 @@
|
|
|
1
1
|
import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
|
|
2
|
+
import { resolveRemediationHintForViolationCode } from '../gate/remediationCatalog';
|
|
2
3
|
import { resolveLearningContextExperimentalFeature } from '../policy/experimentalFeatures';
|
|
3
4
|
import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
|
|
4
5
|
|
|
5
|
-
const AUTO_FIX_BY_CODE: Readonly<Record<string, string>> = {
|
|
6
|
-
EVIDENCE_MISSING: 'Ejecuta una auditoría para generar .ai_evidence.json.',
|
|
7
|
-
EVIDENCE_INVALID: 'Regenera .ai_evidence.json y vuelve a evaluar.',
|
|
8
|
-
EVIDENCE_STALE: 'Refresca evidencia antes de continuar.',
|
|
9
|
-
EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual.',
|
|
10
|
-
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este repositorio.',
|
|
11
|
-
PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta git push --set-upstream origin <branch>.',
|
|
12
|
-
GITFLOW_PROTECTED_BRANCH: 'Crea una rama feature/* y mueve el trabajo allí.',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
6
|
const PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
|
|
16
7
|
|
|
17
8
|
export type EnterpriseAiGateCheckResult = {
|
|
@@ -112,7 +103,7 @@ const buildAutoFixes = (
|
|
|
112
103
|
if (emittedCodes.has(violation.code)) {
|
|
113
104
|
continue;
|
|
114
105
|
}
|
|
115
|
-
const fix =
|
|
106
|
+
const fix = resolveRemediationHintForViolationCode(violation.code);
|
|
116
107
|
if (!fix) {
|
|
117
108
|
continue;
|
|
118
109
|
}
|
|
@@ -4,7 +4,8 @@ import { resolveLearningContextExperimentalFeature } from '../policy/experimenta
|
|
|
4
4
|
import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
|
|
5
5
|
|
|
6
6
|
const ACTIONABLE_HINTS_BY_CODE: Readonly<Record<string, string>> = {
|
|
7
|
-
EVIDENCE_MISSING:
|
|
7
|
+
EVIDENCE_MISSING:
|
|
8
|
+
'Ejecuta una auditoría (1/2/3/4 u opciones de motor 11–14) para regenerar .ai_evidence.json.',
|
|
8
9
|
EVIDENCE_INVALID: 'Regenera .ai_evidence.json desde una opción de auditoría.',
|
|
9
10
|
EVIDENCE_INTEGRITY_MISSING: 'Refresca evidencia para regenerar metadatos de integridad.',
|
|
10
11
|
EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
|
|
@@ -17,20 +17,29 @@ type OpenSpecCommandResult = {
|
|
|
17
17
|
stderr: string;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
const resolveOpenSpecBinary = (repoRoot: string): string => {
|
|
20
|
+
const resolveOpenSpecBinary = (repoRoot: string): string | undefined => {
|
|
21
21
|
const binaryName = process.platform === 'win32' ? 'openspec.cmd' : 'openspec';
|
|
22
22
|
const localBinaryPath = join(repoRoot, 'node_modules', '.bin', binaryName);
|
|
23
23
|
if (existsSync(localBinaryPath)) {
|
|
24
24
|
return localBinaryPath;
|
|
25
25
|
}
|
|
26
|
-
return
|
|
26
|
+
return undefined;
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
const runOpenSpecCommand = (
|
|
30
30
|
args: ReadonlyArray<string>,
|
|
31
31
|
cwd: string
|
|
32
32
|
): OpenSpecCommandResult => {
|
|
33
|
-
const
|
|
33
|
+
const binary = resolveOpenSpecBinary(cwd);
|
|
34
|
+
if (!binary) {
|
|
35
|
+
return {
|
|
36
|
+
exitCode: 127,
|
|
37
|
+
stdout: '',
|
|
38
|
+
stderr:
|
|
39
|
+
'OpenSpec CLI not found under repo node_modules/.bin (add @fission-ai/openspec to this repository).',
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
const result = runSpawnSync(binary, [...args], {
|
|
34
43
|
cwd,
|
|
35
44
|
encoding: 'utf8',
|
|
36
45
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.72",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -259,10 +259,13 @@
|
|
|
259
259
|
"docs/product/*.md",
|
|
260
260
|
"docs/rule-packs/*.md",
|
|
261
261
|
"docs/validation/*.md",
|
|
262
|
+
"docs/tracking/plan-curso-pumuki-stack-my-architecture.md",
|
|
262
263
|
"assets/**/*",
|
|
263
264
|
"vendor/skills/**/*",
|
|
264
265
|
"index.js",
|
|
265
266
|
"README.md",
|
|
267
|
+
"AGENTS.md",
|
|
268
|
+
"CHANGELOG.md",
|
|
266
269
|
"LICENSE",
|
|
267
270
|
"VERSION",
|
|
268
271
|
"tsconfig.json",
|