railsforge 2.0.0 → 2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2c8f6efda41c0957fd4a4675fbd5e310fb825c2a26001c4e5af5abb02af97347
4
- data.tar.gz: 91f9e9baf1d073a0bbe8cd7db41972504183281bc27945aaee77185f02229718
3
+ metadata.gz: b004345e61120afc6f11432c72baeddb6981aee6e504d50b5eedf5b0334f71d6
4
+ data.tar.gz: 51bae87bc37e99da845f89ea3e9af014a2d9b6d85128417223978e5ffef14958
5
5
  SHA512:
6
- metadata.gz: bb84ad0cbdbdd3b154c3e4b4dbeb6acbe909f5d67fa8511b2002dccd18756436531e33d508e247335a5ed2603fc5b6654b84136cc2c73d1e75bd3308e797e6f4
7
- data.tar.gz: 2705a473ed8ae7323f5800c75e7a5ecb0a077ff07beef6707e830c409d24737a02ce2310241b421127f2ff9cb64bbd583096599a2291c6762246c2077f29bfaf
6
+ metadata.gz: 1bc07580544ae7d06679dd03986cf6268b7598ead40a495a54ff8b2f8d0da1730871d01738ec419455998992f85cbda3619dbd60ccf77fb6ea73cce53ce7179c
7
+ data.tar.gz: 7b1b7c639d4da0968d6db7ea4e2a9d98a28398186f93ffeaddab9ee86c60c9481296d72ab64c0c9d552e795f635de3d146664e7cc6415d9c919db16bfe68aa18
data/README.md CHANGED
@@ -107,19 +107,60 @@ Returns structured output for CI pipelines and automated tooling:
107
107
  "summary": {
108
108
  "total": 9,
109
109
  "by_severity": {
110
+ "critical": 0,
110
111
  "high": 2,
111
112
  "medium": 4,
112
- "low": 3
113
+ "low": 3,
114
+ "info": 0
113
115
  }
114
116
  }
115
117
  }
116
118
  ```
117
119
 
118
- **Fail a CI build when score drops below threshold:**
120
+ All five severity keys are always present, even when zero. Score is always an integer. Output is deterministic.
121
+
122
+ ---
123
+
124
+ ## CI Usage
125
+
126
+ RailsForge provides signal. CI decides consequences.
119
127
 
120
128
  ```bash
121
- score=$(railsforge doctor --format=json | jq '.score')
122
- [ "$score" -ge 70 ] || exit 1
129
+ railsforge doctor --format=json | jq '.score'
130
+ ```
131
+
132
+ RailsForge always exits `0` on a successful run. Your CI script owns the pass/fail logic.
133
+
134
+ ### GitHub Actions
135
+
136
+ ```yaml
137
+ name: RailsForge Health Check
138
+
139
+ on: [push, pull_request]
140
+
141
+ jobs:
142
+ railsforge:
143
+ runs-on: ubuntu-latest
144
+
145
+ steps:
146
+ - uses: actions/checkout@v3
147
+
148
+ - name: Setup Ruby
149
+ uses: ruby/setup-ruby@v1
150
+ with:
151
+ ruby-version: 3.2
152
+
153
+ - name: Install RailsForge
154
+ run: gem install railsforge
155
+
156
+ - name: Run RailsForge
157
+ run: |
158
+ score=$(railsforge doctor --format=json | jq '.score')
159
+ echo "Score: $score"
160
+ if [ "$score" -lt 80 ]; then
161
+ echo "RailsForge score below threshold"
162
+ exit 1
163
+ fi
123
164
  ```
124
165
 
125
166
  ---
@@ -134,14 +175,6 @@ score=$(railsforge doctor --format=json | jq '.score')
134
175
 
135
176
  ---
136
177
 
137
- ## What RailsForge Is Not
138
-
139
- RailsForge is not a generator, scaffolding system, or DevOps platform.
140
-
141
- It does not create architecture. It diagnoses existing architecture.
142
-
143
- ---
144
-
145
178
  ## Installation
146
179
 
147
180
  ```bash
@@ -1,5 +1,5 @@
1
1
  # CLI — RailsForge v2 command dispatcher
2
- # Supported commands: doctor
2
+ # Supported commands: doctor, diff
3
3
 
4
4
  module RailsForge
5
5
  class CLI
@@ -9,6 +9,8 @@ module RailsForge
9
9
  case command
10
10
  when "doctor"
11
11
  handle_doctor_command(args)
12
+ when "diff"
13
+ handle_diff_command(args)
12
14
  when "--version", "-v"
13
15
  puts "RailsForge #{VERSION}"
14
16
  when nil, "--help", "-h"
@@ -25,10 +27,11 @@ module RailsForge
25
27
  RailsForge — Rails application health diagnostics
26
28
 
