pumuki 6.3.101 → 6.3.102

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 CHANGED
@@ -6,6 +6,14 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.3.102] - 2026-04-22
10
+
11
+ ### Fixed
12
+
13
+ - **`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.
14
+ - **`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.
15
+ - **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.
16
+
9
17
  ## [6.3.101] - 2026-04-22
10
18
 
11
19
  ### Fixed
package/VERSION CHANGED
@@ -1 +1 @@
1
- v6.3.100
1
+ v6.3.102
@@ -6,6 +6,13 @@ 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.102)
10
+
11
+ - **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.
12
+ - **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.
13
+ - **Wiring fiable en `pre-push`:** el hook gestionado se antepone también cuando el hook previo termina en `exec`, evitando bloques inalcanzables.
14
+ - **Rollout recomendado:** publicar `pumuki@6.3.102`, repin inmediato en `RuralGo` y revalidar `status` / `doctor` / `pre-push` para cerrar `PUMUKI-INC-080`.
15
+
9
16
  ### 2026-04-22 (v6.3.101)
10
17
 
11
18
  - **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 strict = policyStrictModeFromEnv();
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 (strict) {
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
  }
@@ -104,7 +104,7 @@ const ensureExecutableHeader = (contents: string): string => {
104
104
  return contents;
105
105
  };
106
106
 
107
- const isPreCommitFrameworkWithExecTerminator = (contents: string): boolean => {
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 strict = policyStrictModeFromEnv();
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 (strict) {
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.101",
3
+ "version": "6.3.102",
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": {