@juxtapose/core 0.2.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.
Files changed (57) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +17 -0
  3. package/dist/agent/handoff.d.ts +30 -0
  4. package/dist/agent/handoff.js +121 -0
  5. package/dist/agent/handoff.js.map +1 -0
  6. package/dist/analysis/affected.d.ts +19 -0
  7. package/dist/analysis/affected.js +132 -0
  8. package/dist/analysis/affected.js.map +1 -0
  9. package/dist/baseline/git-store.d.ts +26 -0
  10. package/dist/baseline/git-store.js +95 -0
  11. package/dist/baseline/git-store.js.map +1 -0
  12. package/dist/baseline/status.d.ts +22 -0
  13. package/dist/baseline/status.js +49 -0
  14. package/dist/baseline/status.js.map +1 -0
  15. package/dist/capture/capture.d.ts +32 -0
  16. package/dist/capture/capture.js +153 -0
  17. package/dist/capture/capture.js.map +1 -0
  18. package/dist/capture/launch.d.ts +35 -0
  19. package/dist/capture/launch.js +56 -0
  20. package/dist/capture/launch.js.map +1 -0
  21. package/dist/capture/pool.d.ts +42 -0
  22. package/dist/capture/pool.js +109 -0
  23. package/dist/capture/pool.js.map +1 -0
  24. package/dist/capture/settle.d.ts +50 -0
  25. package/dist/capture/settle.js +162 -0
  26. package/dist/capture/settle.js.map +1 -0
  27. package/dist/diff/diff.d.ts +25 -0
  28. package/dist/diff/diff.js +86 -0
  29. package/dist/diff/diff.js.map +1 -0
  30. package/dist/diff/regions.d.ts +8 -0
  31. package/dist/diff/regions.js +90 -0
  32. package/dist/diff/regions.js.map +1 -0
  33. package/dist/git/ancestry.d.ts +5 -0
  34. package/dist/git/ancestry.js +43 -0
  35. package/dist/git/ancestry.js.map +1 -0
  36. package/dist/index.d.ts +17 -0
  37. package/dist/index.js +16 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/platform.d.ts +83 -0
  40. package/dist/platform.js +5 -0
  41. package/dist/platform.js.map +1 -0
  42. package/dist/report/comment.d.ts +16 -0
  43. package/dist/report/comment.js +99 -0
  44. package/dist/report/comment.js.map +1 -0
  45. package/dist/report/github.d.ts +28 -0
  46. package/dist/report/github.js +118 -0
  47. package/dist/report/github.js.map +1 -0
  48. package/dist/run/run.d.ts +30 -0
  49. package/dist/run/run.js +120 -0
  50. package/dist/run/run.js.map +1 -0
  51. package/dist/types.d.ts +137 -0
  52. package/dist/types.js +4 -0
  53. package/dist/types.js.map +1 -0
  54. package/dist/url-platform.d.ts +10 -0
  55. package/dist/url-platform.js +22 -0
  56. package/dist/url-platform.js.map +1 -0
  57. package/package.json +42 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Magic Factory LLC
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,17 @@
1
+ # @juxtapose/core
2
+
3
+ Platform-agnostic capture, diff, baseline and analysis core for juxtapose.
4
+
5
+ Part of [juxtapose](https://github.com/juxtapose-dev/juxtapose), visual
6
+ regression testing for component workbenches.
7
+
8
+ **You probably don't need to install this directly.** Use
9
+ [`@juxtapose/cli`](https://www.npmjs.com/package/@juxtapose/cli) for the
10
+ command line, or
11
+ [`@juxtapose/storybook`](https://www.npmjs.com/package/@juxtapose/storybook)
12
+ to review diffs inside Storybook. This package is a building block those
13
+ depend on.
14
+
15
+ Full documentation: https://github.com/juxtapose-dev/juxtapose
16
+
17
+ MIT © Magic Factory LLC
@@ -0,0 +1,30 @@
1
+ import type { ChangedRegion, DiffStatus, Viewport } from '../types.js';
2
+ export interface HandoffPayload {
3
+ storyId: string;
4
+ storyTitle: string;
5
+ status: DiffStatus;
6
+ mismatchPercent: number;
7
+ mismatchPixels: number;
8
+ changedRegions: ChangedRegion[];
9
+ viewport: Viewport;
10
+ /** Absolute path to the story's source file, when known. */
11
+ componentPath: string;
12
+ /** Absolute path to the machine-readable sidecar for this diff. */
13
+ diffDataPath?: string;
14
+ images: {
15
+ baseline?: string;
16
+ current?: string;
17
+ diff?: string;
18
+ };
19
+ /** So a session running in the wrong repo can detect the mismatch. */
20
+ projectRoot: string;
21
+ }
22
+ export declare function buildHandoffPrompt(p: HandoffPayload): string;
23
+ /**
24
+ * Describe a region in language a model reasons about well.
25
+ *
26
+ * Raw coordinates are precise but hard to reason over; "top-left, 2.3% of the
27
+ * viewport" carries the spatial meaning that actually drives the judgement.
28
+ * Both are given.
29
+ */
30
+ export declare function describeRegion(r: ChangedRegion, viewport: Viewport): string;
@@ -0,0 +1,121 @@
1
+ // Turn a failing story into a prompt an agent can act on.
2
+ //
3
+ // The central bet, inherited from the original tool and worth restating:
4
+ // send STRUCTURED DATA, not screenshots. `mismatchPercent: 3.72` and a box at
5
+ // (412, 88) are exact; an LLM's spatial reasoning over a PNG is probabilistic
6
+ // and costs 10-50x the tokens. Images are listed last, as a fallback.
7
+ //
8
+ // Pure function. No I/O, no clipboard — string in, string out, so it is
9
+ // trivially testable and callable from both the CLI and a server.
10
+ export function buildHandoffPrompt(p) {
11
+ const lines = [];
12
+ lines.push(`# Visual change: ${p.storyTitle}`);
13
+ lines.push('');
14
+ lines.push(`Story \`${p.storyId}\` is **${p.status}**.`);
15
+ lines.push('');
16
+ if (p.status === 'new') {
17
+ lines.push('This story has no baseline yet — nothing to compare against. Decide whether', 'the rendered output is correct, then either approve it to establish the', 'baseline or fix the component first.', '', ...actions(p, { approveVerb: 'establish this as the baseline' }));
18
+ return lines.join('\n');
19
+ }
20
+ if (p.status === 'stale') {
21
+ lines.push('The baseline for this story was approved on a commit that is not an ancestor', 'of HEAD — most likely a branch that never merged. The pixels may well match;', 'the provenance is what is in question.', '', 'Re-run the capture first. If it then passes, re-approve to re-anchor the', 'baseline to the current history.', '', ...actions(p, { approveVerb: 're-anchor the baseline' }));
22
+ return lines.join('\n');
23
+ }
24
+ if (p.diffDataPath) {
25
+ lines.push(`Read \`${p.diffDataPath}\` for the full machine-readable diff.`, '');
26
+ }
27
+ lines.push('## What changed');
28
+ lines.push('');
29
+ lines.push('```json');
30
+ lines.push(JSON.stringify({
31
+ storyId: p.storyId,
32
+ mismatchPercent: round(p.mismatchPercent),
33
+ mismatchPixels: p.mismatchPixels,
34
+ viewport: p.viewport,
35
+ regionCount: p.changedRegions.length,
36
+ }, null, 2));
37
+ lines.push('```');
38
+ lines.push('');
39
+ if (p.changedRegions.length > 0) {
40
+ lines.push('## Where it changed');
41
+ lines.push('');
42
+ for (const r of p.changedRegions.slice(0, MAX_REGIONS)) {
43
+ lines.push(`- ${describeRegion(r, p.viewport)}`);
44
+ }
45
+ if (p.changedRegions.length > MAX_REGIONS) {
46
+ lines.push(`- …and ${p.changedRegions.length - MAX_REGIONS} more (see the diff data file)`);
47
+ }
48
+ lines.push('');
49
+ }
50
+ lines.push('## Assessment');
51
+ lines.push('');
52
+ lines.push(magnitudeHint(p.mismatchPercent));
53
+ lines.push('');
54
+ lines.push(...actions(p, { approveVerb: 'accept the new rendering' }));
55
+ if (p.images.baseline || p.images.current || p.images.diff) {
56
+ lines.push('');
57
+ lines.push('## Images (inspect only if the data above is insufficient)');
58
+ lines.push('');
59
+ if (p.images.baseline)
60
+ lines.push(`- baseline: \`${p.images.baseline}\``);
61
+ if (p.images.current)
62
+ lines.push(`- current: \`${p.images.current}\``);
63
+ if (p.images.diff)
64
+ lines.push(`- diff overlay: \`${p.images.diff}\``);
65
+ }
66
+ lines.push('');
67
+ lines.push('---');
68
+ lines.push(`Generated for \`${p.projectRoot}\`. If your session is rooted elsewhere, stop — this prompt is for a different repository.`);
69
+ return lines.join('\n');
70
+ }
71
+ const MAX_REGIONS = 12;
72
+ function actions(p, opts) {
73
+ const out = ['## What to do', ''];
74
+ if (p.componentPath) {
75
+ out.push(`1. Inspect the component: \`${p.componentPath}\``, `2. Check recent history: \`git log -p -- ${p.componentPath}\``, '3. Decide: intentional change, or regression?');
76
+ }
77
+ else {
78
+ out.push('1. Locate the component behind this story.', '2. Check its recent history for an explanation.', '3. Decide: intentional change, or regression?');
79
+ }
80
+ out.push('', `- **Intentional** → \`jux approve --story ${p.storyId}\` to ${opts.approveVerb}.`, '- **Regression** → propose a fix. Do not apply it without confirmation.');
81
+ return out;
82
+ }
83
+ /**
84
+ * Describe a region in language a model reasons about well.
85
+ *
86
+ * Raw coordinates are precise but hard to reason over; "top-left, 2.3% of the
87
+ * viewport" carries the spatial meaning that actually drives the judgement.
88
+ * Both are given.
89
+ */
90
+ export function describeRegion(r, viewport) {
91
+ const areaPct = ((r.width * r.height) / (viewport.width * viewport.height)) * 100;
92
+ const cx = r.x + r.width / 2;
93
+ const cy = r.y + r.height / 2;
94
+ const vertical = cy < viewport.height / 3
95
+ ? 'top'
96
+ : cy > (viewport.height * 2) / 3
97
+ ? 'bottom'
98
+ : 'middle';
99
+ const horizontal = cx < viewport.width / 3 ? 'left' : cx > (viewport.width * 2) / 3 ? 'right' : 'center';
100
+ return `(${r.x}, ${r.y}) ${r.width}×${r.height}px — ${vertical}-${horizontal}, ${round(areaPct)}% of viewport`;
101
+ }
102
+ /**
103
+ * Calibrate the agent's suspicion to the size of the change.
104
+ *
105
+ * Without this, agents treat a 0.02% antialiasing shift and a 40% layout
106
+ * collapse as equally alarming.
107
+ */
108
+ function magnitudeHint(pct) {
109
+ if (pct < 0.1) {
110
+ return 'Under 0.1% — consistent with sub-pixel antialiasing noise. Usually safe to approve, but confirm the changed regions are not on meaningful content.';
111
+ }
112
+ if (pct < 1) {
113
+ return 'Under 1% — a small, localized change. Check whether the component or its tokens were recently touched.';
114
+ }
115
+ if (pct < 10) {
116
+ return 'Between 1% and 10% — a substantive visual change. Investigate before approving.';
117
+ }
118
+ return 'Over 10% — a large change, possibly a layout break or a failed render. Investigate carefully; do not approve without understanding the cause.';
119
+ }
120
+ const round = (n) => Math.round(n * 100) / 100;
121
+ //# sourceMappingURL=handoff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"handoff.js","sourceRoot":"","sources":["../../src/agent/handoff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import type { StoryManifest } from '../types.js';
2
+ export interface AffectedOptions {
3
+ projectRoot: string;
4
+ /** Directories whose contents invalidate everything, from the platform
5
+ * adapter (`.storybook`, `.ladle`, …). */
6
+ configDirs: string[];
7
+ /** Built workbench dir, for locating the Vite manifest. Omit for dev-server
8
+ * mode, where no manifest exists on disk. */
9
+ buildDir?: string;
10
+ /** Explicit file list, bypassing git. Used by the HMR path and by tests. */
11
+ changedFiles?: string[];
12
+ }
13
+ export interface AffectedResult {
14
+ stories: StoryManifest[];
15
+ /** Why this set was chosen — surfaced in the report so a full run is never
16
+ * mysterious. */
17
+ reason: 'no-baseline' | 'config-changed' | 'dependency-changed' | 'graph-walk' | 'no-match-fallback' | 'git-unavailable' | 'no-changes';
18
+ }
19
+ export declare function resolveAffected(stories: StoryManifest[], sinceCommit: string | null, opts: AffectedOptions): Promise<AffectedResult>;
@@ -0,0 +1,132 @@
1
+ // Which stories does a change actually affect?
2
+ //
3
+ // Capturing 700 stories when 3 changed is the difference between a check
4
+ // developers wait for and one they route around. But the failure modes are
5
+ // asymmetric and that shapes every decision here:
6
+ //
7
+ // false positive → a few seconds of wasted capture
8
+ // false negative → a visual regression ships
9
+ //
10
+ // So every uncertain path escalates to a full run. This module is deliberately
11
+ // eager to give up.
12
+ import { execFileSync } from 'node:child_process';
13
+ import { readFile } from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ const LOCKFILES = new Set([
16
+ 'package.json',
17
+ 'package-lock.json',
18
+ 'yarn.lock',
19
+ 'pnpm-lock.yaml',
20
+ 'bun.lockb',
21
+ ]);
22
+ export async function resolveAffected(stories, sinceCommit, opts) {
23
+ // No point of comparison — everything is affected.
24
+ if (sinceCommit === null && !opts.changedFiles) {
25
+ return { stories, reason: 'no-baseline' };
26
+ }
27
+ const changed = opts.changedFiles ?? gitChangedFiles(sinceCommit, opts.projectRoot);
28
+ if (changed === null)
29
+ return { stories, reason: 'git-unavailable' };
30
+ if (changed.length === 0)
31
+ return { stories: [], reason: 'no-changes' };
32
+ for (const file of changed) {
33
+ const base = file.split('/').pop() ?? file;
34
+ if (LOCKFILES.has(base) && !file.includes('/')) {
35
+ return { stories, reason: 'dependency-changed' };
36
+ }
37
+ if (opts.configDirs.some((dir) => file === dir || file.startsWith(dir + '/'))) {
38
+ return { stories, reason: 'config-changed' };
39
+ }
40
+ }
41
+ const manifest = opts.buildDir ? await loadViteManifest(opts.buildDir) : null;
42
+ if (!manifest) {
43
+ // Without a bundler graph we can't prove what's unaffected. Say so rather
44
+ // than guessing with filename heuristics.
45
+ return { stories, reason: 'no-match-fallback' };
46
+ }
47
+ const affected = walkDependents(buildReverseGraph(manifest), changed);
48
+ const matched = stories.filter((s) => {
49
+ const rel = toPosixRelative(opts.projectRoot, s.componentPath);
50
+ return affected.has(rel);
51
+ });
52
+ // Files changed but nothing matched — the change is outside the graph we
53
+ // know about. Could be a public asset, a global stylesheet, a util the
54
+ // manifest didn't record. Capture everything.
55
+ if (matched.length === 0)
56
+ return { stories, reason: 'no-match-fallback' };
57
+ return { stories: matched, reason: 'graph-walk' };
58
+ }
59
+ /**
60
+ * Build `imported → importers` from Vite's manifest.
61
+ *
62
+ * Vite's `imports[]` holds chunk keys, not source paths, so resolving an edge
63
+ * means mapping key → src first.
64
+ */
65
+ function buildReverseGraph(manifest) {
66
+ const keyToSrc = new Map();
67
+ for (const [key, entry] of Object.entries(manifest)) {
68
+ if (entry.src)
69
+ keyToSrc.set(key, entry.src);
70
+ }
71
+ const reverse = new Map();
72
+ for (const entry of Object.values(manifest)) {
73
+ const importer = entry.src;
74
+ if (!importer)
75
+ continue;
76
+ for (const key of [...(entry.imports ?? []), ...(entry.dynamicImports ?? [])]) {
77
+ const imported = keyToSrc.get(key) ?? key;
78
+ let set = reverse.get(imported);
79
+ if (!set)
80
+ reverse.set(imported, (set = new Set()));
81
+ set.add(importer);
82
+ }
83
+ }
84
+ return reverse;
85
+ }
86
+ /** Transitive dependents, including the seeds themselves — a changed story
87
+ * file is its own story's entry point. */
88
+ function walkDependents(reverse, seeds) {
89
+ const seen = new Set();
90
+ const stack = [...seeds];
91
+ while (stack.length > 0) {
92
+ const file = stack.pop();
93
+ if (seen.has(file))
94
+ continue;
95
+ seen.add(file);
96
+ for (const dependent of reverse.get(file) ?? []) {
97
+ if (!seen.has(dependent))
98
+ stack.push(dependent);
99
+ }
100
+ }
101
+ return seen;
102
+ }
103
+ async function loadViteManifest(buildDir) {
104
+ for (const rel of ['.vite/manifest.json', 'manifest.json']) {
105
+ try {
106
+ return JSON.parse(await readFile(path.join(buildDir, rel), 'utf8'));
107
+ }
108
+ catch {
109
+ // try next
110
+ }
111
+ }
112
+ return null;
113
+ }
114
+ function gitChangedFiles(sinceCommit, cwd) {
115
+ try {
116
+ const out = execFileSync('git', ['diff', '--name-only', `${sinceCommit}..HEAD`], {
117
+ cwd,
118
+ encoding: 'utf8',
119
+ });
120
+ return out
121
+ .split('\n')
122
+ .map((s) => s.trim())
123
+ .filter(Boolean);
124
+ }
125
+ catch {
126
+ return null;
127
+ }
128
+ }
129
+ function toPosixRelative(root, abs) {
130
+ return path.relative(root, abs).split(path.sep).join('/');
131
+ }
132
+ //# sourceMappingURL=affected.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"affected.js","sourceRoot":"","sources":["../../src/analysis/affected.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,EAAE;AACF,yEAAyE;AACzE,2EAA2E;AAC3E,kDAAkD;AAClD,EAAE;AACF,qDAAqD;AACrD,+CAA+C;AAC/C,EAAE;AACF,+EAA+E;AAC/E,oBAAoB;AAEpB,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAsC7B,MAAM,SAAS,GAAG,IAAI,GAAG,CAAC;IACxB,cAAc;IACd,mBAAmB;IACnB,WAAW;IACX,gBAAgB;IAChB,WAAW;CACZ,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAwB,EACxB,WAA0B,EAC1B,IAAqB;IAErB,mDAAmD;IACnD,IAAI,WAAW,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC;QAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,aAAa,EAAE,CAAC;IAC5C,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,IAAI,eAAe,CAAC,WAAY,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAErF,IAAI,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IACpE,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,EAAE,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;IAEvE,KAAK,MAAM,IAAI,IAAI,OAAO,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC;QAC3C,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,oBAAoB,EAAE,CAAC;QACnD,CAAC;QACD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,KAAK,GAAG,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,EAAE,CAAC;YAC9E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,CAAC;QAC/C,CAAC;IACH,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,gBAAgB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC9E,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,0EAA0E;QAC1E,0CAA0C;QAC1C,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAClD,CAAC;IAED,MAAM,QAAQ,GAAG,cAAc,CAAC,iBAAiB,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC;IAEtE,MAAM,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QACnC,MAAM,GAAG,GAAG,eAAe,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,aAAa,CAAC,CAAC;QAC/D,OAAO,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC,CAAC,CAAC;IAEH,yEAAyE;IACzE,uEAAuE;IACvE,8CAA8C;IAC9C,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,CAAC;IAE1E,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,EAAE,CAAC;AACpD,CAAC;AAED;;;;;GAKG;AACH,SAAS,iBAAiB,CAAC,QAAsB;IAC/C,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAC;QACpD,IAAI,KAAK,CAAC,GAAG;YAAE,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAC9C,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,GAAG,EAAuB,CAAC;IAC/C,KAAK,MAAM,KAAK,IAAI,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC;QAC5C,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC;QAC3B,IAAI,CAAC,QAAQ;YAAE,SAAS;QACxB,KAAK,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,IAAI,EAAE,CAAC,EAAE,GAAG,CAAC,KAAK,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC;YAC9E,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC;YAC1C,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAChC,IAAI,CAAC,GAAG;gBAAE,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;YACnD,GAAG,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACpB,CAAC;IACH,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;2CAC2C;AAC3C,SAAS,cAAc,CAAC,OAAiC,EAAE,KAAe;IACxE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,KAAK,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC;IACzB,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,EAAG,CAAC;QAC1B,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,KAAK,MAAM,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;YAChD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;gBAAE,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QAClD,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,KAAK,UAAU,gBAAgB,CAAC,QAAgB;IAC9C,KAAK,MAAM,GAAG,IAAI,CAAC,qBAAqB,EAAE,eAAe,CAAC,EAAE,CAAC;QAC3D,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,CAAiB,CAAC;QACtF,CAAC;QAAC,MAAM,CAAC;YACP,WAAW;QACb,CAAC;IACH,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,eAAe,CAAC,WAAmB,EAAE,GAAW;IACvD,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,KAAK,EAAE,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,WAAW,QAAQ,CAAC,EAAE;YAC/E,GAAG;YACH,QAAQ,EAAE,MAAM;SACjB,CAAC,CAAC;QACH,OAAO,GAAG;aACP,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;aACpB,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,IAAY,EAAE,GAAW;IAChD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC"}
@@ -0,0 +1,26 @@
1
+ import type { ApprovalMeta, BaselineManifest, BaselineStore } from '../types.js';
2
+ export interface GitStoreOptions {
3
+ projectRoot: string;
4
+ /** Where baseline images live, relative to projectRoot. */
5
+ dir?: string;
6
+ }
7
+ export declare class GitBaselineStore implements BaselineStore {
8
+ private readonly imageDir;
9
+ private readonly manifestPath;
10
+ constructor(opts: GitStoreOptions);
11
+ private imagePath;
12
+ get(storyId: string): Promise<Buffer | null>;
13
+ put(storyId: string, image: Buffer, meta: ApprovalMeta): Promise<void>;
14
+ remove(storyId: string): Promise<void>;
15
+ readManifest(): Promise<BaselineManifest>;
16
+ writeManifest(manifest: BaselineManifest): Promise<void>;
17
+ }
18
+ /**
19
+ * Make a story id safe as a single path segment.
20
+ *
21
+ * Story ids come from the platform and are usually kebab-case, but that is an
22
+ * assumption, not a guarantee — the original tool interpolated them raw into
23
+ * S3 keys and file paths. A `/` or `..` in an id would escape the baseline
24
+ * directory, so encode anything outside a conservative allowlist.
25
+ */
26
+ export declare function encodeStoryId(storyId: string): string;
@@ -0,0 +1,95 @@
1
+ // Git-backed baseline store — the default.
2
+ //
3
+ // Baselines are WebP files committed under .juxtapose/baselines/. No bucket to
4
+ // provision, no credentials, and the tool does something useful the moment it
5
+ // is installed.
6
+ //
7
+ // Three properties fall out of this that a bucket does not give you:
8
+ // - Approving a baseline is a reviewable diff in the PR that changed it.
9
+ // - Branches get correct baselines by construction: checking out a branch
10
+ // checks out its baselines. No branch-keyed prefixes, no fallback-to-main.
11
+ // - Reverting a bad approval is `git revert`.
12
+ //
13
+ // The cost is repo size. WebP at quality 90 keeps a 1280x800 story around
14
+ // 30-60KB; a 700-story suite is tens of megabytes. Real, but tolerable, and
15
+ // the S3 store exists for suites where it isn't.
16
+ import { readFile, writeFile, mkdir, rm } from 'node:fs/promises';
17
+ import { existsSync } from 'node:fs';
18
+ import path from 'node:path';
19
+ import { hashImage } from './status.js';
20
+ const MANIFEST_NAME = 'juxtapose.manifest.json';
21
+ const EMPTY = { version: 1, baselines: {} };
22
+ export class GitBaselineStore {
23
+ imageDir;
24
+ manifestPath;
25
+ constructor(opts) {
26
+ this.imageDir = path.join(opts.projectRoot, opts.dir ?? '.juxtapose/baselines');
27
+ this.manifestPath = path.join(opts.projectRoot, MANIFEST_NAME);
28
+ }
29
+ imagePath(storyId) {
30
+ return path.join(this.imageDir, `${encodeStoryId(storyId)}.webp`);
31
+ }
32
+ async get(storyId) {
33
+ try {
34
+ return await readFile(this.imagePath(storyId));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ async put(storyId, image, meta) {
41
+ await mkdir(this.imageDir, { recursive: true });
42
+ await writeFile(this.imagePath(storyId), image);
43
+ const manifest = await this.readManifest();
44
+ manifest.baselines[storyId] = { ...meta, hash: hashImage(image) };
45
+ await this.writeManifest(manifest);
46
+ }
47
+ async remove(storyId) {
48
+ await rm(this.imagePath(storyId), { force: true });
49
+ const manifest = await this.readManifest();
50
+ delete manifest.baselines[storyId];
51
+ await this.writeManifest(manifest);
52
+ }
53
+ async readManifest() {
54
+ if (!existsSync(this.manifestPath))
55
+ return structuredClone(EMPTY);
56
+ try {
57
+ const parsed = JSON.parse(await readFile(this.manifestPath, 'utf8'));
58
+ if (parsed?.version !== 1 || typeof parsed.baselines !== 'object') {
59
+ return structuredClone(EMPTY);
60
+ }
61
+ return parsed;
62
+ }
63
+ catch {
64
+ // A corrupt manifest means every story reads as `new`, which is loud and
65
+ // recoverable. Throwing here would block the run entirely.
66
+ return structuredClone(EMPTY);
67
+ }
68
+ }
69
+ async writeManifest(manifest) {
70
+ // Sort keys so the committed file produces a minimal, readable git diff —
71
+ // the whole point of storing baselines in the repo.
72
+ const sorted = { version: 1, baselines: {} };
73
+ for (const id of Object.keys(manifest.baselines).sort()) {
74
+ sorted.baselines[id] = manifest.baselines[id];
75
+ }
76
+ await writeFile(this.manifestPath, JSON.stringify(sorted, null, 2) + '\n');
77
+ }
78
+ }
79
+ /**
80
+ * Make a story id safe as a single path segment.
81
+ *
82
+ * Story ids come from the platform and are usually kebab-case, but that is an
83
+ * assumption, not a guarantee — the original tool interpolated them raw into
84
+ * S3 keys and file paths. A `/` or `..` in an id would escape the baseline
85
+ * directory, so encode anything outside a conservative allowlist.
86
+ */
87
+ export function encodeStoryId(storyId) {
88
+ // `.` is deliberately NOT in the allowlist. Escaping only separators would
89
+ // leave `..` intact, which cannot traverse without a `/` but still yields
90
+ // filenames that read as relative paths — a trap for anything downstream
91
+ // that joins, splits, or normalizes them. Encoding every dot makes the
92
+ // result inert by construction rather than by argument.
93
+ return storyId.replace(/[^a-zA-Z0-9_-]/g, (c) => '%' + c.charCodeAt(0).toString(16).padStart(2, '0'));
94
+ }
95
+ //# sourceMappingURL=git-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git-store.js","sourceRoot":"","sources":["../../src/baseline/git-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ import type { BaselineManifest, DiffStatus } from '../types.js';
2
+ export type BaselineStatus = 'pass' | 'new' | 'changed' | 'stale';
3
+ /** Hash the stored representation, not an intermediate.
4
+ *
5
+ * Always the WebP bytes — never the PNG. The WebP is what gets persisted, so
6
+ * hashing it means the hash is reproducible from stored artifacts alone. */
7
+ export declare function hashImage(webp: Buffer): string;
8
+ export interface AncestryCheck {
9
+ /** True if `commit` is an ancestor of HEAD. */
10
+ isAncestor(commit: string): boolean;
11
+ }
12
+ /**
13
+ * Resolve one story's status against the manifest.
14
+ *
15
+ * `stale` is the subtle one: a baseline approved on a branch that never merged
16
+ * is not a valid baseline for HEAD, even though the bytes match. Without this
17
+ * check, an abandoned branch's approvals silently become the truth.
18
+ */
19
+ export declare function resolveStatus(storyId: string, webp: Buffer, manifest: BaselineManifest, ancestry?: AncestryCheck): BaselineStatus;
20
+ /** Map baseline status onto the reported diff status. Single definition —
21
+ * the original had this mapping written out in three places. */
22
+ export declare function toDiffStatus(status: BaselineStatus): DiffStatus;
@@ -0,0 +1,49 @@
1
+ // Pass/fail authority.
2
+ //
3
+ // A story passes if its captured bytes hash to the same value the manifest
4
+ // recorded at approval time. Byte-exact — there is no threshold to hide behind,
5
+ // which is why capture determinism gets the attention it does elsewhere in this
6
+ // package.
7
+ //
8
+ // This module MUST NOT write to disk. It is called once per story per run and
9
+ // is the hot path on a green run.
10
+ import { createHash } from 'node:crypto';
11
+ /** Hash the stored representation, not an intermediate.
12
+ *
13
+ * Always the WebP bytes — never the PNG. The WebP is what gets persisted, so
14
+ * hashing it means the hash is reproducible from stored artifacts alone. */
15
+ export function hashImage(webp) {
16
+ return createHash('sha256').update(webp).digest('hex');
17
+ }
18
+ /**
19
+ * Resolve one story's status against the manifest.
20
+ *
21
+ * `stale` is the subtle one: a baseline approved on a branch that never merged
22
+ * is not a valid baseline for HEAD, even though the bytes match. Without this
23
+ * check, an abandoned branch's approvals silently become the truth.
24
+ */
25
+ export function resolveStatus(storyId, webp, manifest, ancestry) {
26
+ const entry = manifest.baselines[storyId];
27
+ if (!entry)
28
+ return 'new';
29
+ if (entry.hash !== hashImage(webp))
30
+ return 'changed';
31
+ if (ancestry && !ancestry.isAncestor(entry.commit))
32
+ return 'stale';
33
+ return 'pass';
34
+ }
35
+ /** Map baseline status onto the reported diff status. Single definition —
36
+ * the original had this mapping written out in three places. */
37
+ export function toDiffStatus(status) {
38
+ switch (status) {
39
+ case 'pass':
40
+ return 'pass';
41
+ case 'changed':
42
+ return 'fail';
43
+ case 'stale':
44
+ return 'stale';
45
+ case 'new':
46
+ return 'new';
47
+ }
48
+ }
49
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/baseline/status.ts"],"names":[],"mappings":"AAAA,uBAAuB;AACvB,EAAE;AACF,2EAA2E;AAC3E,gFAAgF;AAChF,gFAAgF;AAChF,WAAW;AACX,EAAE;AACF,8EAA8E;AAC9E,kCAAkC;AAElC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAKzC;;;6EAG6E;AAC7E,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACzD,CAAC;AAOD;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAC3B,OAAe,EACf,IAAY,EACZ,QAA0B,EAC1B,QAAwB;IAExB,MAAM,KAAK,GAAG,QAAQ,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC;IAC1C,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzB,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,CAAC,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACrD,IAAI,QAAQ,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,OAAO,OAAO,CAAC;IACnE,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;iEACiE;AACjE,MAAM,UAAU,YAAY,CAAC,MAAsB;IACjD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,MAAM;YACT,OAAO,MAAM,CAAC;QAChB,KAAK,SAAS;YACZ,OAAO,MAAM,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,OAAO,CAAC;QACjB,KAAK,KAAK;YACR,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC"}
@@ -0,0 +1,32 @@
1
+ import type { Page } from 'playwright';
2
+ import type { Platform } from '../platform.js';
3
+ import type { Snapshot, StoryManifest, Viewport } from '../types.js';
4
+ export interface CaptureDeps {
5
+ platform: Platform;
6
+ baseUrl: string;
7
+ /** WebP encode effort, 0-6. Higher is smaller and slower. Capture is
8
+ * browser-bound, so 0 is the right default — the bytes are transient. */
9
+ webpEffort?: number;
10
+ /**
11
+ * Reuse a warm page and switch stories over the platform's channel instead
12
+ * of navigating. Roughly 4x faster per story.
13
+ *
14
+ * OFF BY DEFAULT, and that is a deliberate, measured decision rather than
15
+ * caution. A warm switch does not reproduce a cold load's pixels exactly:
16
+ * the previous story leaves residue — retained style and layout state,
17
+ * settled transitions, a different paint history — and the same story
18
+ * captured warm differs from the same story captured cold by a handful of
19
+ * antialiased pixels.
20
+ *
21
+ * Each path is internally stable. They just disagree with each other. Under
22
+ * a byte-exact hash that makes a story's verdict depend on where it happened
23
+ * to land in the shuffled work queue, which is to say: random. Measured on a
24
+ * 16-story suite, 1-4 stories failed per run purely from queue order.
25
+ *
26
+ * The speed is real and worth having, so this is not abandoned — see the
27
+ * tracking issue. But correctness is not negotiable against it, and a
28
+ * visual test that cries wolf is worse than a slow one.
29
+ */
30
+ warmSwitch?: boolean;
31
+ }
32
+ export declare function captureStory(page: Page, story: StoryManifest, deps: CaptureDeps, viewport: Viewport): Promise<Snapshot | null>;