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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 200bad03eb94e193c93e5965c52743f8bc9c4d232003149d482cf8b4bc72d77c
4
+ data.tar.gz: cd365fa2244f58b1c294f317119be08e03da868f050cc1aae268d6ff524237a0
5
+ SHA512:
6
+ metadata.gz: 1c23e135b24dcbf76c3d98983ee339a22800fdf4a40fbc4b13dcbebaecfbf8b7160a5aa4472f01440fb20bac8bf6585ada295207189aae11f8809f894ef70003
7
+ data.tar.gz: 56572b2788586158520982bf9d3d0f18e4920136aab255dec5ef88b08d7c7ef0347ba653e71564f0b439c2c94db53c7eee30a241f9a5c516fc72722900769074
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Meticulous
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,302 @@
1
+ # Guardrails
2
+
3
+ A Rails toolset that prevents UI drift in AI-assisted applications. Static audits over your views, components, stylesheets, JS controllers, and tokens — surfacing the kinds of inconsistencies that compound silently as an AI assistant ships code faster than design-system discipline can keep up.
4
+
5
+ Built and maintained by [Meticulous](https://meticulous.com).
6
+
7
+ **Current release:** 1.0.0 — V0 + V1 + V2 complete, published on [RubyGems.org as `ui_guardrails`](https://rubygems.org/gems/ui_guardrails). The Ruby module stays `Guardrails` (so `require "guardrails"` is unchanged) — only the gem package name on rubygems carries the `ui_` prefix, to clear RubyGems' similarity rule against the unrelated [`guard-rails`](https://rubygems.org/gems/guard-rails) gem. See [`doc/ROADMAP.md`](doc/ROADMAP.md) for status, [`CHANGELOG.md`](CHANGELOG.md) for the full naming rationale.
8
+
9
+ ---
10
+
11
+ ## The problem
12
+
13
+ AI-assisted Rails development is fast. Too fast. Without design guardrails in place, codebases accumulate:
14
+
15
+ - Inline `style=` attributes nobody asked for
16
+ - Hex literals scattered across views that should reference a token
17
+ - `bg-[#fa3]` arbitrary Tailwind values when a theme color already exists
18
+ - Six near-duplicate "card" partials when one parameterized component would do
19
+ - `<button>` elements with no accessible name
20
+ - Orphan ViewComponent slots and Stimulus controllers
21
+ - The same 7-class utility soup pasted onto 30 buttons
22
+
23
+ Guardrails catches these patterns without rendering anything — every detector is a static AST walk over your source. Findings stream into a unified report (text or JSON) with the same exit-code contract every other lint tool uses.
24
+
25
+ ---
26
+
27
+ ## Installation
28
+
29
+ Guardrails works two ways: **installed in your project** (auto-loaded via a Railtie, runs the audit on every CI pass) or **sidecar** (cloned alongside your app, audits any tree on demand without modifying its Gemfile).
30
+
31
+ ### Installed in-project (recommended)
32
+
33
+ ```ruby
34
+ # Gemfile
35
+ group :development, :test do
36
+ gem "ui_guardrails", "~> 1.0"
37
+ end
38
+ ```
39
+
40
+ ```bash
41
+ bundle install
42
+ bundle exec rake guardrails:init # writes guardrails.yml, scaffolds MQs
43
+ bundle exec rake guardrails:audit # run the full audit
44
+ ```
45
+
46
+ The gem's Railtie auto-loads rake tasks and (when [Lookbook](https://lookbook.build) is also in your Gemfile) registers the **Guardrails** panel inside every preview's inspector.
47
+
48
+ ### Sidecar (no Gemfile change)
49
+
50
+ Useful when you want to audit a Rails app without committing to the gem yet, or in CI for repos you don't own.
51
+
52
+ ```bash
53
+ git clone https://github.com/meticulous/guardrails ~/code/guardrails
54
+ cd ~/code/guardrails && bundle install
55
+
56
+ # From your Rails app's root:
57
+ cd ~/code/my-rails-app
58
+ bundle exec --gemfile=~/code/guardrails/Gemfile \
59
+ rake -f ~/code/guardrails/lib/tasks/guardrails.rake \
60
+ guardrails:audit
61
+ ```
62
+
63
+ Every rake task accepts the same env vars in sidecar mode as in-project. The tasks default `root` to `Rails.root` when Rails is loaded, else `Dir.pwd` — so just run from the target app's root.
64
+
65
+ ---
66
+
67
+ ## Quick start
68
+
69
+ ```bash
70
+ # One-time setup — detects your token stack, writes guardrails.yml,
71
+ # scaffolds prefers-color-scheme media queries if your stylesheet
72
+ # doesn't already have them. Interactive when on a TTY; CI-safe
73
+ # default fallback otherwise.
74
+ bundle exec rake guardrails:init
75
+
76
+ # Full audit — exits 1 on any violation.
77
+ bundle exec rake guardrails:audit
78
+
79
+ # Get a markdown checklist of fixable findings:
80
+ SUGGEST=1 bundle exec rake guardrails:audit
81
+ # → writes doc/guardrails-suggestions-{TIMESTAMP}.md
82
+
83
+ # Auto-fix raw_color and tailwind_arbitrary where a token matches:
84
+ APPLY=1 bundle exec rake guardrails:audit
85
+ ```
86
+
87
+ ---
88
+
89
+ ## Tasks at a glance
90
+
91
+ | Task | What it does |
92
+ |---|---|
93
+ | `guardrails:init` | Stack detection, writes `guardrails.yml`, scaffolds prefers-color-scheme / prefers-contrast media queries. Refuses to overwrite an existing config — `FORCE=1` overrides. |
94
+ | `guardrails:audit` | Runs every detector — view drift, stimulus, partial similarity, view-components, a11y, cross-codebase patterns, class-itis. Exits 1 on violations. |
95
+ | `guardrails:icons` | Generates an SVG sprite from `app/assets/images/icons/`, flags inline `<svg>` in views, reports unused icons. |
96
+ | `guardrails:tokens` | Parses your color and type-scale tokens (CSS vars / SCSS vars / Tailwind v3 config / Tailwind v4 `@theme`), reports hex literals in stylesheets that should reference a token. |
97
+ | `guardrails:a11y:deep` | Reads axe-core JSON output and folds it into the unified report. Doesn't run axe itself (no Capybara / headless Chrome runtime deps) — point it at axe output your existing tooling produces. |
98
+ | `guardrails:visual:deep` | Consumes screenshot-diff tool output (snap_diff-capybara today; BackstopJS in flight, [#15](https://github.com/meticulous/guardrails/issues/15)) and reports visual regressions. Same parse-only design — your existing test toolchain runs screenshots; Guardrails reports. |
99
+
100
+ ---
101
+
102
+ ## Detectors
103
+
104
+ ### View-level drift (`guardrails:audit`)
105
+
106
+ Static AST walk over every `.html.erb` under `app/views/` and `app/components/` (via [Herb](https://github.com/marcoroth/herb), the ERB-aware parser):
107
+
108
+ | Detector | Catches |
109
+ |---|---|
110
+ | `inline_style` | `<div style="…">` — inline styles bypass tokens entirely. |
111
+ | `raw_color` | Hex / rgb literals in color-bearing attributes (`fill`, `stroke`, `color`, `bgcolor`, `background`, `data-*color*`, etc.). |
112
+ | `tailwind_arbitrary` | `bg-[#fa3]`, `text-[14px]`, `p-[7px]` — arbitrary values that bypass the theme. |
113
+ | `helper_recommended` | `<button>` / `<a>` wrapping `<%= … %>` ERB output. Suggests `tag.button` / `link_to` / `button_to` for clean accessible names. Skips elements that already have `aria-label`. |
114
+ | `image_alt` / `button_name` / `link_name` / `input_label` | The four most common static a11y misses (see [`doc/A11Y.md`](doc/A11Y.md)). |
115
+
116
+ ### Suggest mode and auto-fix
117
+
118
+ - `SUGGEST=1 rake guardrails:audit` writes `doc/guardrails-suggestions-{TIMESTAMP}.md` — a markdown checklist with one `[ ]` per fixable finding, the rule that fired, and the proposed replacement (closest token by exact match, else near-match within a configurable channel-distance).
119
+ - `APPLY=1 rake guardrails:audit` auto-fixes `raw_color` (→ `var(--token)`) and `tailwind_arbitrary` (→ Tailwind theme color shorthand) when a matching token exists. Near-match auto-fix is gated by `near_match_policy` in `guardrails.yml` (`fix` / `leave` / `notify`).
120
+
121
+ ### Tokens
122
+
123
+ `Guardrails::Tokens` parses every token source it can find:
124
+
125
+ - CSS custom properties in your configured `colors_file`
126
+ - SCSS variables in the same
127
+ - Tailwind v3 `tailwind.config.js` — flat colors and nested scales (`gray.50` → `gray-50`)
128
+ - Tailwind v4 `@theme {}` blocks — picked up by the CSS-custom-property scanner
129
+
130
+ Then scans every other stylesheet for hex literals and reports drift, matching each to the closest defined token. Block + line comments are stripped before matching, preserving line/column positions so reports are accurate.
131
+
132
+ ### Stimulus
133
+
134
+ `Guardrails::StimulusAudit` cross-references `data-controller="…"` attributes against `app/javascript/**/controllers/*_controller.{js,ts}` (works across importmap, Webpacker, Vite, and Avo's `app/javascript/js/controllers/` layout):
135
+
136
+ - **Orphaned** — controller referenced in a view but no JS file defines it.
137
+ - **Dead** — JS file exists but no view references it.
138
+ - Picks up Ruby helper syntax: `tag.div(data: { controller: "foo" })`.
139
+
140
+ ### ViewComponent
141
+
142
+ `Guardrails::ViewComponentAudit`:
143
+
144
+ - Reports components without a preview file (Lookbook discoverability).
145
+ - Reports `renders_one` / `renders_many` slots declared in the component class but never referenced in the template (orphan slots).
146
+
147
+ ### Partial similarity
148
+
149
+ `Guardrails::PartialSimilarity` runs n-gram Jaccard over the tag-stream of every partial and component template, groups near-duplicates by connected components, and reports clusters above the configured threshold (default 0.7). Pair sample lines surface the most similar match in each cluster.
150
+
151
+ ### Cross-codebase patterns (0.3.0)
152
+
153
+ `Guardrails::CrossCodebasePatterns` walks every element subtree, fingerprints the tag-only shape (`article(header(h2),section(p,p),footer(a))`), and reports shapes appearing 3+ times with ≥ 5 elements. Distinct from PartialSimilarity (which compares **existing** partials) — this finds shapes that **should** be partials but aren't yet. Dedupes redundant nested patterns so a repeating table doesn't generate three findings (`table(…)`, `thead(…)`, `tr(…)`) for the same locations.
154
+
155
+ ### Class-itis (0.4.0)
156
+
157
+ `Guardrails::ClassItis` groups elements by `(tag, sorted-uniq-class-list)`. Reports tuples with ≥ 5 distinct classes appearing on the same tag in ≥ 3 places — the classic AI-assisted-Rails failure mode of 8-utility soup pasted across many buttons when the codebase should have a shared component or `@apply` rule. ERB-driven class fragments are dropped; only the static portion is fingerprinted.
158
+
159
+ ### Visual diff via snap_diff-capybara (0.8.0)
160
+
161
+ `Guardrails::VisualDiff` consumes screenshot-diff tool output and folds findings into the unified report. The shipped adapter is [`snap_diff-capybara`](https://github.com/snap-diff/snap_diff-capybara) — the Rails-native baselines-in-git visual-regression gem:
162
+
163
+ ```bash
164
+ # Your existing system tests produce diffs under doc/screenshots/
165
+ bundle exec rspec spec/system/
166
+
167
+ # Fold visual-diff findings into the audit:
168
+ VISUAL_DIFF=1 bundle exec rake guardrails:audit
169
+ ```
170
+
171
+ Or standalone via `rake guardrails:visual:deep`. Parse-only — same trade as deep a11y, no Capybara/Chromium runtime deps in the gem. BackstopJS adapter tracked in [#15](https://github.com/meticulous/guardrails/issues/15). See [`doc/VISUAL-DIFF.md`](doc/VISUAL-DIFF.md).
172
+
173
+ ### Deep a11y via axe-core JSON (0.6.0)
174
+
175
+ `Guardrails::A11yDeep` consumes axe-core JSON output and folds findings into the unified report:
176
+
177
+ ```bash
178
+ npx @axe-core/cli http://localhost:3000/ --save axe.json
179
+ AXE_JSON=axe.json bundle exec rake guardrails:audit
180
+ ```
181
+
182
+ Or standalone via `rake guardrails:a11y:deep`. Stays parse-only — your existing test toolchain runs axe (axe-core-rspec, the CLI, Puppeteer, a CDP script — anything that emits axe v4 JSON), Guardrails provides the merge + report. See [`doc/A11Y.md`](doc/A11Y.md).
183
+
184
+ ### Lookbook auto-panel (0.5.0)
185
+
186
+ When [Lookbook](https://lookbook.build) is in the Gemfile, Guardrails auto-registers a `:guardrails` panel that appears next to every preview's Source / Notes panels. The panel renders `Guardrails::Lookbook::ComponentReport#for(component_class_name)` — drift in the template, orphan slots, similar templates — inline. Host apps override by dropping their own partial at `app/views/lookbook_panels/_guardrails.html.erb`. See [`doc/LOOKBOOK.md`](doc/LOOKBOOK.md).
187
+
188
+ ---
189
+
190
+ ## Output
191
+
192
+ Every task prints a human-readable text report by default and exits 1 when violations or failing findings exist. For machine consumption:
193
+
194
+ ```bash
195
+ FORMAT=json bundle exec rake guardrails:audit > findings.json
196
+ ```
197
+
198
+ The JSON payload has a `summary:` block with finding counts per category plus per-detector arrays — see the rake task source for the exact shape.
199
+
200
+ ### Common env vars
201
+
202
+ | Var | Effect |
203
+ |---|---|
204
+ | `SUGGEST=1` | Write the markdown checklist alongside the text report. |
205
+ | `APPLY=1` | Auto-fix raw_color + tailwind_arbitrary where tokens match. |
206
+ | `FORMAT=json` | Emit one JSON document to stdout (all other audit output is suppressed). |
207
+ | `FORCE=1` | Bypass `init`'s refuse-to-overwrite default. |
208
+ | `AXE_JSON=path` | Fold axe-core findings into the unified report. |
209
+ | `VISUAL_DIFF=1` | Fold visual-diff findings into `guardrails:audit`. Embedded installs can flip this on permanently via `Guardrails.configure { \|c\| c.visual_diff.enabled = true }`. |
210
+ | `VISUAL_DIFF_DIR=path` / `VISUAL_DIFF_THRESHOLD=0.02` | Tune the visual-diff snap_diff adapter and mismatch threshold. |
211
+ | `SIMILARITY_THRESHOLD=0.85` | Override the partial-similarity Jaccard threshold. |
212
+ | `PATTERN_MIN_SIZE=8` / `PATTERN_MIN_OCCURRENCES=4` | Tune the cross-codebase pattern detector. |
213
+ | `CLASSITIS_MIN_CLASSES=6` / `CLASSITIS_MIN_OCCURRENCES=4` | Tune the class-itis detector. |
214
+
215
+ ---
216
+
217
+ ## CI
218
+
219
+ The audit task is a single shell command:
220
+
221
+ ```yaml
222
+ # .github/workflows/ci.yml
223
+ - name: Guardrails audit
224
+ run: bundle exec rake guardrails:audit
225
+ ```
226
+
227
+ For richer integration:
228
+
229
+ ```yaml
230
+ - name: Guardrails audit (JSON)
231
+ run: bundle exec rake guardrails:audit FORMAT=json > findings.json
232
+ continue-on-error: true
233
+
234
+ - name: Upload findings
235
+ uses: actions/upload-artifact@v4
236
+ with:
237
+ name: guardrails-findings
238
+ path: findings.json
239
+ ```
240
+
241
+ ---
242
+
243
+ ## Configuration
244
+
245
+ `guardrails.yml` at the repo root. `guardrails:init` writes a sensible default after detecting your stack — what's below is annotated to show every available key:
246
+
247
+ ```yaml
248
+ guardrails:
249
+ scan_paths:
250
+ - app/views
251
+ - app/components
252
+ ignore:
253
+ - app/views/layouts
254
+ tokens:
255
+ # Where your color tokens live. The init task picks this up from
256
+ # stack detection (CSS vars, SCSS, Tailwind v3/v4); edit if it
257
+ # guessed wrong.
258
+ colors_file: app/assets/stylesheets/tokens/_colors.css
259
+ type_scale_file: app/assets/stylesheets/tokens/_type.css
260
+ # Per-channel R/G/B distance at which a near-match becomes a
261
+ # "close enough to suggest" finding. 0 = exact only; 4 = default
262
+ # (catches #0066ff ↔ #0067fe); 20+ = aggressive.
263
+ near_match_threshold: 4
264
+ # What to do with near-matches: notify (default; suggest in the
265
+ # report), fix (auto-fix with APPLY=1), or leave (silence).
266
+ near_match_policy: notify
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Demo app
272
+
273
+ [`examples/demo`](examples/demo) is a bootable Rails 7.2 app with intentionally seeded findings — every detector trips at least once. Clone, `bin/setup`, `bin/rails server`, then visit `/`, `/broken`, and `/rails/lookbook` to see the auto-registered Guardrails panel. `bundle exec rake guardrails:audit` runs the full audit against the seeded tree. See [`examples/demo/README.md`](examples/demo/README.md).
274
+
275
+ ---
276
+
277
+ ## Status & roadmap
278
+
279
+ - **V0** (foundation) — ✅ shipped
280
+ - **V1** (polish + ecosystem) — ✅ shipped
281
+ - **V2** (advanced) — ✅ shipped (cross-codebase patterns, class-itis, visual diff via snap_diff-capybara)
282
+
283
+ Full status table and decision log: [`doc/ROADMAP.md`](doc/ROADMAP.md).
284
+
285
+ ---
286
+
287
+ ## Development
288
+
289
+ ```bash
290
+ bundle install
291
+ bundle exec rspec # 453 examples
292
+ ```
293
+
294
+ Real-world signal lives in `examples/demo` (integration spec) and in the dogfood patches against four real codebases (Patchvault, Talos, Forem, Avo — see `CHANGELOG.md` for what each round caught).
295
+
296
+ Issues and PRs: <https://github.com/meticulous/guardrails>
297
+
298
+ ---
299
+
300
+ ## License
301
+
302
+ MIT
data/doc/A11Y.md ADDED
@@ -0,0 +1,87 @@
1
+ # Accessibility (a11y) checks
2
+
3
+ Guardrails ships **static** a11y checks — element-level rules that can be answered from your view source without rendering. These run as part of `rails guardrails:audit` and contribute to the exit code.
4
+
5
+ For full a11y coverage (color contrast, focus order, ARIA tree, dynamic content), wire **axe-core** alongside Guardrails in your test suite. Static checks catch the obvious template-level mistakes; runtime checks catch everything else.
6
+
7
+ ## Static checks (built-in)
8
+
9
+ Run via `rails guardrails:audit`. Findings appear under "Guardrails a11y" in the report and as `a11y` in `FORMAT=json` output.
10
+
11
+ | Rule | Catches |
12
+ |---|---|
13
+ | `image_alt` | `<img>` without an `alt` attribute (use `alt=""` for decorative images) |
14
+ | `button_name` | `<button>` with no text content and no `aria-label` / `aria-labelledby` |
15
+ | `link_name` | `<a href="...">` with no text, no `aria-label`, no `title` |
16
+ | `input_label` | Interactive `<input>` without `aria-label`, `aria-labelledby`, or a matching `<label for>` |
17
+
18
+ These rules align conceptually with the corresponding rules in [axe-core](https://dequeuniversity.com/rules/axe/) but run against template source rather than rendered DOM.
19
+
20
+ ## Deep a11y via axe-core JSON (`AXE_JSON=…`)
21
+
22
+ When you run axe-core in CI or locally, pass the JSON output to Guardrails to fold runtime findings into the unified report:
23
+
24
+ ```bash
25
+ # Run axe-core however you already do (one of these):
26
+ npx @axe-core/cli http://localhost:3000/ http://localhost:3000/dashboard --save axe.json
27
+ # or via axe-core-rspec, axe-puppeteer, your CDP script — anything that emits axe JSON v4
28
+
29
+ # Hand the JSON to Guardrails:
30
+ AXE_JSON=axe.json bundle exec rake guardrails:audit
31
+ ```
32
+
33
+ Or run the deep check standalone:
34
+
35
+ ```bash
36
+ AXE_JSON=axe.json bundle exec rake guardrails:a11y:deep
37
+ ```
38
+
39
+ The report groups findings by URL and prints rule + impact + selector + help URL per node. Exit code is 1 when any finding's impact is in the failing set (default: `minor`, `moderate`, `serious`, `critical` — i.e. any impact fails). The full payload is also included in `FORMAT=json` output under the `a11y_deep` key.
40
+
41
+ The input accepts either axe's single-page result object (`{ "url": "...", "violations": [...] }`) or an array of those for multi-URL runs — what `npx @axe-core/cli --save` emits.
42
+
43
+ Why parse-only and not bundled: see ["Why not bundle axe-core directly?"](#why-not-bundle-axe-core-directly) below. The short version is "your test suite already runs a browser; we don't need to duplicate that infrastructure."
44
+
45
+ ## Runtime axe-core (recommended addition)
46
+
47
+ For a system test suite:
48
+
49
+ ```ruby
50
+ # Gemfile
51
+ group :test do
52
+ gem "axe-core-rspec"
53
+ end
54
+ ```
55
+
56
+ ```ruby
57
+ # spec/rails_helper.rb
58
+ require "axe-rspec"
59
+ ```
60
+
61
+ ```ruby
62
+ # spec/system/checkout_spec.rb
63
+ require "rails_helper"
64
+
65
+ RSpec.describe "checkout flow", type: :system do
66
+ it "is accessible at the cart screen" do
67
+ visit cart_path
68
+ expect(page).to be_axe_clean.according_to(:wcag2a, :wcag2aa)
69
+ end
70
+ end
71
+ ```
72
+
73
+ For component-level a11y inside Lookbook previews, see `doc/LOOKBOOK.md` and add an axe panel via Lookbook's panel API.
74
+
75
+ ## Why not bundle axe-core directly?
76
+
77
+ axe-core needs a real browser. Adding Capybara + headless Chrome as runtime dependencies of a static-analysis gem would balloon installation cost for users who don't run system tests. Keeping the integration as a documented opt-in lets you size your a11y testing to match your actual test infrastructure.
78
+
79
+ ## What static checks miss
80
+
81
+ - Color contrast (needs rendered colors in context)
82
+ - Focus order and keyboard navigation
83
+ - ARIA relationships and live regions
84
+ - Dynamic content that appears after JS runs
85
+ - Heading hierarchy across a full page (we see one component at a time)
86
+
87
+ If any of these matter to you, layer axe-core on top.
data/doc/LOOKBOOK.md ADDED
@@ -0,0 +1,52 @@
1
+ # Lookbook integration
2
+
3
+ When you have both Guardrails and [Lookbook](https://lookbook.build) in your Gemfile, Guardrails auto-registers a `:guardrails` panel that appears next to every preview, surfacing per-component audit findings inline. No initializer wiring required.
4
+
5
+ ## What you see
6
+
7
+ In a preview's inspector, alongside the standard Source / Notes / Params panels, a **Guardrails** tab shows:
8
+
9
+ - **Drift in template** — `inline_style`, `raw_color`, `tailwind_arbitrary`, `helper_recommended` findings for the component's `.html.erb`
10
+ - **Orphan slots** — `renders_one` / `renders_many` declarations not rendered by the template
11
+ - **Similar templates** — other partials / components above the similarity threshold
12
+
13
+ If the component has no findings, the panel renders a "No findings — this component is clean." message. If Guardrails can't locate the component's class file under `app/components/`, the panel says so.
14
+
15
+ ## How auto-registration works
16
+
17
+ The Railtie's `guardrails.lookbook_panel` initializer runs at app boot and is a no-op unless `defined?(::Lookbook)`. When Lookbook is present:
18
+
19
+ 1. The gem **appends** its view directory (`lib/guardrails/lookbook/views`) to `ActionController::Base.view_paths`. Append, not prepend, so the host's `app/views/lookbook_panels/_guardrails.html.erb` (if present) still wins.
20
+ 2. It calls `Rails.application.config.lookbook.preview_inspector.panels.add(:guardrails)` with a locals lambda that, per render, runs `Guardrails::Lookbook::ComponentReport` against the current preview's component class.
21
+
22
+ ## Programmatic access
23
+
24
+ The same data the panel renders is exposed as a plain Hash, useful for CI dashboards or custom rendering:
25
+
26
+ ```ruby
27
+ Guardrails::Lookbook::ComponentReport.new(root: Rails.root).for("ButtonComponent")
28
+ # => {
29
+ # component: "ButtonComponent",
30
+ # class_file: "app/components/button_component.rb",
31
+ # template_file: "app/components/button_component.html.erb",
32
+ # violations: [
33
+ # { type: :raw_color, file: "...", line: 3, column: 12, snippet: "...", value: "#0066ff" }
34
+ # ],
35
+ # orphan_slots: [
36
+ # { component: "button", slot: "icon", slot_kind: :renders_one, file: "...", line: 4 }
37
+ # ],
38
+ # similar_templates: [
39
+ # { partner: "app/components/icon_button_component.html.erb", score: 0.92 }
40
+ # ]
41
+ # }
42
+ ```
43
+
44
+ Returns `nil` when the component class file can't be located.
45
+
46
+ ## Overriding the partial
47
+
48
+ The shipped partial deliberately uses class hooks (`lookbook-guardrails-empty`, `lookbook-guardrails-clean`) but no styling — the host app's design system wins. If you need a different layout entirely, drop a file at `app/views/lookbook_panels/_guardrails.html.erb` in your app: standard Rails view-path precedence puts the host's version ahead of the gem's.
49
+
50
+ ## Performance note
51
+
52
+ `ComponentReport#for` runs the full audit, view-component audit, and partial-similarity pass on every panel render, then filters by component. For large codebases that's wasteful when Lookbook re-renders frequently. If you hit perf issues, cache results per Lookbook session or move audit invocation to a background job that writes JSON to disk for the panel to read.
data/doc/PRD.md ADDED
@@ -0,0 +1,145 @@
1
+ # Guardrails — Product Requirements Document
2
+
3
+ **Status:** Draft
4
+ **Owner:** John Athayde / Meticulous
5
+ **Last updated:** 2026-05-04
6
+
7
+ ---
8
+
9
+ ## Problem Statement
10
+
11
+ AI-assisted Rails development accelerates shipping but introduces a new failure mode: UI drift. Developers using Copilot, Claude, or similar tools generate UI code fast — too fast to maintain design system consistency. The result is:
12
+
13
+ - Duplicate or near-duplicate components
14
+ - Ad-hoc color values that bypass design tokens
15
+ - Icon sprawl (inline SVGs, mixed icon sets, no sprite optimization)
16
+ - Type scale violations (hardcoded font sizes, inconsistent heading hierarchy)
17
+ - No enforcement layer between "AI wrote this" and "this ships"
18
+
19
+ Rails developers without dedicated designers have no tooling to catch this before it compounds.
20
+
21
+ ---
22
+
23
+ ## Target User
24
+
25
+ **Primary:** Rails developers at small-to-mid-sized product companies who:
26
+ - Ship UI features frequently (weekly or faster)
27
+ - Use AI coding assistants
28
+ - Don't have a dedicated designer or design system engineer
29
+ - Care about consistency but lack the time to enforce it manually
30
+
31
+ **Secondary:** Design-aware Rails consultants (Meticulous ICP) who want to hand clients something they can run themselves post-engagement.
32
+
33
+ ---
34
+
35
+ ## Goals
36
+
37
+ 1. Give Rails devs a single command to audit UI consistency across a codebase
38
+ 2. Enforce design token usage (colors, type scale) via configurable rules
39
+ 3. Generate optimized icon sprites from raw SVG assets
40
+ 4. Integrate naturally into existing Rails workflows (rake tasks, CI)
41
+ 5. Be opinionated but configurable
42
+
43
+ ---
44
+
45
+ ## Non-Goals
46
+
47
+ - Not a design system generator (doesn't create components for you)
48
+ - Not a linter replacement (Rubocop, ESLint still own their domains)
49
+ - Not a visual regression tool (no screenshots/diffs)
50
+ - Not a component library
51
+
52
+ ---
53
+
54
+ ## Features
55
+
56
+ ### 1. Component Audit (`rails guardrails:audit`)
57
+
58
+ Scans views and partials for:
59
+ - Near-duplicate partials (structural similarity, not just naming)
60
+ - Inline styles that bypass CSS custom properties
61
+ - Hardcoded color values (hex, rgb) outside of token files
62
+ - Hardcoded font-size/line-height values outside of type scale
63
+
64
+ Outputs: summary report to stdout + optional JSON/HTML report
65
+
66
+ ### 2. Icon Management (`rails guardrails:icons`)
67
+
68
+ - Scans configured SVG source directory
69
+ - Generates optimized SVG sprite sheet
70
+ - Audits for inline SVGs in views that should be sprite references
71
+ - Reports icon usage frequency (find dead icons)
72
+
73
+ ### 3. Design Token Enforcement (`rails guardrails:tokens`)
74
+
75
+ - Reads a `guardrails.yml` token definition (colors, type scale, spacing)
76
+ - Scans CSS/SCSS/Tailwind config for values that don't match defined tokens
77
+ - Reports violations with file + line number
78
+ - Optional: auto-suggest the closest token for a given raw value
79
+
80
+ ### 4. Configuration (`guardrails.yml`)
81
+
82
+ ```yaml
83
+ guardrails:
84
+ icons:
85
+ source: app/assets/images/icons
86
+ sprite_output: app/assets/images/icons/sprite.svg
87
+
88
+ tokens:
89
+ colors_file: app/assets/stylesheets/tokens/_colors.scss
90
+ type_scale_file: app/assets/stylesheets/tokens/_type.scss
91
+
92
+ audit:
93
+ scan_paths:
94
+ - app/views
95
+ - app/components
96
+ ignore:
97
+ - app/views/layouts
98
+ ```
99
+
100
+ ### 5. CI Integration
101
+
102
+ - Exit codes: 0 = clean, 1 = violations found
103
+ - `--strict` flag to fail CI on any violation
104
+ - `--format json` for machine-readable output
105
+
106
+ ---
107
+
108
+ ## Technical Approach
109
+
110
+ - Ruby gem, distributed via RubyGems
111
+ - Rake tasks registered via Railtie
112
+ - No runtime dependency — development/test group only
113
+ - Minimal dependencies (avoid pulling in heavy gems)
114
+
115
+ ---
116
+
117
+ ## Success Metrics
118
+
119
+ - Developers can run `rails guardrails:audit` in < 30 seconds on a mid-sized app
120
+ - Catches at least 80% of common UI drift patterns in a test app
121
+ - Zero false positives on a clean, well-structured Rails app
122
+ - Used by at least 3 Meticulous clients within 6 months of launch
123
+
124
+ ---
125
+
126
+ ## Open Questions
127
+
128
+ - [ ] Tailwind support: scan for arbitrary values in class strings? (complex but high value)
129
+ - [ ] ViewComponent vs ERB partials: audit strategy differs
130
+ - [ ] Auto-fix mode: is this in scope for v1?
131
+ - [ ] VS Code extension as a companion? (out of scope for now)
132
+ - [ ] Where do Stimulus controllers fit in the audit?
133
+
134
+ ---
135
+
136
+ ## Milestones
137
+
138
+ | Milestone | Target | Notes |
139
+ |-----------|--------|-------|
140
+ | PRD + README stub | 2026-05-04 | ✅ Done |
141
+ | Gem skeleton + Railtie | TBD | First Claude Code session |
142
+ | `guardrails:audit` v0 | TBD | |
143
+ | `guardrails:icons` v0 | TBD | |
144
+ | `guardrails:tokens` v0 | TBD | |
145
+ | Public beta / RubyGems publish | TBD | Before next speaking slot |