pumuki-ast-hooks 5.5.60 → 5.5.65
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/README.md +254 -1140
- package/docs/ARCHITECTURE.md +66 -1
- package/docs/TODO.md +41 -0
- package/docs/images/ast_intelligence_01.svg +40 -0
- package/docs/images/ast_intelligence_02.svg +39 -0
- package/docs/images/ast_intelligence_03.svg +55 -0
- package/docs/images/ast_intelligence_04.svg +39 -0
- package/docs/images/ast_intelligence_05.svg +45 -0
- package/docs/images/logo.png +0 -0
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +16 -0
- package/scripts/hooks-system/application/DIValidationService.js +43 -0
- package/scripts/hooks-system/application/__tests__/DIValidationService.spec.js +81 -0
- package/scripts/hooks-system/config/di-rules.json +42 -0
- package/scripts/hooks-system/domain/ports/FileSystemPort.js +19 -0
- package/scripts/hooks-system/domain/strategies/ConcreteDependencyStrategy.js +78 -0
- package/scripts/hooks-system/domain/strategies/DIStrategy.js +31 -0
- package/scripts/hooks-system/infrastructure/adapters/NodeFileSystemAdapter.js +28 -0
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +16 -0
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +28 -8
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +133 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +3 -2
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +1 -1
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +40 -46
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +87 -1
- package/scripts/hooks-system/infrastructure/registry/StrategyRegistry.js +63 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const DIStrategy = require('./DIStrategy');
|
|
2
|
+
|
|
3
|
+
class ConcreteDependencyStrategy extends DIStrategy {
|
|
4
|
+
constructor(config) {
|
|
5
|
+
super('ios.solid.dip.concrete_dependency', config);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
canHandle(node, context) {
|
|
9
|
+
const { className } = context;
|
|
10
|
+
return this.config.targetClasses.some(target => className.includes(target));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
detect(node, context) {
|
|
14
|
+
const { properties, className, filePath } = context;
|
|
15
|
+
const violations = [];
|
|
16
|
+
|
|
17
|
+
for (const prop of properties) {
|
|
18
|
+
const typename = prop['key.typename'] || '';
|
|
19
|
+
const propName = prop['key.name'] || '';
|
|
20
|
+
|
|
21
|
+
if (this._shouldSkipType(typename, propName, className)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (this._isConcreteService(typename)) {
|
|
26
|
+
violations.push({
|
|
27
|
+
property: propName,
|
|
28
|
+
type: typename,
|
|
29
|
+
message: `'${className}' depends on concrete '${typename}' - use protocol`
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return violations;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_shouldSkipType(typename, propName, className) {
|
|
38
|
+
if (this.config.allowedTypes.includes(typename)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (this._isGenericTypeParameter(typename, propName, className)) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_isGenericTypeParameter(typename, propName, className) {
|
|
50
|
+
const patterns = this.config.genericTypePatterns;
|
|
51
|
+
|
|
52
|
+
const isSingleLetter = patterns.singleLetter && typename.length === 1;
|
|
53
|
+
|
|
54
|
+
const isCamelCase = patterns.camelCase &&
|
|
55
|
+
new RegExp(patterns.camelCase).test(typename) &&
|
|
56
|
+
!typename.includes('Impl');
|
|
57
|
+
|
|
58
|
+
const hasContextHint = patterns.contextHints.some(hint =>
|
|
59
|
+
className.includes(hint) || propName === hint
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return isSingleLetter || (isCamelCase && hasContextHint);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
_isConcreteService(typename) {
|
|
66
|
+
const hasConcretePattern = this.config.concretePatterns.some(pattern =>
|
|
67
|
+
new RegExp(pattern).test(typename)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const hasProtocolIndicator = this.config.protocolIndicators.some(indicator =>
|
|
71
|
+
typename.includes(indicator)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return hasConcretePattern && !hasProtocolIndicator;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = ConcreteDependencyStrategy;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class DIStrategy {
|
|
2
|
+
constructor(id, config) {
|
|
3
|
+
this.id = id;
|
|
4
|
+
this.config = config;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
canHandle(node, context) {
|
|
8
|
+
throw new Error('canHandle must be implemented');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
detect(node, context) {
|
|
12
|
+
throw new Error('detect must be implemented');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getSeverity() {
|
|
16
|
+
return this.config.severity || 'medium';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
report(violation, context) {
|
|
20
|
+
const { analyzer, filePath, line } = context;
|
|
21
|
+
analyzer.pushFinding(
|
|
22
|
+
this.id,
|
|
23
|
+
this.getSeverity(),
|
|
24
|
+
filePath,
|
|
25
|
+
line,
|
|
26
|
+
violation.message
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = DIStrategy;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require('fs').promises;
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const FileSystemPort = require('../../domain/ports/FileSystemPort');
|
|
4
|
+
|
|
5
|
+
class NodeFileSystemAdapter extends FileSystemPort {
|
|
6
|
+
async readDir(dirPath) {
|
|
7
|
+
return fs.readdir(dirPath);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async readFile(filePath, encoding = 'utf8') {
|
|
11
|
+
return fs.readFile(filePath, encoding);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
resolvePath(...paths) {
|
|
15
|
+
return path.resolve(...paths);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async exists(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
await fs.access(filePath);
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = NodeFileSystemAdapter;
|
|
@@ -125,6 +125,16 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
125
125
|
console.error(`[GOD CLASS BASELINE] Skipping non-backend file: ${filePath}`);
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
+
if (
|
|
129
|
+
/\/infrastructure\/ast\//i.test(filePath) ||
|
|
130
|
+
/\/analyzers?\//i.test(filePath) ||
|
|
131
|
+
/\/detectors?\//i.test(filePath) ||
|
|
132
|
+
/\/ios\/analyzers\//i.test(filePath) ||
|
|
133
|
+
filePath.endsWith('/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js')
|
|
134
|
+
) {
|
|
135
|
+
console.error(`[GOD CLASS BASELINE] Skipping AST infra/analyzer/detector file: ${filePath}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
128
138
|
// NO excluir archivos AST - la librería debe auto-auditarse
|
|
129
139
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
130
140
|
const className = cls.getName() || '';
|
|
@@ -148,6 +158,12 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
148
158
|
return null;
|
|
149
159
|
}
|
|
150
160
|
|
|
161
|
+
const minSamples = env.getNumber('AST_GODCLASS_MIN_BASELINE_SAMPLES', 30);
|
|
162
|
+
if (metrics.length < minSamples) {
|
|
163
|
+
console.error(`[GOD CLASS BASELINE] Insufficient samples for baseline: ${metrics.length} < ${minSamples}. Falling back to absolute thresholds.`);
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
151
167
|
console.error(`[GOD CLASS BASELINE] Collected ${metrics.length} class metrics for baseline`);
|
|
152
168
|
|
|
153
169
|
const pOutlier = env.getNumber('AST_GODCLASS_P_OUTLIER', 90);
|
|
@@ -2,7 +2,16 @@
|
|
|
2
2
|
* God Class detector extracted from ast-backend.js to keep responsibilities separated.
|
|
3
3
|
*/
|
|
4
4
|
function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godClassBaseline }) {
|
|
5
|
-
|
|
5
|
+
const filePath = sourceFile.getFilePath().replace(/\\/g, '/');
|
|
6
|
+
if (
|
|
7
|
+
/\/infrastructure\/ast\//i.test(filePath) ||
|
|
8
|
+
/\/analyzers?\//i.test(filePath) ||
|
|
9
|
+
/\/detectors?\//i.test(filePath) ||
|
|
10
|
+
/\/ios\/analyzers\//i.test(filePath) ||
|
|
11
|
+
filePath.endsWith('/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js')
|
|
12
|
+
) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
6
15
|
|
|
7
16
|
sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
8
17
|
const className = cls.getName() || '';
|
|
@@ -42,6 +51,24 @@ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godC
|
|
|
42
51
|
if (/\bgit\b|rev-parse|git diff|git status|git log/i.test(clsText)) concerns.add('git');
|
|
43
52
|
const concernCount = concerns.size;
|
|
44
53
|
|
|
54
|
+
const isMassiveFile = lineCount > 500;
|
|
55
|
+
const isAbsoluteGod = lineCount > 1000 ||
|
|
56
|
+
(lineCount > 500 && complexity > 50) ||
|
|
57
|
+
(lineCount > 500 && methodsCount > 20) ||
|
|
58
|
+
(lineCount > 600 && methodsCount > 30 && complexity > 80);
|
|
59
|
+
const isUnderThreshold = lineCount < 300 && methodsCount < 15 && complexity < 30;
|
|
60
|
+
|
|
61
|
+
if (!godClassBaseline) {
|
|
62
|
+
if (!isUnderThreshold && (isMassiveFile || isAbsoluteGod)) {
|
|
63
|
+
console.error(`[GOD CLASS DEBUG] ${className}: methods=${methodsCount}, props=${propertiesCount}, lines=${lineCount}, complexity=${complexity}, concerns=${concernCount}, isAbsoluteGod=${isAbsoluteGod}, signalCount=ABSOLUTE`);
|
|
64
|
+
pushFinding("backend.antipattern.god_classes", "critical", sourceFile, cls,
|
|
65
|
+
`God class detected: ${methodsCount} methods, ${propertiesCount} properties, ${lineCount} lines, complexity ${complexity}, concerns ${concernCount} - VIOLATES SRP`,
|
|
66
|
+
findings
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
45
72
|
const methodsZ = godClassBaseline.robustZ(methodsCount, godClassBaseline.med.methodsCount, godClassBaseline.mad.methodsCount);
|
|
46
73
|
const propsZ = godClassBaseline.robustZ(propertiesCount, godClassBaseline.med.propertiesCount, godClassBaseline.mad.propertiesCount);
|
|
47
74
|
const linesZ = godClassBaseline.robustZ(lineCount, godClassBaseline.med.lineCount, godClassBaseline.mad.lineCount);
|
|
@@ -54,13 +81,6 @@ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godC
|
|
|
54
81
|
const complexityOutlier = complexityZ >= godClassBaseline.thresholds.outlier.complexityZ;
|
|
55
82
|
const concernOutlier = concernCount >= godClassBaseline.thresholds.outlier.concerns;
|
|
56
83
|
|
|
57
|
-
const isMassiveFile = lineCount > 500;
|
|
58
|
-
const isAbsoluteGod = lineCount > 1000 ||
|
|
59
|
-
(lineCount > 500 && complexity > 50) ||
|
|
60
|
-
(lineCount > 500 && methodsCount > 20) ||
|
|
61
|
-
(lineCount > 600 && methodsCount > 30 && complexity > 80);
|
|
62
|
-
const isUnderThreshold = lineCount < 300 && methodsCount < 15 && complexity < 30;
|
|
63
|
-
|
|
64
84
|
let signalCount = 0;
|
|
65
85
|
if (sizeOutlier) signalCount++;
|
|
66
86
|
if (complexityOutlier) signalCount++;
|
|
@@ -2,6 +2,98 @@ const path = require('path');
|
|
|
2
2
|
const { pushFinding, SyntaxKind, platformOf, getRepoRoot } = require(path.join(__dirname, '../ast-core'));
|
|
3
3
|
const { BDDTDDWorkflowRules } = require(path.join(__dirname, 'BDDTDDWorkflowRules'));
|
|
4
4
|
|
|
5
|
+
function normalizePath(filePath) {
|
|
6
|
+
return String(filePath || '').replace(/\\/g, '/');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isTestFilePath(filePath) {
|
|
10
|
+
const p = normalizePath(filePath).toLowerCase();
|
|
11
|
+
const isInTestFolder = /\/(tests?|__tests__|test|spec|e2e|uitests)\//i.test(p);
|
|
12
|
+
const hasTestName = /(\.spec\.|\.test\.|tests\.swift$|test\.swift$|tests\.kt$|test\.kt$|tests\.kts$|test\.kts$)/i.test(p);
|
|
13
|
+
const isSwiftTests = p.endsWith('tests.swift') || (p.includes('/tests/') && p.endsWith('tests.swift'));
|
|
14
|
+
const isXcTest = p.includes('xctest') || p.includes('uitests');
|
|
15
|
+
|
|
16
|
+
const isSwiftFile = p.endsWith('.swift');
|
|
17
|
+
if (isSwiftFile) {
|
|
18
|
+
return hasTestName || isSwiftTests || isXcTest;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return isInTestFolder || hasTestName || isSwiftTests || isXcTest;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function matchesAnyGlob(filePath, rawGlobs) {
|
|
25
|
+
const globs = String(rawGlobs || '')
|
|
26
|
+
.split(',')
|
|
27
|
+
.map(s => s.trim())
|
|
28
|
+
.filter(Boolean);
|
|
29
|
+
if (globs.length === 0) return false;
|
|
30
|
+
|
|
31
|
+
const p = normalizePath(filePath);
|
|
32
|
+
|
|
33
|
+
for (const pattern of globs) {
|
|
34
|
+
const escaped = pattern
|
|
35
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
36
|
+
.replace(/\*\*/g, '.*')
|
|
37
|
+
.replace(/\*/g, '[^/]*');
|
|
38
|
+
try {
|
|
39
|
+
const re = new RegExp(`^${escaped}$`, 'i');
|
|
40
|
+
if (re.test(p)) return true;
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shouldSkipSpyOverMockRule(filePath) {
|
|
50
|
+
return matchesAnyGlob(filePath, process.env.AST_TESTING_SPY_OVER_MOCK_EXCEPTIONS);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function shouldSkipMakeSUTRule(filePath) {
|
|
54
|
+
return matchesAnyGlob(filePath, process.env.AST_TESTING_MAKESUT_EXCEPTIONS);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function shouldSkipTrackForMemoryLeaksRule(filePath) {
|
|
58
|
+
return matchesAnyGlob(filePath, process.env.AST_TESTING_TRACK_FOR_MEMORY_LEAKS_EXCEPTIONS);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function hasMakeSUT(content) {
|
|
62
|
+
return /\bmakeSUT\s*\(/.test(content) || /\bmakeSut\s*\(/.test(content);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasTrackForMemoryLeaks(content) {
|
|
66
|
+
return /\btrackForMemoryLeaks\s*\(/.test(content);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function hasTrackForMemoryLeaksEvidence(content) {
|
|
70
|
+
return hasTrackForMemoryLeaks(content) || hasMakeSUT(content);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function detectMockSignals(content) {
|
|
74
|
+
const signals = [
|
|
75
|
+
/\bjest\.mock\s*\(/,
|
|
76
|
+
/\bvi\.mock\s*\(/,
|
|
77
|
+
/\bmockk\b/i,
|
|
78
|
+
/\bMockito\b/,
|
|
79
|
+
/\bmock\s*\(/i,
|
|
80
|
+
/\bclass\s+Mock[A-Za-z0-9_]*/,
|
|
81
|
+
/\bstruct\s+Mock[A-Za-z0-9_]*/,
|
|
82
|
+
/\binterface\s+Mock[A-Za-z0-9_]*/,
|
|
83
|
+
];
|
|
84
|
+
return signals.some((re) => re.test(content));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function detectSpySignals(content) {
|
|
88
|
+
const signals = [
|
|
89
|
+
/\bjest\.spyOn\s*\(/,
|
|
90
|
+
/\bvi\.spyOn\s*\(/,
|
|
91
|
+
/\bspy\s*\(/i,
|
|
92
|
+
/\bSpy[A-Za-z0-9_]*\b/,
|
|
93
|
+
];
|
|
94
|
+
return signals.some((re) => re.test(content));
|
|
95
|
+
}
|
|
96
|
+
|
|
5
97
|
function getTypeContext(node) {
|
|
6
98
|
const parent = node.getParent();
|
|
7
99
|
if (!parent) return 'unknown';
|
|
@@ -115,6 +207,47 @@ function runCommonIntelligence(project, findings) {
|
|
|
115
207
|
if (/\/hooks-system\/infrastructure\/ast\//i.test(filePath)) return;
|
|
116
208
|
if (/\/ast-(?:backend|frontend|android|ios|common|core|intelligence)\.js$/.test(filePath)) return;
|
|
117
209
|
|
|
210
|
+
if (isTestFilePath(filePath)) {
|
|
211
|
+
const content = sf.getFullText();
|
|
212
|
+
|
|
213
|
+
if (!shouldSkipMakeSUTRule(filePath) && !hasMakeSUT(content)) {
|
|
214
|
+
pushFinding(
|
|
215
|
+
'common.testing.missing_makesut',
|
|
216
|
+
'high',
|
|
217
|
+
sf,
|
|
218
|
+
sf,
|
|
219
|
+
'Test file without makeSUT factory - use makeSUT pattern for consistent DI and teardown',
|
|
220
|
+
findings
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!shouldSkipTrackForMemoryLeaksRule(filePath) && !hasTrackForMemoryLeaksEvidence(content)) {
|
|
225
|
+
pushFinding(
|
|
226
|
+
'common.testing.missing_track_for_memory_leaks',
|
|
227
|
+
'critical',
|
|
228
|
+
sf,
|
|
229
|
+
sf,
|
|
230
|
+
'Test file without trackForMemoryLeaks - add memory leak tracking to prevent leaks and cross-test interference',
|
|
231
|
+
findings
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (!shouldSkipSpyOverMockRule(filePath)) {
|
|
236
|
+
const hasMocks = detectMockSignals(content);
|
|
237
|
+
const hasSpies = detectSpySignals(content);
|
|
238
|
+
if (hasMocks && !hasSpies) {
|
|
239
|
+
pushFinding(
|
|
240
|
+
'common.testing.prefer_spy_over_mock',
|
|
241
|
+
'high',
|
|
242
|
+
sf,
|
|
243
|
+
sf,
|
|
244
|
+
'Mocks detected in tests - prefer spies when possible to keep behavior closer to real implementations',
|
|
245
|
+
findings
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
118
251
|
|
|
119
252
|
sf.getDescendantsOfKind(SyntaxKind.TypeReference).forEach((tref) => {
|
|
120
253
|
const txt = tref.getText();
|
|
@@ -10,6 +10,7 @@ const {
|
|
|
10
10
|
finalizeGodClassDetection,
|
|
11
11
|
} = require('../detectors/ios-ast-intelligent-strategies');
|
|
12
12
|
|
|
13
|
+
|
|
13
14
|
class iOSASTIntelligentAnalyzer {
|
|
14
15
|
constructor(findings) {
|
|
15
16
|
this.findings = findings;
|
|
@@ -122,7 +123,7 @@ class iOSASTIntelligentAnalyzer {
|
|
|
122
123
|
}
|
|
123
124
|
}
|
|
124
125
|
|
|
125
|
-
analyzeFile(filePath, options = {}) {
|
|
126
|
+
async analyzeFile(filePath, options = {}) {
|
|
126
127
|
if (!filePath || !String(filePath).endsWith('.swift')) return;
|
|
127
128
|
|
|
128
129
|
const repoRoot = options.repoRoot || require('../ast-core').getRepoRoot();
|
|
@@ -175,7 +176,7 @@ class iOSASTIntelligentAnalyzer {
|
|
|
175
176
|
const substructure = ast['key.substructure'] || [];
|
|
176
177
|
|
|
177
178
|
collectAllNodes(this, substructure, null);
|
|
178
|
-
analyzeCollectedNodes(this, displayPath);
|
|
179
|
+
await analyzeCollectedNodes(this, displayPath);
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
safeStringify(obj) {
|
|
@@ -62,7 +62,7 @@ async function runIOSIntelligence(project, findings, platform) {
|
|
|
62
62
|
|
|
63
63
|
for (const swiftFile of swiftFilesForAST) {
|
|
64
64
|
const rel = stagingOnly ? path.relative(root, swiftFile).replace(/\\/g, '/') : null;
|
|
65
|
-
astAnalyzer.analyzeFile(swiftFile, {
|
|
65
|
+
await astAnalyzer.analyzeFile(swiftFile, {
|
|
66
66
|
repoRoot: root,
|
|
67
67
|
displayPath: swiftFile,
|
|
68
68
|
stagedRelPath: stagingOnly ? rel : null
|
package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const DIValidationService = require('../../application/DIValidationService');
|
|
3
|
+
|
|
4
|
+
const diValidationService = new DIValidationService();
|
|
2
5
|
|
|
3
6
|
function resetCollections(analyzer) {
|
|
4
7
|
analyzer.allNodes = [];
|
|
@@ -37,13 +40,13 @@ function collectAllNodes(analyzer, nodes, parent) {
|
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
40
|
-
function analyzeCollectedNodes(analyzer, filePath) {
|
|
43
|
+
async function analyzeCollectedNodes(analyzer, filePath) {
|
|
41
44
|
extractImports(analyzer);
|
|
42
45
|
|
|
43
46
|
analyzeImportsAST(analyzer, filePath);
|
|
44
47
|
|
|
45
48
|
for (const cls of analyzer.classes) {
|
|
46
|
-
analyzeClassAST(analyzer, cls, filePath);
|
|
49
|
+
await analyzeClassAST(analyzer, cls, filePath);
|
|
47
50
|
}
|
|
48
51
|
|
|
49
52
|
for (const struct of analyzer.structs) {
|
|
@@ -134,7 +137,7 @@ function analyzeImportsAST(analyzer, filePath) {
|
|
|
134
137
|
}
|
|
135
138
|
}
|
|
136
139
|
|
|
137
|
-
function analyzeClassAST(analyzer, node, filePath) {
|
|
140
|
+
async function analyzeClassAST(analyzer, node, filePath) {
|
|
138
141
|
const name = node['key.name'] || '';
|
|
139
142
|
const line = node['key.line'] || 1;
|
|
140
143
|
const bodyLength = countLinesInBody(analyzer, node) || 0;
|
|
@@ -233,15 +236,8 @@ function analyzeClassAST(analyzer, node, filePath) {
|
|
|
233
236
|
}
|
|
234
237
|
|
|
235
238
|
// Skip ISP validation for test files - spies/mocks are allowed to have unused properties
|
|
236
|
-
<<<<<<< HEAD
|
|
237
|
-
// Also skip ObservableObject classes - their @Published properties are inherently observed externally
|
|
238
|
-
const isTestFile = /Tests?\/|Spec|Mock|Spy|Stub|Fake|Dummy/.test(filePath);
|
|
239
|
-
const isObservableObject = inheritedTypes.some((t) => t['key.name'] === 'ObservableObject');
|
|
240
|
-
if (!isTestFile && !isObservableObject) {
|
|
241
|
-
=======
|
|
242
239
|
const isTestFile = /Tests?\/|Spec|Mock|Spy|Stub|Fake|Dummy/.test(filePath);
|
|
243
240
|
if (!isTestFile) {
|
|
244
|
-
>>>>>>> origin/main
|
|
245
241
|
const unusedProps = findUnusedPropertiesAST(analyzer, properties, methods);
|
|
246
242
|
for (const prop of unusedProps) {
|
|
247
243
|
analyzer.pushFinding(
|
|
@@ -254,7 +250,7 @@ function analyzeClassAST(analyzer, node, filePath) {
|
|
|
254
250
|
}
|
|
255
251
|
}
|
|
256
252
|
|
|
257
|
-
checkDependencyInjectionAST(analyzer, properties, filePath, name, line);
|
|
253
|
+
await checkDependencyInjectionAST(analyzer, properties, filePath, name, line);
|
|
258
254
|
|
|
259
255
|
const hasDeinit = methods.some((m) => m['key.name'] === 'deinit');
|
|
260
256
|
const hasObservers = analyzer.closures.some((c) => c._parent === node) || properties.some((p) => analyzer.hasAttribute(p, 'Published'));
|
|
@@ -564,8 +560,10 @@ function analyzeAdditionalRules(analyzer, filePath) {
|
|
|
564
560
|
}
|
|
565
561
|
|
|
566
562
|
for (const cls of analyzer.classes) {
|
|
567
|
-
const
|
|
568
|
-
|
|
563
|
+
const fileContent = analyzer.fileContent || '';
|
|
564
|
+
const hasSharedState = /\bstatic\s+var\b/.test(fileContent);
|
|
565
|
+
const hasActorIsolation = fileContent.includes('actor ') || fileContent.includes('@MainActor');
|
|
566
|
+
if (hasSharedState && !hasActorIsolation) {
|
|
569
567
|
const name = cls['key.name'] || '';
|
|
570
568
|
const line = cls['key.line'] || 1;
|
|
571
569
|
analyzer.pushFinding('ios.concurrency.missing_actor', 'high', filePath, line, `Class '${name}' has shared state - consider actor for thread safety`);
|
|
@@ -595,15 +593,38 @@ function findUnusedPropertiesAST(analyzer, properties, methods) {
|
|
|
595
593
|
continue;
|
|
596
594
|
}
|
|
597
595
|
|
|
598
|
-
|
|
596
|
+
const typeName = String(prop['key.typename'] || '');
|
|
597
|
+
const isReactiveType =
|
|
598
|
+
typeName.includes('Publisher') ||
|
|
599
|
+
typeName.includes('AnyPublisher') ||
|
|
600
|
+
typeName.includes('Subject') ||
|
|
601
|
+
typeName.includes('PassthroughSubject') ||
|
|
602
|
+
typeName.includes('CurrentValueSubject') ||
|
|
603
|
+
typeName.includes('Published<');
|
|
604
|
+
|
|
605
|
+
if (isReactiveType) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const escapedPropName = String(propName).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
610
|
+
const usagePattern = new RegExp(`(^|[^A-Za-z0-9_])\\$?${escapedPropName}([^A-Za-z0-9_]|$)`);
|
|
611
|
+
|
|
612
|
+
let isUsed = false;
|
|
613
|
+
|
|
614
|
+
const fileContent = analyzer.fileContent || '';
|
|
615
|
+
const fileMatches = fileContent.match(new RegExp(`\\b${escapedPropName}\\b`, 'g')) || [];
|
|
616
|
+
if (fileMatches.length > 1) {
|
|
617
|
+
isUsed = true;
|
|
618
|
+
}
|
|
599
619
|
for (const method of methods) {
|
|
600
620
|
const methodText = analyzer.safeStringify(method);
|
|
601
|
-
if (
|
|
602
|
-
|
|
621
|
+
if (usagePattern.test(methodText)) {
|
|
622
|
+
isUsed = true;
|
|
623
|
+
break;
|
|
603
624
|
}
|
|
604
625
|
}
|
|
605
626
|
|
|
606
|
-
if (
|
|
627
|
+
if (!isUsed) {
|
|
607
628
|
unused.push(propName);
|
|
608
629
|
}
|
|
609
630
|
}
|
|
@@ -657,35 +678,8 @@ function countStatementsOfType(substructure, stmtType) {
|
|
|
657
678
|
return count;
|
|
658
679
|
}
|
|
659
680
|
|
|
660
|
-
function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
|
|
661
|
-
|
|
662
|
-
return;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
for (const prop of properties) {
|
|
666
|
-
const typename = prop['key.typename'] || '';
|
|
667
|
-
|
|
668
|
-
if (['String', 'Int', 'Bool', 'Double', 'Float', 'Date', 'URL', 'Data'].includes(typename)) {
|
|
669
|
-
continue;
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
// Skip generic type parameters (e.g., "Client" in LoginUseCaseImpl<Client: APIClientProtocol>)
|
|
673
|
-
// These are not concrete dependencies - they are constrained by protocols
|
|
674
|
-
const propName = prop['key.name'] || '';
|
|
675
|
-
const isGenericTypeParameter = typename.length === 1 ||
|
|
676
|
-
(typename.match(/^[A-Z][a-z]*$/) && !typename.includes('Impl') &&
|
|
677
|
-
(className.includes('<') || propName === 'apiClient' || propName === 'client'));
|
|
678
|
-
|
|
679
|
-
if (isGenericTypeParameter) {
|
|
680
|
-
continue;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
const isConcreteService = /Service$|Repository$|UseCase$|Client$/.test(typename) && !typename.includes('Protocol') && !typename.includes('any ') && !typename.includes('some ');
|
|
684
|
-
|
|
685
|
-
if (isConcreteService) {
|
|
686
|
-
analyzer.pushFinding('ios.solid.dip.concrete_dependency', 'high', filePath, line, `'${className}' depends on concrete '${typename}' - use protocol`);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
681
|
+
async function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
|
|
682
|
+
await diValidationService.validateDependencyInjection(analyzer, properties, filePath, className, line);
|
|
689
683
|
}
|
|
690
684
|
|
|
691
685
|
function finalizeGodClassDetection(analyzer) {
|
|
@@ -25,6 +25,88 @@ function deriveCategoryFromRuleId(ruleId) {
|
|
|
25
25
|
return parts[0] || 'unknown';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Generate semantic_snapshot automatically from current evidence state.
|
|
30
|
+
* This is the Semantic Memory Layer - derived, never manually input.
|
|
31
|
+
*/
|
|
32
|
+
function generateSemanticSnapshot(evidence, violations, gateResult) {
|
|
33
|
+
const now = new Date();
|
|
34
|
+
const activePlatforms = Object.entries(evidence.platforms || {})
|
|
35
|
+
.filter(([, v]) => v.detected)
|
|
36
|
+
.map(([k]) => k);
|
|
37
|
+
|
|
38
|
+
const violationSummary = violations.length > 0
|
|
39
|
+
? violations.slice(0, 5).map(v => `${v.severity}: ${v.ruleId || v.rule || 'unknown'}`).join('; ')
|
|
40
|
+
: 'No violations';
|
|
41
|
+
|
|
42
|
+
const healthScore = Math.max(0, 100 - (violations.length * 5) -
|
|
43
|
+
(violations.filter(v => v.severity === 'CRITICAL').length * 20) -
|
|
44
|
+
(violations.filter(v => v.severity === 'HIGH').length * 10));
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
generated_at: now.toISOString(),
|
|
48
|
+
derivation_source: 'auto:updateAIEvidence',
|
|
49
|
+
context_hash: `ctx-${Date.now().toString(36)}`,
|
|
50
|
+
summary: {
|
|
51
|
+
health_score: healthScore,
|
|
52
|
+
gate_status: gateResult.passed ? 'PASSED' : 'FAILED',
|
|
53
|
+
active_platforms: activePlatforms,
|
|
54
|
+
violation_count: violations.length,
|
|
55
|
+
violation_preview: violationSummary,
|
|
56
|
+
branch: evidence.current_context?.current_branch || 'unknown',
|
|
57
|
+
session_id: evidence.session_id || 'unknown'
|
|
58
|
+
},
|
|
59
|
+
feature_state: {
|
|
60
|
+
ai_gate_enabled: true,
|
|
61
|
+
token_monitoring: evidence.watchers?.token_monitor?.enabled ?? true,
|
|
62
|
+
auto_refresh: evidence.watchers?.evidence_watcher?.auto_refresh ?? true,
|
|
63
|
+
protocol_3_active: evidence.protocol_3_questions?.answered ?? false
|
|
64
|
+
},
|
|
65
|
+
decisions: {
|
|
66
|
+
last_gate_decision: gateResult.passed ? 'allow' : 'block',
|
|
67
|
+
blocking_reason: gateResult.blockedBy || null,
|
|
68
|
+
recommended_action: gateResult.passed
|
|
69
|
+
? 'proceed_with_development'
|
|
70
|
+
: 'fix_violations_before_commit'
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Preserve or initialize human_intent layer.
|
|
77
|
+
* This is the Intentional Memory Layer - set by human, preserved across updates.
|
|
78
|
+
*/
|
|
79
|
+
function preserveOrInitHumanIntent(existingEvidence) {
|
|
80
|
+
const existing = existingEvidence.human_intent;
|
|
81
|
+
|
|
82
|
+
if (existing && typeof existing === 'object' && existing.primary_goal) {
|
|
83
|
+
const expiresAt = existing.expires_at ? new Date(existing.expires_at) : null;
|
|
84
|
+
const isExpired = expiresAt && expiresAt < new Date();
|
|
85
|
+
|
|
86
|
+
if (!isExpired) {
|
|
87
|
+
return {
|
|
88
|
+
...existing,
|
|
89
|
+
preserved_at: new Date().toISOString(),
|
|
90
|
+
preservation_count: (existing.preservation_count || 0) + 1
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
primary_goal: null,
|
|
97
|
+
secondary_goals: [],
|
|
98
|
+
non_goals: [],
|
|
99
|
+
constraints: [],
|
|
100
|
+
confidence_level: 'unset',
|
|
101
|
+
set_by: null,
|
|
102
|
+
set_at: null,
|
|
103
|
+
expires_at: null,
|
|
104
|
+
preserved_at: new Date().toISOString(),
|
|
105
|
+
preservation_count: 0,
|
|
106
|
+
_hint: 'Set via CLI: ast-hooks intent --goal "your goal" or manually edit this file'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
28
110
|
function detectPlatformsFromStagedFiles(stagedFiles) {
|
|
29
111
|
const platforms = new Set();
|
|
30
112
|
const files = Array.isArray(stagedFiles) ? stagedFiles : [];
|
|
@@ -669,8 +751,12 @@ async function updateAIEvidence(violations, gateResult, tokenUsage) {
|
|
|
669
751
|
}
|
|
670
752
|
};
|
|
671
753
|
|
|
754
|
+
evidence.human_intent = preserveOrInitHumanIntent(evidence);
|
|
755
|
+
|
|
756
|
+
evidence.semantic_snapshot = generateSemanticSnapshot(evidence, violations, gateResult);
|
|
757
|
+
|
|
672
758
|
fs.writeFileSync(evidencePath, JSON.stringify(evidence, null, 2));
|
|
673
|
-
console.log('[Intelligent Audit] ✅ .AI_EVIDENCE.json updated with complete format (ai_gate, severity_metrics, token_usage, git_flow, watchers)');
|
|
759
|
+
console.log('[Intelligent Audit] ✅ .AI_EVIDENCE.json updated with complete format (ai_gate, severity_metrics, token_usage, git_flow, watchers, human_intent, semantic_snapshot)');
|
|
674
760
|
|
|
675
761
|
const MacNotificationSender = require('../../application/services/notification/MacNotificationSender');
|
|
676
762
|
const notificationSender = new MacNotificationSender(null);
|