diff --git a/docs/sprints/3071-rfc-0001-baseline.harness-self-test.json b/docs/sprints/3071-rfc-0001-baseline.harness-self-test.json
new file mode 100644
index 0000000..cb2f67b
--- /dev/null
+++ b/docs/sprints/3071-rfc-0001-baseline.harness-self-test.json
@@ -0,0 +1,83 @@
+
+> @domscribe/test-fixtures@0.0.1 test:falsifier /tmp/wt-sprint-3071-task-a/packages/domscribe-test-fixtures
+> tsx styling/scripts/falsifier.ts
+
+{
+ "mode": "self-test",
+ "total": 10,
+ "passes": 10,
+ "fails": 0,
+ "oneShotRate": 1,
+ "annotations": [
+ {
+ "id": "A001",
+ "fixture": "tailwind",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A002",
+ "fixture": "tailwind",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A003",
+ "fixture": "tailwind",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A004",
+ "fixture": "tailwind",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A005",
+ "fixture": "tailwind",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A101",
+ "fixture": "styled",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A102",
+ "fixture": "styled",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A103",
+ "fixture": "styled",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A104",
+ "fixture": "styled",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ },
+ {
+ "id": "A105",
+ "fixture": "styled",
+ "passed": true,
+ "pixelDiffRatio": 0,
+ "diffPixels": 0
+ }
+ ]
+}
diff --git a/docs/sprints/3071-rfc-0001-baseline.md b/docs/sprints/3071-rfc-0001-baseline.md
new file mode 100644
index 0000000..911b64d
--- /dev/null
+++ b/docs/sprints/3071-rfc-0001-baseline.md
@@ -0,0 +1,100 @@
+# Sprint 3071 — RFC 0001 baseline + positioning verdict
+
+**Author:** Staff SWE (sprint run 3071, issue #51)
+**Date:** 2026-06-08
+**Decides:** the positioning language for `verify_after_edit` (RFC 0002).
+**Does not decide:** whether to ship `verify_after_edit` — that bet is made by the DOP memo and RFC 0002; this doc only sequences how it is framed.
+
+---
+
+## TL;DR
+
+| Quantity | Value | Source |
+| ----------------------------------------------------------- | --------------------------------------- | ----------------------------------------------- |
+| RFC 0001 falsifier (≥70% agent one-shot styling completion) | **unmeasured** | no agent-integration harness exists on `main` |
+| RFC 0001 mechanism self-test | **10/10 (100%), 0 pixel diff** | `styling/scripts/falsifier.ts --mode=self-test` |
+| Positioning verdict | **self-correction layer (<85% branch)** | conservative default in absence of measurement |
+| Slack alert (≥85% trigger) | **not posted** | threshold neither met nor measurable |
+
+The lift of the comparator into `@domscribe/verify` (this PR, Task A3) is independently validated by the self-test: the harness re-imports the comparator and continues to grade all 10 baselines at 0 pixel diff.
+
+## What the harness can measure today
+
+The RFC 0001 falsifier harness (`packages/domscribe-test-fixtures/styling/scripts/falsifier.ts`) supports three modes:
+
+1. **`self-test`** — builds the Tailwind and styled-components fixture apps, screenshots each annotation's `afterRoute`, and diffs against the committed baseline. Expected pass rate is **100% by design** — this is the harness's own correctness check, not a measurement of agent capability. The README is explicit:
+
+ > It does not invoke an agent. The agent-integration loop is built on top of this — see `--mode=measure`.
+
+2. **`record`** — re-captures the baseline PNGs from the canonical `/after` routes.
+
+3. **`measure --agent-output=
`** — production grading: reads one screenshot per annotation from an external directory (produced by an agent-integration harness) and diffs against the baseline. **This is the mode that would actually answer "what is the agent's one-shot styling completion rate?"**
+
+## What is missing
+
+The agent-integration loop required to run `--mode=measure` does **not** exist on `main`. Specifically, there is no harness that:
+
+- Reads each annotation from `styling/annotations.json`,
+- Drives an agent (Claude / Codex / similar) through the edit using the intent + source-file context,
+- Boots the fixture from the post-edit source,
+- Screenshots the rendered element into a per-annotation PNG,
+- Hands the directory to `falsifier.ts --mode=measure`.
+
+Until that loop exists, the inherited RFC 0001 falsifier (≥70% one-shot agent styling completion by sprint 2734+6) is **unmeasured**. The self-test pass rate is structurally **not** a substitute — the self-test screenshots the canonical-after route, not an agent's edit, so it cannot fall below 100% no matter how poorly an agent would perform.
+
+## Self-test result (mechanism-only)
+
+```
+mode=self-test, total=10, passes=10, fails=0, oneShotRate=1.0
+all annotations: pixelDiffRatio=0, diffPixels=0
+```
+
+Raw JSON: [`3071-rfc-0001-baseline.harness-self-test.json`](./3071-rfc-0001-baseline.harness-self-test.json).
+
+The 100% pass rate means:
+
+- The Vite build for both fixtures is reproducible.
+- Chromium + screenshot capture is locale/font/viewport-deterministic in this CI environment.
+- The lifted comparator in `@domscribe/verify` (this PR) diffs identically to the inline version it replaces — none of the 10 baseline diffs shifted off zero.
+
+The 100% pass rate **does not mean** the agent's one-shot styling completion rate is 100%. That number is unknown.
+
+## Methodology
+
+- **Where:** ephemeral dev sandbox; node v20.19.4; pnpm 9.12.0; playwright 1.58.2 (chrome-headless-shell 1208); locale `en-US`, timezone `UTC`, viewport `800×600`, scale 1, animations disabled (matches the harness defaults).
+- **Source:** worktree at `origin/main@a171724` (RFC 0001 Task B merge), plus the `@domscribe/verify` lift introduced by this PR.
+- **Command:** `pnpm --filter @domscribe/test-fixtures test:falsifier`.
+- **Reproducibility:** the same command on the same commit on a CI runner with the documented Playwright cache returns the same JSON. Re-recording baselines (`--mode=record`) would only be needed if the canonical-after routes or the Chromium build changes.
+
+## Positioning verdict
+
+Per RFC 0002 §Implications-for-PM and issue #51, the baseline gates how `verify_after_edit` is framed:
+
+- **≥85% → trust layer.** Verify catches the long tail; the build is conservative; PM may consider deferring relay registration (Task B) if capacity is tight.
+- **<85% → self-correction layer.** Verify is load-bearing for the value loop; the full build proceeds.
+
+The baseline is unmeasured. The conservative default in the absence of measurement is the **self-correction layer** branch — we cannot justify treating verify as a long-tail polish layer when we have no evidence the short tail is solved. The full build proceeds; the Slack alert (which fires only on ≥85%) is **not** posted.
+
+## What this means for Task B
+
+No change. Task B (runtime `ScreenshotCapturer` + relay `verify_after_edit` MCP tool) ships as planned, soft-recommended in MCP prompts, no lifecycle gate. The package-level value of `@domscribe/verify` is independent of the agent one-shot rate — the harness already consumes it, and the relay tool will consume it on the same contract.
+
+## Follow-up — agent-integration harness (next sprint)
+
+The cleanest way to retire this measurement gap is to add `--mode=agent` (or a separate driver script under `styling/scripts/`) that:
+
+1. For each annotation in `annotations.json`, spawns the agent under test with a fixed prompt (intent + sourceFile + sourceLine + the merged RFC 0001 `styleSource` + `componentStyles`).
+2. Applies the agent's edit to a scratch copy of the fixture, builds it, screenshots `afterRoute`.
+3. Writes `.png` into a deterministic agent-output directory.
+4. Invokes the existing `--mode=measure` with that directory.
+
+This is the prerequisite for measuring both the inherited RFC 0001 falsifier (≥70% one-shot) **and** the RFC 0002 falsifier (≥60% retry-resolution rate). Sized as a separate sprint task; out of scope for issue #51 (per the issue's "Out of scope" enumeration, which lists agent-side work as a P1 follow-up rather than in-scope).
+
+## References
+
+- [RFC 0001 — Two-tier component-style attribution](../rfcs/0001-component-styles-capture.md)
+- [RFC 0002 — Post-edit verification as an MCP diagnostic tool](../rfcs/0002-post-edit-verify-mcp-tool.md)
+- Issue [#51](https://github.com/patchorbit/domscribe/issues/51), Issue [#52](https://github.com/patchorbit/domscribe/issues/52)
+- PRs [#49](https://github.com/patchorbit/domscribe/pull/49), [#50](https://github.com/patchorbit/domscribe/pull/50) (RFC 0001 Tasks A and B)
+- Harness source: [`packages/domscribe-test-fixtures/styling/scripts/falsifier.ts`](../../packages/domscribe-test-fixtures/styling/scripts/falsifier.ts)
+- Harness README: [`packages/domscribe-test-fixtures/styling/README.md`](../../packages/domscribe-test-fixtures/styling/README.md)
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 1b6ac55..fd1622a 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -43,6 +43,19 @@ export default [
'scope:adapter',
],
},
+ {
+ // scope:test consumes the same packages adapters do — it
+ // grades them. Notably, `@domscribe/test-fixtures` now imports
+ // `@domscribe/verify` (scope:infra) so the harness and the
+ // relay verify_after_edit tool share one comparator.
+ sourceTag: 'scope:test',
+ onlyDependOnLibsWithTags: [
+ 'scope:core',
+ 'scope:infra',
+ 'scope:build',
+ 'scope:adapter',
+ ],
+ },
],
},
],
diff --git a/packages/domscribe-core/src/lib/migrations/annotation-migrations.spec.ts b/packages/domscribe-core/src/lib/migrations/annotation-migrations.spec.ts
index 27b6171..c389df8 100644
--- a/packages/domscribe-core/src/lib/migrations/annotation-migrations.spec.ts
+++ b/packages/domscribe-core/src/lib/migrations/annotation-migrations.spec.ts
@@ -141,4 +141,31 @@ describe('migrateAnnotation', () => {
expect(result.context.runtimeContext).toBeUndefined();
});
+
+ it('should migrate a v2 annotation up to v3 (additive verifyHistory, no field rewrite)', () => {
+ // Simulates a v2 annotation persisted between RFC 0001 (v1→v2) and
+ // RFC 0002 (v2→v3). The v2 → v3 step is purely additive (verifyHistory
+ // is a new optional field) — pre-existing runtimeContext data must
+ // survive untouched.
+ const raw = buildRawAnnotation({
+ metadata: { schemaVersion: 2 },
+ context: {
+ pageUrl: 'http://localhost:3000',
+ pageTitle: 'Test',
+ viewport: { width: 1920, height: 1080 },
+ userAgent: 'test-agent',
+ runtimeContext: {
+ componentStyles: { computed: { padding: '16px' } },
+ },
+ },
+ });
+
+ const result = migrateAnnotation(raw);
+
+ expect(result.metadata.schemaVersion).toBe(ANNOTATION_SCHEMA_VERSION);
+ expect(result.context.runtimeContext).toEqual({
+ componentStyles: { computed: { padding: '16px' } },
+ });
+ expect(result.context.verifyHistory).toBeUndefined();
+ });
});
diff --git a/packages/domscribe-core/src/lib/migrations/annotation-migrations.ts b/packages/domscribe-core/src/lib/migrations/annotation-migrations.ts
index 8ade356..8a851a1 100644
--- a/packages/domscribe-core/src/lib/migrations/annotation-migrations.ts
+++ b/packages/domscribe-core/src/lib/migrations/annotation-migrations.ts
@@ -31,6 +31,16 @@ const migrationSteps: Record) => void> =
1: () => {
// No-op: v1 → v2 is purely additive.
},
+ /**
+ * v2 → v3: additive only (per RFC 0002).
+ *
+ * v3 adds optional `context.verifyHistory` (an array of `VerifyResult`
+ * records emitted by the `verify_after_edit` MCP tool). The field is
+ * absent on v2 payloads, so no field rewriting is required.
+ */
+ 2: () => {
+ // No-op: v2 → v3 is purely additive.
+ },
};
/**
diff --git a/packages/domscribe-core/src/lib/types/annotation.spec.ts b/packages/domscribe-core/src/lib/types/annotation.spec.ts
new file mode 100644
index 0000000..ca08b62
--- /dev/null
+++ b/packages/domscribe-core/src/lib/types/annotation.spec.ts
@@ -0,0 +1,117 @@
+/**
+ * Schema tests for RFC 0002 additions to @domscribe/core.
+ *
+ * Covers the additive surface: VerifyResultSchema, AnnotationContext.verifyHistory,
+ * and the v3 schema-version bump. The pre-RFC 0002 annotation shape is exercised
+ * exhaustively in `annotation-migrations.spec.ts` and the wider integration
+ * suites; this spec is scoped to the new fields.
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ ANNOTATION_SCHEMA_VERSION,
+ AnnotationContextSchema,
+ VerifyResultSchema,
+ VerifyVerdictSchema,
+} from './annotation.js';
+
+describe('ANNOTATION_SCHEMA_VERSION', () => {
+ it('is at v3 (RFC 0002 — verifyHistory)', () => {
+ expect(ANNOTATION_SCHEMA_VERSION).toBe(3);
+ });
+});
+
+describe('VerifyVerdictSchema', () => {
+ it.each(['match', 'partial', 'no_change', 'regression'] as const)(
+ 'accepts %s',
+ (verdict) => {
+ expect(VerifyVerdictSchema.parse(verdict)).toBe(verdict);
+ },
+ );
+
+ it('rejects unknown verdicts', () => {
+ expect(() => VerifyVerdictSchema.parse('ok')).toThrow();
+ });
+});
+
+describe('VerifyResultSchema', () => {
+ it('parses a minimal match result (verdict + timestamp only)', () => {
+ const parsed = VerifyResultSchema.parse({
+ verdict: 'match',
+ timestamp: '2026-06-08T12:00:00.000Z',
+ });
+ expect(parsed.verdict).toBe('match');
+ expect(parsed.componentStylesDelta).toBeUndefined();
+ expect(parsed.screenshotRef).toBeUndefined();
+ });
+
+ it('parses a fully-populated partial result with all delta arrays', () => {
+ const parsed = VerifyResultSchema.parse({
+ verdict: 'partial',
+ timestamp: '2026-06-08T12:00:00.000Z',
+ pixelDiffRatio: 0.012,
+ componentStylesDelta: [
+ { property: 'padding', before: '16px', after: '24px' },
+ ],
+ computedStyleDelta: [
+ { property: 'background-color', before: null, after: 'rgb(0, 0, 0)' },
+ ],
+ boundingRectDelta: [{ field: 'height', before: 32, after: 40 }],
+ screenshotRef: 'blob://relay/ann_x/post-edit-1.png',
+ notes: 'padding matched intent; background-color regressed',
+ });
+ expect(parsed.componentStylesDelta).toHaveLength(1);
+ expect(parsed.boundingRectDelta?.[0]?.field).toBe('height');
+ expect(parsed.screenshotRef).toMatch(/^blob:\/\//);
+ });
+
+ it('rejects out-of-range pixelDiffRatio', () => {
+ expect(() =>
+ VerifyResultSchema.parse({
+ verdict: 'match',
+ timestamp: '2026-06-08T12:00:00.000Z',
+ pixelDiffRatio: 1.5,
+ }),
+ ).toThrow();
+ });
+
+ it('rejects unknown BoundingRectDelta fields', () => {
+ expect(() =>
+ VerifyResultSchema.parse({
+ verdict: 'partial',
+ timestamp: '2026-06-08T12:00:00.000Z',
+ boundingRectDelta: [
+ // @ts-expect-error — runtime rejection is the point
+ { field: 'depth', before: 0, after: 10 },
+ ],
+ }),
+ ).toThrow();
+ });
+});
+
+describe('AnnotationContextSchema.verifyHistory', () => {
+ it('accepts a context without verifyHistory (older clients silently ignore)', () => {
+ const parsed = AnnotationContextSchema.parse({
+ pageUrl: 'http://localhost:3000',
+ pageTitle: 'Test',
+ viewport: { width: 1920, height: 1080 },
+ userAgent: 'test-agent',
+ });
+ expect(parsed.verifyHistory).toBeUndefined();
+ });
+
+ it('accepts an append-only history of VerifyResults', () => {
+ const parsed = AnnotationContextSchema.parse({
+ pageUrl: 'http://localhost:3000',
+ pageTitle: 'Test',
+ viewport: { width: 1920, height: 1080 },
+ userAgent: 'test-agent',
+ verifyHistory: [
+ { verdict: 'partial', timestamp: '2026-06-08T12:00:00.000Z' },
+ { verdict: 'match', timestamp: '2026-06-08T12:00:05.000Z' },
+ ],
+ });
+ expect(parsed.verifyHistory).toHaveLength(2);
+ expect(parsed.verifyHistory?.[1]?.verdict).toBe('match');
+ });
+});
diff --git a/packages/domscribe-core/src/lib/types/annotation.ts b/packages/domscribe-core/src/lib/types/annotation.ts
index e41f38e..ce4ec50 100644
--- a/packages/domscribe-core/src/lib/types/annotation.ts
+++ b/packages/domscribe-core/src/lib/types/annotation.ts
@@ -103,6 +103,114 @@ export const AnnotationIdSchema = z
.regex(PATTERNS.ANNOTATION_ID)
.describe('Unique identifier: ann__');
+/**
+ * Verdict for a single post-edit verification call.
+ *
+ * - `match`: the post-edit state matches the user's intent (within the
+ * visual-diff threshold and the requested computed-style deltas).
+ * - `partial`: some requested deltas match and some do not — the agent
+ * should consult `componentStylesDelta` / `computedStyleDelta` and retry.
+ * - `no_change`: the comparator detected no visual or computed-style delta
+ * from the pre-edit baseline — the agent's edit did not affect the
+ * rendered element. Typically a wrong selector or a no-op edit.
+ * - `regression`: the post-edit state diverged from the baseline in a way
+ * that did not match the intent — the agent likely broke something.
+ */
+export const VerifyVerdictSchema = z.enum([
+ 'match',
+ 'partial',
+ 'no_change',
+ 'regression',
+]);
+
+/**
+ * Numeric delta between a baseline value and a post-edit value for a single
+ * `BoundingRect` field. Recorded only when a non-zero delta is observed.
+ */
+export const BoundingRectDeltaSchema = z.object({
+ field: z
+ .enum(['x', 'y', 'width', 'height', 'top', 'right', 'bottom', 'left'])
+ .describe('Which BoundingRect field changed'),
+ before: z.number().describe('Baseline value (px)'),
+ after: z.number().describe('Post-edit value (px)'),
+});
+
+/**
+ * Delta between baseline and post-edit values for a single style property.
+ * Used for both `componentStyles.computed` and `selectedElement.computedStyles`.
+ * Recorded only when the property's resolved value differs.
+ */
+export const StylePropertyDeltaSchema = z.object({
+ property: z.string().describe('CSS property name (e.g. "padding")'),
+ before: z
+ .string()
+ .nullable()
+ .describe(
+ 'Baseline resolved value; `null` when the property was absent before',
+ ),
+ after: z
+ .string()
+ .nullable()
+ .describe(
+ 'Post-edit resolved value; `null` when the property is absent after',
+ ),
+});
+
+/**
+ * Result of a single `verify_after_edit` MCP call (RFC 0002).
+ *
+ * Returned by the relay's verify tool to the agent so it can reason about
+ * its own edit on retry rather than re-guessing from source. Shape is
+ * deliberately structured (not a binary pass/fail) — the falsifier
+ * (≥60% retry-resolution rate) requires actionable deltas.
+ *
+ * All delta fields are optional; the verdict alone may be sufficient when
+ * the baseline matches exactly.
+ *
+ * `screenshotRef` is intentionally a relay-side blob reference, never inline
+ * bytes — preserves the 4 KB-per-element serialization budget from RFC 0001.
+ */
+export const VerifyResultSchema = z.object({
+ verdict: VerifyVerdictSchema.describe('Overall verification outcome'),
+ timestamp: z.string().describe('ISO 8601 timestamp of the verify call'),
+ pixelDiffRatio: z
+ .number()
+ .min(0)
+ .max(1)
+ .optional()
+ .describe(
+ 'Fraction of pixels that differ between the post-edit screenshot and the pre-edit baseline (0 = identical)',
+ ),
+ componentStylesDelta: z
+ .array(StylePropertyDeltaSchema)
+ .optional()
+ .describe(
+ 'Per-property deltas on the runtime `componentStyles.computed` allowlist (RFC 0001)',
+ ),
+ computedStyleDelta: z
+ .array(StylePropertyDeltaSchema)
+ .optional()
+ .describe(
+ 'Per-property deltas on `selectedElement.computedStyles` outside the componentStyles allowlist',
+ ),
+ boundingRectDelta: z
+ .array(BoundingRectDeltaSchema)
+ .optional()
+ .describe('Per-field deltas on the element bounding rectangle'),
+ screenshotRef: z
+ .string()
+ .optional()
+ .describe(
+ 'Relay-side blob reference for the post-edit element screenshot (never inlined bytes)',
+ ),
+ notes: z
+ .string()
+ .optional()
+ .describe(
+ 'Human-readable reason or comparator note (e.g. "baseline missing")',
+ ),
+});
+
/**
* Current annotation schema version. Bump when the Annotation shape changes.
*
@@ -111,8 +219,11 @@ export const AnnotationIdSchema = z
* - v2: added optional `runtimeContext.componentStyles` (computed-style
* allowlist + CSS custom properties) and optional `styleSource` on
* embedded `manifestSnapshot` entries, per RFC 0001.
+ * - v3: added optional `context.verifyHistory` (an array of `VerifyResult`
+ * records from `verify_after_edit` MCP calls), per RFC 0002. Older
+ * clients silently ignore the field.
*/
-export const ANNOTATION_SCHEMA_VERSION = 2;
+export const ANNOTATION_SCHEMA_VERSION = 3;
export const AnnotationMetadataSchema = z.object({
id: AnnotationIdSchema,
@@ -193,6 +304,12 @@ export const AnnotationContextSchema = z.object({
runtimeContext: RuntimeContextSchema.optional().describe(
'Runtime context (Phase 1 & 2 features)',
),
+ verifyHistory: z
+ .array(VerifyResultSchema)
+ .optional()
+ .describe(
+ 'Append-only history of `verify_after_edit` calls for this annotation (RFC 0002). Optional — absent on annotations that were not verified, and silently ignored by pre-v3 clients.',
+ ),
});
export const AgentResponseSchema = z.object({
@@ -240,6 +357,10 @@ export type Viewport = z.infer;
export type Environment = z.infer;
export type RuntimeContext = z.infer;
export type ComponentStyles = z.infer;
+export type VerifyVerdict = z.infer;
+export type VerifyResult = z.infer;
+export type StylePropertyDelta = z.infer;
+export type BoundingRectDelta = z.infer;
/**
* Allowlist of computed-style property names captured by the runtime
diff --git a/packages/domscribe-test-fixtures/package.json b/packages/domscribe-test-fixtures/package.json
index be382e5..f3c5923 100644
--- a/packages/domscribe-test-fixtures/package.json
+++ b/packages/domscribe-test-fixtures/package.json
@@ -9,13 +9,12 @@
"test:falsifier": "tsx styling/scripts/falsifier.ts",
"test:falsifier:record": "tsx styling/scripts/falsifier.ts --mode=record"
},
+ "dependencies": {
+ "@domscribe/verify": "workspace:*"
+ },
"devDependencies": {
"@playwright/test": "^1.49.0",
- "@types/pixelmatch": "^5.2.6",
- "@types/pngjs": "^6.0.5",
- "pixelmatch": "^7.1.0",
"playwright": "^1.49.0",
- "pngjs": "^7.0.0",
"tsx": "^4.21.0"
}
}
diff --git a/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts b/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts
index 1ef0d39..55ad402 100644
--- a/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts
+++ b/packages/domscribe-test-fixtures/styling/scripts/falsifier.ts
@@ -46,32 +46,16 @@ import path from 'node:path';
import { spawn, type ChildProcess } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { chromium, type Browser, type Page } from 'playwright';
-import pixelmatch from 'pixelmatch';
-import { PNG } from 'pngjs';
+import { MAX_DIFF_RATIO, diff, loadPng } from '@domscribe/verify';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const STYLING_ROOT = path.resolve(__dirname, '..');
const BASELINES_ROOT = path.join(STYLING_ROOT, 'baselines');
const ANNOTATIONS_FILE = path.join(STYLING_ROOT, 'annotations.json');
-/**
- * Pixel-diff tolerance.
- *
- * PER_PIXEL_THRESHOLD — pixelmatch's `threshold` (color distance per
- * pixel below which two pixels are considered equal). 0.1 is the
- * library's recommended starting point; we keep it modest so the
- * harness catches real visual deltas but tolerates AA jitter.
- *
- * MAX_DIFF_RATIO — the fraction of total pixels that may differ before
- * we call the annotation a fail. The canonical-after path diffs at 0,
- * so this is a defensive floor for CI worker AA jitter on text glyphs.
- * 0.1% (0.001) is tight enough that two images that happen to share
- * a mostly-white background (a real false-positive risk we observed in
- * sanity testing) cannot slip through, while still absorbing a few
- * pixels of subpixel font rendering noise.
- */
-const PER_PIXEL_THRESHOLD = 0.1;
-const MAX_DIFF_RATIO = 0.001;
+// Pixel-diff tolerance (`PER_PIXEL_THRESHOLD`, `MAX_DIFF_RATIO`) lives in
+// `@domscribe/verify` so the falsifier and the relay verify_after_edit
+// MCP tool share one comparator.
const VIEWPORT = { width: 800, height: 600 };
@@ -270,27 +254,6 @@ async function screenshotRoute(
});
}
-function loadPng(buf: Buffer): PNG {
- return PNG.sync.read(buf);
-}
-
-function diff(a: PNG, b: PNG): { diffPixels: number; ratio: number } {
- if (a.width !== b.width || a.height !== b.height) {
- return {
- diffPixels: a.width * a.height,
- ratio: 1,
- };
- }
- const out = new PNG({ width: a.width, height: a.height });
- const diffPixels = pixelmatch(a.data, b.data, out.data, a.width, a.height, {
- threshold: PER_PIXEL_THRESHOLD,
- });
- return {
- diffPixels,
- ratio: diffPixels / (a.width * a.height),
- };
-}
-
interface BrowserContext {
browser: Browser;
page: Page;
diff --git a/packages/domscribe-verify/README.md b/packages/domscribe-verify/README.md
new file mode 100644
index 0000000..affc192
--- /dev/null
+++ b/packages/domscribe-verify/README.md
@@ -0,0 +1,30 @@
+# @domscribe/verify
+
+Pure-TS visual-snapshot comparator for Domscribe.
+
+The comparator is **lifted verbatim** from the RFC 0001 falsifier harness
+in `@domscribe/test-fixtures`. It compares two PNG screenshot buffers
+pixel-by-pixel via [`pixelmatch`](https://github.com/mapbox/pixelmatch)
+and returns a structured delta. No DOM, no browser — runnable from Node
+CI **and** the relay runtime.
+
+## Install
+
+```bash
+npm install @domscribe/verify
+```
+
+## Note
+
+Internal package used by `@domscribe/test-fixtures` (the RFC 0001
+styling falsifier) and `@domscribe/relay` (the RFC 0002
+`verify_after_edit` MCP tool). You probably don't need to install this
+directly.
+
+## Links
+
+Part of [Domscribe](https://github.com/patchorbit/domscribe).
+
+## License
+
+MIT
diff --git a/packages/domscribe-verify/package.json b/packages/domscribe-verify/package.json
new file mode 100644
index 0000000..4b9d5a3
--- /dev/null
+++ b/packages/domscribe-verify/package.json
@@ -0,0 +1,27 @@
+{
+ "name": "@domscribe/verify",
+ "version": "0.5.2",
+ "description": "Pure-TS visual-snapshot comparator for Domscribe. Lifted verbatim from the RFC 0001 falsifier harness; powers both the test-fixtures suite and the verify_after_edit MCP tool (RFC 0002).",
+ "type": "module",
+ "main": "src/index.ts",
+ "publishConfig": {
+ "access": "restricted"
+ },
+ "dependencies": {
+ "@domscribe/core": "workspace:*",
+ "pixelmatch": "^7.1.0",
+ "pngjs": "^7.0.0"
+ },
+ "devDependencies": {
+ "@types/pixelmatch": "^5.2.6",
+ "@types/pngjs": "^6.0.5"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/patchorbit/domscribe.git",
+ "directory": "packages/domscribe-verify"
+ }
+}
diff --git a/packages/domscribe-verify/project.json b/packages/domscribe-verify/project.json
new file mode 100644
index 0000000..b9bf83f
--- /dev/null
+++ b/packages/domscribe-verify/project.json
@@ -0,0 +1,49 @@
+{
+ "name": "domscribe-verify",
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
+ "projectType": "library",
+ "sourceRoot": "packages/domscribe-verify/src",
+ "tags": ["scope:infra", "type:lib", "type:test"],
+ "targets": {
+ "build": {
+ "executor": "@nx/js:tsc",
+ "outputs": ["{workspaceRoot}/dist/packages/domscribe-verify"],
+ "options": {
+ "rootDir": "packages/domscribe-verify/src",
+ "outputPath": "dist/packages/domscribe-verify",
+ "main": "packages/domscribe-verify/src/index.ts",
+ "tsConfig": "packages/domscribe-verify/tsconfig.lib.json",
+ "generatePackageJson": true,
+ "generateExportsField": true,
+ "assets": ["packages/domscribe-verify/*.md"]
+ }
+ },
+ "lint": {
+ "executor": "@nx/eslint:lint",
+ "outputs": ["{options.outputFile}"],
+ "options": {
+ "eslintConfig": "eslint.config.mjs",
+ "lintFilePatterns": [
+ "packages/domscribe-verify/**/*.ts",
+ "packages/domscribe-verify/**/*.tsx"
+ ]
+ }
+ },
+ "test": {
+ "executor": "@nx/vitest:test",
+ "outputs": ["{projectRoot}/test-output"],
+ "options": {
+ "config": "packages/domscribe-verify/vite.config.ts"
+ }
+ },
+ "typecheck": {
+ "executor": "nx:noop"
+ },
+ "watch-deps": {
+ "executor": "nx:noop"
+ },
+ "build-deps": {
+ "executor": "nx:noop"
+ }
+ }
+}
diff --git a/packages/domscribe-verify/src/index.ts b/packages/domscribe-verify/src/index.ts
new file mode 100644
index 0000000..8cfa8be
--- /dev/null
+++ b/packages/domscribe-verify/src/index.ts
@@ -0,0 +1,19 @@
+/**
+ * @domscribe/verify - Pure-TS visual-snapshot comparator for Domscribe.
+ *
+ * Originally lifted verbatim from the RFC 0001 falsifier harness in
+ * `@domscribe/test-fixtures`; productized as a standalone package so the
+ * harness, the relay (RFC 0002 `verify_after_edit` MCP tool), and any
+ * downstream CI consumer share one implementation.
+ *
+ * @module @domscribe/verify
+ */
+
+export {
+ MAX_DIFF_RATIO,
+ PER_PIXEL_THRESHOLD,
+ compareScreenshots,
+ diff,
+ loadPng,
+} from './lib/comparator.js';
+export type { DiffResult } from './lib/comparator.js';
diff --git a/packages/domscribe-verify/src/lib/comparator.spec.ts b/packages/domscribe-verify/src/lib/comparator.spec.ts
new file mode 100644
index 0000000..b59e810
--- /dev/null
+++ b/packages/domscribe-verify/src/lib/comparator.spec.ts
@@ -0,0 +1,123 @@
+/**
+ * Unit tests for the visual-snapshot comparator.
+ *
+ * The comparator is the only behaviour @domscribe/verify owns; these tests
+ * pin the contract the RFC 0001 falsifier and (future) RFC 0002
+ * verify_after_edit MCP tool depend on.
+ */
+
+import { describe, it, expect } from 'vitest';
+import { PNG } from 'pngjs';
+import {
+ MAX_DIFF_RATIO,
+ PER_PIXEL_THRESHOLD,
+ compareScreenshots,
+ diff,
+ loadPng,
+} from './comparator.js';
+
+function makePng(
+ width: number,
+ height: number,
+ fill: [number, number, number, number],
+): Buffer {
+ const png = new PNG({ width, height });
+ for (let i = 0; i < png.data.length; i += 4) {
+ png.data[i] = fill[0];
+ png.data[i + 1] = fill[1];
+ png.data[i + 2] = fill[2];
+ png.data[i + 3] = fill[3];
+ }
+ return PNG.sync.write(png);
+}
+
+function paintRect(
+ buf: Buffer,
+ rect: { x: number; y: number; w: number; h: number },
+ rgba: [number, number, number, number],
+): Buffer {
+ const png = PNG.sync.read(buf);
+ for (let y = rect.y; y < rect.y + rect.h; y++) {
+ for (let x = rect.x; x < rect.x + rect.w; x++) {
+ const i = (y * png.width + x) * 4;
+ png.data[i] = rgba[0];
+ png.data[i + 1] = rgba[1];
+ png.data[i + 2] = rgba[2];
+ png.data[i + 3] = rgba[3];
+ }
+ }
+ return PNG.sync.write(png);
+}
+
+describe('comparator constants', () => {
+ it('keeps the falsifier defaults (PER_PIXEL_THRESHOLD = 0.1, MAX_DIFF_RATIO = 0.001)', () => {
+ // These defaults are load-bearing: every committed RFC 0001 baseline
+ // was recorded against them. Bumping them silently would shift CI
+ // verdicts on already-merged fixture annotations.
+ expect(PER_PIXEL_THRESHOLD).toBe(0.1);
+ expect(MAX_DIFF_RATIO).toBe(0.001);
+ });
+});
+
+describe('diff', () => {
+ it('returns zero diff for two pixel-identical PNGs', () => {
+ const a = loadPng(makePng(20, 20, [255, 0, 0, 255]));
+ const b = loadPng(makePng(20, 20, [255, 0, 0, 255]));
+ expect(diff(a, b)).toEqual({ diffPixels: 0, ratio: 0 });
+ });
+
+ it('reports a small ratio for a localised pixel change', () => {
+ const baseline = makePng(20, 20, [255, 255, 255, 255]);
+ const altered = paintRect(
+ baseline,
+ { x: 0, y: 0, w: 2, h: 2 },
+ [0, 0, 0, 255],
+ );
+ const result = diff(loadPng(altered), loadPng(baseline));
+ // 4 changed pixels out of 400 = 1% — well above MAX_DIFF_RATIO.
+ expect(result.diffPixels).toBe(4);
+ expect(result.ratio).toBeCloseTo(0.01, 5);
+ });
+
+ it('short-circuits to ratio = 1 on dimension mismatch (no throw)', () => {
+ const a = loadPng(makePng(10, 10, [0, 0, 0, 255]));
+ const b = loadPng(makePng(20, 20, [0, 0, 0, 255]));
+ expect(diff(a, b)).toEqual({ diffPixels: 100, ratio: 1 });
+ });
+});
+
+describe('compareScreenshots', () => {
+ it('passes when actual equals baseline', () => {
+ const buf = makePng(20, 20, [10, 20, 30, 255]);
+ const result = compareScreenshots(buf, buf);
+ expect(result.passed).toBe(true);
+ expect(result.ratio).toBe(0);
+ });
+
+ it('fails when the change exceeds the default tolerance', () => {
+ const baseline = makePng(20, 20, [255, 255, 255, 255]);
+ const altered = paintRect(
+ baseline,
+ { x: 0, y: 0, w: 4, h: 4 },
+ [0, 0, 0, 255],
+ );
+ const result = compareScreenshots(altered, baseline);
+ // 16/400 = 4%, well above 0.1% tolerance.
+ expect(result.passed).toBe(false);
+ });
+
+ it('honours a custom maxDiffRatio override', () => {
+ const baseline = makePng(40, 40, [255, 255, 255, 255]);
+ // Change 4 pixels out of 1600 = 0.25%. Default tolerance (0.1%) fails;
+ // a relaxed 1% tolerance passes.
+ const altered = paintRect(
+ baseline,
+ { x: 0, y: 0, w: 2, h: 2 },
+ [0, 0, 0, 255],
+ );
+ expect(compareScreenshots(altered, baseline).passed).toBe(false);
+ expect(
+ compareScreenshots(altered, baseline, { maxDiffRatio: 0.01 }).passed,
+ ).toBe(true);
+ });
+});
diff --git a/packages/domscribe-verify/src/lib/comparator.ts b/packages/domscribe-verify/src/lib/comparator.ts
new file mode 100644
index 0000000..ea3c2f8
--- /dev/null
+++ b/packages/domscribe-verify/src/lib/comparator.ts
@@ -0,0 +1,105 @@
+/**
+ * Pixel-diff comparator. Lifted verbatim from the RFC 0001 falsifier
+ * harness in `@domscribe/test-fixtures` (originally
+ * `styling/scripts/falsifier.ts`).
+ *
+ * The numeric defaults (`PER_PIXEL_THRESHOLD`, `MAX_DIFF_RATIO`) and the
+ * `loadPng` / `diff` semantics are unchanged so the existing styling-
+ * fixture baselines stay valid and CI behaviour does not shift on this
+ * package lift.
+ *
+ * @module @domscribe/verify/lib/comparator
+ */
+
+import pixelmatch from 'pixelmatch';
+import { PNG } from 'pngjs';
+
+/**
+ * Pixel-diff tolerance.
+ *
+ * PER_PIXEL_THRESHOLD — pixelmatch's `threshold` (color distance per
+ * pixel below which two pixels are considered equal). 0.1 is the
+ * library's recommended starting point; we keep it modest so the
+ * harness catches real visual deltas but tolerates AA jitter.
+ *
+ * MAX_DIFF_RATIO — the fraction of total pixels that may differ before
+ * we call the annotation a fail. The canonical-after path diffs at 0,
+ * so this is a defensive floor for CI worker AA jitter on text glyphs.
+ * 0.1% (0.001) is tight enough that two images that happen to share
+ * a mostly-white background (a real false-positive risk we observed in
+ * sanity testing) cannot slip through, while still absorbing a few
+ * pixels of subpixel font rendering noise.
+ */
+export const PER_PIXEL_THRESHOLD = 0.1;
+export const MAX_DIFF_RATIO = 0.001;
+
+/**
+ * Decode a PNG buffer into a `PNG` instance. Thin wrapper kept exported so
+ * callers can pre-decode and reuse the structure across multiple diffs.
+ */
+export function loadPng(buf: Buffer): PNG {
+ return PNG.sync.read(buf);
+}
+
+/**
+ * Result of comparing two decoded PNGs.
+ *
+ * `diffPixels` is the absolute count of pixels that exceed
+ * `PER_PIXEL_THRESHOLD`; `ratio` is `diffPixels / totalPixels` in `[0, 1]`.
+ *
+ * When the two PNGs have different dimensions the comparator returns
+ * `ratio = 1` and `diffPixels = a.width * a.height` — i.e. "everything
+ * differs," matching the falsifier's existing behaviour.
+ */
+export interface DiffResult {
+ diffPixels: number;
+ ratio: number;
+}
+
+/**
+ * Pixel-diff two decoded PNGs using `pixelmatch` with `PER_PIXEL_THRESHOLD`.
+ *
+ * Dimension-mismatched images short-circuit to `ratio = 1` rather than
+ * throwing, so a baseline-vs-agent-screenshot size mismatch reports as a
+ * failed verify rather than crashing the harness or the relay tool.
+ */
+export function diff(a: PNG, b: PNG): DiffResult {
+ if (a.width !== b.width || a.height !== b.height) {
+ return {
+ diffPixels: a.width * a.height,
+ ratio: 1,
+ };
+ }
+ const out = new PNG({ width: a.width, height: a.height });
+ const diffPixels = pixelmatch(a.data, b.data, out.data, a.width, a.height, {
+ threshold: PER_PIXEL_THRESHOLD,
+ });
+ return {
+ diffPixels,
+ ratio: diffPixels / (a.width * a.height),
+ };
+}
+
+/**
+ * One-shot convenience for the common "compare two screenshots" call site.
+ *
+ * Decodes both buffers and runs `diff`. Use the lower-level `loadPng` +
+ * `diff` pair when the same baseline is compared against many candidates.
+ *
+ * Returns the `DiffResult` plus a `passed` flag against the package-default
+ * `MAX_DIFF_RATIO`. Pass a stricter `maxDiffRatio` (e.g. `0.0001`) to the
+ * options to tighten on a per-call basis — e.g. the RFC 0002 retry round
+ * documents a tighter tolerance than the first-attempt round.
+ */
+export function compareScreenshots(
+ actual: Buffer,
+ baseline: Buffer,
+ options: { maxDiffRatio?: number } = {},
+): DiffResult & { passed: boolean } {
+ const maxDiffRatio = options.maxDiffRatio ?? MAX_DIFF_RATIO;
+ const result = diff(loadPng(actual), loadPng(baseline));
+ return {
+ ...result,
+ passed: result.ratio <= maxDiffRatio,
+ };
+}
diff --git a/packages/domscribe-verify/tsconfig.json b/packages/domscribe-verify/tsconfig.json
new file mode 100644
index 0000000..c823650
--- /dev/null
+++ b/packages/domscribe-verify/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "files": [],
+ "include": [],
+ "references": [
+ { "path": "./tsconfig.lib.json" },
+ { "path": "./tsconfig.spec.json" }
+ ]
+}
diff --git a/packages/domscribe-verify/tsconfig.lib.json b/packages/domscribe-verify/tsconfig.lib.json
new file mode 100644
index 0000000..f35ab9e
--- /dev/null
+++ b/packages/domscribe-verify/tsconfig.lib.json
@@ -0,0 +1,40 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "tsBuildInfoFile": "../../dist/packages/domscribe-verify/tsconfig.tsbuildinfo",
+ "lib": [
+ "es2024",
+ "ESNext.Array",
+ "ESNext.Collection",
+ "ESNext.Iterator",
+ "ESNext.Promise"
+ ],
+ "target": "es2024",
+ "module": "nodenext",
+ "moduleResolution": "nodenext"
+ },
+ "include": ["src/**/*.ts"],
+ "exclude": [
+ "vite.config.ts",
+ "vite.config.mts",
+ "vitest.config.ts",
+ "vitest.config.mts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx",
+ "**/dist",
+ "**/build",
+ "**/.next",
+ "**/.turbo",
+ "**/coverage",
+ "**/generated",
+ "**/tmp",
+ "**/test-output",
+ "**/.nx"
+ ]
+}
diff --git a/packages/domscribe-verify/tsconfig.spec.json b/packages/domscribe-verify/tsconfig.spec.json
new file mode 100644
index 0000000..fa98b71
--- /dev/null
+++ b/packages/domscribe-verify/tsconfig.spec.json
@@ -0,0 +1,35 @@
+{
+ "extends": "../../tsconfig.base.json",
+ "compilerOptions": {
+ "types": [
+ "vitest/globals",
+ "vitest/importMeta",
+ "vite/client",
+ "vitest",
+ "node"
+ ],
+ "tsBuildInfoFile": "../../dist/packages/domscribe-verify/tsconfig.spec.tsbuildinfo",
+ "module": "nodenext",
+ "moduleResolution": "nodenext"
+ },
+ "include": [
+ "vite.config.ts",
+ "vite.config.mts",
+ "vitest.config.ts",
+ "vitest.config.mts",
+ "src/**/*.test.ts",
+ "src/**/*.spec.ts",
+ "src/**/*.test.tsx",
+ "src/**/*.spec.tsx",
+ "src/**/*.test.js",
+ "src/**/*.spec.js",
+ "src/**/*.test.jsx",
+ "src/**/*.spec.jsx",
+ "src/**/*.d.ts"
+ ],
+ "references": [
+ {
+ "path": "./tsconfig.lib.json"
+ }
+ ]
+}
diff --git a/packages/domscribe-verify/vite.config.ts b/packages/domscribe-verify/vite.config.ts
new file mode 100644
index 0000000..4d21373
--- /dev/null
+++ b/packages/domscribe-verify/vite.config.ts
@@ -0,0 +1,28 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ name: '@domscribe/verify',
+ watch: false,
+ globals: true,
+ environment: 'node',
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
+ reporters: ['default'],
+ outputFile: './test-output/vitest/report.json',
+ coverage: {
+ enabled: true,
+ provider: 'v8',
+ reporter: ['text', 'json-summary'],
+ reportsDirectory: './test-output/vitest/coverage',
+ thresholds: {
+ lines: 0.8,
+ functions: 0.8,
+ branches: 0.7,
+ statements: 0.8,
+ },
+ },
+ typecheck: {
+ tsconfig: './tsconfig.spec.json',
+ },
+ },
+});
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c4b9387..88a1b49 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -295,25 +295,17 @@ importers:
version: 12.10.3
packages/domscribe-test-fixtures:
+ dependencies:
+ '@domscribe/verify':
+ specifier: workspace:*
+ version: link:../domscribe-verify
devDependencies:
'@playwright/test':
specifier: ^1.49.0
version: 1.58.2
- '@types/pixelmatch':
- specifier: ^5.2.6
- version: 5.2.6
- '@types/pngjs':
- specifier: ^6.0.5
- version: 6.0.5
- pixelmatch:
- specifier: ^7.1.0
- version: 7.2.0
playwright:
specifier: ^1.49.0
version: 1.58.2
- pngjs:
- specifier: ^7.0.0
- version: 7.0.0
tsx:
specifier: ^4.21.0
version: 4.21.0
@@ -432,6 +424,25 @@ importers:
specifier: ^5.102.0
version: 5.102.0(@swc/core@1.15.8(@swc/helpers@0.5.19))
+ packages/domscribe-verify:
+ dependencies:
+ '@domscribe/core':
+ specifier: workspace:*
+ version: link:../domscribe-core
+ pixelmatch:
+ specifier: ^7.1.0
+ version: 7.2.0
+ pngjs:
+ specifier: ^7.0.0
+ version: 7.0.0
+ devDependencies:
+ '@types/pixelmatch':
+ specifier: ^5.2.6
+ version: 5.2.6
+ '@types/pngjs':
+ specifier: ^6.0.5
+ version: 6.0.5
+
packages/domscribe-vue:
dependencies:
'@domscribe/core':
diff --git a/tsconfig.base.json b/tsconfig.base.json
index abacbdc..2af4fae 100644
--- a/tsconfig.base.json
+++ b/tsconfig.base.json
@@ -34,6 +34,8 @@
"@domscribe/transform/*": ["packages/domscribe-transform/src/*"],
"@domscribe/manifest": ["packages/domscribe-manifest/src/index.ts"],
"@domscribe/manifest/*": ["packages/domscribe-manifest/src/*"],
+ "@domscribe/verify": ["packages/domscribe-verify/src/index.ts"],
+ "@domscribe/verify/*": ["packages/domscribe-verify/src/*"],
"@domscribe/vue": ["packages/domscribe-vue/src/index.ts"],
"@domscribe/vue/*": ["packages/domscribe-vue/src/*"],
"@domscribe/nuxt": ["packages/domscribe-nuxt/src/index.ts"],
diff --git a/tsconfig.json b/tsconfig.json
index acd8100..7acec16 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -5,6 +5,7 @@
"references": [
{ "path": "./packages/domscribe-core" },
{ "path": "./packages/domscribe-manifest" },
+ { "path": "./packages/domscribe-verify" },
{ "path": "./packages/domscribe-relay" },
{ "path": "./packages/domscribe-runtime" },
{ "path": "./packages/domscribe-overlay" },