@design-parity/diff 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -0
- package/dist/checks.d.ts +36 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +22 -0
- package/dist/checks.js.map +1 -0
- package/dist/config.d.ts +44 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +27 -0
- package/dist/config.js.map +1 -0
- package/dist/diff.d.ts +48 -0
- package/dist/diff.d.ts.map +1 -0
- package/dist/diff.js +152 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/semantic.d.ts +16 -0
- package/dist/semantic.d.ts.map +1 -0
- package/dist/semantic.js +49 -0
- package/dist/semantic.js.map +1 -0
- package/dist/summary.d.ts +11 -0
- package/dist/summary.d.ts.map +1 -0
- package/dist/summary.js +38 -0
- package/dist/summary.js.map +1 -0
- package/dist/tokens.d.ts +21 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +102 -0
- package/dist/tokens.js.map +1 -0
- package/dist/visual.d.ts +44 -0
- package/dist/visual.d.ts.map +1 -0
- package/dist/visual.js +156 -0
- package/dist/visual.js.map +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @design-parity/diff
|
|
2
|
+
|
|
3
|
+
The source-agnostic diff engine. Consume a `(DesignReference, CandidateRender)`
|
|
4
|
+
pair — from any adapter plus the candidate renderer — and emit a deterministic
|
|
5
|
+
[`Verdict`](../core/src/types.ts), a markdown summary, and a
|
|
6
|
+
reference/candidate/diff triptych per image.
|
|
7
|
+
|
|
8
|
+
Per [docs/PRINCIPLES.md](../../docs/PRINCIPLES.md): the engine runs **only
|
|
9
|
+
committed, deterministic rules/config — no model calls at run time**, and the
|
|
10
|
+
verdict **leads with a11y + i18n**, then token compliance, then the raw visual
|
|
11
|
+
diff.
|
|
12
|
+
|
|
13
|
+
## Use
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { diff } from "@design-parity/diff";
|
|
17
|
+
|
|
18
|
+
const { verdict, summary, triptychs } = await diff(reference, candidate, {
|
|
19
|
+
repoRoot, // image `uri`s resolve against this (default: cwd)
|
|
20
|
+
outDir: "out", // optional: write triptych-<key>.png here
|
|
21
|
+
// config: { spacingTolerance: 0, pixelThreshold: 0.05, ... },
|
|
22
|
+
// checksConfig: { contrastLevel: "AAA" }, // a11y/i18n thresholds
|
|
23
|
+
// checks: myProvider, // swap the default @design-parity/checks provider
|
|
24
|
+
});
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
`verdict.status` is `pass | warn | fail` (any `error` finding ⇒ `fail`); findings
|
|
28
|
+
are ordered a11y → token → semantic → visual; `verdict.visualScores` maps each
|
|
29
|
+
`state/theme/size` key to its differing-pixel fraction.
|
|
30
|
+
|
|
31
|
+
## Dimensions
|
|
32
|
+
|
|
33
|
+
- **a11y + i18n** — delegated to [`@design-parity/checks`](../checks) (#10)
|
|
34
|
+
through a [`ChecksProvider`](./src/checks.ts) seam: WCAG contrast, touch
|
|
35
|
+
targets, semantic roles/labels, and i18n risks. `defaultChecks` wires the real
|
|
36
|
+
package; inject a custom provider (or `checksConfig`) to override.
|
|
37
|
+
- **Token compliance** — flatten the candidate's tree tokens and compare against
|
|
38
|
+
the reference spec: numeric tokens honour a committed tolerance, colours and
|
|
39
|
+
typography must match exactly.
|
|
40
|
+
- **Semantics** — theme-coverage and structural (role/label) deltas.
|
|
41
|
+
- **Visual** — per-pixel diff via `pixelmatch`, plus the triptych.
|
|
42
|
+
|
|
43
|
+
Everything is committed config (see [`config.ts`](./src/config.ts)); the same
|
|
44
|
+
pair always yields the same verdict.
|
|
45
|
+
|
|
46
|
+
## Develop
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
npm run build --workspace @design-parity/diff
|
|
50
|
+
npm test --workspace @design-parity/diff
|
|
51
|
+
```
|
package/dist/checks.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility + i18n checks — the highest-value signal (Principle 2), so the
|
|
3
|
+
* verdict leads with them.
|
|
4
|
+
*
|
|
5
|
+
* These checks are owned by `@design-parity/checks` (issue #10), which has
|
|
6
|
+
* landed: the {@link defaultChecks} provider delegates straight to its
|
|
7
|
+
* `runChecks`. The {@link ChecksProvider} seam stays so a consumer can inject a
|
|
8
|
+
* stricter config or a future provider without the diff engine changing — but
|
|
9
|
+
* the default is the real package, not a stub.
|
|
10
|
+
*/
|
|
11
|
+
import { type ChecksConfig } from "@design-parity/checks";
|
|
12
|
+
import type { CandidateRender, DesignReference, Finding } from "@design-parity/core";
|
|
13
|
+
/** What an a11y/i18n check provider receives. Source-agnostic by construction. */
|
|
14
|
+
export interface ChecksInput {
|
|
15
|
+
reference: DesignReference;
|
|
16
|
+
candidate: CandidateRender;
|
|
17
|
+
/** Committed thresholds/levels passed through to the provider. */
|
|
18
|
+
config: ChecksConfig;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The a11y + i18n seam. An implementation inspects the candidate (never a live
|
|
22
|
+
* model) and returns deterministic findings. Async is allowed so a heavier
|
|
23
|
+
* provider can read files, but it must not depend on network or a model at run
|
|
24
|
+
* time.
|
|
25
|
+
*/
|
|
26
|
+
export interface ChecksProvider {
|
|
27
|
+
run(input: ChecksInput): Finding[] | Promise<Finding[]>;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The default provider: `@design-parity/checks` (#10). Deterministic, offline,
|
|
31
|
+
* and committed — WCAG contrast, touch targets, semantic roles/labels, plus the
|
|
32
|
+
* i18n risks (text expansion, RTL, hardcoded strings).
|
|
33
|
+
*/
|
|
34
|
+
export declare const defaultChecks: ChecksProvider;
|
|
35
|
+
export type { ChecksConfig };
|
|
36
|
+
//# sourceMappingURL=checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.d.ts","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAa,KAAK,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrE,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,OAAO,EACR,MAAM,qBAAqB,CAAC;AAE7B,kFAAkF;AAClF,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,eAAe,CAAC;IAC3B,SAAS,EAAE,eAAe,CAAC;IAC3B,kEAAkE;IAClE,MAAM,EAAE,YAAY,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,GAAG,CAAC,KAAK,EAAE,WAAW,GAAG,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC;CACzD;AAED;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,cAI3B,CAAC;AAEF,YAAY,EAAE,YAAY,EAAE,CAAC"}
|
package/dist/checks.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Accessibility + i18n checks — the highest-value signal (Principle 2), so the
|
|
3
|
+
* verdict leads with them.
|
|
4
|
+
*
|
|
5
|
+
* These checks are owned by `@design-parity/checks` (issue #10), which has
|
|
6
|
+
* landed: the {@link defaultChecks} provider delegates straight to its
|
|
7
|
+
* `runChecks`. The {@link ChecksProvider} seam stays so a consumer can inject a
|
|
8
|
+
* stricter config or a future provider without the diff engine changing — but
|
|
9
|
+
* the default is the real package, not a stub.
|
|
10
|
+
*/
|
|
11
|
+
import { runChecks } from "@design-parity/checks";
|
|
12
|
+
/**
|
|
13
|
+
* The default provider: `@design-parity/checks` (#10). Deterministic, offline,
|
|
14
|
+
* and committed — WCAG contrast, touch targets, semantic roles/labels, plus the
|
|
15
|
+
* i18n risks (text expansion, RTL, hardcoded strings).
|
|
16
|
+
*/
|
|
17
|
+
export const defaultChecks = {
|
|
18
|
+
run({ reference, candidate, config }) {
|
|
19
|
+
return runChecks(reference, candidate, config);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=checks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"checks.js","sourceRoot":"","sources":["../src/checks.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAqB,MAAM,uBAAuB,CAAC;AAyBrE;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAmB;IAC3C,GAAG,CAAC,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAe;QAC/C,OAAO,SAAS,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,CAAC;IACjD,CAAC;CACF,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff thresholds and rules.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/PRINCIPLES.md (Principle 1) every threshold the engine uses is
|
|
5
|
+
* committed config, never inferred at run time. A consumer overrides any field;
|
|
6
|
+
* the rest fall back to {@link defaultDiffConfig}. The same config in → the same
|
|
7
|
+
* verdict out.
|
|
8
|
+
*
|
|
9
|
+
* a11y + i18n thresholds (contrast levels, touch-target sizes, expansion
|
|
10
|
+
* factors) are *not* here — they belong to `@design-parity/checks` and are
|
|
11
|
+
* configured through {@link DiffOptions.checksConfig}.
|
|
12
|
+
*/
|
|
13
|
+
export interface DiffConfig {
|
|
14
|
+
/** Max allowed absolute delta (dp/px) for a spacing token before it fails. */
|
|
15
|
+
spacingTolerance: number;
|
|
16
|
+
/** Max allowed absolute delta (dp/px) for a radius token before it fails. */
|
|
17
|
+
radiusTolerance: number;
|
|
18
|
+
/**
|
|
19
|
+
* pixelmatch per-pixel matching threshold (0 strict … 1 loose). Pixels whose
|
|
20
|
+
* colour distance exceeds this fraction are counted as differing. Tighter
|
|
21
|
+
* than pixelmatch's 0.1 default so a visible theme-colour drift registers.
|
|
22
|
+
*/
|
|
23
|
+
pixelThreshold: number;
|
|
24
|
+
/**
|
|
25
|
+
* Fraction of differing pixels (0…1) above which an image pair raises a
|
|
26
|
+
* visual finding. Visual diff is the least-valuable signal (Principle 2), so
|
|
27
|
+
* it never escalates past `warn`.
|
|
28
|
+
*/
|
|
29
|
+
visualWarnRatio: number;
|
|
30
|
+
/**
|
|
31
|
+
* Max per-axis dimension delta (px) between reference and candidate that is
|
|
32
|
+
* still diffed as an aligned overlap rather than scored a 100% mismatch. Two
|
|
33
|
+
* render tools rounding density differently (e.g. Robolectric 2.625 vs a
|
|
34
|
+
* `deviceScaleFactor=2` capture) can differ by a pixel or two without any real
|
|
35
|
+
* visual drift; tolerating that keeps the heatmap informative (#47). A delta
|
|
36
|
+
* beyond this on either axis is a genuine total mismatch.
|
|
37
|
+
*/
|
|
38
|
+
visualDimTolerancePx: number;
|
|
39
|
+
}
|
|
40
|
+
/** The committed defaults: exact-match tokens, sensitive visual diff. */
|
|
41
|
+
export declare const defaultDiffConfig: DiffConfig;
|
|
42
|
+
/** Merge a partial override over the committed defaults. */
|
|
43
|
+
export declare function resolveConfig(override?: Partial<DiffConfig>): DiffConfig;
|
|
44
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,MAAM,WAAW,UAAU;IACzB,8EAA8E;IAC9E,gBAAgB,EAAE,MAAM,CAAC;IACzB,6EAA6E;IAC7E,eAAe,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,cAAc,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,eAAe,EAAE,MAAM,CAAC;IACxB;;;;;;;OAOG;IACH,oBAAoB,EAAE,MAAM,CAAC;CAC9B;AAED,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,EAAE,UAM/B,CAAC;AAEF,4DAA4D;AAC5D,wBAAgB,aAAa,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,UAAU,CAGxE"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Diff thresholds and rules.
|
|
3
|
+
*
|
|
4
|
+
* Per docs/PRINCIPLES.md (Principle 1) every threshold the engine uses is
|
|
5
|
+
* committed config, never inferred at run time. A consumer overrides any field;
|
|
6
|
+
* the rest fall back to {@link defaultDiffConfig}. The same config in → the same
|
|
7
|
+
* verdict out.
|
|
8
|
+
*
|
|
9
|
+
* a11y + i18n thresholds (contrast levels, touch-target sizes, expansion
|
|
10
|
+
* factors) are *not* here — they belong to `@design-parity/checks` and are
|
|
11
|
+
* configured through {@link DiffOptions.checksConfig}.
|
|
12
|
+
*/
|
|
13
|
+
/** The committed defaults: exact-match tokens, sensitive visual diff. */
|
|
14
|
+
export const defaultDiffConfig = {
|
|
15
|
+
spacingTolerance: 0,
|
|
16
|
+
radiusTolerance: 0,
|
|
17
|
+
pixelThreshold: 0.05,
|
|
18
|
+
visualWarnRatio: 0,
|
|
19
|
+
visualDimTolerancePx: 8,
|
|
20
|
+
};
|
|
21
|
+
/** Merge a partial override over the committed defaults. */
|
|
22
|
+
export function resolveConfig(override) {
|
|
23
|
+
if (!override)
|
|
24
|
+
return defaultDiffConfig;
|
|
25
|
+
return { ...defaultDiffConfig, ...override };
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AA8BH,yEAAyE;AACzE,MAAM,CAAC,MAAM,iBAAiB,GAAe;IAC3C,gBAAgB,EAAE,CAAC;IACnB,eAAe,EAAE,CAAC;IAClB,cAAc,EAAE,IAAI;IACpB,eAAe,EAAE,CAAC;IAClB,oBAAoB,EAAE,CAAC;CACxB,CAAC;AAEF,4DAA4D;AAC5D,MAAM,UAAU,aAAa,CAAC,QAA8B;IAC1D,IAAI,CAAC,QAAQ;QAAE,OAAO,iBAAiB,CAAC;IACxC,OAAO,EAAE,GAAG,iBAAiB,EAAE,GAAG,QAAQ,EAAE,CAAC;AAC/C,CAAC"}
|
package/dist/diff.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { CandidateRender, DesignReference, Verdict } from "@design-parity/core";
|
|
2
|
+
import { type ChecksConfig, type ChecksProvider } from "./checks.js";
|
|
3
|
+
import { type DiffConfig } from "./config.js";
|
|
4
|
+
export interface DiffOptions {
|
|
5
|
+
/** Repo root the image `uri`s resolve against. Defaults to `process.cwd()`. */
|
|
6
|
+
repoRoot?: string;
|
|
7
|
+
/** Threshold/rule overrides; unset fields use the committed defaults. */
|
|
8
|
+
config?: Partial<DiffConfig>;
|
|
9
|
+
/**
|
|
10
|
+
* a11y + i18n provider. Defaults to {@link defaultChecks}, which delegates to
|
|
11
|
+
* `@design-parity/checks` (#10). Inject to swap in a custom provider.
|
|
12
|
+
*/
|
|
13
|
+
checks?: ChecksProvider;
|
|
14
|
+
/** Committed a11y/i18n thresholds passed to the checks provider. */
|
|
15
|
+
checksConfig?: ChecksConfig;
|
|
16
|
+
/**
|
|
17
|
+
* If set, triptych PNGs are written here as `triptych-<key>.png` and their
|
|
18
|
+
* paths land in {@link DiffResult.triptychs}. Omit to skip disk writes; the
|
|
19
|
+
* buffers are still returned.
|
|
20
|
+
*/
|
|
21
|
+
outDir?: string;
|
|
22
|
+
}
|
|
23
|
+
/** A rendered triptych for one image pair. */
|
|
24
|
+
export interface Triptych {
|
|
25
|
+
/** `state/theme/size` key, shared with {@link Verdict.visualScores}. */
|
|
26
|
+
key: string;
|
|
27
|
+
png: Buffer;
|
|
28
|
+
/** Absolute path, present only when {@link DiffOptions.outDir} was set. */
|
|
29
|
+
path?: string;
|
|
30
|
+
/**
|
|
31
|
+
* The standalone diff heatmap PNG for this pair (the pixelmatch panel only),
|
|
32
|
+
* so a consumer laying out its own columns can inline it — e.g. the HTML
|
|
33
|
+
* report (#50). Absent when there was no aligned region to diff.
|
|
34
|
+
*/
|
|
35
|
+
diff?: Buffer;
|
|
36
|
+
}
|
|
37
|
+
export interface DiffResult {
|
|
38
|
+
verdict: Verdict;
|
|
39
|
+
/** Markdown summary for the PR surface. */
|
|
40
|
+
summary: string;
|
|
41
|
+
triptychs: Triptych[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Run the full parity diff. Resolves images, runs every dimension, orders the
|
|
45
|
+
* findings by value, and returns the verdict, its markdown, and the triptychs.
|
|
46
|
+
*/
|
|
47
|
+
export declare function diff(reference: DesignReference, candidate: CandidateRender, options?: DiffOptions): Promise<DiffResult>;
|
|
48
|
+
//# sourceMappingURL=diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.d.ts","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EAGf,OAAO,EAER,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,cAAc,EACpB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAiB,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAa7D,MAAM,WAAW,WAAW;IAC1B,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yEAAyE;IACzE,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC7B;;;OAGG;IACH,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB,oEAAoE;IACpE,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8CAA8C;AAC9C,MAAM,WAAW,QAAQ;IACvB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,QAAQ,EAAE,CAAC;CACvB;AAuDD;;;GAGG;AACH,wBAAsB,IAAI,CACxB,SAAS,EAAE,eAAe,EAC1B,SAAS,EAAE,eAAe,EAC1B,OAAO,GAAE,WAAgB,GACxB,OAAO,CAAC,UAAU,CAAC,CA6ErB"}
|
package/dist/diff.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The diff engine entry point: consume a `(DesignReference, CandidateRender)`
|
|
3
|
+
* pair and emit a deterministic {@link Verdict} plus a human summary and the
|
|
4
|
+
* visual triptychs.
|
|
5
|
+
*
|
|
6
|
+
* Findings are assembled in value order (Principle 2): a11y + i18n (the checks
|
|
7
|
+
* provider) → token compliance → semantics → visual. No network, no model, no
|
|
8
|
+
* clock — the same pair always yields the same verdict (Principle 1).
|
|
9
|
+
*/
|
|
10
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { defaultChecks, } from "./checks.js";
|
|
13
|
+
import { resolveConfig } from "./config.js";
|
|
14
|
+
import { diffSemantics } from "./semantic.js";
|
|
15
|
+
import { renderSummary } from "./summary.js";
|
|
16
|
+
import { collectTokens, diffTokens } from "./tokens.js";
|
|
17
|
+
import { diffImagePair, imageKey, looseKey, pairKey, sizeCompatible, } from "./visual.js";
|
|
18
|
+
/**
|
|
19
|
+
* Pair candidate images to reference images. Exact `state/theme/normalized-size`
|
|
20
|
+
* first (so `"Compact"`/`"600dp"`/`"compact"` line up); then a size-tolerant
|
|
21
|
+
* fallback by `state/theme` when one side omits/uses an unknown size. Two
|
|
22
|
+
* *different known* sizes never pair. Reference variants left over are reported
|
|
23
|
+
* rather than silently dropped.
|
|
24
|
+
*/
|
|
25
|
+
function pairImages(reference, candidate) {
|
|
26
|
+
const exact = new Map();
|
|
27
|
+
for (const i of candidate.images) {
|
|
28
|
+
if (!exact.has(pairKey(i)))
|
|
29
|
+
exact.set(pairKey(i), i);
|
|
30
|
+
}
|
|
31
|
+
const used = new Set();
|
|
32
|
+
const pairs = [];
|
|
33
|
+
const unmatched = [];
|
|
34
|
+
for (const ref of reference.referenceImages) {
|
|
35
|
+
let cand = exact.get(pairKey(ref));
|
|
36
|
+
if (cand && used.has(cand))
|
|
37
|
+
cand = undefined;
|
|
38
|
+
if (!cand) {
|
|
39
|
+
cand = candidate.images.find((c) => !used.has(c) &&
|
|
40
|
+
looseKey(c) === looseKey(ref) &&
|
|
41
|
+
sizeCompatible(ref, c));
|
|
42
|
+
}
|
|
43
|
+
if (cand) {
|
|
44
|
+
used.add(cand);
|
|
45
|
+
pairs.push({ reference: ref, candidate: cand });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
unmatched.push(ref);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { pairs, unmatched };
|
|
52
|
+
}
|
|
53
|
+
function statusFor(findings) {
|
|
54
|
+
if (findings.some((f) => f.severity === "error"))
|
|
55
|
+
return "fail";
|
|
56
|
+
if (findings.some((f) => f.severity === "warn"))
|
|
57
|
+
return "warn";
|
|
58
|
+
return "pass";
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Run the full parity diff. Resolves images, runs every dimension, orders the
|
|
62
|
+
* findings by value, and returns the verdict, its markdown, and the triptychs.
|
|
63
|
+
*/
|
|
64
|
+
export async function diff(reference, candidate, options = {}) {
|
|
65
|
+
const repoRoot = options.repoRoot ?? process.cwd();
|
|
66
|
+
const config = resolveConfig(options.config);
|
|
67
|
+
const checks = options.checks ?? defaultChecks;
|
|
68
|
+
const candidateTokens = collectTokens(candidate.semantics.root);
|
|
69
|
+
// 1. a11y + i18n (highest value, leads the verdict).
|
|
70
|
+
const a11y = await checks.run({
|
|
71
|
+
reference,
|
|
72
|
+
candidate,
|
|
73
|
+
config: options.checksConfig ?? {},
|
|
74
|
+
});
|
|
75
|
+
// 2. token compliance.
|
|
76
|
+
const tokens = diffTokens(reference.tokens, candidateTokens, config);
|
|
77
|
+
// 3. semantics (+ reference variants with no candidate counterpart).
|
|
78
|
+
const semantic = diffSemantics(reference, candidate);
|
|
79
|
+
const { pairs, unmatched } = pairImages(reference, candidate);
|
|
80
|
+
const candidateThemes = new Set(candidate.images.map((i) => i.theme).filter(Boolean));
|
|
81
|
+
for (const ref of unmatched) {
|
|
82
|
+
// A wholly-missing theme is already reported by diffSemantics; only flag
|
|
83
|
+
// finer gaps (a missing size/state within a theme the candidate renders).
|
|
84
|
+
if (ref.theme && !candidateThemes.has(ref.theme))
|
|
85
|
+
continue;
|
|
86
|
+
semantic.push({
|
|
87
|
+
kind: "semantic",
|
|
88
|
+
severity: "warn",
|
|
89
|
+
message: `reference variant '${imageKey(ref)}' has no candidate render to compare`,
|
|
90
|
+
detail: { variant: imageKey(ref) },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 4. visual diff (table stakes).
|
|
94
|
+
const visuals = [];
|
|
95
|
+
for (const pair of pairs) {
|
|
96
|
+
visuals.push(await diffImagePair(repoRoot, pair.reference, pair.candidate, config));
|
|
97
|
+
}
|
|
98
|
+
const visualScores = {};
|
|
99
|
+
const visualFindings = [];
|
|
100
|
+
for (const v of visuals) {
|
|
101
|
+
visualScores[v.key] = round(v.score);
|
|
102
|
+
if (v.dimensionMismatch) {
|
|
103
|
+
// The pair was diffed over its overlap after a sub-tolerance size delta;
|
|
104
|
+
// note it so the reviewer knows the score is an aligned comparison (#47).
|
|
105
|
+
visualFindings.push({
|
|
106
|
+
kind: "visual",
|
|
107
|
+
severity: "info",
|
|
108
|
+
message: `${v.key}: reference and candidate differ slightly in size; compared over their overlap`,
|
|
109
|
+
detail: { key: v.key },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (v.score > config.visualWarnRatio) {
|
|
113
|
+
visualFindings.push({
|
|
114
|
+
kind: "visual",
|
|
115
|
+
severity: "warn",
|
|
116
|
+
message: `${v.key}: ${(v.score * 100).toFixed(1)}% of pixels differ from reference`,
|
|
117
|
+
detail: { key: v.key, score: round(v.score), diffPixels: v.diffPixels },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const findings = [...a11y, ...tokens, ...semantic, ...visualFindings];
|
|
122
|
+
const verdict = {
|
|
123
|
+
componentId: candidate.componentId,
|
|
124
|
+
status: statusFor(findings),
|
|
125
|
+
findings,
|
|
126
|
+
visualScores,
|
|
127
|
+
};
|
|
128
|
+
const triptychs = await emitTriptychs(visuals, options.outDir);
|
|
129
|
+
return { verdict, summary: renderSummary(verdict), triptychs };
|
|
130
|
+
}
|
|
131
|
+
async function emitTriptychs(visuals, outDir) {
|
|
132
|
+
if (outDir)
|
|
133
|
+
await mkdir(outDir, { recursive: true });
|
|
134
|
+
const out = [];
|
|
135
|
+
for (const v of visuals) {
|
|
136
|
+
const safeKey = v.key.replace(/[^a-z0-9]+/gi, "-");
|
|
137
|
+
const triptych = { key: v.key, png: v.triptych };
|
|
138
|
+
if (v.diffPng)
|
|
139
|
+
triptych.diff = v.diffPng;
|
|
140
|
+
if (outDir) {
|
|
141
|
+
const path = join(outDir, `triptych-${safeKey}.png`);
|
|
142
|
+
await writeFile(path, v.triptych);
|
|
143
|
+
triptych.path = path;
|
|
144
|
+
}
|
|
145
|
+
out.push(triptych);
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
function round(n) {
|
|
150
|
+
return Math.round(n * 1e6) / 1e6;
|
|
151
|
+
}
|
|
152
|
+
//# sourceMappingURL=diff.js.map
|
package/dist/diff.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diff.js","sourceRoot":"","sources":["../src/diff.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@design-parity/diff` — the source-agnostic diff engine.
|
|
3
|
+
*
|
|
4
|
+
* Consume a `(DesignReference, CandidateRender)` pair (from any adapter +
|
|
5
|
+
* the candidate renderer) and emit a deterministic {@link Verdict}, a markdown
|
|
6
|
+
* summary, and reference/candidate/diff triptychs. a11y + i18n lead; tokens,
|
|
7
|
+
* semantics, and raw visual diff follow (docs/PRINCIPLES.md).
|
|
8
|
+
*/
|
|
9
|
+
export { diff } from "./diff.js";
|
|
10
|
+
export type { DiffOptions, DiffResult, Triptych } from "./diff.js";
|
|
11
|
+
export { defaultDiffConfig, resolveConfig } from "./config.js";
|
|
12
|
+
export type { DiffConfig } from "./config.js";
|
|
13
|
+
export { defaultChecks } from "./checks.js";
|
|
14
|
+
export type { ChecksProvider, ChecksInput, ChecksConfig } from "./checks.js";
|
|
15
|
+
export { collectTokens, diffTokens } from "./tokens.js";
|
|
16
|
+
export { diffSemantics, referenceThemes, candidateThemes } from "./semantic.js";
|
|
17
|
+
export { diffImagePair, imageKey } from "./visual.js";
|
|
18
|
+
export type { VisualResult } from "./visual.js";
|
|
19
|
+
export { renderSummary } from "./summary.js";
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAEnE,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC/D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE7E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@design-parity/diff` — the source-agnostic diff engine.
|
|
3
|
+
*
|
|
4
|
+
* Consume a `(DesignReference, CandidateRender)` pair (from any adapter +
|
|
5
|
+
* the candidate renderer) and emit a deterministic {@link Verdict}, a markdown
|
|
6
|
+
* summary, and reference/candidate/diff triptychs. a11y + i18n lead; tokens,
|
|
7
|
+
* semantics, and raw visual diff follow (docs/PRINCIPLES.md).
|
|
8
|
+
*/
|
|
9
|
+
export { diff } from "./diff.js";
|
|
10
|
+
export { defaultDiffConfig, resolveConfig } from "./config.js";
|
|
11
|
+
export { defaultChecks } from "./checks.js";
|
|
12
|
+
export { collectTokens, diffTokens } from "./tokens.js";
|
|
13
|
+
export { diffSemantics, referenceThemes, candidateThemes } from "./semantic.js";
|
|
14
|
+
export { diffImagePair, imageKey } from "./visual.js";
|
|
15
|
+
export { renderSummary } from "./summary.js";
|
|
16
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG/D,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAG5C,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACxD,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEtD,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic diff: structural / theme deltas between reference and candidate.
|
|
3
|
+
*
|
|
4
|
+
* A {@link DesignReference} carries images + tokens but no semantic tree, so the
|
|
5
|
+
* deltas we can assert deterministically are coverage-shaped: does the candidate
|
|
6
|
+
* render every theme the reference exposes, and is its root node a labelled,
|
|
7
|
+
* roled element (the minimum an a11y tree needs)? Deeper a11y lives in the
|
|
8
|
+
* checks provider (issue #10).
|
|
9
|
+
*/
|
|
10
|
+
import type { CandidateRender, DesignReference, Finding, Theme } from "@design-parity/core";
|
|
11
|
+
/** Themes the reference exposes, in first-seen order. */
|
|
12
|
+
export declare function referenceThemes(reference: DesignReference): Theme[];
|
|
13
|
+
/** Themes the candidate renders, in first-seen order. */
|
|
14
|
+
export declare function candidateThemes(candidate: CandidateRender): Theme[];
|
|
15
|
+
export declare function diffSemantics(reference: DesignReference, candidate: CandidateRender): Finding[];
|
|
16
|
+
//# sourceMappingURL=semantic.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../src/semantic.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,OAAO,EACP,KAAK,EACN,MAAM,qBAAqB,CAAC;AAE7B,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,SAAS,EAAE,eAAe,GAAG,KAAK,EAAE,CAEnE;AAED,yDAAyD;AACzD,wBAAgB,eAAe,CAAC,SAAS,EAAE,eAAe,GAAG,KAAK,EAAE,CAEnE;AAUD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,eAAe,EAC1B,SAAS,EAAE,eAAe,GACzB,OAAO,EAAE,CAkCX"}
|
package/dist/semantic.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Themes the reference exposes, in first-seen order. */
|
|
2
|
+
export function referenceThemes(reference) {
|
|
3
|
+
return uniqueThemes(reference.referenceImages.map((i) => i.theme));
|
|
4
|
+
}
|
|
5
|
+
/** Themes the candidate renders, in first-seen order. */
|
|
6
|
+
export function candidateThemes(candidate) {
|
|
7
|
+
return uniqueThemes(candidate.images.map((i) => i.theme));
|
|
8
|
+
}
|
|
9
|
+
function uniqueThemes(themes) {
|
|
10
|
+
const out = [];
|
|
11
|
+
for (const t of themes) {
|
|
12
|
+
if (t && !out.includes(t))
|
|
13
|
+
out.push(t);
|
|
14
|
+
}
|
|
15
|
+
return out;
|
|
16
|
+
}
|
|
17
|
+
export function diffSemantics(reference, candidate) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
const have = candidateThemes(candidate);
|
|
20
|
+
for (const theme of referenceThemes(reference)) {
|
|
21
|
+
if (!have.includes(theme)) {
|
|
22
|
+
findings.push({
|
|
23
|
+
kind: "semantic",
|
|
24
|
+
severity: "warn",
|
|
25
|
+
message: `reference exposes a ${theme} theme the candidate does not render`,
|
|
26
|
+
detail: { theme, missing: "candidate-image" },
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const root = candidate.semantics.root;
|
|
31
|
+
if (!root.role) {
|
|
32
|
+
findings.push({
|
|
33
|
+
kind: "semantic",
|
|
34
|
+
severity: "warn",
|
|
35
|
+
message: "candidate root node has no accessibility role",
|
|
36
|
+
detail: { node: "root", field: "role" },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
if (!root.label) {
|
|
40
|
+
findings.push({
|
|
41
|
+
kind: "semantic",
|
|
42
|
+
severity: "warn",
|
|
43
|
+
message: "candidate root node has no accessible label",
|
|
44
|
+
detail: { node: "root", field: "label" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=semantic.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantic.js","sourceRoot":"","sources":["../src/semantic.ts"],"names":[],"mappings":"AAgBA,yDAAyD;AACzD,MAAM,UAAU,eAAe,CAAC,SAA0B;IACxD,OAAO,YAAY,CAAC,SAAS,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AACrE,CAAC;AAED,yDAAyD;AACzD,MAAM,UAAU,eAAe,CAAC,SAA0B;IACxD,OAAO,YAAY,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;AAC5D,CAAC;AAED,SAAS,YAAY,CAAC,MAAgC;IACpD,MAAM,GAAG,GAAY,EAAE,CAAC;IACxB,KAAK,MAAM,CAAC,IAAI,MAAM,EAAE,CAAC;QACvB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,aAAa,CAC3B,SAA0B,EAC1B,SAA0B;IAE1B,MAAM,QAAQ,GAAc,EAAE,CAAC;IAE/B,MAAM,IAAI,GAAG,eAAe,CAAC,SAAS,CAAC,CAAC;IACxC,KAAK,MAAM,KAAK,IAAI,eAAe,CAAC,SAAS,CAAC,EAAE,CAAC;QAC/C,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,QAAQ,CAAC,IAAI,CAAC;gBACZ,IAAI,EAAE,UAAU;gBAChB,QAAQ,EAAE,MAAM;gBAChB,OAAO,EAAE,uBAAuB,KAAK,sCAAsC;gBAC3E,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,iBAAiB,EAAE;aAC9C,CAAC,CAAC;QACL,CAAC;IACH,CAAC;IAED,MAAM,IAAI,GAAG,SAAS,CAAC,SAAS,CAAC,IAAI,CAAC;IACtC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;QACf,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,+CAA+C;YACxD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE;SACxC,CAAC,CAAC;IACL,CAAC;IACD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;QAChB,QAAQ,CAAC,IAAI,CAAC;YACZ,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,MAAM;YAChB,OAAO,EAAE,6CAA6C;YACtD,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE;SACzC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable markdown summary of a {@link Verdict}.
|
|
3
|
+
*
|
|
4
|
+
* The machine `Verdict` is for downstream consumers; this is what a reviewer
|
|
5
|
+
* reads in the PR comment. Sections are ordered by value (Principle 2):
|
|
6
|
+
* a11y + i18n first, then tokens, then semantics, then the visual diff.
|
|
7
|
+
*/
|
|
8
|
+
import type { Verdict } from "@design-parity/core";
|
|
9
|
+
/** Render a verdict as a self-contained markdown block. */
|
|
10
|
+
export declare function renderSummary(verdict: Verdict): string;
|
|
11
|
+
//# sourceMappingURL=summary.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.d.ts","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,KAAK,EAAwB,OAAO,EAAE,MAAM,qBAAqB,CAAC;AAsBzE,2DAA2D;AAC3D,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CA8BtD"}
|
package/dist/summary.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const STATUS_ICON = { pass: "✅", warn: "⚠️", fail: "❌" };
|
|
2
|
+
const SEVERITY_ICON = { info: "ℹ️", warn: "⚠️", error: "❌" };
|
|
3
|
+
// a11y + i18n lead (Principle 2); contrast/a11y/i18n are the checks signals.
|
|
4
|
+
const SECTIONS = [
|
|
5
|
+
{ title: "Accessibility & i18n", kinds: ["contrast", "a11y", "i18n"] },
|
|
6
|
+
{ title: "Token compliance", kinds: ["token"] },
|
|
7
|
+
{ title: "Semantics", kinds: ["semantic"] },
|
|
8
|
+
{ title: "Visual", kinds: ["visual"] },
|
|
9
|
+
];
|
|
10
|
+
function renderFinding(f) {
|
|
11
|
+
return `- ${SEVERITY_ICON[f.severity]} ${f.message}`;
|
|
12
|
+
}
|
|
13
|
+
/** Render a verdict as a self-contained markdown block. */
|
|
14
|
+
export function renderSummary(verdict) {
|
|
15
|
+
const lines = [
|
|
16
|
+
`## Parity verdict: \`${verdict.componentId}\` — ${STATUS_ICON[verdict.status]} ${verdict.status}`,
|
|
17
|
+
];
|
|
18
|
+
for (const section of SECTIONS) {
|
|
19
|
+
const matched = verdict.findings.filter((f) => section.kinds.includes(f.kind));
|
|
20
|
+
if (matched.length === 0)
|
|
21
|
+
continue;
|
|
22
|
+
lines.push("", `**${section.title}**`, ...matched.map(renderFinding));
|
|
23
|
+
}
|
|
24
|
+
const scores = verdict.visualScores;
|
|
25
|
+
if (scores && Object.keys(scores).length > 0) {
|
|
26
|
+
const detail = Object.entries(scores)
|
|
27
|
+
.map(([key, score]) => score === 0
|
|
28
|
+
? `\`${key}\` match`
|
|
29
|
+
: `\`${key}\` ${(score * 100).toFixed(1)}% differ`)
|
|
30
|
+
.join(", ");
|
|
31
|
+
lines.push("", `_Visual diff: ${detail}._`);
|
|
32
|
+
}
|
|
33
|
+
if (verdict.findings.length === 0) {
|
|
34
|
+
lines.push("", "No parity findings — candidate matches the reference spec.");
|
|
35
|
+
}
|
|
36
|
+
return lines.join("\n");
|
|
37
|
+
}
|
|
38
|
+
//# sourceMappingURL=summary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"summary.js","sourceRoot":"","sources":["../src/summary.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAW,CAAC;AAClE,MAAM,aAAa,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAW,CAAC;AAOtE,6EAA6E;AAC7E,MAAM,QAAQ,GAAc;IAC1B,EAAE,KAAK,EAAE,sBAAsB,EAAE,KAAK,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;IACtE,EAAE,KAAK,EAAE,kBAAkB,EAAE,KAAK,EAAE,CAAC,OAAO,CAAC,EAAE;IAC/C,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,UAAU,CAAC,EAAE;IAC3C,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,QAAQ,CAAC,EAAE;CACvC,CAAC;AAEF,SAAS,aAAa,CAAC,CAAU;IAC/B,OAAO,KAAK,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC;AACvD,CAAC;AAED,2DAA2D;AAC3D,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,MAAM,KAAK,GAAa;QACtB,wBAAwB,OAAO,CAAC,WAAW,QAAQ,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,OAAO,CAAC,MAAM,EAAE;KACnG,CAAC;IAEF,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,MAAM,OAAO,GAAG,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAC5C,OAAO,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAC/B,CAAC;QACF,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACnC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,KAAK,OAAO,CAAC,KAAK,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,MAAM,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IACpC,IAAI,MAAM,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC7C,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC;aAClC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CACpB,KAAK,KAAK,CAAC;YACT,CAAC,CAAC,KAAK,GAAG,UAAU;YACpB,CAAC,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,GAAG,GAAG,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CACrD;aACA,IAAI,CAAC,IAAI,CAAC,CAAC;QACd,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,iBAAiB,MAAM,IAAI,CAAC,CAAC;IAC9C,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,4DAA4D,CAAC,CAAC;IAC/E,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
|
package/dist/tokens.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token collection + token-compliance diff.
|
|
3
|
+
*
|
|
4
|
+
* A {@link DesignReference} carries one flat {@link DesignTokens} bag; a
|
|
5
|
+
* {@link CandidateRender} scatters tokens across its semantic tree. We flatten
|
|
6
|
+
* the candidate's tree into the same shape, then compare key-by-key against the
|
|
7
|
+
* reference spec. Numeric tokens honour a committed tolerance; colours and
|
|
8
|
+
* typography must match exactly.
|
|
9
|
+
*/
|
|
10
|
+
import type { DesignTokens, Finding, SemanticNode } from "@design-parity/core";
|
|
11
|
+
import type { DiffConfig } from "./config.js";
|
|
12
|
+
/** Deep-merge every node's tokens into one flat bag (children override parents). */
|
|
13
|
+
export declare function collectTokens(root: SemanticNode): DesignTokens;
|
|
14
|
+
/**
|
|
15
|
+
* Compare candidate tokens against the reference spec. Findings are emitted in
|
|
16
|
+
* a stable order — spacing, radius, colours, typography — so the verdict is
|
|
17
|
+
* reproducible. A reference token absent from the candidate is a `missing`
|
|
18
|
+
* finding; tokens the candidate adds beyond the spec are ignored.
|
|
19
|
+
*/
|
|
20
|
+
export declare function diffTokens(spec: DesignTokens | undefined, candidate: DesignTokens, config: DiffConfig): Finding[];
|
|
21
|
+
//# sourceMappingURL=tokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.d.ts","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AACH,OAAO,KAAK,EACV,YAAY,EACZ,OAAO,EACP,YAAY,EAEb,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,oFAAoF;AACpF,wBAAgB,aAAa,CAAC,IAAI,EAAE,YAAY,GAAG,YAAY,CAQ9D;AAqBD;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,YAAY,GAAG,SAAS,EAC9B,SAAS,EAAE,YAAY,EACvB,MAAM,EAAE,UAAU,GACjB,OAAO,EAAE,CAuCX"}
|
package/dist/tokens.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/** Deep-merge every node's tokens into one flat bag (children override parents). */
|
|
2
|
+
export function collectTokens(root) {
|
|
3
|
+
const out = {};
|
|
4
|
+
const visit = (node) => {
|
|
5
|
+
mergeInto(out, node.tokens);
|
|
6
|
+
for (const child of node.children ?? [])
|
|
7
|
+
visit(child);
|
|
8
|
+
};
|
|
9
|
+
visit(root);
|
|
10
|
+
return out;
|
|
11
|
+
}
|
|
12
|
+
function mergeInto(target, src) {
|
|
13
|
+
if (!src)
|
|
14
|
+
return;
|
|
15
|
+
if (src.spacing)
|
|
16
|
+
target.spacing = { ...target.spacing, ...src.spacing };
|
|
17
|
+
if (src.radius)
|
|
18
|
+
target.radius = { ...target.radius, ...src.radius };
|
|
19
|
+
if (src.colors)
|
|
20
|
+
target.colors = { ...target.colors, ...src.colors };
|
|
21
|
+
if (src.typography)
|
|
22
|
+
target.typography = { ...target.typography, ...src.typography };
|
|
23
|
+
}
|
|
24
|
+
function typographyEqual(a, b) {
|
|
25
|
+
return (a.fontFamily === b.fontFamily &&
|
|
26
|
+
a.fontSize === b.fontSize &&
|
|
27
|
+
a.fontWeight === b.fontWeight &&
|
|
28
|
+
a.lineHeight === b.lineHeight &&
|
|
29
|
+
a.letterSpacing === b.letterSpacing);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Compare candidate tokens against the reference spec. Findings are emitted in
|
|
33
|
+
* a stable order — spacing, radius, colours, typography — so the verdict is
|
|
34
|
+
* reproducible. A reference token absent from the candidate is a `missing`
|
|
35
|
+
* finding; tokens the candidate adds beyond the spec are ignored.
|
|
36
|
+
*/
|
|
37
|
+
export function diffTokens(spec, candidate, config) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
if (!spec)
|
|
40
|
+
return findings;
|
|
41
|
+
for (const [name, want] of Object.entries(spec.spacing ?? {})) {
|
|
42
|
+
const got = candidate.spacing?.[name];
|
|
43
|
+
numericFinding("spacing", name, want, got, config.spacingTolerance, findings);
|
|
44
|
+
}
|
|
45
|
+
for (const [name, want] of Object.entries(spec.radius ?? {})) {
|
|
46
|
+
const got = candidate.radius?.[name];
|
|
47
|
+
numericFinding("radius", name, want, got, config.radiusTolerance, findings);
|
|
48
|
+
}
|
|
49
|
+
for (const [name, want] of Object.entries(spec.colors ?? {})) {
|
|
50
|
+
const got = candidate.colors?.[name];
|
|
51
|
+
if (got === undefined) {
|
|
52
|
+
findings.push(missing("colors", name, want));
|
|
53
|
+
}
|
|
54
|
+
else if (got.toLowerCase() !== want.toLowerCase()) {
|
|
55
|
+
findings.push({
|
|
56
|
+
kind: "token",
|
|
57
|
+
severity: "warn",
|
|
58
|
+
message: `colors.${name}: ${got} vs spec ${want}`,
|
|
59
|
+
detail: { token: `colors.${name}`, expected: want, actual: got },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for (const [name, want] of Object.entries(spec.typography ?? {})) {
|
|
64
|
+
const got = candidate.typography?.[name];
|
|
65
|
+
if (got === undefined) {
|
|
66
|
+
findings.push(missing("typography", name, JSON.stringify(want)));
|
|
67
|
+
}
|
|
68
|
+
else if (!typographyEqual(want, got)) {
|
|
69
|
+
findings.push({
|
|
70
|
+
kind: "token",
|
|
71
|
+
severity: "warn",
|
|
72
|
+
message: `typography.${name} differs from spec`,
|
|
73
|
+
detail: { token: `typography.${name}`, expected: want, actual: got },
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return findings;
|
|
78
|
+
}
|
|
79
|
+
function numericFinding(group, name, want, got, tolerance, findings) {
|
|
80
|
+
if (got === undefined) {
|
|
81
|
+
findings.push(missing(group, name, String(want)));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const delta = Math.abs(got - want);
|
|
85
|
+
if (delta > tolerance) {
|
|
86
|
+
findings.push({
|
|
87
|
+
kind: "token",
|
|
88
|
+
severity: "error",
|
|
89
|
+
message: `${group}.${name}: ${got} vs spec ${want} (Δ${delta})`,
|
|
90
|
+
detail: { token: `${group}.${name}`, expected: want, actual: got, delta },
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function missing(group, name, want) {
|
|
95
|
+
return {
|
|
96
|
+
kind: "token",
|
|
97
|
+
severity: "error",
|
|
98
|
+
message: `${group}.${name} missing from candidate (spec ${want})`,
|
|
99
|
+
detail: { token: `${group}.${name}`, expected: want, actual: null },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=tokens.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tokens.js","sourceRoot":"","sources":["../src/tokens.ts"],"names":[],"mappings":"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"}
|
package/dist/visual.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type Image } from "@design-parity/core";
|
|
2
|
+
import type { DiffConfig } from "./config.js";
|
|
3
|
+
/** The result of comparing one reference image against its candidate twin. */
|
|
4
|
+
export interface VisualResult {
|
|
5
|
+
/** `state/theme/size` key shared with {@link Verdict.visualScores}. */
|
|
6
|
+
key: string;
|
|
7
|
+
/** Fraction of differing pixels, 0 (identical) … 1. */
|
|
8
|
+
score: number;
|
|
9
|
+
diffPixels: number;
|
|
10
|
+
totalPixels: number;
|
|
11
|
+
/** Side-by-side reference | candidate | diff PNG. */
|
|
12
|
+
triptych: Buffer;
|
|
13
|
+
/**
|
|
14
|
+
* The standalone diff heatmap PNG (just the pixelmatch panel), for consumers
|
|
15
|
+
* that lay out their own reference/candidate columns — e.g. the HTML report
|
|
16
|
+
* (#50). Absent when there was no aligned region to diff (a beyond-tolerance
|
|
17
|
+
* dimension mismatch).
|
|
18
|
+
*/
|
|
19
|
+
diffPng?: Buffer;
|
|
20
|
+
/**
|
|
21
|
+
* True when reference and candidate had different dimensions but within
|
|
22
|
+
* {@link DiffConfig.visualDimTolerancePx}, so they were diffed over their
|
|
23
|
+
* top-left overlap rather than scored a total mismatch (#47).
|
|
24
|
+
*/
|
|
25
|
+
dimensionMismatch?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/** Human-readable key (raw size) — also the {@link Verdict.visualScores} key. */
|
|
28
|
+
export declare function imageKey(img: Image): string;
|
|
29
|
+
/**
|
|
30
|
+
* Pairing key with the size normalized, so `"Compact"` / `"compact"` / `"600dp"`
|
|
31
|
+
* all match. Differs from {@link imageKey} (which keeps the raw label for humans).
|
|
32
|
+
*/
|
|
33
|
+
export declare function pairKey(img: Image): string;
|
|
34
|
+
/** Looser key ignoring size — used to pair when a side omits/uses an unknown size. */
|
|
35
|
+
export declare function looseKey(img: Image): string;
|
|
36
|
+
/**
|
|
37
|
+
* Whether two images may pair despite different size labels: compatible when at
|
|
38
|
+
* least one side has no recognized size, or both canonicalize to the same one.
|
|
39
|
+
* Two *different* known sizes (compact vs expanded) are genuinely distinct.
|
|
40
|
+
*/
|
|
41
|
+
export declare function sizeCompatible(a: Image, b: Image): boolean;
|
|
42
|
+
/** Compare one matched pair, returning its score and triptych. */
|
|
43
|
+
export declare function diffImagePair(repoRoot: string, reference: Image, candidate: Image, config: DiffConfig): Promise<VisualResult>;
|
|
44
|
+
//# sourceMappingURL=visual.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual.d.ts","sourceRoot":"","sources":["../src/visual.ts"],"names":[],"mappings":"AAWA,OAAO,EAAiB,KAAK,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAIhE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAW9C,8EAA8E;AAC9E,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,GAAG,EAAE,MAAM,CAAC;IACZ,uDAAuD;IACvD,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,qDAAqD;IACrD,QAAQ,EAAE,MAAM,CAAC;IACjB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,iFAAiF;AACjF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAE3C;AAOD;;;GAGG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAE1C;AAED,sFAAsF;AACtF,wBAAgB,QAAQ,CAAC,GAAG,EAAE,KAAK,GAAG,MAAM,CAE3C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,GAAG,OAAO,CAI1D;AAsBD,kEAAkE;AAClE,wBAAsB,aAAa,CACjC,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,KAAK,EAChB,SAAS,EAAE,KAAK,EAChB,MAAM,EAAE,UAAU,GACjB,OAAO,CAAC,YAAY,CAAC,CAqDvB"}
|
package/dist/visual.js
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visual diff: per-pixel comparison + the reference/candidate/diff triptych.
|
|
3
|
+
*
|
|
4
|
+
* pixelmatch gives a deterministic differing-pixel count (Principle 1); the
|
|
5
|
+
* triptych is a single PNG that stacks reference, candidate, and the pixelmatch
|
|
6
|
+
* heatmap side by side so a reviewer sees what moved. Visual diff is table
|
|
7
|
+
* stakes (Principle 2) — it informs, it never gates.
|
|
8
|
+
*/
|
|
9
|
+
import { readFile } from "node:fs/promises";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { normalizeSize } from "@design-parity/core";
|
|
12
|
+
import pixelmatch from "pixelmatch";
|
|
13
|
+
import { PNG } from "pngjs";
|
|
14
|
+
const GAP = 8;
|
|
15
|
+
const GAP_RGBA = [0x1f, 0x1f, 0x24, 0xff];
|
|
16
|
+
/** Human-readable key (raw size) — also the {@link Verdict.visualScores} key. */
|
|
17
|
+
export function imageKey(img) {
|
|
18
|
+
return [img.state, img.theme, img.size].filter(Boolean).join("/");
|
|
19
|
+
}
|
|
20
|
+
/** The size token used for pairing: canonical when recognized, else the raw label. */
|
|
21
|
+
function sizeToken(img) {
|
|
22
|
+
return normalizeSize(img.size) ?? img.size?.toLowerCase();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pairing key with the size normalized, so `"Compact"` / `"compact"` / `"600dp"`
|
|
26
|
+
* all match. Differs from {@link imageKey} (which keeps the raw label for humans).
|
|
27
|
+
*/
|
|
28
|
+
export function pairKey(img) {
|
|
29
|
+
return [img.state, img.theme, sizeToken(img)].filter(Boolean).join("/");
|
|
30
|
+
}
|
|
31
|
+
/** Looser key ignoring size — used to pair when a side omits/uses an unknown size. */
|
|
32
|
+
export function looseKey(img) {
|
|
33
|
+
return [img.state, img.theme].filter(Boolean).join("/");
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Whether two images may pair despite different size labels: compatible when at
|
|
37
|
+
* least one side has no recognized size, or both canonicalize to the same one.
|
|
38
|
+
* Two *different* known sizes (compact vs expanded) are genuinely distinct.
|
|
39
|
+
*/
|
|
40
|
+
export function sizeCompatible(a, b) {
|
|
41
|
+
const ca = normalizeSize(a.size);
|
|
42
|
+
const cb = normalizeSize(b.size);
|
|
43
|
+
return ca === undefined || cb === undefined || ca === cb;
|
|
44
|
+
}
|
|
45
|
+
/** Decode a `data:<mediatype>;base64,<payload>` URI to its raw bytes. */
|
|
46
|
+
function decodeDataUri(uri) {
|
|
47
|
+
const comma = uri.indexOf(",");
|
|
48
|
+
const meta = comma === -1 ? "" : uri.slice("data:".length, comma);
|
|
49
|
+
if (!/;base64$/i.test(meta)) {
|
|
50
|
+
throw new Error(`visual: unsupported data: URI (expected base64): ${meta}`);
|
|
51
|
+
}
|
|
52
|
+
return Buffer.from(uri.slice(comma + 1), "base64");
|
|
53
|
+
}
|
|
54
|
+
async function readRaster(repoRoot, uri) {
|
|
55
|
+
// A source may hand us the PNG inline as a `data:` URI (e.g. a `.zip` bundle,
|
|
56
|
+
// which has no standalone repo file); decode it rather than reading from disk.
|
|
57
|
+
const buf = uri.startsWith("data:")
|
|
58
|
+
? decodeDataUri(uri)
|
|
59
|
+
: await readFile(resolve(repoRoot, uri));
|
|
60
|
+
const png = PNG.sync.read(buf);
|
|
61
|
+
return { width: png.width, height: png.height, data: png.data };
|
|
62
|
+
}
|
|
63
|
+
/** Compare one matched pair, returning its score and triptych. */
|
|
64
|
+
export async function diffImagePair(repoRoot, reference, candidate, config) {
|
|
65
|
+
const ref = await readRaster(repoRoot, reference.uri);
|
|
66
|
+
const cand = await readRaster(repoRoot, candidate.uri);
|
|
67
|
+
const key = imageKey(reference);
|
|
68
|
+
const dw = Math.abs(ref.width - cand.width);
|
|
69
|
+
const dh = Math.abs(ref.height - cand.height);
|
|
70
|
+
const sameSize = dw === 0 && dh === 0;
|
|
71
|
+
// A sub-tolerance dimension delta (e.g. density rounding between two render
|
|
72
|
+
// tools) is diffed over the shared top-left overlap rather than written off as
|
|
73
|
+
// a total mismatch, so the real content drift isn't masked (#47).
|
|
74
|
+
const aligned = !sameSize &&
|
|
75
|
+
dw <= config.visualDimTolerancePx &&
|
|
76
|
+
dh <= config.visualDimTolerancePx;
|
|
77
|
+
// Score against the union box so the non-overlapping border (counted as
|
|
78
|
+
// differing below) can't push the ratio past 1; for equal sizes this is just
|
|
79
|
+
// the shared area.
|
|
80
|
+
const totalPixels = Math.max(ref.width, cand.width) * Math.max(ref.height, cand.height);
|
|
81
|
+
let diffPixels;
|
|
82
|
+
let diff = null;
|
|
83
|
+
let diffPng;
|
|
84
|
+
if (sameSize || aligned) {
|
|
85
|
+
const ow = Math.min(ref.width, cand.width);
|
|
86
|
+
const oh = Math.min(ref.height, cand.height);
|
|
87
|
+
const out = new PNG({ width: ow, height: oh });
|
|
88
|
+
const overlapDiff = pixelmatch(cropTopLeft(ref, ow, oh), cropTopLeft(cand, ow, oh), out.data, ow, oh, { threshold: config.pixelThreshold });
|
|
89
|
+
diff = { width: ow, height: oh, data: out.data };
|
|
90
|
+
diffPng = PNG.sync.write(out);
|
|
91
|
+
// Differing overlap pixels + the border only one image covers.
|
|
92
|
+
diffPixels = overlapDiff + (totalPixels - ow * oh);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// Dimension drift beyond tolerance is a genuine total mismatch; there's no
|
|
96
|
+
// meaningful aligned region to render.
|
|
97
|
+
diffPixels = totalPixels;
|
|
98
|
+
}
|
|
99
|
+
const score = totalPixels === 0 ? 0 : diffPixels / totalPixels;
|
|
100
|
+
const triptych = composeTriptych([ref, cand, diff]);
|
|
101
|
+
const result = { key, score, diffPixels, totalPixels, triptych };
|
|
102
|
+
if (diffPng)
|
|
103
|
+
result.diffPng = diffPng;
|
|
104
|
+
if (aligned)
|
|
105
|
+
result.dimensionMismatch = true;
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Copy the top-left `w×h` region of a raster into a fresh, tightly-packed RGBA
|
|
110
|
+
* buffer (pixelmatch needs both inputs at one common stride). A no-op when the
|
|
111
|
+
* region already spans the whole raster.
|
|
112
|
+
*/
|
|
113
|
+
function cropTopLeft(src, w, h) {
|
|
114
|
+
if (src.width === w && src.height === h)
|
|
115
|
+
return src.data;
|
|
116
|
+
const out = Buffer.alloc(w * h * 4);
|
|
117
|
+
const rowBytes = w * 4;
|
|
118
|
+
for (let y = 0; y < h; y++) {
|
|
119
|
+
const srcStart = y * src.width * 4;
|
|
120
|
+
src.data.copy(out, y * rowBytes, srcStart, srcStart + rowBytes);
|
|
121
|
+
}
|
|
122
|
+
return out;
|
|
123
|
+
}
|
|
124
|
+
/** Lay panels out left-to-right on a gap-coloured strip, top-aligned. */
|
|
125
|
+
function composeTriptych(panels) {
|
|
126
|
+
const present = panels.map((p) => p ?? { width: 1, height: 1, data: Buffer.alloc(4) });
|
|
127
|
+
const height = Math.max(...present.map((p) => p.height));
|
|
128
|
+
const width = present.reduce((sum, p) => sum + p.width, 0) + GAP * (present.length - 1);
|
|
129
|
+
const out = new PNG({ width, height });
|
|
130
|
+
// Fill with the gap colour so seams and short panels read as deliberate.
|
|
131
|
+
for (let i = 0; i < out.data.length; i += 4) {
|
|
132
|
+
out.data[i] = GAP_RGBA[0];
|
|
133
|
+
out.data[i + 1] = GAP_RGBA[1];
|
|
134
|
+
out.data[i + 2] = GAP_RGBA[2];
|
|
135
|
+
out.data[i + 3] = GAP_RGBA[3];
|
|
136
|
+
}
|
|
137
|
+
let xOffset = 0;
|
|
138
|
+
for (const panel of present) {
|
|
139
|
+
blit(out, panel, xOffset);
|
|
140
|
+
xOffset += panel.width + GAP;
|
|
141
|
+
}
|
|
142
|
+
return PNG.sync.write(out);
|
|
143
|
+
}
|
|
144
|
+
function blit(dest, src, xOffset) {
|
|
145
|
+
for (let y = 0; y < src.height; y++) {
|
|
146
|
+
for (let x = 0; x < src.width; x++) {
|
|
147
|
+
const si = (y * src.width + x) * 4;
|
|
148
|
+
const di = (y * dest.width + (x + xOffset)) * 4;
|
|
149
|
+
dest.data[di] = src.data[si];
|
|
150
|
+
dest.data[di + 1] = src.data[si + 1];
|
|
151
|
+
dest.data[di + 2] = src.data[si + 2];
|
|
152
|
+
dest.data[di + 3] = src.data[si + 3];
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
//# sourceMappingURL=visual.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"visual.js","sourceRoot":"","sources":["../src/visual.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,OAAO,EAAE,aAAa,EAAc,MAAM,qBAAqB,CAAC;AAChE,OAAO,UAAU,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,GAAG,EAAE,MAAM,OAAO,CAAC;AAI5B,MAAM,GAAG,GAAG,CAAC,CAAC;AACd,MAAM,QAAQ,GAAqC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAiC5E,iFAAiF;AACjF,MAAM,UAAU,QAAQ,CAAC,GAAU;IACjC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACpE,CAAC;AAED,sFAAsF;AACtF,SAAS,SAAS,CAAC,GAAU;IAC3B,OAAO,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,GAAG,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,OAAO,CAAC,GAAU;IAChC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1E,CAAC;AAED,sFAAsF;AACtF,MAAM,UAAU,QAAQ,CAAC,GAAU;IACjC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC1D,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,cAAc,CAAC,CAAQ,EAAE,CAAQ;IAC/C,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACjC,MAAM,EAAE,GAAG,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACjC,OAAO,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE,CAAC;AAC3D,CAAC;AAED,yEAAyE;AACzE,SAAS,aAAa,CAAC,GAAW;IAChC,MAAM,KAAK,GAAG,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAC/B,MAAM,IAAI,GAAG,KAAK,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAClE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,oDAAoD,IAAI,EAAE,CAAC,CAAC;IAC9E,CAAC;IACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AACrD,CAAC;AAED,KAAK,UAAU,UAAU,CAAC,QAAgB,EAAE,GAAW;IACrD,8EAA8E;IAC9E,+EAA+E;IAC/E,MAAM,GAAG,GAAG,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC;QACjC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC;QACpB,CAAC,CAAC,MAAM,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/B,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;AAClE,CAAC;AAED,kEAAkE;AAClE,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,QAAgB,EAChB,SAAgB,EAChB,SAAgB,EAChB,MAAkB;IAElB,MAAM,GAAG,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,QAAQ,EAAE,SAAS,CAAC,GAAG,CAAC,CAAC;IACvD,MAAM,GAAG,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC;IAEhC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;IAC5C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC9C,MAAM,QAAQ,GAAG,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IACtC,4EAA4E;IAC5E,+EAA+E;IAC/E,kEAAkE;IAClE,MAAM,OAAO,GACX,CAAC,QAAQ;QACT,EAAE,IAAI,MAAM,CAAC,oBAAoB;QACjC,EAAE,IAAI,MAAM,CAAC,oBAAoB,CAAC;IAEpC,wEAAwE;IACxE,6EAA6E;IAC7E,mBAAmB;IACnB,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;IACxF,IAAI,UAAkB,CAAC;IACvB,IAAI,IAAI,GAAkB,IAAI,CAAC;IAC/B,IAAI,OAA2B,CAAC;IAEhC,IAAI,QAAQ,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC;QAC3C,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAC7C,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,CAAC;QAC/C,MAAM,WAAW,GAAG,UAAU,CAC5B,WAAW,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,EACxB,WAAW,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,EACzB,GAAG,CAAC,IAAI,EACR,EAAE,EACF,EAAE,EACF,EAAE,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE,CACrC,CAAC;QACF,IAAI,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;QACjD,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAC9B,+DAA+D;QAC/D,UAAU,GAAG,WAAW,GAAG,CAAC,WAAW,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;IACrD,CAAC;SAAM,CAAC;QACN,2EAA2E;QAC3E,uCAAuC;QACvC,UAAU,GAAG,WAAW,CAAC;IAC3B,CAAC;IAED,MAAM,KAAK,GAAG,WAAW,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,WAAW,CAAC;IAC/D,MAAM,QAAQ,GAAG,eAAe,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;IAEpD,MAAM,MAAM,GAAiB,EAAE,GAAG,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC;IAC/E,IAAI,OAAO;QAAE,MAAM,CAAC,OAAO,GAAG,OAAO,CAAC;IACtC,IAAI,OAAO;QAAE,MAAM,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAC7C,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,GAAW,EAAE,CAAS,EAAE,CAAS;IACpD,IAAI,GAAG,CAAC,KAAK,KAAK,CAAC,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,IAAI,CAAC;IACzD,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IACpC,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,CAAC;IACvB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC;QACnC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,GAAG,QAAQ,EAAE,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,yEAAyE;AACzE,SAAS,eAAe,CAAC,MAA4B;IACnD,MAAM,OAAO,GAAG,MAAM,CAAC,GAAG,CACxB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAC3D,CAAC;IACF,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;IACzD,MAAM,KAAK,GACT,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IAEvC,yEAAyE;IACzE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC;QAC5C,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC1B,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAChC,CAAC;IAED,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,IAAI,CAAC,GAAG,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;QAC1B,OAAO,IAAI,KAAK,CAAC,KAAK,GAAG,GAAG,CAAC;IAC/B,CAAC;IACD,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC;AAED,SAAS,IAAI,CAAC,IAAS,EAAE,GAAW,EAAE,OAAe;IACnD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC;YACnC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;YAChD,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,CAAE,CAAC;YAC9B,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAE,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAE,CAAC;YACtC,IAAI,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAE,CAAC;QACxC,CAAC;IACH,CAAC;AACH,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@design-parity/diff",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Source-agnostic diff engine: consume a (DesignReference, CandidateRender) pair and emit a deterministic Verdict — a11y + i18n first, then token compliance, then visual diff.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc --build",
|
|
17
|
+
"test": "vitest run"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@design-parity/checks": "^0.1.0",
|
|
21
|
+
"@design-parity/core": "^0.1.0",
|
|
22
|
+
"pixelmatch": "^7.1.0",
|
|
23
|
+
"pngjs": "^7.0.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/pngjs": "^6.0.5"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/yschimke/design-parity.git",
|
|
31
|
+
"directory": "packages/diff"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/yschimke/design-parity/tree/main/packages/diff#readme",
|
|
34
|
+
"bugs": "https://github.com/yschimke/design-parity/issues"
|
|
35
|
+
}
|