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.
- package/CHANGELOG.md +149 -2
- package/README.md +78 -3
- package/dist/cli.d.ts +50 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +204 -18
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +11 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/config.d.ts +38 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +20 -1
- package/dist/config.js.map +1 -1
- package/dist/coverage/screens.d.ts +11 -6
- package/dist/coverage/screens.d.ts.map +1 -1
- package/dist/coverage/screens.js +12 -7
- package/dist/coverage/screens.js.map +1 -1
- package/dist/reporter/assets/report.css +415 -51
- package/dist/reporter/assets/report.js +502 -68
- package/dist/reporter/template.d.ts +8 -1
- package/dist/reporter/template.d.ts.map +1 -1
- package/dist/reporter/template.js +517 -85
- package/dist/reporter/template.js.map +1 -1
- package/dist/reporter/writeReport.d.ts +2 -2
- package/dist/reporter/writeReport.d.ts.map +1 -1
- package/dist/reporter/writeReport.js +3 -3
- package/dist/reporter/writeReport.js.map +1 -1
- package/dist/runner/approve.d.ts +34 -9
- package/dist/runner/approve.d.ts.map +1 -1
- package/dist/runner/approve.js +54 -10
- package/dist/runner/approve.js.map +1 -1
- package/dist/runner/approveFrom.d.ts +58 -0
- package/dist/runner/approveFrom.d.ts.map +1 -0
- package/dist/runner/approveFrom.js +384 -0
- package/dist/runner/approveFrom.js.map +1 -0
- package/dist/runner/breakpointPasses.d.ts +39 -0
- package/dist/runner/breakpointPasses.d.ts.map +1 -0
- package/dist/runner/breakpointPasses.js +107 -0
- package/dist/runner/breakpointPasses.js.map +1 -0
- package/dist/runner/bridges/database.d.ts +4 -3
- package/dist/runner/bridges/database.d.ts.map +1 -1
- package/dist/runner/bridges/database.js +4 -3
- package/dist/runner/bridges/database.js.map +1 -1
- package/dist/runner/exitCode.d.ts +34 -0
- package/dist/runner/exitCode.d.ts.map +1 -0
- package/dist/runner/exitCode.js +41 -0
- package/dist/runner/exitCode.js.map +1 -0
- package/dist/runner/manifest.d.ts +149 -0
- package/dist/runner/manifest.d.ts.map +1 -0
- package/dist/runner/manifest.js +229 -0
- package/dist/runner/manifest.js.map +1 -0
- package/dist/runner/mode.d.ts +41 -0
- package/dist/runner/mode.d.ts.map +1 -0
- package/dist/runner/mode.js +43 -0
- package/dist/runner/mode.js.map +1 -0
- package/dist/runner/orphanScan.d.ts +68 -0
- package/dist/runner/orphanScan.d.ts.map +1 -0
- package/dist/runner/orphanScan.js +150 -0
- package/dist/runner/orphanScan.js.map +1 -0
- package/dist/runner/run.d.ts +85 -1
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +267 -41
- package/dist/runner/run.js.map +1 -1
- package/dist/runner/runAction.d.ts +16 -0
- package/dist/runner/runAction.d.ts.map +1 -1
- package/dist/runner/runAction.js +112 -13
- package/dist/runner/runAction.js.map +1 -1
- package/dist/runner/runStory.d.ts +15 -4
- package/dist/runner/runStory.d.ts.map +1 -1
- package/dist/runner/runStory.js +41 -19
- package/dist/runner/runStory.js.map +1 -1
- package/dist/runner/storyFilter.d.ts +8 -0
- package/dist/runner/storyFilter.d.ts.map +1 -1
- package/dist/runner/storyFilter.js +11 -0
- package/dist/runner/storyFilter.js.map +1 -1
- package/dist/schema/action.d.ts +11 -11
- package/dist/schema/result.d.ts +125 -6
- package/dist/schema/result.d.ts.map +1 -1
- package/dist/schema/result.js.map +1 -1
- package/dist/screenshots/baselineStore.d.ts +92 -10
- package/dist/screenshots/baselineStore.d.ts.map +1 -1
- package/dist/screenshots/baselineStore.js +102 -18
- package/dist/screenshots/baselineStore.js.map +1 -1
- package/dist/screenshots/capture.d.ts +10 -5
- package/dist/screenshots/capture.d.ts.map +1 -1
- package/dist/screenshots/capture.js +31 -18
- package/dist/screenshots/capture.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { lstat, readFile, readdir, rm } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
import { PNG } from 'pngjs';
|
|
4
|
+
import { ACTION_NAME_PATTERN, BREAKPOINT_SEGMENT_PATTERN, writePng, writeText, } from "../screenshots/baselineStore.js";
|
|
5
|
+
import { CAPTURE_SCHEMA, MANIFEST_FILENAME, SCHEMA_VERSION, validateManifestShape, } from "./manifest.js";
|
|
6
|
+
import { A11Y_SUFFIX, LEGACY_A11Y, LEGACY_BREAKPOINT, LEGACY_PNG, } from "./orphanScan.js";
|
|
7
|
+
import { parseRunResult } from "../schema/result.js";
|
|
8
|
+
/**
|
|
9
|
+
* Error thrown when the candidate tree fails a trust-boundary check. Carries no
|
|
10
|
+
* extra data — the message is the whole contract. Distinct type so the CLI can
|
|
11
|
+
* present it plainly; every throw here happens BEFORE any write, so the promise
|
|
12
|
+
* rejecting guarantees `paths.baselines` was not touched.
|
|
13
|
+
*/
|
|
14
|
+
export class ApproveFromError extends Error {
|
|
15
|
+
constructor(message) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ApproveFromError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Promotes a CI candidate tree (`<dir>/<action>/<breakpoint>.png` + optional
|
|
22
|
+
* `.a11y.yaml`, plus a top-level `results.json`) into the committed
|
|
23
|
+
* `paths.baselines` set, and writes `<baselines>/manifest.json` from the
|
|
24
|
+
* candidate run's captured environment.
|
|
25
|
+
*
|
|
26
|
+
* TRUST BOUNDARY. `<dir>` is an untrusted artifact — a human or bot unzipped it
|
|
27
|
+
* from CI. This is the only path that writes committed baselines, so it is
|
|
28
|
+
* fail-closed and validate-all-then-write:
|
|
29
|
+
* 1. `results.json` must parse, describe a clean CI run (`mode: 'ci'` with
|
|
30
|
+
* `totals.failed === 0` — never promote a local or broken run's partial
|
|
31
|
+
* tree, see {@link assertPromotableRun}), and carry a well-shaped
|
|
32
|
+
* `environment.actual` block (validated against the manifest shape, not
|
|
33
|
+
* just present).
|
|
34
|
+
* 2. Every tree entry is validated (allowed shape, safe names, no symlinks, no
|
|
35
|
+
* traversal) and every PNG is decoded to prove it is a real PNG.
|
|
36
|
+
* 3. ONLY after the whole tree passes are any bytes written. A single bad
|
|
37
|
+
* entry aborts with {@link ApproveFromError} and zero filesystem changes.
|
|
38
|
+
*
|
|
39
|
+
* Every candidate file's bytes are read once during phase 1 and RETAINED for
|
|
40
|
+
* phase 2, which writes exactly those bytes — the sources are never re-read, so
|
|
41
|
+
* the committed baseline is provably the payload that passed validation (no
|
|
42
|
+
* decode-swap / symlink-swap window between the check and the write).
|
|
43
|
+
*
|
|
44
|
+
* PNGs are written through {@link writePng} (decode→recompress→encode), so the
|
|
45
|
+
* promoted baseline is losslessly recompressed and provably decodable in one
|
|
46
|
+
* step — never a raw `copyFile` of unvalidated artifact bytes.
|
|
47
|
+
*/
|
|
48
|
+
export async function approveFrom(config, options) {
|
|
49
|
+
const candidateDir = options.from;
|
|
50
|
+
await assertRealDirectory(candidateDir);
|
|
51
|
+
// Phase 1 — validate the whole tree and build a write plan. Every source is
|
|
52
|
+
// read exactly ONCE here and its bytes are retained on the plan; no writes yet.
|
|
53
|
+
const result = await readCandidateResults(candidateDir);
|
|
54
|
+
assertPromotableRun(result);
|
|
55
|
+
const environment = extractEnvironment(result);
|
|
56
|
+
const plan = await planWrites(candidateDir, config.paths.baselines);
|
|
57
|
+
await assertAllPngsDecode(plan);
|
|
58
|
+
const pruneTargets = options.prune
|
|
59
|
+
? computePruneTargets(result, config.paths.baselines)
|
|
60
|
+
: [];
|
|
61
|
+
// Phase 2 — every check passed; commit the RETAINED bytes. Nothing is re-read
|
|
62
|
+
// from the candidate tree here, so the bytes committed are exactly the bytes
|
|
63
|
+
// that were validated (no decode-swap / symlink-swap window).
|
|
64
|
+
for (const write of plan) {
|
|
65
|
+
if (write.kind === 'png') {
|
|
66
|
+
await writePng(write.destination, write.bytes);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
await writeText(write.destination, write.bytes.toString('utf8'));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
await writeManifest(config.paths.baselines, environment);
|
|
73
|
+
let pruned = 0;
|
|
74
|
+
for (const group of pruneTargets) {
|
|
75
|
+
for (const path of group) {
|
|
76
|
+
await rm(path, { force: true });
|
|
77
|
+
}
|
|
78
|
+
pruned += 1;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
written: plan.filter((write) => write.kind === 'png').length,
|
|
82
|
+
pruned,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
/** Aborts unless `dir` exists and is a real directory (not a symlink). */
|
|
86
|
+
async function assertRealDirectory(dir) {
|
|
87
|
+
let stats;
|
|
88
|
+
try {
|
|
89
|
+
stats = await lstat(dir);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
throw new ApproveFromError(`--from directory not found: ${dir}`);
|
|
93
|
+
}
|
|
94
|
+
if (stats.isSymbolicLink()) {
|
|
95
|
+
throw new ApproveFromError(`--from path is a symlink, refusing: ${dir}`);
|
|
96
|
+
}
|
|
97
|
+
if (!stats.isDirectory()) {
|
|
98
|
+
throw new ApproveFromError(`--from path is not a directory: ${dir}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Reads + parses `<dir>/results.json`. Absence is a hard error: the candidate
|
|
103
|
+
* artifact is incomplete without it (prune targets and the environment manifest
|
|
104
|
+
* both come from here).
|
|
105
|
+
*/
|
|
106
|
+
async function readCandidateResults(dir) {
|
|
107
|
+
const resultsPath = join(dir, 'results.json');
|
|
108
|
+
let raw;
|
|
109
|
+
try {
|
|
110
|
+
raw = await readFile(resultsPath, 'utf8');
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
throw new ApproveFromError(`Candidate tree is missing results.json at ${resultsPath}`);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return parseRunResult(raw, resultsPath);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new ApproveFromError(error instanceof Error ? error.message : String(error));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Refuses to promote a candidate tree that did not come from a clean CI run.
|
|
124
|
+
* Three fail-closed gates, all derived from the candidate's own `results.json`:
|
|
125
|
+
* - `mode !== 'ci'` — a local (advisory) run's candidate tree is rendered
|
|
126
|
+
* against the per-machine cache on the developer's own platform, so
|
|
127
|
+
* promoting it commits cross-platform pixels the CI gate would immediately
|
|
128
|
+
* re-flag. Only a CI-mode run's renders are eligible for the committed set.
|
|
129
|
+
* - `totals.failed` is not a number — `parseRunResult` validates only that
|
|
130
|
+
* `stories` is an array, so a truncated or foreign artifact can reach here
|
|
131
|
+
* with `totals` (or `totals.failed`) missing or non-numeric. Reading it
|
|
132
|
+
* unguarded would either throw a raw `TypeError` (not an `ApproveFromError`,
|
|
133
|
+
* breaking the trust-boundary contract) or, worse, evaluate `undefined > 0`
|
|
134
|
+
* to `false` and SILENTLY pass the gate. A non-numeric `failed` is therefore
|
|
135
|
+
* an explicit fail-closed refusal, not an assumed-zero.
|
|
136
|
+
* - `totals.failed > 0` — a run with failed stories produced a PARTIAL tree
|
|
137
|
+
* (broken stories wrote no candidate, or wrote a candidate mid-failure), so
|
|
138
|
+
* promoting it would commit an incomplete baseline set. A broken run is
|
|
139
|
+
* never a source of truth.
|
|
140
|
+
* All abort before any write. `mode` is read defensively (older artifacts may
|
|
141
|
+
* omit it) — a missing `mode` is treated as non-`ci` and refused, since only a
|
|
142
|
+
* current CI run stamps `mode: 'ci'`.
|
|
143
|
+
*/
|
|
144
|
+
function assertPromotableRun(result) {
|
|
145
|
+
if (result.mode !== 'ci') {
|
|
146
|
+
throw new ApproveFromError(`Candidate results.json was produced by a ${result.mode ?? 'pre-mode'} ` +
|
|
147
|
+
'run, not a CI run — only `mode: "ci"` candidates may be promoted into ' +
|
|
148
|
+
'committed baselines. Re-run under `tuffgal run --ci`.');
|
|
149
|
+
}
|
|
150
|
+
if (typeof result.totals?.failed !== 'number') {
|
|
151
|
+
throw new ApproveFromError('Candidate results.json is missing a numeric totals.failed count — the ' +
|
|
152
|
+
'artifact is truncated or was not produced by tuffgal. Refusing to ' +
|
|
153
|
+
'promote a run whose pass/fail status cannot be verified.');
|
|
154
|
+
}
|
|
155
|
+
if (result.totals.failed > 0) {
|
|
156
|
+
throw new ApproveFromError(`Candidate results.json reports ${result.totals.failed} failed ` +
|
|
157
|
+
'story(ies) — a broken run produces a partial candidate tree that must ' +
|
|
158
|
+
'never be promoted. Fix the failures and re-run before approving.');
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Pulls the captured environment (`environment.actual`) out of the candidate
|
|
163
|
+
* run's results and validates its SHAPE before it can be promoted. Two failure
|
|
164
|
+
* modes, both fail closed:
|
|
165
|
+
* - Missing: a candidate artifact produced by a pre-manifest tuffgal cannot
|
|
166
|
+
* populate `<baselines>/manifest.json`, and writing baselines without the
|
|
167
|
+
* manifest that gates them would silently defeat the environment-drift
|
|
168
|
+
* check. The user re-runs with a manifest-aware tuffgal.
|
|
169
|
+
* - Present but wrong-shaped: `parseRunResult` only checks that `stories` is
|
|
170
|
+
* an array, so a well-formed-but-false `environment.actual` block would
|
|
171
|
+
* otherwise flow straight into the committed manifest and defeat the drift
|
|
172
|
+
* gate from the inside. It is run through the SAME {@link
|
|
173
|
+
* validateManifestShape} check `readManifest` uses (reused, not duplicated),
|
|
174
|
+
* so a malformed block aborts before any write.
|
|
175
|
+
*/
|
|
176
|
+
function extractEnvironment(result) {
|
|
177
|
+
const actual = result.environment?.actual;
|
|
178
|
+
if (!actual) {
|
|
179
|
+
throw new ApproveFromError('Candidate results.json has no environment block — it was produced by a ' +
|
|
180
|
+
'pre-manifest tuffgal. Re-run with a manifest-aware version before ' +
|
|
181
|
+
'approving, so baselines/manifest.json can be written.');
|
|
182
|
+
}
|
|
183
|
+
const invalid = validateManifestShape(actual);
|
|
184
|
+
if (invalid) {
|
|
185
|
+
throw new ApproveFromError(`Candidate results.json has a malformed environment block: ${invalid}`);
|
|
186
|
+
}
|
|
187
|
+
return actual;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Walks the candidate tree and returns the validated set of files to write.
|
|
191
|
+
* Enforces the whole allow-list: top level holds only `results.json` and action
|
|
192
|
+
* directories; each action directory holds only `<breakpoint>.png` /
|
|
193
|
+
* `<breakpoint>.a11y.yaml` (or legacy `0.png` / `a11y.yaml`). Every other shape
|
|
194
|
+
* — stray extensions, nested dirs, dotfiles, symlinks, traversal names — aborts.
|
|
195
|
+
* Every PNG is decoded here so a corrupt payload fails validation, not a write.
|
|
196
|
+
*/
|
|
197
|
+
async function planWrites(candidateDir, baselinesDir) {
|
|
198
|
+
const plan = [];
|
|
199
|
+
const topEntries = await readdir(candidateDir, { withFileTypes: true });
|
|
200
|
+
for (const entry of topEntries) {
|
|
201
|
+
assertSafeName(entry.name, candidateDir);
|
|
202
|
+
const entryPath = join(candidateDir, entry.name);
|
|
203
|
+
await assertNotSymlink(entryPath);
|
|
204
|
+
if (entry.name === 'results.json') {
|
|
205
|
+
continue; // consumed separately; never written into baselines
|
|
206
|
+
}
|
|
207
|
+
if (!entry.isDirectory()) {
|
|
208
|
+
throw new ApproveFromError(`Unexpected top-level file in candidate tree: ${entry.name} ` +
|
|
209
|
+
'(only results.json and action directories are allowed)');
|
|
210
|
+
}
|
|
211
|
+
if (!ACTION_NAME_PATTERN.test(entry.name)) {
|
|
212
|
+
throw new ApproveFromError(`Invalid action directory name "${entry.name}" — must be ` +
|
|
213
|
+
'lowercase-kebab ([a-z0-9-]+)');
|
|
214
|
+
}
|
|
215
|
+
plan.push(...(await planActionDir(candidateDir, baselinesDir, entry.name)));
|
|
216
|
+
}
|
|
217
|
+
return plan;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Validates one action directory's contents and returns its planned writes. Each
|
|
221
|
+
* source file is read exactly once here (after its symlink/name checks pass) and
|
|
222
|
+
* its bytes are retained on the {@link PlannedWrite} for phase 2 — the source is
|
|
223
|
+
* never re-read, closing the validate-then-swap TOCTOU window.
|
|
224
|
+
*/
|
|
225
|
+
async function planActionDir(candidateDir, baselinesDir, action) {
|
|
226
|
+
const actionPath = join(candidateDir, action);
|
|
227
|
+
const files = await readdir(actionPath, { withFileTypes: true });
|
|
228
|
+
const plan = [];
|
|
229
|
+
for (const file of files) {
|
|
230
|
+
assertSafeName(file.name, candidateDir);
|
|
231
|
+
const source = join(actionPath, file.name);
|
|
232
|
+
await assertNotSymlink(source);
|
|
233
|
+
if (!file.isFile()) {
|
|
234
|
+
throw new ApproveFromError(`Unexpected non-file entry in candidate tree: ${action}/${file.name} ` +
|
|
235
|
+
'(action directories hold only PNG + a11y.yaml files)');
|
|
236
|
+
}
|
|
237
|
+
const kind = classifyCandidateFile(file.name, `${action}/${file.name}`);
|
|
238
|
+
// Read once, after the symlink check, and retain the bytes — phase 2 writes
|
|
239
|
+
// exactly these, never re-reading `source`.
|
|
240
|
+
const bytes = await readFile(source);
|
|
241
|
+
// The destination is derived purely from validated names — never from any
|
|
242
|
+
// path string in the artifact — so it always lands inside <baselines>.
|
|
243
|
+
plan.push({
|
|
244
|
+
source,
|
|
245
|
+
destination: join(baselinesDir, action, file.name),
|
|
246
|
+
kind,
|
|
247
|
+
bytes,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return plan;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Decodes every planned PNG's RETAINED bytes (validation phase) so a corrupt or
|
|
254
|
+
* non-PNG payload aborts the whole approve with zero writes. Decoding the bytes
|
|
255
|
+
* already held on the plan (not a fresh read) is what makes the decode-check and
|
|
256
|
+
* the eventual write see the same payload. `writePng`'s own recompress step
|
|
257
|
+
* SWALLOWS a decode failure (it falls back to writing the given bytes), which is
|
|
258
|
+
* the right behaviour for a trusted capture but wrong for an untrusted artifact
|
|
259
|
+
* — a corrupt candidate PNG must fail closed, not land verbatim as a baseline.
|
|
260
|
+
*/
|
|
261
|
+
async function assertAllPngsDecode(plan) {
|
|
262
|
+
for (const write of plan) {
|
|
263
|
+
if (write.kind !== 'png')
|
|
264
|
+
continue;
|
|
265
|
+
try {
|
|
266
|
+
PNG.sync.read(write.bytes);
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
throw new ApproveFromError(`Candidate PNG is not a decodable PNG: ${write.source}`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Classifies a candidate file inside an action dir, validating its breakpoint
|
|
275
|
+
* stem. Accepts breakpoint-keyed `<bp>.png` / `<bp>.a11y.yaml` and the legacy
|
|
276
|
+
* `0.png` / `a11y.yaml` pair. Anything else aborts.
|
|
277
|
+
*/
|
|
278
|
+
function classifyCandidateFile(name, label) {
|
|
279
|
+
if (name === LEGACY_A11Y)
|
|
280
|
+
return 'a11y'; // legacy companion
|
|
281
|
+
if (name === LEGACY_PNG)
|
|
282
|
+
return 'png'; // legacy PNG
|
|
283
|
+
if (name.endsWith(A11Y_SUFFIX)) {
|
|
284
|
+
assertBreakpointStem(name.slice(0, -A11Y_SUFFIX.length), label);
|
|
285
|
+
return 'a11y';
|
|
286
|
+
}
|
|
287
|
+
if (name.endsWith('.png')) {
|
|
288
|
+
assertBreakpointStem(name.slice(0, -'.png'.length), label);
|
|
289
|
+
return 'png';
|
|
290
|
+
}
|
|
291
|
+
throw new ApproveFromError(`Unexpected file in candidate tree: ${label} ` +
|
|
292
|
+
'(only <breakpoint>.png and <breakpoint>.a11y.yaml are allowed)');
|
|
293
|
+
}
|
|
294
|
+
/** Aborts unless `stem` matches the runner's own breakpoint-segment shape. */
|
|
295
|
+
function assertBreakpointStem(stem, label) {
|
|
296
|
+
if (!BREAKPOINT_SEGMENT_PATTERN.test(stem)) {
|
|
297
|
+
throw new ApproveFromError(`Invalid breakpoint name in candidate tree: ${label} ` +
|
|
298
|
+
'(must be [a-z0-9_-]+)');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Rejects a filesystem name that is a dotfile or carries a path separator /
|
|
303
|
+
* traversal segment. `readdir` yields single path segments, so a `..` or a
|
|
304
|
+
* separator here means a hostile or malformed entry, not normal nesting.
|
|
305
|
+
*/
|
|
306
|
+
function assertSafeName(name, context) {
|
|
307
|
+
if (name === '' || name === '.' || name === '..') {
|
|
308
|
+
throw new ApproveFromError(`Unsafe entry name "${name}" in candidate tree under ${context}`);
|
|
309
|
+
}
|
|
310
|
+
if (name.startsWith('.')) {
|
|
311
|
+
throw new ApproveFromError(`Dotfile "${name}" not allowed in candidate tree under ${context}`);
|
|
312
|
+
}
|
|
313
|
+
if (name.includes('/') || name.includes('\\') || name.includes(sep)) {
|
|
314
|
+
throw new ApproveFromError(`Path separator in candidate entry name "${name}" under ${context}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/** Aborts if `path` is a symlink — never follow a link out of the tree. */
|
|
318
|
+
async function assertNotSymlink(path) {
|
|
319
|
+
const stats = await lstat(path);
|
|
320
|
+
if (stats.isSymbolicLink()) {
|
|
321
|
+
throw new ApproveFromError(`Symlink not allowed in candidate tree: ${path}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Recomputes the local prune targets for `--prune` from the candidate run's
|
|
326
|
+
* `results.deleted`, entirely under the resolved `baselinesDir`. The artifact's
|
|
327
|
+
* own `baselinePaths[]` strings are ignored — they describe the CI machine's
|
|
328
|
+
* filesystem and could point anywhere. Instead each entry's `action` +
|
|
329
|
+
* `breakpoint` is re-validated and re-rooted here, so a prune can only ever
|
|
330
|
+
* delete files inside this repo's baselines directory.
|
|
331
|
+
*/
|
|
332
|
+
function computePruneTargets(result, baselinesDir) {
|
|
333
|
+
const groups = [];
|
|
334
|
+
for (const entry of result.deleted ?? []) {
|
|
335
|
+
if (!ACTION_NAME_PATTERN.test(entry.action)) {
|
|
336
|
+
throw new ApproveFromError(`results.deleted has an invalid action name "${entry.action}"`);
|
|
337
|
+
}
|
|
338
|
+
const actionDir = join(baselinesDir, entry.action);
|
|
339
|
+
const paths = entry.breakpoint === LEGACY_BREAKPOINT
|
|
340
|
+
? [join(actionDir, LEGACY_PNG), join(actionDir, LEGACY_A11Y)]
|
|
341
|
+
: breakpointKeyedPaths(entry.breakpoint, actionDir);
|
|
342
|
+
for (const path of paths) {
|
|
343
|
+
assertInsideBaselines(path, baselinesDir);
|
|
344
|
+
}
|
|
345
|
+
groups.push(paths);
|
|
346
|
+
}
|
|
347
|
+
return groups;
|
|
348
|
+
}
|
|
349
|
+
/** The PNG + a11y companion paths for a breakpoint-keyed orphan. */
|
|
350
|
+
function breakpointKeyedPaths(breakpoint, actionDir) {
|
|
351
|
+
if (!BREAKPOINT_SEGMENT_PATTERN.test(breakpoint)) {
|
|
352
|
+
throw new ApproveFromError(`results.deleted has an invalid breakpoint name "${breakpoint}"`);
|
|
353
|
+
}
|
|
354
|
+
return [
|
|
355
|
+
join(actionDir, `${breakpoint}.png`),
|
|
356
|
+
join(actionDir, `${breakpoint}${A11Y_SUFFIX}`),
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Belt-and-suspenders: even though prune paths are recomputed from validated
|
|
361
|
+
* names, assert the resolved path stays inside `baselinesDir` before any delete.
|
|
362
|
+
* A path that escapes (it should be impossible after name validation) aborts.
|
|
363
|
+
*/
|
|
364
|
+
function assertInsideBaselines(path, baselinesDir) {
|
|
365
|
+
const rel = relative(baselinesDir, path);
|
|
366
|
+
if (rel === '' || rel.startsWith('..') || rel.includes(`..${sep}`)) {
|
|
367
|
+
throw new ApproveFromError(`Refusing to prune a path outside the baselines directory: ${path}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* Writes `<baselines>/manifest.json` from the candidate run's captured
|
|
372
|
+
* environment. `schemaVersion`/`captureSchema` are re-stamped from this
|
|
373
|
+
* tuffgal's constants (the single source of truth for the file format), so a
|
|
374
|
+
* candidate carrying a stale schema number is normalised on promotion.
|
|
375
|
+
*/
|
|
376
|
+
async function writeManifest(baselinesDir, environment) {
|
|
377
|
+
const manifest = {
|
|
378
|
+
...environment,
|
|
379
|
+
schemaVersion: SCHEMA_VERSION,
|
|
380
|
+
captureSchema: CAPTURE_SCHEMA,
|
|
381
|
+
};
|
|
382
|
+
await writeText(join(baselinesDir, MANIFEST_FILENAME), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
383
|
+
}
|
|
384
|
+
//# sourceMappingURL=approveFrom.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"approveFrom.js","sourceRoot":"","sources":["../../src/runner/approveFrom.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ResolvedBreakpoint, ResolvedConfig } from '../config.ts';
|
|
2
|
+
import type { StoryResult } from '../schema/result.ts';
|
|
3
|
+
import type { ScheduledStory } from './scheduler.ts';
|
|
4
|
+
/**
|
|
5
|
+
* True when `item` renders at `breakpoint` — i.e. its resolved run set (its own
|
|
6
|
+
* `breakpoints` override, else the project default) contains a breakpoint with
|
|
7
|
+
* the same name AND dimensions.
|
|
8
|
+
*/
|
|
9
|
+
export declare function storyRendersAt(item: ScheduledStory, config: ResolvedConfig, breakpoint: ResolvedBreakpoint): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* The ordered list of breakpoint passes a run executes. Multi-breakpoint runs
|
|
12
|
+
* are driven as one pass per breakpoint — a full reset/seed then the whole
|
|
13
|
+
* schedule — so each breakpoint sees a pristine database instead of inheriting
|
|
14
|
+
* the previous breakpoint's mutations (the password a `mobile` pass changed no
|
|
15
|
+
* longer leaks into the `desktop` pass). Order follows `config.breakpoints`
|
|
16
|
+
* first so the default run keeps its configured order, then appends any extra
|
|
17
|
+
* breakpoints that only per-story overrides introduce. Deduped by name+dims.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveBreakpointPasses(stories: ScheduledStory[], config: ResolvedConfig): ResolvedBreakpoint[];
|
|
20
|
+
/**
|
|
21
|
+
* Re-keys a pass's stories so the scheduler only waits on prerequisites that
|
|
22
|
+
* actually run in THIS pass. A `needs` label whose producer renders at a
|
|
23
|
+
* different breakpoint than this pass would otherwise never clear and deadlock
|
|
24
|
+
* the drain; that producer instead persisted its auth state to disk in its own
|
|
25
|
+
* (earlier) pass, so the dependency is already satisfied off-disk. `produces` is
|
|
26
|
+
* left intact so failure cascades still propagate within the pass.
|
|
27
|
+
*/
|
|
28
|
+
export declare function adaptNeedsForPass(participating: ScheduledStory[]): ScheduledStory[];
|
|
29
|
+
/**
|
|
30
|
+
* Folds one story's per-pass results back into the single `StoryResult` the
|
|
31
|
+
* report expects. Each part is the same story at one breakpoint; their actions
|
|
32
|
+
* (already tagged with their breakpoint) concatenate in pass order, the status
|
|
33
|
+
* is the worst across passes, the window spans the earliest start to the latest
|
|
34
|
+
* finish, and the first trace zip produced wins (mirroring the single-pass
|
|
35
|
+
* "first failing breakpoint" rule). A single part returns an equivalent result,
|
|
36
|
+
* so single-breakpoint runs are unchanged.
|
|
37
|
+
*/
|
|
38
|
+
export declare function mergeStoryResults(parts: StoryResult[]): StoryResult;
|
|
39
|
+
//# sourceMappingURL=breakpointPasses.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breakpointPasses.d.ts","sourceRoot":"","sources":["../../src/runner/breakpointPasses.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAYrD;;;;GAIG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,cAAc,EACpB,MAAM,EAAE,cAAc,EACtB,UAAU,EAAE,kBAAkB,GAC7B,OAAO,CAGT;AAED;;;;;;;;GAQG;AACH,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,cAAc,EAAE,EACzB,MAAM,EAAE,cAAc,GACrB,kBAAkB,EAAE,CActB;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAC/B,aAAa,EAAE,cAAc,EAAE,GAC9B,cAAc,EAAE,CASlB;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,WAAW,EAAE,GAAG,WAAW,CA6BnE"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { mergeStoryStatus, resolveRunSet } from "./runStory.js";
|
|
2
|
+
/**
|
|
3
|
+
* A breakpoint pass is uniquely a (name, width, height) triple: two stories may
|
|
4
|
+
* both name `desktop` but override its dimensions differently, and those are
|
|
5
|
+
* genuinely separate render targets that each want their own clean database.
|
|
6
|
+
*/
|
|
7
|
+
function passKey(breakpoint) {
|
|
8
|
+
return `${breakpoint.name}@${breakpoint.width}x${breakpoint.height}`;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* True when `item` renders at `breakpoint` — i.e. its resolved run set (its own
|
|
12
|
+
* `breakpoints` override, else the project default) contains a breakpoint with
|
|
13
|
+
* the same name AND dimensions.
|
|
14
|
+
*/
|
|
15
|
+
export function storyRendersAt(item, config, breakpoint) {
|
|
16
|
+
const key = passKey(breakpoint);
|
|
17
|
+
return resolveRunSet(item.story, config).some((bp) => passKey(bp) === key);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The ordered list of breakpoint passes a run executes. Multi-breakpoint runs
|
|
21
|
+
* are driven as one pass per breakpoint — a full reset/seed then the whole
|
|
22
|
+
* schedule — so each breakpoint sees a pristine database instead of inheriting
|
|
23
|
+
* the previous breakpoint's mutations (the password a `mobile` pass changed no
|
|
24
|
+
* longer leaks into the `desktop` pass). Order follows `config.breakpoints`
|
|
25
|
+
* first so the default run keeps its configured order, then appends any extra
|
|
26
|
+
* breakpoints that only per-story overrides introduce. Deduped by name+dims.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveBreakpointPasses(stories, config) {
|
|
29
|
+
const seen = new Set();
|
|
30
|
+
const passes = [];
|
|
31
|
+
const add = (bp) => {
|
|
32
|
+
const key = passKey(bp);
|
|
33
|
+
if (seen.has(key))
|
|
34
|
+
return;
|
|
35
|
+
seen.add(key);
|
|
36
|
+
passes.push(bp);
|
|
37
|
+
};
|
|
38
|
+
for (const bp of config.breakpoints)
|
|
39
|
+
add(bp);
|
|
40
|
+
for (const item of stories) {
|
|
41
|
+
for (const bp of resolveRunSet(item.story, config))
|
|
42
|
+
add(bp);
|
|
43
|
+
}
|
|
44
|
+
return passes;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Re-keys a pass's stories so the scheduler only waits on prerequisites that
|
|
48
|
+
* actually run in THIS pass. A `needs` label whose producer renders at a
|
|
49
|
+
* different breakpoint than this pass would otherwise never clear and deadlock
|
|
50
|
+
* the drain; that producer instead persisted its auth state to disk in its own
|
|
51
|
+
* (earlier) pass, so the dependency is already satisfied off-disk. `produces` is
|
|
52
|
+
* left intact so failure cascades still propagate within the pass.
|
|
53
|
+
*/
|
|
54
|
+
export function adaptNeedsForPass(participating) {
|
|
55
|
+
const producesInPass = new Set();
|
|
56
|
+
for (const item of participating) {
|
|
57
|
+
for (const label of item.produces)
|
|
58
|
+
producesInPass.add(label);
|
|
59
|
+
}
|
|
60
|
+
return participating.map((item) => ({
|
|
61
|
+
...item,
|
|
62
|
+
needs: item.needs.filter((label) => producesInPass.has(label)),
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Folds one story's per-pass results back into the single `StoryResult` the
|
|
67
|
+
* report expects. Each part is the same story at one breakpoint; their actions
|
|
68
|
+
* (already tagged with their breakpoint) concatenate in pass order, the status
|
|
69
|
+
* is the worst across passes, the window spans the earliest start to the latest
|
|
70
|
+
* finish, and the first trace zip produced wins (mirroring the single-pass
|
|
71
|
+
* "first failing breakpoint" rule). A single part returns an equivalent result,
|
|
72
|
+
* so single-breakpoint runs are unchanged.
|
|
73
|
+
*/
|
|
74
|
+
export function mergeStoryResults(parts) {
|
|
75
|
+
if (parts.length === 1)
|
|
76
|
+
return parts[0];
|
|
77
|
+
const first = parts[0];
|
|
78
|
+
let status = first.status;
|
|
79
|
+
let startedAt = first.startedAt;
|
|
80
|
+
let finishedAt = first.finishedAt;
|
|
81
|
+
let durationMs = 0;
|
|
82
|
+
let tracePath;
|
|
83
|
+
const actions = [];
|
|
84
|
+
for (const part of parts) {
|
|
85
|
+
status = mergeStoryStatus(status, part.status);
|
|
86
|
+
if (part.startedAt < startedAt)
|
|
87
|
+
startedAt = part.startedAt;
|
|
88
|
+
if (part.finishedAt > finishedAt)
|
|
89
|
+
finishedAt = part.finishedAt;
|
|
90
|
+
durationMs += part.durationMs;
|
|
91
|
+
if (tracePath === undefined && part.tracePath !== undefined) {
|
|
92
|
+
tracePath = part.tracePath;
|
|
93
|
+
}
|
|
94
|
+
actions.push(...part.actions);
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
story: first.story,
|
|
98
|
+
file: first.file,
|
|
99
|
+
status,
|
|
100
|
+
startedAt,
|
|
101
|
+
finishedAt,
|
|
102
|
+
durationMs,
|
|
103
|
+
actions,
|
|
104
|
+
tracePath,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
//# sourceMappingURL=breakpointPasses.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"breakpointPasses.js","sourceRoot":"","sources":["../../src/runner/breakpointPasses.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { ResolvedConfig } from '../../config.ts';
|
|
2
2
|
/**
|
|
3
|
-
* Invokes the consumer-supplied database reset. Called once
|
|
4
|
-
*
|
|
5
|
-
* a
|
|
3
|
+
* Invokes the consumer-supplied database reset. Called once per breakpoint
|
|
4
|
+
* pass, before that pass's stories dispatch, so each breakpoint renders against
|
|
5
|
+
* a freshly seeded database. No-op when the consumer did not supply a
|
|
6
|
+
* `database.reset` function (e.g., a static site with no backend).
|
|
6
7
|
*/
|
|
7
8
|
export declare function resetDatabase(config: ResolvedConfig): Promise<void>;
|
|
8
9
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/runner/bridges/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD
|
|
1
|
+
{"version":3,"file":"database.d.ts","sourceRoot":"","sources":["../../../src/runner/bridges/database.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzE;AAED;;;;;GAKG;AACH,wBAAsB,YAAY,CAChC,MAAM,EAAE,cAAc,EACtB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Invokes the consumer-supplied database reset. Called once
|
|
3
|
-
*
|
|
4
|
-
* a
|
|
2
|
+
* Invokes the consumer-supplied database reset. Called once per breakpoint
|
|
3
|
+
* pass, before that pass's stories dispatch, so each breakpoint renders against
|
|
4
|
+
* a freshly seeded database. No-op when the consumer did not supply a
|
|
5
|
+
* `database.reset` function (e.g., a static site with no backend).
|
|
5
6
|
*/
|
|
6
7
|
export async function resetDatabase(config) {
|
|
7
8
|
const reset = config.database?.reset;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../../src/runner/bridges/database.ts"],"names":[],"mappings":"AAEA
|
|
1
|
+
{"version":3,"file":"database.js","sourceRoot":"","sources":["../../../src/runner/bridges/database.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,MAAsB;IACxD,MAAM,KAAK,GAAG,MAAM,CAAC,QAAQ,EAAE,KAAK,CAAC;IACrC,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,OAAO;IACT,CAAC;IACD,MAAM,KAAK,EAAE,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,MAAsB,EACtB,IAAY;IAEZ,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC3C,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;QACjC,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC;QACrE,MAAM,IAAI,KAAK,CACb,qBAAqB,IAAI,2CAA2C,KAAK,EAAE,CAC5E,CAAC;IACJ,CAAC;IACD,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { RunMode } from './mode.ts';
|
|
2
|
+
import type { RunResult } from '../schema/result.ts';
|
|
3
|
+
/**
|
|
4
|
+
* The process exit code the CLI reports for a completed run. A small, pure
|
|
5
|
+
* derivation kept apart from the run driver so its precedence table is
|
|
6
|
+
* unit-testable in isolation.
|
|
7
|
+
*
|
|
8
|
+
* Precedence, highest wins:
|
|
9
|
+
* - `1` — one or more stories failed (a step threw / the harness broke).
|
|
10
|
+
* Always outranks every other signal: a broken run's failure is the
|
|
11
|
+
* headline, not the visual diff it never got to compute cleanly.
|
|
12
|
+
* - `3` — CI mode only: the committed environment manifest is present but its
|
|
13
|
+
* pixel-affecting keys diverge from this run's capture environment (a
|
|
14
|
+
* platform/browser/capture-mode/… drift). The comparison still ran — this
|
|
15
|
+
* is not an abort — but the diffs are computed against baselines rendered in
|
|
16
|
+
* a different environment, so the signal is "expect a full re-approve",
|
|
17
|
+
* distinct from ordinary pending changes. Outranks `2`: an environment
|
|
18
|
+
* mismatch subsumes and explains the `new`/`changed` churn it produces.
|
|
19
|
+
* - `2` — CI mode only: pending baseline changes a human must approve —
|
|
20
|
+
* `new` or `changed` stories, or `deleted` (orphaned committed baselines an
|
|
21
|
+
* approve/prune should retire). This is the PR-gate signal — the run itself
|
|
22
|
+
* succeeded, but committed baselines don't yet reflect the current UI. A
|
|
23
|
+
* deleted-only run still gates: the baseline set has drifted from the story
|
|
24
|
+
* set even though nothing rendered differently.
|
|
25
|
+
* - `0` — clean: every story passed, the environment matched (or no manifest
|
|
26
|
+
* to check), and no baselines were orphaned.
|
|
27
|
+
*
|
|
28
|
+
* Local mode never emits `2` or `3`: local runs are advisory and never read
|
|
29
|
+
* `paths.baselines` at all (no committed manifest to check, no committed set to
|
|
30
|
+
* gate against), so `new`/`changed`/`deleted`/mismatch there are informational.
|
|
31
|
+
* It keeps the "fail only on failed" behaviour (`1` or `0`).
|
|
32
|
+
*/
|
|
33
|
+
export declare function deriveExitCode(mode: RunMode, totals: RunResult['totals'], mismatch: boolean): 0 | 1 | 2 | 3;
|
|
34
|
+
//# sourceMappingURL=exitCode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exitCode.d.ts","sourceRoot":"","sources":["../../src/runner/exitCode.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACzC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,OAAO,EACb,MAAM,EAAE,SAAS,CAAC,QAAQ,CAAC,EAC3B,QAAQ,EAAE,OAAO,GAChB,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAOf"}
|