@crvouga/atlas 0.3.0 → 0.5.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 +12 -0
- package/README.md +178 -604
- 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 +30 -1
- package/dist/core/src/config.d.ts +70 -4
- package/dist/core/src/config.js +62 -3
- 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 +69 -16
- package/dist/core/src/run/execute.js +67 -16
- 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 +83 -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 +40 -4
- package/dist/reports/src/evidence-live.js +52 -4
- package/dist/reports/src/evidence-types.d.ts +70 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +58 -6
- package/dist/reports/src/evidence-view.js +44 -5
- 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 +52 -0
- package/dist/reports/src/http-store.js +145 -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 +364 -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 +48 -0
- package/dist/reports/src/layer-axes.js +71 -0
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +19 -2
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +154 -33
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +14 -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 +34 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +88 -12
- 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 +30 -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 +44 -9
- package/dist/reports/src/publish.js +40 -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 +485 -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 +40 -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/schema/src/api.d.ts +799 -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 +14 -0
- package/dist/schema/src/index.js +14 -0
- package/dist/schema/src/layer.d.ts +2273 -0
- package/dist/schema/src/layer.js +462 -0
- package/dist/schema/src/manifest.d.ts +95 -0
- package/dist/schema/src/manifest.js +84 -0
- package/dist/schema/src/report-sources.d.ts +28 -0
- package/dist/schema/src/report-sources.js +25 -0
- package/dist/schema/src/runs-index.d.ts +16 -0
- package/dist/schema/src/runs-index.js +15 -0
- package/dist/schema/src/server-config.d.ts +93 -0
- package/dist/schema/src/server-config.js +73 -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 +62 -0
- package/dist/server/src/app.js +419 -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 +187 -0
- package/dist/server/src/dev.d.ts +12 -0
- package/dist/server/src/dev.js +80 -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 +16 -0
- package/dist/server/src/index.js +16 -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 +112 -0
- package/dist/server/src/openapi.d.ts +59 -0
- package/dist/server/src/openapi.js +374 -0
- package/dist/server/src/problem.d.ts +8 -0
- package/dist/server/src/problem.js +39 -0
- package/dist/server/src/site.d.ts +13 -0
- package/dist/server/src/site.js +43 -0
- package/dist/server/src/store-api.d.ts +28 -0
- package/dist/server/src/store-api.js +159 -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 +146 -0
- package/docs/reference/core.md +2426 -0
- package/docs/reference/detox.md +150 -0
- package/docs/reference/http-api.md +303 -0
- package/docs/reference/playwright.md +303 -0
- package/docs/reference/reports.md +7098 -0
- package/docs/reference/schema.md +1651 -0
- package/docs/reference/server.md +685 -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/server.md +285 -0
- package/docs/statecharts.md +641 -0
- package/llms-full.txt +3506 -0
- package/llms.txt +47 -0
- package/package.json +33 -5
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +69 -2
- package/packages/reports/STORAGE.md +5 -1
- package/packages/schema/README.md +14 -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 +22 -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/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/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 +194 -0
- package/packages/schema/schemas/server-info.schema.json +114 -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 +6699 -0
- package/packages/visualizer/README.md +27 -5
- package/packages/visualizer/example/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/example/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/example/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/example/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/example/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/failures/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/full/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/malformed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/partial/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/removed/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/cart.qnt +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/checkout.openslo.yaml +10 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/drink-price.dmn +24 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/loyalty.feature +7 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/ordering.feature +36 -0
- package/packages/visualizer/fixtures/spec-only/specs/coffee/ordering/payment.als +4 -0
- package/packages/visualizer/scripts/make-fixtures.ts +2 -0
- package/packages/visualizer/scripts/publish-data.ts +1 -2
- package/packages/visualizer/src/app/AppShell.tsx +9 -2
- package/packages/visualizer/src/app/report-sources.tsx +6 -4
- package/packages/visualizer/src/app/router.tsx +3 -3
- package/packages/visualizer/src/components/Header.module.css +35 -0
- package/packages/visualizer/src/components/Header.tsx +41 -4
- package/packages/visualizer/src/data/adapters/adapter.ts +5 -17
- package/packages/visualizer/src/data/adapters/index.ts +10 -1
- package/packages/visualizer/src/evidence/api.ts +14 -2
- 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 +38 -4
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- package/packages/visualizer/src/evidence/evidence.module.css +20 -0
- package/packages/visualizer/src/evidence/marks.tsx +2 -0
- package/packages/visualizer/src/evidence/overview.tsx +3 -3
- package/packages/visualizer/src/evidence/sources-panel.tsx +88 -34
- package/packages/visualizer/src/evidence/spec-page.tsx +23 -16
- package/packages/visualizer/src/find/Finder.tsx +1 -7
- package/packages/visualizer/src/find/open.ts +4 -0
- package/packages/visualizer/src/live/live-stream.ts +204 -0
- package/packages/visualizer/src/runtime.ts +19 -0
- package/packages/visualizer/src/state/ui-store.ts +6 -2
- package/packages/visualizer/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite/example-reports.ts +475 -0
- package/packages/visualizer/vite.config.ts +18 -2
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { LayerHeader, LayerRow } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import type { CheckDetail, EvidenceHolder } from '@crvouga/atlas/reports/evidence-detail';
|
|
3
|
+
import { roleOfType } from '@crvouga/atlas/reports/evidence-detail';
|
|
4
|
+
import { contentTypeOf } from '@crvouga/atlas/reports/media-types';
|
|
5
|
+
import type { Annotation, Attachment, Attempt, Step } from '@crvouga/atlas/schema';
|
|
6
|
+
import { useQuery } from '@tanstack/react-query';
|
|
7
|
+
import { useMemo, type ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { reportAssetUrl, reportJson, reportText } from './api';
|
|
10
|
+
import styles from './artifacts.module.css';
|
|
11
|
+
import { duration, plural } from './format';
|
|
12
|
+
import { StatusMark } from './marks';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* How one kind of attachment is shown. The first renderer that matches wins, so a new kind of artifact (a
|
|
16
|
+
* flame graph, a HAR, a coverage map) is one more entry, and an attachment nobody has a renderer for is still
|
|
17
|
+
* listed, with its name, what it is and a way to download it. Nothing here fails on what it does not know.
|
|
18
|
+
*/
|
|
19
|
+
export type ArtifactRenderer = {
|
|
20
|
+
readonly id: string;
|
|
21
|
+
readonly matches: (attachment: Attachment, type: string) => boolean;
|
|
22
|
+
/** `url` is where the bytes are, if they are anywhere a browser can reach; `path` is the report API path of a stored file. */
|
|
23
|
+
readonly render: (attachment: Attachment, context: { url: string | null; path: string | null; type: string }) => ReactNode;
|
|
24
|
+
/** Wide renderers take a whole row of the gallery. */
|
|
25
|
+
readonly wide?: boolean;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const mediaType = (attachment: Attachment) => (attachment.contentType ?? contentTypeOf(attachment.name)).split(';')[0]!.trim().toLowerCase();
|
|
29
|
+
const label = (attachment: Attachment) => attachment.description ?? attachment.name;
|
|
30
|
+
|
|
31
|
+
/** Only addresses a browser may follow: never `javascript:` or `data:` from a producer. */
|
|
32
|
+
function safeUri(uri: string | undefined): string | null {
|
|
33
|
+
if (!uri) return null;
|
|
34
|
+
try {
|
|
35
|
+
const url = new URL(uri, 'https://atlas.invalid/');
|
|
36
|
+
return url.protocol === 'http:' || url.protocol === 'https:' ? uri : null;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function TextValue({ attachment, path }: { attachment: Attachment; path: string | null }) {
|
|
43
|
+
const stored = useQuery({ queryKey: ['artifact-text', path ?? ''], queryFn: ({ signal }) => reportText(path!, signal), enabled: attachment.text === undefined && path !== null, staleTime: Number.POSITIVE_INFINITY, retry: false });
|
|
44
|
+
const raw = attachment.text ?? stored.data;
|
|
45
|
+
if (raw === undefined) return <p className={styles.where}>{stored.isError ? 'Could not be read.' : 'Loading…'}</p>;
|
|
46
|
+
const shown = raw.length > 200_000 ? raw.slice(0, 200_000) : raw;
|
|
47
|
+
const pretty = /json/.test(mediaType(attachment)) ? tryPretty(shown) : shown;
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<pre className={styles.text}>{pretty}</pre>
|
|
51
|
+
{shown.length < raw.length && <p className={styles.where}>Showing the first 200 000 characters.</p>}
|
|
52
|
+
</>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryPretty(text: string) {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.stringify(JSON.parse(text), null, 2);
|
|
59
|
+
} catch {
|
|
60
|
+
return text;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** In order: the first that matches. Add to the front with {@link registerArtifactRenderer}. */
|
|
65
|
+
export const artifactRenderers: ArtifactRenderer[] = [
|
|
66
|
+
{ id: 'image', matches: (_, type) => type.startsWith('image/') && type !== 'image/svg+xml', render: (a, { url }) => (url ? <a href={url} target="_blank" rel="noreferrer"><img src={url} alt={label(a)} loading="lazy" width={a.width} height={a.height} /></a> : null) },
|
|
67
|
+
// SVG can carry script: shown as a picture only, never inlined.
|
|
68
|
+
{ id: 'svg', matches: (_, type) => type === 'image/svg+xml', render: (a, { url }) => (url ? <img src={url} alt={label(a)} loading="lazy" /> : null) },
|
|
69
|
+
{ id: 'video', wide: true, matches: (_, type) => type.startsWith('video/'), render: (a, { url }) => (url ? <video src={url} controls preload="metadata" aria-label={label(a)} /> : null) },
|
|
70
|
+
{ id: 'audio', matches: (_, type) => type.startsWith('audio/'), render: (a, { url }) => (url ? <audio src={url} controls preload="none" aria-label={label(a)} /> : null) },
|
|
71
|
+
// A page snapshot runs nothing: the frame has no permissions and the file is served sandboxed.
|
|
72
|
+
{ id: 'snapshot', wide: true, matches: (_, type) => type === 'text/html' || type === 'application/xhtml+xml', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
|
|
73
|
+
{ id: 'pdf', wide: true, matches: (_, type) => type === 'application/pdf', render: (a, { url }) => (url ? <iframe sandbox="" src={url} title={label(a)} loading="lazy" /> : null) },
|
|
74
|
+
{ id: 'text', wide: true, matches: (a, type) => a.text !== undefined || type.startsWith('text/') || /(json|xml|yaml|x-ndjson|x-diff)/.test(type), render: (a, { path }) => <TextValue attachment={a} path={path} /> }
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
export function registerArtifactRenderer(renderer: ArtifactRenderer) {
|
|
78
|
+
artifactRenderers.unshift(renderer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function sizeOf(bytes: number | undefined) {
|
|
82
|
+
if (bytes === undefined) return '';
|
|
83
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
84
|
+
return bytes < 1024 * 1024 ? `${(bytes / 1024).toFixed(0)} KiB` : `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function Artifact({ attachment, layer }: { attachment: Attachment; layer: string }) {
|
|
88
|
+
const type = mediaType(attachment);
|
|
89
|
+
const path = attachment.bundleKey ? `/layers/${encodeURIComponent(layer)}/blobs/${attachment.bundleKey.split('/').map(encodeURIComponent).join('/')}` : null;
|
|
90
|
+
const url = path ? reportAssetUrl(path) : safeUri(attachment.uri);
|
|
91
|
+
const renderer = artifactRenderers.find((candidate) => candidate.matches(attachment, type));
|
|
92
|
+
const body = renderer?.render(attachment, { url, path, type });
|
|
93
|
+
return (
|
|
94
|
+
<li className={styles.figure} data-wide={renderer?.wide || undefined} data-role={attachment.role ?? roleOfType(type)}>
|
|
95
|
+
{body}
|
|
96
|
+
<figcaption>
|
|
97
|
+
<strong>{attachment.name}</strong>
|
|
98
|
+
{attachment.role && <span className={styles.role}>{attachment.role}</span>}
|
|
99
|
+
<span>{[type, sizeOf(attachment.bytes), attachment.durationMs ? duration(attachment.durationMs) : ''].filter(Boolean).join(' · ')}</span>
|
|
100
|
+
{url && <a href={url} download={attachment.name} target="_blank" rel="noreferrer">{body ? 'Download' : 'Open'}</a>}
|
|
101
|
+
{!url && attachment.text === undefined && <span>No file to show</span>}
|
|
102
|
+
</figcaption>
|
|
103
|
+
</li>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function Attachments({ attachments, layer }: { attachments: readonly Attachment[]; layer: string }) {
|
|
108
|
+
if (!attachments.length) return null;
|
|
109
|
+
return (
|
|
110
|
+
<section aria-label="Attachments">
|
|
111
|
+
<h4>{plural(attachments.length, 'attachment', 'attachments')}</h4>
|
|
112
|
+
<ul className={styles.gallery}>
|
|
113
|
+
{attachments.map((attachment, index) => <Artifact key={`${attachment.id ?? attachment.name}:${index}`} attachment={attachment} layer={layer} />)}
|
|
114
|
+
</ul>
|
|
115
|
+
</section>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const STEP_STATUS: Record<string, 'passed' | 'failed' | 'running' | 'not-run'> = { passed: 'passed', pass: 'passed', ok: 'passed', failed: 'failed', fail: 'failed', error: 'failed', running: 'running', skipped: 'not-run', 'not-run': 'not-run', 'not-reached': 'not-run', pending: 'not-run' };
|
|
120
|
+
|
|
121
|
+
/** The timeline: ordered by when each step started, nested by `parentId`. A step whose parent is missing is shown at the top. */
|
|
122
|
+
export function Timeline({ steps, layer }: { steps: readonly Step[]; layer: string }) {
|
|
123
|
+
const tree = useMemo(() => {
|
|
124
|
+
const ids = new Set(steps.flatMap((step) => (step.id ? [step.id] : [])));
|
|
125
|
+
const children = new Map<string | null, Step[]>();
|
|
126
|
+
for (const step of steps) {
|
|
127
|
+
const parent = step.parentId && ids.has(step.parentId) ? step.parentId : null;
|
|
128
|
+
children.set(parent, [...(children.get(parent) ?? []), step]);
|
|
129
|
+
}
|
|
130
|
+
for (const list of children.values()) list.sort((a, b) => (a.offsetMs ?? 0) - (b.offsetMs ?? 0));
|
|
131
|
+
return children;
|
|
132
|
+
}, [steps]);
|
|
133
|
+
const render = (parent: string | null): ReactNode => (
|
|
134
|
+
<ol>
|
|
135
|
+
{(tree.get(parent) ?? []).map((step, index) => {
|
|
136
|
+
const status = STEP_STATUS[(step.status ?? '').toLowerCase()];
|
|
137
|
+
return (
|
|
138
|
+
<li key={`${step.id ?? step.name}:${index}`} className={styles.step} data-status={status ?? step.status ?? 'none'}>
|
|
139
|
+
<div className={styles.stepRow}>
|
|
140
|
+
<StatusMark status={status ?? 'none'} size={14} />
|
|
141
|
+
<span className={styles.stepName}>{step.name}</span>
|
|
142
|
+
{step.kind && <span className={styles.stepKind}>{step.kind}</span>}
|
|
143
|
+
{step.status && !status && <span className={styles.stepKind}>{step.status}</span>}
|
|
144
|
+
<span className={styles.spacer} />
|
|
145
|
+
<span className={styles.stepTime}>{[step.offsetMs !== undefined ? `+${duration(step.offsetMs)}` : '', duration(step.durationMs)].filter(Boolean).join(' · ')}</span>
|
|
146
|
+
</div>
|
|
147
|
+
{step.detail && <p className={styles.stepDetail}>{step.detail}</p>}
|
|
148
|
+
{step.annotations && <Notes notes={step.annotations} />}
|
|
149
|
+
{step.attachments && <Attachments attachments={step.attachments} layer={layer} />}
|
|
150
|
+
{step.id && tree.has(step.id) && render(step.id)}
|
|
151
|
+
</li>
|
|
152
|
+
);
|
|
153
|
+
})}
|
|
154
|
+
</ol>
|
|
155
|
+
);
|
|
156
|
+
return (
|
|
157
|
+
<section aria-label="Timeline" className={styles.timeline}>
|
|
158
|
+
<h4>{plural(steps.length, 'step', 'steps')}</h4>
|
|
159
|
+
{render(null)}
|
|
160
|
+
</section>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function Notes({ notes }: { notes: readonly Annotation[] }) {
|
|
165
|
+
if (!notes.length) return null;
|
|
166
|
+
return (
|
|
167
|
+
<ul className={styles.notes} aria-label="Annotations">
|
|
168
|
+
{notes.map((note, index) => (
|
|
169
|
+
<li key={index} className={styles.note}>
|
|
170
|
+
<span className={styles.level} data-level={note.level ?? 'info'}>{note.level && ['info', 'warning', 'error'].includes(note.level) ? note.level : note.type}</span>
|
|
171
|
+
<span>
|
|
172
|
+
{note.message}
|
|
173
|
+
{note.source?.file && <span className={styles.where}> {note.source.file}{note.source.line ? `:${note.source.line}` : ''}</span>}
|
|
174
|
+
{note.level && !['info', 'warning', 'error'].includes(note.level) && <span className={styles.where}> ({note.level})</span>}
|
|
175
|
+
</span>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
178
|
+
</ul>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function Tries({ attempts, layer }: { attempts: readonly Attempt[]; layer: string }) {
|
|
183
|
+
if (!attempts.length) return null;
|
|
184
|
+
return (
|
|
185
|
+
<section aria-label="Tries">
|
|
186
|
+
<h4>{plural(attempts.length, 'try', 'tries')}</h4>
|
|
187
|
+
<ol className={styles.timeline}>
|
|
188
|
+
{attempts.map((attempt) => (
|
|
189
|
+
<li key={attempt.index} className={styles.step}>
|
|
190
|
+
<div className={styles.stepRow}>
|
|
191
|
+
<StatusMark status={attempt.status} size={14} />
|
|
192
|
+
<span className={styles.stepName}>Try {attempt.index + 1}</span>
|
|
193
|
+
<span className={styles.spacer} />
|
|
194
|
+
<span className={styles.stepTime}>{duration(attempt.durationMs)}</span>
|
|
195
|
+
</div>
|
|
196
|
+
{attempt.detail && <p className={styles.stepDetail}>{attempt.detail}</p>}
|
|
197
|
+
{attempt.attachments && <Attachments attachments={attempt.attachments} layer={layer} />}
|
|
198
|
+
</li>
|
|
199
|
+
))}
|
|
200
|
+
</ol>
|
|
201
|
+
</section>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** What is not known: kept, and shown as it is, so nothing a tool recorded is out of reach. */
|
|
206
|
+
function Raw({ extensions }: { extensions: Readonly<Record<string, unknown>> }) {
|
|
207
|
+
const names = Object.keys(extensions);
|
|
208
|
+
if (!names.length) return null;
|
|
209
|
+
return (
|
|
210
|
+
<details className={styles.raw}>
|
|
211
|
+
<summary>{plural(names.length, 'extension', 'extensions')}: {names.join(', ')}</summary>
|
|
212
|
+
<pre className={styles.text}>{JSON.stringify(extensions, null, 2)}</pre>
|
|
213
|
+
</details>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Facts({ holder }: { holder: EvidenceHolder }) {
|
|
218
|
+
const labels = Object.entries(holder.labels ?? {});
|
|
219
|
+
const tags = holder.tags ?? [];
|
|
220
|
+
const links = (holder.links ?? []).flatMap((link) => { const url = safeUri(link.url); return url ? [{ ...link, url }] : []; });
|
|
221
|
+
if (!labels.length && !tags.length && !links.length) return null;
|
|
222
|
+
return (
|
|
223
|
+
<div>
|
|
224
|
+
<ul className={styles.chips} aria-label="Facts">
|
|
225
|
+
{tags.map((tag) => <li key={`t:${tag}`} className={styles.chip}>{tag}</li>)}
|
|
226
|
+
{labels.map(([name, value]) => <li key={`l:${name}`} className={styles.chip}>{name}<b>{value}</b></li>)}
|
|
227
|
+
{links.map((link) => <li key={`k:${link.url}`} className={styles.chip}><a href={link.url} target="_blank" rel="noreferrer">{link.name ?? link.rel ?? link.url}</a></li>)}
|
|
228
|
+
</ul>
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function Evidence({ holder, layer, steps }: { holder: EvidenceHolder; layer: string; steps?: boolean }) {
|
|
234
|
+
return (
|
|
235
|
+
<>
|
|
236
|
+
<Facts holder={holder} />
|
|
237
|
+
{holder.annotations && <Notes notes={holder.annotations} />}
|
|
238
|
+
{steps && holder.steps && holder.steps.length > 0 && <Timeline steps={holder.steps} layer={layer} />}
|
|
239
|
+
{holder.attempts && holder.attempts.length > 1 && <Tries attempts={holder.attempts} layer={layer} />}
|
|
240
|
+
{holder.attachments && <Attachments attachments={holder.attachments} layer={layer} />}
|
|
241
|
+
{holder.extensions && <Raw extensions={holder.extensions} />}
|
|
242
|
+
</>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** Everything one result carries beyond its outcome, read when it is opened, and what the run around it carried. */
|
|
247
|
+
export function ResultEvidence({ layer, row }: { layer: LayerHeader; row: LayerRow }) {
|
|
248
|
+
const wanted = row.evidence !== undefined || layer.evidence !== undefined;
|
|
249
|
+
const detail = useQuery({
|
|
250
|
+
queryKey: ['evidence-detail', layer.backend, layer.id, row.kind, row.id],
|
|
251
|
+
queryFn: ({ signal }) => reportJson<CheckDetail>(`/evidence/detail/${encodeURIComponent(layer.backend)}/${encodeURIComponent(layer.id)}?check=${encodeURIComponent(row.id)}&kind=${encodeURIComponent(row.kind)}`, signal),
|
|
252
|
+
enabled: wanted && /^[0-9a-f]{64}$/.test(layer.id),
|
|
253
|
+
staleTime: Number.POSITIVE_INFINITY,
|
|
254
|
+
retry: false
|
|
255
|
+
});
|
|
256
|
+
if (!wanted) return null;
|
|
257
|
+
if (detail.isLoading) return <p className={styles.where} role="status">Loading what this result carries…</p>;
|
|
258
|
+
if (detail.isError || !detail.data) return <p className={styles.where} role="alert">What this result carries could not be read.</p>;
|
|
259
|
+
const { check, run } = detail.data;
|
|
260
|
+
const hasRun = Object.values(run).some((value) => (Array.isArray(value) ? value.length > 0 : value && typeof value === 'object' ? Object.keys(value).length > 0 : false));
|
|
261
|
+
return (
|
|
262
|
+
<div className={styles.panel} aria-label="Evidence">
|
|
263
|
+
<Evidence holder={check} layer={layer.id} steps />
|
|
264
|
+
{hasRun && (
|
|
265
|
+
<section aria-label="From the run">
|
|
266
|
+
<h4>From the run</h4>
|
|
267
|
+
<Evidence holder={run} layer={layer.id} />
|
|
268
|
+
</section>
|
|
269
|
+
)}
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { recordingPaths } from '@crvouga/atlas/reports/evidence-types';
|
|
2
2
|
import { checkHistory, type CheckSlot, type HistoryEntry } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
4
|
+
import type { EvidenceResult } from '@crvouga/atlas/reports/layer-merge';
|
|
3
5
|
import { flattenFrames } from '@crvouga/atlas/reports/frames';
|
|
4
6
|
import type { ReportStep, RunReport } from '@crvouga/atlas/reports/report-types';
|
|
5
7
|
import { useQuery } from '@tanstack/react-query';
|
|
6
8
|
import { useMemo, useState, type ReactNode } from 'react';
|
|
7
9
|
|
|
8
10
|
import { reportJson } from './api';
|
|
11
|
+
import { ResultEvidence } from './artifacts';
|
|
9
12
|
import { useEvidence } from './evidence-context';
|
|
10
13
|
import styles from './evidence.module.css';
|
|
11
14
|
import { ago, duration, plural, statusWord, useNow } from './format';
|
|
@@ -34,18 +37,48 @@ export function useRecordedReport(entry: HistoryEntry | null, enabled = true) {
|
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
/** One check in a list: its outcome at a glance, and everything recorded for it when opened. */
|
|
40
|
+
const ENV_RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 } as const;
|
|
41
|
+
|
|
42
|
+
/** One mark per client when the same check was recorded on more than one. */
|
|
43
|
+
function environmentMarks(slot: CheckSlot) {
|
|
44
|
+
const seen = new Map<string, CheckSlot['status']>();
|
|
45
|
+
for (const outcome of slot.outcomes) {
|
|
46
|
+
const id = outcome.layer.environment ?? '';
|
|
47
|
+
const prior = seen.get(id);
|
|
48
|
+
if (prior === undefined || ENV_RANK[outcome.row.status] > ENV_RANK[prior]) seen.set(id, outcome.row.status);
|
|
49
|
+
}
|
|
50
|
+
if (seen.size < 2) return [];
|
|
51
|
+
return [...seen].map(([id, status]) => ({ id, status }));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function outcomeName(outcome: EvidenceResult) {
|
|
55
|
+
const environment = outcome.layer.environment ? environmentLabel(outcome.layer.environment) : '';
|
|
56
|
+
const parameters = Object.entries(outcome.parameters).map(([name, value]) => `${name}: ${value}`).join(', ');
|
|
57
|
+
return [environment, parameters].filter(Boolean).join(' · ') || outcome.layer.producer;
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
export function ResultRow({ label, slot, open, onToggle, children }: { label: string; slot: CheckSlot | null; open: boolean; onToggle: () => void; children: ReactNode }) {
|
|
38
61
|
const now = useNow();
|
|
39
62
|
const newest = slot?.outcomes[0];
|
|
63
|
+
const marks = slot ? environmentMarks(slot) : [];
|
|
40
64
|
const failure = slot?.status === 'failed' ? slot.outcomes.find((outcome) => outcome.row.status === 'failed')?.row.detail.split('\n')[0] : '';
|
|
41
65
|
return (
|
|
42
66
|
<li className={styles.result} data-status={slot?.status ?? 'none'} data-open={open || undefined}>
|
|
43
67
|
<button type="button" className={styles.resultHead} aria-expanded={open} onClick={onToggle}>
|
|
44
68
|
<StatusMark status={slot?.status ?? 'none'} />
|
|
45
|
-
<span className={styles.resultName}>
|
|
69
|
+
<span className={styles.resultName}>
|
|
70
|
+
{label}
|
|
71
|
+
{marks.length > 0 && (
|
|
72
|
+
<span className={styles.envMarks}>
|
|
73
|
+
{marks.map((mark) => (
|
|
74
|
+
<span key={mark.id || 'unspecified'} data-status={mark.status}>{environmentLabel(mark.id || null)}</span>
|
|
75
|
+
))}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
</span>
|
|
46
79
|
{slot?.live && <span className={styles.liveTag}>Running now</span>}
|
|
47
80
|
<span className={styles.resultAside}>
|
|
48
|
-
{newest ? [duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
81
|
+
{newest ? [newest.row.evidence?.attachments ? plural(newest.row.evidence.attachments, 'attachment', 'attachments') : '', duration(newest.row.durationMs), slot.live ? '' : ago(slot.latestAtMs, now)].filter(Boolean).join(' · ') : 'No result'}
|
|
49
82
|
</span>
|
|
50
83
|
<span className={styles.chevron} aria-hidden="true" />
|
|
51
84
|
</button>
|
|
@@ -185,11 +218,11 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
|
185
218
|
return (
|
|
186
219
|
<div className={styles.detail}>
|
|
187
220
|
{slot.outcomes.length > 1 && !earlier && (
|
|
188
|
-
<div className={styles.variants} role="group" aria-label="
|
|
221
|
+
<div className={styles.variants} role="group" aria-label="Environments">
|
|
189
222
|
{slot.outcomes.map((outcome, index) => (
|
|
190
223
|
<button key={outcome.key} type="button" aria-pressed={index === variant} onClick={() => setVariant(index)}>
|
|
191
224
|
<StatusMark status={outcome.row.status} size={12} />
|
|
192
|
-
{
|
|
225
|
+
{outcomeName(outcome)}
|
|
193
226
|
</button>
|
|
194
227
|
))}
|
|
195
228
|
</div>
|
|
@@ -221,6 +254,7 @@ export function CheckDetail({ slot, layerId, select, reads, written }: Props) {
|
|
|
221
254
|
</p>
|
|
222
255
|
);
|
|
223
256
|
}))}
|
|
257
|
+
<ResultEvidence layer={chosen.layer} row={chosen.row} />
|
|
224
258
|
{(moments > 0 || chosen.layer.runUrl) && (
|
|
225
259
|
<p className={styles.actionsLine}>
|
|
226
260
|
{moments > 0 && (
|
|
@@ -1,20 +1,29 @@
|
|
|
1
1
|
import type { ActiveRun, BackendInfo, EvidencePage, LayerEvidence, LayerHeader, LayerRows } from '@crvouga/atlas/reports/evidence-types';
|
|
2
|
+
import { applyEffectiveDelta, isEffectiveDelta, type EffectiveDelta } from '@crvouga/atlas/reports/effective-delta';
|
|
3
|
+
import type { EffectiveEvidence } from '@crvouga/atlas/reports/report-index';
|
|
2
4
|
import { contributes, evidenceView, type EvidenceView, type SourceSelection } from '@crvouga/atlas/reports/evidence-view';
|
|
3
5
|
import type { SpecEntry } from '@crvouga/atlas/reports/explorer-types';
|
|
4
6
|
import { combineEvidence, distinctHeaders } from '@crvouga/atlas/reports/layer-merge';
|
|
5
7
|
import type { ReportOutline } from '@crvouga/atlas/reports/layer-types';
|
|
6
8
|
import { SpecIndexSchema } from '@crvouga/atlas/schema';
|
|
7
|
-
import { useInfiniteQuery, useQueries, useQuery } from '@tanstack/react-query';
|
|
9
|
+
import { useInfiniteQuery, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
|
|
8
10
|
import { createContext, useContext, useEffect, useMemo, useRef, type ReactNode } from 'react';
|
|
9
11
|
|
|
10
12
|
import { keys, specKey, useAdapter } from '../data/queries';
|
|
13
|
+
import { eventsAddress, listenLive, throttled, useLiveStatus } from '../live/live-stream';
|
|
11
14
|
import { fileName, folderOf, kindOfFile, outlinePath } from '../spec-kind';
|
|
12
15
|
import { useUiStore } from '../state/ui-store';
|
|
13
|
-
import { reportJson, staticEvidence } from './api';
|
|
16
|
+
import { effectiveEvidence, reportJson, staticEvidence } from './api';
|
|
14
17
|
|
|
15
18
|
/** How often the layer list is asked for while a spec run is writing results, and while nothing is. */
|
|
16
19
|
const LIVE_POLL_MS = 2_000;
|
|
17
20
|
const IDLE_POLL_MS = 15_000;
|
|
21
|
+
/** The effective report is revalidated with its ETag, so asking often costs one small answer. */
|
|
22
|
+
const EFFECTIVE_POLL_MS = 5_000;
|
|
23
|
+
/** While the server's stream is open it says when to read again; this is only the safety net behind it. */
|
|
24
|
+
const LIVE_SAFETY_POLL_MS = 60_000;
|
|
25
|
+
/** At most this often is the report read again because the server said it changed. */
|
|
26
|
+
const LIVE_REREAD_MS = 250;
|
|
18
27
|
|
|
19
28
|
export type EvidenceState = {
|
|
20
29
|
/** The layer list has been read at least once. */
|
|
@@ -63,26 +72,76 @@ function outlineOf(files: readonly string[]): ReportOutline {
|
|
|
63
72
|
return { specs, sha: null, changedFiles: [] };
|
|
64
73
|
}
|
|
65
74
|
|
|
75
|
+
const NONE: readonly LayerEvidence[] = [];
|
|
76
|
+
|
|
77
|
+
function hasExclusions(excluded: SourceSelection): boolean {
|
|
78
|
+
return Object.values(excluded).some((list) => list.length > 0);
|
|
79
|
+
}
|
|
80
|
+
|
|
66
81
|
export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
67
82
|
const adapter = useAdapter();
|
|
68
83
|
const source = specKey(adapter);
|
|
69
84
|
const excluded = useUiStore((s) => s.excluded);
|
|
70
85
|
const setExcluded = useUiStore((s) => s.setExcluded);
|
|
71
86
|
|
|
87
|
+
const client = useQueryClient();
|
|
88
|
+
const stream = useLiveStatus();
|
|
89
|
+
const streaming = stream === 'live';
|
|
90
|
+
// The server says when results arrive: read the report again then, not on a timer.
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const url = eventsAddress();
|
|
93
|
+
if (!url) return;
|
|
94
|
+
// A burst of results (a CI run finishing, many shards at once) is read once, not once for each.
|
|
95
|
+
const reread = throttled(() => {
|
|
96
|
+
void client.invalidateQueries({ queryKey: ['evidence-effective'] });
|
|
97
|
+
void client.invalidateQueries({ queryKey: ['evidence-index'] });
|
|
98
|
+
}, LIVE_REREAD_MS);
|
|
99
|
+
const reoutline = throttled(() => {
|
|
100
|
+
void client.invalidateQueries({ queryKey: ['evidence-outline'] });
|
|
101
|
+
void client.invalidateQueries({ queryKey: ['evidence-effective'] });
|
|
102
|
+
}, LIVE_REREAD_MS);
|
|
103
|
+
const stop = listenLive(url, (event) => {
|
|
104
|
+
if (event.type === 'change' && event.change.scope === 'specs') return reoutline();
|
|
105
|
+
if (event.type !== 'report' && event.type !== 'open') return;
|
|
106
|
+
reread();
|
|
107
|
+
if (event.type === 'open') reoutline();
|
|
108
|
+
});
|
|
109
|
+
return () => { stop(); reread.cancel(); reoutline.cancel(); };
|
|
110
|
+
}, [client]);
|
|
111
|
+
|
|
72
112
|
const specIndex = useQuery({ queryKey: keys.specIndex(source), queryFn: adapter.specIndex, retry: false });
|
|
73
113
|
const files = useMemo(() => {
|
|
74
114
|
const parsed = SpecIndexSchema.safeParse(specIndex.data);
|
|
75
115
|
return parsed.success ? parsed.data.files : [];
|
|
76
116
|
}, [specIndex.data]);
|
|
77
117
|
|
|
118
|
+
// The node answers with only what the report shows, in one request. Leaving a source out needs the
|
|
119
|
+
// history it hides, so that one case reads every layer, as a store without a node always does.
|
|
120
|
+
const effective = effectiveEvidence() && !staticEvidence && !hasExclusions(excluded);
|
|
121
|
+
const overview = useQuery({
|
|
122
|
+
queryKey: ['evidence-effective'],
|
|
123
|
+
// Holding a report, ask only for what changed since it; the server answers with all of it when it cannot.
|
|
124
|
+
queryFn: async ({ signal }) => {
|
|
125
|
+
const held = client.getQueryData<EffectiveEvidence>(['evidence-effective']);
|
|
126
|
+
const answer = await reportJson<EffectiveEvidence | EffectiveDelta>(held ? `/evidence/effective?since=${encodeURIComponent(held.version)}` : '/evidence/effective', signal);
|
|
127
|
+
if (!isEffectiveDelta(answer)) return answer;
|
|
128
|
+
return (held && applyEffectiveDelta(held, answer)) ?? reportJson<EffectiveEvidence>('/evidence/effective', signal);
|
|
129
|
+
},
|
|
130
|
+
enabled: effective,
|
|
131
|
+
staleTime: 0,
|
|
132
|
+
retry: false,
|
|
133
|
+
refetchInterval: streaming ? LIVE_SAFETY_POLL_MS : EFFECTIVE_POLL_MS
|
|
134
|
+
});
|
|
135
|
+
|
|
78
136
|
const index = useInfiniteQuery({
|
|
79
137
|
queryKey: ['evidence-index'],
|
|
138
|
+
enabled: !effective,
|
|
80
139
|
initialPageParam: null as string | null,
|
|
81
140
|
queryFn: ({ pageParam, signal }) => reportJson<EvidencePage>(`/evidence${pageParam ? `?cursor=${encodeURIComponent(pageParam)}` : ''}`, signal),
|
|
82
141
|
getNextPageParam: (page) => page.cursor,
|
|
83
142
|
staleTime: 0,
|
|
84
143
|
retry: false,
|
|
85
|
-
refetchInterval: (query) => (staticEvidence ? false : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
|
|
144
|
+
refetchInterval: (query) => (staticEvidence ? false : streaming ? LIVE_SAFETY_POLL_MS : query.state.data?.pages[0]?.active.length ? LIVE_POLL_MS : IDLE_POLL_MS)
|
|
86
145
|
});
|
|
87
146
|
// Show the newest layers at once, then read the older pages behind them.
|
|
88
147
|
const { hasNextPage, isFetching, isError, fetchNextPage } = index;
|
|
@@ -98,7 +157,7 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
|
98
157
|
const serverOutline = useQuery({ queryKey: ['evidence-outline'], queryFn: ({ signal }) => reportJson<ReportOutline>('/outline', signal), staleTime: 30_000, retry: false });
|
|
99
158
|
const outline = useMemo(() => serverOutline.data ?? outlineOf(files), [serverOutline.data, files]);
|
|
100
159
|
|
|
101
|
-
const wanted = useMemo(() => layers.filter((layer) => contributes(layer, excluded)), [layers, excluded]);
|
|
160
|
+
const wanted = useMemo(() => (effective ? [] : layers.filter((layer) => contributes(layer, excluded))), [layers, excluded, effective]);
|
|
102
161
|
const rows = useQueries({
|
|
103
162
|
queries: wanted.map((layer) => ({
|
|
104
163
|
queryKey: rowsKey(layer),
|
|
@@ -126,23 +185,26 @@ export function EvidenceProvider({ children }: { children: ReactNode }) {
|
|
|
126
185
|
// `stamp` changes exactly when a layer's rows do.
|
|
127
186
|
}, [wanted, rows.stamp]);
|
|
128
187
|
|
|
129
|
-
const
|
|
188
|
+
const shown = effective ? overview.data?.layers ?? NONE : evidence;
|
|
189
|
+
const view = useMemo(() => evidenceView(combineEvidence(outline, shown)), [outline, shown]);
|
|
130
190
|
const titles = useMemo(() => new Map(outline.specs.map((spec) => [spec.path, spec.title])), [outline]);
|
|
191
|
+
const effectiveHeaders = useMemo(() => (overview.data?.layers ?? NONE).map((entry) => entry.layer), [overview.data]);
|
|
131
192
|
|
|
132
193
|
const state: EvidenceState = {
|
|
133
|
-
ready: pages !== undefined,
|
|
134
|
-
unavailable: pages === undefined && index.error ? index.error.message : null,
|
|
194
|
+
ready: effective ? overview.data !== undefined : pages !== undefined,
|
|
195
|
+
unavailable: effective ? (overview.data === undefined && overview.error ? overview.error.message : null) : pages === undefined && index.error ? index.error.message : null,
|
|
135
196
|
retry: () => {
|
|
197
|
+
if (effective) { void Promise.all([overview.refetch(), serverOutline.refetch()]); return; }
|
|
136
198
|
void reportJson<EvidencePage>('/evidence?retry=1')
|
|
137
199
|
.catch(() => null)
|
|
138
200
|
.then(() => Promise.all([index.refetch(), serverOutline.refetch()]));
|
|
139
201
|
},
|
|
140
202
|
pending: wanted.length - evidence.length,
|
|
141
|
-
layers,
|
|
142
|
-
evidence,
|
|
143
|
-
backends: first?.backends ?? [],
|
|
144
|
-
active: first?.active ?? [],
|
|
145
|
-
notices,
|
|
203
|
+
layers: effective ? effectiveHeaders : layers,
|
|
204
|
+
evidence: shown,
|
|
205
|
+
backends: effective ? overview.data?.backends ?? [] : first?.backends ?? [],
|
|
206
|
+
active: effective ? [] : first?.active ?? [],
|
|
207
|
+
notices: effective ? overview.data?.notices ?? [] : notices,
|
|
146
208
|
view,
|
|
147
209
|
files,
|
|
148
210
|
filesPending: specIndex.isPending,
|
|
@@ -455,11 +455,31 @@
|
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
.resultName {
|
|
458
|
+
display: flex;
|
|
458
459
|
flex: 1;
|
|
460
|
+
flex-direction: column;
|
|
459
461
|
min-width: 0;
|
|
460
462
|
font-weight: 500;
|
|
461
463
|
}
|
|
462
464
|
|
|
465
|
+
.envMarks {
|
|
466
|
+
display: flex;
|
|
467
|
+
flex-wrap: wrap;
|
|
468
|
+
gap: 8px;
|
|
469
|
+
margin-top: 2px;
|
|
470
|
+
color: var(--ink-3);
|
|
471
|
+
font-size: 0.75rem;
|
|
472
|
+
font-weight: 400;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.envMarks span[data-status='passed'] {
|
|
476
|
+
color: var(--passed-ink);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.envMarks span[data-status='failed'] {
|
|
480
|
+
color: var(--failed-ink);
|
|
481
|
+
}
|
|
482
|
+
|
|
463
483
|
.result[data-status='none'] .resultName {
|
|
464
484
|
color: var(--ink-3);
|
|
465
485
|
font-weight: 400;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BackendInfo, LayerHeader, StatusCounts } from '@crvouga/atlas/reports/evidence-types';
|
|
2
2
|
import type { SpecStatus, SpecSummary } from '@crvouga/atlas/reports/evidence-view';
|
|
3
|
+
import { environmentLabel } from '@crvouga/atlas/reports/layer-axes';
|
|
3
4
|
import { useState, type ReactNode } from 'react';
|
|
4
5
|
|
|
5
6
|
import styles from './evidence.module.css';
|
|
@@ -89,6 +90,7 @@ export function Origin({ layer, backends, now }: { layer: LayerHeader; backends:
|
|
|
89
90
|
{layer.live && <span className={styles.liveTag}>Running now</span>}
|
|
90
91
|
<span>{backendLabel(backends, layer.backend)}</span>
|
|
91
92
|
{layer.branch && <span>{layer.branch}</span>}
|
|
93
|
+
{layer.environment && <span>{environmentLabel(layer.environment)}</span>}
|
|
92
94
|
{sha && <code>{sha}</code>}
|
|
93
95
|
{layer.freshness === 'modified' && <span>with local changes</span>}
|
|
94
96
|
{layer.freshness === 'unknown' && <span>origin not captured</span>}
|
|
@@ -67,7 +67,7 @@ export function Overview({ kind }: { kind: SpecKind }) {
|
|
|
67
67
|
</p>
|
|
68
68
|
))}
|
|
69
69
|
{!evidence.ready && !evidence.unavailable && <p className={styles.aside} role="status">Loading recorded results…</p>}
|
|
70
|
-
{evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more
|
|
70
|
+
{evidence.ready && evidence.pending > 0 && <p className={styles.aside} role="status">Reading {plural(evidence.pending, 'more layer', 'more layers')}…</p>}
|
|
71
71
|
|
|
72
72
|
{evidence.active.length > 0 && (
|
|
73
73
|
<section className={styles.block}>
|
|
@@ -111,13 +111,13 @@ export function Overview({ kind }: { kind: SpecKind }) {
|
|
|
111
111
|
<p className={styles.aside}>
|
|
112
112
|
{evidence.layers.length === 0
|
|
113
113
|
? `Nothing has been recorded yet. Results appear here as a spec run finishes each test.`
|
|
114
|
-
: `None of the ${plural(evidence.layers.length, '
|
|
114
|
+
: `None of the ${plural(evidence.layers.length, 'layer', 'layers')} in the combined report checked a ${kind.one}.`}
|
|
115
115
|
</p>
|
|
116
116
|
)}
|
|
117
117
|
|
|
118
118
|
{evidence.ready && (
|
|
119
119
|
<p className={styles.footLine}>
|
|
120
|
-
{plural(evidence.layers.length, '
|
|
120
|
+
{plural(evidence.layers.length, 'layer', 'layers')} from {evidence.backends.filter((backend) => backend.available).map((backend) => backendLabel(evidence.backends, backend.id)).join(' and ') || 'no report store'}.{' '}
|
|
121
121
|
<button type="button" className={styles.link} onClick={() => setSourcesOpen(true)}>Choose sources</button>
|
|
122
122
|
{empty > 0 && (
|
|
123
123
|
<>
|