wogiflow 2.1.3 → 2.3.0
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/.claude/commands/wogi-audit.md +189 -3
- package/.claude/commands/wogi-onboard.md +30 -8
- package/.claude/commands/wogi-review.md +86 -13
- package/.claude/commands/wogi-start.md +66 -21
- package/.claude/docs/claude-code-compatibility.md +28 -0
- package/.workflow/templates/claude-md.hbs +32 -2
- package/package.json +1 -1
- package/scripts/flow-api-index.js +128 -63
- package/scripts/flow-audit.js +158 -1
- package/scripts/flow-function-index.js +65 -63
- package/scripts/flow-pattern-extractor.js +1 -1
- package/scripts/flow-progress-tracker.js +289 -0
- package/scripts/flow-prompt-capture.js +263 -170
- package/scripts/flow-scanner-base.js +200 -7
- package/scripts/flow-skill-generator.js +1 -0
- package/scripts/flow-standards-learner.js +167 -3
- package/scripts/flow-task-checkpoint.js +2 -0
- package/scripts/flow-template-extractor.js +1 -1
- package/scripts/flow-version-check.js +1 -0
- package/scripts/hooks/core/commit-log-gate.js +146 -0
- package/scripts/hooks/core/post-compact.js +81 -8
- package/scripts/hooks/core/task-completed.js +19 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +60 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +27 -0
- package/scripts/registries/component-registry.js +141 -4
|
@@ -45,6 +45,13 @@ const DEFAULT_CONFIG = {
|
|
|
45
45
|
'src/mutations'
|
|
46
46
|
],
|
|
47
47
|
|
|
48
|
+
// Glob patterns discover co-located API files (any project structure)
|
|
49
|
+
globPatterns: [
|
|
50
|
+
'src/**/queries',
|
|
51
|
+
'src/**/mutations',
|
|
52
|
+
'src/**/api'
|
|
53
|
+
],
|
|
54
|
+
|
|
48
55
|
filePatterns: ['**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'],
|
|
49
56
|
|
|
50
57
|
excludePatterns: [
|
|
@@ -75,6 +82,7 @@ class APIScanner extends BaseScanner {
|
|
|
75
82
|
super({
|
|
76
83
|
configKey: 'apiRegistry',
|
|
77
84
|
directories: DEFAULT_CONFIG.directories,
|
|
85
|
+
globPatterns: DEFAULT_CONFIG.globPatterns,
|
|
78
86
|
filePatterns: DEFAULT_CONFIG.filePatterns,
|
|
79
87
|
excludePatterns: DEFAULT_CONFIG.excludePatterns,
|
|
80
88
|
...config
|
|
@@ -131,36 +139,27 @@ class APIScanner extends BaseScanner {
|
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
/**
|
|
134
|
-
* Parse file with Babel AST
|
|
142
|
+
* Parse file with Babel AST using shared two-pass approach from BaseScanner.
|
|
143
|
+
* Handles every JS/TS export pattern by design.
|
|
135
144
|
*/
|
|
136
145
|
parseWithBabel(content, filePath, service) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
sourceType: 'module',
|
|
140
|
-
plugins: ['typescript', 'jsx', 'decorators-legacy']
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
this.traverse(ast, {
|
|
144
|
-
ExportNamedDeclaration: (nodePath) => {
|
|
145
|
-
const declaration = nodePath.node.declaration;
|
|
146
|
-
if (!declaration) return;
|
|
147
|
-
|
|
148
|
-
if (declaration.type === 'FunctionDeclaration' && declaration.id) {
|
|
149
|
-
this.extractAPIFunction(declaration, filePath, service, content);
|
|
150
|
-
} else if (declaration.type === 'VariableDeclaration') {
|
|
151
|
-
for (const decl of declaration.declarations) {
|
|
152
|
-
if (decl.init &&
|
|
153
|
-
(decl.init.type === 'ArrowFunctionExpression' ||
|
|
154
|
-
decl.init.type === 'FunctionExpression')) {
|
|
155
|
-
this.extractAPIFunctionFromVariable(decl, filePath, service, content);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
} catch (err) {
|
|
162
|
-
// Fall back to regex if babel fails
|
|
146
|
+
const result = this.collectExportedDeclarations(content);
|
|
147
|
+
if (!result) {
|
|
163
148
|
this.parseWithRegex(content, filePath, service);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const { declarations, exported } = result;
|
|
153
|
+
|
|
154
|
+
for (const [name, _exportInfo] of exported) {
|
|
155
|
+
const declInfo = declarations.get(name);
|
|
156
|
+
if (!declInfo) continue;
|
|
157
|
+
|
|
158
|
+
if (declInfo.kind === 'func') {
|
|
159
|
+
this.extractAPIFunction(declInfo.node, filePath, service, content);
|
|
160
|
+
} else {
|
|
161
|
+
this.extractAPIFunctionFromVariable(declInfo.node, filePath, service, content);
|
|
162
|
+
}
|
|
164
163
|
}
|
|
165
164
|
}
|
|
166
165
|
|
|
@@ -181,7 +180,7 @@ class APIScanner extends BaseScanner {
|
|
|
181
180
|
name,
|
|
182
181
|
params,
|
|
183
182
|
method: isAPIFunction.method || this.inferMethodFromName(name),
|
|
184
|
-
endpoint: isAPIFunction.endpoint,
|
|
183
|
+
endpoint: isAPIFunction.endpoint || 'dynamic',
|
|
185
184
|
description: jsdoc.description || '',
|
|
186
185
|
file: filePath,
|
|
187
186
|
service,
|
|
@@ -210,7 +209,7 @@ class APIScanner extends BaseScanner {
|
|
|
210
209
|
name,
|
|
211
210
|
params,
|
|
212
211
|
method: isAPIFunction.method || this.inferMethodFromName(name),
|
|
213
|
-
endpoint: isAPIFunction.endpoint,
|
|
212
|
+
endpoint: isAPIFunction.endpoint || 'dynamic',
|
|
214
213
|
description: jsdoc.description || '',
|
|
215
214
|
file: filePath,
|
|
216
215
|
service,
|
|
@@ -228,13 +227,15 @@ class APIScanner extends BaseScanner {
|
|
|
228
227
|
/^(post|create|add|save|submit)/i,
|
|
229
228
|
/^(put|update|modify|patch)/i,
|
|
230
229
|
/^(delete|remove|destroy)/i,
|
|
230
|
+
/^use(Get|Fetch|Load|Create|Update|Delete|Post|Put|Patch|Remove|Query|Mutation)/,
|
|
231
231
|
/(api|endpoint|request|mutation|query)$/i
|
|
232
232
|
];
|
|
233
233
|
|
|
234
234
|
const nameMatches = apiNamePatterns.some(p => p.test(name));
|
|
235
235
|
|
|
236
|
-
// Check function body for HTTP calls
|
|
237
|
-
|
|
236
|
+
// Check function body for HTTP calls — use full body including nested scopes
|
|
237
|
+
// Find the matching closing brace to capture nested arrow functions
|
|
238
|
+
const funcBody = this._extractFullBody(content, startPos, endPos);
|
|
238
239
|
const httpPatterns = [
|
|
239
240
|
/fetch\s*\(/,
|
|
240
241
|
/axios\./,
|
|
@@ -247,7 +248,9 @@ class APIScanner extends BaseScanner {
|
|
|
247
248
|
/apiClient/i,
|
|
248
249
|
/useSWR/,
|
|
249
250
|
/useQuery/,
|
|
250
|
-
/useMutation
|
|
251
|
+
/useMutation/,
|
|
252
|
+
/useInfiniteQuery/,
|
|
253
|
+
/useSuspenseQuery/
|
|
251
254
|
];
|
|
252
255
|
|
|
253
256
|
const bodyMatches = httpPatterns.some(p => p.test(funcBody));
|
|
@@ -280,6 +283,68 @@ class APIScanner extends BaseScanner {
|
|
|
280
283
|
};
|
|
281
284
|
}
|
|
282
285
|
|
|
286
|
+
/**
|
|
287
|
+
* Extract the full function body content, including nested scopes.
|
|
288
|
+
* Falls back to substring if brace matching fails.
|
|
289
|
+
* @param {string} content - Full file content
|
|
290
|
+
* @param {number} startPos - Start position of the function
|
|
291
|
+
* @param {number} endPos - End position from AST (may be too narrow)
|
|
292
|
+
* @returns {string} Function body content
|
|
293
|
+
*/
|
|
294
|
+
_extractFullBody(content, startPos, endPos) {
|
|
295
|
+
const MAX_SCAN = 50000; // Cap at 50KB to avoid stalls on large files
|
|
296
|
+
const braceStart = content.indexOf('{', startPos);
|
|
297
|
+
if (braceStart === -1 || braceStart > endPos + 100) {
|
|
298
|
+
return content.substring(startPos, Math.min(endPos + 500, content.length));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Match braces with string/comment awareness
|
|
302
|
+
let depth = 0;
|
|
303
|
+
let i = braceStart;
|
|
304
|
+
const limit = Math.min(content.length, braceStart + MAX_SCAN);
|
|
305
|
+
while (i < limit) {
|
|
306
|
+
const ch = content[i];
|
|
307
|
+
|
|
308
|
+
// Skip single-line comments
|
|
309
|
+
if (ch === '/' && content[i + 1] === '/') {
|
|
310
|
+
i = content.indexOf('\n', i + 2);
|
|
311
|
+
if (i === -1) break;
|
|
312
|
+
i++;
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
// Skip multi-line comments
|
|
316
|
+
if (ch === '/' && content[i + 1] === '*') {
|
|
317
|
+
i = content.indexOf('*/', i + 2);
|
|
318
|
+
if (i === -1) break;
|
|
319
|
+
i += 2;
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
// Skip string literals
|
|
323
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
324
|
+
i++;
|
|
325
|
+
while (i < limit) {
|
|
326
|
+
if (content[i] === '\\') { i += 2; continue; }
|
|
327
|
+
if (content[i] === ch) break;
|
|
328
|
+
i++;
|
|
329
|
+
}
|
|
330
|
+
i++;
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
if (ch === '{') depth++;
|
|
335
|
+
else if (ch === '}') {
|
|
336
|
+
depth--;
|
|
337
|
+
if (depth === 0) {
|
|
338
|
+
return content.substring(startPos, i + 1);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
i++;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Fallback to AST bounds with buffer
|
|
345
|
+
return content.substring(startPos, Math.min(endPos + 200, content.length));
|
|
346
|
+
}
|
|
347
|
+
|
|
283
348
|
/**
|
|
284
349
|
* Infer HTTP method from function name
|
|
285
350
|
*/
|
|
@@ -294,56 +359,55 @@ class APIScanner extends BaseScanner {
|
|
|
294
359
|
}
|
|
295
360
|
|
|
296
361
|
/**
|
|
297
|
-
* Parse with regex (fallback)
|
|
362
|
+
* Parse with regex (fallback, two-pass approach)
|
|
298
363
|
*/
|
|
299
364
|
parseWithRegex(content, filePath, service) {
|
|
300
|
-
//
|
|
301
|
-
const funcRegex = /export\s+(async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
|
|
365
|
+
// Two-pass regex: find declarations, find exports, intersect
|
|
302
366
|
let match;
|
|
303
367
|
|
|
304
|
-
|
|
305
|
-
|
|
368
|
+
// Pass 1: Find all function-like declarations
|
|
369
|
+
const declarations = new Map(); // name -> { line, paramsStr }
|
|
306
370
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
this.addClientFunction({
|
|
315
|
-
name,
|
|
316
|
-
params: this.parseParamsFromString(paramsStr),
|
|
317
|
-
method: this.inferMethodFromName(name),
|
|
318
|
-
endpoint: null,
|
|
319
|
-
description: jsdoc,
|
|
320
|
-
file: filePath,
|
|
321
|
-
service,
|
|
322
|
-
line: this.getLineNumber(content, match.index)
|
|
371
|
+
const funcRegex = /(?:export\s+(?:default\s+)?)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g;
|
|
372
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
373
|
+
declarations.set(match[1], {
|
|
374
|
+
line: this.getLineNumber(content, match.index),
|
|
375
|
+
paramsStr: match[2],
|
|
376
|
+
pos: match.index
|
|
323
377
|
});
|
|
324
378
|
}
|
|
325
379
|
|
|
326
|
-
|
|
327
|
-
const arrowRegex = /export\s+const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/g;
|
|
328
|
-
|
|
380
|
+
const arrowRegex = /(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s*)?\([^)]*\)\s*(?::\s*[^=]+)?\s*=>/g;
|
|
329
381
|
while ((match = arrowRegex.exec(content)) !== null) {
|
|
330
|
-
|
|
382
|
+
if (!declarations.has(match[1])) {
|
|
383
|
+
declarations.set(match[1], {
|
|
384
|
+
line: this.getLineNumber(content, match.index),
|
|
385
|
+
paramsStr: '',
|
|
386
|
+
pos: match.index
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Pass 2: Use shared export-collection from BaseScanner
|
|
392
|
+
const exported = this.collectExportedNamesRegex(content);
|
|
331
393
|
|
|
332
|
-
|
|
394
|
+
// Intersect: register exported declarations that look like API functions
|
|
395
|
+
for (const [name, info] of declarations) {
|
|
396
|
+
if (!exported.has(name)) continue;
|
|
397
|
+
if (!this.isLikelyAPIFunctionFromName(name) && !this.hasHTTPCall(content, info.pos)) {
|
|
333
398
|
continue;
|
|
334
399
|
}
|
|
335
400
|
|
|
336
|
-
const jsdoc = this.extractJSDocBefore(content,
|
|
337
|
-
|
|
401
|
+
const jsdoc = this.extractJSDocBefore(content, info.pos);
|
|
338
402
|
this.addClientFunction({
|
|
339
403
|
name,
|
|
340
|
-
params: [],
|
|
404
|
+
params: info.paramsStr ? this.parseParamsFromString(info.paramsStr) : [],
|
|
341
405
|
method: this.inferMethodFromName(name),
|
|
342
|
-
endpoint:
|
|
406
|
+
endpoint: 'dynamic',
|
|
343
407
|
description: jsdoc,
|
|
344
408
|
file: filePath,
|
|
345
409
|
service,
|
|
346
|
-
line:
|
|
410
|
+
line: info.line
|
|
347
411
|
});
|
|
348
412
|
}
|
|
349
413
|
}
|
|
@@ -353,6 +417,7 @@ class APIScanner extends BaseScanner {
|
|
|
353
417
|
*/
|
|
354
418
|
isLikelyAPIFunctionFromName(name) {
|
|
355
419
|
return /^(get|fetch|load|post|create|put|update|patch|delete|remove|query|mutation)/i.test(name) ||
|
|
420
|
+
/^use(Get|Fetch|Load|Create|Update|Delete|Post|Put|Patch|Remove|Query|Mutation)/i.test(name) ||
|
|
356
421
|
/(api|endpoint|request)$/i.test(name);
|
|
357
422
|
}
|
|
358
423
|
|
package/scripts/flow-audit.js
CHANGED
|
@@ -254,6 +254,134 @@ function calculateHealthScore(scores) {
|
|
|
254
254
|
};
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// ============================================================
|
|
258
|
+
// Pattern Promotion (Audit → Learning Pipeline)
|
|
259
|
+
// ============================================================
|
|
260
|
+
|
|
261
|
+
// Severity classification thresholds for audit patterns
|
|
262
|
+
const SYSTEMIC_THRESHOLD = 5; // 5+ files = HIGH severity / systemic
|
|
263
|
+
const MEDIUM_THRESHOLD = 3; // 3-4 files = MEDIUM severity
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Process AI-clustered audit findings through the learning pipeline.
|
|
267
|
+
*
|
|
268
|
+
* For each clustered pattern:
|
|
269
|
+
* 1. Check if a rule already exists in decisions.md → ENFORCEMENT_GAP
|
|
270
|
+
* 2. Record/increment in feedback-patterns.md
|
|
271
|
+
* 3. Check promotion threshold → auto-promote if met
|
|
272
|
+
* 4. Check last-audit.json → detect RECURRING patterns
|
|
273
|
+
*
|
|
274
|
+
* @param {Object[]} clusters - AI-clustered patterns from audit
|
|
275
|
+
* @param {Object} [previousAudit] - Previous audit data for recurrence detection
|
|
276
|
+
* @returns {Object} Promotion results per pattern
|
|
277
|
+
*/
|
|
278
|
+
function promoteAuditPatterns(clusters, previousAudit) {
|
|
279
|
+
const {
|
|
280
|
+
recordAuditPattern,
|
|
281
|
+
checkEnforcementGap,
|
|
282
|
+
promoteToDecisions,
|
|
283
|
+
syncToRulesDir,
|
|
284
|
+
mapAuditCategoryToLearnerCategory,
|
|
285
|
+
buildAuditRuleTemplate
|
|
286
|
+
} = require('./flow-standards-learner');
|
|
287
|
+
|
|
288
|
+
const previousPatternIds = new Set(
|
|
289
|
+
(previousAudit?.patterns || []).map(p => p.patternId)
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const results = {
|
|
293
|
+
patterns: [],
|
|
294
|
+
summary: {
|
|
295
|
+
total: clusters.length,
|
|
296
|
+
promoted: 0,
|
|
297
|
+
promotionFailed: 0,
|
|
298
|
+
tracking: 0,
|
|
299
|
+
enforcementGaps: 0,
|
|
300
|
+
newPatterns: 0,
|
|
301
|
+
recurring: 0
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
for (const cluster of clusters) {
|
|
306
|
+
const patternResult = {
|
|
307
|
+
patternId: cluster.patternId,
|
|
308
|
+
category: cluster.category,
|
|
309
|
+
description: cluster.description,
|
|
310
|
+
instanceCount: cluster.instanceCount,
|
|
311
|
+
severity: cluster.severity || (cluster.instanceCount >= SYSTEMIC_THRESHOLD ? 'HIGH' : cluster.instanceCount >= MEDIUM_THRESHOLD ? 'MEDIUM' : 'LOW'),
|
|
312
|
+
isSystemic: cluster.isSystemic || cluster.instanceCount >= SYSTEMIC_THRESHOLD,
|
|
313
|
+
status: 'NEW',
|
|
314
|
+
count: 0,
|
|
315
|
+
rootCause: null,
|
|
316
|
+
recommendation: null
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// Step 1: Check for enforcement gap
|
|
320
|
+
const gapCheck = checkEnforcementGap(cluster.patternId, cluster.description);
|
|
321
|
+
if (gapCheck.exists) {
|
|
322
|
+
patternResult.status = 'ENFORCEMENT_GAP';
|
|
323
|
+
patternResult.ruleLocation = gapCheck.section;
|
|
324
|
+
patternResult.ruleText = gapCheck.ruleText;
|
|
325
|
+
results.summary.enforcementGaps++;
|
|
326
|
+
} else {
|
|
327
|
+
// Step 2: Record/increment in feedback-patterns
|
|
328
|
+
const recordResult = recordAuditPattern(cluster);
|
|
329
|
+
|
|
330
|
+
if (recordResult.recorded) {
|
|
331
|
+
patternResult.count = recordResult.newCount;
|
|
332
|
+
|
|
333
|
+
// Step 3: Check promotion threshold
|
|
334
|
+
if (recordResult.shouldPromote) {
|
|
335
|
+
// Build a learning object for promotion (reuse learner functions)
|
|
336
|
+
const learning = {
|
|
337
|
+
canLearn: true,
|
|
338
|
+
violationType: cluster.category,
|
|
339
|
+
category: mapAuditCategoryToLearnerCategory(cluster.category),
|
|
340
|
+
patternName: cluster.description.slice(0, 100),
|
|
341
|
+
message: `${cluster.instanceCount} instances found (audit source, ${recordResult.newCount} occurrences)`,
|
|
342
|
+
ruleTemplate: buildAuditRuleTemplate(cluster)
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const promoteResult = promoteToDecisions(learning, recordResult.newCount);
|
|
346
|
+
if (promoteResult.promoted) {
|
|
347
|
+
patternResult.status = 'PROMOTED';
|
|
348
|
+
results.summary.promoted++;
|
|
349
|
+
|
|
350
|
+
// Also sync to rules dir
|
|
351
|
+
const syncLearning = {
|
|
352
|
+
...learning,
|
|
353
|
+
subcategory: cluster.patternId
|
|
354
|
+
};
|
|
355
|
+
syncToRulesDir(syncLearning);
|
|
356
|
+
} else {
|
|
357
|
+
// Distinguish "not yet at threshold" from "promotion failed"
|
|
358
|
+
patternResult.status = 'PROMOTION_FAILED';
|
|
359
|
+
patternResult.failureReason = promoteResult.reason || 'Unknown promotion failure';
|
|
360
|
+
results.summary.promotionFailed++;
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
patternResult.status = `TRACKING (${recordResult.newCount}/${recordResult.threshold})`;
|
|
364
|
+
results.summary.tracking++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Step 4: Check recurrence
|
|
370
|
+
if (previousPatternIds.has(cluster.patternId)) {
|
|
371
|
+
if (patternResult.status !== 'ENFORCEMENT_GAP' && patternResult.status !== 'PROMOTED') {
|
|
372
|
+
patternResult.status = `RECURRING — ${patternResult.status}`;
|
|
373
|
+
}
|
|
374
|
+
results.summary.recurring++;
|
|
375
|
+
} else if (patternResult.status === 'NEW' || patternResult.status.startsWith('TRACKING')) {
|
|
376
|
+
results.summary.newPatterns++;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
results.patterns.push(patternResult);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return results;
|
|
383
|
+
}
|
|
384
|
+
|
|
257
385
|
// ============================================================
|
|
258
386
|
// CLI Interface
|
|
259
387
|
// ============================================================
|
|
@@ -307,6 +435,30 @@ function main() {
|
|
|
307
435
|
break;
|
|
308
436
|
}
|
|
309
437
|
|
|
438
|
+
case 'promote': {
|
|
439
|
+
// Process AI-clustered findings through the learning pipeline
|
|
440
|
+
// Usage: node scripts/flow-audit.js promote '<clusters-json>'
|
|
441
|
+
const clustersArg = process.argv[3];
|
|
442
|
+
if (!clustersArg) {
|
|
443
|
+
console.error('Usage: flow-audit.js promote \'[{patternId, category, description, instanceCount, instances}]\'');
|
|
444
|
+
process.exit(1);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const clusters = safeJsonParseString(clustersArg, null);
|
|
448
|
+
if (!Array.isArray(clusters)) {
|
|
449
|
+
console.error('Invalid JSON clusters argument — expected an array');
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Load previous audit for recurrence detection
|
|
454
|
+
const lastAuditPath = path.join(PATHS.state, 'last-audit.json');
|
|
455
|
+
const previousAudit = safeJsonParse(lastAuditPath, {});
|
|
456
|
+
|
|
457
|
+
const promoteResults = promoteAuditPatterns(clusters, previousAudit);
|
|
458
|
+
console.log(JSON.stringify(promoteResults, null, 2));
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
|
|
310
462
|
default: {
|
|
311
463
|
console.log(`
|
|
312
464
|
Wogi Flow - Project Audit Helpers
|
|
@@ -319,9 +471,13 @@ Commands:
|
|
|
319
471
|
outdated Run npm outdated (structured JSON output)
|
|
320
472
|
audit Run npm audit (structured JSON output)
|
|
321
473
|
score Calculate weighted health score from agent grades
|
|
474
|
+
promote Process AI-clustered findings through learning pipeline
|
|
322
475
|
|
|
323
476
|
Score usage:
|
|
324
477
|
node scripts/flow-audit.js score '{"architecture":"B+","dependencies":"A-"}'
|
|
478
|
+
|
|
479
|
+
Promote usage:
|
|
480
|
+
node scripts/flow-audit.js promote '[{"patternId":"missing-error-handling","category":"security","description":"...","instanceCount":7}]'
|
|
325
481
|
`);
|
|
326
482
|
break;
|
|
327
483
|
}
|
|
@@ -333,7 +489,8 @@ module.exports = {
|
|
|
333
489
|
findTodos,
|
|
334
490
|
getOutdatedDeps,
|
|
335
491
|
getAuditResults,
|
|
336
|
-
calculateHealthScore
|
|
492
|
+
calculateHealthScore,
|
|
493
|
+
promoteAuditPatterns
|
|
337
494
|
};
|
|
338
495
|
|
|
339
496
|
if (require.main === module) {
|
|
@@ -38,6 +38,7 @@ const DEFAULT_CONFIG = {
|
|
|
38
38
|
'src/utils',
|
|
39
39
|
'src/lib',
|
|
40
40
|
'src/helpers',
|
|
41
|
+
'src/services',
|
|
41
42
|
'utils',
|
|
42
43
|
'lib',
|
|
43
44
|
'helpers',
|
|
@@ -45,6 +46,13 @@ const DEFAULT_CONFIG = {
|
|
|
45
46
|
'shared'
|
|
46
47
|
],
|
|
47
48
|
|
|
49
|
+
// Glob patterns discover co-located directories (any project structure)
|
|
50
|
+
globPatterns: [
|
|
51
|
+
'src/**/hooks',
|
|
52
|
+
'src/**/helpers',
|
|
53
|
+
'src/**/utils'
|
|
54
|
+
],
|
|
55
|
+
|
|
48
56
|
filePatterns: ['**/*.ts', '**/*.js', '**/*.tsx', '**/*.jsx'],
|
|
49
57
|
|
|
50
58
|
excludePatterns: [
|
|
@@ -70,6 +78,7 @@ class FunctionScanner extends BaseScanner {
|
|
|
70
78
|
super({
|
|
71
79
|
configKey: 'functionRegistry',
|
|
72
80
|
directories: DEFAULT_CONFIG.directories,
|
|
81
|
+
globPatterns: DEFAULT_CONFIG.globPatterns,
|
|
73
82
|
filePatterns: DEFAULT_CONFIG.filePatterns,
|
|
74
83
|
excludePatterns: DEFAULT_CONFIG.excludePatterns,
|
|
75
84
|
...config
|
|
@@ -113,42 +122,27 @@ class FunctionScanner extends BaseScanner {
|
|
|
113
122
|
}
|
|
114
123
|
|
|
115
124
|
/**
|
|
116
|
-
* Parse file with Babel AST
|
|
125
|
+
* Parse file with Babel AST using shared two-pass approach from BaseScanner.
|
|
126
|
+
* Handles every JS/TS export pattern by design — no whack-a-mole.
|
|
117
127
|
*/
|
|
118
128
|
parseWithBabel(content, filePath, category) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
sourceType: 'module',
|
|
122
|
-
plugins: ['typescript', 'jsx', 'decorators-legacy']
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
this.traverse(ast, {
|
|
126
|
-
ExportNamedDeclaration: (nodePath) => {
|
|
127
|
-
const declaration = nodePath.node.declaration;
|
|
128
|
-
if (!declaration) return;
|
|
129
|
-
|
|
130
|
-
if (declaration.type === 'FunctionDeclaration' && declaration.id) {
|
|
131
|
-
this.extractFunction(declaration, filePath, category, content);
|
|
132
|
-
} else if (declaration.type === 'VariableDeclaration') {
|
|
133
|
-
for (const decl of declaration.declarations) {
|
|
134
|
-
if (decl.init &&
|
|
135
|
-
(decl.init.type === 'ArrowFunctionExpression' ||
|
|
136
|
-
decl.init.type === 'FunctionExpression')) {
|
|
137
|
-
this.extractFunctionFromVariable(decl, filePath, category, content);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
},
|
|
142
|
-
ExportDefaultDeclaration: (nodePath) => {
|
|
143
|
-
const declaration = nodePath.node.declaration;
|
|
144
|
-
if (declaration.type === 'FunctionDeclaration' && declaration.id) {
|
|
145
|
-
this.extractFunction(declaration, filePath, category, content, true);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
} catch (err) {
|
|
150
|
-
// Fall back to regex if babel fails
|
|
129
|
+
const result = this.collectExportedDeclarations(content);
|
|
130
|
+
if (!result) {
|
|
151
131
|
this.parseWithRegex(content, filePath, category);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const { declarations, exported } = result;
|
|
136
|
+
|
|
137
|
+
for (const [name, exportInfo] of exported) {
|
|
138
|
+
const declInfo = declarations.get(name);
|
|
139
|
+
if (!declInfo) continue;
|
|
140
|
+
|
|
141
|
+
if (declInfo.kind === 'func') {
|
|
142
|
+
this.extractFunction(declInfo.node, filePath, category, content, exportInfo.isDefault);
|
|
143
|
+
} else {
|
|
144
|
+
this.extractFunctionFromVariable(declInfo.node, filePath, category, content);
|
|
145
|
+
}
|
|
152
146
|
}
|
|
153
147
|
}
|
|
154
148
|
|
|
@@ -223,44 +217,52 @@ class FunctionScanner extends BaseScanner {
|
|
|
223
217
|
* Parse file with regex (fallback)
|
|
224
218
|
*/
|
|
225
219
|
parseWithRegex(content, filePath, category) {
|
|
226
|
-
//
|
|
227
|
-
const
|
|
220
|
+
// Pass 1: Find all function-like declarations
|
|
221
|
+
const declarations = new Map();
|
|
228
222
|
let match;
|
|
229
223
|
|
|
224
|
+
const functionRegex = /(?:export\s+(?:default\s+)?)?(?:(async)\s+)?function\s+(\w+)\s*(?:<[^>]*>)?\s*\(([^)]*)\)(?:\s*:\s*([^\s{]+))?\s*\{/g;
|
|
230
225
|
while ((match = functionRegex.exec(content)) !== null) {
|
|
231
|
-
const [, isAsync, name,
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
this.addFunction({
|
|
236
|
-
name,
|
|
237
|
-
params,
|
|
226
|
+
const [, isAsync, name, paramsStr, returnType] = match;
|
|
227
|
+
declarations.set(name, {
|
|
228
|
+
line: this.getLineNumber(content, match.index),
|
|
229
|
+
params: this.parseParamsFromString(paramsStr),
|
|
238
230
|
returnType: returnType || (isAsync ? 'Promise<any>' : null),
|
|
239
|
-
|
|
240
|
-
file: filePath,
|
|
241
|
-
category,
|
|
242
|
-
isDefault: false,
|
|
243
|
-
line: this.getLineNumber(content, match.index)
|
|
231
|
+
jsdoc: this.extractJSDocBefore(content, match.index)
|
|
244
232
|
});
|
|
245
233
|
}
|
|
246
234
|
|
|
247
|
-
//
|
|
248
|
-
const arrowRegex = /export\s+const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\([^)]
|
|
249
|
-
|
|
235
|
+
// Const arrow functions — capture params group (finding-005 fix)
|
|
236
|
+
const arrowRegex = /(?:export\s+)?const\s+(\w+)\s*(?::\s*[^=]+)?\s*=\s*(?:async\s*)?\(([^)]*)\)\s*(?::\s*([^\s=>]+))?\s*=>/g;
|
|
250
237
|
while ((match = arrowRegex.exec(content)) !== null) {
|
|
251
|
-
const [, name, returnType] = match;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
238
|
+
const [, name, paramsStr, returnType] = match;
|
|
239
|
+
if (!declarations.has(name)) {
|
|
240
|
+
declarations.set(name, {
|
|
241
|
+
line: this.getLineNumber(content, match.index),
|
|
242
|
+
params: paramsStr ? this.parseParamsFromString(paramsStr) : [],
|
|
243
|
+
returnType,
|
|
244
|
+
jsdoc: this.extractJSDocBefore(content, match.index)
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Pass 2: Use shared export-collection from BaseScanner
|
|
250
|
+
const exported = this.collectExportedNamesRegex(content);
|
|
251
|
+
|
|
252
|
+
// Intersect: register all exported declarations
|
|
253
|
+
for (const [name, info] of declarations) {
|
|
254
|
+
if (exported.has(name)) {
|
|
255
|
+
this.addFunction({
|
|
256
|
+
name,
|
|
257
|
+
params: info.params,
|
|
258
|
+
returnType: info.returnType,
|
|
259
|
+
description: info.jsdoc,
|
|
260
|
+
file: filePath,
|
|
261
|
+
category,
|
|
262
|
+
isDefault: false,
|
|
263
|
+
line: info.line
|
|
264
|
+
});
|
|
265
|
+
}
|
|
264
266
|
}
|
|
265
267
|
}
|
|
266
268
|
|
|
@@ -92,7 +92,7 @@ const IGNORE_PATTERNS = [
|
|
|
92
92
|
];
|
|
93
93
|
|
|
94
94
|
// Colors for CLI output
|
|
95
|
-
const { colors: c } = require('./flow-output');
|
|
95
|
+
const { colors: c, getTodayDate } = require('./flow-output');
|
|
96
96
|
|
|
97
97
|
// ============================================================================
|
|
98
98
|
// Utility Functions
|