@penpair/reflection-check 0.3.6 → 0.4.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 +18 -3
- package/dist/image-evidence.d.ts +6 -0
- package/dist/image-evidence.js +13 -0
- package/dist/run-cases.js +8 -8
- package/dist/types.d.ts +1 -0
- package/package.json +2 -2
- package/dist/png-evidence.d.ts +0 -6
- package/dist/png-evidence.js +0 -23
package/README.md
CHANGED
|
@@ -15,19 +15,19 @@ penpair reflection check --map design/implementation-cases.json --provider desig
|
|
|
15
15
|
|
|
16
16
|
Mapping: `{"schemaVersion":1,"cases":[{"id":"dashboard-empty-dark","setup":"dark-empty"}]}`. A case must have current exact approved provenance, matching file pin and an explicit setup name. Missing mappings, legacy approvals and changed case metadata remain pending. No setup is silently replaced by the default route state. Baselines remain owned by the core; this adapter neither captures nor approves design references.
|
|
17
17
|
|
|
18
|
-
The provider module exports `async runCase(test)` and receives the immutable case ID, route, setup key, declared intent, baseline relative path/hash/scale and threshold. It must activate the route/theme/state/data fixture, verify actual browser state and fonts independently, capture a new
|
|
18
|
+
The provider module exports `async runCase(test)` and receives the immutable case ID, route, setup key, declared intent, baseline relative path/hash/scale and threshold. It must activate the route/theme/state/data fixture, verify actual browser state and fonts independently, capture a new static lossless WebP at the requested scale (legacy PNG evidence remains supported), and use the installed checker to compare it with the approved image. It returns:
|
|
19
19
|
|
|
20
20
|
```json
|
|
21
21
|
{
|
|
22
22
|
"schemaVersion":1,"id":"dashboard-empty-dark","route":"/dashboard","setup":"dark-empty",
|
|
23
23
|
"observed":{"viewport":{"width":1280,"height":900},"theme":"dark","state":"empty","fixture":"no-items"},
|
|
24
24
|
"fonts":["Expected Font"],"fontsReady":true,"baselineHash":"EXACT_SHA256",
|
|
25
|
-
"screenshot":".penpair/comparison/dashboard.
|
|
25
|
+
"screenshot":".penpair/comparison/dashboard.webp","diffPixelRatio":0.01,
|
|
26
26
|
"provider":{"name":"PROJECT_PINNED_CHECKER","version":"ACTUAL_VERSION"}
|
|
27
27
|
}
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
Observed context must come from browser readback, not copied test inputs. The adapter checks context/font evidence, independent screenshot path,
|
|
30
|
+
Observed context must come from browser readback, not copied test inputs. The adapter checks context/font evidence, independent screenshot path, exact image dimensions, byte hashes and unchanged baseline bytes. A trusted provider can lie: this interface is an evidence contract, not a sandbox or cryptographic attestation of browser work. Modules and transitive imports execute project code. Pixel ratios are checker evidence, never a visual verdict, accessibility assessment or owner approval. Exit 0 means complete evidence within thresholds; 2 means measured drift; 3 means missing/incomplete evidence. Every result still says `owner-review-required`.
|
|
31
31
|
|
|
32
32
|
The shipped browser starter has been exercised against local fixture routes; the
|
|
33
33
|
adapter's offline integration test uses a named stub. Neither establishes parity
|
|
@@ -35,3 +35,18 @@ for a consumer's implementation. Use the shipped `penpair-verify` skill, run the
|
|
|
35
35
|
consumer's real provider against approved references and retain the resulting
|
|
36
36
|
images before claiming comparison readiness. Owner review remains separate.
|
|
37
37
|
Release/publishing is the owner's act and all six packages remain in lockstep.
|
|
38
|
+
|
|
39
|
+
Evidence formats are detected from bytes, not filenames: PNG and static lossless
|
|
40
|
+
WebP (`VP8L`, including a matching `VP8X` canvas) are accepted. Lossy or animated
|
|
41
|
+
WebP and malformed/truncated containers are rejected. The adapter inspects image
|
|
42
|
+
metadata and hashes; the project provider owns actual decoding and pixel comparison.
|
|
43
|
+
Existing PNG baseline approvals retain their exact bytes and hashes. New core
|
|
44
|
+
baseline/approval images default to lossless WebP; public Penpot PNG exports remain
|
|
45
|
+
PNG. Existing approved references are never silently converted.
|
|
46
|
+
|
|
47
|
+
The browser starter saves `.webp` evidence by default. Supply `encodeLosslessWebp`
|
|
48
|
+
from the project's pinned codec, for example Sharp with `lossless: true` and
|
|
49
|
+
`exact: true` to preserve transparent-pixel RGB. The starter independently checks
|
|
50
|
+
that encoding preserves decoded pixels before saving. Use `imageFormat: "png"`
|
|
51
|
+
for an explicit legacy provider path. Neither the adapter nor core gains a native
|
|
52
|
+
image-codec runtime dependency.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readImageEvidence as readCoreImageEvidence } from "@penpair/core";
|
|
3
|
+
export function hashImageBytes(bytes) {
|
|
4
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
5
|
+
}
|
|
6
|
+
export async function readImageEvidence(root, file) {
|
|
7
|
+
const { bytes, width, height } = await readCoreImageEvidence(root, file);
|
|
8
|
+
return {
|
|
9
|
+
bytes,
|
|
10
|
+
width,
|
|
11
|
+
height,
|
|
12
|
+
};
|
|
13
|
+
}
|
package/dist/run-cases.js
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { resolveProjectInput } from "@penpair/core";
|
|
2
2
|
import { validateProviderEvidence } from "./evidence-validation.js";
|
|
3
|
-
import {
|
|
3
|
+
import { hashImageBytes, readImageEvidence } from "./image-evidence.js";
|
|
4
4
|
async function collectCaseEvidence(root, implementationCase, provider) {
|
|
5
5
|
if (!(Number.isFinite(implementationCase.baseline.scale) && implementationCase.baseline.scale > 0)) {
|
|
6
6
|
throw new Error("Missing capture scale");
|
|
7
7
|
}
|
|
8
|
-
const baselineImage = await
|
|
9
|
-
if (
|
|
8
|
+
const baselineImage = await readImageEvidence(root, implementationCase.baseline.path);
|
|
9
|
+
if (hashImageBytes(baselineImage.bytes) !== implementationCase.baseline.sha256) {
|
|
10
10
|
throw new Error("Approved baseline bytes changed");
|
|
11
11
|
}
|
|
12
12
|
const evidence = await provider.runCase(structuredClone(implementationCase));
|
|
13
13
|
validateProviderEvidence(evidence, implementationCase);
|
|
14
|
-
const implementationImage = await
|
|
14
|
+
const implementationImage = await readImageEvidence(root, evidence.screenshot);
|
|
15
15
|
const screenshotPath = await resolveProjectInput(root, evidence.screenshot);
|
|
16
16
|
const baselinePath = await resolveProjectInput(root, implementationCase.baseline.path);
|
|
17
17
|
if (screenshotPath === baselinePath) {
|
|
@@ -23,10 +23,10 @@ async function collectCaseEvidence(root, implementationCase, provider) {
|
|
|
23
23
|
implementationImage.height !== Math.round(viewport.height * scale) ||
|
|
24
24
|
baselineImage.width !== implementationImage.width ||
|
|
25
25
|
baselineImage.height !== implementationImage.height) {
|
|
26
|
-
throw new Error("Provider
|
|
26
|
+
throw new Error("Provider image scale/viewport differs from the design case");
|
|
27
27
|
}
|
|
28
|
-
const baselineAfterCapture = await
|
|
29
|
-
if (
|
|
28
|
+
const baselineAfterCapture = await readImageEvidence(root, implementationCase.baseline.path);
|
|
29
|
+
if (hashImageBytes(baselineAfterCapture.bytes) !== implementationCase.baseline.sha256) {
|
|
30
30
|
throw new Error("Baseline changed during provider execution");
|
|
31
31
|
}
|
|
32
32
|
return {
|
|
@@ -34,7 +34,7 @@ async function collectCaseEvidence(root, implementationCase, provider) {
|
|
|
34
34
|
complete: true,
|
|
35
35
|
withinThreshold: evidence.diffPixelRatio <= implementationCase.threshold,
|
|
36
36
|
evidence,
|
|
37
|
-
screenshotHash:
|
|
37
|
+
screenshotHash: hashImageBytes(implementationImage.bytes),
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
export async function runImplementationCases(root, plan, provider) {
|
package/dist/types.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export interface ProviderEvidence {
|
|
|
82
82
|
fontsReady: boolean;
|
|
83
83
|
baselineHash: string;
|
|
84
84
|
capture?: BrowserCaptureMetadata;
|
|
85
|
+
/** Independently captured static lossless WebP (preferred) or legacy PNG evidence. */
|
|
85
86
|
screenshot: string;
|
|
86
87
|
diffPixelRatio: number;
|
|
87
88
|
provider: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@penpair/reflection-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Explicit design case planning and verified implementation evidence for penpair baselines.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"jiti": "^2.4.2",
|
|
44
|
-
"@penpair/core": "0.
|
|
44
|
+
"@penpair/core": "0.4.0"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
47
|
"build": "tsc -p tsconfig.json",
|
package/dist/png-evidence.d.ts
DELETED
package/dist/png-evidence.js
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readFile, stat } from "node:fs/promises";
|
|
3
|
-
import { resolveProjectInput } from "@penpair/core";
|
|
4
|
-
export function hashPngBytes(bytes) {
|
|
5
|
-
return createHash("sha256").update(bytes).digest("hex");
|
|
6
|
-
}
|
|
7
|
-
export async function readPngEvidence(root, file) {
|
|
8
|
-
const input = await resolveProjectInput(root, file);
|
|
9
|
-
if ((await stat(input)).size > 64 * 1024 * 1024) {
|
|
10
|
-
throw new Error("Image exceeds 64 MiB");
|
|
11
|
-
}
|
|
12
|
-
const bytes = await readFile(input);
|
|
13
|
-
if (bytes.length < 24 ||
|
|
14
|
-
bytes.subarray(0, 8).toString("hex") !== "89504e470d0a1a0a" ||
|
|
15
|
-
bytes.subarray(12, 16).toString() !== "IHDR") {
|
|
16
|
-
throw new Error("Provider evidence must reference PNG bytes");
|
|
17
|
-
}
|
|
18
|
-
return {
|
|
19
|
-
bytes,
|
|
20
|
-
width: bytes.readUInt32BE(16),
|
|
21
|
-
height: bytes.readUInt32BE(20),
|
|
22
|
-
};
|
|
23
|
-
}
|