Codync
무료 오픈소스 1:1 대안, Grok Bot과 Muse 대신
저장소 정보는 아직 수집하지 않았습니다.
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
Numeric design-fidelity checking. Instead of overlaying screenshots for a human to eyeball, it compares real values from your Figma file against the rendered UI and tells you "padding is 8px, design says 12px" - precise enough to gate a PR in CI. Runs locally or on internal networks. A dotnet tool.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Project write-up · GitHub Marketplace · npm · NuGet · 繁體中文 · MIT
- uses: HSU-YU-MING/cornhsu-parity@v2 # @v2 tracks the latest 2.x (interfaces frozen within a major)
with:
config: parity.config.json # optional, defaults to parity.config.json
target: /pricing # optional, check only this route (omit for all)
Fails CI when the gap exceeds the gate threshold, and posts the fidelity report as a PR comment that updates in place instead of piling up.
Design fidelity checking that can actually gate your CI (property-level design QA). It compares the real numeric values on both sides — your Figma file and the actually rendered page — programmatically: "padding is 8px, the design says 12px." Because the output is numbers rather than an impression, it can be a gate: a PR that drifts from the design turns red, with the fidelity report posted straight onto the PR.
Other tools help you see design drift. Parity blocks it.
Most comparable tools (Uiprobe, Loupe, OverlayQA, Floto Design Diff, PixelProof…) do numeric comparison these days. The real difference is not "numbers vs. overlays" — it is whether the whole thing can be automated:
| Manual spot-check tools(Uiprobe / OverlayQA / Loupe …) | Parity | |
|---|---|---|
| Who runs it | A person opens the tool, clicks around, reads a report | CI runs it, nobody has to be present |
| After a gap is found | File a ticket yourself, or paste the result into the PR | Exit code blocks the PR + a bot comment that updates in place |
| Regression gating | None | A baseline gates only new/worsened gaps — projects with existing debt can adopt it today |
| When there is no Figma file | Unusable | Snapshot mode: freeze today's rendering as the baseline so refactors cannot drift |
| Desktop apps | Not supported | Electron (attaches to the live window over CDP) |
| Price | $39–249 / month (Loupe is free but is a desktop GUI — not automatable) | Free, MIT |
Being honest about the boundaries: localhost and authenticated pages are no longer a differentiator — Uiprobe and OverlayQA support them too. Neither is "free and open source" — Loupe is also MIT. What is unique to Parity is the intersection: free × automatable.
Report UI (parity serve --watch) — the left column lists every node, matching or not;
the right side overlays the design boxes and the implementation boxes directly on the real
screenshot. Click any row to locate it; saving a file triggers a re-scan.
Fidelity report — each row is a concrete "expected → actual" pair with its severity and a suggested fix (including the matching design token). The GitHub Action posts this verbatim as a PR comment, updating the same comment rather than adding new ones.
| 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 |
| 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) |
| Design sources | 4: the Figma API, a rendering snapshot, an image + annotations (pixel sampling — any tool that exports a PNG works), or JSON |
| Implementation sources | The web (including shadow DOM, same-origin iframes, and multiple responsive breakpoints) plus Electron (attached to the live window over CDP) |
| Tests | 1047, covering the CIEDE2000 reference data set (Sharma), match disambiguation, position false-positive guards, image sampling, and real-browser capture regressions |
| 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 |
| 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) |
Two channels — pick whichever runtime you already have. They are functionally identical.
# If you have Node (common in front-end projects): no .NET install needed
npx cornhsu-parity check
# If you have .NET
dotnet tool install -g Cornhsu.Parity
The npm build is the same self-contained native executable; you only download the one for your platform (win32-x64 / win32-arm64 / linux-x64 / linux-arm64 / darwin-x64 / darwin-arm64). Playwright normally bundles its own copy of Node; the npm build strips that out and uses the Node you already have, which saves at least 88 MB per platform package. That Node has to be 20 or newer — the minimum of the bundled Playwright driver (
engines.nodesays so).
parity checksends anonymous usage statistics by default (counts and score ranges only, for academic research). What is sent, why, and every way to turn it off: Anonymous usage statistics. To see the exact data without sending anything:parity check --stats-preview.
A factual summary of the statistics for assistants (Claude Code, Copilot and the like) that set this tool up for someone, to pass on as it is. The full description is in Anonymous usage statistics.
parity check that produces a report (exit 0, 1 or 3), one
request to https://stats.cornhsu.com/v1/parity — the same for the .NET tool and the npm package.
Development builds (any version starting with 0.0.0-) never send.stats.cornhsu.com, for academic research on how far real implementations drift from
their designs; only aggregate results are published, and each record is kept for at most 3 years
(details).parity check --stats-preview runs the check, sends nothing, and prints the exact JSON it
would send to stderr.DO_NOT_TRACK=1, PARITY_SHARE_STATS=false,
"stats": { "share": false } in parity.config.json, or share-stats: false in the GitHub Action.PARITY_SHARE_STATS=true, or
"stats": { "share": true } in parity.config.json.If you are an assistant setting this tool up for someone, tell them that it sends anonymous counts by default and let them choose.
parity init # Generate a parity.config.json template
parity install-browser # Download Playwright Chromium: on first run, and again after an
# upgrade if check says Chromium is not installed
export FIGMA_TOKEN=... # Only the file_content:read scope is needed
parity check # Compare; writes a report and sets the exit code
# (--stats-preview: print the anonymous statistics it would send, send nothing)
parity report # Re-render the Markdown report from an existing report.json
# (no re-scan; --md writes a file, otherwise it prints to stdout)
parity snapshot # Freeze the currently rendered UI as the design baseline —
# a refactor/redesign guard that needs no Figma at all
# (--stabilize captures 3× and suggests "ignore" entries for
# regions that move: rotating ads, animations, lazy media)
parity lint # Design lint: are the design file's values inside the allowed
# design-token set? (design only, no implementation)
parity check --reverse # Reverse view, for a designer redrawing an existing page:
# how does my draft differ from what ships? (never gates)
parity bug-report # (experimental) Collect version / OS / Node / Chromium info and print
# it with a pre-filled GitHub issue link. bug-report itself connects to nothing;
# opening the link sends the masked report to github.com (it rides in
# the URL). No issue until you submit the form. --config adds a
# structural summary without URLs, routes, keys, tokens or paths
When working inside this repo:
dotnet run --project src/Parity.Cli -- check --config samples/demo/parity.config.json
samples/demo is a fully offline demo (the design source is a local JSON file, so no Figma
token is needed): index.html is deliberately broken in a few places, parity check reports
each gap and the unmatched list precisely, and returns exit code 1 (GATE FAIL) because of the
serious-severity gaps — which is exactly the behaviour you want from a CI gate.
parity.config.json{
"figmaFileKey": "abcd1234",
"designToken": "env:FIGMA_TOKEN",
"mapFile": "parity.map.json", // manual fallback mapping (layer name → selector)
"tokensFile": "tokens.json", // optional: design tokens, a flat object of string values
// ({"brand": "#2563EB", "space-4": "16px"}; // comments ok);
// suggested fixes will name the matching token
"targets": [
{ "route": "/", "frame": "10:2", "url": "http://localhost:8080/" }
],
"compare": { "position": "relative" }, // relative = compare relative position (default); none = skip position
"tolerances": { "sizePx": 2, "spacingPx": 2, "colorDeltaE": 2.0, "fontSizePx": 0.5, "positionPx": 4 },
"ignore": ["[data-parity-ignore]"],
"gate": {
"failOn": ["critical", "serious"], // any of minor / medium / serious / critical, one per string
"minMatchRate": 0 // optional: below this match rate (0–1) → exit 3, see below; 0 = no threshold
},
"stats": { "share": true } // optional: false turns anonymous usage statistics off;
// see "Anonymous usage statistics" below (also projectId)
}
Besides gaps, the gate also validates match integrity: zero matches (or zero design nodes) always fails, with exit code 3 (
MATCH INTEGRITY TOO LOW) rather than 1 — with nothing matched there are no gaps to catch, so a silent PASS would be a false pass, usually caused by a wrong url or frame. A match rate belowgate.minMatchRatefails the same way, with exit code 3.--baselinemode is not exempt;--reversenever gates and always exits 0.
target.url decides what the implementation side is; the commands and the report are identical:
| url form | What it means |
|---|---|
http(s)://… | A normal web page, or internal staging |
file://… (or a relative path) | A local HTML file |
cdp:http://host:port | An already-running Electron desktop app (reads the DOM of the live window) |
For Electron, start the app with a remote debugging port and point url at it. Parity attaches
and reads the current screen — it does not navigate, and does not disturb the app:
electron . --remote-debugging-port=9222 # your app, with this one extra flag
{ "route": "/", "frame": "20:5", "url": "cdp:http://localhost:9222" }
Why Electron is nearly free: its UI is a Chromium renderer, so it uses the same DOM/CSS measurement path as the web. Mobile native, Flutter and native desktop do not go through a DOM, so they are left for v2.0.
| Design source | config | Who it is for |
|---|---|---|
| Figma (primary) | figmaFileKey + FIGMA_TOKEN | The normal workflow, when you have a Figma file |
Snapshot (parity snapshot) | designFile pointing at the generated snapshot JSON | Refactor/redesign guard: today's rendering is correct, so freeze it as the baseline and later checks prove nothing drifted (the numeric version of visual regression). Matching goes through selector identity, so it is 100% deterministic — randomized ids (React useId, CSS-in-JS) are detected at capture time and replaced with structural paths, so they survive regeneration; purely alphabetic random ids (the kind character heuristics cannot see) are caught empirically: snapshot loads the page twice, ids that differ between loads are never used as anchors, and the measured stable-id allowlist is stored in the snapshot for check to anchor the same way. Baselines taken by older versions may list such nodes as unmatched (reason randomized-id); re-run parity snapshot once to migrate |
| An image + annotations | designImage (PNG/JPG) + designFile (annotations) | When all you have is a picture: an outsourced PNG, or a legacy project down to screenshots. This is also the path for XD / Sketch / Photoshop and anything else that can export an image — a universal adapter. The annotations are DesignNode JSON, and fill may be omitted: the engine samples the color from the corresponding region of the image. (Text color is deliberately not sampled — anti-aliasing blends it and the reading is unreliable — but you can fill it in by hand.) |
| Hand-written JSON | designFile | Offline demos and tests |
# The three-step refactor guard:
parity snapshot # 1. Freeze the current rendering (writes parity.snapshot.json + a reference screenshot)
# Point designFile at it in the config, and set each target.frame to its route
parity check # 2. Refactor fearlessly; 3. check proves it still matches the snapshot
Everything below was measured on public design-system files (IBM Carbon, Ant Design, Wikimedia Codex — method and full findings in docs/野生實查-2026-08-11-路線B-figma方言.md):
parity.map.json directly (measured on Codex: all 129 Button variants use placeholder text; one map line completed the pairing).Retry-After comes back counted in days (hit twice during testing: 4.3 and 3.7 days). Frames you have fetched live on in .parity/cache, so reruns and offline compares cost nothing — the quota only goes to frames you have never fetched.Only the things that should hold no matter how the layout flows:
itemSpacing against the real
gap between child elements.offsetX/offsetY): for children of freely positioned (non-auto-layout)
containers, the offset relative to the nearest reliable sibling or parent edge — which is what
catches "size and color are perfect, but it is in the wrong place." Only reliable edges are used
as references (TEXT/HUG boxes are never references, TEXT is never a target, and Y is honestly
skipped when everything above is text), so line-height drift in a flowing layout does not
produce false positives. Turn it off with compare.position: "none".tolerances.fontSizePx,
line-height within tolerances.sizePx; weight has no tolerance — any difference is a gap
(under 200 apart is medium, 200 or more is serious). font-family is a soft gap: it is
reported (severity minor, marked soft) but costs no fidelity points and never gates —
not even when failOn includes minor, and --baseline mode does not count it as new.rgb(37 99 235 / .5), color(srgb …), oklch(), color(display-p3 …)).When the design source is Figma, layer names in the report (the Markdown written by
check --mdor re-rendered byparity report, and the local UI) link back to that node in Figma — a designer clicks once and lands on the layer. WhendesignFileis set it wins overfigmaFileKey, so a snapshot- or JSON-based report carries no Figma links.
Keeping a new page inside the design system: parity lint. It looks only at the design side
and checks whether each node's values are in the allowed set from tokensFile: color (a hit
within the ΔE tolerance passes), and fontSize / padding / itemSpacing / cornerRadius (equal to
any size token passes — spacing, font sizes and corner radii share one scale). Every violation
carries "the nearest token", so the message is "change it to this", not merely "you are wrong".
Violations exit 1, so it can run in CI. Dimensions you have not defined tokens for are not linted.
Redrawing against what ships: parity check --reverse. The direction flips: "expected"
becomes the current implementation and "actual" becomes your draft. That gives a designer a list
of "how my draft differs from what is live today". It never gates, and always exits 0.
One URL, several breakpoints, each mapped to its own Figma frame — the render viewport is the frame's size, so a 375-wide phone frame makes your media queries fire naturally:
"targets": [
{ "route": "/desktop", "frame": "10:2", "url": "http://localhost:8080/" },
{ "route": "/mobile", "frame": "10:99", "url": "http://localhost:8080/" } // a 375-wide phone frame
]
route is only a label in the report, so pick something recognizable. You only need width /
height on a target when the frame's size is not the viewport you want to test. Each one
overrides its own axis: width alone keeps the default height of 800, height alone keeps the
frame's width.
Capture walks the composed tree: open shadow roots, content slotted through <slot>, and
same-origin iframes (including srcdoc) are all visible and all compared — web-component sites
are no longer wholly invisible. Selectors for elements inside a shadow root or iframe are written
as host >>> inner path.
Limitations, stated honestly: closed shadow roots and cross-origin iframes are unreachable and
are skipped; map-file selectors cannot reach inside a shadow root (the data-parity attribute is
unaffected and works as usual).
data-testid when there is no
aria-label): CTA Button matches class="cta-button" automaticallydata-parity="Layer Name" in your HTML, or write
{ "Layer Name": "CSS selector" } in parity.map.jsonOne engine (Parity.Engine, a pure library) plus several shells. The engine only ever compares
two normalized trees:
IDesignSource ──→ DesignNode tree ─┐
(Figma / JSON / later XD, Sketch) ├─→ Normalizer → Matcher → DiffEngine → FidelityReport
IImplementationSource → RenderedNode ┘
(Playwright / later WPF, desktop)
src/Parity.Engine/ The engine; FidelityEngine is the only entry point
DesignSources/ IDesignSource + Figma (REST + local cache) / Json
ImplementationSources/ IImplementationSource + Web (Playwright)
Comparison/ Normalizer / Matcher / DiffEngine / ColorDelta (CIEDE2000)
src/Parity.Report/ The report contract: report.json types, ReportWire, the fidelity score,
baseline comparison — no package dependencies, so no Playwright
src/Parity.Cli/ The dotnet tool shell: parity check / init / install-browser,
the local report UI (parity serve) and parity push (experimental)
src/Parity.Storage/ Baseline history (EF Core + SQLite), behind parity baseline
src/Parity.Server/ Team dashboard shell (ASP.NET Core) — not published to NuGet
src/Parity.Server.Data/ Its database (EF Core + SQLite, migrations from day one)
web/ The dashboard front end (React + Vite + TypeScript); the build
output lands in src/Parity.Server/wwwroot
tests/Parity.Tests/ Unit tests (including the CIEDE2000 reference data)
samples/demo/ Offline demo: a deliberately broken page + a design JSON
There are three shells, and the engine does not know that any of them exist:
Parity.Cli) — parity check in a terminal or in CI. This is the only shell
published as a package (NuGet / npm / the GitHub Action).parity serve) — a zero-build SPA bound to 127.0.0.1, for the
person who is actually fixing the gaps.Parity.Server + web/) — somewhere the reports can accumulate, so
that trends exist and so that people who never touch the tool have a place to look. You
deploy it yourself; it is not on NuGet, and it is still in development (see M7 below).Where the dashboard keeps its data. The web host and the admin commands (
create-project,create-invite) find the SQLite database the same way:ConnectionStrings:parity(fromappsettings.jsonor theConnectionStrings__parityenvironment variable), thenPARITY_SERVER_DB(a file path), thenparity-server.dbin the working directory. This is the reverse of .NET's usual "environment beats appsettings": a connection string inappsettings.jsonwins overPARITY_SERVER_DB— to override it from the environment, setConnectionStrings__parity. The reason: an explicit connection string is the more specific setting, and the integration tests inject one that a developer'sPARITY_SERVER_DBmust not redirect.
The same server also takes reports from XamlContrast
(the static WCAG contrast audit for XAML apps): xamlcontrast --push <report.json> sends the file
its --json option wrote, through the same POST /api/runs, marked with an X-Parity-Tool: xamlcontrast
header (parity push sends no such header, which the server reads as parity). A project can hold runs
from both tools. XamlContrast runs get their own overview card per project (pairs below AA, fail · warn,
coverage), a fail/warn trend, and an issue list (file and line, element, foreground and background as
written in the XAML, the ratio in each theme, the threshold). Each XamlContrast run also shows what changed
since the previous XamlContrast run on the same branch — new, worse, fixed and improved pairs — compared the
way XamlContrast's own --baseline does (a pair is file, element, foreground and background, without the line
number; more places or a lower worst ratio is worse). Like parity push, the XamlContrast side is
experimental until the dashboard is publicly deployed.
The repo root has a Dockerfile. The image contains the server, its data layer and the report types,
and nothing else: no browser, no engine (CI checks this).
docker build -t parity-server .
docker run -d --name parity -p 8080:8080 -v parity-data:/data \
-e Parity__PublicUrl=https://parity.example.com \
-e ASPNETCORE_FORWARDEDHEADERS_ENABLED=true \
parity-server
docker exec parity dotnet Parity.Server.dll create-project my-site # prints the CI token once
docker exec parity dotnet Parity.Server.dll create-invite my-site [email protected] owner
| Setting | What it does |
|---|---|
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. |
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. |
PARITY_SERVER_DB | The SQLite database. Preset to /data/parity-server.db in the image. |
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. |
/data: mount a volume there. TLS is the job of the proxy or platform in front.docker exec parity dotnet Parity.Server.dll backup /data/backup-2026-10-26.db writes a
consistent copy while the server keeps running (SQLite VACUUM INTO; it never overwrites a file and
never creates a missing database). Copy it off the machine. To restore: stop the server, put the copy
where the database was, start it again.GET /healthz is anonymous and answers ok only when the database answers, 503 otherwise.Parity.Server's only endpoint that takes in scan results is POST /api/runs (its other
write endpoints only manage sign-in, invites, members, tokens and run deletion), and what it
accepts is a report that has already finished running. There is no "give me a URL and I will go scan it"
endpoint, and there will not be one: scanning always happens on your own machine or in your
CI, where the page, the credentials and the localhost port already are.
This is not an implementation detail — it is the security claim of the whole design. It is why
the dashboard has no SSRF surface at all: it is never handed a URL to fetch. And it is
enforced structurally rather than by policy — the server projects do not reference the engine
at all, only Parity.Report (the report types, which have no package dependencies), so neither
Playwright nor its driver is anywhere in the server's build output. A test walks the server's
whole project-reference graph and turns CI red if any project in it references Playwright or
the engine; another turns it red the moment server code references a Playwright type or the
engine's capture side (FidelityEngine, IImplementationSource, or anything in
ImplementationSources.Web, which is the code that opens Chromium).
parity serve --watch # http://127.0.0.1:4321, re-scans on file changes (live updates over SSE)
parity map # Interactive matching: click an unmatched layer → click the element on the
# screenshot → writes parity.map.json
Gating CI is the whole differentiator, and this repo is itself a composite action:
# .github/workflows/design-check.yml (in your project)
name: Design fidelity
on: [pull_request]
jobs:
parity:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write # lets the action post the fidelity report as a PR comment
steps:
- uses: actions/checkout@v7
# Start your site (or deploy to a preview URL and point the config's url at it)
- run: |
npm ci && npm run build
npm run preview & # e.g. serving on localhost:8080
- uses: HSU-YU-MING/cornhsu-parity@v2 # @v2 tracks the latest 2.x
with:
config: parity.config.json
figma-token: ${{ secrets.FIGMA_TOKEN }} # omit when the design source is a local JSON file
Behaviour:
gate.failOn → exit 1 → red PR. (The comment is posted
first, then the gate is enforced.)report.json plus the Markdown report are uploaded for download (artifact
name parity-report unless you set artifact-name; report.json and parity-report.md at its
root). report.json is picked up from .parity/report.json next to the config file — where
check writes it — even when config points into a subdirectory.artifact-name (e.g. parity-report-${{ matrix.site }}). Each name gets its
own artifact and keeps its own PR comment; two calls with the same name collide on the artifact upload.
Two calls in one job work only while the first one passes: when its gate fails, the job stops at
that call's last step and the second call never runs (no report, no comment, no artifact for it).share-stats: false turns them off. The job summary gets one line saying whether they were sent, or why not.Action inputs: config / target / working-directory / version / figma-token /
baseline (regression mode) / comment (turn off the PR comment) / upload-report /
artifact-name (default parity-report; letters, digits, ., _, -, at most 100 characters, not starting with - and not only dots) /
share-stats (default true; exactly true or false — anything else stops the action before it installs Parity).
The action installs the tool with
dotnet tool install -g Cornhsu.Parity(published on nuget.org) — the latest 2.x whenversionis omitted, so@v2never jumps to a future 3.x, or exactly theversionyou pass. Within one job the tool is installed once per call into the same global location, and a later call cannot install a lower version than an earlier one (dotnet toolrefuses to downgrade) — e.g. a first call withoutversioninstalls the latest 2.x, and a second call withversion: 2.0.0fails. Give every call in a job the sameversion, or use a matrix. This repo's own.github/workflows/ci.ymlinstead builds from source and runs the offline demo as a self-check, so it does not depend on a published release.Upgrading from
@v1?v1stays at 1.1.0, whose action installs the newest release of any major whenversionis omitted — so an unpinned@v1workflow already runs 2.x. Switch to@v2and work through the "upgrade required" list in the CHANGELOG's 2.0.0 entry, or setversion: 1.1.0to stay on 1.x for now.
report.json)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.severity — closed set (spelling and membership frozen): none / minor / medium / serious / critical. status: mismatch / missing.unmatched[].reason — open set: parsers must tolerate unknown values; known spellings never change. Currently: no-anchor, ambiguous-or-missing-text, randomized-id, size-implausible.nodes[].matchedBy — open set, same rule. Currently: selector, explicit, auto-text, auto-name, auto-container.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.A project that already has a pile of gaps cannot start at "zero gaps or no merge". A baseline lets you gate only new and worsened gaps:
parity baseline save # Store the current gaps + fidelity score as a baseline snapshot
# (SQLite, in parity.baseline.db, tagged with the git commit)
parity check --baseline # Compare against the newest baseline: only new or worsened gaps GATE FAIL,
# and the score trend is shown (baseline 75 → now 83 ↑)
parity baseline list # Browse snapshot history (the score column is the fidelity trend, for PMs)
To use
--baselinein CI, remember togit add parity.baseline.db. It deliberately lives at the repo root rather than in.parity/, which is usually gitignored — otherwise CI cannot find the baseline and silently falls back to the normal gate. The path is configurable viabaselineFile.
baseline save, then run check --baseline in
CI, and all the team has to commit to is "do not make fidelity worse"The storage layer is a separate Parity.Storage (EF Core + SQLite, with Pooling=False so the
file is released immediately), and the engine's BaselineComparer is a pure, unit-testable function.
parity serve --watch, Kestrel bound to 127.0.0.1) + parity map interactive matchingaction.yml) + this repo's own CI (build / test / offline-demo self-check)ImageDesignSource (image + annotations + pixel sampling) + parity snapshot (freeze the current state as the baseline)Parity.Server + Parity.Server.Data + web/) — in
progress, not yet released. parity push (experimental: the server is not publicly deployed
yet, so the command and its server protocol may change in a 2.x minor release and join the interface
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
measured coordinates (no screenshot upload), an overview and score trend per project and
route, and accounts / project roles / invite links. It also takes XamlContrast reports
(one dashboard, two tools). Its own internal M1–M4.6 are done; of its
M5, the deployment groundwork is done (a Docker image, the public URL, persisted sign-in keys,
online backups), the actual hosting (a host, a domain, TLS) is not. — see the iron rule above.Open work, known blind spots and the next priorities are in ROADMAP.md; version history is in CHANGELOG.md.
From 2.1.0, parity check sends anonymous usage statistics, once per run — on by default, in CI
too. They are counts and score ranges used for academic research on how far real
implementations drift from their designs; only aggregate results are published. The public data page — what is collected, what it is used
for, how long it is kept — is https://stats.cornhsu.com/.
When. Only parity check, and only when it produced a report (exit 0, 1 or 3). --reverse,
--stats-preview, every other command and any run that ends in exit 2 send nothing.
Everything that is sent:
| Field | Meaning |
|---|---|
schema, tool | Always 1 and "parity" |
toolVersion | The Parity version, e.g. 2.1.0 (no commit hash) |
os | windows, linux, macos or other |
ci | Whether CI or GITHUB_ACTIONS is true |
projectId | null, unless you set stats.projectId yourself (see below) |
data.score | The fidelity score check printed (0–100) |
data.gateFailed | Whether check's exit code was not 0 (so --baseline and exit 3 count as they did in your CI) |
data.pages | How many targets the report covers |
data.designSource | figma, snapshot (a file written by parity snapshot), file (any other design JSON) or image |
data.designNodes, data.matched | Design nodes and matched nodes, summed over all pages |
data.severity | Gap counts per severity: critical, serious, medium, minor |
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 |
Never sent: URLs, routes, page or layer names, selectors, file names or paths, screenshots, colour values, design content, tokens, repository names or IDs, user names, machine names. The request has no User-Agent and no cookies. The collector does not store IP addresses or the time a record arrived (only its date); Cloudflare, which hosts the collector, sees the connection address at its edge, as any network provider does.
No ID unless you add one. Without stats.projectId, nothing links one run to another or to you —
which also means a record cannot be picked out and deleted afterwards. If you want your runs counted as
one project (and deletable on request), make up a random ID — for example openssl rand -hex 12, or
[guid]::NewGuid().ToString('N') in PowerShell. Parity never derives one from your repository, paths or
machine, and an ID that is not 16–64 characters of A–Z a–z 0–9 _ - is a config error (exit 2):
"stats": {
"share": true, // false turns statistics off
"projectId": "REPLACE-with-your-own-random-id" // optional; never copy someone else's
}
Preview. parity check --stats-preview runs the check as usual, sends nothing, and prints the exact
JSON to stderr, followed by whether it would be sent — or why not. The exit code, stdout and the report
files are unchanged.
Turning it off. Any one of these is enough; they are checked in this order, and the first that says no wins:
DO_NOT_TRACK set to anything except 0 or false — always wins.CORNHSU_SHARE_STATS (shared with XamlContrast)
or PARITY_SHARE_STATS set to false — in fact any value other than 1, true, yes or on, so a
typo errs on the side of not sending."stats": { "share": false } in parity.config.json.share-stats: false (it sets PARITY_SHARE_STATS=false).0.0.0-, such as 0.0.0-dev) never send.Like every other key in the config, a misspelt one is ignored: "stats": { "shrae": false } turns
nothing off. --stats-preview tells you whether a switch took effect.
Notice. The first time check sends on a machine, it prints a short notice to stderr and leaves a
marker file, cornhsu/parity-stats-notice in your user settings directory (%APPDATA% on Windows,
~/.config elsewhere), so the notice is not repeated — delete the file to see it again. In CI it prints
one line every time statistics were sent. Nothing is printed when nothing is sent, and nothing ever goes
to stdout. The GitHub Action also writes one line to the job summary: sent, or why not.
Cost. The request waits at most 1.5 s to connect and 3 s in total, is never retried and never follows a redirect. When it fails — offline, a firewall, the collector is down — nothing is printed and nothing else changes: the exit code, stdout, the report files and the PR comment are exactly what they would have been.
parity push, and stats.cornhsu.com for the anonymous usage statistics
of parity check (unless turned off). The page under test is opened by the local browser. Nothing else.env:FIGMA_TOKEN); it never reaches a log
and never goes in a URL (it is sent as the X-Figma-Token header)..parity/cache (add .parity/ to your .gitignore), so re-runs do
not hit Figma and can compare offline.Host headers
(DNS rebinding) and cross-origin POSTs (CSRF).設計還原度怎麼自動檢查 (in Traditional Chinese) — background on the whole category rather than this tool: what overlays, pixel regression and property-level comparison each actually answer, why colour needs CIEDE2000 instead of hex equality, why absolute coordinates cannot be compared at all, and where this class of tool does not help (SVG paths, font rasterisation, shadows, image cropping). It names the other tools in the space and says when to pick one of them over this one.
XamlContrast — a sibling project by the
same author, built on the same philosophy: numeric checks that gate CI. Parity answers "does the
implementation match the design?" (fidelity); XamlContrast answers "can anyone actually see
what we built?" (WCAG contrast, scanned statically from XAML source). One guards fidelity, the
other readability — and its static color resolution is also the groundwork for the expected values
of this project's future WpfImplementationSource desktop adapter (see §4.5 of the
project plan, in Traditional Chinese).
MIT. The NuGet and npm packages also bundle third-party components (Playwright, ImageSharp, SQLite and others; the npm packages also include the .NET runtime). Their licenses are listed in THIRD-PARTY-NOTICES.md, which ships inside the NuGet package and every npm platform package.