@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
|
@@ -1,43 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer composition: slots, winners and de-duplication
|
|
3
|
+
*
|
|
4
|
+
* The pure core of the composition rule: it flattens layers into rows, computes the slot key each row fills, decides
|
|
5
|
+
* which result wins a slot, and removes the duplicate copies of one immutable layer held by several backends. It does
|
|
6
|
+
* no I/O. The winner order (a completed result beats an unfinished one, then the newer `recordedAtMs`, then
|
|
7
|
+
* `failed` over `passed` and `running` over `not-run`, then the greater `layerId|backendId`) is a total order and is
|
|
8
|
+
* part of the stable report contract: changing it changes every report. `report-index.ts` folds layers into the same
|
|
9
|
+
* winners incrementally and must always equal `combineEvidence`.
|
|
10
|
+
*
|
|
11
|
+
* @see docs/report-contract.md#how-layers-compose
|
|
12
|
+
*/
|
|
1
13
|
import type { EvidenceStatus, SpecEntry } from './explorer-types.js';
|
|
2
14
|
import type { LayerEvidence, LayerHeader, LayerRow, StatusCounts } from './evidence-types.js';
|
|
3
15
|
import type { LayerParameters, LayerSummary, ReportOutline } from './layer-types.js';
|
|
4
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* A short, deterministic fingerprint of a spec's text, shaped `<length>:<hash>:<hash>` (the length in UTF-16 code
|
|
18
|
+
* units and two 32-bit hashes in hex). A layer records it per spec so a reader can warn when the spec changed
|
|
19
|
+
* since ("Potentially outdated: spec changed"). Advisory identity only: it is not collision resistant and never an
|
|
20
|
+
* integrity check.
|
|
21
|
+
*/
|
|
5
22
|
export declare function textFingerprint(text: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Normalises a check's or spec's path to its `specs/<path inside the specs directory>` form, so the same spec
|
|
25
|
+
* recorded from different checkouts is one spec.
|
|
26
|
+
*
|
|
27
|
+
* Converts `\` to `/`, drops a leading `./`, and drops everything before the first path segment named exactly
|
|
28
|
+
* `specs`. A path with no such segment is returned unchanged (apart from the first two steps).
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* specPath('packages/app/specs/join.feature'); // 'specs/join.feature'
|
|
32
|
+
* specPath('myspecs/x.feature'); // 'myspecs/x.feature'
|
|
33
|
+
*/
|
|
6
34
|
export declare function specPath(path: string): string;
|
|
35
|
+
/** Serialises parameters as JSON key/value pairs sorted by key, so equal parameter sets compare equal as strings. */
|
|
7
36
|
export declare function parameterKey(parameters: LayerParameters): string;
|
|
8
|
-
/**
|
|
37
|
+
/** Whether a part `kind` only qualifies an outcome (today only `performance`) and so is not counted as a check when anything else is recorded. */
|
|
9
38
|
export declare function isDiagnosticKind(kind: string): boolean;
|
|
39
|
+
/** A fresh status tally with every status at zero. */
|
|
10
40
|
export declare function emptyCounts(): Record<EvidenceStatus, number>;
|
|
11
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Counts rows by status, leaving out the diagnostic rows (see `isDiagnosticKind`) that only qualify an outcome.
|
|
43
|
+
* When every row is diagnostic they are counted, so a layer of timings alone is not empty.
|
|
44
|
+
*/
|
|
12
45
|
export declare function outcomeCounts(rows: readonly LayerRow[]): StatusCounts;
|
|
13
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Flattens a layer into what composition reads: a header (origin, normalised environment and parameters, counts,
|
|
48
|
+
* evidence summary), one row per check of every part, and the spec fingerprints it recorded.
|
|
49
|
+
*
|
|
50
|
+
* `backend` is the id of the backend the layer was read from; it only breaks ties between copies. Paths in rows and
|
|
51
|
+
* fingerprints go through `specPath`. When the layer has a recorded `source`, a row whose check id matches one of its
|
|
52
|
+
* tests also gets that test's `testId`, `variant` and duration. The result is `live: false`: a layer read from a
|
|
53
|
+
* store is never a run still in progress.
|
|
54
|
+
*/
|
|
14
55
|
export declare function layerEvidence(summary: LayerSummary, backend: string): LayerEvidence;
|
|
56
|
+
/** The winning result of one slot: which layer and row it came from, and whether it may be stale. */
|
|
15
57
|
export type EvidenceResult = {
|
|
16
|
-
/** One producer's check of one kind under one parameter set. */
|
|
58
|
+
/** One producer's check of one kind under one parameter set. See `resultKey`. */
|
|
17
59
|
readonly key: string;
|
|
18
|
-
/** The check itself, whatever kind of evidence and parameters describe it. */
|
|
60
|
+
/** The check itself, whatever kind of evidence and parameters describe it. See `slotKey`. */
|
|
19
61
|
readonly slot: string;
|
|
62
|
+
/** The layer that recorded this result. */
|
|
20
63
|
readonly layer: LayerHeader;
|
|
64
|
+
/** The check as recorded. */
|
|
21
65
|
readonly row: LayerRow;
|
|
66
|
+
/** The layer's parameters other than the environment. */
|
|
22
67
|
readonly parameters: LayerParameters;
|
|
68
|
+
/** Why the result may be out of date, or `null` when nothing suggests it. Advisory: never changes the status. */
|
|
23
69
|
readonly warning: string | null;
|
|
24
70
|
};
|
|
71
|
+
/**
|
|
72
|
+
* Whether `next` should take the slot currently held by `prior`: the winner rule of the report contract.
|
|
73
|
+
*
|
|
74
|
+
* In order: a completed result (`passed`, `failed`) beats an unfinished one (`running`, `not-run`) whatever their
|
|
75
|
+
* times; otherwise the greater `recordedAtMs` wins; at the same time `failed` beats `passed` and `running` beats
|
|
76
|
+
* `not-run`; at the same status the greater `layerId|backendId` wins. The last step makes the order total, so the
|
|
77
|
+
* answer never depends on the order results are offered in. Returns `false` for two identical results.
|
|
78
|
+
*/
|
|
79
|
+
export declare function replaces(next: EvidenceResult, prior: EvidenceResult): boolean;
|
|
80
|
+
/** The winners of every slot, grouped under the spec of the current outline they belong to. */
|
|
25
81
|
export type CombinedEvidence = {
|
|
82
|
+
/** One entry per spec of the outline, in outline order (a spec with no results has an empty list). */
|
|
26
83
|
readonly specs: readonly {
|
|
27
84
|
readonly spec: SpecEntry;
|
|
28
85
|
readonly results: EvidenceResult[];
|
|
29
86
|
}[];
|
|
87
|
+
/** Winners whose spec path is absent or not in the outline (a removed spec keeps its evidence here). */
|
|
30
88
|
readonly unassigned: EvidenceResult[];
|
|
31
89
|
};
|
|
32
|
-
/**
|
|
90
|
+
/**
|
|
91
|
+
* Lays layers over the outline of current specs and returns the winner of every slot.
|
|
92
|
+
*
|
|
93
|
+
* One linear pass over the rows of the layers (after `distinctEvidence`). The slot is producer, kind, spec path,
|
|
94
|
+
* check id, environment and parameters; `replaces` picks the winner, so the result does not depend on the order of
|
|
95
|
+
* `layers`. Branch, sha, backend and run never split a slot. Each result carries a freshness `warning`, computed
|
|
96
|
+
* against `outline`, that does not affect which result wins.
|
|
97
|
+
*
|
|
98
|
+
* @see docs/report-contract.md#how-layers-compose
|
|
99
|
+
*/
|
|
33
100
|
export declare function combineEvidence(outline: ReportOutline, layers: readonly LayerEvidence[]): CombinedEvidence;
|
|
101
|
+
/**
|
|
102
|
+
* The key of the slot a row fills: a JSON array of producer, kind, spec path (`''` when none), check id, environment
|
|
103
|
+
* (`''` when none) and the sorted parameters. Two results with the same key compete; the winner is `replaces`.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resultKey(layer: Pick<LayerHeader, 'producer' | 'environment' | 'parameters'>, row: Pick<LayerRow, 'kind' | 'specPath' | 'id'>): string;
|
|
106
|
+
/**
|
|
107
|
+
* `resultKey` for every row of one layer, which share all but three parts of it. The strings are exactly
|
|
108
|
+
* those `resultKey` makes (they are keys that persist in no store, but two ways of making one must agree),
|
|
109
|
+
* without sorting and writing the layer's parameters once per check.
|
|
110
|
+
*/
|
|
111
|
+
export declare function layerResultKeys(layer: Pick<LayerHeader, 'producer' | 'environment' | 'parameters'>): (row: Pick<LayerRow, 'kind' | 'specPath' | 'id'>) => string;
|
|
112
|
+
/** The key of a check regardless of producer, environment and parameters: a JSON array of its spec path and check id. */
|
|
34
113
|
export declare function slotKey(path: string, id: string): string;
|
|
35
|
-
/**
|
|
114
|
+
/**
|
|
115
|
+
* De-duplicates layer headers: the same layer held by several backends is one layer.
|
|
116
|
+
*
|
|
117
|
+
* Suppresses the mutable address of an execution already archived as an immutable layer. An
|
|
118
|
+
* immutable layer (64-hex id) held by several backends is one layer, reported by the backend whose id sorts
|
|
119
|
+
* first, whatever order they were read in. A mutable one is its backend's own and is kept once per backend.
|
|
120
|
+
* Returns the survivors, not necessarily in input order.
|
|
121
|
+
*/
|
|
36
122
|
export declare function distinctHeaders<T extends {
|
|
37
123
|
readonly id: string;
|
|
38
124
|
readonly execution: string;
|
|
125
|
+
readonly backend?: string;
|
|
39
126
|
}>(layers: readonly T[]): T[];
|
|
127
|
+
/** Keeps, in input order, only the layers `distinctHeaders` keeps, and each `backend|id` once. */
|
|
40
128
|
export declare function distinctEvidence(layers: readonly LayerEvidence[]): LayerEvidence[];
|
|
41
|
-
/**
|
|
129
|
+
/** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
|
|
42
130
|
export declare function distinctLayers(layers: readonly LayerSummary[]): LayerSummary[];
|
|
131
|
+
/** A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts. */
|
|
43
132
|
export declare function layerExecutionKey(layer: LayerSummary): string;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
|
|
1
|
+
import { summarizeEvidence } from './evidence-detail.js';
|
|
2
|
+
import { layerPlace } from './layer-axes.js';
|
|
3
|
+
/**
|
|
4
|
+
* A short, deterministic fingerprint of a spec's text, shaped `<length>:<hash>:<hash>` (the length in UTF-16 code
|
|
5
|
+
* units and two 32-bit hashes in hex). A layer records it per spec so a reader can warn when the spec changed
|
|
6
|
+
* since ("Potentially outdated: spec changed"). Advisory identity only: it is not collision resistant and never an
|
|
7
|
+
* integrity check.
|
|
8
|
+
*/
|
|
2
9
|
export function textFingerprint(text) {
|
|
3
10
|
let a = 2166136261, b = 5381;
|
|
4
11
|
for (let i = 0; i < text.length; i++) {
|
|
@@ -7,20 +14,37 @@ export function textFingerprint(text) {
|
|
|
7
14
|
}
|
|
8
15
|
return `${text.length}:${(a >>> 0).toString(16)}:${(b >>> 0).toString(16)}`;
|
|
9
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* Normalises a check's or spec's path to its `specs/<path inside the specs directory>` form, so the same spec
|
|
19
|
+
* recorded from different checkouts is one spec.
|
|
20
|
+
*
|
|
21
|
+
* Converts `\` to `/`, drops a leading `./`, and drops everything before the first path segment named exactly
|
|
22
|
+
* `specs`. A path with no such segment is returned unchanged (apart from the first two steps).
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* specPath('packages/app/specs/join.feature'); // 'specs/join.feature'
|
|
26
|
+
* specPath('myspecs/x.feature'); // 'myspecs/x.feature'
|
|
27
|
+
*/
|
|
10
28
|
export function specPath(path) {
|
|
11
|
-
|
|
29
|
+
// Up to the first folder named exactly `specs`: `myspecs/x` is not in one.
|
|
30
|
+
return path.replaceAll('\\', '/').replace(/^\.\//, '').replace(/^(?:.*?\/)?(?=specs\/)/, '');
|
|
12
31
|
}
|
|
32
|
+
/** Serialises parameters as JSON key/value pairs sorted by key, so equal parameter sets compare equal as strings. */
|
|
13
33
|
export function parameterKey(parameters) {
|
|
14
34
|
return JSON.stringify(Object.entries(parameters).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0));
|
|
15
35
|
}
|
|
16
|
-
/**
|
|
36
|
+
/** Whether a part `kind` only qualifies an outcome (today only `performance`) and so is not counted as a check when anything else is recorded. */
|
|
17
37
|
export function isDiagnosticKind(kind) {
|
|
18
38
|
return kind === 'performance';
|
|
19
39
|
}
|
|
40
|
+
/** A fresh status tally with every status at zero. */
|
|
20
41
|
export function emptyCounts() {
|
|
21
42
|
return { passed: 0, failed: 0, running: 0, 'not-run': 0 };
|
|
22
43
|
}
|
|
23
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Counts rows by status, leaving out the diagnostic rows (see `isDiagnosticKind`) that only qualify an outcome.
|
|
46
|
+
* When every row is diagnostic they are counted, so a layer of timings alone is not empty.
|
|
47
|
+
*/
|
|
24
48
|
export function outcomeCounts(rows) {
|
|
25
49
|
const outcomes = rows.some(row => !isDiagnosticKind(row.kind)) ? rows.filter(row => !isDiagnosticKind(row.kind)) : rows;
|
|
26
50
|
const counts = emptyCounts();
|
|
@@ -28,27 +52,49 @@ export function outcomeCounts(rows) {
|
|
|
28
52
|
counts[row.status] += 1;
|
|
29
53
|
return counts;
|
|
30
54
|
}
|
|
31
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Flattens a layer into what composition reads: a header (origin, normalised environment and parameters, counts,
|
|
57
|
+
* evidence summary), one row per check of every part, and the spec fingerprints it recorded.
|
|
58
|
+
*
|
|
59
|
+
* `backend` is the id of the backend the layer was read from; it only breaks ties between copies. Paths in rows and
|
|
60
|
+
* fingerprints go through `specPath`. When the layer has a recorded `source`, a row whose check id matches one of its
|
|
61
|
+
* tests also gets that test's `testId`, `variant` and duration. The result is `live: false`: a layer read from a
|
|
62
|
+
* store is never a run still in progress.
|
|
63
|
+
*/
|
|
32
64
|
export function layerEvidence(summary, backend) {
|
|
65
|
+
const place = layerPlace(summary);
|
|
33
66
|
const source = summary.source;
|
|
34
67
|
const tests = new Map(source?.tests.map(test => [`${source.source}/${test.testId}`, test]) ?? []);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
// A layer's checks repeat a handful of spec paths; each is normalized once.
|
|
69
|
+
const normalized = new Map();
|
|
70
|
+
const specOf = (path) => {
|
|
71
|
+
let found = normalized.get(path);
|
|
72
|
+
if (found === undefined)
|
|
73
|
+
normalized.set(path, found = specPath(path));
|
|
74
|
+
return found;
|
|
75
|
+
};
|
|
76
|
+
const rows = [];
|
|
77
|
+
for (const part of summary.parts) {
|
|
78
|
+
for (const check of part.checks) {
|
|
79
|
+
const test = tests.get(check.id);
|
|
80
|
+
const evidence = summarizeEvidence(check);
|
|
81
|
+
rows.push({
|
|
82
|
+
kind: part.kind,
|
|
83
|
+
id: check.id,
|
|
84
|
+
title: check.title,
|
|
85
|
+
status: check.status,
|
|
86
|
+
specPath: check.specPath ? specOf(check.specPath) : null,
|
|
87
|
+
detail: check.detail,
|
|
88
|
+
measurements: check.measurements,
|
|
89
|
+
testId: test?.testId ?? null,
|
|
90
|
+
variant: test ? { system: test.system, runtime: test.runtime, seed: test.seed } : {},
|
|
91
|
+
durationMs: test?.durationMs ?? check.durationMs ?? null,
|
|
92
|
+
...(evidence ? { evidence } : {}),
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
51
96
|
const recording = summary.recording;
|
|
97
|
+
const layerSummaryEvidence = summarizeEvidence(summary, ...summary.parts);
|
|
52
98
|
const layer = {
|
|
53
99
|
...(summary.freshness ? { freshness: summary.freshness } : {}),
|
|
54
100
|
id: summary.id,
|
|
@@ -58,12 +104,14 @@ export function layerEvidence(summary, backend) {
|
|
|
58
104
|
recordedAtMs: summary.recordedAtMs,
|
|
59
105
|
sha: summary.sha,
|
|
60
106
|
branch: summary.branch,
|
|
107
|
+
environment: place.environment,
|
|
61
108
|
runUrl: summary.runUrl,
|
|
62
|
-
parameters:
|
|
109
|
+
parameters: place.parameters,
|
|
63
110
|
recording: recording === null ? null : recording.type === 'layer' ? { type: 'layer', layer: summary.id } : recording,
|
|
64
111
|
live: false,
|
|
65
112
|
counts: outcomeCounts(rows),
|
|
66
113
|
execution: textFingerprint(layerExecutionKey(summary)),
|
|
114
|
+
...(layerSummaryEvidence ? { evidence: layerSummaryEvidence } : {}),
|
|
67
115
|
};
|
|
68
116
|
const fingerprints = {};
|
|
69
117
|
for (const spec of summary.specs)
|
|
@@ -71,6 +119,12 @@ export function layerEvidence(summary, backend) {
|
|
|
71
119
|
fingerprints[specPath(spec.path)] = spec.fingerprint;
|
|
72
120
|
return { id: summary.id, layer, rows, fingerprints };
|
|
73
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* The freshness warning for one result, or `null` when it is current. Checked in this order: a live run has none;
|
|
124
|
+
* a `modified` or `unknown` layer; a spec that is not in the outline; a missing fingerprint on either side; a
|
|
125
|
+
* fingerprint that differs from the current spec; related files that changed; a layer sha that differs from the
|
|
126
|
+
* outline's (or either absent). Advisory only: a warning never changes a status.
|
|
127
|
+
*/
|
|
74
128
|
function warning(layer, path, current, outline, recorded, relatedChanged) {
|
|
75
129
|
if (layer.live)
|
|
76
130
|
return null;
|
|
@@ -94,8 +148,15 @@ const RANK = { passed: 0, 'not-run': 1, running: 2, failed: 3 };
|
|
|
94
148
|
function completed(status) {
|
|
95
149
|
return status === 'passed' || status === 'failed';
|
|
96
150
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
151
|
+
/**
|
|
152
|
+
* Whether `next` should take the slot currently held by `prior`: the winner rule of the report contract.
|
|
153
|
+
*
|
|
154
|
+
* In order: a completed result (`passed`, `failed`) beats an unfinished one (`running`, `not-run`) whatever their
|
|
155
|
+
* times; otherwise the greater `recordedAtMs` wins; at the same time `failed` beats `passed` and `running` beats
|
|
156
|
+
* `not-run`; at the same status the greater `layerId|backendId` wins. The last step makes the order total, so the
|
|
157
|
+
* answer never depends on the order results are offered in. Returns `false` for two identical results.
|
|
158
|
+
*/
|
|
159
|
+
export function replaces(next, prior) {
|
|
99
160
|
const done = completed(next.row.status);
|
|
100
161
|
const priorDone = completed(prior.row.status);
|
|
101
162
|
if (done !== priorDone)
|
|
@@ -104,9 +165,22 @@ function replaces(next, prior) {
|
|
|
104
165
|
return next.layer.recordedAtMs > prior.layer.recordedAtMs;
|
|
105
166
|
if (RANK[next.row.status] !== RANK[prior.row.status])
|
|
106
167
|
return RANK[next.row.status] > RANK[prior.row.status];
|
|
107
|
-
|
|
168
|
+
// The last tie-break is total, so no input order can change the answer.
|
|
169
|
+
return layerOrder(next.layer) > layerOrder(prior.layer);
|
|
170
|
+
}
|
|
171
|
+
function layerOrder(layer) {
|
|
172
|
+
return `${layer.id}|${layer.backend}`;
|
|
108
173
|
}
|
|
109
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Lays layers over the outline of current specs and returns the winner of every slot.
|
|
176
|
+
*
|
|
177
|
+
* One linear pass over the rows of the layers (after `distinctEvidence`). The slot is producer, kind, spec path,
|
|
178
|
+
* check id, environment and parameters; `replaces` picks the winner, so the result does not depend on the order of
|
|
179
|
+
* `layers`. Branch, sha, backend and run never split a slot. Each result carries a freshness `warning`, computed
|
|
180
|
+
* against `outline`, that does not affect which result wins.
|
|
181
|
+
*
|
|
182
|
+
* @see docs/report-contract.md#how-layers-compose
|
|
183
|
+
*/
|
|
110
184
|
export function combineEvidence(outline, layers) {
|
|
111
185
|
const specs = new Map(outline.specs.map(spec => [specPath(spec.path), { spec, results: [] }]));
|
|
112
186
|
const changed = new Set(outline.changedFiles.map(specPath));
|
|
@@ -114,10 +188,13 @@ export function combineEvidence(outline, layers) {
|
|
|
114
188
|
for (const { layer, rows, fingerprints } of distinctEvidence(layers)) {
|
|
115
189
|
const scope = layer.packageDir;
|
|
116
190
|
const related = outline.changedFiles.some(file => scope ? file.startsWith(`${scope}/`) : !specPath(file).startsWith('specs/'));
|
|
191
|
+
const keyOf = layerResultKeys(layer);
|
|
117
192
|
for (const row of rows) {
|
|
118
|
-
|
|
193
|
+
// Environment and the layer's other choices split the slot. A harness
|
|
194
|
+
// seed does not: a focused rerun replaces the check it repeated.
|
|
195
|
+
const parameters = layer.parameters;
|
|
119
196
|
const path = row.specPath ?? '';
|
|
120
|
-
const key =
|
|
197
|
+
const key = keyOf(row);
|
|
121
198
|
const result = { key, slot: slotKey(path, row.id), layer, row, parameters, warning: warning(layer, path, specs.get(path)?.spec, outline, fingerprints[path], related || changed.has(path)) };
|
|
122
199
|
const prior = winners.get(key);
|
|
123
200
|
if (!prior || replaces(result, prior))
|
|
@@ -129,26 +206,70 @@ export function combineEvidence(outline, layers) {
|
|
|
129
206
|
(specs.get(result.row.specPath ?? '')?.results ?? unassigned).push(result);
|
|
130
207
|
return { specs: [...specs.values()], unassigned };
|
|
131
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* The key of the slot a row fills: a JSON array of producer, kind, spec path (`''` when none), check id, environment
|
|
211
|
+
* (`''` when none) and the sorted parameters. Two results with the same key compete; the winner is `replaces`.
|
|
212
|
+
*/
|
|
213
|
+
export function resultKey(layer, row) {
|
|
214
|
+
return JSON.stringify([layer.producer, row.kind, row.specPath ?? '', row.id, layer.environment ?? '', parameterKey(layer.parameters)]);
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* `resultKey` for every row of one layer, which share all but three parts of it. The strings are exactly
|
|
218
|
+
* those `resultKey` makes (they are keys that persist in no store, but two ways of making one must agree),
|
|
219
|
+
* without sorting and writing the layer's parameters once per check.
|
|
220
|
+
*/
|
|
221
|
+
export function layerResultKeys(layer) {
|
|
222
|
+
const producer = JSON.stringify(layer.producer);
|
|
223
|
+
const tail = `,${JSON.stringify(layer.environment ?? '')},${JSON.stringify(parameterKey(layer.parameters))}]`;
|
|
224
|
+
return row => `[${producer},${JSON.stringify(row.kind)},${JSON.stringify(row.specPath ?? '')},${JSON.stringify(row.id)}${tail}`;
|
|
225
|
+
}
|
|
226
|
+
/** The key of a check regardless of producer, environment and parameters: a JSON array of its spec path and check id. */
|
|
132
227
|
export function slotKey(path, id) {
|
|
133
228
|
return JSON.stringify([path, id]);
|
|
134
229
|
}
|
|
230
|
+
/** Whether a layer id is a content digest (64 lowercase hex digits), that is an immutable layer rather than a mutable address. */
|
|
135
231
|
function archived(id) {
|
|
136
232
|
return /^[0-9a-f]{64}$/.test(id);
|
|
137
233
|
}
|
|
138
|
-
/**
|
|
234
|
+
/**
|
|
235
|
+
* De-duplicates layer headers: the same layer held by several backends is one layer.
|
|
236
|
+
*
|
|
237
|
+
* Suppresses the mutable address of an execution already archived as an immutable layer. An
|
|
238
|
+
* immutable layer (64-hex id) held by several backends is one layer, reported by the backend whose id sorts
|
|
239
|
+
* first, whatever order they were read in. A mutable one is its backend's own and is kept once per backend.
|
|
240
|
+
* Returns the survivors, not necessarily in input order.
|
|
241
|
+
*/
|
|
139
242
|
export function distinctHeaders(layers) {
|
|
140
243
|
const kept = new Set(layers.filter(layer => archived(layer.id)).map(layer => layer.execution));
|
|
141
|
-
|
|
244
|
+
const chosen = new Map();
|
|
245
|
+
for (const layer of layers) {
|
|
246
|
+
if (!archived(layer.id) && kept.has(layer.execution))
|
|
247
|
+
continue;
|
|
248
|
+
const key = archived(layer.id) ? layer.id : `${layer.backend ?? ''}|${layer.id}`;
|
|
249
|
+
const prior = chosen.get(key);
|
|
250
|
+
if (!prior || (layer.backend ?? '') < (prior.backend ?? ''))
|
|
251
|
+
chosen.set(key, layer);
|
|
252
|
+
}
|
|
253
|
+
return [...chosen.values()];
|
|
142
254
|
}
|
|
255
|
+
/** Keeps, in input order, only the layers `distinctHeaders` keeps, and each `backend|id` once. */
|
|
143
256
|
export function distinctEvidence(layers) {
|
|
144
|
-
const kept = new Set(distinctHeaders(layers.map(entry => entry.layer)).map(layer => layer.id));
|
|
145
|
-
|
|
257
|
+
const kept = new Set(distinctHeaders(layers.map(entry => entry.layer)).map(layer => `${layer.backend}|${layer.id}`));
|
|
258
|
+
const emitted = new Set();
|
|
259
|
+
return layers.filter(entry => {
|
|
260
|
+
const key = `${entry.layer.backend}|${entry.id}`;
|
|
261
|
+
if (!kept.has(key) || emitted.has(key))
|
|
262
|
+
return false;
|
|
263
|
+
emitted.add(key);
|
|
264
|
+
return true;
|
|
265
|
+
});
|
|
146
266
|
}
|
|
147
|
-
/**
|
|
267
|
+
/** Drops each mutable layer whose execution is already archived as an immutable one, and keeps one summary per id. */
|
|
148
268
|
export function distinctLayers(layers) {
|
|
149
269
|
const kept = new Set(layers.filter(layer => archived(layer.id)).map(layerExecutionKey));
|
|
150
270
|
return [...new Map(layers.filter(layer => archived(layer.id) || !kept.has(layerExecutionKey(layer))).map(layer => [layer.id, layer])).values()];
|
|
151
271
|
}
|
|
272
|
+
/** A string equal for the mutable and the archived address of one execution: its producer, sha, time and parts. */
|
|
152
273
|
export function layerExecutionKey(layer) {
|
|
153
274
|
return JSON.stringify([layer.producer, layer.sha, layer.recordedAtMs, layer.parts]);
|
|
154
275
|
}
|
|
@@ -1,6 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Archiving a recorded run as a layer
|
|
3
|
+
*
|
|
4
|
+
* Turns one published source (a package bucket of recorded statechart or test runs with timelines and screenshots)
|
|
5
|
+
* into an immutable layer, so it composes with every other layer. It is the bridge from the commit-keyed recordings
|
|
6
|
+
* to the layer store and is called only by `publishSource`.
|
|
7
|
+
*/
|
|
1
8
|
import type { HtmlReportTest } from './recording-bundle.js';
|
|
2
9
|
import { type BundleIndex } from './bundle.js';
|
|
3
10
|
import type { ObjectStore } from './object-store.js';
|
|
4
11
|
import type { SourceRecord } from './protocol.js';
|
|
5
12
|
import type { PublishSourceArgs } from './publish.js';
|
|
13
|
+
/**
|
|
14
|
+
* Writes `source` and its recorded files as an immutable layer and returns it (see `publishLayerBundle`).
|
|
15
|
+
*
|
|
16
|
+
* The layer is recorded at the later of the commit's start and the source's `updatedAtMs`, with the commit's branch and
|
|
17
|
+
* run URL, the commit's spec inventory (empty when none), the source's parts, `parameters` from `args`, the environment
|
|
18
|
+
* normalised from `args.environment` or the `environment` parameter, and `recording: { type: 'layer' }`. Its bundle
|
|
19
|
+
* holds the commit's `frame.css` and each test's `tests/<testId>/report.json` and `tests/<testId>/files/<file>`.
|
|
20
|
+
* `bundle`, when given, is an index of those files already written for this source (keys under
|
|
21
|
+
* `commits/<sha>/tests/<source>/`); then only `frame.css` is written here and the index is re-keyed to the layer's
|
|
22
|
+
* layout. The same inputs give the same layer, so publishing twice is harmless. Store errors reject.
|
|
23
|
+
*/
|
|
6
24
|
export declare function archiveSourceLayer(store: ObjectStore, args: PublishSourceArgs, source: SourceRecord, tests: readonly HtmlReportTest[], bundle?: BundleIndex): Promise<import('./layer-types.js').ReportLayer>;
|
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import { writeBundle } from './bundle.js';
|
|
2
|
+
import { normalizeEnvironment } from './layer-axes.js';
|
|
2
3
|
import { readCommit, readFrameCss } from './read.js';
|
|
3
4
|
import { readInventory } from './spec-inventory.js';
|
|
4
5
|
import { sourceParts } from './source-part.js';
|
|
5
6
|
import { sourceEntries } from './source-entries.js';
|
|
6
7
|
import { publishLayerBundle } from './layer-store.js';
|
|
8
|
+
/**
|
|
9
|
+
* Writes `source` and its recorded files as an immutable layer and returns it (see `publishLayerBundle`).
|
|
10
|
+
*
|
|
11
|
+
* The layer is recorded at the later of the commit's start and the source's `updatedAtMs`, with the commit's branch and
|
|
12
|
+
* run URL, the commit's spec inventory (empty when none), the source's parts, `parameters` from `args`, the environment
|
|
13
|
+
* normalised from `args.environment` or the `environment` parameter, and `recording: { type: 'layer' }`. Its bundle
|
|
14
|
+
* holds the commit's `frame.css` and each test's `tests/<testId>/report.json` and `tests/<testId>/files/<file>`.
|
|
15
|
+
* `bundle`, when given, is an index of those files already written for this source (keys under
|
|
16
|
+
* `commits/<sha>/tests/<source>/`); then only `frame.css` is written here and the index is re-keyed to the layer's
|
|
17
|
+
* layout. The same inputs give the same layer, so publishing twice is harmless. Store errors reject.
|
|
18
|
+
*/
|
|
7
19
|
export async function archiveSourceLayer(store, args, source, tests, bundle) {
|
|
8
20
|
const [commit, inventory, css] = await Promise.all([readCommit(store, args.sha), readInventory(store, args.sha), readFrameCss(store, args.sha)]);
|
|
9
21
|
const recordedAtMs = Math.max(commit?.startedAtMs ?? 0, source.updatedAtMs);
|
|
@@ -25,8 +37,10 @@ export async function archiveSourceLayer(store, args, source, tests, bundle) {
|
|
|
25
37
|
const index = await writeBundle(store, entries(), 'layers/blobs/');
|
|
26
38
|
for (const [key, value] of Object.entries(bundle ?? {}))
|
|
27
39
|
index[layerPath(key)] = value;
|
|
40
|
+
const environment = normalizeEnvironment(args.environment ?? args.parameters?.environment);
|
|
28
41
|
return publishLayerBundle(store, {
|
|
29
42
|
...(args.freshness ? { freshness: args.freshness } : {}), producer: source.source, recordedAtMs, sha: args.sha, branch: commit?.branch ?? null, runUrl: commit?.runUrl ?? null,
|
|
43
|
+
...(environment ? { environment } : {}),
|
|
30
44
|
parameters: args.parameters ?? {}, specs: inventory?.entries ?? [], parts: sourceParts(args.sha, source), source, recording: { type: 'layer' },
|
|
31
45
|
}, index);
|
|
32
46
|
}
|
|
@@ -1,10 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer storage
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes layers in an `ObjectStore`: the immutable manifest at `layers/manifests/<id>.json`, the catalog
|
|
5
|
+
* entry at `layers/index/<inverted-time>-<id>.json`, and the captured files under `layers/blobs/`. A layer is written
|
|
6
|
+
* bottom up (files, then manifest, then catalog entry) so a reader never meets an entry whose content has not arrived,
|
|
7
|
+
* and every key is derived from content so two producers never write different bytes to one key.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md#where-bytes-go
|
|
10
|
+
*/
|
|
1
11
|
import { type BundleIndex, type BundleEntry } from './bundle.js';
|
|
2
12
|
import type { ObjectStore } from './object-store.js';
|
|
3
13
|
import type { LayerSummary, ReportLayer } from './layer-types.js';
|
|
14
|
+
/** The key prefix under which every layer's catalog entry is written. */
|
|
4
15
|
export declare const LAYER_INDEX = "layers/index/";
|
|
16
|
+
/** The store key of a layer's manifest. Throws `Invalid layer id` unless `id` is 64 lowercase hex digits, so an id can never address another key. */
|
|
5
17
|
export declare function layerKey(id: string): string;
|
|
18
|
+
/**
|
|
19
|
+
* Writes a layer whose captured files arrive as a stream, then its manifest and catalog entry, and returns it.
|
|
20
|
+
*
|
|
21
|
+
* The files are packed into content-addressed blobs under `layers/blobs/` first (`writeBundle`), then
|
|
22
|
+
* `publishLayerBundle` does the rest. The id is computed from the content, so publishing the same layer again
|
|
23
|
+
* writes the same keys.
|
|
24
|
+
*/
|
|
6
25
|
export declare function publishLayer(store: ObjectStore, summary: Omit<LayerSummary, 'id'>, entries: AsyncIterable<BundleEntry>): Promise<ReportLayer>;
|
|
26
|
+
/**
|
|
27
|
+
* The key of a layer's catalog entry: `layers/index/<16 digits>-<id>.json`, where the digits are
|
|
28
|
+
* `Number.MAX_SAFE_INTEGER - floor(max(0, recordedAtMs))` zero-padded. The time is inverted so a listing in key order
|
|
29
|
+
* is newest first. The entry makes a layer visible, so it is written last, after the layer and its files.
|
|
30
|
+
*/
|
|
31
|
+
export declare function layerCatalogKey(recordedAtMs: number, id: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Writes a layer whose captured files are already stored (`bundle` locates them) and returns it with its id.
|
|
34
|
+
*
|
|
35
|
+
* The id is the SHA-256 of the JSON of `{ schemaVersion, ...summary, bundle }`, so recording the same thing the same
|
|
36
|
+
* way gives the same id. Writes the manifest only if `head` says it is absent, then always writes the catalog entry
|
|
37
|
+
* (same key, same bytes on a repeat), so a retry is harmless and a crash between the two leaves a manifest no catalog
|
|
38
|
+
* entry names, never the reverse. Store errors reject; nothing is validated here (the publishers do that).
|
|
39
|
+
*/
|
|
7
40
|
export declare function publishLayerBundle(store: ObjectStore, summary: Omit<LayerSummary, 'id'>, bundle: BundleIndex): Promise<ReportLayer>;
|
|
41
|
+
/**
|
|
42
|
+
* Reads and validates a layer manifest, returning it without `schemaVersion`, or `null` when no manifest exists.
|
|
43
|
+
*
|
|
44
|
+
* A manifest that is present but cannot be read is an error naming why, never a half-read layer: it throws when the
|
|
45
|
+
* bytes are not JSON, fail the layer schema, or are a newer major schema version. Callers turn that into a notice,
|
|
46
|
+
* which fails the gate. Throws `Invalid layer id` for a malformed `id`. The id is not recomputed from the content.
|
|
47
|
+
*/
|
|
8
48
|
export declare function readLayer(store: ObjectStore, id: string): Promise<ReportLayer | null>;
|
|
49
|
+
/** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
|
|
9
50
|
export declare function readLayerFile(store: ObjectStore, id: string, file: string): Promise<BundleEntry | null>;
|
|
51
|
+
/** Drops a layer's bundle index, leaving the summary composition reads. */
|
|
10
52
|
export declare function layerSummary(layer: ReportLayer): LayerSummary;
|
|
@@ -1,35 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Layer storage
|
|
3
|
+
*
|
|
4
|
+
* Reads and writes layers in an `ObjectStore`: the immutable manifest at `layers/manifests/<id>.json`, the catalog
|
|
5
|
+
* entry at `layers/index/<inverted-time>-<id>.json`, and the captured files under `layers/blobs/`. A layer is written
|
|
6
|
+
* bottom up (files, then manifest, then catalog entry) so a reader never meets an entry whose content has not arrived,
|
|
7
|
+
* and every key is derived from content so two producers never write different bytes to one key.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md#where-bytes-go
|
|
10
|
+
*/
|
|
1
11
|
import { createBundleReader, writeBundle } from './bundle.js';
|
|
2
12
|
import { readUtf8, writeUtf8 } from './object-io.js';
|
|
13
|
+
import { LAYER_SCHEMA_VERSION, parseLayerDocument } from '../../schema/src/layer.js';
|
|
14
|
+
/** The key prefix under which every layer's catalog entry is written. */
|
|
3
15
|
export const LAYER_INDEX = 'layers/index/';
|
|
16
|
+
/** The store key of a layer's manifest. Throws `Invalid layer id` unless `id` is 64 lowercase hex digits, so an id can never address another key. */
|
|
4
17
|
export function layerKey(id) {
|
|
5
18
|
if (!/^[0-9a-f]{64}$/.test(id))
|
|
6
19
|
throw new Error('Invalid layer id');
|
|
7
20
|
return `layers/manifests/${id}.json`;
|
|
8
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Writes a layer whose captured files arrive as a stream, then its manifest and catalog entry, and returns it.
|
|
24
|
+
*
|
|
25
|
+
* The files are packed into content-addressed blobs under `layers/blobs/` first (`writeBundle`), then
|
|
26
|
+
* `publishLayerBundle` does the rest. The id is computed from the content, so publishing the same layer again
|
|
27
|
+
* writes the same keys.
|
|
28
|
+
*/
|
|
9
29
|
export async function publishLayer(store, summary, entries) {
|
|
10
30
|
const bundle = await writeBundle(store, entries, 'layers/blobs/');
|
|
11
31
|
return publishLayerBundle(store, summary, bundle);
|
|
12
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The key of a layer's catalog entry: `layers/index/<16 digits>-<id>.json`, where the digits are
|
|
35
|
+
* `Number.MAX_SAFE_INTEGER - floor(max(0, recordedAtMs))` zero-padded. The time is inverted so a listing in key order
|
|
36
|
+
* is newest first. The entry makes a layer visible, so it is written last, after the layer and its files.
|
|
37
|
+
*/
|
|
38
|
+
export function layerCatalogKey(recordedAtMs, id) {
|
|
39
|
+
return `${LAYER_INDEX}${String(Number.MAX_SAFE_INTEGER - Math.max(0, Math.floor(recordedAtMs))).padStart(16, '0')}-${id}.json`;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Writes a layer whose captured files are already stored (`bundle` locates them) and returns it with its id.
|
|
43
|
+
*
|
|
44
|
+
* The id is the SHA-256 of the JSON of `{ schemaVersion, ...summary, bundle }`, so recording the same thing the same
|
|
45
|
+
* way gives the same id. Writes the manifest only if `head` says it is absent, then always writes the catalog entry
|
|
46
|
+
* (same key, same bytes on a repeat), so a retry is harmless and a crash between the two leaves a manifest no catalog
|
|
47
|
+
* entry names, never the reverse. Store errors reject; nothing is validated here (the publishers do that).
|
|
48
|
+
*/
|
|
13
49
|
export async function publishLayerBundle(store, summary, bundle) {
|
|
14
|
-
|
|
50
|
+
// The id is the digest of exactly this document without it, so every producer that records the same thing writes the same layer.
|
|
51
|
+
const body = JSON.stringify({ schemaVersion: LAYER_SCHEMA_VERSION, ...summary, bundle });
|
|
15
52
|
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(body));
|
|
16
53
|
const id = [...new Uint8Array(digest)].map(byte => byte.toString(16).padStart(2, '0')).join('');
|
|
17
54
|
const layer = { ...summary, id, bundle };
|
|
18
55
|
// Immutable manifest first, independent catalog entry last: concurrent producers never overwrite one another.
|
|
19
56
|
if (!(await store.head(layerKey(id))))
|
|
20
|
-
await writeUtf8(store, layerKey(id), JSON.stringify(layer), 'application/json');
|
|
21
|
-
|
|
22
|
-
await writeUtf8(store, `${LAYER_INDEX}${inverse}-${id}.json`, JSON.stringify({ id }), 'application/json');
|
|
57
|
+
await writeUtf8(store, layerKey(id), JSON.stringify({ schemaVersion: LAYER_SCHEMA_VERSION, ...layer }), 'application/json');
|
|
58
|
+
await writeUtf8(store, layerCatalogKey(summary.recordedAtMs, id), JSON.stringify({ id }), 'application/json');
|
|
23
59
|
return layer;
|
|
24
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Reads and validates a layer manifest, returning it without `schemaVersion`, or `null` when no manifest exists.
|
|
63
|
+
*
|
|
64
|
+
* A manifest that is present but cannot be read is an error naming why, never a half-read layer: it throws when the
|
|
65
|
+
* bytes are not JSON, fail the layer schema, or are a newer major schema version. Callers turn that into a notice,
|
|
66
|
+
* which fails the gate. Throws `Invalid layer id` for a malformed `id`. The id is not recomputed from the content.
|
|
67
|
+
*/
|
|
25
68
|
export async function readLayer(store, id) {
|
|
26
69
|
const raw = await readUtf8(store, layerKey(id));
|
|
27
|
-
|
|
70
|
+
if (raw === null)
|
|
71
|
+
return null;
|
|
72
|
+
let json;
|
|
73
|
+
try {
|
|
74
|
+
json = JSON.parse(raw);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
throw new Error(`Layer ${id.slice(0, 12)} is not JSON`);
|
|
78
|
+
}
|
|
79
|
+
const parsed = parseLayerDocument(json);
|
|
80
|
+
if (!parsed.ok)
|
|
81
|
+
throw new Error(`Layer ${id.slice(0, 12)}: ${parsed.reason}`);
|
|
82
|
+
const { schemaVersion: _version, ...layer } = parsed.layer;
|
|
83
|
+
return layer;
|
|
28
84
|
}
|
|
85
|
+
/** Reads one captured file (a screenshot, a frame, a recorded report) of a layer, or `null` when the layer or the file is missing. Throws like `readLayer`. */
|
|
29
86
|
export async function readLayerFile(store, id, file) {
|
|
30
87
|
const layer = await readLayer(store, id);
|
|
31
88
|
return layer ? createBundleReader(store, layer.bundle)(file) : null;
|
|
32
89
|
}
|
|
90
|
+
/** Drops a layer's bundle index, leaving the summary composition reads. */
|
|
33
91
|
export function layerSummary(layer) {
|
|
34
92
|
const { bundle: _, ...summary } = layer;
|
|
35
93
|
return summary;
|