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
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 |
|