pumuki-ast-hooks 5.5.49 → 5.5.51

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 (26) hide show
  1. package/hooks/git-status-monitor.ts +5 -0
  2. package/hooks/notify-macos.ts +1 -0
  3. package/hooks/pre-tool-use-evidence-validator.ts +1 -0
  4. package/package.json +1 -1
  5. package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +56 -0
  6. package/scripts/hooks-system/application/services/guard/GuardConfig.js +4 -2
  7. package/scripts/hooks-system/application/services/installation/GitEnvironmentService.js +20 -2
  8. package/scripts/hooks-system/application/services/installation/HookAssetsInstaller.js +0 -0
  9. package/scripts/hooks-system/application/services/installation/McpConfigurator.js +84 -18
  10. package/scripts/hooks-system/application/services/monitoring/EvidenceMonitor.js +5 -146
  11. package/scripts/hooks-system/application/services/monitoring/EvidenceRefreshRunner.js +161 -0
  12. package/scripts/hooks-system/infrastructure/ast/ast-core.js +13 -1
  13. package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +2 -3
  14. package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +1 -4
  15. package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +1 -2
  16. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/StagedSwiftFilePreparer.js +59 -0
  17. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftAstRunner.js +51 -0
  18. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/SwiftToolchainResolver.js +57 -0
  19. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +27 -137
  20. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSAstAnalysisOrchestrator.js +32 -0
  21. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseAnalyzer.js +34 -397
  22. package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +350 -0
  23. package/scripts/hooks-system/infrastructure/mcp/ast-intelligence-automation.js +407 -5
  24. package/scripts/hooks-system/infrastructure/orchestration/__tests__/intelligent-audit.spec.js +16 -0
  25. package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +1 -1
  26. package/scripts/hooks-system/infrastructure/shell/gitflow/gitflow-enforcer.sh +33 -11
