pumuki 6.3.141 → 6.3.143

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,20 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [6.3.143] - 2026-05-05
10
+
11
+ ### Fixed
12
+
13
+ - **PUMUKI-INC-060 baseline TDD/BDD fresco:** los cambios in-scope bloquean si la evidencia de baseline TDD/BDD está caducada, obligando a reejecutar los tests baseline del componente antes de editar código relacionado.
14
+ - **Ventana configurable de evidencia:** `PUMUKI_TDD_BDD_EVIDENCE_MAX_AGE_SECONDS` permite ajustar la frescura máxima; por defecto son 900 segundos y los valores inválidos mantienen el modo estricto.
15
+
16
+ ## [6.3.142] - 2026-05-05
17
+
18
+ ### Fixed
19
+
20
+ - **PUMUKI-INC-059 iOS SOLID en PRE_WRITE:** la skill iOS `Verificar que NO viole SOLID (SRP, OCP, LSP, ISP, DIP)` se normaliza al id canónico `skills.ios.no-solid-violations` y al alias real del lock legacy, activa los nodos AST OCP/SRP/DIP/ISP/LSP y bloquea desde `PRE_WRITE` sin depender de `PUMUKI_ENABLE_AST_HEURISTICS`.
21
+ - **Skills hard-blocking multi-stage:** `no-solid-violations` se promueve a bloqueo desde `PRE_WRITE`, `PRE_COMMIT`, `PRE_PUSH` y `CI`, evitando que una violación iOS OCP/SRP llegue a disco o al commit.
22
+
9
23
  ## [6.3.141] - 2026-05-05
10
24
 
11
25
  ### Fixed
@@ -4248,6 +4248,34 @@ test('hasHardcodedValuePattern detecta literals de configuracion y omite valores
4248
4248
  assert.equal(hasHardcodedValuePattern(neutralLiteralAst), false);
4249
4249
  });
4250
4250
 
