claudecode-omc 5.6.6 → 5.6.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
  2. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  3. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  4. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  5. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  10. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  11. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  12. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  13. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  40. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  41. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  42. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  43. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  44. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  45. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  46. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  47. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  48. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  49. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  50. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  51. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  52. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  53. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  54. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  55. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  56. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  57. package/bundled/manifest.json +1 -1
  58. package/package.json +1 -1
@@ -0,0 +1,201 @@
1
+ ---
2
+ name: h5-to-swiftui
3
+ version: 1.0.0
4
+ description: >-
5
+ Convert an H5 / web app's source into a native SwiftUI iOS app by native
6
+ rewrite (NOT a WebView shell, NOT a transpiler). Use when the user wants to
7
+ port, re-implement, or migrate a web/H5 frontend to native SwiftUI with
8
+ high visual fidelity, asks to "turn this web app into a real iOS app", or
9
+ wants a measured render-diff convergence loop against a browser baseline.
10
+ Auto-detects the web stack (v1: vanilla + React; other stacks are detected
11
+ then gated, not guessed), extracts design tokens, calibrates a
12
+ cross-renderer fidelity floor, rewrites per component, and drives a bounded
13
+ render→diff→correct loop that reports a quantified visual residual plus an
14
+ independent judge verdict. It does NOT promise literal pixel-identity:
15
+ cross-renderer differences impose a measured floor it reports honestly.
16
+ Triages canvas/WebGL/complex-animation/3rd-party-SDK/backend surfaces
17
+ instead of silently emitting wrong code.
18
+ argument-hint: "<path-to-h5-source> [--ios-floor 17] [--device 'iPhone 15 Pro'] [--max-iter 3]"
19
+ disable-model-invocation: false
20
+ user-invocable: true
21
+ allowed-tools:
22
+ - Read
23
+ - Grep
24
+ - Glob
25
+ - Bash
26
+ - Write
27
+ - Edit
28
+ - Agent
29
+ model: sonnet
30
+ ---
31
+
32
+ # H5 → Native SwiftUI (perceptually-convergent, residual-quantified)
33
+
34
+ Convert a web/H5 app to a **native SwiftUI** iOS app by reading the source and
35
+ **re-implementing** UI + logic in idiomatic SwiftUI. This is a native rewrite,
36
+ **not** a WebView wrapper and **not** a mechanical transpiler — those cannot
37
+ reach native fidelity (see `references/stack-detection.md` for why).
38
+
39
+ ## Honest promise (read this first)
40
+
41
+ Literal pixel-identity is **physically unreachable** for any text-bearing
42
+ screen: H5 renders in WebKit/Skia, SwiftUI in CoreText/Core Animation;
43
+ per-glyph subpixel/hinting and sRGB-vs-Display-P3 differences impose a
44
+ **non-zero residual** even when the SwiftUI is perfectly correct. This skill
45
+ therefore does **not** claim "pixel-perfect". It:
46
+
47
+ 1. **measures** the achievable cross-renderer floor for the current toolchain
48
+ (Stage 2.5 calibration),
49
+ 2. drives a render→diff→correct loop that **strictly reduces** visual delta
50
+ toward that measured floor,
51
+ 3. stops with a **quantified residual**, an **independent adversarial judge**
52
+ verdict, and a tiered outcome: `converged` / `close` / `needs-human`,
53
+ 4. **triages** anything it cannot safely convert — never silently emits
54
+ plausible-wrong code.
55
+
56
+ Never describe output as "pixel-accurate/perfect" in any report.
57
+
58
+ ## When to use / not use
59
+
60
+ Use for: porting a web/H5 frontend to native SwiftUI; "make this a real iOS
61
+ app (not a webview)"; measured visual migration. Not for: building a webview
62
+ wrapper (decline — out of scope); generic SwiftUI feature work (use normal
63
+ dev flow); design-mock→code with no source app (different problem).
64
+
65
+ ## Inputs
66
+
67
+ - `<path-to-h5-source>` (required)
68
+ - `--ios-floor` (default `17`; changes API availability + risk tiers)
69
+ - `--device` (default `iPhone 15 Pro`; sets logical viewport + safe-area)
70
+ - `--max-iter` (default `3`; Stage 5 per-component iteration cap)
71
+ - `--thresholds` (optional override; default = **calibrated**, not asserted)
72
+
73
+ ## Environment reality (hard gate)
74
+
75
+ Stage 5 needs macOS + Xcode + iOS Simulator. If absent OR the generated
76
+ project fails to build, the affected component is `needs-human`/`blocked`
77
+ and is **never counted converged**. The skill must not fabricate
78
+ convergence. Stages 0–4 + triage still run and produce value.
79
+
80
+ ## Pipeline (Stage 0–7) — product contracts
81
+
82
+ All stage products go under the *target project*'s `.h5-to-swiftui/` work
83
+ dir (inspectable, resumable). Every convergence artifact header pins
84
+ `sim_runtime`, `browser_version`, `model_id`, `temperature:0` — re-runs must
85
+ reproduce the same **verdict** (not the same pixels).
86
+
87
+ | Stage | Does | Key output |
88
+ |---|---|---|
89
+ | 0 Intake + detect + **v1 gate** | `scripts/detect-stack.mjs`; framework ∉ {vanilla,React} ⇒ write report & **STOP** (no guess) | `stack-report.json` |
90
+ | 1 Static analysis + facts + risk | inventory; `scripts/extract-tokens.mjs` (static∪runtime DTCG); risk triage pass *before* any generation; token-miss ⇒ `token-gaps.json` (never inline) | `pages/components/state-model/api/tokens/token-gaps/risk-triage.json` |
91
+ | 2 Reference capture | `scripts/capture-reference.mjs` (Playwright, iOS viewport, animations frozen, webfont/async settle, masks, browser pinned) | `reference/**`, `reference/manifest.json` |
92
+ | 2.5 **Render-equivalence calibration** | `scripts/calibrate-render.mjs` on bundled known-correct pair → normalization + **measured floor**; unmeasurable ⇒ `blocked.json` | `calibration.json` → see `references/render-equivalence-calibration.md` |
93
+ | 3 Scaffold | Xcode skeleton; `DesignTokens` (DTCG→Color/Font/spacing/radius + `.colorset` dark); router→`NavigationStack`; state skeleton | compiling skeleton |
94
+ | 4 Per-component rewrite | LLM rewrites each component using **only** token vocab + `references/css-to-swiftui-map.md`; **each component MUST emit a snapshot host** (isolated render entry); idiomatic-lint rejects `.position/.offset`-pinned layouts; Tier-3 ⇒ non-compiling `fatalError` stub | per-component `.swift` + host |
95
+ | 5 **Convergence loop** ★ | per component: host-render→normalize(calibration)→cascade diff (`pixel-diff.mjs`)→feedback payload→structured patch→recompile→re-measure; cap `--max-iter`. **The verdict is emitted ONLY by `scripts/evaluate-convergence.mjs`** — it mechanically enforces every anti-gaming guard and exits non-zero on any violation; the LLM never hand-writes `convergence/<component>.json` | `convergence/<component>.json` (written by `evaluate-convergence.mjs`) → see `references/visual-diff-loop-protocol.md` |
96
+ | 6 Behavioral parity | port `URLSession` async / Keychain tokens / ATS-flag `http://` / state / models; equivalence checks | `parity-report.json` |
97
+ | 7 Assemble + honest report | build; aggregate; summary leads with `needs-human` if it dominates | Xcode project + `conversion-report.json` + `convergence-summary.json` |
98
+
99
+ ## Hard rules (non-negotiable)
100
+
101
+ - **No silent failure.** Tier-3 surfaces (WebGL/WebGPU, RAF physics, WebRTC,
102
+ payments/secrets, analytics/ATT) ⇒ non-compiling `fatalError` stub +
103
+ machine-readable entry in `conversion-report.json`. See
104
+ `references/high-risk-triage.md`.
105
+ - **Anti-gaming (Stage 5) — ENFORCED IN CODE by
106
+ `scripts/evaluate-convergence.mjs`** (the sole thing that may emit the
107
+ verdict; it exits non-zero on any violation so a pipeline cannot ignore a
108
+ failed gate): the calibration gate is **recomputed from `calib.floor`** and
109
+ a hand-loosened gate is rejected (`gate-floor-mismatch`, exit 1 — this binds
110
+ the *gate to the floor*, not the floor's value); the **identity of the
111
+ bundled twin source files** (excluding build output/dotfiles) is bound via
112
+ `calibration_source` source-tree hashes recomputed from
113
+ `assets/calibration/{h5-twin,swiftui-twin}` (`calibration-twin-mismatch`,
114
+ exit 1 — binds the twin *source identity*, not the measured floor value);
115
+ the `floor` *value* is asserted to satisfy `calibrate-render.mjs`'s own
116
+ sanity envelope via the shared `scripts/_calib-consts.mjs`
117
+ (`floor-implausible`, exit 1 — a floor calibrate-render could not have
118
+ emitted is rejected, killing the absurd-floor attack; a floor *within* that
119
+ envelope but looser than the true measured one is a disclosed residual, see
120
+ below); the judge **negative control is bound** to the shipped, hash-pinned
121
+ `assets/calibration/swiftui-twin-divergent` source files (structured
122
+ `{stimulus_source_hash,rejected,differences}` under **forced-difference-3**;
123
+ the legacy bare string is rejected and an unbound control VOIDs any `YES`);
124
+ mask budget ≤10% with a non-empty reason per mask; the structured gate is
125
+ evaluated per iteration (a text-region `iou` of `null` is a FAIL; pHash is
126
+ necessary-not-sufficient and never short-circuits); best-of-N retains
127
+ **only built + gate-passing iterations** chosen by the script
128
+ (monotone-or-fail; caller's pick ignored); a present `blocked.json` or no
129
+ built+passing iteration ⇒ never converged. **Named irreducible residuals
130
+ (honest, §1.1) — BOTH disclosed:** (1) the grader cannot re-run the
131
+ simulator, so it trusts the per-iteration `pixel-diff.mjs` JSONs were
132
+ produced by the real `pixel-diff.mjs` on real `sim-screenshot.sh` renders
133
+ (bounded by that script's no-fake spine); (2) the grader cannot re-measure
134
+ the calibration floor — it asserts the supplied `floor` is within
135
+ `calibrate-render.mjs`'s own sanity envelope and recomputes the gate from
136
+ it, but a floor *within* that envelope yet looser than the true measured
137
+ floor is trusted, mitigated by the orchestrator's obligation to run the
138
+ real, sanity-spined `calibrate-render.mjs` and the recorded
139
+ `calibration_provenance`. Maximally provenance-bound, not zero-trust. The
140
+ **whole-assembled-screen SSIM-trend check is NOT an automated Stage-5
141
+ guard** — see the known limitation below; it is a documented **Stage-7
142
+ manual** cross-check.
143
+ - **Calibrated, not asserted.** Stage 5 gates against the **measured** floor
144
+ from `calibration.json`, never a hardcoded SSIM constant.
145
+ - **v1 scope.** Mapping authored for vanilla + React only; other detected
146
+ stacks stop at Stage 0 with an explicit report.
147
+ - **Compile-failure branch.** Non-building patch ⇒ revert to best
148
+ gate-passing iteration, consume an iteration; terminal ⇒ `needs-human`.
149
+
150
+ ## References (read on demand — progressive disclosure)
151
+
152
+ - `references/stack-detection.md` — detection heuristics, why rewrite > webview/transpile, v1 gate
153
+ - `references/design-token-extraction.md` — static∪runtime DTCG pipeline, token-gap rule
154
+ - `references/css-to-swiftui-map.md` — flex/grid/positioning/box-model tables, custom-`Layout` triggers
155
+ - `references/render-equivalence-calibration.md` — Stage 2.5 normalization + floor measurement + `calibration.json` schema
156
+ - `references/visual-diff-loop-protocol.md` — Stage 5 mechanism, payload + artifact schemas, tiered verdict, anti-gaming
157
+ - `references/high-risk-triage.md` — Tier 1/2/3 catalog, stub format, `conversion-report.json` schema
158
+
159
+ ## Scripts
160
+
161
+ `detect-stack.mjs` `extract-tokens.mjs` `capture-reference.mjs`
162
+ `calibrate-render.mjs` `pixel-diff.mjs` `mark-overlay.mjs`
163
+ `evaluate-convergence.mjs` `sim-screenshot.sh` — each supports `--help`;
164
+ capability/build probes degrade to an explicit block, never a fake success.
165
+
166
+ `scripts/evaluate-convergence.mjs` is the **sole executable convergence
167
+ authority**: it consumes the per-iteration `pixel-diff.mjs` JSON, the
168
+ structured `calibration.json` gate, the masks, and the judge result, then
169
+ mechanically decides the tier and **exits non-zero** (3 = needs-human/guard
170
+ violation, 4 = blocked) so the verdict cannot be faked or ignored. The
171
+ orchestrator MUST call it and MUST NOT hand-write `convergence/<component>.json`.
172
+
173
+ ### Known limitation (honest)
174
+
175
+ The **whole-assembled-screen SSIM-trend check** described in `spec.md` is
176
+ **not** an automated Stage-5 guard — there is no executable component that
177
+ diffs the assembled screen during the loop, so advertising it as active
178
+ would be a prose-only claim. It is instead a **Stage-7 manual cross-check**:
179
+ after assembly, a human (or a separate verification pass) compares the
180
+ assembled-screen capture against the reference so per-component `converged`
181
+ results cannot mask a broken composition. Treat per-component verdicts as
182
+ authoritative only at component granularity until that Stage-7 check is done.
183
+
184
+ ## Assets
185
+
186
+ `assets/calibration/` — known-correct SwiftUI screen (`swiftui-twin/`) + H5
187
+ twin (`h5-twin/`) for Stage 2.5, plus a deliberately-wrong
188
+ `swiftui-twin-divergent/` used as the Stage-5 judge **negative control**.
189
+ Calibration content is **textured** (text + a 4-stripe multi-color region),
190
+ never flat solids, so SSIM is meaningful (`calibrate-render.mjs` blocks a
191
+ flat pair). `assets/sample-h5-vanilla/` + `assets/sample-h5-react/` are the
192
+ dry-run fixtures (the React one is the text-heavy / custom-`Layout` / Tier-3
193
+ hard path).
194
+
195
+ ## Done = evidence
196
+
197
+ A run is complete only with: `stack-report.json`, `calibration.json` (finite
198
+ floor), per-component `convergence/*.json` (pinned-version header, iteration
199
+ history, masks, negative-control result, tiered verdict),
200
+ `conversion-report.json`, and an honest `convergence-summary.json`. No
201
+ success claim without these.
@@ -0,0 +1,176 @@
1
+ # Stage 2.5 Calibration Pair
2
+
3
+ This directory contains a **hand-built, known-correct calibration pair** for
4
+ the `h5-to-swiftui` skill's render-equivalence calibration stage. The two
5
+ sides (`h5-twin/` and `swiftui-twin/`) render the **same intended design** on
6
+ an iPhone 15 Pro logical viewport (393×852 pt).
7
+
8
+ > **Key principle:** any measured rendering difference between these two sides
9
+ > is the **irreducible cross-renderer floor** — not a defect. The pair is
10
+ > equivalent by construction, so the floor reflects only the unavoidable
11
+ > divergence between WebKit/Skia glyph rasterization and CoreText/Metal
12
+ > rendering, color-space pipeline differences (sRGB vs Display-P3), and
13
+ > sub-pixel sampling. Never grade real conversion screens against a tighter
14
+ > bound than this measured floor.
15
+
16
+ ---
17
+
18
+ ## Contents
19
+
20
+ | Path | Description |
21
+ |---|---|
22
+ | `h5-twin/index.html` | Static H5 page — open directly, no build step |
23
+ | `h5-twin/style.css` | All design tokens as CSS custom properties (`--*`) |
24
+ | `swiftui-twin/Package.swift` | Swift Package manifest (Swift 5.9+, iOS 17+) |
25
+ | `swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift` | `CalibrationScreenView` + `#Preview` — the **known-correct** SwiftUI side |
26
+ | `swiftui-twin-divergent/Package.swift` | Swift Package manifest for the negative control |
27
+ | `swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift` | **Deliberately WRONG** SwiftUI screen — the Stage 5 judge **negative control** |
28
+ | `tokens.json` | Ground-truth DTCG-style token map — the single source of truth |
29
+ | `.gitignore` | Ignores Swift Package `.build/` (never commit build output) |
30
+ | `README.md` | This file |
31
+
32
+ ### The divergent twin (negative control)
33
+
34
+ `swiftui-twin-divergent/` is **intentionally wrong** versus the H5 twin:
35
+ wrong colors (green background, blue circle, recolored stripes, inverted text
36
+ colors), shifted layout (much larger top inset, doubled section gap, circle
37
+ moved to the trailing edge, near-square taller card), and different copy.
38
+
39
+ Its role: the Stage 5 **independent judge must REJECT** the pair
40
+ (`h5-twin` vs `swiftui-twin-divergent`) under the adversarial
41
+ forced-difference-3 framing. If the judge instead calls them equivalent, the
42
+ run's `judge.negative_control` is recorded as **`failed`** and any `YES`
43
+ verdict is **VOID** for that run. This is enforced mechanically — not by
44
+ prose — in `scripts/evaluate-convergence.mjs` (guard 4).
45
+
46
+ ### Textured-content requirement (NOT flat solids)
47
+
48
+ The bundled calibration content **must be textured** — text plus a
49
+ multi-color structured region. Flat solid blocks make SSIM **insensitive to a
50
+ uniform mean shift**: a clearly-divergent solid pair can still score ~0.95,
51
+ yielding a falsely-high floor. The card is therefore a **4-stripe multi-color
52
+ region** (blue/purple/teal/amber), not a single solid block, on every side
53
+ (`h5-twin`, `swiftui-twin`, `swiftui-twin-divergent`).
54
+ `scripts/calibrate-render.mjs` additionally **blocks** (writes `blocked.json`,
55
+ exits 1) if BOTH normalized images are effectively flat (near-zero luma
56
+ variance) — so a flat calibration pair can never produce a trusted floor.
57
+
58
+ ---
59
+
60
+ ## Design Tokens
61
+
62
+ All values are shared across both sides. `tokens.json` is the authoritative
63
+ source; `style.css` and `CalibrationScreen.swift` must match it exactly.
64
+
65
+ ### Colors
66
+
67
+ | Token | Hex | CSS var | Swift `Color(red:green:blue:)` |
68
+ |---|---|---|---|
69
+ | `color.background` | `#F2F2F7` | `--color-background` | `r=0.9490 g=0.9490 b=0.9686` |
70
+ | `color.card` | `#4A90D9` | `--color-card` | `r=0.2902 g=0.5647 b=0.8510` |
71
+ | `color.circle` | `#E8744F` | `--color-circle` | `r=0.9098 g=0.4549 b=0.3098` |
72
+ | `color.textHeading` | `#1C1C1E` | `--color-text-heading` | `r=0.1098 g=0.1098 b=0.1176` |
73
+ | `color.textBody` | `#636366` | `--color-text-body` | `r=0.3882 g=0.3882 b=0.4000` |
74
+ | `color.stripeA` | `#4A90D9` | `--color-stripe-a` | `r=0.2902 g=0.5647 b=0.8510` |
75
+ | `color.stripeB` | `#7E57C2` | `--color-stripe-b` | `r=0.4941 g=0.3412 b=0.7608` |
76
+ | `color.stripeC` | `#26A69A` | `--color-stripe-c` | `r=0.1490 g=0.6510 b=0.6039` |
77
+ | `color.stripeD` | `#F4B400` | `--color-stripe-d` | `r=0.9569 g=0.7059 b=0.0000` |
78
+
79
+ The card is a **4 equal-width vertical stripe** region (A→D, left to right),
80
+ not a flat solid — see the textured-content requirement above.
81
+
82
+ ### Spacing (CSS px = Swift pt at 1× logical scale)
83
+
84
+ | Token | Value | CSS var | Swift constant |
85
+ |---|---|---|---|
86
+ | `space.safeTop` | 59 px/pt | `--space-safe-top` | `spaceSafeTop = 59` |
87
+ | `space.pageH` | 20 px/pt | `--space-page-h` | `spacePageH = 20` |
88
+ | `space.sectionGap` | 24 px/pt | `--space-section-gap` | `spaceSectionGap = 24` |
89
+ | `space.cardPadding` | 20 px/pt | `--space-card-padding`| `spaceCardPadding = 20` |
90
+ | `space.cardGap` | 16 px/pt | `--space-card-gap` | `spaceCardGap = 16` |
91
+
92
+ ### Radii
93
+
94
+ | Token | Value | CSS var | Swift constant |
95
+ |---|---|---|---|
96
+ | `radius.card` | 16 px/pt | `--radius-card` | `radiusCard = 16` |
97
+
98
+ ### Typography
99
+
100
+ | Token | Value | CSS | Swift |
101
+ |---|---|---|---|
102
+ | `font.family` | `-apple-system` | `font-family: -apple-system, system-ui` | `.system(size:weight:)` |
103
+ | `font.sizeHeading` | 22 px/pt | `font-size: 22px` | `.system(size: 22, weight: .semibold)` |
104
+ | `font.weightHeading` | 600 / semibold | `font-weight: 600` | `.semibold` |
105
+ | `font.lineHeightHeading` | 1.27× | `line-height: 1.27` | `lineSpacing: 22 × 0.27 ≈ 5.9 pt` |
106
+ | `font.sizeBody` | 15 px/pt | `font-size: 15px` | `.system(size: 15, weight: .regular)` |
107
+ | `font.weightBody` | 400 / regular | `font-weight: 400` | `.regular` |
108
+ | `font.lineHeightBody` | 1.47× | `line-height: 1.47` | `lineSpacing: 15 × 0.47 ≈ 7.1 pt` |
109
+
110
+ ### Shape
111
+
112
+ | Token | Value | CSS var | Swift constant |
113
+ |---|---|---|---|
114
+ | `shape.circleSize` | 64 px/pt | `--size-circle` | `sizeCircle = 64` |
115
+
116
+ ---
117
+
118
+ ## Screen layout (both sides)
119
+
120
+ ```
121
+ ┌─────────────── 393 pt ───────────────┐
122
+ │ │
123
+ │ ← 59 pt top safe-area inset → │
124
+ │ │
125
+ │ ┌─ 20pt padding ──────────────────┐ │
126
+ │ │ Calibration Screen │ │ ← heading 22pt semibold #1C1C1E
127
+ │ │ This screen is the known-… │ │ ← body 15pt regular #636366
128
+ │ └─────────────────────────────────┘ │
129
+ │ │ ← 24pt gap
130
+ │ ┌─ 20pt padding ──────────────────┐ │
131
+ │ │ ┃A┃B┃C┃D┃ 4-stripe card r=16pt │ │ ← stripes #4A90D9 #7E57C2
132
+ │ │ ┗━┻━┻━┻━┛ (textured, 40pt h) │ │ #26A69A #F4B400
133
+ │ │ ● ← #E8744F circle 64×64pt │ │ ← 16pt gap below card
134
+ │ └─────────────────────────────────┘ │
135
+ │ │
136
+ └───────────────────────────────────────┘
137
+ total height: 852 pt
138
+ ```
139
+
140
+ ---
141
+
142
+ ## How to run
143
+
144
+ **H5 twin** — open `h5-twin/index.html` directly in a browser. No build step.
145
+ Set browser viewport to 393×852. (Playwright capture uses `--viewport 393x852`.)
146
+
147
+ **SwiftUI twin** (known-correct) — requires Xcode 15+ or Swift 5.9+ with iOS SDK:
148
+
149
+ ```bash
150
+ cd swiftui-twin && swift build
151
+ ```
152
+
153
+ **Divergent twin** (negative control) — same toolchain:
154
+
155
+ ```bash
156
+ cd swiftui-twin-divergent && swift build
157
+ ```
158
+
159
+ Preview in Xcode: open either package; the `#Preview` renders the screen in
160
+ an iPhone 15 Pro simulator. `.build/` is gitignored — never commit it.
161
+
162
+ ---
163
+
164
+ ## Updating this pair
165
+
166
+ If any token value is changed:
167
+ 1. Update `tokens.json`.
168
+ 2. Update the matching `--*` custom property in `h5-twin/style.css`.
169
+ 3. Update the matching constant in `swiftui-twin/.../CalibrationScreen.swift`.
170
+ 4. Re-run Stage 2.5 calibration to re-measure the floor.
171
+
172
+ The known-correct trio (`tokens.json`, `h5-twin/style.css`,
173
+ `swiftui-twin/.../CalibrationScreen.swift`) must stay in sync; `tokens.json`
174
+ is the single source of truth. The **divergent twin is intentionally NOT in
175
+ sync** — its job is to differ; only keep it textured (text + stripes) and
176
+ buildable.
@@ -0,0 +1,52 @@
1
+ <!DOCTYPE html>
2
+ <!--
3
+ Calibration Twin — h5-twin/index.html
4
+ Known-correct H5 side of the Stage 2.5 render-equivalence calibration pair.
5
+ Open directly as a static file — zero JS framework, zero build step.
6
+
7
+ IMPORTANT: every color, size, and spacing value here is driven by the CSS
8
+ custom properties in style.css, which must remain in sync with
9
+ tokens.json and swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift.
10
+ -->
11
+ <html lang="en">
12
+ <head>
13
+ <meta charset="UTF-8" />
14
+ <!--
15
+ Viewport: fixed 393px logical width, no user scaling.
16
+ Matches iPhone 15 Pro logical resolution used in the SwiftUI twin.
17
+ -->
18
+ <meta name="viewport" content="width=393, initial-scale=1.0, user-scalable=no" />
19
+ <title>Calibration Twin</title>
20
+ <link rel="stylesheet" href="style.css" />
21
+ </head>
22
+ <body>
23
+ <div class="screen">
24
+
25
+ <!-- TEXT BLOCK: heading + body paragraph -->
26
+ <section class="text-block" aria-label="text-block">
27
+ <h1 class="text-block__heading">Calibration Screen</h1>
28
+ <p class="text-block__body">
29
+ This screen is the known-correct H5 twin used to measure the
30
+ cross-renderer fidelity floor. It is not a conversion target.
31
+ </p>
32
+ </section>
33
+
34
+ <!-- SHAPE BLOCK: structured (multi-color stripes) card + circle.
35
+ Deliberately NOT a flat solid block — SSIM is insensitive to a
36
+ uniform mean shift on flat content, so the calibration region must
37
+ carry structure for the floor to be meaningful. -->
38
+ <section class="shape-block" aria-label="shape-block">
39
+ <div class="shape-block__card" aria-label="card">
40
+ <div class="shape-block__stripe shape-block__stripe--a"></div>
41
+ <div class="shape-block__stripe shape-block__stripe--b"></div>
42
+ <div class="shape-block__stripe shape-block__stripe--c"></div>
43
+ <div class="shape-block__stripe shape-block__stripe--d"></div>
44
+ </div>
45
+ <div class="shape-block__circle-row">
46
+ <div class="shape-block__circle" aria-label="circle"></div>
47
+ </div>
48
+ </section>
49
+
50
+ </div>
51
+ </body>
52
+ </html>
@@ -0,0 +1,133 @@
1
+ /* Calibration Twin — style.css
2
+ Design tokens as CSS custom properties.
3
+ Every value here MUST match tokens.json and CalibrationScreen.swift exactly.
4
+ Do NOT add, remove, or change values without updating all three files.
5
+ */
6
+
7
+ /* ── Design Tokens ──────────────────────────────────────────────── */
8
+ :root {
9
+ /* colors */
10
+ --color-background: #F2F2F7;
11
+ --color-card: #4A90D9;
12
+ --color-circle: #E8744F;
13
+ --color-text-heading: #1C1C1E;
14
+ --color-text-body: #636366;
15
+
16
+ /* card stripes — structured multi-color region (NOT a flat block) so the
17
+ measured SSIM floor is meaningful (flat content makes SSIM blind to a
18
+ uniform mean shift). */
19
+ --color-stripe-a: #4A90D9; /* blue */
20
+ --color-stripe-b: #7E57C2; /* purple */
21
+ --color-stripe-c: #26A69A; /* teal */
22
+ --color-stripe-d: #F4B400; /* amber */
23
+
24
+ /* spacing */
25
+ --space-safe-top: 59px; /* iPhone 15 Pro top safe-area inset */
26
+ --space-page-h: 20px; /* horizontal page padding */
27
+ --space-section-gap: 24px; /* gap between text block and shape block */
28
+ --space-card-padding: 20px; /* inner padding of the blue card */
29
+ --space-card-gap: 16px; /* gap between card and circle row */
30
+
31
+ /* radii */
32
+ --radius-card: 16px;
33
+
34
+ /* typography */
35
+ --font-family: -apple-system, system-ui, BlinkMacSystemFont, sans-serif;
36
+ --font-size-heading: 22px;
37
+ --font-weight-heading: 600; /* semibold */
38
+ --line-height-heading: 1.27; /* ~28px → matches SF heading leading */
39
+ --font-size-body: 15px;
40
+ --font-weight-body: 400; /* regular */
41
+ --line-height-body: 1.47; /* ~22px → matches SF body leading */
42
+
43
+ /* shape */
44
+ --size-circle: 64px;
45
+ }
46
+
47
+ /* ── Reset / Base ───────────────────────────────────────────────── */
48
+ *, *::before, *::after {
49
+ box-sizing: border-box;
50
+ margin: 0;
51
+ padding: 0;
52
+ }
53
+
54
+ html, body {
55
+ width: 393px; /* iPhone 15 Pro logical width */
56
+ min-height: 852px; /* iPhone 15 Pro logical height */
57
+ background-color: var(--color-background);
58
+ font-family: var(--font-family);
59
+ -webkit-font-smoothing: antialiased;
60
+ }
61
+
62
+ /* ── Screen container ───────────────────────────────────────────── */
63
+ .screen {
64
+ width: 393px;
65
+ min-height: 852px;
66
+ background-color: var(--color-background);
67
+ padding-top: var(--space-safe-top);
68
+ padding-left: var(--space-page-h);
69
+ padding-right: var(--space-page-h);
70
+ }
71
+
72
+ /* ── Text block ─────────────────────────────────────────────────── */
73
+ .text-block {
74
+ /* sits immediately below safe-area inset, no extra top margin */
75
+ }
76
+
77
+ .text-block__heading {
78
+ font-family: var(--font-family);
79
+ font-size: var(--font-size-heading);
80
+ font-weight: var(--font-weight-heading);
81
+ line-height: var(--line-height-heading);
82
+ color: var(--color-text-heading);
83
+ }
84
+
85
+ .text-block__body {
86
+ margin-top: 8px;
87
+ font-family: var(--font-family);
88
+ font-size: var(--font-size-body);
89
+ font-weight: var(--font-weight-body);
90
+ line-height: var(--line-height-body);
91
+ color: var(--color-text-body);
92
+ }
93
+
94
+ /* ── Shape block ────────────────────────────────────────────────── */
95
+ .shape-block {
96
+ margin-top: var(--space-section-gap);
97
+ }
98
+
99
+ .shape-block__card {
100
+ background-color: var(--color-card);
101
+ border-radius: var(--radius-card);
102
+ /* fixed height instead of padding-only so the stripes have a stable box;
103
+ 2 × card padding (40px) keeps the same overall card height as before. */
104
+ height: calc(var(--space-card-padding) * 2);
105
+ overflow: hidden;
106
+ display: flex;
107
+ flex-direction: row;
108
+ }
109
+
110
+ /* 4 equal-width vertical stripes — a structured, multi-color region. */
111
+ .shape-block__stripe {
112
+ flex: 1 1 25%;
113
+ height: 100%;
114
+ }
115
+ .shape-block__stripe--a { background-color: var(--color-stripe-a); }
116
+ .shape-block__stripe--b { background-color: var(--color-stripe-b); }
117
+ .shape-block__stripe--c { background-color: var(--color-stripe-c); }
118
+ .shape-block__stripe--d { background-color: var(--color-stripe-d); }
119
+
120
+ .shape-block__circle-row {
121
+ margin-top: var(--space-card-gap);
122
+ display: flex;
123
+ flex-direction: row;
124
+ align-items: center;
125
+ }
126
+
127
+ .shape-block__circle {
128
+ width: var(--size-circle);
129
+ height: var(--size-circle);
130
+ border-radius: 50%;
131
+ background-color: var(--color-circle);
132
+ flex-shrink: 0;
133
+ }
@@ -0,0 +1,26 @@
1
+ // swift-tools-version: 5.9
2
+ // Calibration Twin — SwiftUI Package
3
+ // Zero third-party dependencies. Requires Xcode 15+ / Swift 5.9+.
4
+
5
+ import PackageDescription
6
+
7
+ let package = Package(
8
+ name: "CalibrationScreen",
9
+ platforms: [
10
+ .iOS(.v17),
11
+ .macOS(.v14)
12
+ ],
13
+ products: [
14
+ .library(
15
+ name: "CalibrationScreen",
16
+ targets: ["CalibrationScreen"]
17
+ )
18
+ ],
19
+ targets: [
20
+ .target(
21
+ name: "CalibrationScreen",
22
+ dependencies: [],
23
+ path: "Sources/CalibrationScreen"
24
+ )
25
+ ]
26
+ )