rails-doctor 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33475f8da3160d83e02d37cb0854981fee269452d9d6ec18ee40d928e8355c82
4
- data.tar.gz: e21a6029b7fb00a519455fbca1a94f6f529e3dafc551c309629f6e017e752f3f
3
+ metadata.gz: f6d4db14cadca4a355a495d14a6e005e9d34dd7c4b9e338c686f23614e66c02b
4
+ data.tar.gz: 47fa1a18bcdb0f011e371aebfbb18d75cd8b24e6c6a08f91e2f7ea74359b47a7
5
5
  SHA512:
6
- metadata.gz: d6b1afe0d88e480f68941c9e375324c67003775c2f3836bb4864f91270a8413074aea3e0f793f66ce758d0ddbb543d12392d3e10bf8f7b6de7bcf04f7a460e38
7
- data.tar.gz: b6de0130d08f3467d220895d1d7f0d1a136e06a649c543fd396bffbee93d4313341813934e60a5c9b7f7cc25ca72f6e2d46c73d1744a5250e2b9337f0c936d5a
6
+ metadata.gz: 4acb580364cf3d04eeef44162668ada9642264c918f8a3918d14c8fad47708e1c0003ade9e419b25c9804701cf48cdc47efbc78c403d67c9745569c147224cfd
7
+ data.tar.gz: 431f3efe7bb6d6a1e82ecdc2993f85776f79ffe8ebb3175e88bf1fd54728366dff9ba778b87d0b8fa965a3ce6a6c6ee1a66aacca0ba3a86b9b1308ea597abbc9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - Improved Rails schema parsing for inline indexes, single-column indexes, scoped uniqueness validations, partial unique indexes, and string-backed foreign keys.
6
+ - Added normalized tool-run statuses and report notes so nonzero advisory tool exits are easier to interpret.
7
+ - Added deeper `rails-doctor init --profile deep` setup guidance with exact companion-tool install commands.
8
+ - Bumped the JSON output schema to `1.2` for tool-run status metadata.
9
+
3
10
  ## 0.1.0
4
11
 
5
12
  - Initial Rails Doctor CLI and gem scaffold.
data/README.md CHANGED
@@ -1,3 +1,7 @@
1
+ <p align="center">
2
+ <img src="site/assets/rails-doctor-logo.png" alt="" width="220">
3
+ </p>
4
+
1
5
  # Rails Doctor
2
6
 