27
29
  Usage:
28
- railsforge doctor Run full diagnostic scan
29
- railsforge doctor --format=json Output as JSON
30
- railsforge --version Show version
31
- railsforge --help Show this help
30
+ railsforge doctor Run full diagnostic scan
31
+ railsforge doctor --format=json Output as JSON
32
+ railsforge diff <baseline.json> <current> Show only new issues
33
+ railsforge --version Show version
34
+ railsforge --help Show this help
32
35
  HELP
33
36
  end
34
37
 
@@ -45,5 +48,21 @@ module RailsForge
45
48
  puts "Error: #{e.message}"
46
49
  exit 1
47
50
  end
51
+
52
+ def self.handle_diff_command(args)
53
+ baseline_path = args[0]
54
+ current_path = args[1]
55
+
56
+ unless baseline_path && current_path
57
+ puts "Usage: railsforge diff <baseline.json> <current.json>"
58
+ exit 1
59
+ end
60
+
61
+ new_issues = RailsForge::Diff.run(baseline_path, current_path)
62
+ puts RailsForge::Formatter.render_diff(new_issues)
63
+ rescue RailsForge::Diff::DiffError => e
64
+ puts "Error: #{e.message}"
65
+ exit 1
66
+ end
48
67
  end
49
68
  end
@@ -25,6 +25,7 @@ module RailsForge
25
25
  require_relative "analyzers/security_analyzer"
26
26
  require_relative "analyzers/performance_analyzer"
27
27
  require_relative "doctor"
28
+ require_relative "diff"
28
29
  require_relative "cli"
29
30
 
30
31
  CLI.start([command] + args)
@@ -36,10 +37,11 @@ module RailsForge
36
37
  RailsForge — Rails application health diagnostics
37
38
 
38
39
  Usage:
39
- railsforge doctor Run full diagnostic scan
40
- railsforge doctor --format=json Output as JSON
41
- railsforge --version Show version
42
- railsforge --help Show this help
40
+ railsforge doctor Run full diagnostic scan
41
+ railsforge doctor --format=json Output as JSON
42
+ railsforge diff <baseline.json> <current> Show only new issues
43
+ railsforge --version Show version
44
+ railsforge --help Show this help
43
45
  HELP
44
46
  end
45
47
  end
@@ -0,0 +1,57 @@
1
+ # Diff — compare two RailsForge JSON outputs and return only new issues.
2
+ #
3
+ # Usage:
4
+ # new_issues = RailsForge::Diff.run("baseline.json", "current.json")
5
+ #
6
+ # Two issues are considered equal when analyzer + type + file + line + message
7
+ # all match. Severity and suggestion are intentionally ignored.
8
+
9
+ require "json"
10
+
11
+ module RailsForge
12
+ class Diff
13
+ class DiffError < StandardError; end
14
+
15
+ def self.run(baseline_path, current_path)
16
+ baseline = load_issues(baseline_path)
17
+ current = load_issues(current_path)
18
+
19
+ baseline_fingerprints = baseline.map { |i| fingerprint(i) }
20
+
21
+ current.reject { |i| baseline_fingerprints.include?(fingerprint(i)) }
22
+ end
23
+
24
+ class << self
25
+ private
26
+
27
+ def load_issues(path)
28
+ raise DiffError, "File not found: #{path}" unless File.exist?(path)
29
+
30
+ raw = JSON.parse(File.read(path))
31
+ raise DiffError, "Invalid format: missing 'issues' key in #{path}" unless raw.key?("issues")
32
+
33
+ raw["issues"].map do |h|
34
+ RailsForge::Issue.new(
35
+ analyzer: h["analyzer"]&.to_sym,
36
+ type: h["type"]&.to_sym,
37
+ severity: h["severity"],
38
+ file: h["file"],
39
+ line: h["line"],
40
+ message: h["message"],
41
+ suggestion: h["suggestion"]
42
+ )
43
+ end
44
+ end
45
+
46
+ def fingerprint(issue)
47
+ [
48
+ issue.analyzer.to_s,
49
+ issue.type.to_s,
50
+ issue.file.to_s,
51
+ issue.line.to_s,
52
+ issue.message.to_s
53
+ ]
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,6 +1,6 @@
1
1
  # Formatter — RailsForge v2 output renderer
2
2
  #
3
- # Responsibility: take raw Doctor results and produce human-readable or
3
+ # Responsibility: take raw results and produce human-readable or
4
4
  # machine-readable output. No analysis, no scoring, no side effects.
5
5
  #
6
6
  # Principle: Doctor decides what is true. Formatter decides how it looks.
@@ -11,7 +11,7 @@ module RailsForge
11
11
  module Formatter
12
12
  SEVERITY_ORDER = %w[critical high medium low info].freeze
13
13
 
