pumuki-ast-hooks 5.5.50 → 5.5.52

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.
Files changed (30) hide show
  1. package/docs/MCP_SERVERS.md +16 -2
  2. package/docs/RELEASE_NOTES.md +34 -0
  3. package/hooks/git-status-monitor.ts +0 -5
  4. package/hooks/notify-macos.ts +0 -1
  5. package/hooks/pre-tool-use-evidence-validator.ts +0 -1
  6. package/package.json +2 -2
  7. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +96 -0
  8. package/scripts/hooks-system/application/services/guard/GuardConfig.js +2 -4
  9. package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +2 -20
  10. package/scripts/hooks-system/application/services/installation/McpConfigurator.js +9 -139
  11. package/scripts/hooks-system/application/services/installation/mcp/McpGlobalConfigCleaner.js +49 -0
  12. package/scripts/hooks-system/application/services/installation/mcp/McpProjectConfigWriter.js +59 -0
  13. package/scripts/hooks-system/application/services/installation/mcp/McpServerConfigBuilder.js +103 -0
  14. package/scripts/hooks-system/infrastructure/ast/ast-core.js +1 -13
  15. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +3 -2
  16. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +17 -9
  17. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +2 -1
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDChecks.js +385 -0
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSCICDRules.js +38 -408
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +397 -34
  21. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMChecks.js +408 -0
  22. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSSPMRules.js +36 -442
  23. package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenExtractor.js +146 -0
  24. package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenParser.js +22 -190
  25. package/scripts/hooks-system/infrastructure/ast/ios/parsers/SourceKittenRunner.js +62 -0
  26. package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +398 -1
  27. package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +0 -16
  28. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +14 -25
  29. package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +20 -76
  30. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +0 -350
@@ -0,0 +1,103 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const crypto = require('crypto');
5
+ const env = require('../../../../config/env.js');
6
+
7
+ function slugifyId(input) {
8
+ return String(input || '')
9
+ .trim()
10
+ .toLowerCase()
11
+ .replace(/[^a-z0-9]+/g, '-')
12
+ .replace(/^-+|-+$/g, '')
13
+ .slice(0, 48);
14
+ }
15
+
16
+ class McpServerConfigBuilder {
17
+ constructor(targetRoot, hookSystemRoot, logger = null) {
18
+ this.targetRoot = targetRoot;
19
+ this.hookSystemRoot = hookSystemRoot;
20
+ this.logger = logger;
21
+ }
22
+
23
+ build() {
24
+ const serverId = this.computeServerIdForRepo(this.targetRoot);
25
+ const entrypoint = this.resolveAutomationEntrypoint();
26
+ const nodePath = this.resolveNodeBinary();
27
+
28
+ const mcpConfig = {
29
+ mcpServers: {
30
+ [serverId]: {
31
+ command: nodePath,
32
+ args: [entrypoint],
33
+ env: {
34
+ REPO_ROOT: this.targetRoot,
35
+ AUTO_COMMIT_ENABLED: 'false',
36
+ AUTO_PUSH_ENABLED: 'false',
37
+ AUTO_PR_ENABLED: 'false'
38
+ }
39
+ }
40
+ }
41
+ };
42
+
43
+ return { serverId, mcpConfig };
44
+ }
45
+
46
+ resolveAutomationEntrypoint() {
47
+ const candidates = [
48
+ this.hookSystemRoot
49
+ ? path.join(this.hookSystemRoot, 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
50
+ : null,
51
+ path.join(this.targetRoot, 'scripts', 'hooks-system', 'infrastructure', 'mcp', 'ast-intelligence-automation.js')
52
+ ].filter(Boolean);
53
+
54
+ for (const candidate of candidates) {
55
+ try {
56
+ if (fs.existsSync(candidate)) return candidate;
57
+ } catch (error) {
58
+ this.logger?.warn?.('MCP_ENTRYPOINT_CHECK_FAILED', { candidate, error: error.message });
59
+ }
60
+ }
61
+
62
+ return path.join(this.targetRoot, 'scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js');
63
+ }
64
+
65
+ resolveNodeBinary() {
66
+ let nodePath = process.execPath;
67
+ if (nodePath && fs.existsSync(nodePath)) return nodePath;
68
+
69
+ try {
70
+ return execSync('which node', { encoding: 'utf-8' }).trim();
71
+ } catch (error) {
72
+ if (process.env.DEBUG) {
73
+ process.stderr.write(`[MCP] which node failed: ${error && error.message ? error.message : String(error)}\n`);
74
+ }
75
+ return 'node';
76
+ }
77
+ }
78
+
79
+ computeServerIdForRepo(repoRoot) {
80
+ const legacyServerId = 'ast-intelligence-automation';
81
+ const forced = (env.get('MCP_SERVER_ID', '') || '').trim();
82
+ if (forced.length > 0) return forced;
83
+
84
+ const repoName = path.basename(repoRoot || process.cwd());
85
+ const slug = slugifyId(repoName) || 'repo';
86
+ const fp = this.computeRepoFingerprint(repoRoot);
87
+ return `${legacyServerId}-${slug}-${fp}`;
88
+ }
89
+
90
+ computeRepoFingerprint(repoRoot) {
91
+ try {
92
+ const real = fs.realpathSync(repoRoot);
93
+ return crypto.createHash('sha1').update(real).digest('hex').slice(0, 8);
94
+ } catch (error) {
95
+ if (process.env.DEBUG) {
96
+ process.stderr.write(`[MCP] computeRepoFingerprint fallback: ${error && error.message ? error.message : String(error)}\n`);
97
+ }
98
+ return crypto.createHash('sha1').update(String(repoRoot || '')).digest('hex').slice(0, 8);
99
+ }
100
+ }
101
+ }
102
+
103
+ module.exports = McpServerConfigBuilder;
@@ -45,18 +45,6 @@ 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
- }
60
48
  if (p.includes("node_modules/")) return true;
