@kevinrabun/judges 3.55.0 → 3.57.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/CHANGELOG.md +24 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/api-versioning-audit.d.ts +6 -0
- package/dist/commands/api-versioning-audit.d.ts.map +1 -0
- package/dist/commands/api-versioning-audit.js +234 -0
- package/dist/commands/api-versioning-audit.js.map +1 -0
- package/dist/commands/boundary-enforce.d.ts +6 -0
- package/dist/commands/boundary-enforce.d.ts.map +1 -0
- package/dist/commands/boundary-enforce.js +256 -0
- package/dist/commands/boundary-enforce.js.map +1 -0
- package/dist/commands/cache-audit.d.ts +5 -0
- package/dist/commands/cache-audit.d.ts.map +1 -0
- package/dist/commands/cache-audit.js +220 -0
- package/dist/commands/cache-audit.js.map +1 -0
- package/dist/commands/comment-drift.d.ts +5 -0
- package/dist/commands/comment-drift.d.ts.map +1 -0
- package/dist/commands/comment-drift.js +229 -0
- package/dist/commands/comment-drift.js.map +1 -0
- package/dist/commands/error-taxonomy.d.ts +6 -0
- package/dist/commands/error-taxonomy.d.ts.map +1 -0
- package/dist/commands/error-taxonomy.js +227 -0
- package/dist/commands/error-taxonomy.js.map +1 -0
- package/dist/commands/error-ux.d.ts +5 -0
- package/dist/commands/error-ux.d.ts.map +1 -0
- package/dist/commands/error-ux.js +253 -0
- package/dist/commands/error-ux.js.map +1 -0
- package/dist/commands/event-leak.d.ts +5 -0
- package/dist/commands/event-leak.d.ts.map +1 -0
- package/dist/commands/event-leak.js +263 -0
- package/dist/commands/event-leak.js.map +1 -0
- package/dist/commands/idempotency-audit.d.ts +5 -0
- package/dist/commands/idempotency-audit.d.ts.map +1 -0
- package/dist/commands/idempotency-audit.js +223 -0
- package/dist/commands/idempotency-audit.js.map +1 -0
- package/dist/commands/log-quality.d.ts +6 -0
- package/dist/commands/log-quality.d.ts.map +1 -0
- package/dist/commands/log-quality.js +212 -0
- package/dist/commands/log-quality.js.map +1 -0
- package/dist/commands/null-safety-audit.d.ts +6 -0
- package/dist/commands/null-safety-audit.d.ts.map +1 -0
- package/dist/commands/null-safety-audit.js +222 -0
- package/dist/commands/null-safety-audit.js.map +1 -0
- package/dist/commands/ownership-map.d.ts +6 -0
- package/dist/commands/ownership-map.d.ts.map +1 -0
- package/dist/commands/ownership-map.js +229 -0
- package/dist/commands/ownership-map.js.map +1 -0
- package/dist/commands/privilege-path.d.ts +5 -0
- package/dist/commands/privilege-path.d.ts.map +1 -0
- package/dist/commands/privilege-path.js +234 -0
- package/dist/commands/privilege-path.js.map +1 -0
- package/dist/commands/retry-pattern-audit.d.ts +6 -0
- package/dist/commands/retry-pattern-audit.d.ts.map +1 -0
- package/dist/commands/retry-pattern-audit.js +216 -0
- package/dist/commands/retry-pattern-audit.js.map +1 -0
- package/dist/commands/test-isolation.d.ts +6 -0
- package/dist/commands/test-isolation.d.ts.map +1 -0
- package/dist/commands/test-isolation.js +235 -0
- package/dist/commands/test-isolation.js.map +1 -0
- package/dist/commands/timeout-audit.d.ts +5 -0
- package/dist/commands/timeout-audit.d.ts.map +1 -0
- package/dist/commands/timeout-audit.js +211 -0
- package/dist/commands/timeout-audit.js.map +1 -0
- package/dist/commands/type-boundary.d.ts +5 -0
- package/dist/commands/type-boundary.d.ts.map +1 -0
- package/dist/commands/type-boundary.js +236 -0
- package/dist/commands/type-boundary.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event leak — detect orphaned event listeners, unsubscribed observables, dangling async handles.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, extname } from "path";
|
|
6
|
+
// ─── File Collection ────────────────────────────────────────────────────────
|
|
7
|
+
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
8
|
+
function collectFiles(dir, max = 300) {
|
|
9
|
+
const files = [];
|
|
10
|
+
function walk(d) {
|
|
11
|
+
if (files.length >= max)
|
|
12
|
+
return;
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = readdirSync(d);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
if (files.length >= max)
|
|
22
|
+
return;
|
|
23
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build")
|
|
24
|
+
continue;
|
|
25
|
+
const full = join(d, e);
|
|
26
|
+
try {
|
|
27
|
+
if (statSync(full).isDirectory())
|
|
28
|
+
walk(full);
|
|
29
|
+
else if (CODE_EXTS.has(extname(full)))
|
|
30
|
+
files.push(full);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* skip */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
walk(dir);
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
// ─── Analysis ───────────────────────────────────────────────────────────────
|
|
41
|
+
function analyzeFile(filepath) {
|
|
42
|
+
const issues = [];
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = readFileSync(filepath, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
const lines = content.split("\n");
|
|
51
|
+
const fullText = content;
|
|
52
|
+
for (let i = 0; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
// addEventListener without removeEventListener
|
|
55
|
+
if (/\.addEventListener\s*\(/.test(line)) {
|
|
56
|
+
const eventMatch = line.match(/addEventListener\s*\(\s*['"](\w+)['"]/);
|
|
57
|
+
if (eventMatch) {
|
|
58
|
+
const event = eventMatch[1];
|
|
59
|
+
if (!fullText.includes(`removeEventListener`) || !fullText.includes(`'${event}'`)) {
|
|
60
|
+
// Check for AbortController signal
|
|
61
|
+
const block = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
|
|
62
|
+
if (!/signal|AbortController|once:\s*true/i.test(block)) {
|
|
63
|
+
issues.push({
|
|
64
|
+
file: filepath,
|
|
65
|
+
line: i + 1,
|
|
66
|
+
issue: "addEventListener without cleanup",
|
|
67
|
+
severity: "high",
|
|
68
|
+
detail: `'${event}' listener added but no removeEventListener found — memory leak risk`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
// .on() without .off() / .removeListener()
|
|
75
|
+
if (/\.on\s*\(\s*['"](\w+)['"]/.test(line) && !/\.once\s*\(/.test(line)) {
|
|
76
|
+
const eventMatch = line.match(/\.on\s*\(\s*['"](\w+)['"]/);
|
|
77
|
+
if (eventMatch) {
|
|
78
|
+
const event = eventMatch[1];
|
|
79
|
+
if (!/\.off\(|\.removeListener\(|\.removeAllListeners\(/i.test(fullText) ||
|
|
80
|
+
!new RegExp(`(?:\\.off|\\.removeListener)\\s*\\(\\s*['"]${event}['"]`).test(fullText)) {
|
|
81
|
+
issues.push({
|
|
82
|
+
file: filepath,
|
|
83
|
+
line: i + 1,
|
|
84
|
+
issue: "Event emitter .on() without .off()",
|
|
85
|
+
severity: "medium",
|
|
86
|
+
detail: `'${event}' listener registered but no corresponding .off() or .removeListener()`,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Observable subscribe without unsubscribe
|
|
92
|
+
if (/\.subscribe\s*\(/.test(line)) {
|
|
93
|
+
const block = lines.slice(i, Math.min(i + 10, lines.length)).join("\n");
|
|
94
|
+
if (!/unsubscribe|takeUntil|take\(|first\(\)|pipe.*take|subscription.*=|\.add\(/i.test(block) &&
|
|
95
|
+
!/unsubscribe|takeUntil|ngOnDestroy|componentWillUnmount|useEffect.*return/i.test(fullText)) {
|
|
96
|
+
issues.push({
|
|
97
|
+
file: filepath,
|
|
98
|
+
line: i + 1,
|
|
99
|
+
issue: "Observable subscribe without unsubscribe",
|
|
100
|
+
severity: "high",
|
|
101
|
+
detail: "Subscription not cleaned up — will leak memory and may trigger after component unmounts",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// setInterval without clearInterval
|
|
106
|
+
if (/setInterval\s*\(/.test(line)) {
|
|
107
|
+
if (!/clearInterval/i.test(fullText)) {
|
|
108
|
+
issues.push({
|
|
109
|
+
file: filepath,
|
|
110
|
+
line: i + 1,
|
|
111
|
+
issue: "setInterval without clearInterval",
|
|
112
|
+
severity: "high",
|
|
113
|
+
detail: "Interval runs forever — add cleanup in destructor/unmount/close handler",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// setTimeout stored but never cleared
|
|
118
|
+
if (/setTimeout\s*\(/.test(line)) {
|
|
119
|
+
const assignMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*setTimeout/);
|
|
120
|
+
if (assignMatch) {
|
|
121
|
+
const varName = assignMatch[1];
|
|
122
|
+
if (!fullText.includes(`clearTimeout(${varName})`)) {
|
|
123
|
+
issues.push({
|
|
124
|
+
file: filepath,
|
|
125
|
+
line: i + 1,
|
|
126
|
+
issue: "setTimeout assigned but never cleared",
|
|
127
|
+
severity: "low",
|
|
128
|
+
detail: `Timer '${varName}' stored but clearTimeout never called — may fire after disposal`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// WebSocket/SSE without close handler
|
|
134
|
+
if (/new WebSocket\s*\(|new EventSource\s*\(/.test(line)) {
|
|
135
|
+
const block = lines.slice(i, Math.min(i + 15, lines.length)).join("\n");
|
|
136
|
+
if (!/\.close\s*\(|onclose|\.addEventListener.*close/i.test(block)) {
|
|
137
|
+
issues.push({
|
|
138
|
+
file: filepath,
|
|
139
|
+
line: i + 1,
|
|
140
|
+
issue: "WebSocket/SSE without close handler",
|
|
141
|
+
severity: "medium",
|
|
142
|
+
detail: "Connection opened but no close handler — resource leak on navigation or unmount",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// React useEffect without cleanup return
|
|
147
|
+
if (/useEffect\s*\(\s*\(\s*\)\s*=>\s*\{/.test(line)) {
|
|
148
|
+
// Find the matching effect body
|
|
149
|
+
let depth = 0;
|
|
150
|
+
let effectEnd = i;
|
|
151
|
+
for (let j = i; j < Math.min(i + 30, lines.length); j++) {
|
|
152
|
+
for (const ch of lines[j]) {
|
|
153
|
+
if (ch === "{")
|
|
154
|
+
depth++;
|
|
155
|
+
if (ch === "}")
|
|
156
|
+
depth--;
|
|
157
|
+
}
|
|
158
|
+
if (depth <= 0) {
|
|
159
|
+
effectEnd = j;
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const effectBody = lines.slice(i, effectEnd + 1).join("\n");
|
|
164
|
+
const hasSideEffect = /addEventListener|\.on\(|subscribe|setInterval|setTimeout|fetch|WebSocket/i.test(effectBody);
|
|
165
|
+
const hasCleanup = /return\s*\(\s*\)\s*=>|return\s*\(\)\s*\{|return\s*function/i.test(effectBody);
|
|
166
|
+
if (hasSideEffect && !hasCleanup) {
|
|
167
|
+
issues.push({
|
|
168
|
+
file: filepath,
|
|
169
|
+
line: i + 1,
|
|
170
|
+
issue: "useEffect with side effects but no cleanup",
|
|
171
|
+
severity: "high",
|
|
172
|
+
detail: "Effect creates subscriptions/listeners but returns no cleanup function — leak on re-render",
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// MutationObserver / IntersectionObserver / ResizeObserver without disconnect
|
|
177
|
+
if (/new (?:Mutation|Intersection|Resize)Observer\s*\(/.test(line)) {
|
|
178
|
+
if (!/\.disconnect\s*\(/i.test(fullText)) {
|
|
179
|
+
const observerType = line.match(/(Mutation|Intersection|Resize)Observer/)?.[1] || "Observer";
|
|
180
|
+
issues.push({
|
|
181
|
+
file: filepath,
|
|
182
|
+
line: i + 1,
|
|
183
|
+
issue: `${observerType}Observer without disconnect`,
|
|
184
|
+
severity: "medium",
|
|
185
|
+
detail: "Observer created but never disconnected — continues observing after disposal",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// AbortController created but never aborted
|
|
190
|
+
if (/new AbortController\s*\(/.test(line)) {
|
|
191
|
+
const varMatch = line.match(/(?:const|let|var)\s+(\w+)\s*=\s*new AbortController/);
|
|
192
|
+
if (varMatch) {
|
|
193
|
+
const varName = varMatch[1];
|
|
194
|
+
if (!fullText.includes(`${varName}.abort()`)) {
|
|
195
|
+
issues.push({
|
|
196
|
+
file: filepath,
|
|
197
|
+
line: i + 1,
|
|
198
|
+
issue: "AbortController never aborted",
|
|
199
|
+
severity: "low",
|
|
200
|
+
detail: `AbortController '${varName}' created but .abort() never called — no cleanup on cancel`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return issues;
|
|
207
|
+
}
|
|
208
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
209
|
+
export function runEventLeak(argv) {
|
|
210
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
211
|
+
console.log(`
|
|
212
|
+
judges event-leak — Detect orphaned listeners, unsubscribed observables, dangling handles
|
|
213
|
+
|
|
214
|
+
Usage:
|
|
215
|
+
judges event-leak [dir]
|
|
216
|
+
judges event-leak src/ --format json
|
|
217
|
+
|
|
218
|
+
Options:
|
|
219
|
+
[dir] Directory to scan (default: .)
|
|
220
|
+
--format json JSON output
|
|
221
|
+
--help, -h Show this help
|
|
222
|
+
|
|
223
|
+
Checks: addEventListener without cleanup, .on() without .off(), observable leaks,
|
|
224
|
+
setInterval without clear, useEffect without cleanup, Observer without disconnect.
|
|
225
|
+
`);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
229
|
+
const dir = argv.find((a) => !a.startsWith("-") && argv.indexOf(a) > 0) || ".";
|
|
230
|
+
const files = collectFiles(dir);
|
|
231
|
+
const allIssues = [];
|
|
232
|
+
for (const f of files)
|
|
233
|
+
allIssues.push(...analyzeFile(f));
|
|
234
|
+
const highCount = allIssues.filter((i) => i.severity === "high").length;
|
|
235
|
+
const medCount = allIssues.filter((i) => i.severity === "medium").length;
|
|
236
|
+
const score = Math.max(0, 100 - highCount * 10 - medCount * 4);
|
|
237
|
+
if (format === "json") {
|
|
238
|
+
console.log(JSON.stringify({
|
|
239
|
+
issues: allIssues,
|
|
240
|
+
score,
|
|
241
|
+
summary: { high: highCount, medium: medCount, total: allIssues.length },
|
|
242
|
+
timestamp: new Date().toISOString(),
|
|
243
|
+
}, null, 2));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const badge = score >= 80 ? "✅ CLEAN" : score >= 50 ? "⚠️ LEAKY" : "❌ LEAKING";
|
|
247
|
+
console.log(`\n Event Leaks: ${badge} (${score}/100)\n ─────────────────────────────`);
|
|
248
|
+
if (allIssues.length === 0) {
|
|
249
|
+
console.log(" No event leaks detected.\n");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
for (const issue of allIssues.slice(0, 25)) {
|
|
253
|
+
const icon = issue.severity === "high" ? "🔴" : issue.severity === "medium" ? "🟡" : "🔵";
|
|
254
|
+
console.log(` ${icon} ${issue.issue}`);
|
|
255
|
+
console.log(` ${issue.file}:${issue.line}`);
|
|
256
|
+
console.log(` ${issue.detail}`);
|
|
257
|
+
}
|
|
258
|
+
if (allIssues.length > 25)
|
|
259
|
+
console.log(` ... and ${allIssues.length - 25} more`);
|
|
260
|
+
console.log(`\n Total: ${allIssues.length} | High: ${highCount} | Medium: ${medCount} | Score: ${score}/100\n`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=event-leak.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-leak.js","sourceRoot":"","sources":["../../src/commands/event-leak.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency-audit.d.ts","sourceRoot":"","sources":["../../src/commands/idempotency-audit.ts"],"names":[],"mappings":"AAAA;;GAEG;AA2LH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA+DxD"}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency audit — verify retried/webhook operations are safely idempotent.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync, readdirSync, statSync } from "fs";
|
|
5
|
+
import { join, extname } from "path";
|
|
6
|
+
// ─── File Collection ────────────────────────────────────────────────────────
|
|
7
|
+
const CODE_EXTS = new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".java", ".go", ".rs"]);
|
|
8
|
+
function collectFiles(dir, max = 300) {
|
|
9
|
+
const files = [];
|
|
10
|
+
function walk(d) {
|
|
11
|
+
if (files.length >= max)
|
|
12
|
+
return;
|
|
13
|
+
let entries;
|
|
14
|
+
try {
|
|
15
|
+
entries = readdirSync(d);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
for (const e of entries) {
|
|
21
|
+
if (files.length >= max)
|
|
22
|
+
return;
|
|
23
|
+
if (e.startsWith(".") || e === "node_modules" || e === "dist" || e === "build")
|
|
24
|
+
continue;
|
|
25
|
+
const full = join(d, e);
|
|
26
|
+
try {
|
|
27
|
+
if (statSync(full).isDirectory())
|
|
28
|
+
walk(full);
|
|
29
|
+
else if (CODE_EXTS.has(extname(full)))
|
|
30
|
+
files.push(full);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
/* skip */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
walk(dir);
|
|
38
|
+
return files;
|
|
39
|
+
}
|
|
40
|
+
// ─── Analysis ───────────────────────────────────────────────────────────────
|
|
41
|
+
function analyzeFile(filepath) {
|
|
42
|
+
const issues = [];
|
|
43
|
+
let content;
|
|
44
|
+
try {
|
|
45
|
+
content = readFileSync(filepath, "utf-8");
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return issues;
|
|
49
|
+
}
|
|
50
|
+
const lines = content.split("\n");
|
|
51
|
+
const fullText = content;
|
|
52
|
+
const isRetryContext = /retry|webhook|queue|worker|consumer|handler|idempoten/i.test(fullText);
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
const line = lines[i];
|
|
55
|
+
// INSERT without ON CONFLICT / upsert in retry context
|
|
56
|
+
if (/INSERT\s+INTO/i.test(line) && isRetryContext) {
|
|
57
|
+
const block = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
|
|
58
|
+
if (!/ON\s+CONFLICT|ON\s+DUPLICATE|UPSERT|IF\s+NOT\s+EXISTS|MERGE/i.test(block)) {
|
|
59
|
+
issues.push({
|
|
60
|
+
file: filepath,
|
|
61
|
+
line: i + 1,
|
|
62
|
+
issue: "INSERT without conflict handling in retry path",
|
|
63
|
+
severity: "high",
|
|
64
|
+
detail: "Retry can cause duplicate rows — use INSERT ... ON CONFLICT or UPSERT",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Auto-increment counter mutation in handler
|
|
69
|
+
if (/\+\+|\+=\s*1|\.increment|\.incr\b/i.test(line)) {
|
|
70
|
+
if (/handler|webhook|consumer|worker|queue|retry/i.test(fullText)) {
|
|
71
|
+
const block = lines.slice(Math.max(0, i - 5), Math.min(i + 5, lines.length)).join("\n");
|
|
72
|
+
if (!/idempotency|dedup|idempotent|already.*processed/i.test(block)) {
|
|
73
|
+
issues.push({
|
|
74
|
+
file: filepath,
|
|
75
|
+
line: i + 1,
|
|
76
|
+
issue: "Counter increment in retry-able path",
|
|
77
|
+
severity: "high",
|
|
78
|
+
detail: "Counter mutation is not idempotent — repeated execution will over-count",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Email/SMS/notification send without dedup
|
|
84
|
+
if (/sendEmail|sendSMS|sendNotification|notify|\.send\s*\(/i.test(line)) {
|
|
85
|
+
if (isRetryContext) {
|
|
86
|
+
const block = lines.slice(Math.max(0, i - 8), Math.min(i + 5, lines.length)).join("\n");
|
|
87
|
+
if (!/idempotency|dedup|already.*sent|sentIds|processed/i.test(block)) {
|
|
88
|
+
issues.push({
|
|
89
|
+
file: filepath,
|
|
90
|
+
line: i + 1,
|
|
91
|
+
issue: "Notification send without dedup in retry path",
|
|
92
|
+
severity: "high",
|
|
93
|
+
detail: "Retry will send duplicate notifications — track sent IDs or use idempotency key",
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Payment/charge without idempotency key
|
|
99
|
+
if (/charge|payment|transfer|payout|refund/i.test(line) && /\.(?:create|post|execute)\s*\(/i.test(line)) {
|
|
100
|
+
const block = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
101
|
+
if (!/idempotency|idempotent|dedup|Idempotency-Key/i.test(block)) {
|
|
102
|
+
issues.push({
|
|
103
|
+
file: filepath,
|
|
104
|
+
line: i + 1,
|
|
105
|
+
issue: "Financial operation without idempotency key",
|
|
106
|
+
severity: "high",
|
|
107
|
+
detail: "Payment operation lacks idempotency key — retry can cause double-charge",
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Webhook handler without idempotency check
|
|
112
|
+
if (/webhook|eventHandler|onEvent|handleEvent/i.test(line) && /function|=>|async/.test(line)) {
|
|
113
|
+
const funcBlock = lines.slice(i, Math.min(i + 20, lines.length)).join("\n");
|
|
114
|
+
if (!/idempotency|dedup|already.*processed|processedIds|eventId/i.test(funcBlock)) {
|
|
115
|
+
issues.push({
|
|
116
|
+
file: filepath,
|
|
117
|
+
line: i + 1,
|
|
118
|
+
issue: "Webhook handler without idempotency guard",
|
|
119
|
+
severity: "medium",
|
|
120
|
+
detail: "Webhook providers may deliver events multiple times — check for prior processing",
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Queue consumer ACK before processing completes
|
|
125
|
+
if (/\.ack\s*\(|\.acknowledge/i.test(line)) {
|
|
126
|
+
const beforeBlock = lines.slice(Math.max(0, i - 3), i + 1).join("\n");
|
|
127
|
+
if (!/await|then|\.catch|try/i.test(beforeBlock)) {
|
|
128
|
+
issues.push({
|
|
129
|
+
file: filepath,
|
|
130
|
+
line: i + 1,
|
|
131
|
+
issue: "Queue ACK before processing completion",
|
|
132
|
+
severity: "high",
|
|
133
|
+
detail: "Acknowledging message before processing finishes — crash loses the message",
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// File write without atomic rename pattern
|
|
138
|
+
if (/writeFileSync|writeFile\s*\(|fs\.write/i.test(line)) {
|
|
139
|
+
if (isRetryContext) {
|
|
140
|
+
const block = lines.slice(i, Math.min(i + 5, lines.length)).join("\n");
|
|
141
|
+
if (!/rename|\.tmp|\.temp|atomic|swap/i.test(block)) {
|
|
142
|
+
issues.push({
|
|
143
|
+
file: filepath,
|
|
144
|
+
line: i + 1,
|
|
145
|
+
issue: "File write without atomic rename",
|
|
146
|
+
severity: "low",
|
|
147
|
+
detail: "Non-atomic write in retry path — crash during write corrupts the file",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// DELETE without WHERE in retry context
|
|
153
|
+
if (/DELETE\s+FROM/i.test(line) && isRetryContext) {
|
|
154
|
+
const block = lines.slice(i, Math.min(i + 3, lines.length)).join("\n");
|
|
155
|
+
if (!/WHERE/i.test(block)) {
|
|
156
|
+
issues.push({
|
|
157
|
+
file: filepath,
|
|
158
|
+
line: i + 1,
|
|
159
|
+
issue: "DELETE without WHERE in retry path",
|
|
160
|
+
severity: "high",
|
|
161
|
+
detail: "Unbounded DELETE is dangerous in retry context — could wipe entire table",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return issues;
|
|
167
|
+
}
|
|
168
|
+
// ─── CLI ────────────────────────────────────────────────────────────────────
|
|
169
|
+
export function runIdempotencyAudit(argv) {
|
|
170
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
171
|
+
console.log(`
|
|
172
|
+
judges idempotency-audit — Verify retry/webhook operations are safely idempotent
|
|
173
|
+
|
|
174
|
+
Usage:
|
|
175
|
+
judges idempotency-audit [dir]
|
|
176
|
+
judges idempotency-audit src/ --format json
|
|
177
|
+
|
|
178
|
+
Options:
|
|
179
|
+
[dir] Directory to scan (default: .)
|
|
180
|
+
--format json JSON output
|
|
181
|
+
--help, -h Show this help
|
|
182
|
+
|
|
183
|
+
Checks: INSERT without conflict handling, counter mutation in retries, notification dedup,
|
|
184
|
+
payment idempotency keys, webhook handler guards, queue ACK ordering, atomic file writes.
|
|
185
|
+
`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const format = argv.find((_a, i) => argv[i - 1] === "--format") || "text";
|
|
189
|
+
const dir = argv.find((a) => !a.startsWith("-") && argv.indexOf(a) > 0) || ".";
|
|
190
|
+
const files = collectFiles(dir);
|
|
191
|
+
const allIssues = [];
|
|
192
|
+
for (const f of files)
|
|
193
|
+
allIssues.push(...analyzeFile(f));
|
|
194
|
+
const highCount = allIssues.filter((i) => i.severity === "high").length;
|
|
195
|
+
const medCount = allIssues.filter((i) => i.severity === "medium").length;
|
|
196
|
+
const score = Math.max(0, 100 - highCount * 10 - medCount * 4);
|
|
197
|
+
if (format === "json") {
|
|
198
|
+
console.log(JSON.stringify({
|
|
199
|
+
issues: allIssues,
|
|
200
|
+
score,
|
|
201
|
+
summary: { high: highCount, medium: medCount, total: allIssues.length },
|
|
202
|
+
timestamp: new Date().toISOString(),
|
|
203
|
+
}, null, 2));
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
const badge = score >= 80 ? "✅ SAFE" : score >= 50 ? "⚠️ RISKY" : "❌ UNSAFE";
|
|
207
|
+
console.log(`\n Idempotency: ${badge} (${score}/100)\n ─────────────────────────────`);
|
|
208
|
+
if (allIssues.length === 0) {
|
|
209
|
+
console.log(" No idempotency issues detected.\n");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const issue of allIssues.slice(0, 25)) {
|
|
213
|
+
const icon = issue.severity === "high" ? "🔴" : issue.severity === "medium" ? "🟡" : "🔵";
|
|
214
|
+
console.log(` ${icon} ${issue.issue}`);
|
|
215
|
+
console.log(` ${issue.file}:${issue.line}`);
|
|
216
|
+
console.log(` ${issue.detail}`);
|
|
217
|
+
}
|
|
218
|
+
if (allIssues.length > 25)
|
|
219
|
+
console.log(` ... and ${allIssues.length - 25} more`);
|
|
220
|
+
console.log(`\n Total: ${allIssues.length} | High: ${highCount} | Medium: ${medCount} | Score: ${score}/100\n`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=idempotency-audit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idempotency-audit.js","sourceRoot":"","sources":["../../src/commands/idempotency-audit.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"log-quality.d.ts","sourceRoot":"","sources":["../../src/commands/log-quality.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA+KH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CA+DlD"}
|