pumuki-ast-hooks 6.2.1 → 6.2.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pumuki-ast-hooks",
3
- "version": "6.2.1",
3
+ "version": "6.2.2",
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": {
@@ -136,4 +136,4 @@
136
136
  "./skills": "./skills/skill-rules.json",
137
137
  "./hooks": "./hooks/index.js"
138
138
  }
139
- }
139
+ }
@@ -1,5 +1,6 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
+ const { execSync } = require('child_process');
3
4
  const { pushFinding, SyntaxKind, platformOf, getRepoRoot } = require(path.join(__dirname, '../ast-core'));
4
5
  const { BDDTDDWorkflowRules } = require(path.join(__dirname, 'BDDTDDWorkflowRules'));
5
6
 
@@ -71,6 +72,56 @@ function hasTrackForMemoryLeaksEvidence(content) {
71
72
  return hasTrackForMemoryLeaks(content) || hasMakeSUT(content);
72
73
  }
73
74
 
75
+ function loadTestFileContent(filePath) {
76
+ try {
77
+ const diskContent = fs.readFileSync(filePath, 'utf8');
78
+ if (diskContent && diskContent.trim().length > 0) {
79
+ return diskContent;
80
+ }
81
+ } catch (error) {
82
+ if (process.env.DEBUG) {
83
+ console.debug(`[ast-common] Failed to read test file content for ${filePath}: ${error.message}`);
84
+ }
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function resolveTestFileContent(filePath, currentContent) {
90
+ const diskContent = loadTestFileContent(filePath);
91
+ if (diskContent) {
92
+ return diskContent;
93
+ }
94
+
95
+ const repoRoot = getRepoRoot();
96
+ const resolvedPath = path.isAbsolute(filePath) ? filePath : path.join(repoRoot, filePath);
97
+ if (resolvedPath !== filePath) {
98
+ const resolvedContent = loadTestFileContent(resolvedPath);
99
+ if (resolvedContent) {
100
+ return resolvedContent;
101
+ }
102
+ }
103
+
104
+ try {
105
+ const relPath = path.relative(repoRoot, resolvedPath).replace(/\\/g, '/');
106
+ if (relPath && !relPath.startsWith('..')) {
107
+ const stagedContent = execSync(`git show :${relPath}`, {
108
+ encoding: 'utf8',
109
+ cwd: repoRoot,
110
+ stdio: ['ignore', 'pipe', 'ignore']
111
+ });
112
+ if (stagedContent && stagedContent.trim().length > 0) {
113
+ return stagedContent;
114
+ }
115
+ }
116
+ } catch (error) {
117
+ if (process.env.DEBUG) {
118
+ console.debug(`[ast-common] Failed to read staged content for ${filePath}: ${error.message}`);
119
+ }
120
+ }
121
+
122
+ return currentContent;
123
+ }
124
+
74
125
  /**
75
126
  * Detect if a test file is a simple value type test that doesn't need makeSUT/trackForMemoryLeaks.
76
127
  * Simple tests typically:
@@ -275,16 +326,7 @@ function runCommonIntelligence(project, findings) {
275
326
  const isSwiftOrKotlinTest = ext === '.swift' || ext === '.kt' || ext === '.kts';
276
327
 
277
328
  if (isSwiftOrKotlinTest) {
278
- try {
279
- const diskContent = fs.readFileSync(filePath, 'utf8');
280
- if (diskContent && diskContent.trim().length > 0) {
281
- content = diskContent;
282
- }
283
- } catch (error) {
284
- if (process.env.DEBUG) {
285
- console.debug(`[ast-common] Failed to read test file content for ${filePath}: ${error.message}`);
286
- }
287
- }
329
+ content = resolveTestFileContent(filePath, content);
288
330
  }
289
331
 
290
332
  if (isSwiftOrKotlinTest) {
@@ -76,7 +76,7 @@ class WorkflowRules {
76
76
  const featureName = this.extractFeatureName(content);
77
77
 
78
78
  if (featureName) {
79
- const testFiles = glob.sync(`**/*${featureName}*.{test,spec}.{ts,tsx,swift,kt}`, {
79
+ let testFiles = glob.sync(`**/*${featureName}*.{test,spec}.{ts,tsx,swift,kt}`, {
80
80
  cwd: this.projectRoot,
81
81
  absolute: true,
82
82
  nocase: true
@@ -89,6 +89,17 @@ class WorkflowRules {
89
89
  nocase: true
90
90
  });
91
91
 
92
+ if (testFiles.length === 0) {
93
+ const tokens = this.splitFeatureName(featureName);
94
+ if (tokens.length > 1) {
95
+ testFiles = tokens.flatMap(token => glob.sync(`**/*${token}*.{test,spec}.{ts,tsx,swift,kt}`, {
96
+ cwd: this.projectRoot,
97
+ absolute: true,
98
+ nocase: true
99
+ }));
100
+ }
101
+ }
102
+
92
103
  if (testFiles.length === 0) {
93
104
  pushFileFinding(
94
105
  'workflow.triad.feature_without_tests',
@@ -128,6 +139,15 @@ class WorkflowRules {
128
139
  });
129
140
  }
130
141
 
142
+ splitFeatureName(name) {
143
+ const parts = String(name).split(/And|&|_/).map(part => part.trim()).filter(Boolean);
144
+ if (parts.length > 1) {
145
+ return parts;
146
+ }
147
+ const camelParts = String(name).match(/[A-Z][a-z0-9]+/g) || [name];
148
+ return camelParts.length > 0 ? camelParts : [name];
149
+ }
150
+
131
151
  extractFeatureName(content) {
132
152
  const match = content.match(/Feature:\s*(.+)/);
133
153
  return match ? match[1].trim().replace(/\s+/g, '') : null;
@@ -146,7 +146,7 @@ function analyzeMemoryManagement({ content, filePath, addFinding }) {
146
146
  }
147
147
 
148
148
  function analyzeOptionals({ content, filePath, addFinding }) {
149
- const forceUnwraps = content.match(/(\w+)\s*!/g);
149
+ const forceUnwraps = content.match(/\b\w+!/g);
150
150
  if (forceUnwraps && forceUnwraps.length > 0) {
151
151
  const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
152
152
  if (nonIBOutlets.length > 0) {
@@ -451,7 +451,7 @@ function analyzeFunctionAST(analyzer, node, filePath) {
451
451
  const ifStatements = countStatementsOfType(substructure, 'stmt.if');
452
452
  const guardStatements = countStatementsOfType(substructure, 'stmt.guard');
453
453
 
454
- const hasEarlyReturns = (analyzer.fileContent || '').includes('return') && guardStatements > 0;
454
+ const hasEarlyReturns = guardStatements > 0;
455
455
  const nestingLevel = calculateNestingDepth(substructure);
456
456
 
457
457
  if (nestingLevel >= 3 && !hasEarlyReturns) {
@@ -496,8 +496,6 @@ function analyzePropertyAST(analyzer, node, filePath) {
496
496
  }
497
497
 
498
498
  function analyzeClosuresAST(analyzer, filePath) {
499
- const isStruct = analyzer.structs.length > 0;
500
-
501
499
  for (const closure of analyzer.closures) {
502
500
  const closureText = analyzer.safeStringify(closure);
503
501
  const hasSelfReference = closureText.includes('"self"') || closureText.includes('key.name":"self');
@@ -505,7 +503,13 @@ function analyzeClosuresAST(analyzer, filePath) {
505
503
  const parentFunc = closure._parent;
506
504
  const isEscaping = parentFunc && (parentFunc['key.typename'] || '').includes('@escaping');
507
505
 
508
- if (hasSelfReference && isEscaping && !isStruct) {
506
+ let container = closure._parent;
507
+ while (container && !['source.lang.swift.decl.class', 'source.lang.swift.decl.struct'].includes(container['key.kind'])) {
508
+ container = container._parent;
509
+ }
510
+ const isContainerStruct = container && container['key.kind'] === 'source.lang.swift.decl.struct';
511
+
512
+ if (hasSelfReference && isEscaping && !isContainerStruct) {
509
513
  const offset = closure['key.offset'] || 0;
510
514
  const length = closure['key.length'] || 100;
511
515
  const closureCode = (analyzer.fileContent || '').substring(offset, offset + length);
@@ -0,0 +1,57 @@
1
+ const { startPollingLoops } = require('./ast-intelligence-automation');
2
+ const { getCompositionRoot } = require('../../../application/composition-root');
3
+
4
+ jest.mock('../../../application/composition-root');
5
+
6
+ describe('Polling Loops', () => {
7
+ let mockEvidenceMonitor;
8
+
9
+ beforeEach(() => {
10
+ mockEvidenceMonitor = {
11
+ start: jest.fn(),
12
+ refresh: jest.fn(),
13
+ isStale: jest.fn().mockReturnValue(true)
14
+ };
15
+
16
+ getCompositionRoot.mockReturnValue({
17
+ getEvidenceMonitor: () => mockEvidenceMonitor,
18
+ getGitFlowService: jest.fn(),
19
+ getGitQueryAdapter: jest.fn(),
20
+ getOrchestrator: jest.fn()
21
+ });
22
+ });
23
+
24
+ it('should configure evidence monitor refresh interval to 3 minutes', () => {
25
+ const originalSetInterval = setInterval;
26
+ let intervalCallback;
27
+ let intervalTime;
28
+
29
+ global.setInterval = (callback, time) => {
30
+ intervalCallback = callback;
31
+ intervalTime = time;
32
+ return originalSetInterval(callback, time);
33
+ };
34
+
35
+ startPollingLoops();
36
+
37
+ expect(intervalTime).toBe(180000);
38
+ expect(mockEvidenceMonitor.start).toHaveBeenCalled();
39
+
40
+ intervalCallback();
41
+ expect(mockEvidenceMonitor.refresh).toHaveBeenCalled();
42
+
43
+ global.setInterval = originalSetInterval;
44
+ });
45
+
46
+ it('should handle evidence refresh errors with retry', () => {
47
+ mockEvidenceMonitor.refresh.mockRejectedValue(new Error('Refresh failed'));
48
+
49
+ startPollingLoops();
50
+
51
+ const intervalCallback = setInterval.mock.calls[0][0];
52
+ intervalCallback();
53
+
54
+ expect(mockEvidenceMonitor.refresh).toHaveBeenCalled();
55
+ expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 30000);
56
+ });
57
+ });