pumuki 6.3.101 → 6.3.103
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/CHANGELOG.md +16 -0
- package/VERSION +1 -1
- package/docs/operations/RELEASE_NOTES.md +13 -0
- package/integrations/gate/stagePolicies.ts +43 -6
- package/integrations/git/aiGateRepoPolicyFindings.ts +81 -1
- package/integrations/git/runPlatformGateOutput.ts +22 -2
- package/integrations/lifecycle/hookBlock.ts +2 -6
- package/integrations/lifecycle/policyReconcile.ts +6 -0
- package/integrations/policy/policyAsCode.ts +43 -6
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,22 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [6.3.103] - 2026-04-22
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **Diagnóstico accionable del tracking canónico en consumers:** `status`, `doctor` y el gate repo-policy ya incluyen `TRACKING_CANONICAL_IN_PROGRESS_INVALID` junto con referencias a entradas activas y al board canónico del repo consumidor cuando existe.
|
|
14
|
+
- **Separación explícita entre blocker y warning secundario:** la salida de `PRE_WRITE` conserva un `block-summary` primario y añade `warning-summary` para warnings de higiene (`EVIDENCE_PREWRITE_WORKTREE_WARN`) cuando conviven con un bloqueo duro de tracking.
|
|
15
|
+
- **Cobertura de regresión del hotfix:** nuevas pruebas fijan el parsing de boards tabulares `🚧 reported activo` y la impresión del resumen jerárquico con warning secundario.
|
|
16
|
+
|
|
17
|
+
## [6.3.102] - 2026-04-22
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **`strict` efectivo alineado con el contrato firmado:** `policyAsCode` y `stagePolicies` dejan de publicar `validation.strict` desde el entorno cuando el contrato persistido ya declara el valor por stage.
|
|
22
|
+
- **`policy reconcile --strict --apply` materializa el contrato completo:** el archivo `.pumuki/policy-as-code.json` pasa a persistir el mapa `strict` por stage para que `status`, `doctor` y runtime converjan sobre la misma fuente.
|
|
23
|
+
- **Wiring robusto de `pre-push` con hooks previos terminados en `exec`:** el bloque gestionado de Pumuki se recoloca antes del `exec` también en `pre-push`, evitando que el enforcement quede detrás de código muerto.
|
|
24
|
+
|
|
9
25
|
## [6.3.101] - 2026-04-22
|
|
10
26
|
|
|
11
27
|
### Fixed
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
v6.3.
|
|
1
|
+
v6.3.103
|
|
@@ -6,6 +6,19 @@ 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-22 (v6.3.103)
|
|
10
|
+
|
|
11
|
+
- **Tracking canónico accionable:** `status`, `doctor` y el gate repo-policy enriquecen `TRACKING_CANONICAL_IN_PROGRESS_INVALID` con referencias a las entradas activas detectadas en el board del consumer.
|
|
12
|
+
- **PRE_WRITE menos ambiguo:** cuando un warning de higiene de worktree convive con un bloqueo duro, el runtime imprime `warning-summary` separado del `block-summary`.
|
|
13
|
+
- **Rollout recomendado:** publicar `pumuki@6.3.103`, repin inmediato en `RuralGo` y revalidar `status` / `doctor` / `git commit` sobre un board con varias filas `🚧 reported activo`.
|
|
14
|
+
|
|
15
|
+
### 2026-04-22 (v6.3.102)
|
|
16
|
+
|
|
17
|
+
- **Convergencia de policy efectiva:** `strict` deja de depender solo de `PUMUKI_POLICY_STRICT` cuando el contrato firmado ya lo declara por stage; `status`, `doctor` y runtime vuelven a hablar el mismo idioma.
|
|
18
|
+
- **Autofix persistente de contrato:** `policy reconcile --strict --apply` escribe el mapa `strict` completo en `.pumuki/policy-as-code.json`, cerrando la deriva entre reconcile y lectura posterior.
|
|
19
|
+
- **Wiring fiable en `pre-push`:** el hook gestionado se antepone también cuando el hook previo termina en `exec`, evitando bloques inalcanzables.
|
|
20
|
+
- **Rollout recomendado:** publicar `pumuki@6.3.102`, repin inmediato en `RuralGo` y revalidar `status` / `doctor` / `pre-push` para cerrar `PUMUKI-INC-080`.
|
|
21
|
+
|
|
9
22
|
### 2026-04-22 (v6.3.101)
|
|
10
23
|
|
|
11
24
|
- **Hotfix de ruta bloqueante:** `gate.blocked` deja de lanzar `ReferenceError: options is not defined` al construir la remediación visible en `PRE_WRITE`.
|
|
@@ -234,6 +234,7 @@ type PolicyAsCodeContract = {
|
|
|
234
234
|
source: 'default' | 'skills.policy' | 'hard-mode';
|
|
235
235
|
signatures: Partial<Record<SkillsStage, string>> & Record<'PRE_COMMIT' | 'PRE_PUSH' | 'CI', string>;
|
|
236
236
|
expires_at?: string;
|
|
237
|
+
strict?: Partial<Record<SkillsStage, boolean>>;
|
|
237
238
|
};
|
|
238
239
|
|
|
239
240
|
const resolveContractSignatureForStage = (
|
|
@@ -264,6 +265,23 @@ const isIsoDateString = (value: unknown): value is string => {
|
|
|
264
265
|
return Number.isFinite(Date.parse(value));
|
|
265
266
|
};
|
|
266
267
|
|
|
268
|
+
const isPolicyStrictRecord = (
|
|
269
|
+
value: unknown
|
|
270
|
+
): value is Partial<Record<SkillsStage, boolean>> => {
|
|
271
|
+
if (!isObject(value)) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
return Object.entries(value).every(([stage, strict]) => {
|
|
275
|
+
return (
|
|
276
|
+
(stage === 'PRE_WRITE' ||
|
|
277
|
+
stage === 'PRE_COMMIT' ||
|
|
278
|
+
stage === 'PRE_PUSH' ||
|
|
279
|
+
stage === 'CI') &&
|
|
280
|
+
typeof strict === 'boolean'
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
|
|
267
285
|
const policyStrictModeFromEnv = (): boolean => {
|
|
268
286
|
const raw = process.env.PUMUKI_POLICY_STRICT?.trim().toLowerCase();
|
|
269
287
|
if (!raw) {
|
|
@@ -292,6 +310,9 @@ const isPolicyAsCodeContract = (value: unknown): value is PolicyAsCodeContract =
|
|
|
292
310
|
if (typeof value.expires_at !== 'undefined' && !isIsoDateString(value.expires_at)) {
|
|
293
311
|
return false;
|
|
294
312
|
}
|
|
313
|
+
if (typeof value.strict !== 'undefined' && !isPolicyStrictRecord(value.strict)) {
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
295
316
|
return (
|
|
296
317
|
(typeof value.signatures.PRE_WRITE === 'undefined' || isSha256Hex(value.signatures.PRE_WRITE)) &&
|
|
297
318
|
isSha256Hex(value.signatures.PRE_COMMIT) &&
|
|
@@ -300,6 +321,17 @@ const isPolicyAsCodeContract = (value: unknown): value is PolicyAsCodeContract =
|
|
|
300
321
|
);
|
|
301
322
|
};
|
|
302
323
|
|
|
324
|
+
const resolvePolicyAsCodeStrict = (params: {
|
|
325
|
+
contract?: PolicyAsCodeContract;
|
|
326
|
+
stage: SkillsStage;
|
|
327
|
+
}): boolean => {
|
|
328
|
+
const declared = params.contract?.strict?.[params.stage];
|
|
329
|
+
if (typeof declared === 'boolean') {
|
|
330
|
+
return declared;
|
|
331
|
+
}
|
|
332
|
+
return policyStrictModeFromEnv();
|
|
333
|
+
};
|
|
334
|
+
|
|
303
335
|
const createPolicyAsCodeSignature = (params: {
|
|
304
336
|
stage: SkillsStage;
|
|
305
337
|
source: 'default' | 'skills.policy' | 'hard-mode';
|
|
@@ -332,7 +364,7 @@ const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
332
364
|
policySource: string;
|
|
333
365
|
validation: NonNullable<ResolvedStagePolicy['trace']['validation']>;
|
|
334
366
|
} => {
|
|
335
|
-
const
|
|
367
|
+
const envStrict = policyStrictModeFromEnv();
|
|
336
368
|
const computedVersion = `policy-as-code/${params.source}@${POLICY_AS_CODE_VERSION}`;
|
|
337
369
|
const computedSignature = createPolicyAsCodeSignature({
|
|
338
370
|
stage: params.stage,
|
|
@@ -344,7 +376,7 @@ const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
344
376
|
const contractPath = join(params.repoRoot, POLICY_AS_CODE_CONTRACT_PATH);
|
|
345
377
|
|
|
346
378
|
if (!existsSync(contractPath)) {
|
|
347
|
-
if (
|
|
379
|
+
if (envStrict) {
|
|
348
380
|
return {
|
|
349
381
|
version: computedVersion,
|
|
350
382
|
signature: computedSignature,
|
|
@@ -354,7 +386,7 @@ const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
354
386
|
code: 'POLICY_AS_CODE_UNSIGNED',
|
|
355
387
|
message:
|
|
356
388
|
'Policy-as-code contract is missing; runtime policy metadata is unsigned.',
|
|
357
|
-
strict,
|
|
389
|
+
strict: envStrict,
|
|
358
390
|
},
|
|
359
391
|
};
|
|
360
392
|
}
|
|
@@ -367,7 +399,7 @@ const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
367
399
|
status: 'valid',
|
|
368
400
|
code: 'POLICY_AS_CODE_VALID',
|
|
369
401
|
message: 'Policy-as-code metadata generated from active runtime policy.',
|
|
370
|
-
strict,
|
|
402
|
+
strict: envStrict,
|
|
371
403
|
},
|
|
372
404
|
};
|
|
373
405
|
}
|
|
@@ -383,11 +415,16 @@ const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
383
415
|
status: 'invalid',
|
|
384
416
|
code: 'POLICY_AS_CODE_CONTRACT_INVALID',
|
|
385
417
|
message: 'Policy-as-code contract is malformed.',
|
|
386
|
-
strict,
|
|
418
|
+
strict: envStrict,
|
|
387
419
|
},
|
|
388
420
|
};
|
|
389
421
|
}
|
|
390
422
|
|
|
423
|
+
const strict = resolvePolicyAsCodeStrict({
|
|
424
|
+
contract: raw,
|
|
425
|
+
stage: params.stage,
|
|
426
|
+
});
|
|
427
|
+
|
|
391
428
|
if (raw.source !== params.source) {
|
|
392
429
|
return {
|
|
393
430
|
version: `policy-as-code/${raw.source}@${raw.version}`,
|
|
@@ -465,7 +502,7 @@ const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
465
502
|
status: 'invalid',
|
|
466
503
|
code: 'POLICY_AS_CODE_CONTRACT_INVALID',
|
|
467
504
|
message: 'Policy-as-code contract cannot be parsed as JSON.',
|
|
468
|
-
strict,
|
|
505
|
+
strict: envStrict,
|
|
469
506
|
},
|
|
470
507
|
};
|
|
471
508
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
1
3
|
import type { Finding } from '../../core/gate/Finding';
|
|
2
4
|
import type { GateStage } from '../../core/gate/GateStage';
|
|
3
5
|
import { evaluateAiGate, type AiGateStage } from '../gate/evaluateAiGate';
|
|
@@ -6,10 +8,82 @@ const AI_GATE_STAGES = new Set<AiGateStage>(['PRE_WRITE', 'PRE_COMMIT', 'PRE_PUS
|
|
|
6
8
|
|
|
7
9
|
const REPO_POLICY_CODES = new Set<string>([
|
|
8
10
|
'GITFLOW_PROTECTED_BRANCH',
|
|
11
|
+
'TRACKING_CANONICAL_IN_PROGRESS_INVALID',
|
|
9
12
|
'EVIDENCE_PREWRITE_WORKTREE_OVER_LIMIT',
|
|
10
13
|
'EVIDENCE_PREWRITE_WORKTREE_WARN',
|
|
11
14
|
]);
|
|
12
15
|
|
|
16
|
+
const TRACKING_CANDIDATE_FILES = [
|
|
17
|
+
'docs/technical/08-validation/refactor/pumuki-integration-feedback.md',
|
|
18
|
+
'docs/pumuki/PUMUKI_BUGS_MEJORAS.md',
|
|
19
|
+
'docs/BUGS_Y_MEJORAS_PUMUKI.md',
|
|
20
|
+
'PUMUKI-RESET-MASTER-PLAN.md',
|
|
21
|
+
'RURALGO_SEGUIMIENTO.md',
|
|
22
|
+
] as const;
|
|
23
|
+
|
|
24
|
+
type TrackingActiveEntry = {
|
|
25
|
+
taskId: string | null;
|
|
26
|
+
lineNumber: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const collectTrackingActiveEntriesFromMarkdown = (
|
|
30
|
+
markdown: string
|
|
31
|
+
): ReadonlyArray<TrackingActiveEntry> => {
|
|
32
|
+
const entries: TrackingActiveEntry[] = [];
|
|
33
|
+
const lines = markdown.split(/\r?\n/u);
|
|
34
|
+
for (const [index, line] of lines.entries()) {
|
|
35
|
+
const tableMatch = line.match(
|
|
36
|
+
/^\|\s*\d+\s*\|\s*`([^`]+)`\s*\|.*\|\s*🚧(?:\s+reported\s+activo|\s+En construcción|\s+En construccion)?\s*\|/u
|
|
37
|
+
);
|
|
38
|
+
if (tableMatch) {
|
|
39
|
+
entries.push({
|
|
40
|
+
taskId: tableMatch[1]!.trim(),
|
|
41
|
+
lineNumber: index + 1,
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const bulletMatch = line.match(/^- 🚧 (`?P[0-9A-Za-z.-]+`?)/u);
|
|
46
|
+
if (bulletMatch) {
|
|
47
|
+
entries.push({
|
|
48
|
+
taskId: bulletMatch[1]!.replaceAll('`', '').trim(),
|
|
49
|
+
lineNumber: index + 1,
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (/^- Estado:\s*🚧/u.test(line)) {
|
|
54
|
+
entries.push({
|
|
55
|
+
taskId: null,
|
|
56
|
+
lineNumber: index + 1,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return entries;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const formatTrackingEntry = (entry: TrackingActiveEntry): string =>
|
|
64
|
+
entry.taskId ? `${entry.taskId}@L${entry.lineNumber}` : `line_${entry.lineNumber}`;
|
|
65
|
+
|
|
66
|
+
export const appendTrackingActionableContext = (params: {
|
|
67
|
+
repoRoot: string;
|
|
68
|
+
message: string;
|
|
69
|
+
}): string => {
|
|
70
|
+
for (const candidate of TRACKING_CANDIDATE_FILES) {
|
|
71
|
+
const candidatePath = resolve(params.repoRoot, candidate);
|
|
72
|
+
if (!existsSync(candidatePath)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const source = readFileSync(candidatePath, 'utf8');
|
|
76
|
+
const entries = collectTrackingActiveEntriesFromMarkdown(source);
|
|
77
|
+
if (entries.length === 0) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const preview = entries.slice(0, 3).map(formatTrackingEntry).join(', ');
|
|
81
|
+
const overflow = entries.length > 3 ? ` (+${entries.length - 3} more)` : '';
|
|
82
|
+
return `${params.message} active_entries=${preview}${overflow} tracking_source=${candidate}`;
|
|
83
|
+
}
|
|
84
|
+
return params.message;
|
|
85
|
+
};
|
|
86
|
+
|
|
13
87
|
const toRepoPolicyFinding = (params: {
|
|
14
88
|
code: string;
|
|
15
89
|
message: string;
|
|
@@ -39,7 +113,13 @@ export const collectAiGateRepoPolicyFindings = (params: {
|
|
|
39
113
|
.map((v) =>
|
|
40
114
|
toRepoPolicyFinding({
|
|
41
115
|
code: v.code,
|
|
42
|
-
message:
|
|
116
|
+
message:
|
|
117
|
+
v.code === 'TRACKING_CANONICAL_IN_PROGRESS_INVALID'
|
|
118
|
+
? appendTrackingActionableContext({
|
|
119
|
+
repoRoot: params.repoRoot,
|
|
120
|
+
message: v.message,
|
|
121
|
+
})
|
|
122
|
+
: v.message,
|
|
43
123
|
severity: v.severity === 'ERROR' ? 'ERROR' : 'WARN',
|
|
44
124
|
})
|
|
45
125
|
);
|
|
@@ -52,6 +52,15 @@ const resolvePrimaryFinding = (findings: ReadonlyArray<Finding>): Finding => {
|
|
|
52
52
|
return primary ?? findings[0]!;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
+
const sortFindingsBySeverity = (findings: ReadonlyArray<Finding>): ReadonlyArray<Finding> =>
|
|
56
|
+
[...findings].sort((left, right) => {
|
|
57
|
+
const severityDelta = severityWeight(right.severity) - severityWeight(left.severity);
|
|
58
|
+
if (severityDelta !== 0) {
|
|
59
|
+
return severityDelta;
|
|
60
|
+
}
|
|
61
|
+
return left.ruleId.localeCompare(right.ruleId);
|
|
62
|
+
});
|
|
63
|
+
|
|
55
64
|
const normalizeAnchorLine = (lines: Finding['lines']): number => {
|
|
56
65
|
if (Array.isArray(lines)) {
|
|
57
66
|
const candidates = lines
|
|
@@ -98,7 +107,8 @@ export const printGateFindings = (findings: ReadonlyArray<Finding>): void => {
|
|
|
98
107
|
if (findings.length === 0) {
|
|
99
108
|
return;
|
|
100
109
|
}
|
|
101
|
-
const
|
|
110
|
+
const orderedFindings = sortFindingsBySeverity(findings);
|
|
111
|
+
const primary = resolvePrimaryFinding(orderedFindings);
|
|
102
112
|
const nextAction =
|
|
103
113
|
BLOCK_NEXT_ACTION_BY_CODE[primary.code]
|
|
104
114
|
?? 'Corrige el bloqueante primario y vuelve a ejecutar el mismo comando.';
|
|
@@ -106,7 +116,17 @@ export const printGateFindings = (findings: ReadonlyArray<Finding>): void => {
|
|
|
106
116
|
`[pumuki][block-summary] primary=${primary.code} severity=${primary.severity.toUpperCase()} rule=${primary.ruleId}\n`
|
|
107
117
|
);
|
|
108
118
|
process.stdout.write(`[pumuki][block-summary] next_action=${nextAction}\n`);
|
|
109
|
-
|
|
119
|
+
const secondaryWarnings = orderedFindings.filter(
|
|
120
|
+
(finding) =>
|
|
121
|
+
finding !== primary &&
|
|
122
|
+
severityWeight(finding.severity) < severityWeight(primary.severity)
|
|
123
|
+
);
|
|
124
|
+
for (const finding of secondaryWarnings) {
|
|
125
|
+
process.stdout.write(
|
|
126
|
+
`[pumuki][warning-summary] secondary=${finding.code} severity=${finding.severity.toUpperCase()} rule=${finding.ruleId}\n`
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
for (const finding of orderedFindings) {
|
|
110
130
|
process.stdout.write(`${formatFinding(finding)}\n`);
|
|
111
131
|
}
|
|
112
132
|
};
|
|
@@ -104,7 +104,7 @@ const ensureExecutableHeader = (contents: string): string => {
|
|
|
104
104
|
return contents;
|
|
105
105
|
};
|
|
106
106
|
|
|
107
|
-
const
|
|
107
|
+
const isFrameworkHookWithExecTerminator = (contents: string): boolean => {
|
|
108
108
|
if (!contents.includes('pre-commit.com') && !contents.includes('pre_commit')) {
|
|
109
109
|
return false;
|
|
110
110
|
}
|
|
@@ -141,11 +141,7 @@ export const upsertPumukiManagedBlock = (params: {
|
|
|
141
141
|
)
|
|
142
142
|
);
|
|
143
143
|
|
|
144
|
-
if (
|
|
145
|
-
params.hook === 'pre-commit' &&
|
|
146
|
-
core.length > 0 &&
|
|
147
|
-
isPreCommitFrameworkWithExecTerminator(core)
|
|
148
|
-
) {
|
|
144
|
+
if (core.length > 0 && isFrameworkHookWithExecTerminator(core)) {
|
|
149
145
|
const merged = prependManagedBlockAfterShebang({ contents: core, block });
|
|
150
146
|
return `${trimTrailingWhitespace(merged)}\n`;
|
|
151
147
|
}
|
|
@@ -148,6 +148,12 @@ const tryApplyPolicyAutofix = (params: {
|
|
|
148
148
|
PRE_PUSH: signatures.PRE_PUSH,
|
|
149
149
|
CI: signatures.CI,
|
|
150
150
|
},
|
|
151
|
+
strict: {
|
|
152
|
+
PRE_WRITE: params.report.stages.PRE_WRITE.strict,
|
|
153
|
+
PRE_COMMIT: params.report.stages.PRE_COMMIT.strict,
|
|
154
|
+
PRE_PUSH: params.report.stages.PRE_PUSH.strict,
|
|
155
|
+
CI: params.report.stages.CI.strict,
|
|
156
|
+
},
|
|
151
157
|
expires_at: '2999-01-01T00:00:00.000Z',
|
|
152
158
|
};
|
|
153
159
|
|
|
@@ -32,6 +32,7 @@ type PolicyAsCodeContract = {
|
|
|
32
32
|
source: PolicyProfileSource;
|
|
33
33
|
signatures: Record<SkillsStage, string>;
|
|
34
34
|
expires_at?: string;
|
|
35
|
+
strict?: Partial<Record<SkillsStage, boolean>>;
|
|
35
36
|
};
|
|
36
37
|
|
|
37
38
|
const isObject = (value: unknown): value is Record<string, unknown> => {
|
|
@@ -49,6 +50,23 @@ const isIsoDateString = (value: unknown): value is string => {
|
|
|
49
50
|
return Number.isFinite(Date.parse(value));
|
|
50
51
|
};
|
|
51
52
|
|
|
53
|
+
const isPolicyStrictRecord = (
|
|
54
|
+
value: unknown
|
|
55
|
+
): value is Partial<Record<SkillsStage, boolean>> => {
|
|
56
|
+
if (!isObject(value)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return Object.entries(value).every(([stage, strict]) => {
|
|
60
|
+
return (
|
|
61
|
+
(stage === 'PRE_WRITE' ||
|
|
62
|
+
stage === 'PRE_COMMIT' ||
|
|
63
|
+
stage === 'PRE_PUSH' ||
|
|
64
|
+
stage === 'CI') &&
|
|
65
|
+
typeof strict === 'boolean'
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
52
70
|
const policyStrictModeFromEnv = (): boolean => {
|
|
53
71
|
const raw = process.env.PUMUKI_POLICY_STRICT?.trim().toLowerCase();
|
|
54
72
|
if (!raw) {
|
|
@@ -77,6 +95,9 @@ const isPolicyAsCodeContract = (value: unknown): value is PolicyAsCodeContract =
|
|
|
77
95
|
if (typeof value.expires_at !== 'undefined' && !isIsoDateString(value.expires_at)) {
|
|
78
96
|
return false;
|
|
79
97
|
}
|
|
98
|
+
if (typeof value.strict !== 'undefined' && !isPolicyStrictRecord(value.strict)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
80
101
|
return (
|
|
81
102
|
isSha256Hex(value.signatures.PRE_COMMIT) &&
|
|
82
103
|
isSha256Hex(value.signatures.PRE_PUSH) &&
|
|
@@ -84,6 +105,17 @@ const isPolicyAsCodeContract = (value: unknown): value is PolicyAsCodeContract =
|
|
|
84
105
|
);
|
|
85
106
|
};
|
|
86
107
|
|
|
108
|
+
const resolvePolicyAsCodeStrict = (params: {
|
|
109
|
+
contract?: PolicyAsCodeContract;
|
|
110
|
+
stage: SkillsStage;
|
|
111
|
+
}): boolean => {
|
|
112
|
+
const declared = params.contract?.strict?.[params.stage];
|
|
113
|
+
if (typeof declared === 'boolean') {
|
|
114
|
+
return declared;
|
|
115
|
+
}
|
|
116
|
+
return policyStrictModeFromEnv();
|
|
117
|
+
};
|
|
118
|
+
|
|
87
119
|
export const createPolicyAsCodeSignature = (params: {
|
|
88
120
|
stage: SkillsStage;
|
|
89
121
|
source: PolicyProfileSource;
|
|
@@ -111,7 +143,7 @@ export const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
111
143
|
hash: string;
|
|
112
144
|
repoRoot: string;
|
|
113
145
|
}): PolicyAsCodeTraceMetadata => {
|
|
114
|
-
const
|
|
146
|
+
const envStrict = policyStrictModeFromEnv();
|
|
115
147
|
const computedVersion = `policy-as-code/${params.source}@${POLICY_AS_CODE_VERSION}`;
|
|
116
148
|
const computedSignature = createPolicyAsCodeSignature({
|
|
117
149
|
stage: params.stage,
|
|
@@ -123,7 +155,7 @@ export const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
123
155
|
const contractPath = join(params.repoRoot, POLICY_AS_CODE_CONTRACT_PATH);
|
|
124
156
|
|
|
125
157
|
if (!existsSync(contractPath)) {
|
|
126
|
-
if (
|
|
158
|
+
if (envStrict) {
|
|
127
159
|
return {
|
|
128
160
|
version: computedVersion,
|
|
129
161
|
signature: computedSignature,
|
|
@@ -133,7 +165,7 @@ export const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
133
165
|
code: 'POLICY_AS_CODE_UNSIGNED',
|
|
134
166
|
message:
|
|
135
167
|
'Policy-as-code contract is missing; runtime policy metadata is unsigned.',
|
|
136
|
-
strict,
|
|
168
|
+
strict: envStrict,
|
|
137
169
|
},
|
|
138
170
|
};
|
|
139
171
|
}
|
|
@@ -146,7 +178,7 @@ export const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
146
178
|
status: 'valid',
|
|
147
179
|
code: 'POLICY_AS_CODE_VALID',
|
|
148
180
|
message: 'Policy-as-code metadata generated from active runtime policy.',
|
|
149
|
-
strict,
|
|
181
|
+
strict: envStrict,
|
|
150
182
|
},
|
|
151
183
|
};
|
|
152
184
|
}
|
|
@@ -162,11 +194,16 @@ export const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
162
194
|
status: 'invalid',
|
|
163
195
|
code: 'POLICY_AS_CODE_CONTRACT_INVALID',
|
|
164
196
|
message: 'Policy-as-code contract is malformed.',
|
|
165
|
-
strict,
|
|
197
|
+
strict: envStrict,
|
|
166
198
|
},
|
|
167
199
|
};
|
|
168
200
|
}
|
|
169
201
|
|
|
202
|
+
const strict = resolvePolicyAsCodeStrict({
|
|
203
|
+
contract: raw,
|
|
204
|
+
stage: params.stage,
|
|
205
|
+
});
|
|
206
|
+
|
|
170
207
|
if (raw.source !== params.source) {
|
|
171
208
|
return {
|
|
172
209
|
version: `policy-as-code/${raw.source}@${raw.version}`,
|
|
@@ -243,7 +280,7 @@ export const resolvePolicyAsCodeTraceMetadata = (params: {
|
|
|
243
280
|
status: 'invalid',
|
|
244
281
|
code: 'POLICY_AS_CODE_CONTRACT_INVALID',
|
|
245
282
|
message: 'Policy-as-code contract cannot be parsed as JSON.',
|
|
246
|
-
strict,
|
|
283
|
+
strict: envStrict,
|
|
247
284
|
},
|
|
248
285
|
};
|
|
249
286
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.103",
|
|
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": {
|