claudecode-omc 5.6.6 → 5.6.8

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 (179) hide show
  1. package/.local/skills/THIRD_PARTY_LICENSES/AvdLee-SwiftUI-Agent-Skill.LICENSE +21 -0
  2. package/.local/skills/THIRD_PARTY_LICENSES/Dimillian-Skills.LICENSE +21 -0
  3. package/.local/skills/THIRD_PARTY_LICENSES/README.md +36 -0
  4. package/.local/skills/THIRD_PARTY_LICENSES/twostraws-swiftui-agent-skill.LICENSE +21 -0
  5. package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  10. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  11. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  12. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  13. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  40. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  41. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  42. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  43. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  44. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  45. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  46. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  47. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  48. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  49. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  50. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  51. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  52. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  53. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  54. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  55. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  56. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  57. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  58. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  59. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  60. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  61. package/.local/skills/ios-debugger-agent/SKILL.md +51 -0
  62. package/.local/skills/ios-debugger-agent/agents/openai.yaml +4 -0
  63. package/.local/skills/swift-concurrency-expert/SKILL.md +105 -0
  64. package/.local/skills/swift-concurrency-expert/agents/openai.yaml +4 -0
  65. package/.local/skills/swift-concurrency-expert/references/approachable-concurrency.md +63 -0
  66. package/.local/skills/swift-concurrency-expert/references/swift-6-2-concurrency.md +272 -0
  67. package/.local/skills/swift-concurrency-expert/references/swiftui-concurrency-tour-wwdc.md +33 -0
  68. package/.local/skills/swiftui-expert-skill/SKILL.md +162 -0
  69. package/.local/skills/swiftui-expert-skill/references/accessibility-patterns.md +215 -0
  70. package/.local/skills/swiftui-expert-skill/references/animation-advanced.md +403 -0
  71. package/.local/skills/swiftui-expert-skill/references/animation-basics.md +284 -0
  72. package/.local/skills/swiftui-expert-skill/references/animation-transitions.md +326 -0
  73. package/.local/skills/swiftui-expert-skill/references/charts-accessibility.md +135 -0
  74. package/.local/skills/swiftui-expert-skill/references/charts.md +602 -0
  75. package/.local/skills/swiftui-expert-skill/references/focus-patterns.md +299 -0
  76. package/.local/skills/swiftui-expert-skill/references/image-optimization.md +203 -0
  77. package/.local/skills/swiftui-expert-skill/references/latest-apis.md +488 -0
  78. package/.local/skills/swiftui-expert-skill/references/layout-best-practices.md +266 -0
  79. package/.local/skills/swiftui-expert-skill/references/liquid-glass.md +423 -0
  80. package/.local/skills/swiftui-expert-skill/references/list-patterns.md +446 -0
  81. package/.local/skills/swiftui-expert-skill/references/macos-scenes.md +318 -0
  82. package/.local/skills/swiftui-expert-skill/references/macos-views.md +357 -0
  83. package/.local/skills/swiftui-expert-skill/references/macos-window-styling.md +303 -0
  84. package/.local/skills/swiftui-expert-skill/references/performance-patterns.md +403 -0
  85. package/.local/skills/swiftui-expert-skill/references/scroll-patterns.md +293 -0
  86. package/.local/skills/swiftui-expert-skill/references/sheet-navigation-patterns.md +363 -0
  87. package/.local/skills/swiftui-expert-skill/references/state-management.md +388 -0
  88. package/.local/skills/swiftui-expert-skill/references/text-patterns.md +32 -0
  89. package/.local/skills/swiftui-expert-skill/references/trace-analysis.md +295 -0
  90. package/.local/skills/swiftui-expert-skill/references/trace-recording.md +134 -0
  91. package/.local/skills/swiftui-expert-skill/references/view-structure.md +780 -0
  92. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/analyze_trace.cpython-313.pyc +0 -0
  93. package/.local/skills/swiftui-expert-skill/scripts/__pycache__/record_trace.cpython-313.pyc +0 -0
  94. package/.local/skills/swiftui-expert-skill/scripts/analyze_trace.py +301 -0
  95. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__init__.py +1 -0
  96. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/__init__.cpython-313.pyc +0 -0
  97. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/causes.cpython-313.pyc +0 -0
  98. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/correlate.cpython-313.pyc +0 -0
  99. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/events.cpython-313.pyc +0 -0
  100. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hangs.cpython-313.pyc +0 -0
  101. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/hitches.cpython-313.pyc +0 -0
  102. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/summary.cpython-313.pyc +0 -0
  103. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/swiftui.cpython-313.pyc +0 -0
  104. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/time_profiler.cpython-313.pyc +0 -0
  105. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xctrace.cpython-313.pyc +0 -0
  106. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/__pycache__/xml_utils.cpython-313.pyc +0 -0
  107. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/causes.py +187 -0
  108. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/correlate.py +179 -0
  109. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/events.py +291 -0
  110. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hangs.py +108 -0
  111. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/hitches.py +145 -0
  112. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/summary.py +243 -0
  113. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/swiftui.py +195 -0
  114. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/time_profiler.py +135 -0
  115. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xctrace.py +117 -0
  116. package/.local/skills/swiftui-expert-skill/scripts/instruments_parser/xml_utils.py +224 -0
  117. package/.local/skills/swiftui-expert-skill/scripts/record_trace.py +252 -0
  118. package/.local/skills/swiftui-liquid-glass/SKILL.md +90 -0
  119. package/.local/skills/swiftui-liquid-glass/agents/openai.yaml +4 -0
  120. package/.local/skills/swiftui-liquid-glass/references/liquid-glass.md +280 -0
  121. package/.local/skills/swiftui-performance-audit/SKILL.md +106 -0
  122. package/.local/skills/swiftui-performance-audit/agents/openai.yaml +4 -0
  123. package/.local/skills/swiftui-performance-audit/references/code-smells.md +150 -0
  124. package/.local/skills/swiftui-performance-audit/references/demystify-swiftui-performance-wwdc23.md +46 -0
  125. package/.local/skills/swiftui-performance-audit/references/optimizing-swiftui-performance-instruments.md +29 -0
  126. package/.local/skills/swiftui-performance-audit/references/profiling-intake.md +44 -0
  127. package/.local/skills/swiftui-performance-audit/references/report-template.md +47 -0
  128. package/.local/skills/swiftui-performance-audit/references/understanding-hangs-in-your-app.md +33 -0
  129. package/.local/skills/swiftui-performance-audit/references/understanding-improving-swiftui-performance.md +52 -0
  130. package/.local/skills/swiftui-pro/SKILL.md +108 -0
  131. package/.local/skills/swiftui-pro/agents/openai.yaml +10 -0
  132. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.png +0 -0
  133. package/.local/skills/swiftui-pro/assets/swiftui-pro-icon.svg +29 -0
  134. package/.local/skills/swiftui-pro/references/accessibility.md +13 -0
  135. package/.local/skills/swiftui-pro/references/api.md +39 -0
  136. package/.local/skills/swiftui-pro/references/data.md +43 -0
  137. package/.local/skills/swiftui-pro/references/design.md +32 -0
  138. package/.local/skills/swiftui-pro/references/hygiene.md +9 -0
  139. package/.local/skills/swiftui-pro/references/navigation.md +14 -0
  140. package/.local/skills/swiftui-pro/references/performance.md +46 -0
  141. package/.local/skills/swiftui-pro/references/swift.md +56 -0
  142. package/.local/skills/swiftui-pro/references/views.md +36 -0
  143. package/.local/skills/swiftui-ui-patterns/SKILL.md +95 -0
  144. package/.local/skills/swiftui-ui-patterns/agents/openai.yaml +4 -0
  145. package/.local/skills/swiftui-ui-patterns/references/app-wiring.md +201 -0
  146. package/.local/skills/swiftui-ui-patterns/references/async-state.md +96 -0
  147. package/.local/skills/swiftui-ui-patterns/references/components-index.md +50 -0
  148. package/.local/skills/swiftui-ui-patterns/references/controls.md +57 -0
  149. package/.local/skills/swiftui-ui-patterns/references/deeplinks.md +66 -0
  150. package/.local/skills/swiftui-ui-patterns/references/focus.md +90 -0
  151. package/.local/skills/swiftui-ui-patterns/references/form.md +97 -0
  152. package/.local/skills/swiftui-ui-patterns/references/grids.md +71 -0
  153. package/.local/skills/swiftui-ui-patterns/references/haptics.md +71 -0
  154. package/.local/skills/swiftui-ui-patterns/references/input-toolbar.md +51 -0
  155. package/.local/skills/swiftui-ui-patterns/references/lightweight-clients.md +93 -0
  156. package/.local/skills/swiftui-ui-patterns/references/list.md +86 -0
  157. package/.local/skills/swiftui-ui-patterns/references/loading-placeholders.md +38 -0
  158. package/.local/skills/swiftui-ui-patterns/references/macos-settings.md +71 -0
  159. package/.local/skills/swiftui-ui-patterns/references/matched-transitions.md +59 -0
  160. package/.local/skills/swiftui-ui-patterns/references/media.md +73 -0
  161. package/.local/skills/swiftui-ui-patterns/references/menu-bar.md +101 -0
  162. package/.local/skills/swiftui-ui-patterns/references/navigationstack.md +159 -0
  163. package/.local/skills/swiftui-ui-patterns/references/overlay.md +45 -0
  164. package/.local/skills/swiftui-ui-patterns/references/performance.md +62 -0
  165. package/.local/skills/swiftui-ui-patterns/references/previews.md +48 -0
  166. package/.local/skills/swiftui-ui-patterns/references/scroll-reveal.md +133 -0
  167. package/.local/skills/swiftui-ui-patterns/references/scrollview.md +87 -0
  168. package/.local/skills/swiftui-ui-patterns/references/searchable.md +71 -0
  169. package/.local/skills/swiftui-ui-patterns/references/sheets.md +155 -0
  170. package/.local/skills/swiftui-ui-patterns/references/split-views.md +72 -0
  171. package/.local/skills/swiftui-ui-patterns/references/tabview.md +114 -0
  172. package/.local/skills/swiftui-ui-patterns/references/theming.md +71 -0
  173. package/.local/skills/swiftui-ui-patterns/references/title-menus.md +93 -0
  174. package/.local/skills/swiftui-ui-patterns/references/top-bar.md +49 -0
  175. package/.local/skills/swiftui-view-refactor/SKILL.md +202 -0
  176. package/.local/skills/swiftui-view-refactor/agents/openai.yaml +4 -0
  177. package/.local/skills/swiftui-view-refactor/references/mv-patterns.md +161 -0
  178. package/bundled/manifest.json +1 -1
  179. package/package.json +1 -1
