rails-doctor 0.1.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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +21 -0
  4. data/README.md +148 -0
  5. data/docs/adapter-architecture.md +21 -0
  6. data/docs/agent-handoff.md +22 -0
  7. data/docs/architecture.md +21 -0
  8. data/docs/cli-reference.md +48 -0
  9. data/docs/config-reference.md +50 -0
  10. data/docs/github-actions.md +36 -0
  11. data/docs/monetization.md +14 -0
  12. data/docs/output-schema.md +57 -0
  13. data/docs/scoring-model.md +23 -0
  14. data/examples/github-actions/rails-doctor.yml +40 -0
  15. data/examples/report.html +704 -0
  16. data/examples/report.json +971 -0
  17. data/examples/report.md +261 -0
  18. data/examples/report.txt +45 -0
  19. data/exe/rails-doctor +8 -0
  20. data/lib/rails_doctor/adapters/base.rb +109 -0
  21. data/lib/rails_doctor/adapters/brakeman.rb +47 -0
  22. data/lib/rails_doctor/adapters/bundler_audit.rb +54 -0
  23. data/lib/rails_doctor/adapters/dependency_freshness.rb +51 -0
  24. data/lib/rails_doctor/adapters/flay.rb +41 -0
  25. data/lib/rails_doctor/adapters/flog.rb +41 -0
  26. data/lib/rails_doctor/adapters/reek.rb +39 -0
  27. data/lib/rails_doctor/adapters/rubocop.rb +40 -0
  28. data/lib/rails_doctor/adapters/strong_migrations.rb +52 -0
  29. data/lib/rails_doctor/adapters/test_coverage.rb +400 -0
  30. data/lib/rails_doctor/adapters/test_runner.rb +79 -0
  31. data/lib/rails_doctor/adapters/zeitwerk.rb +42 -0
  32. data/lib/rails_doctor/agent/handoff.rb +159 -0
  33. data/lib/rails_doctor/checks/rails_checks.rb +371 -0
  34. data/lib/rails_doctor/cli.rb +232 -0
  35. data/lib/rails_doctor/command_runner.rb +55 -0
  36. data/lib/rails_doctor/config.rb +161 -0
  37. data/lib/rails_doctor/init/runner.rb +191 -0
  38. data/lib/rails_doctor/models.rb +280 -0
  39. data/lib/rails_doctor/project.rb +95 -0
  40. data/lib/rails_doctor/reporters/html.rb +400 -0
  41. data/lib/rails_doctor/reporters/json.rb +18 -0
  42. data/lib/rails_doctor/reporters/markdown.rb +132 -0
  43. data/lib/rails_doctor/reporters/terminal.rb +101 -0
  44. data/lib/rails_doctor/scanner.rb +173 -0
  45. data/lib/rails_doctor/scorer.rb +74 -0
  46. data/lib/rails_doctor/version.rb +5 -0
  47. data/lib/rails_doctor.rb +15 -0
  48. data/site/assets/cli-output.png +0 -0
  49. data/site/assets/report-preview.png +0 -0
  50. data/site/index.html +294 -0
  51. metadata +167 -0
