@crvouga/atlas 0.2.0 → 0.4.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 +13 -1
- package/README.md +21 -2
- package/dist/reports/src/evidence-live.d.ts +6 -0
- package/dist/reports/src/evidence-live.js +35 -3
- package/dist/reports/src/evidence-types.d.ts +2 -0
- package/dist/reports/src/evidence-view.d.ts +15 -2
- package/dist/reports/src/evidence-view.js +31 -3
- package/dist/reports/src/export-evidence.js +2 -10
- package/dist/reports/src/frames.d.ts +3 -1
- package/dist/reports/src/frames.js +21 -0
- package/dist/reports/src/layer-axes.d.ts +18 -0
- package/dist/reports/src/layer-axes.js +51 -0
- package/dist/reports/src/layer-catalog.js +1 -1
- package/dist/reports/src/layer-merge.d.ts +1 -1
- package/dist/reports/src/layer-merge.js +9 -4
- package/dist/reports/src/layer-source.js +3 -0
- package/dist/reports/src/layer-types.d.ts +2 -0
- package/dist/reports/src/layers-route.js +2 -5
- package/dist/reports/src/protocol.d.ts +4 -0
- package/dist/reports/src/publish.d.ts +2 -0
- package/dist/reports/src/publish.js +6 -11
- package/dist/reports/src/recording-keys.js +4 -0
- package/dist/reports/src/report-parts.js +3 -0
- package/dist/reports/src/report-types.d.ts +19 -2
- package/dist/reports/src/source-entries.js +2 -12
- package/package.json +5 -4
- package/packages/reports/README.md +4 -2
- package/packages/reports/STORAGE.md +1 -1
- package/packages/visualizer/README.md +9 -6
- package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/scripts/make-fixtures.ts +2 -0
- package/packages/visualizer/scripts/publish-data.ts +1 -2
- package/packages/visualizer/src/app/AppShell.tsx +11 -12
- package/packages/visualizer/src/app/gazetteer.module.css +12 -2
- package/packages/visualizer/src/app/gazetteer.tsx +38 -26
- package/packages/visualizer/src/components/Header.module.css +20 -4
- package/packages/visualizer/src/components/Header.tsx +7 -4
- package/packages/visualizer/src/data/model/build.ts +17 -5
- package/packages/visualizer/src/data/model/types.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +6 -1
- package/packages/visualizer/src/evidence/check-detail.tsx +35 -3
- package/packages/visualizer/src/evidence/evidence.module.css +20 -0
- package/packages/visualizer/src/evidence/marks.tsx +2 -0
- package/packages/visualizer/src/evidence/overview.tsx +3 -3
- package/packages/visualizer/src/evidence/recording-page.tsx +19 -2
- package/packages/visualizer/src/evidence/recording.module.css +4 -8
- package/packages/visualizer/src/evidence/recording.tsx +74 -83
- package/packages/visualizer/src/evidence/snapshot.tsx +5 -4
- package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
- package/packages/visualizer/src/evidence/spec-index.tsx +3 -11
- package/packages/visualizer/src/find/Finder.module.css +168 -0
- package/packages/visualizer/src/find/Finder.tsx +303 -0
- package/packages/visualizer/src/find/corpus.test.ts +77 -0
- package/packages/visualizer/src/find/corpus.ts +368 -0
- package/packages/visualizer/src/find/open.ts +51 -0
- package/packages/visualizer/src/find/rank.test.ts +237 -0
- package/packages/visualizer/src/find/rank.ts +405 -0
- package/packages/visualizer/src/find/semantic.test.ts +62 -0
- package/packages/visualizer/src/find/semantic.ts +93 -0
- package/packages/visualizer/src/find/semantic.worker.ts +40 -0
- package/packages/visualizer/src/looks/Stage.tsx +322 -0
- package/packages/visualizer/src/looks/collect.test.ts +158 -0
- package/packages/visualizer/src/looks/collect.ts +245 -0
- package/packages/visualizer/src/looks/stage.module.css +300 -0
- package/packages/visualizer/src/reachable.test.ts +1 -1
- package/packages/visualizer/src/state/ui-store.ts +6 -2
- package/packages/visualizer/src/views/SidePanel.tsx +39 -49
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +9 -1
- package/packages/visualizer/src/app/search-query.tsx +0 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
# Changelog: @crvouga/atlas
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.4.0 (2026-10-14)
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- compose evidence by environment (#6) ([030f86b](https://github.com/crvouga/atlas/commit/030f86b1680851e3744cc977dc1c2a44e91f9926))
|
|
8
|
+
|
|
9
|
+
## 0.3.0 (2026-10-13)
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
- run the todo specs, find them in plain language, and play their evidence (#5) ([caa0df8](https://github.com/crvouga/atlas/commit/caa0df8fec96aa8e910dae53afc59ae705707359))
|
|
14
|
+
|
|
15
|
+
## 0.2.0 (2026-10-12)
|
|
4
16
|
|
|
5
17
|
### Features
|
|
6
18
|
|
package/README.md
CHANGED
|
@@ -81,6 +81,10 @@ served by a small Node server) specified as two charts:
|
|
|
81
81
|
- `specs/getting-started.machine.yaml`, the child chart in XState YAML. Its two final states hand
|
|
82
82
|
back to the parent through `meta.doneEvent`.
|
|
83
83
|
- `specs/journeys.yaml`, four curated journeys.
|
|
84
|
+
- `specs/getting-started.feature` and `specs/working-the-list.feature`, the same stories in Gherkin.
|
|
85
|
+
- `specs/todo.qnt`, the list, its sync, and its actions as a Quint model.
|
|
86
|
+
|
|
87
|
+
The visualizer lists the features under Gherkin and the model under Quint.
|
|
84
88
|
|
|
85
89
|
It has every kind of event: user events (taps and typing), a **system** event ("Sync fails", which
|
|
86
90
|
the test triggers through a real HTTP endpoint on the example server), a **time** event ("Retry
|
|
@@ -98,7 +102,7 @@ bunx playwright install chromium
|
|
|
98
102
|
bun run example:lint # pure, reachable, no undocumented dead ends, journeys replay, everything implemented
|
|
99
103
|
bun run example:plan # 4 journeys + 1 generated path cover all 14 transitions
|
|
100
104
|
bun run example:fast # run every path quickly
|
|
101
|
-
bun run example:run # showcase
|
|
105
|
+
bun run example:run # statechart showcase, then the Gherkin features and the Quint model
|
|
102
106
|
bun test examples # the same spec against an in-memory model, as a test suite
|
|
103
107
|
|
|
104
108
|
bun run example:clients # clients/: one chart across a phone, a desktop and the sync server at once
|
|
@@ -110,12 +114,25 @@ visualizer updates when a run finishes.
|
|
|
110
114
|
|
|
111
115
|
```sh
|
|
112
116
|
bun run atlas:run # example:clients, then example:run: every chart of the example, in showcase mode
|
|
113
|
-
bun run atlas:viz # the example's specs
|
|
117
|
+
bun run atlas:viz # the example's specs, chart runs, and layered reports
|
|
114
118
|
```
|
|
115
119
|
|
|
116
120
|
Each config writes its own run and the visualizer shows one run at a time: the latest is the
|
|
117
121
|
single-browser run, and the run across clients is the one before it in the run picker.
|
|
118
122
|
|
|
123
|
+
`bun run viz:dev` opens the full coffee example with a layered report, without waiting
|
|
124
|
+
for a run. Gherkin, Quint, DMN, Alloy and OpenSLO documents sit beside the charts. The same scenarios
|
|
125
|
+
are filled by separate layers across desktop web, mobile web, iOS and Android, and across versions,
|
|
126
|
+
branches and backends (this machine, shared reports, and a device lab). A layer that repeated one
|
|
127
|
+
scenario leaves the others as the last layer that ran them. One feature is left with no results.
|
|
128
|
+
**Sources** in the header switches any of those off. The chart runs stay on the statechart map.
|
|
129
|
+
|
|
130
|
+
`bun run atlas:viz` opens the todo example the same way. The Gherkin, Quint, DMN, Alloy and OpenSLO
|
|
131
|
+
tabs already show five layers, so each environment is visible before a run. `example:run` adds one
|
|
132
|
+
desktop web layer for the scenarios and traces it actually ran. That layer replaces only those
|
|
133
|
+
desktop web results. The other environments stay. `ATLAS_REPORTS_MODULE` replaces this example API
|
|
134
|
+
with a host's reports.
|
|
135
|
+
|
|
119
136
|
A showcase run ends with:
|
|
120
137
|
|
|
121
138
|
```
|
|
@@ -124,6 +141,8 @@ Transitions 14/14 passed, 0 failed, 0 not reached
|
|
|
124
141
|
Manifest: examples/todo/atlas-runs/<run-id>/manifest.json
|
|
125
142
|
```
|
|
126
143
|
|
|
144
|
+
`example:run` then runs the Gherkin features in a browser and the Quint model, and publishes that desktop web layer beside the ones already in the report.
|
|
145
|
+
|
|
127
146
|
`bun test examples` runs [`model.test.ts`](examples/todo/model.test.ts): the same charts and journeys
|
|
128
147
|
against `TodoModel`, a plain class, through `functionDriver` and `describeAtlas`. Only the driver
|
|
129
148
|
and the implementation differ, which is the point: the spec belongs to the product, not to one
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Clock } from './clock.js';
|
|
2
2
|
import type { ActiveRun, LayerEvidence, LayerHeader } from './evidence-types.js';
|
|
3
|
+
import type { LayerParameters } from './layer-types.js';
|
|
3
4
|
import type { ObjectStore } from './object-store.js';
|
|
4
5
|
/** What a running spec suite announces under `active/` in the local store. */
|
|
5
6
|
export type LiveRunRecord = {
|
|
@@ -7,7 +8,12 @@ export type LiveRunRecord = {
|
|
|
7
8
|
readonly bucket: string;
|
|
8
9
|
readonly startedAtMs: number;
|
|
9
10
|
readonly plannedChecks?: number;
|
|
11
|
+
/** The client this suite is checking. A different client does not replace it. */
|
|
12
|
+
readonly environment?: string;
|
|
13
|
+
readonly parameters?: LayerParameters;
|
|
10
14
|
};
|
|
15
|
+
/** A run announcement with the fields the live layer can trust. Null when it cannot be a run. */
|
|
16
|
+
export declare function liveRunRecord(value: unknown): LiveRunRecord | null;
|
|
11
17
|
/** How the host reaches the results a spec run has written so far. */
|
|
12
18
|
export type LiveSource = {
|
|
13
19
|
/** The bucket the run writes each finished test into, or null when it cannot be read. */
|
|
@@ -1,11 +1,40 @@
|
|
|
1
1
|
import { eachLimited } from './completion-order.js';
|
|
2
|
+
import { environmentOf } from './evidence-view.js';
|
|
2
3
|
import { sourceId } from './keys.js';
|
|
4
|
+
import { normalizeEnvironment } from './layer-axes.js';
|
|
3
5
|
import { layerEvidence } from './layer-merge.js';
|
|
4
6
|
import { readUtf8 } from './object-io.js';
|
|
5
7
|
import { SPEC_RUN_ID } from './publish.js';
|
|
6
8
|
import { testPrefix } from './recording-keys.js';
|
|
7
9
|
import { sourcePart } from './source-part.js';
|
|
8
10
|
import { suiteStatus } from './status.js';
|
|
11
|
+
function readParameters(value) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
13
|
+
return {};
|
|
14
|
+
const parameters = {};
|
|
15
|
+
for (const [key, item] of Object.entries(value))
|
|
16
|
+
if (typeof item === 'string' && item)
|
|
17
|
+
parameters[key] = item;
|
|
18
|
+
return parameters;
|
|
19
|
+
}
|
|
20
|
+
/** A run announcement with the fields the live layer can trust. Null when it cannot be a run. */
|
|
21
|
+
export function liveRunRecord(value) {
|
|
22
|
+
if (!value || typeof value !== 'object')
|
|
23
|
+
return null;
|
|
24
|
+
const run = value;
|
|
25
|
+
if (!run.packageDir || !run.bucket || typeof run.startedAtMs !== 'number')
|
|
26
|
+
return null;
|
|
27
|
+
const parameters = readParameters(run.parameters);
|
|
28
|
+
const environment = typeof run.environment === 'string' ? normalizeEnvironment(run.environment) : null;
|
|
29
|
+
return {
|
|
30
|
+
packageDir: run.packageDir,
|
|
31
|
+
bucket: run.bucket,
|
|
32
|
+
startedAtMs: run.startedAtMs,
|
|
33
|
+
...(typeof run.plannedChecks === 'number' ? { plannedChecks: run.plannedChecks } : {}),
|
|
34
|
+
...(environment ? { environment } : {}),
|
|
35
|
+
...(Object.keys(parameters).length > 0 ? { parameters } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
9
38
|
/** A finished run stays visible this long while its layer is being collected. */
|
|
10
39
|
const LINGER_MS = 30_000;
|
|
11
40
|
const REPORT = '/report.json';
|
|
@@ -46,8 +75,9 @@ export function createLiveEvidence(args) {
|
|
|
46
75
|
}
|
|
47
76
|
});
|
|
48
77
|
const tests = [...state.metas.values()].sort((a, b) => (a.testId < b.testId ? -1 : a.testId > b.testId ? 1 : 0));
|
|
49
|
-
const
|
|
50
|
-
const
|
|
78
|
+
const environment = normalizeEnvironment(state.run.environment ?? state.run.parameters?.environment);
|
|
79
|
+
const record = { source, packageDir: state.run.packageDir, bucket: state.run.bucket, status: suiteStatus(tests), updatedAtMs: args.clock.now(), ...(environment ? { environment } : {}), ...(state.run.parameters ? { parameters: state.run.parameters } : {}), tests };
|
|
80
|
+
const summary = { id: liveLayerId(source), producer: source, recordedAtMs: record.updatedAtMs, sha: origin.sha, branch: origin.branch, runUrl: null, ...(environment ? { environment } : {}), parameters: state.run.parameters ?? {}, specs: [], parts: [sourcePart(origin.sha ?? '', record)], source: record, recording: null };
|
|
51
81
|
const evidence = layerEvidence(summary, args.backend);
|
|
52
82
|
const layer = { ...evidence.layer, live: true, recording: { type: 'live', source }, execution: `live:${state.run.startedAtMs}:${tests.length}` };
|
|
53
83
|
state.evidence = { ...evidence, layer };
|
|
@@ -98,7 +128,9 @@ export function createLiveEvidence(args) {
|
|
|
98
128
|
/* keep what was read while it ran */
|
|
99
129
|
}
|
|
100
130
|
}
|
|
101
|
-
const
|
|
131
|
+
const declared = normalizeEnvironment(state.run.environment ?? state.run.parameters?.environment) ?? 'unspecified';
|
|
132
|
+
// A collected layer of another client is a different result, so this suite stays up.
|
|
133
|
+
const archived = collected.some(layer => layer.producer === source && !layer.live && layer.recordedAtMs >= state.run.startedAtMs && environmentOf(layer) === declared);
|
|
102
134
|
if (archived || now - state.goneAt > LINGER_MS)
|
|
103
135
|
tracked.delete(source);
|
|
104
136
|
}
|
|
@@ -30,6 +30,8 @@ export type LayerHeader = {
|
|
|
30
30
|
readonly recordedAtMs: number;
|
|
31
31
|
readonly sha: string | null;
|
|
32
32
|
readonly branch: string | null;
|
|
33
|
+
/** The client this layer checked. Null when the layer did not say. */
|
|
34
|
+
readonly environment: string | null;
|
|
33
35
|
readonly runUrl: string | null;
|
|
34
36
|
readonly parameters: LayerParameters;
|
|
35
37
|
readonly freshness?: 'unknown' | 'modified';
|
|
@@ -42,15 +42,26 @@ export type EvidenceView = {
|
|
|
42
42
|
readonly total: SpecSummary;
|
|
43
43
|
};
|
|
44
44
|
export declare function evidenceView(combined: CombinedEvidence): EvidenceView;
|
|
45
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Sources left out of the combined report. An empty list means every layer
|
|
47
|
+
* on that axis contributes, including layers that arrive later.
|
|
48
|
+
*/
|
|
46
49
|
export type SourceSelection = {
|
|
47
50
|
readonly backends: readonly string[];
|
|
48
51
|
readonly branches: readonly string[];
|
|
52
|
+
readonly versions: readonly string[];
|
|
53
|
+
readonly environments: readonly string[];
|
|
49
54
|
readonly layers: readonly string[];
|
|
50
55
|
};
|
|
51
56
|
export declare const ALL_SOURCES: SourceSelection;
|
|
57
|
+
/** A saved selection from before versions and environments were axes still means "exclude nothing new". */
|
|
58
|
+
export declare function sourceSelection(excluded: Partial<SourceSelection> | null | undefined): SourceSelection;
|
|
52
59
|
export declare function branchOf(layer: LayerHeader): string;
|
|
53
|
-
|
|
60
|
+
/** The commit a layer checked, or `unknown` when the layer did not record one. */
|
|
61
|
+
export declare function versionOf(layer: Pick<LayerHeader, 'sha'>): string;
|
|
62
|
+
/** The client a layer checked, or `unspecified` when it did not say. */
|
|
63
|
+
export declare function environmentOf(layer: Pick<LayerHeader, 'environment'>): string;
|
|
64
|
+
export declare function contributes(layer: LayerHeader, excluded: Partial<SourceSelection>): boolean;
|
|
54
65
|
export type SourceFacet = {
|
|
55
66
|
readonly id: string;
|
|
56
67
|
readonly layers: number;
|
|
@@ -59,6 +70,8 @@ export type SourceFacet = {
|
|
|
59
70
|
export declare function sourceFacets(layers: readonly LayerHeader[]): {
|
|
60
71
|
backends: SourceFacet[];
|
|
61
72
|
branches: SourceFacet[];
|
|
73
|
+
versions: SourceFacet[];
|
|
74
|
+
environments: SourceFacet[];
|
|
62
75
|
};
|
|
63
76
|
export type HistoryEntry = {
|
|
64
77
|
readonly layer: LayerHeader;
|
|
@@ -79,12 +79,35 @@ export function evidenceView(combined) {
|
|
|
79
79
|
}
|
|
80
80
|
return { specs, unassigned: checkSlots(combined.unassigned), total: addSummaries([...specs.values()].map(entry => entry.summary)) };
|
|
81
81
|
}
|
|
82
|
-
export const ALL_SOURCES = { backends: [], branches: [], layers: [] };
|
|
82
|
+
export const ALL_SOURCES = { backends: [], branches: [], versions: [], environments: [], layers: [] };
|
|
83
|
+
/** A saved selection from before versions and environments were axes still means "exclude nothing new". */
|
|
84
|
+
export function sourceSelection(excluded) {
|
|
85
|
+
return {
|
|
86
|
+
backends: excluded?.backends ?? [],
|
|
87
|
+
branches: excluded?.branches ?? [],
|
|
88
|
+
versions: excluded?.versions ?? [],
|
|
89
|
+
environments: excluded?.environments ?? [],
|
|
90
|
+
layers: excluded?.layers ?? [],
|
|
91
|
+
};
|
|
92
|
+
}
|
|
83
93
|
export function branchOf(layer) {
|
|
84
94
|
return layer.branch ?? 'Unknown branch';
|
|
85
95
|
}
|
|
96
|
+
/** The commit a layer checked, or `unknown` when the layer did not record one. */
|
|
97
|
+
export function versionOf(layer) {
|
|
98
|
+
return layer.sha ?? 'unknown';
|
|
99
|
+
}
|
|
100
|
+
/** The client a layer checked, or `unspecified` when it did not say. */
|
|
101
|
+
export function environmentOf(layer) {
|
|
102
|
+
return layer.environment || 'unspecified';
|
|
103
|
+
}
|
|
86
104
|
export function contributes(layer, excluded) {
|
|
87
|
-
|
|
105
|
+
const selection = sourceSelection(excluded);
|
|
106
|
+
return !selection.backends.includes(layer.backend)
|
|
107
|
+
&& !selection.branches.includes(branchOf(layer))
|
|
108
|
+
&& !selection.versions.includes(versionOf(layer))
|
|
109
|
+
&& !selection.environments.includes(environmentOf(layer))
|
|
110
|
+
&& !selection.layers.includes(layer.id);
|
|
88
111
|
}
|
|
89
112
|
function facets(layers, id) {
|
|
90
113
|
const found = new Map();
|
|
@@ -97,7 +120,12 @@ function facets(layers, id) {
|
|
|
97
120
|
return [...found].map(([key, facet]) => ({ id: key, ...facet })).sort((a, b) => b.latestAtMs - a.latestAtMs || (a.id < b.id ? -1 : 1));
|
|
98
121
|
}
|
|
99
122
|
export function sourceFacets(layers) {
|
|
100
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
backends: facets(layers, layer => layer.backend),
|
|
125
|
+
branches: facets(layers, branchOf),
|
|
126
|
+
versions: facets(layers, versionOf),
|
|
127
|
+
environments: facets(layers, environmentOf),
|
|
128
|
+
};
|
|
101
129
|
}
|
|
102
130
|
/** Every layer's own record of one check, newest first. A superseded result stays readable here. */
|
|
103
131
|
export function checkHistory(layers, path, id) {
|
|
@@ -2,6 +2,7 @@ import { eachLimited } from './completion-order.js';
|
|
|
2
2
|
import { recordingPaths } from './evidence-types.js';
|
|
3
3
|
import { distinctHeaders } from './layer-merge.js';
|
|
4
4
|
import { writeUtf8 } from './object-io.js';
|
|
5
|
+
import { capturedFiles } from './frames.js';
|
|
5
6
|
export function evidenceFile(path) {
|
|
6
7
|
const url = new URL(path || '/', 'http://atlas');
|
|
7
8
|
if (url.pathname === '/')
|
|
@@ -76,19 +77,10 @@ export async function exportEvidence(args) {
|
|
|
76
77
|
const raw = await copy(paths.report);
|
|
77
78
|
if (!raw)
|
|
78
79
|
continue;
|
|
79
|
-
await eachLimited(
|
|
80
|
+
await eachLimited(capturedFiles(JSON.parse(raw)), 4, file => copy(paths.file(file)).then(() => { }));
|
|
80
81
|
}
|
|
81
82
|
});
|
|
82
83
|
await writeUtf8(args.target, evidenceFile(path), JSON.stringify(exported), 'application/json');
|
|
83
84
|
}
|
|
84
85
|
return { revisions: revisions.size, layers, missing };
|
|
85
86
|
}
|
|
86
|
-
function frameFiles(steps, files = new Set()) {
|
|
87
|
-
for (const step of steps) {
|
|
88
|
-
for (const frame of step.frames)
|
|
89
|
-
for (const blob of frame.snapshots)
|
|
90
|
-
files.add(blob.file);
|
|
91
|
-
frameFiles(step.steps, files);
|
|
92
|
-
}
|
|
93
|
-
return files;
|
|
94
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { HtmlReportTest } from './recording-bundle.js';
|
|
2
|
-
import type { ReportFrame, ReportSnapshotBlob, ReportStep } from './report-types.js';
|
|
2
|
+
import type { ReportFrame, ReportSnapshotBlob, ReportStep, RunReport } from './report-types.js';
|
|
3
3
|
import type { Route } from './hash.js';
|
|
4
4
|
export type FlatFrame = {
|
|
5
5
|
step: ReportStep;
|
|
@@ -28,6 +28,8 @@ export declare function testFrames(test: HtmlReportTest): FlatFrame[];
|
|
|
28
28
|
/** Return UI snapshots in one stable actor order, including empty slots. */
|
|
29
29
|
export declare function orderedUiBlobs(frame: FlatFrame | null, test: HtmlReportTest): OrderedUiBlob[];
|
|
30
30
|
export declare function selectedFrame(frames: readonly FlatFrame[], route: Route): FlatFrame | null;
|
|
31
|
+
/** Every file a report names: step snapshots, and files attached to the test as a whole. */
|
|
32
|
+
export declare function capturedFiles(report: Pick<RunReport, 'steps' | 'artifacts'>): string[];
|
|
31
33
|
export declare function isUi(b: ReportSnapshotBlob): boolean;
|
|
32
34
|
export declare function uiBlobs(frame: FlatFrame | null): ReportSnapshotBlob[];
|
|
33
35
|
export declare function stageBlobs(frame: FlatFrame | null): ReportSnapshotBlob[];
|
|
@@ -137,6 +137,27 @@ export function selectedFrame(frames, route) {
|
|
|
137
137
|
}
|
|
138
138
|
return frames[frames.length - 1] ?? null;
|
|
139
139
|
}
|
|
140
|
+
/** Every file a report names: step snapshots, and files attached to the test as a whole. */
|
|
141
|
+
export function capturedFiles(report) {
|
|
142
|
+
const files = new Set();
|
|
143
|
+
const visit = (steps) => {
|
|
144
|
+
for (const step of steps) {
|
|
145
|
+
for (const frame of step.frames)
|
|
146
|
+
for (const blob of frame.snapshots)
|
|
147
|
+
files.add(blob.file);
|
|
148
|
+
visit(step.steps);
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
visit(report.steps);
|
|
152
|
+
for (const artifact of report.artifacts ?? []) {
|
|
153
|
+
files.add(artifact.file);
|
|
154
|
+
if (artifact.poster)
|
|
155
|
+
files.add(artifact.poster);
|
|
156
|
+
if (artifact.captions)
|
|
157
|
+
files.add(artifact.captions);
|
|
158
|
+
}
|
|
159
|
+
return [...files];
|
|
160
|
+
}
|
|
140
161
|
export function isUi(b) {
|
|
141
162
|
return b.media === 'html' || b.media === 'png';
|
|
142
163
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { LayerParameters, LayerSummary } from './layer-types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Clients a product is checked on. Any other recorded name is kept, so a new
|
|
4
|
+
* client layers beside these instead of being dropped or renamed away.
|
|
5
|
+
*/
|
|
6
|
+
export declare const REPORT_ENVIRONMENTS: readonly ["desktop-web", "mobile-web", "ios", "android"];
|
|
7
|
+
/** A stable environment id. Blank means the layer did not say, so older layers still compose. */
|
|
8
|
+
export declare function normalizeEnvironment(value: string | null | undefined): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Environment is its own axis. A layer may name it on the field or as a
|
|
11
|
+
* parameter; the parameter is then not also a free choice.
|
|
12
|
+
*/
|
|
13
|
+
export declare function layerPlace(summary: Pick<LayerSummary, 'environment' | 'parameters'>): {
|
|
14
|
+
environment: string | null;
|
|
15
|
+
parameters: LayerParameters;
|
|
16
|
+
};
|
|
17
|
+
/** How an environment id reads in the report. */
|
|
18
|
+
export declare function environmentLabel(id: string | null | undefined): string;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clients a product is checked on. Any other recorded name is kept, so a new
|
|
3
|
+
* client layers beside these instead of being dropped or renamed away.
|
|
4
|
+
*/
|
|
5
|
+
export const REPORT_ENVIRONMENTS = ['desktop-web', 'mobile-web', 'ios', 'android'];
|
|
6
|
+
const ALIASES = {
|
|
7
|
+
desktop: 'desktop-web',
|
|
8
|
+
'desktop-web': 'desktop-web',
|
|
9
|
+
'desktop-browser': 'desktop-web',
|
|
10
|
+
'web-desktop': 'desktop-web',
|
|
11
|
+
'mobile-web': 'mobile-web',
|
|
12
|
+
'mobile-browser': 'mobile-web',
|
|
13
|
+
'web-mobile': 'mobile-web',
|
|
14
|
+
ios: 'ios',
|
|
15
|
+
iphone: 'ios',
|
|
16
|
+
ipad: 'ios',
|
|
17
|
+
android: 'android',
|
|
18
|
+
};
|
|
19
|
+
/** A stable environment id. Blank means the layer did not say, so older layers still compose. */
|
|
20
|
+
export function normalizeEnvironment(value) {
|
|
21
|
+
if (value == null)
|
|
22
|
+
return null;
|
|
23
|
+
const slug = value.trim().toLowerCase().replace(/[\s_]+/g, '-').replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
|
24
|
+
if (!slug)
|
|
25
|
+
return null;
|
|
26
|
+
return ALIASES[slug] ?? slug;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Environment is its own axis. A layer may name it on the field or as a
|
|
30
|
+
* parameter; the parameter is then not also a free choice.
|
|
31
|
+
*/
|
|
32
|
+
export function layerPlace(summary) {
|
|
33
|
+
const { environment: named, ...rest } = summary.parameters;
|
|
34
|
+
return { environment: normalizeEnvironment(summary.environment ?? named), parameters: rest };
|
|
35
|
+
}
|
|
36
|
+
const LABELS = {
|
|
37
|
+
'desktop-web': 'Desktop web',
|
|
38
|
+
'mobile-web': 'Mobile web',
|
|
39
|
+
ios: 'iOS',
|
|
40
|
+
android: 'Android',
|
|
41
|
+
};
|
|
42
|
+
/** How an environment id reads in the report. */
|
|
43
|
+
export function environmentLabel(id) {
|
|
44
|
+
if (!id)
|
|
45
|
+
return 'Unspecified';
|
|
46
|
+
const known = LABELS[id];
|
|
47
|
+
if (known)
|
|
48
|
+
return known;
|
|
49
|
+
const [first, ...rest] = id.split('-');
|
|
50
|
+
return [first.charAt(0).toUpperCase() + first.slice(1), ...rest].join(' ');
|
|
51
|
+
}
|
|
@@ -31,7 +31,7 @@ export async function catalogLayers(store, entry) {
|
|
|
31
31
|
const stored = await readStoredSource(store, sha, sourceId);
|
|
32
32
|
if (!stored)
|
|
33
33
|
return;
|
|
34
|
-
layers.push({ id: `source-${sha}-${sourceId}`, producer: sourceId, recordedAtMs: Math.max(commit?.startedAtMs ?? 0, stored.record.updatedAtMs), sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null, parameters: {}, specs, parts: sourceParts(sha, stored.record), source: stored.record, recording: { type: 'commit', sha, source: sourceId } });
|
|
34
|
+
layers.push({ ...(stored.record.environment ? { environment: stored.record.environment } : {}), id: `source-${sha}-${sourceId}`, producer: sourceId, recordedAtMs: Math.max(commit?.startedAtMs ?? 0, stored.record.updatedAtMs), sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null, parameters: stored.record.parameters ?? {}, specs, parts: sourceParts(sha, stored.record), source: stored.record, recording: { type: 'commit', sha, source: sourceId } });
|
|
35
35
|
});
|
|
36
36
|
const signature = (part) => JSON.stringify([part.id, part.kind, part.checks]);
|
|
37
37
|
const sourceEvidence = new Set(layers.filter(layer => layer.source).flatMap(layer => layer.parts.map(signature)));
|
|
@@ -29,7 +29,7 @@ export type CombinedEvidence = {
|
|
|
29
29
|
}[];
|
|
30
30
|
readonly unassigned: EvidenceResult[];
|
|
31
31
|
};
|
|
32
|
-
/** One linear pass.
|
|
32
|
+
/** One linear pass. The newest completed result wins only its own check, environment and parameter slot. */
|
|
33
33
|
export declare function combineEvidence(outline: ReportOutline, layers: readonly LayerEvidence[]): CombinedEvidence;
|
|
34
34
|
export declare function slotKey(path: string, id: string): string;
|
|
35
35
|
/** Suppress the mutable address of an execution already archived as an immutable layer. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { layerPlace } from './layer-axes.js';
|
|
1
2
|
/** Advisory identity, never a cryptographic integrity check. */
|
|
2
3
|
export function textFingerprint(text) {
|
|
3
4
|
let a = 2166136261, b = 5381;
|
|
@@ -30,6 +31,7 @@ export function outcomeCounts(rows) {
|
|
|
30
31
|
}
|
|
31
32
|
/** A layer as the explorer reads it: its origin, and each check as one row. */
|
|
32
33
|
export function layerEvidence(summary, backend) {
|
|
34
|
+
const place = layerPlace(summary);
|
|
33
35
|
const source = summary.source;
|
|
34
36
|
const tests = new Map(source?.tests.map(test => [`${source.source}/${test.testId}`, test]) ?? []);
|
|
35
37
|
const rows = summary.parts.flatMap(part => part.checks.map(check => {
|
|
@@ -58,8 +60,9 @@ export function layerEvidence(summary, backend) {
|
|
|
58
60
|
recordedAtMs: summary.recordedAtMs,
|
|
59
61
|
sha: summary.sha,
|
|
60
62
|
branch: summary.branch,
|
|
63
|
+
environment: place.environment,
|
|
61
64
|
runUrl: summary.runUrl,
|
|
62
|
-
parameters:
|
|
65
|
+
parameters: place.parameters,
|
|
63
66
|
recording: recording === null ? null : recording.type === 'layer' ? { type: 'layer', layer: summary.id } : recording,
|
|
64
67
|
live: false,
|
|
65
68
|
counts: outcomeCounts(rows),
|
|
@@ -106,7 +109,7 @@ function replaces(next, prior) {
|
|
|
106
109
|
return RANK[next.row.status] > RANK[prior.row.status];
|
|
107
110
|
return next.layer.id > prior.layer.id;
|
|
108
111
|
}
|
|
109
|
-
/** One linear pass.
|
|
112
|
+
/** One linear pass. The newest completed result wins only its own check, environment and parameter slot. */
|
|
110
113
|
export function combineEvidence(outline, layers) {
|
|
111
114
|
const specs = new Map(outline.specs.map(spec => [specPath(spec.path), { spec, results: [] }]));
|
|
112
115
|
const changed = new Set(outline.changedFiles.map(specPath));
|
|
@@ -115,9 +118,11 @@ export function combineEvidence(outline, layers) {
|
|
|
115
118
|
const scope = layer.packageDir;
|
|
116
119
|
const related = outline.changedFiles.some(file => scope ? file.startsWith(`${scope}/`) : !specPath(file).startsWith('specs/'));
|
|
117
120
|
for (const row of rows) {
|
|
118
|
-
|
|
121
|
+
// Environment and the layer's other choices split the slot. A harness
|
|
122
|
+
// seed does not: a focused rerun replaces the check it repeated.
|
|
123
|
+
const parameters = layer.parameters;
|
|
119
124
|
const path = row.specPath ?? '';
|
|
120
|
-
const key = JSON.stringify([layer.producer, row.kind, path, row.id, parameterKey(parameters)]);
|
|
125
|
+
const key = JSON.stringify([layer.producer, row.kind, path, row.id, layer.environment ?? '', parameterKey(parameters)]);
|
|
121
126
|
const result = { key, slot: slotKey(path, row.id), layer, row, parameters, warning: warning(layer, path, specs.get(path)?.spec, outline, fingerprints[path], related || changed.has(path)) };
|
|
122
127
|
const prior = winners.get(key);
|
|
123
128
|
if (!prior || replaces(result, prior))
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeBundle } from './bundle.js';
|
|
2
|
+
import { normalizeEnvironment } from './layer-axes.js';
|
|
2
3
|
import { readCommit, readFrameCss } from './read.js';
|
|
3
4
|
import { readInventory } from './spec-inventory.js';
|
|
4
5
|
import { sourceParts } from './source-part.js';
|
|
@@ -25,8 +26,10 @@ export async function archiveSourceLayer(store, args, source, tests, bundle) {
|
|
|
25
26
|
const index = await writeBundle(store, entries(), 'layers/blobs/');
|
|
26
27
|
for (const [key, value] of Object.entries(bundle ?? {}))
|
|
27
28
|
index[layerPath(key)] = value;
|
|
29
|
+
const environment = normalizeEnvironment(args.environment ?? args.parameters?.environment);
|
|
28
30
|
return publishLayerBundle(store, {
|
|
29
31
|
...(args.freshness ? { freshness: args.freshness } : {}), producer: source.source, recordedAtMs, sha: args.sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null,
|
|
32
|
+
...(environment ? { environment } : {}),
|
|
30
33
|
parameters: args.parameters ?? {}, specs: inventory?.entries ?? [], parts: sourceParts(args.sha, source), source, recording: { type: 'layer' },
|
|
31
34
|
}, index);
|
|
32
35
|
}
|
|
@@ -9,6 +9,8 @@ export type LayerSummary = {
|
|
|
9
9
|
readonly producer: string;
|
|
10
10
|
readonly sha: string | null;
|
|
11
11
|
readonly branch: string | null;
|
|
12
|
+
/** The client this layer checked, when it said. Normalized when the layer is read. */
|
|
13
|
+
readonly environment?: string | null;
|
|
12
14
|
readonly runUrl: string | null;
|
|
13
15
|
readonly parameters: LayerParameters;
|
|
14
16
|
readonly specs: readonly SpecEntry[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import { createBundleReader } from './bundle.js';
|
|
3
3
|
import { eachLimited } from './completion-order.js';
|
|
4
|
-
import { createLiveEvidence } from './evidence-live.js';
|
|
4
|
+
import { createLiveEvidence, liveRunRecord } from './evidence-live.js';
|
|
5
5
|
import { isFramePath, isKeySegment } from './keys.js';
|
|
6
6
|
import { catalogEntries, catalogLayers } from './layer-catalog.js';
|
|
7
7
|
import { layerEvidence } from './layer-merge.js';
|
|
@@ -130,10 +130,7 @@ export function createLayersApp(deps) {
|
|
|
130
130
|
const raw = await readUtf8(local, key);
|
|
131
131
|
if (raw === null)
|
|
132
132
|
return null;
|
|
133
|
-
|
|
134
|
-
if (!run.packageDir || !run.bucket || typeof run.startedAtMs !== 'number')
|
|
135
|
-
return null;
|
|
136
|
-
return { packageDir: run.packageDir, bucket: run.bucket, startedAtMs: run.startedAtMs, ...(typeof run.plannedChecks === 'number' ? { plannedChecks: run.plannedChecks } : {}) };
|
|
133
|
+
return liveRunRecord(JSON.parse(raw));
|
|
137
134
|
}
|
|
138
135
|
catch {
|
|
139
136
|
return null;
|
|
@@ -30,6 +30,10 @@ export type SourceRecord = {
|
|
|
30
30
|
readonly bucket: string;
|
|
31
31
|
readonly status: ReportTestStatus;
|
|
32
32
|
readonly updatedAtMs: number;
|
|
33
|
+
/** The client this source checked, when the run said. */
|
|
34
|
+
readonly environment?: string;
|
|
35
|
+
/** Recorded choices other than the client, such as an example or a device size. */
|
|
36
|
+
readonly parameters?: Readonly<Record<string, string>>;
|
|
33
37
|
/** Each test's meta, test-runner outcomes already applied. */
|
|
34
38
|
readonly tests: readonly RunReportMeta[];
|
|
35
39
|
};
|
|
@@ -24,6 +24,8 @@ export type PublishSourceArgs = {
|
|
|
24
24
|
/** Execution completion time, separate from a virtual timeline clock. */
|
|
25
25
|
readonly recordedAtMs?: number;
|
|
26
26
|
readonly freshness?: 'unknown' | 'modified';
|
|
27
|
+
/** The client this run checked: desktop web, mobile web, iOS, Android, or another recorded name. */
|
|
28
|
+
readonly environment?: string;
|
|
27
29
|
readonly parameters?: LayerParameters;
|
|
28
30
|
readonly sha: string;
|
|
29
31
|
readonly packageDir: string;
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { archiveSourceLayer } from './layer-source.js';
|
|
2
|
+
import { normalizeEnvironment } from './layer-axes.js';
|
|
2
3
|
import { performancePart } from './performance-part.js';
|
|
3
4
|
import { applyJunitOutcomes, parseJunit, } from './junit.js';
|
|
4
5
|
import { testFrameKey, testPrefix, testReportKey, } from './recording-keys.js';
|
|
5
6
|
import { suiteStatus } from './status.js';
|
|
7
|
+
import { capturedFiles } from './frames.js';
|
|
6
8
|
import { readUtf8, writeUtf8 } from './object-io.js';
|
|
7
9
|
import { bundleIndexKey, writeBundle } from './bundle.js';
|
|
8
10
|
import { eachLimited } from './completion-order.js';
|
|
@@ -67,20 +69,10 @@ async function readTests(bucket, runId) {
|
|
|
67
69
|
...applyJunitOutcomes({ runId, status: 'passed', updatedAtMs: 0, tests }, parseJunit(junit)).tests,
|
|
68
70
|
];
|
|
69
71
|
}
|
|
70
|
-
function frameFiles(steps, out) {
|
|
71
|
-
for (const step of steps) {
|
|
72
|
-
for (const frame of step.frames) {
|
|
73
|
-
for (const blob of frame.snapshots)
|
|
74
|
-
out.add(blob.file);
|
|
75
|
-
}
|
|
76
|
-
frameFiles(step.steps, out);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
72
|
async function publishSnapshots(store, args, tests, contentPrefix) {
|
|
80
73
|
const runId = args.runId ?? SPEC_RUN_ID;
|
|
81
74
|
await eachLimited(tests, 8, async ({ report }) => {
|
|
82
|
-
const files = new Set();
|
|
83
|
-
frameFiles(report.steps, files);
|
|
75
|
+
const files = new Set(capturedFiles(report));
|
|
84
76
|
await publishSnapshotBundle({
|
|
85
77
|
local: args.local,
|
|
86
78
|
remote: store,
|
|
@@ -122,12 +114,15 @@ export async function publishSource(store, args) {
|
|
|
122
114
|
await writeUtf8(store, `${contentPrefix}${report.meta.testId}/report.json`, JSON.stringify(report), JSON_TYPE);
|
|
123
115
|
});
|
|
124
116
|
}
|
|
117
|
+
const environment = normalizeEnvironment(args.environment ?? args.parameters?.environment);
|
|
125
118
|
const record = {
|
|
126
119
|
source,
|
|
127
120
|
packageDir: args.packageDir,
|
|
128
121
|
bucket: args.bucket,
|
|
129
122
|
status,
|
|
130
123
|
updatedAtMs: args.recordedAtMs ?? metas.reduce((latest, meta) => Math.max(latest, meta.endedAtMs), 0),
|
|
124
|
+
...(environment ? { environment } : {}),
|
|
125
|
+
...(args.parameters && Object.keys(args.parameters).length > 0 ? { parameters: args.parameters } : {}),
|
|
131
126
|
tests: metas,
|
|
132
127
|
...(await sourceEvidence(args.local, args.sha, source, tests)),
|
|
133
128
|
};
|
|
@@ -28,6 +28,8 @@ export function mediaExtension(media) {
|
|
|
28
28
|
return 'txt';
|
|
29
29
|
case 'bytes':
|
|
30
30
|
return 'bin';
|
|
31
|
+
case 'video':
|
|
32
|
+
return 'mp4';
|
|
31
33
|
default:
|
|
32
34
|
return 'bin';
|
|
33
35
|
}
|
|
@@ -44,6 +46,8 @@ export function contentTypeForMedia(media) {
|
|
|
44
46
|
return 'text/plain; charset=utf-8';
|
|
45
47
|
case 'bytes':
|
|
46
48
|
return 'application/octet-stream';
|
|
49
|
+
case 'video':
|
|
50
|
+
return 'video/mp4';
|
|
47
51
|
default:
|
|
48
52
|
return 'application/octet-stream';
|
|
49
53
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { normalizeEnvironment } from './layer-axes.js';
|
|
1
2
|
import { publishLayer } from './layer-store.js';
|
|
2
3
|
import { readCommit } from './read.js';
|
|
3
4
|
import { readInventory } from './spec-inventory.js';
|
|
@@ -68,8 +69,10 @@ export async function publishPart(store, part) {
|
|
|
68
69
|
await writeUtf8(store, `${partsPrefix(part.sha)}${part.id}/${part.recordedAtMs}-${hash}.json`, json, 'application/json');
|
|
69
70
|
await recordExplorerVersion(store, part.sha);
|
|
70
71
|
const commit = await readCommit(store, part.sha);
|
|
72
|
+
const environment = normalizeEnvironment(part.parameters?.environment);
|
|
71
73
|
await publishLayer(store, {
|
|
72
74
|
...(part.freshness ? { freshness: part.freshness } : {}), producer: part.id, recordedAtMs: part.recordedAtMs, sha: part.sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null,
|
|
75
|
+
...(environment ? { environment } : {}),
|
|
73
76
|
parameters: part.parameters ?? {}, specs: (await readInventory(store, part.sha))?.entries ?? [], parts: [part], source: null, recording: null,
|
|
74
77
|
}, (async function* () { })());
|
|
75
78
|
}
|