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 +4 -4
- data/README.md +45 -12
- data/lib/railsforge/cli.rb +24 -5
- data/lib/railsforge/cli_minimal.rb +6 -4
- data/lib/railsforge/diff.rb +57 -0
- data/lib/railsforge/formatter.rb +56 -30
- data/lib/railsforge/loader.rb +1 -0
- data/lib/railsforge/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b004345e61120afc6f11432c72baeddb6981aee6e504d50b5eedf5b0334f71d6
|
|
4
|
+
data.tar.gz: 51bae87bc37e99da845f89ea3e9af014a2d9b6d85128417223978e5ffef14958
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
122
|
-
|
|
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
|
data/lib/railsforge/cli.rb
CHANGED
|
@@ -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
|
|
29
|
-
railsforge doctor --format=json
|
|
30
|
-
railsforge
|
|
31
|
-
railsforge --
|
|
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
|
|
40
|
-
railsforge doctor --format=json
|
|
41
|
-
railsforge
|
|
42
|
-
railsforge --
|
|
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
|
data/lib/railsforge/formatter.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Formatter — RailsForge v2 output renderer
|
|
2
2
|
#
|
|
3
|
-
# Responsibility: take raw
|
|
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
|
|
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
|
|
102
|
+
lines << colorize("✓ No new issues", :green)
|
|
52
103
|
lines << ""
|
|
53
104
|
end
|
|
54
105
|
|
|
55
|
-
lines
|
|
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",
|
data/lib/railsforge/loader.rb
CHANGED
data/lib/railsforge/version.rb
CHANGED
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.
|
|
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
|