pumuki 6.3.70 → 6.3.71
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/docs/operations/RELEASE_NOTES.md +8 -0
- package/docs/product/CONFIGURATION.md +12 -0
- package/docs/product/USAGE.md +9 -1
- 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/filterFactsByPathPrefixes.ts +61 -0
- package/integrations/git/runPlatformGate.ts +12 -4
- 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/package.json +1 -1
|
@@ -6,6 +6,14 @@ This file keeps only the operational highlights and rollout notes that matter wh
|
|
|
6
6
|
|
|
7
7
|
## 2026-04 (CLI stability and macOS notifications)
|
|
8
8
|
|
|
9
|
+
### 2026-04-06 (v6.3.71)
|
|
10
|
+
|
|
11
|
+
- **Evidencia v2.1**: bloque `operational_hints` (`requires_second_pass`, resumen operativo, desglose por severidad de reglas). Alineado con PRE_COMMIT solo-docs + evidencia trackeada (INC-069) cuando no se re-stagea el JSON automáticamente.
|
|
12
|
+
- **Monorepo**: `PUMUKI_GATE_SCOPE_PATH_PREFIXES` acota el primer alcance de hechos por prefijos de ruta.
|
|
13
|
+
- **Paridad CI/local**: `pumuki doctor --parity` y fichero opcional `.pumuki/ci-parity-expected.json` (fallo con exit 1 si hay drift respecto al esperado).
|
|
14
|
+
- **MCP y hooks**: mismas pistas de remediación por código de violación vía catálogo compartido.
|
|
15
|
+
- **Rollout**: `pumuki@6.3.71`; repin en consumidores cuando se publique npm; validar hooks y `doctor --parity` si fijáis expectativas de CI.
|
|
16
|
+
|
|
9
17
|
### 2026-04-06 (v6.3.70)
|
|
10
18
|
|
|
11
19
|
- **Consumidores con pre-commit en pre-push**: con `.ai_evidence.json` **versionado**, `PRE_PUSH` en **ALLOW/WARN** omite persistir en disco para no ensuciar el árbol tras el gate; variable `PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE=1` si necesitas el snapshot `PRE_PUSH` en fichero trackeado (puede exigir flujo de commit explícito).
|
|
@@ -317,6 +317,18 @@ Environment variables:
|
|
|
317
317
|
- `PUMUKI_PREWRITE_WORKTREE_WARN_THRESHOLD` (default: `12`)
|
|
318
318
|
- `PUMUKI_PREWRITE_WORKTREE_BLOCK_THRESHOLD` (default: `24`)
|
|
319
319
|
|
|
320
|
+
## Alcance del gate por prefijos (monorepos)
|
|
321
|
+
|
|
322
|
+
- `PUMUKI_GATE_SCOPE_PATH_PREFIXES`: prefijos de ruta separados por **coma** o **punto y coma** (p. ej. `apps/backend,apps/web-app`). Se normalizan barras invertidas a `/`. El **primer** conjunto de hechos del alcance del stage (p. ej. staged o rango de commits) se filtra a rutas bajo esos prefijos. Hechos sin ruta de archivo reconocible (p. ej. algunas dependencias) se conservan para no romper reglas transversales.
|
|
323
|
+
|
|
324
|
+
## Paridad local vs CI (`pumuki doctor`)
|
|
325
|
+
|
|
326
|
+
- Fichero opcional **`.pumuki/ci-parity-expected.json`** (commit en el repo consumer): JSON mínimo con los campos que quieras fijar, p. ej. `pumuki_package_version`, `pre_commit_policy_hash`, `pre_commit_policy_bundle`. Con `pumuki doctor --parity` (y opcionalmente `--json`), Pumuki calcula el perfil actual y, si existe el fichero esperado, rellena `parity_comparison`; cualquier desajuste hace **exit code 1**.
|
|
327
|
+
|
|
328
|
+
## Evidencia en PRE_COMMIT con índice solo documentación
|
|
329
|
+
|
|
330
|
+
- `PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE` (`1|true|yes`): fuerza el `git add` automático de `.ai_evidence.json` después de un `PRE_COMMIT` exitoso aunque el índice solo contenga rutas `*.md` / `*.mdx` (además de la propia evidencia). Por defecto, en ese alcance Pumuki **no** ensarta la evidencia en el commit: el snapshot se refresca en disco y puedes decidir si lo incluyes (`git add -- .ai_evidence.json`). Cubre el caso de commits puramente documentales sin mezclar un diff grande de evidencia (p. ej. informes upstream tipo PUMUKI-INC-069).
|
|
331
|
+
|
|
320
332
|
## Evidencia en PRE_PUSH con `.ai_evidence.json` trackeado
|
|
321
333
|
|
|
322
334
|
- `PUMUKI_PRE_PUSH_ALWAYS_WRITE_TRACKED_EVIDENCE` (`1|true|yes`): fuerza la escritura del snapshot en `PRE_PUSH` aunque `.ai_evidence.json` esté versionado. Por defecto (sin esta variable), si el fichero está en el índice de git y el outcome no es `BLOCK`, Pumuki **no** muta el archivo para no romper hooks encadenados (p. ej. `pre-commit` ejecutado desde `pre-push`).
|
package/docs/product/USAGE.md
CHANGED
|
@@ -287,6 +287,8 @@ npx --yes pumuki install --with-mcp --agent=codex
|
|
|
287
287
|
npx --yes pumuki doctor
|
|
288
288
|
# include deterministic adapter/mcp wiring health checks
|
|
289
289
|
npx --yes pumuki doctor --deep --json
|
|
290
|
+
# parity profile vs .pumuki/ci-parity-expected.json (CI/local alignment)
|
|
291
|
+
npx --yes pumuki doctor --parity --json
|
|
290
292
|
|
|
291
293
|
# show lifecycle status
|
|
292
294
|
npx --yes pumuki status
|
|
@@ -671,7 +673,9 @@ npm run toolkit:clean-artifacts -- --dry-run
|
|
|
671
673
|
|
|
672
674
|
- Reads staged changes with `git diff --cached --name-status`.
|
|
673
675
|
- Builds facts from staged content.
|
|
676
|
+
- Opcional: `PUMUKI_GATE_SCOPE_PATH_PREFIXES` acota el primer alcance de hechos a prefijos del monorepo (ver `docs/product/CONFIGURATION.md`).
|
|
674
677
|
- Requires valid SDD/OpenSpec status (session + active change + validation).
|
|
678
|
+
- Si `.ai_evidence.json` está **versionado** y el hook refresca el snapshot en disco tras un gate **no** bloqueante, por defecto Pumuki vuelve a hacer `git add` de ese fichero **salvo** que lo único en el índice (ignorando `.ai_evidence.json` / `.AI_EVIDENCE.json`) sean rutas de documentación (`*.md`, `*.mdx`). En ese caso la evidencia se actualiza en disco pero **no** se ensarta en el commit: puedes añadirla manualmente (`git add -- .ai_evidence.json`) o activar el comportamiento anterior con `PUMUKI_PRE_COMMIT_ALWAYS_RESTAGE_TRACKED_EVIDENCE=1` (ver `docs/product/CONFIGURATION.md`). En stderr (si no va en modo silencioso) verás un aviso `[pumuki][evidence-sync]`.
|
|
675
679
|
|
|
676
680
|
### PRE_PUSH
|
|
677
681
|
|
|
@@ -711,11 +715,15 @@ Cada ejecución del gate escribe evidencia determinista en:
|
|
|
711
715
|
|
|
712
716
|
- `.ai_evidence.json`
|
|
713
717
|
|
|
714
|
-
|
|
718
|
+
Excepciones:
|
|
719
|
+
|
|
720
|
+
- En `PRE_PUSH`, si el fichero está trackeado y el outcome no es `BLOCK`, la escritura al path anterior se omite (ver sección PRE_PUSH arriba). La telemetría interna del gate sigue generándose; solo se evita mutar el árbol de trabajo.
|
|
721
|
+
- En `PRE_COMMIT`, si el fichero está trackeado y el índice es solo documentación (`*.md` / `*.mdx`), el fichero puede actualizarse en disco sin auto-`git add` (ver sección PRE_COMMIT arriba).
|
|
715
722
|
|
|
716
723
|
Schema and behavior:
|
|
717
724
|
|
|
718
725
|
- `version: "2.1"` is the source of truth
|
|
726
|
+
- `operational_hints` (opcional pero recomendado en runs recientes): `requires_second_pass` (boolean), `second_pass_reason` (string o null), `human_summary_lines` (1–4 líneas legibles), `rule_execution_breakdown` (conteos evaluated / blocking / warn / info / skipped out-of-scope). Útil para tooling y para entender el resultado sin abrir todo el snapshot.
|
|
719
727
|
- `snapshot` + `ledger`
|
|
720
728
|
- `platforms` and `rulesets` tracking
|
|
721
729
|
- `snapshot.sdd_metrics` tracks stage-level SDD enforcement metadata
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ConsolidationSuppressedFinding,
|
|
8
8
|
CompatibilityViolation,
|
|
9
9
|
EvidenceLines,
|
|
10
|
+
EvidenceOperationalHints,
|
|
10
11
|
HumanIntentState,
|
|
11
12
|
LedgerEntry,
|
|
12
13
|
PlatformState,
|
|
@@ -23,6 +24,7 @@ import { buildSnapshotPlatformSummaries } from './platformSummary';
|
|
|
23
24
|
import { resolveHumanIntent } from './humanIntent';
|
|
24
25
|
import { normalizeSnapshotEvaluationMetrics } from './evaluationMetrics';
|
|
25
26
|
import { normalizeSnapshotRulesCoverage } from './rulesCoverage';
|
|
27
|
+
import { buildEvidenceOperationalHints } from './operationalHints';
|
|
26
28
|
|
|
27
29
|
type BuildFindingInput = Finding & {
|
|
28
30
|
file?: string;
|
|
@@ -50,6 +52,9 @@ export type BuildEvidenceParams = {
|
|
|
50
52
|
};
|
|
51
53
|
sddMetrics?: SddMetrics;
|
|
52
54
|
repoState?: RepoState;
|
|
55
|
+
operationalHintsExtra?: Partial<
|
|
56
|
+
Pick<EvidenceOperationalHints, 'requires_second_pass' | 'second_pass_reason'>
|
|
57
|
+
>;
|
|
53
58
|
};
|
|
54
59
|
|
|
55
60
|
const normalizeLines = (lines?: EvidenceLines): EvidenceLines | undefined => {
|
|
@@ -766,9 +771,19 @@ export function buildEvidence(params: BuildEvidenceParams): AiEvidenceV2_1 {
|
|
|
766
771
|
previousEvidence: params.previousEvidence,
|
|
767
772
|
});
|
|
768
773
|
|
|
774
|
+
const operational_hints = buildEvidenceOperationalHints({
|
|
775
|
+
stage: params.stage,
|
|
776
|
+
outcome,
|
|
777
|
+
findings: normalizedFindings,
|
|
778
|
+
rulesCoverage: normalizedRulesCoverage,
|
|
779
|
+
evaluationMetrics: normalizedEvaluationMetrics,
|
|
780
|
+
extra: params.operationalHintsExtra,
|
|
781
|
+
});
|
|
782
|
+
|
|
769
783
|
return {
|
|
770
784
|
version: '2.1',
|
|
771
785
|
timestamp: now,
|
|
786
|
+
operational_hints,
|
|
772
787
|
snapshot: {
|
|
773
788
|
stage: params.stage,
|
|
774
789
|
audit_mode: params.auditMode ?? 'gate',
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { GateOutcome } from '../../core/gate/GateOutcome';
|
|
2
|
+
import type { GateStage } from '../../core/gate/GateStage';
|
|
3
|
+
import { resolveRemediationHintOrDefault } from '../gate/remediationCatalog';
|
|
4
|
+
import type {
|
|
5
|
+
EvidenceOperationalHints,
|
|
6
|
+
EvidenceRuleExecutionBreakdown,
|
|
7
|
+
SnapshotEvaluationMetrics,
|
|
8
|
+
SnapshotFinding,
|
|
9
|
+
SnapshotRulesCoverage,
|
|
10
|
+
} from './schema';
|
|
11
|
+
|
|
12
|
+
const truncate = (value: string, max: number): string => {
|
|
13
|
+
const t = value.trim();
|
|
14
|
+
if (t.length <= max) {
|
|
15
|
+
return t;
|
|
16
|
+
}
|
|
17
|
+
return `${t.slice(0, max - 1)}…`;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const buildRuleExecutionBreakdown = (params: {
|
|
21
|
+
findings: ReadonlyArray<SnapshotFinding>;
|
|
22
|
+
rulesCoverage?: SnapshotRulesCoverage;
|
|
23
|
+
evaluationMetrics?: SnapshotEvaluationMetrics;
|
|
24
|
+
}): EvidenceRuleExecutionBreakdown => {
|
|
25
|
+
const matched_blocking_count = params.findings.filter(
|
|
26
|
+
(f) =>
|
|
27
|
+
f.severity === 'ERROR' ||
|
|
28
|
+
f.severity === 'CRITICAL' ||
|
|
29
|
+
f.blocking === true
|
|
30
|
+
).length;
|
|
31
|
+
const matched_warn_count = params.findings.filter((f) => f.severity === 'WARN').length;
|
|
32
|
+
const matched_info_count = params.findings.filter((f) => f.severity === 'INFO').length;
|
|
33
|
+
const evaluated_count =
|
|
34
|
+
params.rulesCoverage?.counts?.evaluated ?? params.evaluationMetrics?.evaluated_rule_ids.length ?? 0;
|
|
35
|
+
const active = params.rulesCoverage?.counts?.active ?? 0;
|
|
36
|
+
const evaluatedFromCoverage = params.rulesCoverage?.counts?.evaluated;
|
|
37
|
+
const skipped_out_of_scope_count =
|
|
38
|
+
typeof params.rulesCoverage?.counts?.unevaluated === 'number'
|
|
39
|
+
? params.rulesCoverage.counts.unevaluated
|
|
40
|
+
: active > 0 && typeof evaluatedFromCoverage === 'number'
|
|
41
|
+
? Math.max(0, active - evaluatedFromCoverage)
|
|
42
|
+
: 0;
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
evaluated_count: Math.max(0, evaluated_count),
|
|
46
|
+
matched_blocking_count,
|
|
47
|
+
matched_warn_count,
|
|
48
|
+
matched_info_count,
|
|
49
|
+
skipped_out_of_scope_count: Math.max(0, skipped_out_of_scope_count),
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const buildHumanSummaryLines = (params: {
|
|
54
|
+
stage: GateStage;
|
|
55
|
+
outcome: GateOutcome;
|
|
56
|
+
findings: ReadonlyArray<SnapshotFinding>;
|
|
57
|
+
}): string[] => {
|
|
58
|
+
const lines: string[] = [`${params.stage}: outcome=${params.outcome}.`];
|
|
59
|
+
if (params.outcome === 'BLOCK') {
|
|
60
|
+
const blocking =
|
|
61
|
+
params.findings.find((f) => f.severity === 'CRITICAL' || f.severity === 'ERROR') ??
|
|
62
|
+
params.findings.find((f) => f.blocking === true) ??
|
|
63
|
+
params.findings[0];
|
|
64
|
+
if (blocking) {
|
|
65
|
+
lines.push(`${blocking.code}: ${truncate(blocking.message, 140)}`);
|
|
66
|
+
lines.push(resolveRemediationHintOrDefault(blocking.code));
|
|
67
|
+
}
|
|
68
|
+
return lines.slice(0, 3);
|
|
69
|
+
}
|
|
70
|
+
const warn = params.findings.find((f) => f.severity === 'WARN');
|
|
71
|
+
if (warn) {
|
|
72
|
+
lines.push(`WARN ${warn.code}: ${truncate(warn.message, 120)}`);
|
|
73
|
+
}
|
|
74
|
+
return lines.slice(0, 3);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const buildEvidenceOperationalHints = (params: {
|
|
78
|
+
stage: GateStage;
|
|
79
|
+
outcome: GateOutcome;
|
|
80
|
+
findings: ReadonlyArray<SnapshotFinding>;
|
|
81
|
+
rulesCoverage?: SnapshotRulesCoverage;
|
|
82
|
+
evaluationMetrics?: SnapshotEvaluationMetrics;
|
|
83
|
+
extra?: Partial<Pick<EvidenceOperationalHints, 'requires_second_pass' | 'second_pass_reason'>>;
|
|
84
|
+
}): EvidenceOperationalHints => {
|
|
85
|
+
const breakdown = buildRuleExecutionBreakdown({
|
|
86
|
+
findings: params.findings,
|
|
87
|
+
rulesCoverage: params.rulesCoverage,
|
|
88
|
+
evaluationMetrics: params.evaluationMetrics,
|
|
89
|
+
});
|
|
90
|
+
let human_summary_lines = buildHumanSummaryLines({
|
|
91
|
+
stage: params.stage,
|
|
92
|
+
outcome: params.outcome,
|
|
93
|
+
findings: params.findings,
|
|
94
|
+
});
|
|
95
|
+
if (params.extra?.requires_second_pass === true) {
|
|
96
|
+
human_summary_lines = [
|
|
97
|
+
...human_summary_lines,
|
|
98
|
+
'La evidencia trackeada se actualizó en disco pero no entró en el índice; si debe ir en este commit: git add -- .ai_evidence.json',
|
|
99
|
+
];
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
requires_second_pass: params.extra?.requires_second_pass ?? false,
|
|
103
|
+
second_pass_reason:
|
|
104
|
+
params.extra?.requires_second_pass === true
|
|
105
|
+
? (params.extra.second_pass_reason ?? null)
|
|
106
|
+
: null,
|
|
107
|
+
human_summary_lines: human_summary_lines.slice(0, 4),
|
|
108
|
+
rule_execution_breakdown: breakdown,
|
|
109
|
+
};
|
|
110
|
+
};
|
|
@@ -206,10 +206,26 @@ export type EvidenceChain = {
|
|
|
206
206
|
sequence: number;
|
|
207
207
|
};
|
|
208
208
|
|
|
209
|
+
export type EvidenceRuleExecutionBreakdown = {
|
|
210
|
+
evaluated_count: number;
|
|
211
|
+
matched_blocking_count: number;
|
|
212
|
+
matched_warn_count: number;
|
|
213
|
+
matched_info_count: number;
|
|
214
|
+
skipped_out_of_scope_count: number;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
export type EvidenceOperationalHints = {
|
|
218
|
+
requires_second_pass: boolean;
|
|
219
|
+
second_pass_reason: string | null;
|
|
220
|
+
human_summary_lines: string[];
|
|
221
|
+
rule_execution_breakdown?: EvidenceRuleExecutionBreakdown;
|
|
222
|
+
};
|
|
223
|
+
|
|
209
224
|
export type AiEvidenceV2_1 = {
|
|
210
225
|
version: '2.1';
|
|
211
226
|
timestamp: string;
|
|
212
227
|
evidence_chain?: EvidenceChain;
|
|
228
|
+
operational_hints?: EvidenceOperationalHints;
|
|
213
229
|
snapshot: Snapshot;
|
|
214
230
|
ledger: LedgerEntry[];
|
|
215
231
|
platforms: Record<string, PlatformState>;
|
|
@@ -342,6 +342,9 @@ const toStableEvidence = (
|
|
|
342
342
|
return {
|
|
343
343
|
version: '2.1',
|
|
344
344
|
timestamp: evidence.timestamp,
|
|
345
|
+
...(typeof evidence.operational_hints !== 'undefined'
|
|
346
|
+
? { operational_hints: evidence.operational_hints }
|
|
347
|
+
: {}),
|
|
345
348
|
snapshot: {
|
|
346
349
|
stage: evidence.snapshot.stage,
|
|
347
350
|
audit_mode: normalizedAuditMode,
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export const DEFAULT_GATE_REMEDIATION =
|
|
2
|
+
'Corrige la causa del bloqueo y vuelve a ejecutar el gate.';
|
|
3
|
+
|
|
4
|
+
export const REMEDIATION_HINT_BY_CODE: Readonly<Record<string, string>> = {
|
|
5
|
+
EVIDENCE_MISSING: 'Regenera .ai_evidence.json ejecutando una auditoría.',
|
|
6
|
+
EVIDENCE_INVALID: 'Corrige/regenera .ai_evidence.json y vuelve a ejecutar el gate.',
|
|
7
|
+
EVIDENCE_CHAIN_INVALID: 'Regenera evidencia para restaurar la cadena criptográfica.',
|
|
8
|
+
EVIDENCE_STAGE_SYNC_FAILED:
|
|
9
|
+
'Sincroniza la evidencia trackeada y reintenta: git add -- .ai_evidence.json && git commit --amend --no-edit',
|
|
10
|
+
EVIDENCE_STALE: 'Refresca evidencia antes de continuar.',
|
|
11
|
+
EVIDENCE_REPO_ROOT_MISMATCH: 'Regenera evidencia desde este mismo repositorio.',
|
|
12
|
+
EVIDENCE_BRANCH_MISMATCH: 'Regenera evidencia en la rama actual y reintenta.',
|
|
13
|
+
EVIDENCE_RULES_COVERAGE_MISSING: 'Ejecuta auditoría completa para recalcular rules_coverage.',
|
|
14
|
+
EVIDENCE_RULES_COVERAGE_INCOMPLETE: 'Asegura coverage_ratio=1 y unevaluated=0.',
|
|
15
|
+
ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES_HIGH:
|
|
16
|
+
'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',
|
|
17
|
+
EVIDENCE_ACTIVE_RULE_IDS_EMPTY_FOR_CODE_CHANGES:
|
|
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
|
+
GITFLOW_PROTECTED_BRANCH: 'Trabaja en feature/* y evita ramas protegidas.',
|
|
20
|
+
EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT:
|
|
21
|
+
'Reduce archivos staged/unstaged por debajo del umbral (o ajusta PUMUKI_PREWRITE_WORKTREE_*); divide el trabajo en commits más pequeños.',
|
|
22
|
+
EVIDENCE_PREWRITE_WORKTREE_WARN:
|
|
23
|
+
'El worktree supera el umbral de aviso; reduce alcance antes del siguiente commit/push.',
|
|
24
|
+
PRE_PUSH_UPSTREAM_MISSING: 'Ejecuta: git push --set-upstream origin <branch>',
|
|
25
|
+
PRE_PUSH_UPSTREAM_MISALIGNED:
|
|
26
|
+
'Alinea upstream con la rama actual: git branch --unset-upstream && git push --set-upstream origin <branch>',
|
|
27
|
+
MANIFEST_MUTATION_DETECTED:
|
|
28
|
+
'Los hooks/gates no deben modificar manifests. Revisa wiring y ejecuta upgrade explícito solo cuando aplique (por ejemplo: pumuki update --latest).',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const resolveRemediationHintForViolationCode = (code: string): string | undefined => {
|
|
32
|
+
const trimmed = code.trim();
|
|
33
|
+
if (trimmed.length === 0) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return REMEDIATION_HINT_BY_CODE[trimmed];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const resolveRemediationHintOrDefault = (code: string): string =>
|
|
40
|
+
resolveRemediationHintForViolationCode(code) ?? DEFAULT_GATE_REMEDIATION;
|
|
@@ -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,
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.71",
|
|
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": {
|