cornhsu-parity 0.11.1 → 0.12.0

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 (2) hide show
  1. package/README.md +246 -174
  2. package/package.json +7 -7
package/README.md CHANGED
@@ -6,249 +6,302 @@
6
6
  [![CI](https://github.com/HSU-YU-MING/cornhsu-parity/actions/workflows/ci.yml/badge.svg)](https://github.com/HSU-YU-MING/cornhsu-parity/actions/workflows/ci.yml)
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
8
 
9
- **[作品介紹與開發故事](https://cornhsu.com/parity) · [GitHub Marketplace](https://github.com/marketplace/actions/parity-design-fidelity-check) · [npm](https://www.npmjs.com/package/cornhsu-parity) · [NuGet](https://www.nuget.org/packages/Cornhsu.Parity) · MIT**
9
+ **[Project write-up](https://cornhsu.com/parity) · [GitHub Marketplace](https://github.com/marketplace/actions/parity-design-fidelity-check) · [npm](https://www.npmjs.com/package/cornhsu-parity) · [NuGet](https://www.nuget.org/packages/Cornhsu.Parity) · [繁體中文](README.zh-Hant.md) · MIT**
10
10
 
11
11
  ## GitHub Action
12
12
 
13
13
  ```yaml
14
- - uses: HSU-YU-MING/cornhsu-parity@v0.11.1 # 0.x 期間 pin 版本;發 1.0 後可改用移動式 @v1
14
+ - uses: HSU-YU-MING/cornhsu-parity@v0.12.0 # pin exact versions while 0.x; a floating @v1 arrives with 1.0
15
15
  with:
16
- config: parity.config.json # 選填,預設 parity.config.json
17
- target: /pricing # 選填,只檢查這個 route(省略 = 全部)
16
+ config: parity.config.json # optional, defaults to parity.config.json
17
+ target: /pricing # optional, check only this route (omit for all)
18
18
  ```
19
19
 
20
- 落差超過 gate 門檻就讓 CI 失敗,並把還原度報告貼成 PR 留言(原地更新不洗版)。
20
+ Fails CI when the gap exceeds the gate threshold, and posts the fidelity report as a PR
21
+ comment that updates in place instead of piling up.
21
22
 
22
- > **能進 CI 的設計還原度檢查(Property-Level Design QA)。**
23
- > 抓 Figma 設計稿與實際渲染畫面**兩邊的真實數值**做程式比對——「內距 8px,設計是 12px」,
24
- > 精確到數字,所以能當 gate:**PR 讓畫面偏離設計就打紅**,並自動把還原度報告貼成 PR 留言。
23
+ > **Design fidelity checking that can actually gate your CI (property-level design QA).**
24
+ > It compares the **real numeric values on both sides** — your Figma file and the actually
25
+ > rendered page — programmatically: "padding is 8px, the design says 12px." Because the
26
+ > output is numbers rather than an impression, it can be a gate: **a PR that drifts from the
27
+ > design turns red**, with the fidelity report posted straight onto the PR.
25
28
 
26
- **別人幫你「看見」設計落差,Parity 幫你「擋住」它。**
29
+ **Other tools help you *see* design drift. Parity *blocks* it.**
27
30
 
28
- 同類工具(Uiprobe、Loupe、OverlayQA、Floto Design Diff、PixelProof…)現在大多都做數值比對了,
29
- 真正的差別不在「數值 vs 疊圖」,而在**這件事能不能自動化**:
31
+ Most comparable tools (Uiprobe, Loupe, OverlayQA, Floto Design Diff, PixelProof…) do numeric
32
+ comparison these days. The real difference is not "numbers vs. overlays" — it is **whether the
33
+ whole thing can be automated**:
30
34
 
31
- | | 手動抽查型<br>(Uiprobe / OverlayQA / Loupe …) | **Parity** |
35
+ | | Manual spot-check tools<br>(Uiprobe / OverlayQA / Loupe …) | **Parity** |
32
36
  |---|---|---|
33
- | 誰來跑 | 人打開工具、點一點、看報告 | **CI 自動跑**,人不用在場 |
34
- | 發現落差之後 | 自己開 ticket、或把結果複製貼到 PR | **exit code 擋 PR** + 機器人自動留言(原地更新不洗版) |
35
- | 回歸把關 | 無 | **baseline 只擋「新增/惡化」**——有舊債的專案也能今天上車 |
36
- | 沒有 Figma 檔時 | 不能用 | **快照模式**:把現在的畫面凍結成基準,重構不跑版 |
37
- | 桌面 app | 不支援 | **Electron**(CDP attach 活視窗) |
38
- | 價格 | $39–249 / 月(Loupe 免費但為桌面 GUI,無法自動化) | **MIT 免費** |
37
+ | Who runs it | A person opens the tool, clicks around, reads a report | **CI runs it**, nobody has to be present |
38
+ | After a gap is found | File a ticket yourself, or paste the result into the PR | **Exit code blocks the PR** + a bot comment that updates in place |
39
+ | Regression gating | None | **A baseline gates only new/worsened gaps** — projects with existing debt can adopt it today |
40
+ | When there is no Figma file | Unusable | **Snapshot mode**: freeze today's rendering as the baseline so refactors cannot drift |
41
+ | Desktop apps | Not supported | **Electron** (attaches to the live window over CDP) |
42
+ | Price | $39–249 / month (Loupe is free but is a desktop GUI — not automatable) | **Free, MIT** |
39
43
 
40
- > 誠實說明:localhost 與登入後頁面**不再是差異點**——Uiprobe、OverlayQA 也支援。
41
- > 「免費開源」也不是獨有——Loupe 同為 MIT。**Parity 獨有的是「免費 × 可自動化」的交集。**
44
+ > Being honest about the boundaries: localhost and authenticated pages are **no longer a
45
+ > differentiator** — Uiprobe and OverlayQA support them too. Neither is "free and open source" —
46
+ > Loupe is also MIT. **What is unique to Parity is the intersection: free × automatable.**
42
47
 
43
- ## 畫面
48
+ ## Screenshots
44
49
 
45
- **報告 UI(`parity serve --watch`)**——左欄逐項列出落差與一致的節點,右側把設計框與實作框直接疊在真實畫面上;點任一項即定位,存檔後自動重掃。
50
+ **Report UI (`parity serve --watch`)** — the left column lists every node, matching or not;
51
+ the right side overlays the design boxes and the implementation boxes directly on the real
52
+ screenshot. Click any row to locate it; saving a file triggers a re-scan.
46
53
 
47
- ![Parity 報告 UI:左欄落差清單,右側設計框與實作框疊在畫面上](https://raw.githubusercontent.com/HSU-YU-MING/cornhsu-parity/master/docs/serve-ui.png)
54
+ ![Parity report UI: diff list on the left, design and implementation boxes overlaid on the page](https://raw.githubusercontent.com/HSU-YU-MING/cornhsu-parity/master/docs/serve-ui.png)
48
55
 
49
- **還原度報告**——每一列是「期望 → 實際」的具體數值、嚴重度與建議修法(含對應的 design token)。GitHub Action 以此原文自動貼成 PR 留言,同一則原地更新、不洗版。
56
+ **Fidelity report** — each row is a concrete "expected → actual" pair with its severity and a
57
+ suggested fix (including the matching design token). The GitHub Action posts this verbatim as a
58
+ PR comment, updating the same comment rather than adding new ones.
50
59
 
51
- ![Parity 還原度報告:每列為期望→實際的數值落差與建議修法](https://raw.githubusercontent.com/HSU-YU-MING/cornhsu-parity/master/docs/report.png)
60
+ ![Parity fidelity report: each row an expected→actual numeric gap with a suggested fix](https://raw.githubusercontent.com/HSU-YU-MING/cornhsu-parity/master/docs/report.png)
52
61
 
53
- ## 成果一覽
62
+ ## At a glance
54
63
 
55
64
  | | |
56
65
  |---|---|
57
- | 發佈 | NuGet 共 21 版(v0.1.0 → v0.11.1),推 tag 即以 OIDC Trusted Publishing 自動上架,repo 內零長效金鑰 |
58
- | 比對維度 | 尺寸、內距、間距、字體、顏色(CIEDE2000 ΔE)、相對位置——**刻意不比絕對座標**(彈性版面下必然誤報) |
59
- | 設計來源 | 4 種:Figma API、畫面快照、圖片 + 標註(像素取樣,任何工具匯出 PNG 即可)、JSON |
60
- | 實作端 | 網頁(含 **Shadow DOM / 同源 iframe / RWD 多斷點**)+ **Electron**(以 CDP attach 活視窗) |
61
- | 測試 | **160 條**,涵蓋 CIEDE2000 標準測資集(Sharma)、配對消歧、位置誤報防護、圖片取樣 |
62
- | CI 實證 | GitHub Action 以**外部 repo 跑真實 PR** 驗證:擋 PR、自動留言(原地更新不洗版)、baseline 回歸把關 |
63
- | 真實驗證 | **cornhsu.com 全站 21 頁由 Parity 自己守門**——dogfooding 已揪出並修掉三個 flaky 根因(最新一個:捲動觸發的進場動畫,見 0.11.1) |
66
+ | Releases | 22 on NuGet (v0.1.0 → v0.12.0). Pushing a tag publishes via OIDC Trusted Publishing — zero long-lived keys in the repo |
67
+ | Compared properties | Size, padding, spacing, typography, color (CIEDE2000 ΔE), relative position — **absolute coordinates are deliberately excluded** (they are guaranteed false positives in a flexible layout) |
68
+ | Design sources | 4: the Figma API, a rendering snapshot, an image + annotations (pixel sampling — any tool that exports a PNG works), or JSON |
69
+ | Implementation sources | The web (including **shadow DOM, same-origin iframes, and multiple responsive breakpoints**) plus **Electron** (attached to the live window over CDP) |
70
+ | Tests | **160**, covering the CIEDE2000 reference data set (Sharma), match disambiguation, position false-positive guards, and image sampling |
71
+ | Proven in CI | The GitHub Action is validated by **real PRs in an external repo**: blocking the PR, commenting automatically (updating in place), and baseline regression gating |
72
+ | Proven in production | **All 21 pages of cornhsu.com are gated by Parity itself** — dogfooding has already found and fixed three flaky root causes (most recently a scroll-triggered entrance animation; see 0.11.1) |
64
73
 
65
- ## 安裝
74
+ ## Install
66
75
 
67
- 兩條通路,選你手邊有的執行環境即可 —— **功能完全相同**。
76
+ Two channels — pick whichever runtime you already have. **They are functionally identical.**
68
77
 
69
78
  ```sh
70
- # 有 Node(前端專案常見):不需要安裝 .NET
79
+ # If you have Node (common in front-end projects): no .NET install needed
71
80
  npx cornhsu-parity check
72
81
 
73
- # 有 .NET
82
+ # If you have .NET
74
83
  dotnet tool install -g Cornhsu.Parity
75
84
  ```
76
85
 
77
- > npm 版是同一份自帶執行環境的原生執行檔,只下載你這個平台的那一份
78
- > (win32-x64 / win32-arm64 / linux-x64 / linux-arm64 / darwin-x64 / darwin-arm64)。
79
- > Playwright 預設會另外自帶一份 Node,npm 版把它拿掉、改用你現成的 Node,
80
- > 每個平台包因此省下約 88 MB。
86
+ > The npm build is the same self-contained native executable; you only download the one for
87
+ > your platform (win32-x64 / win32-arm64 / linux-x64 / linux-arm64 / darwin-x64 / darwin-arm64).
88
+ > Playwright normally bundles its own copy of Node; the npm build strips that out and uses the
89
+ > Node you already have, which saves about 88 MB per platform package.
81
90
 
82
- ## 快速開始
91
+ ## Quick start
83
92
 
84
93
  ```sh
85
- parity init # 產生 parity.config.json 範本
86
- parity install-browser # 第一次:下載 Playwright Chromium
87
- export FIGMA_TOKEN=... # scope 只需 file_content:read
88
- parity check # 比對,輸出報告 + exit code
89
- parity report # 從既有 report.json 重生 Markdown 報告(免重掃;--md 寫檔,預設印 stdout)
90
- parity snapshot # 把「現在跑著的畫面」凍結成設計基準——重構/改版守門,不需要 Figma
91
- parity lint # design lint:設計稿的值是否落在 design token 允許集合(只看設計,不比實作)
92
- parity check --reverse # 反向檢視:設計師照現有頁面重畫時,看自己的稿跟現況差在哪(不做把關)
94
+ parity init # Generate a parity.config.json template
95
+ parity install-browser # First run only: download Playwright Chromium
96
+ export FIGMA_TOKEN=... # Only the file_content:read scope is needed
97
+ parity check # Compare; writes a report and sets the exit code
98
+ parity report # Re-render the Markdown report from an existing report.json
99
+ # (no re-scan; --md writes a file, otherwise it prints to stdout)
100
+ parity snapshot # Freeze the currently rendered UI as the design baseline —
101
+ # a refactor/redesign guard that needs no Figma at all
102
+ parity lint # Design lint: are the design file's values inside the allowed
103
+ # design-token set? (design only, no implementation)
104
+ parity check --reverse # Reverse view, for a designer redrawing an existing page:
105
+ # how does my draft differ from what ships? (never gates)
93
106
  ```
94
107
 
95
- 在這個 repo 裡開發時:
108
+ When working inside this repo:
96
109
 
97
110
  ```sh
98
111
  dotnet run --project src/Parity.Cli -- check --config samples/demo/parity.config.json
99
112
  ```
100
113
 
101
- `samples/demo` 是一組**離線示範**(設計來源用本機 JSON,不需要 Figma token):
102
- `index.html` 刻意做壞了幾個地方,`parity check` 會精確報出每一條落差與未配對清單,
103
- 並因 serious 級落差回傳 exit code 1(GATE FAIL)——這正是 CI 把關的行為。
114
+ `samples/demo` is a fully **offline demo** (the design source is a local JSON file, so no Figma
115
+ token is needed): `index.html` is deliberately broken in a few places, `parity check` reports
116
+ each gap and the unmatched list precisely, and returns exit code 1 (GATE FAIL) because of the
117
+ serious-severity gaps — which is exactly the behaviour you want from a CI gate.
104
118
 
105
- ## 設定:`parity.config.json`
119
+ ## Configuration: `parity.config.json`
106
120
 
107
121
  ```jsonc
108
122
  {
109
123
  "figmaFileKey": "abcd1234",
110
124
  "designToken": "env:FIGMA_TOKEN",
111
- "mapFile": "parity.map.json", // 手動補漏的對應檔(圖層名 → selector)
112
- "tokensFile": "tokens.json", // 選配:design token(名→值);建議修法會提示對應 token
125
+ "mapFile": "parity.map.json", // manual fallback mapping (layer name → selector)
126
+ "tokensFile": "tokens.json", // optional: design tokens (name → value);
127
+ // suggested fixes will name the matching token
113
128
  "targets": [
114
129
  { "route": "/", "frame": "10:2", "url": "http://localhost:8080/" }
115
130
  ],
116
- "compare": { "position": "relative" }, // relative = 比相對位置(預設);none = 不比位置
131
+ "compare": { "position": "relative" }, // relative = compare relative position (default); none = skip position
117
132
  "tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "positionPx": 4 },
118
133
  "ignore": ["[data-parity-ignore]"],
119
134
  "gate": {
120
135
  "failOn": ["critical", "serious"],
121
- "minMatchRate": 0 // 選配:配對率低於此值(0–1)直接 FAIL;0 = 不設門檻
136
+ "minMatchRate": 0 // optional: FAIL outright below this match rate (0–1); 0 = no threshold
122
137
  }
123
138
  }
124
139
  ```
125
140
 
126
- > gate 除了看落差,也驗**配對可信度**:完全 0 配對(或設計端 0 節點)一律 GATE FAIL——
127
- > 沒配到就沒落差可擋,沉默 PASS 會是假的通過(通常是 url/frame 指錯)。`--baseline` 模式也不豁免。
141
+ > Besides gaps, the gate also validates **match integrity**: zero matches (or zero design nodes)
142
+ > is always a GATE FAIL — with nothing matched there are no gaps to catch, so a silent PASS
143
+ > would be a false pass, usually caused by a wrong url or frame. `--baseline` mode is not exempt.
128
144
 
129
- ## 目標:網頁或 Electron 桌面 app
145
+ ## Targets: a web page or an Electron desktop app
130
146
 
131
- `target.url` 決定實作端是什麼,指令與報告完全一樣:
147
+ `target.url` decides what the implementation side is; the commands and the report are identical:
132
148
 
133
- | url 形式 | 對應 |
149
+ | url form | What it means |
134
150
  |---|---|
135
- | `http(s)://…` | 一般網頁 / 內網 staging |
136
- | `file://…`(或相對路徑) | 本機 HTML 檔 |
137
- | `cdp:http://host:port` | **已在跑的 Electron 桌面 app**(抓活視窗的 DOM) |
151
+ | `http(s)://…` | A normal web page, or internal staging |
152
+ | `file://…` (or a relative path) | A local HTML file |
153
+ | `cdp:http://host:port` | **An already-running Electron desktop app** (reads the DOM of the live window) |
138
154
 
139
- Electron:啟動時加遠端偵錯埠,再把 `url` 指過去即可——Parity 會 attach 進去讀當下畫面,不導頁、不干擾 app:
155
+ For Electron, start the app with a remote debugging port and point `url` at it. Parity attaches
156
+ and reads the current screen — it does not navigate, and does not disturb the app:
140
157
 
141
158
  ```sh
142
- electron . --remote-debugging-port=9222 # 你的 app,加這個旗標
159
+ electron . --remote-debugging-port=9222 # your app, with this one extra flag
143
160
  ```
144
161
  ```jsonc
145
162
  { "route": "/", "frame": "20:5", "url": "cdp:http://localhost:9222" }
146
163
  ```
147
164
 
148
- > 為什麼 Electron 幾乎免費:它的畫面就是一個 Chromium renderer,跟網頁同一套 DOM/CSS 量測。手機原生 / Flutter / 原生桌面不走 DOM,留待 v2.0。
165
+ > Why Electron is nearly free: its UI *is* a Chromium renderer, so it uses the same DOM/CSS
166
+ > measurement path as the web. Mobile native, Flutter and native desktop do not go through a
167
+ > DOM, so they are left for v2.0.
149
168
 
150
- ## 設計來源:Figma、快照、或一張圖
169
+ ## Design sources: Figma, a snapshot, or just an image
151
170
 
152
- | 設計來源 | config | 適合誰 |
171
+ | Design source | config | Who it is for |
153
172
  |---|---|---|
154
- | **Figma**(主力) | `figmaFileKey` + `FIGMA_TOKEN` | 有 Figma 檔的正規流程 |
155
- | **快照**(`parity snapshot`) | `designFile` 指向產出的快照 JSON | **重構/改版守門**:現在的畫面是對的,存成基準,之後 check 保證不跑版(visual regression 的數值版)。配對走 selector 身分,100% 確定性 |
156
- | **一張圖 + 標註** | `designImage`(PNG/JPG)+ `designFile`(標註) | 只有圖的場景:外包 PNG、老專案只剩截圖。**XD / Sketch / PS 等其他工具匯出圖片就能走這條**(萬用轉接頭)。標註 = DesignNode JSON,`fill` 可省略——顏色由引擎從圖片對應區域取樣(TEXT 字色刻意不取樣:反鋸齒混色取不準,可手填) |
157
- | 手寫 JSON | `designFile` | 離線示範/測試 |
173
+ | **Figma** (primary) | `figmaFileKey` + `FIGMA_TOKEN` | The normal workflow, when you have a Figma file |
174
+ | **Snapshot** (`parity snapshot`) | `designFile` pointing at the generated snapshot JSON | **Refactor/redesign guard**: today's rendering is correct, so freeze it as the baseline and later checks prove nothing drifted (the numeric version of visual regression). Matching goes through selector identity, so it is 100% deterministic |
175
+ | **An image + annotations** | `designImage` (PNG/JPG) + `designFile` (annotations) | When all you have is a picture: an outsourced PNG, or a legacy project down to screenshots. **This is also the path for XD / Sketch / Photoshop and anything else that can export an image** — a universal adapter. The annotations are DesignNode JSON, and `fill` may be omitted: the engine samples the color from the corresponding region of the image. (Text color is deliberately not sampled — anti-aliasing blends it and the reading is unreliable — but you can fill it in by hand.) |
176
+ | Hand-written JSON | `designFile` | Offline demos and tests |
158
177
 
159
178
  ```sh
160
- # 重構守門三步:
161
- parity snapshot # 1. 凍結現在的畫面(產出 parity.snapshot.json + 參考截圖)
162
- # config 改 designFile 指向它、target.frame 填 route
163
- parity check # 2. 大膽重構;3. check 保證與快照一致
179
+ # The three-step refactor guard:
180
+ parity snapshot # 1. Freeze the current rendering (writes parity.snapshot.json + a reference screenshot)
181
+ # Point designFile at it in the config, and set each target.frame to its route
182
+ parity check # 2. Refactor fearlessly; 3. check proves it still matches the snapshot
164
183
  ```
165
184
 
166
- ## 比什麼、不比什麼
167
-
168
- 只比「不管版面怎麼流動都該一樣」的東西:
169
-
170
- - **自身尺寸**(寬高;TEXT 節點例外——文字框量測天生不同,比了狂誤報)
171
- - auto-layout 的 **HUG(隨內容)/ FILL(隨父層)** 那一軸也跳過——Figma 量的寬 ≠ 瀏覽器渲染寬是必然的,只比 **FIXED** 的軸
172
- - **內距 / 間距**:padding 四邊、auto-layout `itemSpacing` ↔ 實際子元素 gap
173
- - **相對位置**(`offsetX`/`offsetY`):自由擺放(非 auto-layout)容器的子元素,比「相對最近可靠兄弟/父層邊」的偏移——抓得到「尺寸顏色全對但擺錯位置」。參照只用可靠的邊(TEXT/HUG 的框不當參照、TEXT 不當目標、上方全是文字時 Y 誠實跳過),流動版面的行高漂移不會誤報。`compare.position: "none"` 可關閉
174
- - **字體**:size / weight / line-height / letter-spacing 精確比;font-family 是**軟落差**(不擋 gate)
175
- - **顏色**:CIEDE2000 (ΔE) 設門檻,不是 hex 全等;解析含現代語法(`rgb(37 99 235 / .5)`、`color(srgb …)`、`oklch()`、`color(display-p3 …)`)
176
-
177
- > 設計來源是 Figma 時,報告(Markdown 與本機 UI)裡的圖層名會**連回 Figma 的那個節點**——設計師點一下直接跳到圖層,不用自己翻。
178
- - **刻意不比絕對位置 x/y**:彈性版面下本來就會不同,比了 = 誤報 = 失去信任
179
-
180
- ## 給設計師的兩個方向
181
-
182
- **新頁面守設計系統:`parity lint`**——只看設計稿這一邊,驗每個節點的值是否落在 `tokensFile` 的允許集合:顏色(ΔE 容差內命中即過)、fontSize / padding / itemSpacing / cornerRadius(等於任一尺寸 token 即過;間距/字級/圓角共用同一份 scale)。違規附「最近的 token」——訊息是「改成這個」,不是只有「你錯了」。有違規 exit 1,可進 CI。沒定義某維度的 token 就不 lint 該維度。
183
-
184
- **照現況重畫/改版:`parity check --reverse`**——方向反過來:「期望」= 現況(實作)、「實際」= 你的設計稿。給設計師一張「我的稿跟現在線上差在哪」的清單;不做把關,永遠 exit 0。
185
-
186
- ## RWD 多斷點
187
-
188
- 同一個 URL、不同斷點,各對一個 Figma frame 即可——**渲染視窗 = frame 尺寸**,手機 frame 畫 375 寬,media query 自然生效:
185
+ ## What gets compared, and what does not
186
+
187
+ Only the things that should hold no matter how the layout flows:
188
+
189
+ - **Intrinsic size** (width/height — except on TEXT nodes, where text-box measurement is
190
+ inherently different between Figma and a browser and comparing it produces a flood of noise)
191
+ - The auto-layout **HUG (fit content) / FILL (fill parent)** axis is skipped too: Figma's
192
+ measured width necessarily differs from the browser's rendered width there. Only **FIXED**
193
+ axes are compared.
194
+ - **Padding / spacing**: all four padding edges, and auto-layout `itemSpacing` against the real
195
+ gap between child elements.
196
+ - **Relative position** (`offsetX`/`offsetY`): for children of freely positioned (non-auto-layout)
197
+ containers, the offset relative to the nearest reliable sibling or parent edge — which is what
198
+ catches "size and color are perfect, but it is in the wrong place." Only reliable edges are used
199
+ as references (TEXT/HUG boxes are never references, TEXT is never a target, and Y is honestly
200
+ skipped when everything above is text), so line-height drift in a flowing layout does not
201
+ produce false positives. Turn it off with `compare.position: "none"`.
202
+ - **Typography**: size, weight, line-height and letter-spacing are compared exactly;
203
+ font-family is a **soft gap** and never gates.
204
+ - **Color**: thresholded on CIEDE2000 (ΔE), not hex equality. Parsing covers modern syntax
205
+ (`rgb(37 99 235 / .5)`, `color(srgb …)`, `oklch()`, `color(display-p3 …)`).
206
+ - **Absolute x/y is deliberately never compared**: it legitimately differs in a flexible layout,
207
+ so comparing it means false positives, which means losing trust.
208
+
209
+ > When the design source is Figma, layer names in the report (both the Markdown and the local UI)
210
+ > **link back to that node in Figma** — a designer clicks once and lands on the layer.
211
+
212
+ ## Two workflows for designers
213
+
214
+ **Keeping a new page inside the design system: `parity lint`.** It looks only at the design side
215
+ and checks whether each node's values are in the allowed set from `tokensFile`: color (a hit
216
+ within the ΔE tolerance passes), and fontSize / padding / itemSpacing / cornerRadius (equal to
217
+ any size token passes — spacing, font sizes and corner radii share one scale). Every violation
218
+ carries "the nearest token", so the message is "change it to this", not merely "you are wrong".
219
+ Violations exit 1, so it can run in CI. Dimensions you have not defined tokens for are not linted.
220
+
221
+ **Redrawing against what ships: `parity check --reverse`.** The direction flips: "expected"
222
+ becomes the current implementation and "actual" becomes your draft. That gives a designer a list
223
+ of "how my draft differs from what is live today". It never gates, and always exits 0.
224
+
225
+ ## Responsive breakpoints
226
+
227
+ One URL, several breakpoints, each mapped to its own Figma frame — **the render viewport is the
228
+ frame's size**, so a 375-wide phone frame makes your media queries fire naturally:
189
229
 
190
230
  ```jsonc
191
231
  "targets": [
192
232
  { "route": "/desktop", "frame": "10:2", "url": "http://localhost:8080/" },
193
- { "route": "/mobile", "frame": "10:99", "url": "http://localhost:8080/" } // frame 是 375 寬的手機版
233
+ { "route": "/mobile", "frame": "10:99", "url": "http://localhost:8080/" } // a 375-wide phone frame
194
234
  ]
195
235
  ```
196
236
 
197
- route 只是報告上的標籤,取好認的名字就行。frame 寬 ≠ 想測的視窗寬時,才需要在 target 加 `width` / `height` 覆蓋。
237
+ `route` is only a label in the report, so pick something recognizable. You only need `width` /
238
+ `height` on a target when the frame's width is not the viewport width you want to test.
198
239
 
199
- ## Shadow DOM / iframe
240
+ ## Shadow DOM / iframes
200
241
 
201
- 擷取走**組合樹**:open shadow root、`<slot>` 塞進來的內容、同源 iframe(含 `srcdoc`)都看得到、都會比——web components 網站不再整塊隱形。shadow / iframe 內的元素 selector 以 `host >>> 內部路徑` 表示。
242
+ Capture walks the **composed tree**: open shadow roots, content slotted through `<slot>`, and
243
+ same-origin iframes (including `srcdoc`) are all visible and all compared — web-component sites
244
+ are no longer wholly invisible. Selectors for elements inside a shadow root or iframe are written
245
+ as `host >>> inner path`.
202
246
 
203
- 限制(誠實列):closed shadow root 與跨域 iframe 拿不到,跳過;map 檔的 selector 搆不到 shadow 內(`data-parity` 屬性不受限,照常可用)。
247
+ Limitations, stated honestly: closed shadow roots and cross-origin iframes are unreachable and
248
+ are skipped; map-file selectors cannot reach inside a shadow root (the `data-parity` attribute is
249
+ unaffected and works as usual).
204
250
 
205
- ## 配對策略(以設計端為錨)
251
+ ## Matching strategy (anchored on the design side)
206
252
 
207
- 1. **自動文字錨定**:設計 TEXT 文字 ↔ 頁面文字(唯一才配;多個同文字時用圖層名消歧,仍不硬湊)
208
- 2. **圖層名 ↔ id / class / aria-label**:`CTA Button` 自動對上 `class="cta-button"`
209
- 3. **容器推論**:配不到的容器,用「已配對子孫的最近共同祖先」反推(純結構,不猜)
210
- 4. **手動補漏**:HTML 加 `data-parity="圖層名"`,或 `parity.map.json` 寫 `{ "圖層名": "CSS selector" }`
211
- 5. 配不到 → **誠實列進未配對清單**(真正需要人補的才補)
253
+ 1. **Automatic text anchoring**: design TEXT content ↔ page text (matched only when unique;
254
+ when several elements share the text, the layer name disambiguates — and it still refuses to force a match)
255
+ 2. **Layer name ↔ id / class / aria-label**: `CTA Button` matches `class="cta-button"` automatically
256
+ 3. **Container inference**: an unmatched container is inferred from the lowest common ancestor of
257
+ its already-matched descendants (pure structure — no guessing)
258
+ 4. **Manual fallback**: add `data-parity="Layer Name"` in your HTML, or write
259
+ `{ "Layer Name": "CSS selector" }` in `parity.map.json`
260
+ 5. Still no match → **honestly listed as unmatched**, so you only fill in what genuinely needs a human
212
261
 
213
- ## 架構
262
+ ## Architecture
214
263
 
215
- 一顆引擎(`Parity.Engine`,純函式庫)+ 多個外殼。引擎只比對「兩棵正規化的樹」:
264
+ One engine (`Parity.Engine`, a pure library) plus several shells. The engine only ever compares
265
+ two normalized trees:
216
266
 
217
267
  ```
218
- IDesignSource ──→ DesignNode 樹 ──┐
219
- (Figma / JSON / 未來 XD、Sketch) ├─→ Normalizer → Matcher → DiffEngine → FidelityReport
268
+ IDesignSource ──→ DesignNode tree ─┐
269
+ (Figma / JSON / later XD, Sketch) ├─→ Normalizer → Matcher → DiffEngine → FidelityReport
220
270
  IImplementationSource → RenderedNode ┘
221
- (Playwright / 未來 WPF、桌面)
271
+ (Playwright / later WPF, desktop)
222
272
  ```
223
273
 
224
274
  ```
225
- src/Parity.Engine/ 引擎:唯一進入點 FidelityEngine
226
- DesignSources/ IDesignSource + Figma(REST + 本機快取)/ Json
227
- ImplementationSources/ IImplementationSource + Web(Playwright)
228
- Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta(CIEDE2000)
229
- src/Parity.Cli/ dotnet tool 外殼:parity check / init / install-browser
230
- tests/Parity.Tests/ 單元測試(含 CIEDE2000 標準測資)
231
- samples/demo/ 離線示範:刻意做壞的頁面 + 設計 JSON
275
+ src/Parity.Engine/ The engine; FidelityEngine is the only entry point
276
+ DesignSources/ IDesignSource + Figma (REST + local cache) / Json
277
+ ImplementationSources/ IImplementationSource + Web (Playwright)
278
+ Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta (CIEDE2000)
279
+ src/Parity.Cli/ The dotnet tool shell: parity check / init / install-browser
280
+ tests/Parity.Tests/ Unit tests (including the CIEDE2000 reference data)
281
+ samples/demo/ Offline demo: a deliberately broken page + a design JSON
232
282
  ```
233
283
 
234
- ## 本機報告 UI(M3)
284
+ ## Local report UI (M3)
235
285
 
236
286
  ```sh
237
- parity serve --watch # http://127.0.0.1:4321,檔案變更自動重掃(SSE 即時更新)
238
- parity map # 互動配對模式:點未配對圖層 → 點截圖上的元素 → 寫入 parity.map.json
287
+ parity serve --watch # http://127.0.0.1:4321, re-scans on file changes (live updates over SSE)
288
+ parity map # Interactive matching: click an unmatched layer → click the element on the
289
+ # screenshot → writes parity.map.json
239
290
  ```
240
291
 
241
- - 落差清單(依嚴重度排序,精確數值 + 色票 + ΔE)
242
- - 截圖疊框視圖:實線 = 實作框(顏色 = 嚴重度)、藍虛線 = 設計框、紅虛線 = 未配對
243
- - **只綁 127.0.0.1**:報告含站點結構,不讓區網掃到
244
- - UI 是零建置的靜態 SPA,dotnet tool 不需要 node 工具鏈
292
+ - A gap list sorted by severity, with exact numbers, color swatches and ΔE
293
+ - A screenshot overlay: solid = implementation box (colored by severity), blue dashed = design
294
+ box, red dashed = unmatched
295
+ - **Binds 127.0.0.1 only**: the report contains your site's structure, so it must not be
296
+ reachable from the local network
297
+ - The UI is a zero-build static SPA, so the dotnet tool needs no Node toolchain
245
298
 
246
- ## 進 CI(M4)
299
+ ## In CI (M4)
247
300
 
248
- Parity 的差異化就在「進 CI 把關」。這個 repo 本身就是一個 composite action:
301
+ Gating CI is the whole differentiator, and this repo is itself a composite action:
249
302
 
250
303
  ```yaml
251
- # .github/workflows/design-check.yml(你的專案)
304
+ # .github/workflows/design-check.yml (in your project)
252
305
  name: Design fidelity
253
306
  on: [pull_request]
254
307
  jobs:
@@ -256,69 +309,88 @@ jobs:
256
309
  runs-on: ubuntu-latest
257
310
  permissions:
258
311
  contents: read
259
- pull-requests: write # 讓 action 把還原度報告貼成 PR 留言
312
+ pull-requests: write # lets the action post the fidelity report as a PR comment
260
313
  steps:
261
314
  - uses: actions/checkout@v4
262
- # 起你的站(或改成部署到 preview URL,讓 config 的 url 指過去)
315
+ # Start your site (or deploy to a preview URL and point the config's url at it)
263
316
  - run: |
264
317
  npm ci && npm run build
265
- npm run preview & # 例:serve 在 localhost:8080
266
- - uses: HSU-YU-MING/cornhsu-parity@v0.11.1 # 0.x 期間 pin 版本;發 1.0 後可改用移動式 @v1
318
+ npm run preview & # e.g. serving on localhost:8080
319
+ - uses: HSU-YU-MING/cornhsu-parity@v0.12.0 # pin exact versions while 0.x
267
320
  with:
268
321
  config: parity.config.json
269
- figma-token: ${{ secrets.FIGMA_TOKEN }} # 設計來源用本機 JSON 時可省略
322
+ figma-token: ${{ secrets.FIGMA_TOKEN }} # omit when the design source is a local JSON file
270
323
  ```
271
324
 
272
- 行為:
273
- - **PR 留言**:自動貼一則還原度報告(分數 + 落差表 + **建議修法**),同一則反覆更新不洗版——PM/reviewer 不用碰工具就看得到。
274
- - **擋 PR**:落差超過 `gate.failOn` → exit 1 → PR 打紅(**留言會先貼、再擋**)。
275
- - **artifact**:`report.json` + Markdown 報告上傳供下載。
325
+ Behaviour:
326
+
327
+ - **PR comment**: posts a fidelity report (score + gap table + **suggested fixes**) and keeps
328
+ updating that same comment instead of adding new ones — so PMs and reviewers see it without
329
+ ever touching the tool.
330
+ - **Blocks the PR**: gaps beyond `gate.failOn` → exit 1 → red PR. (**The comment is posted
331
+ first, then the gate is enforced.**)
332
+ - **Artifacts**: `report.json` plus the Markdown report are uploaded for download.
276
333
 
277
- action 輸入:`config` / `target` / `working-directory` / `version` / `figma-token` / `baseline`(回歸模式) / `comment`(關掉 PR 留言) / `upload-report`。
334
+ Action inputs: `config` / `target` / `working-directory` / `version` / `figma-token` /
335
+ `baseline` (regression mode) / `comment` (turn off the PR comment) / `upload-report`.
278
336
 
279
- > action 透過 `dotnet tool install -g Cornhsu.Parity` 安裝(已發佈於 nuget.org)。本 repo 的 `.github/workflows/ci.yml` 則是**直接從原始碼建置**並跑離線示範自我把關,不依賴發佈。
337
+ > The action installs the tool with `dotnet tool install -g Cornhsu.Parity` (published on
338
+ > nuget.org). This repo's own `.github/workflows/ci.yml` instead **builds from source** and runs
339
+ > the offline demo as a self-check, so it does not depend on a published release.
280
340
 
281
- ## 回歸把關:baseline(M5)
341
+ ## Regression gating: baselines (M5)
282
342
 
283
- 已經有一堆落差的專案,不可能一開就「零落差才給過」。baseline 讓你**只擋新增/惡化**:
343
+ A project that already has a pile of gaps cannot start at "zero gaps or no merge". A baseline
344
+ lets you **gate only new and worsened gaps**:
284
345
 
285
346
  ```sh
286
- parity baseline save # 把當前落差 + 還原度分數存成基準快照(SQLite,存 parity.baseline.db,自動標 git commit)
287
- parity check --baseline # 比對現況 vs 最新基準:只有「新增或惡化」才 GATE FAIL;並顯示分數走勢(基準 75 → 現在 83 ↑)
288
- parity baseline list # 看歷史快照(含分數欄 = 還原度走勢,給 PM 看方向)
347
+ parity baseline save # Store the current gaps + fidelity score as a baseline snapshot
348
+ # (SQLite, in parity.baseline.db, tagged with the git commit)
349
+ parity check --baseline # Compare against the newest baseline: only new or worsened gaps GATE FAIL,
350
+ # and the score trend is shown (baseline 75 → now 83 ↑)
351
+ parity baseline list # Browse snapshot history (the score column is the fidelity trend, for PMs)
289
352
  ```
290
353
 
291
- > **CI 要用 `--baseline`,記得 `git add parity.baseline.db` 一起 commit**——它刻意放 repo 根(不放 `.parity/`,那裡通常被 gitignore),否則 CI 找不到基準會靜默退回一般 gate。路徑可用 config `baselineFile` 改。
354
+ > **To use `--baseline` in CI, remember to `git add parity.baseline.db`.** It deliberately lives
355
+ > at the repo root rather than in `.parity/`, which is usually gitignored — otherwise CI cannot
356
+ > find the baseline and silently falls back to the normal gate. The path is configurable via
357
+ > `baselineFile`.
292
358
 
293
- - **新增**(基準沒有、現在有)或**惡化**(嚴重度變高)→ exit 1 擋 PR
294
- - **修好**(基準有、現在沒了)會列出來鼓勵;**不變**的既有落差不擋
295
- - 適合漸進導入:先 `baseline save` 記錄現況,之後 CI 用 `check --baseline`,團隊只需「不要讓還原度更差」
359
+ - **New** (absent from the baseline, present now) or **worsened** (higher severity) → exit 1, PR blocked
360
+ - **Fixed** (in the baseline, gone now) is listed as encouragement; **unchanged** existing gaps do not gate
361
+ - Ideal for gradual adoption: record reality with `baseline save`, then run `check --baseline` in
362
+ CI, and all the team has to commit to is "do not make fidelity worse"
296
363
 
297
- 儲存層是獨立的 `Parity.Storage`(EF Core + SQLite,`Pooling=False` 即時釋放檔案),引擎裡的 `BaselineComparer` 是純函式、可單元測試。
364
+ The storage layer is a separate `Parity.Storage` (EF Core + SQLite, with `Pooling=False` so the
365
+ file is released immediately), and the engine's `BaselineComparer` is a pure, unit-testable function.
298
366
 
299
- ## 里程碑
367
+ ## Milestones
300
368
 
301
- - [x] **M1** 引擎 + CLI 雛形:設計端與實作端兩棵數值樹
302
- - [x] **M2** 比對引擎:配對 + 數值 diff + 容差 + 未配對清單 + gate exit code
303
- - [x] **M3** 本機報告 UI(`parity serve --watch`,Kestrel 綁 127.0.0.1)+ `parity map` 互動配對
304
- - [x] **M4** GitHub Action:可重用 composite action(`action.yml`)+ 本 repo CI(build / test / 離線示範自我把關)
305
- - [x] **M5** EF Core + SQLite baseline / 歷史(回歸把關 + 分數走勢)+ `ImageDesignSource`(圖片+標註+像素取樣)+ `parity snapshot`(凍結現況當基準)
369
+ - [x] **M1** Engine + CLI prototype: two numeric trees, design side and implementation side
370
+ - [x] **M2** Comparison engine: matching + numeric diffing + tolerances + unmatched list + gate exit codes
371
+ - [x] **M3** Local report UI (`parity serve --watch`, Kestrel bound to 127.0.0.1) + `parity map` interactive matching
372
+ - [x] **M4** GitHub Action: a reusable composite action (`action.yml`) + this repo's own CI (build / test / offline-demo self-check)
373
+ - [x] **M5** EF Core + SQLite baselines and history (regression gating + score trend) + `ImageDesignSource` (image + annotations + pixel sampling) + `parity snapshot` (freeze the current state as the baseline)
374
+ - [ ] **M6** (optional) Cloud shell: public-URL scanning + SSRF protection
306
375
 
307
- > 未完成、已知盲點與下一步優先序見 [ROADMAP.md](ROADMAP.md);版本變更見 [CHANGELOG.md](CHANGELOG.md)。
308
- - [ ] **M6**(選配)雲端外殼:公開網址掃描 + SSRF 防護
376
+ > Open work, known blind spots and the next priorities are in [ROADMAP.md](ROADMAP.md);
377
+ > version history is in [CHANGELOG.md](CHANGELOG.md).
309
378
 
310
- ## 安全
379
+ ## Security
311
380
 
312
- - Figma token 走環境變數(`env:FIGMA_TOKEN`),不進 log、不進 URL(用 `X-Figma-Token` header)
313
- - 抓過的 frame 存 `.parity/cache`(已 gitignore),重跑不再打 Figma、可離線比對
381
+ - The Figma token comes from an environment variable (`env:FIGMA_TOKEN`); it never reaches a log
382
+ and never goes in a URL (it is sent as the `X-Figma-Token` header).
383
+ - Fetched frames are cached in `.parity/cache` (gitignored), so re-runs do not hit Figma and can
384
+ compare offline.
314
385
 
315
- ## 另見
386
+ ## See also
316
387
 
317
- [**XamlContrast**](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast) —— 同作者的姊妹專案,
318
- 同一套哲學(數值檢查、CI 把關)。Parity 回答「**實作跟設計稿一不一樣**」(還原度);
319
- XamlContrast 回答「**做出來的東西看不看得見**」(WCAG 對比,靜態掃 XAML 原始碼)。
320
- 一個守還原度、一個守可讀性;它的靜態顏色解析也是本專案未來
321
- `WpfImplementationSource`(桌面 adapter)的預期值地基——見規畫書 4.5 節補記。
388
+ [**XamlContrast**](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast) — a sibling project by the
389
+ same author, built on the same philosophy: numeric checks that gate CI. Parity answers "**does the
390
+ implementation match the design?**" (fidelity); XamlContrast answers "**can anyone actually see
391
+ what we built?**" (WCAG contrast, scanned statically from XAML source). One guards fidelity, the
392
+ other readability — and its static color resolution is also the groundwork for the expected values
393
+ of this project's future `WpfImplementationSource` desktop adapter.
322
394
 
323
395
  ## License
324
396
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cornhsu-parity",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "description": "Property-Level Design QA that can gate your CI. Compares real numeric values from your Figma file against the rendered UI and fails the PR when the implementation drifts from the design.",
5
5
  "keywords": [
6
6
  "figma",
@@ -34,11 +34,11 @@
34
34
  "node": ">=18"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@cornhsu/parity-win32-x64": "0.11.1",
38
- "@cornhsu/parity-win32-arm64": "0.11.1",
39
- "@cornhsu/parity-linux-x64": "0.11.1",
40
- "@cornhsu/parity-linux-arm64": "0.11.1",
41
- "@cornhsu/parity-darwin-x64": "0.11.1",
42
- "@cornhsu/parity-darwin-arm64": "0.11.1"
37
+ "@cornhsu/parity-win32-x64": "0.12.0",
38
+ "@cornhsu/parity-win32-arm64": "0.12.0",
39
+ "@cornhsu/parity-linux-x64": "0.12.0",
40
+ "@cornhsu/parity-linux-arm64": "0.12.0",
41
+ "@cornhsu/parity-darwin-x64": "0.12.0",
42
+ "@cornhsu/parity-darwin-arm64": "0.12.0"
43
43
  }
44
44
  }