ui_guardrails 1.0.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +302 -0
- data/doc/A11Y.md +87 -0
- data/doc/LOOKBOOK.md +52 -0
- data/doc/PRD.md +145 -0
- data/doc/PUBLISHING.md +98 -0
- data/doc/ROADMAP.md +158 -0
- data/doc/UPSTREAM-snap_diff-issue-draft.md +63 -0
- data/doc/VISUAL-DIFF.md +135 -0
- data/lib/guardrails/a11y_audit.rb +249 -0
- data/lib/guardrails/a11y_deep.rb +119 -0
- data/lib/guardrails/audit/auto_fixer.rb +155 -0
- data/lib/guardrails/audit/markdown_writer.rb +218 -0
- data/lib/guardrails/audit.rb +472 -0
- data/lib/guardrails/class_itis.rb +196 -0
- data/lib/guardrails/configuration.rb +101 -0
- data/lib/guardrails/cross_codebase_patterns.rb +242 -0
- data/lib/guardrails/erb_parser.rb +91 -0
- data/lib/guardrails/hex_normalizer.rb +47 -0
- data/lib/guardrails/icons.rb +233 -0
- data/lib/guardrails/init/config_writer.rb +101 -0
- data/lib/guardrails/init/media_query_scaffolder.rb +60 -0
- data/lib/guardrails/init/prompter.rb +60 -0
- data/lib/guardrails/init/stack_detector.rb +108 -0
- data/lib/guardrails/init.rb +115 -0
- data/lib/guardrails/lookbook/component_report.rb +78 -0
- data/lib/guardrails/lookbook/panel_registration.rb +93 -0
- data/lib/guardrails/lookbook/views/lookbook_panels/_guardrails.html.erb +44 -0
- data/lib/guardrails/partial_similarity.rb +231 -0
- data/lib/guardrails/railtie.rb +23 -0
- data/lib/guardrails/stimulus_audit.rb +118 -0
- data/lib/guardrails/token_matcher.rb +40 -0
- data/lib/guardrails/tokens/tailwind_config_parser.rb +140 -0
- data/lib/guardrails/tokens.rb +256 -0
- data/lib/guardrails/version.rb +5 -0
- data/lib/guardrails/view_component_audit.rb +150 -0
- data/lib/guardrails/visual_diff/snap_diff.rb +81 -0
- data/lib/guardrails/visual_diff.rb +117 -0
- data/lib/guardrails.rb +14 -0
- data/lib/tasks/guardrails.rake +176 -0
- data/lib/ui_guardrails.rb +9 -0
- metadata +145 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require_relative "configuration"
|
|
5
|
+
|
|
6
|
+
module Guardrails
|
|
7
|
+
# Consumes screenshot-diff tool output and folds findings into the
|
|
8
|
+
# unified audit report. Same playbook as `A11yDeep` for axe — parser
|
|
9
|
+
# only, no Capybara / Chromium / Playwright runtime deps. Users keep
|
|
10
|
+
# their existing visual-regression toolchain; Guardrails provides the
|
|
11
|
+
# merge + report + exit-code contract.
|
|
12
|
+
#
|
|
13
|
+
# Adapter selection comes from `Guardrails.configuration.visual_diff.adapter`
|
|
14
|
+
# (default `:snap_diff`, the Rails-native baselines-in-git workflow).
|
|
15
|
+
# BackstopJS support tracked in issue #15.
|
|
16
|
+
class VisualDiff
|
|
17
|
+
# Normalized across every adapter. Fields adapter-specific shapes
|
|
18
|
+
# don't supply are nil — consumers should not assume presence.
|
|
19
|
+
#
|
|
20
|
+
# Defined before the adapter requires so the constant exists by the
|
|
21
|
+
# time `lib/guardrails/visual_diff/snap_diff.rb` loads.
|
|
22
|
+
Finding = Struct.new(
|
|
23
|
+
:scenario, # human label, e.g. "checkout/cart" or "homepage_desktop"
|
|
24
|
+
:viewport, # "desktop" / "mobile" / nil if not applicable
|
|
25
|
+
:mismatch_ratio, # 0.0..1.0 when the adapter emits one; nil for boolean adapters (snap_diff)
|
|
26
|
+
:baseline_path, # relative path to the reference image
|
|
27
|
+
:current_path, # relative path to the actual screenshot
|
|
28
|
+
:diff_path, # relative path to the diff image, nil when no diff exists
|
|
29
|
+
:url, # optional — set by adapters whose scenarios carry URLs (BackstopJS)
|
|
30
|
+
:selector, # optional — same
|
|
31
|
+
keyword_init: true
|
|
32
|
+
) do
|
|
33
|
+
def to_h
|
|
34
|
+
super
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def initialize(root:, output: $stdout,
|
|
39
|
+
adapter: nil, threshold: nil)
|
|
40
|
+
@root = Pathname(root)
|
|
41
|
+
@output = output
|
|
42
|
+
cfg = Guardrails.configuration.visual_diff
|
|
43
|
+
# Coerce constructor inputs the same way Configuration setters
|
|
44
|
+
# do, so `VisualDiff.new(adapter: "snap_diff", threshold: "0.1")`
|
|
45
|
+
# works the same as `Guardrails.configure { |c| c.visual_diff.
|
|
46
|
+
# adapter = "snap_diff"; c.visual_diff.threshold = "0.1" }`.
|
|
47
|
+
@adapter_name = coerce_adapter(adapter) || cfg.adapter
|
|
48
|
+
@threshold = threshold.nil? ? cfg.threshold : Float(threshold)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def run
|
|
52
|
+
findings = collect
|
|
53
|
+
print_report(findings)
|
|
54
|
+
findings
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Returns true when any finding's mismatch_ratio exceeds the
|
|
58
|
+
# threshold. Adapters that don't emit a ratio (snap_diff: presence
|
|
59
|
+
# of a .diff.png is binary) report nil, which we treat as
|
|
60
|
+
# "unconditional fail" — those findings always fail.
|
|
61
|
+
def any_failing?(findings)
|
|
62
|
+
findings.any? { |f| failing?(f) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def collect
|
|
66
|
+
adapter.collect
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def adapter
|
|
72
|
+
case @adapter_name
|
|
73
|
+
when :snap_diff
|
|
74
|
+
SnapDiff.new(
|
|
75
|
+
root: @root,
|
|
76
|
+
dir: Guardrails.configuration.visual_diff.snap_diff_dir
|
|
77
|
+
)
|
|
78
|
+
else
|
|
79
|
+
raise ArgumentError, "Unknown visual_diff adapter: #{@adapter_name.inspect}"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def coerce_adapter(value)
|
|
84
|
+
return nil if value.nil?
|
|
85
|
+
|
|
86
|
+
value.to_sym
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failing?(finding)
|
|
90
|
+
ratio = finding.mismatch_ratio
|
|
91
|
+
return true if ratio.nil?
|
|
92
|
+
|
|
93
|
+
ratio > @threshold
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def print_report(findings)
|
|
97
|
+
return if findings.empty?
|
|
98
|
+
|
|
99
|
+
@output.puts ""
|
|
100
|
+
@output.puts "Guardrails visual diff: #{findings.length} finding#{'s' if findings.length != 1} " \
|
|
101
|
+
"(adapter: #{@adapter_name}, threshold: #{@threshold})"
|
|
102
|
+
|
|
103
|
+
findings.each do |f|
|
|
104
|
+
ratio_label = f.mismatch_ratio.nil? ? "[diff present]" : "[#{(f.mismatch_ratio * 100).round(2)}% mismatch]"
|
|
105
|
+
suffix = f.viewport ? " (#{f.viewport})" : ""
|
|
106
|
+
@output.puts ""
|
|
107
|
+
@output.puts " #{ratio_label} #{f.scenario}#{suffix}"
|
|
108
|
+
@output.puts " baseline: #{f.baseline_path}" if f.baseline_path
|
|
109
|
+
@output.puts " diff: #{f.diff_path}" if f.diff_path
|
|
110
|
+
@output.puts " url: #{f.url}" if f.url
|
|
111
|
+
@output.puts " selector: #{f.selector}" if f.selector
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
require_relative "visual_diff/snap_diff"
|
data/lib/guardrails.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "guardrails/version"
|
|
4
|
+
# Configuration carries the `Guardrails.configure { |c| ... }` block
|
|
5
|
+
# users place in `config/initializers/guardrails.rb`. Loaded eagerly
|
|
6
|
+
# at `require "guardrails"` time so the initializer doesn't NoMethodError
|
|
7
|
+
# on first use — caught by the local-build verification of 1.0.0
|
|
8
|
+
# before the third publish attempt.
|
|
9
|
+
require_relative "guardrails/configuration"
|
|
10
|
+
require_relative "guardrails/railtie" if defined?(Rails::Railtie)
|
|
11
|
+
|
|
12
|
+
module Guardrails
|
|
13
|
+
class Error < StandardError; end
|
|
14
|
+
end
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
namespace :guardrails do
|
|
4
|
+
desc "Initialize Guardrails configuration and analyze stylesheet stack (FORCE=1 to overwrite existing config)"
|
|
5
|
+
task :init do
|
|
6
|
+
require "guardrails/init"
|
|
7
|
+
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
8
|
+
force = %w[1 true yes].include?(ENV["FORCE"]&.downcase)
|
|
9
|
+
Guardrails::Init.new(root: root, force: force).run
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "Audit views and components for UI drift (SUGGEST=1, APPLY=1, FORMAT=json)"
|
|
13
|
+
task :audit do
|
|
14
|
+
require "guardrails/audit"
|
|
15
|
+
require "guardrails/stimulus_audit"
|
|
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"
|
|
24
|
+
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
25
|
+
axe_json_path = ENV["AXE_JSON"]
|
|
26
|
+
|
|
27
|
+
# Visual-diff is opt-in (baselines need deliberate setup). Enabled
|
|
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"]
|
|
53
|
+
|
|
54
|
+
if format == :json
|
|
55
|
+
# Run sub-audits silently so the only thing printed to stdout is one
|
|
56
|
+
# JSON document. Audit's own JSON output goes through @output, so we
|
|
57
|
+
# capture it instead of re-emitting.
|
|
58
|
+
sink = StringIO.new
|
|
59
|
+
violations = Guardrails::Audit.new(
|
|
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 || []
|
|
75
|
+
|
|
76
|
+
require "json"
|
|
77
|
+
payload = {
|
|
78
|
+
summary: {
|
|
79
|
+
violations: violations.length,
|
|
80
|
+
stimulus_orphaned: stimulus.orphaned.length,
|
|
81
|
+
stimulus_dead: stimulus.dead.length,
|
|
82
|
+
similar_partials: similarity.length,
|
|
83
|
+
missing_previews: vc.missing_previews.length,
|
|
84
|
+
orphan_slots: vc.orphan_slots.length,
|
|
85
|
+
a11y: a11y.length,
|
|
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)
|
|
109
|
+
else
|
|
110
|
+
violations = Guardrails::Audit.new(
|
|
111
|
+
root: root, suggest: suggest, apply: apply, format: :text
|
|
112
|
+
).run
|
|
113
|
+
stimulus = Guardrails::StimulusAudit.new(root: root).run
|
|
114
|
+
similarity = Guardrails::PartialSimilarity.new(**similarity_opts).run
|
|
115
|
+
vc = Guardrails::ViewComponentAudit.new(root: root).run
|
|
116
|
+
a11y = Guardrails::A11yAudit.new(root: root).run
|
|
117
|
+
patterns = Guardrails::CrossCodebasePatterns.new(**pattern_opts).run
|
|
118
|
+
classitis = Guardrails::ClassItis.new(**classitis_opts).run
|
|
119
|
+
a11y_deep_runner = axe_json_path ? Guardrails::A11yDeep.new(input: axe_json_path) : nil
|
|
120
|
+
a11y_deep = a11y_deep_runner&.run || []
|
|
121
|
+
visual_diff_runner = visual_diff_on ? Guardrails::VisualDiff.new(root: root) : nil
|
|
122
|
+
visual_diff = visual_diff_runner&.run || []
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Deep a11y findings only fail the audit when their impact crosses
|
|
126
|
+
# the configured threshold (default: any impact fails, same as
|
|
127
|
+
# static a11y). When AXE_JSON is not set, a11y_deep is [] and the
|
|
128
|
+
# check is a no-op. Same logic for visual_diff: opt-in via
|
|
129
|
+
# VISUAL_DIFF=1 / Configuration; threshold gates failure.
|
|
130
|
+
a11y_deep_failing = a11y_deep_runner&.any_failing?(a11y_deep) || false
|
|
131
|
+
visual_diff_failing = visual_diff_runner&.any_failing?(visual_diff) || false
|
|
132
|
+
exit 1 if violations.any? || stimulus.violations? || similarity.any? || vc.violations? || a11y.any? || a11y_deep_failing || visual_diff_failing
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
desc "Parse axe-core JSON output and report deep a11y findings (AXE_JSON=path/to/axe.json)"
|
|
136
|
+
task :"a11y:deep" do
|
|
137
|
+
require "guardrails/a11y_deep"
|
|
138
|
+
path = ENV["AXE_JSON"] or abort "Set AXE_JSON=path/to/axe.json (output from `npx @axe-core/cli ... --save`)"
|
|
139
|
+
runner = Guardrails::A11yDeep.new(input: path)
|
|
140
|
+
findings = runner.run
|
|
141
|
+
exit 1 if runner.any_failing?(findings)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
desc "Consume screenshot-diff output and report visual regressions (VISUAL_DIFF_DIR=..., VISUAL_DIFF_THRESHOLD=0.0)"
|
|
145
|
+
task :"visual:deep" do
|
|
146
|
+
require "guardrails/visual_diff"
|
|
147
|
+
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
148
|
+
# The standalone task implies the user is opting in regardless of
|
|
149
|
+
# Configuration; flip enabled on so a no-config sidecar run works.
|
|
150
|
+
Guardrails.configure { |c| c.visual_diff.enabled = true }
|
|
151
|
+
# Same blank-env guard as the main audit task — see note there.
|
|
152
|
+
if (dir = ENV["VISUAL_DIFF_DIR"]) && !dir.strip.empty?
|
|
153
|
+
Guardrails.configure { |c| c.visual_diff.snap_diff_dir = dir.strip }
|
|
154
|
+
end
|
|
155
|
+
if (thr = ENV["VISUAL_DIFF_THRESHOLD"]) && !thr.strip.empty?
|
|
156
|
+
Guardrails.configure { |c| c.visual_diff.threshold = thr.strip }
|
|
157
|
+
end
|
|
158
|
+
runner = Guardrails::VisualDiff.new(root: root)
|
|
159
|
+
findings = runner.run
|
|
160
|
+
exit 1 if runner.any_failing?(findings)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
desc "Generate SVG icon sprite and audit icon usage"
|
|
164
|
+
task :icons do
|
|
165
|
+
require "guardrails/icons"
|
|
166
|
+
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
167
|
+
Guardrails::Icons.new(root: root).run
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
desc "Audit design tokens and report drift"
|
|
171
|
+
task :tokens do
|
|
172
|
+
require "guardrails/tokens"
|
|
173
|
+
root = defined?(Rails) ? Rails.root : Pathname(Dir.pwd)
|
|
174
|
+
Guardrails::Tokens.new(root: root).run
|
|
175
|
+
end
|
|
176
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Shim so that `gem "ui_guardrails"` in a Gemfile and the
|
|
4
|
+
# default Bundler/Rails auto-require pattern (`require "meticulous_
|
|
5
|
+
# guardrails"`) reach the canonical entry point at lib/guardrails.rb.
|
|
6
|
+
# The Ruby module is `Guardrails`; only the gem package name on
|
|
7
|
+
# rubygems.org is namespaced under the org. See the CHANGELOG entry
|
|
8
|
+
# for 1.0.0 for the rename rationale.
|
|
9
|
+
require_relative "guardrails"
|
metadata
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: ui_guardrails
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- John Athayde
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-05-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: railties
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '7.1'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '7.1'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: herb
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0.10'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0.10'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.13'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.13'
|
|
69
|
+
description: 'Opinionated auditing and enforcement for design-system consistency:
|
|
70
|
+
component inventory, icon sprites, type scale, and color token management.'
|
|
71
|
+
email:
|
|
72
|
+
- jmpa@meticulous.com
|
|
73
|
+
executables: []
|
|
74
|
+
extensions: []
|
|
75
|
+
extra_rdoc_files: []
|
|
76
|
+
files:
|
|
77
|
+
- LICENSE
|
|
78
|
+
- README.md
|
|
79
|
+
- doc/A11Y.md
|
|
80
|
+
- doc/LOOKBOOK.md
|
|
81
|
+
- doc/PRD.md
|
|
82
|
+
- doc/PUBLISHING.md
|
|
83
|
+
- doc/ROADMAP.md
|
|
84
|
+
- doc/UPSTREAM-snap_diff-issue-draft.md
|
|
85
|
+
- doc/VISUAL-DIFF.md
|
|
86
|
+
- lib/guardrails.rb
|
|
87
|
+
- lib/guardrails/a11y_audit.rb
|
|
88
|
+
- lib/guardrails/a11y_deep.rb
|
|
89
|
+
- lib/guardrails/audit.rb
|
|
90
|
+
- lib/guardrails/audit/auto_fixer.rb
|
|
91
|
+
- lib/guardrails/audit/markdown_writer.rb
|
|
92
|
+
- lib/guardrails/class_itis.rb
|
|
93
|
+
- lib/guardrails/configuration.rb
|
|
94
|
+
- lib/guardrails/cross_codebase_patterns.rb
|
|
95
|
+
- lib/guardrails/erb_parser.rb
|
|
96
|
+
- lib/guardrails/hex_normalizer.rb
|
|
97
|
+
- lib/guardrails/icons.rb
|
|
98
|
+
- lib/guardrails/init.rb
|
|
99
|
+
- lib/guardrails/init/config_writer.rb
|
|
100
|
+
- lib/guardrails/init/media_query_scaffolder.rb
|
|
101
|
+
- lib/guardrails/init/prompter.rb
|
|
102
|
+
- lib/guardrails/init/stack_detector.rb
|
|
103
|
+
- lib/guardrails/lookbook/component_report.rb
|
|
104
|
+
- lib/guardrails/lookbook/panel_registration.rb
|
|
105
|
+
- lib/guardrails/lookbook/views/lookbook_panels/_guardrails.html.erb
|
|
106
|
+
- lib/guardrails/partial_similarity.rb
|
|
107
|
+
- lib/guardrails/railtie.rb
|
|
108
|
+
- lib/guardrails/stimulus_audit.rb
|
|
109
|
+
- lib/guardrails/token_matcher.rb
|
|
110
|
+
- lib/guardrails/tokens.rb
|
|
111
|
+
- lib/guardrails/tokens/tailwind_config_parser.rb
|
|
112
|
+
- lib/guardrails/version.rb
|
|
113
|
+
- lib/guardrails/view_component_audit.rb
|
|
114
|
+
- lib/guardrails/visual_diff.rb
|
|
115
|
+
- lib/guardrails/visual_diff/snap_diff.rb
|
|
116
|
+
- lib/tasks/guardrails.rake
|
|
117
|
+
- lib/ui_guardrails.rb
|
|
118
|
+
homepage: https://github.com/meticulous/guardrails
|
|
119
|
+
licenses:
|
|
120
|
+
- MIT
|
|
121
|
+
metadata:
|
|
122
|
+
homepage_uri: https://github.com/meticulous/guardrails
|
|
123
|
+
source_code_uri: https://github.com/meticulous/guardrails
|
|
124
|
+
changelog_uri: https://github.com/meticulous/guardrails/blob/main/CHANGELOG.md
|
|
125
|
+
rubygems_mfa_required: 'true'
|
|
126
|
+
post_install_message:
|
|
127
|
+
rdoc_options: []
|
|
128
|
+
require_paths:
|
|
129
|
+
- lib
|
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
131
|
+
requirements:
|
|
132
|
+
- - ">="
|
|
133
|
+
- !ruby/object:Gem::Version
|
|
134
|
+
version: 3.2.0
|
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
136
|
+
requirements:
|
|
137
|
+
- - ">="
|
|
138
|
+
- !ruby/object:Gem::Version
|
|
139
|
+
version: '0'
|
|
140
|
+
requirements: []
|
|
141
|
+
rubygems_version: 3.4.19
|
|
142
|
+
signing_key:
|
|
143
|
+
specification_version: 4
|
|
144
|
+
summary: Rails toolset for preventing UI drift in AI-assisted applications.
|
|
145
|
+
test_files: []
|