pumuki 6.3.139 → 6.3.141

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,19 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.3.141] - 2026-05-05
10
+
11
+ ### Fixed
12
+
13
+ - **PRE_PUSH en ramas actualizadas desde base:** el guard de atomicidad ignora commits heredados de `main`/`develop` y commits merge al validar trazabilidad y límites por commit, evitando bloqueos falsos en rollouts que solo resolvieron conflictos con la rama base.
14
+
15
+ ## [6.3.140] - 2026-05-05
16
+
17
+ ### Fixed
18
+
19
+ - **PRE_PUSH atomicity por commit:** el guard de atomicidad evalúa cada commit del rango de push de forma independiente, evitando que una rama formada por commits atómicos quede bloqueada por el diff agregado.
20
+ - **Falsos positivos de metadata:** `hardcoded-values` y `magic-numbers` dejan de bloquear literales internos de policy/evidence/analytics y constantes estándar, manteniendo la detección real de configuración hardcoded.
21
+
9
22
  ## [6.3.139] - 2026-05-05
10
23
 
11
24
  ### Fixed
@@ -4588,6 +4588,58 @@ const buildMagicNumberPatternMatch = (
4588
4588
  node: unknown
4589
4589
  ): TypeScriptMagicNumberMatch | undefined => {
4590
4590
  const neutralNumericLiterals = new Set([0, 1]);
4591
+ const isNamedConstantInitializer = (ancestors: ReadonlyArray<AstNode>): boolean => {
4592
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
4593
+ const ancestor = ancestors[index];
4594
+ if (ancestor.type === 'CallExpression' || ancestor.type === 'NewExpression') {
4595
+ return false;
4596
+ }
4597
+ if (ancestor.type === 'VariableDeclarator') {
4598
+ return true;
4599
+ }
4600
+ }
4601
+ return false;
4602
+ };
4603
+ const isStandardLibraryNumericArgument = (
4604
+ value: AstNode,
4605
+ ancestors: ReadonlyArray<AstNode>
4606
+ ): boolean => {
4607
+ let callExpression: AstNode | undefined;
4608
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
4609
+ if (ancestors[index].type === 'CallExpression') {
4610
+ callExpression = ancestors[index];
4611
+ break;
4612
+ }
4613
+ }
4614
+ if (!isObject(callExpression) || !Array.isArray(callExpression.arguments)) {
4615
+ return false;
4616
+ }
4617
+ const argumentIndex = callExpression.arguments.indexOf(value);
4618
+ const calleeName =
4619
+ methodNameFromNode(callExpression.callee) ?? memberExpressionPropertyName(callExpression.callee);
4620
+ const memberName = memberExpressionPropertyName(callExpression.callee);
4621
+
4622
+ if ((calleeName === 'parseInt' || memberName === 'parseInt') && argumentIndex === 1) {
4623
+ return true;
4624
+ }
4625
+ if ((memberName === 'slice' || memberName === 'substring') && argumentIndex >= 0) {
4626
+ return true;
4627
+ }
4628
+ if (memberName === 'toFixed' && argumentIndex === 0) {
4629
+ return true;
4630
+ }
4631
+ if (memberName === 'toString' && argumentIndex === 0) {
4632
+ return true;
4633
+ }
4634
+ if ((memberName === 'min' || memberName === 'max') && argumentIndex >= 0) {
4635
+ return true;
4636
+ }
4637
+ if (value.value === 1000 && memberExpressionPropertyName(callExpression.callee) === 'floor') {
4638
+ return true;
4639
+ }
4640
+ return false;
4641
+ };
4642
+
4591
4643
  const match = findFirstNodeWithAncestors(node, (value, ancestors) => {
4592
4644
  if (value.type !== 'NumericLiteral' || typeof value.value !== 'number') {
4593
4645
  return false;
@@ -4595,6 +4647,12 @@ const buildMagicNumberPatternMatch = (
4595
4647
  if (neutralNumericLiterals.has(value.value)) {
4596
4648
  return false;
4597
4649
  }
4650
+ if (isNamedConstantInitializer(ancestors)) {
4651
+ return false;
4652
+ }
4653
+ if (isStandardLibraryNumericArgument(value, ancestors)) {
4654
+ return false;
4655
+ }
4598
4656
 
4599
4657
  return !ancestors.some((ancestor) => {
4600
4658
  return (
@@ -4790,6 +4848,80 @@ const isNeutralHardcodedNumericLiteral = (node: AstNode): boolean => {
4790
4848
  return node.type === 'NumericLiteral' && (node.value === 0 || node.value === 1);
4791
4849
  };
4792
4850
 
4851
+ const isBenignHardcodedConfigLiteral = (node: AstNode): boolean => {
4852
+ if (node.type !== 'StringLiteral') {
4853
+ return false;
4854
+ }
4855
+ const value = String(node.value).trim();
4856
+ if (value.length === 0) {
4857
+ return true;
4858
+ }
4859
+ if (/^(?:0|1|true|false|yes|no|on|off)$/i.test(value)) {
4860
+ return true;
4861
+ }
4862
+ if (
4863
+ [
4864
+ 'all-severities',
4865
+ 'critical-high',
4866
+ 'default',
4867
+ 'detected',
4868
+ 'engine',
4869
+ 'gate',
4870
+ 'hard-mode',
4871
+ 'inferred',
4872
+ 'skills.policy',
4873
+ ].includes(value)
4874
+ ) {
4875
+ return true;
4876
+ }
4877
+ if (/^\d+(?:\.\d+)+$/.test(value)) {
4878
+ return true;
4879
+ }
4880
+ if (
4881
+ value.startsWith('skills.') ||
4882
+ value.startsWith('heuristics.') ||
4883
+ value.startsWith('common.') ||
4884
+ value.startsWith('workflow.')
4885
+ ) {
4886
+ return true;
4887
+ }
4888
+ if (value.startsWith('.pumuki/') || value.startsWith('openspec/')) {
4889
+ return true;
4890
+ }
4891
+ if (/^\.[a-z0-9]+$/i.test(value)) {
4892
+ return true;
4893
+ }
4894
+ if (/^[A-Z][A-Z0-9_]+$/.test(value)) {
4895
+ return true;
4896
+ }
4897
+ if (/^--[a-z0-9-]+$/i.test(value)) {
4898
+ return true;
4899
+ }
4900
+ if (/^[()[\]{}.,:;!/\-\\]+$/.test(value)) {
4901
+ return true;
4902
+ }
4903
+ return false;
4904
+ };
4905
+
4906
+ const isBenignConfigMetadataName = (value: string): boolean => {
4907
+ const normalized = value.trim();
4908
+ if (normalized.length === 0) {
4909
+ return true;
4910
+ }
4911
+ if (
4912
+ normalized.startsWith('skills.') ||
4913
+ normalized.startsWith('heuristics.') ||
4914
+ normalized.startsWith('common.') ||
4915
+ normalized.startsWith('workflow.')
4916
+ ) {
4917
+ return true;
4918
+ }
4919
+ if (/^[A-Z][A-Z0-9_]+$/.test(normalized)) {
4920
+ return true;
4921
+ }
4922
+ return false;
4923
+ };
4924
+
4793
4925
  const isTypeOnlyAstNode = (node: AstNode): boolean => {
4794
4926
  return typeof node.type === 'string' && node.type.startsWith('TS');
4795
4927
  };
@@ -4803,6 +4935,9 @@ const hardcodedValueAssignmentContextFromAncestors = (
4803
4935
  if (ancestor.type === 'VariableDeclarator') {
4804
4936
  const ownerName = hardcodedValueNameFromNode(ancestor.id);
4805
4937
  if (typeof ownerName === 'string' && ownerName.length > 0) {
4938
+ if (isBenignConfigMetadataName(ownerName)) {
4939
+ return undefined;
4940
+ }
4806
4941
  return {
4807
4942
  ownerName,
4808
4943
  ownerKind: 'member',
@@ -4814,6 +4949,9 @@ const hardcodedValueAssignmentContextFromAncestors = (
4814
4949
  if (ancestor.type === 'ObjectProperty' || ancestor.type === 'ClassProperty') {
4815
4950
  const ownerName = hardcodedValueNameFromNode(ancestor.key);
4816
4951
  if (typeof ownerName === 'string' && ownerName.length > 0) {
4952
+ if (isBenignConfigMetadataName(ownerName)) {
4953
+ return undefined;
4954
+ }
4817
4955
  return {
4818
4956
  ownerName,
4819
4957
  ownerKind: 'member',
@@ -4825,6 +4963,9 @@ const hardcodedValueAssignmentContextFromAncestors = (
4825
4963
  if (ancestor.type === 'AssignmentExpression') {
4826
4964
  const ownerName = hardcodedValueNameFromNode(ancestor.left);
4827
4965
  if (typeof ownerName === 'string' && ownerName.length > 0) {
4966
+ if (isBenignConfigMetadataName(ownerName)) {
4967
+ return undefined;
4968
+ }
4828
4969
  return {
4829
4970
  ownerName,
4830
4971
  ownerKind: 'member',
@@ -4981,6 +5122,7 @@ const buildHardcodedValuePatternMatch = (
4981
5122
  isAstNodeTypeLiteral(value) ||
4982
5123
  isPrimitiveTypeGuardLiteral(value) ||
4983
5124
  isRuntimeApiLiteral(value) ||
5125
+ isBenignHardcodedConfigLiteral(value) ||
4984
5126
  isNeutralHardcodedNumericLiteral(value)
4985
5127
  ) {
4986
5128
  return false;
@@ -4,6 +4,18 @@ This file tracks the active deterministic framework line used in this repository
4
4
  Canonical release chronology lives in `CHANGELOG.md`.
5
5
  This file keeps only the operational highlights and rollout notes that matter while running the framework.
6
6
 
7
+ ### 2026-05-05 (v6.3.141)
8
+
9
+ - **PRE_PUSH sin falso bloqueo por historial base:** ramas de rollout que integran `main`/`develop` dejan de fallar por commits merge heredados como `Merge pull request ...`.
10
+ - **Flux/SAAS follow-up:** esta patch desbloquea el push de las resoluciones de conflicto de los PRs de repin abiertos tras `6.3.140`.
11
+ - **Rollout:** publicar `pumuki@6.3.141`, repinear primero RuralGo y repetir Flux/SAAS sin bypass.
12
+
13
+ ### 2026-05-05 (v6.3.140)
14
+
15
+ - **PRE_PUSH compatible con ramas largas:** la atomicidad se valida por commit individual, no por diff agregado de rama.
16
+ - **Skills sin falsos positivos de metadata:** el diff de release queda con `hardcoded-values`/`magic-numbers` a `0` para ficheros cambiados.
17
+ - **Rollout:** publicar `pumuki@6.3.140`, repinear primero RuralGo y repetir validaciones `status`, `doctor` y hooks gestionados.
18
+
7
19
  ### 2026-05-05 (v6.3.139)
8
20
 
9
21
  - **Baseline tests antes de editar:** Pumuki bloquea cambios TDD/BDD in-scope si cada slice no registra un baseline test pasado antes de RED.
@@ -27,6 +27,9 @@ export type WriteEvidenceResult = {
27
27
 
28
28
  const EVIDENCE_FILE_NAME = '.ai_evidence.json';
29
29
  const TEMP_EVIDENCE_PREFIX = '.ai_evidence.json.tmp-';
30
+ const RANDOM_SUFFIX_RADIX = 16;
31
+ const RANDOM_SUFFIX_START_INDEX = 2;
32
+ const RANDOM_SUFFIX_END_INDEX = 10;
30
33
 
31
34
  const normalizeLines = (lines?: EvidenceLines): EvidenceLines | undefined => {
32
35
  if (typeof lines === 'undefined') {
@@ -435,7 +438,9 @@ const resolveRepoRoot = (): string => {
435
438
  };
436
439
 
437
440
  const buildTempEvidencePath = (repoRoot: string): string => {
438
- const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2, 10)}`;
441
+ const uniqueSuffix = `${process.pid}-${Date.now()}-${Math.random()
442
+ .toString(RANDOM_SUFFIX_RADIX)
443
+ .slice(RANDOM_SUFFIX_START_INDEX, RANDOM_SUFFIX_END_INDEX)}`;
439
444
  return join(repoRoot, `${TEMP_EVIDENCE_PREFIX}${uniqueSuffix}`);
440
445
  };
441
446
 
@@ -32,6 +32,14 @@ const ATOMICITY_CONFIG_FILE = '.pumuki/git-atomicity.json';
32
32
  const DEFAULT_COMMIT_PATTERN =
33
33
  '^(feat|fix|chore|refactor|docs|test|perf|build|ci|revert)(\\([^)]+\\))?:\\s.+$';
34
34
  const MANAGED_EVIDENCE_PATHS = new Set(['.ai_evidence.json', '.AI_EVIDENCE.json']);
35
+ const BASELINE_BRANCH_REFS = [
36
+ 'origin/main',
37
+ 'origin/develop',
38
+ 'upstream/main',
39
+ 'upstream/develop',
40
+ 'main',
41
+ 'develop',
42
+ ];
35
43
 
36
44
  const defaultConfig: GitAtomicityConfig = {
37
45
  enabled: true,
@@ -252,12 +260,110 @@ const collectCommitSubjects = (params: {
252
260
  fromRef?: string;
253
261
  toRef?: string;
254
262
  }): ReadonlyArray<string> => {
263
+ return collectCommitRecords(params)
264
+ .filter((record) => shouldEvaluateCommitRecord({ git: params.git, repoRoot: params.repoRoot, record }))
265
+ .map((record) => record.subject);
266
+ };
267
+
268
+ type CommitRecord = {
269
+ hash: string;
270
+ parents: ReadonlyArray<string>;
271
+ subject: string;
272
+ };
273
+
274
+ const parseCommitRecords = (value: string): ReadonlyArray<CommitRecord> =>
275
+ value
276
+ .split('\n')
277
+ .map((line) => line.trim())
278
+ .filter((line) => line.length > 0)
279
+ .map((line) => {
280
+ const [hash = '', parents = '', subject = ''] = line.split('\u0001');
281
+ return {
282
+ hash: hash.trim(),
283
+ parents: parents.split(' ').map((parent) => parent.trim()).filter((parent) => parent.length > 0),
284
+ subject: subject.trim(),
285
+ };
286
+ })
287
+ .filter((record) => record.hash.length > 0);
288
+
289
+ const isCommitReachableFromRef = (params: {
290
+ git: IGitService;
291
+ repoRoot: string;
292
+ commitHash: string;
293
+ ref: string;
294
+ }): boolean => {
295
+ try {
296
+ params.git.runGit(['merge-base', '--is-ancestor', params.commitHash, params.ref], params.repoRoot);
297
+ return true;
298
+ } catch {
299
+ return false;
300
+ }
301
+ };
302
+
303
+ const isInheritedBaselineCommit = (params: {
304
+ git: IGitService;
305
+ repoRoot: string;
306
+ commitHash: string;
307
+ }): boolean =>
308
+ BASELINE_BRANCH_REFS.some((ref) =>
309
+ isCommitReachableFromRef({
310
+ git: params.git,
311
+ repoRoot: params.repoRoot,
312
+ commitHash: params.commitHash,
313
+ ref,
314
+ })
315
+ );
316
+
317
+ const shouldEvaluateCommitRecord = (params: {
318
+ git: IGitService;
319
+ repoRoot: string;
320
+ record: CommitRecord;
321
+ }): boolean => {
322
+ if (params.record.parents.length > 1) {
323
+ return false;
324
+ }
325
+ return !isInheritedBaselineCommit({
326
+ git: params.git,
327
+ repoRoot: params.repoRoot,
328
+ commitHash: params.record.hash,
329
+ });
330
+ };
331
+
332
+ const collectCommitRecords = (params: {
333
+ git: IGitService;
334
+ repoRoot: string;
335
+ fromRef?: string;
336
+ toRef?: string;
337
+ }): ReadonlyArray<CommitRecord> => {
255
338
  if (!params.fromRef || !params.toRef) {
256
339
  return [];
257
340
  }
341
+ try {
342
+ return parseCommitRecords(
343
+ params.git.runGit(
344
+ ['log', '--format=%H%x01%P%x01%s', '--reverse', `${params.fromRef}..${params.toRef}`],
345
+ params.repoRoot
346
+ )
347
+ );
348
+ } catch (error) {
349
+ if (isUnresolvableRevisionError(error)) {
350
+ return [];
351
+ }
352
+ throw error;
353
+ }
354
+ };
355
+
356
+ const collectCommitChangedPaths = (params: {
357
+ git: IGitService;
358
+ repoRoot: string;
359
+ commitHash: string;
360
+ }): ReadonlyArray<string> => {
258
361
  try {
259
362
  return parseLines(
260
- params.git.runGit(['log', '--format=%s', `${params.fromRef}..${params.toRef}`], params.repoRoot)
363
+ params.git.runGit(
364
+ ['diff-tree', '--no-commit-id', '--name-only', '-r', '--diff-filter=ACMR', params.commitHash],
365
+ params.repoRoot
366
+ )
261
367
  );
262
368
  } catch (error) {
263
369
  if (isUnresolvableRevisionError(error)) {
@@ -267,6 +373,95 @@ const collectCommitSubjects = (params: {
267
373
  }
268
374
  };
269
375
 
376
+ const buildPathLimitViolations = (params: {
377
+ changedPaths: ReadonlyArray<string>;
378
+ config: GitAtomicityConfig;
379
+ stage: GitAtomicityStage;
380
+ atomicSlicesRemediation?: string;
381
+ label?: string;
382
+ }): GitAtomicityViolation[] => {
383
+ const violations: GitAtomicityViolation[] = [];
384
+ const labelSuffix = params.label ? ` (${params.label})` : '';
385
+
386
+ if (params.changedPaths.length > params.config.maxFiles) {
387
+ violations.push({
388
+ code: 'GIT_ATOMICITY_TOO_MANY_FILES',
389
+ message:
390
+ `Git atomicity guard blocked at ${params.stage}${labelSuffix}: changed_files=${params.changedPaths.length} exceeds max_files=${params.config.maxFiles}.`,
391
+ remediation:
392
+ `Divide los cambios en commits más pequeños (máximo ${params.config.maxFiles} archivos por commit).`
393
+ + (params.atomicSlicesRemediation ? ` ${params.atomicSlicesRemediation}` : ''),
394
+ });
395
+ }
396
+
397
+ const scopePaths = collectScopePaths(params.changedPaths);
398
+ const scopeKeys = new Set(scopePaths.keys());
399
+ if (scopeKeys.size > params.config.maxScopes) {
400
+ const sortedScopes = [...scopeKeys].sort();
401
+ const suggestedScopeAdds = sortedScopes
402
+ .slice(0, Math.max(1, Math.min(params.config.maxScopes + 1, 3)))
403
+ .map((scope) => `git add ${scope}/`)
404
+ .join(' && ');
405
+ const scopeBreakdown = sortedScopes
406
+ .map((scope) => {
407
+ const paths = scopePaths.get(scope) ?? [];
408
+ const sample = paths.slice(0, 3);
409
+ return `${scope}{count=${paths.length}; sample=[${sample.join(', ')}]}`;
410
+ })
411
+ .join(' | ');
412
+ violations.push({
413
+ code: 'GIT_ATOMICITY_TOO_MANY_SCOPES',
414
+ message:
415
+ `Git atomicity guard blocked at ${params.stage}${labelSuffix}: changed_scopes=${scopeKeys.size} exceeds max_scopes=${params.config.maxScopes}. ` +
416
+ `scope_files=${scopeBreakdown}.`,
417
+ remediation:
418
+ `Agrupa cambios por ámbito funcional (máximo ${params.config.maxScopes} scopes por commit). ` +
419
+ `scopes_detectados=[${sortedScopes.join(', ')}]. ` +
420
+ `Sugerencia split: git restore --staged . && ${suggestedScopeAdds} && git commit -m "<tipo>: <scope>".`
421
+ + (params.atomicSlicesRemediation ? ` ${params.atomicSlicesRemediation}` : ''),
422
+ });
423
+ }
424
+
425
+ return violations;
426
+ };
427
+
428
+ const buildPrePushCommitPathLimitViolations = (params: {
429
+ git: IGitService;
430
+ repoRoot: string;
431
+ config: GitAtomicityConfig;
432
+ fromRef?: string;
433
+ toRef?: string;
434
+ }): GitAtomicityViolation[] | undefined => {
435
+ const commitRecords = collectCommitRecords({
436
+ git: params.git,
437
+ repoRoot: params.repoRoot,
438
+ fromRef: params.fromRef,
439
+ toRef: params.toRef,
440
+ }).filter((record) => shouldEvaluateCommitRecord({ git: params.git, repoRoot: params.repoRoot, record }));
441
+ if (commitRecords.length === 0) {
442
+ return undefined;
443
+ }
444
+
445
+ const violations: GitAtomicityViolation[] = [];
446
+ for (const commitRecord of commitRecords) {
447
+ const changedPaths = collectCommitChangedPaths({
448
+ git: params.git,
449
+ repoRoot: params.repoRoot,
450
+ commitHash: commitRecord.hash,
451
+ }).filter((path) => !isManagedEvidencePath(path));
452
+ violations.push(
453
+ ...buildPathLimitViolations({
454
+ changedPaths,
455
+ config: params.config,
456
+ stage: 'PRE_PUSH',
457
+ label: `commit=${commitRecord.hash.slice(0, 12)}`,
458
+ })
459
+ );
460
+ }
461
+
462
+ return violations;
463
+ };
464
+
270
465
  export const evaluateGitAtomicity = (params: {
271
466
  git?: IGitService;
272
467
  repoRoot?: string;
@@ -299,44 +494,26 @@ export const evaluateGitAtomicity = (params: {
299
494
  stage: params.stage,
300
495
  });
301
496
 
302
- if (changedPaths.length > config.maxFiles) {
303
- violations.push({
304
- code: 'GIT_ATOMICITY_TOO_MANY_FILES',
305
- message:
306
- `Git atomicity guard blocked at ${params.stage}: changed_files=${changedPaths.length} exceeds max_files=${config.maxFiles}.`,
307
- remediation:
308
- `Divide los cambios en commits más pequeños (máximo ${config.maxFiles} archivos por commit).`
309
- + (atomicSlicesRemediation ? ` ${atomicSlicesRemediation}` : ''),
310
- });
311
- }
497
+ const prePushCommitViolations =
498
+ params.stage === 'PRE_PUSH'
499
+ ? buildPrePushCommitPathLimitViolations({
500
+ git,
501
+ repoRoot,
502
+ config,
503
+ fromRef: params.fromRef,
504
+ toRef: params.toRef,
505
+ })
506
+ : undefined;
312
507
 
313
- const scopePaths = collectScopePaths(changedPaths);
314
- const scopeKeys = new Set(scopePaths.keys());
315
- if (scopeKeys.size > config.maxScopes) {
316
- const sortedScopes = [...scopeKeys].sort();
317
- const suggestedScopeAdds = sortedScopes
318
- .slice(0, Math.max(1, Math.min(config.maxScopes + 1, 3)))
319
- .map((scope) => `git add ${scope}/`)
320
- .join(' && ');
321
- const scopeBreakdown = sortedScopes
322
- .map((scope) => {
323
- const paths = scopePaths.get(scope) ?? [];
324
- const sample = paths.slice(0, 3);
325
- return `${scope}{count=${paths.length}; sample=[${sample.join(', ')}]}`;
326
- })
327
- .join(' | ');
328
- violations.push({
329
- code: 'GIT_ATOMICITY_TOO_MANY_SCOPES',
330
- message:
331
- `Git atomicity guard blocked at ${params.stage}: changed_scopes=${scopeKeys.size} exceeds max_scopes=${config.maxScopes}. ` +
332
- `scope_files=${scopeBreakdown}.`,
333
- remediation:
334
- `Agrupa cambios por ámbito funcional (máximo ${config.maxScopes} scopes por commit). ` +
335
- `scopes_detectados=[${sortedScopes.join(', ')}]. ` +
336
- `Sugerencia split: git restore --staged . && ${suggestedScopeAdds} && git commit -m "<tipo>: <scope>".`
337
- + (atomicSlicesRemediation ? ` ${atomicSlicesRemediation}` : ''),
338
- });
339
- }
508
+ violations.push(
509
+ ...(prePushCommitViolations
510
+ ?? buildPathLimitViolations({
511
+ changedPaths,
512
+ config,
513
+ stage: params.stage,
514
+ atomicSlicesRemediation,
515
+ }))
516
+ );
340
517
 
341
518
  if (config.enforceCommitMessagePattern && params.stage !== 'PRE_COMMIT') {
342
519
  let pattern: RegExp;
@@ -77,6 +77,18 @@ import { runPolicyReconcile } from './policyReconcile';
77
77
  import { runLifecycleAudit, type LifecycleAuditStage } from './audit';
78
78
  import { resolvePreWriteEnforcement, type PreWriteEnforcementResolution } from '../policy/preWriteEnforcement';
79
79
 
80
+ const POLICY_SUBCOMMAND_ARG_COUNT = 2;
81
+ const PRE_WRITE_PANEL_MIN_COLUMNS = 86;
82
+ const PRE_WRITE_PANEL_MAX_COLUMNS = 140;
83
+ const PRE_WRITE_PANEL_BORDER_COLUMNS = 2;
84
+ const PRE_WRITE_PANEL_PADDING_COLUMNS = 4;
85
+ const WORKTREE_SLICE_PLAN_COUNT = 3;
86
+ const WORKTREE_SLICE_FILE_COUNT = 4;
87
+ const CLI_JSON_INDENT = 2;
88
+ const ANALYTICS_TOOL_NAME = 'analytics';
89
+ const ANALYTICS_FEATURE_NAME = 'analytics';
90
+ const SAAS_INGESTION_FEATURE_NAME = 'saas_ingestion';
91
+
80
92
  type LifecycleCommand =
81
93
  | 'bootstrap'
82
94
  | 'install'
@@ -811,9 +823,9 @@ export const parseLifecycleCliArgs = (argv: ReadonlyArray<string>): ParsedArgs =
811
823
  }
812
824
  let policyStrict = false;
813
825
  let policyApply = false;
814
- const policyFlagsOffset =
815
- typeof firstArg === 'string' && !firstArg.startsWith('--') ? 2 : 1;
816
- for (const arg of argv.slice(policyFlagsOffset)) {
826
+ const policyArgStartIndex =
827
+ typeof firstArg === 'string' && !firstArg.startsWith('--') ? POLICY_SUBCOMMAND_ARG_COUNT : 1;
828
+ for (const arg of argv.slice(policyArgStartIndex)) {
817
829
  if (arg === '--json') {
818
830
  json = true;
819
831
  continue;
@@ -1748,7 +1760,7 @@ const buildAnalyticsExperimentalDisabledEnvelope = (
1748
1760
  feature: LifecycleExperimentalFeaturesSnapshot['features']['analytics'],
1749
1761
  action: AnalyticsHotspotsCommand
1750
1762
  ): AnalyticsExperimentalDisabledEnvelope => ({
1751
- tool: 'analytics',
1763
+ tool: ANALYTICS_TOOL_NAME,
1752
1764
  dryRun: true,
1753
1765
  executed: false,
1754
1766
  success: true,
@@ -1756,7 +1768,7 @@ const buildAnalyticsExperimentalDisabledEnvelope = (
1756
1768
  code: 'ANALYTICS_EXPERIMENTAL_DISABLED',
1757
1769
  message:
1758
1770
  'Analytics hotspots está desactivado explícitamente. Usa PUMUKI_EXPERIMENTAL_ANALYTICS=advisory o strict si necesitas este flujo.',
1759
- experimental_feature: 'analytics',
1771
+ experimental_feature: ANALYTICS_FEATURE_NAME,
1760
1772
  mode: feature.mode,
1761
1773
  source: feature.source,
1762
1774
  activation_variable: feature.activationVariable,
@@ -1770,7 +1782,7 @@ const buildAnalyticsExperimentalDisabledEnvelope = (
1770
1782
  const buildSaasIngestionExperimentalDisabledEnvelope = (
1771
1783
  feature: LifecycleExperimentalFeaturesSnapshot['features']['saas_ingestion']
1772
1784
  ): SaasIngestionExperimentalDisabledEnvelope => ({
1773
- tool: 'analytics',
1785
+ tool: ANALYTICS_TOOL_NAME,
1774
1786
  dryRun: true,
1775
1787
  executed: false,
1776
1788
  success: true,
@@ -1778,7 +1790,7 @@ const buildSaasIngestionExperimentalDisabledEnvelope = (
1778
1790
  code: 'SAAS_INGESTION_EXPERIMENTAL_DISABLED',
1779
1791
  message:
1780
1792
  'SaaS ingestion/federation está desactivado explícitamente. Usa PUMUKI_EXPERIMENTAL_SAAS_INGESTION=advisory o strict si necesitas este flujo.',
1781
- experimental_feature: 'saas_ingestion',
1793
+ experimental_feature: SAAS_INGESTION_FEATURE_NAME,
1782
1794
  mode: feature.mode,
1783
1795
  source: feature.source,
1784
1796
  activation_variable: feature.activationVariable,
@@ -1908,8 +1920,8 @@ export const resolvePreWriteNextAction = (params: {
1908
1920
  if (atomicSliceViolation) {
1909
1921
  const plan = collectWorktreeAtomicSlices({
1910
1922
  repoRoot: params.aiGate.repo_state.repo_root,
1911
- maxSlices: 3,
1912
- maxFilesPerSlice: 4,
1923
+ maxSlices: WORKTREE_SLICE_PLAN_COUNT,
1924
+ maxFilesPerSlice: WORKTREE_SLICE_FILE_COUNT,
1913
1925
  });
1914
1926
  const firstSliceCommand = plan.slices[0]?.staged_command ?? 'git add -p';
1915
1927
  return {
@@ -1975,11 +1987,14 @@ const renderPreWritePanel = (lines: ReadonlyArray<string>): string => {
1975
1987
  const terminalWidth = Number.isFinite(process.stdout.columns ?? NaN)
1976
1988
  ? Number(process.stdout.columns)
1977
1989
  : 110;
1978
- const width = Math.min(140, Math.max(86, terminalWidth - 2));
1979
- const innerWidth = width - 4;
1990
+ const width = Math.min(
1991
+ PRE_WRITE_PANEL_MAX_COLUMNS,
1992
+ Math.max(PRE_WRITE_PANEL_MIN_COLUMNS, terminalWidth - PRE_WRITE_PANEL_BORDER_COLUMNS)
1993
+ );
1994
+ const innerWidth = width - PRE_WRITE_PANEL_PADDING_COLUMNS;
1980
1995
  const normalized = lines.flatMap((line) => wrapPreWritePanelLine(line, innerWidth));
1981
- const top = `╔${'═'.repeat(width - 2)}╗`;
1982
- const bottom = `╚${'═'.repeat(width - 2)}╝`;
1996
+ const top = `╔${'═'.repeat(width - PRE_WRITE_PANEL_BORDER_COLUMNS)}╗`;
1997
+ const bottom = `╚${'═'.repeat(width - PRE_WRITE_PANEL_BORDER_COLUMNS)}╝`;
1983
1998
  const body = normalized.map((line) => `║ ${line.padEnd(innerWidth, ' ')} ║`);
1984
1999
  return [top, ...body, bottom].join('\n');
1985
2000
  };
@@ -2121,7 +2136,7 @@ const writeLoopAttemptEvidence = (params: {
2121
2136
  const relativePath = `.pumuki/loop-sessions/${params.sessionId}.attempt-${params.attempt}.json`;
2122
2137
  const absolutePath = resolve(params.repoRoot, relativePath);
2123
2138
  mkdirSync(dirname(absolutePath), { recursive: true });
2124
- writeFileSync(absolutePath, `${JSON.stringify(params.payload, null, 2)}\n`, 'utf8');
2139
+ writeFileSync(absolutePath, `${JSON.stringify(params.payload, null, CLI_JSON_INDENT)}\n`, 'utf8');
2125
2140
  return relativePath;
2126
2141
  };
2127
2142
 
@@ -13,6 +13,7 @@ import { writeInfo } from './cliOutputs';
13
13
  import { formatTrackingActionableContext } from './trackingState';
14
14
 
15
15
  const DEFAULT_PROTECTED_BRANCHES = new Set(['main', 'master', 'develop', 'dev']);
16
+ const STRICT_ENV_VALUE = 'strict';
16
17
 
17
18
  export type GovernanceEvidenceSummary = {
18
19
  path: string;
@@ -78,7 +79,7 @@ const truthyEnv = (value: string | undefined): boolean => {
78
79
  return false;
79
80
  }
80
81
  const normalized = value.trim().toLowerCase();
81
- return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === 'strict';
82
+ return normalized === '1' || normalized === 'true' || normalized === 'yes' || normalized === STRICT_ENV_VALUE;
82
83
  };
83
84
 
84
85
  const readCurrentBranch = (git: ILifecycleGitService, repoRoot: string): string | null => {
@@ -20,6 +20,8 @@ type PolicyReconcileDriftCode =
20
20
  | 'POLICY_HASH_DIVERGENCE'
21
21
  | 'POLICY_STAGE_NON_STRICT';
22
22
 
23
+ const POLICY_CONTRACT_JSON_INDENT = 2;
24
+
23
25
  export type PolicyReconcileDrift = {
24
26
  code: PolicyReconcileDriftCode;
25
27
  severity: PolicyReconcileSeverity;
@@ -180,7 +182,7 @@ const tryApplyPolicyAutofix = (params: {
180
182
 
181
183
  try {
182
184
  mkdirSync(dirname(contractPath), { recursive: true });
183
- writeFileSync(contractPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf8');
185
+ writeFileSync(contractPath, `${JSON.stringify(contract, null, POLICY_CONTRACT_JSON_INDENT)}\n`, 'utf8');
184
186
  return {
185
187
  attempted: true,
186
188
  status: 'APPLIED',
@@ -7,6 +7,9 @@ import { readSddLearningContext, type SddLearningContext } from '../sdd/learning
7
7
  type AutoExecuteAction = 'proceed' | 'ask';
8
8
  type AutoExecutePhase = 'GREEN' | 'RED';
9
9
 
10
+ const WORKTREE_SLICE_PLAN_COUNT = 3;
11
+ const WORKTREE_SLICE_FILE_COUNT = 4;
12
+
10
13
  type AutoExecuteNextAction = {
11
14
  kind: 'info' | 'run_command';
12
15
  message: string;
@@ -132,8 +135,8 @@ const nextActionFromViolation = (
132
135
  {
133
136
  const plan = collectWorktreeAtomicSlices({
134
137
  repoRoot,
135
- maxSlices: 3,
136
- maxFilesPerSlice: 4,
138
+ maxSlices: WORKTREE_SLICE_PLAN_COUNT,
139
+ maxFilesPerSlice: WORKTREE_SLICE_FILE_COUNT,
137
140
  });
138
141
  if (plan.slices.length > 0) {
139
142
  const firstSlice = plan.slices[0];
@@ -10,6 +10,9 @@ import { resolveLearningContextExperimentalFeature } from '../policy/experimenta
10
10
  import { resolvePreWriteEnforcement } from '../policy/preWriteEnforcement';
11
11
  import { readSddLearningContext, type SddLearningContext } from '../sdd/learningInsights';
12
12
 
13
+ const WORKTREE_SLICE_PLAN_COUNT = 3;
14
+ const WORKTREE_SLICE_FILE_COUNT = 4;
15
+
13
16
  const resolveTddStatus = (
14
17
  repoRoot: string
15
18
  ): 'skipped' | 'passed' | 'advisory' | 'blocked' | 'waived' | null => {
@@ -114,8 +117,8 @@ const buildPreFlightHints = (params: {
114
117
  if (params.stage === 'PRE_WRITE' && hasWorktreeViolation) {
115
118
  const plan = collectWorktreeAtomicSlices({
116
119
  repoRoot: params.repoRoot,
117
- maxSlices: 3,
118
- maxFilesPerSlice: 4,
120
+ maxSlices: WORKTREE_SLICE_PLAN_COUNT,
121
+ maxFilesPerSlice: WORKTREE_SLICE_FILE_COUNT,
119
122
  });
120
123
  if (plan.slices.length > 0) {
121
124
  hints.push('ATOMIC_SLICES: staging sugerido por scope.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.139",
3
+ "version": "6.3.141",
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": {