3
7
  [![CI](https://github.com/joshsaintjacque/rails-doctor/actions/workflows/ci.yml/badge.svg)](https://github.com/joshsaintjacque/rails-doctor/actions/workflows/ci.yml)
@@ -1,6 +1,6 @@
1
1
  # Output Schema
2
2
 
3
- `rails-doctor --format json` emits schema version `1.1`.
3
+ `rails-doctor --format json` emits schema version `1.2`.
4
4
 
5
5
  Top-level fields:
6
6
 
@@ -54,4 +54,18 @@ Each finding contains:
54
54
 
55
55
  Agents should treat `agent_instruction` and `suggested_commands` as guidance, not permission to mutate code. Mutation only occurs through an explicit agent workflow outside JSON report generation.
56
56
 
57
+ Each tool run contains:
58
+
59
+ - `name`
60
+ - `available`
61
+ - `skipped`
62
+ - `command`
63
+ - `exit_status`
64
+ - `duration_ms`
65
+ - `skip_reason`
66
+ - `status`
67
+ - `metadata`
68
+
69
+ `status` is one of `completed`, `completed_with_findings`, `completed_with_filtered_findings`, `adapter_failed`, `skipped`, or `nonzero_exit`. Raw tool exit codes are preserved in `exit_status`; `status` describes Rails Doctor's normalized interpretation for the current report. Nonzero exits may still be `completed` when the adapter parsed no actionable findings. `metadata.status_explanation` explains nonzero exits that need interpretation, and `metadata.parsed_finding_count` records how many findings the adapter produced before report filtering or deduplication.
70
+
57
71
  `metadata` includes detected Ruby and Rails versions, current branch, optional base ref, and changed files used for changed-file scoring.
data/examples/report.html CHANGED
@@ -103,6 +103,12 @@
103
103
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
104
104
  font-size: 13px;
105
105
  }
106
+ .tool-note {
107
+ padding: 14px 0;
108
+ border-top: 1px solid var(--line);
109
+ }
110
+ .tool-note strong { display: block; margin-bottom: 4px; }
111
+ .tool-meta { color: var(--muted); font-size: 13px; }
106
112
  details { border-top: 1px solid var(--line); padding: 14px 0; }
107
113
  summary { cursor: pointer; font-weight: 700; }
108
114
  pre { white-space: pre-wrap; overflow: auto; background: #171717; color: #f7f5f0; padding: 16px; }
@@ -141,7 +147,7 @@
141
147
  <div class="score-ring">
142
148
  <div class="score-label">Overall health</div>
143
149
  <div class="score-number">0</div>
144
- <div>Changed files: <strong>100</strong> · Confidence: <strong>100%</strong></div>
150
+ <div>Changed files: <strong>86</strong> · Confidence: <strong>100%</strong></div>
145
151
  </div>
146
152
  </div>
147
153
  </header>
@@ -152,7 +158,7 @@
152
158
  <div class="metric"><span>Medium</span><strong>10</strong></div>
153
159
  <div class="metric"><span>Coverage</span><strong>48.00%</strong></div>
154
160
  <div class="metric"><span>Skipped</span><strong>0</strong></div>
155
- <div class="metric"><span>Duration</span><strong>600ms</strong></div>
161
+ <div class="metric"><span>Duration</span><strong>1497ms</strong></div>
156
162
  </section>
157
163
 
158
164
  <section class="section">
@@ -326,6 +332,43 @@
326
332
  - medium: Update the deprecated API usage and add a regression test when behavior could change.</pre>
327
333
  </section>
328
334
 
335
+ <section class="section">
336
+ <h2>Tool Run Notes</h2>
337
+
338
+
339
+ <div class="tool-note">
340
+ <strong>rubocop</strong>
341
+ <div class="tool-meta">Status: completed_with_findings · Exit: 1</div>
342
+ <p>The tool exited nonzero and Rails Doctor normalized actionable findings from its output.</p>
343
+ </div>
344
+
345
+ <div class="tool-note">
346
+ <strong>brakeman</strong>
347
+ <div class="tool-meta">Status: completed_with_findings · Exit: 3</div>
348
+ <p>The tool exited nonzero and Rails Doctor normalized actionable findings from its output.</p>
349
+ </div>
350
+
351
+ <div class="tool-note">
352
+ <strong>bundler_audit</strong>
353
+ <div class="tool-meta">Status: completed_with_findings · Exit: 1</div>
354
+ <p>The tool exited nonzero and Rails Doctor normalized actionable findings from its output.</p>
355
+ </div>
356
+
357
+ <div class="tool-note">
358
+ <strong>reek</strong>
359
+ <div class="tool-meta">Status: completed_with_findings · Exit: 2</div>
360
+ <p>The tool exited nonzero and Rails Doctor normalized actionable findings from its output.</p>
361
+ </div>
362
+
363
+ <div class="tool-note">
364
+ <strong>dependency_freshness</strong>
365
+ <div class="tool-meta">Status: completed_with_findings · Exit: 1</div>
366
+ <p>The tool exited nonzero and Rails Doctor normalized actionable findings from its output.</p>
367
+ </div>
368
+
369
+
370
+ </section>
371
+
329
372
  <section class="section">
330
373
  <h2>Findings</h2>
331
374
  <div class="filters" role="toolbar" aria-label="Finding filters">
@@ -536,7 +579,71 @@
536
579
  <td>dependency_freshness</td>
537
580
  <td>dependency-freshness</td>
538
581
  <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>
582
+ <td><strong>brakeman appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
583
+ </tr>
584
+
585
+ <tr data-severity="low">
586
+ <td><span class="chip low">low</span></td>
587
+ <td>dependency_freshness</td>
588
+ <td>dependency-freshness</td>
589
+ <td>Gemfile.lock</td>
590
+ <td><strong>bundler-audit appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
591
+ </tr>
592
+
593
+ <tr data-severity="low">
594
+ <td><span class="chip low">low</span></td>
595
+ <td>dependency_freshness</td>
596
+ <td>dependency-freshness</td>
597
+ <td>Gemfile.lock</td>
598
+ <td><strong>flay appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
599
+ </tr>
600
+
601
+ <tr data-severity="low">
602
+ <td><span class="chip low">low</span></td>
603
+ <td>dependency_freshness</td>
604
+ <td>dependency-freshness</td>
605
+ <td>Gemfile.lock</td>
606
+ <td><strong>flog appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
607
+ </tr>
608
+
609
+ <tr data-severity="low">
610
+ <td><span class="chip low">low</span></td>
611
+ <td>dependency_freshness</td>
612
+ <td>dependency-freshness</td>
613
+ <td>Gemfile.lock</td>
614
+ <td><strong>prosopite appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
615
+ </tr>
616
+
617
+ <tr data-severity="low">
618
+ <td><span class="chip low">low</span></td>
619
+ <td>dependency_freshness</td>
620
+ <td>dependency-freshness</td>
621
+ <td>Gemfile.lock</td>
622
+ <td><strong>rails appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
623
+ </tr>
624
+
625
+ <tr data-severity="low">
626
+ <td><span class="chip low">low</span></td>
627
+ <td>dependency_freshness</td>
628
+ <td>dependency-freshness</td>
629
+ <td>Gemfile.lock</td>
630
+ <td><strong>rubocop appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
631
+ </tr>
632
+
633
+ <tr data-severity="low">
634
+ <td><span class="chip low">low</span></td>
635
+ <td>dependency_freshness</td>
636
+ <td>dependency-freshness</td>
637
+ <td>Gemfile.lock</td>
638
+ <td><strong>rubocop-rails appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
639
+ </tr>
640
+
641
+ <tr data-severity="low">
642
+ <td><span class="chip low">low</span></td>
643
+ <td>dependency_freshness</td>
644
+ <td>dependency-freshness</td>
645
+ <td>Gemfile.lock</td>
646
+ <td><strong>strong_migrations appears to be outdated</strong><br>Review the update risk and update in a separate dependency-focused change.</td>
540
647
  </tr>
541
648
 
542
649
  </tbody>
@@ -569,11 +676,11 @@
569
676
 
570
677
  <tr>
571
678
  <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>
679
+ <td>29</td>
680
+ <td>10</td>
681
+ <td>3</td>
682
+ <td>true</td>
683
+ <td>Changed file with 10 findings across dependency-security, dependency-freshness.</td>
577
684
  </tr>
578
685
 
579
686
  <tr>
@@ -628,51 +735,51 @@
628
735
 
629
736
 
630
737
  <details>
631
- <summary>rubocop</summary>
738
+ <summary>rubocop · completed_with_findings · exit 1</summary>
632
739
  <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
740
  \n</pre>
634
741
  </details>
635
742
 
636
743
  <details>
637
- <summary>brakeman</summary>
744
+ <summary>brakeman · completed_with_findings · exit 3</summary>
638
745
  <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
746
  \n</pre>
640
747
  </details>
641
748
 
642
749
  <details>
643
- <summary>bundler_audit</summary>
750
+ <summary>bundler_audit · completed_with_findings · exit 1</summary>
644
751
  <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
752
  \n</pre>
646
753
  </details>
647
754
 
648
755
  <details>
649
- <summary>zeitwerk</summary>
756
+ <summary>zeitwerk · completed · exit 0</summary>
650
757
  <pre>Hold on, I am eager loading the application.
651
758
  All is good!
652
759
  \n</pre>
653
760
  </details>
654
761
 
655
762
  <details>
656
- <summary>reek</summary>
763
+ <summary>reek · completed_with_findings · exit 2</summary>
657
764
  <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
765
  \n</pre>
659
766
  </details>
660
767
 
661
768
  <details>
662
- <summary>test_runner</summary>
769
+ <summary>test_runner · completed_with_findings · exit 0</summary>
663
770
  <pre>DEPRECATION WARNING: old API is deprecated
664
771
  Prosopite: N+1 queries detected for Post =&gt; [:user]
665
772
  \n</pre>
666
773
  </details>
667
774
 
668
775
  <details>
669
- <summary>flog</summary>
776
+ <summary>flog · completed_with_findings · exit 0</summary>
670
777
  <pre> 32.5 Post#publish! app/models/post.rb:4
671
778
  \n</pre>
672
779
  </details>
673
780
 
674
781
  <details>
675
- <summary>flay</summary>
782
+ <summary>flay · completed_with_findings · exit 0</summary>
676
783
  <pre>Similar code found in :iter (mass = 42)
677
784
  app/models/post.rb:4
678
785
  app/models/user.rb:2
@@ -680,14 +787,25 @@ Prosopite: N+1 queries detected for Post =&gt; [:user]
680
787
  </details>
681
788
 
682
789
  <details>
683
- <summary>dependency_freshness</summary>
684
- <pre>rack (newest 3.0.0, installed 2.2.0)
685
- \n</pre>
790
+ <summary>dependency_freshness · completed_with_findings · exit 1</summary>
791
+ <pre>
792
+ brakeman (newest 8.0.4, installed 7.0.0)
793
+ bundler-audit (newest 0.9.3, installed 0.9.0)
794
+ flay (newest 2.14.4, installed 2.13.3)
795
+ flog (newest 4.9.4, installed 4.8.0)
796
+ prosopite (newest 2.2.0, installed 1.4.0)
797
+ rails (newest 8.1.3, installed 8.0.0, requested ~&gt; 8.0)
798
+ rubocop (newest 1.87.0, installed 1.70.0)
799
+ rubocop-rails (newest 2.35.3, installed 2.28.0)
800
+ strong_migrations (newest 2.8.0, installed 2.0.0)
801
+ \nFetching gem metadata from https://rubygems.org/..........
802
+ Resolving dependencies...
803
+ </pre>
686
804
  </details>
687
805
 
688
806
  </section>
689
807
  </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>
808
+ <script type="application/json" id="rails-doctor-data">{"schema_version":"1.2","generated_at":"2026-06-02T00:44:54-07:00","project_root":"/Users/joshs/.codex/worktrees/86c9/rails-doctor/test/fixtures/rails_apps/sample_app","profile":"deep","metadata":{"rails_app":true,"ruby_version":"3.4.1","rails_version":"8.0.0","branch":"codex/rails-doctor-feedback","changed_files":["CHANGELOG.md","Gemfile.lock","examples/report.html","examples/report.json","examples/report.md","lib/rails_doctor/version.rb"]},"summary":{"profile":"deep","duration_ms":1497,"finding_count":32,"severity_counts":{"medium":10,"critical":1,"high":10,"low":11},"skipped_tools":[],"score":{"overall":0,"changed_files":86,"confidence":100,"penalties":[{"id":"rd-umvcmo2jx3ej","severity":"medium","file":"app/models/post.rb","message":"Rails/TimeZone: Use Time.current instead of Time.now.","penalty":3.0},{"id":"rd-1012b90h4skt","severity":"critical","file":"app/models/post.rb","message":"SQL Injection: Possible SQL injection","penalty":15.0},{"id":"rd-euhneannuy8g","severity":"high","file":"Gemfile.lock","message":"rack: Example vulnerability","penalty":7.0},{"id":"rd-m3d20263f351","severity":"medium","file":"app/models/post.rb","message":"TooManyStatements: has the smell of too many statements","penalty":3.0},{"id":"rd-jqcjizfjy1bj","severity":"low","file":"config/initializers/strong_migrations.rb","message":"strong_migrations is installed but no initializer was found","penalty":0.75},{"id":"rd-vxswmqgnex1","severity":"high","file":"db/schema.rb","message":"posts.user_id has no index","penalty":7.0},{"id":"rd-eeg09srhctrg","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-viv39xpaou4q","severity":"high","file":"config/routes.rb","message":"Routes reference missing ghosts_controller.rb","penalty":7.0},{"id":"rd-35hpmof55rrc","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#create","penalty":7.0},{"id":"rd-43ws5mpdqpau","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#destroy","penalty":7.0},{"id":"rd-jq42qh002niq","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#edit","penalty":7.0},{"id":"rd-3zze50gr5bhz","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#new","penalty":7.0},{"id":"rd-83ah00od1xdk","severity":"medium","file":"app/controllers/posts_controller.rb","message":"posts#show has no matching template or explicit response","penalty":2.25},{"id":"rd-21pwdm41rqyx","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#update","penalty":7.0},{"id":"rd-dnpx28helhlp","severity":"low","file":"app/controllers/posts_controller.rb","message":"posts#archive is not referenced by simple route analysis","penalty":0.4},{"id":"rd-4q2ytrqtdk55","severity":"medium","file":"app/models/post.rb","message":"1 TODO/FIXME/HACK marker in 10 lines","penalty":2.25},{"id":"rd-fdpj1pz9wcjs","severity":"medium","file":null,"message":"DEPRECATION WARNING: old API is deprecated","penalty":2.25},{"id":"rd-sniv3674shv","severity":"high","file":null,"message":"Prosopite: N+1 queries detected for Post =\u003e [:user]","penalty":5.25},{"id":"rd-b8krrpfcahfo","severity":"medium","file":null,"message":"Line coverage 48.00% is below the 90.00% threshold","penalty":3.0},{"id":"rd-yjazqvzca79b","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-mqmakuk5efa3","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-5j6a8yb13paz","severity":"medium","file":"app/models/post.rb","message":"High complexity score 32.5 for Post#publish!","penalty":2.25},{"id":"rd-2h34p5sc8thu","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-m1ly6yvtaw5i","severity":"low","file":"Gemfile.lock","message":"brakeman appears to be outdated","penalty":0.75},{"id":"rd-5c65cl20d67s","severity":"low","file":"Gemfile.lock","message":"bundler-audit appears to be outdated","penalty":0.75},{"id":"rd-rcxs7mw0ony6","severity":"low","file":"Gemfile.lock","message":"flay appears to be outdated","penalty":0.75},{"id":"rd-molifhqxqz88","severity":"low","file":"Gemfile.lock","message":"flog appears to be outdated","penalty":0.75},{"id":"rd-6hx5xwejxac7","severity":"low","file":"Gemfile.lock","message":"prosopite appears to be outdated","penalty":0.75},{"id":"rd-196gusw92scx","severity":"low","file":"Gemfile.lock","message":"rails appears to be outdated","penalty":0.75},{"id":"rd-3wa8qni8z72s","severity":"low","file":"Gemfile.lock","message":"rubocop appears to be outdated","penalty":0.75},{"id":"rd-q5605256tdqy","severity":"low","file":"Gemfile.lock","message":"rubocop-rails appears to be outdated","penalty":0.75},{"id":"rd-qslk943qk2yb","severity":"low","file":"Gemfile.lock","message":"strong_migrations appears to be outdated","penalty":0.75}],"top_score_movers":[{"id":"rd-1012b90h4skt","severity":"critical","file":"app/models/post.rb","message":"SQL Injection: Possible SQL injection","penalty":15.0},{"id":"rd-euhneannuy8g","severity":"high","file":"Gemfile.lock","message":"rack: Example vulnerability","penalty":7.0},{"id":"rd-21pwdm41rqyx","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#update","penalty":7.0},{"id":"rd-3zze50gr5bhz","severity":"high","file":"app/controllers/posts_controller.rb","message":"Route points to missing posts#new","penalty":7.0},{"id":"rd-vxswmqgnex1","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-umvcmo2jx3ej","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-1012b90h4skt","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-euhneannuy8g","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-m3d20263f351","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-jqcjizfjy1bj","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-vxswmqgnex1","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-eeg09srhctrg","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-viv39xpaou4q","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-35hpmof55rrc","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-43ws5mpdqpau","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-jq42qh002niq","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-3zze50gr5bhz","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-83ah00od1xdk","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-21pwdm41rqyx","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-dnpx28helhlp","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-4q2ytrqtdk55","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-fdpj1pz9wcjs","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-sniv3674shv","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-b8krrpfcahfo","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-yjazqvzca79b","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-mqmakuk5efa3","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-5j6a8yb13paz","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-2h34p5sc8thu","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-m1ly6yvtaw5i","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"brakeman 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 brakeman conservatively and run the full test suite.","suggested_commands":["bundle update brakeman"],"metadata":{}},{"id":"rd-5c65cl20d67s","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"bundler-audit 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 bundler-audit conservatively and run the full test suite.","suggested_commands":["bundle update bundler-audit"],"metadata":{}},{"id":"rd-rcxs7mw0ony6","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"flay 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 flay conservatively and run the full test suite.","suggested_commands":["bundle update flay"],"metadata":{}},{"id":"rd-molifhqxqz88","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"flog 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 flog conservatively and run the full test suite.","suggested_commands":["bundle update flog"],"metadata":{}},{"id":"rd-6hx5xwejxac7","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"prosopite 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 prosopite conservatively and run the full test suite.","suggested_commands":["bundle update prosopite"],"metadata":{}},{"id":"rd-196gusw92scx","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"rails 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 rails conservatively and run the full test suite.","suggested_commands":["bundle update rails"],"metadata":{}},{"id":"rd-3wa8qni8z72s","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"rubocop 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 rubocop conservatively and run the full test suite.","suggested_commands":["bundle update rubocop"],"metadata":{}},{"id":"rd-q5605256tdqy","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"rubocop-rails 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 rubocop-rails conservatively and run the full test suite.","suggested_commands":["bundle update rubocop-rails"],"metadata":{}},{"id":"rd-qslk943qk2yb","severity":"low","category":"dependency-freshness","tool":"dependency_freshness","file":"Gemfile.lock","confidence":"medium","message":"strong_migrations 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 strong_migrations conservatively and run the full test suite.","suggested_commands":["bundle update strong_migrations"],"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":29,"finding_count":10,"churn":3,"changed":true,"categories":["dependency-freshness","dependency-security"],"summary":"Changed file with 10 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":35,"status":"completed_with_findings","metadata":{"parsed_finding_count":1,"status_explanation":"The tool exited nonzero and Rails Doctor normalized actionable findings from its output."}},{"name":"brakeman","available":true,"skipped":false,"command":"../../fake_bin/brakeman","exit_status":3,"duration_ms":33,"status":"completed_with_findings","metadata":{"parsed_finding_count":1,"status_explanation":"The tool exited nonzero and Rails Doctor normalized actionable findings from its output."}},{"name":"bundler_audit","available":true,"skipped":false,"command":"../../fake_bin/bundle-audit","exit_status":1,"duration_ms":34,"status":"completed_with_findings","metadata":{"parsed_finding_count":1,"status_explanation":"The tool exited nonzero and Rails Doctor normalized actionable findings from its output."}},{"name":"zeitwerk","available":true,"skipped":false,"command":"../../fake_bin/rails zeitwerk:check","exit_status":0,"duration_ms":30,"status":"completed","metadata":{"parsed_finding_count":0}},{"name":"reek","available":true,"skipped":false,"command":"../../fake_bin/reek","exit_status":2,"duration_ms":33,"status":"completed_with_findings","metadata":{"parsed_finding_count":1,"status_explanation":"The tool exited nonzero and Rails Doctor normalized actionable findings from its output."}},{"name":"strong_migrations","available":true,"skipped":false,"exit_status":0,"status":"completed_with_findings","metadata":{"coverage":"strong_migrations gem detected","initializer_present":false,"parsed_finding_count":1}},{"name":"rails_checks","available":true,"skipped":false,"exit_status":0,"status":"completed_with_findings","metadata":{"checks":["indexes","uniqueness","routes","views","size","todos","tests","coverage-gaps"],"parsed_finding_count":11}},{"name":"test_runner","available":true,"skipped":false,"command":"../../fake_bin/passing_tests","exit_status":0,"duration_ms":30,"status":"completed_with_findings","metadata":{"parsed_finding_count":2}},{"name":"test_coverage","available":true,"skipped":false,"exit_status":0,"duration_ms":0,"status":"completed_with_findings","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":{}},"parsed_finding_count":3}},{"name":"flog","available":true,"skipped":false,"command":"../../fake_bin/flog","exit_status":0,"duration_ms":30,"status":"completed_with_findings","metadata":{"parsed_finding_count":1}},{"name":"flay","available":true,"skipped":false,"command":"../../fake_bin/flay","exit_status":0,"duration_ms":30,"status":"completed_with_findings","metadata":{"parsed_finding_count":1}},{"name":"dependency_freshness","available":true,"skipped":false,"command":"bundle outdated --parseable","exit_status":1,"duration_ms":1222,"status":"completed_with_findings","metadata":{"parsed_finding_count":9,"status_explanation":"The tool exited nonzero and Rails Doctor normalized actionable findings from its output."}}]}</script>
691
809
  <script>
692
810
  document.querySelectorAll("[data-filter]").forEach((button) => {
693
811
  button.addEventListener("click", () => {