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,971 @@
1
+ {
2
+ "schema_version": "1.1",
3
+ "generated_at": "2026-05-30T20:00:00Z",
4
+ "project_root": "/path/to/app",
5
+ "profile": "deep",
6
+ "metadata": {
7
+ "rails_app": true,
8
+ "ruby_version": "3.4.1",
9
+ "rails_version": "8.0.0",
10
+ "changed_files": []
11
+ },
12
+ "summary": {
13
+ "profile": "deep",
14
+ "duration_ms": 600,
15
+ "finding_count": 24,
16
+ "severity_counts": {
17
+ "medium": 10,
18
+ "critical": 1,
19
+ "high": 10,
20
+ "low": 3
21
+ },
22
+ "skipped_tools": [],
23
+ "score": {
24
+ "overall": 0,
25
+ "changed_files": 100,
26
+ "confidence": 100,
27
+ "penalties": [
28
+ {
29
+ "id": "rd-gfnxqbg1qkup",
30
+ "severity": "medium",
31
+ "file": "app/models/post.rb",
32
+ "message": "Rails/TimeZone: Use Time.current instead of Time.now.",
33
+ "penalty": 3.0
34
+ },
35
+ {
36
+ "id": "rd-op6mq8hfnm99",
37
+ "severity": "critical",
38
+ "file": "app/models/post.rb",
39
+ "message": "SQL Injection: Possible SQL injection",
40
+ "penalty": 15.0
41
+ },
42
+ {
43
+ "id": "rd-habnfsz1zgdj",
44
+ "severity": "high",
45
+ "file": "Gemfile.lock",
46
+ "message": "rack: Example vulnerability",
47
+ "penalty": 7.0
48
+ },
49
+ {
50
+ "id": "rd-2er9zgsavtk6",
51
+ "severity": "medium",
52
+ "file": "app/models/post.rb",
53
+ "message": "TooManyStatements: has the smell of too many statements",
54
+ "penalty": 3.0
55
+ },
56
+ {
57
+ "id": "rd-ci7ub1199rvp",
58
+ "severity": "low",
59
+ "file": "config/initializers/strong_migrations.rb",
60
+ "message": "strong_migrations is installed but no initializer was found",
61
+ "penalty": 0.75
62
+ },
63
+ {
64
+ "id": "rd-iad8kr5ch7mh",
65
+ "severity": "high",
66
+ "file": "db/schema.rb",
67
+ "message": "posts.user_id has no index",
68
+ "penalty": 7.0
69
+ },
70
+ {
71
+ "id": "rd-nd2oek4kujwu",
72
+ "severity": "high",
73
+ "file": "app/models/user.rb",
74
+ "message": "users.email has a Rails uniqueness validation without a unique database index",
75
+ "penalty": 5.25
76
+ },
77
+ {
78
+ "id": "rd-p9vqylkxcybn",
79
+ "severity": "high",
80
+ "file": "config/routes.rb",
81
+ "message": "Routes reference missing ghosts_controller.rb",
82
+ "penalty": 7.0
83
+ },
84
+ {
85
+ "id": "rd-stmr7t8qmjti",
86
+ "severity": "high",
87
+ "file": "app/controllers/posts_controller.rb",
88
+ "message": "Route points to missing posts#create",
89
+ "penalty": 7.0
90
+ },
91
+ {
92
+ "id": "rd-c9eld5z4i9rf",
93
+ "severity": "high",
94
+ "file": "app/controllers/posts_controller.rb",
95
+ "message": "Route points to missing posts#destroy",
96
+ "penalty": 7.0
97
+ },
98
+ {
99
+ "id": "rd-hdn01soot3gf",
100
+ "severity": "high",
101
+ "file": "app/controllers/posts_controller.rb",
102
+ "message": "Route points to missing posts#edit",
103
+ "penalty": 7.0
104
+ },
105
+ {
106
+ "id": "rd-ygjxjocmpubf",
107
+ "severity": "high",
108
+ "file": "app/controllers/posts_controller.rb",
109
+ "message": "Route points to missing posts#new",
110
+ "penalty": 7.0
111
+ },
112
+ {
113
+ "id": "rd-q2wm1ehy5qn",
114
+ "severity": "medium",
115
+ "file": "app/controllers/posts_controller.rb",
116
+ "message": "posts#show has no matching template or explicit response",
117
+ "penalty": 2.25
118
+ },
119
+ {
120
+ "id": "rd-w4g0okt214t6",
121
+ "severity": "high",
122
+ "file": "app/controllers/posts_controller.rb",
123
+ "message": "Route points to missing posts#update",
124
+ "penalty": 7.0
125
+ },
126
+ {
127
+ "id": "rd-elkkq3siut9a",
128
+ "severity": "low",
129
+ "file": "app/controllers/posts_controller.rb",
130
+ "message": "posts#archive is not referenced by simple route analysis",
131
+ "penalty": 0.4
132
+ },
133
+ {
134
+ "id": "rd-4f2vjquwx6t4",
135
+ "severity": "medium",
136
+ "file": "app/models/post.rb",
137
+ "message": "1 TODO/FIXME/HACK marker in 10 lines",
138
+ "penalty": 2.25
139
+ },
140
+ {
141
+ "id": "rd-4dacnomu05u7",
142
+ "severity": "medium",
143
+ "file": null,
144
+ "message": "DEPRECATION WARNING: old API is deprecated",
145
+ "penalty": 2.25
146
+ },
147
+ {
148
+ "id": "rd-bg982jjw6xt3",
149
+ "severity": "high",
150
+ "file": null,
151
+ "message": "Prosopite: N+1 queries detected for Post => [:user]",
152
+ "penalty": 5.25
153
+ },
154
+ {
155
+ "id": "rd-qx8han58wk0o",
156
+ "severity": "medium",
157
+ "file": null,
158
+ "message": "Line coverage 48.00% is below the 90.00% threshold",
159
+ "penalty": 3.0
160
+ },
161
+ {
162
+ "id": "rd-9ikgvif63uif",
163
+ "severity": "medium",
164
+ "file": "app/controllers/posts_controller.rb",
165
+ "message": "app/controllers/posts_controller.rb line coverage 22.22% is below the 80.00% per-file threshold",
166
+ "penalty": 3.0
167
+ },
168
+ {
169
+ "id": "rd-s83lai6gkb0r",
170
+ "severity": "medium",
171
+ "file": "app/models/post.rb",
172
+ "message": "app/models/post.rb line coverage 44.44% is below the 80.00% per-file threshold",
173
+ "penalty": 3.0
174
+ },
175
+ {
176
+ "id": "rd-l380zgdih1jq",
177
+ "severity": "medium",
178
+ "file": "app/models/post.rb",
179
+ "message": "High complexity score 32.5 for Post#publish!",
180
+ "penalty": 2.25
181
+ },
182
+ {
183
+ "id": "rd-xq812p6rik5z",
184
+ "severity": "medium",
185
+ "file": "app/models/post.rb",
186
+ "message": "Similar code group 1 across app/models/post.rb:4, app/models/user.rb:2",
187
+ "penalty": 2.25
188
+ },
189
+ {
190
+ "id": "rd-7wpu6grg34b2",
191
+ "severity": "low",
192
+ "file": "Gemfile.lock",
193
+ "message": "rack appears to be outdated",
194
+ "penalty": 0.75
195
+ }
196
+ ],
197
+ "top_score_movers": [
198
+ {
199
+ "id": "rd-op6mq8hfnm99",
200
+ "severity": "critical",
201
+ "file": "app/models/post.rb",
202
+ "message": "SQL Injection: Possible SQL injection",
203
+ "penalty": 15.0
204
+ },
205
+ {
206
+ "id": "rd-habnfsz1zgdj",
207
+ "severity": "high",
208
+ "file": "Gemfile.lock",
209
+ "message": "rack: Example vulnerability",
210
+ "penalty": 7.0
211
+ },
212
+ {
213
+ "id": "rd-w4g0okt214t6",
214
+ "severity": "high",
215
+ "file": "app/controllers/posts_controller.rb",
216
+ "message": "Route points to missing posts#update",
217
+ "penalty": 7.0
218
+ },
219
+ {
220
+ "id": "rd-ygjxjocmpubf",
221
+ "severity": "high",
222
+ "file": "app/controllers/posts_controller.rb",
223
+ "message": "Route points to missing posts#new",
224
+ "penalty": 7.0
225
+ },
226
+ {
227
+ "id": "rd-iad8kr5ch7mh",
228
+ "severity": "high",
229
+ "file": "db/schema.rb",
230
+ "message": "posts.user_id has no index",
231
+ "penalty": 7.0
232
+ }
233
+ ]
234
+ },
235
+ "coverage": {
236
+ "available": true,
237
+ "status": "below_threshold",
238
+ "source": "simplecov",
239
+ "line_percent": 48.0,
240
+ "branch_percent": 50.0,
241
+ "thresholds": {
242
+ "line": 90.0,
243
+ "file_line": 80.0,
244
+ "branch": null
245
+ },
246
+ "low_file_count": 2,
247
+ "changed_file_low_count": 0
248
+ }
249
+ },
250
+ "coverage": {
251
+ "available": true,
252
+ "status": "below_threshold",
253
+ "source": "simplecov",
254
+ "report_path": "coverage/.resultset.json",
255
+ "line_percent": 48.0,
256
+ "branch_percent": 50.0,
257
+ "covered_lines": 12,
258
+ "missed_lines": 13,
259
+ "total_lines": 25,
260
+ "covered_branches": 1,
261
+ "missed_branches": 1,
262
+ "total_branches": 2,
263
+ "thresholds": {
264
+ "line": 90.0,
265
+ "file_line": 80.0,
266
+ "branch": null
267
+ },
268
+ "top_files": [
269
+ {
270
+ "file": "app/controllers/posts_controller.rb",
271
+ "line_percent": 22.22,
272
+ "covered_lines": 2,
273
+ "missed_lines": 7,
274
+ "total_lines": 9,
275
+ "below_threshold": true
276
+ },
277
+ {
278
+ "file": "app/models/post.rb",
279
+ "line_percent": 44.44,
280
+ "covered_lines": 4,
281
+ "missed_lines": 5,
282
+ "total_lines": 9,
283
+ "branch_percent": 50.0,
284
+ "covered_branches": 1,
285
+ "missed_branches": 1,
286
+ "total_branches": 2,
287
+ "below_threshold": true
288
+ },
289
+ {
290
+ "file": "app/models/user.rb",
291
+ "line_percent": 80.0,
292
+ "covered_lines": 4,
293
+ "missed_lines": 1,
294
+ "total_lines": 5,
295
+ "below_threshold": false
296
+ },
297
+ {
298
+ "file": "app/controllers/health_controller.rb",
299
+ "line_percent": 100.0,
300
+ "covered_lines": 2,
301
+ "missed_lines": 0,
302
+ "total_lines": 2,
303
+ "below_threshold": false
304
+ }
305
+ ],
306
+ "low_file_count": 2,
307
+ "changed_files_below_threshold": [],
308
+ "metadata": {}
309
+ },
310
+ "findings": [
311
+ {
312
+ "id": "rd-gfnxqbg1qkup",
313
+ "severity": "medium",
314
+ "category": "lint",
315
+ "tool": "rubocop",
316
+ "file": "app/models/post.rb",
317
+ "line": 6,
318
+ "confidence": "high",
319
+ "message": "Rails/TimeZone: Use Time.current instead of Time.now.",
320
+ "recommendation": "Fix the RuboCop offense or document why this cop should be configured differently.",
321
+ "agent_instruction": "Apply a minimal change that satisfies Rails/TimeZone. Preserve behavior and run the relevant tests.",
322
+ "suggested_commands": [
323
+ "bundle exec rubocop app/models/post.rb"
324
+ ],
325
+ "metadata": {}
326
+ },
327
+ {
328
+ "id": "rd-op6mq8hfnm99",
329
+ "severity": "critical",
330
+ "category": "security",
331
+ "tool": "brakeman",
332
+ "file": "app/models/post.rb",
333
+ "line": 8,
334
+ "confidence": "high",
335
+ "message": "SQL Injection: Possible SQL injection",
336
+ "recommendation": "Review Brakeman guidance: https://brakemanscanner.org/docs/warning_types/sql_injection/",
337
+ "agent_instruction": "Fix this security finding with the smallest behavior-preserving change. Prefer framework-safe APIs and add regression tests.",
338
+ "suggested_commands": [],
339
+ "metadata": {
340
+ "fingerprint": "abc123"
341
+ }
342
+ },
343
+ {
344
+ "id": "rd-habnfsz1zgdj",
345
+ "severity": "high",
346
+ "category": "dependency-security",
347
+ "tool": "bundler_audit",
348
+ "file": "Gemfile.lock",
349
+ "confidence": "high",
350
+ "message": "rack: Example vulnerability",
351
+ "recommendation": "Update rack to a patched version and rerun Bundler Audit.",
352
+ "agent_instruction": "Update the vulnerable gem conservatively, refresh the lockfile, and run the test suite.",
353
+ "suggested_commands": [
354
+ "bundle update rack",
355
+ "bundle exec bundle-audit check"
356
+ ],
357
+ "metadata": {
358
+ "advisory": "CVE-2099-0001",
359
+ "url": "https://example.test/advisory"
360
+ }
361
+ },
362
+ {
363
+ "id": "rd-2er9zgsavtk6",
364
+ "severity": "medium",
365
+ "category": "code-smell",
366
+ "tool": "reek",
367
+ "file": "app/models/post.rb",
368
+ "line": 4,
369
+ "confidence": "high",
370
+ "message": "TooManyStatements: has the smell of too many statements",
371
+ "recommendation": "Refactor the local smell without broad behavior changes.",
372
+ "agent_instruction": "Refactor only the affected method/class. Preserve public behavior and add or run tests around the changed code.",
373
+ "suggested_commands": [],
374
+ "metadata": {
375
+ "context": "Post#publish!",
376
+ "smell_type": "TooManyStatements"
377
+ }
378
+ },
379
+ {
380
+ "id": "rd-ci7ub1199rvp",
381
+ "severity": "low",
382
+ "category": "migration-safety",
383
+ "tool": "strong_migrations",
384
+ "file": "config/initializers/strong_migrations.rb",
385
+ "confidence": "medium",
386
+ "message": "strong_migrations is installed but no initializer was found",
387
+ "recommendation": "Generate or review the Strong Migrations initializer so project-specific safety settings are explicit.",
388
+ "agent_instruction": "Add the standard Strong Migrations initializer only after checking project database adapter and deployment practices.",
389
+ "suggested_commands": [],
390
+ "metadata": {}
391
+ },
392
+ {
393
+ "id": "rd-iad8kr5ch7mh",
394
+ "severity": "high",
395
+ "category": "database-integrity",
396
+ "tool": "rails_checks",
397
+ "file": "db/schema.rb",
398
+ "confidence": "high",
399
+ "message": "posts.user_id has no index",
400
+ "recommendation": "Add an index for the foreign key column to avoid slow association lookups.",
401
+ "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.",
402
+ "suggested_commands": [
403
+ "bin/rails generate migration AddIndexToPostsUserId"
404
+ ],
405
+ "metadata": {}
406
+ },
407
+ {
408
+ "id": "rd-nd2oek4kujwu",
409
+ "severity": "high",
410
+ "category": "database-integrity",
411
+ "tool": "rails_checks",
412
+ "file": "app/models/user.rb",
413
+ "confidence": "medium",
414
+ "message": "users.email has a Rails uniqueness validation without a unique database index",
415
+ "recommendation": "Back uniqueness validations with a unique index to prevent race-condition duplicates.",
416
+ "agent_instruction": "Add a unique index migration for users.email, handle existing duplicate data if necessary, and rerun tests.",
417
+ "suggested_commands": [
418
+ "bin/rails generate migration AddUniqueIndexToUsersEmail"
419
+ ],
420
+ "metadata": {}
421
+ },
422
+ {
423
+ "id": "rd-p9vqylkxcybn",
424
+ "severity": "high",
425
+ "category": "routing",
426
+ "tool": "rails_checks",
427
+ "file": "config/routes.rb",
428
+ "confidence": "high",
429
+ "message": "Routes reference missing ghosts_controller.rb",
430
+ "recommendation": "Create the controller or remove/rename the route.",
431
+ "agent_instruction": "Align routes with real controller names. Prefer removing stale routes over creating empty controllers.",
432
+ "suggested_commands": [],
433
+ "metadata": {}
434
+ },
435
+ {
436
+ "id": "rd-stmr7t8qmjti",
437
+ "severity": "high",
438
+ "category": "routing",
439
+ "tool": "rails_checks",
440
+ "file": "app/controllers/posts_controller.rb",
441
+ "confidence": "high",
442
+ "message": "Route points to missing posts#create",
443
+ "recommendation": "Implement the action or update/remove the route.",
444
+ "agent_instruction": "Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.",
445
+ "suggested_commands": [],
446
+ "metadata": {}
447
+ },
448
+ {
449
+ "id": "rd-c9eld5z4i9rf",
450
+ "severity": "high",
451
+ "category": "routing",
452
+ "tool": "rails_checks",
453
+ "file": "app/controllers/posts_controller.rb",
454
+ "confidence": "high",
455
+ "message": "Route points to missing posts#destroy",
456
+ "recommendation": "Implement the action or update/remove the route.",
457
+ "agent_instruction": "Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.",
458
+ "suggested_commands": [],
459
+ "metadata": {}
460
+ },
461
+ {
462
+ "id": "rd-hdn01soot3gf",
463
+ "severity": "high",
464
+ "category": "routing",
465
+ "tool": "rails_checks",
466
+ "file": "app/controllers/posts_controller.rb",
467
+ "confidence": "high",
468
+ "message": "Route points to missing posts#edit",
469
+ "recommendation": "Implement the action or update/remove the route.",
470
+ "agent_instruction": "Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.",
471
+ "suggested_commands": [],
472
+ "metadata": {}
473
+ },
474
+ {
475
+ "id": "rd-ygjxjocmpubf",
476
+ "severity": "high",
477
+ "category": "routing",
478
+ "tool": "rails_checks",
479
+ "file": "app/controllers/posts_controller.rb",
480
+ "confidence": "high",
481
+ "message": "Route points to missing posts#new",
482
+ "recommendation": "Implement the action or update/remove the route.",
483
+ "agent_instruction": "Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.",
484
+ "suggested_commands": [],
485
+ "metadata": {}
486
+ },
487
+ {
488
+ "id": "rd-q2wm1ehy5qn",
489
+ "severity": "medium",
490
+ "category": "routing",
491
+ "tool": "rails_checks",
492
+ "file": "app/controllers/posts_controller.rb",
493
+ "confidence": "medium",
494
+ "message": "posts#show has no matching template or explicit response",
495
+ "recommendation": "Add a template or explicit render/redirect/head response.",
496
+ "agent_instruction": "Inspect the action intent. Add the missing view or explicit response and cover the route with a request/controller test.",
497
+ "suggested_commands": [],
498
+ "metadata": {}
499
+ },
500
+ {
501
+ "id": "rd-w4g0okt214t6",
502
+ "severity": "high",
503
+ "category": "routing",
504
+ "tool": "rails_checks",
505
+ "file": "app/controllers/posts_controller.rb",
506
+ "confidence": "high",
507
+ "message": "Route points to missing posts#update",
508
+ "recommendation": "Implement the action or update/remove the route.",
509
+ "agent_instruction": "Do not add an empty action. Determine the intended route behavior, then implement or remove the stale route.",
510
+ "suggested_commands": [],
511
+ "metadata": {}
512
+ },
513
+ {
514
+ "id": "rd-elkkq3siut9a",
515
+ "severity": "low",
516
+ "category": "dead-code",
517
+ "tool": "rails_checks",
518
+ "file": "app/controllers/posts_controller.rb",
519
+ "confidence": "low",
520
+ "message": "posts#archive is not referenced by simple route analysis",
521
+ "recommendation": "Review whether this action is reached by custom routing or can be removed.",
522
+ "agent_instruction": "Do not remove this action automatically. First search routes, tests, links, and callers for dynamic usage.",
523
+ "suggested_commands": [],
524
+ "metadata": {}
525
+ },
526
+ {
527
+ "id": "rd-4f2vjquwx6t4",
528
+ "severity": "medium",
529
+ "category": "technical-debt",
530
+ "tool": "rails_checks",
531
+ "file": "app/models/post.rb",
532
+ "confidence": "medium",
533
+ "message": "1 TODO/FIXME/HACK marker in 10 lines",
534
+ "recommendation": "Convert stale markers into tracked work or resolve them while the context is fresh.",
535
+ "agent_instruction": "Do not delete markers without addressing or preserving the underlying work item. Prefer resolving changed-file markers.",
536
+ "suggested_commands": [],
537
+ "metadata": {}
538
+ },
539
+ {
540
+ "id": "rd-4dacnomu05u7",
541
+ "severity": "medium",
542
+ "category": "deprecation",
543
+ "tool": "test_runner",
544
+ "line": 1,
545
+ "confidence": "medium",
546
+ "message": "DEPRECATION WARNING: old API is deprecated",
547
+ "recommendation": "Resolve deprecation warnings before framework or gem upgrades make them failures.",
548
+ "agent_instruction": "Update the deprecated API usage and add a regression test when behavior could change.",
549
+ "suggested_commands": [],
550
+ "metadata": {}
551
+ },
552
+ {
553
+ "id": "rd-bg982jjw6xt3",
554
+ "severity": "high",
555
+ "category": "runtime-n-plus-one",
556
+ "tool": "test_runner",
557
+ "line": 2,
558
+ "confidence": "medium",
559
+ "message": "Prosopite: N+1 queries detected for Post => [:user]",
560
+ "recommendation": "Fix the N+1 query by eager loading or adjusting the query path exercised by tests.",
561
+ "agent_instruction": "Use includes/preload/eager_load or query restructuring. Verify with the same test command.",
562
+ "suggested_commands": [
563
+ "../../fake_bin/passing_tests"
564
+ ],
565
+ "metadata": {}
566
+ },
567
+ {
568
+ "id": "rd-qx8han58wk0o",
569
+ "severity": "medium",
570
+ "category": "test-coverage",
571
+ "tool": "test_coverage",
572
+ "confidence": "high",
573
+ "message": "Line coverage 48.00% is below the 90.00% threshold",
574
+ "recommendation": "Add tests for uncovered application code, starting with the lowest-coverage files.",
575
+ "agent_instruction": "Prioritize behavior tests for uncovered app/lib code. Use the coverage metadata to start with files below the configured threshold.",
576
+ "suggested_commands": [],
577
+ "metadata": {
578
+ "line_percent": 48.0,
579
+ "threshold": 90.0,
580
+ "low_files": [
581
+ {
582
+ "file": "app/controllers/posts_controller.rb",
583
+ "line_percent": 22.22,
584
+ "covered_lines": 2,
585
+ "missed_lines": 7,
586
+ "total_lines": 9,
587
+ "below_threshold": true
588
+ },
589
+ {
590
+ "file": "app/models/post.rb",
591
+ "line_percent": 44.44,
592
+ "covered_lines": 4,
593
+ "missed_lines": 5,
594
+ "total_lines": 9,
595
+ "branch_percent": 50.0,
596
+ "covered_branches": 1,
597
+ "missed_branches": 1,
598
+ "total_branches": 2,
599
+ "below_threshold": true
600
+ }
601
+ ]
602
+ }
603
+ },
604
+ {
605
+ "id": "rd-9ikgvif63uif",
606
+ "severity": "medium",
607
+ "category": "test-coverage",
608
+ "tool": "test_coverage",
609
+ "file": "app/controllers/posts_controller.rb",
610
+ "confidence": "high",
611
+ "message": "app/controllers/posts_controller.rb line coverage 22.22% is below the 80.00% per-file threshold",
612
+ "recommendation": "Add focused tests that exercise the uncovered behavior in this file.",
613
+ "agent_instruction": "Add or update tests for this file before expanding the implementation. Prefer behavior-level tests that cover the missing branches or lines.",
614
+ "suggested_commands": [],
615
+ "metadata": {
616
+ "file": "app/controllers/posts_controller.rb",
617
+ "line_percent": 22.22,
618
+ "covered_lines": 2,
619
+ "missed_lines": 7,
620
+ "total_lines": 9,
621
+ "below_threshold": true,
622
+ "threshold": 80.0
623
+ }
624
+ },
625
+ {
626
+ "id": "rd-s83lai6gkb0r",
627
+ "severity": "medium",
628
+ "category": "test-coverage",
629
+ "tool": "test_coverage",
630
+ "file": "app/models/post.rb",
631
+ "confidence": "high",
632
+ "message": "app/models/post.rb line coverage 44.44% is below the 80.00% per-file threshold",
633
+ "recommendation": "Add focused tests that exercise the uncovered behavior in this file.",
634
+ "agent_instruction": "Add or update tests for this file before expanding the implementation. Prefer behavior-level tests that cover the missing branches or lines.",
635
+ "suggested_commands": [],
636
+ "metadata": {
637
+ "file": "app/models/post.rb",
638
+ "line_percent": 44.44,
639
+ "covered_lines": 4,
640
+ "missed_lines": 5,
641
+ "total_lines": 9,
642
+ "branch_percent": 50.0,
643
+ "covered_branches": 1,
644
+ "missed_branches": 1,
645
+ "total_branches": 2,
646
+ "below_threshold": true,
647
+ "threshold": 80.0
648
+ }
649
+ },
650
+ {
651
+ "id": "rd-l380zgdih1jq",
652
+ "severity": "medium",
653
+ "category": "complexity",
654
+ "tool": "flog",
655
+ "file": "app/models/post.rb",
656
+ "line": 4,
657
+ "confidence": "medium",
658
+ "message": "High complexity score 32.5 for Post#publish!",
659
+ "recommendation": "Extract simpler methods or objects around the complex branch.",
660
+ "agent_instruction": "Reduce complexity with behavior-preserving extraction. Do not combine this with unrelated cleanup.",
661
+ "suggested_commands": [],
662
+ "metadata": {
663
+ "flog_score": 32.5
664
+ }
665
+ },
666
+ {
667
+ "id": "rd-xq812p6rik5z",
668
+ "severity": "medium",
669
+ "category": "duplication",
670
+ "tool": "flay",
671
+ "file": "app/models/post.rb",
672
+ "line": 4,
673
+ "confidence": "medium",
674
+ "message": "Similar code group 1 across app/models/post.rb:4, app/models/user.rb:2",
675
+ "recommendation": "Review whether this duplication is intentional. Extract shared behavior only if the abstraction is clear.",
676
+ "agent_instruction": "Do not blindly abstract. Compare the duplicated code paths, preserve semantics, and add tests if extracting shared code.",
677
+ "suggested_commands": [],
678
+ "metadata": {
679
+ "locations": [
680
+ "app/models/post.rb:4",
681
+ "app/models/user.rb:2"
682
+ ]
683
+ }
684
+ },
685
+ {
686
+ "id": "rd-7wpu6grg34b2",
687
+ "severity": "low",
688
+ "category": "dependency-freshness",
689
+ "tool": "dependency_freshness",
690
+ "file": "Gemfile.lock",
691
+ "confidence": "medium",
692
+ "message": "rack appears to be outdated",
693
+ "recommendation": "Review the update risk and update in a separate dependency-focused change.",
694
+ "agent_instruction": "Do not batch this with feature work. Update rack conservatively and run the full test suite.",
695
+ "suggested_commands": [
696
+ "bundle update rack"
697
+ ],
698
+ "metadata": {}
699
+ }
700
+ ],
701
+ "hotspots": [
702
+ {
703
+ "file": "app/controllers/posts_controller.rb",
704
+ "score": 42,
705
+ "finding_count": 8,
706
+ "churn": 0,
707
+ "changed": false,
708
+ "categories": [
709
+ "dead-code",
710
+ "routing",
711
+ "test-coverage"
712
+ ],
713
+ "summary": "Inherited file with 8 findings across routing, dead-code, test-coverage."
714
+ },
715
+ {
716
+ "file": "app/models/post.rb",
717
+ "score": 33,
718
+ "finding_count": 7,
719
+ "churn": 0,
720
+ "changed": false,
721
+ "categories": [
722
+ "code-smell",
723
+ "complexity",
724
+ "duplication",
725
+ "lint",
726
+ "security",
727
+ "technical-debt",
728
+ "test-coverage"
729
+ ],
730
+ "summary": "Inherited file with 7 findings across lint, security, code-smell, technical-debt, test-coverage, complexity, duplication."
731
+ },
732
+ {
733
+ "file": "Gemfile.lock",
734
+ "score": 8,
735
+ "finding_count": 2,
736
+ "churn": 0,
737
+ "changed": false,
738
+ "categories": [
739
+ "dependency-freshness",
740
+ "dependency-security"
741
+ ],
742
+ "summary": "Inherited file with 2 findings across dependency-security, dependency-freshness."
743
+ },
744
+ {
745
+ "file": "db/schema.rb",
746
+ "score": 7,
747
+ "finding_count": 1,
748
+ "churn": 0,
749
+ "changed": false,
750
+ "categories": [
751
+ "database-integrity"
752
+ ],
753
+ "summary": "Inherited file with 1 finding across database-integrity."
754
+ },
755
+ {
756
+ "file": "app/models/user.rb",
757
+ "score": 7,
758
+ "finding_count": 1,
759
+ "churn": 0,
760
+ "changed": false,
761
+ "categories": [
762
+ "database-integrity"
763
+ ],
764
+ "summary": "Inherited file with 1 finding across database-integrity."
765
+ },
766
+ {
767
+ "file": "config/routes.rb",
768
+ "score": 7,
769
+ "finding_count": 1,
770
+ "churn": 0,
771
+ "changed": false,
772
+ "categories": [
773
+ "routing"
774
+ ],
775
+ "summary": "Inherited file with 1 finding across routing."
776
+ },
777
+ {
778
+ "file": "config/initializers/strong_migrations.rb",
779
+ "score": 1,
780
+ "finding_count": 1,
781
+ "churn": 0,
782
+ "changed": false,
783
+ "categories": [
784
+ "migration-safety"
785
+ ],
786
+ "summary": "Inherited file with 1 finding across migration-safety."
787
+ }
788
+ ],
789
+ "tool_runs": [
790
+ {
791
+ "name": "rubocop",
792
+ "available": true,
793
+ "skipped": false,
794
+ "command": "../../fake_bin/rubocop",
795
+ "exit_status": 1,
796
+ "duration_ms": 50,
797
+ "metadata": {}
798
+ },
799
+ {
800
+ "name": "brakeman",
801
+ "available": true,
802
+ "skipped": false,
803
+ "command": "../../fake_bin/brakeman",
804
+ "exit_status": 3,
805
+ "duration_ms": 50,
806
+ "metadata": {}
807
+ },
808
+ {
809
+ "name": "bundler_audit",
810
+ "available": true,
811
+ "skipped": false,
812
+ "command": "../../fake_bin/bundle-audit",
813
+ "exit_status": 1,
814
+ "duration_ms": 50,
815
+ "metadata": {}
816
+ },
817
+ {
818
+ "name": "zeitwerk",
819
+ "available": true,
820
+ "skipped": false,
821
+ "command": "../../fake_bin/rails zeitwerk:check",
822
+ "exit_status": 0,
823
+ "duration_ms": 50,
824
+ "metadata": {}
825
+ },
826
+ {
827
+ "name": "reek",
828
+ "available": true,
829
+ "skipped": false,
830
+ "command": "../../fake_bin/reek",
831
+ "exit_status": 2,
832
+ "duration_ms": 50,
833
+ "metadata": {}
834
+ },
835
+ {
836
+ "name": "strong_migrations",
837
+ "available": true,
838
+ "skipped": false,
839
+ "exit_status": 0,
840
+ "duration_ms": 50,
841
+ "metadata": {
842
+ "coverage": "strong_migrations gem detected",
843
+ "initializer_present": false
844
+ }
845
+ },
846
+ {
847
+ "name": "rails_checks",
848
+ "available": true,
849
+ "skipped": false,
850
+ "exit_status": 0,
851
+ "duration_ms": 50,
852
+ "metadata": {
853
+ "checks": [
854
+ "indexes",
855
+ "uniqueness",
856
+ "routes",
857
+ "views",
858
+ "size",
859
+ "todos",
860
+ "tests",
861
+ "coverage-gaps"
862
+ ]
863
+ }
864
+ },
865
+ {
866
+ "name": "test_runner",
867
+ "available": true,
868
+ "skipped": false,
869
+ "command": "../../fake_bin/passing_tests",
870
+ "exit_status": 0,
871
+ "duration_ms": 50,
872
+ "metadata": {}
873
+ },
874
+ {
875
+ "name": "test_coverage",
876
+ "available": true,
877
+ "skipped": false,
878
+ "exit_status": 0,
879
+ "duration_ms": 1,
880
+ "metadata": {
881
+ "coverage": {
882
+ "available": true,
883
+ "status": "below_threshold",
884
+ "source": "simplecov",
885
+ "report_path": "coverage/.resultset.json",
886
+ "line_percent": 48.0,
887
+ "branch_percent": 50.0,
888
+ "covered_lines": 12,
889
+ "missed_lines": 13,
890
+ "total_lines": 25,
891
+ "covered_branches": 1,
892
+ "missed_branches": 1,
893
+ "total_branches": 2,
894
+ "thresholds": {
895
+ "line": 90.0,
896
+ "file_line": 80.0,
897
+ "branch": null
898
+ },
899
+ "top_files": [
900
+ {
901
+ "file": "app/controllers/posts_controller.rb",
902
+ "line_percent": 22.22,
903
+ "covered_lines": 2,
904
+ "missed_lines": 7,
905
+ "total_lines": 9,
906
+ "below_threshold": true
907
+ },
908
+ {
909
+ "file": "app/models/post.rb",
910
+ "line_percent": 44.44,
911
+ "covered_lines": 4,
912
+ "missed_lines": 5,
913
+ "total_lines": 9,
914
+ "branch_percent": 50.0,
915
+ "covered_branches": 1,
916
+ "missed_branches": 1,
917
+ "total_branches": 2,
918
+ "below_threshold": true
919
+ },
920
+ {
921
+ "file": "app/models/user.rb",
922
+ "line_percent": 80.0,
923
+ "covered_lines": 4,
924
+ "missed_lines": 1,
925
+ "total_lines": 5,
926
+ "below_threshold": false
927
+ },
928
+ {
929
+ "file": "app/controllers/health_controller.rb",
930
+ "line_percent": 100.0,
931
+ "covered_lines": 2,
932
+ "missed_lines": 0,
933
+ "total_lines": 2,
934
+ "below_threshold": false
935
+ }
936
+ ],
937
+ "low_file_count": 2,
938
+ "changed_files_below_threshold": [],
939
+ "metadata": {}
940
+ }
941
+ }
942
+ },
943
+ {
944
+ "name": "flog",
945
+ "available": true,
946
+ "skipped": false,
947
+ "command": "../../fake_bin/flog",
948
+ "exit_status": 0,
949
+ "duration_ms": 50,
950
+ "metadata": {}
951
+ },
952
+ {
953
+ "name": "flay",
954
+ "available": true,
955
+ "skipped": false,
956
+ "command": "../../fake_bin/flay",
957
+ "exit_status": 0,
958
+ "duration_ms": 50,
959
+ "metadata": {}
960
+ },
961
+ {
962
+ "name": "dependency_freshness",
963
+ "available": true,
964
+ "skipped": false,
965
+ "command": "bundle outdated --parseable",
966
+ "exit_status": 1,
967
+ "duration_ms": 50,
968
+ "metadata": {}
969
+ }
970
+ ]
971
+ }