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 +2 -2
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +52 -10
- package/scripts/hooks-system/infrastructure/ast/common/rules/WorkflowRules.js +21 -1
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +8 -4
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.spec.js +57 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "6.2.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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) {
|
package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js
CHANGED
|
@@ -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 =
|
|
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
|
-
|
|
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
|
+
});
|