@@ -0,0 +1,530 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * pixel-diff.mjs — Stage 5: visual diff cascade for h5-to-swiftui
4
+ *
5
+ * Usage:
6
+ * node pixel-diff.mjs <imgA> <imgB> [--bbox-map manifest.json] [--calibration calibration.json]
7
+ * [--component <name>] [--out-mask <path.png>]
8
+ * [--gen-bbox-map gen-bboxes.json]
9
+ *
10
+ * Diff cascade (in order):
11
+ * 1. pHash Hamming (DCT 8×8) — raw distance + phash_fast_candidate flag
12
+ * (necessary-not-sufficient; this script NEVER decides "converged" — the
13
+ * verdict is evaluate-convergence.mjs's job, not pixel-diff's)
14
+ * 2. Region split using bbox-map (if provided):
15
+ * - text regions: scored by layout-box IoU + fg/bg token-color CIEDE2000 ΔE
16
+ * (NOT glyph SSIM — cross-renderer glyphs are not comparable)
17
+ * IoU is REAL only when --gen-bbox-map supplies the generated bbox for
18
+ * the region; otherwise iou is `null` (never a fabricated 1.0).
19
+ * - non-text regions: SSIM (8-pixel window) + CIEDE2000 ΔE
20
+ * 3. AA-tolerant diff mask PNG (YIQ threshold + AA skip)
21
+ * 4. Inter-component spacing delta — `null` unless generated positions are
22
+ * supplied via --gen-bbox-map (never a fabricated {top:0,leading:0})
23
+ *
24
+ * Inputs are assumed CO-REGISTERED (same dimensions). pixel-diff.mjs does NOT
25
+ * normalize: callers MUST pre-normalize both sides via Stage 2.5
26
+ * `calibration.transform` (crop/resample/P3→sRGB) BEFORE calling this.
27
+ * Mismatched dimensions are a HARD error (exit 1), never a silent partial diff.
28
+ *
29
+ * --gen-bbox-map <json> schema (component-relative pixel coords):
30
+ * { "ComponentName": [ { "label": "price", "x": .., "y": .., "w": .., "h": .. }, ... ] }
31
+ * or a flat array [ { "label": .., "x": .., "y": .., "w": .., "h": .. } ].
32
+ * A region's IoU is computed via bboxIoU(refBox, genBox) when a generated
33
+ * box with the same label (or same mark order) is present.
34
+ *
35
+ * Outputs:
36
+ * JSON to stdout — schema: h5-to-swiftui/diff@1
37
+ * Optional diff mask PNG via --out-mask
38
+ *
39
+ * Exit codes:
40
+ * 0 — diff computed, JSON on stdout
41
+ * 1 — fatal error (bad args, file not found, image-size mismatch)
42
+ * 2 — image dependency (pngjs) not installed (actionable hint printed)
43
+ *
44
+ * Examples:
45
+ * node pixel-diff.mjs reference.png generated.png
46
+ * node pixel-diff.mjs reference.png generated.png --bbox-map manifest.json --out-mask diff.png
47
+ * node pixel-diff.mjs reference.png generated.png --calibration calibration.json --component ProductCard
48
+ * node pixel-diff.mjs ref.png gen.png --bbox-map manifest.json --gen-bbox-map gen-bboxes.json
49
+ */
50
+
51
+ import { existsSync, mkdirSync } from 'node:fs';
52
+ import { resolve, dirname } from 'node:path';
53
+
54
+ import {
55
+ requirePng,
56
+ loadPng,
57
+ savePng,
58
+ pHash,
59
+ hammingDistance,
60
+ ssimWindow,
61
+ ciede2000Region,
62
+ buildDiffMask,
63
+ bboxIoU,
64
+ quantile,
65
+ } from './_imglib.mjs';
66
+
67
+ // ── CLI ──────────────────────────────────────────────────────────────────────
68
+
69
+ const args = process.argv.slice(2);
70
+
71
+ if (args.includes('--help') || args.includes('-h')) {
72
+ console.log(`pixel-diff.mjs — h5-to-swiftui visual diff cascade
73
+
74
+ Usage:
75
+ node pixel-diff.mjs <imgA> <imgB> [--bbox-map manifest.json]
76
+ [--calibration calibration.json]
77
+ [--component <name>] [--out-mask <path.png>]
78
+ [--gen-bbox-map <gen-bboxes.json>]
79
+
80
+ Arguments:
81
+ <imgA> Reference image (PNG, sRGB)
82
+ <imgB> Generated image (PNG, sRGB — P3 must be pre-converted)
83
+ --bbox-map <path> reference/manifest.json with bbox data for region split
84
+ --calibration <path> calibration.json from calibrate-render.mjs
85
+ --component <name> Component name for the output schema (default: "unknown")
86
+ --out-mask <path> Write AA-tolerant diff mask PNG to this path
87
+ --gen-bbox-map <path> Generated-component bboxes (component-relative px).
88
+ When present, text-region IoU is the REAL bboxIoU
89
+ against the matching generated box. When absent,
90
+ iou is null (never a fabricated 1.0).
91
+
92
+ Output (stdout):
93
+ JSON matching schema h5-to-swiftui/diff@1
94
+
95
+ Exit codes:
96
+ 0 Success — JSON on stdout
97
+ 1 Fatal error (bad args, file not found, image-size mismatch)
98
+ 2 pngjs not installed
99
+
100
+ Co-registration:
101
+ pixel-diff.mjs does NOT normalize. Inputs MUST already be the same
102
+ dimensions; pre-normalize via Stage 2.5 calibration.transform first.
103
+ Mismatched dimensions are a HARD error (exit 1), not a silent partial diff.
104
+
105
+ Diff cascade:
106
+ pHash Hamming → raw distance + phash_fast_candidate (necessary, NOT
107
+ sufficient — the verdict is evaluate-convergence.mjs's)
108
+ text regions → IoU (real via --gen-bbox-map, else null) + fg/bg ΔE
109
+ non-text regions → SSIM (8px window) + CIEDE2000 ΔE
110
+ full image → AA-tolerant mask + global SSIM
111
+
112
+ Examples:
113
+ node pixel-diff.mjs ref.png gen.png
114
+ node pixel-diff.mjs ref.png gen.png --bbox-map .h5-to-swiftui/reference/manifest.json \\
115
+ --component ProductCard --out-mask .h5-to-swiftui/diff/ProductCard.iter1.mask.png
116
+ node pixel-diff.mjs ref.png gen.png --bbox-map manifest.json \\
117
+ --gen-bbox-map .h5-to-swiftui/gen/ProductCard.bboxes.json --component ProductCard
118
+ `);
119
+ process.exit(0);
120
+ }
121
+
122
+ // Collect positional args
123
+ const positionals = args.filter(a => !a.startsWith('--'));
124
+ if (positionals.length < 2) {
125
+ console.error('Error: <imgA> and <imgB> are required.\nRun with --help for usage.');
126
+ process.exit(1);
127
+ }
128
+
129
+ const imgAPath = resolve(positionals[0]);
130
+ const imgBPath = resolve(positionals[1]);
131
+
132
+ function getFlag(flag, fallback = null) {
133
+ const idx = args.indexOf(flag);
134
+ if (idx === -1) return fallback;
135
+ const val = args[idx + 1];
136
+ if (!val || val.startsWith('--')) {
137
+ console.error(`Error: ${flag} requires an argument.`);
138
+ process.exit(1);
139
+ }
140
+ return val;
141
+ }
142
+
143
+ const bboxMapPath = getFlag('--bbox-map');
144
+ const calibPath = getFlag('--calibration');
145
+ const componentName = getFlag('--component', 'unknown');
146
+ const outMaskPath = getFlag('--out-mask');
147
+ const genBboxMapPath = getFlag('--gen-bbox-map');
148
+
149
+ // ── Validate inputs ───────────────────────────────────────────────────────────
150
+
151
+ for (const [label, p] of [['imgA', imgAPath], ['imgB', imgBPath]]) {
152
+ if (!existsSync(p)) {
153
+ console.error(`Error: ${label} not found: ${p}`);
154
+ process.exit(1);
155
+ }
156
+ }
157
+
158
+ if (bboxMapPath && !existsSync(bboxMapPath)) {
159
+ console.error(`Error: --bbox-map not found: ${bboxMapPath}`);
160
+ process.exit(1);
161
+ }
162
+
163
+ if (calibPath && !existsSync(calibPath)) {
164
+ console.error(`Error: --calibration not found: ${calibPath}`);
165
+ process.exit(1);
166
+ }
167
+
168
+ if (genBboxMapPath && !existsSync(genBboxMapPath)) {
169
+ console.error(`Error: --gen-bbox-map not found: ${genBboxMapPath}`);
170
+ process.exit(1);
171
+ }
172
+
173
+ // ── Load PNG dependency early for clear error ─────────────────────────────────
174
+ await requirePng();
175
+
176
+ // ── Load images ───────────────────────────────────────────────────────────────
177
+
178
+ let imgA, imgB;
179
+ try {
180
+ [imgA, imgB] = await Promise.all([loadPng(imgAPath), loadPng(imgBPath)]);
181
+ } catch (e) {
182
+ console.error(`Error: failed to load image: ${e.message}`);
183
+ process.exit(1);
184
+ }
185
+
186
+ // HARD error on size mismatch — a silent partial diff fabricates a metric.
187
+ // Inputs MUST be co-registered first via Stage 2.5 calibration.transform
188
+ // (crop → resample → P3→sRGB). pixel-diff.mjs deliberately does NOT normalize.
189
+ if (imgA.width !== imgB.width || imgA.height !== imgB.height) {
190
+ console.error(
191
+ `Error: image sizes differ — A=${imgA.width}×${imgA.height}, B=${imgB.width}×${imgB.height}\n` +
192
+ 'pixel-diff.mjs requires CO-REGISTERED inputs (identical dimensions).\n' +
193
+ 'Pre-normalize BOTH sides through Stage 2.5 calibration.transform\n' +
194
+ '(crop simulator chrome → resample to common logical raster → P3→sRGB)\n' +
195
+ 'BEFORE calling pixel-diff.mjs. It refuses a partial overlap diff because\n' +
196
+ 'that would emit a fabricated metric over a mis-registered region.\n' +
197
+ (calibPath
198
+ ? '--calibration was supplied, but pixel-diff.mjs does not itself apply\n' +
199
+ 'the transform: the caller must pre-normalize the rasters.\n'
200
+ : '')
201
+ );
202
+ process.exit(1);
203
+ }
204
+
205
+ const overlapW = imgA.width;
206
+ const overlapH = imgA.height;
207
+
208
+ // ── Load optional JSON inputs ─────────────────────────────────────────────────
209
+
210
+ let bboxMap = null;
211
+ let calibData = null;
212
+ let genBboxMap = null;
213
+
214
+ if (bboxMapPath) {
215
+ try {
216
+ const { readFileSync } = await import('node:fs');
217
+ bboxMap = JSON.parse(readFileSync(bboxMapPath, 'utf8'));
218
+ } catch (e) {
219
+ console.error(`Error: cannot parse --bbox-map: ${e.message}`);
220
+ process.exit(1);
221
+ }
222
+ }
223
+
224
+ if (genBboxMapPath) {
225
+ try {
226
+ const { readFileSync } = await import('node:fs');
227
+ genBboxMap = JSON.parse(readFileSync(genBboxMapPath, 'utf8'));
228
+ } catch (e) {
229
+ console.error(`Error: cannot parse --gen-bbox-map: ${e.message}`);
230
+ process.exit(1);
231
+ }
232
+ }
233
+
234
+ if (calibPath) {
235
+ try {
236
+ const { readFileSync } = await import('node:fs');
237
+ calibData = JSON.parse(readFileSync(calibPath, 'utf8'));
238
+ } catch (e) {
239
+ console.error(`Error: cannot parse --calibration: ${e.message}`);
240
+ process.exit(1);
241
+ }
242
+ }
243
+
244
+ // ── Step 1: pHash Hamming ─────────────────────────────────────────────────────
245
+
246
+ const hashA = pHash(imgA);
247
+ const hashB = pHash(imgB);
248
+ const phashHamming = hammingDistance(hashA, hashB);
249
+
250
+ // ── Step 2: Region-split diff ─────────────────────────────────────────────────
251
+
252
+ /**
253
+ * Extract bounding boxes for a component from the manifest.
254
+ * Returns { textBoxes: [{x,y,w,h,mark,label}], nontextBoxes: [{x,y,w,h,mark,label}] }
255
+ * Falls back to treating the whole image as non-text if no manifest.
256
+ */
257
+ function extractRegions(manifest, compName) {
258
+ const textBoxes = [];
259
+ const nontextBoxes = [];
260
+
261
+ if (!manifest) {
262
+ return { textBoxes, nontextBoxes: [{ x: 0, y: 0, w: overlapW, h: overlapH, mark: 0, label: 'full' }] };
263
+ }
264
+
265
+ // Walk screens → components; match by component name
266
+ let markIndex = 1;
267
+ const screens = manifest.screens ?? [];
268
+ for (const screen of screens) {
269
+ for (const comp of (screen.components ?? [])) {
270
+ if (comp.name !== compName && compName !== 'unknown') continue;
271
+ const bbox = comp.bbox_css_px;
272
+ if (!bbox) continue;
273
+ const region = {
274
+ x: Math.round(bbox.x ?? 0),
275
+ y: Math.round(bbox.y ?? 0),
276
+ w: Math.round(bbox.width ?? bbox.w ?? 0),
277
+ h: Math.round(bbox.height ?? bbox.h ?? 0),
278
+ mark: markIndex++,
279
+ label: comp.name,
280
+ };
281
+ // Heuristic: classify as text if the component name contains a text keyword
282
+ const isText = /text|label|title|heading|paragraph|caption|link|badge|tag/i.test(comp.name) ||
283
+ (comp.is_text === true);
284
+ if (isText) textBoxes.push(region);
285
+ else nontextBoxes.push(region);
286
+ }
287
+ }
288
+
289
+ // Also check for explicit bboxes array on the manifest (bbox_map format)
290
+ const bboxes = manifest.bboxes ?? [];
291
+ for (const bb of bboxes) {
292
+ const region = {
293
+ x: Math.round(bb.x ?? 0),
294
+ y: Math.round(bb.y ?? 0),
295
+ w: Math.round(bb.w ?? bb.width ?? 0),
296
+ h: Math.round(bb.h ?? bb.height ?? 0),
297
+ mark: markIndex++,
298
+ label: bb.label ?? bb.name ?? 'region',
299
+ fg_color: bb.fg_color,
300
+ bg_color: bb.bg_color,
301
+ };
302
+ if (bb.is_text) textBoxes.push(region);
303
+ else nontextBoxes.push(region);
304
+ }
305
+
306
+ // Fallback: no matching regions found → whole image as non-text
307
+ if (textBoxes.length === 0 && nontextBoxes.length === 0) {
308
+ nontextBoxes.push({ x: 0, y: 0, w: overlapW, h: overlapH, mark: 0, label: 'full' });
309
+ }
310
+
311
+ return { textBoxes, nontextBoxes };
312
+ }
313
+
314
+ const { textBoxes, nontextBoxes } = extractRegions(bboxMap, componentName);
315
+
316
+ // Score non-text regions: SSIM + CIEDE2000
317
+ const nontextResults = [];
318
+ for (const box of nontextBoxes) {
319
+ const roi = { x: box.x, y: box.y, w: box.w, h: box.h };
320
+ const ssim = ssimWindow(imgA, imgB, roi, 8);
321
+ const { p95: deltaE_p95 } = ciede2000Region(imgA, imgB, roi);
322
+ nontextResults.push({
323
+ mark: box.mark,
324
+ label: box.label,
325
+ ssim: Math.round(ssim * 1000) / 1000,
326
+ deltaE_p95: Math.round(deltaE_p95 * 100) / 100,
327
+ });
328
+ }
329
+
330
+ // Resolve a generated bbox for a reference text region.
331
+ // genBboxMap accepted forms:
332
+ // { "<component>": [ {label,x,y,w,h}, ... ] } (component-keyed)
333
+ // [ {label,x,y,w,h}, ... ] (flat array)
334
+ // Match priority: same `label`, else positional (Nth text box ↔ Nth gen box).
335
+ function genBoxListFor(map, compName) {
336
+ if (!map) return null;
337
+ if (Array.isArray(map)) return map;
338
+ if (compName && Array.isArray(map[compName])) return map[compName];
339
+ // single-component map with one array value
340
+ const vals = Object.values(map).filter(Array.isArray);
341
+ return vals.length === 1 ? vals[0] : null;
342
+ }
343
+ const genBoxList = genBoxListFor(genBboxMap, componentName);
344
+
345
+ function findGenBox(refBox, idx) {
346
+ if (!genBoxList) return null;
347
+ if (refBox.label) {
348
+ const byLabel = genBoxList.find(
349
+ g => (g.label ?? g.name) === refBox.label
350
+ );
351
+ if (byLabel) return byLabel;
352
+ }
353
+ return genBoxList[idx] ?? null;
354
+ }
355
+
356
+ // Score text regions: IoU + token-color ΔE (fg + bg)
357
+ // - iou: REAL layout-box IoU via bboxIoU(refBox, genBox) when --gen-bbox-map
358
+ // supplies the matching generated box; otherwise `null` (never a
359
+ // fabricated 1.0 — the absence of a generated bbox is reported honestly).
360
+ // - fg_deltaE: median pixel color in the top-center strip (glyph-dominated).
361
+ // - bg_deltaE: corner samples (background-dominated).
362
+ const textResults = [];
363
+ let textIdx = 0;
364
+ for (const box of textBoxes) {
365
+ const refBox = { x: box.x, y: box.y, w: box.w, h: box.h, label: box.label };
366
+
367
+ // REAL IoU only when a generated bbox is supplied for this region.
368
+ const genBox = findGenBox(refBox, textIdx);
369
+ let iou = null;
370
+ let iouNote;
371
+ if (genBox) {
372
+ iou = Math.round(
373
+ bboxIoU(
374
+ { x: refBox.x, y: refBox.y, w: refBox.w, h: refBox.h },
375
+ {
376
+ x: genBox.x ?? 0,
377
+ y: genBox.y ?? 0,
378
+ w: genBox.w ?? genBox.width ?? 0,
379
+ h: genBox.h ?? genBox.height ?? 0,
380
+ }
381
+ ) * 1000
382
+ ) / 1000;
383
+ } else {
384
+ iouNote =
385
+ 'no generated bbox supplied (--gen-bbox-map absent or no match) — ' +
386
+ 'IoU is null, NOT assumed 1.0';
387
+ }
388
+ textIdx++;
389
+
390
+ // fg: top-center strip (glyph color region)
391
+ const fgRoi = {
392
+ x: refBox.x + Math.floor(refBox.w * 0.25),
393
+ y: refBox.y,
394
+ w: Math.max(1, Math.floor(refBox.w * 0.5)),
395
+ h: Math.max(1, Math.floor(refBox.h * 0.3)),
396
+ };
397
+ const { p95: fg_deltaE } = ciede2000Region(imgA, imgB, clipRoi(fgRoi, overlapW, overlapH));
398
+
399
+ // bg: corner samples
400
+ const bgRoi = {
401
+ x: refBox.x,
402
+ y: refBox.y + Math.floor(refBox.h * 0.7),
403
+ w: Math.max(1, Math.floor(refBox.w * 0.2)),
404
+ h: Math.max(1, Math.floor(refBox.h * 0.3)),
405
+ };
406
+ const { p95: bg_deltaE } = ciede2000Region(imgA, imgB, clipRoi(bgRoi, overlapW, overlapH));
407
+
408
+ textResults.push({
409
+ mark: box.mark,
410
+ label: box.label,
411
+ iou, // REAL bboxIoU, or null when no gen bbox
412
+ ...(iouNote ? { iou_note: iouNote } : {}),
413
+ fg_deltaE: Math.round(fg_deltaE * 100) / 100,
414
+ bg_deltaE: Math.round(bg_deltaE * 100) / 100,
415
+ });
416
+ }
417
+
418
+ function clipRoi(roi, maxW, maxH) {
419
+ return {
420
+ x: Math.max(0, Math.min(roi.x, maxW - 1)),
421
+ y: Math.max(0, Math.min(roi.y, maxH - 1)),
422
+ w: Math.max(1, Math.min(roi.w, maxW - roi.x)),
423
+ h: Math.max(1, Math.min(roi.h, maxH - roi.y)),
424
+ };
425
+ }
426
+
427
+ // ── Step 3: AA-tolerant diff mask ─────────────────────────────────────────────
428
+
429
+ const { mask, diffPixelCount, totalPixels } = buildDiffMask(imgA, imgB);
430
+
431
+ if (outMaskPath) {
432
+ const maskDir = dirname(outMaskPath);
433
+ mkdirSync(maskDir, { recursive: true });
434
+ try {
435
+ await savePng(outMaskPath, mask);
436
+ } catch (e) {
437
+ process.stderr.write(`Warning: could not write diff mask: ${e.message}\n`);
438
+ }
439
+ }
440
+
441
+ // ── Step 4: Global SSIM ───────────────────────────────────────────────────────
442
+
443
+ const globalSsim = ssimWindow(imgA, imgB, { x: 0, y: 0, w: overlapW, h: overlapH }, 8);
444
+
445
+ // ── Step 5: Inter-component spacing delta ────────────────────────────────────
446
+
447
+ // Spacing delta is only real when generated component positions are known
448
+ // (supplied via --gen-bbox-map). Without them we emit `null` + a note — NOT
449
+ // fabricated zeros, which would be a false "no drift" signal.
450
+ let interComponentSpacingDelta = null;
451
+ let spacingNote;
452
+
453
+ if (bboxMap && bboxMap.screens) {
454
+ const screens = bboxMap.screens ?? [];
455
+ const allComps = screens.flatMap(s => s.components ?? []);
456
+ if (allComps.length >= 2) {
457
+ const sorted = allComps
458
+ .filter(c => c.bbox_css_px)
459
+ .sort((a, b) => (a.bbox_css_px.y ?? 0) - (b.bbox_css_px.y ?? 0));
460
+ if (sorted.length >= 2) {
461
+ const first = sorted[0].bbox_css_px;
462
+ const second = sorted[1].bbox_css_px;
463
+ const expectedGap = (second.y ?? 0) - ((first.y ?? 0) + (first.height ?? 0));
464
+
465
+ if (genBoxList && genBoxList.length >= 2) {
466
+ // Real delta: compare reference gap vs generated gap.
467
+ const g = [...genBoxList]
468
+ .filter(b => (b.y ?? 0) !== undefined)
469
+ .sort((a, b) => (a.y ?? 0) - (b.y ?? 0));
470
+ const genGap =
471
+ (g[1].y ?? 0) - ((g[0].y ?? 0) + (g[0].h ?? g[0].height ?? 0));
472
+ const topDelta = Math.round(genGap - expectedGap);
473
+ const leadingDelta = Math.round((g[0].x ?? 0) - (first.x ?? 0));
474
+ interComponentSpacingDelta = {
475
+ top: topDelta,
476
+ leading: leadingDelta,
477
+ expected_gap_px: Math.round(expectedGap),
478
+ measured_gap_px: Math.round(genGap),
479
+ };
480
+ } else {
481
+ // No generated positions — report null, not fabricated zeros.
482
+ spacingNote =
483
+ 'null: generated component positions unknown ' +
484
+ '(--gen-bbox-map absent); reporting null, NOT zeros';
485
+ }
486
+ }
487
+ }
488
+ }
489
+
490
+ // ── Assemble output (schema: h5-to-swiftui/diff@1) ───────────────────────────
491
+
492
+ const output = {
493
+ schema: 'h5-to-swiftui/diff@1',
494
+ component: componentName,
495
+ phash_hamming: phashHamming,
496
+ // necessary-not-sufficient: a low phash distance is a FAST CANDIDATE only.
497
+ // It is NOT a terminal "converged" signal — the region gate must still pass.
498
+ // The verdict is evaluate-convergence.mjs's job, not pixel-diff's.
499
+ phash_fast_candidate: phashHamming <= 5,
500
+ regions: {
501
+ text: textResults.map(r => ({
502
+ mark: r.mark,
503
+ ...(r.label && r.label !== 'region' ? { label: r.label } : {}),
504
+ iou: r.iou, // number | null
505
+ ...(r.iou_note ? { iou_note: r.iou_note } : {}),
506
+ fg_deltaE: r.fg_deltaE,
507
+ bg_deltaE: r.bg_deltaE,
508
+ })),
509
+ nontext: nontextResults.map(r => ({
510
+ mark: r.mark,
511
+ ...(r.label && r.label !== 'full' ? { label: r.label } : {}),
512
+ ssim: r.ssim,
513
+ deltaE_p95: r.deltaE_p95,
514
+ })),
515
+ },
516
+ // null (not {top:0,leading:0}) when generated positions are unknown.
517
+ inter_component_spacing_delta_pt: interComponentSpacingDelta,
518
+ ...(spacingNote ? { inter_component_spacing_delta_note: spacingNote } : {}),
519
+ diff_mask_png: outMaskPath ?? null,
520
+ global_ssim: Math.round(globalSsim * 1000) / 1000,
521
+ diff_pixel_fraction: Math.round((diffPixelCount / totalPixels) * 10000) / 10000,
522
+ };
523
+
524
+ // Attach calibration floor reference if provided
525
+ if (calibData?.floor) {
526
+ output.calibration_floor_ref = calibData.floor;
527
+ }
528
+
529
+ process.stdout.write(JSON.stringify(output, null, 2) + '\n');
530
+ process.exit(0);