14
- # Human-readable text report
14
+ # Human-readable health report (from doctor).
15
15
  #
16
16
  # @param issues [Array<RailsForge::Issue>]
17
17
  # @param score [Integer] 0–100
@@ -25,7 +25,58 @@ module RailsForge
25
25
  lines << "Score: #{colorize("#{score}/100", score_color)}"
26
26
  lines << "Issues: #{issues.count} total"
27
27
  lines << ""
28
+ lines.concat(render_issues(issues))
29
+ lines.join("\n")
30
+ end
28
31
 
32
+ # Human-readable diff report (from diff command).
33
+ # Shows only new issues; no score.
34
+ #
35
+ # @param new_issues [Array<RailsForge::Issue>]
36
+ # @return [String]
37
+ def self.render_diff(new_issues)
38
+ lines = []
39
+ lines << "RailsForge Diff"
40
+ lines << "─" * 50
41
+ lines << "New issues: #{new_issues.count}"
42
+ lines << ""
43
+ lines.concat(render_issues(new_issues))
44
+ lines.join("\n")
45
+ end
46
+
47
+ # Machine-readable JSON report.
48
+ #
49
+ # @param issues [Array<RailsForge::Issue>]
50
+ # @param score [Integer] 0–100
51
+ # @param metrics [Hash]
52
+ # @return [String] pretty-printed JSON
53
+ def self.render_json(issues, score, metrics = {})
54
+ counts = issues.group_by { |i| i.severity.to_s }.transform_values(&:count)
55
+
56
+ # All severity keys always present and in defined order, even if zero.
57
+ by_severity = SEVERITY_ORDER.each_with_object({}) { |sev, h| h[sev] = counts.fetch(sev, 0) }
58
+
59
+ output = {
60
+ score: score,
61
+ issues: issues.map(&:to_h),
62
+ summary: {
63
+ total: issues.count,
64
+ by_severity: by_severity
65
+ },
66
+ metrics: metrics
67
+ }
68
+
69
+ JSON.pretty_generate(output)
70
+ end
71
+
72
+ # private helpers
73
+
74
+ # Renders severity-grouped issue list. Shared by render_text and render_diff.
75
+ #
76
+ # @param issues [Array<RailsForge::Issue>]
77
+ # @return [Array<String>] lines (no trailing newline join — caller decides)
78
+ def self.render_issues(issues)
79
+ lines = []
29
80
  grouped = issues.group_by { |i| i.severity.to_s }
30
81
  any_issues = false
31
82
 
@@ -48,38 +99,13 @@ module RailsForge
48
99
  end
49
100
 
50
101
  unless any_issues
51
- lines << colorize("✓ No issues found", :green)
102
+ lines << colorize("✓ No new issues", :green)
52
103
  lines << ""
53
104
  end
54
105
 
55
- lines.join("\n")
56
- end
57
-
58
- # Machine-readable JSON report
59
- #
60
- # @param issues [Array<RailsForge::Issue>]
61
- # @param score [Integer] 0–100
62
- # @param metrics [Hash]
63
- # @return [String] pretty-printed JSON
64
- def self.render_json(issues, score, metrics = {})
65
- by_severity = issues
66
- .group_by { |i| i.severity.to_s }
67
- .transform_values(&:count)
68
-
69
- output = {
70
- score: score,
71
- issues: issues.map(&:to_h),
72
- summary: {
73
- total: issues.count,
74
- by_severity: by_severity
75
- },
76
- metrics: metrics
77
- }
78
-
79
- JSON.pretty_generate(output)
106
+ lines
80
107
  end
81
-
82
- # private helpers
108
+ private_class_method :render_issues
83
109
 
84
110
  def self.colorize(text, color)
85
111
  codes = { green: "\e[32m", yellow: "\e[33m", red: "\e[31m",
@@ -21,6 +21,7 @@ module RailsForge
21
21
  require_relative "analyzers/security_analyzer"
22
22
  require_relative "analyzers/performance_analyzer"
23
23
  require_relative "doctor"
24
+ require_relative "diff"
24
25
  require_relative "cli"
25
26
  end
26
27
  end
@@ -1,5 +1,5 @@
1
1
  # Version module for RailsForge gem
2
2
  # Defines the current version of the gem
3
3
  module RailsForge
4
- VERSION = "2.0.0"
4
+ VERSION = "2.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railsforge
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - RailsForge Contributors
@@ -92,6 +92,7 @@ files:
92
92
  - lib/railsforge/analyzers/spec_analyzer.rb
93
93
  - lib/railsforge/cli.rb
94
94
  - lib/railsforge/cli_minimal.rb
95
+ - lib/railsforge/diff.rb
95
96
  - lib/railsforge/doctor.rb
96
97
  - lib/railsforge/formatter.rb
97
98
  - lib/railsforge/issue.rb