cornhsu-parity 2.0.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.
Files changed (2) hide show
  1. package/README.md +143 -7
  2. 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 | 28 on NuGet (v0.1.0 → v2.0.0). Pushing a tag publishes via OIDC Trusted Publishing — zero long-lived keys in the repo |
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 | **805**, covering the CIEDE2000 reference data set (Sharma), match disambiguation, position false-positive guards, image sampling, and real-browser capture regressions |
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
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,11 @@ 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
+ > **`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`.
96
+
92
97
  ## Quick start
93
98
 
94
99
  ```sh
@@ -97,6 +102,7 @@ parity install-browser # Download Playwright Chromium: on first run, and aga
97
102
  # upgrade if check says Chromium is not installed
98
103
  export FIGMA_TOKEN=... # Only the file_content:read scope is needed
99
104
  parity check # Compare; writes a report and sets the exit code
105
+ # (--stats-preview: print the anonymous statistics it would send, send nothing)
100
106
  parity report # Re-render the Markdown report from an existing report.json
101
107
  # (no re-scan; --md writes a file, otherwise it prints to stdout)
102
108
  parity snapshot # Freeze the currently rendered UI as the design baseline —
@@ -108,7 +114,7 @@ parity lint # Design lint: are the design file's values inside th
108
114
  parity check --reverse # Reverse view, for a designer redrawing an existing page:
109
115
  # how does my draft differ from what ships? (never gates)
110
116
  parity bug-report # (experimental) Collect version / OS / Node / Chromium info and print
111
- # it with a pre-filled GitHub issue link. Parity itself connects to nothing;
117
+ # it with a pre-filled GitHub issue link. bug-report itself connects to nothing;
112
118
  # opening the link sends the masked report to github.com (it rides in
113
119
  # the URL). No issue until you submit the form. --config adds a
114
120
  # structural summary without URLs, routes, keys, tokens or paths
@@ -144,7 +150,9 @@ serious-severity gaps — which is exactly the behaviour you want from a CI gate
144
150
  "gate": {
145
151
  "failOn": ["critical", "serious"], // any of minor / medium / serious / critical, one per string
146
152
  "minMatchRate": 0 // optional: below this match rate (0–1) → exit 3, see below; 0 = no threshold
147
- }
153
+ },
154
+ "stats": { "share": true } // optional: false turns anonymous usage statistics off;
155
+ // see "Anonymous usage statistics" below (also projectId)
148
156
  }
149
157
  ```
150
158
 
@@ -337,6 +345,51 @@ There are three shells, and **the engine does not know that any of them exist**:
337
345
  > setting, and the integration tests inject one that a developer's `PARITY_SERVER_DB` must not
338
346
  > redirect.
339
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
+
340
393
  ### The iron rule: the cloud never runs a browser
341
394
 
342
395
  `Parity.Server`'s only endpoint that takes in scan results is `POST /api/runs` (its other
@@ -413,10 +466,13 @@ Behaviour:
413
466
  own artifact and keeps its own PR comment; two calls with the same name collide on the artifact upload.
414
467
  Two calls in **one job** work only while the first one passes: when its gate fails, the job stops at
415
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.
416
471
 
417
472
  Action inputs: `config` / `target` / `working-directory` / `version` / `figma-token` /
418
473
  `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).
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).
420
476
 
421
477
  > The action installs the tool with `dotnet tool install -g Cornhsu.Parity` (published on
422
478
  > nuget.org) — the latest 2.x when `version` is omitted, so `@v2` never jumps to a future 3.x,
@@ -480,15 +536,95 @@ file is released immediately), and the engine's `BaselineComparer` is a pure, un
480
536
  yet, so the command and its server protocol may change in a 2.x minor release and join the interface
481
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
482
538
  measured coordinates (no screenshot upload), an overview and score trend per project and
483
- route, and accounts / project roles / invite links. Its own internal M1–M4.6 are done; its
484
- M5 (real external deployment: TLS, a domain, a hosted database) is not. **It receives
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
485
543
  reports, it never scans** — see the iron rule above.
486
544
 
487
545
  > Open work, known blind spots and the next priorities are in [ROADMAP.md](ROADMAP.md);
488
546
  > version history is in [CHANGELOG.md](CHANGELOG.md).
489
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
+
490
623
  ## Security
491
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.
492
628
  - The Figma token comes from an environment variable (`env:FIGMA_TOKEN`); it never reaches a log
493
629
  and never goes in a URL (it is sent as the `X-Figma-Token` header).
494
630
  - 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.0.0",
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",
@@ -34,11 +34,11 @@
34
34
  "node": ">=20"
35
35
  },
36
36
  "optionalDependencies": {
37
- "@cornhsu/parity-win32-x64": "2.0.0",
38
- "@cornhsu/parity-win32-arm64": "2.0.0",
39
- "@cornhsu/parity-linux-x64": "2.0.0",
40
- "@cornhsu/parity-linux-arm64": "2.0.0",
41
- "@cornhsu/parity-darwin-x64": "2.0.0",
42
- "@cornhsu/parity-darwin-arm64": "2.0.0"
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
  }