@@ -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(__dirname, '../../config/ast-exclusions.json');
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
- const isTestClass = /Spec$|Test$|Mock/.test(className);
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
- const isTestClass = /Spec$|Test$|Mock/.test(className);
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;
@@ -0,0 +1,59 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const crypto = require('crypto');
4
+ const { execSync } = require('child_process');
5
+
6
+ class StagedSwiftFilePreparer {
7
+ constructor({ repoRoot, auditTmpDir }) {
8
+ this.repoRoot = repoRoot;
9
+ this.auditTmpDir = auditTmpDir || path.join(repoRoot, '.audit_tmp');
10
+ }
11
+
12
+ safeTempFilePath(displayPath) {
13
+ const hash = crypto.createHash('sha1').update(String(displayPath)).digest('hex').slice(0, 10);
14
+ const base = path.basename(displayPath, '.swift');
15
+ const filename = `${base}.${hash}.staged.swift`;
16
+ return path.join(this.auditTmpDir, filename);
17
+ }
18
+
19
+ readStagedFileContent(relPath) {
20
+ try {
21
+ return execSync(`git show :"${relPath}"`, {
22
+ encoding: 'utf8',
23
+ cwd: this.repoRoot,
24
+ stdio: ['ignore', 'pipe', 'pipe']
25
+ });
26
+ } catch (error) {
27
+ if (process.env.DEBUG) {
28
+ console.debug(`[StagedSwiftFilePreparer] Failed to read staged file ${relPath}: ${error.message}`);
29
+ }
30
+ return null;
31
+ }
32
+ }
33
+
34
+ prepare({ filePath, stagedRelPath, stagingOnly }) {
35
+ let parsePath = filePath;
36
+ let contentOverride = null;
37
+
38
+ if (stagingOnly && stagedRelPath) {
39
+ const stagedContent = this.readStagedFileContent(stagedRelPath);
40
+ if (typeof stagedContent === 'string') {
41
+ const tmpPath = this.safeTempFilePath(stagedRelPath);
42
+ try {
43
+ fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
44
+ fs.writeFileSync(tmpPath, stagedContent, 'utf8');
45
+ parsePath = tmpPath;
46
+ contentOverride = stagedContent;
47
+ } catch (error) {
48
+ if (process.env.DEBUG) {
49
+ console.debug(`[StagedSwiftFilePreparer] Failed to write temp staged file ${tmpPath}: ${error.message}`);
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ return { parsePath, contentOverride };
56
+ }
57
+ }
58
+
59
+ module.exports = { StagedSwiftFilePreparer };
@@ -0,0 +1,51 @@
1
+ const { execSync } = require('child_process');
2
+
3
+ class SwiftAstRunner {
4
+ constructor({ toolchain }) {
5
+ this.toolchain = toolchain;
6
+ }
7
+
8
+ runSwiftSyntax(filePath, displayPath, findings) {
9
+ if (!this.toolchain.swiftSyntaxPath) return;
10
+
11
+ try {
12
+ const result = execSync(`"${this.toolchain.swiftSyntaxPath}" "${filePath}"`, {
13
+ encoding: 'utf8',
14
+ timeout: 30000,
15
+ stdio: ['pipe', 'pipe', 'pipe']
16
+ });
17
+ const violations = JSON.parse(result);
18
+ violations.forEach(v => {
19
+ findings.push({
20
+ ruleId: v.ruleId,
21
+ severity: String(v.severity || '').toUpperCase(),
22
+ filePath: displayPath || filePath,
23
+ line: v.line,
24
+ column: v.column,
25
+ message: v.message,
26
+ });
27
+ });
28
+ } catch (error) {
29
+ console.error('[SwiftAstRunner] Error parsing file with SwiftSyntax:', error.message);
30
+ }
31
+ }
32
+
33
+ parseSourceKitten(filePath) {
34
+ if (!this.toolchain.checkSourceKitten()) return null;
35
+
36
+ try {
37
+ const result = execSync(
38
+ `${this.toolchain.sourceKittenPath} structure --file "${filePath}"`,
39
+ { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
40
+ );
41
+ return JSON.parse(result);
42
+ } catch (error) {
43
+ if (process.env.DEBUG) {
44
+ console.debug(`[SwiftAstRunner] SourceKitten parse failed for ${filePath}: ${error.message}`);
45
+ }
46
+ return null;
47
+ }
48
+ }
49
+ }
50
+
51
+ module.exports = { SwiftAstRunner };
@@ -0,0 +1,57 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { execSync } = require('child_process');
4
+ const env = require(path.join(__dirname, '../../../../config/env'));
5
+
6
+ class SwiftToolchainResolver {
7
+ constructor(options = {}) {
8
+ this.sourceKittenPath = options.sourceKittenPath || '/opt/homebrew/bin/sourcekitten';
9
+ this.projectRoot = options.projectRoot || this.detectRepoRoot();
10
+ this.swiftSyntaxPath = this.findSwiftSyntaxBinary(this.projectRoot);
11
+ }
12
+
13
+ detectRepoRoot() {
14
+ try {
15
+ return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
16
+ } catch {
17
+ return process.cwd();
18
+ }
19
+ }
20
+
21
+ resolveAuditTmpDir(repoRoot) {
22
+ const configured = String(env.get('AUDIT_TMP', '') || '').trim();
23
+ if (configured.length > 0) {
24
+ return path.isAbsolute(configured) ? configured : path.join(repoRoot, configured);
25
+ }
26
+ return path.join(repoRoot, '.audit_tmp');
27
+ }
28
+
29
+ checkSourceKitten() {
30
+ try {
31
+ execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
32
+ return true;
33
+ } catch (error) {
34
+ if (process.env.DEBUG) {
35
+ console.debug(`[SwiftToolchainResolver] SourceKitten not available at ${this.sourceKittenPath}: ${error.message}`);
36
+ }
37
+ return false;
38
+ }
39
+ }
40
+
41
+ findSwiftSyntaxBinary(projectRoot) {
42
+ const possiblePaths = [
43
+ path.join(__dirname, '../../../../../CustomLintRules/.build/debug/swift-ast-analyzer'),
44
+ path.join(projectRoot, 'CustomLintRules/.build/debug/swift-ast-analyzer'),
45
+ path.join(projectRoot, '.build/debug/swift-ast-analyzer'),
46
+ ];
47
+
48
+ for (const p of possiblePaths) {
49
+ if (fs.existsSync(p)) {
50
+ return p;
51
+ }
52
+ }
53
+ return null;
54
+ }
55
+ }
56
+
57
+ module.exports = { SwiftToolchainResolver };
@@ -1,7 +1,6 @@
1
1
  const { execSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
- const crypto = require('crypto');
5
4
  const env = require(path.join(__dirname, '../../../../config/env'));
6
5
  const {
7
6
  resetCollections,
@@ -9,13 +8,21 @@ const {
9
8
  analyzeCollectedNodes,
10
9
  finalizeGodClassDetection,
11
10
  } = require('../detectors/ios-ast-intelligent-strategies');
11
+ const { SwiftToolchainResolver } = require('./SwiftToolchainResolver');
12
+ const { StagedSwiftFilePreparer } = require('./StagedSwiftFilePreparer');
13
+ const { SwiftAstRunner } = require('./SwiftAstRunner');
14
+ const { iOSAstAnalysisOrchestrator } = require('./iOSAstAnalysisOrchestrator');
12
15
 
13
16
  class iOSASTIntelligentAnalyzer {
14
17
  constructor(findings) {
15
18
  this.findings = findings;
16
- this.sourceKittenPath = '/opt/homebrew/bin/sourcekitten';
17
- this.isAvailable = this.checkSourceKitten();
18
- this.hasSwiftSyntax = this.checkSwiftSyntax();
19
+ this.toolchain = new SwiftToolchainResolver();
20
+ this.sourceKittenPath = this.toolchain.sourceKittenPath;
21
+ this.isAvailable = this.toolchain.checkSourceKitten();
22
+ this.swiftSyntaxPath = this.toolchain.swiftSyntaxPath;
23
+ this.hasSwiftSyntax = Boolean(this.swiftSyntaxPath);
24
+ this.swiftRunner = new SwiftAstRunner({ toolchain: this.toolchain });
25
+ this.analysisOrchestrator = new iOSAstAnalysisOrchestrator();
19
26
  this.fileContent = '';
20
27
  this.currentFilePath = '';
21
28
  this.godClassCandidates = [];
@@ -29,99 +36,6 @@ class iOSASTIntelligentAnalyzer {
29
36
  this.closures = [];
30
37
  }
31
38
 
32
- resolveAuditTmpDir(repoRoot) {
33
- const configured = String(env.get('AUDIT_TMP', '') || '').trim();
34
- if (configured.length > 0) {
35
- return path.isAbsolute(configured) ? configured : path.join(repoRoot, configured);
36
- }
37
- return path.join(repoRoot, '.audit_tmp');
38
- }
39
-
40
- safeTempFilePath(repoRoot, displayPath) {
41
- const tmpDir = this.resolveAuditTmpDir(repoRoot);
42
- const hash = crypto.createHash('sha1').update(String(displayPath)).digest('hex').slice(0, 10);
43
- const base = path.basename(displayPath, '.swift');
44
- const filename = `${base}.${hash}.staged.swift`;
45
- return path.join(tmpDir, filename);
46
- }
47
-
48
- readStagedFileContent(repoRoot, relPath) {
49
- try {
50
- return execSync(`git show :"${relPath}"`, { encoding: 'utf8', cwd: repoRoot, stdio: ['ignore', 'pipe', 'pipe'] });
51
- } catch (error) {
52
- if (process.env.DEBUG) {
53
- console.debug(`[iOSASTIntelligentAnalyzer] Failed to read staged file ${relPath}: ${error.message}`);
54
- }
55
- return null;
56
- }
57
- }
58
-
59
- checkSourceKitten() {
60
- try {
61
- execSync(`${this.sourceKittenPath} version`, { stdio: 'pipe' });
62
- return true;
63
- } catch (error) {
64
- if (process.env.DEBUG) {
65
- console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten not available at ${this.sourceKittenPath}: ${error.message}`);
66
- }
67
- return false;
68
- }
69
- }
70
-
71
- checkSwiftSyntax() {
72
- const projectRoot = require('child_process')
73
- .execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' })
74
- .trim();
75
-
76
- const possiblePaths = [
77
- path.join(__dirname, '../../../../../CustomLintRules/.build/debug/swift-ast-analyzer'),
78
- path.join(projectRoot, 'CustomLintRules/.build/debug/swift-ast-analyzer'),
79
- path.join(projectRoot, '.build/debug/swift-ast-analyzer')
80
- ];
81
- for (const p of possiblePaths) {
82
- if (fs.existsSync(p)) {
83
- this.swiftSyntaxPath = p;
84
- return true;
85
- }
86
- }
87
- return false;
88
- }
89
-
90
- analyzeWithSwiftSyntax(filePath, displayPath = null) {
91
- if (!this.swiftSyntaxPath) return;
92
- try {
93
- const result = execSync(`"${this.swiftSyntaxPath}" "${filePath}"`, {
94
- encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe']
95
- });
96
- const violations = JSON.parse(result);
97
- for (const v of violations) {
98
- const reportedPath = displayPath || filePath;
99
- this.findings.push({
100
- ruleId: v.ruleId, severity: v.severity, filePath: reportedPath,
101
- line: v.line, column: v.column, message: v.message
102
- });
103
- }
104
- } catch (error) {
105
- console.error('[iOSASTIntelligentAnalyzer] Error parsing file:', error.message);
106
- }
107
- }
108
-
109
- parseFile(filePath) {
110
- if (!this.isAvailable) return null;
111
- try {
112
- const result = execSync(
113
- `${this.sourceKittenPath} structure --file "${filePath}"`,
114
- { encoding: 'utf8', timeout: 30000, stdio: ['pipe', 'pipe', 'pipe'] }
115
- );
116
- return JSON.parse(result);
117
- } catch (error) {
118
- if (process.env.DEBUG) {
119
- console.debug(`[iOSASTIntelligentAnalyzer] SourceKitten parse failed for ${filePath}: ${error.message}`);
120
- }
121
- return null;
122
- }
123
- }
124
-
125
39
  analyzeFile(filePath, options = {}) {
126
40
  if (!filePath || !String(filePath).endsWith('.swift')) return;
127
41
 
@@ -129,53 +43,29 @@ class iOSASTIntelligentAnalyzer {
129
43
  const displayPath = options.displayPath || filePath;
130
44
  const stagedRelPath = options.stagedRelPath || null;
131
45
  const stagingOnly = env.get('STAGING_ONLY_MODE', '0') === '1';
46
+ const auditTmpDir = this.toolchain.resolveAuditTmpDir(repoRoot);
47
+ const stagedPreparer = new StagedSwiftFilePreparer({ repoRoot, auditTmpDir });
132
48
 
133
- let parsePath = filePath;
134
- let contentOverride = null;
135
-
136
- if (stagingOnly && stagedRelPath) {
137
- const stagedContent = this.readStagedFileContent(repoRoot, stagedRelPath);
138
- if (typeof stagedContent === 'string') {
139
- const tmpPath = this.safeTempFilePath(repoRoot, stagedRelPath);
140
- try {
141
- fs.mkdirSync(path.dirname(tmpPath), { recursive: true });
142
- fs.writeFileSync(tmpPath, stagedContent, 'utf8');
143
- parsePath = tmpPath;
144
- contentOverride = stagedContent;
145
- } catch (error) {
146
- if (process.env.DEBUG) {
147
- console.debug(`[iOSASTIntelligentAnalyzer] Failed to write temp staged file ${tmpPath}: ${error.message}`);
148
- }
149
- // Fall back to working tree file
150
- }
151
- }
152
- }
49
+ const { parsePath, contentOverride } = stagedPreparer.prepare({
50
+ filePath,
51
+ stagedRelPath,
52
+ stagingOnly
53
+ });
153
54
 
154
55
  if (this.hasSwiftSyntax) {
155
- this.analyzeWithSwiftSyntax(parsePath, displayPath);
56
+ this.swiftRunner.runSwiftSyntax(parsePath, displayPath, this.findings);
156
57
  }
157
58
 
158
- const ast = this.parseFile(parsePath);
59
+ const ast = this.swiftRunner.parseSourceKitten(parsePath);
159
60
  if (!ast) return;
160
61
 
161
- this.currentFilePath = displayPath;
162
- resetCollections(this);
163
-
164
- try {
165
- this.fileContent = typeof contentOverride === 'string'
166
- ? contentOverride
167
- : fs.readFileSync(parsePath, 'utf8');
168
- } catch (error) {
169
- if (process.env.DEBUG) {
170
- console.debug(`[iOSASTIntelligentAnalyzer] Failed to read file content ${parsePath}: ${error.message}`);
171
- }
172
- this.fileContent = '';
173
- }
174
-
175
- const substructure = ast['key.substructure'] || [];
176
-
177
- collectAllNodes(this, substructure, null);
178
- analyzeCollectedNodes(this, displayPath);
62
+ this.analysisOrchestrator.run({
63
+ analyzer: this,
64
+ ast,
65
+ parsePath,
66
+ displayPath,
67
+ contentOverride
68
+ });
179
69
  }
180
70
 
181
71
  safeStringify(obj) {
@@ -0,0 +1,32 @@
1
+ const fs = require('fs');
2
+ const {
3
+ resetCollections,
4
+ collectAllNodes,
5
+ analyzeCollectedNodes,
6
+ } = require('../detectors/ios-ast-intelligent-strategies');
7
+
8
+ class iOSAstAnalysisOrchestrator {
9
+ run({ analyzer, ast, parsePath, displayPath, contentOverride }) {
10
+ if (!ast || !analyzer) return;
11
+
12
+ analyzer.currentFilePath = displayPath;
13
+ resetCollections(analyzer);
14
+
15
+ try {
16
+ analyzer.fileContent = typeof contentOverride === 'string'
17
+ ? contentOverride
18
+ : fs.readFileSync(parsePath, 'utf8');
19
+ } catch (error) {
20
+ if (process.env.DEBUG) {
21
+ console.debug(`[iOSAstAnalysisOrchestrator] Failed to read file content ${parsePath}: ${error.message}`);
22
+ }
23
+ analyzer.fileContent = '';
24
+ }
25
+
26
+ const substructure = ast['key.substructure'] || [];
27
+ collectAllNodes(analyzer, substructure, null);
28
+ analyzeCollectedNodes(analyzer, displayPath);
29
+ }
30
+ }
31
+
32
+ module.exports = { iOSAstAnalysisOrchestrator };