cornhsu-parity 0.11.1 → 0.13.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.
- package/README.md +248 -174
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -6,249 +6,304 @@
|
|
|
6
6
|
[](https://github.com/HSU-YU-MING/cornhsu-parity/actions/workflows/ci.yml)
|
|
7
7
|
[](LICENSE)
|
|
8
8
|
|
|
9
|
-
**[
|
|
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.
|
|
14
|
+
- uses: HSU-YU-MING/cornhsu-parity@v0.13.0 # pin exact versions while 0.x; a floating @v1 arrives with 1.0
|
|
15
15
|
with:
|
|
16
|
-
config: parity.config.json #
|
|
17
|
-
target: /pricing #
|
|
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
|
-
|
|
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
|
-
>
|
|
23
|
-
>
|
|
24
|
-
>
|
|
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
|
-
|
|
29
|
+
**Other tools help you *see* design drift. Parity *blocks* it.**
|
|
27
30
|
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
| |
|
|
35
|
+
| | Manual spot-check tools<br>(Uiprobe / OverlayQA / Loupe …) | **Parity** |
|
|
32
36
|
|---|---|---|
|
|
33
|
-
|
|
|
34
|
-
|
|
|
35
|
-
|
|
|
36
|
-
|
|
|
37
|
-
|
|
|
38
|
-
|
|
|
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
|
-
>
|
|
41
|
-
>
|
|
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
|
-
|
|
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
|
-

|
|
48
55
|
|
|
49
|
-
|
|
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
|
-

|
|
52
61
|
|
|
53
|
-
##
|
|
62
|
+
## At a glance
|
|
54
63
|
|
|
55
64
|
| | |
|
|
56
65
|
|---|---|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
| CI
|
|
63
|
-
|
|
|
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
|
-
#
|
|
79
|
+
# If you have Node (common in front-end projects): no .NET install needed
|
|
71
80
|
npx cornhsu-parity check
|
|
72
81
|
|
|
73
|
-
#
|
|
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
|
|
80
|
-
>
|
|
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 #
|
|
86
|
-
parity install-browser #
|
|
87
|
-
export FIGMA_TOKEN=... #
|
|
88
|
-
parity check #
|
|
89
|
-
parity report #
|
|
90
|
-
|
|
91
|
-
parity
|
|
92
|
-
|
|
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
|
+
# (--stabilize captures 3× and suggests "ignore" entries for
|
|
103
|
+
# regions that move: rotating ads, animations, lazy media)
|
|
104
|
+
parity lint # Design lint: are the design file's values inside the allowed
|
|
105
|
+
# design-token set? (design only, no implementation)
|
|
106
|
+
parity check --reverse # Reverse view, for a designer redrawing an existing page:
|
|
107
|
+
# how does my draft differ from what ships? (never gates)
|
|
93
108
|
```
|
|
94
109
|
|
|
95
|
-
|
|
110
|
+
When working inside this repo:
|
|
96
111
|
|
|
97
112
|
```sh
|
|
98
113
|
dotnet run --project src/Parity.Cli -- check --config samples/demo/parity.config.json
|
|
99
114
|
```
|
|
100
115
|
|
|
101
|
-
`samples/demo`
|
|
102
|
-
`index.html`
|
|
103
|
-
|
|
116
|
+
`samples/demo` is a fully **offline demo** (the design source is a local JSON file, so no Figma
|
|
117
|
+
token is needed): `index.html` is deliberately broken in a few places, `parity check` reports
|
|
118
|
+
each gap and the unmatched list precisely, and returns exit code 1 (GATE FAIL) because of the
|
|
119
|
+
serious-severity gaps — which is exactly the behaviour you want from a CI gate.
|
|
104
120
|
|
|
105
|
-
##
|
|
121
|
+
## Configuration: `parity.config.json`
|
|
106
122
|
|
|
107
123
|
```jsonc
|
|
108
124
|
{
|
|
109
125
|
"figmaFileKey": "abcd1234",
|
|
110
126
|
"designToken": "env:FIGMA_TOKEN",
|
|
111
|
-
"mapFile": "parity.map.json", //
|
|
112
|
-
"tokensFile": "tokens.json", //
|
|
127
|
+
"mapFile": "parity.map.json", // manual fallback mapping (layer name → selector)
|
|
128
|
+
"tokensFile": "tokens.json", // optional: design tokens (name → value);
|
|
129
|
+
// suggested fixes will name the matching token
|
|
113
130
|
"targets": [
|
|
114
131
|
{ "route": "/", "frame": "10:2", "url": "http://localhost:8080/" }
|
|
115
132
|
],
|
|
116
|
-
"compare": { "position": "relative" }, // relative =
|
|
133
|
+
"compare": { "position": "relative" }, // relative = compare relative position (default); none = skip position
|
|
117
134
|
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "positionPx": 4 },
|
|
118
135
|
"ignore": ["[data-parity-ignore]"],
|
|
119
136
|
"gate": {
|
|
120
137
|
"failOn": ["critical", "serious"],
|
|
121
|
-
"minMatchRate": 0 //
|
|
138
|
+
"minMatchRate": 0 // optional: FAIL outright below this match rate (0–1); 0 = no threshold
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
141
|
```
|
|
125
142
|
|
|
126
|
-
> gate
|
|
127
|
-
>
|
|
143
|
+
> Besides gaps, the gate also validates **match integrity**: zero matches (or zero design nodes)
|
|
144
|
+
> is always a GATE FAIL — with nothing matched there are no gaps to catch, so a silent PASS
|
|
145
|
+
> would be a false pass, usually caused by a wrong url or frame. `--baseline` mode is not exempt.
|
|
128
146
|
|
|
129
|
-
##
|
|
147
|
+
## Targets: a web page or an Electron desktop app
|
|
130
148
|
|
|
131
|
-
`target.url`
|
|
149
|
+
`target.url` decides what the implementation side is; the commands and the report are identical:
|
|
132
150
|
|
|
133
|
-
| url
|
|
151
|
+
| url form | What it means |
|
|
134
152
|
|---|---|
|
|
135
|
-
| `http(s)://…` |
|
|
136
|
-
| `file://…`(
|
|
137
|
-
| `cdp:http://host:port` |
|
|
153
|
+
| `http(s)://…` | A normal web page, or internal staging |
|
|
154
|
+
| `file://…` (or a relative path) | A local HTML file |
|
|
155
|
+
| `cdp:http://host:port` | **An already-running Electron desktop app** (reads the DOM of the live window) |
|
|
138
156
|
|
|
139
|
-
Electron
|
|
157
|
+
For Electron, start the app with a remote debugging port and point `url` at it. Parity attaches
|
|
158
|
+
and reads the current screen — it does not navigate, and does not disturb the app:
|
|
140
159
|
|
|
141
160
|
```sh
|
|
142
|
-
electron . --remote-debugging-port=9222 #
|
|
161
|
+
electron . --remote-debugging-port=9222 # your app, with this one extra flag
|
|
143
162
|
```
|
|
144
163
|
```jsonc
|
|
145
164
|
{ "route": "/", "frame": "20:5", "url": "cdp:http://localhost:9222" }
|
|
146
165
|
```
|
|
147
166
|
|
|
148
|
-
>
|
|
167
|
+
> Why Electron is nearly free: its UI *is* a Chromium renderer, so it uses the same DOM/CSS
|
|
168
|
+
> measurement path as the web. Mobile native, Flutter and native desktop do not go through a
|
|
169
|
+
> DOM, so they are left for v2.0.
|
|
149
170
|
|
|
150
|
-
##
|
|
171
|
+
## Design sources: Figma, a snapshot, or just an image
|
|
151
172
|
|
|
152
|
-
|
|
|
173
|
+
| Design source | config | Who it is for |
|
|
153
174
|
|---|---|---|
|
|
154
|
-
| **Figma**(
|
|
155
|
-
|
|
|
156
|
-
|
|
|
157
|
-
|
|
|
175
|
+
| **Figma** (primary) | `figmaFileKey` + `FIGMA_TOKEN` | The normal workflow, when you have a Figma file |
|
|
176
|
+
| **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 — randomized ids (React `useId`, CSS-in-JS) are detected at capture time and replaced with structural paths, so they survive regeneration. Baselines taken by older versions may list such nodes as unmatched (reason `randomized-id`); re-run `parity snapshot` once to migrate |
|
|
177
|
+
| **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.) |
|
|
178
|
+
| Hand-written JSON | `designFile` | Offline demos and tests |
|
|
158
179
|
|
|
159
180
|
```sh
|
|
160
|
-
#
|
|
161
|
-
parity snapshot # 1.
|
|
162
|
-
# config
|
|
163
|
-
parity check # 2.
|
|
181
|
+
# The three-step refactor guard:
|
|
182
|
+
parity snapshot # 1. Freeze the current rendering (writes parity.snapshot.json + a reference screenshot)
|
|
183
|
+
# Point designFile at it in the config, and set each target.frame to its route
|
|
184
|
+
parity check # 2. Refactor fearlessly; 3. check proves it still matches the snapshot
|
|
164
185
|
```
|
|
165
186
|
|
|
166
|
-
##
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
-
|
|
171
|
-
|
|
172
|
-
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
187
|
+
## What gets compared, and what does not
|
|
188
|
+
|
|
189
|
+
Only the things that should hold no matter how the layout flows:
|
|
190
|
+
|
|
191
|
+
- **Intrinsic size** (width/height — except on TEXT nodes, where text-box measurement is
|
|
192
|
+
inherently different between Figma and a browser and comparing it produces a flood of noise)
|
|
193
|
+
- The auto-layout **HUG (fit content) / FILL (fill parent)** axis is skipped too: Figma's
|
|
194
|
+
measured width necessarily differs from the browser's rendered width there. Only **FIXED**
|
|
195
|
+
axes are compared.
|
|
196
|
+
- **Padding / spacing**: all four padding edges, and auto-layout `itemSpacing` against the real
|
|
197
|
+
gap between child elements.
|
|
198
|
+
- **Relative position** (`offsetX`/`offsetY`): for children of freely positioned (non-auto-layout)
|
|
199
|
+
containers, the offset relative to the nearest reliable sibling or parent edge — which is what
|
|
200
|
+
catches "size and color are perfect, but it is in the wrong place." Only reliable edges are used
|
|
201
|
+
as references (TEXT/HUG boxes are never references, TEXT is never a target, and Y is honestly
|
|
202
|
+
skipped when everything above is text), so line-height drift in a flowing layout does not
|
|
203
|
+
produce false positives. Turn it off with `compare.position: "none"`.
|
|
204
|
+
- **Typography**: size, weight, line-height and letter-spacing are compared exactly;
|
|
205
|
+
font-family is a **soft gap** and never gates.
|
|
206
|
+
- **Color**: thresholded on CIEDE2000 (ΔE), not hex equality. Parsing covers modern syntax
|
|
207
|
+
(`rgb(37 99 235 / .5)`, `color(srgb …)`, `oklch()`, `color(display-p3 …)`).
|
|
208
|
+
- **Absolute x/y is deliberately never compared**: it legitimately differs in a flexible layout,
|
|
209
|
+
so comparing it means false positives, which means losing trust.
|
|
210
|
+
|
|
211
|
+
> When the design source is Figma, layer names in the report (both the Markdown and the local UI)
|
|
212
|
+
> **link back to that node in Figma** — a designer clicks once and lands on the layer.
|
|
213
|
+
|
|
214
|
+
## Two workflows for designers
|
|
215
|
+
|
|
216
|
+
**Keeping a new page inside the design system: `parity lint`.** It looks only at the design side
|
|
217
|
+
and checks whether each node's values are in the allowed set from `tokensFile`: color (a hit
|
|
218
|
+
within the ΔE tolerance passes), and fontSize / padding / itemSpacing / cornerRadius (equal to
|
|
219
|
+
any size token passes — spacing, font sizes and corner radii share one scale). Every violation
|
|
220
|
+
carries "the nearest token", so the message is "change it to this", not merely "you are wrong".
|
|
221
|
+
Violations exit 1, so it can run in CI. Dimensions you have not defined tokens for are not linted.
|
|
222
|
+
|
|
223
|
+
**Redrawing against what ships: `parity check --reverse`.** The direction flips: "expected"
|
|
224
|
+
becomes the current implementation and "actual" becomes your draft. That gives a designer a list
|
|
225
|
+
of "how my draft differs from what is live today". It never gates, and always exits 0.
|
|
226
|
+
|
|
227
|
+
## Responsive breakpoints
|
|
228
|
+
|
|
229
|
+
One URL, several breakpoints, each mapped to its own Figma frame — **the render viewport is the
|
|
230
|
+
frame's size**, so a 375-wide phone frame makes your media queries fire naturally:
|
|
189
231
|
|
|
190
232
|
```jsonc
|
|
191
233
|
"targets": [
|
|
192
234
|
{ "route": "/desktop", "frame": "10:2", "url": "http://localhost:8080/" },
|
|
193
|
-
{ "route": "/mobile", "frame": "10:99", "url": "http://localhost:8080/" } //
|
|
235
|
+
{ "route": "/mobile", "frame": "10:99", "url": "http://localhost:8080/" } // a 375-wide phone frame
|
|
194
236
|
]
|
|
195
237
|
```
|
|
196
238
|
|
|
197
|
-
route
|
|
239
|
+
`route` is only a label in the report, so pick something recognizable. You only need `width` /
|
|
240
|
+
`height` on a target when the frame's width is not the viewport width you want to test.
|
|
198
241
|
|
|
199
|
-
## Shadow DOM /
|
|
242
|
+
## Shadow DOM / iframes
|
|
200
243
|
|
|
201
|
-
|
|
244
|
+
Capture walks the **composed tree**: open shadow roots, content slotted through `<slot>`, and
|
|
245
|
+
same-origin iframes (including `srcdoc`) are all visible and all compared — web-component sites
|
|
246
|
+
are no longer wholly invisible. Selectors for elements inside a shadow root or iframe are written
|
|
247
|
+
as `host >>> inner path`.
|
|
202
248
|
|
|
203
|
-
|
|
249
|
+
Limitations, stated honestly: closed shadow roots and cross-origin iframes are unreachable and
|
|
250
|
+
are skipped; map-file selectors cannot reach inside a shadow root (the `data-parity` attribute is
|
|
251
|
+
unaffected and works as usual).
|
|
204
252
|
|
|
205
|
-
##
|
|
253
|
+
## Matching strategy (anchored on the design side)
|
|
206
254
|
|
|
207
|
-
1.
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
255
|
+
1. **Automatic text anchoring**: design TEXT content ↔ page text (matched only when unique;
|
|
256
|
+
when several elements share the text, the layer name disambiguates — and it still refuses to force a match)
|
|
257
|
+
2. **Layer name ↔ id / class / aria-label**: `CTA Button` matches `class="cta-button"` automatically
|
|
258
|
+
3. **Container inference**: an unmatched container is inferred from the lowest common ancestor of
|
|
259
|
+
its already-matched descendants (pure structure — no guessing)
|
|
260
|
+
4. **Manual fallback**: add `data-parity="Layer Name"` in your HTML, or write
|
|
261
|
+
`{ "Layer Name": "CSS selector" }` in `parity.map.json`
|
|
262
|
+
5. Still no match → **honestly listed as unmatched**, so you only fill in what genuinely needs a human
|
|
212
263
|
|
|
213
|
-
##
|
|
264
|
+
## Architecture
|
|
214
265
|
|
|
215
|
-
|
|
266
|
+
One engine (`Parity.Engine`, a pure library) plus several shells. The engine only ever compares
|
|
267
|
+
two normalized trees:
|
|
216
268
|
|
|
217
269
|
```
|
|
218
|
-
IDesignSource ──→ DesignNode
|
|
219
|
-
(Figma / JSON /
|
|
270
|
+
IDesignSource ──→ DesignNode tree ─┐
|
|
271
|
+
(Figma / JSON / later XD, Sketch) ├─→ Normalizer → Matcher → DiffEngine → FidelityReport
|
|
220
272
|
IImplementationSource → RenderedNode ┘
|
|
221
|
-
(Playwright /
|
|
273
|
+
(Playwright / later WPF, desktop)
|
|
222
274
|
```
|
|
223
275
|
|
|
224
276
|
```
|
|
225
|
-
src/Parity.Engine/
|
|
226
|
-
DesignSources/ IDesignSource + Figma(REST +
|
|
227
|
-
ImplementationSources/ IImplementationSource + Web(Playwright)
|
|
228
|
-
Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta(CIEDE2000)
|
|
229
|
-
src/Parity.Cli/ dotnet tool
|
|
230
|
-
tests/Parity.Tests/
|
|
231
|
-
samples/demo/
|
|
277
|
+
src/Parity.Engine/ The engine; FidelityEngine is the only entry point
|
|
278
|
+
DesignSources/ IDesignSource + Figma (REST + local cache) / Json
|
|
279
|
+
ImplementationSources/ IImplementationSource + Web (Playwright)
|
|
280
|
+
Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta (CIEDE2000)
|
|
281
|
+
src/Parity.Cli/ The dotnet tool shell: parity check / init / install-browser
|
|
282
|
+
tests/Parity.Tests/ Unit tests (including the CIEDE2000 reference data)
|
|
283
|
+
samples/demo/ Offline demo: a deliberately broken page + a design JSON
|
|
232
284
|
```
|
|
233
285
|
|
|
234
|
-
##
|
|
286
|
+
## Local report UI (M3)
|
|
235
287
|
|
|
236
288
|
```sh
|
|
237
|
-
parity serve --watch # http://127.0.0.1:4321
|
|
238
|
-
parity map #
|
|
289
|
+
parity serve --watch # http://127.0.0.1:4321, re-scans on file changes (live updates over SSE)
|
|
290
|
+
parity map # Interactive matching: click an unmatched layer → click the element on the
|
|
291
|
+
# screenshot → writes parity.map.json
|
|
239
292
|
```
|
|
240
293
|
|
|
241
|
-
-
|
|
242
|
-
-
|
|
243
|
-
|
|
244
|
-
-
|
|
294
|
+
- A gap list sorted by severity, with exact numbers, color swatches and ΔE
|
|
295
|
+
- A screenshot overlay: solid = implementation box (colored by severity), blue dashed = design
|
|
296
|
+
box, red dashed = unmatched
|
|
297
|
+
- **Binds 127.0.0.1 only**: the report contains your site's structure, so it must not be
|
|
298
|
+
reachable from the local network
|
|
299
|
+
- The UI is a zero-build static SPA, so the dotnet tool needs no Node toolchain
|
|
245
300
|
|
|
246
|
-
##
|
|
301
|
+
## In CI (M4)
|
|
247
302
|
|
|
248
|
-
|
|
303
|
+
Gating CI is the whole differentiator, and this repo is itself a composite action:
|
|
249
304
|
|
|
250
305
|
```yaml
|
|
251
|
-
# .github/workflows/design-check.yml(
|
|
306
|
+
# .github/workflows/design-check.yml (in your project)
|
|
252
307
|
name: Design fidelity
|
|
253
308
|
on: [pull_request]
|
|
254
309
|
jobs:
|
|
@@ -256,69 +311,88 @@ jobs:
|
|
|
256
311
|
runs-on: ubuntu-latest
|
|
257
312
|
permissions:
|
|
258
313
|
contents: read
|
|
259
|
-
pull-requests: write #
|
|
314
|
+
pull-requests: write # lets the action post the fidelity report as a PR comment
|
|
260
315
|
steps:
|
|
261
316
|
- uses: actions/checkout@v4
|
|
262
|
-
#
|
|
317
|
+
# Start your site (or deploy to a preview URL and point the config's url at it)
|
|
263
318
|
- run: |
|
|
264
319
|
npm ci && npm run build
|
|
265
|
-
npm run preview & #
|
|
266
|
-
- uses: HSU-YU-MING/cornhsu-parity@v0.
|
|
320
|
+
npm run preview & # e.g. serving on localhost:8080
|
|
321
|
+
- uses: HSU-YU-MING/cornhsu-parity@v0.13.0 # pin exact versions while 0.x
|
|
267
322
|
with:
|
|
268
323
|
config: parity.config.json
|
|
269
|
-
figma-token: ${{ secrets.FIGMA_TOKEN }} #
|
|
324
|
+
figma-token: ${{ secrets.FIGMA_TOKEN }} # omit when the design source is a local JSON file
|
|
270
325
|
```
|
|
271
326
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
-
|
|
275
|
-
|
|
327
|
+
Behaviour:
|
|
328
|
+
|
|
329
|
+
- **PR comment**: posts a fidelity report (score + gap table + **suggested fixes**) and keeps
|
|
330
|
+
updating that same comment instead of adding new ones — so PMs and reviewers see it without
|
|
331
|
+
ever touching the tool.
|
|
332
|
+
- **Blocks the PR**: gaps beyond `gate.failOn` → exit 1 → red PR. (**The comment is posted
|
|
333
|
+
first, then the gate is enforced.**)
|
|
334
|
+
- **Artifacts**: `report.json` plus the Markdown report are uploaded for download.
|
|
276
335
|
|
|
277
|
-
|
|
336
|
+
Action inputs: `config` / `target` / `working-directory` / `version` / `figma-token` /
|
|
337
|
+
`baseline` (regression mode) / `comment` (turn off the PR comment) / `upload-report`.
|
|
278
338
|
|
|
279
|
-
> action
|
|
339
|
+
> The action installs the tool with `dotnet tool install -g Cornhsu.Parity` (published on
|
|
340
|
+
> nuget.org). This repo's own `.github/workflows/ci.yml` instead **builds from source** and runs
|
|
341
|
+
> the offline demo as a self-check, so it does not depend on a published release.
|
|
280
342
|
|
|
281
|
-
##
|
|
343
|
+
## Regression gating: baselines (M5)
|
|
282
344
|
|
|
283
|
-
|
|
345
|
+
A project that already has a pile of gaps cannot start at "zero gaps or no merge". A baseline
|
|
346
|
+
lets you **gate only new and worsened gaps**:
|
|
284
347
|
|
|
285
348
|
```sh
|
|
286
|
-
parity baseline save #
|
|
287
|
-
|
|
288
|
-
parity baseline
|
|
349
|
+
parity baseline save # Store the current gaps + fidelity score as a baseline snapshot
|
|
350
|
+
# (SQLite, in parity.baseline.db, tagged with the git commit)
|
|
351
|
+
parity check --baseline # Compare against the newest baseline: only new or worsened gaps GATE FAIL,
|
|
352
|
+
# and the score trend is shown (baseline 75 → now 83 ↑)
|
|
353
|
+
parity baseline list # Browse snapshot history (the score column is the fidelity trend, for PMs)
|
|
289
354
|
```
|
|
290
355
|
|
|
291
|
-
> **
|
|
356
|
+
> **To use `--baseline` in CI, remember to `git add parity.baseline.db`.** It deliberately lives
|
|
357
|
+
> at the repo root rather than in `.parity/`, which is usually gitignored — otherwise CI cannot
|
|
358
|
+
> find the baseline and silently falls back to the normal gate. The path is configurable via
|
|
359
|
+
> `baselineFile`.
|
|
292
360
|
|
|
293
|
-
-
|
|
294
|
-
-
|
|
295
|
-
-
|
|
361
|
+
- **New** (absent from the baseline, present now) or **worsened** (higher severity) → exit 1, PR blocked
|
|
362
|
+
- **Fixed** (in the baseline, gone now) is listed as encouragement; **unchanged** existing gaps do not gate
|
|
363
|
+
- Ideal for gradual adoption: record reality with `baseline save`, then run `check --baseline` in
|
|
364
|
+
CI, and all the team has to commit to is "do not make fidelity worse"
|
|
296
365
|
|
|
297
|
-
|
|
366
|
+
The storage layer is a separate `Parity.Storage` (EF Core + SQLite, with `Pooling=False` so the
|
|
367
|
+
file is released immediately), and the engine's `BaselineComparer` is a pure, unit-testable function.
|
|
298
368
|
|
|
299
|
-
##
|
|
369
|
+
## Milestones
|
|
300
370
|
|
|
301
|
-
- [x] **M1**
|
|
302
|
-
- [x] **M2**
|
|
303
|
-
- [x] **M3**
|
|
304
|
-
- [x] **M4** GitHub Action
|
|
305
|
-
- [x] **M5** EF Core + SQLite
|
|
371
|
+
- [x] **M1** Engine + CLI prototype: two numeric trees, design side and implementation side
|
|
372
|
+
- [x] **M2** Comparison engine: matching + numeric diffing + tolerances + unmatched list + gate exit codes
|
|
373
|
+
- [x] **M3** Local report UI (`parity serve --watch`, Kestrel bound to 127.0.0.1) + `parity map` interactive matching
|
|
374
|
+
- [x] **M4** GitHub Action: a reusable composite action (`action.yml`) + this repo's own CI (build / test / offline-demo self-check)
|
|
375
|
+
- [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)
|
|
376
|
+
- [ ] **M6** (optional) Cloud shell: public-URL scanning + SSRF protection
|
|
306
377
|
|
|
307
|
-
>
|
|
308
|
-
|
|
378
|
+
> Open work, known blind spots and the next priorities are in [ROADMAP.md](ROADMAP.md);
|
|
379
|
+
> version history is in [CHANGELOG.md](CHANGELOG.md).
|
|
309
380
|
|
|
310
|
-
##
|
|
381
|
+
## Security
|
|
311
382
|
|
|
312
|
-
- Figma token
|
|
313
|
-
|
|
383
|
+
- The Figma token comes from an environment variable (`env:FIGMA_TOKEN`); it never reaches a log
|
|
384
|
+
and never goes in a URL (it is sent as the `X-Figma-Token` header).
|
|
385
|
+
- Fetched frames are cached in `.parity/cache` (gitignored), so re-runs do not hit Figma and can
|
|
386
|
+
compare offline.
|
|
314
387
|
|
|
315
|
-
##
|
|
388
|
+
## See also
|
|
316
389
|
|
|
317
|
-
[**XamlContrast**](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast)
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
390
|
+
[**XamlContrast**](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast) — a sibling project by the
|
|
391
|
+
same author, built on the same philosophy: numeric checks that gate CI. Parity answers "**does the
|
|
392
|
+
implementation match the design?**" (fidelity); XamlContrast answers "**can anyone actually see
|
|
393
|
+
what we built?**" (WCAG contrast, scanned statically from XAML source). One guards fidelity, the
|
|
394
|
+
other readability — and its static color resolution is also the groundwork for the expected values
|
|
395
|
+
of this project's future `WpfImplementationSource` desktop adapter.
|
|
322
396
|
|
|
323
397
|
## License
|
|
324
398
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cornhsu-parity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.
|
|
38
|
-
"@cornhsu/parity-win32-arm64": "0.
|
|
39
|
-
"@cornhsu/parity-linux-x64": "0.
|
|
40
|
-
"@cornhsu/parity-linux-arm64": "0.
|
|
41
|
-
"@cornhsu/parity-darwin-x64": "0.
|
|
42
|
-
"@cornhsu/parity-darwin-arm64": "0.
|
|
37
|
+
"@cornhsu/parity-win32-x64": "0.13.0",
|
|
38
|
+
"@cornhsu/parity-win32-arm64": "0.13.0",
|
|
39
|
+
"@cornhsu/parity-linux-x64": "0.13.0",
|
|
40
|
+
"@cornhsu/parity-linux-arm64": "0.13.0",
|
|
41
|
+
"@cornhsu/parity-darwin-x64": "0.13.0",
|
|
42
|
+
"@cornhsu/parity-darwin-arm64": "0.13.0"
|
|
43
43
|
}
|
|
44
44
|
}
|