@hegemonart/get-design-done 1.33.6 → 1.34.2

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.
@@ -0,0 +1,108 @@
1
+ # Xcode Simulator — Connection Specification
2
+
3
+ This file is the connection specification for the **Xcode iOS Simulator** within the get-design-done pipeline. Its role is to provide *rendered* evidence for native-iOS work: it lets the verify stage capture a SwiftUI snapshot from a booted simulator so a native screen can be audited against its design intent, the same way `connections/preview.md` provides browser screenshots for web. It is the iOS half of the native-verify connection pair (the Android half is `connections/android-emulator.md`).
4
+
5
+ **It is OPTIONAL.** Most users — and CI — have no Xcode installed. Per **D-03** this connection is **never hard-required**: when it is absent the verify stage degrades gracefully to code-only structural verification, and no blocker is raised unless a `must_have` explicitly demands rendered evidence. The `swift-executor` agent generates compilable SwiftUI **without** any simulator (D-04/D-10); the simulator only adds rendered confirmation when it happens to be available.
6
+
7
+ See `connections/preview.md` for the sibling browser-preview connection this spec mirrors. (The connection index + capability matrix is maintained separately as part of phase closeout, not by this doc.)
8
+
9
+ ---
10
+
11
+ ## Setup
12
+
13
+ **Prerequisites (all OPTIONAL — absence is the common, supported case):**
14
+
15
+ - **macOS** — the iOS Simulator runs only on macOS. On Linux/Windows this connection is permanently `not_configured`; that is expected and never an error.
16
+ - **Xcode** (full app, not just the Command Line Tools) — provides the Simulator runtimes.
17
+ - **`simctl`** — Apple's simulator-control CLI, available once Xcode is installed.
18
+
19
+ **Why it is optional:** GDD's default test suite and the typical design user have no Xcode. Hard-requiring a simulator would block native-iOS work for the majority of environments. The pipeline therefore treats rendered iOS evidence as an **enhancement, not a requirement** (mirroring `connections/preview.md`'s stance) — exactly the D-03 guarantee.
20
+
21
+ ---
22
+
23
+ ## Tools
24
+
25
+ This connection is driven by Apple's simulator CLI rather than an MCP server.
26
+
27
+ | Capability | Command (illustrative) | Returns | Pipeline use |
28
+ |------------|------------------------|---------|--------------|
29
+ | Availability check | list installed simulator devices | device list (may be empty) | **Lightweight probe** — see below |
30
+ | Boot a device | boot a named device by UDID | boot confirmation | Bring a simulator up before snapshot capture (verify stage only) |
31
+ | Capture a snapshot | screenshot the booted device | PNG image | Rendered evidence for the verify stage |
32
+ | Shut down | shut down the booted device | confirmation | Clean up after capture |
33
+
34
+ The **availability check** is preferred for probing because it does not boot a device — it only reports whether the toolchain and runtimes are present. Booting and capture happen **only** in the verify stage, **only** when a `must_have` calls for rendered evidence and the probe reported `available`.
35
+
36
+ > The default `npm test` suite **never** invokes these commands. Static validation of the native executors is hermetic (D-10) — frontmatter + reference checks only, no Xcode, no simulator, no spawn.
37
+
38
+ ---
39
+
40
+ ## Which Stages Use This Connection
41
+
42
+ | Stage | Skill/Agent | Purpose | Simulator required? |
43
+ |-------|------------|---------|---------------------|
44
+ | design | `agents/swift-executor.md` | Generate compilable SwiftUI views | **No** — code generation needs no simulator (D-04/D-10) |
45
+ | verify | `agents/design-verifier.md` (native-verify branch) | Rendered SwiftUI snapshot audit **when available**, else degrade | No — optional; degrades per Fallback below |
46
+
47
+ This connection feeds the **34.1-05 native-verify branch**: verify probes it at stage entry and either captures a rendered snapshot (if `available`) or follows the degrade path. The `swift-executor` (design stage) never touches it.
48
+
49
+ ---
50
+
51
+ ## Availability Probe
52
+
53
+ Probe **before** any boot/capture, and write the result to `.design/STATE.md` `<connections>` immediately.
54
+
55
+ ```
56
+ Step P1 — toolchain check:
57
+ detect whether Xcode + the simulator CLI are present on this machine
58
+ → not macOS, or Xcode absent → xcode-simulator: not_configured
59
+ → present → proceed to Step P2
60
+
61
+ Step P2 — runtime/device check:
62
+ list installed simulator devices (does NOT boot one)
63
+ → list returns successfully (even empty) → xcode-simulator: available
64
+ → command errors / permission denied → xcode-simulator: unavailable
65
+ ```
66
+
67
+ **Three-value status schema** (mirrors `connections/preview.md`'s three-value model):
68
+
69
+ | Value | Meaning |
70
+ |-------|---------|
71
+ | `available` | Xcode + the simulator CLI are present and the device-list command succeeded (array, even empty). Rendered snapshots are possible. |
72
+ | `unavailable` | The toolchain is present but the device-list command errored (no runtimes installed, permission denied, internal error). Treat as no-simulator. |
73
+ | `not_configured` | Not macOS, or Xcode is not installed. The common, fully-supported case — never an error. |
74
+
75
+ Write the result to `.design/STATE.md`:
76
+
77
+ ```xml
78
+ <connections>
79
+ preview: available
80
+ xcode-simulator: not_configured
81
+ </connections>
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Fallback Behavior
87
+
88
+ When `xcode-simulator` is `not_configured` or `unavailable`, the verify stage **degrades gracefully — no error is raised, no blocker is appended** (the D-03 guarantee: this connection **never hard-requires a simulator**). The degrade is a two-step ladder, exactly mirroring how `connections/preview.md` falls back when no browser is available:
89
+
90
+ 1. **Snapshot-diff on a supplied screenshot** — if the brief/context provides a reference screenshot of the intended iOS screen, the verify stage diffs against that supplied image (using the Phase-23 visual primitives). No live simulator is needed.
91
+ 2. **Code-only structural audit** — if no screenshot exists either, the verify stage falls back to a **code-only** audit of the generated SwiftUI: structural checks against `reference/platforms.md` (safe-area handling present, no left-edge gesture conflict, Dynamic Type styles used, no sub-11pt text) and token-bridge conformance, with **no rendered evidence**. The native screen is verified **without a simulator**.
92
+
93
+ Phase-4B-style rendered-evidence checks are marked `[SKIPPED — simulator not available]`; the verifier continues to gap analysis with partial scores. **No `<blocker>` is appended for a missing simulator** — rendered iOS evidence is an enhancement, not a requirement. The **only** exception: if a `must_have` *explicitly* requires simulator-rendered evidence, THEN (and only then) the verifier appends a blocker noting the simulator is unavailable.
94
+
95
+ ---
96
+
97
+ ## STATE.md Integration
98
+
99
+ Every stage that uses this connection writes its probe result to `.design/STATE.md` under `<connections>` (see schema above). The verify stage **re-probes at stage entry** rather than blindly trusting a prior status, because toolchain availability can change between sessions — however, a status already written earlier in the **same session** can be trusted for the rest of that session (mirroring `connections/preview.md`).
100
+
101
+ ---
102
+
103
+ ## Caveats and Pitfalls
104
+
105
+ 1. **Absence is the default, not a failure.** On the vast majority of machines this connection is `not_configured`. That is the supported, expected state — the pipeline produces SwiftUI and verifies it code-only without ever touching a simulator (D-03/D-10). Never treat `not_configured` as an error or a reason to block.
106
+ 2. **The probe must not boot a device.** Use the device-**list** command for probing; booting a simulator just to check availability is slow and unnecessary. Boot only in the verify stage, only when rendered evidence is actually required and the probe said `available`.
107
+ 3. **Snapshots are screenshots — save by path.** A captured simulator snapshot is a full PNG. Save it to `.design/screenshots/<screen>.png` and reference it by path in markdown; do not embed base64 inline. `.design/` is gitignored, so snapshots are not committed.
108
+ 4. **The default test suite never spawns a simulator.** All native-executor validation is structural and hermetic (D-10). A live simulator is exclusively the verify stage's opt-in, degraded-mode enhancement (D-03) — it is never part of `npm test`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hegemonart/get-design-done",
3
- "version": "1.33.6",
3
+ "version": "1.34.2",
4
4
  "description": "A design-quality pipeline for AI coding agents: brief, plan, implement, and verify UI work against your design system.",
5
5
  "author": "Hegemon",
6
6
  "homepage": "https://github.com/hegemonart/get-design-done",
@@ -0,0 +1,219 @@
1
+ # Email Design — Constraint Catalogue
2
+
3
+ This reference is the **email-constraint catalogue**: the hard email-client rules an
4
+ email template MUST honor. Email is a *constrained* HTML/CSS surface — the modern
5
+ HTML/CSS the web executor emits (flexbox, grid, `<style>` sheets, `position`) is
6
+ dropped or mis-rendered by most email clients. This file is the **authority** that the
7
+ email-executor (Phase 34.2-02) generates against and the design-verifier's email branch
8
+ (34.2-03) audits against; neither re-derives these rules. The deterministic subset of
9
+ this catalogue is checked by [`scripts/lib/email/validate-email-html.cjs`](../scripts/lib/email/validate-email-html.cjs),
10
+ whose emitted `rule` ids are the constraint-ids defined below (the spec is the authority).
11
+
12
+ It is the sibling of [`reference/platforms.md`](./platforms.md). The two files have
13
+ distinct jobs and must not be confused:
14
+
15
+ | File | Job |
16
+ | --- | --- |
17
+ | `reference/platforms.md` (Phase 19) | Interaction **conventions** — navigation, safe areas, gestures, native typography, haptics. *Behavioral* knowledge for native/web screens. |
18
+ | `reference/email-design.md` (Phase 34.2, this file) | The email **constraint catalogue** — table-based layout, inline styles, MSO conditional comments, dark-mode `color-scheme`, ~600px max-width, image/alt rules, and top-20-client quirks. *Structural* knowledge an email template implements. |
19
+
20
+ Per **D-02** there is **no `mjml` runtime dependency**: MJML source is the agent's
21
+ canonical artifact and the HTML is derived by the agent, not a build step. Per **D-03**
22
+ email verify is this catalogue's *static* validator plus an *optional* Litmus /
23
+ Email-on-Acid render-test connection that degrades to the static validator when absent.
24
+ Per **D-10** the static checks are deterministic (same HTML string → same result), with
25
+ no network and no `mjml`, so the default `npm test` stays green on any machine.
26
+
27
+ Each constraint carries a **rule-id** (`EM-<CLASS>-NN`). Section 8 marks exactly which
28
+ ids the static validator asserts versus which are render-tested guidance only.
29
+
30
+ ---
31
+
32
+ ## 1. Purpose
33
+
34
+ Phase 19 shipped platform *references*; Phase 23 shipped the token engine; Phase 34.1
35
+ added native generators. Email is the remaining untouched product surface, and its
36
+ constraints are unlike both web and native: a fifteen-year-old rendering engine
37
+ (Outlook/Word), aggressive `<head>` stripping (Gmail), and a long tail of per-client
38
+ quirks. Instead of each email being authored from memory, the constraints live once
39
+ here (the catalogue) and once in the validator (the statically-checkable subset), and
40
+ the executor + verifier consume them. This file is the single SC#9-email authority.
41
+
42
+ The catalogue is **prose + tables**, not an implementation. Illustrative snippets are
43
+ kept to 2–3 lines. The implementation is `validate-email-html.cjs`.
44
+
45
+ ---
46
+
47
+ ## 2. Layout — tables, not flexbox/grid/position
48
+
49
+ Email layout uses nested `role="presentation"` **tables**, never the modern CSS box
50
+ primitives. `display:flex`, `display:grid`, and `position:absolute|fixed|sticky` are
51
+ dropped or mis-rendered by Outlook (Word engine), older Gmail, and many mobile clients,
52
+ collapsing a layout to a single column or off-screen elements.
53
+
54
+ | Rule-id | Constraint |
55
+ | --- | --- |
56
+ | **EM-LAYOUT-01** | Layout is built from `role="presentation"` tables. The forbidden modern primitives — `display:flex`, `display:grid`, `position:absolute`, `position:fixed` (and `position:sticky`) — MUST NOT appear in any `style`. *(Statically checkable: their presence is flagged.)* |
57
+ | EM-LAYOUT-02 | Body content sits in a fixed-/max-width container of ~**600px** (the safe width across the desktop preview pane and most mobile clients). Wider tables clip in Outlook and force horizontal scroll on mobile. |
58
+ | EM-LAYOUT-03 | Use `cellpadding="0" cellspacing="0" border="0"` on layout tables and prefer cell `padding` over margins (margins are inconsistently honored). |
59
+ | EM-LAYOUT-04 | Single primary column on mobile; multi-column desktop layouts degrade to stacked rows. Do not rely on `float` for columns (use side-by-side `<td>`s or `align`). |
60
+ | EM-LAYOUT-05 | Set explicit `width` on tables/cells; never assume an intrinsic content width. Outlook ignores CSS `max-width` on many elements — pair it with a `<!--[if mso]>` ghost table (see §4). |
61
+
62
+ ```html
63
+ <table role="presentation" width="600" cellpadding="0" cellspacing="0" border="0"
64
+ style="max-width:600px;margin:0 auto;"><tr><td>…</td></tr></table>
65
+ ```
66
+
67
+ ---
68
+
69
+ ## 3. Styling — inline, not a `<style>` block
70
+
71
+ Visual styling lives in **inline `style="…"` attributes** on each element, NOT in a
72
+ `<head><style>` sheet. Gmail (and several webmail clients) strip or heavily limit
73
+ `<head>` styles, so any rule that *must* apply is inlined. A `<style>` block is
74
+ **tolerated only** for progressive enhancement that degrades gracefully — chiefly
75
+ `@media` queries for responsive/dark-mode — never as the primary styling mechanism.
76
+
77
+ | Rule-id | Constraint |
78
+ | --- | --- |
79
+ | **EM-STYLE-01** | Core/visual styling is inline. A `<style>` block used as the PRIMARY styling mechanism (non-`@media` rules, or a large sheet) is forbidden because Gmail strips it. *(Statically checkable: a non-trivial `<style>` block carrying non-`@media` rules is flagged; a small `@media`-only block is tolerated.)* |
80
+ | EM-STYLE-02 | Avoid CSS shorthand that clients mangle (`background` shorthand, unitless line-heights in some clients); prefer longhand (`background-color`). |
81
+ | EM-STYLE-03 | No external stylesheets (`<link rel="stylesheet">`) and no `@import` — clients strip them. |
82
+ | EM-STYLE-04 | A small `@media`-only `<style>` for responsive breakpoints / dark-mode is the one accepted `<style>` use; it must enhance, not be required for, a readable layout. |
83
+
84
+ ---
85
+
86
+ ## 4. Outlook / MSO conditional comments
87
+
88
+ Outlook on Windows renders with Microsoft Word's HTML engine. It needs **MSO
89
+ conditional comments** to apply Outlook-specific fallbacks (ghost tables for centering
90
+ and width, VML for background images and bulletproof buttons) and to hide modern markup
91
+ it would mangle.
92
+
93
+ | Rule-id | Constraint |
94
+ | --- | --- |
95
+ | **EM-MSO-01** | A full email document MUST include at least one MSO conditional comment — `<!--[if mso]> … <![endif]-->` and/or `<!--[if !mso]><!--> … <!--<![endif]-->`. *(Statically checkable: absence in a full-email document is flagged.)* |
96
+ | EM-MSO-02 | Use `<!--[if mso]>` **ghost tables** to give Outlook an explicit fixed width and centering that it would otherwise drop from CSS `max-width`/`margin:auto`. |
97
+ | EM-MSO-03 | Use **VML** (`<v:roundrect>`, `<v:fill>`) inside `<!--[if mso]>` for rounded "bulletproof" buttons and background images, since Outlook ignores `border-radius` and CSS `background-image`. |
98
+ | EM-MSO-04 | Add the MSO DPI/namespace head fixes (`o:OfficeDocumentSettings`, `xmlns:v`/`xmlns:o`, `mso-line-height-rule:exactly`) to stabilize spacing and image scaling. |
99
+
100
+ ```html
101
+ <!--[if mso]><table role="presentation" width="600"><tr><td><![endif]-->
102
+ <!-- modern markup here -->
103
+ <!--[if mso]></td></tr></table><![endif]-->
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 5. Dark mode
109
+
110
+ Clients invert or remap colors in dark mode inconsistently (Outlook.com fully inverts;
111
+ Apple Mail and Gmail partially; some not at all). Declare a **`color-scheme`** signal so
112
+ clients keep the intended palette, and use `prefers-color-scheme` for explicit dark
113
+ overrides.
114
+
115
+ | Rule-id | Constraint |
116
+ | --- | --- |
117
+ | **EM-DARK-01** | Declare a `color-scheme` signal: a `<meta name="color-scheme" content="light dark">` and/or a `color-scheme: light dark;` CSS declaration and/or a `@media (prefers-color-scheme: dark)` block. At least one MUST be present. *(Statically checkable: total absence of any color-scheme signal is flagged. A `<meta name="color-scheme">` alone satisfies it — decoupled from any `<style>` block.)* |
118
+ | EM-DARK-02 | Pair `color-scheme` with `supported-color-schemes` (meta) for Apple Mail / Outlook. |
119
+ | EM-DARK-03 | Beware forced color inversion: set explicit `background-color` AND `color` on text containers so an inverting client cannot produce unreadable low-contrast pairs. |
120
+ | EM-DARK-04 | Provide dark-mode-friendly logos (transparent PNG or a `prefers-color-scheme` image swap) so a dark background does not hide a dark logo. |
121
+
122
+ ---
123
+
124
+ ## 6. Images & accessibility
125
+
126
+ Images are often blocked by default and must degrade gracefully; accessibility rules
127
+ prevent broken layouts and unreadable content. *(Guidance — not all statically
128
+ asserted; see §8.)*
129
+
130
+ | Rule-id | Constraint |
131
+ | --- | --- |
132
+ | EM-IMG-01 | Every `<img>` carries explicit `width` and `height` attributes (Outlook needs them; prevents reflow when images load). |
133
+ | EM-IMG-02 | Every `<img>` carries meaningful `alt` text (shown when images are blocked) and `display:block` to avoid the baseline gap below images. |
134
+ | EM-IMG-03 | Serve retina images at 2× and constrain with `width`/`height` so they render crisp without breaking the layout. |
135
+ | EM-IMG-04 | Buttons are **bulletproof** (table/`<a>` with padding + VML fallback for Outlook), never an image-only CTA that vanishes when images are blocked. |
136
+ | EM-A11Y-01 | Set the document `lang`, a real `<title>`, and a logical heading/reading order; ensure WCAG-AA text contrast that survives dark-mode inversion. |
137
+
138
+ ---
139
+
140
+ ## 7. Top-20 client quirks
141
+
142
+ The top-20-by-market-share email clients and their headline quirks. *(Catalogue —
143
+ most are render-tested via the optional Litmus / Email-on-Acid connection (34.2-02),
144
+ NOT statically checkable; see §8.)*
145
+
146
+ | Rule-id | Client | Headline quirk |
147
+ | --- | --- | --- |
148
+ | EM-CLIENT-01 | Apple Mail (iOS) | Most standards-compliant; honors `<style>` + media queries; auto-scales text — set `meta viewport`. |
149
+ | EM-CLIENT-02 | Apple Mail (macOS) | WebKit-based, robust; respects `prefers-color-scheme`; watch auto-link of dates/addresses. |
150
+ | EM-CLIENT-03 | Gmail (web) | **Strips `<head>` styles** beyond a limited `<style>`; clips messages over ~102KB ("[Message clipped]"); requires inline styles. |
151
+ | EM-CLIENT-04 | Gmail (mobile app, default account) | Supports a `<style>` block + media queries; same ~102KB clip; no `:hover` reliability. |
152
+ | EM-CLIENT-05 | Gmail (mobile, non-default / IMAP "GANGA") | **Strips `<style>` entirely** — only inline styles survive; the strictest Gmail mode. |
153
+ | EM-CLIENT-06 | Outlook 2016–2021 (Windows) | **Word engine**: no flexbox/grid/`position`, ignores `max-width`/`border-radius`/CSS `background-image`; needs ghost tables + VML + `mso-line-height-rule:exactly`. |
154
+ | EM-CLIENT-07 | Outlook (Microsoft 365, Windows) | Word engine like 2016/2019; DPI scaling bugs — set explicit image `width`/`height`. |
155
+ | EM-CLIENT-08 | Outlook.com (web) | Different (better) engine than desktop; **aggressive dark-mode color inversion**; rewrites some colors. |
156
+ | EM-CLIENT-09 | Outlook (macOS) | WebKit-based (unlike Windows); far more capable; still test buttons/spacing. |
157
+ | EM-CLIENT-10 | Outlook (mobile, iOS/Android) | Largely fine; respects media queries; watch link color overrides. |
158
+ | EM-CLIENT-11 | Yahoo Mail | Supports media queries; strips/rewrites some `class`/`id`; avoid unsupported pseudo-classes. |
159
+ | EM-CLIENT-12 | AOL Mail | Shares Yahoo's engine; similar class/id handling. |
160
+ | EM-CLIENT-13 | Samsung Mail (Android) | Webview-based; media-query support varies by version; test stacking. |
161
+ | EM-CLIENT-14 | Android default / Gmail-for-non-Gmail | Inline-only safe mode; treat like the strict Gmail IMAP mode. |
162
+ | EM-CLIENT-15 | Thunderbird | Gecko-based, capable; honors most CSS; test dark theme. |
163
+ | EM-CLIENT-16 | Windows 10/11 Mail | EdgeHTML/Chromium-ish; generally capable; watch padding. |
164
+ | EM-CLIENT-17 | Proton Mail | Sanitizes aggressively; strips remote content until approved; inline-safe. |
165
+ | EM-CLIENT-18 | Fastmail | Standards-friendly; respects media queries and dark mode. |
166
+ | EM-CLIENT-19 | Zoho Mail | Reasonable CSS support; test buttons + dark mode. |
167
+ | EM-CLIENT-20 | GMX / Web.de | EU webmail; limited CSS; lean on inline styles + tables. |
168
+
169
+ ---
170
+
171
+ ## 8. Statically-checkable vs render-tested
172
+
173
+ This table is the **contract** the validator's `rule` ids map to. The four rule-ids
174
+ below are the deterministic subset that `scripts/lib/email/validate-email-html.cjs`
175
+ asserts via regex/string analysis of the supplied HTML string. Every other rule-id in
176
+ this catalogue is **render-tested guidance** — verified by the optional Litmus /
177
+ Email-on-Acid render-test connection (34.2-02), never asserted by the static validator.
178
+
179
+ | Rule-id | Check | Statically checked by the validator? | How verified otherwise |
180
+ | --- | --- | --- | --- |
181
+ | **EM-LAYOUT-01** | No `display:flex` / `display:grid` / `position:absolute\|fixed` in any `style` | **YES** — presence flagged | — |
182
+ | **EM-STYLE-01** | No `<style>` block as the PRIMARY styling mechanism (non-`@media` rules / large sheet); inline styling expected | **YES** — a non-trivial `<style>` block flagged; a small `@media`-only block tolerated | — |
183
+ | **EM-MSO-01** | An MSO conditional comment (`<!--[if mso]>` / `<!--[if !mso]>`) is present in a full-email document | **YES** — absence flagged | — |
184
+ | **EM-DARK-01** | A `color-scheme` signal is present (meta `color-scheme` and/or CSS `color-scheme` and/or `prefers-color-scheme`) | **YES** — total absence flagged (a `<meta name="color-scheme">` alone satisfies it) | — |
185
+ | EM-LAYOUT-02..05 | ~600px width, cellpadding, single-column, explicit widths | No | Render test (Litmus) |
186
+ | EM-STYLE-02..04 | Longhand props, no external CSS, tolerated `@media` | No | Render test |
187
+ | EM-MSO-02..04 | Ghost tables, VML, DPI/namespace head | No | Render test (Outlook) |
188
+ | EM-DARK-02..04 | `supported-color-schemes`, explicit bg+color, dark logos | No | Render test (dark mode) |
189
+ | EM-IMG-01..04, EM-A11Y-01 | width/height, alt, display:block, bulletproof buttons, lang/contrast | No | Render test + a11y review |
190
+ | EM-CLIENT-01..20 | Per-client headline quirks | No | Render test (cross-client screenshots) |
191
+
192
+ Notes on the four statically-checked rules:
193
+
194
+ - **EM-STYLE-01 heuristic (deterministic).** A `<style>…</style>` block is flagged as the
195
+ primary styling mechanism when, after removing `@media { … }` groups and `@font-face`
196
+ from the block, residual CSS rules remain **or** the block exceeds a generous size
197
+ threshold. A small dark-mode/responsive `@media`-only block (per EM-STYLE-04) is
198
+ therefore tolerated and does NOT trip the check.
199
+ - **EM-DARK-01 is decoupled from `<style>`.** A `<meta name="color-scheme" content="…">`
200
+ in `<head>` satisfies EM-DARK-01 on its own, so a fully-inline email with only a
201
+ color-scheme meta (and no `<style>` at all) passes both EM-STYLE-01 and EM-DARK-01.
202
+ - **EM-MSO-01 fires only on a full email.** A document is treated as a full email when it
203
+ has `<html>`/`<body>` or a layout `<table>`; a bare fragment is not flagged for a
204
+ missing MSO comment.
205
+
206
+ ---
207
+
208
+ ## 9. Cross-references
209
+
210
+ - [`reference/platforms.md`](./platforms.md) — the interaction-conventions sibling
211
+ (navigation, safe areas, gestures). The email-executor reads **this** file for the
212
+ email constraints, that file for general platform behavior.
213
+ - [`scripts/lib/email/validate-email-html.cjs`](../scripts/lib/email/validate-email-html.cjs)
214
+ — the deterministic static validator that asserts the §8 subset; its `rule` ids are
215
+ the constraint-ids defined here.
216
+ - [`reference/registry.json`](./registry.json) — this catalogue is registered as the
217
+ `email-design` entry (type `heuristic`, phase `34.2`) so the registry round-trip test
218
+ (`test/suite/reference-registry.test.cjs`) stays green (D-05, the 33.5-01 / 34.1-01
219
+ lesson).
@@ -0,0 +1,273 @@
1
+ # Native Platforms — Token-Bridge Spec
2
+
3
+ This reference is the **token→native-code bridge**: it specifies how the canonical
4
+ CSS-token form produced by the Phase-23 token engine
5
+ (`scripts/lib/design-tokens/`) maps onto the three native theme systems —
6
+ SwiftUI, Jetpack Compose, and Flutter — and pins down the **precision contract**
7
+ that defines what "token identity preserved" means for the deterministic
8
+ round-trip (`reference/native-platforms.md` is the authority that
9
+ `test/suite/native-token-bridge.test.cjs` asserts against).
10
+
11
+ It is the sibling of [`reference/platforms.md`](./platforms.md). Those two files
12
+ have distinct jobs and must not be confused:
13
+
14
+ | File | Job |
15
+ | --- | --- |
16
+ | `reference/platforms.md` (Phase 19) | Interaction **conventions** — navigation, safe areas, gestures, native typography, haptics. *Behavioral* knowledge the executors reference when laying out a screen. |
17
+ | `reference/native-platforms.md` (Phase 34.1, this file) | The token→theme **bridge** — how a design token (`#3B82F6`, `16px`, `Inter`) becomes a SwiftUI `Color` / Compose `Color(0x…)` / Flutter `Color(0x…)`, plus the precision contract for the round-trip. *Structural* knowledge the emitters implement. |
18
+
19
+ Per **D-02** the bridge **extends** the Phase-23 engine with three new emitters
20
+ (`scripts/lib/design-tokens/{swift,compose,flutter}.cjs`) rather than forking a
21
+ separate native IR. There is one canonical token form (below) and one set of
22
+ readers; the native emitters are additional *sinks* on the same facade. Per
23
+ **D-10** the round-trip operates at the **token level** — deterministic emit +
24
+ re-extract with documented precision — never full-view parsing and never a live
25
+ simulator, so the default `npm test` stays green on any machine.
26
+
27
+ ---
28
+
29
+ ## 1. Purpose
30
+
31
+ Phase 19 shipped platform *references* but zero generators; Phase 23 shipped a
32
+ multi-source token reader (`css-vars` / `js-const` / `tailwind` / `figma`) that
33
+ all normalise to a single flat `{ tokens: Record<string, string> }` map. Phase
34
+ 34.1 crosses from platform-knowledge into platform-execution: instead of each
35
+ native executor re-deriving "how does `#3B82F6` become a SwiftUI `Color`", the
36
+ mapping lives once here (the spec) and once in the emitters (the
37
+ implementation), and the executors consume it. This amortizes the Phase-23 token
38
+ investment across SwiftUI / Compose / Flutter.
39
+
40
+ The canonical CSS-token form is the **single input** to all three native
41
+ emitters. This spec maps that one input to three native theme systems and states
42
+ the precision each mapping preserves.
43
+
44
+ ---
45
+
46
+ ## 2. Canonical input shape
47
+
48
+ The emitters consume the exact map shape the Phase-23 readers return: a **flat
49
+ `{ tokens: Record<string, string> }` object** whose keys are the design-token
50
+ names with the leading `--` stripped (exactly as `css-vars.cjs` returns) and
51
+ whose values are the raw token values as strings.
52
+
53
+ ```js
54
+ { tokens: { "color-primary": "#3B82F6", "space-4": "16px", "font-family-body": "Inter, system-ui" } }
55
+ ```
56
+
57
+ Each emitter accepts either the full Phase-23 `TokenSet`
58
+ (`{ tokens, source?, format? }`) **or** a bare `{ tokens }` object — it reads
59
+ `tokenSet.tokens` and throws a `TypeError` only when no `.tokens` object is
60
+ present.
61
+
62
+ ### Prefix → category inference
63
+
64
+ Token **category** is inferred from the key prefix. The emitters use this table
65
+ to decide whether a value is a color, a dimension, or a string:
66
+
67
+ | Key prefix | Category | Native treatment |
68
+ | --- | --- | --- |
69
+ | `color-` | color | hex → native channel form (§3–§5, §6 COLOR) |
70
+ | `space-`, `spacing-` | dimension | px → pt / dp / logical px (§6 DIMENSION) |
71
+ | `radius-` | dimension | px → pt / dp / logical px |
72
+ | `size-` | dimension | px → pt / dp / logical px |
73
+ | `font-`, `text-` | typography | string pass-through (family / weight / named size) |
74
+ | `shadow-` | other | string pass-through (composite values are non-mappable) |
75
+ | *(anything else)* | other | value-sniffed: a `#…` value is treated as color, an `Npx` value as dimension, otherwise string pass-through |
76
+
77
+ A value is **always** re-sniffed regardless of prefix, so a `#…` value under a
78
+ non-color prefix is still emitted as a color and a `Npx` value under a non-space
79
+ prefix is still emitted as a dimension. The prefix is the hint; the value is the
80
+ authority. This keeps the bridge robust against arbitrary token-naming schemes.
81
+
82
+ ---
83
+
84
+ ## 3. SwiftUI mapping
85
+
86
+ Target: a Swift source string exposing an `enum` of static theme constants
87
+ (`enum GDDTheme { … }`) — colors as `Color`, dimensions as `CGFloat` points,
88
+ typography families as `String` (and an optional `Font` helper).
89
+
90
+ | Token | SwiftUI form |
91
+ | --- | --- |
92
+ | color `#RRGGBB` | `Color(red: R/255.0, green: G/255.0, blue: B/255.0, opacity: A/255.0)` from the 8-bit channels |
93
+ | dimension `Npx` | `CGFloat` point literal — integer `N` (pt) |
94
+ | font-family | `String` literal (`"Inter, system-ui"`) |
95
+
96
+ Illustrative (2-line) snippet:
97
+
98
+ ```swift
99
+ static let colorPrimary = Color(red: 59.0/255.0, green: 130.0/255.0, blue: 246.0/255.0, opacity: 255.0/255.0)
100
+ static let space4: CGFloat = 16
101
+ ```
102
+
103
+ SwiftUI uses normalized `0.0…1.0` channel fractions; to keep the round-trip
104
+ **exact** the emitter writes each channel as the 8-bit numerator over `255.0`
105
+ (e.g. `59.0/255.0`) rather than a pre-divided decimal — the re-extractor reads
106
+ the numerator back as the integer channel, avoiding float drift. The `Color` /
107
+ `Font` / `ViewModifier` consumption pattern (applying the constants to views) is
108
+ the executor's job; this emitter produces the *constants*.
109
+
110
+ ---
111
+
112
+ ## 4. Jetpack Compose mapping
113
+
114
+ Target: a Kotlin source string with `Color` vals, a `Shapes` block (from
115
+ `radius-` tokens), a `Typography` block (from `font-`/`text-` tokens), and a
116
+ `MaterialTheme` wiring (`object GDDTheme { val Colors… ; val Shapes… ; val Typography… }`).
117
+
118
+ | Token | Compose form |
119
+ | --- | --- |
120
+ | color `#RRGGBB` | `Color(0xAARRGGBB)` long literal (alpha-first, 8 hex digits) |
121
+ | dimension `Npx` | `N.dp` (integer dp) |
122
+ | radius `Npx` | `RoundedCornerShape(N.dp)` inside `Shapes` |
123
+ | typography family | `String` (fed into a `TextStyle.fontFamily` slot / `Typography`) |
124
+
125
+ Illustrative (2-line) snippet:
126
+
127
+ ```kotlin
128
+ val colorPrimary = Color(0xFF3B82F6)
129
+ val space4 = 16.dp
130
+ ```
131
+
132
+ Compose packs ARGB into a single `0xAARRGGBB` long; alpha is the high byte. The
133
+ re-extractor reads the 8 hex digits straight back to the channels.
134
+
135
+ ---
136
+
137
+ ## 5. Flutter mapping
138
+
139
+ Target: a Dart source string building a `ThemeData` whose `colorScheme`
140
+ (`ColorScheme`) carries the color tokens and whose `textTheme` (`TextTheme`)
141
+ carries the typography tokens, plus a constants class
142
+ (`class GDDTheme { … }`).
143
+
144
+ | Token | Flutter form |
145
+ | --- | --- |
146
+ | color `#RRGGBB` | `Color(0xAARRGGBB)` (alpha-first, 8 hex digits) |
147
+ | dimension `Npx` | logical-px **double** — `N.0` |
148
+ | typography family | `String` (`fontFamily: 'Inter'`) |
149
+
150
+ Illustrative (2-line) snippet:
151
+
152
+ ```dart
153
+ static const colorPrimary = Color(0xFF3B82F6);
154
+ static const space4 = 16.0;
155
+ ```
156
+
157
+ Flutter measures in logical pixels and keeps the value as a `double` (`16.0`),
158
+ so — unlike pt/dp — Flutter dimensions are **not** rounded to integers; the
159
+ fractional part survives.
160
+
161
+ ---
162
+
163
+ ## 6. PRECISION CONTRACT
164
+
165
+ This section is the crux. It defines, per value category, exactly what
166
+ information the emit → re-extract round-trip preserves. The test asserts token
167
+ identity **within this precision** — not bit-exact floats, not lossy
168
+ approximation. An emitter is correct **iff** `reextract(emit({tokens}))`
169
+ reproduces every token in the identity set under these rules.
170
+
171
+ ### COLOR — 8-bit-per-channel, EXACT
172
+
173
+ - Accepted input forms: `#RGB`, `#RRGGBB`, `#RGBA`, `#RRGGBBAA` (case-insensitive).
174
+ - `#RGB` / `#RGBA` shorthand **expands** to `#RRGGBB` / `#RRGGBBAA` by
175
+ duplicating each nibble (`#3af` → `#33aaff`). This expansion is part of the
176
+ contract: the re-extractor recovers the **expanded** `#RRGGBB(AA)` form, so
177
+ `#3af` round-trips to `#33aaff` (canonically equal, the documented identity).
178
+ - Each channel is an 8-bit integer (0–255) and is preserved **exactly** — no
179
+ channel may be off by one. SwiftUI stores channels as `N.0/255.0` numerators;
180
+ Compose/Flutter store them in a `0xAARRGGBB` literal. Both forms recover the
181
+ identical 8-bit channels.
182
+ - **Alpha:** when the input has no alpha (`#RGB`/`#RRGGBB`) the emitted color is
183
+ **opaque** — alpha byte `0xFF` (Compose/Flutter) / `opacity: 255.0/255.0`
184
+ (SwiftUI). The re-extractor emits an alpha channel **only when the original
185
+ had one**: a 6-digit input round-trips to a 6-digit `#RRGGBB` (the implied
186
+ opaque alpha is dropped on the way back); an 8-digit input round-trips to the
187
+ 8-digit `#RRGGBBAA`. This keeps `#3B82F6 → #3B82F6` an exact identity.
188
+
189
+ ### DIMENSION — integer pt/dp, logical-px double
190
+
191
+ - Accepted input: `Npx` or a bare unit-less number (`16px`, `16`). The unit is
192
+ normalised to `px` on the canonical side.
193
+ - **iOS (pt) / Android (dp):** rounded to the nearest integer, **round-half-up**
194
+ (`15.5px` → `16`). Because rounding is lossy for non-integers, the round-trip
195
+ **identity set** is restricted to integer-px dimensions (e.g. `16px`), which
196
+ recover exactly: `16px → 16 (pt/dp) → 16px`.
197
+ - **Flutter (logical px):** kept as a `double` (`16px` → `16.0`), so Flutter
198
+ does **not** round and a fractional dimension survives. The re-extractor
199
+ recovers `Npx` by stripping the trailing `.0` for whole numbers.
200
+ - The re-extractor always recovers the canonical `Npx` string form, so the
201
+ emit→re-extract identity for an integer dimension is `"16px" === "16px"`.
202
+
203
+ ### `rem` / `em` — passed through verbatim (non-mappable)
204
+
205
+ `rem`/`em` values depend on a root/element font-size that the token map does not
206
+ carry, so they are **not** converted. They are treated as **non-mappable**
207
+ (below): emitted verbatim into a raw slot and **excluded** from the round-trip
208
+ identity set. (A future plan may add an explicit base-size option; until then,
209
+ verbatim pass-through is the stated, deterministic behavior.)
210
+
211
+ ### TYPOGRAPHY / NAMED VALUES — string pass-through
212
+
213
+ `font-family`, `font-weight`, named sizes, and any other string token are
214
+ emitted **verbatim** as a string literal and recovered **string-equal**
215
+ (`"Inter, system-ui" → "Inter, system-ui"`). No normalisation, no quoting
216
+ changes that alter the recovered string.
217
+
218
+ ### NON-MAPPABLE — verbatim, EXCLUDED from the identity set
219
+
220
+ Values the emitter cannot represent as a native primitive — CSS `var(--x)`
221
+ references, `calc(…)` expressions, gradients (`linear-gradient(…)`), and `rem`/
222
+ `em` dimensions — are **passed through verbatim** into a raw-string slot
223
+ (a trailing comment such as `// non-mappable: <name> = <value>` or the language
224
+ equivalent) so no information is lost, and are **explicitly excluded** from the
225
+ round-trip identity assertion. The contract documents them as
226
+ "verbatim / not round-tripped": the test asserts the verbatim value appears in
227
+ the emitted source, and does **not** assert it survives re-extraction as a typed
228
+ token.
229
+
230
+ ---
231
+
232
+ ## 7. The round-trip (what the test locks)
233
+
234
+ For each emitter the bridge guarantees:
235
+
236
+ 1. **Determinism.** `emit(x) === emit(x)` byte-for-byte. Keys are iterated in a
237
+ stable sorted order; no `Date`, no `process.env`, no filesystem, no network in
238
+ the emit path (D-10).
239
+ 2. **Identity within precision.** For the identity set (color + integer
240
+ dimension + typography), `reextract(emit({tokens}))` deep-equals `{tokens}`
241
+ under the precision rules above (8-bit color channels exact with `#RGB`
242
+ expansion; integer pt/dp; logical-px double; family/weight string-equal).
243
+ 3. **Verbatim exclusion.** Non-mappable values appear verbatim in the source and
244
+ are not part of the identity assertion.
245
+
246
+ Each emitter module exports a symmetric re-extractor
247
+ (`reextractSwift` / `reextractCompose` / `reextractFlutter`) that parses the
248
+ emitted native source back into a `{ tokens }` map, so the round-trip is
249
+ deterministic and bijective on the identity set and reusable by the Phase-34.1
250
+ regression baseline.
251
+
252
+ ---
253
+
254
+ ## 8. Registration
255
+
256
+ This reference is registered in
257
+ [`reference/registry.json`](./registry.json) as the `native-platforms` entry
258
+ (type `heuristic`, phase `34.1`) so the registry round-trip test
259
+ (`test/suite/reference-registry.test.cjs`) stays green — every `reference/*.md`
260
+ must be registered and resolve (D-05, the 33.5-01 lesson).
261
+
262
+ ---
263
+
264
+ ## 9. Cross-references
265
+
266
+ - [`reference/platforms.md`](./platforms.md) — the interaction-conventions
267
+ sibling (navigation, safe areas, gestures, native typography). Executors read
268
+ **both**: this file for the token→theme bridge, that file for layout/behavior.
269
+ - `scripts/lib/design-tokens/` — the Phase-23 token engine this bridge extends
270
+ (`index.cjs` facade + `css-vars` / `js-const` / `tailwind` / `figma` readers +
271
+ the new `swift` / `compose` / `flutter` emitters).
272
+ - `test/fixtures/mapper-outputs/tokens.json` — the canonical token fixture the
273
+ round-trip test derives its map from.
@@ -888,6 +888,20 @@
888
888
  "type": "data",
889
889
  "phase": 33.6,
890
890
  "description": "Phase 33.6 catalog-derived OpenRouter price sub-table — per-model prompt/completion $/tok snapshot of .design/cache/openrouter-models.json; derived view, the dynamic catalog is the source of truth (D-11 registry round-trip)."
891
+ },
892
+ {
893
+ "name": "native-platforms",
894
+ "path": "reference/native-platforms.md",
895
+ "type": "heuristic",
896
+ "phase": 34.1,
897
+ "description": "Phase 34.1 token-bridge spec — maps the canonical CSS-token form (Phase 23) to SwiftUI Color/Font/ViewModifier, Jetpack Compose Color/Shapes/Typography/MaterialTheme, and Flutter ThemeData/ColorScheme/TextTheme, with the precision contract (color hex→8-bit channels exact, dimension px→pt/dp/logical px) defining token-identity for the round-trip."
898
+ },
899
+ {
900
+ "name": "email-design",
901
+ "path": "reference/email-design.md",
902
+ "type": "heuristic",
903
+ "phase": 34.2,
904
+ "description": "Phase 34.2 email-constraint catalogue — table-based layout (not flexbox/grid/position), inline styles (not a <style> block), MSO conditional comments for Outlook, dark-mode color-scheme/prefers-color-scheme handling, ~600px max-width, image/alt rules, and top-20-client quirks; the authority the email-executor generates against and the design-verifier email branch audits against, and the rule-id source for scripts/lib/email/validate-email-html.cjs."
891
905
  }
892
906
  ]
893
907
  }