@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 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 PNG at the requested scale, and use the installed checker to compare it with the approved image. It returns:
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.png","diffPixelRatio":0.01,
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, PNG 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`.
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,6 @@
1
+ export declare function hashImageBytes(bytes: Buffer): string;
2
+ export declare function readImageEvidence(root: string, file: string): Promise<{
3
+ bytes: Buffer;
4
+ width: number;
5
+ height: number;
6
+ }>;
@@ -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 { hashPngBytes, readPngEvidence } from "./png-evidence.js";
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 readPngEvidence(root, implementationCase.baseline.path);
9
- if (hashPngBytes(baselineImage.bytes) !== implementationCase.baseline.sha256) {
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 readPngEvidence(root, evidence.screenshot);
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 PNG scale/viewport differs from the design case");
26
+ throw new Error("Provider image scale/viewport differs from the design case");
27
27
  }
28
- const baselineAfterCapture = await readPngEvidence(root, implementationCase.baseline.path);
29
- if (hashPngBytes(baselineAfterCapture.bytes) !== implementationCase.baseline.sha256) {
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: hashPngBytes(implementationImage.bytes),
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.6",
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.3.6"
44
+ "@penpair/core": "0.4.0"
45
45
  },
46
46
  "scripts": {
47
47
  "build": "tsc -p tsconfig.json",
@@ -1,6 +0,0 @@
1
- export declare function hashPngBytes(bytes: Buffer): string;
2
- export declare function readPngEvidence(root: string, file: string): Promise<{
3
- bytes: Buffer;
4
- width: number;
5
- height: number;
6
- }>;
@@ -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
- }