pumuki 6.3.139 → 6.3.140
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 +7 -0
- package/core/facts/detectors/typescript/index.ts +142 -0
- package/docs/operations/RELEASE_NOTES.md +6 -0
- package/integrations/evidence/writeEvidence.ts +6 -1
- package/integrations/git/gitAtomicity.ts +148 -36
- package/integrations/lifecycle/cli.ts +29 -14
- package/integrations/lifecycle/governanceObservationSnapshot.ts +2 -1
- package/integrations/lifecycle/policyReconcile.ts +3 -1
- package/integrations/mcp/autoExecuteAiStart.ts +5 -2
- package/integrations/mcp/preFlightCheck.ts +5 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [6.3.140] - 2026-05-05
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **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.
|
|
14
|
+
- **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.
|
|
15
|
+
|
|
9
16
|
## [6.3.139] - 2026-05-05
|
|
10
17
|
|
|
11
18
|
### 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,12 @@ 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.140)
|
|
8
|
+
|
|
9
|
+
- **PRE_PUSH compatible con ramas largas:** la atomicidad se valida por commit individual, no por diff agregado de rama.
|
|
10
|
+
- **Skills sin falsos positivos de metadata:** el diff de release queda con `hardcoded-values`/`magic-numbers` a `0` para ficheros cambiados.
|
|
11
|
+
- **Rollout:** publicar `pumuki@6.3.140`, repinear primero RuralGo y repetir validaciones `status`, `doctor` y hooks gestionados.
|
|
12
|
+
|
|
7
13
|
### 2026-05-05 (v6.3.139)
|
|
8
14
|
|
|
9
15
|
- **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()
|
|
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
|
|
|
@@ -267,55 +267,74 @@ const collectCommitSubjects = (params: {
|
|
|
267
267
|
}
|
|
268
268
|
};
|
|
269
269
|
|
|
270
|
-
|
|
271
|
-
git
|
|
272
|
-
repoRoot
|
|
273
|
-
stage: GitAtomicityStage;
|
|
270
|
+
const collectCommitHashes = (params: {
|
|
271
|
+
git: IGitService;
|
|
272
|
+
repoRoot: string;
|
|
274
273
|
fromRef?: string;
|
|
275
274
|
toRef?: string;
|
|
276
|
-
}):
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
275
|
+
}): ReadonlyArray<string> => {
|
|
276
|
+
if (!params.fromRef || !params.toRef) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
return parseLines(
|
|
281
|
+
params.git.runGit(['log', '--format=%H', '--reverse', `${params.fromRef}..${params.toRef}`], params.repoRoot)
|
|
282
|
+
);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (isUnresolvableRevisionError(error)) {
|
|
285
|
+
return [];
|
|
286
|
+
}
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const collectCommitChangedPaths = (params: {
|
|
292
|
+
git: IGitService;
|
|
293
|
+
repoRoot: string;
|
|
294
|
+
commitHash: string;
|
|
295
|
+
}): ReadonlyArray<string> => {
|
|
296
|
+
try {
|
|
297
|
+
return parseLines(
|
|
298
|
+
params.git.runGit(
|
|
299
|
+
['diff-tree', '--no-commit-id', '--name-only', '-r', '--diff-filter=ACMR', params.commitHash],
|
|
300
|
+
params.repoRoot
|
|
301
|
+
)
|
|
302
|
+
);
|
|
303
|
+
} catch (error) {
|
|
304
|
+
if (isUnresolvableRevisionError(error)) {
|
|
305
|
+
return [];
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
286
308
|
}
|
|
309
|
+
};
|
|
287
310
|
|
|
311
|
+
const buildPathLimitViolations = (params: {
|
|
312
|
+
changedPaths: ReadonlyArray<string>;
|
|
313
|
+
config: GitAtomicityConfig;
|
|
314
|
+
stage: GitAtomicityStage;
|
|
315
|
+
atomicSlicesRemediation?: string;
|
|
316
|
+
label?: string;
|
|
317
|
+
}): GitAtomicityViolation[] => {
|
|
288
318
|
const violations: GitAtomicityViolation[] = [];
|
|
289
|
-
const
|
|
290
|
-
git,
|
|
291
|
-
repoRoot,
|
|
292
|
-
stage: params.stage,
|
|
293
|
-
fromRef: params.fromRef,
|
|
294
|
-
toRef: params.toRef,
|
|
295
|
-
}).filter((path) => !isManagedEvidencePath(path));
|
|
296
|
-
const atomicSlicesRemediation = buildAtomicSlicesRemediation({
|
|
297
|
-
git,
|
|
298
|
-
repoRoot,
|
|
299
|
-
stage: params.stage,
|
|
300
|
-
});
|
|
319
|
+
const labelSuffix = params.label ? ` (${params.label})` : '';
|
|
301
320
|
|
|
302
|
-
if (changedPaths.length > config.maxFiles) {
|
|
321
|
+
if (params.changedPaths.length > params.config.maxFiles) {
|
|
303
322
|
violations.push({
|
|
304
323
|
code: 'GIT_ATOMICITY_TOO_MANY_FILES',
|
|
305
324
|
message:
|
|
306
|
-
`Git atomicity guard blocked at ${params.stage}: changed_files=${changedPaths.length} exceeds max_files=${config.maxFiles}.`,
|
|
325
|
+
`Git atomicity guard blocked at ${params.stage}${labelSuffix}: changed_files=${params.changedPaths.length} exceeds max_files=${params.config.maxFiles}.`,
|
|
307
326
|
remediation:
|
|
308
|
-
`Divide los cambios en commits más pequeños (máximo ${config.maxFiles} archivos por commit).`
|
|
309
|
-
+ (atomicSlicesRemediation ? ` ${atomicSlicesRemediation}` : ''),
|
|
327
|
+
`Divide los cambios en commits más pequeños (máximo ${params.config.maxFiles} archivos por commit).`
|
|
328
|
+
+ (params.atomicSlicesRemediation ? ` ${params.atomicSlicesRemediation}` : ''),
|
|
310
329
|
});
|
|
311
330
|
}
|
|
312
331
|
|
|
313
|
-
const scopePaths = collectScopePaths(changedPaths);
|
|
332
|
+
const scopePaths = collectScopePaths(params.changedPaths);
|
|
314
333
|
const scopeKeys = new Set(scopePaths.keys());
|
|
315
|
-
if (scopeKeys.size > config.maxScopes) {
|
|
334
|
+
if (scopeKeys.size > params.config.maxScopes) {
|
|
316
335
|
const sortedScopes = [...scopeKeys].sort();
|
|
317
336
|
const suggestedScopeAdds = sortedScopes
|
|
318
|
-
.slice(0, Math.max(1, Math.min(config.maxScopes + 1, 3)))
|
|
337
|
+
.slice(0, Math.max(1, Math.min(params.config.maxScopes + 1, 3)))
|
|
319
338
|
.map((scope) => `git add ${scope}/`)
|
|
320
339
|
.join(' && ');
|
|
321
340
|
const scopeBreakdown = sortedScopes
|
|
@@ -328,16 +347,109 @@ export const evaluateGitAtomicity = (params: {
|
|
|
328
347
|
violations.push({
|
|
329
348
|
code: 'GIT_ATOMICITY_TOO_MANY_SCOPES',
|
|
330
349
|
message:
|
|
331
|
-
`Git atomicity guard blocked at ${params.stage}: changed_scopes=${scopeKeys.size} exceeds max_scopes=${config.maxScopes}. ` +
|
|
350
|
+
`Git atomicity guard blocked at ${params.stage}${labelSuffix}: changed_scopes=${scopeKeys.size} exceeds max_scopes=${params.config.maxScopes}. ` +
|
|
332
351
|
`scope_files=${scopeBreakdown}.`,
|
|
333
352
|
remediation:
|
|
334
|
-
`Agrupa cambios por ámbito funcional (máximo ${config.maxScopes} scopes por commit). ` +
|
|
353
|
+
`Agrupa cambios por ámbito funcional (máximo ${params.config.maxScopes} scopes por commit). ` +
|
|
335
354
|
`scopes_detectados=[${sortedScopes.join(', ')}]. ` +
|
|
336
355
|
`Sugerencia split: git restore --staged . && ${suggestedScopeAdds} && git commit -m "<tipo>: <scope>".`
|
|
337
|
-
+ (atomicSlicesRemediation ? ` ${atomicSlicesRemediation}` : ''),
|
|
356
|
+
+ (params.atomicSlicesRemediation ? ` ${params.atomicSlicesRemediation}` : ''),
|
|
338
357
|
});
|
|
339
358
|
}
|
|
340
359
|
|
|
360
|
+
return violations;
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const buildPrePushCommitPathLimitViolations = (params: {
|
|
364
|
+
git: IGitService;
|
|
365
|
+
repoRoot: string;
|
|
366
|
+
config: GitAtomicityConfig;
|
|
367
|
+
fromRef?: string;
|
|
368
|
+
toRef?: string;
|
|
369
|
+
}): GitAtomicityViolation[] | undefined => {
|
|
370
|
+
const commitHashes = collectCommitHashes({
|
|
371
|
+
git: params.git,
|
|
372
|
+
repoRoot: params.repoRoot,
|
|
373
|
+
fromRef: params.fromRef,
|
|
374
|
+
toRef: params.toRef,
|
|
375
|
+
});
|
|
376
|
+
if (commitHashes.length === 0) {
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const violations: GitAtomicityViolation[] = [];
|
|
381
|
+
for (const commitHash of commitHashes) {
|
|
382
|
+
const changedPaths = collectCommitChangedPaths({
|
|
383
|
+
git: params.git,
|
|
384
|
+
repoRoot: params.repoRoot,
|
|
385
|
+
commitHash,
|
|
386
|
+
}).filter((path) => !isManagedEvidencePath(path));
|
|
387
|
+
violations.push(
|
|
388
|
+
...buildPathLimitViolations({
|
|
389
|
+
changedPaths,
|
|
390
|
+
config: params.config,
|
|
391
|
+
stage: 'PRE_PUSH',
|
|
392
|
+
label: `commit=${commitHash.slice(0, 12)}`,
|
|
393
|
+
})
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return violations;
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
export const evaluateGitAtomicity = (params: {
|
|
401
|
+
git?: IGitService;
|
|
402
|
+
repoRoot?: string;
|
|
403
|
+
stage: GitAtomicityStage;
|
|
404
|
+
fromRef?: string;
|
|
405
|
+
toRef?: string;
|
|
406
|
+
}): GitAtomicityEvaluation => {
|
|
407
|
+
const git = params.git ?? new GitService();
|
|
408
|
+
const repoRoot = params.repoRoot ? resolve(params.repoRoot) : git.resolveRepoRoot();
|
|
409
|
+
const config = resolveConfig(repoRoot);
|
|
410
|
+
if (!config.enabled) {
|
|
411
|
+
return {
|
|
412
|
+
enabled: false,
|
|
413
|
+
allowed: true,
|
|
414
|
+
violations: [],
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const violations: GitAtomicityViolation[] = [];
|
|
419
|
+
const changedPaths = collectChangedPaths({
|
|
420
|
+
git,
|
|
421
|
+
repoRoot,
|
|
422
|
+
stage: params.stage,
|
|
423
|
+
fromRef: params.fromRef,
|
|
424
|
+
toRef: params.toRef,
|
|
425
|
+
}).filter((path) => !isManagedEvidencePath(path));
|
|
426
|
+
const atomicSlicesRemediation = buildAtomicSlicesRemediation({
|
|
427
|
+
git,
|
|
428
|
+
repoRoot,
|
|
429
|
+
stage: params.stage,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
const prePushCommitViolations =
|
|
433
|
+
params.stage === 'PRE_PUSH'
|
|
434
|
+
? buildPrePushCommitPathLimitViolations({
|
|
435
|
+
git,
|
|
436
|
+
repoRoot,
|
|
437
|
+
config,
|
|
438
|
+
fromRef: params.fromRef,
|
|
439
|
+
toRef: params.toRef,
|
|
440
|
+
})
|
|
441
|
+
: undefined;
|
|
442
|
+
|
|
443
|
+
violations.push(
|
|
444
|
+
...(prePushCommitViolations
|
|
445
|
+
?? buildPathLimitViolations({
|
|
446
|
+
changedPaths,
|
|
447
|
+
config,
|
|
448
|
+
stage: params.stage,
|
|
449
|
+
atomicSlicesRemediation,
|
|
450
|
+
}))
|
|
451
|
+
);
|
|
452
|
+
|
|
341
453
|
if (config.enforceCommitMessagePattern && params.stage !== 'PRE_COMMIT') {
|
|
342
454
|
let pattern: RegExp;
|
|
343
455
|
try {
|
|
@@ -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
|
|
815
|
-
typeof firstArg === 'string' && !firstArg.startsWith('--') ?
|
|
816
|
-
for (const arg of argv.slice(
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1912
|
-
maxFilesPerSlice:
|
|
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(
|
|
1979
|
-
|
|
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 -
|
|
1982
|
-
const bottom = `╚${'═'.repeat(width -
|
|
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,
|
|
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 ===
|
|
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,
|
|
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:
|
|
136
|
-
maxFilesPerSlice:
|
|
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:
|
|
118
|
-
maxFilesPerSlice:
|
|
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.
|
|
3
|
+
"version": "6.3.140",
|
|
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": {
|