@@ -0,0 +1,704 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>Rails Doctor Report</title>
7
+ <style>
8
+ :root {
9
+ --bg: #f7f5f0;
10
+ --ink: #171717;
11
+ --muted: #68645e;
12
+ --panel: #fffdf8;
13
+ --line: #d8d2c7;
14
+ --accent: #0f766e;
15
+ --critical: #9f1239;
16
+ --high: #b45309;
17
+ --medium: #0369a1;
18
+ --low: #4d7c0f;
19
+ --info: #52525b;
20
+ }
21
+ * { box-sizing: border-box; }
22
+ body {
23
+ margin: 0;
24
+ background: var(--bg);
25
+ color: var(--ink);
26
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
27
+ line-height: 1.45;
28
+ }
29
+ header {
30
+ min-height: 360px;
31
+ padding: 48px clamp(24px, 6vw, 80px);
32
+ background:
33
+ linear-gradient(120deg, rgba(15, 118, 110, 0.18), transparent 42%),
34
+ linear-gradient(180deg, #161616 0%, #29251f 100%);
35
+ color: #fffdf8;
36
+ display: grid;
37
+ align-items: end;
38
+ }
39
+ .hero {
40
+ max-width: 1180px;
41
+ width: 100%;
42
+ margin: 0 auto;
43
+ display: grid;
44
+ grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr);
45
+ gap: 48px;
46
+ align-items: end;
47
+ }
48
+ .brand { font-size: clamp(48px, 9vw, 116px); line-height: .88; letter-spacing: 0; margin: 0 0 18px; }
49
+ .subtitle { max-width: 620px; color: #d8d2c7; font-size: 18px; margin: 0; }
50
+ .score-ring {
51
+ border: 1px solid rgba(255,255,255,.24);
52
+ padding: 28px;
53
+ background: rgba(255,255,255,.06);
54
+ backdrop-filter: blur(8px);
55
+ }
56
+ .score-number { font-size: clamp(64px, 10vw, 120px); line-height: .9; font-weight: 800; }
57
+ .score-label { color: #d8d2c7; text-transform: uppercase; font-size: 12px; letter-spacing: .12em; }
58
+ main { max-width: 1180px; margin: 0 auto; padding: 36px clamp(20px, 4vw, 48px) 80px; }
59
+ .metrics {
60
+ display: grid;
61
+ grid-template-columns: repeat(6, minmax(0, 1fr));
62
+ border-top: 1px solid var(--line);
63
+ border-bottom: 1px solid var(--line);
64
+ margin-bottom: 36px;
65
+ }
66
+ .metric { padding: 18px 16px; border-right: 1px solid var(--line); }
67
+ .metric:last-child { border-right: 0; }
68
+ .metric span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .1em; }
69
+ .metric strong { display: block; margin-top: 6px; font-size: 28px; }
70
+ .section { margin: 44px 0; }
71
+ .section h2 { font-size: 28px; margin: 0 0 16px; }
72
+ .filters { display: flex; flex-wrap: wrap; gap: 10px; margin: 14px 0 24px; }
73
+ .filters button {
74
+ border: 1px solid var(--line);
75
+ background: transparent;
76
+ color: var(--ink);
77
+ padding: 8px 12px;
78
+ cursor: pointer;
79
+ font: inherit;
80
+ }
81
+ .filters button.active { background: var(--ink); color: var(--bg); }
82
+ table { width: 100%; border-collapse: collapse; background: var(--panel); }
83
+ th, td { padding: 12px 10px; border-bottom: 1px solid var(--line); text-align: left; vertical-align: top; }
84
+ th { color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
85
+ .chip { display: inline-block; padding: 3px 8px; color: white; font-size: 12px; font-weight: 700; text-transform: uppercase; }
86
+ .critical { background: var(--critical); }
87
+ .high { background: var(--high); }
88
+ .medium { background: var(--medium); }
89
+ .low { background: var(--low); }
90
+ .info { background: var(--info); }
91
+ .top-fix {
92
+ display: grid;
93
+ grid-template-columns: 110px minmax(0, 1fr);
94
+ gap: 18px;
95
+ padding: 18px 0;
96
+ border-top: 1px solid var(--line);
97
+ }
98
+ .agent {
99
+ background: #171717;
100
+ color: #f7f5f0;
101
+ padding: 20px;
102
+ overflow: auto;
103
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
104
+ font-size: 13px;
105
+ }
106
+ details { border-top: 1px solid var(--line); padding: 14px 0; }
107
+ summary { cursor: pointer; font-weight: 700; }
108
+ pre { white-space: pre-wrap; overflow: auto; background: #171717; color: #f7f5f0; padding: 16px; }
109
+ .coverage-summary {
110
+ display: grid;
111
+ grid-template-columns: repeat(4, minmax(0, 1fr));
112
+ border-top: 1px solid var(--line);
113
+ border-bottom: 1px solid var(--line);
114
+ margin-bottom: 18px;
115
+ }
116
+ .coverage-item { padding: 16px 14px; border-right: 1px solid var(--line); }
117
+ .coverage-item:last-child { border-right: 0; }
118
+ .coverage-item span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; letter-spacing: .08em; }
119
+ .coverage-item strong { display: block; margin-top: 4px; font-size: 22px; }
120
+ @media (max-width: 820px) {
121
+ .hero { grid-template-columns: 1fr; }
122
+ .metrics { grid-template-columns: repeat(2, minmax(0, 1fr)); }
123
+ .coverage-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
124
+ .metric { border-bottom: 1px solid var(--line); }
125
+ .coverage-item { border-bottom: 1px solid var(--line); }
126
+ table, thead, tbody, tr, th, td { display: block; }
127
+ thead { display: none; }
128
+ tr { border-bottom: 1px solid var(--line); padding: 10px 0; }
129
+ td { border: 0; }
130
+ .top-fix { grid-template-columns: 1fr; }
131
+ }
132
+ </style>
133
+ </head>
134
+ <body>
135
+ <header>
136
+ <div class="hero">
137
+ <div>
138
+ <h1 class="brand">Rails Doctor</h1>
139
+ <p class="subtitle">A Rails health report for developers, CI, and AI coding agents. Generated from normalized scanner findings, runtime signals, and Rails-specific checks.</p>
140
+ </div>
141
+ <div class="score-ring">
142
+ <div class="score-label">Overall health</div>
143
+ <div class="score-number">0</div>
144
+ <div>Changed files: <strong>100</strong> · Confidence: <strong>100%</strong></div>
145
+ </div>
146
+ </div>
147
+ </header>
148
+ <main>
149
+ <section class="metrics" aria-label="Report summary">
150
+ <div class="metric"><span>Critical</span><strong>1</strong></div>
151
+ <div class="metric"><span>High</span><strong>10</strong></div>
152
+ <div class="metric"><span>Medium</span><strong>10</strong></div>
153
+ <div class="metric"><span>Coverage</span><strong>48.00%</strong></div>
154
+ <div class="metric"><span>Skipped</span><strong>0</strong></div>
155
+ <div class="metric"><span>Duration</span><strong>600ms</strong></div>
156
+ </section>
157
+
158
+ <section class="section">
159
+ <h2>Coverage</h2>
160
+
161
+ <div class="coverage-summary" aria-label="Coverage summary">
162
+ <div class="coverage-item"><span>Line coverage</span><strong>48.00%</strong></div>
163
+ <div class="coverage-item"><span>Line threshold</span><strong>90.00%</strong></div>
164
+ <div class="coverage-item"><span>Covered lines</span><strong>12/25</strong></div>
165
+ <div class="coverage-item"><span>Branch coverage</span><strong>50.00%</strong></div>
166
+ </div>
167
+
168
+ <table>
169
+ <thead><tr><th>File</th><th>Line coverage</th><th>Covered</th><th>Threshold</th></tr></thead>
170
+ <tbody>
171
+
172
+ <tr>
173
+ <td>app/controllers/posts_controller.rb</td>
174
+ <td>22.22%</td>
175
+ <td>2/9</td>
176
+ <td>80.00%</td>
177
+ </tr>
178
+
179
+ <tr>
180
+ <td>app/models/post.rb</td>
181
+ <td>44.44%</td>
182
+ <td>4/9</td>
183
+ <td>80.00%</td>
184
+ </tr>
185
+
186
+ </tbody>
187
+ </table>
188
+
189
+
190
+ </section>
191
+
192
+ <section class="section">
193
+ <h2>Top Fixes</h2>
194
+
195
+
196
+ <article class="top-fix">
197
+ <div><span class="chip critical">critical</span></div>
198
+ <div>
199
+ <strong>SQL Injection: Possible SQL injection</strong>
200
+ <div>app/models/post.rb:8</div>
201
+ <p>Review Brakeman guidance: https://brakemanscanner.org/docs/warning_types/sql_injection/</p>
202
+ </div>
203
+ </article>
204
+
205
+ <article class="top-fix">
206
+ <div><span class="chip high">high</span></div>
207
+ <div>
208
+ <strong>rack: Example vulnerability</strong>
209
+ <div>Gemfile.lock</div>
210
+ <p>Update rack to a patched version and rerun Bundler Audit.</p>
211
+ </div>
212
+ </article>
213
+
214
+ <article class="top-fix">
215
+ <div><span class="chip high">high</span></div>
216
+ <div>
217
+ <strong>Prosopite: N+1 queries detected for Post =&gt; [:user]</strong>
218
+
219
+ <p>Fix the N+1 query by eager loading or adjusting the query path exercised by tests.</p>
220
+ </div>
221
+ </article>
222
+
223
+ <article class="top-fix">
224
+ <div><span class="chip high">high</span></div>
225
+ <div>
226
+ <strong>posts.user_id has no index</strong>
227
+ <div>db/schema.rb</div>
228
+ <p>Add an index for the foreign key column to avoid slow association lookups.</p>
229
+ </div>
230
+ </article>
231
+
232
+ <article class="top-fix">
233
+ <div><span class="chip high">high</span></div>
234
+ <div>
235
+ <strong>users.email has a Rails uniqueness validation without a unique database index</strong>
236
+ <div>app/models/user.rb</div>
237
+ <p>Back uniqueness validations with a unique index to prevent race-condition duplicates.</p>
238
+ </div>
239
+ </article>
240
+
241
+ <article class="top-fix">
242
+ <div><span class="chip high">high</span></div>
243
+ <div>
244
+ <strong>Routes reference missing ghosts_controller.rb</strong>
245
+ <div>config/routes.rb</div>
246
+ <p>Create the controller or remove/rename the route.</p>
247
+ </div>
248
+ </article>
249
+
250
+ <article class="top-fix">
251
+ <div><span class="chip high">high</span></div>
252
+ <div>
253
+ <strong>Route points to missing posts#create</strong>
254
+ <div>app/controllers/posts_controller.rb</div>
255
+ <p>Implement the action or update/remove the route.</p>
256
+ </div>
257
+ </article>
258
+
259
+ <article class="top-fix">
260
+ <div><span class="chip high">high</span></div>
261
+ <div>
262
+ <strong>Route points to missing posts#destroy</strong>
263
+ <div>app/controllers/posts_controller.rb</div>
264
+ <p>Implement the action or update/remove the route.</p>
265
+ </div>
266
+ </article>
267
+
268
+ <article class="top-fix">
269
+ <div><span class="chip high">high</span></div>
270
+ <div>
271
+ <strong>Route points to missing posts#edit</strong>
272
+ <div>app/controllers/posts_controller.rb</div>
273
+ <p>Implement the action or update/remove the route.</p>
274
+ </div>
275
+ </article>
276
+
277
+ <article class="top-fix">
278
+ <div><span class="chip high">high</span></div>
279
+ <div>
280
+ <strong>Route points to missing posts#new</strong>
281
+ <div>app/controllers/posts_controller.rb</div>
282
+ <p>Implement the action or update/remove the route.</p>
283
+ </div>
284
+ </article>
285
+
286
+ <article class="top-fix">
287
+ <div><span class="chip high">high</span></div>
288
+ <div>
289
+ <strong>Route points to missing posts#update</strong>
290
+ <div>app/controllers/posts_controller.rb</div>
291
+ <p>Implement the action or update/remove the route.</p>
292
+ </div>
293
+ </article>
294
+
295
+ <article class="top-fix">
296
+ <div><span class="chip medium">medium</span></div>
297
+ <div>
298
+ <strong>DEPRECATION WARNING: old API is deprecated</strong>
299
+
300
+ <p>Resolve deprecation warnings before framework or gem upgrades make them failures.</p>
301
+ </div>
302
+ </article>
303
+
304
+ </section>
305
+
306
+ <section class="section">
307
+ <h2>Agent Brief</h2>
308
+ <pre class="agent">Coverage: 48.00% lines
309
+
310
+ ## Coverage
311
+ - app/controllers/posts_controller.rb: 22.22% lines (2/9)
312
+ - app/models/post.rb: 44.44% lines (4/9)
313
+
314
+ ## Findings
315
+ - critical: Fix this security finding with the smallest behavior-preserving change. Prefer framework-safe APIs and add regression tests.
316
+ - high: Update the vulnerable gem conservatively, refresh the lockfile, and run the test suite.
317
+ - high: Use includes/preload/eager_load or query restructuring. Verify with the same test command.
318
+ - high: Create a migration that adds an index on posts.user_id. For PostgreSQL production apps, prefer a concurrent index path compatible with strong_migrations.
319
+ - high: Add a unique index migration for users.email, handle existing duplicate data if necessary, and rerun tests.
320
+ - high: Align routes with real controller names. Prefer removing stale routes over creating empty controllers.
321
+ - high: Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.
322
+ - high: Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.
323
+ - high: Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.
324
+ - high: Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.
325
+ - high: Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.
326
+ - medium: Update the deprecated API usage and add a regression test when behavior could change.</pre>
327
+ </section>
328
+
329
+ <section class="section">
330
+ <h2>Findings</h2>
331
+ <div class="filters" role="toolbar" aria-label="Finding filters">
332
+
333
+ <button type="button" data-filter="all" class="active">All</button>
334
+
335
+ <button type="button" data-filter="critical" class="">Critical</button>
336
+
337
+ <button type="button" data-filter="high" class="">High</button>
338
+
339
+ <button type="button" data-filter="medium" class="">Medium</button>
340
+
341
+ <button type="button" data-filter="low" class="">Low</button>
342
+
343
+ <button type="button" data-filter="info" class="">Info</button>
344
+
345
+ </div>
346
+ <table>
347
+ <thead><tr><th>Severity</th><th>Tool</th><th>Category</th><th>Location</th><th>Finding</th></tr></thead>
348
+ <tbody>
349
+
350
+ <tr data-severity="medium">
351
+ <td><span class="chip medium">medium</span></td>
352
+ <td>rubocop</td>
353
+ <td>lint</td>
354
+ <td>app/models/post.rb:6</td>
355
+ <td><strong>Rails/TimeZone: Use Time.current instead of Time.now.</strong><br>Fix the RuboCop offense or document why this cop should be configured differently.</td>
356
+ </tr>
357
+
358
+ <tr data-severity="critical">
359
+ <td><span class="chip critical">critical</span></td>
360
+ <td>brakeman</td>
361
+ <td>security</td>
362
+ <td>app/models/post.rb:8</td>
363
+ <td><strong>SQL Injection: Possible SQL injection</strong><br>Review Brakeman guidance: https://brakemanscanner.org/docs/warning_types/sql_injection/</td>
364
+ </tr>
365
+
366
+ <tr data-severity="high">
367
+ <td><span class="chip high">high</span></td>
368
+ <td>bundler_audit</td>
369
+ <td>dependency-security</td>
370
+ <td>Gemfile.lock</td>
371
+ <td><strong>rack: Example vulnerability</strong><br>Update rack to a patched version and rerun Bundler Audit.</td>
372
+ </tr>
373
+
374
+ <tr data-severity="medium">
375
+ <td><span class="chip medium">medium</span></td>
376
+ <td>reek</td>
377
+ <td>code-smell</td>
378
+ <td>app/models/post.rb:4</td>
379
+ <td><strong>TooManyStatements: has the smell of too many statements</strong><br>Refactor the local smell without broad behavior changes.</td>
380
+ </tr>
381
+
382
+ <tr data-severity="low">
383
+ <td><span class="chip low">low</span></td>
384
+ <td>strong_migrations</td>
385
+ <td>migration-safety</td>
386
+ <td>config/initializers/strong_migrations.rb</td>
387
+ <td><strong>strong_migrations is installed but no initializer was found</strong><br>Generate or review the Strong Migrations initializer so project-specific safety settings are explicit.</td>
388
+ </tr>
389
+
390
+ <tr data-severity="high">
391
+ <td><span class="chip high">high</span></td>
392
+ <td>rails_checks</td>
393
+ <td>database-integrity</td>
394
+ <td>db/schema.rb</td>
395
+ <td><strong>posts.user_id has no index</strong><br>Add an index for the foreign key column to avoid slow association lookups.</td>
396
+ </tr>
397
+
398
+ <tr data-severity="high">
399
+ <td><span class="chip high">high</span></td>
400
+ <td>rails_checks</td>
401
+ <td>database-integrity</td>
402
+ <td>app/models/user.rb</td>
403
+ <td><strong>users.email has a Rails uniqueness validation without a unique database index</strong><br>Back uniqueness validations with a unique index to prevent race-condition duplicates.</td>
404
+ </tr>
405
+
406
+ <tr data-severity="high">
407
+ <td><span class="chip high">high</span></td>
408
+ <td>rails_checks</td>
409
+ <td>routing</td>
410
+ <td>config/routes.rb</td>
411
+ <td><strong>Routes reference missing ghosts_controller.rb</strong><br>Create the controller or remove/rename the route.</td>
412
+ </tr>
413
+
414
+ <tr data-severity="high">
415
+ <td><span class="chip high">high</span></td>
416
+ <td>rails_checks</td>
417
+ <td>routing</td>
418
+ <td>app/controllers/posts_controller.rb</td>
419
+ <td><strong>Route points to missing posts#create</strong><br>Implement the action or update/remove the route.</td>
420
+ </tr>
421
+
422
+ <tr data-severity="high">
423
+ <td><span class="chip high">high</span></td>
424
+ <td>rails_checks</td>
425
+ <td>routing</td>
426
+ <td>app/controllers/posts_controller.rb</td>
427
+ <td><strong>Route points to missing posts#destroy</strong><br>Implement the action or update/remove the route.</td>
428
+ </tr>
429
+
430
+ <tr data-severity="high">
431
+ <td><span class="chip high">high</span></td>
432
+ <td>rails_checks</td>
433
+ <td>routing</td>
434
+ <td>app/controllers/posts_controller.rb</td>
435
+ <td><strong>Route points to missing posts#edit</strong><br>Implement the action or update/remove the route.</td>
436
+ </tr>
437
+
438
+ <tr data-severity="high">
439
+ <td><span class="chip high">high</span></td>
440
+ <td>rails_checks</td>
441
+ <td>routing</td>
442
+ <td>app/controllers/posts_controller.rb</td>
443
+ <td><strong>Route points to missing posts#new</strong><br>Implement the action or update/remove the route.</td>
444
+ </tr>
445
+
446
+ <tr data-severity="medium">
447
+ <td><span class="chip medium">medium</span></td>
448
+ <td>rails_checks</td>
449
+ <td>routing</td>
450
+ <td>app/controllers/posts_controller.rb</td>
451
+ <td><strong>posts#show has no matching template or explicit response</strong><br>Add a template or explicit render/redirect/head response.</td>
452
+ </tr>
453
+
454
+ <tr data-severity="high">
455
+ <td><span class="chip high">high</span></td>
456
+ <td>rails_checks</td>
457
+ <td>routing</td>
458
+ <td>app/controllers/posts_controller.rb</td>
459
+ <td><strong>Route points to missing posts#update</strong><br>Implement the action or update/remove the route.</td>
460
+ </tr>
461
+
462
+ <tr data-severity="low">
463
+ <td><span class="chip low">low</span></td>
464
+ <td>rails_checks</td>
465
+ <td>dead-code</td>
466
+ <td>app/controllers/posts_controller.rb</td>
467
+ <td><strong>posts#archive is not referenced by simple route analysis</strong><br>Review whether this action is reached by custom routing or can be removed.</td>
468
+ </tr>
469
+
470
+ <tr data-severity="medium">
471
+ <td><span class="chip medium">medium</span></td>
472
+ <td>rails_checks</td>
473
+ <td>technical-debt</td>
474
+ <td>app/models/post.rb</td>
475
+ <td><strong>1 TODO/FIXME/HACK marker in 10 lines</strong><br>Convert stale markers into tracked work or resolve them while the context is fresh.</td>
476
+ </tr>
477
+
478
+ <tr data-severity="medium">
479
+ <td><span class="chip medium">medium</span></td>
480
+ <td>test_runner</td>
481
+ <td>deprecation</td>
482
+ <td>1</td>
483
+ <td><strong>DEPRECATION WARNING: old API is deprecated</strong><br>Resolve deprecation warnings before framework or gem upgrades make them failures.</td>
484
+ </tr>
485
+
486
+ <tr data-severity="high">
487
+ <td><span class="chip high">high</span></td>
488
+ <td>test_runner</td>
489
+ <td>runtime-n-plus-one</td>
490
+ <td>2</td>
491
+ <td><strong>Prosopite: N+1 queries detected for Post =&gt; [:user]</strong><br>Fix the N+1 query by eager loading or adjusting the query path exercised by tests.</td>
492
+ </tr>
493
+
494
+ <tr data-severity="medium">
495
+ <td><span class="chip medium">medium</span></td>
496
+ <td>test_coverage</td>
497
+ <td>test-coverage</td>
498
+ <td></td>
499
+ <td><strong>Line coverage 48.00% is below the 90.00% threshold</strong><br>Add tests for uncovered application code, starting with the lowest-coverage files.</td>
500
+ </tr>
501
+
502
+ <tr data-severity="medium">
503
+ <td><span class="chip medium">medium</span></td>
504
+ <td>test_coverage</td>
505
+ <td>test-coverage</td>
506
+ <td>app/controllers/posts_controller.rb</td>
507
+ <td><strong>app/controllers/posts_controller.rb line coverage 22.22% is below the 80.00% per-file threshold</strong><br>Add focused tests that exercise the uncovered behavior in this file.</td>
508
+ </tr>
509
+
510
+ <tr data-severity="medium">
511
+ <td><span class="chip medium">medium</span></td>
512
+ <td>test_coverage</td>
513
+ <td>test-coverage</td>
514
+ <td>app/models/post.rb</td>
515
+ <td><strong>app/models/post.rb line coverage 44.44% is below the 80.00% per-file threshold</strong><br>Add focused tests that exercise the uncovered behavior in this file.</td>
516
+ </tr>
517
+
518
+ <tr data-severity="medium">
519
+ <td><span class="chip medium">medium</span></td>
520
+ <td>flog</td>
521
+ <td>complexity</td>
522
+ <td>app/models/post.rb:4</td>
523
+ <td><strong>High complexity score 32.5 for Post#publish!</strong><br>Extract simpler methods or objects around the complex branch.</td>
524
+ </tr>
525
+
526
+ <tr data-severity="medium">
527
+ <td><span class="chip medium">medium</span></td>
528
+ <td>flay</td>
529
+ <td>duplication</td>
530
+ <td>app/models/post.rb:4</td>
531
+ <td><strong>Similar code group 1 across app/models/post.rb:4, app/models/user.rb:2</strong><br>Review whether this duplication is intentional. Extract shared behavior only if the abstraction is clear.</td>
532
+ </tr>
533
+
534
+ <tr data-severity="low">
535
+ <td><span class="chip low">low</span></td>
536
+ <td>dependency_freshness</td>
537
+ <td>dependency-freshness</td>
538
+ <td>Gemfile.lock</td>
539
+ <td><strong>rack appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
540
+ </tr>
541
+
542
+ </tbody>
543
+ </table>
544
+ </section>
545
+
546
+ <section class="section">
547
+ <h2>Hotspots</h2>
548
+ <table>
549
+ <thead><tr><th>File</th><th>Score</th><th>Findings</th><th>Churn</th><th>Changed</th><th>Summary</th></tr></thead>
550
+ <tbody>
551
+
552
+ <tr>
553
+ <td>app/controllers/posts_controller.rb</td>
554
+ <td>42</td>
555
+ <td>8</td>
556
+ <td>0</td>
557
+ <td>false</td>
558
+ <td>Inherited file with 8 findings across routing, dead-code, test-coverage.</td>
559
+ </tr>
560
+
561
+ <tr>
562
+ <td>app/models/post.rb</td>
563
+ <td>33</td>
564
+ <td>7</td>
565
+ <td>0</td>
566
+ <td>false</td>
567
+ <td>Inherited file with 7 findings across lint, security, code-smell, technical-debt, test-coverage, complexity, duplication.</td>
568
+ </tr>
569
+
570
+ <tr>
571
+ <td>Gemfile.lock</td>
572
+ <td>8</td>
573
+ <td>2</td>
574
+ <td>0</td>
575
+ <td>false</td>
576
+ <td>Inherited file with 2 findings across dependency-security, dependency-freshness.</td>
577
+ </tr>
578
+
579
+ <tr>
580
+ <td>db/schema.rb</td>
581
+ <td>7</td>
582
+ <td>1</td>
583
+ <td>0</td>
584
+ <td>false</td>
585
+ <td>Inherited file with 1 finding across database-integrity.</td>
586
+ </tr>
587
+
588
+ <tr>
589
+ <td>app/models/user.rb</td>
590
+ <td>7</td>
591
+ <td>1</td>
592
+ <td>0</td>
593
+ <td>false</td>
594
+ <td>Inherited file with 1 finding across database-integrity.</td>
595
+ </tr>
596
+
597
+ <tr>
598
+ <td>config/routes.rb</td>
599
+ <td>7</td>
600
+ <td>1</td>
601
+ <td>0</td>
602
+ <td>false</td>
603
+ <td>Inherited file with 1 finding across routing.</td>
604
+ </tr>
605
+
606
+ <tr>
607
+ <td>config/initializers/strong_migrations.rb</td>
608
+ <td>1</td>
609
+ <td>1</td>
610
+ <td>0</td>
611
+ <td>false</td>
612
+ <td>Inherited file with 1 finding across migration-safety.</td>
613
+ </tr>
614
+
615
+ </tbody>
616
+ </table>
617
+ </section>
618
+
619
+ <section class="section">
620
+ <h2>Skipped Tools</h2>
621
+
622
+ <p>No tools were skipped.</p>
623
+
624
+ </section>
625
+
626
+ <section class="section">
627
+ <h2>Raw Tool Output</h2>
628
+
629
+
630
+ <details>
631
+ <summary>rubocop</summary>
632
+ <pre>{&quot;files&quot;:[{&quot;path&quot;:&quot;app/models/post.rb&quot;,&quot;offenses&quot;:[{&quot;severity&quot;:&quot;warning&quot;,&quot;message&quot;:&quot;Use Time.current instead of Time.now.&quot;,&quot;cop_name&quot;:&quot;Rails/TimeZone&quot;,&quot;location&quot;:{&quot;line&quot;:6}}]}]}
633
+ \n</pre>
634
+ </details>
635
+
636
+ <details>
637
+ <summary>brakeman</summary>
638
+ <pre>{&quot;warnings&quot;:[{&quot;warning_type&quot;:&quot;SQL Injection&quot;,&quot;message&quot;:&quot;Possible SQL injection&quot;,&quot;file&quot;:&quot;app/models/post.rb&quot;,&quot;line&quot;:8,&quot;confidence&quot;:&quot;High&quot;,&quot;fingerprint&quot;:&quot;abc123&quot;,&quot;link&quot;:&quot;https://brakemanscanner.org/docs/warning_types/sql_injection/&quot;}]}
639
+ \n</pre>
640
+ </details>
641
+
642
+ <details>
643
+ <summary>bundler_audit</summary>
644
+ <pre>{&quot;results&quot;:[{&quot;gem&quot;:{&quot;name&quot;:&quot;rack&quot;},&quot;advisory&quot;:{&quot;id&quot;:&quot;CVE-2099-0001&quot;,&quot;title&quot;:&quot;Example vulnerability&quot;,&quot;criticality&quot;:&quot;high&quot;,&quot;url&quot;:&quot;https://example.test/advisory&quot;}}]}
645
+ \n</pre>
646
+ </details>
647
+
648
+ <details>
649
+ <summary>zeitwerk</summary>
650
+ <pre>Hold on, I am eager loading the application.
651
+ All is good!
652
+ \n</pre>
653
+ </details>
654
+
655
+ <details>
656
+ <summary>reek</summary>
657
+ <pre>[{&quot;context&quot;:&quot;Post#publish!&quot;,&quot;lines&quot;:[4],&quot;message&quot;:&quot;has the smell of too many statements&quot;,&quot;smell_type&quot;:&quot;TooManyStatements&quot;,&quot;source&quot;:&quot;app/models/post.rb&quot;}]
658
+ \n</pre>
659
+ </details>
660
+
661
+ <details>
662
+ <summary>test_runner</summary>
663
+ <pre>DEPRECATION WARNING: old API is deprecated
664
+ Prosopite: N+1 queries detected for Post =&gt; [:user]
665
+ \n</pre>
666
+ </details>
667
+
668
+ <details>
669
+ <summary>flog</summary>
670
+ <pre> 32.5 Post#publish! app/models/post.rb:4
671
+ \n</pre>
672
+ </details>
673
+
674
+ <details>
675
+ <summary>flay</summary>
676
+ <pre>Similar code found in :iter (mass = 42)
677
+ app/models/post.rb:4
678
+ app/models/user.rb:2
679
+ \n</pre>
680
+ </details>
681
+
682
+ <details>
683
+ <summary>dependency_freshness</summary>
684
+ <pre>rack (newest 3.0.0, installed 2.2.0)
685
+ \n</pre>
686
+ </details>
687
+
688
+ </section>
689
+ </main>
690
+ <script type="application/json" id="rails-doctor-data">{"schema_version":"1.1","generated_at":"2026-05-30T20:00:00Z","project_root":"/path/to/app","profile":"deep","metadata":{"rails_app":true,"ruby_version":"3.4.1","rails_version":"8.0.0","changed_files":[]},"summary":{"profile":"deep","duration_ms":600,"finding_count":24,"severity_counts":{"medium":10,"critical":1,"high":10,"low":3},"skipped_tools":[],"score":{"overall":0,"changed_files":100,"confidence":100,"penalties":[{"id":"rd-gfnxqbg1qkup","severity":"medium","file":"app/models/post.rb","message":"Rails/TimeZone: Use Time.current instead of Time.now.","penalty":3.0},{"id":"rd-op6mq8hfnm99","severity":"critical","file":"app/models/post.rb","message":"SQL Injection: Possible SQL injection","penalty":15.0},{"id":"rd-habnfsz1zgdj","severity":"high","file":"Gemfile.lock","message":"rack: Example vulnerability","penalty":7.0},{"id":"rd-2er9zgsavtk6","severity":"medium","file":"app/models/post.rb","message":"TooManyStatements: has the smell of too many statements","penalty":3.0},{"id":"rd-ci7ub1199rvp","severity":"low","file":"config/initializers/strong_migrations.rb","message":"strong_migrations is installed but no initializer was found","penalty":0.75},{"id":"rd-iad8kr5ch7mh","severity":"high","file":"db/schema.rb","message":"posts.user_id has no index","penalty":7.0},{"id":"rd-nd2oek4kujwu","severity":"high","file":"app/models/user.rb","message":"users.email has a Rails uniqueness validation without a unique database index","penalty":5.25},{"id":"rd-p9vqylkxcybn","severity":"high","file":"config/routes.rb","message":"Routes reference missing ghosts_controller.rb","penalty":7.0},{"id":"rd-stmr7t8qmjti","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#create","penalty":7.0},{"id":"rd-c9eld5z4i9rf","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#destroy","penalty":7.0},{"id":"rd-hdn01soot3gf","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#edit","penalty":7.0},{"id":"rd-ygjxjocmpubf","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#new","penalty":7.0},{"id":"rd-q2wm1ehy5qn","severity":"medium","file":"app/controllers/posts_controller.rb","message":"posts#show has no matching template or explicit response","penalty":2.25},{"id":"rd-w4g0okt214t6","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#update","penalty":7.0},{"id":"rd-elkkq3siut9a","severity":"low","file":"app/controllers/posts_controller.rb","message":"posts#archive is not referenced by simple route analysis","penalty":0.4},{"id":"rd-4f2vjquwx6t4","severity":"medium","file":"app/models/post.rb","message":"1 TODO/FIXME/HACK marker in 10 lines","penalty":2.25},{"id":"rd-4dacnomu05u7","severity":"medium","file":null,"message":"DEPRECATION WARNING: old API is deprecated","penalty":2.25},{"id":"rd-bg982jjw6xt3","severity":"high","file":null,"message":"Prosopite: N+1 queries detected for Post =\u003e [:user]","penalty":5.25},{"id":"rd-qx8han58wk0o","severity":"medium","file":null,"message":"Line coverage 48.00% is below the 90.00% threshold","penalty":3.0},{"id":"rd-9ikgvif63uif","severity":"medium","file":"app/controllers/posts_controller.rb","message":"app/controllers/posts_controller.rb line coverage 22.22% is below the 80.00% per-file threshold","penalty":3.0},{"id":"rd-s83lai6gkb0r","severity":"medium","file":"app/models/post.rb","message":"app/models/post.rb line coverage 44.44% is below the 80.00% per-file threshold","penalty":3.0},{"id":"rd-l380zgdih1jq","severity":"medium","file":"app/models/post.rb","message":"High complexity score 32.5 for Post#publish!","penalty":2.25},{"id":"rd-xq812p6rik5z","severity":"medium","file":"app/models/post.rb","message":"Similar code group 1 across app/models/post.rb:4, app/models/user.rb:2","penalty":2.25},{"id":"rd-7wpu6grg34b2","severity":"low","file":"Gemfile.lock","message":"rack appears to be outdated","penalty":0.75}],"top_score_movers":[{"id":"rd-op6mq8hfnm99","severity":"critical","file":"app/models/post.rb","message":"SQL Injection: Possible SQL injection","penalty":15.0},{"id":"rd-habnfsz1zgdj","severity":"high","file":"Gemfile.lock","message":"rack: Example vulnerability","penalty":7.0},{"id":"rd-w4g0okt214t6","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#update","penalty":7.0},{"id":"rd-ygjxjocmpubf","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#new","penalty":7.0},{"id":"rd-iad8kr5ch7mh","severity":"high","file":"db/schema.rb","message":"posts.user_id has no index","penalty":7.0}]},"coverage":{"available":true,"status":"below_threshold","source":"simplecov","line_percent":48.0,"branch_percent":50.0,"thresholds":{"line":90.0,"file_line":80.0,"branch":null},"low_file_count":2,"changed_file_low_count":0}},"coverage":{"available":true,"status":"below_threshold","source":"simplecov","report_path":"coverage/.resultset.json","line_percent":48.0,"branch_percent":50.0,"covered_lines":12,"missed_lines":13,"total_lines":25,"covered_branches":1,"missed_branches":1,"total_branches":2,"thresholds":{"line":90.0,"file_line":80.0,"branch":null},"top_files":[{"file":"app/controllers/posts_controller.rb","line_percent":22.22,"covered_lines":2,"missed_lines":7,"total_lines":9,"below_threshold":true},{"file":"app/models/post.rb","line_percent":44.44,"covered_lines":4,"missed_lines":5,"total_lines":9,"branch_percent":50.0,"covered_branches":1,"missed_branches":1,"total_branches":2,"below_threshold":true},{"file":"app/models/user.rb","line_percent":80.0,"covered_lines":4,"missed_lines":1,"total_lines":5,"below_threshold":false},{"file":"app/controllers/health_controller.rb","line_percent":100.0,"covered_lines":2,"missed_lines":0,"total_lines":2,"below_threshold":false}],"low_file_count":2,"changed_files_below_threshold":[],"metadata":{}},"findings":[{"id":"rd-gfnxqbg1qkup","severity":"medium","category":"lint","tool":"rubocop","file":"app/models/post.rb","line":6,"confidence":"high","message":"Rails/TimeZone: Use Time.current instead of Time.now.","recommendation":"Fix the RuboCop offense or document why this cop should be configured differently.","agent_instruction":"Apply a minimal change that satisfies Rails/TimeZone. Preserve behavior and run the relevant tests.","suggested_commands":["bundle exec rubocop app/models/post.rb"],"metadata":{}},{"id":"rd-op6mq8hfnm99","severity":"critical","category":"security","tool":"brakeman","file":"app/models/post.rb","line":8,"confidence":"high","message":"SQL Injection: Possible SQL injection","recommendation":"Review Brakeman guidance: https://brakemanscanner.org/docs/warning_types/sql_injection/","agent_instruction":"Fix this security finding with the smallest behavior-preserving change. Prefer framework-safe APIs and add regression tests.","suggested_commands":[],"metadata":{"fingerprint":"abc123"}},{"id":"rd-habnfsz1zgdj","severity":"high","category":"dependency-security","tool":"bundler_audit","file":"Gemfile.lock","confidence":"high","message":"rack: Example vulnerability","recommendation":"Update rack to a patched version and rerun Bundler Audit.","agent_instruction":"Update the vulnerable gem conservatively, refresh the lockfile, and run the test suite.","suggested_commands":["bundle update rack","bundle exec bundle-audit check"],"metadata":{"advisory":"CVE-2099-0001","url":"https://example.test/advisory"}},{"id":"rd-2er9zgsavtk6","severity":"medium","category":"code-smell","tool":"reek","file":"app/models/post.rb","line":4,"confidence":"high","message":"TooManyStatements: has the smell of too many statements","recommendation":"Refactor the local smell without broad behavior changes.","agent_instruction":"Refactor only the affected method/class. Preserve public behavior and add or run tests around the changed code.","suggested_commands":[],"metadata":{"context":"Post#publish!","smell_type":"TooManyStatements"}},{"id":"rd-ci7ub1199rvp","severity":"low","category":"migration-safety","tool":"strong_migrations","file":"config/initializers/strong_migrations.rb","confidence":"medium","message":"strong_migrations is installed but no initializer was found","recommendation":"Generate or review the Strong Migrations initializer so project-specific safety settings are explicit.","agent_instruction":"Add the standard Strong Migrations initializer only after checking project database adapter and deployment practices.","suggested_commands":[],"metadata":{}},{"id":"rd-iad8kr5ch7mh","severity":"high","category":"database-integrity","tool":"rails_checks","file":"db/schema.rb","confidence":"high","message":"posts.user_id has no index","recommendation":"Add an index for the foreign key column to avoid slow association lookups.","agent_instruction":"Create a migration that adds an index on posts.user_id. For PostgreSQL production apps, prefer a concurrent index path compatible with strong_migrations.","suggested_commands":["bin/rails generate migration AddIndexToPostsUserId"],"metadata":{}},{"id":"rd-nd2oek4kujwu","severity":"high","category":"database-integrity","tool":"rails_checks","file":"app/models/user.rb","confidence":"medium","message":"users.email has a Rails uniqueness validation without a unique database index","recommendation":"Back uniqueness validations with a unique index to prevent race-condition duplicates.","agent_instruction":"Add a unique index migration for users.email, handle existing duplicate data if necessary, and rerun tests.","suggested_commands":["bin/rails generate migration AddUniqueIndexToUsersEmail"],"metadata":{}},{"id":"rd-p9vqylkxcybn","severity":"high","category":"routing","tool":"rails_checks","file":"config/routes.rb","confidence":"high","message":"Routes reference missing ghosts_controller.rb","recommendation":"Create the controller or remove/rename the route.","agent_instruction":"Align routes with real controller names. Prefer removing stale routes over creating empty controllers.","suggested_commands":[],"metadata":{}},{"id":"rd-stmr7t8qmjti","severity":"high","category":"routing","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"high","message":"Route points to missing posts#create","recommendation":"Implement the action or update/remove the route.","agent_instruction":"Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.","suggested_commands":[],"metadata":{}},{"id":"rd-c9eld5z4i9rf","severity":"high","category":"routing","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"high","message":"Route points to missing posts#destroy","recommendation":"Implement the action or update/remove the route.","agent_instruction":"Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.","suggested_commands":[],"metadata":{}},{"id":"rd-hdn01soot3gf","severity":"high","category":"routing","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"high","message":"Route points to missing posts#edit","recommendation":"Implement the action or update/remove the route.","agent_instruction":"Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.","suggested_commands":[],"metadata":{}},{"id":"rd-ygjxjocmpubf","severity":"high","category":"routing","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"high","message":"Route points to missing posts#new","recommendation":"Implement the action or update/remove the route.","agent_instruction":"Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.","suggested_commands":[],"metadata":{}},{"id":"rd-q2wm1ehy5qn","severity":"medium","category":"routing","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"medium","message":"posts#show has no matching template or explicit response","recommendation":"Add a template or explicit render/redirect/head response.","agent_instruction":"Inspect the action intent. Add the missing view or explicit response and cover the route with a request/controller test.","suggested_commands":[],"metadata":{}},{"id":"rd-w4g0okt214t6","severity":"high","category":"routing","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"high","message":"Route points to missing posts#update","recommendation":"Implement the action or update/remove the route.","agent_instruction":"Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.","suggested_commands":[],"metadata":{}},{"id":"rd-elkkq3siut9a","severity":"low","category":"dead-code","tool":"rails_checks","file":"app/controllers/posts_controller.rb","confidence":"low","message":"posts#archive is not referenced by simple route analysis","recommendation":"Review whether this action is reached by custom routing or can be removed.","agent_instruction":"Do not remove this action automatically. First search routes, tests, links, and callers for dynamic usage.","suggested_commands":[],"metadata":{}},{"id":"rd-4f2vjquwx6t4","severity":"medium","category":"technical-debt","tool":"rails_checks","file":"app/models/post.rb","confidence":"medium","message":"1 TODO/FIXME/HACK marker in 10 lines","recommendation":"Convert stale markers into tracked work or resolve them while the context is fresh.","agent_instruction":"Do not delete markers without addressing or preserving the underlying work item. Prefer resolving changed-file markers.","suggested_commands":[],"metadata":{}},{"id":"rd-4dacnomu05u7","severity":"medium","category":"deprecation","tool":"test_runner","line":1,"confidence":"medium","message":"DEPRECATION WARNING: old API is deprecated","recommendation":"Resolve deprecation warnings before framework or gem upgrades make them failures.","agent_instruction":"Update the deprecated API usage and add a regression test when behavior could change.","suggested_commands":[],"metadata":{}},{"id":"rd-bg982jjw6xt3","severity":"high","category":"runtime-n-plus-one","tool":"test_runner","line":2,"confidence":"medium","message":"Prosopite: N+1 queries detected for Post =\u003e [:user]","recommendation":"Fix the N+1 query by eager loading or adjusting the query path exercised by tests.","agent_instruction":"Use includes/preload/eager_load or query restructuring. Verify with the same test command.","suggested_commands":["../../fake_bin/passing_tests"],"metadata":{}},{"id":"rd-qx8han58wk0o","severity":"medium","category":"test-coverage","tool":"test_coverage","confidence":"high","message":"Line coverage 48.00% is below the 90.00% threshold","recommendation":"Add tests for uncovered application code, starting with the lowest-coverage files.","agent_instruction":"Prioritize behavior tests for uncovered app/lib code. Use the coverage metadata to start with files below the configured threshold.","suggested_commands":[],"metadata":{"line_percent":48.0,"threshold":90.0,"low_files":[{"file":"app/controllers/posts_controller.rb","line_percent":22.22,"covered_lines":2,"missed_lines":7,"total_lines":9,"below_threshold":true},{"file":"app/models/post.rb","line_percent":44.44,"covered_lines":4,"missed_lines":5,"total_lines":9,"branch_percent":50.0,"covered_branches":1,"missed_branches":1,"total_branches":2,"below_threshold":true}]}},{"id":"rd-9ikgvif63uif","severity":"medium","category":"test-coverage","tool":"test_coverage","file":"app/controllers/posts_controller.rb","confidence":"high","message":"app/controllers/posts_controller.rb line coverage 22.22% is below the 80.00% per-file threshold","recommendation":"Add focused tests that exercise the uncovered behavior in this file.","agent_instruction":"Add or update tests for this file before expanding the implementation. Prefer behavior-level tests that cover the missing branches or lines.","suggested_commands":[],"metadata":{"file":"app/controllers/posts_controller.rb","line_percent":22.22,"covered_lines":2,"missed_lines":7,"total_lines":9,"below_threshold":true,"threshold":80.0}},{"id":"rd-s83lai6gkb0r","severity":"medium","category":"test-coverage","tool":"test_coverage","file":"app/models/post.rb","confidence":"high","message":"app/models/post.rb line coverage 44.44% is below the 80.00% per-file threshold","recommendation":"Add focused tests that exercise the uncovered behavior in this file.","agent_instruction":"Add or update tests for this file before expanding the implementation. Prefer behavior-level tests that cover the missing branches or lines.","suggested_commands":[],"metadata":{"file":"app/models/post.rb","line_percent":44.44,"covered_lines":4,"missed_lines":5,"total_lines":9,"branch_percent":50.0,"covered_branches":1,"missed_branches":1,"total_branches":2,"below_threshold":true,"threshold":80.0}},{"id":"rd-l380zgdih1jq","severity":"medium","category":"complexity","tool":"flog","file":"app/models/post.rb","line":4,"confidence":"medium","message":"High complexity score 32.5 for Post#publish!","recommendation":"Extract simpler methods or objects around the complex branch.","agent_instruction":"Reduce complexity with behavior-preserving extraction. Do not combine this with unrelated cleanup.","suggested_commands":[],"metadata":{"flog_score":32.5}},{"id":"rd-xq812p6rik5z","severity":"medium","category":"duplication","tool":"flay","file":"app/models/post.rb","line":4,"confidence":"medium","message":"Similar code group 1 across app/models/post.rb:4, app/models/user.rb:2","recommendation":"Review whether this duplication is intentional. Extract shared behavior only if the abstraction is clear.","agent_instruction":"Do not blindly abstract. Compare the duplicated code paths, preserve semantics, and add tests if extracting shared code.","suggested_commands":[],"metadata":{"locations":["app/models/post.rb:4","app/models/user.rb:2"]}},{"id":"rd-7wpu6grg34b2","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"rack appears to be outdated","recommendation":"Review the update risk and update in a separate dependency-focused change.","agent_instruction":"Do not batch this with feature work. Update rack conservatively and run the full test suite.","suggested_commands":["bundle update rack"],"metadata":{}}],"hotspots":[{"file":"app/controllers/posts_controller.rb","score":42,"finding_count":8,"churn":0,"changed":false,"categories":["dead-code","routing","test-coverage"],"summary":"Inherited file with 8 findings across routing, dead-code, test-coverage."},{"file":"app/models/post.rb","score":33,"finding_count":7,"churn":0,"changed":false,"categories":["code-smell","complexity","duplication","lint","security","technical-debt","test-coverage"],"summary":"Inherited file with 7 findings across lint, security, code-smell, technical-debt, test-coverage, complexity, duplication."},{"file":"Gemfile.lock","score":8,"finding_count":2,"churn":0,"changed":false,"categories":["dependency-freshness","dependency-security"],"summary":"Inherited file with 2 findings across dependency-security, dependency-freshness."},{"file":"db/schema.rb","score":7,"finding_count":1,"churn":0,"changed":false,"categories":["database-integrity"],"summary":"Inherited file with 1 finding across database-integrity."},{"file":"app/models/user.rb","score":7,"finding_count":1,"churn":0,"changed":false,"categories":["database-integrity"],"summary":"Inherited file with 1 finding across database-integrity."},{"file":"config/routes.rb","score":7,"finding_count":1,"churn":0,"changed":false,"categories":["routing"],"summary":"Inherited file with 1 finding across routing."},{"file":"config/initializers/strong_migrations.rb","score":1,"finding_count":1,"churn":0,"changed":false,"categories":["migration-safety"],"summary":"Inherited file with 1 finding across migration-safety."}],"tool_runs":[{"name":"rubocop","available":true,"skipped":false,"command":"../../fake_bin/rubocop","exit_status":1,"duration_ms":50,"metadata":{}},{"name":"brakeman","available":true,"skipped":false,"command":"../../fake_bin/brakeman","exit_status":3,"duration_ms":50,"metadata":{}},{"name":"bundler_audit","available":true,"skipped":false,"command":"../../fake_bin/bundle-audit","exit_status":1,"duration_ms":50,"metadata":{}},{"name":"zeitwerk","available":true,"skipped":false,"command":"../../fake_bin/rails zeitwerk:check","exit_status":0,"duration_ms":50,"metadata":{}},{"name":"reek","available":true,"skipped":false,"command":"../../fake_bin/reek","exit_status":2,"duration_ms":50,"metadata":{}},{"name":"strong_migrations","available":true,"skipped":false,"exit_status":0,"duration_ms":50,"metadata":{"coverage":"strong_migrations gem detected","initializer_present":false}},{"name":"rails_checks","available":true,"skipped":false,"exit_status":0,"duration_ms":50,"metadata":{"checks":["indexes","uniqueness","routes","views","size","todos","tests","coverage-gaps"]}},{"name":"test_runner","available":true,"skipped":false,"command":"../../fake_bin/passing_tests","exit_status":0,"duration_ms":50,"metadata":{}},{"name":"test_coverage","available":true,"skipped":false,"exit_status":0,"duration_ms":1,"metadata":{"coverage":{"available":true,"status":"below_threshold","source":"simplecov","report_path":"coverage/.resultset.json","line_percent":48.0,"branch_percent":50.0,"covered_lines":12,"missed_lines":13,"total_lines":25,"covered_branches":1,"missed_branches":1,"total_branches":2,"thresholds":{"line":90.0,"file_line":80.0,"branch":null},"top_files":[{"file":"app/controllers/posts_controller.rb","line_percent":22.22,"covered_lines":2,"missed_lines":7,"total_lines":9,"below_threshold":true},{"file":"app/models/post.rb","line_percent":44.44,"covered_lines":4,"missed_lines":5,"total_lines":9,"branch_percent":50.0,"covered_branches":1,"missed_branches":1,"total_branches":2,"below_threshold":true},{"file":"app/models/user.rb","line_percent":80.0,"covered_lines":4,"missed_lines":1,"total_lines":5,"below_threshold":false},{"file":"app/controllers/health_controller.rb","line_percent":100.0,"covered_lines":2,"missed_lines":0,"total_lines":2,"below_threshold":false}],"low_file_count":2,"changed_files_below_threshold":[],"metadata":{}}}},{"name":"flog","available":true,"skipped":false,"command":"../../fake_bin/flog","exit_status":0,"duration_ms":50,"metadata":{}},{"name":"flay","available":true,"skipped":false,"command":"../../fake_bin/flay","exit_status":0,"duration_ms":50,"metadata":{}},{"name":"dependency_freshness","available":true,"skipped":false,"command":"bundle outdated --parseable","exit_status":1,"duration_ms":50,"metadata":{}}]}</script>
691
+ <script>
692
+ document.querySelectorAll("[data-filter]").forEach((button) => {
693
+ button.addEventListener("click", () => {
694
+ document.querySelectorAll("[data-filter]").forEach((item) => item.classList.remove("active"));
695
+ button.classList.add("active");
696
+ const filter = button.dataset.filter;
697
+ document.querySelectorAll("tr[data-severity]").forEach((row) => {
698
+ row.style.display = filter === "all" || row.dataset.severity === filter ? "" : "none";
699
+ });
700
+ });
701
+ });
702
+ </script>
703
+ </body>
704
+ </html>