tuffgal 0.1.0-alpha.9 → 0.2.0-alpha.2

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 (89) hide show
  1. package/CHANGELOG.md +149 -2
  2. package/README.md +78 -3
  3. package/dist/cli.d.ts +50 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/cli.js +204 -18
  6. package/dist/cli.js.map +1 -1
  7. package/dist/commands/init.d.ts +1 -1
  8. package/dist/commands/init.d.ts.map +1 -1
  9. package/dist/commands/init.js +11 -1
  10. package/dist/commands/init.js.map +1 -1
  11. package/dist/config.d.ts +38 -1
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js +20 -1
  14. package/dist/config.js.map +1 -1
  15. package/dist/coverage/screens.d.ts +11 -6
  16. package/dist/coverage/screens.d.ts.map +1 -1
  17. package/dist/coverage/screens.js +12 -7
  18. package/dist/coverage/screens.js.map +1 -1
  19. package/dist/reporter/assets/report.css +415 -51
  20. package/dist/reporter/assets/report.js +502 -68
  21. package/dist/reporter/template.d.ts +8 -1
  22. package/dist/reporter/template.d.ts.map +1 -1
  23. package/dist/reporter/template.js +517 -85
  24. package/dist/reporter/template.js.map +1 -1
  25. package/dist/reporter/writeReport.d.ts +2 -2
  26. package/dist/reporter/writeReport.d.ts.map +1 -1
  27. package/dist/reporter/writeReport.js +3 -3
  28. package/dist/reporter/writeReport.js.map +1 -1
  29. package/dist/runner/approve.d.ts +34 -9
  30. package/dist/runner/approve.d.ts.map +1 -1
  31. package/dist/runner/approve.js +54 -10
  32. package/dist/runner/approve.js.map +1 -1
  33. package/dist/runner/approveFrom.d.ts +58 -0
  34. package/dist/runner/approveFrom.d.ts.map +1 -0
  35. package/dist/runner/approveFrom.js +384 -0
  36. package/dist/runner/approveFrom.js.map +1 -0
  37. package/dist/runner/breakpointPasses.d.ts +39 -0
  38. package/dist/runner/breakpointPasses.d.ts.map +1 -0
  39. package/dist/runner/breakpointPasses.js +107 -0
  40. package/dist/runner/breakpointPasses.js.map +1 -0
  41. package/dist/runner/bridges/database.d.ts +4 -3
  42. package/dist/runner/bridges/database.d.ts.map +1 -1
  43. package/dist/runner/bridges/database.js +4 -3
  44. package/dist/runner/bridges/database.js.map +1 -1
  45. package/dist/runner/exitCode.d.ts +34 -0
  46. package/dist/runner/exitCode.d.ts.map +1 -0
  47. package/dist/runner/exitCode.js +41 -0
  48. package/dist/runner/exitCode.js.map +1 -0
  49. package/dist/runner/manifest.d.ts +149 -0
  50. package/dist/runner/manifest.d.ts.map +1 -0
  51. package/dist/runner/manifest.js +229 -0
  52. package/dist/runner/manifest.js.map +1 -0
  53. package/dist/runner/mode.d.ts +41 -0
  54. package/dist/runner/mode.d.ts.map +1 -0
  55. package/dist/runner/mode.js +43 -0
  56. package/dist/runner/mode.js.map +1 -0
  57. package/dist/runner/orphanScan.d.ts +68 -0
  58. package/dist/runner/orphanScan.d.ts.map +1 -0
  59. package/dist/runner/orphanScan.js +150 -0
  60. package/dist/runner/orphanScan.js.map +1 -0
  61. package/dist/runner/run.d.ts +85 -1
  62. package/dist/runner/run.d.ts.map +1 -1
  63. package/dist/runner/run.js +267 -41
  64. package/dist/runner/run.js.map +1 -1
  65. package/dist/runner/runAction.d.ts +16 -0
  66. package/dist/runner/runAction.d.ts.map +1 -1
  67. package/dist/runner/runAction.js +112 -13
  68. package/dist/runner/runAction.js.map +1 -1
  69. package/dist/runner/runStory.d.ts +15 -4
  70. package/dist/runner/runStory.d.ts.map +1 -1
  71. package/dist/runner/runStory.js +41 -19
  72. package/dist/runner/runStory.js.map +1 -1
  73. package/dist/runner/storyFilter.d.ts +8 -0
  74. package/dist/runner/storyFilter.d.ts.map +1 -1
  75. package/dist/runner/storyFilter.js +11 -0
  76. package/dist/runner/storyFilter.js.map +1 -1
  77. package/dist/schema/action.d.ts +11 -11
  78. package/dist/schema/result.d.ts +125 -6
  79. package/dist/schema/result.d.ts.map +1 -1
  80. package/dist/schema/result.js.map +1 -1
  81. package/dist/screenshots/baselineStore.d.ts +92 -10
  82. package/dist/screenshots/baselineStore.d.ts.map +1 -1
  83. package/dist/screenshots/baselineStore.js +102 -18
  84. package/dist/screenshots/baselineStore.js.map +1 -1
  85. package/dist/screenshots/capture.d.ts +10 -5
  86. package/dist/screenshots/capture.d.ts.map +1 -1
  87. package/dist/screenshots/capture.js +31 -18
  88. package/dist/screenshots/capture.js.map +1 -1
  89. package/package.json +1 -1
