pumuki-ast-hooks 5.5.58 → 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 +2 -2
- 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/application/services/installation/McpConfigurator.js +0 -3
- 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 +138 -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 +41 -40
- 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();
|
|
@@ -281,6 +414,11 @@ function runCommonIntelligence(project, findings) {
|
|
|
281
414
|
|
|
282
415
|
const full = sf.getFullText();
|
|
283
416
|
const isSpecFile = /\.(spec|test)\.(ts|tsx|js|jsx)$/.test(filePath);
|
|
417
|
+
const isSwiftFile = /\.swift$/i.test(filePath);
|
|
418
|
+
|
|
419
|
+
// Skip secret detection for Swift struct/class properties - they're not hardcoded secrets
|
|
420
|
+
if (isSwiftFile) return;
|
|
421
|
+
|
|
284
422
|
const secretPattern = /(PASSWORD|TOKEN|SECRET|API_KEY)\s*[:=]\s*['"]([^'"]{8,})['"]/gi;
|
|
285
423
|
const matches = Array.from(full.matchAll(secretPattern));
|
|
286
424
|
|
|
@@ -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;
|
|
@@ -153,7 +156,7 @@ function analyzeClassAST(analyzer, node, filePath) {
|
|
|
153
156
|
return true;
|
|
154
157
|
});
|
|
155
158
|
|
|
156
|
-
if (name && !/Spec$|Test$|Mock/.test(name)) {
|
|
159
|
+
if (name && !/Spec$|Test$|Mock/.test(name) && !name.includes('Coordinator')) {
|
|
157
160
|
const complexity = calculateComplexityAST(substructure);
|
|
158
161
|
analyzer.godClassCandidates.push({
|
|
159
162
|
name,
|
|
@@ -247,7 +250,7 @@ function analyzeClassAST(analyzer, node, filePath) {
|
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
checkDependencyInjectionAST(analyzer, properties, filePath, name, line);
|
|
253
|
+
await checkDependencyInjectionAST(analyzer, properties, filePath, name, line);
|
|
251
254
|
|
|
252
255
|
const hasDeinit = methods.some((m) => m['key.name'] === 'deinit');
|
|
253
256
|
const hasObservers = analyzer.closures.some((c) => c._parent === node) || properties.some((p) => analyzer.hasAttribute(p, 'Published'));
|
|
@@ -557,8 +560,10 @@ function analyzeAdditionalRules(analyzer, filePath) {
|
|
|
557
560
|
}
|
|
558
561
|
|
|
559
562
|
for (const cls of analyzer.classes) {
|
|
560
|
-
const
|
|
561
|
-
|
|
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) {
|
|
562
567
|
const name = cls['key.name'] || '';
|
|
563
568
|
const line = cls['key.line'] || 1;
|
|
564
569
|
analyzer.pushFinding('ios.concurrency.missing_actor', 'high', filePath, line, `Class '${name}' has shared state - consider actor for thread safety`);
|
|
@@ -588,15 +593,38 @@ function findUnusedPropertiesAST(analyzer, properties, methods) {
|
|
|
588
593
|
continue;
|
|
589
594
|
}
|
|
590
595
|
|
|
591
|
-
|
|
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
|
+
}
|
|
592
619
|
for (const method of methods) {
|
|
593
620
|
const methodText = analyzer.safeStringify(method);
|
|
594
|
-
if (
|
|
595
|
-
|
|
621
|
+
if (usagePattern.test(methodText)) {
|
|
622
|
+
isUsed = true;
|
|
623
|
+
break;
|
|
596
624
|
}
|
|
597
625
|
}
|
|
598
626
|
|
|
599
|
-
if (
|
|
627
|
+
if (!isUsed) {
|
|
600
628
|
unused.push(propName);
|
|
601
629
|
}
|
|
602
630
|
}
|
|
@@ -650,35 +678,8 @@ function countStatementsOfType(substructure, stmtType) {
|
|
|
650
678
|
return count;
|
|
651
679
|
}
|
|
652
680
|
|
|
653
|
-
function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
|
|
654
|
-
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
for (const prop of properties) {
|
|
659
|
-
const typename = prop['key.typename'] || '';
|
|
660
|
-
|
|
661
|
-
if (['String', 'Int', 'Bool', 'Double', 'Float', 'Date', 'URL', 'Data'].includes(typename)) {
|
|
662
|
-
continue;
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Skip generic type parameters (e.g., "Client" in LoginUseCaseImpl<Client: APIClientProtocol>)
|
|
666
|
-
// These are not concrete dependencies - they are constrained by protocols
|
|
667
|
-
const propName = prop['key.name'] || '';
|
|
668
|
-
const isGenericTypeParameter = typename.length === 1 ||
|
|
669
|
-
(typename.match(/^[A-Z][a-z]*$/) && !typename.includes('Impl') &&
|
|
670
|
-
(className.includes('<') || propName === 'apiClient' || propName === 'client'));
|
|
671
|
-
|
|
672
|
-
if (isGenericTypeParameter) {
|
|
673
|
-
continue;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const isConcreteService = /Service$|Repository$|UseCase$|Client$/.test(typename) && !typename.includes('Protocol') && !typename.includes('any ') && !typename.includes('some ');
|
|
677
|
-
|
|
678
|
-
if (isConcreteService) {
|
|
679
|
-
analyzer.pushFinding('ios.solid.dip.concrete_dependency', 'high', filePath, line, `'${className}' depends on concrete '${typename}' - use protocol`);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
681
|
+
async function checkDependencyInjectionAST(analyzer, properties, filePath, className, line) {
|
|
682
|
+
await diValidationService.validateDependencyInjection(analyzer, properties, filePath, className, line);
|
|
682
683
|
}
|
|
683
684
|
|
|
684
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);
|