ui_guardrails 1.2.0 → 1.3.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 +20 -2
- data/lib/guardrails/a11y_audit.rb +26 -3
- data/lib/guardrails/a11y_deep.rb +27 -2
- data/lib/guardrails/audit.rb +31 -3
- data/lib/guardrails/class_itis.rb +30 -3
- data/lib/guardrails/cross_codebase_patterns.rb +30 -3
- data/lib/guardrails/partial_similarity.rb +30 -3
- data/lib/guardrails/report/finding.rb +63 -0
- data/lib/guardrails/report/html/template.html.erb +237 -0
- data/lib/guardrails/report/html.rb +104 -0
- data/lib/guardrails/report/run.rb +224 -0
- data/lib/guardrails/stimulus_audit.rb +88 -12
- data/lib/guardrails/tui/editor.rb +71 -0
- data/lib/guardrails/tui/keys.rb +101 -0
- data/lib/guardrails/tui/screen.rb +361 -0
- data/lib/guardrails/tui/state.rb +356 -0
- data/lib/guardrails/tui.rb +154 -0
- data/lib/guardrails/version.rb +1 -1
- data/lib/guardrails/view_component_audit.rb +57 -7
- data/lib/guardrails/visual_diff.rb +36 -6
- data/lib/tasks/guardrails.rake +41 -192
- metadata +10 -1
data/lib/tasks/guardrails.rake
CHANGED
|
@@ -9,214 +9,63 @@ namespace :guardrails do
|
|
|
9
9
|
Guardrails::Init.new(root: root, force: force).run
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
desc "Audit views and components for UI drift (SUGGEST=1, APPLY=1, FORMAT=json)"
|
|
12
|
+
desc "Audit views and components for UI drift (SUGGEST=1, APPLY=1, FORMAT=json|html)"
|
|
13
13
|
task :audit do
|
|
14
|
-
require "guardrails/
|
|
15
|
-
require "guardrails/
|
|
16
|
-
require "guardrails/partial_similarity"
|
|
17
|
-
require "guardrails/view_component_audit"
|
|
18
|
-
require "guardrails/a11y_audit"
|
|
19
|
-
require "guardrails/cross_codebase_patterns"
|
|
20
|
-
require "guardrails/class_itis"
|
|
21
|
-
require "guardrails/a11y_deep"
|
|
22
|
-
require "guardrails/visual_diff"
|
|
23
|
-
require "stringio"
|
|
14
|
+
require "guardrails/report/run"
|
|
15
|
+
require "guardrails/report/style"
|
|
24
16
|
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
25
|
-
axe_json_path = ENV["AXE_JSON"]
|
|
26
17
|
|
|
27
|
-
|
|
28
|
-
# when either VISUAL_DIFF=1 is set in the env (sidecar mode) or
|
|
29
|
-
# Guardrails.configuration.visual_diff.enabled was flipped on by a
|
|
30
|
-
# Rails initializer (embedded mode). Env overrides Configuration.
|
|
31
|
-
visual_diff_env = %w[1 true yes].include?(ENV["VISUAL_DIFF"]&.downcase)
|
|
32
|
-
visual_diff_on = visual_diff_env || Guardrails.configuration.visual_diff.enabled
|
|
33
|
-
# Strip + reject blank env values — an empty VISUAL_DIFF_DIR would
|
|
34
|
-
# otherwise be applied as snap_diff_dir = "" and glob from the repo
|
|
35
|
-
# root (potentially scanning the whole tree).
|
|
36
|
-
if (dir = ENV["VISUAL_DIFF_DIR"]) && !dir.strip.empty?
|
|
37
|
-
Guardrails.configure { |c| c.visual_diff.snap_diff_dir = dir.strip }
|
|
38
|
-
end
|
|
39
|
-
if (thr = ENV["VISUAL_DIFF_THRESHOLD"]) && !thr.strip.empty?
|
|
40
|
-
Guardrails.configure { |c| c.visual_diff.threshold = thr.strip }
|
|
41
|
-
end
|
|
42
|
-
suggest = %w[1 true yes].include?(ENV["SUGGEST"]&.downcase)
|
|
43
|
-
apply = %w[1 true yes].include?(ENV["APPLY"]&.downcase)
|
|
44
|
-
format = ENV["FORMAT"]&.downcase == "json" ? :json : :text
|
|
45
|
-
similarity_opts = { root: root }
|
|
46
|
-
similarity_opts[:threshold] = ENV["SIMILARITY_THRESHOLD"].to_f if ENV["SIMILARITY_THRESHOLD"]
|
|
47
|
-
pattern_opts = { root: root }
|
|
48
|
-
pattern_opts[:min_size] = ENV["PATTERN_MIN_SIZE"].to_i if ENV["PATTERN_MIN_SIZE"]
|
|
49
|
-
pattern_opts[:min_occurrences] = ENV["PATTERN_MIN_OCCURRENCES"].to_i if ENV["PATTERN_MIN_OCCURRENCES"]
|
|
50
|
-
classitis_opts = { root: root }
|
|
51
|
-
classitis_opts[:min_classes] = ENV["CLASSITIS_MIN_CLASSES"].to_i if ENV["CLASSITIS_MIN_CLASSES"]
|
|
52
|
-
classitis_opts[:min_occurrences] = ENV["CLASSITIS_MIN_OCCURRENCES"].to_i if ENV["CLASSITIS_MIN_OCCURRENCES"]
|
|
18
|
+
format = { "json" => :json, "html" => :html }.fetch(ENV["FORMAT"].to_s.downcase, :text)
|
|
53
19
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
root: root, output: sink, suggest: suggest, apply: apply, format: :text
|
|
61
|
-
).run
|
|
62
|
-
stimulus = Guardrails::StimulusAudit.new(root: root, output: sink).run
|
|
63
|
-
similarity_opts[:output] = sink
|
|
64
|
-
similarity = Guardrails::PartialSimilarity.new(**similarity_opts).run
|
|
65
|
-
vc = Guardrails::ViewComponentAudit.new(root: root, output: sink).run
|
|
66
|
-
a11y = Guardrails::A11yAudit.new(root: root, output: sink).run
|
|
67
|
-
pattern_opts[:output] = sink
|
|
68
|
-
patterns = Guardrails::CrossCodebasePatterns.new(**pattern_opts).run
|
|
69
|
-
classitis_opts[:output] = sink
|
|
70
|
-
classitis = Guardrails::ClassItis.new(**classitis_opts).run
|
|
71
|
-
a11y_deep_runner = axe_json_path ? Guardrails::A11yDeep.new(input: axe_json_path, output: sink) : nil
|
|
72
|
-
a11y_deep = a11y_deep_runner&.run || []
|
|
73
|
-
visual_diff_runner = visual_diff_on ? Guardrails::VisualDiff.new(root: root, output: sink) : nil
|
|
74
|
-
visual_diff = visual_diff_runner&.run || []
|
|
20
|
+
# One Style bound to the real terminal, threaded through every
|
|
21
|
+
# detector so the whole report tracks the same TTY/NO_COLOR signal
|
|
22
|
+
# (see Report::Run). JSON and HTML modes discard the report body,
|
|
23
|
+
# so they run unstyled.
|
|
24
|
+
report_style = format == :text ? Guardrails::Report::Style.new(io: $stdout) : nil
|
|
25
|
+
run = Guardrails::Report::Run.from_env(root: root, style: report_style).call
|
|
75
26
|
|
|
27
|
+
if format == :json
|
|
76
28
|
require "json"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
a11y_deep: a11y_deep.length,
|
|
87
|
-
patterns: patterns.length,
|
|
88
|
-
classitis: classitis.length,
|
|
89
|
-
visual_diff: visual_diff.length
|
|
90
|
-
},
|
|
91
|
-
violations: violations.map(&:to_h),
|
|
92
|
-
stimulus: { orphaned: stimulus.orphaned, dead: stimulus.dead },
|
|
93
|
-
similar_partials: similarity.map(&:to_h),
|
|
94
|
-
view_components: {
|
|
95
|
-
missing_previews: vc.missing_previews,
|
|
96
|
-
orphan_slots: vc.orphan_slots.map(&:to_h)
|
|
97
|
-
},
|
|
98
|
-
a11y: a11y.map(&:to_h),
|
|
99
|
-
a11y_deep: a11y_deep.map(&:to_h),
|
|
100
|
-
patterns: patterns.map { |p|
|
|
101
|
-
{ fingerprint: p.fingerprint, shape: p.shape, size: p.size, count: p.count, occurrences: p.occurrences.map(&:to_h) }
|
|
102
|
-
},
|
|
103
|
-
classitis: classitis.map { |c|
|
|
104
|
-
{ tag: c.tag, classes: c.classes, count: c.count, occurrences: c.occurrences.map(&:to_h) }
|
|
105
|
-
},
|
|
106
|
-
visual_diff: visual_diff.map(&:to_h)
|
|
107
|
-
}
|
|
108
|
-
$stdout.puts JSON.pretty_generate(payload)
|
|
29
|
+
$stdout.puts JSON.pretty_generate(run.to_h)
|
|
30
|
+
elsif format == :html
|
|
31
|
+
# One self-contained file — for a browser, a PR attachment, or a
|
|
32
|
+
# CI artifact. OUTPUT= overrides the default tmp/ location.
|
|
33
|
+
require "guardrails/report/html"
|
|
34
|
+
output = ENV["OUTPUT"].to_s.strip
|
|
35
|
+
path = Guardrails::Report::Html.new(categories: run.categories, root: root)
|
|
36
|
+
.write(output.empty? ? Guardrails::Report::Html::DEFAULT_PATH : output)
|
|
37
|
+
$stdout.puts "Guardrails audit: #{run.findings.length} findings → #{path}"
|
|
109
38
|
else
|
|
110
|
-
# Run each sub-audit against an in-memory sink first so we can
|
|
111
|
-
# render the top-of-report summary before the per-category
|
|
112
|
-
# details (the summary needs every detector's count). Then
|
|
113
|
-
# write the sink + per-category sections out together.
|
|
114
|
-
#
|
|
115
|
-
# Important: detectors write into `sink` (StringIO) but make
|
|
116
|
-
# ANSI-color decisions against `$stdout`. If we let each
|
|
117
|
-
# detector instantiate its own Style bound to the sink, every
|
|
118
|
-
# color() call would be false (StringIO isn't a TTY) and the
|
|
119
|
-
# body of the report would print plain even on a real terminal,
|
|
120
|
-
# while the top/bottom summary printed straight to $stdout
|
|
121
|
-
# would still be colored. Pre-build one Style here and thread
|
|
122
|
-
# it through every detector so the whole report tracks the
|
|
123
|
-
# real terminal's TTY/NO_COLOR signal consistently.
|
|
124
|
-
require "guardrails/report/summary"
|
|
125
|
-
require "guardrails/report/style"
|
|
126
|
-
sink = StringIO.new
|
|
127
|
-
report_style = Guardrails::Report::Style.new(io: $stdout)
|
|
128
|
-
violations = Guardrails::Audit.new(
|
|
129
|
-
root: root, output: sink, suggest: suggest, apply: apply, format: :text,
|
|
130
|
-
style: report_style
|
|
131
|
-
).run
|
|
132
|
-
stimulus = Guardrails::StimulusAudit.new(root: root, output: sink, style: report_style).run
|
|
133
|
-
similarity_opts[:output] = sink
|
|
134
|
-
similarity_opts[:style] = report_style
|
|
135
|
-
similarity = Guardrails::PartialSimilarity.new(**similarity_opts).run
|
|
136
|
-
vc = Guardrails::ViewComponentAudit.new(root: root, output: sink, style: report_style).run
|
|
137
|
-
a11y = Guardrails::A11yAudit.new(root: root, output: sink, style: report_style).run
|
|
138
|
-
pattern_opts[:output] = sink
|
|
139
|
-
pattern_opts[:style] = report_style
|
|
140
|
-
patterns = Guardrails::CrossCodebasePatterns.new(**pattern_opts).run
|
|
141
|
-
classitis_opts[:output] = sink
|
|
142
|
-
classitis_opts[:style] = report_style
|
|
143
|
-
classitis = Guardrails::ClassItis.new(**classitis_opts).run
|
|
144
|
-
a11y_deep_runner = axe_json_path ? Guardrails::A11yDeep.new(input: axe_json_path, output: sink, style: report_style) : nil
|
|
145
|
-
a11y_deep = a11y_deep_runner&.run || []
|
|
146
|
-
visual_diff_runner = visual_diff_on ? Guardrails::VisualDiff.new(root: root, output: sink, style: report_style) : nil
|
|
147
|
-
visual_diff = visual_diff_runner&.run || []
|
|
148
|
-
|
|
149
|
-
summary_entries = [
|
|
150
|
-
Guardrails::Report::Summary::Entry.new(
|
|
151
|
-
category: "raw_color", count: violations.count { |v| v.type == :raw_color },
|
|
152
|
-
severity: :error, auto_fix: true
|
|
153
|
-
),
|
|
154
|
-
Guardrails::Report::Summary::Entry.new(
|
|
155
|
-
category: "tailwind_arbitrary", count: violations.count { |v| v.type == :tailwind_arbitrary },
|
|
156
|
-
severity: :error, auto_fix: true
|
|
157
|
-
),
|
|
158
|
-
Guardrails::Report::Summary::Entry.new(
|
|
159
|
-
category: "inline_style", count: violations.count { |v| v.type == :inline_style },
|
|
160
|
-
severity: :warning
|
|
161
|
-
),
|
|
162
|
-
Guardrails::Report::Summary::Entry.new(
|
|
163
|
-
category: "helper_recommended", count: violations.count { |v| v.type == :helper_recommended },
|
|
164
|
-
severity: :warning
|
|
165
|
-
),
|
|
166
|
-
Guardrails::Report::Summary::Entry.new(
|
|
167
|
-
category: "a11y (static)", count: a11y.length, severity: :error
|
|
168
|
-
),
|
|
169
|
-
Guardrails::Report::Summary::Entry.new(
|
|
170
|
-
category: "a11y (deep)", count: a11y_deep.length, severity: :error
|
|
171
|
-
),
|
|
172
|
-
Guardrails::Report::Summary::Entry.new(
|
|
173
|
-
category: "stimulus orphaned", count: stimulus.orphaned.length, severity: :warning
|
|
174
|
-
),
|
|
175
|
-
Guardrails::Report::Summary::Entry.new(
|
|
176
|
-
category: "stimulus dead", count: stimulus.dead.length, severity: :warning
|
|
177
|
-
),
|
|
178
|
-
Guardrails::Report::Summary::Entry.new(
|
|
179
|
-
category: "missing previews", count: vc.missing_previews.length, severity: :warning
|
|
180
|
-
),
|
|
181
|
-
Guardrails::Report::Summary::Entry.new(
|
|
182
|
-
category: "orphan slots", count: vc.orphan_slots.length, severity: :warning
|
|
183
|
-
),
|
|
184
|
-
Guardrails::Report::Summary::Entry.new(
|
|
185
|
-
category: "visual diff", count: visual_diff.length, severity: :error
|
|
186
|
-
),
|
|
187
|
-
Guardrails::Report::Summary::Entry.new(
|
|
188
|
-
category: "similar partials", count: similarity.length, severity: :suggestion,
|
|
189
|
-
unit: "pairs", action: "consider deduplicating"
|
|
190
|
-
),
|
|
191
|
-
Guardrails::Report::Summary::Entry.new(
|
|
192
|
-
category: "cross-codebase patterns", count: patterns.length, severity: :suggestion,
|
|
193
|
-
unit: "candidates", action: "consider extracting partials"
|
|
194
|
-
),
|
|
195
|
-
Guardrails::Report::Summary::Entry.new(
|
|
196
|
-
category: "class-itis", count: classitis.length, severity: :suggestion,
|
|
197
|
-
unit: "clusters", action: "consider extracting component / @apply"
|
|
198
|
-
)
|
|
199
|
-
]
|
|
200
|
-
|
|
201
39
|
# Render the summary twice — once at the top so the reader
|
|
202
40
|
# knows what to expect, once at the bottom as a recap so they
|
|
203
41
|
# don't have to scroll back up after the per-detector dump.
|
|
204
42
|
# On a long output (Patchvault has 981 findings) the bottom
|
|
205
43
|
# recap is the load-bearing one.
|
|
206
|
-
summary = Guardrails::Report::Summary.new(entries: summary_entries, output: $stdout, style: report_style)
|
|
44
|
+
summary = Guardrails::Report::Summary.new(entries: run.summary_entries, output: $stdout, style: report_style)
|
|
207
45
|
summary.render
|
|
208
|
-
$stdout.write
|
|
46
|
+
$stdout.write run.body
|
|
209
47
|
summary.render(recap: true)
|
|
210
48
|
end
|
|
211
49
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
50
|
+
exit 1 if run.failing?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
desc "Browse audit findings interactively (same env options as guardrails:audit)"
|
|
54
|
+
task :tui do
|
|
55
|
+
require "guardrails/report/run"
|
|
56
|
+
require "guardrails/tui"
|
|
57
|
+
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
58
|
+
|
|
59
|
+
# APPLY / SUGGEST are deliberately not honored here: browsing
|
|
60
|
+
# shouldn't rewrite views or drop markdown files, least of all
|
|
61
|
+
# again on every `r` re-run.
|
|
62
|
+
env = ENV.to_h.reject { |key, _| %w[APPLY SUGGEST].include?(key) }
|
|
63
|
+
runner = -> { Guardrails::Report::Run.from_env(root: root, env: env).call }
|
|
64
|
+
begin
|
|
65
|
+
Guardrails::TUI.new(runner: runner, root: root).start
|
|
66
|
+
rescue Guardrails::TUI::NotInteractive => e
|
|
67
|
+
abort e.message
|
|
68
|
+
end
|
|
220
69
|
end
|
|
221
70
|
|
|
222
71
|
desc "Parse axe-core JSON output and report deep a11y findings (AXE_JSON=path/to/axe.json)"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ui_guardrails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- John Athayde
|
|
@@ -105,12 +105,21 @@ files:
|
|
|
105
105
|
- lib/guardrails/lookbook/views/lookbook_panels/_guardrails.html.erb
|
|
106
106
|
- lib/guardrails/partial_similarity.rb
|
|
107
107
|
- lib/guardrails/railtie.rb
|
|
108
|
+
- lib/guardrails/report/finding.rb
|
|
109
|
+
- lib/guardrails/report/html.rb
|
|
110
|
+
- lib/guardrails/report/html/template.html.erb
|
|
111
|
+
- lib/guardrails/report/run.rb
|
|
108
112
|
- lib/guardrails/report/style.rb
|
|
109
113
|
- lib/guardrails/report/summary.rb
|
|
110
114
|
- lib/guardrails/stimulus_audit.rb
|
|
111
115
|
- lib/guardrails/token_matcher.rb
|
|
112
116
|
- lib/guardrails/tokens.rb
|
|
113
117
|
- lib/guardrails/tokens/tailwind_config_parser.rb
|
|
118
|
+
- lib/guardrails/tui.rb
|
|
119
|
+
- lib/guardrails/tui/editor.rb
|
|
120
|
+
- lib/guardrails/tui/keys.rb
|
|
121
|
+
- lib/guardrails/tui/screen.rb
|
|
122
|
+
- lib/guardrails/tui/state.rb
|
|
114
123
|
- lib/guardrails/version.rb
|
|
115
124
|
- lib/guardrails/view_component_audit.rb
|
|
116
125
|
- lib/guardrails/visual_diff.rb
|