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.
Files changed (76) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/LICENSE +21 -0
  3. package/README.md +301 -0
  4. package/bin/install.js +256 -0
  5. package/lib/copy-templates.mjs +52 -0
  6. package/lib/install-deps.mjs +62 -0
  7. package/lib/prompt-config.mjs +83 -0
  8. package/lib/verify-env.mjs +19 -0
  9. package/package.json +63 -0
  10. package/scripts/sync-templates.mjs +71 -0
  11. package/templates/commands/build-page.md +490 -0
  12. package/templates/commands/build-site.md +548 -0
  13. package/templates/commands/clip-section.md +519 -0
  14. package/templates/memory/anti-patterns.md +212 -0
  15. package/templates/memory/design-knowledge.md +225 -0
  16. package/templates/memory/fixes.md +163 -0
  17. package/templates/memory/patterns.md +681 -0
  18. package/templates/presets/shopify-section.yaml +51 -0
  19. package/templates/presets/wp-elementor.yaml +49 -0
  20. package/templates/reports/fixtures/mock-run-1.json +115 -0
  21. package/templates/reports/fixtures/mock-run-2.json +72 -0
  22. package/templates/reports/report-renderer.mjs +218 -0
  23. package/templates/reports/report-template.html +571 -0
  24. package/templates/skills/sb-build-shopify/SKILL.md +104 -0
  25. package/templates/skills/sb-build-shopify/references/shopify-build-rules.md +563 -0
  26. package/templates/skills/sb-build-shopify/scripts/build-shopify.mjs +637 -0
  27. package/templates/skills/sb-build-shopify/scripts/tests/test-build-shopify.mjs +424 -0
  28. package/templates/skills/sb-build-wp/SKILL.md +83 -0
  29. package/templates/skills/sb-build-wp/references/wp-build-rules.md +376 -0
  30. package/templates/skills/sb-build-wp/scripts/build-wp.mjs +327 -0
  31. package/templates/skills/sb-build-wp/scripts/tests/test-build-wp.mjs +224 -0
  32. package/templates/skills/sb-compare-visual/SKILL.md +121 -0
  33. package/templates/skills/sb-compare-visual/scripts/compare-visual.mjs +387 -0
  34. package/templates/skills/sb-compare-visual/scripts/lib/compare-tokens.mjs +273 -0
  35. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-tokens.mjs +350 -0
  36. package/templates/skills/sb-compare-visual/scripts/tests/test-compare-visual.mjs +626 -0
  37. package/templates/skills/sb-crawl-and-list/SKILL.md +99 -0
  38. package/templates/skills/sb-crawl-and-list/scripts/crawl-and-list.mjs +437 -0
  39. package/templates/skills/sb-crawl-and-list/scripts/lib/blocklist-filter.mjs +176 -0
  40. package/templates/skills/sb-crawl-and-list/scripts/lib/fallback-crawler.mjs +107 -0
  41. package/templates/skills/sb-crawl-and-list/scripts/lib/page-classifier.mjs +89 -0
  42. package/templates/skills/sb-crawl-and-list/scripts/lib/sitemap-parser.mjs +118 -0
  43. package/templates/skills/sb-crawl-and-list/scripts/tests/test-blocklist-filter.mjs +204 -0
  44. package/templates/skills/sb-crawl-and-list/scripts/tests/test-crawl-and-list.mjs +276 -0
  45. package/templates/skills/sb-crawl-and-list/scripts/tests/test-fallback-crawler.mjs +243 -0
  46. package/templates/skills/sb-crawl-and-list/scripts/tests/test-page-classifier.mjs +120 -0
  47. package/templates/skills/sb-crawl-and-list/scripts/tests/test-sitemap-parser.mjs +157 -0
  48. package/templates/skills/sb-extract-assets/SKILL.md +112 -0
  49. package/templates/skills/sb-extract-assets/scripts/extract-assets.mjs +484 -0
  50. package/templates/skills/sb-extract-assets/scripts/tests/test-extract-assets.mjs +112 -0
  51. package/templates/skills/sb-inspect-live/SKILL.md +105 -0
  52. package/templates/skills/sb-inspect-live/scripts/inspect-live.mjs +693 -0
  53. package/templates/skills/sb-inspect-live/scripts/tests/test-inspect-live.mjs +181 -0
  54. package/templates/skills/sb-review-checks/SKILL.md +113 -0
  55. package/templates/skills/sb-review-checks/references/review-rules.md +195 -0
  56. package/templates/skills/sb-review-checks/scripts/lib/anti-patterns.mjs +379 -0
  57. package/templates/skills/sb-review-checks/scripts/lib/cross-reference.mjs +115 -0
  58. package/templates/skills/sb-review-checks/scripts/lib/design-quality.mjs +541 -0
  59. package/templates/skills/sb-review-checks/scripts/review-checks.mjs +250 -0
  60. package/templates/skills/sb-review-checks/scripts/tests/test-anti-patterns.mjs +343 -0
  61. package/templates/skills/sb-review-checks/scripts/tests/test-cross-reference.mjs +170 -0
  62. package/templates/skills/sb-review-checks/scripts/tests/test-design-quality.mjs +493 -0
  63. package/templates/skills/sb-review-checks/scripts/tests/test-review-checks.mjs +267 -0
  64. package/templates/skills/sb-tweak/SKILL.md +130 -0
  65. package/templates/skills/sb-tweak/references/tweak-patterns.md +157 -0
  66. package/templates/skills/sb-tweak/scripts/lib/diff-summarizer.mjs +140 -0
  67. package/templates/skills/sb-tweak/scripts/lib/element-locator.mjs +507 -0
  68. package/templates/skills/sb-tweak/scripts/lib/intent-parser.mjs +324 -0
  69. package/templates/skills/sb-tweak/scripts/tests/test-diff-summarizer.mjs +248 -0
  70. package/templates/skills/sb-tweak/scripts/tests/test-element-locator.mjs +418 -0
  71. package/templates/skills/sb-tweak/scripts/tests/test-intent-parser.mjs +496 -0
  72. package/templates/skills/sb-tweak/scripts/tests/test-tweak.mjs +407 -0
  73. package/templates/skills/sb-tweak/scripts/tweak.mjs +656 -0
  74. package/templates/skills/sb-validate-render/SKILL.md +120 -0
  75. package/templates/skills/sb-validate-render/scripts/tests/test-validate-render.mjs +304 -0
  76. package/templates/skills/sb-validate-render/scripts/validate-render.mjs +645 -0
