@tanqory/dev-mcp 0.0.0-stage → 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 (65) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +94 -2
  3. package/bin/tanqory-dev-mcp.mjs +16 -0
  4. package/bundled/examples/blank/config/settings.json +3 -0
  5. package/bundled/examples/blank/locales/en.json +22 -0
  6. package/bundled/examples/blank/locales/th.json +22 -0
  7. package/bundled/examples/blank/regions/footer.json +35 -0
  8. package/bundled/examples/blank/regions/header.json +13 -0
  9. package/bundled/examples/blank/src/bundle.ts +15 -0
  10. package/bundled/examples/blank/src/components/parts.tsx +32 -0
  11. package/bundled/examples/blank/src/entry.client.tsx +6 -0
  12. package/bundled/examples/blank/src/entry.server.tsx +13 -0
  13. package/bundled/examples/blank/src/sections/chrome.tsx +132 -0
  14. package/bundled/examples/blank/src/sections/commerce.tsx +242 -0
  15. package/bundled/examples/blank/src/sections/content.tsx +111 -0
  16. package/bundled/examples/blank/src/sections/editorial.tsx +107 -0
  17. package/bundled/examples/blank/src/settings.ts +15 -0
  18. package/bundled/examples/blank/src/styles/theme.css +195 -0
  19. package/bundled/examples/blank/src/templates.ts +14 -0
  20. package/bundled/examples/blank/src/theme.tsx +76 -0
  21. package/bundled/examples/blank/template.json +24 -0
  22. package/bundled/examples/blank/templates/404.json +14 -0
  23. package/bundled/examples/blank/templates/account.json +14 -0
  24. package/bundled/examples/blank/templates/article.json +14 -0
  25. package/bundled/examples/blank/templates/blog.json +14 -0
  26. package/bundled/examples/blank/templates/cart.json +14 -0
  27. package/bundled/examples/blank/templates/collection.json +14 -0
  28. package/bundled/examples/blank/templates/index.json +36 -0
  29. package/bundled/examples/blank/templates/page.contact.json +18 -0
  30. package/bundled/examples/blank/templates/page.json +14 -0
  31. package/bundled/examples/blank/templates/product.json +18 -0
  32. package/bundled/examples/blank/templates/search.json +14 -0
  33. package/bundled/examples/blank/tests/theme.test.ts +49 -0
  34. package/bundled/theme-abi/contracts/render-context.v2.d.ts +849 -0
  35. package/bundled/theme-abi/contracts/theme-abi-v2.md +1833 -0
  36. package/bundled/theme-abi/generated/capabilities.json +80 -0
  37. package/bundled/theme-abi/generated/field-types.json +869 -0
  38. package/bundled/theme-abi/generated/limits.json +173 -0
  39. package/bundled/theme-abi/schemas/conformance-report.v2.schema.json +482 -0
  40. package/bundled/theme-abi/schemas/theme-artifact.v1.schema.json +230 -0
  41. package/bundled/theme-abi/schemas/theme-manifest.v2.schema.json +254 -0
  42. package/bundled/theme-conformance/docs/storefront-tier.md +250 -0
  43. package/bundled/theme-conformance/schema/conformance-report.v2.schema.json +482 -0
  44. package/bundled/theme-conformance/schema/storefront-report.v1.schema.json +110 -0
  45. package/bundled/theme-conformance/schema/x-tanqory-ai.v0.schema.json +70 -0
  46. package/bundled/theme-kit/docs/headless-api.md +220 -0
  47. package/bundled/theme-kit/generated/kit-strings.json +180 -0
  48. package/bundled/theme-kit/types/headless/index.d.ts +36 -0
  49. package/bundled/theme-kit/types/headless/theme.d.ts +99 -0
  50. package/bundled/theme-kit/types/sdk/schema.d.ts +197 -0
  51. package/bundled/versions.json +8 -0
  52. package/package.json +53 -3
  53. package/src/bm25.mjs +69 -0
  54. package/src/browser.mjs +123 -0
  55. package/src/cli.mjs +127 -0
  56. package/src/devserver.mjs +82 -0
  57. package/src/docs.mjs +329 -0
  58. package/src/index.mjs +33 -0
  59. package/src/kit.mjs +150 -0
  60. package/src/manifest.mjs +82 -0
  61. package/src/prompts.mjs +97 -0
  62. package/src/scaffold.mjs +471 -0
  63. package/src/schemas.mjs +127 -0
  64. package/src/server.mjs +595 -0
  65. package/src/theme.mjs +91 -0
