@crvouga/atlas 0.4.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 +7 -1
- package/README.md +178 -617
- 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 +35 -5
- package/dist/reports/src/evidence-live.js +18 -2
- package/dist/reports/src/evidence-types.d.ts +68 -1
- package/dist/reports/src/evidence-types.js +5 -1
- package/dist/reports/src/evidence-view.d.ts +43 -4
- package/dist/reports/src/evidence-view.js +13 -2
- package/dist/reports/src/explorer-types.d.ts +40 -3
- package/dist/reports/src/explorer-types.js +8 -0
- package/dist/reports/src/explorer-versions.d.ts +9 -1
- package/dist/reports/src/explorer-versions.js +18 -1
- package/dist/reports/src/export-evidence.d.ts +16 -1
- package/dist/reports/src/export-evidence.js +52 -1
- package/dist/reports/src/fetcher.d.ts +27 -3
- package/dist/reports/src/fetcher.js +11 -3
- package/dist/reports/src/format-date.d.ts +6 -0
- package/dist/reports/src/format-date.js +6 -0
- package/dist/reports/src/format.d.ts +16 -0
- package/dist/reports/src/format.js +10 -0
- package/dist/reports/src/frames.d.ts +36 -3
- package/dist/reports/src/frames.js +22 -3
- package/dist/reports/src/hash.d.ts +18 -0
- package/dist/reports/src/hash.js +13 -0
- package/dist/reports/src/host.d.ts +29 -0
- package/dist/reports/src/host.js +64 -0
- package/dist/reports/src/html-frame.d.ts +5 -6
- package/dist/reports/src/html-frame.js +12 -1
- package/dist/reports/src/http-store.d.ts +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 +35 -5
- package/dist/reports/src/layer-axes.js +25 -5
- package/dist/reports/src/layer-catalog.d.ts +28 -1
- package/dist/reports/src/layer-catalog.js +18 -1
- package/dist/reports/src/layer-merge.d.ts +98 -9
- package/dist/reports/src/layer-merge.js +147 -31
- package/dist/reports/src/layer-source.d.ts +18 -0
- package/dist/reports/src/layer-source.js +11 -0
- package/dist/reports/src/layer-store.d.ts +42 -0
- package/dist/reports/src/layer-store.js +63 -5
- package/dist/reports/src/layer-types.d.ts +32 -1
- package/dist/reports/src/layers-route.d.ts +47 -4
- package/dist/reports/src/layers-route.js +86 -7
- package/dist/reports/src/media-types.d.ts +11 -0
- package/dist/reports/src/media-types.js +22 -0
- package/dist/reports/src/namespace-guard.d.ts +10 -1
- package/dist/reports/src/namespace-guard.js +10 -1
- package/dist/reports/src/navigation.d.ts +12 -0
- package/dist/reports/src/navigation.js +7 -0
- package/dist/reports/src/object-io.d.ts +11 -4
- package/dist/reports/src/object-io.js +4 -4
- package/dist/reports/src/object-store-conformance.d.ts +27 -0
- package/dist/reports/src/object-store-conformance.js +87 -0
- package/dist/reports/src/object-store.d.ts +33 -24
- package/dist/reports/src/object-store.js +17 -24
- package/dist/reports/src/parse-with.d.ts +16 -0
- package/dist/reports/src/parse-with.js +14 -0
- package/dist/reports/src/performance-part.d.ts +25 -1
- package/dist/reports/src/performance-part.js +15 -1
- package/dist/reports/src/performance.d.ts +22 -2
- package/dist/reports/src/performance.js +12 -2
- package/dist/reports/src/persist.d.ts +12 -3
- package/dist/reports/src/persist.js +12 -3
- package/dist/reports/src/phone.d.ts +18 -1
- package/dist/reports/src/phone.js +18 -1
- package/dist/reports/src/protocol.d.ts +26 -4
- package/dist/reports/src/protocol.js +15 -4
- package/dist/reports/src/publication.d.ts +21 -1
- package/dist/reports/src/publication.js +21 -1
- package/dist/reports/src/publish-checks.d.ts +35 -0
- package/dist/reports/src/publish-checks.js +122 -0
- package/dist/reports/src/publish.d.ts +42 -9
- package/dist/reports/src/publish.js +36 -7
- package/dist/reports/src/read.d.ts +46 -16
- package/dist/reports/src/read.js +38 -14
- package/dist/reports/src/recording-bundle.d.ts +13 -2
- package/dist/reports/src/recording-bundle.js +4 -2
- package/dist/reports/src/recording-keys.d.ts +15 -1
- package/dist/reports/src/recording-keys.js +15 -1
- package/dist/reports/src/remote-report.d.ts +28 -7
- package/dist/reports/src/remote-report.js +15 -7
- package/dist/reports/src/report-cli.d.ts +51 -0
- package/dist/reports/src/report-cli.js +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 +37 -3
- package/dist/reports/src/report-status.d.ts +48 -0
- package/dist/reports/src/report-status.js +156 -0
- package/dist/reports/src/report-types.d.ts +73 -0
- package/dist/reports/src/report-types.js +3 -0
- package/dist/reports/src/reporter.d.ts +67 -0
- package/dist/reports/src/reporter.js +128 -0
- package/dist/reports/src/reports-explorer-route.d.ts +541 -0
- package/dist/reports/src/reports-explorer-route.js +11 -0
- package/dist/reports/src/reports-list-route.d.ts +9 -0
- package/dist/reports/src/reports-list-route.js +9 -0
- package/dist/reports/src/reports-route.d.ts +561 -22
- package/dist/reports/src/reports-route.js +11 -3
- package/dist/reports/src/reports-store.d.ts +19 -2
- package/dist/reports/src/reports-store.js +10 -2
- package/dist/reports/src/resilient-cache.d.ts +14 -1
- package/dist/reports/src/resilient-cache.js +8 -1
- package/dist/reports/src/retention.d.ts +17 -1
- package/dist/reports/src/retention.js +5 -1
- package/dist/reports/src/run-bundle.d.ts +63 -0
- package/dist/reports/src/run-bundle.js +159 -0
- package/dist/reports/src/s3-store.d.ts +79 -0
- package/dist/reports/src/s3-store.js +295 -0
- package/dist/reports/src/scenario.d.ts +14 -4
- package/dist/reports/src/scenario.js +5 -4
- package/dist/reports/src/sigv4.d.ts +72 -0
- package/dist/reports/src/sigv4.js +96 -0
- package/dist/reports/src/snapshot-bundle.d.ts +24 -1
- package/dist/reports/src/snapshot-bundle.js +27 -2
- package/dist/reports/src/snapshot-surface.d.ts +20 -9
- package/dist/reports/src/snapshot-surface.js +32 -19
- package/dist/reports/src/source-content.d.ts +23 -0
- package/dist/reports/src/source-content.js +30 -1
- package/dist/reports/src/source-entries.d.ts +13 -0
- package/dist/reports/src/source-entries.js +6 -0
- package/dist/reports/src/source-part.d.ts +15 -0
- package/dist/reports/src/source-part.js +8 -0
- package/dist/reports/src/source-reader.d.ts +11 -0
- package/dist/reports/src/source-reader.js +5 -0
- package/dist/reports/src/spec-evidence.d.ts +12 -2
- package/dist/reports/src/spec-evidence.js +6 -2
- package/dist/reports/src/spec-inventory.d.ts +15 -1
- package/dist/reports/src/spec-inventory.js +22 -1
- package/dist/reports/src/spec-link.d.ts +28 -0
- package/dist/reports/src/spec-link.js +70 -0
- package/dist/reports/src/spec-set.d.ts +63 -0
- package/dist/reports/src/spec-set.js +177 -0
- package/dist/reports/src/status.d.ts +10 -0
- package/dist/reports/src/status.js +5 -0
- package/dist/reports/src/store-prefix.d.ts +28 -0
- package/dist/reports/src/store-prefix.js +22 -0
- package/dist/reports/src/store-url.d.ts +89 -0
- package/dist/reports/src/store-url.js +160 -0
- package/dist/reports/src/test-catalog-entity.d.ts +11 -0
- package/dist/reports/src/test-catalog-entity.js +6 -0
- package/dist/reports/src/types.d.ts +6 -1
- package/dist/reports/src/use-report-nav.d.ts +35 -0
- package/dist/reports/src/use-report-nav.js +12 -0
- package/dist/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 +32 -4
- package/packages/core/README.md +2 -1
- package/packages/reports/README.md +65 -0
- package/packages/reports/STORAGE.md +4 -0
- 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 +21 -0
- 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 +40 -3
- 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 +3 -1
- package/packages/visualizer/src/evidence/evidence-context.tsx +74 -12
- 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/vite/atlas-server-proxy.ts +36 -0
- package/packages/visualizer/vite.config.ts +11 -3
|
@@ -1,37 +1,68 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @module Catalog search, filter, group and sort
|
|
3
|
+
*
|
|
4
|
+
* The pure view logic behind the viewer's list of reports (and of the tests inside one): given entities and a
|
|
5
|
+
* query (text, status, grouping, sort), it returns the groups to draw. It owns no data and does no I/O; the
|
|
6
|
+
* caller maps its own records to {@link CatalogEntity}. Input is never mutated.
|
|
7
|
+
*/
|
|
8
|
+
/** A status filter: one of the four result statuses as the list names them (`skipped` is a check that was `not-run`), or `all`. */
|
|
2
9
|
export type CatalogStatus = 'all' | 'passed' | 'failed' | 'skipped' | 'running';
|
|
10
|
+
/** What the list is grouped by; `none` gives one group named `All`. */
|
|
3
11
|
export type CatalogGroup = 'none' | 'file' | 'status' | 'package' | 'runtime';
|
|
12
|
+
/** How entities are ordered inside a group: `recent` keeps the input order, `name` is by title, `duration` is longest first, `status` is failed first then by title. */
|
|
4
13
|
export type CatalogSort = 'recent' | 'name' | 'duration' | 'status';
|
|
14
|
+
/** How the list is drawn. */
|
|
5
15
|
export type CatalogLayout = 'list' | 'grid';
|
|
16
|
+
/** The user's current choices, mirrored in the URL (see {@link catalogParams}). */
|
|
6
17
|
export type CatalogQuery = {
|
|
18
|
+
/** Free text; matched case-insensitively as a substring of each entity's `search` text. Blank matches everything. */
|
|
7
19
|
readonly q: string;
|
|
8
20
|
readonly status: CatalogStatus;
|
|
9
21
|
readonly group: CatalogGroup;
|
|
10
22
|
readonly sort: CatalogSort;
|
|
11
23
|
readonly layout: CatalogLayout;
|
|
12
24
|
};
|
|
25
|
+
/** One row of the list: a report or a test, flattened to what the list needs. */
|
|
13
26
|
export type CatalogEntity = {
|
|
27
|
+
/** Stable identifier of the row. */
|
|
14
28
|
readonly id: string;
|
|
15
29
|
readonly title: string;
|
|
30
|
+
/** A status string; only `passed`, `failed`, `skipped` and `running` are counted and ranked, any other sorts after them. */
|
|
16
31
|
readonly status: string;
|
|
32
|
+
/** Duration in milliseconds, or null when unknown (sorted last by `duration`, counted as 0 in a group's total). */
|
|
17
33
|
readonly durationMs: number | null;
|
|
18
34
|
/** Group label for `file` (a source file, or a branch). */
|
|
19
35
|
readonly file: string;
|
|
36
|
+
/** Group label for `package`; empty groups under `Other`. */
|
|
20
37
|
readonly packageName: string;
|
|
38
|
+
/** Group label for `runtime`; empty groups under `Other`. */
|
|
21
39
|
readonly runtime: string;
|
|
22
40
|
/** One muted line under the title: a failure, or a date. */
|
|
23
41
|
readonly detail: string;
|
|
42
|
+
/** Everything the text query may match (title, ids, paths, ...), built by the caller. */
|
|
24
43
|
readonly search: string;
|
|
44
|
+
/** Where selecting the row navigates, or null when it is not a link. */
|
|
25
45
|
readonly href: string | null;
|
|
26
46
|
};
|
|
47
|
+
/** One drawn group of the list. */
|
|
27
48
|
export type CatalogGroupView = {
|
|
28
49
|
readonly name: string;
|
|
50
|
+
/** Sum of the members' durations in milliseconds; unknown durations count as 0. */
|
|
29
51
|
readonly durationMs: number;
|
|
52
|
+
/** How many members have status `failed`. */
|
|
30
53
|
readonly failed: number;
|
|
31
54
|
readonly entities: readonly CatalogEntity[];
|
|
32
55
|
};
|
|
56
|
+
/** Counts entities per status filter (`all` is the total); a status the list does not know is counted only in `all`. */
|
|
33
57
|
export declare function catalogCounts(entities: readonly CatalogEntity[]): Record<CatalogStatus, number>;
|
|
34
|
-
/**
|
|
58
|
+
/** Keeps only the list's own URL search params (`q`, `status`, `group`, `sort`, `layout`, `spec`, `tab`, `partq`) and drops empty ones, so a test route's frame and zoom params do not leak into a link back to the list. */
|
|
35
59
|
export declare function catalogParams(params: Readonly<Record<string, string>>): Record<string, string>;
|
|
36
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* Filters entities by the query's status and text, sorts them, then splits them into one group per value of the
|
|
62
|
+
* chosen key. `recent` keeps input order.
|
|
63
|
+
*
|
|
64
|
+
* Group order: for `status`, failed, running, skipped, passed; otherwise alphabetical by name with `Other` (the
|
|
65
|
+
* label for an empty file, package or runtime) last. With `none` there is a single group named `All`. When
|
|
66
|
+
* nothing matches, no group is returned.
|
|
67
|
+
*/
|
|
37
68
|
export declare function arrangeCatalog(entities: readonly CatalogEntity[], query: CatalogQuery): readonly CatalogGroupView[];
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* @module Catalog search, filter, group and sort
|
|
3
|
+
*
|
|
4
|
+
* The pure view logic behind the viewer's list of reports (and of the tests inside one): given entities and a
|
|
5
|
+
* query (text, status, grouping, sort), it returns the groups to draw. It owns no data and does no I/O; the
|
|
6
|
+
* caller maps its own records to {@link CatalogEntity}. Input is never mutated.
|
|
7
|
+
*/
|
|
2
8
|
const STATUS_RANK = {
|
|
3
9
|
failed: 0,
|
|
4
10
|
running: 1,
|
|
5
11
|
skipped: 2,
|
|
6
12
|
passed: 3,
|
|
7
13
|
};
|
|
14
|
+
/** Counts entities per status filter (`all` is the total); a status the list does not know is counted only in `all`. */
|
|
8
15
|
export function catalogCounts(entities) {
|
|
9
16
|
const counts = {
|
|
10
17
|
all: entities.length,
|
|
@@ -83,7 +90,7 @@ const LIST_KEYS = [
|
|
|
83
90
|
'tab',
|
|
84
91
|
'partq',
|
|
85
92
|
];
|
|
86
|
-
/**
|
|
93
|
+
/** Keeps only the list's own URL search params (`q`, `status`, `group`, `sort`, `layout`, `spec`, `tab`, `partq`) and drops empty ones, so a test route's frame and zoom params do not leak into a link back to the list. */
|
|
87
94
|
export function catalogParams(params) {
|
|
88
95
|
const out = {};
|
|
89
96
|
for (const key of LIST_KEYS) {
|
|
@@ -93,7 +100,14 @@ export function catalogParams(params) {
|
|
|
93
100
|
}
|
|
94
101
|
return out;
|
|
95
102
|
}
|
|
96
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Filters entities by the query's status and text, sorts them, then splits them into one group per value of the
|
|
105
|
+
* chosen key. `recent` keeps input order.
|
|
106
|
+
*
|
|
107
|
+
* Group order: for `status`, failed, running, skipped, passed; otherwise alphabetical by name with `Other` (the
|
|
108
|
+
* label for an empty file, package or runtime) last. With `none` there is a single group named `All`. When
|
|
109
|
+
* nothing matches, no group is returned.
|
|
110
|
+
*/
|
|
97
111
|
export function arrangeCatalog(entities, query) {
|
|
98
112
|
const shown = entities.filter((entity) => matches(entity, query));
|
|
99
113
|
const sorted = query.sort === 'recent'
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Stores named on a command line
|
|
3
|
+
*
|
|
4
|
+
* Turns `--store`, `--backends` and `ATLAS_STORE` into opened backends for the `atlas report` commands, and reads the
|
|
5
|
+
* small flag grammar those commands share (`--name value`, repeatable). A mistyped directory is an error here, never
|
|
6
|
+
* an empty store: a gate that read nothing must not read as a pass.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/deploying.md
|
|
9
|
+
*/
|
|
10
|
+
import type { BackendInput } from './compose.js';
|
|
11
|
+
import { type OpenedStore } from './store-url.js';
|
|
12
|
+
type Env = Readonly<Record<string, string | undefined>>;
|
|
13
|
+
/** A backend of a command: what `combineBackends` reads, plus, when it came from a URL, how it was opened (`location`, `server`, `s3`). */
|
|
14
|
+
export type CliBackend = BackendInput & Partial<OpenedStore>;
|
|
15
|
+
/** Every value given for `--<name>`, in order. `--name` with nothing after it is ignored. */
|
|
16
|
+
export declare function values(argv: readonly string[], name: string): string[];
|
|
17
|
+
/** The first value given for `--<name>`, or `undefined`. */
|
|
18
|
+
export declare function value(argv: readonly string[], name: string): string | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* Every store a command reads: `--store` (a directory or `s3://…`, optionally `id=url`), then
|
|
21
|
+
* `--backends <module>`, or `ATLAS_STORE` when neither is given. A directory that is not there is an
|
|
22
|
+
* error: a mistyped path must never read as an empty store.
|
|
23
|
+
*/
|
|
24
|
+
export declare function backendsOf(argv: readonly string[], base: string, env: Env): Promise<CliBackend[]>;
|
|
25
|
+
/** The one store a command writes to. A directory is created; a bucket must already exist. */
|
|
26
|
+
export declare function writeStoreOf(flag: string | undefined, base: string, env: Env, name?: string): OpenedStore;
|
|
27
|
+
/**
|
|
28
|
+
* Which layers take part, from `--branch <name>` (repeatable) and `--ignore-modified`. By default all do:
|
|
29
|
+
* a report is whatever the newest results are, wherever they were recorded. A gate that must speak for
|
|
30
|
+
* a branch narrows it; the narrowing is yours to choose, never Atlas's.
|
|
31
|
+
*/
|
|
32
|
+
export declare function layerFilter(argv: readonly string[], env: Env): ((layer: {
|
|
33
|
+
readonly branch: string | null;
|
|
34
|
+
readonly freshness?: 'unknown' | 'modified';
|
|
35
|
+
}) => boolean) | undefined;
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Stores named on a command line
|
|
3
|
+
*
|
|
4
|
+
* Turns `--store`, `--backends` and `ATLAS_STORE` into opened backends for the `atlas report` commands, and reads the
|
|
5
|
+
* small flag grammar those commands share (`--name value`, repeatable). A mistyped directory is an error here, never
|
|
6
|
+
* an empty store: a gate that read nothing must not read as a pass.
|
|
7
|
+
*
|
|
8
|
+
* @see docs/deploying.md
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync, mkdirSync, statSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { pathToFileURL } from 'node:url';
|
|
13
|
+
import { openBackends, openStore, storeSpecs } from './store-url.js';
|
|
14
|
+
/** Every value given for `--<name>`, in order. `--name` with nothing after it is ignored. */
|
|
15
|
+
export function values(argv, name) {
|
|
16
|
+
return argv.flatMap((token, i) => (token === `--${name}` && argv[i + 1] !== undefined ? [argv[i + 1]] : []));
|
|
17
|
+
}
|
|
18
|
+
/** The first value given for `--<name>`, or `undefined`. */
|
|
19
|
+
export function value(argv, name) {
|
|
20
|
+
return values(argv, name)[0];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Every store a command reads: `--store` (a directory or `s3://…`, optionally `id=url`), then
|
|
24
|
+
* `--backends <module>`, or `ATLAS_STORE` when neither is given. A directory that is not there is an
|
|
25
|
+
* error: a mistyped path must never read as an empty store.
|
|
26
|
+
*/
|
|
27
|
+
export async function backendsOf(argv, base, env) {
|
|
28
|
+
const specs = storeSpecs(values(argv, 'store'), env);
|
|
29
|
+
const backends = openBackends(specs, { base, env });
|
|
30
|
+
backends.forEach((backend, i) => {
|
|
31
|
+
const location = backend.location;
|
|
32
|
+
if (location?.kind === 'directory' && (!existsSync(location.directory) || !statSync(location.directory).isDirectory())) {
|
|
33
|
+
throw new Error(`Store directory not found: ${specs[i].replace(/^[a-z0-9-]+=/, '')}. If a node failed to publish or upload, fix that first`);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const used = new Set(backends.map(backend => backend.id));
|
|
37
|
+
for (const file of values(argv, 'backends')) {
|
|
38
|
+
const mod = (await import(pathToFileURL(path.resolve(base, file)).href));
|
|
39
|
+
if (!mod.default)
|
|
40
|
+
throw new Error(`${file} has no default export: a list of { id, label?, store }`);
|
|
41
|
+
for (const backend of typeof mod.default === 'function' ? await mod.default() : mod.default) {
|
|
42
|
+
if (used.has(backend.id))
|
|
43
|
+
throw new Error(`Two stores are named "${backend.id}"`);
|
|
44
|
+
used.add(backend.id);
|
|
45
|
+
backends.push(backend);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!backends.length)
|
|
49
|
+
throw new Error('Give at least one --store <dir or s3://bucket/prefix> (or --backends <module>), or set ATLAS_STORE');
|
|
50
|
+
return backends;
|
|
51
|
+
}
|
|
52
|
+
/** The one store a command writes to. A directory is created; a bucket must already exist. */
|
|
53
|
+
export function writeStoreOf(flag, base, env, name = 'store') {
|
|
54
|
+
const specs = flag ? [flag] : storeSpecs([], env);
|
|
55
|
+
if (specs.length !== 1)
|
|
56
|
+
throw new Error(specs.length ? `Give one --${name} to write to (ATLAS_STORE names ${specs.length})` : `Give --${name} <dir or s3://bucket/prefix>, or set ATLAS_STORE`);
|
|
57
|
+
const opened = openStore(/^([a-z0-9-]+)=(.+)$/.exec(specs[0])?.[2] ?? specs[0], { base, env });
|
|
58
|
+
if (opened.location.kind === 'directory')
|
|
59
|
+
mkdirSync(opened.location.directory, { recursive: true });
|
|
60
|
+
return opened;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Which layers take part, from `--branch <name>` (repeatable) and `--ignore-modified`. By default all do:
|
|
64
|
+
* a report is whatever the newest results are, wherever they were recorded. A gate that must speak for
|
|
65
|
+
* a branch narrows it; the narrowing is yours to choose, never Atlas's.
|
|
66
|
+
*/
|
|
67
|
+
export function layerFilter(argv, env) {
|
|
68
|
+
const branches = [...values(argv, 'branch'), ...(env.ATLAS_ONLY_BRANCH ?? '').split(/[\s,]+/).filter(Boolean)];
|
|
69
|
+
const ignoreModified = argv.includes('--ignore-modified') || env.ATLAS_IGNORE_MODIFIED === '1' || env.ATLAS_IGNORE_MODIFIED === 'true';
|
|
70
|
+
if (!branches.length && !ignoreModified)
|
|
71
|
+
return undefined;
|
|
72
|
+
return layer => (!branches.length || (layer.branch !== null && branches.includes(layer.branch))) && (!ignoreModified || layer.freshness === undefined);
|
|
73
|
+
}
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Injectable
|
|
3
|
-
*
|
|
2
|
+
* @module Injectable time
|
|
3
|
+
*
|
|
4
|
+
* The two time dependencies of the reports code, as types, so tests and simulations can supply virtual time.
|
|
5
|
+
* Production hosts pass `{ now: Date.now }` and wrappers over the global timers (see `host.ts`). There is no
|
|
6
|
+
* default implementation in this module.
|
|
4
7
|
*/
|
|
8
|
+
/** The clock a reader or route uses to decide when polling or tailing has run long enough. */
|
|
5
9
|
export type Clock = {
|
|
6
10
|
/** Epoch milliseconds (virtual or wall). */
|
|
7
11
|
readonly now: () => number;
|
|
8
12
|
};
|
|
13
|
+
/** The timer functions the reports code schedules work with; each handle is opaque and only passed back to the matching `clear*`. */
|
|
9
14
|
export type Timers = {
|
|
10
15
|
readonly setTimeout: (fn: () => void, ms: number) => unknown;
|
|
11
16
|
readonly clearTimeout: (handle: unknown) => void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Injectable
|
|
3
|
-
*
|
|
2
|
+
* @module Injectable time
|
|
3
|
+
*
|
|
4
|
+
* The two time dependencies of the reports code, as types, so tests and simulations can supply virtual time.
|
|
5
|
+
* Production hosts pass `{ now: Date.now }` and wrappers over the global timers (see `host.ts`). There is no
|
|
6
|
+
* default implementation in this module.
|
|
4
7
|
*/
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @module Bounded concurrency
|
|
3
|
+
*
|
|
4
|
+
* Small helpers that run async work with a cap on how many tasks are in flight, used for reading and writing many
|
|
5
|
+
* store objects without flooding the backend. They differ from `Promise.all` in three ways: the cap, results in
|
|
6
|
+
* completion order (not input order), and failure handling: once one task rejects, no new task is started,
|
|
7
|
+
* those already started are allowed to finish, and then the first error is rethrown.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Runs `tasks` at most `limit` at a time (at least 1) and yields each result the moment it
|
|
11
|
+
* settles, so one slow read never holds back faster siblings. Results come in completion order, not input
|
|
12
|
+
* order. A rejection is rethrown to the consumer once every started task has settled; no results are yielded
|
|
13
|
+
* after the first failure.
|
|
5
14
|
*/
|
|
6
15
|
export declare function inCompletionOrder<T>(tasks: readonly (() => Promise<T>)[], limit: number): AsyncGenerator<T>;
|
|
7
|
-
/**
|
|
16
|
+
/** Applies `work` to every item, at most `limit` at a time, and resolves when all are done; rejects with the first error as {@link inCompletionOrder} does. */
|
|
8
17
|
export declare function eachLimited<T>(items: readonly T[], limit: number, work: (item: T) => Promise<void>): Promise<void>;
|
|
9
|
-
/**
|
|
18
|
+
/**
|
|
19
|
+
* Consumes a lazy (async) input with backpressure: it pulls the next item only while fewer than `limit` works are
|
|
20
|
+
* running, so a huge or generated input is never held in memory at once. After a failure it stops pulling,
|
|
21
|
+
* waits for the started work to settle, then rethrows the first error.
|
|
22
|
+
*/
|
|
10
23
|
export declare function eachLimitedStream<T>(items: AsyncIterable<T>, limit: number, work: (item: T) => Promise<void>): Promise<void>;
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* @module Bounded concurrency
|
|
3
|
+
*
|
|
4
|
+
* Small helpers that run async work with a cap on how many tasks are in flight, used for reading and writing many
|
|
5
|
+
* store objects without flooding the backend. They differ from `Promise.all` in three ways: the cap, results in
|
|
6
|
+
* completion order (not input order), and failure handling: once one task rejects, no new task is started,
|
|
7
|
+
* those already started are allowed to finish, and then the first error is rethrown.
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* Runs `tasks` at most `limit` at a time (at least 1) and yields each result the moment it
|
|
11
|
+
* settles, so one slow read never holds back faster siblings. Results come in completion order, not input
|
|
12
|
+
* order. A rejection is rethrown to the consumer once every started task has settled; no results are yielded
|
|
13
|
+
* after the first failure.
|
|
5
14
|
*/
|
|
6
15
|
export async function* inCompletionOrder(tasks, limit) {
|
|
7
16
|
const width = Math.max(1, Math.floor(limit));
|
|
@@ -42,14 +51,18 @@ export async function* inCompletionOrder(tasks, limit) {
|
|
|
42
51
|
if (outcome.failure !== null)
|
|
43
52
|
throw outcome.failure.error;
|
|
44
53
|
}
|
|
45
|
-
/**
|
|
54
|
+
/** Applies `work` to every item, at most `limit` at a time, and resolves when all are done; rejects with the first error as {@link inCompletionOrder} does. */
|
|
46
55
|
export async function eachLimited(items, limit, work) {
|
|
47
56
|
const tasks = items.map((item) => () => work(item));
|
|
48
57
|
for await (const _ of inCompletionOrder(tasks, limit)) {
|
|
49
58
|
// Results are void; draining is the point.
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Consumes a lazy (async) input with backpressure: it pulls the next item only while fewer than `limit` works are
|
|
63
|
+
* running, so a huge or generated input is never held in memory at once. After a failure it stops pulling,
|
|
64
|
+
* waits for the started work to settle, then rethrows the first error.
|
|
65
|
+
*/
|
|
53
66
|
export async function eachLimitedStream(items, limit, work) {
|
|
54
67
|
const width = Math.max(1, Math.floor(limit));
|
|
55
68
|
const running = new Set();
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { BackendInfo, LayerHeader, StatusCounts } from './evidence-types.js';
|
|
2
|
+
import { type CombinedEvidence, type EvidenceResult } from './layer-merge.js';
|
|
3
|
+
import type { ReportOutline } from './layer-types.js';
|
|
4
|
+
import type { ObjectStore } from './object-store.js';
|
|
5
|
+
/** A store that contributes layers. Any {@link ObjectStore} is one: a directory, a bucket, a device lab. */
|
|
6
|
+
export type BackendInput = {
|
|
7
|
+
/** Stable and unique: lowercase letters, digits and hyphens. It names where a result came from. */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** What a person sees in notices and in the viewer. Defaults to `id`. */
|
|
10
|
+
readonly label?: string;
|
|
11
|
+
/** Where the layers are. Only read from. */
|
|
12
|
+
readonly store: ObjectStore;
|
|
13
|
+
};
|
|
14
|
+
/** Options of {@link combineBackends}. */
|
|
15
|
+
export type CombineOptions = {
|
|
16
|
+
/** The specs the report is laid out against. When omitted, the specs the layers recorded. */
|
|
17
|
+
readonly outline?: ReportOutline;
|
|
18
|
+
/** Layers read at once, across all backends. Default 16. */
|
|
19
|
+
readonly concurrency?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Leave layers out, to see what earlier ones said (a layer for which this returns false takes no part: not in the
|
|
22
|
+
* results, the layer list, the counts, or the recorded specs). Layers are still read; this filters after reading.
|
|
23
|
+
*/
|
|
24
|
+
readonly include?: (layer: LayerHeader) => boolean;
|
|
25
|
+
};
|
|
26
|
+
/** What a report says in numbers. Timings that only qualify a check are not counted as checks. */
|
|
27
|
+
export type ReportSummary = {
|
|
28
|
+
/** Counts of the winning results by status, and their total. */
|
|
29
|
+
readonly checks: StatusCounts & {
|
|
30
|
+
readonly total: number;
|
|
31
|
+
};
|
|
32
|
+
/** The same counts per environment slug, keys sorted; results with no environment are under `unspecified`. */
|
|
33
|
+
readonly byEnvironment: Readonly<Record<string, StatusCounts & {
|
|
34
|
+
readonly total: number;
|
|
35
|
+
}>>;
|
|
36
|
+
/** Specs in the outline with no result at all. */
|
|
37
|
+
readonly specsWithoutEvidence: readonly string[];
|
|
38
|
+
};
|
|
39
|
+
/** A report: the results laid over the specs, with what was read and what could not be. */
|
|
40
|
+
export type CombinedReport = {
|
|
41
|
+
/** Every spec in the outline with the winning result of each of its slots, plus results that belong to no spec in the outline. */
|
|
42
|
+
readonly combined: CombinedEvidence;
|
|
43
|
+
/** Every distinct layer that was read, newest first. A layer several backends hold is one layer. */
|
|
44
|
+
readonly layers: readonly LayerHeader[];
|
|
45
|
+
/** Every backend given, sorted by id, with whether it could be read. */
|
|
46
|
+
readonly backends: readonly BackendInfo[];
|
|
47
|
+
/** What could not be read. A gate treats any of these as unknown, never as a pass. */
|
|
48
|
+
readonly notices: readonly string[];
|
|
49
|
+
/** The numbers of `combined`, computed once for the gate and the viewer. */
|
|
50
|
+
readonly summary: ReportSummary;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Counts results by status. Diagnostic results (parts of kind `performance`, which only qualify a check) are left out
|
|
54
|
+
* whenever any other result is present; if only diagnostics exist, they are counted so the report is not empty.
|
|
55
|
+
*/
|
|
56
|
+
export declare function countResults(results: readonly EvidenceResult[]): StatusCounts & {
|
|
57
|
+
total: number;
|
|
58
|
+
};
|
|
59
|
+
/** Counts a combined report's winning results in total and per environment, and lists the specs that have no result at all. */
|
|
60
|
+
export declare function summarize(combined: CombinedEvidence): ReportSummary;
|
|
61
|
+
/**
|
|
62
|
+
* Reads every layer of every backend and lays them over the specs. Layers are the only input:
|
|
63
|
+
* a report is a function of the set of layers, so the order of backends and layers, and how they
|
|
64
|
+
* were split across machines, branches and runs, does not change it.
|
|
65
|
+
*
|
|
66
|
+
* A backend that cannot be read, or a layer that cannot be understood, is named in `notices` and
|
|
67
|
+
* leaves everything else usable; a gate must treat any notice as unknown, never as a pass. A layer held by
|
|
68
|
+
* several backends is one layer. Only an invalid backend `id` (not lowercase letters, digits and hyphens, or
|
|
69
|
+
* repeated) throws, before anything is read. Backends are only read from.
|
|
70
|
+
*
|
|
71
|
+
* When `options.outline` is omitted, the specs are the ones the included layers recorded (the newest
|
|
72
|
+
* recording of each spec path wins), with no commit sha.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const outline = await loadDirectoryOutline('./specs'); // optional: without it the layers' own specs are used
|
|
76
|
+
* const report = await combineBackends([{ id: 'ci', store: directoryStore('./atlas-reports') }], { outline });
|
|
77
|
+
* report.summary.checks.failed; // number of slots whose winner failed
|
|
78
|
+
* report.notices; // non-empty means something could not be read
|
|
79
|
+
*/
|
|
80
|
+
export declare function combineBackends(backends: readonly BackendInput[], options?: CombineOptions): Promise<CombinedReport>;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Combining backends into one report
|
|
3
|
+
*
|
|
4
|
+
* The entry point that turns stores of layers into a report: it reads every layer from every {@link BackendInput},
|
|
5
|
+
* lays them over the specs, and counts the outcome. It owns reading and failure reporting (an unreadable backend or
|
|
6
|
+
* layer becomes a notice, never a pass); the rule for which result wins a slot lives in `layer-merge.ts`. A report
|
|
7
|
+
* is computed, never stored, and is a function of the set of layers: the order and split of backends does not
|
|
8
|
+
* change it.
|
|
9
|
+
*
|
|
10
|
+
* @see docs/report-contract.md (how layers compose, guarantees)
|
|
11
|
+
*/
|
|
12
|
+
import { eachLimited } from './completion-order.js';
|
|
13
|
+
import { catalogEntries, catalogLayers } from './layer-catalog.js';
|
|
14
|
+
import { combineEvidence, distinctHeaders, emptyCounts, isDiagnosticKind, layerEvidence, specPath } from './layer-merge.js';
|
|
15
|
+
const BACKEND_ID = /^[a-z0-9-]+$/;
|
|
16
|
+
/**
|
|
17
|
+
* Counts results by status. Diagnostic results (parts of kind `performance`, which only qualify a check) are left out
|
|
18
|
+
* whenever any other result is present; if only diagnostics exist, they are counted so the report is not empty.
|
|
19
|
+
*/
|
|
20
|
+
export function countResults(results) {
|
|
21
|
+
const outcomes = results.some(result => !isDiagnosticKind(result.row.kind)) ? results.filter(result => !isDiagnosticKind(result.row.kind)) : results;
|
|
22
|
+
const counts = emptyCounts();
|
|
23
|
+
for (const result of outcomes)
|
|
24
|
+
counts[result.row.status] += 1;
|
|
25
|
+
return { ...counts, total: outcomes.length };
|
|
26
|
+
}
|
|
27
|
+
/** Counts a combined report's winning results in total and per environment, and lists the specs that have no result at all. */
|
|
28
|
+
export function summarize(combined) {
|
|
29
|
+
const results = [...combined.specs.flatMap(entry => entry.results), ...combined.unassigned];
|
|
30
|
+
const environments = new Map();
|
|
31
|
+
for (const result of results) {
|
|
32
|
+
const key = result.layer.environment ?? 'unspecified';
|
|
33
|
+
const group = environments.get(key);
|
|
34
|
+
if (group)
|
|
35
|
+
group.push(result);
|
|
36
|
+
else
|
|
37
|
+
environments.set(key, [result]);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
checks: countResults(results),
|
|
41
|
+
byEnvironment: Object.fromEntries([...environments].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)).map(([key, value]) => [key, countResults(value)])),
|
|
42
|
+
specsWithoutEvidence: combined.specs.filter(entry => entry.results.length === 0).map(entry => entry.spec.path)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Reads every layer of every backend and lays them over the specs. Layers are the only input:
|
|
47
|
+
* a report is a function of the set of layers, so the order of backends and layers, and how they
|
|
48
|
+
* were split across machines, branches and runs, does not change it.
|
|
49
|
+
*
|
|
50
|
+
* A backend that cannot be read, or a layer that cannot be understood, is named in `notices` and
|
|
51
|
+
* leaves everything else usable; a gate must treat any notice as unknown, never as a pass. A layer held by
|
|
52
|
+
* several backends is one layer. Only an invalid backend `id` (not lowercase letters, digits and hyphens, or
|
|
53
|
+
* repeated) throws, before anything is read. Backends are only read from.
|
|
54
|
+
*
|
|
55
|
+
* When `options.outline` is omitted, the specs are the ones the included layers recorded (the newest
|
|
56
|
+
* recording of each spec path wins), with no commit sha.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* const outline = await loadDirectoryOutline('./specs'); // optional: without it the layers' own specs are used
|
|
60
|
+
* const report = await combineBackends([{ id: 'ci', store: directoryStore('./atlas-reports') }], { outline });
|
|
61
|
+
* report.summary.checks.failed; // number of slots whose winner failed
|
|
62
|
+
* report.notices; // non-empty means something could not be read
|
|
63
|
+
*/
|
|
64
|
+
export async function combineBackends(backends, options = {}) {
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const backend of backends) {
|
|
67
|
+
if (!BACKEND_ID.test(backend.id) || seen.has(backend.id))
|
|
68
|
+
throw new Error(`Invalid report backend id: "${backend.id}". Use unique lowercase letters, digits and hyphens`);
|
|
69
|
+
seen.add(backend.id);
|
|
70
|
+
}
|
|
71
|
+
const notices = [];
|
|
72
|
+
const info = [];
|
|
73
|
+
const entries = [];
|
|
74
|
+
await Promise.all(backends.map(async (backend) => {
|
|
75
|
+
try {
|
|
76
|
+
for (const entry of await catalogEntries(backend.store))
|
|
77
|
+
entries.push({ backend, entry });
|
|
78
|
+
info.push({ id: backend.id, label: backend.label ?? backend.id, live: false, available: true });
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
info.push({ id: backend.id, label: backend.label ?? backend.id, live: false, available: false });
|
|
82
|
+
notices.push(`${backend.label ?? backend.id} is unavailable: ${error instanceof Error ? error.message : String(error)}`);
|
|
83
|
+
}
|
|
84
|
+
}));
|
|
85
|
+
entries.sort((a, b) => (a.backend.id < b.backend.id ? -1 : a.backend.id > b.backend.id ? 1 : a.entry < b.entry ? -1 : a.entry > b.entry ? 1 : 0));
|
|
86
|
+
const loaded = [];
|
|
87
|
+
await eachLimited(entries, options.concurrency ?? 16, async ({ backend, entry }) => {
|
|
88
|
+
try {
|
|
89
|
+
for (const summary of await catalogLayers(backend.store, entry)) {
|
|
90
|
+
loaded.push({ evidence: layerEvidence(summary, backend.id), specs: summary.specs });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
notices.push(`${backend.label ?? backend.id}: a layer could not be read, so its outcome is unknown (${error instanceof Error ? error.message : String(error)})`);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
const include = options.include;
|
|
98
|
+
const kept = include ? loaded.filter(item => include(item.evidence.layer)) : loaded;
|
|
99
|
+
const layers = kept.map(item => item.evidence);
|
|
100
|
+
// The specs a report is laid over come from the layers that are in it, not from ones left out.
|
|
101
|
+
const recorded = new Map();
|
|
102
|
+
if (!options.outline) {
|
|
103
|
+
for (const { evidence, specs } of kept) {
|
|
104
|
+
for (const spec of specs) {
|
|
105
|
+
const key = specPath(spec.path);
|
|
106
|
+
const prior = recorded.get(key);
|
|
107
|
+
const at = evidence.layer.recordedAtMs;
|
|
108
|
+
if (!prior || at > prior.at || (at === prior.at && (spec.fingerprint ?? '') > (prior.entry.fingerprint ?? '')))
|
|
109
|
+
recorded.set(key, { entry: { ...spec, path: key }, at });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const outline = options.outline ?? { specs: [...recorded.values()].map(item => item.entry).sort((a, b) => (a.path < b.path ? -1 : 1)), sha: null, changedFiles: [] };
|
|
114
|
+
const combined = combineEvidence(outline, layers);
|
|
115
|
+
const headers = distinctHeaders(layers.map(evidence => evidence.layer)).sort((a, b) => b.recordedAtMs - a.recordedAtMs || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
116
|
+
return { combined, layers: headers, backends: info.sort((a, b) => (a.id < b.id ? -1 : 1)), notices: [...new Set(notices)].sort(), summary: summarize(combined) };
|
|
117
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Copying layers between stores
|
|
3
|
+
*
|
|
4
|
+
* `copyStore` copies the layers one store has and another lacks: how partial reports meet (each CI node publishes to its own
|
|
5
|
+
* store; copying them together is the aggregate). It is idempotent, commutative and safe to repeat, because layers are
|
|
6
|
+
* immutable and named by their content, and it writes in dependency order (files, then documents, then catalog entries) so a
|
|
7
|
+
* reader of the destination, or a server that checks, never sees something before what it names.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md
|
|
10
|
+
*/
|
|
11
|
+
import type { ObjectStore } from './object-store.js';
|
|
12
|
+
/**
|
|
13
|
+
* Copy every layer object (`layers/…`) of `from` that `to` does not have. Only layers: they are
|
|
14
|
+
* immutable, so an object that exists is already right. Commit-keyed legacy objects are mutable and
|
|
15
|
+
* are left where they are. This is how partial reports meet: each CI
|
|
16
|
+
* node publishes into its own store, and copying them together is the aggregate, because layers are
|
|
17
|
+
* immutable, addressed by content, and never share a key with different bytes. It is idempotent,
|
|
18
|
+
* commutative and safe to repeat after a failure.
|
|
19
|
+
*
|
|
20
|
+
* A layer's catalog entry is copied last, and its manifest after its files, so a reader of `to` never sees an
|
|
21
|
+
* entry for a layer whose content has not arrived.
|
|
22
|
+
*/
|
|
23
|
+
export declare function copyStore(from: ObjectStore, to: ObjectStore, options?: {
|
|
24
|
+
readonly concurrency?: number;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
readonly copied: number;
|
|
27
|
+
readonly skipped: number;
|
|
28
|
+
readonly ignored: number;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Copying layers between stores
|
|
3
|
+
*
|
|
4
|
+
* `copyStore` copies the layers one store has and another lacks: how partial reports meet (each CI node publishes to its own
|
|
5
|
+
* store; copying them together is the aggregate). It is idempotent, commutative and safe to repeat, because layers are
|
|
6
|
+
* immutable and named by their content, and it writes in dependency order (files, then documents, then catalog entries) so a
|
|
7
|
+
* reader of the destination, or a server that checks, never sees something before what it names.
|
|
8
|
+
*
|
|
9
|
+
* @see docs/report-contract.md
|
|
10
|
+
*/
|
|
11
|
+
import { eachLimited } from './completion-order.js';
|
|
12
|
+
import { readBytes } from './object-io.js';
|
|
13
|
+
/**
|
|
14
|
+
* What is copied after what it names, so a reader (or a server that checks) never sees one without its content:
|
|
15
|
+
* files first, then the documents that name files (a layer's manifest, a spec set, a run's manifest), then the
|
|
16
|
+
* catalog entries that make a document visible.
|
|
17
|
+
*/
|
|
18
|
+
const isCatalog = (key) => key.includes('/index/');
|
|
19
|
+
const isDocument = (key) => /^layers\/(manifests\/|specs\/sets\/|runs\/[^/]+\/manifest\.json$)/.test(key);
|
|
20
|
+
/**
|
|
21
|
+
* Copy every layer object (`layers/…`) of `from` that `to` does not have. Only layers: they are
|
|
22
|
+
* immutable, so an object that exists is already right. Commit-keyed legacy objects are mutable and
|
|
23
|
+
* are left where they are. This is how partial reports meet: each CI
|
|
24
|
+
* node publishes into its own store, and copying them together is the aggregate, because layers are
|
|
25
|
+
* immutable, addressed by content, and never share a key with different bytes. It is idempotent,
|
|
26
|
+
* commutative and safe to repeat after a failure.
|
|
27
|
+
*
|
|
28
|
+
* A layer's catalog entry is copied last, and its manifest after its files, so a reader of `to` never sees an
|
|
29
|
+
* entry for a layer whose content has not arrived.
|
|
30
|
+
*/
|
|
31
|
+
export async function copyStore(from, to, options = {}) {
|
|
32
|
+
const everything = await from.listKeys();
|
|
33
|
+
const keys = everything.filter(key => key.startsWith('layers/'));
|
|
34
|
+
const width = options.concurrency ?? 16;
|
|
35
|
+
let copied = 0, skipped = 0;
|
|
36
|
+
const copy = async (key) => {
|
|
37
|
+
if (await to.head(key)) {
|
|
38
|
+
skipped += 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const stored = await from.get(key);
|
|
42
|
+
if (!stored)
|
|
43
|
+
return;
|
|
44
|
+
await to.put(key, await readBytes(stored.body), stored.contentType);
|
|
45
|
+
copied += 1;
|
|
46
|
+
};
|
|
47
|
+
await eachLimited(keys.filter(key => !isCatalog(key) && !isDocument(key)), width, copy);
|
|
48
|
+
await eachLimited(keys.filter(key => !isCatalog(key) && isDocument(key)), width, copy);
|
|
49
|
+
await eachLimited(keys.filter(isCatalog), width, copy);
|
|
50
|
+
// Said, not silent: whatever is not a layer was left where it is.
|
|
51
|
+
return { copied, skipped, ignored: everything.length - keys.length };
|
|
52
|
+
}
|