pumuki-ast-hooks 5.5.52 → 5.5.54
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/hooks/git-status-monitor.ts +5 -0
- package/hooks/notify-macos.ts +1 -0
- package/hooks/pre-tool-use-evidence-validator.ts +1 -0
- package/package.json +2 -2
- package/scripts/hooks-system/application/services/AutonomousOrchestrator.js +37 -0
- package/scripts/hooks-system/application/services/guard/GuardConfig.js +4 -2
- package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +20 -2
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +13 -1
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +2 -3
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +1 -4
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +1 -2
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +15 -8
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +34 -397
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +350 -0
- package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +44 -21
- package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +16 -0
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +25 -14
- package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +10 -0
|
@@ -42,6 +42,7 @@ function getGitStatus(projectDir: string): GitStatus | null {
|
|
|
42
42
|
hasUncommittedChanges: lines.length > 0
|
|
43
43
|
};
|
|
44
44
|
} catch (err) {
|
|
45
|
+
console.error(`[git-status-monitor] Failed to read git status: ${(err as Error).message}`);
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -79,6 +80,7 @@ function detectPlatformFromFiles(projectDir: string): string[] {
|
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
82
|
} catch (err) {
|
|
83
|
+
console.error(`[git-status-monitor] Failed to detect platforms from files: ${(err as Error).message}`);
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
return platforms.length > 0 ? platforms : ['frontend', 'backend', 'ios', 'android'];
|
|
@@ -134,6 +136,7 @@ async function main() {
|
|
|
134
136
|
sound: 'Ping'
|
|
135
137
|
});
|
|
136
138
|
} catch (err) {
|
|
139
|
+
console.error(`[git-status-monitor] Notification failed (staged): ${(err as Error).message}`);
|
|
137
140
|
}
|
|
138
141
|
} else if (totalChanges > 10) {
|
|
139
142
|
try {
|
|
@@ -144,11 +147,13 @@ async function main() {
|
|
|
144
147
|
sound: 'Glass'
|
|
145
148
|
});
|
|
146
149
|
} catch (err) {
|
|
150
|
+
console.error(`[git-status-monitor] Notification failed (unstaged): ${(err as Error).message}`);
|
|
147
151
|
}
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
process.exit(0);
|
|
151
155
|
} catch (err) {
|
|
156
|
+
console.error(`[git-status-monitor] Unexpected error: ${(err as Error).message}`);
|
|
152
157
|
process.exit(0);
|
|
153
158
|
}
|
|
154
159
|
}
|
package/hooks/notify-macos.ts
CHANGED
|
@@ -24,6 +24,7 @@ export function sendMacOSNotification(options: NotificationOptions): void {
|
|
|
24
24
|
try {
|
|
25
25
|
execSync(`osascript -e '${script}'`, { stdio: 'ignore' });
|
|
26
26
|
} catch (err) {
|
|
27
|
+
console.error(`[notify-macos] Failed to send notification: ${(err as Error).message}`);
|
|
27
28
|
}
|
|
28
29
|
}
|
|
29
30
|
|
|
@@ -235,6 +235,7 @@ async function main() {
|
|
|
235
235
|
sound: 'Basso'
|
|
236
236
|
});
|
|
237
237
|
} catch (err) {
|
|
238
|
+
process.stderr.write(`Notification failed: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
238
239
|
}
|
|
239
240
|
process.stderr.write(`${validation.error || ''}\n`);
|
|
240
241
|
process.exit(2);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki-ast-hooks",
|
|
3
|
-
"version": "5.5.
|
|
3
|
+
"version": "5.5.54",
|
|
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": {
|
|
@@ -134,4 +134,4 @@
|
|
|
134
134
|
"./skills": "./skills/skill-rules.json",
|
|
135
135
|
"./hooks": "./hooks/index.js"
|
|
136
136
|
}
|
|
137
|
-
}
|
|
137
|
+
}
|
|
@@ -31,10 +31,47 @@ class AutonomousOrchestrator {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
detectFromBranchKeywords(branchName) {
|
|
35
|
+
try {
|
|
36
|
+
const PlatformHeuristics = require('./platform/PlatformHeuristics');
|
|
37
|
+
const heuristics = new PlatformHeuristics(this.platformDetector);
|
|
38
|
+
return heuristics.detectFromBranchKeywords(branchName);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const msg = error && error.message ? error.message : String(error);
|
|
41
|
+
this.logger?.debug?.('ORCHESTRATOR_BRANCH_KEYWORDS_DETECTION_ERROR', { error: msg });
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
detectFromEvidenceFile() {
|
|
47
|
+
try {
|
|
48
|
+
const PlatformHeuristics = require('./platform/PlatformHeuristics');
|
|
49
|
+
const heuristics = new PlatformHeuristics(this.platformDetector);
|
|
50
|
+
return heuristics.detectFromEvidenceFile();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const msg = error && error.message ? error.message : String(error);
|
|
53
|
+
this.logger?.debug?.('ORCHESTRATOR_EVIDENCE_FILE_DETECTION_ERROR', { error: msg });
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
detectFromASTSystemFilesLegacy(files) {
|
|
35
59
|
return this.detectFromASTSystemFiles(files);
|
|
36
60
|
}
|
|
37
61
|
|
|
62
|
+
async scoreConfidence(platforms) {
|
|
63
|
+
try {
|
|
64
|
+
const PlatformAnalysisService = require('./PlatformAnalysisService');
|
|
65
|
+
const analysisService = new PlatformAnalysisService(this.platformDetector);
|
|
66
|
+
const context = await this.contextEngine.detectContext();
|
|
67
|
+
return analysisService.analyzeConfidence(platforms || [], context || {});
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const msg = error && error.message ? error.message : String(error);
|
|
70
|
+
this.logger?.debug?.('ORCHESTRATOR_SCORE_CONFIDENCE_ERROR', { error: msg });
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
38
75
|
async analyzeContext() {
|
|
39
76
|
const platforms = await this.detectActivePlatforms();
|
|
40
77
|
const scores = await this.scoreConfidence(platforms);
|
|
@@ -3,9 +3,11 @@ const AuditLogger = require('../logging/AuditLogger');
|
|
|
3
3
|
|
|
4
4
|
class GuardConfig {
|
|
5
5
|
constructor(env = envHelper) {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
this.auditLogger = new AuditLogger({ repoRoot: process.cwd() });
|
|
7
|
+
|
|
8
|
+
const getNumber = (name, def) =>
|
|
8
9
|
typeof env.getNumber === 'function' ? env.getNumber(name, def) : Number(env[name] || def);
|
|
10
|
+
|
|
9
11
|
const getBool = (name, def) =>
|
|
10
12
|
typeof env.getBool === 'function' ? env.getBool(name, def) : (env[name] !== 'false');
|
|
11
13
|
|
|
@@ -128,6 +128,20 @@ if [[ "$CURRENT_BRANCH" == "main" ]] || [[ "$CURRENT_BRANCH" == "master" ]] || [
|
|
|
128
128
|
exit 1
|
|
129
129
|
fi
|
|
130
130
|
|
|
131
|
+
# Enforce Git Flow checks (strict) before allowing commit
|
|
132
|
+
ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
|
|
133
|
+
if [[ -f "$ENFORCER_SCRIPT" ]]; then
|
|
134
|
+
echo ""
|
|
135
|
+
echo "🔍 Running Git Flow checks (strict)..."
|
|
136
|
+
echo ""
|
|
137
|
+
if ! GITFLOW_STRICT_CHECK=true bash "$ENFORCER_SCRIPT" check; then
|
|
138
|
+
echo ""
|
|
139
|
+
echo "🚨 COMMIT BLOCKED: Git Flow checks failed"
|
|
140
|
+
echo ""
|
|
141
|
+
exit 1
|
|
142
|
+
fi
|
|
143
|
+
fi
|
|
144
|
+
|
|
131
145
|
# Check if there are staged files
|
|
132
146
|
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM 2>/dev/null | grep -E '\\.(ts|tsx|js|jsx|swift|kt)$' || true)
|
|
133
147
|
if [ -z "$STAGED_FILES" ]; then
|
|
@@ -263,10 +277,14 @@ fi
|
|
|
263
277
|
# Run gitflow-enforcer if available (optional validation)
|
|
264
278
|
ENFORCER_SCRIPT="scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh"
|
|
265
279
|
if [[ -f "$ENFORCER_SCRIPT" ]]; then
|
|
266
|
-
|
|
280
|
+
echo ""
|
|
281
|
+
echo "🔍 Running Git Flow checks (strict)..."
|
|
282
|
+
echo ""
|
|
283
|
+
if ! GITFLOW_STRICT_CHECK=true bash "$ENFORCER_SCRIPT" check; then
|
|
267
284
|
echo ""
|
|
268
|
-
echo "
|
|
285
|
+
echo "🚨 PUSH BLOCKED: Git Flow checks failed"
|
|
269
286
|
echo ""
|
|
287
|
+
exit 1
|
|
270
288
|
fi
|
|
271
289
|
fi
|
|
272
290
|
|
|
@@ -45,6 +45,18 @@ function getRepoRoot() {
|
|
|
45
45
|
*/
|
|
46
46
|
function shouldIgnore(file) {
|
|
47
47
|
const p = file.replace(/\\/g, "/");
|
|
48
|
+
try {
|
|
49
|
+
const configPaths = loadExclusions()?.exclusions?.paths;
|
|
50
|
+
if (configPaths && typeof configPaths === 'object') {
|
|
51
|
+
for (const [key, enabled] of Object.entries(configPaths)) {
|
|
52
|
+
if (enabled && p.includes(key)) return true;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (process.env.DEBUG) {
|
|
57
|
+
console.debug(`[ast-core] Failed to load exclusions for shouldIgnore: ${error.message || String(error)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
48
60
|
if (p.includes("node_modules/")) return true;
|
|
49
61
|
if (p.includes("/.next/")) return true;
|
|
50
62
|
if (p.includes("/dist/")) return true;
|
|
@@ -125,7 +137,7 @@ let exclusionsConfig = null;
|
|
|
125
137
|
function loadExclusions() {
|
|
126
138
|
if (exclusionsConfig) return exclusionsConfig;
|
|
127
139
|
try {
|
|
128
|
-
const configPath = path.join(
|
|
140
|
+
const configPath = path.join(getRepoRoot(), 'config', 'ast-exclusions.json');
|
|
129
141
|
if (fs.existsSync(configPath)) {
|
|
130
142
|
exclusionsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
131
143
|
}
|
|
@@ -22,7 +22,7 @@ function formatLocalTimestamp(date = new Date()) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
const astModulesPath = __dirname;
|
|
25
|
-
const { createProject, platformOf, mapToLevel } = require(path.join(astModulesPath, "ast-core"));
|
|
25
|
+
const { createProject, platformOf, mapToLevel, shouldIgnore: coreShouldIgnore } = require(path.join(astModulesPath, "ast-core"));
|
|
26
26
|
const MacOSNotificationAdapter = require(path.join(__dirname, '../adapters/MacOSNotificationAdapter'));
|
|
27
27
|
const { runBackendIntelligence } = require(path.join(astModulesPath, "backend/ast-backend"));
|
|
28
28
|
const { runFrontendIntelligence } = require(path.join(astModulesPath, "frontend/ast-frontend"));
|
|
@@ -138,8 +138,6 @@ function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
|
|
|
138
138
|
if (p.includes('/build/')) return true;
|
|
139
139
|
if (p.includes('/coverage/')) return true;
|
|
140
140
|
if (p.includes('/.audit_tmp/')) return true;
|
|
141
|
-
if (p.includes('/infrastructure/ast/')) return true;
|
|
142
|
-
if (p.includes('/scripts/hooks-system/')) return true;
|
|
143
141
|
return false;
|
|
144
142
|
};
|
|
145
143
|
|
|
@@ -706,6 +704,7 @@ function listSourceFiles(root) {
|
|
|
706
704
|
*/
|
|
707
705
|
function shouldIgnore(file) {
|
|
708
706
|
const p = file.replace(/\\/g, "/");
|
|
707
|
+
if (typeof coreShouldIgnore === 'function' && coreShouldIgnore(p)) return true;
|
|
709
708
|
if (p.includes("node_modules/")) return true;
|
|
710
709
|
if (p.includes("/.cursor/")) return true;
|
|
711
710
|
if (/\.bak/i.test(p)) return true;
|
|
@@ -126,13 +126,10 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
128
|
// NO excluir archivos AST - la librería debe auto-auditarse
|
|
129
|
-
if (isTestFile(filePath)) return;
|
|
130
|
-
|
|
131
129
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
132
130
|
const className = cls.getName() || '';
|
|
133
131
|
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
134
|
-
|
|
135
|
-
if (isValueObject || isTestClass) return;
|
|
132
|
+
if (isValueObject) return;
|
|
136
133
|
|
|
137
134
|
const methodsCount = cls.getMethods().length;
|
|
138
135
|
const propertiesCount = cls.getProperties().length;
|
|
@@ -7,8 +7,7 @@ function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godC
|
|
|
7
7
|
sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
8
8
|
const className = cls.getName() || '';
|
|
9
9
|
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
10
|
-
|
|
11
|
-
if (isValueObject || isTestClass) return;
|
|
10
|
+
if (isValueObject) return;
|
|
12
11
|
|
|
13
12
|
const methodsCount = cls.getMethods().length;
|
|
14
13
|
const propertiesCount = cls.getProperties().length;
|
|
@@ -148,15 +148,22 @@ function runCommonIntelligence(project, findings) {
|
|
|
148
148
|
sf.getDescendantsOfKind(SyntaxKind.CatchClause).forEach((clause) => {
|
|
149
149
|
const block = typeof clause.getBlock === 'function' ? clause.getBlock() : null;
|
|
150
150
|
const statements = block && typeof block.getStatements === 'function' ? block.getStatements() : [];
|
|
151
|
+
|
|
151
152
|
if ((statements || []).length === 0) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
153
|
+
const blockText = block ? block.getText() : '';
|
|
154
|
+
const hasTestAssertions = /XCTFail|XCTAssert|guard\s+case|expect\(|assert/i.test(blockText);
|
|
155
|
+
const hasErrorHandling = /throw|console\.|logger\.|log\(|print\(/i.test(blockText);
|
|
156
|
+
|
|
157
|
+
if (!hasTestAssertions && !hasErrorHandling) {
|
|
158
|
+
pushFinding(
|
|
159
|
+
'common.error.empty_catch',
|
|
160
|
+
'critical',
|
|
161
|
+
sf,
|
|
162
|
+
clause,
|
|
163
|
+
'Empty catch block detected - always handle errors (log, rethrow, wrap, or return Result)',
|
|
164
|
+
findings
|
|
165
|
+
);
|
|
166
|
+
}
|
|
160
167
|
}
|
|
161
168
|
});
|
|
162
169
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs').promises;
|
|
3
3
|
const { SourceKittenParser } = require('../parsers/SourceKittenParser');
|
|
4
|
-
const
|
|
4
|
+
const checks = require('./iOSEnterpriseChecks');
|
|
5
5
|
|
|
6
6
|
class iOSEnterpriseAnalyzer {
|
|
7
7
|
constructor() {
|
|
@@ -9,230 +9,6 @@ class iOSEnterpriseAnalyzer {
|
|
|
9
9
|
this.findings = [];
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
async analyzeFile(filePath, findings) {
|
|
13
|
-
this.findings = findings;
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
const ast = await this.parser.parseFile(filePath);
|
|
17
|
-
|
|
18
|
-
if (!ast.parsed) {
|
|
19
|
-
console.warn(`[iOS Enterprise] Could not parse ${filePath}: ${ast.error}`);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
24
|
-
|
|
25
|
-
const classes = this.parser.extractClasses(ast);
|
|
26
|
-
const functions = this.parser.extractFunctions(ast);
|
|
27
|
-
const properties = this.parser.extractProperties(ast);
|
|
28
|
-
const protocols = this.parser.extractProtocols(ast);
|
|
29
|
-
|
|
30
|
-
await this.analyzeSwiftModerno(ast, content, filePath);
|
|
31
|
-
await this.analyzeSwiftUI(ast, classes, filePath);
|
|
32
|
-
await this.analyzeUIKit(ast, classes, filePath);
|
|
33
|
-
await this.analyzeProtocolOriented(protocols, filePath);
|
|
34
|
-
await this.analyzeValueTypes(classes, filePath);
|
|
35
|
-
await this.analyzeMemoryManagement(content, filePath);
|
|
36
|
-
await this.analyzeOptionals(content, filePath);
|
|
37
|
-
await this.analyzeDependencyInjection(classes, filePath);
|
|
38
|
-
await this.analyzeNetworking(content, filePath);
|
|
39
|
-
await this.analyzePersistence(content, filePath);
|
|
40
|
-
await this.analyzeCombine(content, filePath);
|
|
41
|
-
await this.analyzeConcurrency(content, filePath);
|
|
42
|
-
await this.analyzeTesting(content, filePath);
|
|
43
|
-
await this.analyzeUITesting(content, filePath);
|
|
44
|
-
await this.analyzeSecurity(content, filePath);
|
|
45
|
-
await this.analyzeAccessibility(content, filePath);
|
|
46
|
-
await this.analyzeLocalization(content, filePath);
|
|
47
|
-
await this.analyzeArchitecturePatterns(classes, functions, filePath);
|
|
48
|
-
await this.analyzePerformance(functions, content, filePath);
|
|
49
|
-
await this.analyzeCodeOrganization(filePath, content);
|
|
50
|
-
|
|
51
|
-
} catch (error) {
|
|
52
|
-
console.error(`[iOS Enterprise] Error analyzing ${filePath}:`, error.message);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async analyzeSwiftModerno(ast, content, filePath) {
|
|
57
|
-
if (content.includes('completion:') && !content.includes('async ')) {
|
|
58
|
-
this.addFinding('ios.async_await_missing', 'medium', filePath, 1,
|
|
59
|
-
'Using completion handlers instead of async/await (Swift 5.9+ required)');
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const taskCount = (content.match(/\bTask\s*\{/g) || []).length;
|
|
63
|
-
if (taskCount > 3 && !content.includes('TaskGroup')) {
|
|
64
|
-
this.addFinding('ios.structured_concurrency_missing', 'medium', filePath, 1,
|
|
65
|
-
`Multiple Task blocks (${taskCount}) without TaskGroup - use structured concurrency`);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (content.includes('actor ') && !content.includes(': Sendable')) {
|
|
69
|
-
this.addFinding('ios.sendable_missing', 'low', filePath, 1,
|
|
70
|
-
'Actor should conform to Sendable protocol for thread-safe types');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (content.includes('func ') && content.includes('-> View') && !content.includes('some View')) {
|
|
74
|
-
this.addFinding('ios.opaque_types_missing', 'low', filePath, 1,
|
|
75
|
-
'Use "some View" instead of explicit View protocol return');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (content.includes('UIViewController') && !content.includes('@State') && !content.includes('@Binding')) {
|
|
79
|
-
this.addFinding('ios.property_wrappers_missing', 'info', filePath, 1,
|
|
80
|
-
'Consider using SwiftUI property wrappers (@State, @Binding) for state management');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const functions = this.parser.extractFunctions(ast);
|
|
84
|
-
functions.forEach(fn => {
|
|
85
|
-
if (fn.name.includes('Array') || fn.name.includes('Collection')) {
|
|
86
|
-
if (!content.includes('<T>') && !content.includes('<Element>')) {
|
|
87
|
-
this.addFinding('ios.generics_missing', 'low', filePath, fn.line,
|
|
88
|
-
`Function ${fn.name} should use generics for type safety`);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
});
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async analyzeSwiftUI(ast, classes, filePath) {
|
|
95
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
96
|
-
const usesSwiftUI = this.parser.usesSwiftUI(ast);
|
|
97
|
-
const usesUIKit = this.parser.usesUIKit(ast);
|
|
98
|
-
|
|
99
|
-
if (usesUIKit && !usesSwiftUI) {
|
|
100
|
-
this.addFinding('ios.swiftui_first', 'medium', filePath, 1,
|
|
101
|
-
'Consider migrating to SwiftUI for new views (UIKit only when strictly necessary)');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (usesSwiftUI) {
|
|
105
|
-
if (!content.includes('@State')) {
|
|
106
|
-
this.addFinding('ios.state_local_missing', 'info', filePath, 1,
|
|
107
|
-
'SwiftUI view without @State - consider if local state is needed');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (content.includes('ObservableObject') && !content.includes('@StateObject')) {
|
|
111
|
-
this.addFinding('ios.stateobject_missing', 'high', filePath, 1,
|
|
112
|
-
'ObservableObject should be owned with @StateObject, not @ObservedObject');
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (content.includes('class') && content.includes('ObservableObject') && !content.includes('@EnvironmentObject')) {
|
|
116
|
-
this.addFinding('ios.environmentobject_missing', 'info', filePath, 1,
|
|
117
|
-
'Consider using @EnvironmentObject for dependency injection in SwiftUI');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (content.includes('.frame(') && content.includes('CGRect(')) {
|
|
121
|
-
this.addFinding('ios.declarativo_missing', 'medium', filePath, 1,
|
|
122
|
-
'Using imperative CGRect in SwiftUI - use declarative .frame() modifiers');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const geometryReaderCount = (content.match(/GeometryReader/g) || []).length;
|
|
126
|
-
if (geometryReaderCount > 2) {
|
|
127
|
-
this.addFinding('ios.geometryreader_moderation', 'medium', filePath, 1,
|
|
128
|
-
`Excessive GeometryReader usage (${geometryReaderCount}x) - use only when necessary`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
async analyzeUIKit(ast, classes, filePath) {
|
|
134
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
135
|
-
|
|
136
|
-
classes.forEach(cls => {
|
|
137
|
-
if (cls.name.includes('ViewController')) {
|
|
138
|
-
const linesCount = cls.substructure.length * 10;
|
|
139
|
-
if (linesCount > 300) {
|
|
140
|
-
this.addFinding('ios.massive_viewcontrollers', 'high', filePath, cls.line,
|
|
141
|
-
`Massive ViewController ${cls.name} (~${linesCount} lines) - break down into smaller components`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (!content.includes('ViewModel')) {
|
|
145
|
-
this.addFinding('ios.uikit.viewmodel_delegation', 'medium', filePath, cls.line,
|
|
146
|
-
`ViewController ${cls.name} should delegate logic to ViewModel (MVVM pattern)`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (filePath.endsWith('.swift') && !filePath.includes('analyzer') && !filePath.includes('detector')) {
|
|
152
|
-
if (content.includes('storyboard') || content.includes('.xib') || content.includes('.nib')) {
|
|
153
|
-
this.addFinding('ios.storyboards', 'high', filePath, 1,
|
|
154
|
-
'Storyboard/XIB detected - use programmatic UI for better version control');
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async analyzeProtocolOriented(protocols, filePath) {
|
|
160
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
161
|
-
|
|
162
|
-
if (protocols.length > 0 && !content.includes('extension ')) {
|
|
163
|
-
this.addFinding('ios.pop.missing_extensions', 'low', filePath, 1,
|
|
164
|
-
'Protocols detected but no extensions - consider protocol extensions for default implementations');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (content.includes('class ') && content.includes(': ')) {
|
|
168
|
-
const inheritanceCount = (content.match(/class\s+\w+\s*:\s*\w+/g) || []).length;
|
|
169
|
-
if (inheritanceCount > 2) {
|
|
170
|
-
this.addFinding('ios.pop.missing_composition_over_inheritance', 'medium', filePath, 1,
|
|
171
|
-
`Excessive class inheritance (${inheritanceCount}x) - prefer protocol composition`);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async analyzeValueTypes(classes, filePath) {
|
|
177
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
178
|
-
|
|
179
|
-
classes.forEach(cls => {
|
|
180
|
-
if (!cls.inheritedTypes.length && !content.includes('ObservableObject')) {
|
|
181
|
-
this.addFinding('ios.values.classes_instead_structs', 'medium', filePath, cls.line,
|
|
182
|
-
`Class ${cls.name} without inheritance - consider struct for value semantics`);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
const varCount = (content.match(/\bvar\s+/g) || []).length;
|
|
187
|
-
const letCount = (content.match(/\blet\s+/g) || []).length;
|
|
188
|
-
if (varCount > letCount) {
|
|
189
|
-
this.addFinding('ios.values.mutability', 'low', filePath, 1,
|
|
190
|
-
`More var (${varCount}) than let (${letCount}) - prefer immutability`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
async analyzeMemoryManagement(content, filePath) {
|
|
195
|
-
const closureMatches = content.match(/\{\s*\[/g);
|
|
196
|
-
const weakSelfMatches = content.match(/\[weak self\]/g);
|
|
197
|
-
if (closureMatches && closureMatches.length > (weakSelfMatches?.length || 0)) {
|
|
198
|
-
this.addFinding('ios.memory.missing_weak_self', 'high', filePath, 1,
|
|
199
|
-
'Closures without [weak self] - potential retain cycles');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (content.includes('self.') && content.includes('{') && !content.includes('[weak self]')) {
|
|
203
|
-
this.addFinding('ios.memory.retain_cycles', 'high', filePath, 1,
|
|
204
|
-
'Potential retain cycle - closure captures self without [weak self]');
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
if (content.includes('class ') && !content.includes('deinit')) {
|
|
208
|
-
this.addFinding('ios.memory.missing_deinit', 'low', filePath, 1,
|
|
209
|
-
'Class without deinit - consider adding for cleanup verification');
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
async analyzeOptionals(content, filePath) {
|
|
214
|
-
const forceUnwraps = content.match(/(\w+)\s*!/g);
|
|
215
|
-
if (forceUnwraps && forceUnwraps.length > 0) {
|
|
216
|
-
const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
|
|
217
|
-
if (nonIBOutlets.length > 0) {
|
|
218
|
-
this.addFinding('ios.force_unwrapping', 'high', filePath, 1,
|
|
219
|
-
`Force unwrapping (!) detected ${nonIBOutlets.length}x - use if let or guard let`);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const ifLetCount = (content.match(/if\s+let\s+/g) || []).length;
|
|
224
|
-
const guardLetCount = (content.match(/guard\s+let\s+/g) || []).length;
|
|
225
|
-
if (ifLetCount === 0 && guardLetCount === 0 && content.includes('?')) {
|
|
226
|
-
this.addFinding('ios.optionals.optional_binding', 'medium', filePath, 1,
|
|
227
|
-
'Optionals present but no optional binding - use if let or guard let');
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (content.includes('?') && !content.includes('??')) {
|
|
231
|
-
this.addFinding('ios.optionals.missing_nil_coalescing', 'info', filePath, 1,
|
|
232
|
-
'Consider using nil coalescing operator (??) for default values');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
12
|
addFinding(ruleId, severity, filePath, line, message) {
|
|
237
13
|
this.findings.push({
|
|
238
14
|
ruleId,
|
|
@@ -244,180 +20,41 @@ class iOSEnterpriseAnalyzer {
|
|
|
244
20
|
});
|
|
245
21
|
}
|
|
246
22
|
|
|
247
|
-
async
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
classes.forEach(cls => {
|
|
256
|
-
if (cls.name.includes('ViewModel') || cls.name.includes('Service')) {
|
|
257
|
-
const hasInit = content.includes(`init(`);
|
|
258
|
-
if (!hasInit) {
|
|
259
|
-
this.addFinding('ios.di.missing_protocol_injection', 'medium', filePath, cls.line,
|
|
260
|
-
`${cls.name} should inject dependencies via initializer`);
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
if (content.includes('init(') && content.match(/init\([^)]{50,}\)/)) {
|
|
266
|
-
this.addFinding('ios.di.missing_factory', 'low', filePath, 1,
|
|
267
|
-
'Complex initialization - consider factory pattern');
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async analyzeNetworking(content, filePath) {
|
|
272
|
-
if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
if (!content.includes('URLSession') && !content.includes('Alamofire')) {
|
|
276
|
-
if (content.includes('http://') || content.includes('https://')) {
|
|
277
|
-
this.addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
|
|
278
|
-
'Network URLs detected but no URLSession/Alamofire usage');
|
|
23
|
+
async analyzeFile(filePath, findings) {
|
|
24
|
+
this.findings = findings;
|
|
25
|
+
try {
|
|
26
|
+
const ast = await this.parser.parseFile(filePath);
|
|
27
|
+
if (!ast.parsed) {
|
|
28
|
+
console.warn(`[iOS Enterprise] Could not parse ${filePath}: ${ast.error}`);
|
|
29
|
+
return;
|
|
279
30
|
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
if (content.includes('URLSession') && content.includes('completionHandler:') && !content.includes('async')) {
|
|
283
|
-
this.addFinding('ios.networking.completion_handlers_instead_async', 'medium', filePath, 1,
|
|
284
|
-
'Using completion handlers with URLSession - migrate to async/await');
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (content.includes('JSONSerialization') && !content.includes('Codable')) {
|
|
288
|
-
this.addFinding('ios.networking.missing_codable', 'medium', filePath, 1,
|
|
289
|
-
'Manual JSON parsing - use Codable for type safety');
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
if (content.includes('URLSession') && !content.includes('NetworkError')) {
|
|
293
|
-
this.addFinding('ios.networking.missing_error_handling', 'high', filePath, 1,
|
|
294
|
-
'Network code without custom NetworkError enum');
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Check for SSL pinning implementation
|
|
298
|
-
const hasSSLPinningImplementation =
|
|
299
|
-
content.includes('serverTrustPolicy') ||
|
|
300
|
-
content.includes('pinning') ||
|
|
301
|
-
(content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
|
|
302
|
-
|
|
303
|
-
if (content.includes('URLSession') && !hasSSLPinningImplementation) {
|
|
304
|
-
this.addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
|
|
305
|
-
'Consider SSL pinning for high-security apps');
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (content.includes('URLSession') && !content.includes('retry')) {
|
|
309
|
-
this.addFinding('ios.networking.missing_retry', 'low', filePath, 1,
|
|
310
|
-
'Network requests without retry logic');
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
async analyzePersistence(content, filePath) {
|
|
315
|
-
if (content.includes('UserDefaults') && (content.includes('password') || content.includes('token') || content.includes('auth'))) {
|
|
316
|
-
this.addFinding('ios.persistence.userdefaults_sensitive', 'critical', filePath, 1,
|
|
317
|
-
'Sensitive data in UserDefaults - use Keychain instead');
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if ((content.includes('password') || content.includes('token')) && !content.includes('Keychain') && !content.includes('Security')) {
|
|
321
|
-
this.addFinding('ios.persistence.missing_keychain', 'critical', filePath, 1,
|
|
322
|
-
'Sensitive data detected but no Keychain usage');
|
|
323
|
-
}
|
|
324
31
|
|
|
325
|
-
|
|
326
|
-
this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (content.includes('Future<') && !content.includes('async')) {
|
|
353
|
-
this.addFinding('ios.combine.prefer_async_await', 'low', filePath, 1,
|
|
354
|
-
'Combine Future for single value - consider async/await instead');
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
async analyzeConcurrency(content, filePath) {
|
|
359
|
-
if (content.includes('DispatchQueue') && !content.includes('async func')) {
|
|
360
|
-
this.addFinding('ios.concurrency.dispatchqueue_old', 'medium', filePath, 1,
|
|
361
|
-
'Using DispatchQueue - prefer async/await for new code');
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (content.includes('DispatchQueue.main') && content.includes('UI')) {
|
|
365
|
-
this.addFinding('ios.concurrency.missing_mainactor', 'medium', filePath, 1,
|
|
366
|
-
'Manual main thread dispatch - use @MainActor annotation');
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
if (content.includes('Task {') && !content.includes('.cancel()') && !content.includes('Task.isCancelled')) {
|
|
370
|
-
this.addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
|
|
371
|
-
'Task without cancellation handling');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
|
|
375
|
-
this.addFinding('ios.concurrency.actor_missing', 'medium', filePath, 1,
|
|
376
|
-
'Manual synchronization with queue - consider actor for thread safety');
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
async analyzeTesting(content, filePath) {
|
|
381
|
-
if (filePath.includes('Test') && !content.includes('XCTest') && !content.includes('Quick')) {
|
|
382
|
-
this.addFinding('ios.testing.missing_xctest', 'high', filePath, 1,
|
|
383
|
-
'Test file without XCTest or Quick import');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
if (filePath.includes('Test') && !content.includes('makeSUT') && content.includes('func test')) {
|
|
387
|
-
this.addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
|
|
388
|
-
'Test without makeSUT pattern - centralize system under test creation');
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
|
|
392
|
-
this.addFinding('ios.testing.missing_memory_leak_tracking', 'medium', filePath, 1,
|
|
393
|
-
'Test without trackForMemoryLeaks helper');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
if (filePath.includes('Test') && content.includes('init(') && !content.includes('Protocol')) {
|
|
397
|
-
this.addFinding('ios.testing.concrete_dependencies', 'medium', filePath, 1,
|
|
398
|
-
'Test using concrete dependencies - inject protocols for testability');
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
async analyzeUITesting(content, filePath) {
|
|
403
|
-
if (filePath.includes('UITest') && !content.includes('XCUIApplication')) {
|
|
404
|
-
this.addFinding('ios.uitesting.missing_xcuitest', 'high', filePath, 1,
|
|
405
|
-
'UI test file without XCUIApplication');
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
if (filePath.includes('UITest') && !content.includes('accessibilityIdentifier')) {
|
|
409
|
-
this.addFinding('ios.uitesting.missing_accessibility', 'medium', filePath, 1,
|
|
410
|
-
'UI test without accessibility identifiers for element location');
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
if (filePath.includes('UITest') && content.includes('XCUIElement') && !content.includes('Page')) {
|
|
414
|
-
this.addFinding('ios.uitesting.missing_page_object', 'low', filePath, 1,
|
|
415
|
-
'UI test without Page Object pattern for encapsulation');
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
if (filePath.includes('UITest') && content.includes('.tap()') && !content.includes('waitForExistence')) {
|
|
419
|
-
this.addFinding('ios.uitesting.missing_wait', 'high', filePath, 1,
|
|
420
|
-
'UI test tapping without waitForExistence - flaky test');
|
|
32
|
+
const content = await fs.readFile(filePath, 'utf-8');
|
|
33
|
+
const classes = this.parser.extractClasses(ast) || [];
|
|
34
|
+
const functions = this.parser.extractFunctions(ast) || [];
|
|
35
|
+
const protocols = this.parser.extractProtocols(ast) || [];
|
|
36
|
+
const usesSwiftUI = typeof this.parser.usesSwiftUI === 'function' ? this.parser.usesSwiftUI(ast) : false;
|
|
37
|
+
const usesUIKit = typeof this.parser.usesUIKit === 'function' ? this.parser.usesUIKit(ast) : false;
|
|
38
|
+
|
|
39
|
+
const add = (ruleId, severity, line, message) =>
|
|
40
|
+
this.addFinding(ruleId, severity, filePath, line, message);
|
|
41
|
+
|
|
42
|
+
checks.analyzeSwiftModerno({ content, functions, filePath, addFinding: add });
|
|
43
|
+
checks.analyzeSwiftUI({ usesSwiftUI, usesUIKit, content, classes, filePath, addFinding: add });
|
|
44
|
+
checks.analyzeUIKit({ classes, content, filePath, addFinding: add });
|
|
45
|
+
checks.analyzeProtocolOriented({ protocols, content, filePath, addFinding: add });
|
|
46
|
+
checks.analyzeValueTypes({ classes, content, filePath, addFinding: add });
|
|
47
|
+
checks.analyzeMemoryManagement({ content, filePath, addFinding: add });
|
|
48
|
+
checks.analyzeOptionals({ content, filePath, addFinding: add });
|
|
49
|
+
checks.analyzeDependencyInjection({ classes, content, filePath, addFinding: add });
|
|
50
|
+
checks.analyzeNetworking({ content, filePath, addFinding: add });
|
|
51
|
+
checks.analyzePersistence({ content, filePath, addFinding: add });
|
|
52
|
+
checks.analyzeCombine({ content, filePath, addFinding: add });
|
|
53
|
+
checks.analyzeConcurrency({ content, filePath, addFinding: add });
|
|
54
|
+
checks.analyzeTesting({ content, filePath, addFinding: add });
|
|
55
|
+
checks.analyzeUITesting({ content, filePath, addFinding: add });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error(`[iOS Enterprise] Error analyzing ${filePath}:`, error.message);
|
|
421
58
|
}
|
|
422
59
|
}
|
|
423
60
|
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
function analyzeSwiftModerno({ content, functions = [], filePath, addFinding }) {
|
|
2
|
+
if (content.includes('completion:') && !content.includes('async ')) {
|
|
3
|
+
addFinding('ios.async_await_missing', 'medium', filePath, 1,
|
|
4
|
+
'Using completion handlers instead of async/await (Swift 5.9+ required)');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const taskCount = (content.match(/\bTask\s*\{/g) || []).length;
|
|
8
|
+
if (taskCount > 3 && !content.includes('TaskGroup')) {
|
|
9
|
+
addFinding('ios.structured_concurrency_missing', 'medium', filePath, 1,
|
|
10
|
+
`Multiple Task blocks (${taskCount}) without TaskGroup - use structured concurrency`);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (content.includes('actor ') && !content.includes(': Sendable')) {
|
|
14
|
+
addFinding('ios.sendable_missing', 'low', filePath, 1,
|
|
15
|
+
'Actor should conform to Sendable protocol for thread-safe types');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (content.includes('func ') && content.includes('-> View') && !content.includes('some View')) {
|
|
19
|
+
addFinding('ios.opaque_types_missing', 'low', filePath, 1,
|
|
20
|
+
'Use "some View" instead of explicit View protocol return');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (content.includes('UIViewController') && !content.includes('@State') && !content.includes('@Binding') && !content.includes('@ObservedObject')) {
|
|
24
|
+
addFinding('ios.property_wrappers_missing', 'info', filePath, 1,
|
|
25
|
+
'Consider using SwiftUI property wrappers (@State, @Binding, @ObservedObject) for state management');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const extractedFunctions = parser.extractFunctions ? parser.extractFunctions({ parsed: true }) || [] : [];
|
|
29
|
+
extractedFunctions.forEach(fn => {
|
|
30
|
+
if (fn.name.includes('Array') || fn.name.includes('Collection')) {
|
|
31
|
+
if (!content.includes('<T>') && !content.includes('<Element>')) {
|
|
32
|
+
addFinding('ios.generics_missing', 'low', filePath, fn.line,
|
|
33
|
+
`Function ${fn.name} should use generics for type safety`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function analyzeSwiftUI({ usesSwiftUI, usesUIKit, content, classes, filePath, addFinding }) {
|
|
40
|
+
if (usesUIKit && !usesSwiftUI) {
|
|
41
|
+
addFinding('ios.swiftui_first', 'medium', filePath, 1,
|
|
42
|
+
'Consider migrating to SwiftUI for new views (UIKit only when strictly necessary)');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (usesSwiftUI) {
|
|
46
|
+
if (!content.includes('@State')) {
|
|
47
|
+
addFinding('ios.state_local_missing', 'info', filePath, 1,
|
|
48
|
+
'SwiftUI view without @State - consider if local state is needed');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (content.includes('ObservableObject') && !content.includes('@StateObject')) {
|
|
52
|
+
addFinding('ios.stateobject_missing', 'high', filePath, 1,
|
|
53
|
+
'ObservableObject should be owned with @StateObject, not @ObservedObject');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (content.includes('class') && content.includes('ObservableObject') && !content.includes('@EnvironmentObject')) {
|
|
57
|
+
addFinding('ios.environmentobject_missing', 'info', filePath, 1,
|
|
58
|
+
'Consider using @EnvironmentObject for dependency injection in SwiftUI');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (content.includes('.frame(') && content.includes('CGRect(')) {
|
|
62
|
+
addFinding('ios.declarativo_missing', 'medium', filePath, 1,
|
|
63
|
+
'Using imperative CGRect in SwiftUI - use declarative .frame() modifiers');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const geometryReaderCount = (content.match(/GeometryReader/g) || []).length;
|
|
67
|
+
if (geometryReaderCount > 2) {
|
|
68
|
+
addFinding('ios.geometryreader_moderation', 'medium', filePath, 1,
|
|
69
|
+
`Excessive GeometryReader usage (${geometryReaderCount}x) - use only when necessary`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function analyzeUIKit({ classes, content, filePath, addFinding }) {
|
|
75
|
+
classes.forEach(cls => {
|
|
76
|
+
if (cls.name.includes('ViewController')) {
|
|
77
|
+
const linesCount = cls.substructure.length * 10;
|
|
78
|
+
if (linesCount > 300) {
|
|
79
|
+
addFinding('ios.massive_viewcontrollers', 'high', filePath, cls.line,
|
|
80
|
+
`Massive ViewController ${cls.name} (~${linesCount} lines) - break down into smaller components`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!content.includes('ViewModel')) {
|
|
84
|
+
addFinding('ios.uikit.viewmodel_delegation', 'medium', filePath, cls.line,
|
|
85
|
+
`ViewController ${cls.name} should delegate logic to ViewModel (MVVM pattern)`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (filePath.endsWith('.swift') && !filePath.includes('analyzer') && !filePath.includes('detector')) {
|
|
91
|
+
if (content.includes('storyboard') || content.includes('.xib') || content.includes('.nib')) {
|
|
92
|
+
addFinding('ios.storyboards', 'high', filePath, 1,
|
|
93
|
+
'Storyboard/XIB detected - use programmatic UI for better version control');
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function analyzeProtocolOriented({ protocols, content, filePath, addFinding }) {
|
|
99
|
+
if (protocols.length > 0 && !content.includes('extension ')) {
|
|
100
|
+
addFinding('ios.pop.missing_extensions', 'low', filePath, 1,
|
|
101
|
+
'Protocols detected but no extensions - consider protocol extensions for default implementations');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (content.includes('class ') && content.includes(': ')) {
|
|
105
|
+
const inheritanceCount = (content.match(/class\s+\w+\s*:\s*\w+/g) || []).length;
|
|
106
|
+
if (inheritanceCount > 2) {
|
|
107
|
+
addFinding('ios.pop.missing_composition_over_inheritance', 'medium', filePath, 1,
|
|
108
|
+
`Excessive class inheritance (${inheritanceCount}x) - prefer protocol composition`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function analyzeValueTypes({ classes, content, filePath, addFinding }) {
|
|
114
|
+
classes.forEach(cls => {
|
|
115
|
+
if (!cls.inheritedTypes.length && !content.includes('ObservableObject')) {
|
|
116
|
+
addFinding('ios.values.classes_instead_structs', 'medium', filePath, cls.line,
|
|
117
|
+
`Class ${cls.name} without inheritance - consider struct for value semantics`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const varCount = (content.match(/\bvar\s+/g) || []).length;
|
|
122
|
+
const letCount = (content.match(/\blet\s+/g) || []).length;
|
|
123
|
+
if (varCount > letCount) {
|
|
124
|
+
addFinding('ios.values.mutability', 'low', filePath, 1,
|
|
125
|
+
`More var (${varCount}) than let (${letCount}) - prefer immutability`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function analyzeMemoryManagement({ content, filePath, addFinding }) {
|
|
130
|
+
const closureMatches = content.match(/\{\s*\[/g);
|
|
131
|
+
const weakSelfMatches = content.match(/\[weak self\]/g);
|
|
132
|
+
if (closureMatches && closureMatches.length > (weakSelfMatches?.length || 0)) {
|
|
133
|
+
addFinding('ios.memory.missing_weak_self', 'high', filePath, 1,
|
|
134
|
+
'Closures without [weak self] - potential retain cycles');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (content.includes('self.') && content.includes('{') && !content.includes('[weak self]')) {
|
|
138
|
+
addFinding('ios.memory.retain_cycles', 'high', filePath, 1,
|
|
139
|
+
'Potential retain cycle - closure captures self without [weak self]');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (content.includes('class ') && !content.includes('deinit')) {
|
|
143
|
+
addFinding('ios.memory.missing_deinit', 'low', filePath, 1,
|
|
144
|
+
'Class without deinit - consider adding for cleanup verification');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function analyzeOptionals({ content, filePath, addFinding }) {
|
|
149
|
+
const forceUnwraps = content.match(/(\w+)\s*!/g);
|
|
150
|
+
if (forceUnwraps && forceUnwraps.length > 0) {
|
|
151
|
+
const nonIBOutlets = forceUnwraps.filter(match => !content.includes(`@IBOutlet`));
|
|
152
|
+
if (nonIBOutlets.length > 0) {
|
|
153
|
+
addFinding('ios.force_unwrapping', 'high', filePath, 1,
|
|
154
|
+
`Force unwrapping (!) detected ${nonIBOutlets.length}x - use if let or guard let`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const ifLetCount = (content.match(/if\s+let\s+/g) || []).length;
|
|
159
|
+
const guardLetCount = (content.match(/guard\s+let\s+/g) || []).length;
|
|
160
|
+
if (ifLetCount === 0 && guardLetCount === 0 && content.includes('?')) {
|
|
161
|
+
addFinding('ios.optionals.optional_binding', 'medium', filePath, 1,
|
|
162
|
+
'Optionals present but no optional binding - use if let or guard let');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (content.includes('?') && !content.includes('??')) {
|
|
166
|
+
addFinding('ios.optionals.missing_nil_coalescing', 'info', filePath, 1,
|
|
167
|
+
'Consider using nil coalescing operator (??) for default values');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function analyzeDependencyInjection({ classes, content, filePath, addFinding }) {
|
|
172
|
+
if (content.includes('.shared') || content.includes('static let shared')) {
|
|
173
|
+
addFinding('ios.di.singleton_usage', 'high', filePath, 1,
|
|
174
|
+
'Singleton detected - use dependency injection instead');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
classes.forEach(cls => {
|
|
178
|
+
if (cls.name.includes('ViewModel') || cls.name.includes('Service')) {
|
|
179
|
+
const hasInit = content.includes('init(');
|
|
180
|
+
if (!hasInit) {
|
|
181
|
+
addFinding('ios.di.missing_protocol_injection', 'medium', filePath, cls.line,
|
|
182
|
+
`${cls.name} should inject dependencies via initializer`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (content.includes('init(') && content.match(/init\([^)]{50,}\)/)) {
|
|
188
|
+
addFinding('ios.di.missing_factory', 'low', filePath, 1,
|
|
189
|
+
'Complex initialization - consider factory pattern');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function analyzeNetworking({ content, filePath, addFinding }) {
|
|
194
|
+
if (String(filePath || '').endsWith('/Package.swift') || String(filePath || '').endsWith('Package.swift')) {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if (!content.includes('URLSession') && !content.includes('Alamofire')) {
|
|
198
|
+
if (content.includes('http://') || content.includes('https://')) {
|
|
199
|
+
addFinding('ios.networking.missing_urlsession', 'high', filePath, 1,
|
|
200
|
+
'Network URLs detected but no URLSession/Alamofire usage');
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (content.includes('URLSession') && content.includes('completionHandler:') && !content.includes('async')) {
|
|
205
|
+
addFinding('ios.networking.completion_handlers_instead_async', 'medium', filePath, 1,
|
|
206
|
+
'Using completion handlers with URLSession - migrate to async/await');
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (content.includes('JSONSerialization') && !content.includes('Codable')) {
|
|
210
|
+
addFinding('ios.networking.missing_codable', 'medium', filePath, 1,
|
|
211
|
+
'Manual JSON parsing - use Codable for type safety');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (content.includes('URLSession') && !content.includes('NetworkError')) {
|
|
215
|
+
addFinding('ios.networking.missing_error_handling', 'high', filePath, 1,
|
|
216
|
+
'Network code without custom NetworkError enum');
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const hasSSLPinningImplementation =
|
|
220
|
+
content.includes('serverTrustPolicy') ||
|
|
221
|
+
content.includes('pinning') ||
|
|
222
|
+
(content.includes('URLSessionDelegate') && content.includes('URLAuthenticationChallenge'));
|
|
223
|
+
|
|
224
|
+
if (content.includes('URLSession') && !hasSSLPinningImplementation) {
|
|
225
|
+
addFinding('ios.networking.missing_ssl_pinning', 'medium', filePath, 1,
|
|
226
|
+
'Consider SSL pinning for high-security apps');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (content.includes('URLSession') && !content.includes('retry')) {
|
|
230
|
+
addFinding('ios.networking.missing_retry', 'low', filePath, 1,
|
|
231
|
+
'Network requests without retry logic');
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function analyzePersistence({ content, filePath, addFinding }) {
|
|
236
|
+
if (content.includes('UserDefaults') && (content.includes('password') || content.includes('token') || content.includes('auth'))) {
|
|
237
|
+
addFinding('ios.persistence.userdefaults_sensitive', 'critical', filePath, 1,
|
|
238
|
+
'Sensitive data in UserDefaults - use Keychain instead');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if ((content.includes('password') || content.includes('token')) && !content.includes('Keychain') && !content.includes('Security')) {
|
|
242
|
+
addFinding('ios.persistence.missing_keychain', 'critical', filePath, 1,
|
|
243
|
+
'Sensitive data detected but no Keychain usage');
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (content.includes('NSManagedObjectContext') && content.includes('.main')) {
|
|
247
|
+
addFinding('ios.persistence.core_data_on_main', 'high', filePath, 1,
|
|
248
|
+
'Core Data operations on main thread - use background context');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (content.includes('NSPersistentContainer') && !content.includes('NSMigrationManager')) {
|
|
252
|
+
addFinding('ios.persistence.missing_migration', 'medium', filePath, 1,
|
|
253
|
+
'Core Data without migration strategy');
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function analyzeCombine({ content, filePath, addFinding }) {
|
|
258
|
+
if (content.includes('.sink(') && !content.includes('AnyCancellable')) {
|
|
259
|
+
addFinding('ios.combine.missing_cancellables', 'high', filePath, 1,
|
|
260
|
+
'Combine sink without storing AnyCancellable - memory leak');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (content.includes('@Published') && !content.includes('import Combine')) {
|
|
264
|
+
addFinding('ios.combine.published_without_combine', 'high', filePath, 1,
|
|
265
|
+
'@Published used but Combine not imported');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (content.includes('.sink(') && !content.includes('receiveCompletion')) {
|
|
269
|
+
addFinding('ios.combine.error_handling', 'medium', filePath, 1,
|
|
270
|
+
'Combine subscriber without error handling (receiveCompletion)');
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (content.includes('Future<') && !content.includes('async')) {
|
|
274
|
+
addFinding('ios.combine.prefer_async_await', 'low', filePath, 1,
|
|
275
|
+
'Combine Future for single value - consider async/await instead');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function analyzeConcurrency({ content, filePath, addFinding }) {
|
|
280
|
+
if (content.includes('DispatchQueue') && !content.includes('async func')) {
|
|
281
|
+
addFinding('ios.concurrency.dispatchqueue_old', 'medium', filePath, 1,
|
|
282
|
+
'Using DispatchQueue - prefer async/await for new code');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (content.includes('DispatchQueue.main') && content.includes('UI')) {
|
|
286
|
+
addFinding('ios.concurrency.missing_mainactor', 'medium', filePath, 1,
|
|
287
|
+
'Manual main thread dispatch - use @MainActor annotation');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (content.includes('Task {') && !content.includes('.cancel()') && !content.includes('Task.isCancelled')) {
|
|
291
|
+
addFinding('ios.concurrency.task_cancellation', 'low', filePath, 1,
|
|
292
|
+
'Task without cancellation handling');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (content.includes('var ') && content.includes('queue') && !content.includes('actor')) {
|
|
296
|
+
addFinding('ios.concurrency.actor_missing', 'medium', filePath, 1,
|
|
297
|
+
'Manual synchronization with queue - consider actor for thread safety');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function analyzeTesting({ content, filePath, addFinding }) {
|
|
302
|
+
if (filePath.includes('Test') && !content.includes('XCTest') && !content.includes('Quick')) {
|
|
303
|
+
addFinding('ios.testing.missing_xctest', 'high', filePath, 1,
|
|
304
|
+
'Test file without XCTest or Quick import');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (filePath.includes('Test') && !content.includes('makeSUT') && content.includes('func test')) {
|
|
308
|
+
addFinding('ios.testing.missing_makesut', 'medium', filePath, 1,
|
|
309
|
+
'Test without makeSUT pattern - centralize system under test creation');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (filePath.includes('Test') && !content.includes('trackForMemoryLeaks') && content.includes('class')) {
|
|
313
|
+
addFinding('ios.testing.missing_memory_leak_tracking', 'medium', filePath, 1,
|
|
314
|
+
'Test without trackForMemoryLeaks helper');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
if (filePath.includes('Test') && content.includes('init(') && !content.includes('Protocol')) {
|
|
318
|
+
addFinding('ios.testing.concrete_dependencies', 'medium', filePath, 1,
|
|
319
|
+
'Test using concrete dependencies - inject protocols for testability');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function analyzeUITesting({ content, filePath, addFinding }) {
|
|
324
|
+
if (filePath.includes('UITest') && !content.includes('XCTest')) {
|
|
325
|
+
addFinding('ios.uitesting.missing_xctest', 'medium', filePath, 1,
|
|
326
|
+
'UI Test without XCTest import');
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (filePath.includes('UITest') && !content.includes('XCUIApplication')) {
|
|
330
|
+
addFinding('ios.uitesting.missing_application_launch', 'medium', filePath, 1,
|
|
331
|
+
'UI Test missing XCUIApplication launch');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
module.exports = {
|
|
336
|
+
analyzeSwiftModerno,
|
|
337
|
+
analyzeSwiftUI,
|
|
338
|
+
analyzeUIKit,
|
|
339
|
+
analyzeProtocolOriented,
|
|
340
|
+
analyzeValueTypes,
|
|
341
|
+
analyzeMemoryManagement,
|
|
342
|
+
analyzeOptionals,
|
|
343
|
+
analyzeDependencyInjection,
|
|
344
|
+
analyzeNetworking,
|
|
345
|
+
analyzePersistence,
|
|
346
|
+
analyzeCombine,
|
|
347
|
+
analyzeConcurrency,
|
|
348
|
+
analyzeTesting,
|
|
349
|
+
analyzeUITesting,
|
|
350
|
+
};
|
|
@@ -992,48 +992,64 @@ async function aiGateCheck() {
|
|
|
992
992
|
}
|
|
993
993
|
}
|
|
994
994
|
|
|
995
|
-
const
|
|
996
|
-
|
|
997
|
-
|
|
995
|
+
const fallbackPlatforms = ['backend', 'frontend', 'ios', 'android'];
|
|
996
|
+
const platformsForRules = (detectedPlatforms.length > 0 ? detectedPlatforms : fallbackPlatforms)
|
|
997
|
+
.filter(Boolean);
|
|
998
|
+
const normalizedPlatforms = Array.from(new Set(platformsForRules));
|
|
998
999
|
|
|
999
1000
|
let mandatoryRules = null;
|
|
1000
1001
|
try {
|
|
1001
|
-
const rulesData = await loadPlatformRules(
|
|
1002
|
+
const rulesData = await loadPlatformRules(normalizedPlatforms);
|
|
1003
|
+
const rulesSample = rulesData.criticalRules.slice(0, 5).map(r => r.rule || r);
|
|
1004
|
+
const rulesCount = rulesData.criticalRules.length;
|
|
1002
1005
|
mandatoryRules = {
|
|
1003
|
-
platforms:
|
|
1004
|
-
criticalRules: rulesData.criticalRules
|
|
1005
|
-
rulesLoaded: Object.keys(rulesData.rules
|
|
1006
|
-
|
|
1006
|
+
platforms: normalizedPlatforms,
|
|
1007
|
+
criticalRules: rulesData.criticalRules,
|
|
1008
|
+
rulesLoaded: Object.keys(rulesData.rules),
|
|
1009
|
+
totalRulesCount: rulesCount,
|
|
1010
|
+
rulesSample,
|
|
1011
|
+
proofOfRead: `✅ VERIFIED: ${rulesCount} critical rules loaded from ${Object.keys(rulesData.rules).join(', ')}`
|
|
1007
1012
|
};
|
|
1008
|
-
} catch (
|
|
1013
|
+
} catch (error) {
|
|
1009
1014
|
if (process.env.DEBUG) {
|
|
1010
|
-
process.stderr.write(`[MCP] loadPlatformRules failed: ${
|
|
1015
|
+
process.stderr.write(`[MCP] loadPlatformRules failed: ${error.message}\n`);
|
|
1011
1016
|
}
|
|
1017
|
+
|
|
1012
1018
|
mandatoryRules = {
|
|
1013
|
-
platforms:
|
|
1019
|
+
platforms: normalizedPlatforms,
|
|
1014
1020
|
criticalRules: [],
|
|
1015
1021
|
rulesLoaded: [],
|
|
1016
|
-
|
|
1017
|
-
error:
|
|
1022
|
+
status: 'FAILED_TO_LOAD',
|
|
1023
|
+
error: `Failed to load rules content: ${error && error.message ? error.message : String(error)}`
|
|
1018
1024
|
};
|
|
1019
1025
|
}
|
|
1020
1026
|
|
|
1027
|
+
const rulesLoadedSuccessfully = mandatoryRules &&
|
|
1028
|
+
mandatoryRules.criticalRules &&
|
|
1029
|
+
mandatoryRules.criticalRules.length > 0;
|
|
1030
|
+
|
|
1031
|
+
if (!rulesLoadedSuccessfully) {
|
|
1032
|
+
violations.push('❌ RULES_NOT_LOADED: Critical platform rules could not be loaded. AI cannot proceed without reading mandatory rules.');
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const finalBlocked = isBlocked || !rulesLoadedSuccessfully;
|
|
1036
|
+
|
|
1021
1037
|
return {
|
|
1022
|
-
status:
|
|
1038
|
+
status: finalBlocked ? 'BLOCKED' : 'ALLOWED',
|
|
1023
1039
|
timestamp: new Date().toISOString(),
|
|
1024
1040
|
branch: currentBranch,
|
|
1025
1041
|
violations,
|
|
1026
1042
|
warnings,
|
|
1027
1043
|
autoFixes,
|
|
1028
|
-
mandatory_rules:
|
|
1029
|
-
|
|
1044
|
+
mandatory_rules: rulesLoadedSuccessfully
|
|
1045
|
+
? { ...mandatoryRules, status: 'LOADED_OK' }
|
|
1046
|
+
: mandatoryRules,
|
|
1047
|
+
summary: finalBlocked
|
|
1030
1048
|
? `🚫 BLOCKED: ${violations.length} violation(s). Fix before proceeding.`
|
|
1031
|
-
: `🚦 ALLOWED: Gate passed
|
|
1032
|
-
instructions:
|
|
1049
|
+
: `🚦 ALLOWED: Gate passed. ${mandatoryRules.totalRulesCount} critical rules loaded and verified.`,
|
|
1050
|
+
instructions: finalBlocked
|
|
1033
1051
|
? 'DO NOT proceed with user task. Announce violations and fix them first.'
|
|
1034
|
-
: mandatoryRules
|
|
1035
|
-
? `You may proceed with user task. CRITICAL: Review mandatory_rules.criticalRules BEFORE generating ANY code.`
|
|
1036
|
-
: 'You may proceed with user task.'
|
|
1052
|
+
: `✅ ${mandatoryRules.totalRulesCount} RULES LOADED. Sample: ${mandatoryRules.rulesSample.slice(0, 2).join(' | ')}... Review ALL rules in mandatory_rules.criticalRules before ANY code generation.`
|
|
1037
1053
|
};
|
|
1038
1054
|
};
|
|
1039
1055
|
|
|
@@ -1050,6 +1066,13 @@ async function aiGateCheck() {
|
|
|
1050
1066
|
violations: ['❌ GATE_TIMEOUT: AI gate check timed out. Retry or run ai-start manually.'],
|
|
1051
1067
|
warnings: [],
|
|
1052
1068
|
autoFixes: [],
|
|
1069
|
+
mandatory_rules: {
|
|
1070
|
+
platforms: ['backend', 'frontend', 'ios', 'android'],
|
|
1071
|
+
criticalRules: [],
|
|
1072
|
+
rulesLoaded: [],
|
|
1073
|
+
warning: '⚠️ AI MUST read and follow these rules before ANY code generation or modification',
|
|
1074
|
+
error: 'Rules could not be loaded due to timeout'
|
|
1075
|
+
},
|
|
1053
1076
|
summary: '🚫 BLOCKED: Gate check timed out.',
|
|
1054
1077
|
instructions: 'DO NOT proceed with user task. Retry the gate check.'
|
|
1055
1078
|
};
|
package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js
CHANGED
|
@@ -11,6 +11,22 @@ describe('intelligent-audit', () => {
|
|
|
11
11
|
const mod = require('../intelligent-audit');
|
|
12
12
|
expect(typeof mod.runIntelligentAudit).toBe('function');
|
|
13
13
|
});
|
|
14
|
+
|
|
15
|
+
it('should filter staged violations strictly (no substring matches, no .audit_tmp)', () => {
|
|
16
|
+
const mod = require('../intelligent-audit');
|
|
17
|
+
|
|
18
|
+
expect(typeof mod.isViolationInStagedFiles).toBe('function');
|
|
19
|
+
|
|
20
|
+
const stagedSet = new Set([
|
|
21
|
+
'apps/ios/Application/AppCoordinator.swift'
|
|
22
|
+
]);
|
|
23
|
+
|
|
24
|
+
expect(mod.isViolationInStagedFiles('apps/ios/Application/AppCoordinator.swift', stagedSet)).toBe(true);
|
|
25
|
+
expect(mod.isViolationInStagedFiles('apps/ios/Application/AppCoordinator.swift.backup', stagedSet)).toBe(false);
|
|
26
|
+
expect(mod.isViolationInStagedFiles('.audit_tmp/AppCoordinator.123.staged.swift', stagedSet)).toBe(false);
|
|
27
|
+
expect(mod.isViolationInStagedFiles('some/dir/.audit_tmp/AppCoordinator.123.staged.swift', stagedSet)).toBe(false);
|
|
28
|
+
expect(mod.isViolationInStagedFiles('apps/ios/Application/AppCoordinator', stagedSet)).toBe(false);
|
|
29
|
+
});
|
|
14
30
|
});
|
|
15
31
|
|
|
16
32
|
describe('AI_EVIDENCE.json structure validation', () => {
|
|
@@ -235,6 +235,28 @@ function toRepoRelativePath(filePath) {
|
|
|
235
235
|
return normalized;
|
|
236
236
|
}
|
|
237
237
|
|
|
238
|
+
function isAuditTmpPath(repoRelativePath) {
|
|
239
|
+
const normalized = normalizePathForMatch(repoRelativePath);
|
|
240
|
+
return normalized.startsWith('.audit_tmp/') || normalized.includes('/.audit_tmp/');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function isViolationInStagedFiles(violationPath, stagedSet) {
|
|
244
|
+
if (!violationPath) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const repoRelative = toRepoRelativePath(violationPath);
|
|
249
|
+
if (!repoRelative) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (isAuditTmpPath(repoRelative)) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return stagedSet.has(repoRelative);
|
|
258
|
+
}
|
|
259
|
+
|
|
238
260
|
function resolveAuditTmpDir() {
|
|
239
261
|
const configured = (env.get('AUDIT_TMP', '') || '').trim();
|
|
240
262
|
if (configured.length > 0) {
|
|
@@ -273,18 +295,7 @@ async function runIntelligentAudit() {
|
|
|
273
295
|
|
|
274
296
|
const stagedViolations = rawViolations.filter(v => {
|
|
275
297
|
const violationPath = toRepoRelativePath(v.filePath || v.file || '');
|
|
276
|
-
|
|
277
|
-
return false;
|
|
278
|
-
}
|
|
279
|
-
if (stagedSet.has(violationPath)) {
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
for (const sf of stagedSet) {
|
|
283
|
-
if (sf && (violationPath === sf || violationPath.endsWith('/' + sf) || violationPath.includes('/' + sf))) {
|
|
284
|
-
return true;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
return false;
|
|
298
|
+
return isViolationInStagedFiles(violationPath, stagedSet);
|
|
288
299
|
});
|
|
289
300
|
|
|
290
301
|
console.log(`[Intelligent Audit] Gate scope: STAGING (${stagedFiles.length} files)`);
|
|
@@ -550,7 +561,7 @@ async function updateAIEvidence(violations, gateResult, tokenUsage) {
|
|
|
550
561
|
file: v.filePath || v.file || 'unknown',
|
|
551
562
|
line: v.line || null,
|
|
552
563
|
severity: v.severity,
|
|
553
|
-
|
|
564
|
+
rule_id: ruleId,
|
|
554
565
|
message: v.message || v.description || '',
|
|
555
566
|
category: v.category || deriveCategoryFromRuleId(ruleId),
|
|
556
567
|
intelligent_evaluation: v.intelligentEvaluation || false,
|
|
@@ -683,4 +694,4 @@ if (require.main === module) {
|
|
|
683
694
|
});
|
|
684
695
|
}
|
|
685
696
|
|
|
686
|
-
module.exports = { runIntelligentAudit };
|
|
697
|
+
module.exports = { runIntelligentAudit, isViolationInStagedFiles, toRepoRelativePath };
|
|
@@ -198,6 +198,16 @@ verify_atomic_commit() {
|
|
|
198
198
|
done
|
|
199
199
|
|
|
200
200
|
if (( root_count > 1 )); then
|
|
201
|
+
local has_scripts=0
|
|
202
|
+
local has_tests=0
|
|
203
|
+
for root in $roots_list; do
|
|
204
|
+
[[ "$root" == "scripts" ]] && has_scripts=1
|
|
205
|
+
[[ "$root" == "tests" ]] && has_tests=1
|
|
206
|
+
done
|
|
207
|
+
if [[ $has_scripts -eq 1 && $has_tests -eq 1 && $root_count -eq 2 ]]; then
|
|
208
|
+
printf "${GREEN}✅ Commit %s toca scripts + tests (permitido para bugfixes/features con tests).${NC}\n" "$commit"
|
|
209
|
+
return 0
|
|
210
|
+
fi
|
|
201
211
|
printf "${RED}❌ Commit %s toca múltiples raíces (%s). Divide los cambios en commits atómicos.${NC}\n" "$commit" "$roots_list"
|
|
202
212
|
return 1
|
|
203
213
|
fi
|