pumuki-ast-hooks 5.5.48 → 5.5.50
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/docs/VIOLATIONS_RESOLUTION_PLAN.md +5 -128
- 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/.audit_tmp/hook-metrics.jsonl +68 -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/ios/analyzers/iOSEnterpriseAnalyzer.js +34 -397
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSEnterpriseChecks.js +350 -0
- 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 +85 -17
|
@@ -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
|
+
};
|
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 };
|
|
@@ -25,6 +25,7 @@ AUTO_MERGE_PR=${GITFLOW_AUTO_MERGE:-false}
|
|
|
25
25
|
PR_BASE_BRANCH=${GITFLOW_PR_BASE:-develop}
|
|
26
26
|
STRICT_ATOMIC=${GITFLOW_STRICT_ATOMIC:-true}
|
|
27
27
|
REQUIRE_TEST_RELATIONS=${GITFLOW_REQUIRE_TESTS:-true}
|
|
28
|
+
STRICT_CHECK=${GITFLOW_STRICT_CHECK:-false}
|
|
28
29
|
|
|
29
30
|
print_section() {
|
|
30
31
|
printf "${BLUE}═══════════════════════════════════════════════════════════════${NC}\n"
|
|
@@ -167,12 +168,17 @@ verify_atomic_commit() {
|
|
|
167
168
|
return 0
|
|
168
169
|
fi
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
local files=()
|
|
172
|
+
while IFS= read -r file; do
|
|
173
|
+
[[ -z "$file" ]] && continue
|
|
174
|
+
files+=("$file")
|
|
175
|
+
done < <($GIT_BIN diff --name-only "${commit}^..${commit}")
|
|
171
176
|
if [[ "${#files[@]}" -eq 0 ]]; then
|
|
172
177
|
return 0
|
|
173
178
|
fi
|
|
174
179
|
|
|
175
|
-
|
|
180
|
+
local roots_list
|
|
181
|
+
roots_list=()
|
|
176
182
|
for file in "${files[@]}"; do
|
|
177
183
|
local root="${file%%/*}"
|
|
178
184
|
if [[ "$root" == "$file" ]]; then
|
|
@@ -186,12 +192,37 @@ verify_atomic_commit() {
|
|
|
186
192
|
root="(root)"
|
|
187
193
|
;;
|
|
188
194
|
esac
|
|
189
|
-
|
|
195
|
+
|
|
196
|
+
local seen=0
|
|
197
|
+
local existing
|
|
198
|
+
if (( ${#roots_list[@]:-0} > 0 )); then
|
|
199
|
+
for existing in "${roots_list[@]}"; do
|
|
200
|
+
if [[ "$existing" == "$root" ]]; then
|
|
201
|
+
seen=1
|
|
202
|
+
break
|
|
203
|
+
fi
|
|
204
|
+
done
|
|
205
|
+
fi
|
|
206
|
+
if [[ "$seen" -eq 0 ]]; then
|
|
207
|
+
roots_list+=("$root")
|
|
208
|
+
fi
|
|
190
209
|
done
|
|
191
210
|
|
|
192
|
-
local root_count=${#
|
|
211
|
+
local root_count=${#roots_list[@]}
|
|
193
212
|
if (( root_count > 1 )); then
|
|
194
|
-
|
|
213
|
+
local has_scripts=0
|
|
214
|
+
local has_tests=0
|
|
215
|
+
for root in "${roots_list[@]}"; do
|
|
216
|
+
[[ "$root" == "scripts" ]] && has_scripts=1
|
|
217
|
+
[[ "$root" == "tests" ]] && has_tests=1
|
|
218
|
+
done
|
|
219
|
+
|
|
220
|
+
if [[ $has_scripts -eq 1 && $has_tests -eq 1 && $root_count -eq 2 ]]; then
|
|
221
|
+
printf "${GREEN}✅ Commit %s toca scripts + tests (permitido para bugfixes/features con tests).${NC}\n" "$commit"
|
|
222
|
+
return 0
|
|
223
|
+
fi
|
|
224
|
+
|
|
225
|
+
printf "${RED}❌ Commit %s toca múltiples raíces (%s). Divide los cambios en commits atómicos.${NC}\n" "$commit" "$(printf "%s " "${roots_list[@]}")"
|
|
195
226
|
return 1
|
|
196
227
|
fi
|
|
197
228
|
if (( root_count == 0 )); then
|
|
@@ -199,7 +230,7 @@ verify_atomic_commit() {
|
|
|
199
230
|
return 0
|
|
200
231
|
fi
|
|
201
232
|
local root_name
|
|
202
|
-
for root_name in "${
|
|
233
|
+
for root_name in "${roots_list[@]}"; do
|
|
203
234
|
printf "${GREEN}✅ Commit %s cumple atomicidad (raíz %s).${NC}\n" "$commit" "$root_name"
|
|
204
235
|
done
|
|
205
236
|
return 0
|
|
@@ -219,7 +250,11 @@ verify_pending_commits_atomic() {
|
|
|
219
250
|
return $?
|
|
220
251
|
fi
|
|
221
252
|
|
|
222
|
-
|
|
253
|
+
local commits=()
|
|
254
|
+
while IFS= read -r commit; do
|
|
255
|
+
[[ -z "$commit" ]] && continue
|
|
256
|
+
commits+=("$commit")
|
|
257
|
+
done < <($GIT_BIN rev-list "${base_ref}..${branch}")
|
|
223
258
|
local failed=0
|
|
224
259
|
for commit in "${commits[@]}"; do
|
|
225
260
|
if ! verify_atomic_commit "$commit"; then
|
|
@@ -349,22 +384,50 @@ cmd_check() {
|
|
|
349
384
|
local branch
|
|
350
385
|
branch=$(current_branch)
|
|
351
386
|
printf "${CYAN}📍 Rama actual: %s${NC}\n" "$branch"
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
387
|
+
local failed=0
|
|
388
|
+
|
|
389
|
+
if [[ "${STRICT_CHECK}" == "true" ]]; then
|
|
390
|
+
ensure_evidence_fresh || failed=1
|
|
391
|
+
lint_hooks_system || failed=1
|
|
392
|
+
run_mobile_checks || failed=1
|
|
393
|
+
else
|
|
394
|
+
ensure_evidence_fresh || true
|
|
395
|
+
lint_hooks_system || true
|
|
396
|
+
run_mobile_checks || true
|
|
397
|
+
fi
|
|
355
398
|
print_sync_table
|
|
356
399
|
print_cleanup_candidates
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
400
|
+
if [[ "${STRICT_CHECK}" == "true" ]]; then
|
|
401
|
+
verify_atomic_commit "HEAD" || failed=1
|
|
402
|
+
if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
|
|
403
|
+
verify_related_files_commit "HEAD" || failed=1
|
|
404
|
+
fi
|
|
405
|
+
if ! verify_pending_commits_atomic "$branch"; then
|
|
406
|
+
failed=1
|
|
407
|
+
fi
|
|
408
|
+
if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
|
|
409
|
+
if ! verify_pending_commits_related "$branch"; then
|
|
410
|
+
failed=1
|
|
411
|
+
fi
|
|
412
|
+
fi
|
|
413
|
+
else
|
|
414
|
+
verify_atomic_commit "HEAD" || true
|
|
415
|
+
if [[ "$REQUIRE_TEST_RELATIONS" == "true" ]]; then
|
|
416
|
+
verify_related_files_commit "HEAD" || true
|
|
417
|
+
fi
|
|
360
418
|
fi
|
|
361
419
|
local pending
|
|
362
420
|
pending=$(unpushed_commits "$branch")
|
|
363
421
|
if [[ "$pending" != "0" ]]; then
|
|
364
422
|
printf "${YELLOW}⚠️ Commits sin subir (${pending}). Ejecuta git push.${NC}\n"
|
|
423
|
+
if [[ "${STRICT_CHECK}" == "true" ]]; then
|
|
424
|
+
failed=1
|
|
425
|
+
fi
|
|
365
426
|
else
|
|
366
427
|
printf "${GREEN}✅ No hay commits pendientes de push.${NC}\n"
|
|
367
428
|
fi
|
|
429
|
+
|
|
430
|
+
return $failed
|
|
368
431
|
}
|
|
369
432
|
|
|
370
433
|
cmd_cycle() {
|
|
@@ -518,8 +581,6 @@ main() {
|
|
|
518
581
|
esac
|
|
519
582
|
}
|
|
520
583
|
|
|
521
|
-
main "$@"
|
|
522
|
-
|
|
523
584
|
is_test_file() {
|
|
524
585
|
local file="$1"
|
|
525
586
|
case "$file" in
|
|
@@ -620,10 +681,15 @@ verify_pending_commits_related() {
|
|
|
620
681
|
local base_ref="origin/${branch}"
|
|
621
682
|
|
|
622
683
|
if ! $GIT_BIN show-ref --verify --quiet "refs/remotes/origin/${branch}"; then
|
|
623
|
-
|
|
684
|
+
verify_related_files_commit "HEAD"
|
|
685
|
+
return $?
|
|
624
686
|
fi
|
|
625
687
|
|
|
626
|
-
|
|
688
|
+
local commits=()
|
|
689
|
+
while IFS= read -r commit; do
|
|
690
|
+
[[ -z "$commit" ]] && continue
|
|
691
|
+
commits+=("$commit")
|
|
692
|
+
done < <($GIT_BIN rev-list "${base_ref}..${branch}")
|
|
627
693
|
local failed=0
|
|
628
694
|
local commit
|
|
629
695
|
for commit in "${commits[@]}"; do
|
|
@@ -633,3 +699,5 @@ verify_pending_commits_related() {
|
|
|
633
699
|
done
|
|
634
700
|
return $failed
|
|
635
701
|
}
|
|
702
|
+
|
|
703
|
+
main "$@"
|