4251
+ test('hasHardcodedValuePattern no bloquea tokens internos de metadata AST', () => {
4252
+ const astNodeTokenAst = {
4253
+ type: 'VariableDeclarator',
4254
+ id: { type: 'Identifier', name: 'astNodeToken' },
4255
+ init: {
4256
+ type: 'StringLiteral',
4257
+ value: 'none',
4258
+ loc: { start: { line: 4 }, end: { line: 4 } },
4259
+ },
4260
+ loc: { start: { line: 4 }, end: { line: 4 } },
4261
+ };
4262
+ const astNodesTokenAst = {
4263
+ type: 'VariableDeclarator',
4264
+ id: { type: 'Identifier', name: 'astNodesToken' },
4265
+ init: {
4266
+ type: 'StringLiteral',
4267
+ value: 'none',
4268
+ loc: { start: { line: 8 }, end: { line: 8 } },
4269
+ },
4270
+ loc: { start: { line: 8 }, end: { line: 8 } },
4271
+ };
4272
+
4273
+ assert.equal(hasHardcodedValuePattern(astNodeTokenAst), false);
4274
+ assert.equal(findHardcodedValuePatternMatch(astNodeTokenAst), undefined);
4275
+ assert.equal(hasHardcodedValuePattern(astNodesTokenAst), false);
4276
+ assert.equal(findHardcodedValuePatternMatch(astNodesTokenAst), undefined);
4277
+ });
4278
+
4251
4279
  test('hasHardcodedValuePattern usa tokens exactos y no subcadenas accidentales', () => {
4252
4280
  const reportFunctionAst = {
4253
4281
  type: 'FunctionDeclaration',
@@ -4908,6 +4908,10 @@ const isBenignConfigMetadataName = (value: string): boolean => {
4908
4908
  if (normalized.length === 0) {
4909
4909
  return true;
4910
4910
  }
4911
+ const tokenSet = new Set(identifierNameTokens(normalized));
4912
+ if (tokenSet.has('ast') && (tokenSet.has('node') || tokenSet.has('nodes')) && tokenSet.has('token')) {
4913
+ return true;
4914
+ }
4911
4915
  if (
4912
4916
  normalized.startsWith('skills.') ||
4913
4917
  normalized.startsWith('heuristics.') ||
@@ -33,6 +33,11 @@ test('iosEnterpriseRuleSet define reglas locked para plataforma ios', () => {
33
33
  assert.equal(byId.get('ios.solid.isp.fat-protocol-dependency')?.when.kind, 'Heuristic');
34
34
  assert.equal(byId.get('ios.solid.lsp.narrowed-precondition-substitution')?.when.kind, 'Heuristic');
35
35
  assert.equal(byId.get('ios.solid.srp.presentation-mixed-responsibilities')?.when.kind, 'Heuristic');
36
+ assert.equal(byId.get('ios.solid.ocp.discriminator-switch-branching')?.stage, 'PRE_WRITE');
37
+ assert.equal(byId.get('ios.solid.dip.concrete-framework-dependency')?.stage, 'PRE_WRITE');
38
+ assert.equal(byId.get('ios.solid.isp.fat-protocol-dependency')?.stage, 'PRE_WRITE');
39
+ assert.equal(byId.get('ios.solid.lsp.narrowed-precondition-substitution')?.stage, 'PRE_WRITE');
40
+ assert.equal(byId.get('ios.solid.srp.presentation-mixed-responsibilities')?.stage, 'PRE_WRITE');
36
41
  assert.equal(byId.get('ios.canary-001.presentation-mixed-responsibilities')?.when.kind, 'Heuristic');
37
42
  assert.equal(byId.get('ios.tdd.domain-changes-require-tests')?.when.kind, 'All');
38
43
  assert.equal(byId.get('ios.no-completion-handlers-outside-bridges')?.when.kind, 'Heuristic');
@@ -44,7 +44,7 @@ export const iosEnterpriseRuleSet: RuleSet = [
44
44
  'Blocks iOS application or presentation types that must be modified to support new discriminator cases instead of extending behavior through abstractions.',
45
45
  severity: 'CRITICAL',
46
46
  platform: 'ios',
47
- stage: 'PRE_COMMIT',
47
+ stage: 'PRE_WRITE',
48
48
  locked: true,
49
49
  scope: {
50
50
  include: ['**/*.swift'],
@@ -70,7 +70,7 @@ export const iosEnterpriseRuleSet: RuleSet = [
70
70
  'Blocks iOS application or presentation types that depend directly on concrete framework services instead of abstractions.',
71
71
  severity: 'CRITICAL',
72
72
  platform: 'ios',
73
- stage: 'PRE_COMMIT',
73
+ stage: 'PRE_WRITE',
74
74
  locked: true,
75
75
  scope: {
76
76
  include: ['**/*.swift'],
@@ -96,7 +96,7 @@ export const iosEnterpriseRuleSet: RuleSet = [
96
96
  'Blocks iOS application or presentation types that depend on fat protocols instead of a minimal port tailored to the members they actually use.',
97
97
  severity: 'CRITICAL',
98
98
  platform: 'ios',
99
- stage: 'PRE_COMMIT',
99
+ stage: 'PRE_WRITE',
100
100
  locked: true,
101
101
  scope: {
102
102
  include: ['**/*.swift'],
@@ -122,7 +122,7 @@ export const iosEnterpriseRuleSet: RuleSet = [
122
122
  'Blocks iOS application or presentation types whose subtype narrows the contract preconditions and becomes unsafe to substitute for the base protocol or abstraction.',
123
123
  severity: 'CRITICAL',
124
124
  platform: 'ios',
125
- stage: 'PRE_COMMIT',
125
+ stage: 'PRE_WRITE',
126
126
  locked: true,
127
127
  scope: {
128
128
  include: ['**/*.swift'],
@@ -148,7 +148,7 @@ export const iosEnterpriseRuleSet: RuleSet = [
148
148
  'Blocks iOS presentation types that mix session, networking, persistence and navigation responsibilities in the same semantic node.',
149
149
  severity: 'CRITICAL',
150
150
  platform: 'ios',
151
- stage: 'PRE_COMMIT',
151
+ stage: 'PRE_WRITE',
152
152
  locked: true,
153
153
  scope: {
154
154
  include: ['**/*.swift'],
@@ -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.143)
8
+
9
+ - **RuralGo PUMUKI-INC-060:** PRE_WRITE deja de aceptar evidencia TDD/BDD de baseline caducada para cambios in-scope.
10
+ - **Baseline antes de editar:** si el componente tocado requiere TDD/BDD, la evidencia debe ser reciente y pasada; si no, Pumuki bloquea y pide reejecutar baseline tests.
11
+ - **Rollout:** publicar `pumuki@6.3.143`, repinear primero RuralGo y revalidar `status`, `doctor` y el bloqueo por evidencia stale.
12
+
13
+ ### 2026-05-05 (v6.3.142)
14
+
15
+ - **RuralGo PUMUKI-INC-059:** PRE_WRITE vuelve a pedir hechos AST para iOS SOLID aunque el flag experimental de heurísticas esté apagado.
16
+ - **OCP/SRP iOS bloqueante temprano:** `skills.ios.no-solid-violations` y el id real legacy del lock se vinculan a OCP/SRP/DIP/ISP/LSP; los findings se promueven a `ERROR` desde PRE_WRITE.
17
+ - **Rollout:** publicar `pumuki@6.3.142`, repinear primero RuralGo y revalidar `status`, `doctor` y un canary OCP iOS en PRE_WRITE/PRE_COMMIT.
18
+
7
19
  ### 2026-05-05 (v6.3.141)
8
20
 
9
21
  - **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 ...`.
@@ -160,6 +160,21 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
160
160
  'ios.core-data.nsmanagedobject-state-leak',
161
161
  ['heuristics.ios.core-data.nsmanagedobject-state-leak.ast']
162
162
  ),
163
+ 'skills.ios.no-solid-violations': heuristicDetector('ios.solid', [
164
+ 'heuristics.ios.solid.srp.presentation-mixed-responsibilities.ast',
165
+ 'heuristics.ios.solid.dip.concrete-framework-dependency.ast',
166
+ 'heuristics.ios.solid.ocp.discriminator-switch.ast',
167
+ 'heuristics.ios.solid.isp.fat-protocol-dependency.ast',
168
+ 'heuristics.ios.solid.lsp.narrowed-precondition.ast',
169
+ ]),
170
+ 'skills.ios.guideline.ios.verificar-que-no-viole-solid-srp-ocp-lsp-isp-dip':
171
+ heuristicDetector('ios.solid', [
172
+ 'heuristics.ios.solid.srp.presentation-mixed-responsibilities.ast',
173
+ 'heuristics.ios.solid.dip.concrete-framework-dependency.ast',
174
+ 'heuristics.ios.solid.ocp.discriminator-switch.ast',
175
+ 'heuristics.ios.solid.isp.fat-protocol-dependency.ast',
176
+ 'heuristics.ios.solid.lsp.narrowed-precondition.ast',
177
+ ]),
163
178
  'skills.android.no-solid-violations': heuristicDetector('android.solid', [
164
179
  'heuristics.android.solid.srp.presentation-mixed-responsibilities.ast',
165
180
  'heuristics.android.solid.dip.concrete-framework-dependency.ast',
@@ -90,6 +90,8 @@ const inferRuleStage = (raw: string): SkillsStage | undefined => {
90
90
  };
91
91
 
92
92
  const KNOWN_RULE_DEFAULT_STAGE: Readonly<Record<string, SkillsStage>> = {
93
+ 'skills.ios.no-solid-violations': 'PRE_WRITE',
94
+ 'skills.ios.guideline.ios.verificar-que-no-viole-solid-srp-ocp-lsp-isp-dip': 'PRE_WRITE',
93
95
  'skills.backend.no-solid-violations': 'PRE_PUSH',
94
96
  'skills.frontend.no-solid-violations': 'PRE_PUSH',
95
97
  'skills.backend.enforce-clean-architecture': 'PRE_PUSH',
@@ -275,8 +277,27 @@ const normalizeKnownRuleTarget = (
275
277
  normalizedDescription: string
276
278
  ): string | null => {
277
279
  const includes = (needle: string): boolean => normalizedDescription.includes(needle);
280
+ const hasWord = (needle: string): boolean =>
281
+ new RegExp(`(^|\\s)${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$)`).test(
282
+ normalizedDescription
283
+ );
278
284
 
279
285
  if (platform === 'ios') {
286
+ if (
287
+ includes('solid') ||
288
+ includes('single responsibility') ||
289
+ hasWord('srp') ||
290
+ hasWord('ocp') ||
291
+ hasWord('lsp') ||
292
+ hasWord('isp') ||
293
+ hasWord('dip') ||
294
+ includes('open closed') ||
295
+ includes('open-closed') ||
296
+ includes('verificar que no viole solid') ||
297
+ includes('no viole solid')
298
+ ) {
299
+ return 'skills.ios.no-solid-violations';
300
+ }
280
301
  if (
281
302
  includes('force unwrap') ||
282
303
  includes('force unwrapping') ||
@@ -369,7 +369,10 @@ const resolveRuleSeverity = (params: {
369
369
  const promotedRuleIds = params.bundlePolicy?.promoteToErrorRuleIds ?? [];
370
370
  const shouldPromoteBySolidContract =
371
371
  params.rule.id.endsWith('.no-solid-violations') &&
372
- (params.stage === 'PRE_PUSH' || params.stage === 'CI');
372
+ (params.stage === 'PRE_WRITE' ||
373
+ params.stage === 'PRE_COMMIT' ||
374
+ params.stage === 'PRE_PUSH' ||
375
+ params.stage === 'CI');
373
376
  const shouldPromote =
374
377
  shouldPromoteBySolidContract || promotedRuleIds.includes(params.rule.id);
375
378
 
@@ -76,10 +76,33 @@ const isTimelineOrdered = (timestamps: ReadonlyArray<string | undefined>): boole
76
76
  return true;
77
77
  };
78
78
 
79
+ const DEFAULT_EVIDENCE_MAX_AGE_SECONDS = 900;
80
+
81
+ const resolveEvidenceMaxAgeSeconds = (): number => {
82
+ const raw = process.env.PUMUKI_TDD_BDD_EVIDENCE_MAX_AGE_SECONDS?.trim();
83
+ if (!raw) {
84
+ return DEFAULT_EVIDENCE_MAX_AGE_SECONDS;
85
+ }
86
+ const parsed = Number.parseInt(raw, 10);
87
+ if (!Number.isFinite(parsed) || parsed <= 0) {
88
+ return DEFAULT_EVIDENCE_MAX_AGE_SECONDS;
89
+ }
90
+ return parsed;
91
+ };
92
+
93
+ const resolveEvidenceAgeSeconds = (generatedAt: string, nowMs: number): number | null => {
94
+ const generatedAtMs = new Date(generatedAt).getTime();
95
+ if (Number.isNaN(generatedAtMs)) {
96
+ return null;
97
+ }
98
+ return Math.max(0, Math.floor((nowMs - generatedAtMs) / 1000));
99
+ };
100
+
79
101
  export const enforceTddBddPolicy = (params: {
80
102
  facts: ReadonlyArray<Fact>;
81
103
  repoRoot: string;
82
104
  branch: string | null;
105
+ now?: () => number;
83
106
  }): TddBddEnforcementResult => {
84
107
  const scope = classifyTddBddScope(params.facts);
85
108
  const baseSnapshot: TddBddSnapshot = {
@@ -210,6 +233,46 @@ export const enforceTddBddPolicy = (params: {
210
233
  };
211
234
  }
212
235
 
236
+ const maxAgeSeconds = resolveEvidenceMaxAgeSeconds();
237
+ const ageSeconds = resolveEvidenceAgeSeconds(
238
+ evidenceRead.evidence.generated_at,
239
+ params.now?.() ?? Date.now()
240
+ );
241
+ if (ageSeconds === null || ageSeconds > maxAgeSeconds) {
242
+ const messageAge =
243
+ ageSeconds === null ? 'unknown' : `${ageSeconds}s`;
244
+ const finding = buildFinding({
245
+ ruleId: 'generic_tdd_baseline_required',
246
+ code: 'TDD_BDD_EVIDENCE_STALE',
247
+ message:
248
+ `TDD/BDD evidence is stale for this PRE_WRITE baseline: age=${messageAge}, max=${maxAgeSeconds}s. Re-run the baseline tests for the touched component and refresh evidence before editing related code.`,
249
+ filePath: evidenceRead.path,
250
+ });
251
+ return {
252
+ findings: [finding],
253
+ snapshot: {
254
+ ...baseSnapshot,
255
+ status: 'blocked',
256
+ evidence: {
257
+ ...baseSnapshot.evidence,
258
+ state: 'valid',
259
+ version: evidenceRead.evidence.version,
260
+ slices_total: evidenceRead.evidence.slices.length,
261
+ slices_valid: 0,
262
+ slices_invalid: evidenceRead.evidence.slices.length,
263
+ integrity_ok: evidenceRead.integrity.valid,
264
+ errors: ['TDD_BDD_EVIDENCE_STALE'],
265
+ baseline: {
266
+ required: true,
267
+ passed: 0,
268
+ missing: 0,
269
+ failed: 0,
270
+ },
271
+ },
272
+ },
273
+ };
274
+ }
275
+
213
276
  const sliceFindings: Finding[] = [];
214
277
  const seenSliceIds = new Set<string>();
215
278
  let validSlices = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.141",
3
+ "version": "6.3.143",
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": {