@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Directory-backed object store
|
|
3
|
+
*
|
|
4
|
+
* The {@link ObjectStore} implementation for a local folder: a CI workspace, a downloaded artifact, a mount or a
|
|
5
|
+
* laptop. Each key is a file under the root; its content type is kept in a sidecar `<key>.content-type`. Writes go
|
|
6
|
+
* to a temporary `.tmp-*` file and are renamed into place so a reader sees all of an object or none of it. Keys
|
|
7
|
+
* that could escape the root (`..`, absolute, backslash, empty segments) or collide with the sidecar or temporary
|
|
8
|
+
* names are rejected with an error. Node/Bun only.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/report-contract.md (backends)
|
|
11
|
+
*/
|
|
12
|
+
import { mkdir, readFile, readdir, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
import { pathToFileURL } from 'node:url';
|
|
15
|
+
import { ObjectStoreWithPrefix } from './store-prefix.js';
|
|
16
|
+
const TYPE_SUFFIX = '.content-type';
|
|
17
|
+
const TEMP_PREFIX = '.tmp-';
|
|
18
|
+
function rejectKey(key) {
|
|
19
|
+
const parts = key.split('/');
|
|
20
|
+
const last = parts.at(-1) ?? '';
|
|
21
|
+
if (key.length === 0 || key.startsWith('/') || key.includes('\\') || parts.some((part) => part.length === 0 || part === '.' || part === '..') || last.endsWith(TYPE_SUFFIX) || last.startsWith(TEMP_PREFIX)) {
|
|
22
|
+
throw new Error(`Invalid object store key: ${key}`);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function fileOf(root, key) {
|
|
26
|
+
rejectKey(key);
|
|
27
|
+
const rootPath = path.resolve(root);
|
|
28
|
+
const file = path.resolve(rootPath, key);
|
|
29
|
+
if (file !== rootPath && !file.startsWith(rootPath + path.sep))
|
|
30
|
+
throw new Error(`Invalid object store key: ${key}`);
|
|
31
|
+
return file;
|
|
32
|
+
}
|
|
33
|
+
function missing(error) {
|
|
34
|
+
const code = error.code;
|
|
35
|
+
return code === 'ENOENT' || code === 'ENOTDIR';
|
|
36
|
+
}
|
|
37
|
+
let counter = 0;
|
|
38
|
+
/** Write beside the target and rename into place: a reader sees the whole object or none of it. */
|
|
39
|
+
async function writeAtomically(file, bytes) {
|
|
40
|
+
const temporary = path.join(path.dirname(file), `${TEMP_PREFIX}${process.pid}-${Date.now().toString(36)}-${(counter += 1).toString(36)}`);
|
|
41
|
+
try {
|
|
42
|
+
await writeFile(temporary, bytes);
|
|
43
|
+
await rename(temporary, file);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
await rm(temporary, { force: true });
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Creates an {@link ObjectStore} on a directory: one file per key, its content type in a sidecar file. The
|
|
52
|
+
* backend for a CI workspace, a downloaded artifact or a laptop. Writes are atomic, listing reads
|
|
53
|
+
* only the directories a prefix can reach, and two stores over copies of the same layers can be
|
|
54
|
+
* merged with `cp -R` because layers are immutable and addressed by content.
|
|
55
|
+
*
|
|
56
|
+
* The directory need not exist yet: `put` creates it, and `get`/`head`/`listKeys` on a missing one answer
|
|
57
|
+
* null/false/`[]`. A missing sidecar reads as `application/octet-stream`. `put` writes the content type before
|
|
58
|
+
* the bytes, so an object is never visible without its type. `listKeys` returns keys in code-unit order, skips
|
|
59
|
+
* temporary and sidecar files, and throws on a prefix that is absolute, contains a backslash or `..`. `getUri`
|
|
60
|
+
* is a `file://` URL. Also implements `listPage` (`after` is exclusive, `limit` defaults to 1000, `next` is the
|
|
61
|
+
* last returned key while more remain).
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const store = directoryStore('./atlas-reports');
|
|
65
|
+
* await store.put('layers/blobs/x', new TextEncoder().encode('hi'), 'text/plain');
|
|
66
|
+
* await store.listKeys('layers/'); // ['layers/blobs/x']
|
|
67
|
+
*/
|
|
68
|
+
export function directoryStore(root) {
|
|
69
|
+
const store = {
|
|
70
|
+
async get(key) {
|
|
71
|
+
const file = fileOf(root, key);
|
|
72
|
+
try {
|
|
73
|
+
// The type is beside the object: both are asked for at once.
|
|
74
|
+
const [content, contentType] = await Promise.all([readFile(file), readFile(file + TYPE_SUFFIX, 'utf8').catch(() => 'application/octet-stream')]);
|
|
75
|
+
const bytes = content;
|
|
76
|
+
return {
|
|
77
|
+
body: new ReadableStream({
|
|
78
|
+
start(controller) {
|
|
79
|
+
controller.enqueue(bytes);
|
|
80
|
+
controller.close();
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
contentType: contentType.trim() || 'application/octet-stream',
|
|
84
|
+
size: bytes.byteLength
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (missing(error))
|
|
89
|
+
return null;
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
async put(key, bytes, contentType) {
|
|
94
|
+
const file = fileOf(root, key);
|
|
95
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
96
|
+
// The type first: an object is never visible without it.
|
|
97
|
+
await writeAtomically(file + TYPE_SUFFIX, contentType);
|
|
98
|
+
await writeAtomically(file, bytes);
|
|
99
|
+
},
|
|
100
|
+
async head(key) {
|
|
101
|
+
try {
|
|
102
|
+
return (await stat(fileOf(root, key))).isFile();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (missing(error))
|
|
106
|
+
return false;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async delete(key) {
|
|
111
|
+
const file = fileOf(root, key);
|
|
112
|
+
await rm(file, { force: true });
|
|
113
|
+
await rm(file + TYPE_SUFFIX, { force: true });
|
|
114
|
+
},
|
|
115
|
+
async deleteMany(keys) {
|
|
116
|
+
for (const key of keys)
|
|
117
|
+
await store.delete(key);
|
|
118
|
+
},
|
|
119
|
+
async getUri(key) {
|
|
120
|
+
return (await store.head(key)) ? pathToFileURL(fileOf(root, key)).href : null;
|
|
121
|
+
},
|
|
122
|
+
async listKeys(prefix = '') {
|
|
123
|
+
const rootPath = path.resolve(root);
|
|
124
|
+
const found = [];
|
|
125
|
+
const walk = async (dir) => {
|
|
126
|
+
let entries;
|
|
127
|
+
try {
|
|
128
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (missing(error))
|
|
132
|
+
return;
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
await Promise.all(entries.map(async (entry) => {
|
|
136
|
+
if (entry.name.startsWith(TEMP_PREFIX) || entry.name.endsWith(TYPE_SUFFIX))
|
|
137
|
+
return;
|
|
138
|
+
const full = path.join(dir, entry.name);
|
|
139
|
+
if (entry.isDirectory())
|
|
140
|
+
return walk(full);
|
|
141
|
+
if (!entry.isFile())
|
|
142
|
+
return;
|
|
143
|
+
const key = path.relative(rootPath, full).split(path.sep).join('/');
|
|
144
|
+
if (key.startsWith(prefix))
|
|
145
|
+
found.push(key);
|
|
146
|
+
}));
|
|
147
|
+
};
|
|
148
|
+
// Only the directory a prefix names can hold its keys.
|
|
149
|
+
const slash = prefix.lastIndexOf('/');
|
|
150
|
+
if (prefix.startsWith('/') || prefix.includes('\\') || prefix.split('/').some((part) => part === '..'))
|
|
151
|
+
throw new Error(`Invalid object store prefix: ${prefix}`);
|
|
152
|
+
await walk(slash < 0 ? rootPath : path.resolve(rootPath, prefix.slice(0, slash)));
|
|
153
|
+
return found.sort();
|
|
154
|
+
},
|
|
155
|
+
async listPage(prefix, options = {}) {
|
|
156
|
+
const all = await store.listKeys(prefix);
|
|
157
|
+
const from = options.after === undefined ? 0 : all.findIndex(key => key > options.after);
|
|
158
|
+
const keys = from < 0 ? [] : all.slice(from, from + (options.limit ?? 1000));
|
|
159
|
+
return { keys, next: from >= 0 && from + keys.length < all.length ? keys.at(-1) ?? null : null };
|
|
160
|
+
},
|
|
161
|
+
withPrefix(prefix) {
|
|
162
|
+
return new ObjectStoreWithPrefix(store, prefix);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
return store;
|
|
166
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Store doctor and IAM policy
|
|
3
|
+
*
|
|
4
|
+
* `inspectStore` checks that a store works end to end (credentials, listing, what it holds, writing and reading back a
|
|
5
|
+
* scratch object) and says what to fix when it does not; `iamPolicy` prints the least-privilege AWS policy for publishers
|
|
6
|
+
* and readers. Used by `atlas report doctor` and `atlas report policy`, for folders, S3-compatible buckets and Atlas
|
|
7
|
+
* servers alike.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/deploying.md
|
|
10
|
+
*/
|
|
11
|
+
import type { CliBackend } from './cli-stores.js';
|
|
12
|
+
/** One thing `inspectStore` checked: its `name` (`store`, `list`, `contents`, `write`, …), whether it is `ok`, what was seen, and, when it is not, what to `fix`. */
|
|
13
|
+
export type Check = {
|
|
14
|
+
readonly ok: boolean;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly detail: string;
|
|
17
|
+
readonly fix?: string;
|
|
18
|
+
};
|
|
19
|
+
/** The least an account needs, as an AWS IAM policy. The combined server role also stores operational leases under server/live/. R2, MinIO and the rest have their own policy languages: grant the same actions. */
|
|
20
|
+
export declare function iamPolicy(location: {
|
|
21
|
+
bucket: string;
|
|
22
|
+
prefix: string;
|
|
23
|
+
}, role: 'publisher' | 'reader' | 'both'): object;
|
|
24
|
+
/** What the account being checked is for: `publisher` (writes layers), `reader` (reads them) or `both`. It decides which checks run. */
|
|
25
|
+
export type Role = 'publisher' | 'reader' | 'both';
|
|
26
|
+
/** Is this store usable for Atlas, and if not, what exactly is wrong? Each step says what to do. */
|
|
27
|
+
export declare function inspectStore(backend: CliBackend, options?: {
|
|
28
|
+
readonly role?: Role;
|
|
29
|
+
}): Promise<Check[]>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Store doctor and IAM policy
|
|
3
|
+
*
|
|
4
|
+
* `inspectStore` checks that a store works end to end (credentials, listing, what it holds, writing and reading back a
|
|
5
|
+
* scratch object) and says what to fix when it does not; `iamPolicy` prints the least-privilege AWS policy for publishers
|
|
6
|
+
* and readers. Used by `atlas report doctor` and `atlas report policy`, for folders, S3-compatible buckets and Atlas
|
|
7
|
+
* servers alike.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/deploying.md
|
|
10
|
+
*/
|
|
11
|
+
import { LAYER_INDEX } from './layer-store.js';
|
|
12
|
+
import { verifyObjectStore } from './object-store-conformance.js';
|
|
13
|
+
/** The least an account needs, as an AWS IAM policy. The combined server role also stores operational leases under server/live/. R2, MinIO and the rest have their own policy languages: grant the same actions. */
|
|
14
|
+
export function iamPolicy(location, role) {
|
|
15
|
+
const base = location.prefix ? `${location.prefix}/` : '';
|
|
16
|
+
const bucketArn = `arn:aws:s3:::${location.bucket}`;
|
|
17
|
+
const statements = [];
|
|
18
|
+
if (role !== 'reader') {
|
|
19
|
+
statements.push({ Sid: 'AtlasPublish', Effect: 'Allow', Action: ['s3:PutObject', 's3:GetObject'], Resource: `${bucketArn}/${base}layers/*` }, { Sid: 'AtlasDoctorCleanup', Effect: 'Allow', Action: 's3:DeleteObject', Resource: `${bucketArn}/${base}layers/doctor-*` });
|
|
20
|
+
}
|
|
21
|
+
if (role !== 'publisher') {
|
|
22
|
+
statements.push({ Sid: 'AtlasRead', Effect: 'Allow', Action: 's3:GetObject', Resource: `${bucketArn}/${base}layers/*` });
|
|
23
|
+
}
|
|
24
|
+
if (role === 'both')
|
|
25
|
+
statements.push({ Sid: 'AtlasLiveRuns', Effect: 'Allow', Action: ['s3:GetObject', 's3:PutObject'], Resource: `${bucketArn}/${base}server/live/*` }, { Sid: 'AtlasProjects', Effect: 'Allow', Action: ['s3:GetObject', 's3:PutObject', 's3:DeleteObject'], Resource: [`${bucketArn}/${base}projects/*`, `${bucketArn}/${base}server/projects/*`] });
|
|
26
|
+
// Publishers need this too: without it S3 answers 403, not 404, when asked whether a layer exists yet.
|
|
27
|
+
statements.push({ Sid: 'AtlasList', Effect: 'Allow', Action: 's3:ListBucket', Resource: bucketArn, // IfExists: a listing always carries a prefix and is held to it; a HEAD or GET of one object carries none, and
|
|
28
|
+
// must still be answered "not found" rather than "forbidden".
|
|
29
|
+
...(base ? { Condition: { StringLikeIfExists: { 's3:prefix': [base, `${base}*`] } } } : {}) });
|
|
30
|
+
return { Version: '2012-10-17', Statement: statements };
|
|
31
|
+
}
|
|
32
|
+
const when = (inverted) => {
|
|
33
|
+
const time = inverted ? Number.MAX_SAFE_INTEGER - Number(inverted) : NaN;
|
|
34
|
+
return Number.isFinite(time) ? new Date(time).toISOString() : 'unknown';
|
|
35
|
+
};
|
|
36
|
+
/** Is this store usable for Atlas, and if not, what exactly is wrong? Each step says what to do. */
|
|
37
|
+
export async function inspectStore(backend, options = {}) {
|
|
38
|
+
const role = options.role ?? 'both';
|
|
39
|
+
const checks = [];
|
|
40
|
+
const add = (check) => { checks.push(check); return check.ok; };
|
|
41
|
+
const store = backend.store;
|
|
42
|
+
const s3 = backend.s3;
|
|
43
|
+
add({ ok: true, name: 'store', detail: s3?.label ?? backend.label ?? backend.id });
|
|
44
|
+
if (s3 && role !== 'publisher') {
|
|
45
|
+
const ping = await s3.ping();
|
|
46
|
+
add(ping.ok
|
|
47
|
+
? { ok: true, name: 'list', detail: `${ping.ms} ms${s3.credentialSource() ? `, credentials from ${s3.credentialSource()}` : ''}` }
|
|
48
|
+
: { ok: false, name: 'list', detail: ping.error ?? 'failed', fix: 'Check the bucket, region and endpoint in the store URL, and the credentials. `atlas report policy --store <url>` prints the permissions needed.' });
|
|
49
|
+
if (!ping.ok)
|
|
50
|
+
return checks;
|
|
51
|
+
}
|
|
52
|
+
if (role !== 'publisher') {
|
|
53
|
+
try {
|
|
54
|
+
const keys = await store.listKeys(LAYER_INDEX);
|
|
55
|
+
const [sets, runs] = await Promise.all([store.listKeys('layers/specs/index/'), store.listKeys('layers/runs/index/')]);
|
|
56
|
+
add({ ok: true, name: 'contents', detail: `${keys.length} layer${keys.length === 1 ? '' : 's'}${keys.length ? ` (newest ${when(/\/(\d{16})-/.exec(keys[0])?.[1])})` : ''}, ${sets.length} spec set${sets.length === 1 ? '' : 's'}, ${runs.length} run${runs.length === 1 ? '' : 's'}` });
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
add({ ok: false, name: 'contents', detail: error instanceof Error ? error.message : String(error) });
|
|
60
|
+
return checks;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (role === 'reader')
|
|
64
|
+
return checks;
|
|
65
|
+
const scratch = `layers/doctor-${Math.random().toString(36).slice(2, 10)}/`;
|
|
66
|
+
if (role === 'both') {
|
|
67
|
+
const verification = await verifyObjectStore(store, scratch);
|
|
68
|
+
add(verification.ok
|
|
69
|
+
? { ok: true, name: 'write', detail: 'wrote, listed, read back and deleted a scratch object: the store meets the ObjectStore contract' }
|
|
70
|
+
: { ok: false, name: 'write', detail: verification.failures.join('; '), fix: 'Publishers need s3:PutObject and s3:GetObject on layers/*, and the doctor s3:DeleteObject on layers/doctor-*. To check a publisher-only or read-only account, pass --role publisher or --role reader.' });
|
|
71
|
+
return checks;
|
|
72
|
+
}
|
|
73
|
+
// A publisher cannot list: prove what it does, which is put, head and get.
|
|
74
|
+
const key = `${scratch}probe.txt`;
|
|
75
|
+
try {
|
|
76
|
+
await store.put(key, new TextEncoder().encode('atlas'), 'text/plain');
|
|
77
|
+
const found = await store.head(key);
|
|
78
|
+
await store.delete(key).catch(() => { });
|
|
79
|
+
add(found ? { ok: true, name: 'write', detail: 'wrote and found a scratch object' } : { ok: false, name: 'write', detail: 'an object was written but could not be found again', fix: 'The account needs s3:GetObject (HeadObject) on layers/*.' });
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
add({ ok: false, name: 'write', detail: error instanceof Error ? error.message : String(error), fix: 'Publishers need s3:PutObject and s3:GetObject on layers/*, and s3:ListBucket on the bucket (S3 answers 403 instead of 404 for a missing object without it).' });
|
|
83
|
+
}
|
|
84
|
+
return checks;
|
|
85
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Report deltas
|
|
3
|
+
*
|
|
4
|
+
* The client half of "send only what changed": `applyEffectiveDelta` turns the report a viewer holds and a delta from the
|
|
5
|
+
* server (`GET /api/reports/evidence/effective?since=<version>`) into the current report. Pure and free of Node APIs, so a
|
|
6
|
+
* browser bundle uses it. The server half is `ReportIndex.delta`; a test holds the two together over random runs.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/server.md#reading-only-what-changed
|
|
9
|
+
*/
|
|
10
|
+
import type { LayerEvidence } from './evidence-types.js';
|
|
11
|
+
import type { EffectiveEvidence } from './report-index.js';
|
|
12
|
+
/**
|
|
13
|
+
* What a server answers when a viewer asks for the report `since` a version it holds and the server still
|
|
14
|
+
* remembers that version: only what changed. `layers` are whole layers (with the rows that win), replacing those
|
|
15
|
+
* of the same id; `removed` are layers that no longer win anything. Everything else, `backends` and `notices`
|
|
16
|
+
* and the count, is sent whole because it is small.
|
|
17
|
+
*
|
|
18
|
+
* It is an optimisation and nothing else: applying it gives exactly the report the server would have sent in
|
|
19
|
+
* full, and a server that does not know it sends the full report, which a viewer takes as it always did.
|
|
20
|
+
*/
|
|
21
|
+
export type EffectiveDelta = Omit<EffectiveEvidence, 'layers'> & {
|
|
22
|
+
readonly delta: true;
|
|
23
|
+
/** The version this applies to. */
|
|
24
|
+
readonly base: string;
|
|
25
|
+
readonly layers: readonly LayerEvidence[];
|
|
26
|
+
readonly removed: readonly string[];
|
|
27
|
+
};
|
|
28
|
+
/** Is `value` a delta (`delta: true`, a `base` version, `layers` and `removed`) and not a whole report? Decides which of the two a server answered. */
|
|
29
|
+
export declare const isEffectiveDelta: (value: unknown) => value is EffectiveDelta;
|
|
30
|
+
/**
|
|
31
|
+
* The report a delta leads to, or null when it does not apply to `held` (a different version): ask for the
|
|
32
|
+
* whole report then.
|
|
33
|
+
*/
|
|
34
|
+
export declare function applyEffectiveDelta(held: EffectiveEvidence, delta: EffectiveDelta): EffectiveEvidence | null;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Report deltas
|
|
3
|
+
*
|
|
4
|
+
* The client half of "send only what changed": `applyEffectiveDelta` turns the report a viewer holds and a delta from the
|
|
5
|
+
* server (`GET /api/reports/evidence/effective?since=<version>`) into the current report. Pure and free of Node APIs, so a
|
|
6
|
+
* browser bundle uses it. The server half is `ReportIndex.delta`; a test holds the two together over random runs.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/server.md#reading-only-what-changed
|
|
9
|
+
*/
|
|
10
|
+
/** Is `value` a delta (`delta: true`, a `base` version, `layers` and `removed`) and not a whole report? Decides which of the two a server answered. */
|
|
11
|
+
export const isEffectiveDelta = (value) => typeof value === 'object' && value !== null && value.delta === true && typeof value.base === 'string' && Array.isArray(value.layers) && Array.isArray(value.removed);
|
|
12
|
+
/** The report order: newest first, ties by id. */
|
|
13
|
+
const byRecency = (a, b) => b.layer.recordedAtMs - a.layer.recordedAtMs || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
14
|
+
/**
|
|
15
|
+
* The report a delta leads to, or null when it does not apply to `held` (a different version): ask for the
|
|
16
|
+
* whole report then.
|
|
17
|
+
*/
|
|
18
|
+
export function applyEffectiveDelta(held, delta) {
|
|
19
|
+
if (held.version !== delta.base)
|
|
20
|
+
return null;
|
|
21
|
+
const gone = new Set(delta.removed);
|
|
22
|
+
const replaced = new Set(delta.layers.map(layer => layer.id));
|
|
23
|
+
const layers = [...held.layers.filter(layer => !gone.has(layer.id) && !replaced.has(layer.id)), ...delta.layers].sort(byRecency);
|
|
24
|
+
return { version: delta.version, layers, backends: delta.backends, notices: delta.notices, totalLayers: delta.totalLayers };
|
|
25
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Evidence of one result
|
|
3
|
+
*
|
|
4
|
+
* Reads the evidence (attachments, steps, annotations, attempts, tags, labels, links, extensions) that a layer, a
|
|
5
|
+
* part or a check carries beyond its outcome: the short summary that rides in every report row, one result's full
|
|
6
|
+
* detail, and the bundle keys its files live under. It only reads; evidence never decides a status, a slot or a
|
|
7
|
+
* winner, so nothing here affects who wins.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/extending.md
|
|
10
|
+
* @see docs/report-contract.md (evidence)
|
|
11
|
+
*/
|
|
12
|
+
import type { Annotation, Attachment, Attempt, Check, Step } from '../../schema/src/layer.js';
|
|
13
|
+
import type { EvidenceSummary } from './evidence-types.js';
|
|
14
|
+
/** Anything that may carry evidence: a layer, a part or a check. */
|
|
15
|
+
export type EvidenceHolder = {
|
|
16
|
+
readonly tags?: readonly string[];
|
|
17
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
18
|
+
readonly links?: readonly {
|
|
19
|
+
readonly url: string;
|
|
20
|
+
readonly name?: string | undefined;
|
|
21
|
+
readonly rel?: string | undefined;
|
|
22
|
+
}[];
|
|
23
|
+
readonly attachments?: readonly Attachment[];
|
|
24
|
+
readonly annotations?: readonly Annotation[];
|
|
25
|
+
readonly steps?: readonly Step[];
|
|
26
|
+
readonly attempts?: readonly Attempt[];
|
|
27
|
+
readonly extensions?: Readonly<Record<string, unknown>>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* What a result carries beyond its outcome, as counts and short facts. It rides in every row of the
|
|
31
|
+
* report so a list can show "3 attachments, 12 steps" without opening anything; the evidence itself is
|
|
32
|
+
* read when someone asks for it. Undefined when there is none.
|
|
33
|
+
*
|
|
34
|
+
* Several holders (for example a check and its layer) are summed: `attachments` also counts those on steps and
|
|
35
|
+
* attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
|
|
36
|
+
* label the later holder wins. `roles` are the attachments' roles (or, without one, what their media type or file
|
|
37
|
+
* name implies), sorted and distinct.
|
|
38
|
+
*/
|
|
39
|
+
export declare function summarizeEvidence(...holders: readonly EvidenceHolder[]): EvidenceSummary | undefined;
|
|
40
|
+
/** A role to show when a producer gave none, from the media type: `image`, `video`, `audio`, `snapshot` (text/html), `text` (text, JSON, XML) or `file`. Roles are an open vocabulary; a producer's own role always wins. */
|
|
41
|
+
export declare function roleOfType(contentType: string | undefined): string;
|
|
42
|
+
/** All of one result's evidence, read when someone opens it. */
|
|
43
|
+
export type CheckDetail = {
|
|
44
|
+
/** The id of the layer the check was read from. */
|
|
45
|
+
readonly layer: string;
|
|
46
|
+
/** The kind of the part that holds the check. */
|
|
47
|
+
readonly kind: string;
|
|
48
|
+
/** The check as recorded, with all of its evidence. */
|
|
49
|
+
readonly check: Check;
|
|
50
|
+
/** What the run as a whole carries: its logs, its links, its labels. */
|
|
51
|
+
readonly run: EvidenceHolder;
|
|
52
|
+
};
|
|
53
|
+
/** One result's evidence, from a layer: the first check with id `checkId` (in a part of kind `kind`, when given) plus the layer's own evidence. Null when the layer holds no such check. */
|
|
54
|
+
export declare function checkDetailOf(layer: {
|
|
55
|
+
readonly id: string;
|
|
56
|
+
readonly parts: readonly {
|
|
57
|
+
readonly kind: string;
|
|
58
|
+
readonly checks: readonly Check[];
|
|
59
|
+
}[];
|
|
60
|
+
} & EvidenceHolder, checkId: string, kind?: string): CheckDetail | null;
|
|
61
|
+
/** What a layer carries as a whole (its tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
62
|
+
export declare function runEvidence(layer: EvidenceHolder): EvidenceHolder;
|
|
63
|
+
/** Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none. */
|
|
64
|
+
export declare function bundleKeysOf(holder: EvidenceHolder): string[];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { contentTypeOf } from './media-types.js';
|
|
2
|
+
function count(holder) {
|
|
3
|
+
return (holder.attachments?.length ?? 0) + (holder.steps?.length ?? 0) + (holder.annotations?.length ?? 0) + (holder.attempts?.length ?? 0) + (holder.tags?.length ?? 0) + (holder.links?.length ?? 0) + Object.keys(holder.labels ?? {}).length + Object.keys(holder.extensions ?? {}).length;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* What a result carries beyond its outcome, as counts and short facts. It rides in every row of the
|
|
7
|
+
* report so a list can show "3 attachments, 12 steps" without opening anything; the evidence itself is
|
|
8
|
+
* read when someone asks for it. Undefined when there is none.
|
|
9
|
+
*
|
|
10
|
+
* Several holders (for example a check and its layer) are summed: `attachments` also counts those on steps and
|
|
11
|
+
* attempts, `steps` counts only each holder's own top-level steps, tags are de-duplicated, and for a repeated
|
|
12
|
+
* label the later holder wins. `roles` are the attachments' roles (or, without one, what their media type or file
|
|
13
|
+
* name implies), sorted and distinct.
|
|
14
|
+
*/
|
|
15
|
+
export function summarizeEvidence(...holders) {
|
|
16
|
+
if (!holders.some(holder => count(holder) > 0))
|
|
17
|
+
return undefined;
|
|
18
|
+
const roles = new Set();
|
|
19
|
+
const tags = new Set();
|
|
20
|
+
const labels = {};
|
|
21
|
+
let attachments = 0, steps = 0, annotations = 0, attempts = 0, links = 0;
|
|
22
|
+
const attachmentsOf = (list) => { for (const item of list ?? []) {
|
|
23
|
+
attachments += 1;
|
|
24
|
+
roles.add(item.role ?? roleOfType(item.contentType ?? contentTypeOf(item.name)));
|
|
25
|
+
} };
|
|
26
|
+
for (const holder of holders) {
|
|
27
|
+
attachmentsOf(holder.attachments);
|
|
28
|
+
// Attachments on steps and attempts are part of the check too.
|
|
29
|
+
for (const step of holder.steps ?? [])
|
|
30
|
+
attachmentsOf(step.attachments);
|
|
31
|
+
for (const attempt of holder.attempts ?? []) {
|
|
32
|
+
attachmentsOf(attempt.attachments);
|
|
33
|
+
for (const step of attempt.steps ?? [])
|
|
34
|
+
attachmentsOf(step.attachments);
|
|
35
|
+
}
|
|
36
|
+
steps += holder.steps?.length ?? 0;
|
|
37
|
+
annotations += holder.annotations?.length ?? 0;
|
|
38
|
+
attempts += holder.attempts?.length ?? 0;
|
|
39
|
+
links += holder.links?.length ?? 0;
|
|
40
|
+
for (const tag of holder.tags ?? [])
|
|
41
|
+
tags.add(tag);
|
|
42
|
+
Object.assign(labels, holder.labels);
|
|
43
|
+
}
|
|
44
|
+
return { attachments, steps, annotations, attempts, links, tags: [...tags], labels, roles: [...roles].sort() };
|
|
45
|
+
}
|
|
46
|
+
/** A role to show when a producer gave none, from the media type: `image`, `video`, `audio`, `snapshot` (text/html), `text` (text, JSON, XML) or `file`. Roles are an open vocabulary; a producer's own role always wins. */
|
|
47
|
+
export function roleOfType(contentType) {
|
|
48
|
+
const type = (contentType ?? '').toLowerCase();
|
|
49
|
+
if (type.startsWith('image/'))
|
|
50
|
+
return 'image';
|
|
51
|
+
if (type.startsWith('video/'))
|
|
52
|
+
return 'video';
|
|
53
|
+
if (type.startsWith('audio/'))
|
|
54
|
+
return 'audio';
|
|
55
|
+
if (type === 'text/html')
|
|
56
|
+
return 'snapshot';
|
|
57
|
+
if (type.startsWith('text/') || type.includes('json') || type.includes('xml'))
|
|
58
|
+
return 'text';
|
|
59
|
+
return 'file';
|
|
60
|
+
}
|
|
61
|
+
/** One result's evidence, from a layer: the first check with id `checkId` (in a part of kind `kind`, when given) plus the layer's own evidence. Null when the layer holds no such check. */
|
|
62
|
+
export function checkDetailOf(layer, checkId, kind) {
|
|
63
|
+
for (const part of layer.parts) {
|
|
64
|
+
if (kind !== undefined && part.kind !== kind)
|
|
65
|
+
continue;
|
|
66
|
+
const check = part.checks.find(item => item.id === checkId);
|
|
67
|
+
if (check)
|
|
68
|
+
return { layer: layer.id, kind: part.kind, check, run: runEvidence(layer) };
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
/** What a layer carries as a whole (its tags, labels, links, attachments, annotations and extensions; not its parts' checks). Absent fields stay absent. */
|
|
73
|
+
export function runEvidence(layer) {
|
|
74
|
+
const { tags, labels, links, attachments, annotations, extensions } = layer;
|
|
75
|
+
return { ...(tags ? { tags } : {}), ...(labels ? { labels } : {}), ...(links ? { links } : {}), ...(attachments ? { attachments } : {}), ...(annotations ? { annotations } : {}), ...(extensions ? { extensions } : {}) };
|
|
76
|
+
}
|
|
77
|
+
/** Every distinct bundle key (`bundleKey`) that the attachments of a holder, its steps and its attempts point to, so a layer's files can be copied. Attachments held by `uri` or inline `text` have none. */
|
|
78
|
+
export function bundleKeysOf(holder) {
|
|
79
|
+
const keys = new Set();
|
|
80
|
+
const take = (list) => { for (const item of list ?? [])
|
|
81
|
+
if (item.bundleKey)
|
|
82
|
+
keys.add(item.bundleKey); };
|
|
83
|
+
take(holder.attachments);
|
|
84
|
+
for (const step of holder.steps ?? [])
|
|
85
|
+
take(step.attachments);
|
|
86
|
+
for (const attempt of holder.attempts ?? []) {
|
|
87
|
+
take(attempt.attachments);
|
|
88
|
+
for (const step of attempt.steps ?? [])
|
|
89
|
+
take(step.attachments);
|
|
90
|
+
}
|
|
91
|
+
return [...keys];
|
|
92
|
+
}
|
|
@@ -1,35 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Live evidence from running suites
|
|
3
|
+
*
|
|
4
|
+
* Shows results the moment each test finishes, before its package has been collected into an archived layer. A
|
|
5
|
+
* running suite announces itself with a record under `active/` in the local store (see {@link LiveRunRecord});
|
|
6
|
+
* this module reads the test reports that suite has written so far into a temporary layer (id `live-<source>`,
|
|
7
|
+
* `live: true`) and drops it once the archived layer arrives or after a short linger. Live layers are a viewing
|
|
8
|
+
* aid only (this module writes nothing), and an unreadable or half-written report is skipped, not guessed at.
|
|
9
|
+
*/
|
|
1
10
|
import type { Clock } from './clock.js';
|
|
2
11
|
import type { ActiveRun, LayerEvidence, LayerHeader } from './evidence-types.js';
|
|
3
12
|
import type { LayerParameters } from './layer-types.js';
|
|
4
13
|
import type { ObjectStore } from './object-store.js';
|
|
5
14
|
/** What a running spec suite announces under `active/` in the local store. */
|
|
6
15
|
export type LiveRunRecord = {
|
|
16
|
+
/** The package directory the suite runs in, relative to the repository (`libs/chess`); with `bucket`, it names the source. */
|
|
7
17
|
readonly packageDir: string;
|
|
18
|
+
/** The suite's bucket within the package (`part-2`); where the run writes each finished test. */
|
|
8
19
|
readonly bucket: string;
|
|
20
|
+
/** When the run started, epoch milliseconds. A new value for the same source means a new run: tracking restarts. */
|
|
9
21
|
readonly startedAtMs: number;
|
|
22
|
+
/** How many checks the run expects to execute, when known. */
|
|
10
23
|
readonly plannedChecks?: number;
|
|
11
24
|
/** The client this suite is checking. A different client does not replace it. */
|
|
12
25
|
readonly environment?: string;
|
|
26
|
+
/** Other string choices that make the results their own slot; non-string or empty values are dropped on reading. */
|
|
13
27
|
readonly parameters?: LayerParameters;
|
|
14
28
|
};
|
|
15
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Validates a parsed `active/` announcement and keeps only the fields the live layer can trust (the environment is
|
|
31
|
+
* normalised to its slug, parameters are string-to-string). Null when `packageDir`, `bucket` or a numeric
|
|
32
|
+
* `startedAtMs` is missing, so garbage in `active/` is ignored rather than shown.
|
|
33
|
+
*/
|
|
16
34
|
export declare function liveRunRecord(value: unknown): LiveRunRecord | null;
|
|
17
35
|
/** How the host reaches the results a spec run has written so far. */
|
|
18
36
|
export type LiveSource = {
|
|
19
|
-
/** The bucket the run writes each finished test into, or null when it cannot be read. */
|
|
37
|
+
/** The bucket the run writes each finished test into, or null when it cannot be read (the run is then not tracked). */
|
|
20
38
|
readonly bucket: (run: LiveRunRecord) => ObjectStore | null;
|
|
21
|
-
/** The checkout the runs execute. */
|
|
39
|
+
/** The checkout the runs execute: its commit and branch, stamped on every live layer. Asked once per refresh pass. */
|
|
22
40
|
readonly origin?: () => Promise<{
|
|
23
41
|
readonly sha: string | null;
|
|
24
42
|
readonly branch: string | null;
|
|
25
43
|
}>;
|
|
26
|
-
/** The stylesheet the runs' snapshots render with. */
|
|
44
|
+
/** The stylesheet the runs' snapshots render with (served as `frame.css`). */
|
|
27
45
|
readonly frameCss?: () => Promise<string | null>;
|
|
28
46
|
};
|
|
47
|
+
/** The id of the temporary layer for a source: `live-<source>`. Not a content hash, unlike an archived layer's id. */
|
|
29
48
|
export declare function liveLayerId(source: string): string;
|
|
30
49
|
/**
|
|
31
|
-
*
|
|
50
|
+
* Creates the tracker of finished tests of runs that have not been collected into a layer yet, so a
|
|
32
51
|
* result appears the moment its test ends instead of when its package does.
|
|
52
|
+
*
|
|
53
|
+
* The tracker is stateful and meant to live as long as the server: call `refresh` on each poll with the current
|
|
54
|
+
* `active/` records and the newest archived layers. It returns the live layers (only those with at least one result)
|
|
55
|
+
* and the runs still in progress with their completed and failed counts. A run that disappears is read one last
|
|
56
|
+
* time and kept for 30 seconds, or until an archived (non-live) layer of the same producer and environment recorded
|
|
57
|
+
* at or after its start appears. `find(id)` returns a live layer by `live-<source>` id, and `store(source)` the
|
|
58
|
+
* bucket its files are read from. A bucket that is being replaced or a report still being written is skipped until
|
|
59
|
+
* the next pass; `refresh` does not throw for those.
|
|
60
|
+
*
|
|
61
|
+
* @param args.live How the host reaches the buckets; without it nothing is tracked.
|
|
62
|
+
* @param args.backend The backend id stamped on live layers.
|
|
33
63
|
*/
|
|
34
64
|
export declare function createLiveEvidence(args: {
|
|
35
65
|
readonly live: LiveSource | undefined;
|
|
@@ -17,7 +17,11 @@ function readParameters(value) {
|
|
|
17
17
|
parameters[key] = item;
|
|
18
18
|
return parameters;
|
|
19
19
|
}
|
|
20
|
-
/**
|
|
20
|
+
/**
|
|
21
|
+
* Validates a parsed `active/` announcement and keeps only the fields the live layer can trust (the environment is
|
|
22
|
+
* normalised to its slug, parameters are string-to-string). Null when `packageDir`, `bucket` or a numeric
|
|
23
|
+
* `startedAtMs` is missing, so garbage in `active/` is ignored rather than shown.
|
|
24
|
+
*/
|
|
21
25
|
export function liveRunRecord(value) {
|
|
22
26
|
if (!value || typeof value !== 'object')
|
|
23
27
|
return null;
|
|
@@ -38,6 +42,7 @@ export function liveRunRecord(value) {
|
|
|
38
42
|
/** A finished run stays visible this long while its layer is being collected. */
|
|
39
43
|
const LINGER_MS = 30_000;
|
|
40
44
|
const REPORT = '/report.json';
|
|
45
|
+
/** The id of the temporary layer for a source: `live-<source>`. Not a content hash, unlike an archived layer's id. */
|
|
41
46
|
export function liveLayerId(source) {
|
|
42
47
|
return `live-${source}`;
|
|
43
48
|
}
|
|
@@ -50,8 +55,19 @@ function sourceOf(run) {
|
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
/**
|
|
53
|
-
*
|
|
58
|
+
* Creates the tracker of finished tests of runs that have not been collected into a layer yet, so a
|
|
54
59
|
* result appears the moment its test ends instead of when its package does.
|
|
60
|
+
*
|
|
61
|
+
* The tracker is stateful and meant to live as long as the server: call `refresh` on each poll with the current
|
|
62
|
+
* `active/` records and the newest archived layers. It returns the live layers (only those with at least one result)
|
|
63
|
+
* and the runs still in progress with their completed and failed counts. A run that disappears is read one last
|
|
64
|
+
* time and kept for 30 seconds, or until an archived (non-live) layer of the same producer and environment recorded
|
|
65
|
+
* at or after its start appears. `find(id)` returns a live layer by `live-<source>` id, and `store(source)` the
|
|
66
|
+
* bucket its files are read from. A bucket that is being replaced or a report still being written is skipped until
|
|
67
|
+
* the next pass; `refresh` does not throw for those.
|
|
68
|
+
*
|
|
69
|
+
* @param args.live How the host reaches the buckets; without it nothing is tracked.
|
|
70
|
+
* @param args.backend The backend id stamped on live layers.
|
|
55
71
|
*/
|
|
56
72
|
export function createLiveEvidence(args) {
|
|
57
73
|
const tracked = new Map();
|