@@ -0,0 +1,250 @@
1
+ # Storefront-ready tier and budgets tier
2
+
3
+ `theme-conformance run <theme-dir> --tier storefront` checks that a theme **works like a full storefront**:
4
+ a shopper can pick a variant, add to the cart, change and remove lines, reach checkout, sort, filter, page,
5
+ search, switch market and language, use a keyboard, and the merchant can live-edit it in the editor.
6
+ `--tier budgets` checks size budgets and a lab approximation of the mobile performance and accessibility
7
+ scores. `--tier security` (the default) is the nine ABI suites and is unchanged. `--tier all` runs all three.
8
+
9
+ ```
10
+ theme-conformance run ./dist --tier storefront # → ./storefront-report.json
11
+ theme-conformance run ./dist --tier all --evidence ./evidence --storefront-out ./reports/storefront.json
12
+ ```
13
+
14
+ The storefront tier is **theme-agnostic**. It never reads a class name or the markup of a particular
15
+ theme. It relies only on:
16
+
17
+ 1. the ABI contract (`theme-abi-v2.md`): route kinds, `ctx.data`, `/_tq/data/*`, `/_tq/actions/*`,
18
+ the CSRF meta tag, `data-tq-form`, `data-tq-slot="account"`, `data-tq-node`, `?variant=`, `?country=`,
19
+ `?locale=`, the editor's canvas protocol;
20
+ 2. the theme's `manifest.json` (routes, components, presets, capabilities, forms);
21
+ 3. semantic HTML and ARIA (roles, accessible names, `aria-expanded`, `disabled`);
22
+ 4. the theme's **own locale strings** (`locales/<lang>.json`), found by key patterns (§4);
23
+ 5. optional `data-tq-*` hooks (§5) — a theme that emits them is found exactly; a theme that does not is
24
+ found through 1–4.
25
+
26
+ ## 1. How it runs
27
+
28
+ - The theme's `server/entry.mjs` runs under **workerd** with the conformance harness shim, in storefront mode
29
+ (`job.storefront: true`): data answers depend on their arguments (sort, filters, cursors, `page`, search
30
+ query) and cart actions keep real state (versions, conflicts, `checkoutUrl`). The nine security suites
31
+ never use this mode; their behaviour and their report are unchanged.
32
+ - A **gateway** in the runner plays the platform edge for headless Chromium (one browser): route
33
+ resolution (§2.1), templates (`templateSuffix` → `routes[kind]` → platform minimal page), market and
34
+ language from `?country=` / `?locale=` remembered for the session, the cart cookie, the native form POST
35
+ decision order (§1), `/_tq/data/*`, `/_tq/actions/*` (the `x-tq-csrf` header is required), `/_tq/csrf`,
36
+ the CSRF meta tag and hidden form fields, the platform CSP, `/checkout*` interception (the hand-off URL
37
+ is recorded), and the preview channel (`/_tq/editor-bridge.js`, `?tq_section_preview=`).
38
+ - Nothing leaves the machine: DNS and every connection are locked down (as suite 5), and every request is
39
+ answered by the gateway.
40
+ - **Fixture store** (`tq-storefront-fixture/1`, generated by `src/storefront/fixture.ts`): 60 products in
41
+ `all` (more than one page of 50), collection `linen`, two vendors (`Atelier North`, `Field & Co`), a hero
42
+ product `linen-shirt` with option `Colour` = Ivory 37.50 (in stock), Charcoal 52.25 (3 left), Moss 44.75
43
+ (sold out), each with its own image; markets `m_us` USD (default), `m_th` THB (2 decimals), `m_jp` JPY
44
+ (0 decimals); languages `en` (default) and `th`; page `about`, blog `journal` with two articles, two
45
+ policies, menus `main-menu` and `footer`; consent unknown (a banner is required) for consent checks.
46
+ - Viewports: desktop 1280×900, mobile 390×844 (DPR 2, touch).
47
+
48
+ ## 2. Checks
49
+
50
+ Status per check: `pass`, `fail`, `warn` (reported, never fails the tier), `not_applicable`, `incomplete`
51
+ (the runner could not perform it; fails the tier). Every non-pass check carries a `fixHint`.
52
+
53
+ | Group | Check id | Passes when |
54
+ |---|---|---|
55
+ | templates | `templates:home` `…:collection` `…:product` `…:search` `…:cart` `…:page` `…:blog` `…:article` | the theme maps the route kind and renders `200 text/html` with the fixture |
56
+ | templates | `templates:404` | `routes.not_found` renders with status 404 |
57
+ | templates | `templates:404-missing-resource` | `/products/no-such-product` answers 404 |
58
+ | templates | `templates:password` | always `not_applicable`: the password gate is platform-owned in ABI v2 |
59
+ | templates | `templates:account-slot` | `/account` holds `<iframe data-tq-slot="account" src="<data.customer.accountUrl()>" sandbox="allow-scripts allow-forms allow-same-origin">` (server HTML or after hydration), or the theme has no `routes.customer` (the platform shell holds it) |
60
+ | seo | `seo:title`, `seo:meta-description` | every page has a `<title>`; home/product/collection/page/article have a meta description |
61
+ | seo | `seo:canonical` | exactly one canonical link, equal to `ctx.route.canonical` (no query) — also with `?variant=` / `?sort=` |
62
+ | seo | `seo:hreflang` | home, product and collection link every published language (`<link rel="alternate" hreflang>` with absolute same-origin hrefs) |
63
+ | seo | `seo:product-jsonld` | exactly one Product JSON-LD, valid JSON, `name`, offers with a real variant price, `priceCurrency` of the market, a schema.org availability URL |
64
+ | seo | `seo:sitemap-robots` | no custom route claims `/sitemap*.xml` or `/robots.txt` |
65
+ | product | `product:variant-url-ssr` | `?variant=<Charcoal id>` shows Charcoal's price in the server HTML (JS off) |
66
+ | product | `product:variant-price` | choosing Charcoal shows its price in the product section |
67
+ | product | `product:variant-image` | after choosing Charcoal the largest visible product image is Charcoal's |
68
+ | product | `product:variant-stock` | choosing the sold-out Moss shows a sold-out state (sold-out string, disabled add) or the option itself is disabled |
69
+ | product | `product:oos-disables-add` | with `?variant=<Moss id>` the add control is disabled/absent and sends no cart action |
70
+ | cart | `cart:add` | the add control adds the selected variant (browser `cart.create`/`cart.addLines`, or a declared form post that calls `ctx.actions.cart`) |
71
+ | cart | `cart:totals` | text that appears after the add (a drawer) or `/cart` shows the line title and the cart total |
72
+ | cart | `cart:quantity` | setting the quantity to 2 on `/cart` updates the cart and shows the new total |
73
+ | cart | `cart:checkout-handoff` | the checkout control loads exactly `Cart.checkoutUrl` |
74
+ | cart | `cart:remove` | the remove control empties the cart and the page no longer shows the line |
75
+ | collection | `collection:sort` | some sort choice yields a product list that is correctly sorted (price, title, date or best-selling, either direction) and differs from the default |
76
+ | collection | `collection:filter` | choosing vendor `Field & Co` narrows the list to that vendor (facets behind ARIA disclosures are opened) |
77
+ | collection | `collection:pagination` | products beyond the first page are reachable (`?page=2` link, `rel=next`, load-more, infinite scroll), or all 60 are on the first page |
78
+ | search | `search:results` | `/search?q=linen` links the three linen products |
79
+ | search | `search:predictive` | `not_applicable` unless `capabilities.browser` has `data:search`; then typing `lin` makes a `catalog.predictive`/`catalog.search` call and a "Linen Shirt" option/link appears |
80
+ | i18n | `i18n:locales` | `locales/en.json` and `locales/th.json` exist |
81
+ | i18n | `i18n:keys` | (warn) every en key exists in th |
82
+ | i18n | `i18n:hardcoded-text` | no visible string is identical in en and th unless it is store data, merchant content (the theme's defaults/presets), a number/price/punctuation, a language/country name, or identical in both locale files |
83
+ | i18n | `i18n:html-lang` | `<html lang>` follows `?locale=` |
84
+ | i18n | `i18n:currency` | the hero price shows in USD (2 decimals), THB (2) and JPY (0 decimals) with a currency mark |
85
+ | i18n | `i18n:market-selector` | from the UI the shopper reaches `?country=JP` and prices switch to JPY |
86
+ | a11y | `a11y:axe` | axe-core (wcag2a/aa, wcag21a/aa, wcag22aa) reports no serious/critical violation on home, product, collection, cart (with a line), search, 404 — desktop and mobile |
87
+ | a11y | `a11y:keyboard-desktop` `a11y:keyboard-mobile` | Tab from the top focuses the cart control and the menu within 80 presses |
88
+ | a11y | `a11y:touch-targets` | on mobile every standalone interactive element's real hit area (hit-tested, so stretched links count) covers 44×44 CSS px; links inside running text and targets with a large-enough equivalent link to the same href are exempt |
89
+ | editor | `editor:data-tq-node` | every section of the page document has a `data-tq-node` element in the live DOM |
90
+ | editor | `editor:live-edit` | framed by an editor page on the editor origin, the platform bridge forwards `tanqory-preview-update-section` (then `tq:set-content`) and the section shows the new text without a reload (rendered text, compared case-insensitively, so CSS `text-transform` does not matter) |
91
+ | editor | `editor:section-preview` | every addable section (role ≠ block, with `presets[0]`) renders with height > 0 under `?tq_section_preview=<type>` (on home, or on a page whose template ships it) |
92
+ | editor | `editor:preview-hides-consent` | the consent banner shown on a live first visit does not show on the preview channel |
93
+ | consent | `consent:banner` | (warn) a consent banner shows when `consent.get` answers `bannerRequired: true` |
94
+ | ai | `ai:profile` | (warn) `manifest["x-tanqory-ai"]` is declared and valid (§7) |
95
+
96
+ ## 3. Budgets tier
97
+
98
+ | Check id | Measures | Budget |
99
+ |---|---|---|
100
+ | `budgets:client-initial` | gzip -9 of the client manifest entry + its `imports` + `css` (the same set suite 4 measures as `clientInitialGzipBytes`); JS and CSS reported separately | ABI §8 `clientInitialGzipBytes.budget` (256,000) |
101
+ | `budgets:ssr-entry` | gzip of `server/entry.mjs` | `ssrEntryGzipBytes.budget` |
102
+ | (measurement) `clientInitialTransferredGzipBytes` | script/style the browser fetched before the home page settled | reported |
103
+ | `budgets:performance` | lab performance score, mobile, min over home and product | ≥ 85 |
104
+ | `budgets:accessibility` | lab accessibility score, mobile, min over home and product | ≥ 95 |
105
+
106
+ **What is approximated (exactly).** Lighthouse is not run: its own navigation would bypass the gateway
107
+ that answers every request in-process. The runner measures in its own Chromium instead:
108
+ - mobile 390×844 DPR 2; CPU slowed 4× relative to the **uncontended** host (see "Busy hosts" below);
109
+ every response delayed by 150 ms + (bytes on the wire × 8 / 1638.4 kbps) by the gateway, text counted
110
+ gzipped (Lighthouse's "slow 4G" figures applied per response, not a simulated connection pool);
111
+ - FCP, LCP and CLS from `PerformanceObserver`; TBT = Σ(long task − 50 ms) after FCP over load + 3 s;
112
+ - **Speed Index is not measured** (no filmstrip); it is taken as (FCP + LCP) / 2;
113
+ - the score uses Lighthouse's mobile log-normal curves and weights (FCP 10, SI 10, LCP 25, TBT 30, CLS 25);
114
+ - the accessibility score is the share of applicable axe-core rules that pass, all rules weighted equally
115
+ (Lighthouse weights its audits individually).
116
+
117
+ Both scores are labelled `lab-approximation` in the report and are not Lighthouse scores.
118
+
119
+ **Busy hosts (host calibration).** `Emulation.setCPUThrottlingRate` slows the renderer relative to the machine
120
+ it runs on, so on a machine whose cores are busy with other work the page is slowed by the throttle *and* by
121
+ the contention: the same theme scored 96–99 on a quiet 6-core host and 76–83 at load 50–240. The runner
122
+ therefore measures and removes the contention instead of letting it count against the theme:
123
+
124
+ 1. *Before* each lab run it times a fixed busy loop in the page's renderer, unthrottled: wall-clock ÷ the main
125
+ thread's CPU time (`Performance.getMetrics` `ThreadTime`) is the host contention, 1.0 on an idle core. The
126
+ CDP rate is `4 ÷ contention`, never below 1.
127
+ 2. *During* the run it records a `toplevel` trace. Σ wall ÷ Σ CPU time (`dur` / `tdur`) of the renderer main
128
+ thread's tasks is the contention the measured page actually saw (the throttle burns CPU on that thread,
129
+ so it cancels out). `effectiveSlowdown = rate × that factor` is the slowdown the page really got.
130
+ 3. A run *decides* the verdict when its effective slowdown is 4× within ×1.25 (3.2–5×). A run slowed more
131
+ than that still proves a **pass** (the theme passed under a harsher device); a run slowed less still
132
+ proves a **fail**. A run off the target is repeated with the rate corrected by its own measured contention,
133
+ kept strictly between the highest rate that slowed the page too little and the lowest that slowed it too
134
+ much (the contention depends on the rate: a heavily throttled renderer burns CPU and is scheduled like a
135
+ batch job, a lightly throttled one stays interactive, so a plain correction can overshoot the band).
136
+ 4. One run per page is enough when it decides and is ≥ 5 points from the budget; otherwise up to four.
137
+ The page score is the lower median of its deciding runs; the check is the minimum over the pages.
138
+ 5. If a page has no deciding run (every run was over-slowed and below 85, or under-slowed and above it) and
139
+ no decided page failed, `budgets:performance` is **`warn`** with `inconclusive: this host was too busy…`
140
+ and the measured factors — not a failure, and not a pass of the theme. Re-run on a quieter machine.
141
+
142
+ On a quiet host the contention is ~1.0, the rate stays 4× and the result is what it was before calibration.
143
+ The busy-loop is not an absolute reference device: throttling stays relative to the machine's own idle
144
+ speed, as before, so a quiet fast machine and a quiet slow one judge a theme exactly as they did. Every run's
145
+ `hostFactor`, `cpuRate`, `windowFactor` and `effectiveSlowdown` is in the check's `evidence.details.pages[].runs`,
146
+ and the maxima in the `performanceScoreMobile` measurement note.
147
+
148
+ ## 4. Locale-key patterns (the semantic fallback)
149
+
150
+ When no hook is present, a control is a button/link (by ARIA role) whose accessible name equals one of the
151
+ theme's own `locales/en.json` values whose key matches the pattern (last key segment or whole key,
152
+ case-insensitive; `{placeholder}` matches anything):
153
+
154
+ | Control | Key pattern |
155
+ |---|---|
156
+ | add to cart | `add[._-]?to[._-]?(cart\|bag\|basket)`, `addtocart` |
157
+ | sold out | `sold[._-]?out`, `out[._-]?of[._-]?stock`, `unavailable` |
158
+ | checkout | `check[._-]?out`, `proceed[._-]?to[._-]?checkout` |
159
+ | remove line | `remove`, `delete`, `remove[._-]?(item\|line\|product)` |
160
+ | increase quantity | `increase`, `increment`, `plus`, `increase[._-]?quantity` |
161
+ | cart control | `cart`, `bag`, `basket`, `cart[._-]?count`, `open[._-]?cart`, `view[._-]?cart` |
162
+ | menu | `menu`, `open[._-]?menu`, `main[._-]?menu`, `navigation` |
163
+ | search | `search`, `open[._-]?search` |
164
+ | sort | `sort`, `sort[._-]?by` |
165
+ | filter | `filter(s)`, `show[._-]?filters` |
166
+ | apply / update | `apply`, `show[._-]?results`, `done`, `update`, `save`, `confirm` |
167
+ | next page | `next`, `next[._-]?page`, `load[._-]?more`, `show[._-]?more` |
168
+ | country / market | `country`, `country[._-]?region`, `region`, `market`, `currency`, `region[._-]?language` |
169
+ | empty cart | `cart[._-]?empty`, `empty`, `your[._-]?cart[._-]?is[._-]?empty` |
170
+ | consent | accept: `accept`, `accept[._-]?all`, `allow`, `agree`; reject: `reject`, `decline`, `necessary[._-]?only` |
171
+
172
+ Platform semantics used without any key: a link to `/cart`, links inside `<nav>`, `a[href*="country="]`,
173
+ `select[name="country"]`, `a[href*="sort="]`, `a[rel~=next]`, `a[href*="page=2"]`, `input[type=search]`,
174
+ `input[name=q]`, ARIA `searchbox`/`combobox`/`spinbutton`/`radio`/`option`, `aria-expanded="false"`
175
+ disclosures and `<details>`, a link whose href is `Cart.checkoutUrl`, links to `/products/<handle>`.
176
+
177
+ ## 5. Hooks — `tq-storefront-hooks/1`
178
+
179
+ All optional, all plain data attributes, no styling meaning. Each costs one attribute on an element the
180
+ theme already renders. A theme that emits a hook is found by it first; the hook does not change what a
181
+ check requires.
182
+
183
+ | Hook | On | Meaning |
184
+ |---|---|---|
185
+ | `data-tq-action="cart:add"` | the add-to-cart `<button>` (or `<input type=submit>`) | adds the selected variant |
186
+ | `data-tq-action="cart:checkout"` | the checkout link/button | hands off to `Cart.checkoutUrl` |
187
+ | `data-tq-action="cart:remove"` | a line's remove control | removes that line |
188
+ | `data-tq-action="cart:increment"` | a line's "+" control | quantity + 1 |
189
+ | `data-tq-action="cart:open"` | the header cart link/button | opens the cart (drawer or page) |
190
+ | `data-tq-action="menu:open"` | the menu button (mobile) | opens the main menu |
191
+ | `data-tq-quantity` | a line's quantity `<input>`/`<select>` | the line quantity |
192
+ | `data-tq-price` | the element showing the selected variant's price | updated on variant change |
193
+ | `data-tq-variant-picker` | the container of the option controls | scope for option values |
194
+ | `data-tq-sort` | the sort `<select>`, a container holding it, or the container of sort links | sort choices |
195
+ | `data-tq-filter` | the container of facet controls | facet values |
196
+ | `data-tq-pagination` | the container of the next-page / load-more control | paging |
197
+ | `data-tq-localization` | the container of the market/language selector | `?country=` / `?locale=` |
198
+ | `data-tq-search` | the search `<input>` | predictive search |
199
+ | `data-tq-consent-banner` | the consent banner root | shown when `bannerRequired` |
200
+
201
+ The hooks are versioned by `hooksVersion` in the report. A new hook is additive (same version); changing
202
+ the meaning of one is a new version.
203
+
204
+ ## 6. The live-edit protocol (what the check sends)
205
+
206
+ The check uses the platform's own canvas bridge (vendored byte-for-byte from storefront-edge
207
+ `core/editor-bridge.client.mjs`, `src/storefront/vendor/`), injected on the preview channel exactly as the
208
+ edge does. The bridge hands each editor message to the page as a window `CustomEvent('tq:canvas',
209
+ {detail: message})`:
210
+
211
+ - `{type: 'tanqory-preview-update-section', sectionId, settings, blocks?, order?}` — the check sends this
212
+ first, with one text setting of a home section set to a new value;
213
+ - `{type: 'tq:set-content', doc: [{id, type, settings}]}` — sent if the first did not apply.
214
+
215
+ A theme applies the message to the section in place and announces what it applies with
216
+ `window.dispatchEvent(new CustomEvent('tq:canvas-live', {detail: {types: [...]}}))` (again whenever it
217
+ receives `tq:canvas-bridge`). The bridge reports the announced types to the editor in `tq:ready.live`;
218
+ the check records them.
219
+
220
+ ## 7. `x-tanqory-ai` profile (draft v0, warn only)
221
+
222
+ `manifest["x-tanqory-ai"]` names which of the theme's own sections and settings play the roles Tanqory AI
223
+ fills. Schema: `schema/x-tanqory-ai.v0.schema.json`. Draft; the AI-compat work owns the final shape.
224
+
225
+ ```json
226
+ "x-tanqory-ai": {
227
+ "version": 0,
228
+ "roles": {
229
+ "hero": [{ "component": "hero", "heading": "heading", "subheading": "subheading", "image": "backgroundImage", "button": "buttonLabel", "link": "buttonLink" }],
230
+ "headerMenu": { "component": "header", "setting": "menu" },
231
+ "footerMenus": [{ "component": "footer", "block": "footer-menu", "setting": "menu" }],
232
+ "footerBrand": { "component": "footer", "block": "footer-brand" }
233
+ }
234
+ }
235
+ ```
236
+
237
+ Beyond the schema, every `component`/`block` must exist in `manifest.components` and every setting it names
238
+ must exist with a matching type (`menu` for menus; `collection`/`product` for `productGrid.setting`; text
239
+ types for headings; `image` for images).
240
+
241
+ ## 8. Report
242
+
243
+ `storefront-report.json`, format `tanqory-storefront-report/1`, schema
244
+ `schema/storefront-report.v1.schema.json`. Each check: `id`, `tier`, `group`, `title`, `status`, and when
245
+ relevant `message`, `route`, `locale`, `viewport`, `evidence` (`selector`, `screenshot` relative to the
246
+ `--evidence` directory, `url`, `details`) and `fixHint`. `measurements` hold the budgets. The security
247
+ tier's `conformance-report.json` (format 2) is not changed by this tier.
248
+
249
+ The default output is `./storefront-report.json` in the working directory — never inside the theme
250
+ directory, whose files are digested.