@hegemonart/get-design-done 1.34.1 → 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.
- package/.claude-plugin/marketplace.json +4 -3
- package/.claude-plugin/plugin.json +3 -2
- package/CHANGELOG.md +22 -0
- package/README.md +8 -0
- package/agents/design-context-builder.md +7 -5
- package/agents/design-verifier.md +9 -2
- package/agents/email-executor.md +148 -0
- package/connections/connections.md +2 -0
- package/connections/litmus.md +134 -0
- package/package.json +1 -1
- package/reference/email-design.md +219 -0
- package/reference/registry.json +7 -0
- package/scripts/lib/email/validate-email-html.cjs +157 -0
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
},
|
|
6
6
|
"metadata": {
|
|
7
7
|
"description": "Get Design Done — 5-stage agent-orchestrated design pipeline with 9 connections, handoff-first workflow, bidirectional Figma write-back, 22+ specialized agents, queryable knowledge layer (intel store, dependency analysis, learnings extraction), and a self-improvement loop (reflector, frontmatter + budget feedback, global-skills layer). v1.20.0 ships the SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream, and resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) for rate-limit + 429 + context-overflow recovery. Full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation (auto-tag + GitHub Release + release-time smoke test).",
|
|
8
|
-
"version": "1.34.
|
|
8
|
+
"version": "1.34.2"
|
|
9
9
|
},
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "get-design-done",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), Claude Design handoff, bidirectional Figma write-back, and a queryable intel store (.design/intel/) for dependency and learnings queries. Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows) and release automation. Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain.",
|
|
15
|
-
"version": "1.34.
|
|
15
|
+
"version": "1.34.2",
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "hegemonart"
|
|
18
18
|
},
|
|
@@ -74,7 +74,8 @@
|
|
|
74
74
|
"design-system-sync",
|
|
75
75
|
"swift",
|
|
76
76
|
"compose",
|
|
77
|
-
"flutter"
|
|
77
|
+
"flutter",
|
|
78
|
+
"email"
|
|
78
79
|
]
|
|
79
80
|
}
|
|
80
81
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "get-design-done",
|
|
3
3
|
"short_name": "gdd",
|
|
4
|
-
"version": "1.34.
|
|
4
|
+
"version": "1.34.2",
|
|
5
5
|
"description": "Agent-orchestrated 5-stage design pipeline: Brief → Explore → Plan → Design → Verify. 22+ specialized agents, 9 connections (Figma, Refero, Preview, Storybook, Chromatic, Figma Writer, Graphify, Pinterest, Claude Design), handoff-first workflow via Claude Design bundles, bidirectional Figma write-back (annotations, Code Connect), queryable intel store (`.design/intel/`) for O(1) design surface lookups, and self-improvement loop (reflector agent, frontmatter + budget feedback, global-skills layer at `~/.claude/gdd/global-skills/`). Standalone commands: style, darkmode, compare, figma-write, graphify, handoff, analyze-dependencies, skill-manifest, extract-learnings, reflect, apply-reflections. Embeds NNG heuristics, WCAG thresholds, typographic systems, motion framework, and anti-pattern catalog. Ships with a full CI/CD pipeline (Node 22/24 × Linux/macOS/Windows, lint + schema + frontmatter + stale-ref + shellcheck + gitleaks + injection-scan + blocking size-budget) and release automation (auto-tag + GitHub Release + release-time smoke test). Optimization layer (v1.0.4.1, retroactive): gdd-router + gdd-cache-manager skills, PreToolUse budget-enforcer hook, tier-aware agent frontmatter, lazy checker gates, streaming synthesizer, /gdd:warm-cache + /gdd:optimize commands, and cost telemetry at .design/telemetry/costs.jsonl — targeting 50-70% per-task token-cost reduction with no quality-floor regression. v1.20.0 SDK foundation: gdd-state MCP server (11 typed tools), lockfile-safe STATE.md mutations, event stream at .design/telemetry/events.jsonl, resilience primitives (jittered-backoff, rate-guard, error-classifier, iteration-budget) with rate-limit + 429 + context-overflow recovery, and TypeScript toolchain. v1.27.7 ships gdd-mcp (Phase 27.7): 12 read-only MCP tools for sub-3s priming. v1.28.0 (Phase 28): Foundational References Tier 2 — 5 new reference files (color-theory, composition, proportion-systems, i18n, contrast-advanced), 2 verifier i18n probes + 1 explore i18n-readiness probe, 12 additive cross-link insertions across 10 existing references, 2 orthogonal audit-scoring lens-tags (composition_alignment + i18n_readiness).",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "hegemonart",
|
|
@@ -68,7 +68,8 @@
|
|
|
68
68
|
"design-system-sync",
|
|
69
69
|
"swift",
|
|
70
70
|
"compose",
|
|
71
|
-
"flutter"
|
|
71
|
+
"flutter",
|
|
72
|
+
"email"
|
|
72
73
|
],
|
|
73
74
|
"skills": [
|
|
74
75
|
"./skills/"
|
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,28 @@ All notable changes to get-design-done are documented here. Versions follow [sem
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.34.2] - 2026-05-31
|
|
8
|
+
|
|
9
|
+
### Phase 34.2 — Non-Web Output Layer: Email
|
|
10
|
+
|
|
11
|
+
Second sub-phase of the split Phase 34 (Non-Web Output Layer). Adds **email-template output** — a dedicated executor that generates email honoring the real client constraints (table-based layout, inline styles, MSO conditional comments for Outlook's Word engine, dark-mode `color-scheme` handling, and the top-20-client quirks) that modern web HTML/CSS breaks on — behind the same project-type detector that routes native (34.1). Email generation is **opt-in via project-type detection — web remains the default**. A decimal release on the v1.34.x arc (CHANGELOG-only, D-01); **no new runtime dependency** — the executor generates the email as an agent-prompt and the plugin checks it with a deterministic static validator, no `mjml` runtime (D-02/D-10). Native (Phase 34.1) shipped; Print/PDF (Phase 34.3) is the next sub-phase, out of 34.2 (D-07). 4 plans across Waves A–C.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Email-constraint catalogue + static validator (no `mjml` dependency, D-02/D-03).** `reference/email-design.md` — the authoritative email-constraint catalogue (~30 catalogued constraints: table-based layout, inline styles, MSO conditional comments, dark-mode `color-scheme`, ~600px width, image/alt rules, and the top-20-client quirks), registered in `reference/registry.json`. `scripts/lib/email/validate-email-html.cjs` — a pure, deterministic static checker (zero `require`, no fs/network/mjml) that flags the statically-verifiable subset: `EM-LAYOUT-01` (no flexbox/grid/`position`), `EM-STYLE-01` (no `<style>` as the primary styling mechanism; a small `@media`-only block is tolerated), `EM-MSO-01` (an MSO conditional comment in a full email), `EM-DARK-01` (a `color-scheme` signal present) — returning `{ ok, violations:[{ rule, detail }] }`.
|
|
16
|
+
- **`email-executor` agent (MJML canonical + derived HTML, D-02/D-04).** `agents/email-executor.md` — generates **one email template per task**: an MJML source (the canonical artifact) plus the equivalent derived HTML, generated against the `reference/email-design.md` catalogue and run through the static validator as its own self-check. It is an agent-prompt (like `design-executor`/`flutter-executor`), **not** a compiler — no running `mjml`, no Litmus account, no network is required to produce the email. Carries a `## Record` section from the start (the record-contract lesson) and an honest `size_budget`.
|
|
17
|
+
- **`litmus` connection (optional render-test, degrade-to-static-validator, D-03).** `connections/litmus.md` — cross-client rendered screenshots (Email-on-Acid is the documented alternative) for the verify stage when present, mirroring `connections/chromatic.md`. **Never hard-required**: when absent the verify stage degrades to the static email-HTML validator / code-only structural audit. Added to the `connections/connections.md` index + Capability Matrix in this closeout.
|
|
18
|
+
- **`design-context-builder` `email` project-type route + `design-verifier` email-verify branch (delegated, D-06/D-09).** The context-builder appends the `email` enum + the `email-executor` route at the 34.1 seam (the seam is left open for 34.3/print). The verifier gains an email-verify branch by **delegation** to `reference/email-design.md` (the verifier stayed within its ≤700-line budget).
|
|
19
|
+
- **Regression baseline.** `test/fixtures/baselines/phase-34-2/` freezes the email surface — a valid email fixture (`email-good.html`), a validator golden (`validator-golden.json`, the recorded `validateEmailHtml` output for a passing + a failing fixture, proving the rule-output shape is frozen), and `manifests-version.txt`=1.34.2 — pinned by `test/suite/phase-34-2-baseline.test.cjs` so a future change cannot silently break the validator or its verdict shape.
|
|
20
|
+
|
|
21
|
+
### Notes
|
|
22
|
+
|
|
23
|
+
- All Phase 34.2 tests are hermetic (D-10): the static email-HTML validator is a pure string→verdict function (fixture HTML → constraint checks, no network/mjml), the email-executor is validated **structurally** (frontmatter + catalogue reference + validator reference + presence), and the default `npm test` invokes **no** Litmus/Email-on-Acid and pulls in **no** `mjml` runtime. Rendered cross-client verification is the opt-in degraded-mode path (D-03).
|
|
24
|
+
- The 31.5 tarball golden (`test/fixtures/baselines/phase-31-5/tarball-manifest.txt`) was regenerated as a reviewed delta: **+2** newly-shipped files (`agents/email-executor.md`, `connections/litmus.md`), zero removals (641 paths). The other two new email files (`reference/email-design.md`, `scripts/lib/email/validate-email-html.cjs`) were already in the golden from their Wave-A (34.2-01) commit — so the four new shipped files net to a +2 line-delta on the golden.
|
|
25
|
+
- 6-manifest lockstep at **v1.34.2** (`package.json` + `package-lock.json` (root + `packages.""`) + `.claude-plugin/plugin.json` + `.claude-plugin/marketplace.json` (metadata.version + plugins[0].version) + `.cursor-plugin/plugin.json` + `.codex-plugin/plugin.json`); marketplace `plugins[0].keywords` + plugin keywords gain `email`. Version-sync hygiene done upfront (D-08): `OFF_CADENCE_VERSIONS.add('1.34.2')` + the 16 live-pinned `manifests-version.txt` baselines forward-propagated 1.34.1 → 1.34.2 (phase-34-1, the prior closeout's own baseline, joined the set).
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
7
29
|
## [1.34.1] - 2026-05-31
|
|
8
30
|
|
|
9
31
|
### Phase 34.1 — Non-Web Output Layer: Native Mobile
|
package/README.md
CHANGED
|
@@ -114,6 +114,14 @@ GDD now generates **native mobile** UI, not just web. A project-type detector ro
|
|
|
114
114
|
|
|
115
115
|
All three are fed by a shared **token-bridge** ([`reference/native-platforms.md`](reference/native-platforms.md)) that extends the Phase-23 token engine with `swift`/`compose`/`flutter` emitters — mapping your canonical tokens (`#3B82F6`, `16px`, `Inter`) deterministically onto each platform's theme primitives (`Color`/`Font`, `MaterialTheme`, `ThemeData`) with a documented, round-trippable precision contract, so the executors never hand-author colour or dimension math. Rendered verification via the iOS Simulator / Android emulator is **optional** — when no simulator is present the verify stage degrades to a code-only structural audit (the simulator only *adds* rendered confirmation).
|
|
116
116
|
|
|
117
|
+
### Email output (v1.34.2)
|
|
118
|
+
|
|
119
|
+
GDD also generates **email templates** — the project-type detector routes an `email` brief to a dedicated executor that honors the real client constraints web HTML/CSS breaks on:
|
|
120
|
+
|
|
121
|
+
- **[`email-executor`](agents/email-executor.md)** — generates one email per task as **MJML source (canonical) + the derived HTML**, against the constraint catalogue, with the static validator as its own self-check. It is an agent-prompt, not a compiler — **no `mjml` runtime, no Litmus account, no network** is needed to produce the email.
|
|
122
|
+
|
|
123
|
+
The constraints live in [`reference/email-design.md`](reference/email-design.md) — table-based layout (no flexbox/grid/`position`), inline styles (no `<style>` sheet in Gmail), MSO conditional comments for Outlook's Word engine, dark-mode `color-scheme` handling, ~600px width, and the top-20-client quirks. A deterministic static checker ([`scripts/lib/email/validate-email-html.cjs`](scripts/lib/email/validate-email-html.cjs)) flags the statically-verifiable subset (`EM-LAYOUT`/`EM-STYLE`/`EM-MSO`/`EM-DARK`). Cross-client rendered verification via [`Litmus`](connections/litmus.md) (or Email-on-Acid) is **optional** — when absent the verify stage degrades to the static validator. Email generation is opt-in; **web stays the default**.
|
|
124
|
+
|
|
117
125
|
### Previous releases
|
|
118
126
|
|
|
119
127
|
- **v1.26.0** — Headless Model Resolver (per-runtime tier→model map, `resolved_models` router field, per-runtime price tables, `reasoning-class` runtime-neutral alias).
|
|
@@ -218,18 +218,19 @@ Proceed to Step 0E regardless of whether Step 0D ran or was skipped.
|
|
|
218
218
|
|
|
219
219
|
Detect the **project type** so the pipeline routes the brief to the correct executor. Reuse the Step 0C / Step 1 grep/glob idiom (file reads only, < 1 second, no skip condition).
|
|
220
220
|
|
|
221
|
-
**Enum (
|
|
221
|
+
**Enum (5 values — D-06):** `web` (DEFAULT) · `native-ios` · `native-android` · `flutter` · `email`.
|
|
222
222
|
|
|
223
|
-
**Detection signals + precedence** (first match wins; brief overrides — if the user explicitly says "iOS app" / "Android app" / "Flutter app", honor that):
|
|
223
|
+
**Detection signals + precedence** (first match wins; brief overrides — if the user explicitly says "iOS app" / "Android app" / "Flutter app" / "email" / "newsletter" / "email template", honor that):
|
|
224
224
|
|
|
225
225
|
```bash
|
|
226
226
|
ls pubspec.yaml 2>/dev/null # → flutter
|
|
227
227
|
ls *.xcodeproj Package.swift 2>/dev/null # → native-ios (when no pubspec)
|
|
228
228
|
ls build.gradle build.gradle.kts settings.gradle 2>/dev/null # → native-android (when no pubspec)
|
|
229
|
+
ls **/*.mjml email/ emails/ templates/email 2>/dev/null # → email (email-template signals)
|
|
229
230
|
ls package.json 2>/dev/null # → web (default; also the fallback when none match)
|
|
230
231
|
```
|
|
231
232
|
|
|
232
|
-
Precedence: `pubspec.yaml` (flutter) > `*.xcodeproj`/`Package.swift` (native-ios) > `build.gradle*`/`settings.gradle` (native-android) > `package.json` / none (web — DEFAULT).
|
|
233
|
+
Precedence: an explicit brief override (the user says "email" / "newsletter" / "email template") wins like the other brief-overrides; otherwise `pubspec.yaml` (flutter) > `*.xcodeproj`/`Package.swift` (native-ios) > `build.gradle*`/`settings.gradle` (native-android) > `.mjml` files / an `email/` templates directory (email) > `package.json` / none (web — DEFAULT).
|
|
233
234
|
|
|
234
235
|
**Routing table** (project type → executor — one row per type, trivially appendable):
|
|
235
236
|
|
|
@@ -239,10 +240,11 @@ Precedence: `pubspec.yaml` (flutter) > `*.xcodeproj`/`Package.swift` (native-ios
|
|
|
239
240
|
| native-ios | swift-executor |
|
|
240
241
|
| native-android | compose-executor |
|
|
241
242
|
| flutter | flutter-executor |
|
|
243
|
+
| email | email-executor |
|
|
242
244
|
|
|
243
|
-
<!-- 34.2 (
|
|
245
|
+
<!-- 34.2 added `email` (above) → email-executor; 34.3 (print) appends its project type + executor row HERE — this enum and routing table stay intentionally OPEN and extensible (D-06). 34.2 adds email ONLY; do NOT add a print row until 34.3. -->
|
|
244
246
|
|
|
245
|
-
Record the detected type in DESIGN-CONTEXT.md as a `<project_type>` line (e.g. `<project_type>native-ios</project_type>`) so downstream stages route correctly. The native specifics (token→theme bridge) live in `reference/native-platforms.md` — do not inline
|
|
247
|
+
Record the detected type in DESIGN-CONTEXT.md as a `<project_type>` line (e.g. `<project_type>native-ios</project_type>`) so downstream stages route correctly. The native specifics (token→theme bridge) live in `reference/native-platforms.md`; the email specifics (table layout, inline styles, MSO/dark-mode constraints) live in `reference/email-design.md` — do not inline either here.
|
|
246
248
|
|
|
247
249
|
Proceed to Step 1 regardless of outcome.
|
|
248
250
|
|
|
@@ -359,9 +359,16 @@ In DESIGN-VERIFICATION.md, add a `## Phase 4B — Screenshot Evidence` section l
|
|
|
359
359
|
When `<project_type>` in DESIGN-CONTEXT.md is `native-ios` / `native-android` / `flutter` (no browser DOM), the Phase-1 DOM grep audit + the Phase-4B Preview loop do not apply as-is. Run instead:
|
|
360
360
|
|
|
361
361
|
- **Snapshot audit** — IF a simulator/emulator screenshot is supplied (via `connections/xcode-simulator.md`, `connections/android-emulator.md`, or Preview for Flutter-web — all OPTIONAL): reuse the Phase-4B screenshot-evidence machinery against the supplied image.
|
|
362
|
-
- **Code-only structural audit** (DEFAULT — no screenshot/simulator): verify the generated native source structurally — expected SwiftUI views / Compose composables / Flutter widgets present + token-bridge usage — instead of rendered pixels. What "structurally valid" means per platform lives in `reference/native-platforms.md` (do not inline it).
|
|
362
|
+
- **Code-only structural audit** (DEFAULT — no screenshot/simulator): verify the generated native source structurally — expected SwiftUI views / Compose composables / Flutter widgets present + token-bridge usage — instead of rendered pixels. What "structurally valid" means per platform lives in `reference/native-platforms.md` (do not inline it). Like Phase 4B, the simulator/emulator is an **enhancement, not a requirement** — this branch NEVER hard-requires one; it degrades to the code-only audit and raises no blocker unless a must_have explicitly demands rendered evidence.
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Phase 4E — Email Verify (project_type: email)
|
|
367
|
+
|
|
368
|
+
When `<project_type>` is `email` (no browser DOM — the Phase-1 DOM grep + Phase-4B Preview loop do not apply), run an email-constraint audit BY DELEGATION (the ~30 constraints live in `reference/email-design.md` — the authority; do NOT inline them):
|
|
369
|
+
|
|
370
|
+
- **Static constraint audit** (DEFAULT) — run `scripts/lib/email/validate-email-html.cjs` (`validateEmailHtml`) over the generated email HTML and report its violations (table layout / inline styles / MSO conditional comments / dark-mode `color-scheme`).
|
|
371
|
+
- **Rendered enhancement** (OPTIONAL) — IF the Litmus connection (`connections/litmus.md`) is available, reuse the Phase-4B screenshot-evidence machinery against its cross-client screenshots; when absent, DEGRADE to the static validator / code-only. Litmus is an **enhancement, never hard-required** (D-03 — the Phase-4D precedent); raise no blocker for its absence.
|
|
365
372
|
|
|
366
373
|
---
|
|
367
374
|
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: email-executor
|
|
3
|
+
description: Executes one plan task by generating an email template — MJML source (canonical) + the derived HTML — honoring reference/email-design.md constraints, validated by the static email-HTML checker. Single-shot; mirrors design-executor.
|
|
4
|
+
tools: Read, Write, Edit, Bash, Grep, Glob
|
|
5
|
+
color: magenta
|
|
6
|
+
default-tier: sonnet
|
|
7
|
+
tier-rationale: "Follows an Opus-authored plan; executes email codegen rather than plans it"
|
|
8
|
+
size_budget: M
|
|
9
|
+
size_budget_rationale: "Honest tier sized to the actual ~148-line body (M cap 300), NOT inflated to the design-family XXL default. Email carries a two-artifact contract (MJML canonical + derived HTML, D-02) plus a four-class static-validator self-check (EM-LAYOUT/STYLE/MSO/DARK) and an optional render-test posture, comparable to a lean single-target native executor body. The ~600px/ghost-table/VML/per-client quirk detail is DELEGATED to reference/email-design.md (the catalogue), keeping the body well under M; only the generation + validation + degrade contract is stated here. Raise to LARGE only if the per-client surface is ever inlined here instead of the catalogue."
|
|
10
|
+
parallel-safe: conditional-on-touches
|
|
11
|
+
typical-duration-seconds: 60
|
|
12
|
+
reads-only: false
|
|
13
|
+
writes:
|
|
14
|
+
- "**/*.mjml"
|
|
15
|
+
- "**/*.html"
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
@reference/shared-preamble.md
|
|
19
|
+
|
|
20
|
+
# email-executor
|
|
21
|
+
|
|
22
|
+
## Role
|
|
23
|
+
|
|
24
|
+
You execute **exactly one task** from the plan: you generate **one email template** — an **MJML source** file (the canonical artifact) and the **equivalent derived HTML** — honoring the email-client constraints. Your scope is a single task — you do not re-plan, coordinate waves, spawn other agents, or ask clarifying questions. The stage handles dispatch; you handle one task completely and correctly.
|
|
25
|
+
|
|
26
|
+
You are a single-shot agent: receive context, read the references, generate the MJML + HTML, write the file(s), run the static validator, commit, emit the completion marker, done.
|
|
27
|
+
|
|
28
|
+
You are an **agent-prompt**, not a compiler (D-04): GDD generates the email when an LLM (you) invokes this prompt, consistent with `design-executor.md` / `flutter-executor.md`. You do **not** require a running `mjml` compiler, a Litmus account, or any network to produce the email — rendered cross-client verification is the verify stage's degraded-mode concern, never a precondition here (D-03/D-10).
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Required Reading
|
|
33
|
+
|
|
34
|
+
Read every file the stage lists in its `<required_reading>` block before taking any action. At minimum:
|
|
35
|
+
|
|
36
|
+
- `.design/STATE.md` — pipeline state (decisions, blockers, must-haves)
|
|
37
|
+
- `.design/DESIGN-PLAN.md` — your task is identified by `task_id`
|
|
38
|
+
- `.design/DESIGN-CONTEXT.md` — brand decisions, constraints, locked choices
|
|
39
|
+
- **`reference/email-design.md`** — the **authoritative** email-constraint catalogue: table-based layout (no flexbox/grid/`position`), inline styles (no `<style>` sheet), MSO conditional comments for Outlook's Word engine, dark-mode `color-scheme`, ~600px max-width, image/alt rules, and the top-20-client quirks. This is how you pick the correct email idiom — you **generate against the catalogue**, you do **not** re-derive these rules (the `flutter-executor`→`reference/native-platforms.md` precedent).
|
|
40
|
+
|
|
41
|
+
**Invariant:** read all listed files FIRST, before making any changes.
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## MJML canonical + HTML derived (the D-02 two-artifact contract)
|
|
46
|
+
|
|
47
|
+
Per **D-02** the executor emits **two artifacts**, and **you (the LLM) perform the MJML→HTML expansion as your contract** — there is **NO `mjml` build step / runtime dependency** (an opt-in real `mjml` compile is out of scope, like the simulator connections):
|
|
48
|
+
|
|
49
|
+
| Artifact | Role | Notes |
|
|
50
|
+
| --- | --- | --- |
|
|
51
|
+
| **`*.mjml`** | **CANONICAL** — the source of truth | Semantic MJML the maintainer edits |
|
|
52
|
+
| **`*.html`** | **DERIVED** — generated by you from the MJML | The shippable email; what the static validator checks |
|
|
53
|
+
|
|
54
|
+
- Emit **both**: write the MJML source, then write the equivalent table-based HTML you expand from it (inline styles, ghost tables, MSO comments, color-scheme — per the catalogue).
|
|
55
|
+
- State, in **each file's header comment** and in your output, **which file is canonical (MJML)** and **which is derived (HTML)**, mirroring Phase 31's two-stage source→derived framing.
|
|
56
|
+
- Do **not** add `mjml` to `package.json` or shell out to an `mjml` binary — the expansion is your job.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Token Consumption — the canonical token form
|
|
61
|
+
|
|
62
|
+
Where the task themes the email (colors, spacing, type), consume the **canonical design tokens** (the css-vars token form) for those values rather than inventing ad-hoc hex/px — consistent with the design-family executors. Email cannot use CSS custom properties reliably across clients, so **resolve** the token values to literals **inline** at generation time (the token is the source; the email carries the resolved value). Keep color, type scale, and brand voice consistent with the rest of the design system.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Self-check via the static validator (the deterministic gate)
|
|
67
|
+
|
|
68
|
+
Before completing, **run the static email-HTML validator** on your **derived HTML**:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
const { validateEmailHtml } = require('scripts/lib/email/validate-email-html.cjs');
|
|
72
|
+
const { ok, violations } = validateEmailHtml(htmlString);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`validateEmailHtml` (Phase 34.2-01) deterministically checks the four statically-verifiable constraint classes — **EM-LAYOUT-01** (no flexbox/grid/`position`), **EM-STYLE-01** (no `<style>` block as the primary styling mechanism), **EM-MSO-01** (an MSO conditional comment in a full email), **EM-DARK-01** (a `color-scheme` signal present). **Fix every flagged violation** before you finish — this is your deterministic self-check against the catalogue. The remaining catalogue rules (~600px width, ghost tables/VML, image width/height/alt, per-client quirks) are render-tested guidance, not statically asserted — honor them from the catalogue.
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## Optional Litmus render-test (degraded / not a precondition)
|
|
80
|
+
|
|
81
|
+
Code generation needs **no** render service (D-04/D-10). Cross-client **rendered** verification is the **verify stage's** degraded-mode concern (D-03): point it at the `connections/litmus.md` render-test connection **as an enhancement**, **never** a precondition.
|
|
82
|
+
|
|
83
|
+
- When **Litmus** (or **Email-on-Acid**, the documented alternative) is available → the verify stage captures cross-client screenshots.
|
|
84
|
+
- When **absent** → verification **degrades** to the static validator above, then a code-only structural audit. Never hard-require Litmus.
|
|
85
|
+
|
|
86
|
+
Email ships its render-test connection at `connections/litmus.md`; you only **name** it — you never run it to generate.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Execution Principles
|
|
91
|
+
|
|
92
|
+
1. **Honor DESIGN-CONTEXT.md decisions as locked.** `D-XX:` decisions are non-negotiable.
|
|
93
|
+
2. **`reference/email-design.md` is authoritative** for the email constraints. Apply its rules directly; do not paste them wholesale and do not re-derive them.
|
|
94
|
+
3. **Observable outcomes only.** Acceptance criteria describe observable states ("the HTML uses `role="presentation"` tables", "an MSO conditional comment is present", "validateEmailHtml returns `ok: true`").
|
|
95
|
+
4. **Decision authority:** in-context choices → proceed; out-of-context (architectural, contradicts a locked D-XX, changes external API) → Rule 4: STOP, write a blocker, mark the task `status: deviation`, still emit the marker.
|
|
96
|
+
5. **Single-task scope.** Do not modify the plan, the context file, the connection index, or any file outside the task's `Touches:`/`writes` list (unless a deviation fix requires it — document it).
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
## Deviation Rules
|
|
101
|
+
|
|
102
|
+
Apply automatically; track each in the task output `## Deviations` section.
|
|
103
|
+
|
|
104
|
+
- **Rule 1 — Bug:** broken HTML/MJML, a flagged `validateEmailHtml` violation, wrong token resolution in files you author → fix inline.
|
|
105
|
+
- **Rule 2 — Missing Critical:** missing MSO comment, missing `color-scheme` signal, a flexbox/grid/`position` style, a missing `alt`/`width`/`height` on an `<img>` → add it (the catalogue requires it).
|
|
106
|
+
- **Rule 3 — Blocking:** a referenced file/import missing, the validator not resolvable → fix (resolve import, create stub) and note it.
|
|
107
|
+
- **Rule 4 — Architectural:** switching the email framework, restructuring the template system, a schema-level change, or anything contradicting a locked D-XX → STOP, write a `<blocker>`, mark `status: deviation`, still emit the marker.
|
|
108
|
+
|
|
109
|
+
**Scope boundary:** only fix issues directly caused by this task's changes. **Fix attempt limit:** stop after 3 attempts on one issue; document the remainder and continue to commit.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Output
|
|
114
|
+
|
|
115
|
+
Emit the **MJML source** + the **derived HTML** to the path(s) the task declares. In your final response, state: the file(s) written, **which is canonical (MJML)** and **which is derived (HTML)**, how tokens were resolved, and the `validateEmailHtml` result (`ok: true` / the violations you fixed). Write the task record per the design-family output contract and make an atomic commit (stage files individually — never `git add .`/`-A`; never run `git clean`).
|
|
116
|
+
|
|
117
|
+
Terminate with exactly this line, on its own line:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
## EXECUTION COMPLETE
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Constraints
|
|
126
|
+
|
|
127
|
+
This agent MUST NOT:
|
|
128
|
+
|
|
129
|
+
- Run `git clean` (any flags) — absolute prohibition.
|
|
130
|
+
- Require a running `mjml` compiler, a Litmus/Email-on-Acid account, or any network to generate the email (D-04/D-10).
|
|
131
|
+
- Add a `mjml` dependency to `package.json` or shell out to an `mjml` binary — the MJML→HTML expansion is the agent's contract (D-02).
|
|
132
|
+
- Re-derive the email constraints — consume `reference/email-design.md` (the catalogue).
|
|
133
|
+
- Emit only HTML or only MJML — both artifacts are the contract (MJML canonical, HTML derived).
|
|
134
|
+
- Create or edit the connection index, or modify the plan or context file, re-plan, spawn other agents, ask clarifying questions, or `git add .`/`-A`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Record
|
|
139
|
+
|
|
140
|
+
At run-end, append one JSONL line to `.design/intel/insights.jsonl`:
|
|
141
|
+
|
|
142
|
+
```json
|
|
143
|
+
{"ts":"<ISO-8601>","agent":"email-executor","cycle":"<cycle from STATE.md>","stage":"<stage from STATE.md>","one_line_insight":"<which email template (MJML+HTML) was generated + the validateEmailHtml result>","artifacts_written":["<files written>"]}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Schema: `reference/schemas/insight-line.schema.json`.
|
|
147
|
+
|
|
148
|
+
## EXECUTION COMPLETE
|
|
@@ -26,6 +26,7 @@ This directory contains connection specifications for external tools and MCPs th
|
|
|
26
26
|
| OpenRouter | Active | [`connections/openrouter.md`](connections/openrouter.md) | Model-router (no MCP); env: `OPENROUTER_API_KEY` (optional `OPENROUTER_BASE_URL`); opt-in tier-resolution overlay, graceful-degrade-to-native |
|
|
27
27
|
| Xcode Simulator | Active | [`connections/xcode-simulator.md`](connections/xcode-simulator.md) | **Optional** (macOS-only); CLI: `simctl` (no MCP); native-iOS rendered evidence for verify; degrade-to-code-only when absent (D-03) |
|
|
28
28
|
| Android Emulator | Active | [`connections/android-emulator.md`](connections/android-emulator.md) | **Optional**; CLI: `adb` / `emulator` (no MCP); native-Android rendered evidence for verify; degrade-to-code-only when absent (D-03) |
|
|
29
|
+
| Litmus | Active | [`connections/litmus.md`](connections/litmus.md) | **Optional** render-test (email; Email-on-Acid alternative); cross-client rendered screenshots for verify; degrade-to-static-validator / code-only when absent (D-03) |
|
|
29
30
|
|
|
30
31
|
---
|
|
31
32
|
|
|
@@ -51,6 +52,7 @@ Each cell describes what the connection contributes at that pipeline stage, or `
|
|
|
51
52
|
| OpenRouter | — | — | — | — | — | — | ✓ (model-router: tier→model resolution, all stages) |
|
|
52
53
|
| Xcode Simulator | — | — | — | native iOS code-gen target (swift-executor / emitSwift) | rendered SwiftUI snapshot when simulator available, else degrade to code-only structural audit (D-03) | — | — |
|
|
53
54
|
| Android Emulator | — | — | — | native Android code-gen target (compose-executor / emitCompose) | rendered Compose screenshot when emulator available, else degrade to code-only structural audit (D-03) | — | — |
|
|
55
|
+
| Litmus | — | — | — | email render-test target (email-executor) | cross-client rendered evidence when Litmus available, else degrade to the static email-HTML validator / code-only (D-03) | — | — |
|
|
54
56
|
|
|
55
57
|
**Column definitions:**
|
|
56
58
|
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Litmus — Connection Specification
|
|
2
|
+
|
|
3
|
+
This file is the connection specification for Litmus within the get-design-done pipeline. It lives in `connections/` alongside other connection specs. See the connection index for the full connection capability matrix (the litmus row is added at the 34.2 closeout).
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Litmus is the **verify stage's cross-client email render-test** for the `email` project type. It renders an email's HTML across the **top-20 email clients** (Outlook's Word engine, the various Gmail modes, Apple Mail, Yahoo/AOL, Proton, etc.) and returns per-client screenshots. Its pipeline role: after the `email-executor` generates an email (MJML source + derived HTML), the verify stage uses Litmus — **when available** — to surface per-client rendering breakage that the static validator cannot see, and narrates the result in plain English.
|
|
8
|
+
|
|
9
|
+
**Key relationship to the static validator:** Litmus is the *rendered* complement to the *static* checker `scripts/lib/email/validate-email-html.cjs`. The static validator deterministically checks constraint **classes** (table layout, inline styles, MSO comments, color-scheme — `EM-LAYOUT/STYLE/MSO/DARK`); Litmus checks **rendered pixels** across real clients. Litmus is **optional** — its absence is a quality reduction, not a blocking error (D-03).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
**Prerequisites:**
|
|
16
|
+
|
|
17
|
+
- A Litmus account at [litmus.com](https://www.litmus.com) — paid (a trial is available)
|
|
18
|
+
- API access enabled on the account (the Litmus Email Previews / Instant API), or the Litmus CLI
|
|
19
|
+
|
|
20
|
+
**Account and token:**
|
|
21
|
+
|
|
22
|
+
1. Create a Litmus account and enable API access in the account settings
|
|
23
|
+
2. Copy the API key / token from the account's API settings page
|
|
24
|
+
3. Set the environment variable — **NEVER commit the token to git or to a tracked `.env` file:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export LITMUS_API_KEY=<your-token>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Add this to your shell profile or CI environment secrets. `LITMUS_API_KEY` grants access to your Litmus account (create previews, consume render quota) — treat it like a password: never commit it (not in source files, not in `.env`, not in configuration files), never log it in CI output, and rotate it if it is exposed.
|
|
31
|
+
|
|
32
|
+
**Verification:**
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
test -n "${LITMUS_API_KEY}" && echo "litmus token present" || echo "litmus token absent"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Why Litmus is useful
|
|
41
|
+
|
|
42
|
+
Email rendering breakage is invisible to both code review and the static validator. An email's HTML can pass every static class-check (`validateEmailHtml` returns `ok: true`) and still render broken in a specific client: Outlook's Word engine collapses a layout that lacked a ghost table, Outlook.com inverts colors in dark mode and produces unreadable low-contrast text, the strict Gmail IMAP mode strips a `<style>` block the static heuristic tolerated, a retina image overflows on a mobile client.
|
|
43
|
+
|
|
44
|
+
The static validator checks constraint **classes**; it cannot render. Litmus renders the **actual** email across real clients and returns a screenshot per client, so per-client breakage surfaces as a visible image rather than a user report weeks later.
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## When to use Litmus
|
|
49
|
+
|
|
50
|
+
**Verify stage:** After the `email-executor` generates the email, run Litmus — when available — to capture cross-client screenshots and check for per-client rendering breakage. The verify stage narrates the per-client delta and notes it in `DESIGN-VERIFICATION.md`.
|
|
51
|
+
|
|
52
|
+
Litmus is **not** used at generation time. The `email-executor` needs no Litmus account, no network, and no render service to produce the MJML + HTML — generation is gated by the static validator only (D-04/D-10).
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## CLI / API Commands (Bash, not MCP tools)
|
|
57
|
+
|
|
58
|
+
Litmus is consumed via its HTTP API (or the Litmus CLI), not an MCP. All interactions are via Bash.
|
|
59
|
+
|
|
60
|
+
| Interaction | Auth | Returns | Pipeline use |
|
|
61
|
+
|---|---|---|---|
|
|
62
|
+
| Create an Email Preview (POST the HTML) | `LITMUS_API_KEY` | A preview id + per-client screenshot URLs | verify: cross-client render evidence |
|
|
63
|
+
| Poll the preview's client results | `LITMUS_API_KEY` | Per-client screenshot status (ready / processing) | verify: delta narration |
|
|
64
|
+
|
|
65
|
+
Write the rendered results to `.design/litmus-results.json` for the verify stage to narrate, mirroring the chromatic-results.json pattern. Exact endpoint shapes are documented at the Litmus API reference; this connection only requires the token-present probe + the degrade contract below.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Availability Probe
|
|
70
|
+
|
|
71
|
+
Litmus is consumed via API/CLI — the probe is Bash-based, not ToolSearch-based.
|
|
72
|
+
|
|
73
|
+
**Step L1 — CLI/API presence:**
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
command -v litmus 2>/dev/null || test -n "${LITMUS_API_KEY}"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- A Litmus CLI is found, OR an API key is present → proceed to Step L2
|
|
80
|
+
- Neither → `litmus: not_configured` (skip all Litmus steps)
|
|
81
|
+
|
|
82
|
+
**Step L2 — Token check:**
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
test -n "${LITMUS_API_KEY}"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- Non-empty → `litmus: available`
|
|
89
|
+
- Empty → `litmus: unavailable` (no API key to authenticate render requests)
|
|
90
|
+
|
|
91
|
+
**Write litmus status to `.design/STATE.md` `<connections>` after probing.**
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Fallback Behavior
|
|
96
|
+
|
|
97
|
+
**verify stage:**
|
|
98
|
+
|
|
99
|
+
- `litmus: unavailable` → skip cross-client render-testing; **degrade** to the static validator `scripts/lib/email/validate-email-html.cjs` (the `EM-LAYOUT/STYLE/MSO/DARK` class-checks), then a **code-only** structural audit of the HTML; note in `DESIGN-VERIFICATION.md`: "Cross-client render-test skipped — LITMUS_API_KEY not set; verified via the static email-HTML validator + a code-only structural audit."
|
|
100
|
+
- `litmus: not_configured` → same as unavailable; note: "Cross-client render-test skipped — Litmus not configured; verified via the static validator + a code-only audit. (Alternative: Email-on-Acid.)"
|
|
101
|
+
|
|
102
|
+
**Graceful degradation required:** the pipeline must continue when Litmus is unavailable. Missing cross-client render data is a **quality reduction, not a blocking error** (D-03 — the email render-test is an enhancement, never hard-required, mirroring the 34.1 simulator connections). The static validator is always available and is the deterministic floor; Litmus is the rendered ceiling on top of it.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## STATE.md Integration
|
|
107
|
+
|
|
108
|
+
Every stage that probes Litmus writes the result to `.design/STATE.md` under the `<connections>` section:
|
|
109
|
+
|
|
110
|
+
```xml
|
|
111
|
+
<connections>
|
|
112
|
+
figma: available
|
|
113
|
+
litmus: unavailable
|
|
114
|
+
</connections>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Status values:**
|
|
118
|
+
|
|
119
|
+
| Value | Meaning |
|
|
120
|
+
|---|---|
|
|
121
|
+
| `available` | CLI/API reachable AND `LITMUS_API_KEY` is set |
|
|
122
|
+
| `unavailable` | CLI/API present but `LITMUS_API_KEY` is empty |
|
|
123
|
+
| `not_configured` | No Litmus CLI and no API key — Litmus not set up |
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Alternative render-test services
|
|
128
|
+
|
|
129
|
+
Litmus is **not** the only cross-client render-test. If you prefer a different provider, the connection's contract (probe → available/unavailable/not_configured; degrade to the static validator when absent) is identical — only the token name and endpoint change:
|
|
130
|
+
|
|
131
|
+
- **Email on Acid** — the documented primary alternative; comparable cross-client previews and an API. Swap `LITMUS_API_KEY` for the Email-on-Acid API key and keep the same degrade behavior.
|
|
132
|
+
- **Putsmail** / **Mailtrap** — lighter options for sending an email to your own inboxes for manual spot-checks (not full cross-client screenshot matrices, but useful when no render-test account exists).
|
|
133
|
+
|
|
134
|
+
In every case the **static validator remains the deterministic floor**; the render-test service is the optional rendered enhancement that degrades gracefully when absent.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hegemonart/get-design-done",
|
|
3
|
-
"version": "1.34.
|
|
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).
|
package/reference/registry.json
CHANGED
|
@@ -895,6 +895,13 @@
|
|
|
895
895
|
"type": "heuristic",
|
|
896
896
|
"phase": 34.1,
|
|
897
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."
|
|
898
905
|
}
|
|
899
906
|
]
|
|
900
907
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* email/validate-email-html.cjs — static email-HTML constraint validator
|
|
3
|
+
* (Phase 34.2-01).
|
|
4
|
+
*
|
|
5
|
+
* Pure, deterministic regex/string analysis of an email-HTML STRING. Same
|
|
6
|
+
* input -> identical output. It checks the statically-verifiable SUBSET of the
|
|
7
|
+
* constraint catalogue in `reference/email-design.md` §8 — the spec is the
|
|
8
|
+
* authority; the `rule` ids emitted here are constraint-ids defined there.
|
|
9
|
+
*
|
|
10
|
+
* WHAT IS CHECKED (the four deterministic classes, emitted in a stable order):
|
|
11
|
+
* EM-LAYOUT-01 no `display:flex` / `display:grid` / `position:absolute|fixed`
|
|
12
|
+
* (and `sticky`) in any style — modern box primitives email
|
|
13
|
+
* clients drop. (Presence is flagged; table layout is expected.)
|
|
14
|
+
* EM-STYLE-01 no `<style>` block used as the PRIMARY styling mechanism. A
|
|
15
|
+
* `<style>` block is flagged when, after removing `@media { … }`
|
|
16
|
+
* groups, `@font-face`, `@import` and comments, residual CSS
|
|
17
|
+
* rules remain OR the block exceeds a generous size threshold.
|
|
18
|
+
* A small `@media`-only dark-mode/responsive block (EM-STYLE-04)
|
|
19
|
+
* is tolerated.
|
|
20
|
+
* EM-MSO-01 a full-email document (has <html>/<body> or a layout <table>)
|
|
21
|
+
* contains at least one MSO conditional comment
|
|
22
|
+
* (`<!--[if mso]>` / `<!--[if !mso]>`). Absence is flagged.
|
|
23
|
+
* A bare fragment is NOT flagged.
|
|
24
|
+
* EM-DARK-01 a color-scheme signal is present — a `<meta name="color-scheme">`
|
|
25
|
+
* and/or a CSS `color-scheme:` declaration and/or a
|
|
26
|
+
* `prefers-color-scheme` query. Total absence is flagged. A meta
|
|
27
|
+
* alone satisfies it (decoupled from any <style>).
|
|
28
|
+
*
|
|
29
|
+
* WHAT IS *NOT* CHECKED (catalogued in reference/email-design.md as render-tested
|
|
30
|
+
* guidance — verified by the optional Litmus / Email-on-Acid connection at
|
|
31
|
+
* 34.2-02, never by this validator): ~600px width, ghost tables/VML, bulletproof
|
|
32
|
+
* buttons, image width/height/alt, per-client (EM-CLIENT-01..20) pixel quirks.
|
|
33
|
+
*
|
|
34
|
+
* PURITY (D-02 / D-10): operates only on the passed string — no fs of the
|
|
35
|
+
* document, no network, no child-process spawn, no mjml runtime import, no Date,
|
|
36
|
+
* no process.env. This file has zero require() calls (node builtins included).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
'use strict';
|
|
40
|
+
|
|
41
|
+
// Modern box primitives email clients drop (EM-LAYOUT-01). Whitespace-tolerant.
|
|
42
|
+
const FLEX_RE = /display\s*:\s*flex\b/i;
|
|
43
|
+
const GRID_RE = /display\s*:\s*grid\b/i;
|
|
44
|
+
const POSITION_RE = /position\s*:\s*(?:absolute|fixed|sticky)\b/i;
|
|
45
|
+
|
|
46
|
+
// <style>…</style> block capture (EM-STYLE-01).
|
|
47
|
+
const STYLE_BLOCK_RE = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
|
|
48
|
+
const AT_MEDIA_GROUP_RE = /@media[^{]*\{(?:[^{}]*\{[^{}]*\})*[^{}]*\}/gi;
|
|
49
|
+
const AT_FONTFACE_GROUP_RE = /@font-face\s*\{[^{}]*\}/gi;
|
|
50
|
+
const AT_IMPORT_RE = /@import[^;]*;/gi;
|
|
51
|
+
const CSS_COMMENT_RE = /\/\*[\s\S]*?\*\//g;
|
|
52
|
+
// A CSS rule = a selector followed by a `{ … }` declaration block.
|
|
53
|
+
const CSS_RULE_RE = /[^{}@;]+\{[^{}]*\}/;
|
|
54
|
+
// Generous size threshold: a <style> body this large is a primary sheet even if
|
|
55
|
+
// it parsed as @media-only (deterministic guard against huge tolerated blocks).
|
|
56
|
+
const STYLE_PRIMARY_CHAR_THRESHOLD = 1024;
|
|
57
|
+
|
|
58
|
+
// MSO conditional comments (EM-MSO-01).
|
|
59
|
+
const MSO_COMMENT_RE = /<!--\[if\s+(?:!\s*)?mso/i;
|
|
60
|
+
// "Full email" signal — only then is a missing MSO comment flagged.
|
|
61
|
+
const FULL_EMAIL_RE = /<html[\s>]|<body[\s>]|<table[\s>]/i;
|
|
62
|
+
|
|
63
|
+
// color-scheme signals (EM-DARK-01) — any one satisfies the check.
|
|
64
|
+
const META_COLOR_SCHEME_RE = /<meta\b[^>]*name\s*=\s*["']?\s*color-scheme\b/i;
|
|
65
|
+
const CSS_COLOR_SCHEME_RE = /color-scheme\s*:/i;
|
|
66
|
+
const PREFERS_COLOR_SCHEME_RE = /prefers-color-scheme/i;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Decide whether a captured <style> block body is a PRIMARY styling mechanism.
|
|
70
|
+
* Tolerates an @media-only (responsive/dark) block per EM-STYLE-04.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} body raw inner text of a <style>…</style>
|
|
73
|
+
* @returns {boolean} true when the block carries non-@media rules or is oversized
|
|
74
|
+
*/
|
|
75
|
+
function isPrimaryStyleBlock(body) {
|
|
76
|
+
if (body.length > STYLE_PRIMARY_CHAR_THRESHOLD) return true;
|
|
77
|
+
const residual = body
|
|
78
|
+
.replace(CSS_COMMENT_RE, '')
|
|
79
|
+
.replace(AT_MEDIA_GROUP_RE, '')
|
|
80
|
+
.replace(AT_FONTFACE_GROUP_RE, '')
|
|
81
|
+
.replace(AT_IMPORT_RE, '');
|
|
82
|
+
return CSS_RULE_RE.test(residual);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Validate an email-HTML string against the statically-checkable constraint
|
|
87
|
+
* subset of reference/email-design.md §8.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} html the email HTML (the caller reads the file and passes the
|
|
90
|
+
* content — this function never touches the filesystem)
|
|
91
|
+
* @param {{ checks?: string[] }} [opts] reserved for future toggles; default
|
|
92
|
+
* runs all four classes
|
|
93
|
+
* @returns {{ ok: boolean, violations: Array<{ rule: string, detail: string }> }}
|
|
94
|
+
* `ok === (violations.length === 0)`; each violation's `rule` is a catalogued
|
|
95
|
+
* constraint-id and `detail` is a short human string.
|
|
96
|
+
*/
|
|
97
|
+
function validateEmailHtml(html, opts) {
|
|
98
|
+
if (typeof html !== 'string') {
|
|
99
|
+
throw new TypeError('validateEmailHtml(html): html must be a string');
|
|
100
|
+
}
|
|
101
|
+
void opts; // reserved
|
|
102
|
+
/** @type {Array<{ rule: string, detail: string }>} */
|
|
103
|
+
const violations = [];
|
|
104
|
+
|
|
105
|
+
// --- EM-LAYOUT-01: no flexbox/grid/absolute|fixed positioning -------------
|
|
106
|
+
const layoutHits = [];
|
|
107
|
+
if (FLEX_RE.test(html)) layoutHits.push('display:flex');
|
|
108
|
+
if (GRID_RE.test(html)) layoutHits.push('display:grid');
|
|
109
|
+
if (POSITION_RE.test(html)) layoutHits.push('position:absolute|fixed|sticky');
|
|
110
|
+
if (layoutHits.length > 0) {
|
|
111
|
+
violations.push({
|
|
112
|
+
rule: 'EM-LAYOUT-01',
|
|
113
|
+
detail: `email layout must use role="presentation" tables, not modern box primitives (found ${layoutHits.join(', ')})`,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// --- EM-STYLE-01: no <style> block as the primary styling mechanism -------
|
|
118
|
+
STYLE_BLOCK_RE.lastIndex = 0;
|
|
119
|
+
let m;
|
|
120
|
+
let primaryStyle = false;
|
|
121
|
+
while ((m = STYLE_BLOCK_RE.exec(html)) !== null) {
|
|
122
|
+
if (isPrimaryStyleBlock(m[1])) {
|
|
123
|
+
primaryStyle = true;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (primaryStyle) {
|
|
128
|
+
violations.push({
|
|
129
|
+
rule: 'EM-STYLE-01',
|
|
130
|
+
detail: 'visual styling must be inline; a <style> block with non-@media rules is stripped by Gmail (only a small @media-only block is tolerated)',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// --- EM-MSO-01: an MSO conditional comment in a full-email document -------
|
|
135
|
+
if (FULL_EMAIL_RE.test(html) && !MSO_COMMENT_RE.test(html)) {
|
|
136
|
+
violations.push({
|
|
137
|
+
rule: 'EM-MSO-01',
|
|
138
|
+
detail: 'a full email must include an MSO conditional comment (<!--[if mso]> … <![endif]-->) for Outlook\'s Word rendering engine',
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// --- EM-DARK-01: a color-scheme signal is present -------------------------
|
|
143
|
+
const hasColorScheme =
|
|
144
|
+
META_COLOR_SCHEME_RE.test(html) ||
|
|
145
|
+
CSS_COLOR_SCHEME_RE.test(html) ||
|
|
146
|
+
PREFERS_COLOR_SCHEME_RE.test(html);
|
|
147
|
+
if (!hasColorScheme) {
|
|
148
|
+
violations.push({
|
|
149
|
+
rule: 'EM-DARK-01',
|
|
150
|
+
detail: 'declare a color-scheme signal (<meta name="color-scheme">, CSS color-scheme:, or @media prefers-color-scheme) so clients keep the intended palette',
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return { ok: violations.length === 0, violations };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = { validateEmailHtml };
|