pumuki 6.3.140 → 6.3.142

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.142] - 2026-05-05
10
+
11
+ ### Fixed
12
+
13
+ - **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`.
14
+ - **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.
15
+
16
+ ## [6.3.141] - 2026-05-05
17
+
18
+ ### Fixed
19
+
20
+ - **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.
21
+
9
22
  ## [6.3.140] - 2026-05-05
10
23
 
11
24
  ### 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.142)
8
+
9
+ - **RuralGo PUMUKI-INC-059:** PRE_WRITE vuelve a pedir hechos AST para iOS SOLID aunque el flag experimental de heurísticas esté apagado.
10
+ - **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.
11
+ - **Rollout:** publicar `pumuki@6.3.142`, repinear primero RuralGo y revalidar `status`, `doctor` y un canary OCP iOS en PRE_WRITE/PRE_COMMIT.
12
+
13
+ ### 2026-05-05 (v6.3.141)
14
+
15
+ - **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 ...`.
16
+ - **Flux/SAAS follow-up:** esta patch desbloquea el push de las resoluciones de conflicto de los PRs de repin abiertos tras `6.3.140`.
17
+ - **Rollout:** publicar `pumuki@6.3.141`, repinear primero RuralGo y repetir Flux/SAAS sin bypass.
18
+
7
19
  ### 2026-05-05 (v6.3.140)
8
20
 
9
21
  - **PRE_PUSH compatible con ramas largas:** la atomicidad se valida por commit individual, no por diff agregado de rama.
@@ -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
 
@@ -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,33 +260,90 @@ const collectCommitSubjects = (params: {
252
260
  fromRef?: string;
253
261
  toRef?: string;
254
262
  }): ReadonlyArray<string> => {
255
- if (!params.fromRef || !params.toRef) {
256
- return [];
257
- }
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 => {
258
295
  try {
259
- return parseLines(
260
- params.git.runGit(['log', '--format=%s', `${params.fromRef}..${params.toRef}`], params.repoRoot)
261
- );
262
- } catch (error) {
263
- if (isUnresolvableRevisionError(error)) {
264
- return [];
265
- }
266
- throw error;
296
+ params.git.runGit(['merge-base', '--is-ancestor', params.commitHash, params.ref], params.repoRoot);
297
+ return true;
298
+ } catch {
299
+ return false;
267
300
  }
268
301
  };
269
302
 
270
- const collectCommitHashes = (params: {
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: {
271
333
  git: IGitService;
272
334
  repoRoot: string;
273
335
  fromRef?: string;
274
336
  toRef?: string;
275
- }): ReadonlyArray<string> => {
337
+ }): ReadonlyArray<CommitRecord> => {
276
338
  if (!params.fromRef || !params.toRef) {
277
339
  return [];
278
340
  }
279
341
  try {
280
- return parseLines(
281
- params.git.runGit(['log', '--format=%H', '--reverse', `${params.fromRef}..${params.toRef}`], params.repoRoot)
342
+ return parseCommitRecords(
343
+ params.git.runGit(
344
+ ['log', '--format=%H%x01%P%x01%s', '--reverse', `${params.fromRef}..${params.toRef}`],
345
+ params.repoRoot
346
+ )
282
347
  );
283
348
  } catch (error) {
284
349
  if (isUnresolvableRevisionError(error)) {
@@ -367,29 +432,29 @@ const buildPrePushCommitPathLimitViolations = (params: {
367
432
  fromRef?: string;
368
433
  toRef?: string;
369
434
  }): GitAtomicityViolation[] | undefined => {
370
- const commitHashes = collectCommitHashes({
435
+ const commitRecords = collectCommitRecords({
371
436
  git: params.git,
372
437
  repoRoot: params.repoRoot,
373
438
  fromRef: params.fromRef,
374
439
  toRef: params.toRef,
375
- });
376
- if (commitHashes.length === 0) {
440
+ }).filter((record) => shouldEvaluateCommitRecord({ git: params.git, repoRoot: params.repoRoot, record }));
441
+ if (commitRecords.length === 0) {
377
442
  return undefined;
378
443
  }
379
444
 
380
445
  const violations: GitAtomicityViolation[] = [];
381
- for (const commitHash of commitHashes) {
446
+ for (const commitRecord of commitRecords) {
382
447
  const changedPaths = collectCommitChangedPaths({
383
448
  git: params.git,
384
449
  repoRoot: params.repoRoot,
385
- commitHash,
450
+ commitHash: commitRecord.hash,
386
451
  }).filter((path) => !isManagedEvidencePath(path));
387
452
  violations.push(
388
453
  ...buildPathLimitViolations({
389
454
  changedPaths,
390
455
  config: params.config,
391
456
  stage: 'PRE_PUSH',
392
- label: `commit=${commitHash.slice(0, 12)}`,
457
+ label: `commit=${commitRecord.hash.slice(0, 12)}`,
393
458
  })
394
459
  );
395
460
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki",
3
- "version": "6.3.140",
3
+ "version": "6.3.142",
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": {