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,209 @@
1
+ # Design Token Extraction — Stage 1
2
+
3
+ Used by `scripts/extract-tokens.mjs`. Produces `tokens.json` (W3C DTCG format)
4
+ and `token-gaps.json`. Both files are mandatory inputs to Stage 3 (scaffold) and
5
+ Stage 4 (per-component rewrite). Stage 4 must never inline a value that belongs
6
+ in `token-gaps.json` — see the token-miss rule below.
7
+
8
+ ---
9
+
10
+ ## The static ∪ runtime pipeline
11
+
12
+ Two extraction passes run unconditionally. Their outputs are merged then
13
+ normalized. Neither pass alone is sufficient.
14
+
15
+ | Pass | What it gives | What it misses | How to run |
16
+ |---|---|---|---|
17
+ | Static parse | All _declared_ custom props, Tailwind config, Sass vars — complete enumeration including unused tokens | Values behind `calc()`, `var()` chains, media-query overrides, `@layer` override priority | Read source files: CSS `--*` props; `tailwind.config.js` `theme.extend`; `@theme` blocks (Tailwind v4); compiled `.css` output of Sass |
18
+ | Runtime `getComputedStyle` | _Resolved_ values as the browser actually computes them — ground truth for any `calc`, `var`, inheritance, or conditional override | Tokens that are declared but never applied to a rendered element | Playwright: load each page, call `getComputedStyle` on a representative element per token class; run twice — once with `prefers-color-scheme: light`, once with `dark` |
19
+
20
+ Merge rule: static gives the token namespace; runtime gives the resolved value.
21
+ If a static token has no runtime-resolved value, keep static value with
22
+ `"source": "static-only"` and flag in `token-gaps.json` if the value contains
23
+ unresolved `var()` or `calc()`.
24
+
25
+ Source: findings.md RQ3 (W3C DTCG draft format; Project Wallace; Style
26
+ Dictionary v4; Tokens Studio).
27
+
28
+ ---
29
+
30
+ ## Tradeoff table
31
+
32
+ | Criterion | Static parse only | Runtime only | Static ∪ runtime |
33
+ |---|---|---|---|
34
+ | Completeness | High — all declared tokens found | Low — only tokens applied to visible elements | High |
35
+ | Resolved truth | Low — `var()`/`calc()` unresolved | High — browser computed values | High |
36
+ | Dark-mode pairs | Manual inference required | Automatic (run twice) | Automatic |
37
+ | Build required | No | Yes (Playwright + dev server or static build) | Dev server preferred; static build acceptable |
38
+ | Speed | Fast | Slow (~5–30 s per page) | Moderate (static fast, runtime adds per page) |
39
+
40
+ ---
41
+
42
+ ## Normalization steps (run after merge, in order)
43
+
44
+ ### 1. Color deduplication
45
+ Compare all color values using CIEDE2000 (ΔE00). If ΔE00 < 2 between two colors,
46
+ they are the same perceptual token — keep the one with the more semantic name
47
+ (e.g. `--color-primary` over `--tw-color-blue-600`) and discard the duplicate.
48
+ This typically collapses "25 grays" from Tailwind into 4–6 semantic tokens.
49
+
50
+ ### 2. Spacing scale inference
51
+ Collect all spacing values (padding, margin, gap, width/height in px/rem). Detect
52
+ the base unit: if values cluster around multiples of 4 px (or 0.25 rem), the
53
+ project uses a 4 px grid. Flag values that do not fit the inferred scale in
54
+ `token-gaps.json` (they may be one-offs or errors). Map to SwiftUI `CGFloat`
55
+ points (1 pt = 1 CSS px at 1× logical resolution).
56
+
57
+ ### 3. Type scale grouping
58
+ Group font-size values by recurrence. A value appearing on 3+ elements is a
59
+ type-scale step. Assign semantic names: `body`, `caption`, `title`, `headline`,
60
+ `largeTitle` (follow Apple HIG naming where possible for SwiftUI
61
+ `Font.TextStyle` matching). Record both `size` and `weight` per step.
62
+
63
+ ### 4. Light/dark pairing
64
+ For each color token, pair the light-scheme resolved value with the dark-scheme
65
+ resolved value. Unpaired tokens (no dark equivalent found) are flagged in
66
+ `token-gaps.json` with `"issue": "no-dark-pair"`.
67
+
68
+ ---
69
+
70
+ ## `tokens.json` (W3C DTCG draft format — `$value`/`$type`)
71
+
72
+ ```json
73
+ {
74
+ "color": {
75
+ "primary": {
76
+ "$value": "#0A7AFF",
77
+ "$type": "color",
78
+ "$description": "Brand primary, resolved from --color-primary via runtime",
79
+ "dark": { "$value": "#3395FF" }
80
+ },
81
+ "background": {
82
+ "$value": "#FFFFFF",
83
+ "$type": "color",
84
+ "dark": { "$value": "#000000" }
85
+ }
86
+ },
87
+ "spacing": {
88
+ "base": { "$value": "4px", "$type": "dimension" },
89
+ "md": { "$value": "16px", "$type": "dimension" },
90
+ "lg": { "$value": "24px", "$type": "dimension" }
91
+ },
92
+ "typography": {
93
+ "body": {
94
+ "$type": "typography",
95
+ "$value": { "fontSize": "16px", "fontWeight": "400", "lineHeight": "1.5" }
96
+ },
97
+ "title": {
98
+ "$type": "typography",
99
+ "$value": { "fontSize": "20px", "fontWeight": "600", "lineHeight": "1.3" }
100
+ }
101
+ }
102
+ }
103
+ ```
104
+
105
+ All values use CSS-native units in the DTCG file. Stage 3 (scaffold) converts to
106
+ SwiftUI `CGFloat` / `Font` equivalents during the `DesignTokens` enum generation.
107
+
108
+ ---
109
+
110
+ ## `token-gaps.json` shape
111
+
112
+ ```json
113
+ {
114
+ "schema": "h5-to-swiftui/token-gaps@1",
115
+ "gaps": [
116
+ {
117
+ "property": "border-radius",
118
+ "css_value": "var(--radius-card)",
119
+ "resolved_value": null,
120
+ "issue": "unresolved-var",
121
+ "source_file": "src/components/Card.module.css",
122
+ "source_line": 14,
123
+ "action": "manual-extraction-required"
124
+ },
125
+ {
126
+ "property": "color",
127
+ "css_value": "#9B9B9B",
128
+ "issue": "no-dark-pair",
129
+ "source_file": "src/components/Label.tsx",
130
+ "source_line": 8,
131
+ "action": "verify-dark-contrast"
132
+ }
133
+ ]
134
+ }
135
+ ```
136
+
137
+ ---
138
+
139
+ ## The token-miss rule (hard constraint for Stage 4)
140
+
141
+ > **Any CSS value with no extractable token in `tokens.json` goes to
142
+ > `token-gaps.json` and is NEVER silently inlined by Stage 4.**
143
+
144
+ Stage 4's LLM prompt must include the full `tokens.json` vocabulary and must
145
+ instruct the model: "Use only tokens from the provided vocabulary. If a required
146
+ value is not in the vocabulary, emit a `// TOKEN-MISSING: <property>` comment and
147
+ use the closest token — do not hardcode the raw value."
148
+
149
+ This rule exists because inlined magic numbers break:
150
+ - Dark-mode adaptation (`.colorset` references the token name, not the hex)
151
+ - The convergence loop's color ΔE tracking (tokens give expected values)
152
+ - Any future design-system update
153
+
154
+ A Stage 4 component that contains hardcoded color hex, raw spacing numbers, or
155
+ raw font sizes without a corresponding `tokens.json` entry fails the idiomatic
156
+ lint check and is reprocessed, not delivered.
157
+
158
+ ---
159
+
160
+ ## Style Dictionary → SwiftUI output (Stage 3)
161
+
162
+ Stage 3 runs Style Dictionary v4 (DTCG-native) on `tokens.json` to produce:
163
+
164
+ ### `DesignTokens.swift` — a Swift enum namespace
165
+
166
+ ```swift
167
+ // Auto-generated by extract-tokens.mjs — do not edit manually
168
+ import SwiftUI
169
+
170
+ enum DesignTokens {
171
+ enum Color {
172
+ static let primary = SwiftUI.Color("dt/primary")
173
+ static let background = SwiftUI.Color("dt/background")
174
+ }
175
+ enum Spacing {
176
+ static let base: CGFloat = 4
177
+ static let md: CGFloat = 16
178
+ static let lg: CGFloat = 24
179
+ }
180
+ enum Typography {
181
+ static let body = Font.system(size: 16, weight: .regular)
182
+ static let title = Font.system(size: 20, weight: .semibold)
183
+ }
184
+ }
185
+ ```
186
+
187
+ ### XCAssets `.colorset` files (dark-mode automatic)
188
+
189
+ For each color token pair, Style Dictionary emits a `.colorset` directory with
190
+ `Contents.json` containing both light and dark `value` entries. The `dt/<name>`
191
+ asset catalog name matches the `SwiftUI.Color("dt/<name>")` initializer above.
192
+
193
+ This means dark mode requires **no conditional code** in Stage 4 components —
194
+ `DesignTokens.Color.primary` automatically resolves to the correct scheme.
195
+
196
+ ---
197
+
198
+ ## Extraction strategy by detected styling system
199
+
200
+ From `stack-report.json` (`styling` field), `extract-tokens.mjs` selects:
201
+
202
+ | Detected styling | Static strategy |
203
+ |---|---|
204
+ | `tailwind-v3` | Parse `tailwind.config.js` `theme` + `theme.extend`; resolve `colors`, `spacing`, `fontSize`, `fontWeight`, `borderRadius` |
205
+ | `tailwind-v4` | Parse `@theme` block in the primary CSS entry point; no config file exists |
206
+ | `css-modules` | Parse each `.module.css` file for `--*` custom properties; also scan for Compose-style utility patterns |
207
+ | `sass` | Parse compiled CSS output (run `sass` if build script present); scan `.scss` for `$var` declarations |
208
+ | `css-in-js` | Static parse is limited (values are runtime JS expressions); rely more heavily on the runtime `getComputedStyle` pass; flag all JS-expression values in `token-gaps.json` |
209
+ | `plain-css` | Parse all `--*` custom properties in CSS files imported by the entry point |
@@ -0,0 +1,209 @@
1
+ # High-Risk Surface Triage — Stage 1
2
+
3
+ Triage runs during Stage 1 (static analysis), **before any code generation**. Its
4
+ output is `risk-triage.json`. Every surface in the tier catalog below is evaluated
5
+ against every discovered usage in the source. Tier assignment is final for a given
6
+ iOS floor — it is not re-evaluated per component.
7
+
8
+ Source: findings.md RQ6 (Android→iOS pilot 2507.16037; lottie-ios 4.3; rive-ios;
9
+ Stripe iOS; Firebase iOS; Google Maps SPM; Apple WWDC; WebRTC iOS).
10
+
11
+ ---
12
+
13
+ ## Tier definitions
14
+
15
+ | Tier | Name | What the skill emits | Compiler behavior | Human action required |
16
+ |---|---|---|---|---|
17
+ | 1 | **Auto** | Full, correct native implementation | Compiles and runs | Verify behavior, no code change expected |
18
+ | 2 | **Assisted** | Compiling stub with `// VERIFY` comment at every deviation | Compiles; may not match source behavior exactly | Review marked deviations; test on device |
19
+ | 3 | **Human-only** | Non-compiling stub with `fatalError` + `// OMC-CONVERSION: HUMAN-ONLY` block | **Does not compile by design** | Must be replaced before the app ships |
20
+
21
+ Tier 3 stubs **must fail to ship**. Using `fatalError`/`preconditionFailure`
22
+ ensures the Xcode build reminder is the app crashing at the stub call site during
23
+ testing — not a subtle runtime wrong behavior that reaches users. (findings.md RQ6
24
+ cites the 2507.16037 pilot: 43.2% of auto-converted files were invalid with no
25
+ clean build "without substantial human effort".)
26
+
27
+ ---
28
+
29
+ ## iOS floor awareness
30
+
31
+ Some conversions are only available above a minimum iOS version. When
32
+ `--ios-floor` is below the listed floor, the tier upgrades one level (Tier 1 →
33
+ Tier 2 if a workaround exists, Tier 2 → Tier 3 if not).
34
+
35
+ | API | Minimum iOS |
36
+ |---|---|
37
+ | `KeyframeAnimator` | 17 |
38
+ | `scrollTransition` | 17 |
39
+ | `onScrollGeometryChange` | 18 |
40
+ | Swift Charts (`Chart`) | 16 |
41
+ | `Canvas` | 15 |
42
+ | `Layout` protocol | 16 |
43
+ | `AVPlayer` with HLS | 7 (always available in v1 scope) |
44
+
45
+ ---
46
+
47
+ ## Tier catalog
48
+
49
+ Detection signal: file/line reference from static scan + grep of source, not
50
+ runtime behavior. The `detect` column is what `scripts/detect-stack.mjs` and the
51
+ Stage 1 static analyzer grep for.
52
+
53
+ | Surface | Detection signal in source | Native iOS counterpart | Tier | Fidelity risk |
54
+ |---|---|---|---|---|
55
+ | **Lottie `.json` / `.lottie`** | `lottie-web` dep; `new Lottie.loadAnimation`; `.lottie` file references | `lottie-ios 4.3` — same asset file, `LottieAnimationView` | **1 Auto** | Low — asset file identical, timing matches |
56
+ | **Rive `.riv`** | `@rive-app/canvas` dep; `new Rive(...)` with `.riv` file | `rive-ios` — same `.riv` asset, `RiveViewModel` | **1 Auto** | Low — asset file identical |
57
+ | **REST + JSON** | `fetch('/api/...')`, `axios.get`, `XMLHttpRequest` JSON endpoints | `URLSession.shared.data(from:)` async/await | **1 Auto** | None — semantics equivalent; ATS flag applies to `http://` |
58
+ | **HLS `<video>`** | `<video src="...m3u8">`, HLS.js dep | `AVPlayer` + `AVPlayerViewController` or custom `VideoPlayer` | **1 Auto** | Low — HLS is a first-class iOS media format |
59
+ | **`localStorage` KV** | `localStorage.getItem`, `localStorage.setItem` | `UserDefaults.standard` | **1 Auto** (non-secrets only) | **Critical caveat:** auth tokens/secrets must NOT go to UserDefaults — see anti-pattern section |
60
+ | **Chart.js / D3 charts** | `chart.js` dep; `new Chart(ctx, {type:...})`; `d3` dep with `d3.select` | Swift Charts (`Chart`) iOS 16+ | **2 Assisted** | Medium — data series ports cleanly; custom chart types (radar, custom scales) may not |
61
+ | **Static SVG** | Inline `<svg>` elements or `.svg` file `<img>` embeds with no animation | SwiftUI `Path` / `Image(systemName:)` / `Canvas` | **2 Assisted** | Medium — simple shapes port; complex filters, patterns, and gradients require `// VERIFY` |
62
+ | **Scroll-linked effects** | `window.addEventListener('scroll', ...)`, `IntersectionObserver`, `scroll-timeline` CSS | `.scrollTransition` (iOS 17+) / `onScrollGeometryChange` (iOS 18+) | **2 Assisted** | Medium — parallax and fade effects port; physics-based scroll effects are Tier 3 |
63
+ | **Maps (basic region + markers)** | `google.maps.Map`, `mapboxgl.Map`, `<MapContainer>` (leaflet) basic usage | `MapKit` / `Map` SwiftUI view (iOS 17+) | **2 Assisted** | Medium — region display and pin markers port; custom tile layers, routing UI, Street View are Tier 3 |
64
+ | **GSAP / CSS keyframes (linear/cubic)** | `gsap.to(...)` linear/cubic easing; `@keyframes` with `from`/`to` or step keyframes | `KeyframeAnimator` (iOS 17+) / `withAnimation(.easeInOut)` | **2 Assisted** | Medium — simple easing ports; spring physics, morphing, and stagger sequences are Tier 3 |
65
+ | **`http://` REST endpoints** | `fetch('http://...')` or `axios.get('http://...')` with non-TLS scheme | `URLSession` with ATS exemption or enforce TLS | **2 Assisted** | High — ATS blocks `http://` by default; emit `// WARNING: ATS — this URL must be upgraded to HTTPS or an ATS exception added to Info.plist` |
66
+ | **`IndexedDB` / complex storage** | `indexedDB.open(...)`, `idb` dep | No direct equivalent; use CoreData, SwiftData (iOS 17+), or SQLite | **2 Assisted** | High — data model ports; query API is completely different |
67
+ | **Canvas 2D (non-chart)** | `canvas.getContext('2d')`, `ctx.drawImage`, `ctx.fillRect` as primary rendering | SwiftUI `Canvas` (iOS 15+) / `drawRect` in `UIView` | **2 Assisted** (simple drawing) / **3** (pixel-manipulation shaders) | High — drawing commands port individually; `getImageData`/`putImageData` pixel manipulation is Tier 3 |
68
+ | **WebGL / WebGPU** | `canvas.getContext('webgl')`, `canvas.getContext('webgpu')`, `three.js`, `babylon.js` deps | Metal / MetalKit / RealityKit (requires GLSL→MSL translation, state model redesign) | **3 Human-only** | Critical — GLSL and WGSL do not mechanically translate to MSL; coordinate system, depth buffer conventions, and GPU state model differ; no automatable path |
69
+ | **`requestAnimationFrame` physics** | `requestAnimationFrame` in a game loop or physics update; Matter.js, Cannon.js deps | UIKit `CADisplayLink` or custom physics engine | **3 Human-only** | Critical — frame-loop physics is architectural; no declarative SwiftUI equivalent |
70
+ | **WebRTC** | `RTCPeerConnection`, `navigator.mediaDevices.getUserMedia`, `simple-peer` dep | `WebRTC.framework` (Google) or `LiveKit` SDK; requires entitlements + provisioning | **3 Human-only** | Critical — signaling protocol, ICE, and media pipeline require full reimplementation |
71
+ | **Stripe.js / payment surfaces** | `@stripe/stripe-js`, `loadStripe(...)`, `CardElement` | Stripe iOS SDK + Apple Pay entitlement | **3 Human-only** | Critical — publishable key handling, webhook secrets, and PCI compliance require human audit; never auto-port |
72
+ | **Firebase / OAuth provisioning** | `firebase/auth`, `GoogleSignIn`, `signInWithPopup` | Firebase iOS SDK / GoogleSignIn iOS; requires `GoogleService-Info.plist`, URL schemes, entitlements | **3 Human-only** | High — client IDs, redirect URIs, and entitlement provisioning cannot be inferred from web config |
73
+ | **Analytics / ATT** | `gtag`, `mixpanel`, `amplitude`, `posthog`; any event tracking | ATT framework (`AppTrackingTransparency`); native analytics SDK | **3 Human-only** | High — ATT permission prompt must appear before any IDFA access; timing and phrasing are legally significant in some jurisdictions |
74
+ | **`SessionStorage` / auth tokens in storage** | `sessionStorage.setItem('token', ...)`, `localStorage.setItem('authToken', ...)` | `Keychain` via `Security.framework` | **3 Human-only** | Critical — see anti-pattern section |
75
+
76
+ ---
77
+
78
+ ## Anti-pattern call-out (never emit these)
79
+
80
+ These three patterns are the canonical examples of **confident, compiling, subtly
81
+ wrong code** — the exact failure mode the skill forbids.
82
+
83
+ ### 1. Auth tokens in UserDefaults (not Keychain)
84
+ ```swift
85
+ // WRONG — never emit this for auth tokens
86
+ UserDefaults.standard.set(token, forKey: "authToken")
87
+
88
+ // Correct — emit a Tier-3 stub instead
89
+ // OMC-CONVERSION: HUMAN-ONLY
90
+ // Reason: auth tokens must be stored in Keychain (Security.framework kSecClassGenericPassword),
91
+ // not UserDefaults. UserDefaults is not encrypted at rest and is accessible without the
92
+ // Secure Enclave. Port this using SecItemAdd/SecItemCopyMatching or a Keychain wrapper library.
93
+ preconditionFailure("Token storage not implemented — see conversion-report.json")
94
+ ```
95
+
96
+ ### 2. ATS-dropped `http://` URLs
97
+ ```swift
98
+ // WRONG — compiles but ATS blocks this at runtime on all iOS apps without an exception
99
+ let url = URL(string: "http://api.example.com/data")!
100
+
101
+ // Correct — emit Tier-2 stub with explicit warning
102
+ // VERIFY: ATS blocks http:// by default. Either:
103
+ // (a) Upgrade the server to https:// (preferred), or
104
+ // (b) Add NSAppTransportSecurity > NSAllowsArbitraryLoads exception to Info.plist (not recommended for production).
105
+ let url = URL(string: "http://api.example.com/data")! // ATS WARNING — see above
106
+ ```
107
+
108
+ ### 3. Fake-timed `withAnimation` (mimicking CSS duration with a hardcoded constant)
109
+ ```swift
110
+ // WRONG — not an animation port, it is a guess that will look wrong on different devices
111
+ withAnimation(.easeInOut(duration: 0.3)) { ... }
112
+
113
+ // Correct for Tier-2 (when CSS timing is known):
114
+ // VERIFY: CSS animation was 'transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)'
115
+ // The cubic-bezier maps to approximately .timingCurve(0.4, 0, 0.2, 1, duration: 0.3)
116
+ // Verify visually against the reference render.
117
+ withAnimation(.timingCurve(0.4, 0, 0.2, 1, duration: 0.3)) { ... } // VERIFY
118
+ ```
119
+
120
+ ---
121
+
122
+ ## Tier-3 non-compiling stub format
123
+
124
+ Every Tier-3 stub must include the machine-parseable block. This format allows
125
+ `conversion-report.json` to be generated by grep, not by LLM post-processing.
126
+
127
+ ```swift
128
+ // OMC-CONVERSION: HUMAN-ONLY
129
+ // surface: WebGL
130
+ // file: src/components/Globe.tsx
131
+ // reason: WebGL (Three.js) cannot be automatically converted to Metal/MetalKit.
132
+ // GLSL shaders require manual translation to MSL. GPU state model and coordinate
133
+ // conventions differ. Suggested native counterpart: Metal + SceneKit or RealityKit.
134
+ // action: Replace this stub with a Metal-based implementation before shipping.
135
+ struct GlobeView: View {
136
+ var body: some View {
137
+ fatalError("GlobeView: WebGL conversion not implemented — OMC-CONVERSION: HUMAN-ONLY")
138
+ }
139
+ }
140
+ ```
141
+
142
+ The `// OMC-CONVERSION: HUMAN-ONLY` marker on the first line is the grep anchor.
143
+ Fields `surface`, `file`, and `reason` are on subsequent `//` lines in `key: value`
144
+ format. The `fatalError` is the last statement in `body`.
145
+
146
+ ---
147
+
148
+ ## `conversion-report.json` schema
149
+
150
+ Aggregated at Stage 7. One entry per triaged surface (all tiers — Tier 1/2 entries
151
+ confirm what was auto-handled; Tier 3 entries are the required human action list).
152
+
153
+ ```json
154
+ {
155
+ "schema": "h5-to-swiftui/conversion-report@1",
156
+ "generated_at": "2026-05-19T12:00:00Z",
157
+ "ios_floor": 17,
158
+ "surfaces": [
159
+ {
160
+ "surface": "WebGL",
161
+ "file": "Sources/App/Globe/GlobeView.swift",
162
+ "line": 12,
163
+ "severity": "critical",
164
+ "tier": 3,
165
+ "native_counterpart": "Metal + SceneKit or RealityKit",
166
+ "action_required": "Replace fatalError stub with Metal-based implementation; translate GLSL shaders to MSL manually"
167
+ },
168
+ {
169
+ "surface": "Lottie",
170
+ "file": "Sources/App/Onboarding/OnboardingAnimationView.swift",
171
+ "line": 8,
172
+ "severity": "none",
173
+ "tier": 1,
174
+ "native_counterpart": "lottie-ios 4.3 LottieAnimationView",
175
+ "action_required": "None — auto-converted; verify animation timing on device"
176
+ },
177
+ {
178
+ "surface": "http-endpoint",
179
+ "file": "Sources/App/Network/APIClient.swift",
180
+ "line": 34,
181
+ "severity": "high",
182
+ "tier": 2,
183
+ "native_counterpart": "URLSession — upgrade URL to https://",
184
+ "action_required": "Upgrade http:// to https:// or add ATS exception to Info.plist"
185
+ }
186
+ ],
187
+ "summary": {
188
+ "tier1_count": 3,
189
+ "tier2_count": 2,
190
+ "tier3_count": 1,
191
+ "human_action_required": true
192
+ }
193
+ }
194
+ ```
195
+
196
+ | Field | Type | Notes |
197
+ |---|---|---|
198
+ | `surface` | string | Short surface name from the tier catalog (e.g. `"WebGL"`, `"Lottie"`, `"REST"`) |
199
+ | `file` | string | Output Swift file path (not input H5 file) |
200
+ | `line` | integer | Line number of the stub or generated call site in the Swift file |
201
+ | `severity` | string | `"critical"` \| `"high"` \| `"medium"` \| `"low"` \| `"none"` |
202
+ | `tier` | integer | 1, 2, or 3 |
203
+ | `native_counterpart` | string | Specific framework/API/library to use |
204
+ | `action_required` | string | Concrete instruction for the human reviewer |
205
+
206
+ The `conversion-report.json` is the primary deliverable for communicating what a
207
+ human must finish. The final run summary (`convergence-summary.json`) must lead
208
+ with `"needs-human": true` and the Tier-3 count when any Tier-3 surfaces are
209
+ present.
@@ -0,0 +1,193 @@
1
+ # Stage 2.5 — Render-Equivalence Calibration
2
+
3
+ **Why this exists.** A simulator screenshot and a Playwright screenshot of
4
+ the "same" screen are *not* comparable rasters. Diffing them directly makes
5
+ every metric meaningless and the loop oscillates on non-defects. Calibration
6
+ defines the exact normalization that makes them comparable, and **measures
7
+ the best achievable cross-renderer agreement** so Stage 5 gates against a
8
+ real number instead of an asserted constant.
9
+
10
+ ## The non-comparability sources (all must be neutralized)
11
+
12
+ | Source | H5 (Playwright/WebKit) | iOS (simctl/SwiftUI) | Fix |
13
+ |---|---|---|---|
14
+ | Chrome | no status bar / home indicator | full framebuffer incl. status bar, notch, home indicator | crop simulator content rect via known safe-area insets for `--device` |
15
+ | Scale | logical px × `deviceScaleFactor` | physical px (e.g. @3x) | resample **both** to a common logical raster (device logical pt × fixed scale) with an **identical filter** (Lanczos3 both sides) |
16
+ | Color | sRGB-tagged PNG | Display-P3 framebuffer | convert P3 → sRGB (relative colorimetric) before any ΔE |
17
+ | Coords | DOM bbox in CSS px @ logical viewport | view frame in screen pt | apply `transform` (scale + safe-area offset) DOM-bbox → screen-rect |
18
+ | Text raster | Skia glyphs | CoreText glyphs | irreducible — handled in Stage 5 by judging text regions on layout-box IoU + resolved token-color ΔE, NOT glyph-raster SSIM |
19
+
20
+ ## Calibration procedure
21
+
22
+ Inputs: `assets/calibration/` ships a **hand-built known-correct SwiftUI
23
+ screen** + its **H5 twin** (must include both a text block and a non-text
24
+ color/shape block).
25
+
26
+ 1. Render H5 twin via `capture-reference.mjs` settings; render the SwiftUI
27
+ twin via `sim-screenshot.sh` for `--device`.
28
+ 2. Apply the normalization pipeline above → two co-registered logical
29
+ rasters + the `transform`.
30
+ 3. Run `pixel-diff.mjs` on the *known-correct* pair. The result is the
31
+ **best achievable** cross-renderer agreement for this exact toolchain:
32
+ - `floor.ssim_nontext` — SSIM over non-text regions (expect ≥ ~0.98)
33
+ - `floor.deltaE_p95` — 95th-pct CIEDE2000 over color regions
34
+ - `floor.text_iou` — layout-box IoU achievable for text regions
35
+ - `floor.ssim_global` — whole-screen SSIM of a *correct* screen (this is
36
+ the realistic ceiling, typically **< 0.995**, often ~0.97–0.99 with text)
37
+ 4. Sanity bound: if the known-correct pair cannot beat conservative bounds
38
+ (`ssim_nontext ≥ 0.95`, `text_iou ≥ 0.9`), the toolchain is **not
39
+ measurable** → write `blocked.json` and STOP. Never grade real screens
40
+ against an unproven metric.
41
+ 5. **Flat-content guard**: SSIM is insensitive to a uniform mean shift on
42
+ near-zero-variance (effectively flat) images — a clearly-divergent solid
43
+ pair can still score ~0.95. If BOTH normalized images are effectively flat
44
+ (luma variance below the flat threshold on each side), the floor is
45
+ unreliable → `blocked.json` + STOP, never a falsely-high floor. This is
46
+ why the bundled calibration content is **textured** (text + a multi-color
47
+ stripe region), not flat solid blocks — see
48
+ `../assets/calibration/README.md`.
49
+
50
+ ## `calibration.json` schema (consumed by Stage 5)
51
+
52
+ ```json
53
+ {
54
+ "schema": "h5-to-swiftui/calibration@1",
55
+ "pinned": {
56
+ "sim_runtime": "iOS 17.5 (21F79)",
57
+ "device": "iPhone 15 Pro",
58
+ "logical_size": [393, 852],
59
+ "render_scale": 3,
60
+ "browser": "chromium-1180",
61
+ "model_id": "claude-sonnet-4-6",
62
+ "temperature": 0
63
+ },
64
+ "transform": {
65
+ "dom_to_screen_scale": 1.0,
66
+ "safe_area_offset_pt": [0, 59],
67
+ "content_rect_pt": [0, 59, 393, 759],
68
+ "resample_filter": "lanczos3",
69
+ "color": "p3->srgb-relative"
70
+ },
71
+ "twin_hashes": {
72
+ "ref_png": "/abs/path/twin-ref.png",
73
+ "ref_sha256": "…64-hex…",
74
+ "gen_png": "/abs/path/twin-gen.png",
75
+ "gen_sha256": "…64-hex…"
76
+ },
77
+ "calibration_source": {
78
+ "h5_twin_dir": "assets/calibration/h5-twin",
79
+ "swiftui_twin_dir": "assets/calibration/swiftui-twin",
80
+ "h5_twin_source_sha256": "…64-hex…",
81
+ "swiftui_twin_source_sha256": "…64-hex…",
82
+ "source_tree_hash_algo": "sha256/sorted-relpath+filebytes/v1"
83
+ },
84
+ "floor": {
85
+ "ssim_global": 0.982,
86
+ "ssim_nontext": 0.991,
87
+ "deltaE_p95": 1.6,
88
+ "text_iou": null,
89
+ "luma_variance": { "ref": 612.4, "gen": 588.1, "flat_threshold": 9 }
90
+ },
91
+ "gate": {
92
+ "converged": {
93
+ "ssim_nontext_min": 0.986,
94
+ "deltaE_p95_max": 2.0,
95
+ "text_iou_min": null,
96
+ "require_judge_yes": true
97
+ },
98
+ "close": {
99
+ "ssim_nontext_min": 0.981,
100
+ "deltaE_p95_max": 2.4,
101
+ "text_iou_min": null,
102
+ "require_judge_equiv": true
103
+ },
104
+ "gate_explain": "human-readable only — code reads gate.converged / gate.close numerically"
105
+ },
106
+ "measured_at": "ISO8601"
107
+ }
108
+ ```
109
+
110
+ `gate.converged` / `gate.close` are **structured numeric objects**, not a
111
+ string DSL — `scripts/evaluate-convergence.mjs` evaluates them directly
112
+ without a parser (a legacy string gate is rejected as un-enforceable). They
113
+ are derived from the measured floor: `ssim_nontext_min = floor − 0.005`,
114
+ `deltaE_p95_max = floor + 0.4`, `text_iou_min = floor − 0.03` (null ⇒ the
115
+ text-IoU sub-gate is skipped for that calibration); `close` is the 2× band.
116
+ `gate_explain` is a human-only convenience string and is **never** evaluated
117
+ by code. The numbers in `floor` are **measured per run**, not shipped
118
+ constants — different Xcode/sim versions yield different floors, which is
119
+ exactly why this stage exists. `floor.luma_variance` records each side's
120
+ structure (a both-flat pair is blocked, never floored).
121
+
122
+ ### Provenance binding (enforced, not advisory)
123
+
124
+ `scripts/evaluate-convergence.mjs` **mechanically binds** the floor it grades
125
+ against — these are code-enforced fail-closed checks, not prose:
126
+
127
+ 1. **schema** — `calibration.json.schema` MUST be
128
+ `h5-to-swiftui/calibration@1`, else the grader exits 1 (it refuses to
129
+ grade against an unrecognized contract).
130
+ 2. **gate ⇐ floor** — the grader **recomputes** the expected structured gate
131
+ from `floor` with the published tolerances (converged
132
+ `ssim_nontext_min = floor − 0.005`, `deltaE_p95_max = floor + 0.4`,
133
+ `text_iou_min = floor − 0.03` when non-null; `close` = 2× band) and
134
+ rejects any `gate` that deviates beyond a 1e-4 rounding epsilon
135
+ (`gate-floor-mismatch`, exit 1). This binds the **gate to the floor**: a
136
+ hand-loosened `gate` (tight-looking `floor` + loose `gate`) is rejected
137
+ unless the `floor` itself is loosened. It does **not** bind the floor's
138
+ *value* — that is checks 3 and 4.
139
+ 3. **twin source identity ⇐ shipped twins** — `calibration_source` carries a
140
+ **deterministic SHA-256 source-tree hash** of the bundled
141
+ `assets/calibration/h5-twin` and `assets/calibration/swiftui-twin`
142
+ **source files (excluding build output/dotfiles)**. The grader
143
+ **recomputes those hashes from the actual shipped assets** (resolved
144
+ relative to the skill root, not the cwd) and fails closed
145
+ (`calibration-twin-mismatch`, exit 1) on any mismatch. This binds the
146
+ **identity of the bundled twin source files**. It does **not** re-measure
147
+ or bind the `floor` *value*: an attacker can keep the real, public,
148
+ unmodified bundled-twin source hashes here and still write a loose
149
+ `floor`. (The *rendered* PNGs are runtime-produced and not byte-stable
150
+ across machines, so the SOURCE tree — fixed and shippable — is what is
151
+ bound. `twin_hashes` is non-security provenance metadata of the exact PNGs
152
+ the floor was measured on; the *enforced* binding is the source-tree hash
153
+ of the source files.)
154
+ 4. **floor value ⇐ calibrate-render's own sanity envelope** — the grader
155
+ asserts `floor` satisfies the SAME sanity bound `calibrate-render.mjs`
156
+ enforces before it is willing to emit `calibration.json` at all
157
+ (`ssim_nontext ≥ 0.95`, non-null `text_iou ≥ 0.9`, metric-valid
158
+ `deltaE_p95`), via the shared `scripts/_calib-consts.mjs` (single source
159
+ of truth, imported by both producer and consumer). A `floor` that fails
160
+ this envelope could not have been produced by an honest
161
+ `calibrate-render.mjs` run (it writes `blocked.json`, not
162
+ `calibration.json`) ⇒ `floor-implausible`, exit 1. This **kills the
163
+ absurd-floor attack** (e.g. `ssim_nontext:0.05, deltaE_p95:200` with the
164
+ real public twin hashes copied in). It does **not** re-measure the floor:
165
+ a `floor` *within* this envelope yet looser than the TRUE measured floor
166
+ is still trusted — the grader cannot re-render the bundled twins to
167
+ re-derive the real number.
168
+
169
+ **Named irreducible residuals (honest, per §1.1) — BOTH stated, neither
170
+ hidden:**
171
+
172
+ 1. **The grader cannot re-execute the simulator renders.** It trusts the
173
+ per-iteration `pixel-diff.mjs` JSONs were produced by running the real
174
+ `pixel-diff.mjs` on real `sim-screenshot.sh` renders (bounded by that
175
+ script's no-fake build/env spine — no simulator / no build ⇒
176
+ `blocked`/`needs-human`, never converged).
177
+ 2. **The grader cannot re-measure the calibration floor.** Check 4 asserts
178
+ the supplied `floor` is within calibrate-render's own sanity envelope and
179
+ the gate is recomputed from it, but a `floor` *within* that envelope yet
180
+ looser than the true measured floor is trusted (the grader cannot
181
+ re-render the bundled twins to re-derive the real number). Mitigated by:
182
+ the orchestrator's contractual obligation to run the real,
183
+ sanity/flat-image-spined `calibrate-render.mjs`, and the human-readable
184
+ `calibration_provenance` recorded in the convergence artifact.
185
+
186
+ Both residuals are irreducible without the grader itself re-rendering; they
187
+ are stated, not hidden.
188
+
189
+ ## Reproducibility rule
190
+
191
+ Re-running calibration on the same pinned toolchain must produce `floor`
192
+ values within ±0.005 SSIM / ±0.3 ΔE. Larger drift ⇒ environment is unstable;
193
+ record it and degrade the run's claims (Stage 5 verdicts become advisory).