wogiflow 1.0.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/.workflow/agents/reviewer.md +81 -0
- package/.workflow/agents/security.md +94 -0
- package/.workflow/agents/story-writer.md +58 -0
- package/.workflow/bridges/base-bridge.js +395 -0
- package/.workflow/bridges/claude-bridge.js +434 -0
- package/.workflow/bridges/index.js +130 -0
- package/.workflow/lib/assumption-detector.js +481 -0
- package/.workflow/lib/config-substitution.js +371 -0
- package/.workflow/lib/failure-categories.js +478 -0
- package/.workflow/state/app-map.md.template +15 -0
- package/.workflow/state/architecture.md.template +24 -0
- package/.workflow/state/component-index.json.template +5 -0
- package/.workflow/state/decisions.md.template +15 -0
- package/.workflow/state/feedback-patterns.md.template +9 -0
- package/.workflow/state/knowledge-sync.json.template +6 -0
- package/.workflow/state/progress.md.template +14 -0
- package/.workflow/state/ready.json.template +7 -0
- package/.workflow/state/request-log.md.template +14 -0
- package/.workflow/state/session-state.json.template +11 -0
- package/.workflow/state/stack.md.template +33 -0
- package/.workflow/state/testing.md.template +36 -0
- package/.workflow/templates/claude-md.hbs +257 -0
- package/.workflow/templates/correction-report.md +67 -0
- package/.workflow/templates/gemini-md.hbs +52 -0
- package/README.md +1802 -0
- package/bin/flow +205 -0
- package/lib/index.js +33 -0
- package/lib/installer.js +467 -0
- package/lib/release-channel.js +269 -0
- package/lib/skill-registry.js +526 -0
- package/lib/upgrader.js +401 -0
- package/lib/utils.js +305 -0
- package/package.json +64 -0
- package/scripts/flow +985 -0
- package/scripts/flow-adaptive-learning.js +1259 -0
- package/scripts/flow-aggregate.js +488 -0
- package/scripts/flow-archive +133 -0
- package/scripts/flow-auto-context.js +1015 -0
- package/scripts/flow-auto-learn.js +615 -0
- package/scripts/flow-bridge.js +223 -0
- package/scripts/flow-browser-suggest.js +316 -0
- package/scripts/flow-bug.js +247 -0
- package/scripts/flow-cascade.js +711 -0
- package/scripts/flow-changelog +85 -0
- package/scripts/flow-checkpoint.js +483 -0
- package/scripts/flow-cli.js +403 -0
- package/scripts/flow-code-intelligence.js +760 -0
- package/scripts/flow-complexity.js +502 -0
- package/scripts/flow-config-set.js +152 -0
- package/scripts/flow-constants.js +157 -0
- package/scripts/flow-context +152 -0
- package/scripts/flow-context-init.js +482 -0
- package/scripts/flow-context-monitor.js +384 -0
- package/scripts/flow-context-scoring.js +886 -0
- package/scripts/flow-correct.js +458 -0
- package/scripts/flow-damage-control.js +985 -0
- package/scripts/flow-deps +101 -0
- package/scripts/flow-diff.js +700 -0
- package/scripts/flow-done +151 -0
- package/scripts/flow-done.js +489 -0
- package/scripts/flow-durable-session.js +1541 -0
- package/scripts/flow-entropy-monitor.js +345 -0
- package/scripts/flow-export-profile +349 -0
- package/scripts/flow-export-scanner.js +1046 -0
- package/scripts/flow-figma-confirm.js +400 -0
- package/scripts/flow-figma-extract.js +496 -0
- package/scripts/flow-figma-generate.js +683 -0
- package/scripts/flow-figma-index.js +909 -0
- package/scripts/flow-figma-match.js +617 -0
- package/scripts/flow-figma-mcp-server.js +518 -0
- package/scripts/flow-figma-pipeline.js +414 -0
- package/scripts/flow-file-ops.js +301 -0
- package/scripts/flow-gate-confidence.js +825 -0
- package/scripts/flow-guided-edit.js +659 -0
- package/scripts/flow-health +185 -0
- package/scripts/flow-health.js +413 -0
- package/scripts/flow-hooks.js +556 -0
- package/scripts/flow-http-client.js +249 -0
- package/scripts/flow-hybrid-detect.js +167 -0
- package/scripts/flow-hybrid-interactive.js +591 -0
- package/scripts/flow-hybrid-test.js +152 -0
- package/scripts/flow-import-profile +439 -0
- package/scripts/flow-init +253 -0
- package/scripts/flow-instruction-richness.js +827 -0
- package/scripts/flow-jira-integration.js +579 -0
- package/scripts/flow-knowledge-router.js +522 -0
- package/scripts/flow-knowledge-sync.js +589 -0
- package/scripts/flow-linear-integration.js +631 -0
- package/scripts/flow-links.js +774 -0
- package/scripts/flow-log-manager.js +559 -0
- package/scripts/flow-loop-enforcer.js +1246 -0
- package/scripts/flow-loop-retry-learning.js +630 -0
- package/scripts/flow-lsp.js +923 -0
- package/scripts/flow-map-index +348 -0
- package/scripts/flow-map-sync +201 -0
- package/scripts/flow-memory-blocks.js +668 -0
- package/scripts/flow-memory-compactor.js +350 -0
- package/scripts/flow-memory-db.js +1110 -0
- package/scripts/flow-memory-sync.js +484 -0
- package/scripts/flow-metrics.js +353 -0
- package/scripts/flow-migrate-ids.js +370 -0
- package/scripts/flow-model-adapter.js +802 -0
- package/scripts/flow-model-router.js +884 -0
- package/scripts/flow-models.js +1231 -0
- package/scripts/flow-morning.js +517 -0
- package/scripts/flow-multi-approach.js +660 -0
- package/scripts/flow-new-feature +86 -0
- package/scripts/flow-onboard +1042 -0
- package/scripts/flow-orchestrate-llm.js +459 -0
- package/scripts/flow-orchestrate.js +3592 -0
- package/scripts/flow-output.js +123 -0
- package/scripts/flow-parallel-detector.js +399 -0
- package/scripts/flow-parallel-dispatch.js +987 -0
- package/scripts/flow-parallel.js +428 -0
- package/scripts/flow-pattern-enforcer.js +600 -0
- package/scripts/flow-prd-manager.js +282 -0
- package/scripts/flow-progress.js +323 -0
- package/scripts/flow-project-analyzer.js +975 -0
- package/scripts/flow-prompt-composer.js +487 -0
- package/scripts/flow-providers.js +1381 -0
- package/scripts/flow-queue.js +308 -0
- package/scripts/flow-ready +82 -0
- package/scripts/flow-ready.js +189 -0
- package/scripts/flow-regression.js +396 -0
- package/scripts/flow-response-parser.js +450 -0
- package/scripts/flow-resume.js +284 -0
- package/scripts/flow-rules-sync.js +439 -0
- package/scripts/flow-run-trace.js +718 -0
- package/scripts/flow-safety.js +587 -0
- package/scripts/flow-search +104 -0
- package/scripts/flow-security.js +481 -0
- package/scripts/flow-session-end +106 -0
- package/scripts/flow-session-end.js +437 -0
- package/scripts/flow-session-state.js +671 -0
- package/scripts/flow-setup-hooks +216 -0
- package/scripts/flow-setup-hooks.js +377 -0
- package/scripts/flow-skill-create.js +329 -0
- package/scripts/flow-skill-creator.js +572 -0
- package/scripts/flow-skill-generator.js +1046 -0
- package/scripts/flow-skill-learn.js +880 -0
- package/scripts/flow-skill-matcher.js +578 -0
- package/scripts/flow-spec-generator.js +820 -0
- package/scripts/flow-stack-wizard.js +895 -0
- package/scripts/flow-standup +162 -0
- package/scripts/flow-start +74 -0
- package/scripts/flow-start.js +235 -0
- package/scripts/flow-status +110 -0
- package/scripts/flow-status.js +301 -0
- package/scripts/flow-step-browser.js +83 -0
- package/scripts/flow-step-changelog.js +217 -0
- package/scripts/flow-step-comments.js +306 -0
- package/scripts/flow-step-complexity.js +234 -0
- package/scripts/flow-step-coverage.js +218 -0
- package/scripts/flow-step-knowledge.js +193 -0
- package/scripts/flow-step-pr-tests.js +364 -0
- package/scripts/flow-step-regression.js +89 -0
- package/scripts/flow-step-review.js +516 -0
- package/scripts/flow-step-security.js +162 -0
- package/scripts/flow-step-silent-failures.js +290 -0
- package/scripts/flow-step-simplifier.js +346 -0
- package/scripts/flow-story +105 -0
- package/scripts/flow-story.js +500 -0
- package/scripts/flow-suspend.js +252 -0
- package/scripts/flow-sync-daemon.js +654 -0
- package/scripts/flow-task-analyzer.js +606 -0
- package/scripts/flow-team-dashboard.js +748 -0
- package/scripts/flow-team-sync.js +752 -0
- package/scripts/flow-team.js +977 -0
- package/scripts/flow-tech-options.js +528 -0
- package/scripts/flow-templates.js +812 -0
- package/scripts/flow-tiered-learning.js +728 -0
- package/scripts/flow-trace +204 -0
- package/scripts/flow-transcript-chunking.js +1106 -0
- package/scripts/flow-transcript-digest.js +7918 -0
- package/scripts/flow-transcript-language.js +465 -0
- package/scripts/flow-transcript-parsing.js +1085 -0
- package/scripts/flow-transcript-stories.js +2194 -0
- package/scripts/flow-update-map +224 -0
- package/scripts/flow-utils.js +2242 -0
- package/scripts/flow-verification.js +644 -0
- package/scripts/flow-verify.js +1177 -0
- package/scripts/flow-voice-input.js +638 -0
- package/scripts/flow-watch +168 -0
- package/scripts/flow-workflow-steps.js +521 -0
- package/scripts/flow-workflow.js +1029 -0
- package/scripts/flow-worktree.js +489 -0
- package/scripts/hooks/adapters/base-adapter.js +102 -0
- package/scripts/hooks/adapters/claude-code.js +359 -0
- package/scripts/hooks/adapters/index.js +79 -0
- package/scripts/hooks/core/component-check.js +341 -0
- package/scripts/hooks/core/index.js +35 -0
- package/scripts/hooks/core/loop-check.js +241 -0
- package/scripts/hooks/core/session-context.js +294 -0
- package/scripts/hooks/core/task-gate.js +177 -0
- package/scripts/hooks/core/validation.js +230 -0
- package/scripts/hooks/entry/claude-code/post-tool-use.js +65 -0
- package/scripts/hooks/entry/claude-code/pre-tool-use.js +89 -0
- package/scripts/hooks/entry/claude-code/session-end.js +87 -0
- package/scripts/hooks/entry/claude-code/session-start.js +46 -0
- package/scripts/hooks/entry/claude-code/stop.js +43 -0
- package/scripts/postinstall.js +139 -0
- package/templates/browser-test-flow.json +56 -0
- package/templates/bug-report.md +43 -0
- package/templates/component-detail.md +42 -0
- package/templates/component.stories.tsx +49 -0
- package/templates/context/constraints.md +83 -0
- package/templates/context/conventions.md +177 -0
- package/templates/context/stack.md +60 -0
- package/templates/correction-report.md +90 -0
- package/templates/feature-proposal.md +35 -0
- package/templates/hybrid/_base.md +254 -0
- package/templates/hybrid/_patterns.md +45 -0
- package/templates/hybrid/create-component.md +127 -0
- package/templates/hybrid/create-file.md +56 -0
- package/templates/hybrid/create-hook.md +145 -0
- package/templates/hybrid/create-service.md +70 -0
- package/templates/hybrid/fix-bug.md +33 -0
- package/templates/hybrid/modify-file.md +55 -0
- package/templates/story.md +68 -0
- package/templates/task.json +56 -0
- package/templates/trace.md +69 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Silent Failure Hunter Step
|
|
5
|
+
*
|
|
6
|
+
* Detects code patterns that silently swallow errors or failures:
|
|
7
|
+
* - Empty catch blocks
|
|
8
|
+
* - Catch blocks that only log
|
|
9
|
+
* - Async functions without error handling
|
|
10
|
+
* - Promise chains without .catch()
|
|
11
|
+
* - try-finally without catch
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { getProjectRoot, colors } = require('./flow-utils');
|
|
17
|
+
|
|
18
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run silent failure detection as a workflow step
|
|
22
|
+
*
|
|
23
|
+
* @param {object} options
|
|
24
|
+
* @param {string[]} options.files - Files modified
|
|
25
|
+
* @param {object} options.stepConfig - Step configuration
|
|
26
|
+
* @param {string} options.mode - Step mode (block/warn/prompt/auto)
|
|
27
|
+
* @returns {object} - { passed: boolean, message: string, details?: object[] }
|
|
28
|
+
*/
|
|
29
|
+
async function run(options = {}) {
|
|
30
|
+
const { files = [], stepConfig = {} } = options;
|
|
31
|
+
const checkEmptyCatch = stepConfig.checkEmptyCatch !== false;
|
|
32
|
+
const checkLogOnlyCatch = stepConfig.checkLogOnlyCatch !== false;
|
|
33
|
+
const checkUnhandledAsync = stepConfig.checkUnhandledAsync !== false;
|
|
34
|
+
const checkPromiseChains = stepConfig.checkPromiseChains !== false;
|
|
35
|
+
|
|
36
|
+
// Filter to analyzable files
|
|
37
|
+
const analyzableExtensions = ['.js', '.ts', '.jsx', '.tsx'];
|
|
38
|
+
const analyzableFiles = files.filter(f =>
|
|
39
|
+
analyzableExtensions.some(ext => f.endsWith(ext)) &&
|
|
40
|
+
!f.includes('.test.') &&
|
|
41
|
+
!f.includes('.spec.') &&
|
|
42
|
+
!f.includes('.d.ts')
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (analyzableFiles.length === 0) {
|
|
46
|
+
return { passed: true, message: 'No files to analyze' };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const issues = [];
|
|
50
|
+
|
|
51
|
+
for (const file of analyzableFiles) {
|
|
52
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
53
|
+
if (!fs.existsSync(filePath)) continue;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
57
|
+
const fileIssues = analyzeForSilentFailures(content, file, {
|
|
58
|
+
checkEmptyCatch,
|
|
59
|
+
checkLogOnlyCatch,
|
|
60
|
+
checkUnhandledAsync,
|
|
61
|
+
checkPromiseChains,
|
|
62
|
+
});
|
|
63
|
+
issues.push(...fileIssues);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
// Skip unreadable files
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (issues.length === 0) {
|
|
70
|
+
return {
|
|
71
|
+
passed: true,
|
|
72
|
+
message: 'No silent failure patterns detected',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Report issues
|
|
77
|
+
console.log(colors.yellow + '\n Silent Failure Patterns Detected:' + colors.reset);
|
|
78
|
+
for (const issue of issues) {
|
|
79
|
+
const icon = issue.severity === 'high' ? '\u{1F534}' : '\u{1F7E1}';
|
|
80
|
+
console.log(` ${icon} ${issue.file}:${issue.line}`);
|
|
81
|
+
console.log(` ${issue.type}: ${issue.message}`);
|
|
82
|
+
if (issue.suggestion) {
|
|
83
|
+
console.log(colors.dim + ` \u{2192} ${issue.suggestion}` + colors.reset);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const highSeverity = issues.filter(i => i.severity === 'high');
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
passed: highSeverity.length === 0,
|
|
91
|
+
message: `${issues.length} silent failure pattern(s) found (${highSeverity.length} high severity)`,
|
|
92
|
+
details: issues,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Analyze code for silent failure patterns
|
|
98
|
+
*/
|
|
99
|
+
function analyzeForSilentFailures(content, fileName, config) {
|
|
100
|
+
const issues = [];
|
|
101
|
+
const lines = content.split('\n');
|
|
102
|
+
|
|
103
|
+
// Track context for multi-line analysis
|
|
104
|
+
let inTryBlock = false;
|
|
105
|
+
let inCatchBlock = false;
|
|
106
|
+
let catchStartLine = 0;
|
|
107
|
+
let catchContent = [];
|
|
108
|
+
let braceDepth = 0;
|
|
109
|
+
|
|
110
|
+
for (let i = 0; i < lines.length; i++) {
|
|
111
|
+
const line = lines[i];
|
|
112
|
+
const trimmed = line.trim();
|
|
113
|
+
|
|
114
|
+
// Detect try-catch structure
|
|
115
|
+
if (/\btry\s*\{/.test(line)) {
|
|
116
|
+
inTryBlock = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (inTryBlock && /\bcatch\s*\([^)]*\)\s*\{/.test(line)) {
|
|
120
|
+
inCatchBlock = true;
|
|
121
|
+
catchStartLine = i + 1;
|
|
122
|
+
catchContent = [];
|
|
123
|
+
braceDepth = 1;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (inCatchBlock) {
|
|
128
|
+
catchContent.push(trimmed);
|
|
129
|
+
braceDepth += (line.match(/\{/g) || []).length;
|
|
130
|
+
braceDepth -= (line.match(/\}/g) || []).length;
|
|
131
|
+
|
|
132
|
+
if (braceDepth <= 0) {
|
|
133
|
+
// Analyze the complete catch block
|
|
134
|
+
const catchIssues = analyzeCatchBlock(catchContent, catchStartLine, fileName, config);
|
|
135
|
+
issues.push(...catchIssues);
|
|
136
|
+
|
|
137
|
+
inCatchBlock = false;
|
|
138
|
+
inTryBlock = false;
|
|
139
|
+
catchContent = [];
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for try-finally without catch (config.checkUnhandledAsync)
|
|
145
|
+
if (config.checkUnhandledAsync && /\btry\s*\{/.test(line)) {
|
|
146
|
+
// Look ahead for finally without catch
|
|
147
|
+
const tryContext = content.substring(content.indexOf(line));
|
|
148
|
+
if (/try\s*\{[^}]*\}\s*finally/.test(tryContext) && !/catch\s*\(/.test(tryContext.substring(0, tryContext.indexOf('finally')))) {
|
|
149
|
+
issues.push({
|
|
150
|
+
file: fileName,
|
|
151
|
+
line: i + 1,
|
|
152
|
+
type: 'Try-Finally Without Catch',
|
|
153
|
+
severity: 'medium',
|
|
154
|
+
message: 'try-finally without catch will not handle errors',
|
|
155
|
+
suggestion: 'Add a catch block or let errors propagate intentionally',
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check for unhandled promise chains (config.checkPromiseChains)
|
|
161
|
+
if (config.checkPromiseChains) {
|
|
162
|
+
// Look for .then() without .catch()
|
|
163
|
+
if (/\.then\s*\(/.test(line) && !line.includes('.catch(') && !line.includes('await')) {
|
|
164
|
+
// Check if .catch() is on the next few lines
|
|
165
|
+
const nextLines = lines.slice(i, i + 5).join(' ');
|
|
166
|
+
if (!nextLines.includes('.catch(') && !nextLines.includes('.finally(')) {
|
|
167
|
+
issues.push({
|
|
168
|
+
file: fileName,
|
|
169
|
+
line: i + 1,
|
|
170
|
+
type: 'Unhandled Promise Chain',
|
|
171
|
+
severity: 'medium',
|
|
172
|
+
message: 'Promise chain without .catch() handler',
|
|
173
|
+
suggestion: 'Add .catch() handler or use async/await with try-catch',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check for async functions without try-catch (config.checkUnhandledAsync)
|
|
180
|
+
if (config.checkUnhandledAsync) {
|
|
181
|
+
const asyncMatch = line.match(/async\s+(?:function\s+)?(\w+)?/);
|
|
182
|
+
if (asyncMatch && !line.includes('test') && !line.includes('spec')) {
|
|
183
|
+
// Find the function body
|
|
184
|
+
const funcStart = i;
|
|
185
|
+
let funcBraceDepth = 0;
|
|
186
|
+
let funcStarted = false;
|
|
187
|
+
let funcContent = '';
|
|
188
|
+
|
|
189
|
+
for (let j = i; j < Math.min(i + 100, lines.length); j++) {
|
|
190
|
+
funcContent += lines[j] + '\n';
|
|
191
|
+
funcBraceDepth += (lines[j].match(/\{/g) || []).length;
|
|
192
|
+
funcBraceDepth -= (lines[j].match(/\}/g) || []).length;
|
|
193
|
+
|
|
194
|
+
if (lines[j].includes('{')) funcStarted = true;
|
|
195
|
+
if (funcStarted && funcBraceDepth === 0) break;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Check if function has await but no try-catch
|
|
199
|
+
if (/\bawait\b/.test(funcContent) && !/\btry\s*\{/.test(funcContent)) {
|
|
200
|
+
const funcName = asyncMatch[1] || 'anonymous';
|
|
201
|
+
// Don't flag if it's a short function (likely a wrapper)
|
|
202
|
+
if (funcContent.split('\n').length > 5) {
|
|
203
|
+
issues.push({
|
|
204
|
+
file: fileName,
|
|
205
|
+
line: funcStart + 1,
|
|
206
|
+
type: 'Unhandled Async',
|
|
207
|
+
severity: 'medium',
|
|
208
|
+
message: `Async function "${funcName}" uses await without try-catch`,
|
|
209
|
+
suggestion: 'Wrap await calls in try-catch or document that errors propagate',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return issues;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Analyze a catch block for silent failure patterns
|
|
222
|
+
*/
|
|
223
|
+
function analyzeCatchBlock(catchLines, startLine, fileName, config) {
|
|
224
|
+
const issues = [];
|
|
225
|
+
const catchBody = catchLines.join('\n').trim();
|
|
226
|
+
|
|
227
|
+
// Remove the closing brace if present
|
|
228
|
+
const cleanBody = catchBody.replace(/\}\s*$/, '').trim();
|
|
229
|
+
|
|
230
|
+
// Check for empty catch block
|
|
231
|
+
if (config.checkEmptyCatch && cleanBody.length === 0) {
|
|
232
|
+
issues.push({
|
|
233
|
+
file: fileName,
|
|
234
|
+
line: startLine,
|
|
235
|
+
type: 'Empty Catch Block',
|
|
236
|
+
severity: 'high',
|
|
237
|
+
message: 'Empty catch block silently swallows all errors',
|
|
238
|
+
suggestion: 'Log the error, rethrow it, or handle it appropriately',
|
|
239
|
+
});
|
|
240
|
+
return issues;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check for catch blocks that only log
|
|
244
|
+
if (config.checkLogOnlyCatch) {
|
|
245
|
+
const onlyLogging = /^(?:console\.(?:log|error|warn)|logger\.(?:log|error|warn|info))\s*\(/i.test(cleanBody);
|
|
246
|
+
const hasOtherStatements = cleanBody.split(';').filter(s => {
|
|
247
|
+
const t = s.trim();
|
|
248
|
+
return t.length > 0 && !/^console\.|^logger\.|^\/\//.test(t);
|
|
249
|
+
}).length > 0;
|
|
250
|
+
|
|
251
|
+
if (onlyLogging && !hasOtherStatements) {
|
|
252
|
+
issues.push({
|
|
253
|
+
file: fileName,
|
|
254
|
+
line: startLine,
|
|
255
|
+
type: 'Log-Only Catch',
|
|
256
|
+
severity: 'medium',
|
|
257
|
+
message: 'Catch block only logs error without handling it',
|
|
258
|
+
suggestion: 'Consider if error should be rethrown or if recovery logic is needed',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Check for catch blocks that swallow specific errors
|
|
264
|
+
if (/return\s*(?:null|undefined|false|''|""|``)/.test(cleanBody)) {
|
|
265
|
+
issues.push({
|
|
266
|
+
file: fileName,
|
|
267
|
+
line: startLine,
|
|
268
|
+
type: 'Error Suppression',
|
|
269
|
+
severity: 'medium',
|
|
270
|
+
message: 'Catch block returns falsy value, hiding error from caller',
|
|
271
|
+
suggestion: 'Document this behavior or throw a typed error',
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Check for catch block with only a comment
|
|
276
|
+
if (/^\/[/*]/.test(cleanBody) && cleanBody.split('\n').every(l => l.trim().startsWith('//') || l.trim().startsWith('*') || l.trim() === '')) {
|
|
277
|
+
issues.push({
|
|
278
|
+
file: fileName,
|
|
279
|
+
line: startLine,
|
|
280
|
+
type: 'Comment-Only Catch',
|
|
281
|
+
severity: 'high',
|
|
282
|
+
message: 'Catch block contains only comments - errors are silently ignored',
|
|
283
|
+
suggestion: 'Add proper error handling or explicit ignore marker',
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return issues;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
module.exports = { run, analyzeForSilentFailures };
|
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wogi Flow - Code Simplifier Step
|
|
5
|
+
*
|
|
6
|
+
* AI-powered code simplification suggestions.
|
|
7
|
+
* Analyzes: nested conditionals, long functions, deep coupling.
|
|
8
|
+
* Provides specific refactoring suggestions with file:line refs.
|
|
9
|
+
*
|
|
10
|
+
* This is a QUALITATIVE analysis (vs codeComplexityCheck which is QUANTITATIVE).
|
|
11
|
+
* Both can be enabled together for comprehensive analysis.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const { getProjectRoot, colors } = require('./flow-utils');
|
|
17
|
+
|
|
18
|
+
const PROJECT_ROOT = getProjectRoot();
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Run code simplifier analysis step
|
|
22
|
+
*
|
|
23
|
+
* @param {object} options
|
|
24
|
+
* @param {string[]} options.files - Files modified
|
|
25
|
+
* @param {object} options.stepConfig - Step configuration
|
|
26
|
+
* @param {string} options.mode - Step mode (block/warn/prompt/auto)
|
|
27
|
+
* @returns {object} - { passed: boolean, message: string, details?: object[] }
|
|
28
|
+
*/
|
|
29
|
+
async function run(options = {}) {
|
|
30
|
+
const { files = [], stepConfig = {} } = options;
|
|
31
|
+
const maxFunctionLines = stepConfig.maxFunctionLines || 50;
|
|
32
|
+
const maxNestingDepth = stepConfig.maxNestingDepth || 3;
|
|
33
|
+
const suggestExtraction = stepConfig.suggestExtraction !== false;
|
|
34
|
+
|
|
35
|
+
// Filter to analyzable files
|
|
36
|
+
const analyzableExtensions = ['.js', '.ts', '.jsx', '.tsx'];
|
|
37
|
+
const analyzableFiles = files.filter(f =>
|
|
38
|
+
analyzableExtensions.some(ext => f.endsWith(ext)) &&
|
|
39
|
+
!f.includes('.test.') &&
|
|
40
|
+
!f.includes('.spec.') &&
|
|
41
|
+
!f.includes('.d.ts')
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
if (analyzableFiles.length === 0) {
|
|
45
|
+
return { passed: true, message: 'No analyzable files modified' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const suggestions = [];
|
|
49
|
+
|
|
50
|
+
for (const file of analyzableFiles) {
|
|
51
|
+
const filePath = path.join(PROJECT_ROOT, file);
|
|
52
|
+
if (!fs.existsSync(filePath)) continue;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
56
|
+
const fileSuggestions = analyzeForSimplification(content, file, {
|
|
57
|
+
maxFunctionLines,
|
|
58
|
+
maxNestingDepth,
|
|
59
|
+
suggestExtraction,
|
|
60
|
+
});
|
|
61
|
+
suggestions.push(...fileSuggestions);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// Skip files that can't be analyzed
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (suggestions.length === 0) {
|
|
68
|
+
return {
|
|
69
|
+
passed: true,
|
|
70
|
+
message: 'No simplification suggestions',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Report suggestions
|
|
75
|
+
console.log(colors.cyan + '\n Code Simplification Suggestions:' + colors.reset);
|
|
76
|
+
for (const suggestion of suggestions) {
|
|
77
|
+
const icon = suggestion.severity === 'high' ? '\u{1F534}' : '\u{1F7E1}';
|
|
78
|
+
console.log(` ${icon} ${suggestion.file}:${suggestion.line}`);
|
|
79
|
+
console.log(` ${suggestion.type}: ${suggestion.message}`);
|
|
80
|
+
if (suggestion.suggestion) {
|
|
81
|
+
console.log(colors.dim + ` \u{2192} ${suggestion.suggestion}` + colors.reset);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Mode determines if this blocks
|
|
86
|
+
const highSeverity = suggestions.filter(s => s.severity === 'high');
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
passed: highSeverity.length === 0,
|
|
90
|
+
message: `${suggestions.length} simplification suggestion(s) (${highSeverity.length} high severity)`,
|
|
91
|
+
details: suggestions,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Analyze code for simplification opportunities
|
|
97
|
+
*/
|
|
98
|
+
function analyzeForSimplification(content, fileName, config) {
|
|
99
|
+
const suggestions = [];
|
|
100
|
+
const lines = content.split('\n');
|
|
101
|
+
|
|
102
|
+
// Track function boundaries and analyze each
|
|
103
|
+
let inFunction = false;
|
|
104
|
+
let functionName = '';
|
|
105
|
+
let functionStart = 0;
|
|
106
|
+
let functionLines = [];
|
|
107
|
+
let braceCount = 0;
|
|
108
|
+
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
const line = lines[i];
|
|
111
|
+
|
|
112
|
+
// Detect function start
|
|
113
|
+
const funcMatch = line.match(/(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s*)?\(?.*?\)?\s*=>|(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{)/);
|
|
114
|
+
if (funcMatch && !inFunction) {
|
|
115
|
+
inFunction = true;
|
|
116
|
+
functionName = funcMatch[1] || funcMatch[2] || funcMatch[3] || 'anonymous';
|
|
117
|
+
functionStart = i + 1;
|
|
118
|
+
functionLines = [line];
|
|
119
|
+
braceCount = (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
120
|
+
|
|
121
|
+
// Arrow function without braces
|
|
122
|
+
if (line.includes('=>') && !line.includes('{')) {
|
|
123
|
+
inFunction = false;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (inFunction) {
|
|
130
|
+
functionLines.push(line);
|
|
131
|
+
braceCount += (line.match(/\{/g) || []).length - (line.match(/\}/g) || []).length;
|
|
132
|
+
|
|
133
|
+
if (braceCount <= 0) {
|
|
134
|
+
// Analyze the complete function
|
|
135
|
+
const funcSuggestions = analyzeFunctionForSimplification(
|
|
136
|
+
functionLines,
|
|
137
|
+
functionName,
|
|
138
|
+
functionStart,
|
|
139
|
+
fileName,
|
|
140
|
+
config
|
|
141
|
+
);
|
|
142
|
+
suggestions.push(...funcSuggestions);
|
|
143
|
+
inFunction = false;
|
|
144
|
+
functionLines = [];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Check for deeply nested structures anywhere
|
|
149
|
+
const nestingDepth = countNestingDepth(line);
|
|
150
|
+
if (nestingDepth > config.maxNestingDepth) {
|
|
151
|
+
suggestions.push({
|
|
152
|
+
file: fileName,
|
|
153
|
+
line: i + 1,
|
|
154
|
+
type: 'Deep Nesting',
|
|
155
|
+
severity: nestingDepth > config.maxNestingDepth + 2 ? 'high' : 'medium',
|
|
156
|
+
message: `Nesting depth ${nestingDepth} exceeds threshold ${config.maxNestingDepth}`,
|
|
157
|
+
suggestion: 'Consider early returns, guard clauses, or extracting to helper functions',
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Check for code duplication patterns
|
|
163
|
+
const duplicationSuggestions = findDuplicationPatterns(content, fileName);
|
|
164
|
+
suggestions.push(...duplicationSuggestions);
|
|
165
|
+
|
|
166
|
+
return suggestions;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Analyze a single function for simplification
|
|
171
|
+
*/
|
|
172
|
+
function analyzeFunctionForSimplification(lines, funcName, startLine, fileName, config) {
|
|
173
|
+
const suggestions = [];
|
|
174
|
+
const content = lines.join('\n');
|
|
175
|
+
|
|
176
|
+
// Check function length
|
|
177
|
+
if (lines.length > config.maxFunctionLines) {
|
|
178
|
+
suggestions.push({
|
|
179
|
+
file: fileName,
|
|
180
|
+
line: startLine,
|
|
181
|
+
type: 'Long Function',
|
|
182
|
+
severity: lines.length > config.maxFunctionLines * 2 ? 'high' : 'medium',
|
|
183
|
+
message: `Function "${funcName}" is ${lines.length} lines (max ${config.maxFunctionLines})`,
|
|
184
|
+
suggestion: config.suggestExtraction
|
|
185
|
+
? 'Consider extracting logical sections into separate functions'
|
|
186
|
+
: 'Consider breaking into smaller functions',
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check for multiple responsibilities (many different operations)
|
|
191
|
+
const operations = countOperationTypes(content);
|
|
192
|
+
if (operations.types > 4) {
|
|
193
|
+
suggestions.push({
|
|
194
|
+
file: fileName,
|
|
195
|
+
line: startLine,
|
|
196
|
+
type: 'Multiple Responsibilities',
|
|
197
|
+
severity: 'medium',
|
|
198
|
+
message: `Function "${funcName}" appears to have ${operations.types} different operation types`,
|
|
199
|
+
suggestion: 'Consider Single Responsibility Principle - one function, one purpose',
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check for nested callbacks (callback hell)
|
|
204
|
+
const callbackDepth = countCallbackDepth(content);
|
|
205
|
+
if (callbackDepth > 2) {
|
|
206
|
+
suggestions.push({
|
|
207
|
+
file: fileName,
|
|
208
|
+
line: startLine,
|
|
209
|
+
type: 'Callback Nesting',
|
|
210
|
+
severity: callbackDepth > 4 ? 'high' : 'medium',
|
|
211
|
+
message: `Function "${funcName}" has ${callbackDepth} levels of callback nesting`,
|
|
212
|
+
suggestion: 'Consider using async/await, Promise.all, or extracting callbacks',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Check for complex conditionals
|
|
217
|
+
const complexConditions = findComplexConditions(content);
|
|
218
|
+
for (const condition of complexConditions) {
|
|
219
|
+
suggestions.push({
|
|
220
|
+
file: fileName,
|
|
221
|
+
line: startLine + condition.lineOffset,
|
|
222
|
+
type: 'Complex Condition',
|
|
223
|
+
severity: condition.complexity > 4 ? 'high' : 'medium',
|
|
224
|
+
message: `Complex conditional with ${condition.complexity} clauses`,
|
|
225
|
+
suggestion: 'Consider extracting to a named boolean variable or function',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return suggestions;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Count nesting depth at a line
|
|
234
|
+
*/
|
|
235
|
+
function countNestingDepth(line) {
|
|
236
|
+
const leading = line.match(/^(\s*)/)?.[1] || '';
|
|
237
|
+
// Approximate: 2 spaces or 1 tab per level
|
|
238
|
+
const spaces = leading.replace(/\t/g, ' ').length;
|
|
239
|
+
return Math.floor(spaces / 2);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Count different operation types in code
|
|
244
|
+
*/
|
|
245
|
+
function countOperationTypes(content) {
|
|
246
|
+
const types = new Set();
|
|
247
|
+
|
|
248
|
+
if (/fetch|axios|http/i.test(content)) types.add('api');
|
|
249
|
+
if (/querySelector|getElementById|document\./i.test(content)) types.add('dom');
|
|
250
|
+
if (/localStorage|sessionStorage|cookie/i.test(content)) types.add('storage');
|
|
251
|
+
if (/console\.|log\(/i.test(content)) types.add('logging');
|
|
252
|
+
if (/fs\.|readFile|writeFile/i.test(content)) types.add('filesystem');
|
|
253
|
+
if (/\.map\(|\.filter\(|\.reduce\(/i.test(content)) types.add('transformation');
|
|
254
|
+
if (/throw\s+|new Error|reject\(/i.test(content)) types.add('error-handling');
|
|
255
|
+
if (/setState|useState|dispatch/i.test(content)) types.add('state');
|
|
256
|
+
|
|
257
|
+
return { types: types.size, list: Array.from(types) };
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Count callback nesting depth
|
|
262
|
+
*/
|
|
263
|
+
function countCallbackDepth(content) {
|
|
264
|
+
let maxDepth = 0;
|
|
265
|
+
let currentDepth = 0;
|
|
266
|
+
|
|
267
|
+
// Count by looking for callback patterns
|
|
268
|
+
const lines = content.split('\n');
|
|
269
|
+
for (const line of lines) {
|
|
270
|
+
if (/\(\s*(?:function|\([^)]*\)\s*=>|async\s*\()/.test(line)) {
|
|
271
|
+
currentDepth++;
|
|
272
|
+
maxDepth = Math.max(maxDepth, currentDepth);
|
|
273
|
+
}
|
|
274
|
+
if (/\}\s*\)/.test(line)) {
|
|
275
|
+
currentDepth = Math.max(0, currentDepth - 1);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return maxDepth;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Find complex conditional expressions
|
|
284
|
+
*/
|
|
285
|
+
function findComplexConditions(content) {
|
|
286
|
+
const conditions = [];
|
|
287
|
+
const lines = content.split('\n');
|
|
288
|
+
|
|
289
|
+
for (let i = 0; i < lines.length; i++) {
|
|
290
|
+
const line = lines[i];
|
|
291
|
+
if (/\bif\s*\(/.test(line) || /\?\s*[^:]+:/.test(line)) {
|
|
292
|
+
const andOr = (line.match(/&&|\|\|/g) || []).length;
|
|
293
|
+
const ternary = (line.match(/\?[^:]*:/g) || []).length;
|
|
294
|
+
const complexity = andOr + ternary;
|
|
295
|
+
|
|
296
|
+
if (complexity >= 3) {
|
|
297
|
+
conditions.push({
|
|
298
|
+
lineOffset: i,
|
|
299
|
+
complexity,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return conditions;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Find potential code duplication patterns
|
|
310
|
+
*/
|
|
311
|
+
function findDuplicationPatterns(content, fileName) {
|
|
312
|
+
const suggestions = [];
|
|
313
|
+
const lines = content.split('\n');
|
|
314
|
+
|
|
315
|
+
// Look for repeated patterns (simplified)
|
|
316
|
+
const patterns = {};
|
|
317
|
+
for (let i = 0; i < lines.length; i++) {
|
|
318
|
+
const line = lines[i].trim();
|
|
319
|
+
if (line.length > 20 && !line.startsWith('//') && !line.startsWith('*')) {
|
|
320
|
+
// Normalize the line for comparison
|
|
321
|
+
const normalized = line.replace(/\s+/g, ' ').replace(/['"`][^'"`]*['"`]/g, '""');
|
|
322
|
+
if (!patterns[normalized]) {
|
|
323
|
+
patterns[normalized] = [];
|
|
324
|
+
}
|
|
325
|
+
patterns[normalized].push(i + 1);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Report lines that appear 3+ times
|
|
330
|
+
for (const [pattern, lineNumbers] of Object.entries(patterns)) {
|
|
331
|
+
if (lineNumbers.length >= 3) {
|
|
332
|
+
suggestions.push({
|
|
333
|
+
file: fileName,
|
|
334
|
+
line: lineNumbers[0],
|
|
335
|
+
type: 'Code Duplication',
|
|
336
|
+
severity: lineNumbers.length >= 5 ? 'high' : 'medium',
|
|
337
|
+
message: `Similar code appears ${lineNumbers.length} times (lines: ${lineNumbers.slice(0, 5).join(', ')}...)`,
|
|
338
|
+
suggestion: 'Consider extracting to a reusable function or constant',
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return suggestions;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
module.exports = { run, analyzeForSimplification };
|