61
49
  if (p.includes("/.next/")) return true;
62
50
  if (p.includes("/dist/")) return true;
@@ -137,7 +125,7 @@ let exclusionsConfig = null;
137
125
  function loadExclusions() {
138
126
  if (exclusionsConfig) return exclusionsConfig;
139
127
  try {
140
- const configPath = path.join(getRepoRoot(), 'config', 'ast-exclusions.json');
128
+ const configPath = path.join(__dirname, '../../config/ast-exclusions.json');
141
129
  if (fs.existsSync(configPath)) {
142
130
  exclusionsConfig = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
143
131
  }
@@ -22,7 +22,7 @@ function formatLocalTimestamp(date = new Date()) {
22
22
  }
23
23
 
24
24
  const astModulesPath = __dirname;
25
- const { createProject, platformOf, mapToLevel, shouldIgnore: coreShouldIgnore } = require(path.join(astModulesPath, "ast-core"));
25
+ const { createProject, platformOf, mapToLevel } = 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,6 +138,8 @@ 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;
141
143
  return false;
142
144
  };
143
145
 
@@ -704,7 +706,6 @@ function listSourceFiles(root) {
704
706
  */
705
707
  function shouldIgnore(file) {
706
708
  const p = file.replace(/\\/g, "/");
707
- if (typeof coreShouldIgnore === 'function' && coreShouldIgnore(p)) return true;
708
709
  if (p.includes("node_modules/")) return true;
709
710
  if (p.includes("/.cursor/")) return true;
710
711
  if (/\.bak/i.test(p)) return true;
@@ -126,10 +126,13 @@ 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
+
129
131
  sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
130
132
  const className = cls.getName() || '';
131
133
  const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
132
- if (isValueObject) return;
134
+ const isTestClass = /Spec$|Test$|Mock/.test(className);
135
+ if (isValueObject || isTestClass) return;
133
136
 
134
137
  const methodsCount = cls.getMethods().length;
135
138
  const propertiesCount = cls.getProperties().length;
@@ -207,6 +210,9 @@ function runBackendIntelligence(project, findings, platform) {
207
210
 
208
211
  if (platformOf(filePath) !== "backend") return;
209
212
 
213
+ const normalizedBackendPath = filePath.replace(/\\/g, '/');
214
+ const isRealBackendAppFile = normalizedBackendPath.includes('/apps/backend/') || normalizedBackendPath.includes('apps/backend/');
215
+
210
216
  // NO excluir archivos AST - la librería debe auto-auditarse para detectar God classes masivas
211
217
 
212
218
  const fullText = sf.getFullText();
@@ -272,7 +278,7 @@ function runBackendIntelligence(project, findings, platform) {
272
278
  fullTextUpper.includes("ConfigService") ||
273
279
  usesEnvHelper;
274
280
  const hasConfigUsage = /process\.env\b|ConfigService|env\.get(Bool|Number)?\(|\bconfig\s*[\.\[]/i.test(sf.getFullText());
275
- if (!hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
281
+ if (isRealBackendAppFile && !hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
276
282
  pushFinding("backend.config.missing_env_separation", "info", sf, sf, "Missing environment-specific configuration - consider NODE_ENV or ConfigService", findings);
277
283
  }
278
284
 
@@ -287,6 +293,10 @@ function runBackendIntelligence(project, findings, platform) {
287
293
  analyzeGodClasses(sf, findings, { SyntaxKind, pushFinding, godClassBaseline });
288
294
  }
289
295
 
296
+ if (!isRealBackendAppFile) {
297
+ return;
298
+ }
299
+
290
300
  sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
291
301
  const name = cls.getName();
292
302
  if (name && /Entity|Model|Domain/.test(name)) {
@@ -349,16 +359,12 @@ function runBackendIntelligence(project, findings, platform) {
349
359
  }
350
360
  });
351
361
 
352
- const filePathNormalizedForMetrics = filePath.replace(/\\/g, "/");
353
- const filePathNormalizedForMetricsLower = filePathNormalizedForMetrics.toLowerCase();
354
- const isInternalAstToolingFileForMetrics = filePathNormalizedForMetricsLower.includes("/infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("infrastructure/ast/") || filePathNormalizedForMetricsLower.includes("/scripts/hooks-system/infrastructure/ast/");
355
-
356
362
  const fullTextLower = fullText.toLowerCase();
357
363
  const hasMetrics = fullTextLower.includes("micrometer") || fullTextLower.includes("prometheus") ||
358
364
  fullTextLower.includes("actuator") || fullTextLower.includes("metrics");
359
365
  const looksLikeServiceOrController = fullTextLower.includes("controller") || fullTextLower.includes("service");
360
366
 
361
- if (!isInternalAstToolingFileForMetrics && !hasMetrics && looksLikeServiceOrController) {
367
+ if (isRealBackendAppFile && !hasMetrics && looksLikeServiceOrController) {
362
368
  pushFinding("backend.metrics.missing_prometheus", "info", sf, sf, "Missing application metrics - consider Spring Boot Actuator or Micrometer for monitoring", findings);
363
369
  }
364
370
 
@@ -478,7 +484,7 @@ function runBackendIntelligence(project, findings, platform) {
478
484
  sf.getDescendantsOfKind(SyntaxKind.ThrowStatement).forEach((throwStmt) => {
479
485
  const expr = throwStmt.getExpression();
480
486
  if (!expr) return;
481
- const exprText = expr.getText();
487
+ const exprText = throwStmt.getExpression().getText();
482
488
  if (exprText.includes("Error(") || exprText.includes("Exception(")) {
483
489
  const isCustom = exprText.includes("Exception") &&
484
490
  (exprText.includes("Validation") ||
@@ -486,7 +492,9 @@ function runBackendIntelligence(project, findings, platform) {
486
492
  exprText.includes("Unauthorized") ||
487
493
  exprText.includes("Forbidden"));
488
494
  if (!isCustom) {
489
- pushFinding("backend.error.custom_exceptions", "info", sf, throwStmt, "Generic Error/Exception thrown - create custom exception classes for better error handling", findings);
495
+ if (isRealBackendAppFile) {
496
+ pushFinding("backend.error.custom_exceptions", "info", sf, throwStmt, "Generic Error/Exception thrown - create custom exception classes for better error handling", findings);
497
+ }
490
498
  }
491
499
  }
492
500
  });
@@ -7,7 +7,8 @@ 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
- if (isValueObject) return;
10
+ const isTestClass = /Spec$|Test$|Mock/.test(className);
11
+ if (isValueObject || isTestClass) return;
11
12
 
12
13
  const methodsCount = cls.getMethods().length;
13
14
  const propertiesCount = cls.getProperties().length;
@@ -0,0 +1,385 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const glob = require('glob');
4
+
5
+ function checkFastfileExists(ctx) {
6
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
7
+ if (!fs.existsSync(fastfilePath)) {
8
+ ctx.pushFinding(ctx.findings, {
9
+ ruleId: 'ios.cicd.missing_fastfile',
10
+ severity: 'medium',
11
+ message: 'Proyecto iOS sin Fastfile. Fastlane automatiza builds, tests y deployments.',
12
+ filePath: 'PROJECT_ROOT',
13
+ line: 1,
14
+ suggestion: `Inicializar Fastlane:
15
+
16
+ fastlane init
17
+
18
+ Esto crea:
19
+ - fastlane/Fastfile
20
+ - fastlane/Appfile
21
+ - fastlane/Matchfile`
22
+ });
23
+ return;
24
+ }
25
+ checkFastfileLanes(ctx, fastfilePath);
26
+ }
27
+
28
+ function checkFastfileLanes(ctx, fastfilePath) {
29
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
30
+ const requiredLanes = ['test', 'beta', 'release'];
31
+ const existingLanes = content.match(/lane\s+:(\w+)/g)?.map(l => l.match(/:(\w+)/)?.[1]) || [];
32
+ const missingLanes = requiredLanes.filter(lane => !existingLanes.includes(lane));
33
+
34
+ if (missingLanes.length > 0) {
35
+ ctx.pushFinding(ctx.findings, {
36
+ ruleId: 'ios.cicd.fastfile_missing_lanes',
37
+ severity: 'medium',
38
+ message: `Fastfile sin lanes esenciales: ${missingLanes.join(', ')}`,
39
+ filePath: fastfilePath,
40
+ line: 1,
41
+ suggestion: `Añadir lanes:
42
+
43
+ lane :test do
44
+ scan
45
+ end
46
+
47
+ lane :beta do
48
+ build_app
49
+ upload_to_testflight
50
+ end
51
+
52
+ lane :release do
53
+ build_app
54
+ upload_to_app_store
55
+ end`
56
+ });
57
+ }
58
+
59
+ if (!content.includes('increment_build_number') && !content.includes('increment_version_number')) {
60
+ ctx.pushFinding(ctx.findings, {
61
+ ruleId: 'ios.cicd.missing_version_increment',
62
+ severity: 'medium',
63
+ message: 'Fastfile sin incremento automático de versión/build.',
64
+ filePath: fastfilePath,
65
+ line: 1
66
+ });
67
+ }
68
+ }
69
+
70
+ function checkGitHubActionsWorkflow(ctx) {
71
+ const workflowPath = path.join(ctx.projectRoot, '.github/workflows');
72
+
73
+ if (!fs.existsSync(workflowPath)) {
74
+ ctx.pushFinding(ctx.findings, {
75
+ ruleId: 'ios.cicd.missing_github_actions',
76
+ severity: 'low',
77
+ message: 'Proyecto sin GitHub Actions workflows. Automatizar CI/CD.',
78
+ filePath: 'PROJECT_ROOT',
79
+ line: 1,
80
+ suggestion: 'Crear .github/workflows/ios-ci.yml para tests automáticos'
81
+ });
82
+ return;
83
+ }
84
+
85
+ const workflows = glob.sync('*.yml', { cwd: workflowPath, absolute: true });
86
+ const iosWorkflows = workflows.filter(w => {
87
+ const content = fs.readFileSync(w, 'utf-8');
88
+ return content.includes('macos') || content.includes('ios') || content.includes('xcodebuild');
89
+ });
90
+
91
+ if (iosWorkflows.length === 0) {
92
+ ctx.pushFinding(ctx.findings, {
93
+ ruleId: 'ios.cicd.no_ios_workflow',
94
+ severity: 'medium',
95
+ message: 'GitHub Actions sin workflow de iOS.',
96
+ filePath: '.github/workflows/',
97
+ line: 1
98
+ });
99
+ return;
100
+ }
101
+
102
+ iosWorkflows.forEach(workflow => {
103
+ const content = fs.readFileSync(workflow, 'utf-8');
104
+
105
+ if (!content.includes('xcodebuild test')) {
106
+ ctx.pushFinding(ctx.findings, {
107
+ ruleId: 'ios.cicd.workflow_missing_tests',
108
+ severity: 'high',
109
+ message: 'Workflow de iOS sin tests automáticos.',
110
+ filePath: workflow,
111
+ line: 1
112
+ });
113
+ }
114
+
115
+ if (!content.includes('fastlane')) {
116
+ ctx.pushFinding(ctx.findings, {
117
+ ruleId: 'ios.cicd.workflow_without_fastlane',
118
+ severity: 'low',
119
+ message: 'Workflow sin Fastlane. Considerar para simplificar pipeline.',
120
+ filePath: workflow,
121
+ line: 1
122
+ });
123
+ }
124
+ });
125
+ }
126
+
127
+ function checkTestFlightConfiguration(ctx) {
128
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
129
+ if (!fs.existsSync(fastfilePath)) return;
130
+
131
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
132
+ if (content.includes('beta') && !content.includes('upload_to_testflight')) {
133
+ ctx.pushFinding(ctx.findings, {
134
+ ruleId: 'ios.cicd.beta_without_testflight',
135
+ severity: 'medium',
136
+ message: 'Lane beta sin upload_to_testflight. Automatizar distribución.',
137
+ filePath: fastfilePath,
138
+ line: 1
139
+ });
140
+ }
141
+
142
+ if (content.includes('upload_to_testflight') && !content.includes('changelog')) {
143
+ ctx.pushFinding(ctx.findings, {
144
+ ruleId: 'ios.cicd.testflight_without_changelog',
145
+ severity: 'low',
146
+ message: 'TestFlight sin changelog. Añadir notas de release.',
147
+ filePath: fastfilePath,
148
+ line: 1
149
+ });
150
+ }
151
+ }
152
+
153
+ function checkBuildConfiguration(ctx) {
154
+ const projectFiles = glob.sync('**/*.xcodeproj/project.pbxproj', {
155
+ cwd: ctx.projectRoot,
156
+ absolute: true
157
+ });
158
+ if (projectFiles.length === 0) return;
159
+
160
+ projectFiles.forEach(projectFile => {
161
+ const content = fs.readFileSync(projectFile, 'utf-8');
162
+ const configurations = content.match(/buildConfiguration\s*=\s*(\w+)/g) || [];
163
+ const hasDebug = configurations.some(c => c.includes('Debug'));
164
+ const hasRelease = configurations.some(c => c.includes('Release'));
165
+ const hasStaging = configurations.some(c => c.includes('Staging'));
166
+
167
+ if (!hasStaging && (hasDebug && hasRelease)) {
168
+ ctx.pushFinding(ctx.findings, {
169
+ ruleId: 'ios.cicd.missing_staging_config',
170
+ severity: 'low',
171
+ message: 'Sin configuración Staging. Útil para testing pre-production.',
172
+ filePath: projectFile,
173
+ line: 1
174
+ });
175
+ }
176
+ });
177
+ }
178
+
179
+ function checkCertificateManagement(ctx) {
180
+ const matchfilePath = path.join(ctx.projectRoot, 'fastlane/Matchfile');
181
+ if (fs.existsSync(matchfilePath)) return;
182
+
183
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
184
+ if (fs.existsSync(fastfilePath)) {
185
+ ctx.pushFinding(ctx.findings, {
186
+ ruleId: 'ios.cicd.missing_match_config',
187
+ severity: 'medium',
188
+ message: 'Fastlane sin Match para gestión de certificados.',
189
+ filePath: 'fastlane/',
190
+ line: 1,
191
+ suggestion: 'fastlane match init para gestionar certificados en equipo'
192
+ });
193
+ }
194
+ }
195
+
196
+ function checkCodeSigningConfiguration(ctx) {
197
+ const projectFiles = glob.sync('**/*.xcodeproj/project.pbxproj', {
198
+ cwd: ctx.projectRoot,
199
+ absolute: true
200
+ });
201
+
202
+ projectFiles.forEach(projectFile => {
203
+ const content = fs.readFileSync(projectFile, 'utf-8');
204
+ if (content.includes('CODE_SIGN_STYLE = Manual')) {
205
+ ctx.pushFinding(ctx.findings, {
206
+ ruleId: 'ios.cicd.manual_code_signing',
207
+ severity: 'low',
208
+ message: 'Code signing manual. Considerar Automatic o Match para CI/CD.',
209
+ filePath: projectFile,
210
+ line: 1
211
+ });
212
+ }
213
+ });
214
+ }
215
+
216
+ function checkAutomatedTesting(ctx) {
217
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
218
+ if (!fs.existsSync(fastfilePath)) return;
219
+
220
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
221
+ if (!content.includes('scan') && !content.includes('run_tests')) {
222
+ ctx.pushFinding(ctx.findings, {
223
+ ruleId: 'ios.cicd.no_automated_tests',
224
+ severity: 'high',
225
+ message: 'Fastlane sin tests automatizados. Añadir scan action.',
226
+ filePath: fastfilePath,
227
+ line: 1,
228
+ suggestion: `lane :test do
229
+ scan(scheme: "MyApp")
230
+ end`
231
+ });
232
+ }
233
+ }
234
+
235
+ function checkVersionBumping(ctx) {
236
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
237
+ if (!fs.existsSync(fastfilePath)) return;
238
+
239
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
240
+ if (content.includes('upload_to') && !content.includes('increment_build_number')) {
241
+ ctx.pushFinding(ctx.findings, {
242
+ ruleId: 'ios.cicd.missing_build_increment',
243
+ severity: 'medium',
244
+ message: 'Deployment sin incremento automático de build number.',
245
+ filePath: fastfilePath,
246
+ line: 1
247
+ });
248
+ }
249
+ }
250
+
251
+ function checkReleaseNotes(ctx) {
252
+ const changelogPath = path.join(ctx.projectRoot, 'CHANGELOG.md');
253
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
254
+ if (!fs.existsSync(fastfilePath) || fs.existsSync(changelogPath)) return;
255
+
256
+ ctx.pushFinding(ctx.findings, {
257
+ ruleId: 'ios.cicd.missing_changelog',
258
+ severity: 'low',
259
+ message: 'Proyecto sin CHANGELOG.md. Documentar cambios para releases.',
260
+ filePath: 'PROJECT_ROOT',
261
+ line: 1
262
+ });
263
+ }
264
+
265
+ function checkSlackNotifications(ctx) {
266
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
267
+ if (!fs.existsSync(fastfilePath)) return;
268
+
269
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
270
+ if (content.includes('upload_to') && !content.includes('slack')) {
271
+ ctx.pushFinding(ctx.findings, {
272
+ ruleId: 'ios.cicd.missing_notifications',
273
+ severity: 'low',
274
+ message: 'Deployment sin notificaciones. Añadir Slack para avisar al equipo.',
275
+ filePath: fastfilePath,
276
+ line: 1
277
+ });
278
+ }
279
+ }
280
+
281
+ function checkMatchConfiguration(ctx) {
282
+ const matchfilePath = path.join(ctx.projectRoot, 'fastlane/Matchfile');
283
+ if (!fs.existsSync(matchfilePath)) return;
284
+
285
+ const content = fs.readFileSync(matchfilePath, 'utf-8');
286
+ if (!content.includes('git_url')) {
287
+ ctx.pushFinding(ctx.findings, {
288
+ ruleId: 'ios.cicd.match_missing_git_url',
289
+ severity: 'high',
290
+ message: 'Matchfile sin git_url. Match requiere repositorio para certificados.',
291
+ filePath: matchfilePath,
292
+ line: 1
293
+ });
294
+ }
295
+ }
296
+
297
+ function checkGymConfiguration(ctx) {
298
+ const gymfilePath = path.join(ctx.projectRoot, 'fastlane/Gymfile');
299
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
300
+ if (!fs.existsSync(fastfilePath) || fs.existsSync(gymfilePath)) return;
301
+
302
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
303
+ if (content.includes('build_app') || content.includes('gym')) {
304
+ ctx.pushFinding(ctx.findings, {
305
+ ruleId: 'ios.cicd.missing_gymfile',
306
+ severity: 'low',
307
+ message: 'build_app sin Gymfile. Considerar para centralizar configuración de build.',
308
+ filePath: 'fastlane/',
309
+ line: 1
310
+ });
311
+ }
312
+ }
313
+
314
+ function checkScanConfiguration(ctx) {
315
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
316
+ if (!fs.existsSync(fastfilePath)) return;
317
+
318
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
319
+ if (content.includes('scan') && !content.includes('code_coverage')) {
320
+ ctx.pushFinding(ctx.findings, {
321
+ ruleId: 'ios.cicd.scan_without_coverage',
322
+ severity: 'low',
323
+ message: 'scan sin code_coverage: true. Activar para métricas.',
324
+ filePath: fastfilePath,
325
+ line: 1,
326
+ suggestion: 'scan(code_coverage: true, scheme: "MyApp")'
327
+ });
328
+ }
329
+ }
330
+
331
+ function checkPilotConfiguration(ctx) {
332
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
333
+ if (!fs.existsSync(fastfilePath)) return;
334
+
335
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
336
+ if (!content.includes('pilot') && !content.includes('upload_to_testflight')) return;
337
+
338
+ if (!content.includes('changelog') && !content.includes('whats_new')) {
339
+ ctx.pushFinding(ctx.findings, {
340
+ ruleId: 'ios.cicd.pilot_missing_changelog',
341
+ severity: 'low',
342
+ message: 'TestFlight upload sin changelog/whats_new.',
343
+ filePath: fastfilePath,
344
+ line: 1
345
+ });
346
+ }
347
+ }
348
+
349
+ function checkAppStoreMetadata(ctx) {
350
+ const metadataPath = path.join(ctx.projectRoot, 'fastlane/metadata');
351
+ if (fs.existsSync(metadataPath)) return;
352
+
353
+ const fastfilePath = path.join(ctx.projectRoot, 'fastlane/Fastfile');
354
+ if (!fs.existsSync(fastfilePath)) return;
355
+
356
+ const content = fs.readFileSync(fastfilePath, 'utf-8');
357
+ if (content.includes('upload_to_app_store') || content.includes('deliver')) {
358
+ ctx.pushFinding(ctx.findings, {
359
+ ruleId: 'ios.cicd.missing_metadata',
360
+ severity: 'low',
361
+ message: 'Upload a App Store sin metadata/ folder. Versionar descripciones y screenshots.',
362
+ filePath: 'fastlane/',
363
+ line: 1,
364
+ suggestion: 'fastlane deliver init para crear estructura metadata/'
365
+ });
366
+ }
367
+ }
368
+
369
+ module.exports = {
370
+ checkFastfileExists,
371
+ checkGitHubActionsWorkflow,
372
+ checkTestFlightConfiguration,
373
+ checkBuildConfiguration,
374
+ checkCertificateManagement,
375
+ checkCodeSigningConfiguration,
376
+ checkAutomatedTesting,
377
+ checkVersionBumping,
378
+ checkReleaseNotes,
379
+ checkSlackNotifications,
380
+ checkMatchConfiguration,
381
+ checkGymConfiguration,
382
+ checkScanConfiguration,
383
+ checkPilotConfiguration,
384
+ checkAppStoreMetadata,
385
+ };