cornhsu-parity 1.1.0 → 2.1.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/LICENSE +21 -0
- package/README.md +271 -32
- package/bin/parity.js +18 -11
- package/package.json +8 -8
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 許彧銘 Hsu Yu-Ming (Cornhsu)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
## GitHub Action
|
|
12
12
|
|
|
13
13
|
```yaml
|
|
14
|
-
- uses: HSU-YU-MING/cornhsu-parity@
|
|
14
|
+
- uses: HSU-YU-MING/cornhsu-parity@v2 # @v2 tracks the latest 2.x (interfaces frozen within a major)
|
|
15
15
|
with:
|
|
16
16
|
config: parity.config.json # optional, defaults to parity.config.json
|
|
17
17
|
target: /pricing # optional, check only this route (omit for all)
|
|
@@ -63,13 +63,13 @@ PR comment, updating the same comment rather than adding new ones.
|
|
|
63
63
|
|
|
64
64
|
| | |
|
|
65
65
|
|---|---|
|
|
66
|
-
| Releases |
|
|
66
|
+
| Releases | 29 on NuGet (v0.1.0 → v2.1.0). Pushing a tag publishes via OIDC Trusted Publishing — zero long-lived keys in the repo |
|
|
67
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
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
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 | **
|
|
70
|
+
| Tests | **1047**, covering the CIEDE2000 reference data set (Sharma), match disambiguation, position false-positive guards, image sampling, and real-browser capture regressions |
|
|
71
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
|
|
72
|
+
| Proven in production | **All 26 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) |
|
|
73
73
|
|
|
74
74
|
## Install
|
|
75
75
|
|
|
@@ -86,15 +86,23 @@ dotnet tool install -g Cornhsu.Parity
|
|
|
86
86
|
> The npm build is the same self-contained native executable; you only download the one for
|
|
87
87
|
> your platform (win32-x64 / win32-arm64 / linux-x64 / linux-arm64 / darwin-x64 / darwin-arm64).
|
|
88
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
|
|
89
|
+
> Node you already have, which saves at least 88 MB per platform package. That Node has to be
|
|
90
|
+
> **20 or newer** — the minimum of the bundled Playwright driver (`engines.node` says so).
|
|
91
|
+
|
|
92
|
+
> **`parity check` sends anonymous usage statistics by default** (counts and score ranges only, for
|
|
93
|
+
> academic research). What is sent, why, and every way to turn it off:
|
|
94
|
+
> [Anonymous usage statistics](#anonymous-usage-statistics). To see the exact data without sending
|
|
95
|
+
> anything: `parity check --stats-preview`.
|
|
90
96
|
|
|
91
97
|
## Quick start
|
|
92
98
|
|
|
93
99
|
```sh
|
|
94
100
|
parity init # Generate a parity.config.json template
|
|
95
|
-
parity install-browser #
|
|
101
|
+
parity install-browser # Download Playwright Chromium: on first run, and again after an
|
|
102
|
+
# upgrade if check says Chromium is not installed
|
|
96
103
|
export FIGMA_TOKEN=... # Only the file_content:read scope is needed
|
|
97
104
|
parity check # Compare; writes a report and sets the exit code
|
|
105
|
+
# (--stats-preview: print the anonymous statistics it would send, send nothing)
|
|
98
106
|
parity report # Re-render the Markdown report from an existing report.json
|
|
99
107
|
# (no re-scan; --md writes a file, otherwise it prints to stdout)
|
|
100
108
|
parity snapshot # Freeze the currently rendered UI as the design baseline —
|
|
@@ -105,6 +113,11 @@ parity lint # Design lint: are the design file's values inside th
|
|
|
105
113
|
# design-token set? (design only, no implementation)
|
|
106
114
|
parity check --reverse # Reverse view, for a designer redrawing an existing page:
|
|
107
115
|
# how does my draft differ from what ships? (never gates)
|
|
116
|
+
parity bug-report # (experimental) Collect version / OS / Node / Chromium info and print
|
|
117
|
+
# it with a pre-filled GitHub issue link. bug-report itself connects to nothing;
|
|
118
|
+
# opening the link sends the masked report to github.com (it rides in
|
|
119
|
+
# the URL). No issue until you submit the form. --config adds a
|
|
120
|
+
# structural summary without URLs, routes, keys, tokens or paths
|
|
108
121
|
```
|
|
109
122
|
|
|
110
123
|
When working inside this repo:
|
|
@@ -125,24 +138,29 @@ serious-severity gaps — which is exactly the behaviour you want from a CI gate
|
|
|
125
138
|
"figmaFileKey": "abcd1234",
|
|
126
139
|
"designToken": "env:FIGMA_TOKEN",
|
|
127
140
|
"mapFile": "parity.map.json", // manual fallback mapping (layer name → selector)
|
|
128
|
-
"tokensFile": "tokens.json", // optional: design tokens
|
|
141
|
+
"tokensFile": "tokens.json", // optional: design tokens, a flat object of string values
|
|
142
|
+
// ({"brand": "#2563EB", "space-4": "16px"}; // comments ok);
|
|
129
143
|
// suggested fixes will name the matching token
|
|
130
144
|
"targets": [
|
|
131
145
|
{ "route": "/", "frame": "10:2", "url": "http://localhost:8080/" }
|
|
132
146
|
],
|
|
133
147
|
"compare": { "position": "relative" }, // relative = compare relative position (default); none = skip position
|
|
134
|
-
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "positionPx": 4 },
|
|
148
|
+
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "fontSizePx": 0.5, "positionPx": 4 },
|
|
135
149
|
"ignore": ["[data-parity-ignore]"],
|
|
136
150
|
"gate": {
|
|
137
|
-
"failOn": ["critical", "serious"],
|
|
138
|
-
"minMatchRate": 0 // optional:
|
|
139
|
-
}
|
|
151
|
+
"failOn": ["critical", "serious"], // any of minor / medium / serious / critical, one per string
|
|
152
|
+
"minMatchRate": 0 // optional: below this match rate (0–1) → exit 3, see below; 0 = no threshold
|
|
153
|
+
},
|
|
154
|
+
"stats": { "share": true } // optional: false turns anonymous usage statistics off;
|
|
155
|
+
// see "Anonymous usage statistics" below (also projectId)
|
|
140
156
|
}
|
|
141
157
|
```
|
|
142
158
|
|
|
143
159
|
> Besides gaps, the gate also validates **match integrity**: zero matches (or zero design nodes)
|
|
144
|
-
>
|
|
145
|
-
>
|
|
160
|
+
> always fails, with exit code 3 (`MATCH INTEGRITY TOO LOW`) rather than 1 — with nothing
|
|
161
|
+
> matched there are no gaps to catch, so a silent PASS would be a false pass, usually caused by
|
|
162
|
+
> a wrong url or frame. A match rate below `gate.minMatchRate` fails the same way, with exit
|
|
163
|
+
> code 3. `--baseline` mode is not exempt; `--reverse` never gates and always exits 0.
|
|
146
164
|
|
|
147
165
|
## Targets: a web page or an Electron desktop app
|
|
148
166
|
|
|
@@ -209,15 +227,20 @@ Only the things that should hold no matter how the layout flows:
|
|
|
209
227
|
as references (TEXT/HUG boxes are never references, TEXT is never a target, and Y is honestly
|
|
210
228
|
skipped when everything above is text), so line-height drift in a flowing layout does not
|
|
211
229
|
produce false positives. Turn it off with `compare.position: "none"`.
|
|
212
|
-
- **Typography**: size
|
|
213
|
-
|
|
230
|
+
- **Typography**: size and letter-spacing are compared as numbers within `tolerances.fontSizePx`,
|
|
231
|
+
line-height within `tolerances.sizePx`; weight has no tolerance — any difference is a gap
|
|
232
|
+
(under 200 apart is medium, 200 or more is serious). font-family is a **soft gap**: it is
|
|
233
|
+
reported (severity `minor`, marked soft) but costs no fidelity points and **never gates** —
|
|
234
|
+
not even when `failOn` includes `minor`, and `--baseline` mode does not count it as new.
|
|
214
235
|
- **Color**: thresholded on CIEDE2000 (ΔE), not hex equality. Parsing covers modern syntax
|
|
215
236
|
(`rgb(37 99 235 / .5)`, `color(srgb …)`, `oklch()`, `color(display-p3 …)`).
|
|
216
237
|
- **Absolute x/y is deliberately never compared**: it legitimately differs in a flexible layout,
|
|
217
238
|
so comparing it means false positives, which means losing trust.
|
|
218
239
|
|
|
219
|
-
> When the design source is Figma, layer names in the report (
|
|
220
|
-
>
|
|
240
|
+
> When the design source is Figma, layer names in the report (the Markdown written by
|
|
241
|
+
> `check --md` or re-rendered by `parity report`, and the local UI) **link back to that node in
|
|
242
|
+
> Figma** — a designer clicks once and lands on the layer. When `designFile` is set it wins over
|
|
243
|
+
> `figmaFileKey`, so a snapshot- or JSON-based report carries no Figma links.
|
|
221
244
|
|
|
222
245
|
## Two workflows for designers
|
|
223
246
|
|
|
@@ -245,7 +268,9 @@ frame's size**, so a 375-wide phone frame makes your media queries fire naturall
|
|
|
245
268
|
```
|
|
246
269
|
|
|
247
270
|
`route` is only a label in the report, so pick something recognizable. You only need `width` /
|
|
248
|
-
`height` on a target when the frame's
|
|
271
|
+
`height` on a target when the frame's size is not the viewport you want to test. Each one
|
|
272
|
+
overrides its own axis: `width` alone keeps the default height of 800, `height` alone keeps the
|
|
273
|
+
frame's width.
|
|
249
274
|
|
|
250
275
|
## Shadow DOM / iframes
|
|
251
276
|
|
|
@@ -262,7 +287,8 @@ unaffected and works as usual).
|
|
|
262
287
|
|
|
263
288
|
1. **Automatic text anchoring**: design TEXT content ↔ page text (matched only when unique;
|
|
264
289
|
when several elements share the text, the layer name disambiguates — and it still refuses to force a match)
|
|
265
|
-
2. **Layer name ↔ id / class / aria-label
|
|
290
|
+
2. **Layer name ↔ id / class / aria-label** (falling back to `data-testid` when there is no
|
|
291
|
+
aria-label): `CTA Button` matches `class="cta-button"` automatically
|
|
266
292
|
3. **Container inference**: an unmatched container is inferred from the lowest common ancestor of
|
|
267
293
|
its already-matched descendants (pure structure — no guessing)
|
|
268
294
|
4. **Manual fallback**: add `data-parity="Layer Name"` in your HTML, or write
|
|
@@ -286,11 +312,102 @@ src/Parity.Engine/ The engine; FidelityEngine is the only entry point
|
|
|
286
312
|
DesignSources/ IDesignSource + Figma (REST + local cache) / Json
|
|
287
313
|
ImplementationSources/ IImplementationSource + Web (Playwright)
|
|
288
314
|
Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta (CIEDE2000)
|
|
289
|
-
src/Parity.
|
|
315
|
+
src/Parity.Report/ The report contract: report.json types, ReportWire, the fidelity score,
|
|
316
|
+
baseline comparison — no package dependencies, so no Playwright
|
|
317
|
+
src/Parity.Cli/ The dotnet tool shell: parity check / init / install-browser,
|
|
318
|
+
the local report UI (parity serve) and parity push (experimental)
|
|
319
|
+
src/Parity.Storage/ Baseline history (EF Core + SQLite), behind parity baseline
|
|
320
|
+
src/Parity.Server/ Team dashboard shell (ASP.NET Core) — not published to NuGet
|
|
321
|
+
src/Parity.Server.Data/ Its database (EF Core + SQLite, migrations from day one)
|
|
322
|
+
web/ The dashboard front end (React + Vite + TypeScript); the build
|
|
323
|
+
output lands in src/Parity.Server/wwwroot
|
|
290
324
|
tests/Parity.Tests/ Unit tests (including the CIEDE2000 reference data)
|
|
291
325
|
samples/demo/ Offline demo: a deliberately broken page + a design JSON
|
|
292
326
|
```
|
|
293
327
|
|
|
328
|
+
There are three shells, and **the engine does not know that any of them exist**:
|
|
329
|
+
|
|
330
|
+
1. **CLI** (`Parity.Cli`) — `parity check` in a terminal or in CI. This is the only shell
|
|
331
|
+
published as a package (NuGet / npm / the GitHub Action).
|
|
332
|
+
2. **Local report UI** (`parity serve`) — a zero-build SPA bound to 127.0.0.1, for the
|
|
333
|
+
person who is actually fixing the gaps.
|
|
334
|
+
3. **Team dashboard** (`Parity.Server` + `web/`) — somewhere the reports can accumulate, so
|
|
335
|
+
that trends exist and so that people who never touch the tool have a place to look. You
|
|
336
|
+
deploy it yourself; it is not on NuGet, and it is **still in development** (see M7 below).
|
|
337
|
+
|
|
338
|
+
> **Where the dashboard keeps its data.** The web host and the admin commands (`create-project`,
|
|
339
|
+
> `create-invite`) find the SQLite database the same way: `ConnectionStrings:parity` (from
|
|
340
|
+
> `appsettings.json` or the `ConnectionStrings__parity` environment variable), then
|
|
341
|
+
> `PARITY_SERVER_DB` (a file path), then `parity-server.db` in the working directory. This is the
|
|
342
|
+
> reverse of .NET's usual "environment beats appsettings": a connection string in
|
|
343
|
+
> `appsettings.json` wins over `PARITY_SERVER_DB` — to override it from the environment, set
|
|
344
|
+
> `ConnectionStrings__parity`. The reason: an explicit connection string is the more specific
|
|
345
|
+
> setting, and the integration tests inject one that a developer's `PARITY_SERVER_DB` must not
|
|
346
|
+
> redirect.
|
|
347
|
+
|
|
348
|
+
### One dashboard, two tools
|
|
349
|
+
|
|
350
|
+
The same server also takes reports from **[XamlContrast](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast)**
|
|
351
|
+
(the static WCAG contrast audit for XAML apps): `xamlcontrast --push <report.json>` sends the file
|
|
352
|
+
its `--json` option wrote, through the same `POST /api/runs`, marked with an `X-Parity-Tool: xamlcontrast`
|
|
353
|
+
header (`parity push` sends no such header, which the server reads as `parity`). A project can hold runs
|
|
354
|
+
from both tools. XamlContrast runs get their own overview card per project (pairs below AA, fail · warn,
|
|
355
|
+
coverage), a fail/warn trend, and an issue list (file and line, element, foreground and background as
|
|
356
|
+
written in the XAML, the ratio in each theme, the threshold). Each XamlContrast run also shows what changed
|
|
357
|
+
since the previous XamlContrast run on the same branch — new, worse, fixed and improved pairs — compared the
|
|
358
|
+
way XamlContrast's own `--baseline` does (a pair is file, element, foreground and background, without the line
|
|
359
|
+
number; more places or a lower worst ratio is worse). Like `parity push`, the XamlContrast side is
|
|
360
|
+
**experimental** until the dashboard is publicly deployed.
|
|
361
|
+
|
|
362
|
+
### Hosting it yourself
|
|
363
|
+
|
|
364
|
+
The repo root has a `Dockerfile`. The image contains the server, its data layer and the report types,
|
|
365
|
+
and nothing else: no browser, no engine (CI checks this).
|
|
366
|
+
|
|
367
|
+
```sh
|
|
368
|
+
docker build -t parity-server .
|
|
369
|
+
docker run -d --name parity -p 8080:8080 -v parity-data:/data \
|
|
370
|
+
-e Parity__PublicUrl=https://parity.example.com \
|
|
371
|
+
-e ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
|
|
372
|
+
parity-server
|
|
373
|
+
docker exec parity dotnet Parity.Server.dll create-project my-site # prints the CI token once
|
|
374
|
+
docker exec parity dotnet Parity.Server.dll create-invite my-site you@example.com owner
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
| Setting | What it does |
|
|
378
|
+
|---|---|
|
|
379
|
+
| `Parity__PublicUrl` | The address people open. Invite links are built from it, not from the `Host` the server sees (behind a proxy that can be an internal name). It must be an absolute `http(s)` URL; a bad value stops the server and the admin commands. |
|
|
380
|
+
| `Parity__KeysDirectory` | Where the keys that sign the login cookie live. Preset to `/data/keys` in the image. Without it, every redeploy of a container signs everyone out. |
|
|
381
|
+
| `PARITY_SERVER_DB` | The SQLite database. Preset to `/data/parity-server.db` in the image. |
|
|
382
|
+
| `ASPNETCORE_FORWARDEDHEADERS_ENABLED=true` | Behind a reverse proxy, **required**, and only behind a proxy you trust (otherwise anyone can forge their client IP). Without it the server sees one IP for everybody, and a single person can lock all users out of login with 20 attempts a minute. |
|
|
383
|
+
|
|
384
|
+
- **Everything lives in `/data`**: mount a volume there. TLS is the job of the proxy or platform in front.
|
|
385
|
+
- **Run one instance.** The database is SQLite and login attempts are serialized inside one process;
|
|
386
|
+
more than one instance needs a different database first.
|
|
387
|
+
- **Backups**: `docker exec parity dotnet Parity.Server.dll backup /data/backup-2026-10-26.db` writes a
|
|
388
|
+
consistent copy while the server keeps running (SQLite `VACUUM INTO`; it never overwrites a file and
|
|
389
|
+
never creates a missing database). Copy it off the machine. To restore: stop the server, put the copy
|
|
390
|
+
where the database was, start it again.
|
|
391
|
+
- **Uptime**: `GET /healthz` is anonymous and answers `ok` only when the database answers, `503` otherwise.
|
|
392
|
+
|
|
393
|
+
### The iron rule: the cloud never runs a browser
|
|
394
|
+
|
|
395
|
+
`Parity.Server`'s only endpoint that takes in scan results is `POST /api/runs` (its other
|
|
396
|
+
write endpoints only manage sign-in, invites, members, tokens and run deletion), and what it
|
|
397
|
+
accepts is **a report that has already finished running**. There is no "give me a URL and I will go scan it"
|
|
398
|
+
endpoint, and there will not be one: **scanning always happens on your own machine or in your
|
|
399
|
+
CI**, where the page, the credentials and the localhost port already are.
|
|
400
|
+
|
|
401
|
+
This is not an implementation detail — it is the security claim of the whole design. It is why
|
|
402
|
+
the dashboard has **no SSRF surface at all**: it is never handed a URL to fetch. And it is
|
|
403
|
+
enforced structurally rather than by policy — the server projects do not reference the engine
|
|
404
|
+
at all, only `Parity.Report` (the report types, which have no package dependencies), so neither
|
|
405
|
+
Playwright nor its driver is anywhere in the server's build output. A test walks the server's
|
|
406
|
+
whole project-reference graph and turns CI red if any project in it references Playwright or
|
|
407
|
+
the engine; another turns it red the moment server code references a Playwright type or the
|
|
408
|
+
engine's capture side (`FidelityEngine`, `IImplementationSource`, or anything in
|
|
409
|
+
`ImplementationSources.Web`, which is the code that opens Chromium).
|
|
410
|
+
|
|
294
411
|
## Local report UI (M3)
|
|
295
412
|
|
|
296
413
|
```sh
|
|
@@ -321,12 +438,12 @@ jobs:
|
|
|
321
438
|
contents: read
|
|
322
439
|
pull-requests: write # lets the action post the fidelity report as a PR comment
|
|
323
440
|
steps:
|
|
324
|
-
- uses: actions/checkout@
|
|
441
|
+
- uses: actions/checkout@v7
|
|
325
442
|
# Start your site (or deploy to a preview URL and point the config's url at it)
|
|
326
443
|
- run: |
|
|
327
444
|
npm ci && npm run build
|
|
328
445
|
npm run preview & # e.g. serving on localhost:8080
|
|
329
|
-
- uses: HSU-YU-MING/cornhsu-parity@
|
|
446
|
+
- uses: HSU-YU-MING/cornhsu-parity@v2 # @v2 tracks the latest 2.x
|
|
330
447
|
with:
|
|
331
448
|
config: parity.config.json
|
|
332
449
|
figma-token: ${{ secrets.FIGMA_TOKEN }} # omit when the design source is a local JSON file
|
|
@@ -334,25 +451,49 @@ jobs:
|
|
|
334
451
|
|
|
335
452
|
Behaviour:
|
|
336
453
|
|
|
337
|
-
- **PR comment**: posts a fidelity report (score + gap table + **suggested fixes
|
|
454
|
+
- **PR comment**: posts a fidelity report (score + gap table + **suggested fixes**, ending with a
|
|
455
|
+
"Report a misjudgment" link to the issue form — a link only, nothing is sent) and keeps
|
|
338
456
|
updating that same comment instead of adding new ones — so PMs and reviewers see it without
|
|
339
457
|
ever touching the tool.
|
|
340
458
|
- **Blocks the PR**: gaps beyond `gate.failOn` → exit 1 → red PR. (**The comment is posted
|
|
341
459
|
first, then the gate is enforced.**)
|
|
342
|
-
- **Artifacts**: `report.json` plus the Markdown report are uploaded for download
|
|
460
|
+
- **Artifacts**: `report.json` plus the Markdown report are uploaded for download (artifact
|
|
461
|
+
name `parity-report` unless you set `artifact-name`; `report.json` and `parity-report.md` at its
|
|
462
|
+
root). `report.json` is picked up from `.parity/report.json` next to the config file — where
|
|
463
|
+
`check` writes it — even when `config` points into a subdirectory.
|
|
464
|
+
- **Running it more than once per workflow run**: prefer a **matrix** (one job per site or config), and
|
|
465
|
+
give each call its own `artifact-name` (e.g. `parity-report-${{ matrix.site }}`). Each name gets its
|
|
466
|
+
own artifact and keeps its own PR comment; two calls with the same name collide on the artifact upload.
|
|
467
|
+
Two calls in **one job** work only while the first one passes: when its gate fails, the job stops at
|
|
468
|
+
that call's last step and the second call never runs (no report, no comment, no artifact for it).
|
|
469
|
+
- **Anonymous usage statistics**: on by default ([what and why](#anonymous-usage-statistics));
|
|
470
|
+
`share-stats: false` turns them off. The job summary gets one line saying whether they were sent, or why not.
|
|
343
471
|
|
|
344
472
|
Action inputs: `config` / `target` / `working-directory` / `version` / `figma-token` /
|
|
345
|
-
`baseline` (regression mode) / `comment` (turn off the PR comment) / `upload-report
|
|
473
|
+
`baseline` (regression mode) / `comment` (turn off the PR comment) / `upload-report` /
|
|
474
|
+
`artifact-name` (default `parity-report`; letters, digits, `.`, `_`, `-`, at most 100 characters, not starting with `-` and not only dots) /
|
|
475
|
+
`share-stats` (default `true`; exactly `true` or `false` — anything else stops the action before it installs Parity).
|
|
346
476
|
|
|
347
477
|
> The action installs the tool with `dotnet tool install -g Cornhsu.Parity` (published on
|
|
348
|
-
> nuget.org)
|
|
349
|
-
> the
|
|
478
|
+
> nuget.org) — the latest 2.x when `version` is omitted, so `@v2` never jumps to a future 3.x,
|
|
479
|
+
> or exactly the `version` you pass. **Within one job the tool is installed once per call into the same
|
|
480
|
+
> global location, and a later call cannot install a lower version than an earlier one** (`dotnet tool`
|
|
481
|
+
> refuses to downgrade) — e.g. a first call without `version` installs the latest 2.x, and a second call
|
|
482
|
+
> with `version: 2.0.0` fails. Give every call in a job the same `version`, or use a matrix. This repo's own `.github/workflows/ci.yml` instead **builds from
|
|
483
|
+
> source** and runs the offline demo as a self-check, so it does not depend on a published release.
|
|
484
|
+
>
|
|
485
|
+
> **Upgrading from `@v1`?** `v1` stays at 1.1.0, whose action installs the newest release of *any*
|
|
486
|
+
> major when `version` is omitted — so an unpinned `@v1` workflow already runs 2.x. Switch to `@v2` and
|
|
487
|
+
> work through the "upgrade required" list in the CHANGELOG's 2.0.0 entry, or set `version: 1.1.0` to
|
|
488
|
+
> stay on 1.x for now.
|
|
350
489
|
|
|
351
490
|
## Report vocabulary (for anyone parsing `report.json`)
|
|
352
491
|
|
|
492
|
+
- **Unknown keys** — consumers must ignore keys they do not recognize. Within 2.x, `report.json` only *gains* fields; existing keys are never removed, renamed or given a different type (that would be 3.0). Closed sets below never gain members within 2.x either.
|
|
353
493
|
- `severity` — **closed set** (spelling and membership frozen): `none` / `minor` / `medium` / `serious` / `critical`. `status`: `mismatch` / `missing`.
|
|
354
494
|
- `unmatched[].reason` — **open set**: parsers must tolerate unknown values; known spellings never change. Currently: `no-anchor`, `ambiguous-or-missing-text`, `randomized-id`, `size-implausible`.
|
|
355
495
|
- `nodes[].matchedBy` — **open set**, same rule. Currently: `selector`, `explicit`, `auto-text`, `auto-name`, `auto-container`.
|
|
496
|
+
- `direction` (top level, since 2.0.0) — **closed set**: `forward` (a normal `check`) / `reverse` (`check --reverse`: expected and actual swapped, never gated). Always written; files without it read as `forward`. Readers accept exactly the lowercase strings `"forward"` and `"reverse"` — any other value (other casing, a number, `null`) is an error, not a guess. `parity report` does not gate a reverse report, and `parity push` refuses one.
|
|
356
497
|
|
|
357
498
|
## Regression gating: baselines (M5)
|
|
358
499
|
|
|
@@ -387,17 +528,111 @@ file is released immediately), and the engine's `BaselineComparer` is a pure, un
|
|
|
387
528
|
- [x] **M3** Local report UI (`parity serve --watch`, Kestrel bound to 127.0.0.1) + `parity map` interactive matching
|
|
388
529
|
- [x] **M4** GitHub Action: a reusable composite action (`action.yml`) + this repo's own CI (build / test / offline-demo self-check)
|
|
389
530
|
- [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)
|
|
390
|
-
- [ ] **M6** (optional) Cloud shell: public-URL scanning + SSRF protection
|
|
531
|
+
- [ ] **M6** (optional) Cloud shell: public-URL scanning + SSRF protection — **deliberately
|
|
532
|
+
declined** (2026-07-18). This is *not* what M7 is: M7 never scans anything. The reasoning is
|
|
533
|
+
kept in [ROADMAP.md](ROADMAP.md).
|
|
534
|
+
- [ ] **M7** Team dashboard shell (`Parity.Server` + `Parity.Server.Data` + `web/`) — **in
|
|
535
|
+
progress, not yet released.** `parity push` (**experimental**: the server is not publicly deployed
|
|
536
|
+
yet, so the command and its server protocol may change in a 2.x minor release and join the interface
|
|
537
|
+
freeze once the server ships) sends a finished `report.json` to a server you host yourself; the dashboard renders the gaps as an engineering blueprint drawn from the
|
|
538
|
+
measured coordinates (no screenshot upload), an overview and score trend per project and
|
|
539
|
+
route, and accounts / project roles / invite links. It also takes XamlContrast reports
|
|
540
|
+
([one dashboard, two tools](#one-dashboard-two-tools)). Its own internal M1–M4.6 are done; of its
|
|
541
|
+
M5, the deployment groundwork is done (a Docker image, the public URL, persisted sign-in keys,
|
|
542
|
+
online backups), the actual hosting (a host, a domain, TLS) is not. **It receives
|
|
543
|
+
reports, it never scans** — see the iron rule above.
|
|
391
544
|
|
|
392
545
|
> Open work, known blind spots and the next priorities are in [ROADMAP.md](ROADMAP.md);
|
|
393
546
|
> version history is in [CHANGELOG.md](CHANGELOG.md).
|
|
394
547
|
|
|
548
|
+
## Anonymous usage statistics
|
|
549
|
+
|
|
550
|
+
From 2.1.0, `parity check` sends **anonymous usage statistics**, once per run — **on by default, in CI
|
|
551
|
+
too**. They are counts and score ranges used for **academic research** on how far real
|
|
552
|
+
implementations drift from their designs; only aggregate results are published. The public data page — what is collected, what it is used
|
|
553
|
+
for, how long it is kept — is **<https://stats.cornhsu.com/>**.
|
|
554
|
+
|
|
555
|
+
**When.** Only `parity check`, and only when it produced a report (exit 0, 1 or 3). `--reverse`,
|
|
556
|
+
`--stats-preview`, every other command and any run that ends in exit 2 send nothing.
|
|
557
|
+
|
|
558
|
+
**Everything that is sent:**
|
|
559
|
+
|
|
560
|
+
| Field | Meaning |
|
|
561
|
+
|---|---|
|
|
562
|
+
| `schema`, `tool` | Always `1` and `"parity"` |
|
|
563
|
+
| `toolVersion` | The Parity version, e.g. `2.1.0` (no commit hash) |
|
|
564
|
+
| `os` | `windows`, `linux`, `macos` or `other` |
|
|
565
|
+
| `ci` | Whether `CI` or `GITHUB_ACTIONS` is `true` |
|
|
566
|
+
| `projectId` | `null`, unless you set `stats.projectId` yourself (see below) |
|
|
567
|
+
| `data.score` | The fidelity score `check` printed (0–100) |
|
|
568
|
+
| `data.gateFailed` | Whether `check`'s exit code was not 0 (so `--baseline` and exit 3 count as they did in your CI) |
|
|
569
|
+
| `data.pages` | How many targets the report covers |
|
|
570
|
+
| `data.designSource` | `figma`, `snapshot` (a file written by `parity snapshot`), `file` (any other design JSON) or `image` |
|
|
571
|
+
| `data.designNodes`, `data.matched` | Design nodes and matched nodes, summed over all pages |
|
|
572
|
+
| `data.severity` | Gap counts per severity: `critical`, `serious`, `medium`, `minor` |
|
|
573
|
+
| `data.props` | For each property with at least one gap: how many gaps, how many were `missing`, and how many fall in each range of `abs(delta)` — px for lengths (`offsetX`/`offsetY`, size, padding, `itemSpacing`, `cornerRadius`, `fontSize`, `lineHeight`, `letterSpacing`: <1, 1–2, 2–4, 4–8, 8–16, 16–32, ≥32), ΔE for `color`/`background` (<1, 1–2, 2–5, 5–10, 10–20, ≥20), and weight steps for `fontWeight` (<100, 100–200, 200–300, ≥300). `fontFamily` has no ranges, only counts |
|
|
574
|
+
|
|
575
|
+
**Never sent:** URLs, routes, page or layer names, selectors, file names or paths, screenshots, colour
|
|
576
|
+
values, design content, tokens, repository names or IDs, user names, machine names. The request has no
|
|
577
|
+
User-Agent and no cookies. The collector does not store IP addresses or the time a record arrived (only
|
|
578
|
+
its date); Cloudflare, which hosts the collector, sees the connection address at its edge, as any network
|
|
579
|
+
provider does.
|
|
580
|
+
|
|
581
|
+
**No ID unless you add one.** Without `stats.projectId`, nothing links one run to another or to you —
|
|
582
|
+
which also means a record cannot be picked out and deleted afterwards. If you want your runs counted as
|
|
583
|
+
one project (and deletable on request), make up a random ID — for example `openssl rand -hex 12`, or
|
|
584
|
+
`[guid]::NewGuid().ToString('N')` in PowerShell. Parity never derives one from your repository, paths or
|
|
585
|
+
machine, and an ID that is not 16–64 characters of `A–Z a–z 0–9 _ -` is a config error (exit 2):
|
|
586
|
+
|
|
587
|
+
```jsonc
|
|
588
|
+
"stats": {
|
|
589
|
+
"share": true, // false turns statistics off
|
|
590
|
+
"projectId": "REPLACE-with-your-own-random-id" // optional; never copy someone else's
|
|
591
|
+
}
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
**Preview.** `parity check --stats-preview` runs the check as usual, sends nothing, and prints the exact
|
|
595
|
+
JSON to stderr, followed by whether it would be sent — or why not. The exit code, stdout and the report
|
|
596
|
+
files are unchanged.
|
|
597
|
+
|
|
598
|
+
**Turning it off.** Any one of these is enough; they are checked in this order, and the first that says
|
|
599
|
+
no wins:
|
|
600
|
+
|
|
601
|
+
1. `DO_NOT_TRACK` set to anything except `0` or `false` — always wins.
|
|
602
|
+
2. `CORNHSU_SHARE_STATS` (shared with [XamlContrast](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast))
|
|
603
|
+
or `PARITY_SHARE_STATS` set to `false` — in fact any value other than `1`, `true`, `yes` or `on`, so a
|
|
604
|
+
typo errs on the side of *not* sending.
|
|
605
|
+
3. `"stats": { "share": false }` in `parity.config.json`.
|
|
606
|
+
4. The GitHub Action's `share-stats: false` (it sets `PARITY_SHARE_STATS=false`).
|
|
607
|
+
5. Builds from source and other non-release builds (any version starting with `0.0.0-`, such as `0.0.0-dev`) never send.
|
|
608
|
+
|
|
609
|
+
Like every other key in the config, a misspelt one is ignored: `"stats": { "shrae": false }` turns
|
|
610
|
+
nothing off. `--stats-preview` tells you whether a switch took effect.
|
|
611
|
+
|
|
612
|
+
**Notice.** The first time `check` sends on a machine, it prints a short notice to stderr and leaves a
|
|
613
|
+
marker file, `cornhsu/parity-stats-notice` in your user settings directory (`%APPDATA%` on Windows,
|
|
614
|
+
`~/.config` elsewhere), so the notice is not repeated — delete the file to see it again. In CI it prints
|
|
615
|
+
one line every time statistics were sent. Nothing is printed when nothing is sent, and nothing ever goes
|
|
616
|
+
to stdout. The GitHub Action also writes one line to the job summary: sent, or why not.
|
|
617
|
+
|
|
618
|
+
**Cost.** The request waits at most 1.5 s to connect and 3 s in total, is never retried and never follows
|
|
619
|
+
a redirect. When it fails — offline, a firewall, the collector is down — nothing is printed and nothing
|
|
620
|
+
else changes: the exit code, stdout, the report files and the PR comment are exactly what they would have
|
|
621
|
+
been.
|
|
622
|
+
|
|
395
623
|
## Security
|
|
396
624
|
|
|
625
|
+
- **What Parity connects to**: the Figma API (only with a Figma design source), the dashboard you name
|
|
626
|
+
for `parity push`, and `stats.cornhsu.com` for the [anonymous usage statistics](#anonymous-usage-statistics)
|
|
627
|
+
of `parity check` (unless turned off). The page under test is opened by the local browser. Nothing else.
|
|
397
628
|
- The Figma token comes from an environment variable (`env:FIGMA_TOKEN`); it never reaches a log
|
|
398
629
|
and never goes in a URL (it is sent as the `X-Figma-Token` header).
|
|
399
|
-
- Fetched frames are cached in `.parity/cache` (
|
|
400
|
-
compare offline.
|
|
630
|
+
- Fetched frames are cached in `.parity/cache` (add `.parity/` to your `.gitignore`), so re-runs do
|
|
631
|
+
not hit Figma and can compare offline.
|
|
632
|
+
- The local report UI binds 127.0.0.1 only, and additionally rejects unexpected `Host` headers
|
|
633
|
+
(DNS rebinding) and cross-origin `POST`s (CSRF).
|
|
634
|
+
- **The team dashboard never scans anything** — it only receives finished reports, so it has no
|
|
635
|
+
SSRF surface. See [the iron rule](#the-iron-rule-the-cloud-never-runs-a-browser).
|
|
401
636
|
|
|
402
637
|
## See also
|
|
403
638
|
|
|
@@ -413,8 +648,12 @@ same author, built on the same philosophy: numeric checks that gate CI. Parity a
|
|
|
413
648
|
implementation match the design?**" (fidelity); XamlContrast answers "**can anyone actually see
|
|
414
649
|
what we built?**" (WCAG contrast, scanned statically from XAML source). One guards fidelity, the
|
|
415
650
|
other readability — and its static color resolution is also the groundwork for the expected values
|
|
416
|
-
of this project's future `WpfImplementationSource` desktop adapter.
|
|
651
|
+
of this project's future `WpfImplementationSource` desktop adapter (see §4.5 of the
|
|
652
|
+
[project plan](docs/Parity專案規畫書.md), in Traditional Chinese).
|
|
417
653
|
|
|
418
654
|
## License
|
|
419
655
|
|
|
420
|
-
MIT
|
|
656
|
+
MIT. The NuGet and npm packages also bundle third-party components (Playwright, ImageSharp,
|
|
657
|
+
SQLite and others; the npm packages also include the .NET runtime). Their licenses are listed in
|
|
658
|
+
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), which ships inside the NuGet package and every npm
|
|
659
|
+
platform package.
|
package/bin/parity.js
CHANGED
|
@@ -17,24 +17,31 @@
|
|
|
17
17
|
const { spawnSync } = require("node:child_process");
|
|
18
18
|
const path = require("node:path");
|
|
19
19
|
|
|
20
|
-
// 平台 → 子套件名 /
|
|
20
|
+
// 平台 → 子套件名 / 執行檔名。必須與 npm/prepare.mjs 的 TARGETS 一一對應
|
|
21
|
+
// (RepoInvariantsTests 會比對;1.1.0 漏了兩個 arm64,平台包有發佈、npx 卻說不支援)
|
|
21
22
|
const TARGETS = {
|
|
22
23
|
"win32 x64": { pkg: "@cornhsu/parity-win32-x64", bin: "parity.exe" },
|
|
24
|
+
"win32 arm64": { pkg: "@cornhsu/parity-win32-arm64", bin: "parity.exe" },
|
|
23
25
|
"linux x64": { pkg: "@cornhsu/parity-linux-x64", bin: "parity" },
|
|
26
|
+
"linux arm64": { pkg: "@cornhsu/parity-linux-arm64", bin: "parity" },
|
|
24
27
|
"darwin x64": { pkg: "@cornhsu/parity-darwin-x64", bin: "parity" },
|
|
25
28
|
"darwin arm64": { pkg: "@cornhsu/parity-darwin-arm64", bin: "parity" },
|
|
26
29
|
};
|
|
27
30
|
|
|
31
|
+
// 啟動腳本自己的錯誤一律 exit 2:CLI 的契約是 0 通過 / 1 落差超 gate / 2 執行錯誤 / 3 可信度不足,
|
|
32
|
+
// 回 1 會被 CI 讀成「設計還原度不合格」。訊息用英文,與 CLI 本體一致(0.12.0 起)。
|
|
33
|
+
const EXECUTION_ERROR = 2;
|
|
34
|
+
|
|
28
35
|
const key = `${process.platform} ${process.arch}`;
|
|
29
36
|
const target = TARGETS[key];
|
|
30
37
|
|
|
31
38
|
if (!target) {
|
|
32
39
|
console.error(
|
|
33
|
-
`
|
|
34
|
-
|
|
35
|
-
|
|
40
|
+
`cornhsu-parity: there is no prebuilt build for ${key}.\n` +
|
|
41
|
+
`Supported: ${Object.keys(TARGETS).join(", ")}\n` +
|
|
42
|
+
`On other platforms use the .NET tool instead: dotnet tool install -g Cornhsu.Parity`
|
|
36
43
|
);
|
|
37
|
-
process.exit(
|
|
44
|
+
process.exit(EXECUTION_ERROR);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
let binary;
|
|
@@ -45,11 +52,11 @@ try {
|
|
|
45
52
|
binary = path.join(path.dirname(manifest), "bin", target.bin);
|
|
46
53
|
} catch {
|
|
47
54
|
console.error(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
55
|
+
`cornhsu-parity: the platform package ${target.pkg} is not installed.\n` +
|
|
56
|
+
`It is an optional dependency, so the install may have skipped it (--no-optional / --omit=optional) or been interrupted.\n` +
|
|
57
|
+
`Reinstall: npm install cornhsu-parity`
|
|
51
58
|
);
|
|
52
|
-
process.exit(
|
|
59
|
+
process.exit(EXECUTION_ERROR);
|
|
53
60
|
}
|
|
54
61
|
|
|
55
62
|
const result = spawnSync(binary, process.argv.slice(2), {
|
|
@@ -62,8 +69,8 @@ const result = spawnSync(binary, process.argv.slice(2), {
|
|
|
62
69
|
});
|
|
63
70
|
|
|
64
71
|
if (result.error) {
|
|
65
|
-
console.error(
|
|
66
|
-
process.exit(
|
|
72
|
+
console.error(`cornhsu-parity: could not run ${binary}: ${result.error.message}`);
|
|
73
|
+
process.exit(EXECUTION_ERROR);
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
// 被訊號中斷時 status 會是 null;轉成慣例的 128+signal,別讓 CI 誤判為成功
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cornhsu-parity",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.1.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",
|
|
@@ -31,14 +31,14 @@
|
|
|
31
31
|
"README.md"
|
|
32
32
|
],
|
|
33
33
|
"engines": {
|
|
34
|
-
"node": ">=
|
|
34
|
+
"node": ">=20"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@cornhsu/parity-win32-x64": "
|
|
38
|
-
"@cornhsu/parity-win32-arm64": "
|
|
39
|
-
"@cornhsu/parity-linux-x64": "
|
|
40
|
-
"@cornhsu/parity-linux-arm64": "
|
|
41
|
-
"@cornhsu/parity-darwin-x64": "
|
|
42
|
-
"@cornhsu/parity-darwin-arm64": "
|
|
37
|
+
"@cornhsu/parity-win32-x64": "2.1.0",
|
|
38
|
+
"@cornhsu/parity-win32-arm64": "2.1.0",
|
|
39
|
+
"@cornhsu/parity-linux-x64": "2.1.0",
|
|
40
|
+
"@cornhsu/parity-linux-arm64": "2.1.0",
|
|
41
|
+
"@cornhsu/parity-darwin-x64": "2.1.0",
|
|
42
|
+
"@cornhsu/parity-darwin-arm64": "2.1.0"
|
|
43
43
|
}
|
|
44
44
|
}
|