@@ -0,0 +1,212 @@
1
+ # Anti-patterns — bundled canon
2
+
3
+ This file is the **bundled global canon** of build anti-patterns for the SimilarBuild framework. It is read by `sb-build-wp`, `sb-build-shopify`, and `sb-review-checks` via the cascade defined in Pattern #21:
4
+
5
+ 1. `<plugin>/memory/anti-patterns.md` (this file — versioned, distributed by the installer)
6
+ 2. `~/.claude/similarbuild-memory/anti-patterns.md` (per-user auto-learn — overrides/adds)
7
+ 3. `<skill>/references/*.md` (bundled fallback inside the skill itself — only when neither of the above exists)
8
+
9
+ Each entry is **machine-parseable**: same shape, same fields, same order. `sb-review-checks` parses this file to drive its detection layer; `sb-build-{wp,shopify}` reads it to know what NOT to emit.
10
+
11
+ ---
12
+
13
+ ## Entry shape
14
+
15
+ Every anti-pattern below has these fields, in this order:
16
+
17
+ - **id** — stable identifier (`#1`..`#16`).
18
+ - **name** — short human-readable title.
19
+ - **applies-to** — `wp` | `shopify` | `any`.
20
+ - **applies-when** — section type or composition context where the rule fires (`hero`, `button`, `image_picker fallback`, `any`, ...).
21
+ - **symptom** — the visible failure when the anti-pattern slips through.
22
+ - **fix-recipe** — concrete patch instruction (specific enough to feed `fixHints` to `sb-build-{wp,shopify}` programmatically).
23
+ - **detector** — mechanical signal `sb-review-checks` uses (regex / AST shape / attribute presence). `process` means the check is enforced upstream by another skill, not by review-checks.
24
+ - **severity** — `high` | `medium` | `low`.
25
+ - **origin** — where this entry came from (bootstrap migration, smoke test phase + date, Shopify build construction, etc).
26
+
27
+ ---
28
+
29
+ ## Group 1 — Bootstrap canon (#1–#9)
30
+
31
+ Distilled from the Alpha Infuse Shopify→WP migration (~50-turn live session). These are the structural failure modes that every WP/Elementor build has to defend against; most also apply 1:1 to Shopify with the Dawn theme reset substituting Elementor's.
32
+
33
+ ### #1 — Hero `100vh`
34
+
35
+ - **applies-to:** any
36
+ - **applies-when:** hero / full-bleed top section
37
+ - **symptom:** mobile browser chrome (URL bar collapse, safe-area insets) makes `100vh` taller than the visible viewport, so the hero clips below the fold and CTAs are pushed off-screen.
38
+ - **fix-recipe:** replace `height: 100vh` on the hero container with `aspect-ratio: 1 / 1.3; max-height: 700px` (mobile) and `aspect-ratio: 16 / 9; max-height: 720px` inside `@media (min-width: 1000px)`.
39
+ - **detector:** any occurrence of `100vh` anywhere in the scope CSS.
40
+ - **severity:** high
41
+ - **origin:** bootstrap (Alpha Infuse migration)
42
+
43
+ ### #2 — `<img>` with `height: 100%` for full-bleed hero
44
+
45
+ - **applies-to:** any
46
+ - **applies-when:** hero / full-bleed image section
47
+ - **symptom:** Elementor injects `img { height: auto !important }` (Dawn injects `img { max-width: 100%; height: auto; }`) which clobbers the inline height; the hero collapses to natural image height and breaks the section's intended aspect.
48
+ - **fix-recipe:** replace the `<img>` hero with `background-image: url(...)` on the container; set `background-size: cover` and `background-position: center`; use the asset's `localPath` (WP) or `image_url` filter (Shopify).
49
+ - **detector:** an `<img>` whose ancestor matches `class~="hero"` and whose inline `style` contains `height: 100%`.
50
+ - **severity:** high
51
+ - **origin:** bootstrap (Alpha Infuse migration)
52
+
53
+ ### #3 — Round-number opacity (eyeballed, not measured)
54
+
55
+ - **applies-to:** any
56
+ - **applies-when:** any overlay / scrim / faded layer
57
+ - **symptom:** opacity values `0.3`, `0.5`, `0.7` are nearly always guesses; the live source's real reading is an arbitrary number like `0.42`, and the build looks "almost right but off".
58
+ - **fix-recipe:** read the exact alpha from `inspection.tokens` or the inspected element's computed style; replace the round value with the literal token; mark `/* alpha from inspection */` once verified.
59
+ - **detector:** regex `opacity\s*:\s*0?\.[357]` in any style block.
60
+ - **severity:** low
61
+ - **origin:** bootstrap (Alpha Infuse migration)
62
+
63
+ ### #4 — `a { color: inherit }` clobbering button color
64
+
65
+ - **applies-to:** any
66
+ - **applies-when:** any CTA rendered as `<a>` (or any anchor inside a scoped section)
67
+ - **symptom:** the anchor inherits the themed ancestor color (`.elementor a { color: inherit !important }` on WP; `.shopify-section a { color: rgb(var(--color-link)) }` on Dawn), brand color is lost, link looks like body text.
68
+ - **fix-recipe:** replace `color: inherit` with a literal hex from `inspection.tokens`; if the CTA must remain an `<a>`, set its color literally and chain the scope class as ancestor; prefer a real `<button>` for action affordances.
69
+ - **detector:** any rule whose selector ends in `a` (or `a.x` / `a:hover`) with `color: inherit` in the body.
70
+ - **severity:** medium
71
+ - **origin:** bootstrap (Alpha Infuse migration)
72
+
73
+ ### #5 — `<button>` without defensive specificity (includes #5b: bare `!important`)
74
+
75
+ This is the bundled defensive-specificity rule. Two failure modes share the same root cause; both are flagged here.
76
+
77
+ - **applies-to:** any
78
+ - **applies-when:** any styled `<button>` / `.btn` / `.cta` element
79
+ - **symptom (#5):** theme `.elementor *` or `.shopify-section button` rules win over single-class selectors; the button reverts to theme-default appearance the moment Elementor or Dawn loads.
80
+ - **symptom (#5b):** even with `!important`, a bare-selector rule still loses, because the theme has *both* ancestor specificity AND `!important` — yours has only `!important`.
81
+ - **fix-recipe:** rewrite the selector with the scope class as ancestor AND double the modifier class (`.{scope} .{scope}__cta.{scope}__cta`); apply `!important` ONLY on the four font properties (`font-family`, `font-size`, `font-weight`, `color`) plus background and color when the theme bleeds through; never `!important` margins/paddings/widths/heights/transforms.
82
+ - **detector (#5):** 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.
83
+ - **detector (#5b):** a rule whose body contains `!important` and whose selector branches all lack a descendant combinator before the rightmost compound selector.
84
+ - **severity:** high
85
+ - **origin:** bootstrap (Alpha Infuse migration)
86
+
87
+ ### #6 — Overlap-image modeled as a sibling box
88
+
89
+ - **applies-to:** any
90
+ - **applies-when:** image-with-text overlap section (Pattern D)
91
+ - **symptom:** the colored band behind the overlapping image is rendered as a sibling/box on the column wrapper instead of a `::before` on the section. Stacking is fragile, breaks at narrow widths, and the band's bounds don't match the live source.
92
+ - **fix-recipe:** move the `::before` to the section selector; set `position: relative` on the section and `z-index: -1` on the `::before`; pull the band's `inset` values from `inspection.pseudoElements` (do not eyeball).
93
+ - **detector:** a `::before` rule whose selector targets a `wrap|wrapper|container|inner` class with `position: absolute` + an inset/edge declaration.
94
+ - **severity:** medium
95
+ - **origin:** bootstrap (Alpha Infuse migration)
96
+
97
+ ### #7 — Showing output before validating in Playwright
98
+
99
+ - **applies-to:** any
100
+ - **applies-when:** any build, before delivery
101
+ - **symptom:** the user sees a build that looks right in the file but renders wrong inside Elementor / Dawn because no aggressive theme reset was injected during preview; bugs surface only after the merchant pastes it in.
102
+ - **fix-recipe:** every build must pass through `sb-validate-render` before being shown to the user; the skill injects an aggressive `.elementor *` (or `.shopify-section *`) reset and screenshots the rendered fragment for `sb-compare-visual` to diff.
103
+ - **detector:** process — enforced by the orchestrator pipeline, not by review-checks.
104
+ - **severity:** high
105
+ - **origin:** bootstrap (Alpha Infuse migration)
106
+
107
+ ### #8 — Inline SVG impostor where a raster `<img>` belongs
108
+
109
+ - **applies-to:** any
110
+ - **applies-when:** any `<svg>` block whose visual intent is a real-world photo (product, hero, variant card)
111
+ - **symptom:** a hand-drawn SVG approximating a product photo. Looks "off" and misses real-world detail (skin texture, packaging, lighting). Smells of LLM hallucination, not lived design.
112
+ - **fix-recipe:** replace the `<svg>` with `<img src="{assetsMap[originalUrl].localPath}" alt="..." loading="lazy">`; the actual asset is already in `assetsMap`; inline SVG is reserved for icons that came from the source as SVG, not for raster impostors.
113
+ - **detector:** an `<svg>` with `viewBox` whose larger dimension ≥ 400, ≥ 2 `<path>` children, and a `class` / parent class matching `product|photo|hero|image|card|variant`.
114
+ - **severity:** medium
115
+ - **origin:** bootstrap (Alpha Infuse migration)
116
+
117
+ ### #9 — Playwright headless can't trigger third-party widgets
118
+
119
+ - **applies-to:** any
120
+ - **applies-when:** inspection of a page with Klaviyo / GemPages / Yotpo / Shopify product-recommendation widgets
121
+ - **symptom:** widget never renders during inspection, so the section appears empty in the build; LLM tries to fabricate URL or invent markup, producing broken output.
122
+ - **fix-recipe:** fall back through the Plan B chain — try Playwright stealth (`playwright-extra` + `puppeteer-extra-plugin-stealth`) → ask the user for the URL of the same widget on an adjacent page that DOES respond → inspect that adjacent URL and lift markup. Never fabricate a substitute.
123
+ - **detector:** process — enforced by `sb-inspect-live` (composite `widgetBlocked` signal in inspection JSON).
124
+ - **severity:** high
125
+ - **origin:** bootstrap (Alpha Infuse migration)
126
+
127
+ ---
128
+
129
+ ## Group 2 — Discovered during Shopify build construction (#10–#16)
130
+
131
+ Surfaced during construction of `sb-build-shopify` (2026-05-04) and the must-fix bundle that followed (2026-05-04 → 2026-05-05). All Shopify-specific unless noted otherwise.
132
+
133
+ ### #10 — Schema setting `id` rename
134
+
135
+ - **applies-to:** shopify
136
+ - **applies-when:** any `{% schema %}` block, any setting / block setting
137
+ - **symptom:** once a section ships, renaming a setting `id` orphans the merchant's edits — the new id reads as empty, and the merchant's prior values are silently lost.
138
+ - **fix-recipe:** pick stable, semantic ids upfront (`heading`, not `top_text`; `cta_label`, not `button_1_text`); never rename across schema revisions; if the meaning genuinely changes, ship a new id alongside the old one and migrate via theme-app code.
139
+ - **detector:** repository-level diff; not detectable from a single build. (Recommended future check in `sb-review-checks`: warn when a setting id contains UI-driven words like `top_`, `button_1_`, `text_2_`.)
140
+ - **severity:** high (irreversible)
141
+ - **origin:** sb-build-shopify construction (2026-05-04)
142
+
143
+ ### #11 — `image_picker` with `default`
144
+
145
+ - **applies-to:** shopify
146
+ - **applies-when:** any `image_picker` setting in `{% schema %}`
147
+ - **symptom:** Shopify schema spec rejects `default` on `image_picker`. When silently accepted by older theme runtimes, the setting renders with a placeholder image that the merchant cannot easily replace ("logo random aparecendo até alguém trocar"), reading as a bug.
148
+ - **fix-recipe:** emit `image_picker` settings WITHOUT `default`; expose the placeholder via the Liquid `{% if section.settings.X %} ... {% else %} ... {% endif %}` fallback; use the 208-byte base64 SVG placeholder (see #16) for the `{% else %}` branch.
149
+ - **detector:** parse the schema JSON; flag any object where `type === "image_picker"` and `default` is present.
150
+ - **severity:** high
151
+ - **origin:** sb-build-shopify construction (2026-05-04)
152
+
153
+ ### #12 — Missing `presets`
154
+
155
+ - **applies-to:** shopify
156
+ - **applies-when:** any section
157
+ - **symptom:** without at least one preset, the merchant cannot insert the section from "Add section" in the theme editor — it stays invisible in the picker. Section is technically valid but practically uninstallable.
158
+ - **fix-recipe:** add at least one preset to the schema, using inspected values as the preset's defaults; reasonable categories: `Image`, `Text`, `Banners`, `Promotional`. Example: `"presets": [{ "name": "Hero — clone of source", "category": "Image" }]`.
159
+ - **detector:** parse the schema JSON; flag when `presets` is missing or `presets.length === 0`.
160
+ - **severity:** high
161
+ - **origin:** sb-build-shopify construction (2026-05-04)
162
+
163
+ ### #13 — Missing `{{ block.shopify_attributes }}` on iterated block markup
164
+
165
+ - **applies-to:** shopify
166
+ - **applies-when:** any section with `{% for block in section.blocks %}` iteration
167
+ - **symptom:** the editor cannot link the block UI to the rendered element — merchant clicks/drag/reorder produces no visible response, UI breaks silently. No error is raised.
168
+ - **fix-recipe:** every block-iterated element must carry `{{ block.shopify_attributes }}` on the outermost element of its markup (e.g. `<article {{ block.shopify_attributes }}>`).
169
+ - **detector:** for each `{% for block in section.blocks %}` ... `{% endfor %}` body, require at least one `{{ block.shopify_attributes }}` interpolation.
170
+ - **severity:** high
171
+ - **origin:** sb-build-shopify construction (2026-05-04)
172
+
173
+ ### #14 — Google Fonts `<link>` inside the section
174
+
175
+ - **applies-to:** shopify
176
+ - **applies-when:** any section that wants a custom typeface
177
+ - **symptom:** technically renders, but Shopify owns `<head>` and the theme already loads fonts via `font_face` filter or theme settings. `<link>` inline duplicates the fetch on every page where the section appears, fights the theme's font-loading lifecycle, and makes the section feel un-Shopify.
178
+ - **fix-recipe:** if the theme's font is acceptable, use `font-family: inherit` on headings + chain-scope-override only weight/size; if a custom font is genuinely required, expose it as a `font_picker` setting and consume via `{{ section.settings.X | font_face: font_display: 'swap' }}` inside `{% style %}`.
179
+ - **detector:** any `<link rel="stylesheet" href="...fonts.googleapis.com...">` or `<link rel="preconnect" href="...fonts.gstatic.com">` in a Shopify build.
180
+ - **severity:** medium
181
+ - **origin:** sb-build-shopify construction (2026-05-04)
182
+
183
+ ### #15 — Hardcoded CDN URLs in markup
184
+
185
+ - **applies-to:** shopify
186
+ - **applies-when:** any `<img>`, `<source>`, or `url(...)` referencing an external CDN
187
+ - **symptom:** hardcoded `cdn.shopify.com/...` or `cdn.{theme}.com/...` paths bypass Shopify's image transformation pipeline (no `image_url: width: N` variants), break when the merchant uploads a different asset, and orphan when the source CDN expires the URL.
188
+ - **fix-recipe:** for setting-driven images use `{{ section.settings.X | image_url: width: N }}` (and the corresponding `srcset` with multiple widths); for fallbacks use the base64 SVG placeholder (#16); never emit a literal `https://cdn...` URL.
189
+ - **detector:** regex `https?://[^"']*\.(shopify|cdn|cloudfront|cloudinary)[^"']*\.(jpg|jpeg|png|webp|gif|svg)` in markup.
190
+ - **severity:** high
191
+ - **origin:** sb-build-shopify construction (2026-05-04)
192
+
193
+ ### #16 — Developer filesystem paths in fallbacks
194
+
195
+ - **applies-to:** any
196
+ - **applies-when:** any fallback path in markup or CSS (`<img src>`, `url(...)`, `image_picker {% else %}` branch, default for a `text` setting, etc.)
197
+ - **symptom:** absolute filesystem paths from the developer's machine (`/Users/<dev>/...`, `/home/<dev>/...`, `C:\Users\...`) leak into distributable output; (a) `sb-validate-render` cannot resolve the path (no `file://` document base, Chromium blocks cross-origin), so the build screenshot shows a black rectangle and inflates `sb-compare-visual` diff%; (b) the path is meaningless on any other machine — merchant install, CI, another teammate.
198
+ - **fix-recipe:** represent fallbacks as one of (a) **base64 data URLs** for small placeholders (≤500 bytes — preferred for MVP); (b) URLs from the platform's CDN (Shopify `image_url` filter, theme assets); (c) empty settings that force the merchant to choose. The canonical placeholder for `image_picker` `{% else %}` branches is a 208-byte SVG (1170×900, fill `#d4d4d4`):
199
+ ```
200
+ data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMTcwIDkwMCIgcHJlc2VydmVBc3BlY3RSYXRpbz0ieE1pZFlNaWQgc2xpY2UiPjxyZWN0IHdpZHRoPSIxMTcwIiBoZWlnaHQ9IjkwMCIgZmlsbD0iI2Q0ZDRkNCIvPjwvc3ZnPg==
201
+ ```
202
+ - **detector:** regex `/(Users|home|opt)/[^/"'\s]+/` or `[A-Z]:\\(Users|Documents)\\` anywhere in the build output.
203
+ - **severity:** high
204
+ - **origin:** sb-build-shopify must-fix bundle (2026-05-04, generalized 2026-05-05)
205
+
206
+ ---
207
+
208
+ ## Maintenance
209
+
210
+ - New entries get appended in a new group with a header naming the discovery context and date.
211
+ - Numbering is stable and additive — never renumber.
212
+ - When an entry's detector becomes obsolete (e.g. the underlying tooling fixes it upstream), mark `detector: process` and add a `superseded-by:` note instead of deleting; the historical context is what makes the canon worth keeping.
@@ -0,0 +1,225 @@
1
+ # Design knowledge — bundled canon
2
+
3
+ This file is the **bundled global canon** of accessibility, performance, and web-standards checks for the SimilarBuild framework. It is read by `sb-review-checks` (the structural validator) and consulted by `sb-build-{wp,shopify}` when emitting markup, via the cascade defined in Pattern #21:
4
+
5
+ 1. `<plugin>/memory/design-knowledge.md` (this file — versioned, distributed by the installer)
6
+ 2. `~/.claude/similarbuild-memory/design-knowledge.md` (per-user auto-learn — overrides/adds)
7
+ 3. `<skill>/references/review-rules.md` (bundled fallback inside `sb-review-checks` itself — only when neither of the above exists)
8
+
9
+ These checks are **not opinions** — they are baselines every production fragment must clear. Each entry pairs a mechanical detector (regex / DOM shape) with a concrete fix template that `sb-review-checks` can emit as a `candidateFix` specific enough for `sb-build-{wp,shopify}` to apply programmatically. Generic prose ("improve specificity", "fix accessibility") is forbidden — that is the failure mode this layer exists to prevent.
10
+
11
+ The structural detectors do the mechanical work; the LLM judges semantic quality (is the alt text *meaningful*? is the heading hierarchy *coherent given content*?). Both layers run on every build.
12
+
13
+ ---
14
+
15
+ ## Entry shape
16
+
17
+ Every check below has these fields, in this order:
18
+
19
+ - **id** — stable identifier (`a11y-...`, `perf-...`, `web-...`).
20
+ - **name** — short human-readable title.
21
+ - **group** — `a11y` | `performance` | `web-standards`.
22
+ - **applies-to** — `wp` | `shopify` | `any` (some checks are preset-specific; flagged here).
23
+ - **applies-when** — composition context where the check fires.
24
+ - **symptom** — the user-visible failure when the check is missed.
25
+ - **fix-recipe** — concrete patch instruction (must be specific enough to feed `fixHints` to `sb-build-{wp,shopify}` programmatically).
26
+ - **detector** — mechanical signal (DOM query, regex, attribute presence/absence).
27
+ - **severity** — `high` | `medium` | `low`.
28
+ - **origin** — provenance (web-standards baseline, framework smoke test discovery, etc).
29
+
30
+ ---
31
+
32
+ ## Group A — Accessibility (5 checks)
33
+
34
+ ### a11y-img-alt — `<img>` missing `alt`
35
+
36
+ - **id:** a11y-img-alt
37
+ - **group:** a11y
38
+ - **applies-to:** any
39
+ - **applies-when:** any `<img>` element in the markup.
40
+ - **symptom:** screen readers either skip the image entirely or read a useless filename; the section becomes unusable for non-sighted users.
41
+ - **fix-recipe:** add `alt="<short description>"` (8–12 words, what the image conveys, not what it shows literally) for content images; add `alt=""` (empty, intentional) for decorative images. Source: `inspection.imgUrls[].alt` if present, else surrounding-context inference.
42
+ - **detector:** any `<img>` with no `alt` attribute. (Empty `alt=""` is fine — it's the intentional declaration of "decorative".)
43
+ - **severity:** high
44
+ - **origin:** WCAG 2.1 SC 1.1.1 (Non-text Content)
45
+
46
+ ### a11y-button-aria — icon-only `<button>` without `aria-label`
47
+
48
+ - **id:** a11y-button-aria
49
+ - **group:** a11y
50
+ - **applies-to:** any
51
+ - **applies-when:** any `<button>` whose visible text is empty or a single emoji/glyph (`☰`, `×`, `→`).
52
+ - **symptom:** screen readers announce "button" with no purpose; the action is invisible to non-sighted users.
53
+ - **fix-recipe:** add `aria-label="<inferred action>"` — `Close`, `Open menu`, `Search`, `Add to cart`. If the label might be merchant-edited (Shopify), expose an `aria_label` text setting alongside.
54
+ - **detector:** a `<button>` whose `textContent.trim()` is empty (or one emoji/glyph) AND has no `aria-label` AND no `aria-labelledby` AND no inner `<img alt="non-empty">`.
55
+ - **severity:** high
56
+ - **origin:** WCAG 2.1 SC 4.1.2 (Name, Role, Value)
57
+
58
+ ### a11y-heading-hierarchy — broken outline
59
+
60
+ - **id:** a11y-heading-hierarchy
61
+ - **group:** a11y
62
+ - **applies-to:** any
63
+ - **applies-when:** any section emitting headings.
64
+ - **symptom:** screen readers' heading-jump navigation produces a confusing structure; SEO crawlers struggle to extract the page's outline.
65
+ - **fix-recipe:** specific to the case — demote a duplicated `<h1>` to `<h2>`, raise a `<h3>` to `<h2>` when nothing precedes it, fill in the skipped level. The orchestrator may signal that this is a sub-section that should start at `<h2>` (not `<h1>`) — respect that.
66
+ - **detector:** three sub-checks: (1) first heading is `<h3>` or deeper with no `<h1>` / `<h2>` above → severity medium. (2) multiple `<h1>` elements in the same section → severity medium. (3) skip in level (`<h2>` followed directly by `<h4>`) → severity low.
67
+ - **severity:** medium (default; case (3) is low)
68
+ - **origin:** WCAG 2.1 SC 1.3.1 (Info and Relationships)
69
+
70
+ ### a11y-button-type — `<button>` without explicit `type`
71
+
72
+ - **id:** a11y-button-type
73
+ - **group:** a11y
74
+ - **applies-to:** any
75
+ - **applies-when:** any `<button>` element.
76
+ - **symptom:** the implicit `type` is `submit` — clicking the button can submit a parent form unexpectedly. Elementor pages often have hidden forms; Shopify product pages have add-to-cart forms; either silently swallows the click and triggers a submit.
77
+ - **fix-recipe:** add `type="button"` to every `<button>` that isn't explicitly a form submit (which should be vanishingly rare in section markup).
78
+ - **detector:** `<button>` with no `type` attribute, OR with a non-standard value (anything other than `button` / `submit` / `reset`).
79
+ - **severity:** medium
80
+ - **origin:** HTML spec (default `type=submit` is a footgun)
81
+
82
+ ### a11y-nav-semantic — main navigation as `<div>`
83
+
84
+ - **id:** a11y-nav-semantic
85
+ - **group:** a11y
86
+ - **applies-to:** any
87
+ - **applies-when:** the section emits a navigation region.
88
+ - **symptom:** screen readers' "skip to navigation" landmarks miss the region; keyboard users can't jump past it.
89
+ - **fix-recipe:** change `<div class="nav...">` to `<nav class="nav..." aria-label="Main">`. Use `aria-label` to disambiguate when the page has multiple `<nav>` regions (header nav, footer nav, sidebar).
90
+ - **detector:** a `<div>` with class containing `nav|menu|navigation` (excluding `footer|sub|breadcrumb`) that contains ≥ 2 `<a>` children and is NOT inside a `<nav>`.
91
+ - **severity:** medium
92
+ - **origin:** WCAG 2.1 SC 1.3.1 + ARIA Authoring Practices (Landmarks)
93
+
94
+ ---
95
+
96
+ ## Group B — Performance (4 checks)
97
+
98
+ ### perf-img-lazy — non-hero `<img>` without `loading="lazy"`
99
+
100
+ - **id:** perf-img-lazy
101
+ - **group:** performance
102
+ - **applies-to:** any
103
+ - **applies-when:** any `<img>` that is not the hero image.
104
+ - **symptom:** below-the-fold images load eagerly, competing with critical resources; LCP and TBT both regress.
105
+ - **fix-recipe:** add `loading="lazy"` to every non-hero `<img>`. The hero image stays `loading="eager"` (and on Shopify, also `fetchpriority="high"`).
106
+ - **detector:** an `<img>` that is NOT the hero (heuristic: lacks `fetchpriority="high"`, has no ancestor class containing `hero`, and is not the first `<img>` in the document) AND does not declare `loading="lazy"` or `loading="eager"`.
107
+ - **severity:** medium
108
+ - **origin:** Web.dev / Core Web Vitals (LCP optimization)
109
+
110
+ ### perf-hero-preload — hero without preload link
111
+
112
+ - **id:** perf-hero-preload
113
+ - **group:** performance
114
+ - **applies-to:** wp (Shopify owns `<head>`-level preloading; sections must NOT emit `<link rel="preload">`)
115
+ - **applies-when:** any WP build where the hero is identifiable.
116
+ - **symptom:** the LCP image fetch starts only after the CSS / image-element parse phase completes; LCP is delayed by hundreds of ms.
117
+ - **fix-recipe:** add `<link rel="preload" as="image" href="{heroUrl}">` at the top of the fragment. `heroUrl` is the `localPath` of the asset OR the URL inside the first matching CSS `background-image` rule.
118
+ - **detector:** two-step (Pattern #25 — without it, builds with CSS `background-image` heroes get a false positive on the logo `<img>`):
119
+ - **Step 1 — CSS background-image hero.** Scan the scope CSS for a rule whose selector is a single root-scope class (`.foo` or `.foo:pseudo`, no descendant combinator, no `__` BEM modifier) and whose body declares `background-image: url(...)`. The `url(...)` of the FIRST matching rule IS the hero — verify a `<link rel="preload" as="image" href="<bg-url>">` exists; flag against that URL otherwise.
120
+ - **Step 2 — `<img>` hero (only when no CSS-bg hero was found).** Confidence-tiered: `fetchpriority="high"` → ancestor class containing `hero` → `src` / `alt` contains `hero|banner|cover` → declared width ≥ 800px → first `<img>` (last resort, **skipped** here so logos don't false-positive).
121
+ - **Step 3 — neither matches.** Skip the check entirely. No false positive on pages without an obvious hero.
122
+ - **severity:** medium
123
+ - **origin:** Web.dev (LCP). Detector refined post-smoke (fixes.md #25, 2026-05-05).
124
+
125
+ ### perf-font-display — Google Fonts without `&display=swap`
126
+
127
+ - **id:** perf-font-display
128
+ - **group:** performance
129
+ - **applies-to:** wp (Shopify build emits no Google Fonts links — anti-pattern #14)
130
+ - **applies-when:** the WP build references Google Fonts via `<link rel="stylesheet">`.
131
+ - **symptom:** FOIT (flash of invisible text) — the page renders blank typography until the font loads, causing perceived slowness and a CLS spike when the system font swaps to the web font.
132
+ - **fix-recipe:** append `&display=swap` to the `href` query string of the Google Fonts stylesheet link.
133
+ - **detector:** `<link rel="stylesheet" href="...fonts.googleapis.com...">` whose href has no `display=swap` query parameter.
134
+ - **severity:** medium
135
+ - **origin:** Web.dev (FOIT/FOUT, font-display)
136
+
137
+ ### perf-fetchpriority-hero — Shopify hero without `fetchpriority="high"`
138
+
139
+ - **id:** perf-fetchpriority-hero
140
+ - **group:** performance
141
+ - **applies-to:** shopify
142
+ - **applies-when:** the Shopify build emits a hero `<img>` (settings-driven, `image_url` filter).
143
+ - **symptom:** without `fetchpriority`, the hero competes with theme assets in the browser's prioritization queue; LCP regresses by ~100–300ms on slower networks.
144
+ - **fix-recipe:** add `fetchpriority="high"` to the hero `<img>` (alongside `loading="eager"`).
145
+ - **detector:** the hero `<img>` (identified by the same confidence tier as `perf-hero-preload` step 2) has no `fetchpriority="high"`.
146
+ - **severity:** low
147
+ - **origin:** Web.dev (Priority Hints — LCP optimization)
148
+
149
+ ---
150
+
151
+ ## Group C — Web standards (3 checks)
152
+
153
+ ### web-srcset-responsive — wide `<img>` without `srcset`
154
+
155
+ - **id:** web-srcset-responsive
156
+ - **group:** web-standards
157
+ - **applies-to:** any
158
+ - **applies-when:** an `<img>` with explicit width > 800px (via `width="..."` attribute or inline `style="width: ...px"`).
159
+ - **symptom:** mobile devices download the desktop-sized asset; bandwidth waste, slower paint, worse LCP on cellular.
160
+ - **fix-recipe:** add `srcset` with multiple widths + `sizes` directive. WP: feed widths from `inspection.imgUrls[].naturalWidth` and `srcset` candidates from `assetsMap`. Shopify: use `{{ image | image_url: width: N }}` for each candidate width (Shopify CDN auto-generates variants); `sizes` value `"(min-width: 1000px) 50vw, 100vw"` for half-width desktop content, `"100vw"` for full-bleed.
161
+ - **detector:** an `<img>` with explicit width > 800px AND no `srcset` attribute. (Without an explicit width we don't flag — too noisy on intrinsic-sized images.)
162
+ - **severity:** low
163
+ - **origin:** HTML5 (`<img srcset sizes>`)
164
+
165
+ ### web-modal-dialog — modal as `<div>`
166
+
167
+ - **id:** web-modal-dialog
168
+ - **group:** web-standards
169
+ - **applies-to:** any
170
+ - **applies-when:** any modal / lightbox markup.
171
+ - **symptom:** custom-rolled `<div role="dialog">` modals miss `Esc`-to-close, focus trapping, the inert outer page, and the native `::backdrop` styling — keyboard and screen-reader users are stranded.
172
+ - **fix-recipe:** change `<div class="modal" role="dialog">` to `<dialog class="modal">`. Open with `dialog.showModal()`, close with `dialog.close()`. Style the backdrop via `dialog::backdrop`. No library, no custom focus-trap.
173
+ - **detector:** a `<div>` with `role="dialog"` OR class containing `modal` that is NOT inside a `<dialog>`.
174
+ - **severity:** medium
175
+ - **origin:** HTML Living Standard (`<dialog>`) + WCAG 2.1 SC 2.1.2 (No Keyboard Trap)
176
+
177
+ ### web-preconnect-fonts — Google Fonts without preconnect
178
+
179
+ - **id:** web-preconnect-fonts
180
+ - **group:** web-standards
181
+ - **applies-to:** wp (Shopify build emits no Google Fonts links — anti-pattern #14)
182
+ - **applies-when:** the WP build emits at least one `<link rel="stylesheet" href="...fonts.googleapis.com...">`.
183
+ - **symptom:** the browser establishes the TLS handshake with `fonts.googleapis.com` and `fonts.gstatic.com` only when the stylesheet starts parsing; ~150–300ms of head-of-line blocking before the font request fires.
184
+ - **fix-recipe:** add the missing `<link rel="preconnect">` entries before the stylesheet — `<link rel="preconnect" href="https://fonts.googleapis.com">` and `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`. The `crossorigin` attribute on `gstatic` is mandatory (the actual font files are CORS-fetched).
185
+ - **detector:** at least one `<link rel="stylesheet" href="...fonts.googleapis.com...">` exists, but no preconnect to `fonts.googleapis.com` AND/OR `fonts.gstatic.com` precedes it.
186
+ - **severity:** low
187
+ - **origin:** Web.dev (Resource hints — preconnect)
188
+
189
+ ---
190
+
191
+ ## Cross-reference layer (when `--compare-diffs` is supplied to `sb-review-checks`)
192
+
193
+ Not a separate check list — a layer applied on top of groups A, B, C.
194
+
195
+ - 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), **escalate** the violation to `severity: high` and tag with `correlatedDiff`.
196
+ - 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`").
197
+ - Result: one prioritized list. The orchestrator never has to reason across two parallel sources.
198
+
199
+ ---
200
+
201
+ ## Output contract
202
+
203
+ Every emitted violation has the shape:
204
+
205
+ ```json
206
+ {
207
+ "group": "a11y" | "performance" | "web-standards" | "anti-pattern" | "visual-diff" | "design-quality",
208
+ "id": "a11y-button-aria" | "#5b" | "diff-h1",
209
+ "location": "selector `…`" | "<tag …>" | "style block line N",
210
+ "issue": "<one-sentence human-readable description of the problem>",
211
+ "candidateFix": "<specific patch instruction — names the selector/attribute/property>",
212
+ "severity": "high" | "medium" | "low",
213
+ "correlatedDiff": { "area": "…", "issue": "…", "deltaPx": -3 }
214
+ }
215
+ ```
216
+
217
+ Hard rule (machine-parseability — Pattern #34): `candidateFix` MUST be specific enough that `sb-build-{wp,shopify}` can apply it programmatically as a `fixHint`. Generic prose is forbidden — that is the failure mode this layer exists to prevent.
218
+
219
+ ---
220
+
221
+ ## Maintenance
222
+
223
+ - New checks append within their group, with a stable id (`a11y-X`, `perf-X`, `web-X`).
224
+ - When a baseline shifts (new browser landing, WCAG update), update the affected entry in place and bump the file's frontmatter version (handled by the installer's auto-learn merge).
225
+ - Removing a check (e.g. browsers eliminate the underlying problem) — keep the entry, mark `severity: deprecated`, and note the date / browser version that obviated it; never silently delete.
@@ -0,0 +1,163 @@
1
+ # Fix recipes — bundled canon
2
+
3
+ This file is the **bundled global canon** of cross-skill fix recipes for the SimilarBuild framework. It captures the eleven framework-level fixes (Padrões #22–#32 from the build roadmap plan) discovered during smoke testing, codified so that future iterations of the framework — and skills that auto-learn from new sites — have a structured reference.
4
+
5
+ Read by `sb-build-{wp,shopify}` (when `fixHints` from `sb-review-checks` reference one of these IDs), by `sb-review-checks` (to phrase its `candidateFix` strings consistently), and by maintainers extending the framework. Cascade order is the same as the other memory files (Pattern #21):
6
+
7
+ 1. `<plugin>/memory/fixes.md` (this file — versioned, distributed by the installer)
8
+ 2. `~/.claude/similarbuild-memory/fixes.md` (per-user auto-learn — overrides/adds)
9
+ 3. Skill-internal patterns (where applicable)
10
+
11
+ ---
12
+
13
+ ## Entry shape
14
+
15
+ Every fix below has these fields, in this order:
16
+
17
+ - **id** — stable identifier (`#22`..`#32`).
18
+ - **name** — short title summarizing the fix.
19
+ - **applies-to** — which skill(s) the fix lives in (`sb-inspect-live`, `sb-validate-render`, `sb-compare-visual`, `sb-review-checks`, `sb-build-shopify`, orchestrator, ...).
20
+ - **applies-when** — the failure signature that should trigger this recipe.
21
+ - **symptom** — what was breaking before the fix, in user-visible / metric terms.
22
+ - **fix-recipe** — the actual change made, named at the level of file + helper + behavior. Specific enough that a maintainer can re-apply it from scratch on a new branch.
23
+ - **detector** — how a future regression would be caught (test name, smoke metric, or "process — caught by skill X").
24
+ - **origin** — discovery context + date.
25
+
26
+ The IDs `#22`–`#32` are stable framework-wide and align 1:1 with the "Padrões emergidos durante a construção" / "Anti-patterns descobertos pelo smoke test" sections of the build plan. Numbers below `#22` are non-fix learnings (composition patterns, output conventions) and live elsewhere.
27
+
28
+ ---
29
+
30
+ ## #22 — DPR alignment cross-skill
31
+
32
+ - **id:** #22
33
+ - **name:** Cross-skill device-pixel-ratio alignment
34
+ - **applies-to:** sb-validate-render, sb-inspect-live, sb-compare-visual
35
+ - **applies-when:** comparing a build screenshot to a live screenshot, where one was captured at DPR 1 and the other at DPR 3 (or any mismatch).
36
+ - **symptom:** screenshots have different intrinsic dimensions (live 1170w, build 780w in the example-store case) → `sb-compare-visual`'s pad-to-canvas path detects the entire offset region as diff → diff% inflated by tens of percentage points without any genuine fidelity issue. Smoke example-store hero: 94.88% → 52.2% on this fix alone (-42.68pp).
37
+ - **fix-recipe:** in `sb-validate-render/scripts/validate-render.mjs`, set `deviceScaleFactor: 3` on the Playwright context to match `sb-inspect-live`'s iPhone 14 device profile; add a comment naming Pattern #22; do NOT scale via CSS — set DPR at the browser context level so screenshot intrinsic dimensions match.
38
+ - **detector:** smoke metric — diff% on a known-good build should not regress past the post-#22 baseline. No unit test alters DPR explicitly; check is via the smoke pipeline.
39
+ - **origin:** smoke test `/build-page example-store.com` (2026-05-04)
40
+
41
+ ## #23 — `widgetBlocked` composite signal
42
+
43
+ - **id:** #23
44
+ - **name:** Multi-signal `widgetBlocked` heuristic
45
+ - **applies-to:** sb-inspect-live
46
+ - **applies-when:** any inspection of a real page; the legacy single-signal `bodyHtmlLen < 1024` heuristic fired false positives on minimalist real pages (`example.com` 528B, `info.cern.ch` 646B).
47
+ - **symptom:** `widgetBlocked: true` reported on legitimate pages → orchestrator escalates / aborts unnecessarily; user gets a "site blocked" message for a site that loads fine.
48
+ - **fix-recipe:** in `inspect-live.mjs`, replace the size-only check with a composite signal — flag `widgetBlocked=true` when ANY of: (a) `bodyHtmlLen < 256` (extreme-tiny — real pages always exceed); (b) match against expanded `BLOCK_PATTERNS` regex on body innerText (`/just a moment/`, `/checking your browser/`, `/captcha/`, `/forbidden/`, ...); (c) match against new `BLOCK_TITLE_PATTERNS` on `<title>` (Cloudflare's "Just a moment...", "Attention Required! | Cloudflare", "Access Denied"); (d) `bodyHtmlLen` 256–1024 AND `meaningfulChildren < 2` (small AND structurally bare = challenge page).
49
+ - **detector:** 9 unit tests in `test-inspect-live.mjs` covering example.com (NOT blocked), info.cern.ch (NOT blocked), extreme-tiny (blocked), small-and-bare (blocked), Cloudflare title-only (blocked), captcha keyword (blocked), large legitimate page (NOT blocked).
50
+ - **origin:** smoke test follow-up (2026-05-05)
51
+
52
+ ## #24 — Token probe scope-aware
53
+
54
+ - **id:** #24
55
+ - **name:** Validate-render token probe respects fragment scope
56
+ - **applies-to:** sb-validate-render
57
+ - **applies-when:** the rendered fragment uses scope class `.X` and the probe needs to read body-like typography from inside that scope, not from the global `<body>` HTML element.
58
+ - **symptom:** `tokens_build.body` returned `"Times"` / `"Times New Roman"` (the UA default for the bare HTML test page wrapping the fragment) instead of the scope's actual font; `sb-compare-visual`'s token cross-check then reported a phantom font-family mismatch.
59
+ - **fix-recipe:** in `validate-render.mjs`'s `probeInPage`, add `findBodyLikeInScope(scopeRoot)` helper. Search order: (1) first `<p>/<span>/<div>/<li>` inside the scope with non-empty direct text content; (2) the scope root itself when not `document.body`; (3) `document.body` (legacy fallback). Use the resulting element as `bodyEl` for `getComputedStyle`. The token reflects real fragment typography, not UA default.
60
+ - **detector:** 3 stub tests + 1 CLI test in `test-validate-render.mjs` cover helper logic (mocked); browser-driven E2E pending real chromium smoke run.
61
+ - **origin:** smoke test follow-up (2026-05-05)
62
+
63
+ ## #25 — `perf-hero-preload` two-step detection
64
+
65
+ - **id:** #25
66
+ - **name:** Hero detection — CSS background first, `<img>` confidence-tiered second
67
+ - **applies-to:** sb-review-checks
68
+ - **applies-when:** a build uses Pattern B (Full-bleed hero with `background-image`) and the check needs to know whether to flag a missing `<link rel="preload">`.
69
+ - **symptom:** the legacy single-step check picked the FIRST `<img>` in the fragment as "hero" — typically the logo — and flagged missing preload even when the correct `<link rel="preload" as="image">` for the background-image hero was present at the top of the file. False positive emitted as a misleading `candidateFix`.
70
+ - **fix-recipe:** in `scripts/lib/design-quality.mjs`, replace `checkHeroPreload` with a three-tier detector: (1) `findHeroCssBackgroundImage(css)` — first CSS rule whose selector is a single root-class (`.foo` or `.foo:pseudo`, no descendant combinator, no `__` BEM modifier) and whose body declares `background-image: url(...)`; flag if no preload matching that URL. (2) `findHeroImgWithConfidence($)` returning a tier — `explicit` (fetchpriority=high), `hero-ancestor` (class match), `name-match` (src/alt match `hero|banner|cover`), `wide` (width≥800), `fallback` (first `<img>`); SKIP the check when tier is `fallback` to avoid logo false positives. (3) Neither matches → skip silently.
71
+ - **detector:** 8 new tests in `test-design-quality.mjs` covering CSS-bg + preload present/absent, hero-bg + LOGO together (flag only the bg), bare-first-img skip, `<img>` ≥800px (flag), BEM-modifier rejection, descendant-combinator rejection, comma-selectors, pseudo-class.
72
+ - **origin:** smoke test follow-up (2026-05-05)
73
+
74
+ ## #26 — Scope-aware comparison (orchestrator passes bbox)
75
+
76
+ - **id:** #26
77
+ - **name:** Orchestrator forwards section boundingBox into the comparator
78
+ - **applies-to:** orchestrator (`/build-page`, `/build-site`, `/clip-section`), sb-compare-visual, sb-inspect-live
79
+ - **applies-when:** the build composes a single section (e.g. hero ~2500px tall) but the live screenshot is a full-page capture (~17000px). Without bbox awareness, padding-to-canvas paints every live-only region as diff and inflates diff% by ~38pp.
80
+ - **symptom:** comparing a section build against a full-page live screenshot reports diff% of "scope mismatch" rather than fidelity drift; `sb-build-*` cannot react meaningfully because there's no real defect to fix.
81
+ - **fix-recipe:** generalize the pattern as "wide-capture skill A → narrow-consume skill B passes A's bbox to B for cropping before measurement". Concretely: `sb-inspect-live` exposes `sectionBoundingBox` (BFS find of first section-signature descendant) in its JSON. Orchestrator passes `--crop-live-bbox "x,y,w,h"` to `sb-compare-visual`. (Generalization extends to: `sb-extract-assets` could filter `imgUrls` to bbox before downloading; `sb-review-checks` could limit geometric rules to scope.)
82
+ - **detector:** smoke metric — section builds must report diff% reflecting fidelity, not scope mismatch. 5 new tests in `test-compare-visual.mjs` cover crop parsing, dpr=1, dpr=3 multiplication, clamp, bbox-outside-screenshot, null path.
83
+ - **origin:** smoke test `/build-page example-store.com` (2026-05-04)
84
+
85
+ ## #27 — Symmetric crop in the comparator
86
+
87
+ - **id:** #27
88
+ - **name:** Crop both live AND build, not just one side
89
+ - **applies-to:** sb-compare-visual, orchestrator
90
+ - **applies-when:** after #26, ~27pp of phantom diff still came from the build screenshot containing whitespace below the section (validate-render captures full viewport 390×844 but the section is 390×507 → the 337 CSS-px below pads against the cropped live).
91
+ - **symptom:** asymmetric crop fixed live but left the build untrimmed → bottom whitespace of build screenshot vs corresponding region of cropped live → diff inflated by another bucket of phantom pixels.
92
+ - **fix-recipe:** add `--crop-build-bbox` flag to `sb-compare-visual` paralleling `--crop-live-bbox`. Extract `applyCrop()` helper to share logic between the two sides. Orchestrator (`build-page` Step 5) passes `render.geometry.sections[0].bbox` when `viewportOverflow === false` AND the section is meaningfully smaller than the viewport. Smoke example-store: 62.32% (live-only crop) → 35.72% (both crops). Δ -26.6pp.
93
+ - **detector:** 8 new tests in `test-compare-visual.mjs` (now 30 total) cover dual-crop math, clamp, bbox-outside-screenshot, null-path on each side independently.
94
+ - **origin:** smoke test `/build-page example-store.com` (2026-05-04). **Insight:** when fixing one side of a symmetric operation, audit the other side immediately — anti-pattern #10 was simpler than it looked.
95
+
96
+ ## #28 — `review-checks` as ground truth for escalation
97
+
98
+ - **id:** #28
99
+ - **name:** Decision matrix replaces diff%-threshold heuristic
100
+ - **applies-to:** orchestrator (`/build-page`, `/build-site`, `/clip-section`)
101
+ - **applies-when:** orchestrator decides whether to (a) deliver, (b) loop with `fixHints`, (c) escalate to user. The legacy "diff>50 → catastrophic, skip auto-correct" heuristic mis-fired whenever the build was structurally clean but the diff came from scope mismatch or genuine visual drift.
102
+ - **symptom:** clean builds with high diff% were sent into infinite auto-correct loops generating no-op fix attempts; broken builds with low diff% (e.g. coincidental near-match) were delivered without flagging the structural defect.
103
+ - **fix-recipe:** remove the diff-threshold heuristic. Replace with a 2×2 decision matrix on `review.passed` × `compare.passed`:
104
+ - true × true → ✅ deliver
105
+ - false × true → ⚠️ escalate with warning (structural audit)
106
+ - true × false → ⚠️ escalate (visual drift, no actionable fix)
107
+ - false × false → 🔄 loop with `fixHints` (until `auto_correct_max`)
108
+
109
+ `sb-review-checks` runs **always**, not only inside the loop — it's pure deterministic (cheerio + regex, no chromium, ~0s overhead). Compare-visual measures *visual drift*; review-checks measures *structural soundness*; they are orthogonal.
110
+ - **detector:** smoke example-store — review.passed=true × compare.passed=false routed to ⚠️ escalate (no infinite loop). Sanity test: deliberately-bugged build with `100vh` triggers anti-pattern #1 with `severity: high` and a specific `candidateFix`.
111
+ - **origin:** smoke test `/build-page example-store.com` (2026-05-04)
112
+
113
+ ## #29 — Prettier preserving Liquid schema JSON
114
+
115
+ - **id:** #29
116
+ - **name:** Schema extraction → format → reinject around prettier
117
+ - **applies-to:** sb-build-shopify
118
+ - **applies-when:** the `write` subcommand runs `prettier.format(content, { parser: 'html' })` on a Liquid file containing `{% schema %}...{% endschema %}` with long-string defaults.
119
+ - **symptom:** prettier in HTML mode word-wraps long lines unaware of Liquid blocks, inserting `\n` literally inside JSON strings (e.g. `"Revolutionizing Hair Care With Micro\nInfusion."`). Schema becomes invalid JSON; theme silently fails to register the section.
120
+ - **fix-recipe:** in `sb-build-shopify/scripts/build-shopify.mjs`, modify `tryPrettierFormat` to: (1) extract `{% schema %}...{% endschema %}` BEFORE prettier; (2) substitute with `<!--sb-shopify-schema-placeholder-->`; (3) prettier the rest (HTML + Liquid tags-as-text); (4) format the schema content separately via `JSON.stringify(JSON.parse(rawJson), null, 2)`; (5) reinject in place of the placeholder. Defensive fallback: if the placeholder is dropped/mangled, return unformatted with `reason: 'prettier-placeholder-lost'` rather than losing the schema.
121
+ - **detector:** new test in `test-build-shopify.mjs` — `write: schema with long-string default round-trips as parseable JSON` injects 100+ char default, write+validate end-to-end, asserts the string survives verbatim. Generalizable: any skill formatting a multi-language output (HTML+Liquid+JSON, MDX, JSX) does formatter passes via placeholder substitution.
122
+ - **origin:** smoke E2E `sb-build-shopify` example-store.com (2026-05-04)
123
+
124
+ ## #30 — `liquidjs` Shopify-tag normalization
125
+
126
+ - **id:** #30
127
+ - **name:** Pre-process Shopify-native tags before liquidjs parse
128
+ - **applies-to:** sb-validate-render
129
+ - **applies-when:** rendering a Shopify section file with `preset=shopify-section`. `liquidjs` vanilla doesn't know `{% style %}`, `{% javascript %}`, `{% stylesheet %}` (Shopify-native runtime extensions) and throws `ParseError: tag "style" not found`.
130
+ - **symptom:** render path crashes 100% on Shopify builds; smoke dies before generating screenshot; entire `/build-page --target shopify` flow blocked.
131
+ - **fix-recipe:** in `validate-render.mjs` (~line 230–244), normalize Shopify-native tags to HTML wrappers BEFORE `engine.parseAndRender()` — `{% style %}` → `<style>`, `{% endstyle %}` → `</style>`, `{% javascript %}` → `<script>`, `{% endjavascript %}` → `</script>`, idem `{% stylesheet %}`. Inner Liquid (`{{ ... }}`, `{% if %}`) is preserved and still evaluates.
132
+ - **detector:** smoke E2E pipeline must reach screenshot generation on a Shopify build. Backfill Shopify-render unit tests when `tmp/sb-validate-render-deps` scratch dir is set up. Generalizes (Pattern #13): when consuming a platform DSL with custom tags, normalize tags via a shim, not just filters.
133
+ - **origin:** smoke E2E `sb-build-shopify` example-store.com (2026-05-04)
134
+
135
+ ## #31 — Hero fallback path strategy (base64 SVG placeholder)
136
+
137
+ - **id:** #31
138
+ - **name:** Replace dev-filesystem fallback with inlined base64 SVG
139
+ - **applies-to:** sb-build-shopify (rules), anti-patterns memory
140
+ - **applies-when:** any `image_picker` `{% else %}` branch in a Shopify section (hero, content image, logo, etc).
141
+ - **symptom:** prior rules emitted `url('/Users/<dev>/.../assets/8a10....jpg')` literal in the fallback. Two-front breakage: (a) `sb-validate-render` couldn't resolve absolute paths without `file://` document base → build screenshot showed black rectangle, ~30pp of the 37.90% diff came from that; (b) merchant install was completely broken — the path is meaningless on any other machine.
142
+ - **fix-recipe:** rewrite `references/shopify-build-rules.md` in 4+ places to drop `localPath` from fallbacks and use a 208-byte canonical base64 SVG placeholder (1170×900, fill `#d4d4d4`) for `image_picker` `{% else %}` branches. Update: (1) "What to lift into settings" table; (2) "Schema settings — `image_picker`" with the data URI literal + two examples (`<img>` and `{% style %}` background); (3) "Hero rules" `{% if %}{% else %}`; (4) "Output skeleton"; (5) "Responsive images"; (6) add anti-pattern #16 codifying the rule across all targets. The SVG works in any environment without path dependency.
143
+ - **detector:** anti-pattern #16 detector in `sb-review-checks` (regex for `/Users/`, `/home/`, `/opt/`, `[A-Z]:\\Users\\`, etc.) catches future regressions. Smoke metric: re-run `/build-page example-store.com --target shopify` should drop diff from 37.90% toward <10% (pending — see also #32).
144
+ - **origin:** smoke E2E `sb-build-shopify` example-store.com (2026-05-04)
145
+
146
+ ## #32 — `sb-validate-render` `--assets-map-path` for Shopify mock context
147
+
148
+ - **id:** #32
149
+ - **name:** Populate `image_picker` mock settings from the assets map
150
+ - **applies-to:** sb-validate-render, orchestrator
151
+ - **applies-when:** validating a Shopify section that uses `image_picker` settings without `default` (per anti-pattern #11) and a base64 SVG fallback (per #16/#31). The validate-render mock context didn't populate the setting, so the Liquid `{% else %}` branch fired → comparison was placeholder-vs-real-photo.
152
+ - **symptom:** smoke E2E post-#29/#31 reported diff 75.30% (worse than 37.90% pre-fix). `tokenDiff=0`, `structuredDiffs=0`, `review.passed=true` — build structurally perfect; matrix routed to ⚠️ escalate (Pattern #28 working). Average-color analysis confirmed: live RGB(62,51,53) (real photo with overlay), build RGB(139,139,139) (placeholder SVG). Not a regression — limit of validate-render's Shopify path.
153
+ - **fix-recipe:** (a) add optional flag `--assets-map-path <path>` to `sb-validate-render`; when present AND `preset=shopify-section`, `findAssetForImagePicker(settingId, assetsMap)` maps setting id → keyword → context substring (e.g. `hero|banner|cover` → matches `hero` / `ai-hero` / `banner` / `cover`; `logo|brand` → `header` / `ai-hdr` / `logo` / `brand`; `product|gallery|featured` → `ai-fp__gallery` / `product` / `gallery` / `featured`; fallback: first asset with non-empty context). (b) Read the matched asset via `readAssetAsDataUrl()` and populate the mock-context setting as `data:image/jpeg;base64,...`. (c) Liquid `image_url` filter shim returns the data-URL verbatim → `{% if section.settings.hero_image %}` resolves truthy → real branch active. (d) Output JSON gains `mockContext: { populated: [...], skipped: [...] }`. (e) Update orchestrator: `/build-page` Step 4 and `/build-site` Step 4f pass `--assets-map-path` when `target === shopify` and Step 2 produced an `assetsMap`. Graceful fallback when flag absent OR asset not found → original placeholder branch preserved.
154
+ - **detector:** 8 new tests in `test-validate-render.mjs` cover id-keyword × context-keyword heuristics, fallback, empty-map, edge cases, and `--help` exposure (now 20 total, was 9). Smoke metric: example-store Shopify diff should drop 75.30% → ~8% (first <10% threshold validation).
155
+ - **origin:** smoke E2E follow-up (2026-05-05)
156
+
157
+ ---
158
+
159
+ ## Maintenance
160
+
161
+ - New IDs append sequentially (`#33`, `#34`, ...). Numbering is stable and additive — never renumber.
162
+ - When a fix is superseded by upstream tooling improvements (e.g. liquidjs ships native `{% style %}` support → #30 obsolete), keep the entry but add `superseded-by:` rather than deleting; the historical context is the value.
163
+ - Cross-skill fixes (those touching ≥2 skills) need entries in BOTH skills' code commentary referencing the ID — `// see fixes.md #27 for why this crops both sides`. Maintainers grepping for the ID find the recipe immediately.