similarbuild 0.1.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.
- package/CHANGELOG.md +110 -0
- package/LICENSE +21 -0
- package/README.md +301 -0
- package/bin/install.js +256 -0
- package/lib/copy-templates.mjs +52 -0
- package/lib/install-deps.mjs +62 -0
- package/lib/prompt-config.mjs +83 -0
- package/lib/verify-env.mjs +19 -0
- package/package.json +63 -0
- package/scripts/sync-templates.mjs +71 -0
- package/templates/commands/build-page.md +490 -0
- package/templates/commands/build-site.md +548 -0
- package/templates/commands/clip-section.md +519 -0
- package/templates/memory/anti-patterns.md +212 -0
- package/templates/memory/design-knowledge.md +225 -0
- package/templates/memory/fixes.md +163 -0
- package/templates/memory/patterns.md +681 -0
- package/templates/presets/shopify-section.yaml +51 -0
- package/templates/presets/wp-elementor.yaml +49 -0
- package/templates/reports/fixtures/mock-run-1.json +115 -0
- package/templates/reports/fixtures/mock-run-2.json +72 -0
- package/templates/reports/report-renderer.mjs +218 -0
- package/templates/reports/report-template.html +571 -0
- package/templates/skills/sb-build-shopify/SKILL.md +104 -0
- package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
- package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
- package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
- package/templates/skills/sb-build-wp/SKILL.md +83 -0
- package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
- package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
- package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
- package/templates/skills/sb-compare-visual/SKILL.md +121 -0
- package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
- package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
- package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
- package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
- package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
- package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
- package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
- package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
- package/templates/skills/sb-extract-assets/SKILL.md +112 -0
- package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
- package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
- package/templates/skills/sb-inspect-live/SKILL.md +105 -0
- package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
- package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
- package/templates/skills/sb-review-checks/SKILL.md +113 -0
- package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
- package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
- package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
- package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
- package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
- package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
- package/templates/skills/sb-tweak/SKILL.md +130 -0
- package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
- package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
- package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
- package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
- package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
- package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
- package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
- package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
- package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
- package/templates/skills/sb-validate-render/SKILL.md +120 -0
- package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
- package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// test-inspect-live.mjs — Smoke tests that don't require chromium.
|
|
3
|
+
// Validates: --help works, missing args fail with exit code 2, syntax loads.
|
|
4
|
+
// Browser-driven integration tests are deliberately out of scope here —
|
|
5
|
+
// they belong in a separate harness that can pre-install playwright.
|
|
6
|
+
|
|
7
|
+
import { spawnSync } from 'node:child_process'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { dirname, resolve } from 'node:path'
|
|
10
|
+
import { strict as assert } from 'node:assert'
|
|
11
|
+
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const SCRIPT = resolve(here, '..', 'inspect-live.mjs')
|
|
14
|
+
|
|
15
|
+
let passed = 0
|
|
16
|
+
let failed = 0
|
|
17
|
+
|
|
18
|
+
function test(name, fn) {
|
|
19
|
+
try {
|
|
20
|
+
fn()
|
|
21
|
+
process.stdout.write(`ok - ${name}\n`)
|
|
22
|
+
passed++
|
|
23
|
+
} catch (err) {
|
|
24
|
+
process.stdout.write(`not ok - ${name}\n ${err.message}\n`)
|
|
25
|
+
failed++
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
test('--help exits 0 and prints usage', () => {
|
|
30
|
+
const r = spawnSync('node', [SCRIPT, '--help'], { encoding: 'utf8' })
|
|
31
|
+
assert.equal(r.status, 0, `exit code was ${r.status}`)
|
|
32
|
+
assert.match(r.stdout, /inspect-live\.mjs/)
|
|
33
|
+
assert.match(r.stdout, /--url/)
|
|
34
|
+
assert.match(r.stdout, /--viewport-width/)
|
|
35
|
+
assert.match(r.stdout, /--output-dir/)
|
|
36
|
+
assert.match(r.stdout, /--selector/)
|
|
37
|
+
assert.match(r.stdout, /--wait-strategy/)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test('missing --url exits 2', () => {
|
|
41
|
+
const r = spawnSync('node', [SCRIPT, '--output-dir', '/tmp/sb-test'], { encoding: 'utf8' })
|
|
42
|
+
assert.equal(r.status, 2, `exit code was ${r.status}`)
|
|
43
|
+
assert.match(r.stderr, /missing --url/)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test('missing --output-dir exits 2', () => {
|
|
47
|
+
const r = spawnSync('node', [SCRIPT, '--url', 'https://example.com'], { encoding: 'utf8' })
|
|
48
|
+
assert.equal(r.status, 2, `exit code was ${r.status}`)
|
|
49
|
+
assert.match(r.stderr, /missing --output-dir/)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('non-numeric viewport exits 2', () => {
|
|
53
|
+
const r = spawnSync(
|
|
54
|
+
'node',
|
|
55
|
+
[SCRIPT, '--url', 'https://example.com', '--output-dir', '/tmp/sb-test', '--viewport-width', 'foo'],
|
|
56
|
+
{ encoding: 'utf8' },
|
|
57
|
+
)
|
|
58
|
+
assert.equal(r.status, 2, `exit code was ${r.status}`)
|
|
59
|
+
assert.match(r.stderr, /viewport must be numeric/)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ─── Pattern #23 — widgetBlocked composite signal ──────────────────────────
|
|
63
|
+
// The script's main() exits on missing args, so we mirror the regex tables
|
|
64
|
+
// inline (same approach as test-validate-render.mjs uses for schema regex).
|
|
65
|
+
|
|
66
|
+
const BLOCK_PATTERNS = [
|
|
67
|
+
/just a moment/i,
|
|
68
|
+
/checking your browser/i,
|
|
69
|
+
/enable javascript( and cookies)? to continue/i,
|
|
70
|
+
/cloudflare/i,
|
|
71
|
+
/access denied/i,
|
|
72
|
+
/verifying you are human/i,
|
|
73
|
+
/please complete the security check/i,
|
|
74
|
+
/captcha/i,
|
|
75
|
+
/forbidden/i,
|
|
76
|
+
]
|
|
77
|
+
const BLOCK_TITLE_PATTERNS = [
|
|
78
|
+
/just a moment/i,
|
|
79
|
+
/attention required/i,
|
|
80
|
+
/access denied/i,
|
|
81
|
+
/forbidden/i,
|
|
82
|
+
/captcha/i,
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
function blockedFor({ title = '', bodyText = '', bodyHtmlLen = 0, meaningfulChildren = 0 }) {
|
|
86
|
+
const blockedByBodyPattern = BLOCK_PATTERNS.some((re) => re.test(bodyText))
|
|
87
|
+
const blockedByTitlePattern = BLOCK_TITLE_PATTERNS.some((re) => re.test(title))
|
|
88
|
+
const extremeTiny = bodyHtmlLen < 256
|
|
89
|
+
const smallAndBare = bodyHtmlLen >= 256 && bodyHtmlLen < 1024 && meaningfulChildren < 2
|
|
90
|
+
return blockedByBodyPattern || blockedByTitlePattern || extremeTiny || smallAndBare
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
test('Pattern #23: example.com (528B body, 2+ meaningful children) → NOT blocked', () => {
|
|
94
|
+
// example.com renders a real page with <h1>, <p>, <a>; body innerText is
|
|
95
|
+
// benign prose. The OLD `bodyHtmlLen < 1024` heuristic falsely flagged it.
|
|
96
|
+
const r = blockedFor({
|
|
97
|
+
title: 'Example Domain',
|
|
98
|
+
bodyText: 'This domain is for use in illustrative examples in documents.',
|
|
99
|
+
bodyHtmlLen: 528,
|
|
100
|
+
meaningfulChildren: 2,
|
|
101
|
+
})
|
|
102
|
+
assert.equal(r, false, 'minimalist example.com should not be widget-blocked')
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
test('Pattern #23: info.cern.ch-class minimalist page (646B, 2+ children) → NOT blocked', () => {
|
|
106
|
+
const r = blockedFor({
|
|
107
|
+
title: 'World Wide Web',
|
|
108
|
+
bodyText: 'A look at the project that started it all.',
|
|
109
|
+
bodyHtmlLen: 646,
|
|
110
|
+
meaningfulChildren: 3,
|
|
111
|
+
})
|
|
112
|
+
assert.equal(r, false)
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('Pattern #23: extreme-tiny (< 256B) → blocked regardless of patterns', () => {
|
|
116
|
+
const r = blockedFor({ title: '', bodyText: '', bodyHtmlLen: 100, meaningfulChildren: 0 })
|
|
117
|
+
assert.equal(r, true)
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
test('Pattern #23: small AND bare (300B, 1 child) → blocked', () => {
|
|
121
|
+
const r = blockedFor({
|
|
122
|
+
title: 'Loading…',
|
|
123
|
+
bodyText: 'loading',
|
|
124
|
+
bodyHtmlLen: 300,
|
|
125
|
+
meaningfulChildren: 1,
|
|
126
|
+
})
|
|
127
|
+
assert.equal(r, true)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test('Pattern #23: Cloudflare title-only challenge → blocked', () => {
|
|
131
|
+
const r = blockedFor({
|
|
132
|
+
title: 'Just a moment...',
|
|
133
|
+
bodyText: '',
|
|
134
|
+
bodyHtmlLen: 1500,
|
|
135
|
+
meaningfulChildren: 3,
|
|
136
|
+
})
|
|
137
|
+
assert.equal(r, true)
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
test('Pattern #23: "Attention Required" Cloudflare title → blocked', () => {
|
|
141
|
+
const r = blockedFor({
|
|
142
|
+
title: 'Attention Required! | Cloudflare',
|
|
143
|
+
bodyText: 'Please complete the security check.',
|
|
144
|
+
bodyHtmlLen: 2000,
|
|
145
|
+
meaningfulChildren: 3,
|
|
146
|
+
})
|
|
147
|
+
assert.equal(r, true)
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('Pattern #23: body-text "checking your browser" → blocked', () => {
|
|
151
|
+
const r = blockedFor({
|
|
152
|
+
title: '',
|
|
153
|
+
bodyText: 'Checking your browser before accessing the site.',
|
|
154
|
+
bodyHtmlLen: 5000,
|
|
155
|
+
meaningfulChildren: 5,
|
|
156
|
+
})
|
|
157
|
+
assert.equal(r, true)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('Pattern #23: large legitimate page (5K body, 10+ children, no patterns) → NOT blocked', () => {
|
|
161
|
+
const r = blockedFor({
|
|
162
|
+
title: 'My Online Store',
|
|
163
|
+
bodyText: 'Welcome to our shop. Featured products this week. Free shipping over $50.',
|
|
164
|
+
bodyHtmlLen: 50000,
|
|
165
|
+
meaningfulChildren: 25,
|
|
166
|
+
})
|
|
167
|
+
assert.equal(r, false)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('Pattern #23: captcha keyword anywhere in body → blocked', () => {
|
|
171
|
+
const r = blockedFor({
|
|
172
|
+
title: '',
|
|
173
|
+
bodyText: 'Please solve the captcha to continue.',
|
|
174
|
+
bodyHtmlLen: 4000,
|
|
175
|
+
meaningfulChildren: 3,
|
|
176
|
+
})
|
|
177
|
+
assert.equal(r, true)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
process.stdout.write(`\n${passed} passed, ${failed} failed\n`)
|
|
181
|
+
process.exit(failed === 0 ? 0 : 1)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: sb-review-checks
|
|
3
|
+
description: Audits a built HTML/Liquid fragment against canonical visual anti-patterns, accessibility/performance/web-standards baselines, and (optionally) cross-references the violations with `sb-compare-visual` diffs to produce a prioritized list of specific candidate fixes for the auto-correct loop. Use when the SimilarBuild orchestrator (`/build-page`, `/build-site`, `/clip-section`) needs to audit a build before shipping or to generate `fixHints` for `sb-build-wp` / `sb-build-shopify`, or when the user asks to 'review checks' on a built fragment.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# sb-review-checks
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
Closes the **build → validate → compare → review** cycle. Takes whatever `sb-build-wp` or `sb-build-shopify` produced, parses it (cheerio for HTML/Liquid, regex for CSS), and runs three independent groups of deterministic checks:
|
|
11
|
+
|
|
12
|
+
1. **Anti-patterns** — the canon `#1, #2, #3, #4, #5, #5b, #6, #8` from the migration bootstrap (`#7` and `#9` are process-level, prevented by other skills).
|
|
13
|
+
2. **Design quality** — the 12 a11y / performance / web-standards baselines that every production fragment must clear.
|
|
14
|
+
3. **Cross-reference** — when `--compare-diffs` is supplied, violations whose location overlaps a high-severity visual diff are escalated and tagged.
|
|
15
|
+
|
|
16
|
+
Pure determinism — no chromium, no network. The script does the work; this SKILL.md only describes inputs, outputs, and how to react to a non-zero exit.
|
|
17
|
+
|
|
18
|
+
This skill is the **engine of the auto-correct loop**. Its `candidateFix` strings are the contract with `sb-build-{wp,shopify}`: specific enough to apply programmatically as `fixHints`, never generic ("improve specificity" is useless — "rewrite `.hero__title` as `.hero .hero__title`" is actionable). When you generate or hand-edit fix text, treat it as patch instructions, not prose.
|
|
19
|
+
|
|
20
|
+
The split between **mechanical** (script) and **semantic** (LLM) is explicit: regex/AST decides whether `alt` is *present*; you decide whether it's *meaningful*. The script never tries to judge wording or pattern correctness.
|
|
21
|
+
|
|
22
|
+
## Inputs
|
|
23
|
+
|
|
24
|
+
| Argument | Required | Default | Notes |
|
|
25
|
+
| ----------------- | -------- | ------- | -------------------------------------------------------------------------------------- |
|
|
26
|
+
| `file` | yes | — | Path to the built fragment. HTML for `wp-elementor`, Liquid for `shopify-section`. |
|
|
27
|
+
| `preset` | yes | — | `wp-elementor` or `shopify-section`. Determines which checks apply. |
|
|
28
|
+
| `output-dir` | yes | — | Directory where `report.json` is written. |
|
|
29
|
+
| `compare-diffs` | no | — | JSON from `sb-compare-visual` (full report or just the `structuredDiffs` array). |
|
|
30
|
+
| `memory-dir` | no | (auto) | Override `<plugin>/memory` location. Auto-detects `~/.claude/similarbuild-memory` and the bundled `references/` fallback otherwise. |
|
|
31
|
+
|
|
32
|
+
## Output
|
|
33
|
+
|
|
34
|
+
A single JSON object printed to stdout AND saved to `{output-dir}/report.json`:
|
|
35
|
+
|
|
36
|
+
```json
|
|
37
|
+
{
|
|
38
|
+
"passed": false,
|
|
39
|
+
"file": "/abs/path/build.html",
|
|
40
|
+
"preset": "wp-elementor",
|
|
41
|
+
"violationCount": { "high": 2, "medium": 3, "low": 1 },
|
|
42
|
+
"violations": [
|
|
43
|
+
{
|
|
44
|
+
"group": "anti-pattern",
|
|
45
|
+
"id": "#5b",
|
|
46
|
+
"location": "selector `.hero__title`",
|
|
47
|
+
"issue": "`!important` without an ancestor in the selector — theme has both ancestor + !important so this loses anyway",
|
|
48
|
+
"candidateFix": "prefix `.hero__title` with the scope as ancestor: rewrite as `.{scope} .hero__title`",
|
|
49
|
+
"severity": "high",
|
|
50
|
+
"correlatedDiff": { "area": "h1", "issue": "font-size 24px vs 27px expected", "deltaPx": -3 }
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
"report": "{output-dir}/report.json",
|
|
54
|
+
"inputs": { "file": "...", "preset": "...", "compareDiffs": "...", "memoryDir": "...", "memorySource": "bundled|home|flag" }
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`violations` is sorted **high → medium → low**, with cross-referenced (visual-diff-correlated) entries surfacing first within each severity. The orchestrator's first slice is always the most urgent fix.
|
|
59
|
+
|
|
60
|
+
## Dependencies
|
|
61
|
+
|
|
62
|
+
The host project must have `cheerio` installed (the SimilarBuild installer handles it). No browser, no native bindings. Node ≥ 20.
|
|
63
|
+
|
|
64
|
+
## On Activation
|
|
65
|
+
|
|
66
|
+
1. **Resolve inputs.** Collect `file`, `preset`, `output-dir`. Optional: `compare-diffs`, `memory-dir`.
|
|
67
|
+
|
|
68
|
+
2. **Ensure `output-dir` exists.** `mkdir -p` it.
|
|
69
|
+
|
|
70
|
+
3. **Run the script.** From the project root:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
node {skill-root}/scripts/review-checks.mjs \
|
|
74
|
+
--file "{built-file}" \
|
|
75
|
+
--preset "wp-elementor" \
|
|
76
|
+
--output-dir "{output-dir}" \
|
|
77
|
+
[--compare-diffs "{compare-report.json}"] \
|
|
78
|
+
[--memory-dir "{path-to-memory}"]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The script handles: file read, cheerio parse, CSS extraction (also from `{% style %}` Liquid blocks), 8 anti-pattern checks, 12 design-quality checks, optional cross-reference with compare diffs, prioritization, and persistence. See `scripts/review-checks.mjs --help` for the full flag list.
|
|
82
|
+
|
|
83
|
+
4. **Branch on exit code.**
|
|
84
|
+
- `0` — `passed: true`. Either zero violations or only `low`-severity ones. Ship it.
|
|
85
|
+
- `3` — `passed: false`, medium/high violations exist. **Not a script error.** Parse the JSON; the `violations` list is exactly what `sb-build-{wp,shopify}` expects as `fixHints` for the next re-roll.
|
|
86
|
+
- `1` — actual script error (missing dependency, malformed file). Surface stderr to the user and stop.
|
|
87
|
+
- `2` — invalid args. Fix the call.
|
|
88
|
+
|
|
89
|
+
5. **Forward the JSON unchanged** to the caller. Do not summarize, mutate, or strip fields. Downstream consumers (the orchestrator's auto-correct loop, the HTML report) read every field directly.
|
|
90
|
+
|
|
91
|
+
## Memory and presets
|
|
92
|
+
|
|
93
|
+
The skill consults a memory directory in this order: `--memory-dir` flag → `~/.claude/similarbuild-memory/` → bundled `references/`. When present, files there should override the bundled defaults — they reflect the user's most recent curated knowledge. The bundled `references/review-rules.md` ships the canonical anti-patterns and design-quality rules so the skill is fully functional out of the box.
|
|
94
|
+
|
|
95
|
+
The memory files are **data**, not code: adding a new anti-pattern is a `.md` edit, not a `.mjs` change. The orchestrator can hand-curate them and `sb-review-checks` picks them up next run.
|
|
96
|
+
|
|
97
|
+
## Failure modes
|
|
98
|
+
|
|
99
|
+
| Symptom | Likely cause | What to surface |
|
|
100
|
+
| ---------------------------------------------------- | -------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
|
|
101
|
+
| Exit 1, stderr mentions `cheerio` | Dependency not installed | Pass stderr verbatim. Suggest `npm i cheerio`. |
|
|
102
|
+
| Exit 1, "could not read" | Input file missing or not readable | Pass stderr verbatim. The build path may be stale — re-check what `sb-build-{wp,shopify}` returned. |
|
|
103
|
+
| Exit 3 with only `low` severity in `violationCount` | Should not happen — exit 3 implies med/high | If you see this, file a bug — exit-code policy is wrong. |
|
|
104
|
+
| `inputs.memorySource: "bundled"` but you expected curated rules | The user-curated memory dir is not where the skill looks | Pass `--memory-dir` explicitly, or move the curated files to `~/.claude/similarbuild-memory/`. |
|
|
105
|
+
| `--compare-diffs` provided but no `correlatedDiff` annotations | The diffs file has no `severity: 'high'` entries | Cross-reference only escalates on `high` diffs; med/low are advisory. This is by design. |
|
|
106
|
+
|
|
107
|
+
## Conventions
|
|
108
|
+
|
|
109
|
+
- Bare paths (e.g. `scripts/review-checks.mjs`) resolve from the skill root.
|
|
110
|
+
- `{skill-root}` resolves to this skill's installed directory.
|
|
111
|
+
- `{project-root}` resolves to the project working directory.
|
|
112
|
+
- `<plugin>/memory/...` paths are inside the SimilarBuild plugin install — read them when present, fall back to bundled `references/` when absent.
|
|
113
|
+
- `candidateFix` is a contract: every value names the selector, attribute, or property to change. Never emit prose-style suggestions ("improve X"). The auto-correct loop depends on this.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# Review Rules — anti-patterns + design quality (bundled fallback)
|
|
2
|
+
|
|
3
|
+
This file is the **bundled fallback** for `sb-review-checks`. The skill reads it when no curated copy exists at `~/.claude/similarbuild-memory/` or wherever `--memory-dir` points. Treat the curated location (when present) as the source of truth — the user's most recent learning lives there. This file ships the canon so the skill is functional out of the box.
|
|
4
|
+
|
|
5
|
+
These rules are **data**, not code: each entry has the same shape (id, symptom, check, fix template) and updates here don't require touching `scripts/lib/*.mjs`. The check functions in those libs implement detection; this file documents intent so a human reading the report knows *why* something was flagged.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Group 1 — Anti-patterns (bootstrap canon)
|
|
10
|
+
|
|
11
|
+
The 8 anti-patterns the script audits. `#7` (showing output before validating) and `#9` (replicating an image as inline SVG approximation when an asset exists) are **process-level** — they're prevented by `sb-validate-render` and `sb-extract-assets` respectively, so this skill does not re-check them.
|
|
12
|
+
|
|
13
|
+
### #1 — Hero `100vh` in Elementor
|
|
14
|
+
|
|
15
|
+
- **Preset:** `wp-elementor` only.
|
|
16
|
+
- **Symptom:** mobile browser chrome eats viewport, hero clips below the fold.
|
|
17
|
+
- **Check (mechanical):** any occurrence of `100vh` anywhere in the CSS.
|
|
18
|
+
- **Fix template:** `replace 100vh with aspect-ratio + max-height on the hero container`.
|
|
19
|
+
- **Severity:** high.
|
|
20
|
+
|
|
21
|
+
### #2 — `<img>` with `height: 100%` for full-bleed hero
|
|
22
|
+
|
|
23
|
+
- **Symptom:** Elementor injects `img { height: auto !important }` and clobbers the height.
|
|
24
|
+
- **Check (mechanical):** an `<img>` under a `class~="hero"` ancestor with inline `style="… height: 100% …"`.
|
|
25
|
+
- **Fix template:** `replace the <img> hero with background-image on the container`.
|
|
26
|
+
- **Severity:** high.
|
|
27
|
+
|
|
28
|
+
### #3 — Round-number opacity (eyeballed, not measured)
|
|
29
|
+
|
|
30
|
+
- **Symptom:** opacity values `0.3`, `0.5`, `0.7` are nearly always guesses; real readings are arbitrary numbers like `0.42`.
|
|
31
|
+
- **Check (mechanical):** regex `opacity\s*:\s*0?\.[357]` in any style block.
|
|
32
|
+
- **Fix template:** `verify the alpha against inspection.tokens; comment-mark it /* alpha from inspection */ once verified`.
|
|
33
|
+
- **Severity:** low.
|
|
34
|
+
|
|
35
|
+
### #4 — `a { color: inherit }` in scoped CSS
|
|
36
|
+
|
|
37
|
+
- **Symptom:** the anchor inherits the themed ancestor color, brand color is lost.
|
|
38
|
+
- **Check (mechanical):** any rule whose selector ends in `a` (or `a.x`/`a:hover`) with `color: inherit` in the body.
|
|
39
|
+
- **Fix template:** `replace color: inherit with a literal hex from inspection.tokens`.
|
|
40
|
+
- **Severity:** medium.
|
|
41
|
+
|
|
42
|
+
### #5 — `<button>` decorative without defensive specificity
|
|
43
|
+
|
|
44
|
+
- **Symptom:** theme `.elementor *` rules win over single-class selectors.
|
|
45
|
+
- **Check (mechanical):** a rule whose selector contains `button|btn|cta`, has neither a descendant combinator (whitespace) nor a doubled class chain (`.x.x`), and styles background/color/font.
|
|
46
|
+
- **Fix template:** `chain the scope as ancestor: rewrite selector with the scope class as ancestor and double the modifier class`.
|
|
47
|
+
- **Severity:** high.
|
|
48
|
+
|
|
49
|
+
### #5b — `!important` without an ancestor in the selector
|
|
50
|
+
|
|
51
|
+
- **Symptom:** theme has both ancestor specificity AND !important, so a bare-selector !important rule still loses.
|
|
52
|
+
- **Check (mechanical):** a rule whose body contains `!important` and whose selector branches all lack a descendant combinator before the rightmost compound selector.
|
|
53
|
+
- **Fix template:** `prefix the selector with the scope as ancestor`.
|
|
54
|
+
- **Severity:** high.
|
|
55
|
+
|
|
56
|
+
### #6 — Overlap-image modeled as a box on the wrapper
|
|
57
|
+
|
|
58
|
+
- **Symptom:** the colored band is rendered as a sibling/box on the column wrapper instead of a `::before` on the section. Stacking is fragile and breaks at narrow widths.
|
|
59
|
+
- **Check (mechanical):** a `::before` rule whose selector targets a `wrap|wrapper|container|inner` class with `position: absolute` + an inset/edge declaration.
|
|
60
|
+
- **Fix template:** `move the ::before to the section selector; set position: relative on the section and z-index: -1 on the ::before`.
|
|
61
|
+
- **Severity:** medium.
|
|
62
|
+
|
|
63
|
+
### #8 — Inline SVG impostor where a raster `<img>` belongs
|
|
64
|
+
|
|
65
|
+
- **Symptom:** a hand-drawn SVG approximating a product photo. Looks "off" and misses real-world detail.
|
|
66
|
+
- **Check (mechanical):** an `<svg>` with `viewBox` whose larger dimension ≥ 400, ≥ 2 `<path>` children, and a `class`/parent class matching `product|photo|hero|image|card|variant`.
|
|
67
|
+
- **Fix template:** `replace the <svg> with <img src="{assetsMap[originalUrl].localPath}" alt="…" loading="lazy">`.
|
|
68
|
+
- **Severity:** medium.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Group 2 — Design quality (a11y / performance / web standards)
|
|
73
|
+
|
|
74
|
+
12 baseline checks. These are not opinions — they are baselines every production fragment must clear. The script flags presence/absence; the LLM judges semantic quality (e.g. "is this alt text *meaningful*?").
|
|
75
|
+
|
|
76
|
+
### a11y-button-aria — icon-only `<button>` without `aria-label`
|
|
77
|
+
|
|
78
|
+
- **Check:** a `<button>` whose visible text is empty (or one emoji/glyph) with no `aria-label`/`aria-labelledby` and no inner `<img alt="…">`.
|
|
79
|
+
- **Fix:** `add aria-label="<inferred action>"` (Close, Open menu, Search, etc).
|
|
80
|
+
- **Severity:** high.
|
|
81
|
+
|
|
82
|
+
### a11y-img-alt — `<img>` missing `alt`
|
|
83
|
+
|
|
84
|
+
- **Check:** any `<img>` with no `alt` attribute (empty `alt=""` is fine — that means decorative, intentional).
|
|
85
|
+
- **Fix:** `add alt="" (decorative) or alt="<short description>"`.
|
|
86
|
+
- **Severity:** high.
|
|
87
|
+
|
|
88
|
+
### a11y-heading-hierarchy — broken outline
|
|
89
|
+
|
|
90
|
+
- **Checks:**
|
|
91
|
+
- first heading is `<h3>` or deeper (no h1/h2 above) → medium.
|
|
92
|
+
- multiple `<h1>` elements → medium.
|
|
93
|
+
- skip in level (`<h2>` followed by `<h4>`) → low.
|
|
94
|
+
- **Fix:** specific to the case (demote, change to h2, fix the level).
|
|
95
|
+
|
|
96
|
+
### a11y-button-type — `<button>` without explicit `type`
|
|
97
|
+
|
|
98
|
+
- **Check:** `<button>` with no `type` attribute (or a non-standard value).
|
|
99
|
+
- **Fix:** `add type="button"`.
|
|
100
|
+
- **Severity:** medium.
|
|
101
|
+
|
|
102
|
+
### a11y-nav-semantic — main nav as `<div>`
|
|
103
|
+
|
|
104
|
+
- **Check:** a `<div>` with class containing `nav|menu|navigation` (excluding `footer`/`sub`/`breadcrumb`) that contains ≥ 2 `<a>` children and is not inside a `<nav>`.
|
|
105
|
+
- **Fix:** `change <div class="…"> to <nav class="…" aria-label="Main">`.
|
|
106
|
+
- **Severity:** medium.
|
|
107
|
+
|
|
108
|
+
### perf-img-lazy — non-hero `<img>` without `loading="lazy"`
|
|
109
|
+
|
|
110
|
+
- **Check:** an `<img>` that is not the hero (heuristic: has `fetchpriority=high`, ancestor class `hero`, or first `<img>` in document) and does not declare `loading="lazy"`/`loading="eager"`.
|
|
111
|
+
- **Fix:** `add loading="lazy"`.
|
|
112
|
+
- **Severity:** medium.
|
|
113
|
+
|
|
114
|
+
### perf-hero-preload — hero without preload link
|
|
115
|
+
|
|
116
|
+
Two-step detection (Pattern #25 of the plan — without it, builds where the hero is a CSS `background-image` get a false positive on the logo `<img>`):
|
|
117
|
+
|
|
118
|
+
1. **CSS background-image hero.** Scan the scope CSS for a rule whose selector
|
|
119
|
+
is a single root-scope class (`.foo` or `.foo:pseudo`, no descendant
|
|
120
|
+
combinator, no `__` BEM modifier) and whose body declares
|
|
121
|
+
`background-image: url(...)`. The `url(...)` of the **first** matching rule
|
|
122
|
+
IS the hero — verify a `<link rel="preload" as="image" href="<bg-url>">`
|
|
123
|
+
exists, flag against that URL otherwise.
|
|
124
|
+
|
|
125
|
+
2. **`<img>` hero (only when no CSS-bg hero was found).** Confidence-tiered:
|
|
126
|
+
`fetchpriority="high"` → ancestor class containing `hero` → `src`/`alt`
|
|
127
|
+
contains `hero|banner|cover` → declared width ≥ 800px → first `<img>` (last
|
|
128
|
+
resort, **skipped** here so logos don't false-positive).
|
|
129
|
+
|
|
130
|
+
3. **Neither tier matches.** Skip the check entirely — no false positive on
|
|
131
|
+
pages without an obvious hero.
|
|
132
|
+
|
|
133
|
+
- **Fix:** `add <link rel="preload" as="image" href="{heroUrl}">` at the top of the fragment.
|
|
134
|
+
- **Severity:** medium.
|
|
135
|
+
|
|
136
|
+
### perf-font-display — Google Fonts without `&display=swap`
|
|
137
|
+
|
|
138
|
+
- **Check:** `<link rel="stylesheet" href="…fonts.googleapis.com…">` whose href has no `display=swap` query parameter.
|
|
139
|
+
- **Fix:** `append &display=swap to the href`.
|
|
140
|
+
- **Severity:** medium.
|
|
141
|
+
|
|
142
|
+
### perf-fetchpriority-hero — Shopify hero without `fetchpriority="high"`
|
|
143
|
+
|
|
144
|
+
- **Preset:** `shopify-section` only.
|
|
145
|
+
- **Check:** the hero `<img>` has no `fetchpriority="high"`.
|
|
146
|
+
- **Fix:** `add fetchpriority="high" to the hero <img>`.
|
|
147
|
+
- **Severity:** low.
|
|
148
|
+
|
|
149
|
+
### web-srcset-responsive — wide `<img>` without `srcset`
|
|
150
|
+
|
|
151
|
+
- **Check:** an `<img>` with explicit width > 800px (via `width=…` or inline `style="width: …px"`) and no `srcset` attribute. Without an explicit width we don't flag — too noisy.
|
|
152
|
+
- **Fix:** `add srcset + sizes; the orchestrator can fill from inspection`.
|
|
153
|
+
- **Severity:** low.
|
|
154
|
+
|
|
155
|
+
### web-modal-dialog — modal as `<div>`
|
|
156
|
+
|
|
157
|
+
- **Check:** a `<div>` with `role="dialog"` or class containing `modal` that is not inside a `<dialog>`.
|
|
158
|
+
- **Fix:** `change to <dialog>; open with showModal(), close with close()`.
|
|
159
|
+
- **Severity:** medium.
|
|
160
|
+
|
|
161
|
+
### web-preconnect-fonts — Google Fonts without preconnect
|
|
162
|
+
|
|
163
|
+
- **Check:** at least one `<link rel="stylesheet" href="…fonts.googleapis.com…">` exists, but no preconnect to `fonts.googleapis.com` and/or `fonts.gstatic.com`.
|
|
164
|
+
- **Fix:** `add the missing <link rel="preconnect" href="…"> entries before the Google Fonts stylesheet`.
|
|
165
|
+
- **Severity:** low.
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## Group 3 — Cross-reference (when `--compare-diffs` is supplied)
|
|
170
|
+
|
|
171
|
+
Not a separate check list — a layer applied on top of groups 1 & 2.
|
|
172
|
+
|
|
173
|
+
- For each violation, look up its `location` against high-severity entries in `compareDiffs.structuredDiffs`. If they overlap (e.g. a button-related violation and a high-severity button color diff), the violation is escalated to `severity: high` and tagged with `correlatedDiff`.
|
|
174
|
+
- For each high-severity diff with no static-rule backing, synthesize a violation directly from the diff measurements so it joins the prioritized list. Its `candidateFix` names the concrete value to change ("update `h1` from build value `24px` to live-page value `27px`").
|
|
175
|
+
- Result: one prioritized list. The orchestrator never has to reason across two parallel sources.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Output contract
|
|
180
|
+
|
|
181
|
+
Every emitted violation has the shape:
|
|
182
|
+
|
|
183
|
+
```json
|
|
184
|
+
{
|
|
185
|
+
"group": "anti-pattern" | "a11y" | "performance" | "web-standards" | "visual-diff" | "design-quality",
|
|
186
|
+
"id": "#5b" | "a11y-button-aria" | "diff-h1",
|
|
187
|
+
"location": "selector `…`" | "<tag …>" | "style block line N",
|
|
188
|
+
"issue": "<one-sentence human-readable description of the problem>",
|
|
189
|
+
"candidateFix": "<specific patch instruction — names the selector/attribute/property>",
|
|
190
|
+
"severity": "high" | "medium" | "low",
|
|
191
|
+
"correlatedDiff": { "area": "…", "issue": "…", "deltaPx": -3 } // optional, only when cross-referenced
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
The hard rule: `candidateFix` must be specific enough that `sb-build-{wp,shopify}` can apply it programmatically as a `fixHint`. Generic prose ("improve specificity", "fix accessibility") is forbidden — that is the failure mode this skill exists to prevent.
|