@crvouga/atlas 0.4.0 → 0.6.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 +191 -611
- package/dist/bun/src/driver.d.ts +41 -7
- package/dist/bun/src/driver.js +47 -6
- package/dist/bun/src/index.d.ts +8 -0
- package/dist/bun/src/index.js +8 -0
- package/dist/core/src/cli.d.ts +14 -1
- package/dist/core/src/cli.js +35 -1
- package/dist/core/src/config.d.ts +77 -4
- package/dist/core/src/config.js +110 -17
- package/dist/core/src/events/cloudevents.d.ts +54 -9
- package/dist/core/src/events/cloudevents.js +60 -9
- package/dist/core/src/graph.d.ts +21 -0
- package/dist/core/src/graph.js +26 -0
- package/dist/core/src/index.d.ts +11 -0
- package/dist/core/src/index.js +11 -0
- package/dist/core/src/lint.d.ts +30 -0
- package/dist/core/src/lint.js +13 -0
- package/dist/core/src/plan.d.ts +39 -1
- package/dist/core/src/plan.js +28 -1
- package/dist/core/src/privacy.d.ts +29 -2
- package/dist/core/src/privacy.js +19 -2
- package/dist/core/src/report/formats.d.ts +34 -4
- package/dist/core/src/report/formats.js +24 -4
- package/dist/core/src/report/markdown.d.ts +15 -0
- package/dist/core/src/report/markdown.js +8 -0
- package/dist/core/src/run/clients.d.ts +44 -5
- package/dist/core/src/run/clients.js +46 -2
- package/dist/core/src/run/execute.d.ts +75 -16
- package/dist/core/src/run/execute.js +71 -18
- package/dist/core/src/run/frames.d.ts +13 -4
- package/dist/core/src/run/frames.js +19 -4
- package/dist/core/src/run/http.d.ts +34 -1
- package/dist/core/src/run/http.js +14 -1
- package/dist/core/src/run/types.d.ts +76 -3
- package/dist/core/src/run/types.js +19 -3
- package/dist/core/src/signals.d.ts +49 -5
- package/dist/core/src/signals.js +22 -5
- package/dist/core/src/spec/compose.d.ts +23 -1
- package/dist/core/src/spec/compose.js +10 -1
- package/dist/core/src/spec/index.d.ts +9 -1
- package/dist/core/src/spec/index.js +9 -1
- package/dist/core/src/spec/load.d.ts +22 -1
- package/dist/core/src/spec/load.js +31 -1
- package/dist/core/src/spec/parse.d.ts +16 -2
- package/dist/core/src/spec/parse.js +25 -2
- package/dist/core/src/spec/scxml.d.ts +25 -4
- package/dist/core/src/spec/scxml.js +36 -4
- package/dist/core/src/spec/types.d.ts +52 -1
- package/dist/core/src/spec/types.js +24 -0
- package/dist/detox/src/index.d.ts +54 -11
- package/dist/detox/src/index.js +45 -7
- package/dist/playwright/src/driver.d.ts +39 -3
- package/dist/playwright/src/driver.js +27 -3
- package/dist/playwright/src/index.d.ts +10 -0
- package/dist/playwright/src/index.js +10 -0
- package/dist/playwright/src/media.d.ts +50 -4
- package/dist/playwright/src/media.js +55 -4
- package/dist/playwright/src/signals.d.ts +44 -5
- package/dist/playwright/src/signals.js +29 -5
- package/dist/reports/src/attachment-files.d.ts +44 -0
- package/dist/reports/src/attachment-files.js +121 -0
- package/dist/reports/src/attachment-response.d.ts +31 -0
- package/dist/reports/src/attachment-response.js +55 -0
- package/dist/reports/src/aws-credentials.d.ts +48 -0
- package/dist/reports/src/aws-credentials.js +221 -0
- package/dist/reports/src/browser-navigation.d.ts +14 -0
- package/dist/reports/src/browser-navigation.js +7 -0
- package/dist/reports/src/bundle.d.ts +34 -2
- package/dist/reports/src/bundle.js +38 -2
- package/dist/reports/src/catalog.d.ts +34 -3
- package/dist/reports/src/catalog.js +17 -3
- package/dist/reports/src/cli-stores.d.ts +36 -0
- package/dist/reports/src/cli-stores.js +73 -0
- package/dist/reports/src/clock.d.ts +7 -2
- package/dist/reports/src/clock.js +5 -2
- package/dist/reports/src/completion-order.d.ts +18 -5
- package/dist/reports/src/completion-order.js +18 -5
- package/dist/reports/src/compose.d.ts +80 -0
- package/dist/reports/src/compose.js +117 -0
- package/dist/reports/src/copy-store.d.ts +29 -0
- package/dist/reports/src/copy-store.js +52 -0
- package/dist/reports/src/directory-outline.d.ts +38 -0
- package/dist/reports/src/directory-outline.js +103 -0
- package/dist/reports/src/directory-store.d.ts +20 -0
- package/dist/reports/src/directory-store.js +166 -0
- package/dist/reports/src/doctor.d.ts +29 -0
- package/dist/reports/src/doctor.js +85 -0
- package/dist/reports/src/effective-delta.d.ts +34 -0
- package/dist/reports/src/effective-delta.js +25 -0
- package/dist/reports/src/evidence-detail.d.ts +64 -0
- package/dist/reports/src/evidence-detail.js +92 -0
- package/dist/reports/src/evidence-live.d.ts +35 -5
- package/dist/reports/src/evidence-live.js +18 -2
- package/dist/reports/src/evidence-types.d.ts +68 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +43 -4
- package/dist/reports/src/evidence-view.js +13 -2
- package/dist/reports/src/explorer-types.d.ts +40 -3
- package/dist/reports/src/explorer-types.js +8 -0
- package/dist/reports/src/explorer-versions.d.ts +9 -1
- package/dist/reports/src/explorer-versions.js +18 -1
- package/dist/reports/src/export-evidence.d.ts +16 -1
- package/dist/reports/src/export-evidence.js +52 -1
- package/dist/reports/src/fetcher.d.ts +27 -3
- package/dist/reports/src/fetcher.js +11 -3
- package/dist/reports/src/format-date.d.ts +6 -0
- package/dist/reports/src/format-date.js +6 -0
- package/dist/reports/src/format.d.ts +16 -0
- package/dist/reports/src/format.js +10 -0
- package/dist/reports/src/frames.d.ts +36 -3
- package/dist/reports/src/frames.js +22 -3
- package/dist/reports/src/hash.d.ts +18 -0
- package/dist/reports/src/hash.js +13 -0
- package/dist/reports/src/host.d.ts +29 -0
- package/dist/reports/src/host.js +64 -0
- package/dist/reports/src/html-frame.d.ts +5 -6
- package/dist/reports/src/html-frame.js +12 -1
- package/dist/reports/src/http-store.d.ts +56 -0
- package/dist/reports/src/http-store.js +148 -0
- package/dist/reports/src/http.d.ts +7 -0
- package/dist/reports/src/http.js +6 -0
- package/dist/reports/src/ingest.d.ts +54 -0
- package/dist/reports/src/ingest.js +210 -0
- package/dist/reports/src/init.d.ts +40 -0
- package/dist/reports/src/init.js +369 -0
- package/dist/reports/src/junit.d.ts +33 -6
- package/dist/reports/src/junit.js +87 -21
- package/dist/reports/src/keyboard.d.ts +20 -0
- package/dist/reports/src/keyboard.js +11 -0
- package/dist/reports/src/keys.d.ts +21 -3
- package/dist/reports/src/keys.js +21 -3
- package/dist/reports/src/layer-axes.d.ts +35 -5
- package/dist/reports/src/layer-axes.js +25 -5
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +18 -1
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +147 -31
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +11 -0
- package/dist/reports/src/layer-store.d.ts +42 -0
- package/dist/reports/src/layer-store.js +63 -5
- package/dist/reports/src/layer-types.d.ts +32 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +86 -7
- package/dist/reports/src/live-reporter.d.ts +34 -0
- package/dist/reports/src/live-reporter.js +122 -0
- package/dist/reports/src/media-types.d.ts +11 -0
- package/dist/reports/src/media-types.js +22 -0
- package/dist/reports/src/namespace-guard.d.ts +10 -1
- package/dist/reports/src/namespace-guard.js +10 -1
- package/dist/reports/src/navigation.d.ts +12 -0
- package/dist/reports/src/navigation.js +7 -0
- package/dist/reports/src/object-io.d.ts +11 -4
- package/dist/reports/src/object-io.js +4 -4
- package/dist/reports/src/object-store-conformance.d.ts +27 -0
- package/dist/reports/src/object-store-conformance.js +87 -0
- package/dist/reports/src/object-store.d.ts +33 -24
- package/dist/reports/src/object-store.js +17 -24
- package/dist/reports/src/parse-with.d.ts +16 -0
- package/dist/reports/src/parse-with.js +14 -0
- package/dist/reports/src/performance-part.d.ts +25 -1
- package/dist/reports/src/performance-part.js +15 -1
- package/dist/reports/src/performance.d.ts +22 -2
- package/dist/reports/src/performance.js +12 -2
- package/dist/reports/src/persist.d.ts +12 -3
- package/dist/reports/src/persist.js +12 -3
- package/dist/reports/src/phone.d.ts +18 -1
- package/dist/reports/src/phone.js +18 -1
- package/dist/reports/src/protocol.d.ts +26 -4
- package/dist/reports/src/protocol.js +15 -4
- package/dist/reports/src/publication.d.ts +21 -1
- package/dist/reports/src/publication.js +21 -1
- package/dist/reports/src/publish-checks.d.ts +35 -0
- package/dist/reports/src/publish-checks.js +122 -0
- package/dist/reports/src/publish.d.ts +42 -9
- package/dist/reports/src/publish.js +36 -7
- package/dist/reports/src/read.d.ts +46 -16
- package/dist/reports/src/read.js +38 -14
- package/dist/reports/src/recording-bundle.d.ts +13 -2
- package/dist/reports/src/recording-bundle.js +4 -2
- package/dist/reports/src/recording-keys.d.ts +15 -1
- package/dist/reports/src/recording-keys.js +15 -1
- package/dist/reports/src/remote-report.d.ts +28 -7
- package/dist/reports/src/remote-report.js +15 -7
- package/dist/reports/src/report-cli.d.ts +51 -0
- package/dist/reports/src/report-cli.js +486 -0
- package/dist/reports/src/report-index.d.ts +99 -0
- package/dist/reports/src/report-index.js +337 -0
- package/dist/reports/src/report-loader.d.ts +10 -0
- package/dist/reports/src/report-loader.js +6 -0
- package/dist/reports/src/report-parts.d.ts +29 -3
- package/dist/reports/src/report-parts.js +37 -3
- package/dist/reports/src/report-status.d.ts +48 -0
- package/dist/reports/src/report-status.js +156 -0
- package/dist/reports/src/report-types.d.ts +73 -0
- package/dist/reports/src/report-types.js +3 -0
- package/dist/reports/src/reporter.d.ts +67 -0
- package/dist/reports/src/reporter.js +128 -0
- package/dist/reports/src/reports-explorer-route.d.ts +541 -0
- package/dist/reports/src/reports-explorer-route.js +11 -0
- package/dist/reports/src/reports-list-route.d.ts +9 -0
- package/dist/reports/src/reports-list-route.js +9 -0
- package/dist/reports/src/reports-route.d.ts +561 -22
- package/dist/reports/src/reports-route.js +11 -3
- package/dist/reports/src/reports-store.d.ts +19 -2
- package/dist/reports/src/reports-store.js +10 -2
- package/dist/reports/src/resilient-cache.d.ts +14 -1
- package/dist/reports/src/resilient-cache.js +8 -1
- package/dist/reports/src/retention.d.ts +17 -1
- package/dist/reports/src/retention.js +5 -1
- package/dist/reports/src/run-bundle.d.ts +63 -0
- package/dist/reports/src/run-bundle.js +159 -0
- package/dist/reports/src/s3-store.d.ts +79 -0
- package/dist/reports/src/s3-store.js +295 -0
- package/dist/reports/src/scenario.d.ts +14 -4
- package/dist/reports/src/scenario.js +5 -4
- package/dist/reports/src/sigv4.d.ts +72 -0
- package/dist/reports/src/sigv4.js +96 -0
- package/dist/reports/src/snapshot-bundle.d.ts +24 -1
- package/dist/reports/src/snapshot-bundle.js +27 -2
- package/dist/reports/src/snapshot-surface.d.ts +20 -9
- package/dist/reports/src/snapshot-surface.js +32 -19
- package/dist/reports/src/source-content.d.ts +23 -0
- package/dist/reports/src/source-content.js +30 -1
- package/dist/reports/src/source-entries.d.ts +13 -0
- package/dist/reports/src/source-entries.js +6 -0
- package/dist/reports/src/source-part.d.ts +15 -0
- package/dist/reports/src/source-part.js +8 -0
- package/dist/reports/src/source-reader.d.ts +11 -0
- package/dist/reports/src/source-reader.js +5 -0
- package/dist/reports/src/spec-evidence.d.ts +12 -2
- package/dist/reports/src/spec-evidence.js +6 -2
- package/dist/reports/src/spec-inventory.d.ts +15 -1
- package/dist/reports/src/spec-inventory.js +22 -1
- package/dist/reports/src/spec-link.d.ts +28 -0
- package/dist/reports/src/spec-link.js +70 -0
- package/dist/reports/src/spec-set.d.ts +63 -0
- package/dist/reports/src/spec-set.js +177 -0
- package/dist/reports/src/status.d.ts +10 -0
- package/dist/reports/src/status.js +5 -0
- package/dist/reports/src/store-prefix.d.ts +28 -0
- package/dist/reports/src/store-prefix.js +22 -0
- package/dist/reports/src/store-url.d.ts +89 -0
- package/dist/reports/src/store-url.js +160 -0
- package/dist/reports/src/test-catalog-entity.d.ts +11 -0
- package/dist/reports/src/test-catalog-entity.js +6 -0
- package/dist/reports/src/types.d.ts +6 -1
- package/dist/reports/src/use-report-nav.d.ts +35 -0
- package/dist/reports/src/use-report-nav.js +12 -0
- package/dist/runners/src/codecept.d.ts +14 -0
- package/dist/runners/src/codecept.js +16 -0
- package/dist/runners/src/jest.d.ts +50 -0
- package/dist/runners/src/jest.js +52 -0
- package/dist/runners/src/mocha.d.ts +40 -0
- package/dist/runners/src/mocha.js +39 -0
- package/dist/runners/src/node-test.d.ts +28 -0
- package/dist/runners/src/node-test.js +44 -0
- package/dist/runners/src/publish.d.ts +30 -0
- package/dist/runners/src/publish.js +68 -0
- package/dist/schema/src/api.d.ts +800 -0
- package/dist/schema/src/api.js +201 -0
- package/dist/schema/src/cli-results.d.ts +210 -0
- package/dist/schema/src/cli-results.js +124 -0
- package/dist/schema/src/common.d.ts +53 -0
- package/dist/schema/src/common.js +42 -0
- package/dist/schema/src/index.d.ts +16 -0
- package/dist/schema/src/index.js +16 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/live-run.d.ts +123 -0
- package/dist/schema/src/live-run.js +30 -0
- package/dist/schema/src/manifest.d.ts +97 -2
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/project.d.ts +20 -0
- package/dist/schema/src/project.js +10 -0
- package/dist/schema/src/report-sources.d.ts +29 -0
- package/dist/schema/src/report-sources.js +27 -1
- package/dist/schema/src/runs-index.d.ts +17 -1
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +177 -0
- package/dist/schema/src/server-config.js +85 -0
- package/dist/schema/src/spec.d.ts +69 -0
- package/dist/schema/src/spec.js +58 -0
- package/dist/server/src/app.d.ts +72 -0
- package/dist/server/src/app.js +557 -0
- package/dist/server/src/auth.d.ts +41 -0
- package/dist/server/src/auth.js +59 -0
- package/dist/server/src/config.d.ts +83 -0
- package/dist/server/src/config.js +204 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +89 -0
- package/dist/server/src/events.d.ts +29 -0
- package/dist/server/src/events.js +101 -0
- package/dist/server/src/host.d.ts +30 -0
- package/dist/server/src/host.js +115 -0
- package/dist/server/src/index.d.ts +18 -0
- package/dist/server/src/index.js +18 -0
- package/dist/server/src/live-runs.d.ts +52 -0
- package/dist/server/src/live-runs.js +229 -0
- package/dist/server/src/llms.d.ts +19 -0
- package/dist/server/src/llms.js +63 -0
- package/dist/server/src/main.d.ts +30 -0
- package/dist/server/src/main.js +114 -0
- package/dist/server/src/openapi.d.ts +56 -0
- package/dist/server/src/openapi.js +438 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/projects.d.ts +3 -0
- package/dist/server/src/projects.js +154 -0
- package/dist/server/src/settings-page.d.ts +9 -0
- package/dist/server/src/settings-page.js +93 -0
- package/dist/server/src/settings.d.ts +23 -0
- package/dist/server/src/settings.js +72 -0
- package/dist/server/src/site.d.ts +15 -0
- package/dist/server/src/site.js +78 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +180 -0
- package/dist/server/src/validate.d.ts +71 -0
- package/dist/server/src/validate.js +197 -0
- package/dist/vitest/src/index.d.ts +27 -5
- package/dist/vitest/src/index.js +18 -5
- package/dist/webdriver/src/driver.d.ts +63 -11
- package/dist/webdriver/src/driver.js +57 -6
- package/dist/webdriver/src/index.d.ts +8 -0
- package/dist/webdriver/src/index.js +1 -0
- package/dist/webdriver/src/protocol.d.ts +75 -6
- package/dist/webdriver/src/protocol.js +79 -6
- package/docs/agents.md +137 -0
- package/docs/deploying.md +345 -0
- package/docs/dogfooding.md +175 -0
- package/docs/extending.md +242 -0
- package/docs/integrating.md +485 -0
- package/docs/reference/bun.md +141 -0
- package/docs/reference/cli.md +148 -0
- package/docs/reference/core.md +2432 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +799 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7152 -0
- package/docs/reference/runners.md +169 -0
- package/docs/reference/schema.md +1762 -0
- package/docs/reference/server.md +825 -0
- package/docs/reference/vitest.md +45 -0
- package/docs/reference/webdriver.md +330 -0
- package/docs/report-backends.md +144 -0
- package/docs/report-contract.md +400 -0
- package/docs/reporting-issues.md +160 -0
- package/docs/runners.md +69 -0
- package/docs/server.md +321 -0
- package/docs/statecharts.md +643 -0
- package/llms-full.txt +4114 -0
- package/llms.txt +49 -0
- package/package.json +61 -11
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +65 -0
- package/packages/reports/STORAGE.md +4 -0
- package/packages/runners/README.md +6 -0
- package/packages/runners/compat/codecept.cjs +2 -0
- package/packages/runners/compat/jest.cjs +2 -0
- package/packages/runners/compat/mocha.cjs +2 -0
- package/packages/schema/README.md +14 -0
- package/packages/schema/schemas/atlas-change.schema.json +4 -0
- package/packages/schema/schemas/check-detail.schema.json +1032 -0
- package/packages/schema/schemas/checks-result.schema.json +26 -0
- package/packages/schema/schemas/command-error.schema.json +35 -0
- package/packages/schema/schemas/doctor-result.schema.json +60 -0
- package/packages/schema/schemas/effective-delta.schema.json +490 -0
- package/packages/schema/schemas/effective-evidence.schema.json +471 -0
- package/packages/schema/schemas/evidence-page.schema.json +271 -0
- package/packages/schema/schemas/hello-event.schema.json +26 -0
- package/packages/schema/schemas/init-plan.schema.json +347 -0
- package/packages/schema/schemas/layer-rows.schema.json +199 -0
- package/packages/schema/schemas/layer.schema.json +2888 -0
- package/packages/schema/schemas/live-run-start.schema.json +42 -0
- package/packages/schema/schemas/live-run-update.schema.json +93 -0
- package/packages/schema/schemas/live-run.schema.json +225 -0
- package/packages/schema/schemas/merge-result.schema.json +40 -0
- package/packages/schema/schemas/outline.schema.json +62 -0
- package/packages/schema/schemas/problem.schema.json +37 -0
- package/packages/schema/schemas/project-slug.schema.json +8 -0
- package/packages/schema/schemas/project.schema.json +22 -0
- package/packages/schema/schemas/projects.schema.json +33 -0
- package/packages/schema/schemas/publish-checks.schema.json +1413 -0
- package/packages/schema/schemas/publish-result.schema.json +168 -0
- package/packages/schema/schemas/ready.schema.json +68 -0
- package/packages/schema/schemas/report-event.schema.json +100 -0
- package/packages/schema/schemas/report-status.schema.json +370 -0
- package/packages/schema/schemas/server-config.schema.json +208 -0
- package/packages/schema/schemas/server-editable-settings.schema.json +132 -0
- package/packages/schema/schemas/server-info.schema.json +114 -0
- package/packages/schema/schemas/server-settings-snapshot.schema.json +285 -0
- package/packages/schema/schemas/spec-set.schema.json +103 -0
- package/packages/schema/schemas/store-listing.schema.json +29 -0
- package/packages/schema/schemas/store-write-result.schema.json +28 -0
- package/packages/schema/schemas/stream-result.schema.json +59 -0
- package/packages/server/README.md +43 -0
- package/packages/server/openapi.json +11021 -0
- package/packages/visualizer/README.md +32 -11
- package/packages/visualizer/bin/atlas-client.mjs +176 -0
- package/packages/visualizer/bin/atlas-visualizer.mjs +2 -175
- package/packages/visualizer/src/app/AppShell.tsx +32 -16
- package/packages/visualizer/src/app/report-sources.tsx +7 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/ActivityIndicator.module.css +4 -0
- package/packages/visualizer/src/components/ActivityIndicator.tsx +6 -0
- package/packages/visualizer/src/components/Header.module.css +101 -192
- package/packages/visualizer/src/components/Header.tsx +88 -13
- package/packages/visualizer/src/components/ProjectPicker.tsx +64 -0
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/data/adapters/report-backend.test.ts +3 -2
- package/packages/visualizer/src/data/adapters/report-backend.ts +1 -1
- package/packages/visualizer/src/data/queries.ts +14 -7
- package/packages/visualizer/src/evidence/api.ts +16 -3
- package/packages/visualizer/src/evidence/artifacts.module.css +213 -0
- package/packages/visualizer/src/evidence/artifacts.tsx +272 -0
- package/packages/visualizer/src/evidence/check-detail.tsx +21 -4
- package/packages/visualizer/src/evidence/document-page.tsx +6 -2
- package/packages/visualizer/src/evidence/evidence-context.tsx +79 -12
- package/packages/visualizer/src/evidence/evidence.module.css +11 -1
- package/packages/visualizer/src/evidence/feature-page.tsx +7 -3
- package/packages/visualizer/src/evidence/model-page.tsx +6 -2
- package/packages/visualizer/src/evidence/project-home.tsx +45 -0
- package/packages/visualizer/src/evidence/recording-page.tsx +4 -1
- package/packages/visualizer/src/evidence/spec-index.tsx +2 -1
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/evidence/test-filter.tsx +21 -0
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-query.ts +21 -0
- package/packages/visualizer/src/live/live-runs.tsx +69 -0
- package/packages/visualizer/src/live/live-stream.ts +225 -0
- package/packages/visualizer/src/live/report-progress.ts +62 -0
- package/packages/visualizer/src/live/test-progress.tsx +34 -0
- package/packages/visualizer/src/runtime.ts +35 -0
- package/packages/visualizer/src/state/ui-store.ts +3 -2
- package/packages/visualizer/src/views/ChartPage.tsx +2 -0
- package/packages/visualizer/src/views/ChartTests.tsx +43 -0
- package/packages/visualizer/src/views/SidePanel.tsx +2 -0
- package/packages/visualizer/vite/atlas-files.test.ts +4 -4
- package/packages/visualizer/vite/atlas-files.ts +1 -5
- package/packages/visualizer/vite/atlas-server-proxy.ts +37 -0
- package/packages/visualizer/vite.config.ts +11 -3
|
@@ -1,5 +1,11 @@
|
|
|
1
|
+
import { summarizeEvidence } from './evidence-detail.js';
|
|
1
2
|
import { layerPlace } from './layer-axes.js';
|
|
2
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A short, deterministic fingerprint of a spec's text, shaped `<length>:<hash>:<hash>` (the length in UTF-16 code
|
|
5
|
+
* units and two 32-bit hashes in hex). A layer records it per spec so a reader can warn when the spec changed
|
|
6
|
+
* since ("Potentially outdated: spec changed"). Advisory identity only: it is not collision resistant and never an
|
|
7
|
+
* integrity check.
|
|
8
|
+
*/
|
|
3
9
|
export function textFingerprint(text) {
|
|
4
10
|
let a = 2166136261, b = 5381;
|
|
5
11
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -8,20 +14,37 @@ export function textFingerprint(text) {
|
|
|
8
14
|
}
|
|
9
15
|
return `${text.length}:${(a >>> 0).toString(16)}:${(b >>> 0).toString(16)}`;
|
|
10
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalises a check's or spec's path to its `specs/<path inside the specs directory>` form, so the same spec
|
|
19
|
+
* recorded from different checkouts is one spec.
|
|
20
|
+
*
|
|
21
|
+
* Converts `\` to `/`, drops a leading `./`, and drops everything before the first path segment named exactly
|
|
22
|
+
* `specs`. A path with no such segment is returned unchanged (apart from the first two steps).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* specPath('packages/app/specs/join.feature'); // 'specs/join.feature'
|
|
26
|
+
* specPath('myspecs/x.feature'); // 'myspecs/x.feature'
|
|
27
|
+
*/
|
|
11
28
|
export function specPath(path) {
|
|
12
|
-
|
|
29
|
+
// Up to the first folder named exactly `specs`: `myspecs/x` is not in one.
|
|
30
|
+
return path.replaceAll('\\', '/').replace(/^\.\//, '').replace(/^(?:.*?\/)?(?=specs\/)/, '');
|
|
13
31
|
}
|
|
32
|
+
/** Serialises parameters as JSON key/value pairs sorted by key, so equal parameter sets compare equal as strings. */
|
|
14
33
|
export function parameterKey(parameters) {
|
|
15
34
|
return JSON.stringify(Object.entries(parameters).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
16
35
|
}
|
|
17
|
-
/**
|
|
36
|
+
/** Whether a part `kind` only qualifies an outcome (today only `performance`) and so is not counted as a check when anything else is recorded. */
|
|
18
37
|
export function isDiagnosticKind(kind) {
|
|
19
38
|
return kind === 'performance';
|
|
20
39
|
}
|
|
40
|
+
/** A fresh status tally with every status at zero. */
|
|
21
41
|
export function emptyCounts() {
|
|
22
42
|
return { passed: 0, failed: 0, running: 0, 'not-run': 0 };
|
|
23
43
|
}
|
|
24
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Counts rows by status, leaving out the diagnostic rows (see `isDiagnosticKind`) that only qualify an outcome.
|
|
46
|
+
* When every row is diagnostic they are counted, so a layer of timings alone is not empty.
|
|
47
|
+
*/
|
|
25
48
|
export function outcomeCounts(rows) {
|
|
26
49
|
const outcomes = rows.some(row => !isDiagnosticKind(row.kind)) ? rows.filter(row => !isDiagnosticKind(row.kind)) : rows;
|
|
27
50
|
const counts = emptyCounts();
|
|
@@ -29,28 +52,49 @@ export function outcomeCounts(rows) {
|
|
|
29
52
|
counts[row.status] += 1;
|
|
30
53
|
return counts;
|
|
31
54
|
}
|
|
32
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Flattens a layer into what composition reads: a header (origin, normalised environment and parameters, counts,
|
|
57
|
+
* evidence summary), one row per check of every part, and the spec fingerprints it recorded.
|
|
58
|
+
*
|
|
59
|
+
* `backend` is the id of the backend the layer was read from; it only breaks ties between copies. Paths in rows and
|
|
60
|
+
* fingerprints go through `specPath`. When the layer has a recorded `source`, a row whose check id matches one of its
|
|
61
|
+
* tests also gets that test's `testId`, `variant` and duration. The result is `live: false`: a layer read from a
|
|
62
|
+
* store is never a run still in progress.
|
|
63
|
+
*/
|
|
33
64
|
export function layerEvidence(summary, backend) {
|
|
34
65
|
const place = layerPlace(summary);
|
|
35
66
|
const source = summary.source;
|
|
36
67
|
const tests = new Map(source?.tests.map(test => [`${source.source}/${test.testId}`, test]) ?? []);
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
68
|
+
// A layer's checks repeat a handful of spec paths; each is normalized once.
|
|
69
|
+
const normalized = new Map();
|
|
70
|
+
const specOf = (path) => {
|
|
71
|
+
let found = normalized.get(path);
|
|
72
|
+
if (found === undefined)
|
|
73
|
+
normalized.set(path, found = specPath(path));
|
|
74
|
+
return found;
|
|
75
|
+
};
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (const part of summary.parts) {
|
|
78
|
+
for (const check of part.checks) {
|
|
79
|
+
const test = tests.get(check.id);
|
|
80
|
+
const evidence = summarizeEvidence(check);
|
|
81
|
+
rows.push({
|
|
82
|
+
kind: part.kind,
|
|
83
|
+
id: check.id,
|
|
84
|
+
title: check.title,
|
|
85
|
+
status: check.status,
|
|
86
|
+
specPath: check.specPath ? specOf(check.specPath) : null,
|
|
87
|
+
detail: check.detail,
|
|
88
|
+
measurements: check.measurements,
|
|
89
|
+
testId: test?.testId ?? null,
|
|
90
|
+
variant: test ? { system: test.system, runtime: test.runtime, seed: test.seed } : {},
|
|
91
|
+
durationMs: test?.durationMs ?? check.durationMs ?? null,
|
|
92
|
+
...(evidence ? { evidence } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
53
96
|
const recording = summary.recording;
|
|
97
|
+
const layerSummaryEvidence = summarizeEvidence(summary, ...summary.parts);
|
|
54
98
|
const layer = {
|
|
55
99
|
...(summary.freshness ? { freshness: summary.freshness } : {}),
|
|
56
100
|
id: summary.id,
|
|
@@ -67,6 +111,7 @@ export function layerEvidence(summary, backend) {
|
|
|
67
111
|
live: false,
|
|
68
112
|
counts: outcomeCounts(rows),
|
|
69
113
|
execution: textFingerprint(layerExecutionKey(summary)),
|
|
114
|
+
...(layerSummaryEvidence ? { evidence: layerSummaryEvidence } : {}),
|
|
70
115
|
};
|
|
71
116
|
const fingerprints = {};
|
|
72
117
|
for (const spec of summary.specs)
|
|
@@ -74,6 +119,12 @@ export function layerEvidence(summary, backend) {
|
|
|
74
119
|
fingerprints[specPath(spec.path)] = spec.fingerprint;
|
|
75
120
|
return { id: summary.id, layer, rows, fingerprints };
|
|
76
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The freshness warning for one result, or `null` when it is current. Checked in this order: a live run has none;
|
|
124
|
+
* a `modified` or `unknown` layer; a spec that is not in the outline; a missing fingerprint on either side; a
|
|
125
|
+
* fingerprint that differs from the current spec; related files that changed; a layer sha that differs from the
|
|
126
|
+
* outline's (or either absent). Advisory only: a warning never changes a status.
|
|
127
|
+
*/
|
|
77
128
|
function warning(layer, path, current, outline, recorded, relatedChanged) {
|
|
78
129
|
if (layer.live)
|
|
79
130
|
return null;
|
|
@@ -97,8 +148,15 @@ const RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 };
|
|
|
97
148
|
function completed(status) {
|
|
98
149
|
return status === 'passed' || status === 'failed';
|
|
99
150
|
}
|
|
100
|
-
/**
|
|
101
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Whether `next` should take the slot currently held by `prior`: the winner rule of the report contract.
|
|
153
|
+
*
|
|
154
|
+
* In order: a completed result (`passed`, `failed`) beats an unfinished one (`running`, `not-run`) whatever their
|
|
155
|
+
* times; otherwise the greater `recordedAtMs` wins; at the same time `failed` beats `passed` and `running` beats
|
|
156
|
+
* `not-run`; at the same status the greater `layerId|backendId` wins. The last step makes the order total, so the
|
|
157
|
+
* answer never depends on the order results are offered in. Returns `false` for two identical results.
|
|
158
|
+
*/
|
|
159
|
+
export function replaces(next, prior) {
|
|
102
160
|
const done = completed(next.row.status);
|
|
103
161
|
const priorDone = completed(prior.row.status);
|
|
104
162
|
if (done !== priorDone)
|
|
@@ -107,9 +165,22 @@ function replaces(next, prior) {
|
|
|
107
165
|
return next.layer.recordedAtMs > prior.layer.recordedAtMs;
|
|
108
166
|
if (RANK[next.row.status] !== RANK[prior.row.status])
|
|
109
167
|
return RANK[next.row.status] > RANK[prior.row.status];
|
|
110
|
-
|
|
168
|
+
// The last tie-break is total, so no input order can change the answer.
|
|
169
|
+
return layerOrder(next.layer) > layerOrder(prior.layer);
|
|
170
|
+
}
|
|
171
|
+
function layerOrder(layer) {
|
|
172
|
+
return `${layer.id}|${layer.backend}`;
|
|
111
173
|
}
|
|
112
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Lays layers over the outline of current specs and returns the winner of every slot.
|
|
176
|
+
*
|
|
177
|
+
* One linear pass over the rows of the layers (after `distinctEvidence`). The slot is producer, kind, spec path,
|
|
178
|
+
* check id, environment and parameters; `replaces` picks the winner, so the result does not depend on the order of
|
|
179
|
+
* `layers`. Branch, sha, backend and run never split a slot. Each result carries a freshness `warning`, computed
|
|
180
|
+
* against `outline`, that does not affect which result wins.
|
|
181
|
+
*
|
|
182
|
+
* @see docs/report-contract.md#how-layers-compose
|
|
183
|
+
*/
|
|
113
184
|
export function combineEvidence(outline, layers) {
|
|
114
185
|
const specs = new Map(outline.specs.map(spec => [specPath(spec.path), { spec, results: [] }]));
|
|
115
186
|
const changed = new Set(outline.changedFiles.map(specPath));
|
|
@@ -117,12 +188,13 @@ export function combineEvidence(outline, layers) {
|
|
|
117
188
|
for (const { layer, rows, fingerprints } of distinctEvidence(layers)) {
|
|
118
189
|
const scope = layer.packageDir;
|
|
119
190
|
const related = outline.changedFiles.some(file => scope ? file.startsWith(`${scope}/`) : !specPath(file).startsWith('specs/'));
|
|
191
|
+
const keyOf = layerResultKeys(layer);
|
|
120
192
|
for (const row of rows) {
|
|
121
193
|
// Environment and the layer's other choices split the slot. A harness
|
|
122
194
|
// seed does not: a focused rerun replaces the check it repeated.
|
|
123
195
|
const parameters = layer.parameters;
|
|
124
196
|
const path = row.specPath ?? '';
|
|
125
|
-
const key =
|
|
197
|
+
const key = keyOf(row);
|
|
126
198
|
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)) };
|
|
127
199
|
const prior = winners.get(key);
|
|
128
200
|
if (!prior || replaces(result, prior))
|
|
@@ -134,26 +206,70 @@ export function combineEvidence(outline, layers) {
|
|
|
134
206
|
(specs.get(result.row.specPath ?? '')?.results ?? unassigned).push(result);
|
|
135
207
|
return { specs: [...specs.values()], unassigned };
|
|
136
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The key of the slot a row fills: a JSON array of producer, kind, spec path (`''` when none), check id, environment
|
|
211
|
+
* (`''` when none) and the sorted parameters. Two results with the same key compete; the winner is `replaces`.
|
|
212
|
+
*/
|
|
213
|
+
export function resultKey(layer, row) {
|
|
214
|
+
return JSON.stringify([layer.producer, row.kind, row.specPath ?? '', row.id, layer.environment ?? '', parameterKey(layer.parameters)]);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `resultKey` for every row of one layer, which share all but three parts of it. The strings are exactly
|
|
218
|
+
* those `resultKey` makes (they are keys that persist in no store, but two ways of making one must agree),
|
|
219
|
+
* without sorting and writing the layer's parameters once per check.
|
|
220
|
+
*/
|
|
221
|
+
export function layerResultKeys(layer) {
|
|
222
|
+
const producer = JSON.stringify(layer.producer);
|
|
223
|
+
const tail = `,${JSON.stringify(layer.environment ?? '')},${JSON.stringify(parameterKey(layer.parameters))}]`;
|
|
224
|
+
return row => `[${producer},${JSON.stringify(row.kind)},${JSON.stringify(row.specPath ?? '')},${JSON.stringify(row.id)}${tail}`;
|
|
225
|
+
}
|
|
226
|
+
/** The key of a check regardless of producer, environment and parameters: a JSON array of its spec path and check id. */
|
|
137
227
|
export function slotKey(path, id) {
|
|
138
228
|
return JSON.stringify([path, id]);
|
|
139
229
|
}
|
|
230
|
+
/** Whether a layer id is a content digest (64 lowercase hex digits), that is an immutable layer rather than a mutable address. */
|
|
140
231
|
function archived(id) {
|
|
141
232
|
return /^[0-9a-f]{64}$/.test(id);
|
|
142
233
|
}
|
|
143
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* De-duplicates layer headers: the same layer held by several backends is one layer.
|
|
236
|
+
*
|
|
237
|
+
* Suppresses the mutable address of an execution already archived as an immutable layer. An
|
|
238
|
+
* immutable layer (64-hex id) held by several backends is one layer, reported by the backend whose id sorts
|
|
239
|
+
* first, whatever order they were read in. A mutable one is its backend's own and is kept once per backend.
|
|
240
|
+
* Returns the survivors, not necessarily in input order.
|
|
241
|
+
*/
|
|
144
242
|
export function distinctHeaders(layers) {
|
|
145
243
|
const kept = new Set(layers.filter(layer => archived(layer.id)).map(layer => layer.execution));
|
|
146
|
-
|
|
244
|
+
const chosen = new Map();
|
|
245
|
+
for (const layer of layers) {
|
|
246
|
+
if (!archived(layer.id) && kept.has(layer.execution))
|
|
247
|
+
continue;
|
|
248
|
+
const key = archived(layer.id) ? layer.id : `${layer.backend ?? ''}|${layer.id}`;
|
|
249
|
+
const prior = chosen.get(key);
|
|
250
|
+
if (!prior || (layer.backend ?? '') < (prior.backend ?? ''))
|
|
251
|
+
chosen.set(key, layer);
|
|
252
|
+
}
|
|
253
|
+
return [...chosen.values()];
|
|
147
254
|
}
|
|
255
|
+
/** Keeps, in input order, only the layers `distinctHeaders` keeps, and each `backend|id` once. */
|
|
148
256
|
export function distinctEvidence(layers) {
|
|
149
|
-
const kept = new Set(distinctHeaders(layers.map(entry => entry.layer)).map(layer => layer.id));
|
|
150
|
-
|
|
257
|
+
const kept = new Set(distinctHeaders(layers.map(entry => entry.layer)).map(layer => `${layer.backend}|${layer.id}`));
|
|
258
|
+
const emitted = new Set();
|
|
259
|
+
return layers.filter(entry => {
|
|
260
|
+
const key = `${entry.layer.backend}|${entry.id}`;
|
|
261
|
+
if (!kept.has(key) || emitted.has(key))
|
|
262
|
+
return false;
|
|
263
|
+
emitted.add(key);
|
|
264
|
+
return true;
|
|
265
|
+
});
|
|
151
266
|
}
|
|
152
|
-
/**
|
|
267
|
+
/** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
|
|
153
268
|
export function distinctLayers(layers) {
|
|
154
269
|
const kept = new Set(layers.filter(layer => archived(layer.id)).map(layerExecutionKey));
|
|
155
270
|
return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layerExecutionKey(layer))).map(layer => [layer.id, layer])).values()];
|
|
156
271
|
}
|
|
272
|
+
/** A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts. */
|
|
157
273
|
export function layerExecutionKey(layer) {
|
|
158
274
|
return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, layer.parts]);
|
|
159
275
|
}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Archiving a recorded run as a layer
|
|
3
|
+
*
|
|
4
|
+
* Turns one published source (a package bucket of recorded statechart or test runs with timelines and screenshots)
|
|
5
|
+
* into an immutable layer, so it composes with every other layer. It is the bridge from the commit-keyed recordings
|
|
6
|
+
* to the layer store and is called only by `publishSource`.
|
|
7
|
+
*/
|
|
1
8
|
import type { HtmlReportTest } from './recording-bundle.js';
|
|
2
9
|
import { type BundleIndex } from './bundle.js';
|
|
3
10
|
import type { ObjectStore } from './object-store.js';
|
|
4
11
|
import type { SourceRecord } from './protocol.js';
|
|
5
12
|
import type { PublishSourceArgs } from './publish.js';
|
|
13
|
+
/**
|
|
14
|
+
* Writes `source` and its recorded files as an immutable layer and returns it (see `publishLayerBundle`).
|
|
15
|
+
*
|
|
16
|
+
* The layer is recorded at the later of the commit's start and the source's `updatedAtMs`, with the commit's branch and
|
|
17
|
+
* run URL, the commit's spec inventory (empty when none), the source's parts, `parameters` from `args`, the environment
|
|
18
|
+
* normalised from `args.environment` or the `environment` parameter, and `recording: { type: 'layer' }`. Its bundle
|
|
19
|
+
* holds the commit's `frame.css` and each test's `tests/<testId>/report.json` and `tests/<testId>/files/<file>`.
|
|
20
|
+
* `bundle`, when given, is an index of those files already written for this source (keys under
|
|
21
|
+
* `commits/<sha>/tests/<source>/`); then only `frame.css` is written here and the index is re-keyed to the layer's
|
|
22
|
+
* layout. The same inputs give the same layer, so publishing twice is harmless. Store errors reject.
|
|
23
|
+
*/
|
|
6
24
|
export declare function archiveSourceLayer(store: ObjectStore, args: PublishSourceArgs, source: SourceRecord, tests: readonly HtmlReportTest[], bundle?: BundleIndex): Promise<import('./layer-types.js').ReportLayer>;
|
|
@@ -5,6 +5,17 @@ import { readInventory } from './spec-inventory.js';
|
|
|
5
5
|
import { sourceParts } from './source-part.js';
|
|
6
6
|
import { sourceEntries } from './source-entries.js';
|
|
7
7
|
import { publishLayerBundle } from './layer-store.js';
|
|
8
|
+
/**
|
|
9
|
+
* Writes `source` and its recorded files as an immutable layer and returns it (see `publishLayerBundle`).
|
|
10
|
+
*
|
|
11
|
+
* The layer is recorded at the later of the commit's start and the source's `updatedAtMs`, with the commit's branch and
|
|
12
|
+
* run URL, the commit's spec inventory (empty when none), the source's parts, `parameters` from `args`, the environment
|
|
13
|
+
* normalised from `args.environment` or the `environment` parameter, and `recording: { type: 'layer' }`. Its bundle
|
|
14
|
+
* holds the commit's `frame.css` and each test's `tests/<testId>/report.json` and `tests/<testId>/files/<file>`.
|
|
15
|
+
* `bundle`, when given, is an index of those files already written for this source (keys under
|
|
16
|
+
* `commits/<sha>/tests/<source>/`); then only `frame.css` is written here and the index is re-keyed to the layer's
|
|
17
|
+
* layout. The same inputs give the same layer, so publishing twice is harmless. Store errors reject.
|
|
18
|
+
*/
|
|
8
19
|
export async function archiveSourceLayer(store, args, source, tests, bundle) {
|
|
9
20
|
const [commit, inventory, css] = await Promise.all([readCommit(store, args.sha), readInventory(store, args.sha), readFrameCss(store, args.sha)]);
|
|
10
21
|
const recordedAtMs = Math.max(commit?.startedAtMs ?? 0, source.updatedAtMs);
|
|
@@ -1,10 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer storage
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes layers in an `ObjectStore`: the immutable manifest at `layers/manifests/<id>.json`, the catalog
|
|
5
|
+
* entry at `layers/index/<inverted-time>-<id>.json`, and the captured files under `layers/blobs/`. A layer is written
|
|
6
|
+
* bottom up (files, then manifest, then catalog entry) so a reader never meets an entry whose content has not arrived,
|
|
7
|
+
* and every key is derived from content so two producers never write different bytes to one key.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md#where-bytes-go
|
|
10
|
+
*/
|
|
1
11
|
import { type BundleIndex, type BundleEntry } from './bundle.js';
|
|
2
12
|
import type { ObjectStore } from './object-store.js';
|
|
3
13
|
import type { LayerSummary, ReportLayer } from './layer-types.js';
|
|
14
|
+
/** The key prefix under which every layer's catalog entry is written. */
|
|
4
15
|
export declare const LAYER_INDEX = "layers/index/";
|
|
16
|
+
/** The store key of a layer's manifest. Throws `Invalid layer id` unless `id` is 64 lowercase hex digits, so an id can never address another key. */
|
|
5
17
|
export declare function layerKey(id: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Writes a layer whose captured files arrive as a stream, then its manifest and catalog entry, and returns it.
|
|
20
|
+
*
|
|
21
|
+
* The files are packed into content-addressed blobs under `layers/blobs/` first (`writeBundle`), then
|
|
22
|
+
* `publishLayerBundle` does the rest. The id is computed from the content, so publishing the same layer again
|
|
23
|
+
* writes the same keys.
|
|
24
|
+
*/
|
|
6
25
|
export declare function publishLayer(store: ObjectStore, summary: Omit<LayerSummary, 'id'>, entries: AsyncIterable<BundleEntry>): Promise<ReportLayer>;
|
|
26
|
+
/**
|
|
27
|
+
* The key of a layer's catalog entry: `layers/index/<16 digits>-<id>.json`, where the digits are
|
|
28
|
+
* `Number.MAX_SAFE_INTEGER - floor(max(0, recordedAtMs))` zero-padded. The time is inverted so a listing in key order
|
|
29
|
+
* is newest first. The entry makes a layer visible, so it is written last, after the layer and its files.
|
|
30
|
+
*/
|
|
31
|
+
export declare function layerCatalogKey(recordedAtMs: number, id: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Writes a layer whose captured files are already stored (`bundle` locates them) and returns it with its id.
|
|
34
|
+
*
|
|
35
|
+
* The id is the SHA-256 of the JSON of `{ schemaVersion, ...summary, bundle }`, so recording the same thing the same
|
|
36
|
+
* way gives the same id. Writes the manifest only if `head` says it is absent, then always writes the catalog entry
|
|
37
|
+
* (same key, same bytes on a repeat), so a retry is harmless and a crash between the two leaves a manifest no catalog
|
|
38
|
+
* entry names, never the reverse. Store errors reject; nothing is validated here (the publishers do that).
|
|
39
|
+
*/
|
|
7
40
|
export declare function publishLayerBundle(store: ObjectStore, summary: Omit<LayerSummary, 'id'>, bundle: BundleIndex): Promise<ReportLayer>;
|
|
41
|
+
/**
|
|
42
|
+
* Reads and validates a layer manifest, returning it without `schemaVersion`, or `null` when no manifest exists.
|
|
43
|
+
*
|
|
44
|
+
* A manifest that is present but cannot be read is an error naming why, never a half-read layer: it throws when the
|
|
45
|
+
* bytes are not JSON, fail the layer schema, or are a newer major schema version. Callers turn that into a notice,
|
|
46
|
+
* which fails the gate. Throws `Invalid layer id` for a malformed `id`. The id is not recomputed from the content.
|
|
47
|
+
*/
|
|
8
48
|
export declare function readLayer(store: ObjectStore, id: string): Promise<ReportLayer | null>;
|
|
49
|
+
/** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
|
|
9
50
|
export declare function readLayerFile(store: ObjectStore, id: string, file: string): Promise<BundleEntry | null>;
|
|
51
|
+
/** Drops a layer's bundle index, leaving the summary composition reads. */
|
|
10
52
|
export declare function layerSummary(layer: ReportLayer): LayerSummary;
|
|
@@ -1,35 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer storage
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes layers in an `ObjectStore`: the immutable manifest at `layers/manifests/<id>.json`, the catalog
|
|
5
|
+
* entry at `layers/index/<inverted-time>-<id>.json`, and the captured files under `layers/blobs/`. A layer is written
|
|
6
|
+
* bottom up (files, then manifest, then catalog entry) so a reader never meets an entry whose content has not arrived,
|
|
7
|
+
* and every key is derived from content so two producers never write different bytes to one key.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md#where-bytes-go
|
|
10
|
+
*/
|
|
1
11
|
import { createBundleReader, writeBundle } from './bundle.js';
|
|
2
12
|
import { readUtf8, writeUtf8 } from './object-io.js';
|
|
13
|
+
import { LAYER_SCHEMA_VERSION, parseLayerDocument } from '../../schema/src/layer.js';
|
|
14
|
+
/** The key prefix under which every layer's catalog entry is written. */
|
|
3
15
|
export const LAYER_INDEX = 'layers/index/';
|
|
16
|
+
/** The store key of a layer's manifest. Throws `Invalid layer id` unless `id` is 64 lowercase hex digits, so an id can never address another key. */
|
|
4
17
|
export function layerKey(id) {
|
|
5
18
|
if (!/^[0-9a-f]{64}$/.test(id))
|
|
6
19
|
throw new Error('Invalid layer id');
|
|
7
20
|
return `layers/manifests/${id}.json`;
|
|
8
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Writes a layer whose captured files arrive as a stream, then its manifest and catalog entry, and returns it.
|
|
24
|
+
*
|
|
25
|
+
* The files are packed into content-addressed blobs under `layers/blobs/` first (`writeBundle`), then
|
|
26
|
+
* `publishLayerBundle` does the rest. The id is computed from the content, so publishing the same layer again
|
|
27
|
+
* writes the same keys.
|
|
28
|
+
*/
|
|
9
29
|
export async function publishLayer(store, summary, entries) {
|
|
10
30
|
const bundle = await writeBundle(store, entries, 'layers/blobs/');
|
|
11
31
|
return publishLayerBundle(store, summary, bundle);
|
|
12
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The key of a layer's catalog entry: `layers/index/<16 digits>-<id>.json`, where the digits are
|
|
35
|
+
* `Number.MAX_SAFE_INTEGER - floor(max(0, recordedAtMs))` zero-padded. The time is inverted so a listing in key order
|
|
36
|
+
* is newest first. The entry makes a layer visible, so it is written last, after the layer and its files.
|
|
37
|
+
*/
|
|
38
|
+
export function layerCatalogKey(recordedAtMs, id) {
|
|
39
|
+
return `${LAYER_INDEX}${String(Number.MAX_SAFE_INTEGER - Math.max(0, Math.floor(recordedAtMs))).padStart(16, '0')}-${id}.json`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Writes a layer whose captured files are already stored (`bundle` locates them) and returns it with its id.
|
|
43
|
+
*
|
|
44
|
+
* The id is the SHA-256 of the JSON of `{ schemaVersion, ...summary, bundle }`, so recording the same thing the same
|
|
45
|
+
* way gives the same id. Writes the manifest only if `head` says it is absent, then always writes the catalog entry
|
|
46
|
+
* (same key, same bytes on a repeat), so a retry is harmless and a crash between the two leaves a manifest no catalog
|
|
47
|
+
* entry names, never the reverse. Store errors reject; nothing is validated here (the publishers do that).
|
|
48
|
+
*/
|
|
13
49
|
export async function publishLayerBundle(store, summary, bundle) {
|
|
14
|
-
|
|
50
|
+
// The id is the digest of exactly this document without it, so every producer that records the same thing writes the same layer.
|
|
51
|
+
const body = JSON.stringify({ schemaVersion: LAYER_SCHEMA_VERSION, ...summary, bundle });
|
|
15
52
|
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
|
|
16
53
|
const id = [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
17
54
|
const layer = { ...summary, id, bundle };
|
|
18
55
|
// Immutable manifest first, independent catalog entry last: concurrent producers never overwrite one another.
|
|
19
56
|
if (!(await store.head(layerKey(id))))
|
|
20
|
-
await writeUtf8(store, layerKey(id), JSON.stringify(layer), 'application/json');
|
|
21
|
-
|
|
22
|
-
await writeUtf8(store, `${LAYER_INDEX}${inverse}-${id}.json`, JSON.stringify({ id }), 'application/json');
|
|
57
|
+
await writeUtf8(store, layerKey(id), JSON.stringify({ schemaVersion: LAYER_SCHEMA_VERSION, ...layer }), 'application/json');
|
|
58
|
+
await writeUtf8(store, layerCatalogKey(summary.recordedAtMs, id), JSON.stringify({ id }), 'application/json');
|
|
23
59
|
return layer;
|
|
24
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads and validates a layer manifest, returning it without `schemaVersion`, or `null` when no manifest exists.
|
|
63
|
+
*
|
|
64
|
+
* A manifest that is present but cannot be read is an error naming why, never a half-read layer: it throws when the
|
|
65
|
+
* bytes are not JSON, fail the layer schema, or are a newer major schema version. Callers turn that into a notice,
|
|
66
|
+
* which fails the gate. Throws `Invalid layer id` for a malformed `id`. The id is not recomputed from the content.
|
|
67
|
+
*/
|
|
25
68
|
export async function readLayer(store, id) {
|
|
26
69
|
const raw = await readUtf8(store, layerKey(id));
|
|
27
|
-
|
|
70
|
+
if (raw === null)
|
|
71
|
+
return null;
|
|
72
|
+
let json;
|
|
73
|
+
try {
|
|
74
|
+
json = JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new Error(`Layer ${id.slice(0, 12)} is not JSON`);
|
|
78
|
+
}
|
|
79
|
+
const parsed = parseLayerDocument(json);
|
|
80
|
+
if (!parsed.ok)
|
|
81
|
+
throw new Error(`Layer ${id.slice(0, 12)}: ${parsed.reason}`);
|
|
82
|
+
const { schemaVersion: _version, ...layer } = parsed.layer;
|
|
83
|
+
return layer;
|
|
28
84
|
}
|
|
85
|
+
/** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
|
|
29
86
|
export async function readLayerFile(store, id, file) {
|
|
30
87
|
const layer = await readLayer(store, id);
|
|
31
88
|
return layer ? createBundleReader(store, layer.bundle)(file) : null;
|
|
32
89
|
}
|
|
90
|
+
/** Drops a layer's bundle index, leaving the summary composition reads. */
|
|
33
91
|
export function layerSummary(layer) {
|
|
34
92
|
const { bundle: _, ...summary } = layer;
|
|
35
93
|
return summary;
|
|
@@ -1,21 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer types
|
|
3
|
+
*
|
|
4
|
+
* The in-memory shapes of a layer: `LayerSummary` (everything but its captured files), `ReportLayer` (the summary plus
|
|
5
|
+
* the index of those files) and `ReportOutline` (the specs a report is laid over). The stored form is the layer
|
|
6
|
+
* document of `packages/schema/src/layer.ts`; these types are what the reader and publisher code hands around after
|
|
7
|
+
* parsing it. Fields follow the layer document, so adding a field here means adding an optional field there.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md#the-layer-document
|
|
10
|
+
*/
|
|
1
11
|
import type { BundleIndex } from './bundle.js';
|
|
2
12
|
import type { ReportPart, SpecEntry } from './explorer-types.js';
|
|
3
13
|
import type { SourceRecord } from './protocol.js';
|
|
14
|
+
/** String choices (such as `{ device: 'iphone-15' }`) that make a result its own slot. Compared as sorted key/value pairs. */
|
|
4
15
|
export type LayerParameters = Readonly<Record<string, string>>;
|
|
5
|
-
|
|
16
|
+
/** What a layer may carry as a whole. All optional and open: see docs/extending.md. */
|
|
17
|
+
export type LayerExtras = Partial<Pick<import('../../schema/src/layer.js').LayerDocument, 'tags' | 'labels' | 'links' | 'attachments' | 'annotations' | 'extensions'>>;
|
|
18
|
+
/** A layer without its captured files: what composition and the viewer's lists read. */
|
|
19
|
+
export type LayerSummary = LayerExtras & {
|
|
20
|
+
/** `modified`: recorded from a checkout with uncommitted changes. `unknown`: origin not captured. Absent for a clean checkout. Advisory; never changes a status. */
|
|
6
21
|
readonly freshness?: 'unknown' | 'modified';
|
|
22
|
+
/** The layer's content digest: 64 lowercase hex digits. Opaque to readers. Legacy commit-keyed recordings use `part-<sha>-<id>` or `source-<sha>-<id>` instead. */
|
|
7
23
|
readonly id: string;
|
|
24
|
+
/** When the checks ran, epoch milliseconds. The newest completed result wins its slot. */
|
|
8
25
|
readonly recordedAtMs: number;
|
|
26
|
+
/** The stable name of the suite or tool that made the layer. Part of the slot. */
|
|
9
27
|
readonly producer: string;
|
|
28
|
+
/** Provenance: the commit recorded, or `null`. Never part of the slot. */
|
|
10
29
|
readonly sha: string | null;
|
|
30
|
+
/** Provenance: the branch recorded, or `null`. Never part of the slot. */
|
|
11
31
|
readonly branch: string | null;
|
|
12
32
|
/** The client this layer checked, when it said. Normalized when the layer is read. */
|
|
13
33
|
readonly environment?: string | null;
|
|
34
|
+
/** Provenance: the CI run that produced the layer, or `null`. Never part of the slot. */
|
|
14
35
|
readonly runUrl: string | null;
|
|
36
|
+
/** Free choices that make a result its own slot. The `environment` parameter is read as the environment (see `layerPlace`). */
|
|
15
37
|
readonly parameters: LayerParameters;
|
|
38
|
+
/** The specs the layer was recorded against, with fingerprints of their text. */
|
|
16
39
|
readonly specs: readonly SpecEntry[];
|
|
40
|
+
/** The checks, grouped by producer-defined kind. */
|
|
17
41
|
readonly parts: readonly ReportPart[];
|
|
42
|
+
/** The recorded statechart run behind the layer (timelines and tests), or `null` for a layer of plain checks. */
|
|
18
43
|
readonly source: SourceRecord | null;
|
|
44
|
+
/** Where the layer's timelines and captured files are read from: its own bundle (`layer`), an older commit-keyed recording (`commit`), or nowhere (`null`). */
|
|
19
45
|
readonly recording: {
|
|
20
46
|
readonly type: 'layer';
|
|
21
47
|
} | {
|
|
@@ -24,11 +50,16 @@ export type LayerSummary = {
|
|
|
24
50
|
readonly source: string;
|
|
25
51
|
} | null;
|
|
26
52
|
};
|
|
53
|
+
/** A layer with the index that locates its captured files (screenshots, frames) in the store. */
|
|
27
54
|
export type ReportLayer = LayerSummary & {
|
|
28
55
|
readonly bundle: BundleIndex;
|
|
29
56
|
};
|
|
57
|
+
/** The current specs a report is laid over, and the commit and changes freshness warnings are judged against. */
|
|
30
58
|
export type ReportOutline = {
|
|
59
|
+
/** The specs that make the outline of the report. A spec here with no layer shows as having no results. */
|
|
31
60
|
readonly specs: readonly SpecEntry[];
|
|
61
|
+
/** The commit the specs are from, or `null` when unknown. */
|
|
32
62
|
readonly sha: string | null;
|
|
63
|
+
/** Paths changed since the layers were recorded; a result touching one is warned as potentially outdated. */
|
|
33
64
|
readonly changedFiles: readonly string[];
|
|
34
65
|
};
|
|
@@ -1,20 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layers HTTP routes
|
|
3
|
+
*
|
|
4
|
+
* The read-only HTTP API the viewer uses to see layers: a paged list of layer headers merged from every backend, the
|
|
5
|
+
* rows of one layer, its attachments and recorded timelines, the live runs still writing on this machine, and the
|
|
6
|
+
* spec outline. It is mounted under `/api/reports` by `reportsHost` and by the Atlas server. Immutable layers (64-hex
|
|
7
|
+
* ids) are read once and cached for good and answered with long-lived `immutable` cache headers; mutable commit-keyed
|
|
8
|
+
* recordings are re-read after a short wait. A backend that cannot be listed or a layer that cannot be read becomes a
|
|
9
|
+
* notice in the page, never a silent omission and never a pass. Layers the host's `include` filter rejects are absent
|
|
10
|
+
* from every route here, so a filter cannot be undone by asking for history.
|
|
11
|
+
*
|
|
12
|
+
* @see docs/server.md
|
|
13
|
+
* @see docs/report-contract.md#failure-behaviour
|
|
14
|
+
*/
|
|
1
15
|
import { Hono } from 'hono';
|
|
2
16
|
import type { ObjectStore } from './object-store.js';
|
|
3
17
|
import type { ReportsAppDeps } from './reports-route.js';
|
|
4
|
-
/** A further store whose layers join the combined report, beside the local and the shared one. */
|
|
18
|
+
/** A further store whose layers join the combined report, beside the local and the shared one. Passed as `backends` in `ReportsAppDeps`. */
|
|
5
19
|
export type ReportBackend = {
|
|
6
|
-
/** Stable and unique: letters, digits and hyphens. `local` and `shared` are taken. */
|
|
20
|
+
/** Stable and unique: lowercase letters, digits and hyphens. `local` and `shared` are taken. Appears in cursors and URLs. */
|
|
7
21
|
readonly id: string;
|
|
22
|
+
/** Human name shown for the backend in the viewer. */
|
|
8
23
|
readonly label: string;
|
|
24
|
+
/** Where the backend's layers are read from. */
|
|
9
25
|
readonly store: ObjectStore;
|
|
10
26
|
};
|
|
11
27
|
type Backend = ReportBackend & {
|
|
12
28
|
readonly live: boolean;
|
|
13
29
|
};
|
|
30
|
+
/** The id of the backend for this machine's own store (`localStore`), where runs may still be writing. */
|
|
14
31
|
export declare const LOCAL_BACKEND = "local";
|
|
32
|
+
/** The id of the backend for the shared reports store (`store`). */
|
|
15
33
|
export declare const SHARED_BACKEND = "shared";
|
|
16
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The stores that contribute layers, in the order they answer for a recording: `localStore` (id `local`, live), then
|
|
36
|
+
* `store` (id `shared`) when it is a different store, then each of `backends`.
|
|
37
|
+
*
|
|
38
|
+
* Throws `Invalid report backend id: "<id>"` when an extra backend's id is not lowercase letters, digits and hyphens
|
|
39
|
+
* or repeats an earlier id (including `local` and `shared`).
|
|
40
|
+
*/
|
|
17
41
|
export declare function reportBackends(deps: ReportsAppDeps): Backend[];
|
|
18
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* Builds the Hono app that serves layers: bounded pages of layer origins, each layer's checks read once, and the
|
|
44
|
+
* runs still writing. Throws (as `reportBackends`) when a backend id is invalid.
|
|
45
|
+
*
|
|
46
|
+
* Routes, relative to where it is mounted:
|
|
47
|
+
* - `GET /outline` the current specs as `{ specs, sha, changedFiles }` (503 when the host has no outline), cached 30 s.
|
|
48
|
+
* - `GET /outline/spec?path=` one outline spec's text; 404 for a path not in the outline.
|
|
49
|
+
* - `GET /evidence?cursor=&retry=` one page of layer headers (`EvidencePage`) from 40 catalog entries across all
|
|
50
|
+
* backends: immutable layers newest first, then commit-keyed recordings. Without a cursor it also carries the live
|
|
51
|
+
* runs of this machine. `cursor` is the previous page's; 409 when it no longer matches the listing (refresh).
|
|
52
|
+
* Any `retry` value re-lists backends that were unavailable now instead of after the wait.
|
|
53
|
+
* - `GET /evidence/rows/:backend/:id` the checks and spec fingerprints of one layer (`LayerRows`).
|
|
54
|
+
* - `GET /evidence/detail/:backend/:id?check=&kind=` the run-level evidence of an immutable layer, or one check's detail.
|
|
55
|
+
* - `GET /layers/:id/blobs/*` an attachment of an immutable layer by its bundle key, served by `attachmentResponse`.
|
|
56
|
+
* - `GET /layers/:id/*` the recorded `frame.css`, `tests/<test>/report.json` and `tests/<test>/files/<frame>` of a layer.
|
|
57
|
+
* - `GET /live/:source/frame.css` and `GET /live/:source/tests/:testId/*` the same for a run still writing (uncached).
|
|
58
|
+
*
|
|
59
|
+
* Directory listings are cached for 30 s (1.5 s for the local store). A listing that fails keeps the previous keys and
|
|
60
|
+
* marks the backend unavailable; the page then carries a notice.
|
|
61
|
+
*/
|
|
19
62
|
export declare function createLayersApp(deps: ReportsAppDeps): Hono<import("hono/types").BlankEnv, import("hono/types").BlankSchema, "/">;
|
|
20
63
|
export {};
|