@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
@@ -0,0 +1,25 @@
1
+ import type { ChangedRegion, StoryParams, ProjectConfig } from '../types.js';
2
+ export interface DiffOutput {
3
+ mismatchPixels: number;
4
+ mismatchPercent: number;
5
+ changedRegions: ChangedRegion[];
6
+ /** WebP overlay marking changed pixels. Absent when nothing changed. */
7
+ diffImage?: Buffer;
8
+ /** True when dimensions differ — no pixel comparison was possible. */
9
+ dimensionMismatch: boolean;
10
+ }
11
+ /**
12
+ * Compare two encoded images.
13
+ *
14
+ * `threshold` is pixelmatch's YIQ colour-distance tolerance (0-1), not a
15
+ * pixel count. `includeAA: false` makes pixelmatch detect and exclude
16
+ * antialiased pixels, which is what keeps sub-pixel text rendering from
17
+ * dominating every diff.
18
+ */
19
+ export declare function diffImages(baseline: Buffer, current: Buffer, opts?: {
20
+ threshold?: number;
21
+ minDiffPercent?: number;
22
+ }): Promise<DiffOutput>;
23
+ /** Story params override project config override the built-in default. */
24
+ export declare function resolveThreshold(params: StoryParams | undefined, config: ProjectConfig | undefined): number;
25
+ export declare function resolveMinDiffPercent(params: StoryParams | undefined, config: ProjectConfig | undefined): number;
@@ -0,0 +1,86 @@
1
+ // Pixel diff — the COLD path.
2
+ //
3
+ // This only runs for stories that already failed the hash check in
4
+ // baseline/status.ts. It decides nothing; it exists to turn "these bytes
5
+ // differ" into something a reviewer or an agent can act on.
6
+ //
7
+ // Consequence worth keeping in mind while optimizing: on a green run this
8
+ // module never executes. Effort spent here pays off only in proportion to how
9
+ // broken the run already is.
10
+ import sharp from 'sharp';
11
+ import pixelmatch from 'pixelmatch';
12
+ import { computeChangedRegions } from './regions.js';
13
+ const DEFAULT_THRESHOLD = 0.1;
14
+ const DEFAULT_MIN_DIFF_PERCENT = 0;
15
+ /**
16
+ * Compare two encoded images.
17
+ *
18
+ * `threshold` is pixelmatch's YIQ colour-distance tolerance (0-1), not a
19
+ * pixel count. `includeAA: false` makes pixelmatch detect and exclude
20
+ * antialiased pixels, which is what keeps sub-pixel text rendering from
21
+ * dominating every diff.
22
+ */
23
+ export async function diffImages(baseline, current, opts = {}) {
24
+ const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
25
+ const minDiffPercent = opts.minDiffPercent ?? DEFAULT_MIN_DIFF_PERCENT;
26
+ // sharp sniffs the format, so baselines can be WebP and current can be PNG
27
+ // without an explicit conversion step.
28
+ const [a, b] = await Promise.all([
29
+ sharp(baseline).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
30
+ sharp(current).ensureAlpha().raw().toBuffer({ resolveWithObject: true }),
31
+ ]);
32
+ if (a.info.width !== b.info.width || a.info.height !== b.info.height) {
33
+ // No meaningful pixel comparison exists across different dimensions.
34
+ // Report it as total change rather than inventing a number.
35
+ return {
36
+ mismatchPixels: 0,
37
+ mismatchPercent: 100,
38
+ changedRegions: [],
39
+ dimensionMismatch: true,
40
+ };
41
+ }
42
+ const { width, height } = b.info;
43
+ const diff = Buffer.alloc(width * height * 4);
44
+ const mismatchPixels = pixelmatch(a.data, b.data, diff, width, height, {
45
+ threshold,
46
+ includeAA: false,
47
+ });
48
+ const mismatchPercent = (mismatchPixels / (width * height)) * 100;
49
+ if (mismatchPixels === 0 || mismatchPercent < minDiffPercent) {
50
+ return {
51
+ mismatchPixels,
52
+ mismatchPercent,
53
+ changedRegions: [],
54
+ dimensionMismatch: false,
55
+ };
56
+ }
57
+ // Lossless: the overlay is a sparse mask of flat colour, which lossless
58
+ // compresses better than lossy anyway, and blurring the marked pixels would
59
+ // defeat the point of the image.
60
+ const diffImage = await sharp(diff, { raw: { width, height, channels: 4 } })
61
+ .webp({ lossless: true, effort: 0 })
62
+ .toBuffer();
63
+ return {
64
+ mismatchPixels,
65
+ mismatchPercent,
66
+ changedRegions: computeChangedRegions(diff, width, height),
67
+ diffImage,
68
+ dimensionMismatch: false,
69
+ };
70
+ }
71
+ /** Story params override project config override the built-in default. */
72
+ export function resolveThreshold(params, config) {
73
+ return clamp(firstFinite(params?.threshold, config?.threshold, DEFAULT_THRESHOLD), 0, 1);
74
+ }
75
+ export function resolveMinDiffPercent(params, config) {
76
+ return clamp(firstFinite(params?.minDiffPercent, config?.minDiffPercent, DEFAULT_MIN_DIFF_PERCENT), 0, 100);
77
+ }
78
+ function firstFinite(...values) {
79
+ for (const v of values) {
80
+ if (typeof v === 'number' && Number.isFinite(v))
81
+ return v;
82
+ }
83
+ return 0;
84
+ }
85
+ const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
86
+ //# sourceMappingURL=diff.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diff.js","sourceRoot":"","sources":["../../src/diff/diff.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ import type { ChangedRegion } from '../types.js';
2
+ /**
3
+ * Extract merged bounding boxes of changed pixel clusters.
4
+ *
5
+ * Boxes within `gap` pixels of each other merge, so antialiased edges and
6
+ * inter-letter spacing don't fragment one logical change into many.
7
+ */
8
+ export declare function computeChangedRegions(diff: Buffer, width: number, height: number, gap?: number): ChangedRegion[];
@@ -0,0 +1,90 @@
1
+ // Turn a pixelmatch diff buffer into a handful of bounding boxes.
2
+ //
3
+ // Why: a one-word text change produces hundreds of disconnected changed pixels.
4
+ // Reporting those raw is useless to a human and to an agent. Merging them into
5
+ // gap-tolerant boxes turns "412 changed pixels" into "one region, at the
6
+ // heading" — which is reviewable.
7
+ const DEFAULT_GAP = 8;
8
+ /**
9
+ * Is this pixel one pixelmatch marked as changed?
10
+ *
11
+ * Subtle and worth stating, because getting it wrong is silent: pixelmatch
12
+ * writes the WHOLE frame into the diff buffer, not just the differences.
13
+ * Unchanged pixels come back as the faded original at full alpha; changed
14
+ * pixels are painted in its diff colour, red by default.
15
+ *
16
+ * So testing `alpha > 0` matches every pixel in the image and collapses every
17
+ * report into one full-viewport region — which is exactly the bug this
18
+ * replaced. Test for the red marking instead.
19
+ */
20
+ function isChanged(diff, offset) {
21
+ return diff[offset] > 200 && diff[offset + 1] < 100 && diff[offset + 2] < 100;
22
+ }
23
+ /**
24
+ * Extract merged bounding boxes of changed pixel clusters.
25
+ *
26
+ * Boxes within `gap` pixels of each other merge, so antialiased edges and
27
+ * inter-letter spacing don't fragment one logical change into many.
28
+ */
29
+ export function computeChangedRegions(diff, width, height, gap = DEFAULT_GAP) {
30
+ // Pass 1: per-row runs of changed pixels. Rows are cheap to scan and
31
+ // collapse most of the pixel count immediately.
32
+ const spans = [];
33
+ for (let y = 0; y < height; y++) {
34
+ let start = -1;
35
+ for (let x = 0; x <= width; x++) {
36
+ const changed = x < width && isChanged(diff, (y * width + x) * 4);
37
+ if (changed && start === -1)
38
+ start = x;
39
+ else if (!changed && start !== -1) {
40
+ spans.push({ y, x1: start, x2: x - 1 });
41
+ start = -1;
42
+ }
43
+ }
44
+ }
45
+ // Pass 2: merge spans into boxes.
46
+ //
47
+ // Unlike a naive greedy merge, a span that touches several existing boxes
48
+ // must merge *all* of them together — otherwise a change shaped like a "U"
49
+ // reports as two regions that a later span should have joined.
50
+ const boxes = [];
51
+ for (const span of spans) {
52
+ const touched = [];
53
+ for (let i = 0; i < boxes.length; i++) {
54
+ if (overlaps(span, boxes[i], gap))
55
+ touched.push(i);
56
+ }
57
+ if (touched.length === 0) {
58
+ boxes.push({ x1: span.x1, y1: span.y, x2: span.x2, y2: span.y });
59
+ continue;
60
+ }
61
+ const target = boxes[touched[0]];
62
+ target.x1 = Math.min(target.x1, span.x1);
63
+ target.x2 = Math.max(target.x2, span.x2);
64
+ target.y1 = Math.min(target.y1, span.y);
65
+ target.y2 = Math.max(target.y2, span.y);
66
+ // Fold any other boxes this span bridged into the first, back-to-front so
67
+ // the splices don't shift indices we still need.
68
+ for (let i = touched.length - 1; i >= 1; i--) {
69
+ const other = boxes[touched[i]];
70
+ target.x1 = Math.min(target.x1, other.x1);
71
+ target.x2 = Math.max(target.x2, other.x2);
72
+ target.y1 = Math.min(target.y1, other.y1);
73
+ target.y2 = Math.max(target.y2, other.y2);
74
+ boxes.splice(touched[i], 1);
75
+ }
76
+ }
77
+ return boxes.map((b) => ({
78
+ x: b.x1,
79
+ y: b.y1,
80
+ width: b.x2 - b.x1 + 1,
81
+ height: b.y2 - b.y1 + 1,
82
+ }));
83
+ }
84
+ function overlaps(span, box, gap) {
85
+ return (span.y >= box.y1 - gap &&
86
+ span.y <= box.y2 + gap &&
87
+ span.x1 <= box.x2 + gap &&
88
+ span.x2 >= box.x1 - gap);
89
+ }
90
+ //# sourceMappingURL=regions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"regions.js","sourceRoot":"","sources":["../../src/diff/regions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { AncestryCheck } from '../baseline/status.js';
2
+ export declare function createAncestryCheck(cwd: string): AncestryCheck;
3
+ /** Ancestry check that passes everything. For non-git checkouts, where
4
+ * staleness is unknowable and blocking on it would be wrong. */
5
+ export declare const alwaysAncestor: AncestryCheck;
@@ -0,0 +1,43 @@
1
+ // Git ancestry, memoized.
2
+ //
3
+ // `git merge-base --is-ancestor` forks a process at ~40ms. Most stories in a
4
+ // run share a handful of baseline commits, so caching by commit takes this from
5
+ // O(stories) forks to O(distinct commits) — roughly 30s to 50ms on a
6
+ // 738-story suite. The memoization is not an optimization detail; without it
7
+ // the stale check is unaffordable and would get dropped.
8
+ import { execFileSync } from 'node:child_process';
9
+ const SHA_RE = /^[0-9a-f]{40}$/i;
10
+ export function createAncestryCheck(cwd) {
11
+ const cache = new Map();
12
+ return {
13
+ isAncestor(commit) {
14
+ // Sentinels like "unknown" predate proper commit tracking. Treat them as
15
+ // valid rather than marking every such baseline stale.
16
+ if (!SHA_RE.test(commit))
17
+ return true;
18
+ const hit = cache.get(commit);
19
+ if (hit !== undefined)
20
+ return hit;
21
+ let result;
22
+ try {
23
+ execFileSync('git', ['merge-base', '--is-ancestor', commit, 'HEAD'], {
24
+ cwd,
25
+ stdio: 'ignore',
26
+ });
27
+ result = true;
28
+ }
29
+ catch {
30
+ // Non-zero exit means "not an ancestor". It also means "unknown commit"
31
+ // — e.g. a shallow clone that lacks the object. Both should read as
32
+ // stale: we cannot prove the baseline is reachable from HEAD.
33
+ result = false;
34
+ }
35
+ cache.set(commit, result);
36
+ return result;
37
+ },
38
+ };
39
+ }
40
+ /** Ancestry check that passes everything. For non-git checkouts, where
41
+ * staleness is unknowable and blocking on it would be wrong. */
42
+ export const alwaysAncestor = { isAncestor: () => true };
43
+ //# sourceMappingURL=ancestry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ancestry.js","sourceRoot":"","sources":["../../src/git/ancestry.ts"],"names":[],"mappings":"AAAA,0BAA0B;AAC1B,EAAE;AACF,6EAA6E;AAC7E,gFAAgF;AAChF,qEAAqE;AACrE,6EAA6E;AAC7E,yDAAyD;AAEzD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGlD,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAEjC,MAAM,UAAU,mBAAmB,CAAC,GAAW;IAC7C,MAAM,KAAK,GAAG,IAAI,GAAG,EAAmB,CAAC;IAEzC,OAAO;QACL,UAAU,CAAC,MAAc;YACvB,yEAAyE;YACzE,uDAAuD;YACvD,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;gBAAE,OAAO,IAAI,CAAC;YAEtC,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;YAC9B,IAAI,GAAG,KAAK,SAAS;gBAAE,OAAO,GAAG,CAAC;YAElC,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,YAAY,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,eAAe,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE;oBACnE,GAAG;oBACH,KAAK,EAAE,QAAQ;iBAChB,CAAC,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,wEAAwE;gBACxE,oEAAoE;gBACpE,8DAA8D;gBAC9D,MAAM,GAAG,KAAK,CAAC;YACjB,CAAC;YAED,KAAK,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;YAC1B,OAAO,MAAM,CAAC;QAChB,CAAC;KACF,CAAC;AACJ,CAAC;AAED;iEACiE;AACjE,MAAM,CAAC,MAAM,cAAc,GAAkB,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,CAAC"}
@@ -0,0 +1,17 @@
1
+ export type { StoryParams, Viewport, StoryManifest, Snapshot, ChangedRegion, DiffStatus, DiffResult, RunReport, ApprovalMeta, BaselineEntry, BaselineManifest, ProjectConfig, StorageConfig, BaselineStore, } from './types.js';
2
+ export type { Platform, EnumerateTarget, SettleContext } from './platform.js';
3
+ export { navigateToStory } from './url-platform.js';
4
+ export { hashImage, resolveStatus, toDiffStatus, type BaselineStatus, type AncestryCheck, } from './baseline/status.js';
5
+ export { GitBaselineStore, encodeStoryId } from './baseline/git-store.js';
6
+ export { createAncestryCheck, alwaysAncestor } from './git/ancestry.js';
7
+ export { armSettle, drain, waitForSelector, waitForPaint, FREEZE_ANIMATIONS_CSS, DOM_IDLE_MS, DOM_IDLE_TIMEOUT_MS, } from './capture/settle.js';
8
+ export { captureStory, type CaptureDeps } from './capture/capture.js';
9
+ export { deterministicLaunchOptions, DETERMINISTIC_CHROMIUM_ARGS, type LaunchOptions, } from './capture/launch.js';
10
+ export { capturePool, DEFAULT_CONCURRENCY, type PoolOptions, type CaptureOutcome, } from './capture/pool.js';
11
+ export { diffImages, resolveThreshold, resolveMinDiffPercent, type DiffOutput, } from './diff/diff.js';
12
+ export { computeChangedRegions } from './diff/regions.js';
13
+ export { resolveAffected, type AffectedOptions, type AffectedResult, } from './analysis/affected.js';
14
+ export { runCapture, DEFAULT_VIEWPORT, type RunOptions, type RunArtifact, } from './run/run.js';
15
+ export { buildHandoffPrompt, describeRegion, type HandoffPayload, } from './agent/handoff.js';
16
+ export { buildComment, COMMENT_MARKER, type CommentOptions } from './report/comment.js';
17
+ export { resolveGitHubContext, postComment, type GitHubContext, } from './report/github.js';
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ export { navigateToStory } from './url-platform.js';
2
+ export { hashImage, resolveStatus, toDiffStatus, } from './baseline/status.js';
3
+ export { GitBaselineStore, encodeStoryId } from './baseline/git-store.js';
4
+ export { createAncestryCheck, alwaysAncestor } from './git/ancestry.js';
5
+ export { armSettle, drain, waitForSelector, waitForPaint, FREEZE_ANIMATIONS_CSS, DOM_IDLE_MS, DOM_IDLE_TIMEOUT_MS, } from './capture/settle.js';
6
+ export { captureStory } from './capture/capture.js';
7
+ export { deterministicLaunchOptions, DETERMINISTIC_CHROMIUM_ARGS, } from './capture/launch.js';
8
+ export { capturePool, DEFAULT_CONCURRENCY, } from './capture/pool.js';
9
+ export { diffImages, resolveThreshold, resolveMinDiffPercent, } from './diff/diff.js';
10
+ export { computeChangedRegions } from './diff/regions.js';
11
+ export { resolveAffected, } from './analysis/affected.js';
12
+ export { runCapture, DEFAULT_VIEWPORT, } from './run/run.js';
13
+ export { buildHandoffPrompt, describeRegion, } from './agent/handoff.js';
14
+ export { buildComment, COMMENT_MARKER } from './report/comment.js';
15
+ export { resolveGitHubContext, postComment, } from './report/github.js';
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEpD,OAAO,EACL,SAAS,EACT,aAAa,EACb,YAAY,GAGb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE1E,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAExE,OAAO,EACL,SAAS,EACT,KAAK,EACL,eAAe,EACf,YAAY,EACZ,qBAAqB,EACrB,WAAW,EACX,mBAAmB,GACpB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAoB,MAAM,sBAAsB,CAAC;AACtE,OAAO,EACL,0BAA0B,EAC1B,2BAA2B,GAE5B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,WAAW,EACX,mBAAmB,GAGpB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,UAAU,EACV,gBAAgB,EAChB,qBAAqB,GAEtB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE1D,OAAO,EACL,eAAe,GAGhB,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,UAAU,EACV,gBAAgB,GAGjB,MAAM,cAAc,CAAC;AAEtB,OAAO,EACL,kBAAkB,EAClB,cAAc,GAEf,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,YAAY,EAAE,cAAc,EAAuB,MAAM,qBAAqB,CAAC;AACxF,OAAO,EACL,oBAAoB,EACpB,WAAW,GAEZ,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,83 @@
1
+ import type { Page } from 'playwright';
2
+ import type { StoryManifest, StoryParams } from './types.js';
3
+ export interface EnumerateTarget {
4
+ /** Built workbench directory, when capturing a static build. */
5
+ buildDir?: string;
6
+ /** Running dev-server origin, when capturing live. */
7
+ origin?: string;
8
+ /** Project root, for resolving story source paths to absolute. */
9
+ projectRoot: string;
10
+ }
11
+ export interface SettleContext {
12
+ story: StoryManifest;
13
+ params: StoryParams;
14
+ /** Max ms to wait for render-ready before giving up and screenshotting. */
15
+ timeoutMs: number;
16
+ }
17
+ export interface Platform {
18
+ readonly name: string;
19
+ /** True if this platform looks present at the target. Used for autodetection. */
20
+ detect(target: EnumerateTarget): Promise<boolean>;
21
+ /** Discover every capturable story. */
22
+ enumerate(target: EnumerateTarget): Promise<StoryManifest[]>;
23
+ /**
24
+ * Put this story on screen, from whatever state the page is in.
25
+ *
26
+ * The one operation every platform must provide, and deliberately phrased as
27
+ * an action rather than a URL. Most workbenches address a story by URL and
28
+ * implement this as a navigation — `showStory` in `url-platform.ts` builds
29
+ * exactly that. But Playwright's component-testing galleries have no
30
+ * per-story URL at all: every story lives at the same base URL and is
31
+ * rendered by calling `window.mount(storyId)`. Requiring a URL would have
32
+ * excluded them.
33
+ *
34
+ * Implementations must leave the page ready for `settle()` to run.
35
+ */
36
+ showStory(page: Page, storyId: string, baseUrl: string): Promise<void>;
37
+ /** Blank preview URL, for warming a page before any story is selected.
38
+ * Prewarming here means the first real story doesn't pay for bundle
39
+ * compile and service-worker registration. */
40
+ previewUrl(baseUrl: string): string;
41
+ /**
42
+ * URL that renders one story in isolation, when the platform has one.
43
+ *
44
+ * Optional because gallery-based platforms genuinely don't. Only used for
45
+ * reporting — linking a reviewer to a story — never for capture, which goes
46
+ * through `showStory`.
47
+ */
48
+ storyUrl?(baseUrl: string, storyId: string): string;
49
+ /**
50
+ * Block until the story has finished rendering.
51
+ *
52
+ * This is the platform's core responsibility and the thing that makes
53
+ * captures deterministic. Implementations should resolve on the platform's
54
+ * own render signal rather than a fixed sleep. Generic settling that applies
55
+ * to every platform — DOM quiescence, network drain, animation freezing —
56
+ * lives in core and runs after this resolves.
57
+ *
58
+ * Must not throw on timeout: resolve and let core screenshot anyway. A
59
+ * late-rendering story should produce a visible diff, not an error.
60
+ */
61
+ settle(page: Page, ctx: SettleContext): Promise<void>;
62
+ /**
63
+ * Switch a warm page to another story without a navigation.
64
+ *
65
+ * Optional, and purely an optimization — but a ~4x one, so implement it if
66
+ * the platform exposes any client-side story-switch channel. Return false
67
+ * if the switch could not be performed; the pool falls back to `goto`.
68
+ */
69
+ switchStory?(page: Page, storyId: string, ctx: SettleContext): Promise<boolean>;
70
+ /**
71
+ * Read per-story config from the live preview.
72
+ *
73
+ * Optional because not every platform hides parameters from its static
74
+ * index. Storybook does — it only serializes a whitelist — so reading the
75
+ * index alone silently drops per-story thresholds.
76
+ */
77
+ readLiveParams?(page: Page, storyId: string): Promise<StoryParams | undefined>;
78
+ /** Directories whose contents invalidate every story when changed
79
+ * (`.storybook`, `.ladle`). Project-relative. */
80
+ readonly configDirs: readonly string[];
81
+ /** Globs identifying story files, for cache keys and change detection. */
82
+ readonly storyFileGlobs: readonly string[];
83
+ }
@@ -0,0 +1,5 @@
1
+ // The platform seam. Everything Storybook-specific in the original tool
2
+ // reduces to the four operations below; Ladle and Histoire implement the same
3
+ // four. `core` depends on this interface and never on a concrete platform.
4
+ export {};
5
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA,wEAAwE;AACxE,8EAA8E;AAC9E,2EAA2E"}
@@ -0,0 +1,16 @@
1
+ import type { RunReport } from '../types.js';
2
+ /**
3
+ * Invisible marker identifying our own comment.
4
+ *
5
+ * This is what makes the reporter update rather than accumulate: on each run
6
+ * we list the PR's comments, find the one carrying this marker, and edit it.
7
+ * A PR with ten pushes ends up with one comment, not ten.
8
+ */
9
+ export declare const COMMENT_MARKER = "<!-- juxtapose-bot -->";
10
+ export interface CommentOptions {
11
+ report: RunReport;
12
+ /** Where to point someone who wants the images. Omitted when there is no
13
+ * artifact to link to. */
14
+ artifactUrl?: string;
15
+ }
16
+ export declare function buildComment({ report, artifactUrl }: CommentOptions): string;
@@ -0,0 +1,99 @@
1
+ // Build the PR comment body.
2
+ //
3
+ // A pure function, separate from posting it, so the formatting can be tested
4
+ // without a network or a token — and so `jux run --json` consumers can render
5
+ // the same summary elsewhere.
6
+ /**
7
+ * Invisible marker identifying our own comment.
8
+ *
9
+ * This is what makes the reporter update rather than accumulate: on each run
10
+ * we list the PR's comments, find the one carrying this marker, and edit it.
11
+ * A PR with ten pushes ends up with one comment, not ten.
12
+ */
13
+ export const COMMENT_MARKER = '<!-- juxtapose-bot -->';
14
+ /** Stories that block a merge — anything a human or agent must judge. */
15
+ function blocking(report) {
16
+ return report.results.filter((r) => r.status === 'fail' || r.status === 'new' || r.status === 'stale');
17
+ }
18
+ export function buildComment({ report, artifactUrl }) {
19
+ const s = report.summary;
20
+ const needsReview = blocking(report);
21
+ const lines = [COMMENT_MARKER, ''];
22
+ // Lead with the verdict. Someone scanning a PR should not have to parse a
23
+ // table to learn whether they need to do anything.
24
+ if (needsReview.length === 0) {
25
+ lines.push(`### Visual tests passed`, '');
26
+ lines.push(`${s.pass} ${s.pass === 1 ? 'story' : 'stories'} match their baselines.`);
27
+ if (s.skip > 0)
28
+ lines.push(`${s.skip} skipped.`);
29
+ lines.push('', footer(report, artifactUrl));
30
+ return lines.join('\n');
31
+ }
32
+ lines.push(`### Visual changes need review`, '');
33
+ lines.push('| | count |', '| --- | --- |');
34
+ if (s.pass > 0)
35
+ lines.push(`| pass | ${s.pass} |`);
36
+ if (s.fail > 0)
37
+ lines.push(`| **changed** | **${s.fail}** |`);
38
+ if (s.new > 0)
39
+ lines.push(`| **new** | **${s.new}** |`);
40
+ if (s.stale > 0)
41
+ lines.push(`| **stale** | **${s.stale}** |`);
42
+ if (s.skip > 0)
43
+ lines.push(`| skipped | ${s.skip} |`);
44
+ lines.push('');
45
+ // Open by default: the whole point is that a reviewer sees what changed
46
+ // without clicking. Collapsed only when the list is long enough to bury the
47
+ // rest of the PR conversation.
48
+ const open = needsReview.length <= 10 ? ' open' : '';
49
+ lines.push(`<details${open}>`, `<summary>${needsReview.length} ${needsReview.length === 1 ? 'story' : 'stories'}</summary>`, '');
50
+ lines.push('| story | status | changed |', '| --- | --- | --- |');
51
+ for (const r of needsReview.slice(0, MAX_ROWS)) {
52
+ lines.push(`| \`${r.storyId}\` | ${label(r.status)} | ${detail(r)} |`);
53
+ }
54
+ if (needsReview.length > MAX_ROWS) {
55
+ lines.push(`| …and ${needsReview.length - MAX_ROWS} more | | |`);
56
+ }
57
+ lines.push('', '</details>', '');
58
+ lines.push('To accept these changes:', '', '```bash', 'jux approve --all', '```', '');
59
+ lines.push(footer(report, artifactUrl));
60
+ return lines.join('\n');
61
+ }
62
+ /** GitHub comments have a size limit; a suite where everything changed should
63
+ * not produce an unpostable comment. */
64
+ const MAX_ROWS = 50;
65
+ function label(status) {
66
+ switch (status) {
67
+ case 'fail':
68
+ return 'changed';
69
+ case 'new':
70
+ return 'no baseline';
71
+ case 'stale':
72
+ return 'stale baseline';
73
+ default:
74
+ return status;
75
+ }
76
+ }
77
+ function detail(r) {
78
+ if (r.error)
79
+ return r.error;
80
+ if (r.status !== 'fail')
81
+ return '—';
82
+ return formatPercent(r.mismatchPercent);
83
+ }
84
+ function footer(report, artifactUrl) {
85
+ const parts = [
86
+ `\`${report.commitSha.slice(0, 7)}\``,
87
+ `${report.capturedCount}/${report.totalCount} captured`,
88
+ `${(report.durationMs / 1000).toFixed(1)}s`,
89
+ ];
90
+ const meta = `<sub>juxtapose · ${parts.join(' · ')}</sub>`;
91
+ return artifactUrl ? `${meta}\n\n<sub>[Images and diffs](${artifactUrl})</sub>` : meta;
92
+ }
93
+ /** Never show "0.00%" next to the word "changed" — it reads as a tool bug. */
94
+ function formatPercent(pct) {
95
+ if (pct >= 0.01)
96
+ return `${pct.toFixed(2)}%`;
97
+ return pct > 0 ? '<0.01%' : '0%';
98
+ }
99
+ //# sourceMappingURL=comment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comment.js","sourceRoot":"","sources":["../../src/report/comment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Everything needed to post, read from the environment GitHub Actions
3
+ * provides. Absent outside Actions, which is the normal local case.
4
+ */
5
+ export interface GitHubContext {
6
+ token: string;
7
+ /** `owner/repo` */
8
+ repository: string;
9
+ prNumber: number;
10
+ apiUrl: string;
11
+ }
12
+ /**
13
+ * Assemble the context from the workflow environment.
14
+ *
15
+ * Returns null when anything is missing — running locally, on a push rather
16
+ * than a pull request, or without a token. That is an ordinary state, not an
17
+ * error: reporting is a side effect, and a missing token should never fail a
18
+ * build that otherwise passed.
19
+ */
20
+ export declare function resolveGitHubContext(env?: NodeJS.ProcessEnv): Promise<GitHubContext | null>;
21
+ /**
22
+ * Create or update the juxtapose comment.
23
+ *
24
+ * Returns the comment's URL, or null if nothing was posted. Never throws: a
25
+ * reporting failure must not fail a build. The caller decides the exit code
26
+ * from the run itself, which is the thing that actually matters.
27
+ */
28
+ export declare function postComment(ctx: GitHubContext, body: string): Promise<string | null>;