cornhsu-parity 2.0.0 → 2.2.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 +178 -7
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -63,11 +63,11 @@ PR comment, updating the same comment rather than adding new ones.
|
|
|
63
63
|
|
|
64
64
|
| | |
|
|
65
65
|
|---|---|
|
|
66
|
-
| Releases |
|
|
66
|
+
| Releases | 30 on NuGet (v0.1.0 → v2.2.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 | **1068**, 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
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
|
|
|
@@ -89,6 +89,46 @@ dotnet tool install -g Cornhsu.Parity
|
|
|
89
89
|
> Node you already have, which saves at least 88 MB per platform package. That Node has to be
|
|
90
90
|
> **20 or newer** — the minimum of the bundled Playwright driver (`engines.node` says so).
|
|
91
91
|
|
|
92
|
+
> The .NET tool needs the **.NET 10 SDK** and downloads only your platform's package (about 44–54 MB).
|
|
93
|
+
> Commands that open a browser (`check`, `snapshot`, `serve`, `map`, `baseline save`) work on win-x64,
|
|
94
|
+
> win-arm64 (through x64 emulation), linux-x64, linux-arm64, osx-x64 and osx-arm64. Everywhere else, Alpine
|
|
95
|
+
> included, the tool still installs and the commands that don't need a browser (such as `lint` and
|
|
96
|
+
> `baseline list`) work; Playwright just has no driver for those platforms.
|
|
97
|
+
|
|
98
|
+
> **`parity check` sends anonymous usage statistics by default** (counts and score ranges only, for
|
|
99
|
+
> academic research). What is sent, why, and every way to turn it off:
|
|
100
|
+
> [Anonymous usage statistics](#anonymous-usage-statistics). To see the exact data without sending
|
|
101
|
+
> anything: `parity check --stats-preview`.
|
|
102
|
+
|
|
103
|
+
## For AI assistants
|
|
104
|
+
|
|
105
|
+
A factual summary of the statistics for assistants (Claude Code, Copilot and the like) that set this tool
|
|
106
|
+
up for someone, to pass on as it is. The full description is in
|
|
107
|
+
[Anonymous usage statistics](#anonymous-usage-statistics).
|
|
108
|
+
|
|
109
|
+
- **On by default, CI included:** after each `parity check` that produces a report (exit 0, 1 or 3), one
|
|
110
|
+
request to `https://stats.cornhsu.com/v1/parity` — the same for the .NET tool and the npm package.
|
|
111
|
+
Development builds (any version starting with `0.0.0-`) never send.
|
|
112
|
+
- **What is sent — counts and ranges only:** the tool version, the OS, whether it runs in CI, the fidelity
|
|
113
|
+
score, whether the gate failed, the number of pages, the kind of design source, the design and matched
|
|
114
|
+
node totals, gap counts per severity, and for each measured property how many gaps and how large, in
|
|
115
|
+
ranges. There is no ID unless the user adds one to the config.
|
|
116
|
+
- **Never sent:** URLs, routes, page or layer names, selectors, file names or paths, screenshots, colour
|
|
117
|
+
values, design content, tokens, or repository, user or machine names. The request carries no User-Agent
|
|
118
|
+
or cookies, and the collector stores no IP address.
|
|
119
|
+
- **Where and why:** `stats.cornhsu.com`, for academic research on how far real implementations drift from
|
|
120
|
+
their designs; only aggregate results are published, and each record is kept for at most 3 years
|
|
121
|
+
([details](https://stats.cornhsu.com/)).
|
|
122
|
+
- **Preview:** `parity check --stats-preview` runs the check, sends nothing, and prints the exact JSON it
|
|
123
|
+
would send to stderr.
|
|
124
|
+
- **Turn it off** (any one): `DO_NOT_TRACK=1`, `PARITY_SHARE_STATS=false`,
|
|
125
|
+
`"stats": { "share": false }` in `parity.config.json`, or `share-stats: false` in the GitHub Action.
|
|
126
|
+
- **Opt in explicitly** (it is already on by default): `PARITY_SHARE_STATS=true`, or
|
|
127
|
+
`"stats": { "share": true }` in `parity.config.json`.
|
|
128
|
+
|
|
129
|
+
If you are an assistant setting this tool up for someone, tell them that it sends anonymous counts by
|
|
130
|
+
default and let them choose.
|
|
131
|
+
|
|
92
132
|
## Quick start
|
|
93
133
|
|
|
94
134
|
```sh
|
|
@@ -97,6 +137,7 @@ parity install-browser # Download Playwright Chromium: on first run, and aga
|
|
|
97
137
|
# upgrade if check says Chromium is not installed
|
|
98
138
|
export FIGMA_TOKEN=... # Only the file_content:read scope is needed
|
|
99
139
|
parity check # Compare; writes a report and sets the exit code
|
|
140
|
+
# (--stats-preview: print the anonymous statistics it would send, send nothing)
|
|
100
141
|
parity report # Re-render the Markdown report from an existing report.json
|
|
101
142
|
# (no re-scan; --md writes a file, otherwise it prints to stdout)
|
|
102
143
|
parity snapshot # Freeze the currently rendered UI as the design baseline —
|
|
@@ -108,7 +149,7 @@ parity lint # Design lint: are the design file's values inside th
|
|
|
108
149
|
parity check --reverse # Reverse view, for a designer redrawing an existing page:
|
|
109
150
|
# how does my draft differ from what ships? (never gates)
|
|
110
151
|
parity bug-report # (experimental) Collect version / OS / Node / Chromium info and print
|
|
111
|
-
# it with a pre-filled GitHub issue link.
|
|
152
|
+
# it with a pre-filled GitHub issue link. bug-report itself connects to nothing;
|
|
112
153
|
# opening the link sends the masked report to github.com (it rides in
|
|
113
154
|
# the URL). No issue until you submit the form. --config adds a
|
|
114
155
|
# structural summary without URLs, routes, keys, tokens or paths
|
|
@@ -144,7 +185,9 @@ serious-severity gaps — which is exactly the behaviour you want from a CI gate
|
|
|
144
185
|
"gate": {
|
|
145
186
|
"failOn": ["critical", "serious"], // any of minor / medium / serious / critical, one per string
|
|
146
187
|
"minMatchRate": 0 // optional: below this match rate (0–1) → exit 3, see below; 0 = no threshold
|
|
147
|
-
}
|
|
188
|
+
},
|
|
189
|
+
"stats": { "share": true } // optional: false turns anonymous usage statistics off;
|
|
190
|
+
// see "Anonymous usage statistics" below (also projectId)
|
|
148
191
|
}
|
|
149
192
|
```
|
|
150
193
|
|
|
@@ -337,6 +380,51 @@ There are three shells, and **the engine does not know that any of them exist**:
|
|
|
337
380
|
> setting, and the integration tests inject one that a developer's `PARITY_SERVER_DB` must not
|
|
338
381
|
> redirect.
|
|
339
382
|
|
|
383
|
+
### One dashboard, two tools
|
|
384
|
+
|
|
385
|
+
The same server also takes reports from **[XamlContrast](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast)**
|
|
386
|
+
(the static WCAG contrast audit for XAML apps): `xamlcontrast --push <report.json>` sends the file
|
|
387
|
+
its `--json` option wrote, through the same `POST /api/runs`, marked with an `X-Parity-Tool: xamlcontrast`
|
|
388
|
+
header (`parity push` sends no such header, which the server reads as `parity`). A project can hold runs
|
|
389
|
+
from both tools. XamlContrast runs get their own overview card per project (pairs below AA, fail · warn,
|
|
390
|
+
coverage), a fail/warn trend, and an issue list (file and line, element, foreground and background as
|
|
391
|
+
written in the XAML, the ratio in each theme, the threshold). Each XamlContrast run also shows what changed
|
|
392
|
+
since the previous XamlContrast run on the same branch — new, worse, fixed and improved pairs — compared the
|
|
393
|
+
way XamlContrast's own `--baseline` does (a pair is file, element, foreground and background, without the line
|
|
394
|
+
number; more places or a lower worst ratio is worse). Like `parity push`, the XamlContrast side is
|
|
395
|
+
**experimental** until the dashboard is publicly deployed.
|
|
396
|
+
|
|
397
|
+
### Hosting it yourself
|
|
398
|
+
|
|
399
|
+
The repo root has a `Dockerfile`. The image contains the server, its data layer and the report types,
|
|
400
|
+
and nothing else: no browser, no engine (CI checks this).
|
|
401
|
+
|
|
402
|
+
```sh
|
|
403
|
+
docker build -t parity-server .
|
|
404
|
+
docker run -d --name parity -p 8080:8080 -v parity-data:/data \
|
|
405
|
+
-e Parity__PublicUrl=https://parity.example.com \
|
|
406
|
+
-e ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
|
|
407
|
+
parity-server
|
|
408
|
+
docker exec parity dotnet Parity.Server.dll create-project my-site # prints the CI token once
|
|
409
|
+
docker exec parity dotnet Parity.Server.dll create-invite my-site you@example.com owner
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
| Setting | What it does |
|
|
413
|
+
|---|---|
|
|
414
|
+
| `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. |
|
|
415
|
+
| `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. |
|
|
416
|
+
| `PARITY_SERVER_DB` | The SQLite database. Preset to `/data/parity-server.db` in the image. |
|
|
417
|
+
| `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. |
|
|
418
|
+
|
|
419
|
+
- **Everything lives in `/data`**: mount a volume there. TLS is the job of the proxy or platform in front.
|
|
420
|
+
- **Run one instance.** The database is SQLite and login attempts are serialized inside one process;
|
|
421
|
+
more than one instance needs a different database first.
|
|
422
|
+
- **Backups**: `docker exec parity dotnet Parity.Server.dll backup /data/backup-2026-10-26.db` writes a
|
|
423
|
+
consistent copy while the server keeps running (SQLite `VACUUM INTO`; it never overwrites a file and
|
|
424
|
+
never creates a missing database). Copy it off the machine. To restore: stop the server, put the copy
|
|
425
|
+
where the database was, start it again.
|
|
426
|
+
- **Uptime**: `GET /healthz` is anonymous and answers `ok` only when the database answers, `503` otherwise.
|
|
427
|
+
|
|
340
428
|
### The iron rule: the cloud never runs a browser
|
|
341
429
|
|
|
342
430
|
`Parity.Server`'s only endpoint that takes in scan results is `POST /api/runs` (its other
|
|
@@ -413,10 +501,13 @@ Behaviour:
|
|
|
413
501
|
own artifact and keeps its own PR comment; two calls with the same name collide on the artifact upload.
|
|
414
502
|
Two calls in **one job** work only while the first one passes: when its gate fails, the job stops at
|
|
415
503
|
that call's last step and the second call never runs (no report, no comment, no artifact for it).
|
|
504
|
+
- **Anonymous usage statistics**: on by default ([what and why](#anonymous-usage-statistics));
|
|
505
|
+
`share-stats: false` turns them off. The job summary gets one line saying whether they were sent, or why not.
|
|
416
506
|
|
|
417
507
|
Action inputs: `config` / `target` / `working-directory` / `version` / `figma-token` /
|
|
418
508
|
`baseline` (regression mode) / `comment` (turn off the PR comment) / `upload-report` /
|
|
419
|
-
`artifact-name` (default `parity-report`; letters, digits, `.`, `_`, `-`, at most 100 characters, not starting with `-` and not only dots)
|
|
509
|
+
`artifact-name` (default `parity-report`; letters, digits, `.`, `_`, `-`, at most 100 characters, not starting with `-` and not only dots) /
|
|
510
|
+
`share-stats` (default `true`; exactly `true` or `false` — anything else stops the action before it installs Parity).
|
|
420
511
|
|
|
421
512
|
> The action installs the tool with `dotnet tool install -g Cornhsu.Parity` (published on
|
|
422
513
|
> nuget.org) — the latest 2.x when `version` is omitted, so `@v2` never jumps to a future 3.x,
|
|
@@ -480,15 +571,95 @@ file is released immediately), and the engine's `BaselineComparer` is a pure, un
|
|
|
480
571
|
yet, so the command and its server protocol may change in a 2.x minor release and join the interface
|
|
481
572
|
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
|
|
482
573
|
measured coordinates (no screenshot upload), an overview and score trend per project and
|
|
483
|
-
route, and accounts / project roles / invite links.
|
|
484
|
-
|
|
574
|
+
route, and accounts / project roles / invite links. It also takes XamlContrast reports
|
|
575
|
+
([one dashboard, two tools](#one-dashboard-two-tools)). Its own internal M1–M4.6 are done; of its
|
|
576
|
+
M5, the deployment groundwork is done (a Docker image, the public URL, persisted sign-in keys,
|
|
577
|
+
online backups), the actual hosting (a host, a domain, TLS) is not. **It receives
|
|
485
578
|
reports, it never scans** — see the iron rule above.
|
|
486
579
|
|
|
487
580
|
> Open work, known blind spots and the next priorities are in [ROADMAP.md](ROADMAP.md);
|
|
488
581
|
> version history is in [CHANGELOG.md](CHANGELOG.md).
|
|
489
582
|
|
|
583
|
+
## Anonymous usage statistics
|
|
584
|
+
|
|
585
|
+
From 2.1.0, `parity check` sends **anonymous usage statistics**, once per run — **on by default, in CI
|
|
586
|
+
too**. They are counts and score ranges used for **academic research** on how far real
|
|
587
|
+
implementations drift from their designs; only aggregate results are published. The public data page — what is collected, what it is used
|
|
588
|
+
for, how long it is kept — is **<https://stats.cornhsu.com/>**.
|
|
589
|
+
|
|
590
|
+
**When.** Only `parity check`, and only when it produced a report (exit 0, 1 or 3). `--reverse`,
|
|
591
|
+
`--stats-preview`, every other command and any run that ends in exit 2 send nothing.
|
|
592
|
+
|
|
593
|
+
**Everything that is sent:**
|
|
594
|
+
|
|
595
|
+
| Field | Meaning |
|
|
596
|
+
|---|---|
|
|
597
|
+
| `schema`, `tool` | Always `1` and `"parity"` |
|
|
598
|
+
| `toolVersion` | The Parity version, e.g. `2.1.0` (no commit hash) |
|
|
599
|
+
| `os` | `windows`, `linux`, `macos` or `other` |
|
|
600
|
+
| `ci` | Whether `CI` or `GITHUB_ACTIONS` is `true` |
|
|
601
|
+
| `projectId` | `null`, unless you set `stats.projectId` yourself (see below) |
|
|
602
|
+
| `data.score` | The fidelity score `check` printed (0–100) |
|
|
603
|
+
| `data.gateFailed` | Whether `check`'s exit code was not 0 (so `--baseline` and exit 3 count as they did in your CI) |
|
|
604
|
+
| `data.pages` | How many targets the report covers |
|
|
605
|
+
| `data.designSource` | `figma`, `snapshot` (a file written by `parity snapshot`), `file` (any other design JSON) or `image` |
|
|
606
|
+
| `data.designNodes`, `data.matched` | Design nodes and matched nodes, summed over all pages |
|
|
607
|
+
| `data.severity` | Gap counts per severity: `critical`, `serious`, `medium`, `minor` |
|
|
608
|
+
| `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 |
|
|
609
|
+
|
|
610
|
+
**Never sent:** URLs, routes, page or layer names, selectors, file names or paths, screenshots, colour
|
|
611
|
+
values, design content, tokens, repository names or IDs, user names, machine names. The request has no
|
|
612
|
+
User-Agent and no cookies. The collector does not store IP addresses or the time a record arrived (only
|
|
613
|
+
its date); Cloudflare, which hosts the collector, sees the connection address at its edge, as any network
|
|
614
|
+
provider does.
|
|
615
|
+
|
|
616
|
+
**No ID unless you add one.** Without `stats.projectId`, nothing links one run to another or to you —
|
|
617
|
+
which also means a record cannot be picked out and deleted afterwards. If you want your runs counted as
|
|
618
|
+
one project (and deletable on request), make up a random ID — for example `openssl rand -hex 12`, or
|
|
619
|
+
`[guid]::NewGuid().ToString('N')` in PowerShell. Parity never derives one from your repository, paths or
|
|
620
|
+
machine, and an ID that is not 16–64 characters of `A–Z a–z 0–9 _ -` is a config error (exit 2):
|
|
621
|
+
|
|
622
|
+
```jsonc
|
|
623
|
+
"stats": {
|
|
624
|
+
"share": true, // false turns statistics off
|
|
625
|
+
"projectId": "REPLACE-with-your-own-random-id" // optional; never copy someone else's
|
|
626
|
+
}
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Preview.** `parity check --stats-preview` runs the check as usual, sends nothing, and prints the exact
|
|
630
|
+
JSON to stderr, followed by whether it would be sent — or why not. The exit code, stdout and the report
|
|
631
|
+
files are unchanged.
|
|
632
|
+
|
|
633
|
+
**Turning it off.** Any one of these is enough; they are checked in this order, and the first that says
|
|
634
|
+
no wins:
|
|
635
|
+
|
|
636
|
+
1. `DO_NOT_TRACK` set to anything except `0` or `false` — always wins.
|
|
637
|
+
2. `CORNHSU_SHARE_STATS` (shared with [XamlContrast](https://github.com/HSU-YU-MING/cornhsu-xamlcontrast))
|
|
638
|
+
or `PARITY_SHARE_STATS` set to `false` — in fact any value other than `1`, `true`, `yes` or `on`, so a
|
|
639
|
+
typo errs on the side of *not* sending.
|
|
640
|
+
3. `"stats": { "share": false }` in `parity.config.json`.
|
|
641
|
+
4. The GitHub Action's `share-stats: false` (it sets `PARITY_SHARE_STATS=false`).
|
|
642
|
+
5. Builds from source and other non-release builds (any version starting with `0.0.0-`, such as `0.0.0-dev`) never send.
|
|
643
|
+
|
|
644
|
+
Like every other key in the config, a misspelt one is ignored: `"stats": { "shrae": false }` turns
|
|
645
|
+
nothing off. `--stats-preview` tells you whether a switch took effect.
|
|
646
|
+
|
|
647
|
+
**Notice.** The first time `check` sends on a machine, it prints a short notice to stderr and leaves a
|
|
648
|
+
marker file, `cornhsu/parity-stats-notice` in your user settings directory (`%APPDATA%` on Windows,
|
|
649
|
+
`~/.config` elsewhere), so the notice is not repeated — delete the file to see it again. In CI it prints
|
|
650
|
+
one line every time statistics were sent. Nothing is printed when nothing is sent, and nothing ever goes
|
|
651
|
+
to stdout. The GitHub Action also writes one line to the job summary: sent, or why not.
|
|
652
|
+
|
|
653
|
+
**Cost.** The request waits at most 1.5 s to connect and 3 s in total, is never retried and never follows
|
|
654
|
+
a redirect. When it fails — offline, a firewall, the collector is down — nothing is printed and nothing
|
|
655
|
+
else changes: the exit code, stdout, the report files and the PR comment are exactly what they would have
|
|
656
|
+
been.
|
|
657
|
+
|
|
490
658
|
## Security
|
|
491
659
|
|
|
660
|
+
- **What Parity connects to**: the Figma API (only with a Figma design source), the dashboard you name
|
|
661
|
+
for `parity push`, and `stats.cornhsu.com` for the [anonymous usage statistics](#anonymous-usage-statistics)
|
|
662
|
+
of `parity check` (unless turned off). The page under test is opened by the local browser. Nothing else.
|
|
492
663
|
- The Figma token comes from an environment variable (`env:FIGMA_TOKEN`); it never reaches a log
|
|
493
664
|
and never goes in a URL (it is sent as the `X-Figma-Token` header).
|
|
494
665
|
- Fetched frames are cached in `.parity/cache` (add `.parity/` to your `.gitignore`), so re-runs do
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cornhsu-parity",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.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": ">=20"
|
|
35
35
|
},
|
|
36
36
|
"optionalDependencies": {
|
|
37
|
-
"@cornhsu/parity-win32-x64": "2.
|
|
38
|
-
"@cornhsu/parity-win32-arm64": "2.
|
|
39
|
-
"@cornhsu/parity-linux-x64": "2.
|
|
40
|
-
"@cornhsu/parity-linux-arm64": "2.
|
|
41
|
-
"@cornhsu/parity-darwin-x64": "2.
|
|
42
|
-
"@cornhsu/parity-darwin-arm64": "2.
|
|
37
|
+
"@cornhsu/parity-win32-x64": "2.2.0",
|
|
38
|
+
"@cornhsu/parity-win32-arm64": "2.2.0",
|
|
39
|
+
"@cornhsu/parity-linux-x64": "2.2.0",
|
|
40
|
+
"@cornhsu/parity-linux-arm64": "2.2.0",
|
|
41
|
+
"@cornhsu/parity-darwin-x64": "2.2.0",
|
|
42
|
+
"@cornhsu/parity-darwin-arm64": "2.2.0"
|
|
43
43
|
}
|
|
44
44
|
}
|