@@ -0,0 +1,41 @@
1
+ /**
2
+ * The process exit code the CLI reports for a completed run. A small, pure
3
+ * derivation kept apart from the run driver so its precedence table is
4
+ * unit-testable in isolation.
5
+ *
6
+ * Precedence, highest wins:
7
+ * - `1` — one or more stories failed (a step threw / the harness broke).
8
+ * Always outranks every other signal: a broken run's failure is the
9
+ * headline, not the visual diff it never got to compute cleanly.
10
+ * - `3` — CI mode only: the committed environment manifest is present but its
11
+ * pixel-affecting keys diverge from this run's capture environment (a
12
+ * platform/browser/capture-mode/… drift). The comparison still ran — this
13
+ * is not an abort — but the diffs are computed against baselines rendered in
14
+ * a different environment, so the signal is "expect a full re-approve",
15
+ * distinct from ordinary pending changes. Outranks `2`: an environment
16
+ * mismatch subsumes and explains the `new`/`changed` churn it produces.
17
+ * - `2` — CI mode only: pending baseline changes a human must approve —
18
+ * `new` or `changed` stories, or `deleted` (orphaned committed baselines an
19
+ * approve/prune should retire). This is the PR-gate signal — the run itself
20
+ * succeeded, but committed baselines don't yet reflect the current UI. A
21
+ * deleted-only run still gates: the baseline set has drifted from the story
22
+ * set even though nothing rendered differently.
23
+ * - `0` — clean: every story passed, the environment matched (or no manifest
24
+ * to check), and no baselines were orphaned.
25
+ *
26
+ * Local mode never emits `2` or `3`: local runs are advisory and never read
27
+ * `paths.baselines` at all (no committed manifest to check, no committed set to
28
+ * gate against), so `new`/`changed`/`deleted`/mismatch there are informational.
29
+ * It keeps the "fail only on failed" behaviour (`1` or `0`).
30
+ */
31
+ export function deriveExitCode(mode, totals, mismatch) {
32
+ if (totals.failed > 0)
33
+ return 1;
34
+ if (mode === 'ci' && mismatch)
35
+ return 3;
36
+ if (mode === 'ci' && totals.changed + totals.new + totals.deleted > 0) {
37
+ return 2;
38
+ }
39
+ return 0;
40
+ }
41
+ //# sourceMappingURL=exitCode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exitCode.js","sourceRoot":"","sources":["../../src/runner/exitCode.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAa,EACb,MAA2B,EAC3B,QAAiB;IAEjB,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,CAAC;IAChC,IAAI,IAAI,KAAK,IAAI,IAAI,QAAQ;QAAE,OAAO,CAAC,CAAC;IACxC,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,CAAC,OAAO,GAAG,MAAM,CAAC,GAAG,GAAG,MAAM,CAAC,OAAO,GAAG,CAAC,EAAE,CAAC;QACtE,OAAO,CAAC,CAAC;IACX,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC"}
@@ -0,0 +1,149 @@
1
+ import type { ResolvedConfig } from '../config.ts';
2
+ /**
3
+ * Bump when the capture pipeline changes behaviour in a way that shifts pixels
4
+ * for the same input UI — a new default screenshot option, a change to how the
5
+ * clock is frozen, a mask-application tweak, etc. A committed baseline set was
6
+ * rendered under one `CAPTURE_SCHEMA`; a run under a higher value is a pixel-
7
+ * affecting mismatch (see {@link PIXEL_AFFECTING_KEYS}) that prompts re-approve.
8
+ *
9
+ * This is deliberately separate from `schemaVersion` (the manifest *file*
10
+ * format): the file shape can stay stable while capture behaviour evolves, and
11
+ * vice versa.
12
+ */
13
+ export declare const CAPTURE_SCHEMA = 1;
14
+ /** Manifest file-format version. Bumped only when this JSON shape changes. */
15
+ export declare const SCHEMA_VERSION = 1;
16
+ /** One breakpoint's identity as recorded in the manifest. */
17
+ export interface ManifestBreakpoint {
18
+ name: string;
19
+ width: number;
20
+ height: number;
21
+ }
22
+ /**
23
+ * The environment manifest committed beside the baselines (`<baselines>/
24
+ * manifest.json`). Written only by `approve --from` (a later wave) from the
25
+ * environment block of the candidate run's `results.json`; read by `run --ci`
26
+ * to detect when the current capture environment has drifted from the one the
27
+ * committed baselines were rendered under.
28
+ *
29
+ * `tuffgalVersion` and `playwrightVersion` are informational only — they are
30
+ * recorded for debugging but never contribute to a mismatch (a tuffgal patch
31
+ * release or a Playwright bump that does not move the bundled browser leaves
32
+ * pixels untouched). Every other field is pixel-affecting; see
33
+ * {@link PIXEL_AFFECTING_KEYS}.
34
+ */
35
+ export interface EnvironmentManifest {
36
+ schemaVersion: number;
37
+ captureSchema: number;
38
+ /** Informational: the tuffgal release that captured the baselines. */
39
+ tuffgalVersion: string;
40
+ /** Informational: the Playwright release used. */
41
+ playwrightVersion: string;
42
+ browser: string;
43
+ browserVersion: string;
44
+ platform: string;
45
+ captureMode: string;
46
+ breakpoints: ManifestBreakpoint[];
47
+ deviceScaleFactor: number;
48
+ frozenTime: string;
49
+ }
50
+ /**
51
+ * The manifest keys whose divergence between the committed manifest and the
52
+ * current run shifts rendered pixels, so a difference must gate the run (exit
53
+ * 3) and prompt a re-approve. `schemaVersion` is excluded (it versions the file
54
+ * shape, not the pixels); `tuffgalVersion`/`playwrightVersion` are excluded as
55
+ * informational. `browser` is excluded because a browser-*name* change is
56
+ * already implied by `browserVersion` (which carries the vendor+version string)
57
+ * and the runner only launches chromium today — comparing the name would add a
58
+ * redundant key with no independent signal.
59
+ */
60
+ export declare const PIXEL_AFFECTING_KEYS: readonly ["captureSchema", "browserVersion", "platform", "captureMode", "breakpoints", "deviceScaleFactor", "frozenTime"];
61
+ export type PixelAffectingKey = (typeof PIXEL_AFFECTING_KEYS)[number];
62
+ /**
63
+ * Result of reading `<baselines>/manifest.json`. Three outcomes the caller must
64
+ * distinguish, because they gate differently:
65
+ * - `ok` — a well-formed manifest was read; compare it against the run.
66
+ * - `missing` — no file on disk. The bootstrap case: the first `approve
67
+ * --from` has not written a manifest yet, so `run --ci` treats it as "no
68
+ * expectation to violate" — `environment.expected: null`, no mismatch, no
69
+ * exit 3. Never gate a project that has simply not been approved once.
70
+ * - `malformed` — a file exists but is not valid JSON or fails shape checks.
71
+ * A committed manifest that cannot be parsed is a real problem the run
72
+ * should surface, but crashing the whole comparison is too blunt. Treated
73
+ * as a mismatch-with-note: `expected: null`, `mismatch: true`, and
74
+ * `mismatchKeys: ['manifest']` so the report/comment can say "the committed
75
+ * manifest is unreadable — re-approve to rewrite it" without aborting.
76
+ */
77
+ export type ManifestReadResult = {
78
+ status: 'ok';
79
+ manifest: EnvironmentManifest;
80
+ } | {
81
+ status: 'missing';
82
+ } | {
83
+ status: 'malformed';
84
+ reason: string;
85
+ };
86
+ /** The sentinel `mismatchKeys` entry for an unreadable committed manifest. */
87
+ export declare const MALFORMED_MANIFEST_KEY = "manifest";
88
+ /** The filename of the environment manifest inside `paths.baselines`. */
89
+ export declare const MANIFEST_FILENAME = "manifest.json";
90
+ /**
91
+ * Reads and shape-checks `<baselinesDir>/manifest.json`. Tolerant by design
92
+ * (see {@link ManifestReadResult}): a missing file is the bootstrap case, a
93
+ * malformed one is surfaced as a note rather than a crash. Shape validation is
94
+ * intentionally shallow — enough to trust the field reads in {@link
95
+ * compareEnvironment}, not a full structural mirror.
96
+ */
97
+ export declare function readManifest(baselinesDir: string): Promise<ManifestReadResult>;
98
+ /**
99
+ * Returns a human-readable reason string when `value` is not a well-formed
100
+ * {@link EnvironmentManifest}, or `undefined` when it passes. Checks every key
101
+ * the comparison reads so a truncated or wrong-typed manifest is caught here
102
+ * (surfaced as `malformed`) rather than producing a garbage per-key delta.
103
+ *
104
+ * Exported so `approve --from` can run the same shape check against a candidate
105
+ * artifact's `environment.actual` before promoting it into the committed
106
+ * manifest — a well-formed-but-wrong-shaped environment block must fail closed,
107
+ * not slip into `manifest.json` and silently defeat the drift gate.
108
+ */
109
+ export declare function validateManifestShape(value: unknown): string | undefined;
110
+ /** Outcome of comparing the committed manifest against the current run's env. */
111
+ export interface EnvironmentComparison {
112
+ mismatch: boolean;
113
+ mismatchKeys: string[];
114
+ }
115
+ /**
116
+ * Compares a committed manifest read result against the environment captured
117
+ * this run, returning which pixel-affecting keys diverged.
118
+ *
119
+ * - `missing` — bootstrap: nothing to violate. `{ mismatch: false }`.
120
+ * - `malformed` — unreadable committed manifest: mismatch-with-note,
121
+ * `mismatchKeys: ['manifest']` (see {@link MALFORMED_MANIFEST_KEY}).
122
+ * - `ok` — diff each {@link PIXEL_AFFECTING_KEYS}. `breakpoints` compares deep
123
+ * and order-sensitively: the manifest records an ordered list, and a
124
+ * reorder (even at identical dimensions) is treated as a change worth a
125
+ * re-approve prompt rather than silently equal — order-sensitivity is the
126
+ * conservative pixel-safety choice. Informational keys
127
+ * (`tuffgalVersion`/`playwrightVersion`) are never compared.
128
+ */
129
+ export declare function compareEnvironment(expected: ManifestReadResult, actual: EnvironmentManifest): EnvironmentComparison;
130
+ /** The captured browser identity for one run. */
131
+ export interface CapturedBrowser {
132
+ name: string;
133
+ version: string;
134
+ }
135
+ /**
136
+ * Builds the environment manifest describing THIS run — the "actual" side of
137
+ * the comparison and the block a later `approve --from` promotes into the
138
+ * committed manifest. Everything except the live browser version is derived
139
+ * from the resolved config and the host, so the caller only supplies the
140
+ * browser identity it read from the launched Playwright browser.
141
+ *
142
+ * `deviceScaleFactor` is hardcoded `1`: the runner never sets `deviceScaleFactor`
143
+ * on `browser.newContext` (see `runStory.ts`), so Playwright's default of `1`
144
+ * applies. If a future wave makes the scale factor configurable, thread it here
145
+ * and this constant must track it — the manifest's whole job is to notice a DSF
146
+ * change, so a stale hardcode here would silently defeat that.
147
+ */
148
+ export declare function captureEnvironment(config: ResolvedConfig, browser: CapturedBrowser): EnvironmentManifest;
149
+ //# sourceMappingURL=manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/runner/manifest.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,8EAA8E;AAC9E,eAAO,MAAM,cAAc,IAAI,CAAC;AAEhC,6DAA6D;AAC7D,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,kDAAkD;IAClD,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,kBAAkB,EAAE,CAAC;IAClC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,oBAAoB,2HAQvB,CAAC;AAEX,MAAM,MAAM,iBAAiB,GAAG,CAAC,OAAO,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC;AAEtE;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IAAE,MAAM,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,mBAAmB,CAAA;CAAE,GAC/C;IAAE,MAAM,EAAE,SAAS,CAAA;CAAE,GACrB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAE5C,8EAA8E;AAC9E,eAAO,MAAM,sBAAsB,aAAa,CAAC;AAEjD,yEAAyE;AACzE,eAAO,MAAM,iBAAiB,kBAAkB,CAAC;AAEjD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAChC,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,kBAAkB,CAAC,CAmB7B;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CA8CxE;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,QAAQ,EAAE,OAAO,CAAC;IAClB,YAAY,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,kBAAkB,EAC5B,MAAM,EAAE,mBAAmB,GAC1B,qBAAqB,CAqBvB;AA6CD,iDAAiD;AACjD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,cAAc,EACtB,OAAO,EAAE,eAAe,GACvB,mBAAmB,CAkBrB"}
@@ -0,0 +1,229 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { join } from 'node:path';
4
+ /**
5
+ * Bump when the capture pipeline changes behaviour in a way that shifts pixels
6
+ * for the same input UI — a new default screenshot option, a change to how the
7
+ * clock is frozen, a mask-application tweak, etc. A committed baseline set was
8
+ * rendered under one `CAPTURE_SCHEMA`; a run under a higher value is a pixel-
9
+ * affecting mismatch (see {@link PIXEL_AFFECTING_KEYS}) that prompts re-approve.
10
+ *
11
+ * This is deliberately separate from `schemaVersion` (the manifest *file*
12
+ * format): the file shape can stay stable while capture behaviour evolves, and
13
+ * vice versa.
14
+ */
15
+ export const CAPTURE_SCHEMA = 1;
16
+ /** Manifest file-format version. Bumped only when this JSON shape changes. */
17
+ export const SCHEMA_VERSION = 1;
18
+ /**
19
+ * The manifest keys whose divergence between the committed manifest and the
20
+ * current run shifts rendered pixels, so a difference must gate the run (exit
21
+ * 3) and prompt a re-approve. `schemaVersion` is excluded (it versions the file
22
+ * shape, not the pixels); `tuffgalVersion`/`playwrightVersion` are excluded as
23
+ * informational. `browser` is excluded because a browser-*name* change is
24
+ * already implied by `browserVersion` (which carries the vendor+version string)
25
+ * and the runner only launches chromium today — comparing the name would add a
26
+ * redundant key with no independent signal.
27
+ */
28
+ export const PIXEL_AFFECTING_KEYS = [
29
+ 'captureSchema',
30
+ 'browserVersion',
31
+ 'platform',
32
+ 'captureMode',
33
+ 'breakpoints',
34
+ 'deviceScaleFactor',
35
+ 'frozenTime',
36
+ ];
37
+ /** The sentinel `mismatchKeys` entry for an unreadable committed manifest. */
38
+ export const MALFORMED_MANIFEST_KEY = 'manifest';
39
+ /** The filename of the environment manifest inside `paths.baselines`. */
40
+ export const MANIFEST_FILENAME = 'manifest.json';
41
+ /**
42
+ * Reads and shape-checks `<baselinesDir>/manifest.json`. Tolerant by design
43
+ * (see {@link ManifestReadResult}): a missing file is the bootstrap case, a
44
+ * malformed one is surfaced as a note rather than a crash. Shape validation is
45
+ * intentionally shallow — enough to trust the field reads in {@link
46
+ * compareEnvironment}, not a full structural mirror.
47
+ */
48
+ export async function readManifest(baselinesDir) {
49
+ let raw;
50
+ try {
51
+ raw = await readFile(join(baselinesDir, MANIFEST_FILENAME), 'utf8');
52
+ }
53
+ catch {
54
+ return { status: 'missing' };
55
+ }
56
+ let parsed;
57
+ try {
58
+ parsed = JSON.parse(raw);
59
+ }
60
+ catch (error) {
61
+ const reason = error instanceof Error ? error.message : 'invalid JSON';
62
+ return { status: 'malformed', reason };
63
+ }
64
+ const validation = validateManifestShape(parsed);
65
+ if (validation) {
66
+ return { status: 'malformed', reason: validation };
67
+ }
68
+ return { status: 'ok', manifest: parsed };
69
+ }
70
+ /**
71
+ * Returns a human-readable reason string when `value` is not a well-formed
72
+ * {@link EnvironmentManifest}, or `undefined` when it passes. Checks every key
73
+ * the comparison reads so a truncated or wrong-typed manifest is caught here
74
+ * (surfaced as `malformed`) rather than producing a garbage per-key delta.
75
+ *
76
+ * Exported so `approve --from` can run the same shape check against a candidate
77
+ * artifact's `environment.actual` before promoting it into the committed
78
+ * manifest — a well-formed-but-wrong-shaped environment block must fail closed,
79
+ * not slip into `manifest.json` and silently defeat the drift gate.
80
+ */
81
+ export function validateManifestShape(value) {
82
+ if (typeof value !== 'object' || value === null) {
83
+ return 'manifest is not an object';
84
+ }
85
+ const manifest = value;
86
+ for (const key of [
87
+ 'schemaVersion',
88
+ 'captureSchema',
89
+ 'deviceScaleFactor',
90
+ ]) {
91
+ if (typeof manifest[key] !== 'number') {
92
+ return `\`${key}\` must be a number`;
93
+ }
94
+ }
95
+ for (const key of [
96
+ 'tuffgalVersion',
97
+ 'playwrightVersion',
98
+ 'browser',
99
+ 'browserVersion',
100
+ 'platform',
101
+ 'captureMode',
102
+ 'frozenTime',
103
+ ]) {
104
+ if (typeof manifest[key] !== 'string') {
105
+ return `\`${key}\` must be a string`;
106
+ }
107
+ }
108
+ if (!Array.isArray(manifest.breakpoints)) {
109
+ return '`breakpoints` must be an array';
110
+ }
111
+ for (const entry of manifest.breakpoints) {
112
+ if (typeof entry !== 'object' || entry === null) {
113
+ return '`breakpoints[]` must be objects';
114
+ }
115
+ const breakpoint = entry;
116
+ if (typeof breakpoint.name !== 'string') {
117
+ return '`breakpoints[].name` must be a string';
118
+ }
119
+ if (typeof breakpoint.width !== 'number' ||
120
+ typeof breakpoint.height !== 'number') {
121
+ return '`breakpoints[].width`/`height` must be numbers';
122
+ }
123
+ }
124
+ return undefined;
125
+ }
126
+ /**
127
+ * Compares a committed manifest read result against the environment captured
128
+ * this run, returning which pixel-affecting keys diverged.
129
+ *
130
+ * - `missing` — bootstrap: nothing to violate. `{ mismatch: false }`.
131
+ * - `malformed` — unreadable committed manifest: mismatch-with-note,
132
+ * `mismatchKeys: ['manifest']` (see {@link MALFORMED_MANIFEST_KEY}).
133
+ * - `ok` — diff each {@link PIXEL_AFFECTING_KEYS}. `breakpoints` compares deep
134
+ * and order-sensitively: the manifest records an ordered list, and a
135
+ * reorder (even at identical dimensions) is treated as a change worth a
136
+ * re-approve prompt rather than silently equal — order-sensitivity is the
137
+ * conservative pixel-safety choice. Informational keys
138
+ * (`tuffgalVersion`/`playwrightVersion`) are never compared.
139
+ */
140
+ export function compareEnvironment(expected, actual) {
141
+ if (expected.status === 'missing') {
142
+ return { mismatch: false, mismatchKeys: [] };
143
+ }
144
+ if (expected.status === 'malformed') {
145
+ return { mismatch: true, mismatchKeys: [MALFORMED_MANIFEST_KEY] };
146
+ }
147
+ const manifest = expected.manifest;
148
+ const mismatchKeys = [];
149
+ for (const key of PIXEL_AFFECTING_KEYS) {
150
+ if (key === 'breakpoints') {
151
+ if (!breakpointsEqual(manifest.breakpoints, actual.breakpoints)) {
152
+ mismatchKeys.push(key);
153
+ }
154
+ continue;
155
+ }
156
+ if (manifest[key] !== actual[key]) {
157
+ mismatchKeys.push(key);
158
+ }
159
+ }
160
+ return { mismatch: mismatchKeys.length > 0, mismatchKeys };
161
+ }
162
+ /**
163
+ * Deep, order-sensitive equality of two breakpoint lists: same length, and each
164
+ * position matches on `name`/`width`/`height`. Order-sensitive on purpose — see
165
+ * {@link compareEnvironment}.
166
+ */
167
+ function breakpointsEqual(a, b) {
168
+ if (a.length !== b.length)
169
+ return false;
170
+ return a.every((entry, index) => {
171
+ const other = b[index];
172
+ return (other !== undefined &&
173
+ entry.name === other.name &&
174
+ entry.width === other.width &&
175
+ entry.height === other.height);
176
+ });
177
+ }
178
+ const requirePackage = createRequire(import.meta.url);
179
+ /**
180
+ * Reads tuffgal's own version from its `package.json`. Resolved relative to this
181
+ * module so it works from both `src/` (dev/test) and the built `dist/` tree —
182
+ * `../../package.json` is the package root in both layouts. Informational only.
183
+ */
184
+ function readTuffgalVersion() {
185
+ const pkg = requirePackage('../../package.json');
186
+ return pkg.version;
187
+ }
188
+ /**
189
+ * Reads the installed Playwright version from its `package.json` via module
190
+ * resolution (not a hardcoded string), so a dependency bump is reflected
191
+ * automatically. Informational only.
192
+ */
193
+ function readPlaywrightVersion() {
194
+ const pkg = requirePackage('playwright/package.json');
195
+ return pkg.version;
196
+ }
197
+ /**
198
+ * Builds the environment manifest describing THIS run — the "actual" side of
199
+ * the comparison and the block a later `approve --from` promotes into the
200
+ * committed manifest. Everything except the live browser version is derived
201
+ * from the resolved config and the host, so the caller only supplies the
202
+ * browser identity it read from the launched Playwright browser.
203
+ *
204
+ * `deviceScaleFactor` is hardcoded `1`: the runner never sets `deviceScaleFactor`
205
+ * on `browser.newContext` (see `runStory.ts`), so Playwright's default of `1`
206
+ * applies. If a future wave makes the scale factor configurable, thread it here
207
+ * and this constant must track it — the manifest's whole job is to notice a DSF
208
+ * change, so a stale hardcode here would silently defeat that.
209
+ */
210
+ export function captureEnvironment(config, browser) {
211
+ return {
212
+ schemaVersion: SCHEMA_VERSION,
213
+ captureSchema: CAPTURE_SCHEMA,
214
+ tuffgalVersion: readTuffgalVersion(),
215
+ playwrightVersion: readPlaywrightVersion(),
216
+ browser: browser.name,
217
+ browserVersion: browser.version,
218
+ platform: process.platform,
219
+ captureMode: config.captureMode,
220
+ breakpoints: config.breakpoints.map((breakpoint) => ({
221
+ name: breakpoint.name,
222
+ width: breakpoint.width,
223
+ height: breakpoint.height,
224
+ })),
225
+ deviceScaleFactor: 1,
226
+ frozenTime: config.frozenTime,
227
+ };
228
+ }
229
+ //# sourceMappingURL=manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/runner/manifest.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ import type { ResolvedConfig } from '../config.ts';
2
+ /**
3
+ * Which comparison contract a `tuffgal run` executes under. Foundation for the
4
+ * CI-owned-baselines model: `ci` compares actuals against the committed
5
+ * `paths.baselines` set (the source of truth, written only via approval flows),
6
+ * while `local` compares against a per-machine gitignored cache for advisory
7
+ * self-diffing. This wave only resolves + threads the mode; downstream branching
8
+ * on it arrives in later waves.
9
+ */
10
+ export type RunMode = 'ci' | 'local';
11
+ export interface RunModeInputs {
12
+ /** `--ci` flag was passed. */
13
+ ci: boolean;
14
+ /** `--local` flag was passed. */
15
+ local: boolean;
16
+ /**
17
+ * Environment to read the `CI` default from. Injected (rather than reading
18
+ * `process.env` directly) so the resolution matrix is unit-testable without
19
+ * mutating global process state.
20
+ */
21
+ env: NodeJS.ProcessEnv;
22
+ }
23
+ /**
24
+ * Resolves the run mode from the two mutually-exclusive flags and the `CI`
25
+ * environment variable:
26
+ * - `--ci` and `--local` are mutually exclusive; passing both throws.
27
+ * - An explicit flag always wins over the environment.
28
+ * - With no flag, default to `ci` when `CI` is truthy (non-empty and not an
29
+ * explicit negation — see {@link FALSY_CI}), else `local`.
30
+ */
31
+ export declare function resolveRunMode(inputs: RunModeInputs): RunMode;
32
+ /**
33
+ * The baseline root a run compares against for the given mode: the committed
34
+ * `paths.baselines` set in CI mode, the per-machine gitignored `paths.localCache`
35
+ * in local mode. This is the single source of the mode→root mapping — both the
36
+ * per-action comparison in `runAction` and the screen-coverage metric in `run`
37
+ * select their root through here, so the two can never drift. Encodes the PRD
38
+ * invariant that a local run never reads `paths.baselines`.
39
+ */
40
+ export declare function comparisonRootFor(config: ResolvedConfig, mode: RunMode): string;
41
+ //# sourceMappingURL=mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../../src/runner/mode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAEnD;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,IAAI,GAAG,OAAO,CAAC;AAErC,MAAM,WAAW,aAAa;IAC5B,8BAA8B;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,iCAAiC;IACjC,KAAK,EAAE,OAAO,CAAC;IACf;;;;OAIG;IACH,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;CACxB;AAYD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAW7D;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,OAAO,GACZ,MAAM,CAER"}
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Strings a CI system might export to explicitly opt OUT of CI behaviour. Most
3
+ * CI providers (GitHub Actions, Travis, CircleCI, GitLab, …) set `CI=true`, but
4
+ * the de-facto convention — as codified by the widely-used `ci-info`/`is-ci`
5
+ * packages — is that any non-empty `CI` value counts as "in CI" unless it is one
6
+ * of these explicit negations. We mirror that so a developer who exports
7
+ * `CI=false` locally still gets local mode.
8
+ */
9
+ const FALSY_CI = new Set(['', '0', 'false']);
10
+ /**
11
+ * Resolves the run mode from the two mutually-exclusive flags and the `CI`
12
+ * environment variable:
13
+ * - `--ci` and `--local` are mutually exclusive; passing both throws.
14
+ * - An explicit flag always wins over the environment.
15
+ * - With no flag, default to `ci` when `CI` is truthy (non-empty and not an
16
+ * explicit negation — see {@link FALSY_CI}), else `local`.
17
+ */
18
+ export function resolveRunMode(inputs) {
19
+ if (inputs.ci && inputs.local) {
20
+ throw new Error('Cannot pass both --ci and --local; choose one.');
21
+ }
22
+ if (inputs.ci)
23
+ return 'ci';
24
+ if (inputs.local)
25
+ return 'local';
26
+ const raw = inputs.env.CI;
27
+ if (raw !== undefined && !FALSY_CI.has(raw.toLowerCase())) {
28
+ return 'ci';
29
+ }
30
+ return 'local';
31
+ }
32
+ /**
33
+ * The baseline root a run compares against for the given mode: the committed
34
+ * `paths.baselines` set in CI mode, the per-machine gitignored `paths.localCache`
35
+ * in local mode. This is the single source of the mode→root mapping — both the
36
+ * per-action comparison in `runAction` and the screen-coverage metric in `run`
37
+ * select their root through here, so the two can never drift. Encodes the PRD
38
+ * invariant that a local run never reads `paths.baselines`.
39
+ */
40
+ export function comparisonRootFor(config, mode) {
41
+ return mode === 'ci' ? config.paths.baselines : config.paths.localCache;
42
+ }
43
+ //# sourceMappingURL=mode.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.js","sourceRoot":"","sources":["../../src/runner/mode.ts"],"names":[],"mappings":"AAyBA;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;AAE7C;;;;;;;GAOG;AACH,MAAM,UAAU,cAAc,CAAC,MAAqB;IAClD,IAAI,MAAM,CAAC,EAAE,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;IACpE,CAAC;IACD,IAAI,MAAM,CAAC,EAAE;QAAE,OAAO,IAAI,CAAC;IAC3B,IAAI,MAAM,CAAC,KAAK;QAAE,OAAO,OAAO,CAAC;IACjC,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC;IAC1B,IAAI,GAAG,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;QAC1D,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,MAAsB,EACtB,IAAa;IAEb,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;AAC1E,CAAC"}
@@ -0,0 +1,68 @@
1
+ import type { RunMode } from './mode.ts';
2
+ import type { DeletedBaseline, RunResult } from '../schema/result.ts';
3
+ /**
4
+ * A11y companion suffix for a breakpoint-keyed baseline PNG. `<action>/<bp>.png`
5
+ * carries `<action>/<bp>.a11y.yaml` beside it (see `baselineStore.pathsFor`).
6
+ * Exported so `approve --from` derives the same companion filename from the
7
+ * shared constant instead of re-literalling the suffix at its own producer/
8
+ * consumer boundary.
9
+ */
10
+ export declare const A11Y_SUFFIX = ".a11y.yaml";
11
+ /**
12
+ * Pre-breakpoint (legacy) baseline files: a project baselined before the
13
+ * per-breakpoint layout committed `<action>/0.png` + `<action>/a11y.yaml`. The
14
+ * scan keys these under the synthetic breakpoint name `'legacy'` so a prune step
15
+ * can delete them without re-deriving the layout. Exported so `approve --from`
16
+ * shares the same legacy-layout literals rather than re-declaring them.
17
+ */
18
+ export declare const LEGACY_PNG = "0.png";
19
+ export declare const LEGACY_A11Y = "a11y.yaml";
20
+ export declare const LEGACY_BREAKPOINT = "legacy";
21
+ /**
22
+ * Whether the orphan scan should run for this invocation. Two gates, both
23
+ * must hold:
24
+ * - CI mode only. Local runs never read `paths.baselines` (they self-diff
25
+ * against the per-machine cache), so there is no committed set to orphan.
26
+ * - Unfiltered runs only. A `--story` filter runs a deliberate subset, so
27
+ * every unselected story's baseline would look orphaned when it is merely
28
+ * unvisited; a filtered run must never mark a live baseline `deleted`.
29
+ *
30
+ * Kept pure and separate from `run.ts` so the CI/local × filtered/unfiltered
31
+ * matrix is unit-testable without launching a browser.
32
+ */
33
+ export declare function shouldScanForOrphans(mode: RunMode, storyFilter: string | undefined): boolean;
34
+ /**
35
+ * The distinct set of action names that ran at least one result this run. An
36
+ * action directory under `paths.baselines` whose name is absent from this set
37
+ * had no story compare against it — it is a candidate orphan. Derived from the
38
+ * flat `stories[].actions[]` walk so `skipped` entries (an earlier step failed)
39
+ * still count the action as executed: the baseline was reachable this run, the
40
+ * story simply broke before reaching it, which is a failure to surface — not a
41
+ * baseline to retire.
42
+ */
43
+ export declare function executedActionNames(stories: RunResult['stories']): Set<string>;
44
+ /**
45
+ * Scans `baselinesDir` for orphaned committed baselines: action directories no
46
+ * story executed this run. Detection only — the returned entries carry the
47
+ * absolute paths a later `approve --prune` deletes; this function never touches
48
+ * the filesystem beyond reading directory listings.
49
+ *
50
+ * Caller contract (enforced in `run.ts`, documented here so the module reads
51
+ * standalone): only meaningful for an UNFILTERED CI run. A `--story`-filtered run
52
+ * executes a deliberate subset, so every unselected story's baseline would look
53
+ * orphaned; the caller skips the scan entirely on a filtered run rather than
54
+ * mark live baselines `deleted`. Local mode never reads `paths.baselines` at all,
55
+ * so it never calls this.
56
+ *
57
+ * Layout handled (mirrors `baselineStore.pathsFor`):
58
+ * - breakpoint-keyed `<action>/<breakpoint>.png` (+ `.a11y.yaml` companion)
59
+ * - legacy `<action>/0.png` (+ `a11y.yaml` companion)
60
+ *
61
+ * One {@link DeletedBaseline} per orphaned breakpoint (or the single `'legacy'`
62
+ * group), each bundling the PNG with its a11y companion when present. Files that
63
+ * are neither a recognised PNG nor a matching companion are ignored, so a stray
64
+ * file in an orphaned dir neither crashes the scan nor smuggles an untracked
65
+ * path into the prune list.
66
+ */
67
+ export declare function scanOrphanedBaselines(baselinesDir: string, executed: Set<string>): Promise<DeletedBaseline[]>;
68
+ //# sourceMappingURL=orphanScan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orphanScan.d.ts","sourceRoot":"","sources":["../../src/runner/orphanScan.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,eAAe,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAEtE;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,eAAe,CAAC;AAExC;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,UAAU,CAAC;AAClC,eAAO,MAAM,WAAW,cAAc,CAAC;AACvC,eAAO,MAAM,iBAAiB,WAAW,CAAC;AAW1C;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,OAAO,EACb,WAAW,EAAE,MAAM,GAAG,SAAS,GAC9B,OAAO,CAET;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,SAAS,CAAC,SAAS,CAAC,GAC5B,GAAG,CAAC,MAAM,CAAC,CAQb;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,qBAAqB,CACzC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,GAAG,CAAC,MAAM,CAAC,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAuB5B"}