@crvouga/atlas 1.20.0 → 1.21.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.
- package/CHANGELOG.md +7 -1
- package/dist/core/src/cli.d.ts +1 -1
- package/dist/core/src/cli.js +3 -0
- package/dist/reports/src/compose.d.ts +23 -1
- package/dist/reports/src/compose.js +29 -19
- package/dist/reports/src/report-cli.d.ts +13 -0
- package/dist/reports/src/report-cli.js +6 -4
- package/dist/reports/src/ui-catalog-cli.d.ts +10 -0
- package/dist/reports/src/ui-catalog-cli.js +391 -0
- package/dist/reports/src/ui-catalog-diff.d.ts +104 -0
- package/dist/reports/src/ui-catalog-diff.js +130 -0
- package/dist/reports/src/ui-catalog-export.d.ts +82 -0
- package/dist/reports/src/ui-catalog-export.js +221 -0
- package/dist/schema/src/cli-results.d.ts +318 -1
- package/dist/schema/src/cli-results.js +89 -1
- package/docs/agents.md +5 -0
- package/docs/reference/cli.md +56 -1
- package/docs/reference/reports.md +392 -0
- package/docs/reference/schema.md +41 -1
- package/docs/ui-catalog.md +106 -12
- package/docs/ui.md +6 -1
- package/llms-full.txt +170 -11
- package/package.json +1 -1
- package/packages/schema/schemas/ui-catalog-diff-result.schema.json +201 -0
- package/packages/schema/schemas/ui-catalog-export-result.schema.json +83 -0
- package/packages/schema/schemas/ui-catalog-publish-result.schema.json +116 -0
- package/packages/schema/schemas/ui-catalog-status.schema.json +197 -0
- package/packages/visualizer/src/catalog/Picture.tsx +32 -0
- package/packages/visualizer/src/catalog/UiCatalogCompare.tsx +289 -0
- package/packages/visualizer/src/catalog/UiCatalogPage.tsx +18 -28
- package/packages/visualizer/src/catalog/history.ts +61 -0
- package/packages/visualizer/src/catalog/search.ts +14 -3
- package/packages/visualizer/src/evidence/evidence-context.tsx +2 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module Command results (`--format json`)
|
|
3
3
|
*
|
|
4
|
-
* What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings` print on standard output with `--format json` (alias
|
|
4
|
+
* What `atlas report publish`, `stream`, `merge`, `doctor` and `verify-recordings`, and `atlas ui-catalog plan`, `publish`, `status`, `diff` and `export`, print on standard output with `--format json` (alias
|
|
5
5
|
* `--json`), and what any `atlas` command prints on standard error for a command that was wrong: one JSON document,
|
|
6
6
|
* so an agent or a script never has to parse prose. (`status` and `policy` already print documents:
|
|
7
7
|
* `ReportStatusSchema`, and an IAM policy.) Each is also published as a JSON Schema.
|
|
@@ -154,6 +154,89 @@ export const InitPlanSchema = z.looseObject({
|
|
|
154
154
|
nextSteps: z.array(z.string()).describe('What to do next, in order, as plain instructions an agent can follow'),
|
|
155
155
|
warnings: z.array(z.string())
|
|
156
156
|
});
|
|
157
|
+
/** The coverage of one state in one profile, as the UI catalog words it. */
|
|
158
|
+
const UiCoverageSchema = z.enum(['captured', 'failed', 'capturing', 'skipped', 'missing']);
|
|
159
|
+
/** What `atlas ui-catalog plan --format json` and `atlas ui-catalog publish --format json` print: the one layer that was written. */
|
|
160
|
+
export const UiCatalogPublishResultSchema = z.looseObject({
|
|
161
|
+
ok: z.literal(true).describe('True: the layer was written. A state whose picture could not be read is published as `failed` and named in `warnings`; it does not fail the command'),
|
|
162
|
+
store: z.string().describe('Where it was written: a folder, `s3://…` or a server URL'),
|
|
163
|
+
layer: z.looseObject({
|
|
164
|
+
id: z.string().describe('The layer id: 64 hex digits, the SHA-256 of its content. Publishing the same input again gives the same id'),
|
|
165
|
+
kind: z.enum(['ui-state', 'ui-plan']).describe('`ui-state`: captures (`publish`). `ui-plan`: the expected set (`plan`)'),
|
|
166
|
+
producer: z.string(),
|
|
167
|
+
environment: z.string().nullable(),
|
|
168
|
+
parameters: z.record(z.string(), z.string()).describe('The rest of the profile, such as `{ "viewport": "390x844" }`'),
|
|
169
|
+
states: z.number().int().nonnegative().describe('States in the layer'),
|
|
170
|
+
captured: z.number().int().nonnegative().describe('For `publish`: states that were captured (`passed`). Zero for a plan'),
|
|
171
|
+
failed: z.number().int().nonnegative().describe('For `publish`: states recorded as failed, including those whose picture could not be read'),
|
|
172
|
+
pictures: z.number().int().nonnegative().describe('Pictures stored in the layer'),
|
|
173
|
+
pictureBytes: z.number().int().nonnegative(),
|
|
174
|
+
shard: z.string().nullable().describe('The `--shard` label, which describes the layer and never splits its slot')
|
|
175
|
+
}),
|
|
176
|
+
warnings: z.array(z.string()).describe('What was skipped and why, such as a picture file that could not be read. Never fatal')
|
|
177
|
+
});
|
|
178
|
+
/** One thing that keeps a UI catalog from being complete and good. */
|
|
179
|
+
export const UiCatalogProblemSchema = z.looseObject({
|
|
180
|
+
kind: z.enum(['failed', 'missing', 'capturing', 'unavailable', 'unreadable', 'empty']).describe('`failed`: a capture was attempted and failed. `missing`: a plan expects it and nothing was captured. `capturing`: still being taken, so not known. `unavailable`: a backend could not be read. `unreadable`: a layer could not be read. `empty`: the catalog holds no state at all'),
|
|
181
|
+
state: z.string().optional().describe('The state id, for `failed`, `missing` and `capturing`'),
|
|
182
|
+
profile: z.string().optional().describe('The profile label, for `failed`, `missing` and `capturing`'),
|
|
183
|
+
detail: z.string().optional().describe('Why, when the producer said')
|
|
184
|
+
});
|
|
185
|
+
/** What `atlas ui-catalog status --format json` prints. */
|
|
186
|
+
export const UiCatalogStatusSchema = z.looseObject({
|
|
187
|
+
ok: z.boolean().describe('True only when the catalog holds a state and has no problem: nothing failed, nothing expected is missing, nothing is still being captured, and every backend and layer was read (otherwise the exit code is 1). Skipped cells are counted, not problems'),
|
|
188
|
+
summary: z.looseObject({
|
|
189
|
+
states: z.number().int().nonnegative(),
|
|
190
|
+
profiles: z.number().int().nonnegative(),
|
|
191
|
+
cells: z.number().int().nonnegative().describe('States counted once per profile'),
|
|
192
|
+
coverage: z.record(UiCoverageSchema, z.number().int().nonnegative()).describe('Cells by coverage'),
|
|
193
|
+
undeclared: z.number().int().nonnegative().describe('Captured cells no plan expects; zero when no plan exists'),
|
|
194
|
+
planned: z.boolean().describe('Whether any plan was published. Without one, `missing` is always zero because what should exist is unknown'),
|
|
195
|
+
surfaces: z.number().int().nonnegative(),
|
|
196
|
+
uncoveredSurfaces: z.number().int().nonnegative()
|
|
197
|
+
}),
|
|
198
|
+
profiles: z.array(z.looseObject({ key: z.string(), label: z.string() })),
|
|
199
|
+
problems: z.array(UiCatalogProblemSchema),
|
|
200
|
+
sources: z.looseObject({ layers: z.number().int().nonnegative(), backends: z.array(z.looseObject({ id: z.string(), label: z.string(), available: z.boolean() })), notices: z.array(z.string()) })
|
|
201
|
+
});
|
|
202
|
+
/** One side of `atlas ui-catalog diff`. */
|
|
203
|
+
const UiCatalogCommitSchema = z.looseObject({ sha: z.string(), states: z.number().int().nonnegative(), layers: z.number().int().nonnegative() });
|
|
204
|
+
/** What `atlas ui-catalog diff --format json` prints. */
|
|
205
|
+
export const UiCatalogDiffResultSchema = z.looseObject({
|
|
206
|
+
ok: z.boolean().describe('False when a layer or backend could not be read (the comparison may be wrong), or, with `--fail-on-change`, when anything differs. Otherwise true: a difference is not a failure by itself'),
|
|
207
|
+
from: UiCatalogCommitSchema,
|
|
208
|
+
to: UiCatalogCommitSchema,
|
|
209
|
+
summary: z.looseObject({
|
|
210
|
+
coverage: z.number().int().nonnegative().describe('Pairs that stand differently (captured against failed, for example)'),
|
|
211
|
+
picture: z.number().int().nonnegative().describe('Both captured, and the stored bytes differ. Not a measure of how many pixels'),
|
|
212
|
+
unverified: z.number().int().nonnegative().describe('Both captured, but a side has no content hash, so whether the picture changed is unknown'),
|
|
213
|
+
added: z.number().int().nonnegative().describe('Only `to` has it'),
|
|
214
|
+
removed: z.number().int().nonnegative().describe('Only `from` has it. Not a regression: the other side may not have run it'),
|
|
215
|
+
unchanged: z.number().int().nonnegative(),
|
|
216
|
+
total: z.number().int().nonnegative()
|
|
217
|
+
}),
|
|
218
|
+
changes: z.array(z.looseObject({
|
|
219
|
+
state: z.string(), title: z.string(), profile: z.string(),
|
|
220
|
+
change: z.enum(['coverage', 'picture', 'unverified', 'added', 'removed']),
|
|
221
|
+
from: UiCoverageSchema.nullable(), to: UiCoverageSchema.nullable()
|
|
222
|
+
})).describe('Every pair that is not unchanged'),
|
|
223
|
+
notices: z.array(z.string()).describe('What could not be read. Any makes `ok` false: an unreadable layer is unknown, never unchanged')
|
|
224
|
+
});
|
|
225
|
+
/** What `atlas ui-catalog export --format json` prints. */
|
|
226
|
+
export const UiCatalogExportResultSchema = z.looseObject({
|
|
227
|
+
ok: z.literal(true),
|
|
228
|
+
out: z.string().describe('The HTML file that was written'),
|
|
229
|
+
bytes: z.number().int().nonnegative().describe('Its size'),
|
|
230
|
+
states: z.number().int().nonnegative(),
|
|
231
|
+
cells: z.number().int().nonnegative(),
|
|
232
|
+
pictures: z.looseObject({
|
|
233
|
+
embedded: z.number().int().nonnegative(),
|
|
234
|
+
embeddedBytes: z.number().int().nonnegative(),
|
|
235
|
+
left: z.number().int().nonnegative().describe('Pictures not embedded (over the size limit, not stored, not an image, or `--images none`); each is listed on the page with the reason')
|
|
236
|
+
}),
|
|
237
|
+
manifest: z.string().nullable().describe('The JSON manifest written beside it with `--manifest`, or null'),
|
|
238
|
+
warnings: z.array(z.string()).describe('Backends and layers that could not be read, and pictures left out. The page says so too')
|
|
239
|
+
});
|
|
157
240
|
/**
|
|
158
241
|
* JSON command contracts, shared by schema publication, agent documentation and executable command checks.
|
|
159
242
|
* A new result needs an entry here and a runtime scenario in the documentation contract tests.
|
|
@@ -166,5 +249,10 @@ export const CLI_RESULTS = {
|
|
|
166
249
|
doctor: { command: 'atlas report doctor', schemaName: 'doctor-result', schema: DoctorResultSchema },
|
|
167
250
|
status: { command: 'atlas report status', schemaName: 'report-status', schema: ReportStatusSchema },
|
|
168
251
|
verifyRecordings: { command: 'atlas report verify-recordings', schemaName: 'verify-recordings-result', schema: VerifyRecordingsResultSchema },
|
|
252
|
+
uiCatalogPlan: { command: 'atlas ui-catalog plan', schemaName: 'ui-catalog-publish-result', schema: UiCatalogPublishResultSchema },
|
|
253
|
+
uiCatalogPublish: { command: 'atlas ui-catalog publish', schemaName: 'ui-catalog-publish-result', schema: UiCatalogPublishResultSchema },
|
|
254
|
+
uiCatalogStatus: { command: 'atlas ui-catalog status', schemaName: 'ui-catalog-status', schema: UiCatalogStatusSchema },
|
|
255
|
+
uiCatalogDiff: { command: 'atlas ui-catalog diff', schemaName: 'ui-catalog-diff-result', schema: UiCatalogDiffResultSchema },
|
|
256
|
+
uiCatalogExport: { command: 'atlas ui-catalog export', schemaName: 'ui-catalog-export-result', schema: UiCatalogExportResultSchema },
|
|
169
257
|
error: { command: 'a command that was wrong (standard error)', schemaName: 'command-error', schema: CommandErrorSchema }
|
|
170
258
|
};
|
package/docs/agents.md
CHANGED
|
@@ -164,6 +164,11 @@ JSON Schema (`@crvouga/atlas/schemas/<name>.schema.json`, draft 2020-12):
|
|
|
164
164
|
| `atlas report doctor` | `doctor-result` |
|
|
165
165
|
| `atlas report status` | `report-status` |
|
|
166
166
|
| `atlas report verify-recordings` | `verify-recordings-result` |
|
|
167
|
+
| `atlas ui-catalog plan` | `ui-catalog-publish-result` |
|
|
168
|
+
| `atlas ui-catalog publish` | `ui-catalog-publish-result` |
|
|
169
|
+
| `atlas ui-catalog status` | `ui-catalog-status` |
|
|
170
|
+
| `atlas ui-catalog diff` | `ui-catalog-diff-result` |
|
|
171
|
+
| `atlas ui-catalog export` | `ui-catalog-export-result` |
|
|
167
172
|
| `a command that was wrong (standard error)` | `command-error` |
|
|
168
173
|
<!-- /generated:json-command-schemas -->
|
|
169
174
|
|
package/docs/reference/cli.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Generated from the usage text each command prints (`atlas <command> --help`) by `bun run docs`. Do not edit.
|
|
4
4
|
|
|
5
|
-
Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor` and `
|
|
5
|
+
Add `--format json` (alias `--json`) to `init`, `report publish`, `stream`, `merge`, `doctor`, `status` and every `ui-catalog` command for a JSON document on standard output (schemas in `@crvouga/atlas/schemas`). Exit codes: `0` it worked, `1` it ran and the answer is no, `2` the command was wrong.
|
|
6
6
|
|
|
7
7
|
## atlas
|
|
8
8
|
|
|
@@ -21,6 +21,7 @@ atlas — executable statecharts
|
|
|
21
21
|
atlas dev [--port 5180] develop the Atlas server and client on one host
|
|
22
22
|
atlas init [--server <url>] [--write] integrate this project: detect its suites, print the plan (atlas init --help)
|
|
23
23
|
atlas report publish|stream|status|merge|doctor|policy … layered evidence for any kind of spec (atlas report --help)
|
|
24
|
+
atlas ui-catalog plan|publish|status|diff|export … the UI catalog of captured states (atlas ui-catalog --help)
|
|
24
25
|
|
|
25
26
|
--workers path attempts that run at once (default: one per CPU, capped by the driver)
|
|
26
27
|
--seed only paths that start from this seed ("start" for paths setup starts); repeatable
|
|
@@ -172,3 +173,57 @@ atlas report — layered evidence for any kind of spec
|
|
|
172
173
|
|
|
173
174
|
Exit codes: 0 ok, 1 the report does not pass (or a check failed), 2 the command was wrong.
|
|
174
175
|
```
|
|
176
|
+
|
|
177
|
+
## atlas ui-catalog
|
|
178
|
+
|
|
179
|
+
```text
|
|
180
|
+
atlas ui-catalog — a browsable inventory of captured UI states, from any collector
|
|
181
|
+
|
|
182
|
+
atlas ui-catalog plan --states <file.json> --producer <name> [--store <dir|s3://bucket/prefix|https://…>]
|
|
183
|
+
[--environment <name>] [--param key=value]... [--shard <label>]
|
|
184
|
+
[--branch <b>] [--sha <s>] [--run-url <u>] [--run-id <id>] [--run-title <t>] [--recorded-at <iso|ms>]
|
|
185
|
+
atlas ui-catalog publish --captures <file.json> --producer <name> [the same flags as plan]
|
|
186
|
+
[--attach-root <dir>]... [--max-picture-mb <n>]
|
|
187
|
+
atlas ui-catalog status [--store <dir|s3://…|id=url>]... [--backends <module>] [filters] [--branch <name>]...
|
|
188
|
+
[--ignore-modified] [--format text|json|markdown]
|
|
189
|
+
atlas ui-catalog diff --from <commit> --to <commit> [--store …]... [--backends <module>] [filters]
|
|
190
|
+
[--fail-on-change] [--format text|json|markdown]
|
|
191
|
+
atlas ui-catalog export --out <file.html> [--store …]... [--backends <module>] [filters]
|
|
192
|
+
[--images inline|none] [--max-mb <n>] [--manifest <file.json>] [--title <t>]
|
|
193
|
+
|
|
194
|
+
filters (status, diff, export): --q <words> --coverage a,b --profile <key|label>... --area a --route r --cohort c
|
|
195
|
+
--flag f --surface s --producer p
|
|
196
|
+
|
|
197
|
+
plan Write one layer of kind ui-plan: the states (and with "inventory": true, the surfaces) that should exist under
|
|
198
|
+
this profile. Every entry is not-run, so a plan never counts as a pass. The file is {"states":[{"id":…,
|
|
199
|
+
"title"?,"route"?,"fixture"?,"cohort"?,"area"?,"flags"?,"covers"?,"inventory"?}]} or just that list.
|
|
200
|
+
publish Write one layer of kind ui-state: the states one collector or shard captured under this profile. The file is
|
|
201
|
+
{"states":[{"id":…,"title"?,"status"?:"passed|failed|running|not-run","detail"?,"route"?,"fixture"?,"cohort"?,
|
|
202
|
+
"area"?,"flags"?,"covers"?,"labels"?,"tags"?,"durationMs"?,"screenshot"?:"file.png"|{"path"|"uri",
|
|
203
|
+
"width"?,"height"?,"contentType"?}}]}. A picture file is read from beside the file and each --attach-root, never
|
|
204
|
+
from a hidden file or a link out. A passed state whose picture cannot be read is published as failed, with the
|
|
205
|
+
reason: a state without its picture is not shown as captured.
|
|
206
|
+
--environment and each --param key=value are the profile (a viewport, a color scheme, a device): another
|
|
207
|
+
profile is another result side by side. --shard only describes the layer; it never splits a result.
|
|
208
|
+
--recorded-at defaults to the modification time of the file, so publishing the same file twice is one layer.
|
|
209
|
+
status Read every store and print the catalog's summary. Exit 1 unless the catalog holds a state and there is nothing
|
|
210
|
+
to attend to: a failed capture, a state a plan expects with nothing captured, a capture still in progress, a
|
|
211
|
+
backend or layer that could not be read. Without a plan nothing is expected, so nothing is missing.
|
|
212
|
+
--profile takes a profile's key or its label, as printed by status.
|
|
213
|
+
diff Set the catalog recorded at one commit (--from) against another (--to): each state in each profile is paired
|
|
214
|
+
with itself and is unchanged, changed in coverage, changed in picture (the stored bytes differ), unverified (a
|
|
215
|
+
side has no content hash), only in --to (added) or only in --from (removed). A commit is the whole id or at
|
|
216
|
+
least 7 digits of it. Exit 0 unless a store could not be read, or, with --fail-on-change, anything differs.
|
|
217
|
+
export Write the catalog as one HTML file with its pictures embedded and its manifest in it: no server, no network.
|
|
218
|
+
It is the catalog at the moment it was made. --images none leaves the pictures out. --max-mb (default 32) limits
|
|
219
|
+
the embedded pictures; those past it are listed in the page as left out, and the manifest stays complete.
|
|
220
|
+
--manifest writes the catalog's JSON manifest too.
|
|
221
|
+
|
|
222
|
+
--format json (alias --json) every subcommand prints one JSON document instead of prose
|
|
223
|
+
(schemas: ui-catalog-publish-result, ui-catalog-status, ui-catalog-diff-result, ui-catalog-export-result).
|
|
224
|
+
A command that is wrong is {"ok":false,"error":{"message":…,"code":"usage"}} on standard error.
|
|
225
|
+
A store is a directory, s3://bucket/prefix or an Atlas server (https://…), exactly as for atlas report; select a project
|
|
226
|
+
with --project <slug> or ATLAS_PROJECT. When no --store is given, ATLAS_STORE is used.
|
|
227
|
+
|
|
228
|
+
Exit codes: 0 ok, 1 the answer is no (see each command), 2 the command was wrong.
|
|
229
|
+
```
|
|
@@ -1420,6 +1420,32 @@ export function summarize(combined: CombinedEvidence): ReportSummary
|
|
|
1420
1420
|
|
|
1421
1421
|
Counts a combined report's winning results in total and per environment, and lists the specs that have no result at all.
|
|
1422
1422
|
|
|
1423
|
+
#### `BackendLayers`
|
|
1424
|
+
|
|
1425
|
+
```ts
|
|
1426
|
+
export type BackendLayers = {
|
|
1427
|
+
/** Each layer that could be read, with the specs it recorded. A layer several backends hold appears once per backend. */
|
|
1428
|
+
readonly loaded: readonly { readonly evidence: LayerEvidence; readonly specs: readonly SpecEntry[] }[];
|
|
1429
|
+
/** Every backend given, sorted by id, with whether it could be listed. */
|
|
1430
|
+
readonly backends: readonly BackendInfo[];
|
|
1431
|
+
/** What could not be read, sorted and without repeats. */
|
|
1432
|
+
readonly notices: readonly string[];
|
|
1433
|
+
};
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
Every layer of some backends, read but not yet composed.
|
|
1437
|
+
|
|
1438
|
+
#### `readBackendLayers`
|
|
1439
|
+
|
|
1440
|
+
```ts
|
|
1441
|
+
export async function readBackendLayers(backends: readonly BackendInput[], options: { readonly concurrency?: number } = {}): Promise<BackendLayers>
|
|
1442
|
+
```
|
|
1443
|
+
|
|
1444
|
+
Reads every layer of every backend, superseded ones included, without composing them. This is what {@link combineBackends}
|
|
1445
|
+
starts from; a caller that needs layers a newer layer has replaced (to compare two commits, for example) reads them here.
|
|
1446
|
+
A backend that cannot be listed, or a layer that cannot be understood, is a notice, never an omission that looks like a pass.
|
|
1447
|
+
Throws for an invalid or repeated backend `id`, before anything is read.
|
|
1448
|
+
|
|
1423
1449
|
#### `combineBackends`
|
|
1424
1450
|
|
|
1425
1451
|
```ts
|
|
@@ -6992,6 +7018,32 @@ Whether the checks ran on exactly the commit they name. A CI checkout does. A de
|
|
|
6992
7018
|
with edits to tracked files does not: its layer is `modified`, shown with a warning and never mistaken
|
|
6993
7019
|
for the commit's own result. Outside git the origin is `unknown`.
|
|
6994
7020
|
|
|
7021
|
+
#### `keyValue`
|
|
7022
|
+
|
|
7023
|
+
```ts
|
|
7024
|
+
export function keyValue(pair: string, flag: string): [string, string]
|
|
7025
|
+
```
|
|
7026
|
+
|
|
7027
|
+
Splits `key=value` (the form of `--param` and `--label`), or throws a message naming `flag`.
|
|
7028
|
+
|
|
7029
|
+
#### `parseTime`
|
|
7030
|
+
|
|
7031
|
+
```ts
|
|
7032
|
+
export function parseTime(text: string | undefined, flag = '--recorded-at'): number | null
|
|
7033
|
+
```
|
|
7034
|
+
|
|
7035
|
+
An ISO date or epoch milliseconds from a flag, `null` when the flag was not given. Throws a message naming `flag` for anything else.
|
|
7036
|
+
|
|
7037
|
+
#### `executionOf`
|
|
7038
|
+
|
|
7039
|
+
```ts
|
|
7040
|
+
export function executionOf(rest: readonly string[], env: Env, given?: unknown): LayerRun | undefined
|
|
7041
|
+
```
|
|
7042
|
+
|
|
7043
|
+
The execution the producer named, from `--run-id`, `--run-title`, `--run-started-at`, `--run-ended-at` and then
|
|
7044
|
+
`ATLAS_RUN_ID` / `ATLAS_RUN_TITLE`, laid over a `run` the checks file gave. `undefined` when nothing names one, so the
|
|
7045
|
+
documented grouping (CI run URL, else branch@sha) applies. A title or time without an id is refused: it would have nothing to belong to.
|
|
7046
|
+
|
|
6995
7047
|
#### `runReportCommand`
|
|
6996
7048
|
|
|
6997
7049
|
```ts
|
|
@@ -7001,6 +7053,14 @@ export async function runReportCommand(argv: readonly string[]): Promise<number>
|
|
|
7001
7053
|
`atlas report …` for a terminal: a mistake in the command or a path is a message on stderr and exit
|
|
7002
7054
|
code 2, so a CI job can tell a broken invocation (2) from a report that does not pass (1).
|
|
7003
7055
|
|
|
7056
|
+
#### `wantsJson`
|
|
7057
|
+
|
|
7058
|
+
```ts
|
|
7059
|
+
export function wantsJson(argv: readonly string[]): boolean
|
|
7060
|
+
```
|
|
7061
|
+
|
|
7062
|
+
Does the command ask for a JSON document (`--format json` or `--json`) instead of prose?
|
|
7063
|
+
|
|
7004
7064
|
#### `reportMain`
|
|
7005
7065
|
|
|
7006
7066
|
```ts
|
|
@@ -10556,6 +10616,338 @@ export type ReportData = HtmlReportBundle & {
|
|
|
10556
10616
|
|
|
10557
10617
|
A run's tests (`HtmlReportBundle`) plus the document title shown for them, for the standalone / Solid report viewer.
|
|
10558
10618
|
|
|
10619
|
+
### The `atlas ui-catalog` command
|
|
10620
|
+
|
|
10621
|
+
Source: `packages/reports/src/ui-catalog-cli.ts`. Import: `@crvouga/atlas/reports/ui-catalog-cli`
|
|
10622
|
+
|
|
10623
|
+
`uiCatalogMain` is `atlas ui-catalog plan | publish | status | diff | export`: the UI catalog ({@link ./ui-catalog}) without
|
|
10624
|
+
a script. `plan` and `publish` turn a small JSON file (and the pictures beside it) into the layers of the expected set and of
|
|
10625
|
+
the captures; `status` reads every store and says whether the catalog is complete and free of failures; `diff` sets two
|
|
10626
|
+
commits against each other; `export` writes the catalog as one HTML file that needs no server. Like `atlas report`, it is
|
|
10627
|
+
written so it can be called from code and tests: it takes its arguments, environment, working directory, clock and git as
|
|
10628
|
+
parameters and returns the exit code, every command prints a JSON document with `--format json`, and a command that is wrong
|
|
10629
|
+
throws, which `runUiCatalogCommand` turns into a message (or a JSON error) and exit code 2.
|
|
10630
|
+
|
|
10631
|
+
Nothing here knows how a picture was taken: the files it reads are the same data any collector can write. Publishing a
|
|
10632
|
+
catalog to a hosting platform is not a command: it would need that platform's credentials and API, which Atlas does not
|
|
10633
|
+
hold. The file `export` writes, or a folder of them, is what to hand to whatever hosts static files.
|
|
10634
|
+
|
|
10635
|
+
Exit codes: 0 it worked, 1 it ran and the answer is no (an incomplete catalog, a failed capture, an unreadable store, a
|
|
10636
|
+
difference under `--fail-on-change`), 2 the command itself was wrong.
|
|
10637
|
+
|
|
10638
|
+
See docs/ui-catalog.md
|
|
10639
|
+
See docs/agents.md
|
|
10640
|
+
|
|
10641
|
+
#### `UI_CATALOG_USAGE`
|
|
10642
|
+
|
|
10643
|
+
```ts
|
|
10644
|
+
export const UI_CATALOG_USAGE = …
|
|
10645
|
+
```
|
|
10646
|
+
|
|
10647
|
+
The text `atlas ui-catalog --help` prints: every subcommand, its flags, what each does and the exit codes. Also the source of its entry in `docs/reference/cli.md`.
|
|
10648
|
+
|
|
10649
|
+
#### `uiCatalogMain`
|
|
10650
|
+
|
|
10651
|
+
```ts
|
|
10652
|
+
export async function uiCatalogMain(argv: readonly string[], io: ReportIo = { out: text => process.stdout.write(text) }): Promise<number>
|
|
10653
|
+
```
|
|
10654
|
+
|
|
10655
|
+
`atlas ui-catalog …`. Returns the exit code. Throws on a mistake in the command (see {@link runUiCatalogCommand}).
|
|
10656
|
+
|
|
10657
|
+
#### `runUiCatalogCommand`
|
|
10658
|
+
|
|
10659
|
+
```ts
|
|
10660
|
+
export async function runUiCatalogCommand(argv: readonly string[]): Promise<number>
|
|
10661
|
+
```
|
|
10662
|
+
|
|
10663
|
+
`atlas ui-catalog …` for a terminal: a mistake in the command or a path is a message on stderr and exit code 2, so a CI job can
|
|
10664
|
+
tell a broken invocation (2) from a catalog that needs attention (1). With `--format json` the mistake is a JSON document.
|
|
10665
|
+
|
|
10666
|
+
### UI catalog comparison
|
|
10667
|
+
|
|
10668
|
+
Source: `packages/reports/src/ui-catalog-diff.ts`. Import: `@crvouga/atlas/reports/ui-catalog-diff`
|
|
10669
|
+
|
|
10670
|
+
What changed in the UI catalog between two commits, or between two profiles, without claiming more than the evidence
|
|
10671
|
+
supports. Pure functions of {@link UiCatalog} documents (and of the layers they came from), safe in a browser; nothing here
|
|
10672
|
+
changes a status, a slot or a winner.
|
|
10673
|
+
|
|
10674
|
+
- **Two commits.** The catalog of a commit is the catalog of the layers recorded at that commit ({@link uiCatalogAtCommit}).
|
|
10675
|
+
A state is paired with itself in the same profile. A layer replaces the older one of its slot, so the effective report no
|
|
10676
|
+
longer holds an earlier commit's captures: compare from the layers themselves ({@link readBackendLayers}), which keep them.
|
|
10677
|
+
- **Two profiles.** Within one catalog, each state's cell in one profile is set beside its cell in another
|
|
10678
|
+
({@link diffUiProfiles}): desktop against mobile, light against dark.
|
|
10679
|
+
- **Pictures are compared by content.** Two captures have the same picture when their stored bytes have the same SHA-256.
|
|
10680
|
+
A different hash means different bytes, which is not a measure of how different the pixels are. When a side has no
|
|
10681
|
+
content hash (a picture given only by address, or left out), whether it changed is `unverified`, never "unchanged".
|
|
10682
|
+
- **A state only one side has** is `added` or `removed`. It may be new or renamed, or the other side's shard may not have
|
|
10683
|
+
run, and the comparison cannot tell which: it is not called a regression.
|
|
10684
|
+
- **A capture that did not pass is not a picture.** A cell that failed, is being taken or was skipped on a side is a change of
|
|
10685
|
+
`coverage`, never compared by picture.
|
|
10686
|
+
|
|
10687
|
+
See docs/ui-catalog.md
|
|
10688
|
+
|
|
10689
|
+
#### `UI_DIFF_VERSION`
|
|
10690
|
+
|
|
10691
|
+
```ts
|
|
10692
|
+
export const UI_DIFF_VERSION = …
|
|
10693
|
+
```
|
|
10694
|
+
|
|
10695
|
+
The `schemaVersion` of the comparison document.
|
|
10696
|
+
|
|
10697
|
+
#### `UiCellChange`
|
|
10698
|
+
|
|
10699
|
+
```ts
|
|
10700
|
+
export type UiCellChange = 'added' | 'removed' | 'coverage' | 'picture' | 'unverified' | 'unchanged';
|
|
10701
|
+
```
|
|
10702
|
+
|
|
10703
|
+
How one pair of cells stands. `added`: only the after side has the cell. `removed`: only the before side has it.
|
|
10704
|
+
`coverage`: both have it and it stands differently (captured against failed, for example). `picture`: both captured and
|
|
10705
|
+
the bytes differ. `unverified`: both captured, but a side has no content hash, so whether the picture changed is unknown.
|
|
10706
|
+
`unchanged`: the same coverage and, when captured, the same bytes.
|
|
10707
|
+
|
|
10708
|
+
#### `UI_CELL_CHANGES`
|
|
10709
|
+
|
|
10710
|
+
```ts
|
|
10711
|
+
export const UI_CELL_CHANGES = …
|
|
10712
|
+
```
|
|
10713
|
+
|
|
10714
|
+
The values of {@link UiCellChange}, in the order a review reads them.
|
|
10715
|
+
|
|
10716
|
+
#### `UiDiffEntry`
|
|
10717
|
+
|
|
10718
|
+
```ts
|
|
10719
|
+
export type UiDiffEntry = {
|
|
10720
|
+
/** The state's id. */
|
|
10721
|
+
readonly id: string;
|
|
10722
|
+
/** Its title, from the after side when it has the state. */
|
|
10723
|
+
readonly title: string;
|
|
10724
|
+
/** The profile both sides are in (a comparison of commits), or null when the two sides are different profiles. */
|
|
10725
|
+
readonly profile: UiProfile | null;
|
|
10726
|
+
readonly before: UiCell | null;
|
|
10727
|
+
readonly after: UiCell | null;
|
|
10728
|
+
readonly change: UiCellChange;
|
|
10729
|
+
};
|
|
10730
|
+
```
|
|
10731
|
+
|
|
10732
|
+
One state in one profile on each side.
|
|
10733
|
+
|
|
10734
|
+
#### `UiCatalogDiff`
|
|
10735
|
+
|
|
10736
|
+
```ts
|
|
10737
|
+
export type UiCatalogDiff = {
|
|
10738
|
+
readonly schemaVersion: typeof UI_DIFF_VERSION;
|
|
10739
|
+
/** How many entries are in each {@link UiCellChange}. */
|
|
10740
|
+
readonly summary: Readonly<Record<UiCellChange, number>> & { readonly total: number };
|
|
10741
|
+
/** Every pair, ordered by state id and then profile label. */
|
|
10742
|
+
readonly entries: readonly UiDiffEntry[];
|
|
10743
|
+
};
|
|
10744
|
+
```
|
|
10745
|
+
|
|
10746
|
+
A comparison, as `atlas ui-catalog diff --format json` prints it and the viewer shows it.
|
|
10747
|
+
|
|
10748
|
+
#### `pictureIdentity`
|
|
10749
|
+
|
|
10750
|
+
```ts
|
|
10751
|
+
export function pictureIdentity(image: UiImage | null | undefined): string | null
|
|
10752
|
+
```
|
|
10753
|
+
|
|
10754
|
+
What identifies a picture's content: its recorded SHA-256, else the hash in a content-addressed bundle key
|
|
10755
|
+
(`attachments/<sha256>`), else `null`. A picture given only by address, or left out, has no identity here: its content is
|
|
10756
|
+
not Atlas's to vouch for.
|
|
10757
|
+
|
|
10758
|
+
#### `diffUiCatalogs`
|
|
10759
|
+
|
|
10760
|
+
```ts
|
|
10761
|
+
export function diffUiCatalogs(before: UiCatalog, after: UiCatalog): UiCatalogDiff
|
|
10762
|
+
```
|
|
10763
|
+
|
|
10764
|
+
Sets the catalog of an earlier commit (`before`) against a later one (`after`): each state in each profile is paired with
|
|
10765
|
+
itself. A cell that is only a plan (`missing`) on one side is a change of coverage like any other. The order of the
|
|
10766
|
+
states in either catalog does not matter.
|
|
10767
|
+
|
|
10768
|
+
#### `diffUiProfiles`
|
|
10769
|
+
|
|
10770
|
+
```ts
|
|
10771
|
+
export function diffUiProfiles(catalog: UiCatalog, from: string, to: string): UiCatalogDiff
|
|
10772
|
+
```
|
|
10773
|
+
|
|
10774
|
+
Sets one profile of a catalog (`from`, a {@link UiProfile.key}) against another (`to`): each state's cell in `from` beside its
|
|
10775
|
+
cell in `to`. A state that has a cell in only one of them is `added` or `removed`. Throws when the two are the same profile,
|
|
10776
|
+
because that compares a catalog with itself.
|
|
10777
|
+
|
|
10778
|
+
#### `changedUiEntries`
|
|
10779
|
+
|
|
10780
|
+
```ts
|
|
10781
|
+
export function changedUiEntries(diff: UiCatalogDiff): readonly UiDiffEntry[]
|
|
10782
|
+
```
|
|
10783
|
+
|
|
10784
|
+
The entries of a comparison that changed, or all of them. A change the comparison cannot verify is a change worth looking at, so it is kept.
|
|
10785
|
+
|
|
10786
|
+
#### `UiCommit`
|
|
10787
|
+
|
|
10788
|
+
```ts
|
|
10789
|
+
export type UiCommit = {
|
|
10790
|
+
readonly sha: string;
|
|
10791
|
+
/** The branch of the newest layer recorded at the commit, or null. */
|
|
10792
|
+
readonly branch: string | null;
|
|
10793
|
+
/** When the newest layer at the commit was recorded, epoch milliseconds. */
|
|
10794
|
+
readonly recordedAtMs: number;
|
|
10795
|
+
/** How many layers were recorded at the commit. */
|
|
10796
|
+
readonly layers: number;
|
|
10797
|
+
};
|
|
10798
|
+
```
|
|
10799
|
+
|
|
10800
|
+
A commit that has layers of the catalog's producers.
|
|
10801
|
+
|
|
10802
|
+
#### `uiCatalogCommits`
|
|
10803
|
+
|
|
10804
|
+
```ts
|
|
10805
|
+
export function uiCatalogCommits(layers: readonly Pick<LayerEvidence['layer'], 'sha' | 'branch' | 'recordedAtMs' | 'freshness'>[]): UiCommit[]
|
|
10806
|
+
```
|
|
10807
|
+
|
|
10808
|
+
The commits layers were recorded at, newest first. A layer with no commit is left out: it cannot be compared by commit.
|
|
10809
|
+
Layers recorded from a checkout with uncommitted changes (`freshness` `modified`) are not the commit's own result and are
|
|
10810
|
+
left out too, as the gate does by default for comparisons.
|
|
10811
|
+
|
|
10812
|
+
#### `layersAtCommit`
|
|
10813
|
+
|
|
10814
|
+
```ts
|
|
10815
|
+
export function layersAtCommit(layers: readonly LayerEvidence[], sha: string): LayerEvidence[]
|
|
10816
|
+
```
|
|
10817
|
+
|
|
10818
|
+
The layers recorded at one commit (`sha` is the whole commit id, or a prefix of at least 7 hexadecimal digits that names
|
|
10819
|
+
exactly one commit among `layers`). Throws when a prefix names none, or more than one, so a comparison never silently uses
|
|
10820
|
+
the wrong commit.
|
|
10821
|
+
|
|
10822
|
+
#### `uiCatalogAtCommit`
|
|
10823
|
+
|
|
10824
|
+
```ts
|
|
10825
|
+
export function uiCatalogAtCommit(layers: readonly LayerEvidence[], sha: string, options: UiCatalogOptions = {}): UiCatalog
|
|
10826
|
+
```
|
|
10827
|
+
|
|
10828
|
+
The UI catalog as it was recorded at one commit: only the layers recorded there take part. A state the commit's layers do
|
|
10829
|
+
not hold is absent, not failed. See {@link layersAtCommit} for how `sha` is matched.
|
|
10830
|
+
|
|
10831
|
+
### UI catalog single-file export
|
|
10832
|
+
|
|
10833
|
+
Source: `packages/reports/src/ui-catalog-export.ts`. Import: `@crvouga/atlas/reports/ui-catalog-export`
|
|
10834
|
+
|
|
10835
|
+
The UI catalog as one HTML file that needs no server, no network and no build: open it from a disk, attach it to a ticket,
|
|
10836
|
+
host it anywhere that serves a file. Pictures are embedded as `data:` addresses, the manifest is embedded as JSON
|
|
10837
|
+
(`<script type="application/json" id="ui-catalog">`), and a few lines of script filter the states; with scripts off, every
|
|
10838
|
+
state is simply shown. It shows what the viewer's catalog shows, but it does not stream: it is the catalog at the moment it
|
|
10839
|
+
was made, and says when.
|
|
10840
|
+
|
|
10841
|
+
Hosting platforms cap the size of one file, which is the reason a catalog is also a store and a viewer. The export keeps to a
|
|
10842
|
+
budget ({@link gatherUiPictures}): pictures past it are listed as left out with the reason, never silently dropped, and the
|
|
10843
|
+
manifest stays complete.
|
|
10844
|
+
|
|
10845
|
+
Everything in the page is data from a test run. Text is escaped; only PNG, JPEG, GIF and WebP bytes whose first bytes say so
|
|
10846
|
+
are embedded (never SVG, never what a layer merely claims); a page address is linked only when it is `http` or `https`; the
|
|
10847
|
+
page carries a Content-Security-Policy that allows no network request at all.
|
|
10848
|
+
|
|
10849
|
+
See docs/ui-catalog.md
|
|
10850
|
+
|
|
10851
|
+
#### `UiExportPicture`
|
|
10852
|
+
|
|
10853
|
+
```ts
|
|
10854
|
+
export type UiExportPicture = { readonly contentType: string; readonly data: Uint8Array };
|
|
10855
|
+
```
|
|
10856
|
+
|
|
10857
|
+
A picture ready to embed.
|
|
10858
|
+
|
|
10859
|
+
#### `UiExportOptions`
|
|
10860
|
+
|
|
10861
|
+
```ts
|
|
10862
|
+
export type UiExportOptions = {
|
|
10863
|
+
/** The page's title. Default `UI catalog`. */
|
|
10864
|
+
readonly title?: string;
|
|
10865
|
+
/** The pictures to embed, by {@link uiPictureKey}. A cell whose key is not here shows why it has no picture. */
|
|
10866
|
+
readonly pictures?: ReadonlyMap<string, UiExportPicture>;
|
|
10867
|
+
/** Why a picture that could not be embedded is not here, by {@link uiPictureKey}: shown in its place. */
|
|
10868
|
+
readonly left?: ReadonlyMap<string, string>;
|
|
10869
|
+
/** When the export was made, epoch milliseconds. It is stated on the page so nobody mistakes it for a live view. Default: now. */
|
|
10870
|
+
readonly generatedAtMs?: number;
|
|
10871
|
+
};
|
|
10872
|
+
```
|
|
10873
|
+
|
|
10874
|
+
What {@link uiCatalogHtml} may be given besides the catalog.
|
|
10875
|
+
|
|
10876
|
+
#### `uiPictureKey`
|
|
10877
|
+
|
|
10878
|
+
```ts
|
|
10879
|
+
export function uiPictureKey(capture: Pick<UiCapture, 'origin' | 'image'> | null | undefined): string | null
|
|
10880
|
+
```
|
|
10881
|
+
|
|
10882
|
+
The key of a capture's picture in {@link UiExportOptions.pictures}: its layer and its bundle key, or `null` when the picture is not stored in a layer.
|
|
10883
|
+
|
|
10884
|
+
#### `UiPictureWant`
|
|
10885
|
+
|
|
10886
|
+
```ts
|
|
10887
|
+
export type UiPictureWant = { readonly key: string; readonly layer: string; readonly backend: string; readonly bundleKey: string; readonly bytes?: number };
|
|
10888
|
+
```
|
|
10889
|
+
|
|
10890
|
+
One picture an export may embed.
|
|
10891
|
+
|
|
10892
|
+
#### `uiPicturesWanted`
|
|
10893
|
+
|
|
10894
|
+
```ts
|
|
10895
|
+
export function uiPicturesWanted(catalog: UiCatalog): UiPictureWant[]
|
|
10896
|
+
```
|
|
10897
|
+
|
|
10898
|
+
The pictures of a catalog an export would embed, once each, in the order the page shows them. Only a cell that was captured, or
|
|
10899
|
+
that failed (a picture of what the page showed), and whose picture is stored in a layer, has one.
|
|
10900
|
+
|
|
10901
|
+
#### `sniffImage`
|
|
10902
|
+
|
|
10903
|
+
```ts
|
|
10904
|
+
export function sniffImage(data: Uint8Array): string | null
|
|
10905
|
+
```
|
|
10906
|
+
|
|
10907
|
+
The media type a picture's first bytes say it has (`image/png`, `image/jpeg`, `image/gif`, `image/webp`), or `null` for anything else, SVG included.
|
|
10908
|
+
|
|
10909
|
+
#### `UiPicturesGathered`
|
|
10910
|
+
|
|
10911
|
+
```ts
|
|
10912
|
+
export type UiPicturesGathered = {
|
|
10913
|
+
readonly pictures: ReadonlyMap<string, UiExportPicture>;
|
|
10914
|
+
/** Why each picture that is not in `pictures` is not. */
|
|
10915
|
+
readonly left: ReadonlyMap<string, string>;
|
|
10916
|
+
/** Bytes embedded. */
|
|
10917
|
+
readonly bytes: number;
|
|
10918
|
+
};
|
|
10919
|
+
```
|
|
10920
|
+
|
|
10921
|
+
What {@link gatherUiPictures} found.
|
|
10922
|
+
|
|
10923
|
+
#### `gatherUiPictures`
|
|
10924
|
+
|
|
10925
|
+
```ts
|
|
10926
|
+
export async function gatherUiPictures(wanted: readonly UiPictureWant[], read: (want: UiPictureWant) => Promise<Uint8Array | null>, options: { readonly maxBytes: number }): Promise<UiPicturesGathered>
|
|
10927
|
+
```
|
|
10928
|
+
|
|
10929
|
+
Reads the pictures an export wants through `read` (which returns the stored bytes of a picture, or `null` when the store
|
|
10930
|
+
no longer holds it) and keeps those that fit in `maxBytes` in total. A picture that is not stored, is not a PNG, JPEG, GIF or
|
|
10931
|
+
WebP, cannot be read, or would pass the budget is recorded in `left` with the reason; the pictures are taken in the page's
|
|
10932
|
+
order, so which are left out is deterministic. A `read` that throws is a picture left out, never a failed export.
|
|
10933
|
+
|
|
10934
|
+
#### `uiCatalogHtml`
|
|
10935
|
+
|
|
10936
|
+
```ts
|
|
10937
|
+
export function uiCatalogHtml(catalog: UiCatalog, options: UiExportOptions = {}): string
|
|
10938
|
+
```
|
|
10939
|
+
|
|
10940
|
+
The whole catalog as one self-contained HTML document. See the module description for what it contains and how it treats
|
|
10941
|
+
the data in it. Deterministic for the same catalog, pictures and `generatedAtMs`.
|
|
10942
|
+
|
|
10943
|
+
**Example**
|
|
10944
|
+
|
|
10945
|
+
```ts
|
|
10946
|
+
const wanted = uiPicturesWanted(catalog);
|
|
10947
|
+
const { pictures, left } = await gatherUiPictures(wanted, want => readPicture(want), { maxBytes: 32 * 1024 * 1024 });
|
|
10948
|
+
await writeFile('catalog.html', uiCatalogHtml(catalog, { pictures, left }));
|
|
10949
|
+
```
|
|
10950
|
+
|
|
10559
10951
|
### Writing a UI catalog from any collector
|
|
10560
10952
|
|
|
10561
10953
|
Source: `packages/reports/src/ui-catalog-publish.ts`. Import: `@crvouga/atlas/reports/ui-catalog-publish`
|