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.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +302 -0
  4. data/doc/A11Y.md +87 -0
  5. data/doc/LOOKBOOK.md +52 -0
  6. data/doc/PRD.md +145 -0
  7. data/doc/PUBLISHING.md +98 -0
  8. data/doc/ROADMAP.md +158 -0
  9. data/doc/UPSTREAM-snap_diff-issue-draft.md +63 -0
  10. data/doc/VISUAL-DIFF.md +135 -0
  11. data/lib/guardrails/a11y_audit.rb +249 -0
  12. data/lib/guardrails/a11y_deep.rb +119 -0
  13. data/lib/guardrails/audit/auto_fixer.rb +155 -0
  14. data/lib/guardrails/audit/markdown_writer.rb +218 -0
  15. data/lib/guardrails/audit.rb +472 -0
  16. data/lib/guardrails/class_itis.rb +196 -0
  17. data/lib/guardrails/configuration.rb +101 -0
  18. data/lib/guardrails/cross_codebase_patterns.rb +242 -0
  19. data/lib/guardrails/erb_parser.rb +91 -0
  20. data/lib/guardrails/hex_normalizer.rb +47 -0
  21. data/lib/guardrails/icons.rb +233 -0
  22. data/lib/guardrails/init/config_writer.rb +101 -0
  23. data/lib/guardrails/init/media_query_scaffolder.rb +60 -0
  24. data/lib/guardrails/init/prompter.rb +60 -0
  25. data/lib/guardrails/init/stack_detector.rb +108 -0
  26. data/lib/guardrails/init.rb +115 -0
  27. data/lib/guardrails/lookbook/component_report.rb +78 -0
  28. data/lib/guardrails/lookbook/panel_registration.rb +93 -0
  29. data/lib/guardrails/lookbook/views/lookbook_panels/_guardrails.html.erb +44 -0
  30. data/lib/guardrails/partial_similarity.rb +231 -0
  31. data/lib/guardrails/railtie.rb +23 -0
  32. data/lib/guardrails/stimulus_audit.rb +118 -0
  33. data/lib/guardrails/token_matcher.rb +40 -0
  34. data/lib/guardrails/tokens/tailwind_config_parser.rb +140 -0
  35. data/lib/guardrails/tokens.rb +256 -0
  36. data/lib/guardrails/version.rb +5 -0
  37. data/lib/guardrails/view_component_audit.rb +150 -0
  38. data/lib/guardrails/visual_diff/snap_diff.rb +81 -0
  39. data/lib/guardrails/visual_diff.rb +117 -0
  40. data/lib/guardrails.rb +14 -0
  41. data/lib/tasks/guardrails.rake +176 -0
  42. data/lib/ui_guardrails.rb +9 -0
  43. 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: []