cornhsu-parity 1.1.0 → 2.0.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 +134 -31
- 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 | 28 on NuGet (v0.1.0 → v2.0.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 | **805**, 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,13 +86,15 @@ 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).
|
|
90
91
|
|
|
91
92
|
## Quick start
|
|
92
93
|
|
|
93
94
|
```sh
|
|
94
95
|
parity init # Generate a parity.config.json template
|
|
95
|
-
parity install-browser #
|
|
96
|
+
parity install-browser # Download Playwright Chromium: on first run, and again after an
|
|
97
|
+
# upgrade if check says Chromium is not installed
|
|
96
98
|
export FIGMA_TOKEN=... # Only the file_content:read scope is needed
|
|
97
99
|
parity check # Compare; writes a report and sets the exit code
|
|
98
100
|
parity report # Re-render the Markdown report from an existing report.json
|
|
@@ -105,6 +107,11 @@ parity lint # Design lint: are the design file's values inside th
|
|
|
105
107
|
# design-token set? (design only, no implementation)
|
|
106
108
|
parity check --reverse # Reverse view, for a designer redrawing an existing page:
|
|
107
109
|
# how does my draft differ from what ships? (never gates)
|
|
110
|
+
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;
|
|
112
|
+
# opening the link sends the masked report to github.com (it rides in
|
|
113
|
+
# the URL). No issue until you submit the form. --config adds a
|
|
114
|
+
# structural summary without URLs, routes, keys, tokens or paths
|
|
108
115
|
```
|
|
109
116
|
|
|
110
117
|
When working inside this repo:
|
|
@@ -125,24 +132,27 @@ serious-severity gaps — which is exactly the behaviour you want from a CI gate
|
|
|
125
132
|
"figmaFileKey": "abcd1234",
|
|
126
133
|
"designToken": "env:FIGMA_TOKEN",
|
|
127
134
|
"mapFile": "parity.map.json", // manual fallback mapping (layer name → selector)
|
|
128
|
-
"tokensFile": "tokens.json", // optional: design tokens
|
|
135
|
+
"tokensFile": "tokens.json", // optional: design tokens, a flat object of string values
|
|
136
|
+
// ({"brand": "#2563EB", "space-4": "16px"}; // comments ok);
|
|
129
137
|
// suggested fixes will name the matching token
|
|
130
138
|
"targets": [
|
|
131
139
|
{ "route": "/", "frame": "10:2", "url": "http://localhost:8080/" }
|
|
132
140
|
],
|
|
133
141
|
"compare": { "position": "relative" }, // relative = compare relative position (default); none = skip position
|
|
134
|
-
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "positionPx": 4 },
|
|
142
|
+
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "fontSizePx": 0.5, "positionPx": 4 },
|
|
135
143
|
"ignore": ["[data-parity-ignore]"],
|
|
136
144
|
"gate": {
|
|
137
|
-
"failOn": ["critical", "serious"],
|
|
138
|
-
"minMatchRate": 0 // optional:
|
|
145
|
+
"failOn": ["critical", "serious"], // any of minor / medium / serious / critical, one per string
|
|
146
|
+
"minMatchRate": 0 // optional: below this match rate (0–1) → exit 3, see below; 0 = no threshold
|
|
139
147
|
}
|
|
140
148
|
}
|
|
141
149
|
```
|
|
142
150
|
|
|
143
151
|
> Besides gaps, the gate also validates **match integrity**: zero matches (or zero design nodes)
|
|
144
|
-
>
|
|
145
|
-
>
|
|
152
|
+
> always fails, with exit code 3 (`MATCH INTEGRITY TOO LOW`) rather than 1 — with nothing
|
|
153
|
+
> matched there are no gaps to catch, so a silent PASS would be a false pass, usually caused by
|
|
154
|
+
> a wrong url or frame. A match rate below `gate.minMatchRate` fails the same way, with exit
|
|
155
|
+
> code 3. `--baseline` mode is not exempt; `--reverse` never gates and always exits 0.
|
|
146
156
|
|
|
147
157
|
## Targets: a web page or an Electron desktop app
|
|
148
158
|
|
|
@@ -209,15 +219,20 @@ Only the things that should hold no matter how the layout flows:
|
|
|
209
219
|
as references (TEXT/HUG boxes are never references, TEXT is never a target, and Y is honestly
|
|
210
220
|
skipped when everything above is text), so line-height drift in a flowing layout does not
|
|
211
221
|
produce false positives. Turn it off with `compare.position: "none"`.
|
|
212
|
-
- **Typography**: size
|
|
213
|
-
|
|
222
|
+
- **Typography**: size and letter-spacing are compared as numbers within `tolerances.fontSizePx`,
|
|
223
|
+
line-height within `tolerances.sizePx`; weight has no tolerance — any difference is a gap
|
|
224
|
+
(under 200 apart is medium, 200 or more is serious). font-family is a **soft gap**: it is
|
|
225
|
+
reported (severity `minor`, marked soft) but costs no fidelity points and **never gates** —
|
|
226
|
+
not even when `failOn` includes `minor`, and `--baseline` mode does not count it as new.
|
|
214
227
|
- **Color**: thresholded on CIEDE2000 (ΔE), not hex equality. Parsing covers modern syntax
|
|
215
228
|
(`rgb(37 99 235 / .5)`, `color(srgb …)`, `oklch()`, `color(display-p3 …)`).
|
|
216
229
|
- **Absolute x/y is deliberately never compared**: it legitimately differs in a flexible layout,
|
|
217
230
|
so comparing it means false positives, which means losing trust.
|
|
218
231
|
|
|
219
|
-
> When the design source is Figma, layer names in the report (
|
|
220
|
-
>
|
|
232
|
+
> When the design source is Figma, layer names in the report (the Markdown written by
|
|
233
|
+
> `check --md` or re-rendered by `parity report`, and the local UI) **link back to that node in
|
|
234
|
+
> Figma** — a designer clicks once and lands on the layer. When `designFile` is set it wins over
|
|
235
|
+
> `figmaFileKey`, so a snapshot- or JSON-based report carries no Figma links.
|
|
221
236
|
|
|
222
237
|
## Two workflows for designers
|
|
223
238
|
|
|
@@ -245,7 +260,9 @@ frame's size**, so a 375-wide phone frame makes your media queries fire naturall
|
|
|
245
260
|
```
|
|
246
261
|
|
|
247
262
|
`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
|
|
263
|
+
`height` on a target when the frame's size is not the viewport you want to test. Each one
|
|
264
|
+
overrides its own axis: `width` alone keeps the default height of 800, `height` alone keeps the
|
|
265
|
+
frame's width.
|
|
249
266
|
|
|
250
267
|
## Shadow DOM / iframes
|
|
251
268
|
|
|
@@ -262,7 +279,8 @@ unaffected and works as usual).
|
|
|
262
279
|
|
|
263
280
|
1. **Automatic text anchoring**: design TEXT content ↔ page text (matched only when unique;
|
|
264
281
|
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
|
|
282
|
+
2. **Layer name ↔ id / class / aria-label** (falling back to `data-testid` when there is no
|
|
283
|
+
aria-label): `CTA Button` matches `class="cta-button"` automatically
|
|
266
284
|
3. **Container inference**: an unmatched container is inferred from the lowest common ancestor of
|
|
267
285
|
its already-matched descendants (pure structure — no guessing)
|
|
268
286
|
4. **Manual fallback**: add `data-parity="Layer Name"` in your HTML, or write
|
|
@@ -286,11 +304,57 @@ src/Parity.Engine/ The engine; FidelityEngine is the only entry point
|
|
|
286
304
|
DesignSources/ IDesignSource + Figma (REST + local cache) / Json
|
|
287
305
|
ImplementationSources/ IImplementationSource + Web (Playwright)
|
|
288
306
|
Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta (CIEDE2000)
|
|
289
|
-
src/Parity.
|
|
307
|
+
src/Parity.Report/ The report contract: report.json types, ReportWire, the fidelity score,
|
|
308
|
+
baseline comparison — no package dependencies, so no Playwright
|
|
309
|
+
src/Parity.Cli/ The dotnet tool shell: parity check / init / install-browser,
|
|
310
|
+
the local report UI (parity serve) and parity push (experimental)
|
|
311
|
+
src/Parity.Storage/ Baseline history (EF Core + SQLite), behind parity baseline
|
|
312
|
+
src/Parity.Server/ Team dashboard shell (ASP.NET Core) — not published to NuGet
|
|
313
|
+
src/Parity.Server.Data/ Its database (EF Core + SQLite, migrations from day one)
|
|
314
|
+
web/ The dashboard front end (React + Vite + TypeScript); the build
|
|
315
|
+
output lands in src/Parity.Server/wwwroot
|
|
290
316
|
tests/Parity.Tests/ Unit tests (including the CIEDE2000 reference data)
|
|
291
317
|
samples/demo/ Offline demo: a deliberately broken page + a design JSON
|
|
292
318
|
```
|
|
293
319
|
|
|
320
|
+
There are three shells, and **the engine does not know that any of them exist**:
|
|
321
|
+
|
|
322
|
+
1. **CLI** (`Parity.Cli`) — `parity check` in a terminal or in CI. This is the only shell
|
|
323
|
+
published as a package (NuGet / npm / the GitHub Action).
|
|
324
|
+
2. **Local report UI** (`parity serve`) — a zero-build SPA bound to 127.0.0.1, for the
|
|
325
|
+
person who is actually fixing the gaps.
|
|
326
|
+
3. **Team dashboard** (`Parity.Server` + `web/`) — somewhere the reports can accumulate, so
|
|
327
|
+
that trends exist and so that people who never touch the tool have a place to look. You
|
|
328
|
+
deploy it yourself; it is not on NuGet, and it is **still in development** (see M7 below).
|
|
329
|
+
|
|
330
|
+
> **Where the dashboard keeps its data.** The web host and the admin commands (`create-project`,
|
|
331
|
+
> `create-invite`) find the SQLite database the same way: `ConnectionStrings:parity` (from
|
|
332
|
+
> `appsettings.json` or the `ConnectionStrings__parity` environment variable), then
|
|
333
|
+
> `PARITY_SERVER_DB` (a file path), then `parity-server.db` in the working directory. This is the
|
|
334
|
+
> reverse of .NET's usual "environment beats appsettings": a connection string in
|
|
335
|
+
> `appsettings.json` wins over `PARITY_SERVER_DB` — to override it from the environment, set
|
|
336
|
+
> `ConnectionStrings__parity`. The reason: an explicit connection string is the more specific
|
|
337
|
+
> setting, and the integration tests inject one that a developer's `PARITY_SERVER_DB` must not
|
|
338
|
+
> redirect.
|
|
339
|
+
|
|
340
|
+
### The iron rule: the cloud never runs a browser
|
|
341
|
+
|
|
342
|
+
`Parity.Server`'s only endpoint that takes in scan results is `POST /api/runs` (its other
|
|
343
|
+
write endpoints only manage sign-in, invites, members, tokens and run deletion), and what it
|
|
344
|
+
accepts is **a report that has already finished running**. There is no "give me a URL and I will go scan it"
|
|
345
|
+
endpoint, and there will not be one: **scanning always happens on your own machine or in your
|
|
346
|
+
CI**, where the page, the credentials and the localhost port already are.
|
|
347
|
+
|
|
348
|
+
This is not an implementation detail — it is the security claim of the whole design. It is why
|
|
349
|
+
the dashboard has **no SSRF surface at all**: it is never handed a URL to fetch. And it is
|
|
350
|
+
enforced structurally rather than by policy — the server projects do not reference the engine
|
|
351
|
+
at all, only `Parity.Report` (the report types, which have no package dependencies), so neither
|
|
352
|
+
Playwright nor its driver is anywhere in the server's build output. A test walks the server's
|
|
353
|
+
whole project-reference graph and turns CI red if any project in it references Playwright or
|
|
354
|
+
the engine; another turns it red the moment server code references a Playwright type or the
|
|
355
|
+
engine's capture side (`FidelityEngine`, `IImplementationSource`, or anything in
|
|
356
|
+
`ImplementationSources.Web`, which is the code that opens Chromium).
|
|
357
|
+
|
|
294
358
|
## Local report UI (M3)
|
|
295
359
|
|
|
296
360
|
```sh
|
|
@@ -321,12 +385,12 @@ jobs:
|
|
|
321
385
|
contents: read
|
|
322
386
|
pull-requests: write # lets the action post the fidelity report as a PR comment
|
|
323
387
|
steps:
|
|
324
|
-
- uses: actions/checkout@
|
|
388
|
+
- uses: actions/checkout@v7
|
|
325
389
|
# Start your site (or deploy to a preview URL and point the config's url at it)
|
|
326
390
|
- run: |
|
|
327
391
|
npm ci && npm run build
|
|
328
392
|
npm run preview & # e.g. serving on localhost:8080
|
|
329
|
-
- uses: HSU-YU-MING/cornhsu-parity@
|
|
393
|
+
- uses: HSU-YU-MING/cornhsu-parity@v2 # @v2 tracks the latest 2.x
|
|
330
394
|
with:
|
|
331
395
|
config: parity.config.json
|
|
332
396
|
figma-token: ${{ secrets.FIGMA_TOKEN }} # omit when the design source is a local JSON file
|
|
@@ -334,25 +398,46 @@ jobs:
|
|
|
334
398
|
|
|
335
399
|
Behaviour:
|
|
336
400
|
|
|
337
|
-
- **PR comment**: posts a fidelity report (score + gap table + **suggested fixes
|
|
401
|
+
- **PR comment**: posts a fidelity report (score + gap table + **suggested fixes**, ending with a
|
|
402
|
+
"Report a misjudgment" link to the issue form — a link only, nothing is sent) and keeps
|
|
338
403
|
updating that same comment instead of adding new ones — so PMs and reviewers see it without
|
|
339
404
|
ever touching the tool.
|
|
340
405
|
- **Blocks the PR**: gaps beyond `gate.failOn` → exit 1 → red PR. (**The comment is posted
|
|
341
406
|
first, then the gate is enforced.**)
|
|
342
|
-
- **Artifacts**: `report.json` plus the Markdown report are uploaded for download
|
|
407
|
+
- **Artifacts**: `report.json` plus the Markdown report are uploaded for download (artifact
|
|
408
|
+
name `parity-report` unless you set `artifact-name`; `report.json` and `parity-report.md` at its
|
|
409
|
+
root). `report.json` is picked up from `.parity/report.json` next to the config file — where
|
|
410
|
+
`check` writes it — even when `config` points into a subdirectory.
|
|
411
|
+
- **Running it more than once per workflow run**: prefer a **matrix** (one job per site or config), and
|
|
412
|
+
give each call its own `artifact-name` (e.g. `parity-report-${{ matrix.site }}`). Each name gets its
|
|
413
|
+
own artifact and keeps its own PR comment; two calls with the same name collide on the artifact upload.
|
|
414
|
+
Two calls in **one job** work only while the first one passes: when its gate fails, the job stops at
|
|
415
|
+
that call's last step and the second call never runs (no report, no comment, no artifact for it).
|
|
343
416
|
|
|
344
417
|
Action inputs: `config` / `target` / `working-directory` / `version` / `figma-token` /
|
|
345
|
-
`baseline` (regression mode) / `comment` (turn off the PR comment) / `upload-report
|
|
418
|
+
`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).
|
|
346
420
|
|
|
347
421
|
> The action installs the tool with `dotnet tool install -g Cornhsu.Parity` (published on
|
|
348
|
-
> nuget.org)
|
|
349
|
-
> the
|
|
422
|
+
> nuget.org) — the latest 2.x when `version` is omitted, so `@v2` never jumps to a future 3.x,
|
|
423
|
+
> or exactly the `version` you pass. **Within one job the tool is installed once per call into the same
|
|
424
|
+
> global location, and a later call cannot install a lower version than an earlier one** (`dotnet tool`
|
|
425
|
+
> refuses to downgrade) — e.g. a first call without `version` installs the latest 2.x, and a second call
|
|
426
|
+
> 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
|
|
427
|
+
> source** and runs the offline demo as a self-check, so it does not depend on a published release.
|
|
428
|
+
>
|
|
429
|
+
> **Upgrading from `@v1`?** `v1` stays at 1.1.0, whose action installs the newest release of *any*
|
|
430
|
+
> major when `version` is omitted — so an unpinned `@v1` workflow already runs 2.x. Switch to `@v2` and
|
|
431
|
+
> work through the "upgrade required" list in the CHANGELOG's 2.0.0 entry, or set `version: 1.1.0` to
|
|
432
|
+
> stay on 1.x for now.
|
|
350
433
|
|
|
351
434
|
## Report vocabulary (for anyone parsing `report.json`)
|
|
352
435
|
|
|
436
|
+
- **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
437
|
- `severity` — **closed set** (spelling and membership frozen): `none` / `minor` / `medium` / `serious` / `critical`. `status`: `mismatch` / `missing`.
|
|
354
438
|
- `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
439
|
- `nodes[].matchedBy` — **open set**, same rule. Currently: `selector`, `explicit`, `auto-text`, `auto-name`, `auto-container`.
|
|
440
|
+
- `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
441
|
|
|
357
442
|
## Regression gating: baselines (M5)
|
|
358
443
|
|
|
@@ -387,7 +472,17 @@ file is released immediately), and the engine's `BaselineComparer` is a pure, un
|
|
|
387
472
|
- [x] **M3** Local report UI (`parity serve --watch`, Kestrel bound to 127.0.0.1) + `parity map` interactive matching
|
|
388
473
|
- [x] **M4** GitHub Action: a reusable composite action (`action.yml`) + this repo's own CI (build / test / offline-demo self-check)
|
|
389
474
|
- [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
|
|
475
|
+
- [ ] **M6** (optional) Cloud shell: public-URL scanning + SSRF protection — **deliberately
|
|
476
|
+
declined** (2026-07-18). This is *not* what M7 is: M7 never scans anything. The reasoning is
|
|
477
|
+
kept in [ROADMAP.md](ROADMAP.md).
|
|
478
|
+
- [ ] **M7** Team dashboard shell (`Parity.Server` + `Parity.Server.Data` + `web/`) — **in
|
|
479
|
+
progress, not yet released.** `parity push` (**experimental**: the server is not publicly deployed
|
|
480
|
+
yet, so the command and its server protocol may change in a 2.x minor release and join the interface
|
|
481
|
+
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
|
+
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
|
|
485
|
+
reports, it never scans** — see the iron rule above.
|
|
391
486
|
|
|
392
487
|
> Open work, known blind spots and the next priorities are in [ROADMAP.md](ROADMAP.md);
|
|
393
488
|
> version history is in [CHANGELOG.md](CHANGELOG.md).
|
|
@@ -396,8 +491,12 @@ file is released immediately), and the engine's `BaselineComparer` is a pure, un
|
|
|
396
491
|
|
|
397
492
|
- The Figma token comes from an environment variable (`env:FIGMA_TOKEN`); it never reaches a log
|
|
398
493
|
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.
|
|
494
|
+
- Fetched frames are cached in `.parity/cache` (add `.parity/` to your `.gitignore`), so re-runs do
|
|
495
|
+
not hit Figma and can compare offline.
|
|
496
|
+
- The local report UI binds 127.0.0.1 only, and additionally rejects unexpected `Host` headers
|
|
497
|
+
(DNS rebinding) and cross-origin `POST`s (CSRF).
|
|
498
|
+
- **The team dashboard never scans anything** — it only receives finished reports, so it has no
|
|
499
|
+
SSRF surface. See [the iron rule](#the-iron-rule-the-cloud-never-runs-a-browser).
|
|
401
500
|
|
|
402
501
|
## See also
|
|
403
502
|
|
|
@@ -413,8 +512,12 @@ same author, built on the same philosophy: numeric checks that gate CI. Parity a
|
|
|
413
512
|
implementation match the design?**" (fidelity); XamlContrast answers "**can anyone actually see
|
|
414
513
|
what we built?**" (WCAG contrast, scanned statically from XAML source). One guards fidelity, the
|
|
415
514
|
other readability — and its static color resolution is also the groundwork for the expected values
|
|
416
|
-
of this project's future `WpfImplementationSource` desktop adapter.
|
|
515
|
+
of this project's future `WpfImplementationSource` desktop adapter (see §4.5 of the
|
|
516
|
+
[project plan](docs/Parity專案規畫書.md), in Traditional Chinese).
|
|
417
517
|
|
|
418
518
|
## License
|
|
419
519
|
|
|
420
|
-
MIT
|
|
520
|
+
MIT. The NuGet and npm packages also bundle third-party components (Playwright, ImageSharp,
|
|
521
|
+
SQLite and others; the npm packages also include the .NET runtime). Their licenses are listed in
|
|
522
|
+
[THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md), which ships inside the NuGet package and every npm
|
|
523
|
+
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.0.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.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"
|
|
43
43
|
}
|
|
44
44
|
}
|